diff --git a/README.md b/README.md index bcd1a45..70d5f22 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# 💻 Personal C++ Low Latency Stock Exchange - ![CI](https://github.com/sneilan/stock-exchange/actions/workflows/tests.yml/badge.svg) +# 💻 Personal C++ Low Latency Stock Exchange + 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. I built this as a fun nerdy project to show off my skills. Check out my [linkedin](https://linkedin.com/in/seanneilan) @@ -11,6 +11,7 @@ I built this as a fun nerdy project to show off my skills. Check out my [linkedi * Plug in stock or crypto data and test against that * Test against slippage and network failures * Allow trading robots to develop new market patterns and write software to detect them +# Easy install It uses the same techniques and algorithms as [NASDAQ](https://martinfowler.com/articles/lmax.html) but unoptimized. @@ -18,7 +19,7 @@ Compare to [LMAX exchange](https://lmax-exchange.github.io/disruptor/). ## What is an Exchange? -For reference, 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, +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, 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 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 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 @@ -28,7 +29,7 @@ to tell you! ``` git clone git@github.com:sneilan/stock-exchange.git stock-exchange cd stock-exchange -docker-compose up +docker compose up ``` The exchange will start up on the default port 8888. @@ -53,16 +54,139 @@ Honestly there's a lot of work to do but I hope this becomes the premier stock e ## Protocol -<< TODO >> +The exchange is basic for now. You connect, place trades and recieve notifications about your trades. + +### Connect + +Open a connection to the server with a socket. Use this in python + +```python +import socket + +host = '0.0.0.0' +port = 8888 +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.connect((host, port)) +response = sock.recv(1024) +print("Connected!") +``` + +### Authentication + +_or lack thereof_ + +There is no authentication currently. + +You will be assigned a User ID however on connection that is not told +to the client except on trade notifications. Your "user id" is the socket number. + +The exchange supports up to 30 concurrent clients. +First user to connect will have user id 0, second 1 and so one. If User 1 disconnects and a new user reconnects, +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 + +### What is traded + +Currently the exchange trades one unnamed symbol. The name of the symbol +is whatever you want. To trade multiple symbols, start up multiple exchanges. + +### Balances + +Trade balances are infinite. Wallets and balances will come later. -## Current features +### Risk Controls + +No risk controls at the moment. Place as many trades as you like. + +### Sending a trade + +After connecting, send a trade by submitting 9 bytes. +1. Byte 0: Char - buy or sell with 'b' for buy and 's' for sell. +2. Bytes 1-4 (inclusive) - Price as a positive unsigned integer in pennies. +3. Bytes 5-9 (inclusive) - Quantity as a positive unsigned integer. + +Here's an example of sending a buy order in Python for 500 shares at +$1.23 / share. Assuming sock is an open socket to the exchange. + +```python +price = 123 # $1.23 in 1 hundred 23 pennies. +quantity = 500 +side = 'b' # 's' for sell +message = pack( + 'cii', + bytes(side, 'ascii'), + price, + quantity, +) +sock.sendall(message) ``` -[x] Connect from internet -[x] Place trades -[x] Get trade fill notifications -[x] Easy install + +Server will immediately send a trade notification with the id +of the trade. + +### Trade Notifications + +As soon as you send a trade, the server will tell you +the trade is recieved with the ID of the trade. Each trade +notification is 21 bytes. +1. Byte 0: Char - Notification type. +'r' is 'recieved' +'u' is 'updated' +'f' is 'filled' +2. Bytes 1-8 (inclusive): Unsigned long long - trade id +3. Bytes 9-12 (inclusive): Unsigned integer - quantity +4. Bytes 13-16 (inclusive): Unsigned integer - filled quantity +5. Bytes 17-20 (inclusive): Unsigned integer - client id +Client id is not important but will tell you what integer 0-30 your "user id" is. + +You will always get a recieved notification. The other two notifications to a trade are either updated +or filled. + +Here's an example of recieving trade notifications in Python. It assumes that sock is a connected +socket to the server. + +```python +from struct import unpack + +msg_type_to_msg = {} +msg_type_to_msg['u'] = 'updated' +msg_type_to_msg['f'] = 'filled' +msg_type_to_msg['r'] = 'recieved' +while True: + data = sock.recv(21) + if data: + # c is char + # Q is unsigned long long + # i is 4 byte integer + # I originally tried to use 'cQiii' but unpack would not + # parse the bytes correctly. This works for now. + format_string = 'Qiii' + unpacked_data = unpack(format_string, data[1:]) + msg_type = chr(data[0]) + message = msg_type_to_msg[msg_type] + id = unpacked_data[0] + quantity = unpacked_data[1] + filled_quantity = unpacked_data[2] + client_id = unpacked_data[3] + + print('id', id, 'message', message, 'quantity', quantity, 'filled_quantity', filled_quantity, 'client_id', client_id) ``` +Check out scripts/loadTest.py for an example trading client. + +You can paste these protocols into Chat GPT and produce trading frontends in your preferred language. + +### Market Data + +This is not implemented yet. This is a high priority on the roadmap. + +### Getting Current Bid / Ask + +Not implemented. Very high priority! + +### Backups + +Not implemented. + ## Test ``` docker compose run -it core /app/test @@ -73,11 +197,9 @@ Will run all tests automatically. ## TODO There's a lot (to do)[https://github.com/sneilan/stock-exchange/issues] in creating a low-latency stock exchange from the ground up. -``` -[ ] Market data -[ ] Cancel trades -[ ] Authentication / Security -[ ] Journaling (save trades to database) -[ ] Simple trading client / GUI -``` +Check out the issue list. + +## Contributing + +Check out any of the tickets on the issues list or file a new one with a proposal! diff --git a/scripts/loadTest.py b/scripts/loadTest.py index 07b5775..d0d618a 100644 --- a/scripts/loadTest.py +++ b/scripts/loadTest.py @@ -26,15 +26,13 @@ side = 'b' def listener(sock): - while True: + while True: data = sock.recv(21) if data: # c is char # Q is unsigned long long # i is 4 byte integer format_string = 'Qiii' - # import ipdb - # ipdb.set_trace() unpacked_data = unpack(format_string, data[1:]) msg_type = chr(data[0]) message = '' @@ -64,11 +62,10 @@ def listener(sock): price = random.randrange(100, 1000) quantity = random.randrange(100, 200, 10) message = pack( - 'ciic', + 'cii', bytes(side, 'ascii'), price, quantity, - bytes(str(random.randrange(0, 9)), 'ascii') ) side_msg = 'buy' if side == 'b' else 'sell' print(f"Placing {side_msg} for quantity {quantity} at price {price}")