├── src ├── gateway │ ├── epoll.cpp │ ├── epoll.h │ ├── market_data.h │ ├── market_data.cpp │ ├── gateway.h │ ├── util │ │ ├── websocket.h │ │ └── websocket.cpp │ ├── socket.h │ ├── gateway.cpp │ └── socket.cpp ├── util │ ├── mmap_wrapper.h │ ├── types.h │ ├── mmap_wrapper.cpp │ ├── linked_list.h │ ├── hash_map.h │ ├── object_pool.h │ ├── mmap_object_pool.h │ └── disruptor.h ├── user_service │ ├── user_service.h │ └── user_service.cpp ├── order_book │ ├── price_level.h │ ├── book.h │ ├── book.cpp │ ├── order_book.h │ ├── price_level.cpp │ └── order_book.cpp ├── eventstore │ ├── eventstore.h │ └── eventstore.cpp └── main.cpp ├── ssl └── .gitignore ├── scripts ├── deploy │ ├── run.sh │ └── make.sh ├── run │ └── tests.sh ├── operations │ ├── create_local_ssl_keys.sh │ ├── create_databases.py │ └── add_user.py └── python_client.py ├── db └── readme.md ├── docker-compose.yml ├── .env ├── tests ├── helpers.h ├── linked_list.test.cpp ├── gateway │ ├── websocket_test_client.py │ ├── ssl_test_client.py │ ├── socket.test.cpp │ └── util │ │ └── websocket.test.cpp ├── util │ ├── object_pool.test.cpp │ ├── mmap_object_pool.test.cpp │ └── disruptor.test.cpp ├── misc │ └── openssl.test.cpp ├── main.test.cpp ├── helpers.cpp ├── order_book │ ├── book.test.cpp │ ├── price_level.test.cpp │ └── order_book.test.cpp └── user_service │ └── user_service.test.cpp ├── .gitignore ├── Dockerfile ├── .github └── workflows │ └── tests.yml ├── CMakeLists.txt ├── .clang-format ├── README.md └── LICENSE /src/gateway/epoll.cpp: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/gateway/epoll.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ssl/.gitignore: -------------------------------------------------------------------------------- 1 | *pem 2 | 3 | -------------------------------------------------------------------------------- /scripts/deploy/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /app/scripts/deploy/make.sh 4 | /app/main 5 | -------------------------------------------------------------------------------- /scripts/deploy/make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /app 4 | # rm main test 5 | cmake . 6 | make 7 | -------------------------------------------------------------------------------- /db/readme.md: -------------------------------------------------------------------------------- 1 | # Sqlite database dir 2 | 3 | Server will read databases from this directory by default. 4 | 5 | -------------------------------------------------------------------------------- /scripts/run/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker compose run core /app/scripts/deploy/make.sh && docker compose run core /app/test $@ 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | core: 5 | build: . 6 | ports: 7 | - "443:443" 8 | - "4443:4443" 9 | volumes: 10 | - ./:/app 11 | env_file: 12 | - .env 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | USER_DATABASE=/app/db/users.db 2 | PRIVATE_KEY=/app/ssl/private_key.pem 3 | PUBLIC_KEY=/app/ssl/public_key.pem 4 | CERTIFICATE=/app/ssl/certificate.pem 5 | CERTIFICATE_REQUEST=/app/ssl/csr.pem 6 | GATEWAY_PORT=443 7 | MARKET_PORT=4443 8 | -------------------------------------------------------------------------------- /tests/helpers.h: -------------------------------------------------------------------------------- 1 | #ifndef HELPERS_H 2 | #define HELPERS_H 3 | 4 | #include "../src/order_book/order_book.h" 5 | 6 | Order *createDefaultOrder(); 7 | Order *customOrder(int price, int quantity, char side); 8 | 9 | Order *orderQuantity(int quantity); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/ 3 | example.txt 4 | *.dSYM/ 5 | mmap_test 6 | *.o 7 | main 8 | socketTest 9 | tests/run_test 10 | .venv 11 | __pycache__ 12 | .cache 13 | .cmake 14 | CMakeCache.txt 15 | CMakeFiles 16 | test 17 | compile_commands.json 18 | cmake_install.cmake 19 | Makefile 20 | _deps/ 21 | db/users.db 22 | 23 | -------------------------------------------------------------------------------- /scripts/operations/create_local_ssl_keys.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | openssl genpkey -algorithm RSA -out ${PRIVATE_KEY} 4 | openssl rsa -pubout -in ${PRIVATE_KEY} -out ${PUBLIC_KEY} 5 | 6 | openssl req -new -key ${PRIVATE_KEY} -out ${CERTIFICATE_REQUEST} -subj "/C=US/ST=CA/L=San Francisco/O=My Organization/OU=My Department/CN=mydomain.com" 7 | openssl x509 -req -in ${CERTIFICATE_REQUEST} -signkey ${PRIVATE_KEY} -out ${CERTIFICATE} 8 | -------------------------------------------------------------------------------- /tests/linked_list.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../src/util/linked_list.h" 5 | 6 | TEST_CASE("Linked list removes data") { 7 | DoublyLinkedList list; 8 | list.push_back(5); 9 | list.push_back(6); 10 | list.push_back(7); 11 | 12 | REQUIRE(list.get_front()->data == 5); 13 | REQUIRE(list.get_back()->data == 7); 14 | 15 | list.pop_front(); 16 | REQUIRE(list.get_front()->data == 6); 17 | 18 | list.push_back(7); 19 | } 20 | -------------------------------------------------------------------------------- /tests/gateway/websocket_test_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import websocket 4 | import ssl 5 | import json 6 | 7 | print('Connecting to websocket') 8 | 9 | websocket.enableTrace(True) 10 | def mask_key(l): 11 | return b'4544' 12 | # return 'a' * l 13 | 14 | ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE}) 15 | # ws.set_mask_key(mask_key) 16 | 17 | ws.connect("wss://localhost", timeout=5) 18 | message = {'msg': 'Hello world from client!'} 19 | ws.send(json.dumps(message)) 20 | print("Got from server", ws.recv()) 21 | ws.close() 22 | -------------------------------------------------------------------------------- /src/util/mmap_wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef mmap_h 2 | #define mmap_h 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | struct MMapMeta { 15 | int fd; 16 | void *location; 17 | const char *name; 18 | int size; 19 | }; 20 | 21 | MMapMeta *init_mmap(const char *name, int size); 22 | MMapMeta *open_mmap(const char *name, int size); 23 | void delete_mmap(MMapMeta *info); 24 | void close_mmap(MMapMeta *info); 25 | 26 | #endif 27 | -------------------------------------------------------------------------------- /tests/gateway/ssl_test_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import socket 4 | import ssl 5 | import os 6 | import sys 7 | 8 | hostname = 'localhost' 9 | context = ssl.create_default_context() 10 | context.check_hostname = False 11 | context.verify_mode = ssl.CERT_NONE 12 | 13 | with socket.create_connection((hostname, int(os.getenv("GATEWAY_PORT", 443)))) as sock: 14 | with context.wrap_socket(sock, server_hostname=hostname) as ssock: 15 | message = "Hello world" 16 | 17 | ssock.send(message.encode('utf-8')) 18 | 19 | response = ssock.recv(1024) 20 | print("Server response:", response.decode('utf-8')) 21 | 22 | ssock.close() 23 | -------------------------------------------------------------------------------- /src/user_service/user_service.h: -------------------------------------------------------------------------------- 1 | #ifndef user_service_h 2 | #define user_service_h 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | struct AuthRet { 13 | unsigned long long id; 14 | bool authenticated; 15 | }; 16 | 17 | class UserService { 18 | public: 19 | UserService(const char * user_db_location); 20 | UserService(); 21 | ~UserService(); 22 | void authenticate(char* username, char* password, AuthRet* return_val); 23 | void set_db_location(char * db_location); 24 | const char * user_db_location; 25 | void open_database(); 26 | sqlite3 *db; 27 | }; 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /tests/util/object_pool.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/util/object_pool.h" 5 | 6 | TEST_CASE("Basic object pool allocation test") { 7 | // Rest of object pool copied from mmap object pool. 8 | // Could probably subclass them so I don't have to write as many tests. 9 | ObjectPool allocator(10); 10 | 11 | int *a = allocator.allocate(); 12 | REQUIRE(allocator.num_obj_stored() == 1); 13 | 14 | allocator.del(a); 15 | REQUIRE(allocator.num_obj_stored() == 0); 16 | 17 | allocator.cleanup(); 18 | }; 19 | 20 | TEST_CASE("Malloc understanding test") { 21 | int * a = (int*)malloc(sizeof(int) * 5); 22 | int ** b = (int**)malloc(sizeof(int*) * 5); 23 | free(a); 24 | free(b); 25 | } 26 | -------------------------------------------------------------------------------- /tests/misc/openssl.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | TEST_CASE("OpenSSL Test - Encrypting and Decrypting") { 12 | // https://www.openssl.org/docs/manmaster/man3/SSL_CTX_use_certificate_file.html 13 | SSL_CTX *ctx = SSL_CTX_new(SSLv23_server_method()); 14 | REQUIRE(SSL_CTX_use_certificate_file(ctx, getenv("CERTIFICATE"), SSL_FILETYPE_PEM) > 0); 15 | REQUIRE(SSL_CTX_use_PrivateKey_file(ctx, getenv("PRIVATE_KEY"), SSL_FILETYPE_PEM) > 0); 16 | 17 | SSL *ssl = SSL_new(ctx); 18 | SSL_shutdown(ssl); 19 | SSL_free(ssl); 20 | 21 | SSL_CTX_free(ctx); 22 | } 23 | -------------------------------------------------------------------------------- /src/order_book/price_level.h: -------------------------------------------------------------------------------- 1 | #ifndef price_level_h 2 | #define price_level_h 3 | #include "../eventstore/eventstore.h" 4 | #include "../util/linked_list.h" 5 | #include "../util/types.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | /* Keeps list of orders for each price */ 14 | class PriceLevel { 15 | public: 16 | PriceLevel(PRICE price); 17 | Node *addOrder(Order *order); 18 | std::list fillOrder(Order *order); 19 | void cancelOrder(Node *node); 20 | int getVolume(); 21 | int getPrice(); 22 | 23 | private: 24 | // Prices are stored in pennies. $4.56 = 456. 25 | PRICE limitPrice; 26 | long totalVolume; 27 | DoublyLinkedList orders; 28 | }; 29 | 30 | #endif 31 | -------------------------------------------------------------------------------- /tests/main.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char *argv[]) { 11 | spdlog::set_level(spdlog::level::debug); 12 | spdlog::set_pattern("%-5l %E %-16s%-4#%-21! %v"); 13 | 14 | if (sodium_init() == -1) { 15 | // Initialization failed 16 | spdlog::critical("Could not initialize libsodium!"); 17 | return -1; 18 | } 19 | 20 | OpenSSL_add_all_algorithms(); 21 | 22 | int rc = system("/app/scripts/operations/create_local_ssl_keys.sh"); 23 | if (rc != 0) { 24 | throw std::runtime_error("Could not generate public/private keys"); 25 | } 26 | 27 | return Catch::Session().run(argc, argv); 28 | } 29 | -------------------------------------------------------------------------------- /src/eventstore/eventstore.h: -------------------------------------------------------------------------------- 1 | #ifndef _eventstore_h 2 | #define _eventstore_h 3 | 4 | #include "../util/mmap_wrapper.h" 5 | #include "../util/mmap_object_pool.h" 6 | #include "../util/types.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define MAX_OPEN_ORDERS 10000 15 | 16 | extern const char *eventstore_buf_name; 17 | 18 | class EventStore { 19 | public: 20 | EventStore(MMapObjectPool *order_pool); 21 | ~EventStore(); 22 | SEQUENCE_ID newEvent(SIDE side, PRICE limitPrice, int clientId, int quantity); 23 | Order *get(SEQUENCE_ID id); 24 | ORDER_MMAP_OFFSET getOffset(SEQUENCE_ID id); 25 | void remove(SEQUENCE_ID id); 26 | size_t size(); 27 | 28 | private: 29 | SEQUENCE_ID sequence; 30 | std::unordered_map event_store; 31 | MMapObjectPool *order_pool; 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/order_book/book.h: -------------------------------------------------------------------------------- 1 | #ifndef book_h 2 | #define book_h 3 | #include "../eventstore/eventstore.h" 4 | #include "../util/linked_list.h" 5 | #include "../util/types.h" 6 | #include "price_level.h" 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | /* Keeps a list of prices */ 15 | class Book { 16 | public: 17 | Book(); 18 | PriceLevel *get(PRICE price); 19 | // Add order to this book. 20 | Node *addOrder(Order *order); 21 | // Given an order from the other side, attempt to fill it using 22 | // orders from this book. 23 | std::list fillOrder(Order *order, PriceLevel *level); 24 | 25 | void cancelOrder(Node *node); 26 | int getVolume(); 27 | void initPriceDataStructures(int start, int end); 28 | 29 | private: 30 | std::unordered_map *limitMap; 31 | int totalVolume = 0; 32 | }; 33 | 34 | #endif 35 | -------------------------------------------------------------------------------- /src/gateway/market_data.h: -------------------------------------------------------------------------------- 1 | #ifndef _market_h 2 | #define _market_h 3 | 4 | #include "../util/disruptor.h" 5 | #include "../util/mmap_wrapper.h" 6 | #include "../util/types.h" 7 | #include "socket.h" 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #define GATEWAY_BUFLEN 100 19 | 20 | class MarketData : public SocketServer { 21 | public: 22 | MarketData(Consumer *market_l1_data_consumer); 23 | ~MarketData() throw(); 24 | 25 | void newClient(int client_id) override; 26 | void disconnected(int client_id) override; 27 | void readMessage(int client_id, const char *message, 28 | int message_size) override; 29 | void handleOutgoingMessage() override; 30 | void run(); 31 | 32 | private: 33 | Consumer *market_l1_data_consumer; 34 | }; 35 | #endif 36 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.14 2 | 3 | RUN apk update 4 | RUN apk add g++ git cmake make openssl sqlite-dev openssl-dev \ 5 | spdlog-dev wget sqlite-dev python3 \ 6 | openssl openssl-dev 7 | # RUN wget https://download.libsodium.org/libsodium/releases/libsodium-1.0.19-stable.tar.gz && \ 8 | # tar -xzf libsodium-1.0.19-stable.tar.gz && \ 9 | # cd libsodium-stable && \ 10 | # ./configure && \ 11 | # make && make check && make install 12 | 13 | # Testing framework 14 | RUN git clone https://github.com/catchorg/Catch2.git && \ 15 | cd Catch2 && \ 16 | cmake -Bbuild -H. -DBUILD_TESTING=OFF && \ 17 | cmake --build build/ --target install 18 | 19 | RUN apk add py3-pip python3-dev libffi-dev 20 | RUN pip install argon2-cffi websocket-client 21 | 22 | # Logging library 23 | # RUN git clone https://github.com/gabime/spdlog.git && \ 24 | # cd spdlog && mkdir build && cd build && \ 25 | # cmake .. && make -j && make install 26 | 27 | WORKDIR / 28 | COPY /scripts/deploy/run.sh /run.sh 29 | RUN chmod +x /run.sh 30 | 31 | CMD ["/run.sh"] 32 | -------------------------------------------------------------------------------- /tests/helpers.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/order_book/order_book.h" 2 | 3 | #include "helpers.h" 4 | 5 | #define DEFAULT_PRICE 5000 6 | #define DEFAULT_QUANTITY 100 7 | #define DEFAULT_SIDE 'b' 8 | 9 | // Create a logical sequence clock for testing purposes 10 | int sequence_id = 1; 11 | 12 | Order *createDefaultOrder() { 13 | Order *order = new Order(); 14 | order->limitPrice = DEFAULT_PRICE; 15 | order->quantity = DEFAULT_QUANTITY; 16 | order->side = DEFAULT_SIDE; 17 | order->id = sequence_id; 18 | sequence_id++; 19 | return order; 20 | } 21 | 22 | Order *orderQuantity(int quantity) { 23 | Order *order = new Order(); 24 | order->limitPrice = DEFAULT_PRICE; 25 | order->quantity = quantity; 26 | order->side = DEFAULT_SIDE; 27 | order->id = sequence_id; 28 | sequence_id++; 29 | return order; 30 | } 31 | 32 | Order *customOrder(int price, int quantity, char side) { 33 | Order *order = new Order(); 34 | order->limitPrice = price; 35 | order->quantity = quantity; 36 | order->side = side; 37 | order->id = sequence_id; 38 | sequence_id++; 39 | return order; 40 | } 41 | -------------------------------------------------------------------------------- /src/eventstore/eventstore.cpp: -------------------------------------------------------------------------------- 1 | #include "eventstore.h" 2 | 3 | EventStore::EventStore(MMapObjectPool *order_pool) { 4 | sequence = 0; 5 | this->order_pool = order_pool; 6 | } 7 | 8 | EventStore::~EventStore() { } 9 | 10 | SEQUENCE_ID EventStore::newEvent(SIDE side, PRICE limitPrice, int clientId, 11 | int quantity) { 12 | Order *order = order_pool->allocate(); 13 | order->clientId = clientId; 14 | order->side = side; 15 | order->limitPrice = limitPrice; 16 | order->quantity = quantity; 17 | 18 | order->id = sequence; 19 | sequence++; 20 | 21 | ORDER_MMAP_OFFSET offset = order_pool->pointer_to_offset(order); 22 | 23 | event_store.emplace(sequence, offset); 24 | 25 | return sequence; 26 | } 27 | 28 | ORDER_MMAP_OFFSET EventStore::getOffset(SEQUENCE_ID id) { 29 | return event_store.at(id); 30 | } 31 | 32 | void EventStore::remove(SEQUENCE_ID id) { event_store.erase(id); } 33 | 34 | Order *EventStore::get(SEQUENCE_ID id) { 35 | ORDER_MMAP_OFFSET offset = event_store.at(id); 36 | return order_pool->offset_to_pointer(offset); 37 | } 38 | 39 | size_t EventStore::size() { return event_store.size(); } 40 | -------------------------------------------------------------------------------- /scripts/operations/create_databases.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sqlite3 4 | import os 5 | import sys 6 | import argparse 7 | 8 | if __name__ != "__main__": 9 | print("Please run script as docker compose run -it core /app/scripts/operations/create_databases.py") 10 | sys.exit(0) 11 | 12 | parser = argparse.ArgumentParser(description="Create databases") 13 | 14 | parser.add_argument("--user-db", required=False, help="Location of sqlite3 db location") 15 | 16 | args = parser.parse_args() 17 | 18 | user_db_file = args.user_db or os.environ.get('USER_DATABASE') 19 | 20 | if not user_db_file: 21 | raise Exception("put USER_DATABASE back into .env or specify on command line") 22 | 23 | con = sqlite3.connect(user_db_file) 24 | cursor = con.cursor() 25 | 26 | create_table_sql = ''' 27 | CREATE TABLE IF NOT EXISTS users ( 28 | id INTEGER PRIMARY KEY AUTOINCREMENT, 29 | username TEXT NOT NULL, 30 | password TEXT NOT NULL, 31 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 32 | active INTEGER DEFAULT 1 33 | ); 34 | ''' 35 | 36 | cursor.execute(create_table_sql) 37 | 38 | con.commit() 39 | con.close() 40 | 41 | print("Created users table ✅") 42 | -------------------------------------------------------------------------------- /src/gateway/market_data.cpp: -------------------------------------------------------------------------------- 1 | #include "market_data.h" 2 | 3 | MarketData::MarketData(Consumer *market_l1_data_consumer) { 4 | this->market_l1_data_consumer = market_l1_data_consumer; 5 | } 6 | 7 | MarketData::~MarketData() throw() {} 8 | 9 | // I don't care about these for now because I am implementing authentication 10 | // later. For now anyone can get market data if they connect. 11 | 12 | void MarketData::newClient(int client_id) { 13 | const char *msg = "Welcome new market data consumer"; 14 | if (!sendMessage(client_id, const_cast(msg), strlen(msg))) { 15 | forceDisconnect(client_id); 16 | } 17 | } 18 | void MarketData::disconnected(int client_id){}; 19 | void MarketData::readMessage(int client_id, const char *message, 20 | int message_size){}; 21 | 22 | void MarketData::handleOutgoingMessage() { 23 | L1MarketData *market_data = market_l1_data_consumer->get(); 24 | if (market_data == nullptr) { 25 | return; 26 | } 27 | 28 | // For every client, send market data. 29 | sendMessageToAllClients((char *)market_data, sizeof(L1MarketData)); 30 | SPDLOG_DEBUG("Sent Mkt {} Value {} ", market_data->type, market_data->val); 31 | } 32 | 33 | void MarketData::run() { 34 | char *port = getenv("MARKET_PORT"); 35 | bindSocket(atoi(port)); 36 | listenToSocket(); 37 | } 38 | -------------------------------------------------------------------------------- /src/order_book/book.cpp: -------------------------------------------------------------------------------- 1 | #include "book.h" 2 | 3 | Book::Book() { 4 | limitMap = new std::unordered_map(); 5 | this->initPriceDataStructures(ONE_DOLLAR, ONE_HUNDRED_DOLLARS); 6 | } 7 | 8 | void Book::initPriceDataStructures(int start, int end) { 9 | for (int price = start; price < end; price += ONE_CENT) { 10 | PriceLevel *level = new PriceLevel(price); 11 | limitMap->emplace(price, level); 12 | } 13 | } 14 | 15 | std::list Book::fillOrder(Order *order, PriceLevel *level) { 16 | int initial_quantity = order->unfilled_quantity(); 17 | 18 | // here is the bug. we need to fill prices at best bid / ask 19 | // PriceLevel* level = limitMap->at(order->limitPrice); 20 | std::list updated_orders = level->fillOrder(order); 21 | 22 | totalVolume -= (initial_quantity - order->unfilled_quantity()); 23 | 24 | return updated_orders; 25 | } 26 | 27 | Node *Book::addOrder(Order *order) { 28 | totalVolume += order->unfilled_quantity(); 29 | 30 | PriceLevel *level = limitMap->at(order->limitPrice); 31 | return level->addOrder(order); 32 | } 33 | 34 | void Book::cancelOrder(Node *node) { 35 | PriceLevel *level = this->get(node->data->limitPrice); 36 | 37 | totalVolume -= node->data->unfilled_quantity(); 38 | level->cancelOrder(node); 39 | } 40 | 41 | int Book::getVolume() { return totalVolume; } 42 | 43 | PriceLevel *Book::get(PRICE price) { return limitMap->at(price); } 44 | -------------------------------------------------------------------------------- /tests/order_book/book.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/order_book/book.h" 5 | #include "../../src/order_book/order_book.h" 6 | #include "../helpers.h" 7 | 8 | TEST_CASE("Book test - Add & cancel order") { 9 | Book book; 10 | book.initPriceDataStructures(0, 10); 11 | 12 | Order *order = createDefaultOrder(); 13 | order->limitPrice = 5; 14 | Node *node = book.addOrder(order); 15 | 16 | REQUIRE(book.getVolume() == 100); 17 | 18 | book.cancelOrder(node); 19 | 20 | REQUIRE(book.getVolume() == 0); 21 | } 22 | 23 | TEST_CASE("Book test - fill order") { 24 | Book book; 25 | book.initPriceDataStructures(0, 10); 26 | 27 | // Create an order on this side. 28 | Order *order = createDefaultOrder(); 29 | order->limitPrice = 5000; 30 | Node *node = book.addOrder(order); 31 | REQUIRE(node->data->filled_quantity == 0); 32 | REQUIRE(book.get(order->limitPrice)->getVolume() == 100); 33 | 34 | // Create a new order and attempt to fill it. 35 | Order *oppositeOrder = orderQuantity(50); 36 | REQUIRE(order->limitPrice == oppositeOrder->limitPrice); 37 | REQUIRE(book.get(oppositeOrder->limitPrice)->getVolume() == 100); 38 | PriceLevel *level = book.get(oppositeOrder->limitPrice); 39 | 40 | std::list updated_orders = book.fillOrder(oppositeOrder, level); 41 | 42 | REQUIRE(book.getVolume() == 50); 43 | 44 | REQUIRE(book.fillOrder(orderQuantity(5), level).size() == 1); 45 | } 46 | -------------------------------------------------------------------------------- /src/util/types.h: -------------------------------------------------------------------------------- 1 | #ifndef types_h 2 | #define types_h 3 | 4 | #include 5 | #define PRICE int 6 | 7 | #define SIDE char 8 | #define BUY 'b' 9 | #define SELL 's' 10 | 11 | #define SEQUENCE_ID unsigned long long 12 | #define ORDER_MMAP_OFFSET int 13 | #define SUBMITTED 0 14 | #define FILLED 1 15 | #define CANCELLED 2 16 | #define PARTIAL_FILL 3 17 | 18 | #define ONE_DOLLAR 100 19 | #define ONE_HUNDRED_DOLLARS (ONE_DOLLAR * 100) 20 | #define ONE_CENT 1 21 | 22 | #define GATEWAY_CONSUMER 0 23 | #define OUTGOING_MESSAGE_CONSUMER 0 24 | 25 | // @TODO Order / NewOrderEvent could be combined? 26 | struct Order { 27 | int clientId; 28 | int quantity; 29 | SIDE side; 30 | PRICE limitPrice; 31 | 32 | SEQUENCE_ID id; 33 | int filled_quantity = 0; 34 | 35 | int unfilled_quantity() { return quantity - filled_quantity; } 36 | 37 | int status = SUBMITTED; 38 | 39 | int64_t created_at; 40 | // int64_t updated_at; 41 | }; 42 | 43 | struct NewOrderEvent { 44 | char side; 45 | // Stored in pennies 46 | // $10.05 = 1005 47 | PRICE limitPrice; 48 | int quantity; 49 | // For now this is the socket id 50 | // Later on we can create an authentication feature 51 | // and have actual client Ids. 52 | int clientId; 53 | }; 54 | 55 | struct L1MarketData { 56 | unsigned char version = 0; 57 | char type; // 'b' for bid and 'a' for ask. Expandable to other data types. 58 | unsigned int val; 59 | // May include other types of data like volume later. 60 | unsigned long long time_ns; 61 | }; 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /src/gateway/gateway.h: -------------------------------------------------------------------------------- 1 | #ifndef _gateway_h 2 | #define _gateway_h 3 | 4 | #include "../util/disruptor.h" 5 | #include "../util/mmap_wrapper.h" 6 | #include "../util/object_pool.h" 7 | #include "../util/types.h" 8 | #include "socket.h" 9 | #include "util/websocket.h" 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #define GATEWAY_BUFLEN 100 23 | 24 | using namespace std; 25 | 26 | class Gateway : public SocketServer { 27 | public: 28 | Gateway(Producer *incoming_msg_producer, 29 | Consumer *outgoing_message_consumer, 30 | MMapObjectPool *order_pool); 31 | 32 | // Used for testing. 33 | Gateway(); 34 | ~Gateway() throw(); 35 | 36 | void newClient(int client_id) override; 37 | void disconnected(int client_id) override; 38 | void readMessage(int client_id, const char *message, 39 | int message_size) override; 40 | // Whenever a message goes into the outgoing ring buffer 41 | // this function is called to send a message to the client. 42 | void handleOutgoingMessage() override; 43 | void run(); 44 | 45 | private: 46 | Producer *incoming_msg_producer; 47 | Consumer *outgoing_message_consumer; 48 | MMapObjectPool *order_pool; 49 | vector connected_webclients; 50 | }; 51 | #endif 52 | -------------------------------------------------------------------------------- /src/gateway/util/websocket.h: -------------------------------------------------------------------------------- 1 | #ifndef websocket_h 2 | #define websocket_h 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace std; 17 | 18 | // RFC-6455 specifies that we add this string to the websocket request nonce 19 | static const string ws_magic_string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 20 | static const string ws_request_header = "Sec-WebSocket-Key"; 21 | 22 | static const string ws_response = "HTTP/1.1 101 Switching Protocols\r\n" 23 | "Upgrade: websocket\r\n" 24 | "Connection: Upgrade\r\n" 25 | "Sec-WebSocket-Accept: "; 26 | 27 | #define WS_ACCEPT_RESPONSE_LEN 130 28 | #define SHA1_HUMAN_LEN 41 29 | 30 | string base64_encode(const unsigned char *original); 31 | string base64_decode(const string &encoded); 32 | string sha1(const string &input); 33 | map parse_http_headers(const string &headers); 34 | string create_websocket_response_nonce(const string &websocket_request_key); 35 | string websocket_request_response(const string &client_http_request); 36 | 37 | void printStringAsHex(const string &str); 38 | void printByteAsHex(const string &message, const uint8_t byte); 39 | bool decodeWebSocketFrame(string frame, string &message); 40 | vector encodeWebsocketFrame(const string &message); 41 | 42 | #endif 43 | -------------------------------------------------------------------------------- /tests/order_book/price_level.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/order_book/order_book.h" 5 | #include "../helpers.h" 6 | 7 | TEST_CASE("Price levels - filling full orders") { 8 | Order *order = createDefaultOrder(); 9 | PriceLevel *level = new PriceLevel(order->limitPrice); 10 | 11 | REQUIRE(level->getVolume() == 0); 12 | 13 | level->addOrder(order); 14 | 15 | REQUIRE(level->getVolume() == 100); 16 | 17 | Order *order2 = createDefaultOrder(); 18 | level->addOrder(order2); 19 | 20 | REQUIRE(level->getVolume() == 200); 21 | 22 | level->fillOrder(orderQuantity(200)); 23 | 24 | REQUIRE(level->getVolume() == 0); 25 | } 26 | 27 | TEST_CASE("Price levels - filling partial orders") { 28 | Order *order = createDefaultOrder(); 29 | PriceLevel *level = new PriceLevel(order->limitPrice); 30 | 31 | level->addOrder(order); 32 | 33 | Order *order2 = createDefaultOrder(); 34 | order2->id = 1; 35 | level->addOrder(order2); 36 | 37 | std::list updated_orders = level->fillOrder(orderQuantity(50)); 38 | 39 | REQUIRE(updated_orders.size() == 1); 40 | REQUIRE(order->filled_quantity == 50); 41 | REQUIRE(order2->filled_quantity == 0); 42 | 43 | std::list updated_orders2 = level->fillOrder(orderQuantity(50)); 44 | REQUIRE(updated_orders2.size() == 1); 45 | REQUIRE(order->filled_quantity == 100); 46 | REQUIRE(order2->filled_quantity == 0); 47 | 48 | std::list updated_orders3 = level->fillOrder(orderQuantity(50)); 49 | 50 | REQUIRE(updated_orders3.size() == 1); 51 | REQUIRE(order->filled_quantity == 100); 52 | REQUIRE(order2->filled_quantity == 50); 53 | } 54 | -------------------------------------------------------------------------------- /scripts/operations/add_user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from argon2 import PasswordHasher 4 | from argon2.exceptions import VerifyMismatchError 5 | import argparse 6 | import getpass 7 | import sqlite3 8 | import os 9 | import sys 10 | 11 | if __name__ != "__main__": 12 | print("Please run script as docker compose run -it core /app/scripts/operations/add_user.py") 13 | sys.exit(0) 14 | 15 | parser = argparse.ArgumentParser(description="Create new user script") 16 | 17 | parser.add_argument("--username", required=True, help="Enter a username") 18 | parser.add_argument("--password", help="Enter a password (optional) (if not specified will prompt)") 19 | parser.add_argument("--user-db", required=False, help="Location of sqlite3 user db location. Defaults to USER_DATABASE env var.") 20 | 21 | args = parser.parse_args() 22 | 23 | username = args.username 24 | 25 | if args.password: 26 | password = args.password 27 | else: 28 | password = getpass.getpass(prompt="Enter a password: ") 29 | 30 | user_db_file = args.user_db or os.environ.get('USER_DATABASE') 31 | if not user_db_file: 32 | raise Exception("put USER_DATABASE back into .env or specify on command line") 33 | 34 | try: 35 | con = sqlite3.connect(user_db_file) 36 | cursor = con.cursor() 37 | 38 | ph = PasswordHasher() 39 | hash = ph.hash(password) 40 | 41 | insert_sql = ''' 42 | INSERT INTO users (username, password) 43 | VALUES (?, ?); 44 | ''' 45 | 46 | cursor.execute(insert_sql, (username, hash)) 47 | 48 | con.commit() 49 | con.close() 50 | 51 | print(f"User '{username}' created ✅ Begin trading!") 52 | except sqlite3.Error as e: 53 | print(f"Error inserting user: {e} ❌") 54 | -------------------------------------------------------------------------------- /src/order_book/order_book.h: -------------------------------------------------------------------------------- 1 | #ifndef order_book_h_ 2 | #define order_book_h_ 3 | #include "../eventstore/eventstore.h" 4 | #include "../util/disruptor.h" 5 | #include "../util/linked_list.h" 6 | #include "../util/types.h" 7 | #include "book.h" 8 | #include "price_level.h" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | class OrderBook { 20 | // Main entry point for matching orders 21 | public: 22 | OrderBook(Producer *outbound_mkt_l1); 23 | 24 | std::list 25 | newOrder(Order *order); // give a list of orders matched or none at all. 26 | void cancelOrder(SEQUENCE_ID id); 27 | int getVolume(); 28 | PriceLevel *getBid(); 29 | PriceLevel *getAsk(); 30 | 31 | private: 32 | void addOrder(Order *); 33 | std::list fillOrder(Order *order); 34 | bool isOpposingOrderBookBlank(Order *order); 35 | int opposingOrderVolume(Order *order); 36 | int bookOrderVolume(Order *order); 37 | void adjustBidAskIfOrderIsBetterPrice(Order *order); 38 | bool orderCrossedSpread(Order *order); 39 | void setBidAskToReflectMarket(); 40 | void printBestBidAsk(const char *prefix); 41 | void sendMarketData(char type, int val); 42 | 43 | Book *buyBook; 44 | Book *sellBook; 45 | PriceLevel *bestBid; 46 | PriceLevel *bestAsk; 47 | int totalVolume = 0; 48 | // Create an unordered map of sequence ids to iterators 49 | // Then we can later implement a custom allocator to manage these iterators 50 | // so that they don't blow up the heap. 51 | // In a v2 consider implementing my own linked list so we don't need pointers 52 | // to iterators. 53 | std::unordered_map *> *orderMap; 54 | Producer *outbound_mkt_l1; 55 | }; 56 | 57 | #endif 58 | -------------------------------------------------------------------------------- /tests/gateway/socket.test.cpp: -------------------------------------------------------------------------------- 1 | #include "../../src/gateway/socket.h" 2 | #include "../../src/gateway/gateway.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | class SSLSocketTest : public SocketServer { 9 | public: 10 | SSLSocketTest(){}; 11 | ~SSLSocketTest() throw(){}; 12 | 13 | void newClient(int client_id) { 14 | SPDLOG_INFO("Client {} connected", client_id); 15 | }; 16 | 17 | void disconnected(int client_id) { 18 | SPDLOG_INFO("Client {} disconnected. Server exiting", client_id); 19 | exit(0); 20 | }; 21 | 22 | void readMessage(int client_id, char *message) { 23 | SPDLOG_INFO("Read {} from client {}", message, client_id); 24 | // sendMessage(client_id, message, sizeof(message)); 25 | }; 26 | 27 | void handleOutgoingMessage(){}; 28 | }; 29 | 30 | // TEST_CASE("Socket Server") { 31 | // pid_t child_pid; 32 | 33 | // // Create a new process by forking the current process 34 | // child_pid = fork(); 35 | 36 | // if (child_pid == -1) { 37 | // SPDLOG_ERROR("Could not fork ssl socket server"); 38 | // exit(1); 39 | // } 40 | 41 | // if (child_pid == 0) { 42 | // Gateway ss; 43 | 44 | // char *port = getenv("GATEWAY_PORT"); 45 | 46 | // ss.bindSocket(atoi(port)); 47 | // ss.listenToSocket(); 48 | // } else { 49 | // // Attempt to connect to ssl server with test client. 50 | // int rc = system("/app/tests/gateway/ssl_test_client.py > /dev/stdout"); 51 | // // REQUIRE(rc == 0); 52 | 53 | // // if (kill(child_pid, SIGKILL) == -1) { 54 | // // perror("kill"); 55 | // // exit(EXIT_FAILURE); 56 | // // } 57 | 58 | // // int status; 59 | // // pid_t terminated_pid = waitpid(child_pid, &status, 0); 60 | // // If status is 0 then client successfully connected & server is exiting. 61 | // // REQUIRE(status == 0); 62 | // } 63 | // } 64 | -------------------------------------------------------------------------------- /src/user_service/user_service.cpp: -------------------------------------------------------------------------------- 1 | #include "user_service.h" 2 | 3 | UserService::UserService(const char* user_db_location) { 4 | this->user_db_location = user_db_location; 5 | this->open_database(); 6 | } 7 | 8 | UserService::UserService() { 9 | this->user_db_location = getenv("USER_DATABASE"); 10 | this->open_database(); 11 | } 12 | 13 | void UserService::open_database() { 14 | int rc = sqlite3_open_v2(user_db_location, &db, SQLITE_OPEN_READONLY, nullptr); 15 | 16 | if (rc) { 17 | SPDLOG_CRITICAL("Can't open database {} ❌", sqlite3_errmsg(db)); 18 | throw std::runtime_error("Can't open user database"); 19 | } 20 | } 21 | 22 | UserService::~UserService() { 23 | sqlite3_close(db); 24 | } 25 | 26 | void UserService::authenticate(char* username, char* password, AuthRet* return_val) { 27 | const char *sql = "SELECT id, password FROM users WHERE username = ?"; 28 | 29 | sqlite3_stmt *stmt; 30 | 31 | int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); 32 | 33 | if (rc != SQLITE_OK) { 34 | SPDLOG_CRITICAL("Can't prepare statement {}", sqlite3_errmsg(db)); 35 | sqlite3_close(db); 36 | throw std::runtime_error("Can't prepare user selection statement."); 37 | } 38 | 39 | rc = sqlite3_bind_text(stmt, 1, username, -1, SQLITE_STATIC); 40 | 41 | if (rc != SQLITE_OK) { 42 | SPDLOG_CRITICAL("Can't bind username to stmt {}", sqlite3_errmsg(db)); 43 | sqlite3_finalize(stmt); 44 | sqlite3_close(db); 45 | throw std::runtime_error("Can't username to stmt."); 46 | } 47 | 48 | return_val->authenticated = false; 49 | 50 | while ((rc = sqlite3_step(stmt)) == SQLITE_ROW) { 51 | // Process the results here 52 | return_val->id = sqlite3_column_int(stmt, 0); 53 | const char *hashed_password = (const char *)sqlite3_column_text(stmt, 1); 54 | 55 | if (crypto_pwhash_str_verify(hashed_password, password, strlen(password)) == 0) { 56 | return_val->authenticated = true; 57 | } 58 | break; 59 | } 60 | 61 | sqlite3_finalize(stmt); 62 | 63 | if (rc != SQLITE_DONE) { 64 | SPDLOG_CRITICAL("SQL error {}", sqlite3_errmsg(db)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/util/mmap_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "mmap_wrapper.h" 2 | 3 | MMapMeta *init_mmap(const char *name, int size) { 4 | int fd = shm_open(name, O_CREAT | O_RDWR, 0777); 5 | 6 | if (fd == -1) { 7 | std::stringstream ss; 8 | ss << "Could not open file descriptor to mmap " << name << " in controller."; 9 | throw std::runtime_error(ss.str()); 10 | } 11 | 12 | if (ftruncate(fd, size) == -1) { 13 | throw std::runtime_error("Could not resize mmap in controller"); 14 | } 15 | 16 | void *location = 17 | mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 18 | 19 | if (location == MAP_FAILED) { 20 | throw std::runtime_error("Could not map mmap region to controller."); 21 | } 22 | 23 | new (location) char[size]; 24 | 25 | memset(location, 0, size); 26 | 27 | // This is allocated w/o a destructor as it is assumed 28 | // program will instantiate an mmap once. 29 | MMapMeta *info = new MMapMeta(); 30 | 31 | info->location = location; 32 | info->fd = fd; 33 | info->name = name; 34 | info->size = size; 35 | 36 | return info; 37 | }; 38 | 39 | void delete_mmap(MMapMeta *info) { 40 | munmap(info->location, info->size); 41 | shm_unlink(info->name); 42 | } 43 | 44 | MMapMeta *open_mmap(const char *name, int size) { 45 | int fd = shm_open(name, O_RDWR, 0777); 46 | 47 | if (fd == -1) { 48 | std::stringstream ss; 49 | ss << "Could not open file descriptor to mmap " << name << " in client."; 50 | throw std::runtime_error(ss.str()); 51 | } 52 | 53 | void *location = 54 | mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 55 | 56 | if (location == MAP_FAILED) { 57 | throw std::runtime_error("Could not map mmap region to client."); 58 | } 59 | 60 | // This is allocated w/o a destructor as it is assumed 61 | // program will instantiate an mmap once. 62 | MMapMeta *info = new MMapMeta(); 63 | info->location = location; 64 | info->fd = fd; 65 | info->name = name; 66 | info->size = size; 67 | 68 | return info; 69 | } 70 | 71 | void close_mmap(MMapMeta *info) { 72 | munmap(info->location, info->size); 73 | close(info->fd); 74 | } 75 | -------------------------------------------------------------------------------- /src/gateway/socket.h: -------------------------------------------------------------------------------- 1 | #ifndef socket_h 2 | #define socket_h 3 | 4 | #include "../eventstore/eventstore.h" 5 | #include "../util/disruptor.h" 6 | #include "../util/types.h" 7 | #include //close 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include //strlen 16 | #include 17 | #include 18 | #include //FD_SET, FD_ISSET, FD_ZERO macros 19 | #include 20 | #include //close 21 | 22 | #define MAX_CLIENTS 30 23 | #define TIMEOUT_MICROSECONDS 1 24 | #define MAX_OUTGOING_MESSAGES 100 25 | #define MAX_MARKET_DATA_UPDATES 100 26 | 27 | class SocketServer { 28 | public: 29 | SocketServer(); 30 | ~SocketServer(); 31 | void bindSocket(int PORT); 32 | void listenToSocket(); 33 | 34 | // events that will be called on gateway by socket server. 35 | virtual void newClient(int client_id) = 0; 36 | virtual void disconnected(int client_id) = 0; 37 | virtual void readMessage(int client_id, const char *message, 38 | int message_size) = 0; 39 | virtual void handleOutgoingMessage() = 0; 40 | 41 | // Does not need to be implemented by subclass. 42 | bool sendMessage(int client_id, const char *message, int message_size); 43 | void sendMessageToAllClients(const char *message, int message_size); 44 | void forceDisconnect(int client_id); 45 | 46 | protected: 47 | int client_socket[MAX_CLIENTS]; 48 | SSL *connections[MAX_CLIENTS]; 49 | SSL_CTX *ctx; 50 | // Use non-blocking sockets to wait for activity. Only wait for 1 microsecond. 51 | struct timeval timeout; 52 | 53 | int master_socket; 54 | struct sockaddr_in address; 55 | 56 | char buffer[1025]; // data buffer of 1K 57 | 58 | private: 59 | int getMaxClientID(int (*client_socket)[MAX_CLIENTS]); 60 | int readDataFromClient(int client_id); 61 | void acceptNewConn(fd_set *readfds); 62 | void initFDSet(fd_set *fds, int (*client_socket)[MAX_CLIENTS]); 63 | 64 | // Implement websocket handshakes at socket level. 65 | // implement them in a subclass. 66 | }; 67 | 68 | #endif 69 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This starter workflow is for a CMake project running on a single platform. There is a different starter workflow if you need cross-platform coverage. 2 | # See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-multi-platform.yml 3 | name: Tests 4 | 5 | on: 6 | push: 7 | branches: [ "main" ] 8 | pull_request: 9 | branches: [ "main" ] 10 | 11 | env: 12 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 13 | BUILD_TYPE: Release 14 | 15 | jobs: 16 | build: 17 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 18 | # You can convert this to a matrix build if you need cross-platform coverage. 19 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Install dependencies 26 | run: | 27 | sudo apt-get install libspdlog-dev 28 | 29 | # - name: Download spdlog 30 | # run: | 31 | # mkdir third_party 32 | # cd third_party 33 | # git clone https://github.com/gabime/spdlog.git 34 | # cd spdlog 35 | # mkdir build && cd build 36 | # cmake .. 37 | # sudo make install 38 | 39 | - name: Download Catch2 40 | run: | 41 | mkdir third_party 42 | cd third_party 43 | git clone https://github.com/catchorg/Catch2.git 44 | cd Catch2 45 | cmake -Bbuild -H. -DBUILD_TESTING=OFF 46 | sudo cmake --build build/ --target install 47 | 48 | - name: Configure CMake 49 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 50 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 51 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 52 | 53 | - name: Build 54 | # Build your program with the given configuration 55 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 56 | 57 | - name: Test 58 | working-directory: ${{github.workspace}}/build 59 | run: ./test 60 | 61 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15...3.25) 2 | project(StockExchange) 3 | 4 | find_package(spdlog REQUIRED) 5 | find_package(Catch2 REQUIRED) 6 | find_package(SQLite3 REQUIRED) 7 | find_package(OpenSSL REQUIRED) 8 | 9 | # Including sodium with cmake using this magic hack. 10 | # https://github.com/robinlinden/libsodium-cmake/blob/master/README.md 11 | include(FetchContent) 12 | 13 | # Update the commit to point to whatever libsodium-cmake-commit you want to target. 14 | FetchContent_Declare(Sodium 15 | GIT_REPOSITORY https://github.com/robinlinden/libsodium-cmake.git 16 | GIT_TAG 99f14233eab1d4f7f49c2af4ec836f2e701c445e # HEAD as of 2022-05-28 17 | ) 18 | set(SODIUM_DISABLE_TESTS ON) 19 | FetchContent_MakeAvailable(Sodium) 20 | 21 | include(CMakePrintHelpers) 22 | # include_directories(${SQLite3_INCLUDE_DIRS}) 23 | 24 | add_compile_definitions(SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE) 25 | 26 | set(CMAKE_CXX_STANDARD 20) 27 | set(CMAKE_CXX_FLAGS "-Wall -Wextra") 28 | 29 | file(GLOB ORDER_BOOK_SOURCES "src/order_book/*.cpp") 30 | file(GLOB USER_SERVICE_SOURCES "src/user_service/*.cpp") 31 | file(GLOB UTIL_SOURCES "src/util/*.cpp") 32 | file(GLOB GATEWAY_SOURCES "src/gateway/*.cpp") 33 | file(GLOB WEBSOCKET_SOURCES "src/gateway/util/*.cpp") 34 | file(GLOB_RECURSE TEST_FILES "tests/*cpp") 35 | 36 | add_executable(main 37 | src/main.cpp 38 | src/eventstore/eventstore.cpp 39 | ${ORDER_BOOK_SOURCES} 40 | ${GATEWAY_SOURCES} 41 | ${WEBSOCKET_SOURCES} 42 | ${USER_SERVICE_SOURCES} 43 | ${UTIL_SOURCES} 44 | ${spdlog_DIR} 45 | ) 46 | 47 | add_executable(test 48 | ${TEST_FILES} 49 | src/eventstore/eventstore.cpp 50 | ${ORDER_BOOK_SOURCES} 51 | ${GATEWAY_SOURCES} 52 | ${WEBSOCKET_SOURCES} 53 | ${UTIL_SOURCES} 54 | ${USER_SERVICE_SOURCES} 55 | ${spdlog_DIR} 56 | ) 57 | 58 | target_link_libraries(main PRIVATE spdlog::spdlog_header_only) 59 | target_link_libraries(main PRIVATE librt.so) 60 | target_link_libraries(main PRIVATE sodium) 61 | target_link_libraries(main PRIVATE sqlite3) 62 | target_link_libraries(main PRIVATE OpenSSL::SSL OpenSSL::Crypto) 63 | 64 | target_link_libraries(test PRIVATE spdlog::spdlog_header_only) 65 | target_link_libraries(test PRIVATE Catch2::Catch2) 66 | target_link_libraries(test PRIVATE librt.so) 67 | target_link_libraries(test PRIVATE sodium) 68 | target_link_libraries(test PRIVATE sqlite3) 69 | target_link_libraries(test PRIVATE OpenSSL::SSL OpenSSL::Crypto) 70 | -------------------------------------------------------------------------------- /tests/user_service/user_service.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "../../src/user_service/user_service.h" 8 | 9 | TEST_CASE("LibSodium Test - Creating and authenticating password") { 10 | char hashed_password[crypto_pwhash_STRBYTES]; 11 | memset(hashed_password, 0, crypto_pwhash_STRBYTES); 12 | 13 | REQUIRE(hashed_password[0] == 0); 14 | 15 | const char *my_password = "my_password"; 16 | 17 | REQUIRE(crypto_pwhash_str(hashed_password, my_password, strlen(my_password), 18 | crypto_pwhash_OPSLIMIT_INTERACTIVE, 19 | crypto_pwhash_MEMLIMIT_INTERACTIVE) == 0); 20 | 21 | REQUIRE(hashed_password[0] != 0); 22 | 23 | // Make sure hashed password matches hashed version. 24 | REQUIRE(crypto_pwhash_str_verify(hashed_password, my_password, 25 | strlen(my_password)) == 0); 26 | 27 | // Make sure wrong password does not. 28 | const char *wrong_password = "battery staple"; 29 | REQUIRE(crypto_pwhash_str_verify(hashed_password, wrong_password, 30 | strlen(wrong_password)) != 0); 31 | } 32 | 33 | TEST_CASE("Sqlite3 - Smoke test") { 34 | sqlite3 *db; 35 | const char *db_name = "/tmp/test.db"; 36 | 37 | // cleanup just in case 38 | std::remove(db_name); 39 | 40 | int rc = sqlite3_open(db_name, &db); 41 | 42 | REQUIRE(rc == 0); 43 | 44 | sqlite3_close(db); 45 | std::remove(db_name); 46 | } 47 | 48 | TEST_CASE("UserService - Create database, add user and try logging in") { 49 | const char *db_name = "/tmp/users.db"; 50 | 51 | // cleanup just in case 52 | std::remove(db_name); 53 | 54 | int rc = system( 55 | "/app/scripts/operations/create_databases.py --user-db /tmp/users.db"); 56 | REQUIRE(rc == 0); 57 | 58 | rc = system("/app/scripts/operations/add_user.py --username sneilan " 59 | "--password password --user-db /tmp/users.db"); 60 | REQUIRE(rc == 0); 61 | 62 | // test a good password 63 | UserService *user_service = new UserService(db_name); 64 | AuthRet ret; 65 | ret.authenticated = false; 66 | char username[] = "sneilan"; 67 | char password[] = "password"; 68 | user_service->authenticate(username, password, &ret); 69 | 70 | REQUIRE(ret.authenticated == true); 71 | 72 | // Test a bad password 73 | char bad_password[] = "bad_password"; 74 | ret.authenticated = false; 75 | user_service->authenticate(username, bad_password, &ret); 76 | 77 | REQUIRE(ret.authenticated == false); 78 | 79 | // Test what happens when user does not exist 80 | ret.authenticated = false; 81 | char bad_username[] = "bad_username"; 82 | user_service->authenticate(bad_username, password, &ret); 83 | 84 | REQUIRE(ret.authenticated == false); 85 | 86 | std::remove(db_name); 87 | } 88 | -------------------------------------------------------------------------------- /src/util/linked_list.h: -------------------------------------------------------------------------------- 1 | #ifndef linked_list_h 2 | #define linked_list_h 3 | 4 | #include 5 | #include 6 | 7 | // #define LL_DEBUG(x) std::cout << x << "\n"; 8 | #define LL_DEBUG(x) 9 | 10 | // Needed internal implementation of linked list instead of std::list 11 | // because if you have multiple iterators on std::list and you delete nodes 12 | // from one of the iterators, the other iterators get invalidated. 13 | // I needed a way to maintain pointers to orders, delete orders w/o invalidating 14 | // my other pointers to orders. 15 | 16 | template struct Node { 17 | T data; 18 | Node *prev; 19 | Node *next; 20 | Node(T val) : data(val), prev(nullptr), next(nullptr) {} 21 | }; 22 | 23 | template class DoublyLinkedList { 24 | private: 25 | Node *head; 26 | Node *tail; 27 | int total; 28 | 29 | public: 30 | DoublyLinkedList(); 31 | Node *push_back(T val); 32 | Node *get_front(); 33 | Node *get_back(); 34 | void pop_front(); 35 | void remove(Node *node); 36 | int get_total(); 37 | }; 38 | 39 | template DoublyLinkedList::DoublyLinkedList() { 40 | head = nullptr; 41 | tail = nullptr; 42 | total = 0; 43 | } 44 | 45 | template Node *DoublyLinkedList::get_front() { return head; } 46 | 47 | template Node *DoublyLinkedList::get_back() { return tail; } 48 | 49 | template int DoublyLinkedList::get_total() { return total; } 50 | 51 | template Node *DoublyLinkedList::push_back(T val) { 52 | Node *newNode = new Node(val); 53 | if (head == nullptr) { 54 | head = newNode; 55 | tail = newNode; 56 | } else { 57 | tail->next = newNode; 58 | newNode->prev = tail; 59 | tail = newNode; 60 | } 61 | total++; 62 | return newNode; 63 | } 64 | 65 | template void DoublyLinkedList::pop_front() { 66 | if (head == nullptr) { 67 | return; 68 | } 69 | Node *nodeToRemove = head; 70 | 71 | head = head->next; 72 | 73 | if (head != nullptr) { 74 | head->prev = nullptr; 75 | } else { 76 | tail = nullptr; 77 | } 78 | 79 | total--; 80 | delete nodeToRemove; 81 | } 82 | 83 | template void DoublyLinkedList::remove(Node *node) { 84 | if (node == nullptr) { 85 | return; 86 | } 87 | if (node == head) { 88 | LL_DEBUG(1); 89 | head = node->next; 90 | LL_DEBUG(2); 91 | if (head != nullptr) { 92 | LL_DEBUG(3); 93 | head->prev = nullptr; 94 | LL_DEBUG(4); 95 | } 96 | } else if (node == tail) { 97 | tail = node->prev; 98 | LL_DEBUG(5); 99 | if (tail != nullptr) { 100 | tail->next = nullptr; 101 | LL_DEBUG(6); 102 | } 103 | } else { 104 | LL_DEBUG(7); 105 | node->prev->next = node->next; 106 | LL_DEBUG(8); 107 | node->next->prev = node->prev; 108 | LL_DEBUG(9); 109 | } 110 | total--; 111 | LL_DEBUG(10); 112 | delete node; 113 | LL_DEBUG(11); 114 | } 115 | 116 | #endif 117 | -------------------------------------------------------------------------------- /tests/util/mmap_object_pool.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/util/mmap_object_pool.h" 5 | 6 | const char *pool_name = "/object_pool"; 7 | 8 | TEST_CASE("Basic allocation test.") { 9 | MMapObjectPool allocator(10, pool_name, IS_CONTROLLER); 10 | 11 | int *a = allocator.allocate(); 12 | REQUIRE(allocator.num_obj_stored() == 1); 13 | 14 | allocator.del(a); 15 | REQUIRE(allocator.num_obj_stored() == 0); 16 | 17 | allocator.cleanup(); 18 | }; 19 | 20 | TEST_CASE("Allocate and free randomly") { 21 | MMapObjectPool allocator(10, pool_name, IS_CONTROLLER); 22 | 23 | int *a = allocator.allocate(); 24 | REQUIRE(allocator.num_obj_stored() == 1); 25 | int *b = allocator.allocate(); 26 | REQUIRE(allocator.num_obj_stored() == 2); 27 | 28 | allocator.del(b); 29 | REQUIRE(allocator.num_obj_stored() == 1); 30 | REQUIRE(allocator.num_random_free_spaces() == 1); 31 | 32 | allocator.del(a); 33 | REQUIRE(allocator.num_obj_stored() == 0); 34 | REQUIRE(allocator.num_random_free_spaces() == 2); 35 | 36 | // Finally make two allocations to see if 37 | a = allocator.allocate(); 38 | b = allocator.allocate(); 39 | REQUIRE(allocator.num_obj_stored() == 2); 40 | REQUIRE(allocator.num_random_free_spaces() == 0); 41 | 42 | // Now that we've allocated all random free spacers we should be 43 | // using fresh memory locations 44 | a = allocator.allocate(); 45 | REQUIRE(allocator.num_random_free_spaces() == 0); 46 | REQUIRE(allocator.num_obj_stored() == 3); 47 | 48 | allocator.cleanup(); 49 | }; 50 | 51 | TEST_CASE("Crash if we allocate more than we allow") { 52 | MMapObjectPool allocator(1, pool_name, true); 53 | allocator.allocate(); 54 | REQUIRE_THROWS(allocator.allocate()); 55 | 56 | allocator.cleanup(); 57 | }; 58 | 59 | TEST_CASE("Do offsets and memory locations line up") { 60 | MMapObjectPool allocator(10, pool_name, IS_CONTROLLER); 61 | int *a = allocator.allocate(); 62 | REQUIRE(allocator.pointer_to_offset(a) == 0); 63 | 64 | allocator.allocate(); 65 | int *b = allocator.allocate(); 66 | REQUIRE(allocator.pointer_to_offset(b) == 2); 67 | REQUIRE(allocator.offset_to_pointer(2) == b); 68 | 69 | allocator.cleanup(); 70 | } 71 | 72 | TEST_CASE("Can processes share object pool") { 73 | MMapObjectPool allocator(10, pool_name, IS_CONTROLLER); 74 | // allocate one object so we can see an offset of at least one. 75 | allocator.allocate(); 76 | int *a = allocator.allocate(); 77 | int a_offset = allocator.pointer_to_offset(a); 78 | 79 | *a = 5; 80 | 81 | pid_t c_pid = fork(); 82 | if (c_pid > 0) { 83 | // parent. Will continue testing. 84 | REQUIRE(allocator.pointer_to_offset(a) == 1); 85 | int status; 86 | waitpid(c_pid, &status, 0); 87 | } else { 88 | // child 89 | MMapObjectPool client_allocator(10, pool_name, IS_CLIENT); 90 | int *b = client_allocator.offset_to_pointer(a_offset); 91 | REQUIRE(*b == 5); 92 | REQUIRE(client_allocator.pointer_to_offset(b) == 1); 93 | client_allocator.cleanup(); 94 | exit(0); 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /tests/util/disruptor.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/util/disruptor.h" 5 | 6 | const char *disruptor_pool_name = "/disruptor_pool"; 7 | 8 | #define CONSUMER_1 0 9 | #define CONSUMER_2 1 10 | 11 | struct TestStruct { 12 | int a; 13 | int b; 14 | }; 15 | 16 | TEST_CASE("Disruptor Basic Test") { 17 | Producer producer(10, disruptor_pool_name); 18 | 19 | TestStruct item; 20 | item.a = 5; 21 | item.b = 6; 22 | 23 | producer.put(item); 24 | 25 | Consumer consumer(10, disruptor_pool_name, CONSUMER_1); 26 | 27 | TestStruct *pulledItem = consumer.get(); 28 | REQUIRE(pulledItem->a == 5); 29 | REQUIRE(pulledItem->b == 6); 30 | 31 | REQUIRE(consumer.get() == nullptr); 32 | // try again to make sure nothing crashes. 33 | REQUIRE(consumer.get() == nullptr); 34 | 35 | producer.cleanup(); 36 | consumer.cleanup(); 37 | }; 38 | 39 | TEST_CASE("Disruptor Producer Overflow Test") { 40 | int slots = 4; 41 | Producer producer(slots, disruptor_pool_name); 42 | Consumer consumer(slots, disruptor_pool_name, CONSUMER_1); 43 | 44 | TestStruct item; 45 | item.a = 5; 46 | item.b = 6; 47 | 48 | producer.put(item); // put at position 0 49 | producer.put(item); // 1 50 | producer.put(item); // 2 51 | producer.put(item); // 3 52 | 53 | // Consumer is at position 0 54 | // But should be able to get all three items. 55 | // Consumer starts at position 0. 56 | REQUIRE(consumer.get() != nullptr); 57 | REQUIRE(consumer.get() != nullptr); 58 | REQUIRE(consumer.get() != nullptr); 59 | REQUIRE(consumer.get() != nullptr); 60 | 61 | // no more items. 62 | REQUIRE(consumer.get() == nullptr); 63 | 64 | // consumer has consumed everything. should be able to put more stuff in. 65 | REQUIRE(producer.put(item) == true); 66 | REQUIRE(producer.put(item) == true); 67 | 68 | producer.cleanup(); 69 | consumer.cleanup(); 70 | }; 71 | 72 | TEST_CASE("Disruptor Consumer Overflow Test") { 73 | int slots = 4; 74 | Producer producer(slots, disruptor_pool_name); 75 | Consumer consumer(slots, disruptor_pool_name, CONSUMER_1); 76 | 77 | REQUIRE(consumer.get() == nullptr); 78 | 79 | producer.cleanup(); 80 | consumer.cleanup(); 81 | }; 82 | 83 | TEST_CASE("Disruptor Producer / Multiple Consumer") { 84 | int slots = 4; 85 | Producer producer(slots, disruptor_pool_name); 86 | Consumer consumer1(slots, disruptor_pool_name, CONSUMER_1); 87 | Consumer consumer2(slots, disruptor_pool_name, CONSUMER_2); 88 | 89 | TestStruct item; 90 | item.a = 5; 91 | item.b = 6; 92 | 93 | REQUIRE(producer.put(item) == true); 94 | REQUIRE(consumer1.get() != nullptr); 95 | REQUIRE(consumer1.get() == nullptr); 96 | 97 | REQUIRE(consumer2.get() != nullptr); 98 | REQUIRE(consumer2.get() == nullptr); 99 | 100 | REQUIRE(producer.put(item) == true); 101 | 102 | REQUIRE(consumer2.get() != nullptr); 103 | REQUIRE(consumer2.get() == nullptr); 104 | 105 | producer.cleanup(); 106 | consumer1.cleanup(); 107 | consumer2.cleanup(); 108 | }; 109 | -------------------------------------------------------------------------------- /src/util/hash_map.h: -------------------------------------------------------------------------------- 1 | // https://craftinginterpreters.com/hash-tables.html 2 | // Robin Hood hashing 3 | // cuckoo hashing 4 | // double hashing 5 | 6 | // Linear Probing 7 | // http://www.isthe.com/chongo/tech/comp/fnv/ 8 | 9 | // UNUSED. Still using stl library but will probably use soon. 10 | // @TODO Does this beat HashDos attacks? 11 | // Should we use quadratic instead of linear probing? 12 | 13 | #ifndef hashtable_h 14 | #define hashtable_h 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #define TABLE_MAX_LOAD 0.75 21 | 22 | typedef struct { 23 | int key; 24 | int value; 25 | } Entry; 26 | 27 | typedef struct { 28 | int count; 29 | int capacity; 30 | Entry *entries; 31 | } Table; 32 | 33 | void tableAddAll(Table *from, Table *to); 34 | bool tableSet(Table *table, int key, int value); 35 | bool tableGet(Table *table, int key, int value); 36 | void initTable(Table *table); 37 | void freeTable(Table *table); 38 | 39 | void initTable(Table *table) { 40 | table->count = 0; 41 | table->capacity = 0; 42 | table->entries = 0; 43 | } 44 | 45 | void freeTable(Table *table) { 46 | FREE_ARRAY(Entry, table->entries, table->capacity); 47 | initTable(table); 48 | } 49 | 50 | // FNV-1a 51 | static uint32_t hashInt(int key) { 52 | uint32_t hash = 2166136261u; 53 | 54 | for (int i = 0; i < sizeof(int); i++) { 55 | hash ^= (&key)[i]; 56 | hash *= 16777619; 57 | } 58 | 59 | return hash; 60 | } 61 | 62 | static Entry *findEntry(Entry *entries, int capacity, int key) { 63 | uint32_t index = hashInt(key) % capacity; 64 | for (;;) { 65 | Entry *entry = &entries[index]; 66 | 67 | if (entry->key == key || entry->key == NULL) { 68 | return entry; 69 | } 70 | 71 | index = (index + 1) % capacity; 72 | } 73 | } 74 | 75 | void tableAddAll(Table *from, Table *to) { 76 | for (int i = 0; i < from->capacity; i++) { 77 | Entry *entry = &from->entries[i]; 78 | if (entry->key != NULL) { 79 | tableSet(to, entry->key, entry->value); 80 | } 81 | } 82 | } 83 | 84 | static void adjustCapacity(Table *table, int capacity) { 85 | Entry *entries = ALLOCATE(Entry, capacity); 86 | for (int i = 0; i < capacity; i++) { 87 | entries[i].key = NULL; 88 | entries[i].value = NIL_VAL; 89 | } 90 | 91 | for (int i = 0; i < table->capacity; i++) { 92 | Entry *entry = &table->entries[i]; 93 | if (entry->key == NULL) 94 | continue; 95 | 96 | Entry *dest = findEntry(entries, capacity, entry->key); 97 | dest->key = entry->key; 98 | dest->value = dest->value; 99 | } 100 | 101 | FREE_ARRAY(Entry, table->entries, table->capacity); 102 | table->entries = entries; 103 | table->capacity = capacity; 104 | } 105 | 106 | bool tableSet(Table *table, int key, int value) { 107 | if (table->count + 1 > table->capacity * TABLE_MAX_LOAD) { 108 | int capacity = GROW_CAPACITY(table->capacity); 109 | adjustCapacity(table, capacity); 110 | } 111 | 112 | Entry *entry = findEntry(table->entries, table->capacity, key); 113 | bool isNewKey = entry->key == NULL; 114 | if (isNewKey) 115 | table->count++; 116 | 117 | entry->key = key; 118 | entry->value = value; 119 | 120 | return isNewKey; 121 | } 122 | 123 | #endif 124 | -------------------------------------------------------------------------------- /src/order_book/price_level.cpp: -------------------------------------------------------------------------------- 1 | #include "price_level.h" 2 | 3 | // Returns a list of filled orders. 4 | // Quantity of order is modified in place. 5 | // Order is expected to be for opposite side. 6 | std::list PriceLevel::fillOrder(Order *order) { 7 | // Keep popping orders off at price level until we have either filled quantity 8 | // or run out of orders. 9 | 10 | int unfilled_quantity = order->unfilled_quantity(); 11 | SPDLOG_DEBUG("Level {} - Order id {} unfilled_quantity is " 12 | "{}. num orders in queue is {}", 13 | limitPrice, order->id, unfilled_quantity, orders.get_total()); 14 | 15 | std::list updated_orders; 16 | while (orders.get_front() != nullptr && unfilled_quantity > 0) { 17 | Node *node = orders.get_front(); 18 | int quantity_available = node->data->unfilled_quantity(); 19 | SPDLOG_DEBUG("Level {} - iterating node {}, quantity {}", limitPrice, 20 | node->data->id, quantity_available); 21 | 22 | updated_orders.push_back(node->data); 23 | 24 | if (quantity_available >= unfilled_quantity) { 25 | SPDLOG_DEBUG("Level {} - Order id {} filling.", limitPrice, order->id); 26 | node->data->filled_quantity += order->unfilled_quantity(); 27 | totalVolume -= order->unfilled_quantity(); 28 | order->filled_quantity = order->quantity; 29 | // If we've filled the order, stop. 30 | SPDLOG_DEBUG("Level {} - Order id {} filled. volume {} " 31 | "remains on level. Order has {} quantity remaining.", 32 | limitPrice, order->id, totalVolume, 33 | order->unfilled_quantity()); 34 | if (node->data->unfilled_quantity() == 0) { 35 | SPDLOG_DEBUG("Level {} - Order id {} removing node " 36 | "{} because order filled.", 37 | limitPrice, order->id, node->data->id); 38 | orders.remove(node); 39 | } 40 | break; 41 | } else if (unfilled_quantity > quantity_available) { 42 | SPDLOG_DEBUG("Level {} - Order id {} partial filling", limitPrice, 43 | order->id); 44 | order->filled_quantity += quantity_available; 45 | unfilled_quantity -= quantity_available; 46 | totalVolume -= quantity_available; 47 | SPDLOG_DEBUG("Level {} - Order id {} partial filling, " 48 | "removing node {}", 49 | limitPrice, order->id, node->data->id); 50 | orders.remove(node); 51 | SPDLOG_DEBUG("Level {} - Order id {} quantity {} remains", limitPrice, 52 | order->id, order->unfilled_quantity()); 53 | } 54 | } 55 | 56 | return updated_orders; 57 | } 58 | 59 | void PriceLevel::cancelOrder(Node *node) { 60 | this->totalVolume -= node->data->unfilled_quantity(); 61 | orders.remove(node); 62 | SPDLOG_DEBUG("Level {} - Level now has {} orders", limitPrice, 63 | orders.get_total()); 64 | } 65 | 66 | Node *PriceLevel::addOrder(Order *order) { 67 | this->totalVolume += order->quantity; 68 | Node *node = orders.push_back(order); 69 | SPDLOG_DEBUG("Level {} - Level now has {} orders", limitPrice, 70 | orders.get_total()); 71 | return node; 72 | } 73 | 74 | int PriceLevel::getVolume() { return totalVolume; } 75 | 76 | int PriceLevel::getPrice() { return limitPrice; } 77 | 78 | PriceLevel::PriceLevel(PRICE price) { 79 | limitPrice = price; 80 | totalVolume = 0; 81 | } 82 | -------------------------------------------------------------------------------- /src/util/object_pool.h: -------------------------------------------------------------------------------- 1 | #ifndef object_pool_h 2 | #define object_pool_h 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // Note to self, template classes must reside entirely in header files 12 | // Compiler needs access to all source to generate code for template 13 | // https://stackoverflow.com/a/1353981/761726 14 | // http://www.parashift.com/c++-faq-lite/templates-defn-vs-decl.html 15 | 16 | template class ObjectPool { 17 | private: 18 | T *block; 19 | T *next_free_space; 20 | 21 | int max_num_obj; 22 | int cur_num_obj; 23 | 24 | // this is a stack of free spaces 25 | T **free_spaces; 26 | // original pointer to stack of free spaces. 27 | T **free_space_block; 28 | int num_free_spaces; 29 | 30 | public: 31 | ObjectPool(int max_num_obj_); 32 | ~ObjectPool(); 33 | T *allocate(); 34 | void del(T *obj); 35 | int num_obj_stored(); 36 | int num_random_free_spaces(); 37 | void cleanup(); 38 | }; 39 | 40 | template 41 | ObjectPool::ObjectPool(int max_num_obj_) { 42 | max_num_obj = max_num_obj_; 43 | 44 | size_t block_size = sizeof(T) * max_num_obj; 45 | block = (T*)malloc(block_size); 46 | memset(block, 0, block_size); 47 | 48 | next_free_space = block; 49 | num_free_spaces = 0; 50 | cur_num_obj = 0; 51 | 52 | // Keep track of free spaces in a stack in case we delete an object that's not 53 | // at end of array. 54 | size_t free_space_size = sizeof(T*) * max_num_obj; 55 | free_spaces = (T**)malloc(free_space_size); 56 | memset(free_spaces, 0, free_space_size); 57 | free_space_block = free_spaces; 58 | }; 59 | 60 | template void ObjectPool::cleanup() { 61 | free(block); 62 | free(free_space_block); 63 | } 64 | 65 | template ObjectPool::~ObjectPool() { } 66 | 67 | template int ObjectPool::num_obj_stored() { 68 | return cur_num_obj; 69 | } 70 | 71 | template int ObjectPool::num_random_free_spaces() { 72 | return num_free_spaces; 73 | } 74 | 75 | template T *ObjectPool::allocate() { 76 | if (cur_num_obj + 1 > max_num_obj) { 77 | throw std::runtime_error("Could not allocate obj"); 78 | } 79 | 80 | cur_num_obj++; 81 | 82 | // if we want to overwrite some existing spaces 83 | if (num_free_spaces > 0) { 84 | 85 | // subtract from number of existing spaces. 86 | num_free_spaces--; 87 | // Pointer in free spaces always points to null so decrementing gives us a 88 | // pointer to a good memory location. 89 | --free_spaces; 90 | return *free_spaces; 91 | } 92 | 93 | // Return a pointer to the next free space & increment. 94 | return next_free_space++; 95 | }; 96 | 97 | template void ObjectPool::del(T *obj) { 98 | // We don't wipe memory here. Up to caller to take care of allocation / 99 | // setting. 100 | 101 | if (obj > (block + max_num_obj)) { 102 | throw std::runtime_error( 103 | "Attempted to free obj outside of stack allocator bounds."); 104 | } 105 | 106 | cur_num_obj--; 107 | 108 | // if object is deleting something already allocated. 109 | if (obj < next_free_space) { 110 | if (num_free_spaces + 1 > max_num_obj) { 111 | SPDLOG_CRITICAL("Unexpected condition in object pool. {} {}", num_free_spaces, max_num_obj); 112 | throw std::runtime_error("Unexpected condition in object pool"); 113 | } 114 | 115 | // push location of obj we are freeing onto stack of memory spaces 116 | // We can give to other objects. 117 | *free_spaces = obj; 118 | // Point stack at null so we can put another object reference there. 119 | free_spaces++; 120 | num_free_spaces++; 121 | } else { 122 | // Only use new memory if we run out of randomly deleted spaces. 123 | next_free_space--; 124 | } 125 | } 126 | 127 | #endif 128 | -------------------------------------------------------------------------------- /scripts/python_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | import time 4 | from struct import pack, unpack 5 | import random 6 | import threading 7 | 8 | GATEWAY_PORT = 443 9 | MARKET_PORT = 4443 10 | 11 | 12 | class Client: 13 | port = None 14 | host = None 15 | 16 | def log(self, msg): 17 | print(msg) 18 | 19 | def connect(self): 20 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 21 | self.sock.connect((self.host, self.port)) 22 | response = self.sock.recv(1024) 23 | self.log(response) 24 | 25 | def disconnect(self): 26 | self.sock.close() 27 | 28 | def listener(self): 29 | raise NotImplementedError() 30 | 31 | def start_listener(self): 32 | self.thread = threading.Thread(target=self.listener) 33 | self.thread.start() 34 | 35 | def stop_listener(self): 36 | raise NotImplementedError() 37 | 38 | 39 | class MarketClient(Client): 40 | host = '0.0.0.0' 41 | port = MARKET_PORT 42 | 43 | def listener(self): 44 | while True: 45 | data = self.sock.recv(16) 46 | print(data) 47 | print(len(data)) 48 | if not data: 49 | return 50 | 51 | # c is char 52 | # Q is unsigned long long 53 | # i is 4 byte integer 54 | # x is 1 byte padding. 55 | format_string = 'BcxxiQ' 56 | unpacked_data = unpack(format_string, data) 57 | 58 | version = unpacked_data[0] 59 | msg_type = unpacked_data[1].decode() 60 | val = unpacked_data[2] 61 | time_ms = unpacked_data[3] 62 | 63 | self.handle_notification(msg_type, val, time_ms) 64 | 65 | def handle_notification(self, msg_type: str, val: int, time_ms: int): 66 | self.log({'msg_type': msg_type, 'val': val, 'time_ms': time_ms}) 67 | 68 | 69 | class TradingClient(Client): 70 | host = '0.0.0.0' 71 | port = GATEWAY_PORT 72 | 73 | type_to_msg = { 74 | 'u': 'updated', 75 | 'f': 'filled', 76 | 'r': 'recieved' 77 | } 78 | 79 | def __init__(self): 80 | pass 81 | 82 | def trade(self, price: int, quantity: int, side: str): 83 | assert side in ['b', 's'], 'Side must be b or s for buy and sell' 84 | 85 | message = pack( 86 | 'cii', 87 | bytes(side, 'ascii'), 88 | price, 89 | quantity, 90 | ) 91 | 92 | self.sock.sendall(message) 93 | 94 | def listener(self): 95 | while True: 96 | data = self.sock.recv(21) 97 | if not data: 98 | return 99 | 100 | # c is char 101 | # Q is unsigned long long 102 | # i is 4 byte integer 103 | format_string = 'Qiii' 104 | unpacked_data = unpack(format_string, data[1:]) 105 | msg_type = chr(data[0]) 106 | message = self.type_to_msg[msg_type] 107 | 108 | id = unpacked_data[0] 109 | quantity = unpacked_data[1] 110 | filled_quantity = unpacked_data[2] 111 | client_id = unpacked_data[3] 112 | 113 | self.handle_notification(id, quantity, filled_quantity, client_id) 114 | 115 | def handle_notification(self, id, quantity, filled_quantity, client_id): 116 | self.log({'id': id, 'quantity': quantity, 'filled_quantity': filled_quantity, 'client_id': client_id}) 117 | 118 | 119 | client = TradingClient() 120 | client.connect() 121 | client.start_listener() 122 | 123 | mkt = MarketClient() 124 | mkt.connect() 125 | mkt.start_listener() 126 | 127 | while True: 128 | char = sys.stdin.read(1) 129 | if char == 'b': 130 | break 131 | time.sleep(.1) 132 | print("Placing trade.") 133 | price = random.randint(101, 999) 134 | quantity = random.randint(1, 10) 135 | side = 'b' if random.randint(0, 1) == 0 else 's' 136 | client.trade(price, quantity, side) 137 | 138 | -------------------------------------------------------------------------------- /src/util/mmap_object_pool.h: -------------------------------------------------------------------------------- 1 | #ifndef mmap_object_pool_h 2 | #define mmap_object_pool_h 3 | 4 | #include "mmap_wrapper.h" 5 | #include 6 | #include 7 | #include 8 | 9 | // Note to self, template classes must reside entirely in header files 10 | // Compiler needs access to all source to generate code for template 11 | // https://stackoverflow.com/a/1353981/761726 12 | // http://www.parashift.com/c++-faq-lite/templates-defn-vs-decl.html 13 | 14 | #define IS_CONTROLLER true 15 | #define IS_CLIENT false 16 | 17 | template class MMapObjectPool { 18 | private: 19 | T *block; 20 | T *next_free_space; 21 | 22 | // Need to track number of objects allocated so we don't overflow capacity. 23 | int max_num_obj; 24 | int cur_num_obj; 25 | 26 | T **free_spaces; 27 | int num_free_spaces; 28 | MMapMeta *mmap_meta; 29 | bool is_controller; 30 | 31 | public: 32 | MMapObjectPool(int max_num_obj, const char *pool_name, bool _is_controller); 33 | ~MMapObjectPool(); 34 | T *allocate(); 35 | int pointer_to_offset(T *pointer); 36 | T *offset_to_pointer(int offset); 37 | void del(T *obj); 38 | int num_obj_stored(); 39 | int num_random_free_spaces(); 40 | void cleanup(); 41 | }; 42 | 43 | template void MMapObjectPool::cleanup() { 44 | if (is_controller) { 45 | delete_mmap(mmap_meta); 46 | } else { 47 | close_mmap(mmap_meta); 48 | } 49 | } 50 | 51 | template MMapObjectPool::~MMapObjectPool() { cleanup(); } 52 | 53 | template int MMapObjectPool::num_obj_stored() { 54 | return cur_num_obj; 55 | } 56 | 57 | template int MMapObjectPool::num_random_free_spaces() { 58 | return num_free_spaces; 59 | } 60 | 61 | template int MMapObjectPool::pointer_to_offset(T *pointer) { 62 | return pointer - (T *)mmap_meta->location; 63 | } 64 | 65 | template T *MMapObjectPool::offset_to_pointer(int offset) { 66 | return (T *)mmap_meta->location + offset; 67 | } 68 | 69 | template 70 | MMapObjectPool::MMapObjectPool(int max_num_obj_, const char *pool_name, 71 | bool _is_controller) { 72 | is_controller = _is_controller; 73 | 74 | max_num_obj = max_num_obj_; 75 | 76 | if (is_controller) { 77 | mmap_meta = init_mmap(pool_name, sizeof(T) * max_num_obj); 78 | } else { 79 | mmap_meta = open_mmap(pool_name, sizeof(T) * max_num_obj); 80 | } 81 | 82 | block = (T *)mmap_meta->location; 83 | 84 | next_free_space = block; 85 | num_free_spaces = 0; 86 | cur_num_obj = 0; 87 | 88 | // Keep track of free spaces in a stack in case we delete an object that's not 89 | // at end of array. 90 | free_spaces = new T *[max_num_obj]; 91 | }; 92 | 93 | template T *MMapObjectPool::allocate() { 94 | if (cur_num_obj + 1 > max_num_obj) { 95 | throw std::runtime_error("Could not allocate obj"); 96 | } 97 | 98 | cur_num_obj++; 99 | 100 | if (num_free_spaces > 0) { 101 | 102 | num_free_spaces--; 103 | // Pointer in free spaces always points to null so decrementing gives us a 104 | // pointer to a good memory location. 105 | --free_spaces; 106 | return *free_spaces; 107 | } 108 | 109 | // Return a pointer to the next free space & increment. 110 | return next_free_space++; 111 | }; 112 | 113 | template void MMapObjectPool::del(T *obj) { 114 | // We don't wipe memory here. Up to caller to take care of allocation / 115 | // setting. 116 | 117 | if (obj > (block + max_num_obj)) { 118 | throw std::runtime_error( 119 | "Attempted to free obj outside of stack allocator bounds."); 120 | } 121 | 122 | cur_num_obj--; 123 | 124 | if (obj < next_free_space) { 125 | if (num_free_spaces + 1 > max_num_obj) { 126 | throw std::runtime_error("Unexpected condition."); 127 | } 128 | 129 | // push location of obj we are freeing onto stack of memory spaces 130 | // We can give to other objects. 131 | *free_spaces = obj; 132 | // Point stack at null so we can put another object reference there. 133 | free_spaces++; 134 | num_free_spaces++; 135 | } else { 136 | // Only use new memory if we run out of randomly deleted spaces. 137 | next_free_space--; 138 | } 139 | } 140 | 141 | #endif 142 | -------------------------------------------------------------------------------- /src/gateway/gateway.cpp: -------------------------------------------------------------------------------- 1 | #include "gateway.h" 2 | 3 | Gateway::Gateway(Producer *incoming_msg_producer, 4 | Consumer *outgoing_message_consumer, 5 | MMapObjectPool *order_pool) { 6 | this->incoming_msg_producer = incoming_msg_producer; 7 | this->outgoing_message_consumer = outgoing_message_consumer; 8 | this->order_pool = order_pool; 9 | fill(connected_webclients.begin(), connected_webclients.end(), false); 10 | } 11 | 12 | Gateway::Gateway() { 13 | fill(connected_webclients.begin(), connected_webclients.end(), false); 14 | } 15 | 16 | Gateway::~Gateway() throw() {} 17 | 18 | void Gateway::handleOutgoingMessage() { 19 | ORDER_MMAP_OFFSET *offset = outgoing_message_consumer->get(); 20 | if (offset != nullptr) { 21 | Order *order = order_pool->offset_to_pointer(*offset); 22 | 23 | // message type (char) 24 | // sequence ID (unsigned long long) 25 | // total quantity (integer) 26 | // filled quantity (integer) 27 | // (TODO) 28 | // last fill price (integer) 29 | // last quantity filled (integer) 30 | 31 | int total_size = sizeof(char) + sizeof(order->id) + 32 | sizeof(order->quantity) + sizeof(order->filled_quantity) + 33 | sizeof(order->clientId); 34 | char buffer[total_size]; 35 | 36 | char orderRecieved = 'r'; 37 | char orderUpdated = 'u'; 38 | char orderFilled = 'f'; 39 | // @TODO cancelled later. 40 | 41 | if (order->unfilled_quantity() == order->quantity) { 42 | buffer[0] = orderRecieved; 43 | } else if (order->unfilled_quantity() == 0) { 44 | buffer[0] = orderFilled; 45 | } else { 46 | buffer[0] = orderUpdated; 47 | } 48 | 49 | int offset = 1; 50 | std::memcpy(buffer + offset, &order->id, sizeof(order->id)); 51 | offset += sizeof(order->id); 52 | std::memcpy(buffer + offset, &order->quantity, sizeof(order->quantity)); 53 | offset += sizeof(order->quantity); 54 | std::memcpy(buffer + offset, &order->filled_quantity, 55 | sizeof(order->filled_quantity)); 56 | offset += sizeof(order->clientId); 57 | std::memcpy(buffer + offset, &order->clientId, sizeof(order->clientId)); 58 | 59 | sendMessage(order->clientId, buffer, total_size); 60 | SPDLOG_DEBUG("Sent {} message {} about order {}", order->clientId, 61 | buffer[0], order->id); 62 | } 63 | } 64 | 65 | void Gateway::readMessage(int client_id, const char *message, 66 | int message_size) { 67 | if (!connected_webclients[client_id]) { 68 | string response = websocket_request_response(message); 69 | 70 | if (!sendMessage(client_id, response.c_str(), response.length())) { 71 | // @TODO perhaps sendMessage can handle what happens if a client 72 | // disconnects Then call our disconnected handler and let us know so we 73 | // don't have to an error handling pattern everywhere. 74 | forceDisconnect(client_id); 75 | } else { 76 | connected_webclients[client_id] = true; 77 | SPDLOG_INFO("Websocket handshake completed with {}", client_id); 78 | } 79 | 80 | return; 81 | }; 82 | 83 | SPDLOG_INFO("Read message from {}", client_id); 84 | 85 | NewOrderEvent item; 86 | 87 | // Not building an authentication system yet 88 | // so just sending trades back to clients by socket id. 89 | item.clientId = client_id; 90 | item.limitPrice = ((NewOrderEvent *)message)->limitPrice; 91 | item.side = ((NewOrderEvent *)message)->side; 92 | item.quantity = ((NewOrderEvent *)message)->quantity; 93 | 94 | incoming_msg_producer->put(item); 95 | 96 | SPDLOG_INFO("Ring buffer Order recieved from client {} for price {} for " 97 | "side {} quantity {}", 98 | item.clientId, item.limitPrice, item.side, item.quantity); 99 | } 100 | 101 | void Gateway::newClient(int client_id) { 102 | SPDLOG_INFO("New client {}", client_id); 103 | 104 | connected_webclients[client_id] = false; 105 | 106 | // const char *msg = "Welcome new client"; 107 | } 108 | 109 | void Gateway::disconnected(int client_id) { 110 | SPDLOG_INFO("Client disconnected {}", client_id); 111 | } 112 | 113 | void Gateway::run() { 114 | /* 115 | Socket server should have the following events 116 | 1) New connection 117 | 2) Disconnect 118 | 3) Reading data from client 119 | Expose the following methods 120 | 1) Sending data to a client 121 | 2) Getting a list of open clients. 122 | 3) Force closing a client connection 123 | Something else can handle parsing data from clients. 124 | */ 125 | 126 | char *port = getenv("GATEWAY_PORT"); 127 | bindSocket(atoi(port)); 128 | listenToSocket(); 129 | } 130 | -------------------------------------------------------------------------------- /tests/gateway/util/websocket.test.cpp: -------------------------------------------------------------------------------- 1 | #include "../../../src/gateway/util/websocket.h" 2 | #include "../../../src/gateway/gateway.h" 3 | #include "../../../src/gateway/socket.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | using namespace std; 18 | 19 | class SSLWebSocketTest : public SocketServer { 20 | public: 21 | SSLWebSocketTest() { 22 | for (int i = 0; i < MAX_CLIENTS; i++) { 23 | this->connected_webclients.push_back(false); 24 | } 25 | }; 26 | ~SSLWebSocketTest() throw(){}; 27 | 28 | void newClient(int client_id){ 29 | SPDLOG_INFO("Client {} connected", client_id); 30 | }; 31 | 32 | void disconnected(int client_id){ 33 | SPDLOG_INFO("Client {} disconnected. Server exiting", client_id); 34 | }; 35 | 36 | void readMessage(int client_id, const char *message, int message_size) { 37 | if (!connected_webclients[client_id]) { 38 | SPDLOG_INFO("Client {} has not shaken hands", client_id); 39 | 40 | string response = websocket_request_response(message); 41 | 42 | if (!sendMessage(client_id, response.c_str(), response.length())) { 43 | SPDLOG_INFO("Failure to send message to client {}", client_id); 44 | // @TODO perhaps sendMessage can handle what happens if a client 45 | // disconnects Then call our disconnected handler and let us know so we 46 | // don't have to an error handling pattern everywhere. 47 | forceDisconnect(client_id); 48 | } else { 49 | connected_webclients[client_id] = true; 50 | SPDLOG_INFO("Websocket handshake completed with {}", client_id); 51 | } 52 | } else { 53 | string decodedMessage; 54 | bool ret = decodeWebSocketFrame(message, decodedMessage); 55 | if (ret) { 56 | SPDLOG_INFO("Recieved {} from client_id {}", decodedMessage, client_id); 57 | vector message = encodeWebsocketFrame("acknowledged."); 58 | const char *charArray = reinterpret_cast(message.data()); 59 | 60 | sendMessage(client_id, charArray, message.size()); 61 | } else { 62 | SPDLOG_INFO("Could not decode packet from client_id {}", client_id); 63 | forceDisconnect(client_id); 64 | } 65 | } 66 | }; 67 | 68 | void handleOutgoingMessage(){}; 69 | 70 | protected: 71 | vector connected_webclients; 72 | }; 73 | 74 | // Test is failing because I am not understanding the output. 75 | // TEST_CASE("Base64 Encode") { 76 | // string input = "hello world"; 77 | // string expected = "aGVsbG8gd29ybGQAAAAAAHAmEKU="; 78 | 79 | // string encoding_output = base64_encode((const unsigned char *)input.c_str()); 80 | 81 | // REQUIRE(encoding_output == expected); 82 | // } 83 | 84 | TEST_CASE("Base64 Decode") { 85 | string input = "hello world"; 86 | string expected_encoding = "aGVsbG8gd29ybGQ="; 87 | 88 | string encoding_output = base64_encode((const unsigned char *)input.c_str()); 89 | string decoding_output = base64_decode(encoding_output); 90 | 91 | REQUIRE(decoding_output == input); 92 | } 93 | 94 | TEST_CASE("HTTP Header Parsing") { 95 | string headers = "Content-Type: text/html\n" 96 | "Content-Length: 123\n" 97 | "Connection: keep-alive\n" 98 | "Host: www.example.com"; 99 | 100 | map parsed_headers = parse_http_headers(headers); 101 | REQUIRE(parsed_headers.at("Content-Type") == "text/html"); 102 | } 103 | 104 | TEST_CASE("Websocket Request/Response") { 105 | string headers = "GET /chat HTTP/1.1\n" 106 | "Host: example.com:8000\n" 107 | "Upgrade: websocket\n" 108 | "Connection: Upgrade\n" 109 | "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\n" 110 | "Sec-WebSocket-Version: 13"; 111 | string response = websocket_request_response(headers); 112 | SPDLOG_DEBUG("Response {}", response); 113 | } 114 | 115 | TEST_CASE("Websocket Client Connection") { 116 | pid_t child_pid; 117 | 118 | // Create a new process by forking the current process 119 | child_pid = fork(); 120 | 121 | if (child_pid == -1) { 122 | SPDLOG_ERROR("Could not fork ssl socket server"); 123 | exit(1); 124 | } 125 | 126 | if (child_pid == 0) { 127 | SSLWebSocketTest ss; 128 | 129 | char *port = getenv("GATEWAY_PORT"); 130 | 131 | ss.bindSocket(atoi(port)); 132 | ss.listenToSocket(); 133 | } else { 134 | // Attempt to connect to ssl server with test client. 135 | int client_rc = 136 | system("/app/tests/gateway/websocket_test_client.py > /dev/stdout"); 137 | 138 | // If status is 0 then client successfully connected & server is exiting. 139 | REQUIRE(client_rc == 0); 140 | 141 | kill(child_pid, 9); 142 | } 143 | } 144 | 145 | // Test case failing because sha1 output comes as binary not hex. 146 | // TEST_CASE("Sha1") { 147 | // string input = "hello world"; 148 | // string expected_sha1 = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"; 149 | 150 | // string sha1_output = sha1(input); 151 | // // printStringAsHex(sha1_output); 152 | 153 | // REQUIRE(sha1_output == expected_sha1); 154 | // } 155 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "eventstore/eventstore.h" 10 | #include "gateway/gateway.h" 11 | #include "gateway/market_data.h" 12 | #include "gateway/socket.h" 13 | #include "order_book/order_book.h" 14 | #include "util/types.h" 15 | 16 | int main() { 17 | spdlog::set_level(spdlog::level::debug); 18 | // https://github.com/gabime/spdlog/wiki/3.-Custom-formatting 19 | spdlog::set_pattern("%-5l %E %-16s%-4#%-21! %v"); 20 | 21 | if (sodium_init() == -1) { 22 | // Initialization failed 23 | SPDLOG_CRITICAL("Could not initialize libsodium for user auth! ❌"); 24 | return -1; 25 | } 26 | SPDLOG_INFO("libsodium initialized."); 27 | 28 | const char *outgoing_message_buf = "/ss_outgoing_messages"; 29 | 30 | Producer outboundMessage(MAX_OUTGOING_MESSAGES, 31 | outgoing_message_buf); 32 | 33 | SPDLOG_INFO("Allocating EventStore mmap pool.."); 34 | const char *eventstore_buf = "/eventstore_buf"; 35 | MMapObjectPool *order_pool = 36 | new MMapObjectPool(MAX_OPEN_ORDERS, eventstore_buf, IS_CONTROLLER); 37 | SPDLOG_INFO("Allocated EventStore mmap pool!"); 38 | 39 | const char *incoming_msg_buf = "/gateway_ring_buf"; 40 | Producer *producer = 41 | new Producer(GATEWAY_BUFLEN, incoming_msg_buf); 42 | 43 | Consumer *outgoing_message_consumer = 44 | new Consumer(MAX_OUTGOING_MESSAGES, 45 | outgoing_message_buf, 46 | OUTGOING_MESSAGE_CONSUMER); 47 | 48 | order_pool = 49 | new MMapObjectPool(MAX_OPEN_ORDERS, eventstore_buf, IS_CLIENT); 50 | 51 | const char *outbound_market_data_buf = "/l1_market_data"; 52 | Producer *producer_l1_market_data = new Producer( 53 | MAX_MARKET_DATA_UPDATES, outbound_market_data_buf); 54 | 55 | Consumer *consumer_l1_market_data = new Consumer( 56 | MAX_MARKET_DATA_UPDATES, outbound_market_data_buf, 57 | OUTGOING_MESSAGE_CONSUMER); 58 | 59 | Gateway *gateway = 60 | new Gateway(producer, outgoing_message_consumer, order_pool); 61 | MarketData *market_data = new MarketData(consumer_l1_market_data); 62 | 63 | SPDLOG_INFO("Exchange starting"); 64 | 65 | pid_t c_pid = fork(); 66 | 67 | if (c_pid == -1) { 68 | SPDLOG_CRITICAL("fork"); 69 | exit(EXIT_FAILURE); 70 | } 71 | 72 | if (c_pid > 0) { 73 | // Parent 74 | // Listens to new orders from clients and puts them into the mmap ring 75 | // buffer maintained by gateway. 76 | SPDLOG_INFO("Gateway starting"); 77 | gateway->run(); 78 | } else { 79 | pid_t c_pid2 = fork(); 80 | 81 | if (c_pid2 > 0) { 82 | SPDLOG_INFO("MarketData starting"); 83 | market_data->run(); 84 | } else { 85 | // Child 86 | SPDLOG_INFO("Order engine starting"); 87 | EventStore *eventStore = new EventStore(order_pool); 88 | SPDLOG_INFO("Created EventStore"); 89 | 90 | OrderBook *orderBook = new OrderBook(producer_l1_market_data); 91 | SPDLOG_INFO("Created OrderBook"); 92 | 93 | Consumer *incoming_order_consumer = 94 | new Consumer(GATEWAY_BUFLEN, incoming_msg_buf, 95 | GATEWAY_CONSUMER); 96 | SPDLOG_INFO("Created consumer for incoming orders."); 97 | 98 | while (1) { 99 | // Constantly checking for new orders in the gateway ring buffer. 100 | NewOrderEvent *item = incoming_order_consumer->get(); 101 | 102 | if (item == nullptr) { 103 | continue; 104 | } 105 | 106 | SPDLOG_DEBUG("Order get for client {} for price {} for " 107 | "side {} quantity {}", 108 | item->clientId, item->limitPrice, item->side, 109 | item->quantity); 110 | 111 | // Store the event in the event store 112 | SEQUENCE_ID id = eventStore->newEvent(item->side, item->limitPrice, 113 | item->clientId, item->quantity); 114 | SPDLOG_INFO("Sequence ID is now {} & size is now {}", id, 115 | eventStore->size()); 116 | 117 | // Get response here & spool information to new ring buffer 118 | Order *order = eventStore->get(id); 119 | ORDER_MMAP_OFFSET offset = eventStore->getOffset(id); 120 | SPDLOG_INFO("Grabbed order {}", order->id); 121 | std::list updated_orders = orderBook->newOrder(order); 122 | 123 | outboundMessage.put(offset); 124 | // State of order is based on how many fills. 125 | SPDLOG_DEBUG("Order {} recieved message sent", order->id); 126 | 127 | SPDLOG_INFO("Order book volume is now {}", orderBook->getVolume()); 128 | SPDLOG_INFO("Orders updated are size {}", updated_orders.size()); 129 | 130 | for (Order *order : updated_orders) { 131 | // @TODO Stop using socket ids as client ids. Set up a map 132 | // between client ids and sockets. Also create a buffer to try 133 | // to send orders to clients that have disconnected. 134 | outboundMessage.put(order_pool->pointer_to_offset(order)); 135 | SPDLOG_DEBUG("Order {} updated message sent", order->id); 136 | } 137 | } 138 | } 139 | } 140 | 141 | // @TODO create signal handler to clean up 142 | 143 | return 0; 144 | } 145 | -------------------------------------------------------------------------------- /src/util/disruptor.h: -------------------------------------------------------------------------------- 1 | #ifndef disruptor_h 2 | #define disruptor_h 3 | // Implementation of ring buffer for processes to communicate with each other 4 | // See https://martinfowler.com/articles/lmax.html 5 | // 6 | // Basic concept is producer should never produce more than consumer can 7 | // consume. and Consumer should never consume more than consumer can produce. 8 | // Otherwise it's not a ring buffer anymore it's a snake eating it's own tail. 9 | // 10 | // The consumer can consume up until and including the producer position 11 | // But the producer position can only produce up until the consumer position. 12 | 13 | #include "mmap_wrapper.h" 14 | #include 15 | 16 | #define MAX_CONSUMERS 5 17 | 18 | template struct SharedData { 19 | unsigned long long producer_position; 20 | unsigned long long consumer_positions[MAX_CONSUMERS]; 21 | T *entities; 22 | }; 23 | 24 | template class Disruptor { 25 | protected: 26 | MMapMeta *mmap_meta; 27 | SharedData *shared_mem_region; 28 | int get_mmap_size(); 29 | int slots; 30 | }; 31 | 32 | template class Producer : public Disruptor { 33 | public: 34 | Producer(int slots, const char *mmap_name); 35 | void incr(); 36 | T *access_cur(); 37 | ~Producer() throw(); 38 | void cleanup(); 39 | // This creates a copy on function call for simplicity. 40 | // Later on use pointers to improve performance. 41 | bool put(T item); 42 | }; 43 | 44 | template class Consumer : public Disruptor { 45 | public: 46 | // producer / consumer behavior is totally separate but 47 | // we have to specify slots twice. 48 | // For now this is fine because producer and consumer are 49 | // created in separate processes anyway. 50 | 51 | // What if we specified consumer name? Define consu 52 | Consumer(int slots, const char *mmap_name, int consumer_id); 53 | ~Consumer() throw(); 54 | T *get(); 55 | void cleanup(); 56 | 57 | private: 58 | int consumer_id; 59 | }; 60 | 61 | template int Disruptor::get_mmap_size() { 62 | return sizeof(SharedData) + sizeof(T) * slots; 63 | } 64 | 65 | template void Producer::cleanup() { 66 | delete_mmap(this->mmap_meta); 67 | } 68 | 69 | template Producer::~Producer() throw() { cleanup(); } 70 | 71 | template Producer::Producer(int slots, const char *mmap_name) { 72 | this->slots = slots; 73 | this->mmap_meta = init_mmap(mmap_name, this->get_mmap_size()); 74 | this->shared_mem_region = (SharedData *)this->mmap_meta->location; 75 | 76 | this->shared_mem_region->entities = reinterpret_cast( 77 | (char *)this->shared_mem_region + sizeof(SharedData)); 78 | 79 | // producer always starts ahead of consumer 80 | this->shared_mem_region->producer_position = 0; 81 | } 82 | 83 | template T *Producer::access_cur() { 84 | // SPDLOG_DEBUG("{} Producer/Consumer is {}/{}", this->mmap_info->name, 85 | // this->shared_mem_region->producer_position, 86 | // this->shared_mem_region->consumer_position); 87 | // int next_producer_position = (this->shared_mem_region->producer_position+1) 88 | // % this->slots; 89 | 90 | return &this->shared_mem_region 91 | ->entities[this->shared_mem_region->producer_position % 92 | this->slots]; 93 | } 94 | 95 | template void Producer::incr() { 96 | this->shared_mem_region->producer_position++; 97 | } 98 | 99 | template bool Producer::put(T item) { 100 | // SPDLOG_DEBUG("{} Producer/Consumer is {}/{}", this->mmap_info->name, 101 | // this->shared_mem_region->producer_position, 102 | // this->shared_mem_region->consumer_position); 103 | // int next_producer_position = (this->shared_mem_region->producer_position+1) 104 | // % this->slots; 105 | 106 | this->shared_mem_region 107 | ->entities[this->shared_mem_region->producer_position % this->slots] = 108 | item; 109 | 110 | this->shared_mem_region->producer_position++; 111 | 112 | return true; 113 | } 114 | 115 | template 116 | Consumer::Consumer(int slots, const char *mmap_name, int consumer_id) { 117 | this->slots = slots; 118 | this->mmap_meta = open_mmap(mmap_name, this->get_mmap_size()); 119 | this->shared_mem_region = (SharedData *)this->mmap_meta->location; 120 | this->consumer_id = consumer_id; 121 | this->shared_mem_region->consumer_positions[consumer_id] = 0; 122 | } 123 | 124 | template Consumer::~Consumer() throw() { cleanup(); } 125 | 126 | template void Consumer::cleanup() { 127 | close_mmap(this->mmap_meta); 128 | } 129 | 130 | template T *Consumer::get() { 131 | // SPDLOG_DEBUG("{} Producer/Consumer is {}/{}", this->mmap_info->name, 132 | // this->shared_mem_region->producer_position, 133 | // this->shared_mem_region->consumer_position); 134 | 135 | // Consumer can consume up until the producer position. Producer is not 136 | // allowed to produce > consumer position - 1 So producers next position it 137 | // will write to is it's current position and the last position it wrote to is 138 | // position - 1. Then consumer can consume only up to producer position - 1. 139 | // It's a real mind bender but it works. 140 | if (this->shared_mem_region->consumer_positions[this->consumer_id] == 141 | this->shared_mem_region->producer_position) { 142 | return nullptr; 143 | } 144 | 145 | T *item = &this->shared_mem_region 146 | ->entities[this->shared_mem_region 147 | ->consumer_positions[this->consumer_id] % 148 | this->slots]; 149 | this->shared_mem_region->consumer_positions[this->consumer_id]++; 150 | 151 | return item; 152 | } 153 | 154 | #endif 155 | -------------------------------------------------------------------------------- /src/gateway/util/websocket.cpp: -------------------------------------------------------------------------------- 1 | #include "websocket.h" 2 | 3 | using namespace std; 4 | 5 | // I want this to take a string but had issues with the string 6 | // implementation. Will fix later. 7 | string base64_encode(const unsigned char *original) { 8 | BIO *bio, *b64; 9 | BUF_MEM *bufferPtr; 10 | 11 | b64 = BIO_new(BIO_f_base64()); 12 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 13 | bio = BIO_new(BIO_s_mem()); 14 | bio = BIO_push(b64, bio); 15 | 16 | BIO_write(bio, original, SHA_DIGEST_LENGTH); 17 | BIO_flush(bio); 18 | BIO_get_mem_ptr(bio, &bufferPtr); 19 | BIO_set_close(bio, BIO_NOCLOSE); 20 | 21 | string encoded(bufferPtr->data, bufferPtr->length); 22 | BIO_free_all(bio); 23 | 24 | return encoded; 25 | } 26 | 27 | string base64_decode(const string &encoded) { 28 | BIO *bio, *b64; 29 | int decodeLen = encoded.length(); 30 | auto *buffer = (char *)malloc(decodeLen + 1); 31 | 32 | bio = BIO_new_mem_buf(encoded.data(), -1); 33 | b64 = BIO_new(BIO_f_base64()); 34 | BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); 35 | bio = BIO_push(b64, bio); 36 | 37 | decodeLen = BIO_read(bio, buffer, encoded.length()); 38 | buffer[decodeLen] = '\0'; 39 | 40 | string decoded(buffer); 41 | free(buffer); 42 | BIO_free_all(bio); 43 | 44 | return decoded; 45 | } 46 | 47 | string sha1(const string &input) { 48 | unsigned char sha1_hash[SHA_DIGEST_LENGTH]; 49 | SHA1((const unsigned char *)(input.c_str()), input.size(), sha1_hash); 50 | 51 | string ret(reinterpret_cast(sha1_hash)); 52 | return ret; 53 | } 54 | 55 | map parse_http_headers(const string &headers) { 56 | map headerMap; 57 | istringstream stream(headers); 58 | string line; 59 | 60 | while (getline(stream, line)) { 61 | istringstream lineStream(line); 62 | string key, value; 63 | 64 | if (getline(lineStream, key, ':')) { 65 | if (getline(lineStream, value)) { 66 | if (!key.empty() && !value.empty()) { 67 | size_t start = value.find_first_not_of(" "); 68 | if (start != string::npos) { 69 | value = value.substr(start); 70 | } 71 | headerMap[key] = value; 72 | } 73 | } 74 | } 75 | } 76 | 77 | return headerMap; 78 | } 79 | 80 | string create_websocket_response_nonce(const string &websocket_request_key) { 81 | string result = websocket_request_key + ws_magic_string; 82 | 83 | unsigned char sha1_hash[SHA_DIGEST_LENGTH]; 84 | SHA1((const unsigned char *)(result.c_str()), result.size(), sha1_hash); 85 | 86 | return base64_encode(sha1_hash); 87 | } 88 | 89 | string websocket_request_response(const string &client_http_request) { 90 | map http_headers = parse_http_headers(client_http_request); 91 | 92 | try { 93 | string ws_request_nonce = http_headers.at(ws_request_header); 94 | 95 | ws_request_nonce.erase( 96 | std::remove_if(ws_request_nonce.begin(), ws_request_nonce.end(), 97 | [](char c) { return c == '\n' || c == '\r'; }), 98 | ws_request_nonce.end()); 99 | string ws_response_nonce = 100 | create_websocket_response_nonce(ws_request_nonce); 101 | return ws_response + ws_response_nonce + 102 | "\nSec-WebSocket-Protocol: chat\n\n"; 103 | } catch (const out_of_range &e) { 104 | // @TODO Make client disconnect here. 105 | return "meow"; 106 | } 107 | } 108 | 109 | vector encodeWebsocketFrame(const string &message) { 110 | vector frame; 111 | 112 | // Final fragment for bit & opcode 113 | frame.push_back(0x81); // FIN bit (1), Opcode: Text (0x01) 114 | 115 | size_t payload_length = message.length(); 116 | 117 | if (payload_length < 126) { 118 | frame.push_back(static_cast(payload_length)); 119 | } else if (payload_length <= 65535) { 120 | frame.push_back(126); 121 | frame.push_back(static_cast((payload_length >> 8) & 0xFF)); 122 | frame.push_back(static_cast(payload_length & 0xFF)); 123 | } else { 124 | /* 125 | * We don't do payloads above 64k in this department. 126 | * Why don't you try contacting customer service? 127 | */ 128 | } 129 | 130 | for (const char &c : message) { 131 | frame.push_back(static_cast(c)); 132 | } 133 | 134 | return frame; 135 | } 136 | 137 | bool decodeWebSocketFrame(string frame, string &message) { 138 | // invalid header 139 | if (frame.size() < 2) { 140 | return false; 141 | } 142 | 143 | bool fin = (frame[0] & 0x80) != 0; 144 | uint8_t opcode = frame[0] & 0x0F; 145 | if (opcode == 0x8) { 146 | SPDLOG_INFO("Client disconnected using opcode 0x8"); 147 | return false; 148 | } 149 | uint8_t payload_len = frame[1] & 0x7F; 150 | size_t payload_offset = 2; 151 | 152 | if (payload_len == 126) { 153 | // 16 bit payload length 154 | if (frame.size() < 4) { 155 | return false; 156 | } 157 | payload_len = (static_cast(frame[2]) << 8) | 158 | static_cast(frame[3]); 159 | payload_offset = 4; 160 | } else if (payload_len == 127) { 161 | // skip extended payloads 162 | return false; 163 | } 164 | 165 | bool maskBitSet = (frame[1] & 0x80) != 0; 166 | 167 | if (maskBitSet) { 168 | if (frame.size() < payload_offset + 4) { 169 | return false; 170 | } 171 | 172 | string masking_key = frame.substr(payload_offset, 4); 173 | 174 | payload_offset += 4; 175 | 176 | for (size_t i = 0; i < payload_len; ++i) { 177 | frame[payload_offset + i] = 178 | static_cast(frame[payload_offset + i]) ^ 179 | static_cast(masking_key[i % masking_key.size()]); 180 | } 181 | } 182 | 183 | if (frame.size() < payload_offset + payload_len) { 184 | return false; 185 | } 186 | 187 | message.assign(frame, payload_offset, payload_len); 188 | 189 | return true; 190 | } 191 | 192 | void printStringAsHex(const string &str) { 193 | for (size_t i = 0; i < str.size(); ++i) { 194 | cout << hex << setw(2) << setfill('0') << static_cast(str[i]) << " "; 195 | } 196 | 197 | cout << endl; 198 | } 199 | 200 | void printByteAsHex(const string &message, const uint8_t byte) { 201 | cout << message << hex << setw(2) << setfill('0') << static_cast(byte) 202 | << " "; 203 | cout << endl; 204 | } 205 | -------------------------------------------------------------------------------- /tests/order_book/order_book.test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../../src/order_book/order_book.h" 5 | #include "../helpers.h" 6 | 7 | #include 8 | 9 | #define DEBUG spdlog::info 10 | 11 | TEST_CASE("order_book - add order") { 12 | // Hello world order book test. 13 | const char *market_buf = "/test_mkt_buf"; 14 | Producer *producer = new Producer(10, market_buf); 15 | OrderBook *orderBook = new OrderBook(producer); 16 | 17 | Order *order = createDefaultOrder(); 18 | orderBook->newOrder(order); 19 | 20 | REQUIRE(orderBook->getVolume() == 100); 21 | producer->cleanup(); 22 | } 23 | 24 | TEST_CASE("order_book - cancel order") { 25 | const char *market_buf = "/test_mkt_buf"; 26 | Producer *producer = new Producer(10, market_buf); 27 | OrderBook *orderBook = new OrderBook(producer); 28 | Order *order = createDefaultOrder(); 29 | 30 | orderBook->newOrder(order); 31 | 32 | REQUIRE(orderBook->getVolume() == 100); 33 | 34 | orderBook->cancelOrder(order->id); 35 | 36 | REQUIRE(orderBook->getVolume() == 0); 37 | producer->cleanup(); 38 | } 39 | 40 | TEST_CASE("order_book - new orders that do not fill should set bid/ask price") { 41 | const char *market_buf = "/test_mkt_buf"; 42 | Producer *producer = new Producer(10, market_buf); 43 | OrderBook *orderBook = new OrderBook(producer); 44 | 45 | // Making a buy order w/ no sell order should immediately set bid. 46 | Order *buyOrder = createDefaultOrder(); 47 | orderBook->newOrder(buyOrder); 48 | 49 | REQUIRE(orderBook->getBid()->getPrice() == buyOrder->limitPrice); 50 | 51 | // A sell order that does not cross the spread should now set the ask price. 52 | Order *sellOrder = createDefaultOrder(); 53 | sellOrder->side = SELL; 54 | sellOrder->limitPrice = buyOrder->limitPrice + 100; 55 | orderBook->newOrder(sellOrder); 56 | 57 | REQUIRE(orderBook->getAsk()->getPrice() == sellOrder->limitPrice); 58 | producer->cleanup(); 59 | } 60 | 61 | TEST_CASE("order_book - match order") { 62 | const char *market_buf = "/test_mkt_buf"; 63 | Producer *producer = new Producer(10, market_buf); 64 | OrderBook *orderBook = new OrderBook(producer); 65 | 66 | Order *buyOrder = createDefaultOrder(); 67 | orderBook->newOrder(buyOrder); 68 | REQUIRE(orderBook->getBid()->getPrice() == buyOrder->limitPrice); 69 | 70 | Order *sellOrder = createDefaultOrder(); 71 | sellOrder->side = SELL; 72 | orderBook->newOrder(sellOrder); 73 | 74 | REQUIRE(orderBook->getVolume() == 0); 75 | producer->cleanup(); 76 | } 77 | 78 | TEST_CASE("order_book - buy orders with higher prices should move bid up") { 79 | const char *market_buf = "/test_mkt_buf"; 80 | Producer *producer = new Producer(10, market_buf); 81 | OrderBook *orderBook = new OrderBook(producer); 82 | 83 | Order *buyOrder = createDefaultOrder(); 84 | orderBook->newOrder(buyOrder); 85 | REQUIRE(orderBook->getBid()->getPrice() == buyOrder->limitPrice); 86 | 87 | Order *buyOrderHigher = createDefaultOrder(); 88 | buyOrderHigher->limitPrice = buyOrder->limitPrice + 100; 89 | orderBook->newOrder(buyOrderHigher); 90 | REQUIRE(orderBook->getBid()->getPrice() == buyOrderHigher->limitPrice); 91 | producer->cleanup(); 92 | } 93 | 94 | TEST_CASE("order_book - sell orders with lower prices should move ask lower") { 95 | const char *market_buf = "/test_mkt_buf"; 96 | Producer *producer = new Producer(10, market_buf); 97 | OrderBook *orderBook = new OrderBook(producer); 98 | 99 | Order *sellOrder = createDefaultOrder(); 100 | sellOrder->side = SELL; 101 | orderBook->newOrder(sellOrder); 102 | REQUIRE(orderBook->getAsk()->getPrice() == sellOrder->limitPrice); 103 | 104 | Order *sellOrderLower = createDefaultOrder(); 105 | sellOrderLower->side = SELL; 106 | sellOrderLower->limitPrice = sellOrder->limitPrice - 100; 107 | orderBook->newOrder(sellOrderLower); 108 | REQUIRE(orderBook->getAsk()->getPrice() == sellOrderLower->limitPrice); 109 | producer->cleanup(); 110 | } 111 | 112 | TEST_CASE("order_book - testing order fills after order book populated") { 113 | const char *market_buf = "/test_mkt_buf"; 114 | Producer *producer = new Producer(10, market_buf); 115 | OrderBook *orderBook = new OrderBook(producer); 116 | 117 | // initial buy order 118 | Order *order1 = customOrder(100, 100, 'b'); 119 | orderBook->newOrder(order1); 120 | 121 | REQUIRE(orderBook->getVolume() == 100); 122 | 123 | // sell order that does not cross spread. 124 | Order *order2 = customOrder(110, 100, 's'); 125 | orderBook->newOrder(order2); 126 | 127 | REQUIRE(orderBook->getVolume() == 200); 128 | 129 | // buy order that also does not cross spread. 130 | Order *order3 = customOrder(105, 100, 'b'); 131 | orderBook->newOrder(order3); 132 | 133 | REQUIRE(orderBook->getVolume() == 300); 134 | 135 | // sell order that should match only with buy order for 105 and not 100. 136 | Order *order4 = customOrder(103, 100, 's'); 137 | orderBook->newOrder(order4); 138 | 139 | REQUIRE(orderBook->getVolume() == 200); 140 | producer->cleanup(); 141 | } 142 | 143 | TEST_CASE("order_book - testing fillOrder when we attempt to sell more than " 144 | "what is offered.") { 145 | const char *market_buf = "/test_mkt_buf"; 146 | Producer *producer = new Producer(10, market_buf); 147 | OrderBook *orderBook = new OrderBook(producer); 148 | 149 | // initial buy order. 150 | Order *order1 = customOrder(336, 180, 'b'); 151 | orderBook->newOrder(order1); 152 | 153 | REQUIRE(orderBook->getVolume() == 180); 154 | 155 | // sell order that does not cross spread. 156 | Order *order2 = customOrder(698, 170, 's'); 157 | orderBook->newOrder(order2); 158 | 159 | REQUIRE(orderBook->getVolume() == 170 + 180); 160 | 161 | // buy order that does not adjust best bid and does not cross spread. 162 | Order *order3 = customOrder(126, 130, 'b'); 163 | orderBook->newOrder(order3); 164 | 165 | REQUIRE(orderBook->getVolume() == 170 + 180 + 130); 166 | 167 | // Sell order that crosses spread and matches with buy order for 336. 168 | // However the order has one more unit than we have available. 169 | // exchange should know to give up. 170 | Order *order4 = customOrder(180, 181, 's'); 171 | REQUIRE(order4->unfilled_quantity() == 181); 172 | orderBook->newOrder(order4); 173 | REQUIRE(order4->unfilled_quantity() == 0); 174 | 175 | REQUIRE(orderBook->getBid()->getPrice() == 126); 176 | producer->cleanup(); 177 | } 178 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -2 5 | AlignAfterOpenBracket: Align 6 | AlignArrayOfStructures: None 7 | AlignConsecutiveAssignments: 8 | Enabled: false 9 | AcrossEmptyLines: false 10 | AcrossComments: false 11 | AlignCompound: false 12 | PadOperators: true 13 | AlignConsecutiveBitFields: 14 | Enabled: false 15 | AcrossEmptyLines: false 16 | AcrossComments: false 17 | AlignCompound: false 18 | PadOperators: false 19 | AlignConsecutiveDeclarations: 20 | Enabled: false 21 | AcrossEmptyLines: false 22 | AcrossComments: false 23 | AlignCompound: false 24 | PadOperators: false 25 | AlignConsecutiveMacros: 26 | Enabled: false 27 | AcrossEmptyLines: false 28 | AcrossComments: false 29 | AlignCompound: false 30 | PadOperators: false 31 | AlignEscapedNewlines: Right 32 | AlignOperands: Align 33 | AlignTrailingComments: 34 | Kind: Always 35 | OverEmptyLines: 0 36 | AllowAllArgumentsOnNextLine: true 37 | AllowAllParametersOfDeclarationOnNextLine: true 38 | AllowShortBlocksOnASingleLine: Never 39 | AllowShortCaseLabelsOnASingleLine: false 40 | AllowShortEnumsOnASingleLine: true 41 | AllowShortFunctionsOnASingleLine: All 42 | AllowShortIfStatementsOnASingleLine: Never 43 | AllowShortLambdasOnASingleLine: All 44 | AllowShortLoopsOnASingleLine: false 45 | AlwaysBreakAfterDefinitionReturnType: None 46 | AlwaysBreakAfterReturnType: None 47 | AlwaysBreakBeforeMultilineStrings: false 48 | AlwaysBreakTemplateDeclarations: MultiLine 49 | AttributeMacros: 50 | - __capability 51 | BinPackArguments: true 52 | BinPackParameters: true 53 | BitFieldColonSpacing: Both 54 | BraceWrapping: 55 | AfterCaseLabel: false 56 | AfterClass: false 57 | AfterControlStatement: Never 58 | AfterEnum: false 59 | AfterExternBlock: false 60 | AfterFunction: false 61 | AfterNamespace: false 62 | AfterObjCDeclaration: false 63 | AfterStruct: false 64 | AfterUnion: false 65 | BeforeCatch: false 66 | BeforeElse: false 67 | BeforeLambdaBody: false 68 | BeforeWhile: false 69 | IndentBraces: false 70 | SplitEmptyFunction: true 71 | SplitEmptyRecord: true 72 | SplitEmptyNamespace: true 73 | BreakAfterAttributes: Never 74 | BreakAfterJavaFieldAnnotations: false 75 | BreakArrays: true 76 | BreakBeforeBinaryOperators: None 77 | BreakBeforeConceptDeclarations: Always 78 | BreakBeforeBraces: Attach 79 | BreakBeforeInlineASMColon: OnlyMultiline 80 | BreakBeforeTernaryOperators: true 81 | BreakConstructorInitializers: BeforeColon 82 | BreakInheritanceList: BeforeColon 83 | BreakStringLiterals: true 84 | ColumnLimit: 80 85 | CommentPragmas: '^ IWYU pragma:' 86 | CompactNamespaces: false 87 | ConstructorInitializerIndentWidth: 4 88 | ContinuationIndentWidth: 4 89 | Cpp11BracedListStyle: true 90 | DerivePointerAlignment: false 91 | DisableFormat: false 92 | EmptyLineAfterAccessModifier: Never 93 | EmptyLineBeforeAccessModifier: LogicalBlock 94 | ExperimentalAutoDetectBinPacking: false 95 | FixNamespaceComments: true 96 | ForEachMacros: 97 | - foreach 98 | - Q_FOREACH 99 | - BOOST_FOREACH 100 | IfMacros: 101 | - KJ_IF_MAYBE 102 | IncludeBlocks: Preserve 103 | IncludeCategories: 104 | - Regex: '^"(llvm|llvm-c|clang|clang-c)/' 105 | Priority: 2 106 | SortPriority: 0 107 | CaseSensitive: false 108 | - Regex: '^(<|"(gtest|gmock|isl|json)/)' 109 | Priority: 3 110 | SortPriority: 0 111 | CaseSensitive: false 112 | - Regex: '.*' 113 | Priority: 1 114 | SortPriority: 0 115 | CaseSensitive: false 116 | IncludeIsMainRegex: '(Test)?$' 117 | IncludeIsMainSourceRegex: '' 118 | IndentAccessModifiers: false 119 | IndentCaseBlocks: false 120 | IndentCaseLabels: false 121 | IndentExternBlock: AfterExternBlock 122 | IndentGotoLabels: true 123 | IndentPPDirectives: None 124 | IndentRequiresClause: true 125 | IndentWidth: 2 126 | IndentWrappedFunctionNames: false 127 | InsertBraces: false 128 | InsertNewlineAtEOF: false 129 | InsertTrailingCommas: None 130 | IntegerLiteralSeparator: 131 | Binary: 0 132 | BinaryMinDigits: 0 133 | Decimal: 0 134 | DecimalMinDigits: 0 135 | Hex: 0 136 | HexMinDigits: 0 137 | JavaScriptQuotes: Leave 138 | JavaScriptWrapImports: true 139 | KeepEmptyLinesAtTheStartOfBlocks: true 140 | LambdaBodyIndentation: Signature 141 | LineEnding: DeriveLF 142 | MacroBlockBegin: '' 143 | MacroBlockEnd: '' 144 | MaxEmptyLinesToKeep: 1 145 | NamespaceIndentation: None 146 | ObjCBinPackProtocolList: Auto 147 | ObjCBlockIndentWidth: 2 148 | ObjCBreakBeforeNestedBlockParam: true 149 | ObjCSpaceAfterProperty: false 150 | ObjCSpaceBeforeProtocolList: true 151 | PackConstructorInitializers: BinPack 152 | PenaltyBreakAssignment: 2 153 | PenaltyBreakBeforeFirstCallParameter: 19 154 | PenaltyBreakComment: 300 155 | PenaltyBreakFirstLessLess: 120 156 | PenaltyBreakOpenParenthesis: 0 157 | PenaltyBreakString: 1000 158 | PenaltyBreakTemplateDeclaration: 10 159 | PenaltyExcessCharacter: 1000000 160 | PenaltyIndentedWhitespace: 0 161 | PenaltyReturnTypeOnItsOwnLine: 60 162 | PointerAlignment: Right 163 | PPIndentWidth: -1 164 | QualifierAlignment: Leave 165 | ReferenceAlignment: Pointer 166 | ReflowComments: true 167 | RemoveBracesLLVM: false 168 | RemoveSemicolon: false 169 | RequiresClausePosition: OwnLine 170 | RequiresExpressionIndentation: OuterScope 171 | SeparateDefinitionBlocks: Leave 172 | ShortNamespaceLines: 1 173 | SortIncludes: CaseSensitive 174 | SortJavaStaticImport: Before 175 | SortUsingDeclarations: LexicographicNumeric 176 | SpaceAfterCStyleCast: false 177 | SpaceAfterLogicalNot: false 178 | SpaceAfterTemplateKeyword: true 179 | SpaceAroundPointerQualifiers: Default 180 | SpaceBeforeAssignmentOperators: true 181 | SpaceBeforeCaseColon: false 182 | SpaceBeforeCpp11BracedList: false 183 | SpaceBeforeCtorInitializerColon: true 184 | SpaceBeforeInheritanceColon: true 185 | SpaceBeforeParens: ControlStatements 186 | SpaceBeforeParensOptions: 187 | AfterControlStatements: true 188 | AfterForeachMacros: true 189 | AfterFunctionDefinitionName: false 190 | AfterFunctionDeclarationName: false 191 | AfterIfMacros: true 192 | AfterOverloadedOperator: false 193 | AfterRequiresInClause: false 194 | AfterRequiresInExpression: false 195 | BeforeNonEmptyParentheses: false 196 | SpaceBeforeRangeBasedForLoopColon: true 197 | SpaceBeforeSquareBrackets: false 198 | SpaceInEmptyBlock: false 199 | SpaceInEmptyParentheses: false 200 | SpacesBeforeTrailingComments: 1 201 | SpacesInAngles: Never 202 | SpacesInConditionalStatement: false 203 | SpacesInContainerLiterals: true 204 | SpacesInCStyleCastParentheses: false 205 | SpacesInLineCommentPrefix: 206 | Minimum: 1 207 | Maximum: -1 208 | SpacesInParentheses: false 209 | SpacesInSquareBrackets: false 210 | Standard: Latest 211 | StatementAttributeLikeMacros: 212 | - Q_EMIT 213 | StatementMacros: 214 | - Q_UNUSED 215 | - QT_REQUIRE_VERSION 216 | TabWidth: 8 217 | UseTab: Never 218 | WhitespaceSensitiveMacros: 219 | - BOOST_PP_STRINGIZE 220 | - CF_SWIFT_NAME 221 | - NS_SWIFT_NAME 222 | - PP_STRINGIZE 223 | - STRINGIZE 224 | ... 225 | 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/sneilan/stock-exchange/actions/workflows/tests.yml/badge.svg) 2 | 3 | # 💻 Personal C++ Low Latency Stock Exchange 4 | 5 | This is a stock exchange that you can run on your laptop or desktop that will process 10's of thousands of trades per second. 6 | 7 | I built this as a fun nerdy project to show off my skills. Check out my [linkedin](https://linkedin.com/in/seanneilan) 8 | 9 | * Create markets by connecting some trading robots to the exchange. 10 | * Simulate any kind of market anytime - even outside of normal trading hours 11 | * Plug in stock or crypto data and test against that 12 | * Test against slippage and network failures 13 | * Allow trading robots to develop new market patterns and write software to detect them 14 | # Easy install 15 | 16 | It uses the same techniques and algorithms as [NASDAQ](https://martinfowler.com/articles/lmax.html) but unoptimized. 17 | 18 | Compare to [LMAX exchange](https://lmax-exchange.github.io/disruptor/). 19 | 20 | ## What is an Exchange? 21 | 22 | A stock exchange is a server that takes buy/sell orders from traders and matches them up. When you open up Robinhood on your phone, 23 | robinhood takes your order to buy Gamestop and sends it to an exchange called NASDAQ. NASDAQ finds a trader willing to sell you Gamestop and then 24 | Robinhood sends you a notification once that sale is complete. This works vice-versa for sales. If you want to sell that share of Gamestop, Robinhood 25 | sends your request to sell Gamestop to NASDAQ. NASDAQ finds someone willing to buy your share of Gamestop and once someone buys your share, tells Robinhood 26 | to tell you! 27 | 28 | ## Running the Exchange 29 | 30 | Clone with 31 | ``` 32 | git clone git@github.com:sneilan/stock-exchange.git stock-exchange 33 | cd stock-exchange 34 | ``` 35 | 36 | Run with 37 | ``` 38 | docker compose up 39 | ``` 40 | 41 | The exchange will start up on the default port `8888`. 42 | 43 | ## Place Sample Trades 44 | In a separate terminal, not inside of docker, run the following example Python 3 trading client. 45 | Python 3 script will auto-connect to server at `0.0.0.0:8888`. 46 | Will place a random trade each time you press enter. You'll see output on the exchange. 47 | ``` 48 | cd scripts 49 | python3 scripts/loadTest.py 50 | ``` 51 | 52 | Script also functions as a 53 | 54 | ## Limitations 55 | * Do not run on a public server (yet) 56 | * Server loses trades on shutdown (in progress) 57 | * No cancellations, user accounts, balances, wallets. 58 | 59 | Honestly there's a lot of work to do but I hope this becomes the premier stock exchange that everyone uses for personal experiments. 60 | 61 | ## Protocol 62 | 63 | The exchange is basic for now. You connect, place trades and recieve notifications about your trades. 64 | 65 | ### Connect 66 | 67 | Open a connection to the server with a socket. Use this in python 68 | 69 | ```python 70 | import socket 71 | 72 | host = '0.0.0.0' 73 | port = 8888 74 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 75 | sock.connect((host, port)) 76 | response = sock.recv(1024) 77 | print("Connected!") 78 | ``` 79 | 80 | ### Sending a trade 81 | 82 | After connecting, send a trade by submitting 9 bytes. 83 | 1. Byte 0: Char - buy or sell with 'b' for buy and 's' for sell. 84 | 2. Bytes 1-4 (inclusive 4 bytes) - Price as a positive unsigned integer in pennies. 85 | 3. Bytes 5-8 (inclusive 4 bytes) - Quantity as a positive unsigned integer. 86 | 87 | Here's an example of sending a buy order in Python for 500 shares at 88 | $1.23 / share. Assuming sock is an open socket to the exchange. 89 | 90 | ```python 91 | price = 123 # $1.23 in 1 hundred 23 pennies. 92 | quantity = 500 93 | side = 'b' # 's' for sell 94 | message = pack( 95 | 'cii', 96 | bytes(side, 'ascii'), 97 | price, 98 | quantity, 99 | ) 100 | sock.sendall(message) 101 | ``` 102 | 103 | Server will immediately send a trade notification with the id 104 | of the trade. 105 | 106 | ### Trade Notifications 107 | 108 | As soon as you send a trade, the server will tell you 109 | the trade is recieved with the ID of the trade. Each trade 110 | notification is 21 bytes. 111 | 1. Byte 0: Char - Notification type. 112 | 'r' is 'recieved' 113 | 'u' is 'updated' 114 | 'f' is 'filled' 115 | 2. Bytes 1-8 (inclusive 8 bytes): Unsigned long long - trade id 116 | 3. Bytes 9-12 (inclusive 4 bytes): Unsigned integer - quantity 117 | 4. Bytes 13-16 (inclusive 4 bytes): Unsigned integer - filled quantity 118 | 5. Bytes 17-20 (inclusive 4 bytes): Unsigned integer - client id 119 | Client id is not important but will tell you what integer 0-30 your "user id" is. 120 | 121 | You will always get a recieved notification. The other two notifications to a trade are either updated 122 | or filled. 123 | 124 | Here's an example of recieving trade notifications in Python. It assumes that sock is a connected 125 | socket to the server. 126 | 127 | ```python 128 | from struct import unpack 129 | 130 | msg_type_to_msg = {} 131 | msg_type_to_msg['u'] = 'updated' 132 | msg_type_to_msg['f'] = 'filled' 133 | msg_type_to_msg['r'] = 'recieved' 134 | while True: 135 | data = sock.recv(21) 136 | if data: 137 | # c is char 138 | # Q is unsigned long long 139 | # i is 4 byte integer 140 | # I originally tried to use 'cQiii' but unpack would not 141 | # parse the bytes correctly. This works for now. 142 | format_string = 'Qiii' 143 | unpacked_data = unpack(format_string, data[1:]) 144 | msg_type = chr(data[0]) 145 | message = msg_type_to_msg[msg_type] 146 | id = unpacked_data[0] 147 | quantity = unpacked_data[1] 148 | filled_quantity = unpacked_data[2] 149 | client_id = unpacked_data[3] 150 | 151 | print('id', id, 'message', message, 'quantity', quantity, 'filled_quantity', filled_quantity, 'client_id', client_id) 152 | ``` 153 | 154 | Check out scripts/loadTest.py for an example trading client. 155 | 156 | You can paste these protocols into Chat GPT and produce trading frontends in your preferred language. 157 | 158 | ### Authentication 159 | 160 | _or lack thereof_ 161 | 162 | There is no authentication currently. 163 | 164 | You will be assigned a User ID however on connection that is not told 165 | to the client except on trade notifications. Your "user id" is the socket number. 166 | 167 | The exchange supports up to 30 concurrent clients. 168 | First user to connect will have user id 0, second 1 and so one. If User 1 disconnects and a new user reconnects, 169 | the new user will have user id 1 also. It's not great but it works for a demo. You do not need to authenticate 170 | 171 | ### What is traded 172 | 173 | Currently the exchange trades one unnamed symbol. The name of the symbol 174 | is whatever you want. To trade multiple symbols, start up multiple exchanges. 175 | 176 | ### Balances 177 | 178 | Trade balances are infinite. Wallets and balances will come later. 179 | 180 | ### Risk Controls 181 | 182 | No risk controls at the moment. Place as many trades as you like. 183 | 184 | ### Market Data 185 | 186 | This is not implemented yet. This is a high priority on the roadmap. 187 | 188 | ### Getting Current Bid / Ask 189 | 190 | Not implemented. Very high priority! 191 | 192 | ### Backups 193 | 194 | Not implemented. 195 | 196 | ## Test 197 | ``` 198 | docker compose run -it core /app/test 199 | ``` 200 | 201 | Will run all tests automatically. 202 | 203 | ## TODO 204 | There's a lot (to do)[https://github.com/sneilan/stock-exchange/issues] in creating a low-latency stock exchange from the ground up. 205 | 206 | Check out the issue list. 207 | 208 | ## Contributing 209 | 210 | Check out any of the tickets on the issues list or file a new one with a proposal! 211 | 212 | -------------------------------------------------------------------------------- /src/gateway/socket.cpp: -------------------------------------------------------------------------------- 1 | #include "socket.h" 2 | 3 | SocketServer::SocketServer() { 4 | timeout.tv_sec = 0; 5 | timeout.tv_usec = TIMEOUT_MICROSECONDS; 6 | 7 | // Initialise all client_socket[] to 0 8 | for (int i = 0; i < MAX_CLIENTS; i++) { 9 | client_socket[i] = 0; 10 | } 11 | 12 | // Default to SSL. 13 | 14 | ctx = SSL_CTX_new(SSLv23_server_method()); 15 | 16 | if (!ctx) { 17 | SPDLOG_CRITICAL("Could not start SSL_CTX_new"); 18 | exit(1); 19 | } 20 | 21 | if (SSL_CTX_use_certificate_file(ctx, getenv("CERTIFICATE"), 22 | SSL_FILETYPE_PEM) <= 0) { 23 | SPDLOG_CRITICAL( 24 | "Could not load certificate. Check CERTIFICATE env variable."); 25 | exit(1); 26 | } 27 | 28 | if (SSL_CTX_use_PrivateKey_file(ctx, getenv("PRIVATE_KEY"), 29 | SSL_FILETYPE_PEM) <= 0) { 30 | SPDLOG_CRITICAL( 31 | "Could not load private key. Check PRIVATE_KEY env variable."); 32 | exit(1); 33 | } 34 | 35 | // Initialize ssl connection pool 36 | for (int i = 0; i < MAX_CLIENTS; i++) { 37 | connections[i] = SSL_new(ctx); 38 | } 39 | } 40 | 41 | void SocketServer::listenToSocket() { 42 | SPDLOG_INFO("Waiting for connections ..."); 43 | while (1) { 44 | // set of socket descriptors for sockets with data to be read. 45 | fd_set readfds; 46 | // set of socket descriptors for sockets that can be written to. 47 | fd_set writefds; 48 | 49 | int max_sd = getMaxClientID(&client_socket); 50 | initFDSet(&readfds, &client_socket); 51 | initFDSet(&writefds, &client_socket); 52 | 53 | int activity = select(max_sd + 1, &readfds, &writefds, NULL, &timeout); 54 | 55 | // if activity is greater than 0, 1 or more socket descriptors 56 | // ready for reading, writing or error. 0 means no socket descriptors 57 | // ready and we can try again later. -1 means an error happened. 58 | // When -1 happens, call errno to determine the error type. 59 | // EINTR means interrupted system call. All errno's are defined here. 60 | // https://man7.org/linux/man-pages/man3/errno.3.html 61 | if ((activity < 0) && (errno != EINTR)) { 62 | SPDLOG_ERROR("select error {}", errno); 63 | } 64 | 65 | // Accept new connections non-blocking. 66 | acceptNewConn(&readfds); 67 | 68 | // Check all sockets for data to be read. 69 | for (int i = 0; i < MAX_CLIENTS; i++) { 70 | int sd = client_socket[i]; 71 | 72 | if (FD_ISSET(sd, &readfds)) { 73 | int valread = readDataFromClient(i); 74 | 75 | if (valread > 0) { 76 | buffer[valread] = '\0'; // safety precaution 77 | readMessage(i, buffer, valread); 78 | memset(buffer, 0, sizeof(buffer)); 79 | } else { 80 | // If no data was read, unset writefds. 81 | FD_CLR(sd, &writefds); 82 | } 83 | } 84 | } 85 | 86 | handleOutgoingMessage(); 87 | } 88 | } 89 | 90 | SocketServer::~SocketServer() { 91 | for (int i = 0; i < MAX_CLIENTS; i++) { 92 | SSL_free(connections[i]); 93 | } 94 | 95 | SSL_CTX_free(ctx); 96 | } 97 | 98 | void SocketServer::forceDisconnect(int client_id) { 99 | close(client_socket[client_id]); 100 | client_socket[client_id] = 0; 101 | } 102 | 103 | void SocketServer::bindSocket(int PORT) { 104 | // Create a master socket 105 | if ((master_socket = socket(AF_INET, SOCK_STREAM, 0)) == 0) { 106 | SPDLOG_DEBUG("socket failed"); 107 | exit(EXIT_FAILURE); 108 | } 109 | 110 | // set master socket to allow multiple connections , 111 | // this is just a good habit, it will work without this 112 | int opt = 1; 113 | if (setsockopt(master_socket, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, 114 | sizeof(opt)) < 0) { 115 | SPDLOG_DEBUG("setsockopt"); 116 | exit(EXIT_FAILURE); 117 | } 118 | 119 | // Set up the address where we will host the exchange. 120 | address.sin_family = AF_INET; // ipv4 121 | address.sin_addr.s_addr = INADDR_ANY; // bind to all interfaces 122 | address.sin_port = htons(PORT); // bind to PORT 123 | 124 | // Bind master socket to that address. 125 | if (bind(master_socket, (struct sockaddr *)&address, sizeof(address)) < 0) { 126 | SPDLOG_DEBUG("bind failed"); 127 | exit(EXIT_FAILURE); 128 | } 129 | 130 | SPDLOG_INFO("Listener on port {}", PORT); 131 | 132 | // Specify maximum of 3 pending connections for the master socket 133 | if (listen(master_socket, 3) < 0) { 134 | SPDLOG_DEBUG("listen failure"); 135 | exit(EXIT_FAILURE); 136 | } 137 | } 138 | 139 | int SocketServer::getMaxClientID(int (*client_socket)[MAX_CLIENTS]) { 140 | // Return the largest client socket id of all client sockets. 141 | int max_sd = master_socket; 142 | 143 | for (int i = 0; i < MAX_CLIENTS; i++) { 144 | int sd = (*client_socket)[i]; 145 | if (sd > max_sd) 146 | max_sd = sd; 147 | } 148 | 149 | return max_sd; 150 | } 151 | 152 | void SocketServer::initFDSet(fd_set *fds, int (*client_socket)[MAX_CLIENTS]) { 153 | // clear the socket set 154 | FD_ZERO(fds); 155 | 156 | // add master socket to set 157 | FD_SET(master_socket, fds); 158 | 159 | // add child sockets to set 160 | for (int i = 0; i < MAX_CLIENTS; i++) { 161 | // socket descriptor 162 | int sd = (*client_socket)[i]; 163 | 164 | // if valid socket descriptor then add to read list 165 | if (sd > 0) 166 | FD_SET(sd, fds); 167 | } 168 | } 169 | 170 | void SocketServer::acceptNewConn(fd_set *readfds) { 171 | int new_socket = 0; 172 | int addrlen = sizeof(address); 173 | 174 | if (FD_ISSET(master_socket, readfds)) { 175 | if ((new_socket = accept(master_socket, (struct sockaddr *)&address, 176 | (socklen_t *)&addrlen)) < 0) { 177 | SPDLOG_ERROR("Failure to accept new connection."); 178 | exit(EXIT_FAILURE); 179 | } 180 | 181 | // Add new socket to array of sockets 182 | // Find the lowest available socket. 183 | for (int i = 0; i < MAX_CLIENTS; i++) { 184 | if (client_socket[i] == 0) { 185 | SSL *ssl = connections[i]; 186 | 187 | SSL_clear(ssl); 188 | 189 | SSL_set_fd(ssl, new_socket); 190 | 191 | // Perform SSL handshake 192 | if (SSL_accept(ssl) != 1) { 193 | SPDLOG_INFO("Client {} did not accept ssl", i); 194 | forceDisconnect(i); 195 | return; 196 | } 197 | 198 | client_socket[i] = new_socket; 199 | 200 | newClient(i); 201 | SPDLOG_DEBUG("Registered new client {}", i); 202 | 203 | break; 204 | } 205 | } 206 | } 207 | } 208 | 209 | bool SocketServer::sendMessage(int client_id, const char *message, 210 | int message_size) { 211 | int bytes_written = SSL_write(connections[client_id], message, message_size); 212 | 213 | if (bytes_written == 0) { 214 | // client disconnected. 215 | SPDLOG_INFO("Discovered client {} when sending", client_id); 216 | return false; 217 | } else if (bytes_written != message_size) { 218 | // if error is not EAWOULDBLOCK, client is disconnected. 219 | // if EAWOULDBLOCK then have to repeat. 220 | SPDLOG_ERROR("send error {} to client_id {} at socket {}", bytes_written, 221 | client_id, client_socket[client_id]); 222 | return false; 223 | } 224 | 225 | // SPDLOG_INFO("Sent message {} to client {}", message, client_id); 226 | 227 | return true; 228 | } 229 | 230 | void SocketServer::sendMessageToAllClients(const char *message, 231 | int message_size) { 232 | for (int i = 0; i < MAX_CLIENTS; i++) { 233 | int sd = client_socket[i]; 234 | // Skip unplugged sockets. 235 | if (sd == 0) { 236 | continue; 237 | } 238 | 239 | int error = SSL_write(connections[i], message, message_size); 240 | if (error != message_size) { 241 | SPDLOG_ERROR("Send error {} to client_id {} at socket {}", error, i, sd); 242 | } 243 | } 244 | } 245 | 246 | int SocketServer::readDataFromClient(int client_id) { 247 | int valread; 248 | int sd = client_socket[client_id]; 249 | int addrlen = sizeof(address); 250 | 251 | if ((valread = SSL_read(connections[client_id], buffer, 1024)) == 0) { 252 | // Client disconnected. 253 | getpeername(sd, (struct sockaddr *)&address, (socklen_t *)&addrlen); 254 | 255 | close(sd); 256 | client_socket[client_id] = 0; 257 | disconnected(client_id); 258 | SPDLOG_DEBUG("Client disconnected, ip {}, port {}, client {}", 259 | inet_ntoa(address.sin_addr), ntohs(address.sin_port), 260 | client_id); 261 | } 262 | 263 | // SPDLOG_DEBUG("Read valread {} bytes from client_id {}", valread, 264 | // client_id); 265 | 266 | return valread; 267 | } 268 | -------------------------------------------------------------------------------- /src/order_book/order_book.cpp: -------------------------------------------------------------------------------- 1 | #include "order_book.h" 2 | #include 3 | 4 | OrderBook::OrderBook(Producer *outbound_mkt_l1) { 5 | orderMap = new std::unordered_map *>(); 6 | // @TOOD the book should not care about the min / max prices. 7 | buyBook = new Book(); 8 | sellBook = new Book(); 9 | bestBid = nullptr; 10 | bestAsk = nullptr; 11 | this->outbound_mkt_l1 = outbound_mkt_l1; 12 | } 13 | 14 | // Main entry point for matching engine. Consider this the "controller" 15 | std::list OrderBook::newOrder(Order *order) { 16 | std::list updated_orders; 17 | 18 | SPDLOG_INFO("Called newOrder on Order {} side {} price {} quantity {}", 19 | order->id, order->side, order->limitPrice, 20 | order->unfilled_quantity()); 21 | 22 | if (isOpposingOrderBookBlank(order)) { 23 | SPDLOG_DEBUG("isOpposingOrderBookBlank returned true"); 24 | 25 | addOrder(order); 26 | SPDLOG_DEBUG("addOrder called"); 27 | 28 | adjustBidAskIfOrderIsBetterPrice(order); 29 | SPDLOG_DEBUG("adjustBidAskIfOrderIsBetterPrice called"); 30 | 31 | SPDLOG_DEBUG("opposingOrderBook volume is {}", opposingOrderVolume(order)); 32 | SPDLOG_DEBUG("orderBook volume is {}", bookOrderVolume(order)); 33 | 34 | return updated_orders; 35 | } 36 | 37 | // If this order has not crossed the spread meaning 38 | // if it's a buy order it's less than current ask and 39 | // if a sell order greater than current bid. 40 | // It's like saying a user does not want to buy at a price 41 | // anyone wants to sell at and does not want to sell at a price 42 | // anyone wants to buy at. For example, say the bid (highest price 43 | // buyer will pay) is $5.00 but I'm only willing to sell for $6.00. 44 | // This order would not cross the spread because the buyer isn't willing to 45 | // pay $6.00. If I changed my order to $4.50, this would cross the spread 46 | // because the buyer is willing the pay $5.00 and I'm willing to sell for 47 | // $4.50 so that's a match. 48 | if (!orderCrossedSpread(order)) { 49 | SPDLOG_DEBUG("orderCrossedSpread returned false"); 50 | 51 | addOrder(order); 52 | 53 | adjustBidAskIfOrderIsBetterPrice(order); 54 | 55 | SPDLOG_DEBUG("opposingOrderBook volume is {}", opposingOrderVolume(order)); 56 | SPDLOG_DEBUG("orderBook volume is {}", bookOrderVolume(order)); 57 | 58 | return updated_orders; 59 | } 60 | 61 | // In this implementation, if you are willing to cross the spread, you get the 62 | // trade. This is changeable to create different trading scenarios. 63 | 64 | // Iteratively attempt to fill the order until we can't. 65 | // Then insert the rest of the order into the book. 66 | SPDLOG_DEBUG("unfilledQuantity on order is {}", order->unfilled_quantity()); 67 | SPDLOG_DEBUG("opposingOrderBook volume is {}", opposingOrderVolume(order)); 68 | SPDLOG_DEBUG("orderBook volume is {}", bookOrderVolume(order)); 69 | while (order->unfilled_quantity() > 0) { 70 | updated_orders.merge(this->fillOrder(order)); 71 | SPDLOG_DEBUG("Finished fillOrder. Order id {} has unfillled quantity {}", 72 | order->id, order->unfilled_quantity()); 73 | 74 | SPDLOG_DEBUG("opposingOrderBook volume is {}", opposingOrderVolume(order)); 75 | SPDLOG_DEBUG("orderBook volume is {}", bookOrderVolume(order)); 76 | 77 | // Because we filled some orders, update the best ask if necessary. 78 | setBidAskToReflectMarket(); 79 | SPDLOG_DEBUG("Called set bid ask"); 80 | 81 | // If there are no more orders, break. 82 | if (isOpposingOrderBookBlank(order)) { 83 | SPDLOG_DEBUG("Opposing order book blank"); 84 | break; 85 | } 86 | printBestBidAsk("fillOrder requested info. "); 87 | 88 | // If user is looking to buy but no-one is willing to sell as low as they 89 | // want to buy then break because even though we have opposing orders, 90 | // nothing will get filled. People try to sell for as high as possible but 91 | // buy for as low as possible. So we want to see if the ask is less than 92 | // what user is attempting to buy for. 93 | if (order->side == BUY && bestAsk->getPrice() < order->limitPrice) { 94 | break; 95 | } 96 | 97 | // and vice versa. 98 | if (order->side == SELL && bestBid->getPrice() > order->limitPrice) { 99 | break; 100 | } 101 | } 102 | 103 | return updated_orders; 104 | } 105 | 106 | void OrderBook::cancelOrder(SEQUENCE_ID id) { 107 | Node *node = orderMap->at(id); 108 | totalVolume -= node->data->unfilled_quantity(); 109 | 110 | Order *order = node->data; 111 | 112 | if (order->side == BUY) { 113 | buyBook->cancelOrder(node); 114 | } else if (order->side == SELL) { 115 | sellBook->cancelOrder(node); 116 | } 117 | 118 | orderMap->erase(id); 119 | } 120 | 121 | bool OrderBook::orderCrossedSpread(Order *order) { 122 | if (order->side == BUY) { 123 | return order->limitPrice >= bestAsk->getPrice(); 124 | } 125 | 126 | // Order is sell side. 127 | return order->limitPrice <= bestBid->getPrice(); 128 | } 129 | 130 | void OrderBook::adjustBidAskIfOrderIsBetterPrice(Order *order) { 131 | SPDLOG_DEBUG("Order id {}", order->id); 132 | 133 | if (order->side == BUY) { 134 | // If there are no sell orders & this is a higher bid, move up the bid. 135 | if (bestBid == nullptr || order->limitPrice > bestBid->getPrice()) { 136 | 137 | bestBid = buyBook->get(order->limitPrice); 138 | sendMarketData('b', bestBid->getPrice()); 139 | 140 | SPDLOG_DEBUG("bid is {}/{}", bestBid->getPrice(), bestBid->getVolume()); 141 | } 142 | } else if (order->side == SELL) { 143 | // If there are no buy orders & this is a lower ask, lower the ask 144 | if (bestAsk == nullptr || order->limitPrice < bestAsk->getPrice()) { 145 | 146 | bestAsk = sellBook->get(order->limitPrice); 147 | sendMarketData('a', bestAsk->getPrice()); 148 | 149 | SPDLOG_DEBUG("ask is {}/{}", bestAsk->getPrice(), bestAsk->getVolume()); 150 | } 151 | } 152 | } 153 | 154 | void OrderBook::sendMarketData(char type, int val) { 155 | L1MarketData *data = this->outbound_mkt_l1->access_cur(); 156 | data->type = type; 157 | data->val = val; 158 | 159 | struct timespec ts; 160 | if (clock_gettime(CLOCK_REALTIME, &ts) == 0) { 161 | data->time_ns = ts.tv_sec * 1000000000LL + ts.tv_nsec; 162 | } else { 163 | SPDLOG_ERROR("clock_gettime"); 164 | } 165 | this->outbound_mkt_l1->incr(); 166 | } 167 | 168 | void OrderBook::printBestBidAsk(const char *prefix) { 169 | std::stringstream ss; 170 | 171 | ss << prefix << " "; 172 | 173 | ss << "bid is "; 174 | if (bestBid != nullptr) { 175 | ss << bestBid->getPrice() << "/" << bestBid->getVolume() << ". ask is "; 176 | } else { 177 | ss << "null/null."; 178 | } 179 | 180 | ss << "ask is "; 181 | if (bestAsk != nullptr) { 182 | ss << bestAsk->getPrice() << "/" << bestAsk->getVolume(); 183 | } else { 184 | ss << "null/null"; 185 | } 186 | 187 | SPDLOG_DEBUG(ss.str()); 188 | } 189 | 190 | void OrderBook::setBidAskToReflectMarket() { 191 | printBestBidAsk("setBidAskToReflectMarket"); 192 | 193 | // If we ran out of orders at this price level, 194 | // Find the next best selling price & make that the ask. 195 | while (bestAsk != nullptr && bestAsk->getVolume() == 0) { 196 | int nextPrice = bestAsk->getPrice() + ONE_CENT; 197 | 198 | if (nextPrice > ONE_HUNDRED_DOLLARS) { 199 | bestAsk = nullptr; 200 | SPDLOG_DEBUG("ask is null/null"); 201 | return; 202 | } 203 | 204 | bestAsk = sellBook->get(nextPrice); 205 | 206 | // sell book get should return null ptr if we retrieve a bad price. 207 | // or consider adding new prices automagically. 208 | if (bestAsk != nullptr && bestAsk->getVolume() > 0) { 209 | sendMarketData('a', bestAsk->getPrice()); 210 | SPDLOG_DEBUG("ask is {}/{}", bestAsk->getPrice(), bestAsk->getVolume()); 211 | return; 212 | } 213 | } 214 | 215 | while (bestBid != nullptr && bestBid->getVolume() == 0) { 216 | int nextPrice = bestBid->getPrice() - ONE_CENT; 217 | if (nextPrice < ONE_DOLLAR) { 218 | SPDLOG_DEBUG("bid is null/null"); 219 | bestBid = nullptr; 220 | return; 221 | } 222 | 223 | bestBid = buyBook->get(nextPrice); 224 | // sell book get should return null ptr if we retrieve a bad price. 225 | // or consider adding new prices automagically. 226 | if (bestBid != nullptr && bestBid->getVolume() > 0) { 227 | sendMarketData('b', bestBid->getPrice()); 228 | SPDLOG_DEBUG("bid is {}/{}", bestBid->getPrice(), bestBid->getVolume()); 229 | return; 230 | } 231 | } 232 | } 233 | 234 | bool OrderBook::isOpposingOrderBookBlank(Order *order) { 235 | if (order->side == BUY) { 236 | return (sellBook == nullptr || sellBook->getVolume() == 0); 237 | } 238 | 239 | // order is sell side. 240 | return (buyBook == nullptr || buyBook->getVolume() == 0); 241 | } 242 | 243 | int OrderBook::bookOrderVolume(Order *order) { 244 | if (order->side == BUY) { 245 | if (buyBook == nullptr) { 246 | return 0; 247 | } 248 | return buyBook->getVolume(); 249 | } 250 | 251 | // order is sell side. 252 | if (sellBook == nullptr) { 253 | return 0; 254 | } 255 | return sellBook->getVolume(); 256 | } 257 | 258 | int OrderBook::opposingOrderVolume(Order *order) { 259 | if (order->side == BUY) { 260 | if (sellBook == nullptr) { 261 | return 0; 262 | } 263 | return sellBook->getVolume(); 264 | } 265 | 266 | // order is sell side. 267 | if (buyBook == nullptr) { 268 | return 0; 269 | } 270 | return buyBook->getVolume(); 271 | } 272 | 273 | void OrderBook::addOrder(Order *order) { 274 | totalVolume += order->unfilled_quantity(); 275 | SPDLOG_DEBUG("totalVolume is now {}", totalVolume); 276 | 277 | Node *node; 278 | if (order->side == BUY) { 279 | node = buyBook->addOrder(order); 280 | } else if (order->side == SELL) { 281 | node = sellBook->addOrder(order); 282 | } 283 | 284 | orderMap->emplace(order->id, node); 285 | } 286 | 287 | // Attempts to fill an order using the buy / sell books. 288 | std::list OrderBook::fillOrder(Order *order) { 289 | std::list updated_orders; 290 | int initialQuantity = order->unfilled_quantity(); 291 | 292 | if (order->side == BUY) { 293 | if (bestAsk == nullptr) { 294 | SPDLOG_DEBUG("bestAsk is null. returning"); 295 | return updated_orders; 296 | } 297 | updated_orders = sellBook->fillOrder(order, bestAsk); 298 | } else if (order->side == SELL) { 299 | if (bestBid == nullptr) { 300 | SPDLOG_DEBUG("bestBid is null. returning"); 301 | return updated_orders; 302 | } 303 | updated_orders = buyBook->fillOrder(order, bestBid); 304 | } 305 | 306 | totalVolume -= (initialQuantity - order->unfilled_quantity()); 307 | return updated_orders; 308 | } 309 | 310 | int OrderBook::getVolume() { return totalVolume; } 311 | 312 | PriceLevel *OrderBook::getBid() { return bestBid; } 313 | 314 | PriceLevel *OrderBook::getAsk() { return bestAsk; } 315 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Server Side Public License 2 | VERSION 1, NOVEMBER 23, 2023 3 | 4 | Copyright © 2023 Sean Neilan 5 | 6 | Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. 7 | 8 | TERMS AND CONDITIONS 9 | 0. Definitions. 10 | “This License” refers to Server Side Public License. 11 | 12 | “Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. 13 | 14 | “The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. 15 | 16 | To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. 17 | 18 | A “covered work” means either the unmodified Program or a work based on the Program. 19 | 20 | To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. 21 | 22 | To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. 23 | 24 | An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 25 | 26 | 1. Source Code. 27 | The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. 28 | 29 | A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. 30 | 31 | The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. 32 | 33 | The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. 34 | 35 | The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. 36 | 37 | The Corresponding Source for a work in source code form is that same work. 38 | 39 | 2. Basic Permissions. 40 | All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program, subject to section 13. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. 41 | 42 | Subject to section 13, you may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. 43 | 44 | Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 45 | 46 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 47 | No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. 48 | 49 | When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 50 | 51 | 4. Conveying Verbatim Copies. 52 | You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. 53 | 54 | You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 55 | 56 | 5. Conveying Modified Source Versions. 57 | You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: 58 | 59 | a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 60 | b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. 61 | c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. 62 | d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. 63 | A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 64 | 65 | 6. Conveying Non-Source Forms. 66 | You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: 67 | 68 | a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. 69 | b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. 70 | c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. 71 | d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. 72 | e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. 73 | A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. 74 | 75 | A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. 76 | 77 | “Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. 78 | 79 | If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). 80 | 81 | The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. 82 | 83 | Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 84 | 85 | 7. Additional Terms. 86 | “Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. 87 | 88 | When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. 89 | 90 | Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: 91 | 92 | a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 93 | b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or 94 | c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or 95 | d) Limiting the use for publicity purposes of names of licensors or authors of the material; or 96 | e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 97 | f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. 98 | All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. 99 | 100 | If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. 101 | 102 | Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 103 | 104 | 8. Termination. 105 | You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). 106 | 107 | However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. 108 | 109 | Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. 110 | 111 | Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 112 | 113 | 9. Acceptance Not Required for Having Copies. 114 | You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 115 | 116 | 10. Automatic Licensing of Downstream Recipients. 117 | Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. 118 | 119 | An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. 120 | 121 | You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 122 | 123 | 11. Patents. 124 | A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. 125 | 126 | A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. 127 | 128 | Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. 129 | 130 | In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. 131 | 132 | If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. 133 | 134 | If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. 135 | 136 | A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. 137 | 138 | Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 139 | 140 | 12. No Surrender of Others' Freedom. 141 | If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot use, propagate or convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not use, propagate or convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 142 | 143 | 13. Offering the Program as a Service. 144 | If you make the functionality of the Program or a modified version available to third parties as a service, you must make the Service Source Code available via network download to everyone at no charge, under the terms of this License. Making the functionality of the Program or modified version available to third parties as a service includes, without limitation, enabling third parties to interact with the functionality of the Program or modified version remotely through a computer network, offering a service the value of which entirely or primarily derives from the value of the Program or modified version, or offering a service that accomplishes for users the primary purpose of the Program or modified version. 145 | 146 | “Service Source Code” means the Corresponding Source for the Program or the modified version, and the Corresponding Source for all programs that you use to make the Program or modified version available as a service, including, without limitation, management software, user interfaces, application program interfaces, automation software, monitoring software, backup software, storage software and hosting software, all such that a user could run an instance of the service using the Service Source Code you make available. 147 | 148 | 14. Revised Versions of this License. 149 | Sean Neilan may publish revised and/or new versions of the Server Side Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the Server Side Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by Sean Neilan. If the Program does not specify a version number of the Server Side Public License, you may choose any version ever published by Sean Neilan. 152 | 153 | If the Program specifies that a proxy can decide which future versions of the Server Side Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. 154 | 155 | Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 156 | 157 | 15. Disclaimer of Warranty. 158 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 159 | 160 | 16. Limitation of Liability. 161 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 162 | 163 | 17. Interpretation of Sections 15 and 16. 164 | If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 165 | 166 | END OF TERMS AND CONDITIONS 167 | --------------------------------------------------------------------------------