├── setup.py ├── optiver ├── __init__.py ├── test │ ├── __init__.py │ └── main.py ├── trader.py ├── messageService.py ├── MarketState.py └── autotrader.py ├── sms_0.png ├── sms_1.jpg ├── impression.png ├── front_end ├── js │ ├── Price.js │ ├── Trade.js │ ├── theme.js │ ├── main.js │ └── dummy_data.js ├── style.css └── index.html ├── .gitignore └── README.md /setup.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /optiver/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /optiver/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sms_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nout-kleef/HTB-VI/HEAD/sms_0.png -------------------------------------------------------------------------------- /sms_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nout-kleef/HTB-VI/HEAD/sms_1.jpg -------------------------------------------------------------------------------- /impression.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nout-kleef/HTB-VI/HEAD/impression.png -------------------------------------------------------------------------------- /front_end/js/Price.js: -------------------------------------------------------------------------------- 1 | class Price { 2 | constructor(time, price) { 3 | this.time = time; 4 | this.price = price; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /optiver/test/main.py: -------------------------------------------------------------------------------- 1 | def inc(x): 2 | return x + 1 3 | 4 | 5 | def test_answer1(): 6 | assert inc(3) == 4 7 | 8 | 9 | def test_answer2(): 10 | assert inc(3) == 4 11 | -------------------------------------------------------------------------------- /front_end/js/Trade.js: -------------------------------------------------------------------------------- 1 | class Trade { 2 | constructor(time, isBuy, price, volume) { 3 | this.x = time; 4 | this.y = price; 5 | this.color = isBuy ? "green" : "red"; 6 | this.radius = 10; // implement volume later 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /optiver/trader.py: -------------------------------------------------------------------------------- 1 | from autotrader import start_autotrader 2 | import MarketState 3 | 4 | 5 | class Trader(): 6 | def __init__(self): 7 | self.balance = 20000 8 | def startTrading(self): 9 | start_autotrader() 10 | def updateBalance(amount): 11 | print("Updating balance. Current Balance: {0}".format(self.balance)) 12 | self.balance = self.balance + amount 13 | print("Balance after update: ".format(self.balance)) 14 | 15 | 16 | trader = Trader() 17 | trader.startTrading() 18 | -------------------------------------------------------------------------------- /optiver/messageService.py: -------------------------------------------------------------------------------- 1 | from secrets import TWILLIO_AUTH_TOKEN, TWILLIO_ACCOUNT_SID 2 | from twilio.rest import Client 3 | 4 | class MessageService(): 5 | def __init__(self, number_to_inform): 6 | self.client = Client(TWILLIO_ACCOUNT_SID, TWILLIO_AUTH_TOKEN) 7 | self.number_to_inform = number_to_inform 8 | self.sendInitMessage() 9 | 10 | def sendInitMessage(self): 11 | message = self.client.messages.create(from_='+12058462931',to=self.number_to_inform,body="Hello! Automated trading system is started!") 12 | print(message.sid) 13 | 14 | def sendMessageUpdate(self, percentage_profit): 15 | print('Sending Message') 16 | message = self.client.messages.create(body="Hello! I just made: {0} percentage profit!".format(percentage_profit),from_='+12058462931',to=self.number_to_inform) 17 | print(message.sid) 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /front_end/js/theme.js: -------------------------------------------------------------------------------- 1 | Highcharts.theme = { 2 | colors: ['#058DC7', '#50B432', '#ED561B', '#DDDF00', '#24CBE5', '#64E572', 3 | '#FF9655', '#FFF263', '#6AF9C4'], 4 | chart: { 5 | backgroundColor: { 6 | linearGradient: [0, 0, 500, 500], 7 | stops: [ 8 | [0, 'rgb(11, 11, 11)'], 9 | [1, 'rgb(11, 11, 11)'] 10 | ] 11 | }, 12 | }, 13 | title: { 14 | style: { 15 | color: '#fff', 16 | font: 'bold 16px "Trebuchet MS", Verdana, sans-serif' 17 | } 18 | }, 19 | subtitle: { 20 | style: { 21 | color: '#999', 22 | font: 'bold 12px "Trebuchet MS", Verdana, sans-serif' 23 | } 24 | }, 25 | legend: { 26 | itemStyle: { 27 | font: '9pt Trebuchet MS, Verdana, sans-serif', 28 | color: 'black' 29 | }, 30 | itemHoverStyle: { 31 | color: 'gray' 32 | } 33 | } 34 | }; 35 | // Apply the theme 36 | Highcharts.setOptions(Highcharts.theme); 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | secrets.py -------------------------------------------------------------------------------- /front_end/style.css: -------------------------------------------------------------------------------- 1 | html, body, * { 2 | background-color: #111111; 3 | color: white; 4 | } 5 | 6 | div.row { 7 | margin: 0; 8 | } 9 | 10 | h1 { 11 | text-align: center; 12 | } 13 | 14 | #notifications{ 15 | box-shadow: 2px 2px 3px 1px white; 16 | margin-right: 10px; 17 | } 18 | 19 | .highcharts-root{ 20 | border: 1px solid green; 21 | border-radius: 10px; 22 | } 23 | 24 | #container1, #container2{ 25 | margin-bottom: 20px; 26 | z-index: 999; 27 | margin-left: 10px; 28 | margin-right: 5px; 29 | box-shadow: 2px 2px 3px 1px white; 30 | } 31 | 32 | 33 | body { 34 | min-height: 450px; 35 | height: 100vh; 36 | margin: 0; 37 | background: radial-gradient(circle, darken(dodgerblue, 10%), #1f4f96, #1b2949, #000); 38 | } 39 | 40 | .stage { 41 | height: 300px; 42 | width: 500px; 43 | margin: auto; 44 | position: absolute; 45 | top: 0; right: 0; bottom: 0; left: 0; 46 | perspective: 9999px; 47 | transform-style: preserve-3d; 48 | } 49 | 50 | .layer { 51 | width: 100%; 52 | height: 100%; 53 | position: absolute; 54 | transform-style: preserve-3d; 55 | animation: ಠ_ಠ 5s infinite alternate ease-in-out -7.5s; 56 | animation-fill-mode: forwards; 57 | transform: rotateY(40deg) rotateX(33deg) translateZ(0); 58 | } 59 | 60 | .layer:after { 61 | font: 150px/0.65 'Pacifico', 'Kaushan Script', Futura, 'Roboto', 'Trebuchet MS', Helvetica, sans-serif; 62 | content: 'Pure\A css!'; 63 | white-space: pre; 64 | text-align: center; 65 | height: 100%; 66 | width: 100%; 67 | position: absolute; 68 | top: 50px; 69 | color: darken(#fff, 4%); 70 | letter-spacing: -2px; 71 | text-shadow: 4px 0 10px hsla(0, 0%, 0%, .13); 72 | } 73 | 74 | $i: 1; 75 | $NUM_LAYERS: 20; 76 | @for $i from 1 through $NUM_LAYERS { 77 | .layer:nth-child(#{$i}):after{ 78 | transform: translateZ(($i - 1) * -1.5px); 79 | } 80 | } 81 | 82 | .layer:nth-child(n+#{round($NUM_LAYERS/2)}):after { 83 | -webkit-text-stroke: 3px hsla(0, 0%, 0%, .25); 84 | } 85 | 86 | .layer:nth-child(n+#{round($NUM_LAYERS/2 + 1)}):after { 87 | -webkit-text-stroke: 15px dodgerblue; 88 | text-shadow: 6px 0 6px darken(dodgerblue,35%), 89 | 5px 5px 5px darken(dodgerblue,40%), 90 | 0 6px 6px darken(dodgerblue,35%); 91 | } 92 | 93 | .layer:nth-child(n+#{round($NUM_LAYERS/2 + 2)}):after { 94 | -webkit-text-stroke: 15px darken(dodgerblue, 10%); 95 | } 96 | 97 | .layer:last-child:after { 98 | -webkit-text-stroke: 17px hsla(0, 0%, 0%, .1); 99 | } 100 | 101 | .layer:first-child:after{ 102 | color: #fff; 103 | text-shadow: none; 104 | } 105 | 106 | @keyframes ಠ_ಠ { 107 | 100% { transform: rotateY(-40deg) rotateX(-43deg); } 108 | } 109 | 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hack The Burgh - VI 2 | During a sleep depriving but rewarding weekend at **Hack the Burgh**, we set out to build an **automated trading bot**. 3 | This challenge was set up by Optiver, and the goal was to find a trading strategy that would allow the bot to "out-trade" the competition. 4 | 5 | Both me and my team had little to no knowledge of the FinTech world, and it was interesting to gain some insight into how latency (or rather, lack thereof) allows one trader to gain an edge over the others! 6 | 7 | I'm very proud of what my team accomplished: apart from learning how to program an automated trading strategy, we ended up winning the Optiver challenge! 🥳 📈 8 | 9 | # Project - Trading Bot for the Optiver Challenge 10 | 11 | The Challenge is to create a trading algorithm to make money by buy and selling stocks on two indeces : S&P500 (SP) and Eurostoxx (ESX). 12 | 13 | Our project is composed of three parts: 14 | 15 | 1) The trading algorithm itself 16 | 2) An SMS alert functionality to communicate profits and losses 17 | 3) A Web page for data visualization 18 | 19 | ## **1 - Algorithm** 20 | We use the Ichimoku Cloud Indicator, a fairly safe and simple strategy especially for Microtrading. 21 | The indicator is based on four components: 22 | 1) Conversion Line -> midpoint of the last 9 average prices 23 | 2) Base Line -> midpoint of the last 26 average prices 24 | 3) Leading Span A -> midpoint of Conversion Line and Base Line 25 | 4) Leading Span B -> midpoint of the last 52 average prices 26 | 27 | **Trading Strategy**: 28 | The Leading Spans consitute the boundaries of the "cloud". When Leading Span A is moving above Leading Span B it indicates an uptrend is gaining momentum. Conversly, Leading Span B above means a downward trend is taking place. A thin cloud shows indecision and a potentially weakening trend, whereas a wide one means it is a good time to start trading. 29 | 30 | If the trend is Up and the Conversion Line falls below the Base Line, then it is time to buy stocks. Similarly, when the crossover of Conversion Line and Base Line happens in the opposite direction, it is time to sell. 31 | To contain risk, we are allowing the bot to buy only two stocks on each market (four in total) for every "trading period". 32 | ![image](https://user-images.githubusercontent.com/47427204/75623153-5ad41e80-5b9f-11ea-9f65-9c83af4024e7.png) 33 | 34 | ## **2 - SMS alert** 35 | The SMS alert system will send a notification to the user when a trade is closed, communicating losses and profit. 36 | 37 | Some screenshots of the SMS functionality: 38 | 39 | 40 |
41 | 42 | ## **3 - front-end** 43 | 1) `cd front_end/` 44 | 2) `python -m SimpleHTTPServer` 45 | 3) go to `0.0.0.0:8000` 46 | 47 | The following shows an impression of what our front-end looks like. 48 | The graphs show the mid-market prices for the SP and ESX instruments, respectively. 49 | We plan to broadcast the trades our bot makes over SocketIO, which we can also show in the graphs as a scatterplot. 50 | NB: currently, the front-end only shows static data. When we get the SocketIO integration set up, we hope that we can update the chart in real time. 51 | ![image](impression.png) 52 | 53 | ## Authors 54 | [Mihail Yonchev](https://github.com/slaifan) 55 | [Nout Kleef](https://github.com/nout-kleef) 56 | [Vincenzo Incutti](https://github.com/enzo-inc) 57 | -------------------------------------------------------------------------------- /optiver/MarketState.py: -------------------------------------------------------------------------------- 1 | 2 | from messageService import MessageService 3 | from secrets import PHONE_NUMBER 4 | 5 | class MarketState(): 6 | def __init__(self, stock, feedcode): 7 | self.messageService = MessageService(PHONE_NUMBER) 8 | self.stock = stock 9 | self.feedcode = feedcode 10 | self.buy_prices = list() 11 | self.sell_prices = list() 12 | self.entries = 0 13 | self.buy_price = None 14 | self.last_bought_price = None 15 | self.positions = 0 16 | 17 | def addEntry(self, buy_price, sell_price): 18 | if self.entries > 52: 19 | self.buy_prices.pop(0) 20 | self.sell_prices.pop(0) 21 | self.buy_prices.append(buy_price) 22 | self.sell_prices.append(sell_price) 23 | self.entries = self.entries + 1 24 | print(self.buy_prices) 25 | print('entries', self.entries) 26 | 27 | def isEligibleForTradeBuy(self, buy_price): 28 | if self.entries < 52: 29 | return False 30 | conversionLine = self.__calcConversionLine(self.buy_prices) 31 | baseLine = self.__calcBaseLine(self.buy_prices) 32 | cloudPoint = self.__getCloud(conversionLine, baseLine, self.buy_prices) 33 | print(self.stock) 34 | print('Conversion Line over Base Line: ', conversionLine > baseLine) 35 | print('Positions:', self.positions) 36 | print('Cloud Point: ', cloudPoint) 37 | print('Buy Price ', buy_price) 38 | print('----------------------') 39 | if self.__isAboveCloud(cloudPoint, buy_price) and self.positions == 0 and conversionLine > baseLine: 40 | self.positions = self.positions + 1 41 | self.buy_price = buy_price 42 | return True 43 | return False 44 | 45 | def isEligibleForTradeSell(self, sell_price): 46 | if self.entries < 52: 47 | return False 48 | conversionLine = self.__calcConversionLine(self.sell_prices) 49 | baseLine = self.__calcBaseLine(self.sell_prices) 50 | cloudPoint = self.__getCloud(conversionLine, baseLine, self.sell_prices) 51 | if conversionLine < baseLine and self.buy_price and self.buy_price < sell_price: 52 | self.positions = 0 53 | try: 54 | percentage_difference = ((float(sell_price)-self.buy_price)/self.buy_price)*100 55 | self.messageService.sendMessageUpdate(percentage_difference) 56 | except: 57 | print('[ERROR]: Messaging service error!') 58 | pass 59 | return True 60 | return False 61 | 62 | def __calcConversionLine(self, prices): 63 | target_list = prices[-1:-10:-1] 64 | highest = max(target_list) 65 | lowest = min(target_list) 66 | return (highest+lowest)/2 67 | 68 | def __calcBaseLine(self, prices): 69 | target_list = prices[-1:-27:-1] 70 | highest = max(target_list) 71 | lowest = min(target_list) 72 | return (highest+lowest)/2 73 | 74 | def __calcLeadingSpanA(self, conversionLinePrice, baseLinePrice): 75 | return (conversionLinePrice + baseLinePrice) / 2 76 | 77 | def __calcLeadingSpanB(self, prices): 78 | target_list = prices[-1:-52:-1] 79 | highest = max(target_list) 80 | lowest = min(target_list) 81 | return (highest+lowest)/2 82 | 83 | def __getCloud(self, conversionLinePrice, baseLinePrice, prices): 84 | leadingSpanA = self.__calcLeadingSpanA(conversionLinePrice, baseLinePrice) 85 | leadingSpanB = self.__calcLeadingSpanB(prices) 86 | return max(leadingSpanA, leadingSpanB) 87 | 88 | def __isAboveCloud(self, cloudPrice, currentAskPrice): 89 | return currentAskPrice > cloudPrice -------------------------------------------------------------------------------- /front_end/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Ichimoku Cloud Indicator trading bot 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |

Ichimoku Cloud Indicator trading bot

34 | 35 | 36 | 37 |
38 | 39 | 40 |
41 |

SP MidMarket Prices

42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 |

ESX MidMarket Prices

53 |
54 | 55 | sold 2 shares of ESX-FUTURE at 33403
56 | bought 5 shares 57 | of 58 | ESX-FUTURE at 3398
59 | sold 2 shares of 60 | ESX-FUTURE at 61 | 3418
62 | bought 5 shares 63 | of ESX-FUTURE at 64 | 2918
65 | bought 2 shares 66 | of SP-FUTURE at 67 | 2916
68 | bought 1 share 69 | of ESX-FUTURE at 70 | 2900
71 | bought 2 shares 72 | of SP-FUTURE at 73 | 2916
74 | bought 1 share 75 | of ESX-FUTURE at 76 | 3410
77 | bought 2 shares 78 | of SP-FUTURE at 79 | 2926
80 | bought 2 shares 81 | of SP-FUTURE at 82 | 2816
83 | bought 2 shares 84 | of SP-FUTURE at 85 | 2916
86 |
87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /front_end/js/main.js: -------------------------------------------------------------------------------- 1 | const DUMMY = true; 2 | let SP_prices = []; 3 | let ESX_prices = []; 4 | let SP_trades = []; 5 | let ESX_trades = []; 6 | let ws; 7 | 8 | window.onload = main(); 9 | 10 | function main() { 11 | console.info("page loaded"); 12 | if (DUMMY) { 13 | SP_prices = dummy_SP_prices; 14 | ESX_prices = dummy_ESX_prices; 15 | SP_trades = dummy_SP_trades; 16 | ESX_trades = dummy_ESX_trades; 17 | updateGraph(); 18 | } else { 19 | // TODO: live data 20 | startListening(); 21 | } 22 | } 23 | 24 | function startListening() { 25 | // ws = new WebSocket('ws://'); 26 | // ws.onopen = function () { 27 | // console.log('Connected'); 28 | // }; 29 | // ws.onmessage = updateReceived; 30 | } 31 | 32 | // TYPE=PRICE|FEEDCODE=FOOBAR|BID_PRICE=10.0|BID_VOLUME=100|ASK_PRICE=11.0|ASK_VOLUME=20 33 | // TYPE=TRADE|FEEDCODE=FOOBAR|BUY=TRUE|PRICE=22.0|VOLUME=100 34 | function updateReceived(msg) { 35 | // check which list this update should go to 36 | comps = msg.split("|"); 37 | if (comps[0] == "TYPE=PRICE") { 38 | let bid_price = Number(comps[2].split("=")[1]); 39 | let bid_volume = Number(comps[3].split("=")[1]); 40 | let ask_price = Number(comps[4].split("=")[1]); 41 | let ask_volume = Number(comps[5].split("=")[1]); 42 | let mid_market = (bid_price + ask_price) / 2; 43 | if (comps[1] == "FEEDCODE=SP-FUTURE") { 44 | SP_prices.push(mid_market); 45 | } else { 46 | ESX_prices.push(mid_market); 47 | } 48 | } else { 49 | let isBuy = compos[2] == "BUY=TRUE"; 50 | let price = Number(comps[3].split("=")[1]); 51 | let volume = Number(comps[4].split("=")[1]); 52 | notify(isBuy ? "bought" : "sold", volume, comps[1].split("=")[1], price); 53 | if (comps[1] == "FEEDCODE=SP-FUTURE") { 54 | SP_trades.push(new Trade(0, isBuy, price, volume)); 55 | } else { 56 | ESX_trades.push(new Trade(0, isBuy, price, volume)); 57 | } 58 | } 59 | // redraw graph with new data 60 | updateGraph(); 61 | } 62 | 63 | function notify(action, vol, instr, price) { 64 | let icon = ""; 65 | if (action == "bought") { 66 | icon = " "; 67 | } else if (action == "sold") { 68 | icon = " "; 69 | } 70 | const msg = "" + icon + "" + action + " " + vol + " shares of " + instr + " at " + price + "
"; 71 | const currHtml = document.getElementById("notifications").innerHTML; 72 | document.getElementById("notifications").innerHTML = msg + currHtml; 73 | } 74 | 75 | function updateGraph() { 76 | // TODO: convert lists to JSON 77 | let chart1 = Highcharts.chart('container1', { 78 | chart: { 79 | zoomType: 'x' 80 | }, 81 | title: { 82 | text: 'SP over time', 83 | color: "white" 84 | }, 85 | xAxis: { 86 | type: 'datetime' 87 | }, 88 | yAxis: { 89 | }, 90 | legend: { 91 | enabled: false 92 | }, 93 | plotOptions: { 94 | area: { 95 | fillColor: { 96 | linearGradient: { 97 | x1: 0, 98 | y1: 0, 99 | x2: 0, 100 | y2: 1 101 | }, 102 | stops: [ 103 | [0, Highcharts.getOptions().colors[0]], 104 | [1, Highcharts.color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')] 105 | ] 106 | }, 107 | marker: { 108 | radius: 2 109 | }, 110 | lineWidth: 1, 111 | states: { 112 | hover: { 113 | lineWidth: 1 114 | } 115 | }, 116 | threshold: null 117 | } 118 | }, 119 | 120 | series: [{ 121 | type: 'line', 122 | name: 'SP mid-market price', 123 | data: SP_prices 124 | } 125 | ] 126 | }); 127 | 128 | let chart2 = Highcharts.chart('container2', { 129 | chart: { 130 | zoomType: 'x' 131 | }, 132 | title: { 133 | text: 'ESX over time' 134 | }, 135 | xAxis: { 136 | type: 'datetime' 137 | }, 138 | yAxis: { 139 | }, 140 | legend: { 141 | enabled: false 142 | }, 143 | plotOptions: { 144 | area: { 145 | fillColor: { 146 | linearGradient: { 147 | x1: 0, 148 | y1: 0, 149 | x2: 0, 150 | y2: 1 151 | }, 152 | stops: [ 153 | [0, Highcharts.getOptions().colors[0]], 154 | [1, Highcharts.color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')] 155 | ] 156 | }, 157 | marker: { 158 | radius: 2 159 | }, 160 | lineWidth: 1, 161 | states: { 162 | hover: { 163 | lineWidth: 1 164 | } 165 | }, 166 | threshold: null 167 | } 168 | }, 169 | 170 | series: [{ 171 | type: 'line', 172 | name: 'ESX mid-market price', 173 | data: ESX_prices 174 | } 175 | ] 176 | }); 177 | } 178 | -------------------------------------------------------------------------------- /front_end/js/dummy_data.js: -------------------------------------------------------------------------------- 1 | let dummy_SP_prices = [ 2 | 2912.25, 3 | 2912.5, 4 | 2912.25, 5 | 2912.25, 6 | 2912.0, 7 | 2911.75, 8 | 2911.75, 9 | 2911.5, 10 | 2911.5, 11 | 2911.5, 12 | 2912.25, 13 | 2912.25, 14 | 2912.0, 15 | 2911.75, 16 | 2911.5, 17 | 2911.5, 18 | 2911.5, 19 | 2911.75, 20 | 2911.75, 21 | 2911.75, 22 | 2910.75, 23 | 2910.5, 24 | 2910.5, 25 | 2910.5, 26 | 2910.5, 27 | 2910.5, 28 | 2910.25, 29 | 2910.25, 30 | 2910.5, 31 | 2910.5, 32 | 2910.5, 33 | 2910.75, 34 | 2910.75, 35 | 2910.5, 36 | 2911.25, 37 | 2911.25, 38 | 2911.5, 39 | 2911.25, 40 | 2911.5, 41 | 2911.5, 42 | 2911.25, 43 | 2911.25, 44 | 2911.25, 45 | 2911.25, 46 | 2911.25, 47 | 2911.25, 48 | 2911.25, 49 | 2911.5, 50 | 2910.75, 51 | 2911.25, 52 | 2911.25, 53 | 2911.25, 54 | 2911.5, 55 | 2911.0, 56 | 2912.25, 57 | 2912.25, 58 | 2912.25, 59 | 2912.25, 60 | 2912.25, 61 | 2911.75, 62 | 2912.0, 63 | 2912.25, 64 | 2912.5, 65 | 2912.25, 66 | 2912.25, 67 | 2912.25, 68 | 2912.25, 69 | 2912.25, 70 | 2912.0, 71 | 2912.0, 72 | 2911.75, 73 | 2911.75, 74 | 2912.25, 75 | 2912.25, 76 | 2912.5, 77 | 2912.25, 78 | 2912.25, 79 | 2912.25, 80 | 2912.25, 81 | 2912.25, 82 | 2910.75, 83 | 2911.25, 84 | 2911.5, 85 | 2912.0, 86 | 2912.0, 87 | 2912.0, 88 | 2912.0, 89 | 2912.0, 90 | 2912.0, 91 | 2911.75, 92 | 2913.5, 93 | 2914.75, 94 | 2915.5, 95 | 2915.5, 96 | 2915.25, 97 | 2915.25, 98 | 2915.25, 99 | 2915.25, 100 | 2915.25, 101 | 2915.25, 102 | 2915.25, 103 | 2915.5, 104 | 2915.25, 105 | 2915.75, 106 | 2915.5, 107 | 2915.0, 108 | 2915.0, 109 | 2915.0, 110 | 2915.0, 111 | 2914.25, 112 | 2914.25, 113 | 2914.25, 114 | 2914.75, 115 | 2914.75, 116 | 2915.25, 117 | 2914.25, 118 | 2915.5, 119 | 2915.5, 120 | 2915.75, 121 | 2915.75, 122 | 2915.75, 123 | 2916.0, 124 | 2916.0, 125 | 2916.25, 126 | 2915.75, 127 | 2915.75, 128 | 2916.0, 129 | 2916.0, 130 | 2916.0, 131 | 2915.5, 132 | 2915.5, 133 | 2915.25, 134 | 2915.25, 135 | 2915.25, 136 | 2915.25, 137 | 2915.25, 138 | 2915.25, 139 | 2915.0, 140 | 2915.0, 141 | 2915.25, 142 | 2915.25, 143 | 2915.75, 144 | 2915.75, 145 | 2915.75, 146 | 2915.75, 147 | 2915.75, 148 | 2915.75, 149 | 2916.25, 150 | 2916.25, 151 | 2915.75, 152 | 2916.0, 153 | 2916.0, 154 | 2916.25, 155 | 2916.25, 156 | 2916.25, 157 | 2915.75, 158 | 2915.5, 159 | 2913.0, 160 | 2912.75, 161 | 2912.75, 162 | 2913.25, 163 | 2913.25 164 | ]; 165 | let dummy_ESX_prices = [ 166 | 167 | 3353.0, 168 | 3353.0, 169 | 3353.0, 170 | 3353.0, 171 | 3353.25, 172 | 3353.75, 173 | 3354.0, 174 | 3354.0, 175 | 3354.0, 176 | 3354.0, 177 | 3354.0, 178 | 3354.0, 179 | 3354.0, 180 | 3353.75, 181 | 3353.75, 182 | 3353.25, 183 | 3353.25, 184 | 3353.0, 185 | 3353.75, 186 | 3353.25, 187 | 3353.25, 188 | 3353.0, 189 | 3352.75, 190 | 3352.5, 191 | 3352.0, 192 | 3352.25, 193 | 3352.25, 194 | 3352.0, 195 | 3351.75, 196 | 3351.5, 197 | 3351.5, 198 | 3352.25, 199 | 3353.25, 200 | 3353.0, 201 | 3352.25, 202 | 3352.5, 203 | 3352.5, 204 | 3352.5, 205 | 3352.5, 206 | 3352.5, 207 | 3352.25, 208 | 3352.25, 209 | 3352.25, 210 | 3352.0, 211 | 3351.75, 212 | 3351.5, 213 | 3351.5, 214 | 3351.5, 215 | 3351.5, 216 | 3351.5, 217 | 3351.5, 218 | 3351.25, 219 | 3351.5, 220 | 3351.5, 221 | 3351.5, 222 | 3351.75, 223 | 3351.75, 224 | 3350.75, 225 | 3350.5, 226 | 3350.75, 227 | 3351.0, 228 | 3350.75, 229 | 3351.0, 230 | 3351.0, 231 | 3351.0, 232 | 3351.25, 233 | 3351.5, 234 | 3352.0, 235 | 3352.0, 236 | 3352.0, 237 | 3352.0, 238 | 3352.0, 239 | 3352.0, 240 | 3350.75, 241 | 3351.0, 242 | 3350.75, 243 | 3350.75, 244 | 3351.0, 245 | 3351.0, 246 | 3351.0, 247 | 3351.25, 248 | 3351.25, 249 | 3351.25, 250 | 3351.25, 251 | 3351.25, 252 | 3351.25, 253 | 3351.25, 254 | 3350.5, 255 | 3350.5, 256 | 3350.5, 257 | 3350.5, 258 | 3350.75, 259 | 3350.0, 260 | 3350.0, 261 | 3349.75, 262 | 3350.25, 263 | 3350.25, 264 | 3350.25, 265 | 3350.0, 266 | 3350.0, 267 | 3350.0, 268 | 3350.25, 269 | 3350.25, 270 | 3350.5, 271 | 3350.5, 272 | 3350.5, 273 | 3350.25, 274 | 3350.25, 275 | 3350.0, 276 | 3348.5, 277 | 3348.5, 278 | 3348.5, 279 | 3348.5, 280 | 3348.75, 281 | 3348.75, 282 | 3349.0, 283 | 3349.0, 284 | 3349.0, 285 | 3349.0, 286 | 3346.25, 287 | 3346.5, 288 | 3346.5, 289 | 3346.25, 290 | 3346.25, 291 | 3346.0, 292 | 3345.75, 293 | 3346.0, 294 | 3346.25, 295 | 3346.5, 296 | 3346.5, 297 | 3346.5, 298 | 3346.25, 299 | 3346.25, 300 | 3346.0, 301 | 3346.0, 302 | 3346.0, 303 | 3345.75, 304 | 3345.75, 305 | 3346.25, 306 | 3346.5, 307 | 3346.5, 308 | 3346.75, 309 | 3346.75, 310 | 3346.75, 311 | 3346.75, 312 | 3347.0, 313 | 3347.0, 314 | 3348.25, 315 | 3348.75, 316 | 3350.0, 317 | 3350.0, 318 | 3349.75, 319 | 3349.5, 320 | 3349.5, 321 | 3349.5, 322 | 3349.5, 323 | 3348.75, 324 | 3348.5, 325 | 3348.5, 326 | 3348.5, 327 | 3348.5 328 | ]; 329 | let dummy_SP_trades = []; 330 | let dummy_ESX_trades = []; 331 | -------------------------------------------------------------------------------- /optiver/autotrader.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import select 3 | from MarketState import MarketState 4 | from secrets import USERNAME, PASSWORD 5 | 6 | REMOTE_IP = "35.179.45.135" 7 | UDP_ANY_IP = "" 8 | 9 | # ------------------------------------- 10 | # EML code (EML is execution market link) 11 | # ------------------------------------- 12 | 13 | EML_UDP_PORT_LOCAL = 8078 14 | EML_UDP_PORT_REMOTE = 8001 15 | 16 | eml_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 17 | eml_sock.bind((UDP_ANY_IP, EML_UDP_PORT_LOCAL)) 18 | 19 | 20 | # ------------------------------------- 21 | # IML code (IML is information market link) 22 | # ------------------------------------- 23 | 24 | IML_UDP_PORT_LOCAL = 7078 25 | IML_UDP_PORT_REMOTE = 7001 26 | IML_INIT_MESSAGE = "TYPE=SUBSCRIPTION_REQUEST" 27 | 28 | iml_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 29 | iml_sock.bind((UDP_ANY_IP, IML_UDP_PORT_LOCAL)) 30 | 31 | 32 | # ------------------------------------- 33 | # Auto trader 34 | # ------------------------------------- 35 | 36 | def start_autotrader(): 37 | subscribe() 38 | event_listener() 39 | 40 | 41 | def subscribe(): 42 | iml_sock.sendto(IML_INIT_MESSAGE.encode(), 43 | (REMOTE_IP, IML_UDP_PORT_REMOTE)) 44 | 45 | def event_listener(): 46 | """ 47 | Wait for messages from the exchange and 48 | call handle_message on each of them. 49 | """ 50 | marketStateESX = MarketState('ESX', 'ESX-FUTURE') 51 | marketStateSP = MarketState('SP', 'SP-FUTURE') 52 | while True: 53 | ready_socks, _, _ = select.select([iml_sock, eml_sock], [], []) 54 | for socket in ready_socks: 55 | data, addr = socket.recvfrom(1024) 56 | message = data.decode('utf-8') 57 | handle_message(message, marketStateESX, marketStateSP) 58 | 59 | def place_order_if_eligible_sell(market, bid_price, volume): 60 | if market.isEligibleForTradeSell(bid_price): 61 | send_order(market.feedcode,"SELL",bid_price,volume) 62 | print("Selling {0} at: {1}, Volume: {2}".format(market.stock, bid_price, volume)) 63 | 64 | def place_order_if_eligble_buy(market, ask_price, volume): 65 | if market.isEligibleForTradeBuy(ask_price): 66 | send_order(market.feedcode,"BUY",ask_price,volume) 67 | print("Buying {0} at: {1}, Volume: {2}".format(market.stock, ask_price, volume)) 68 | 69 | """ 70 | Adds entry to market. 71 | Calls place_order_if_eligible_sell 72 | Calls place_order_if_eligble_buy 73 | """ 74 | def handle_server_message_for_marketState(market, bid_price, ask_price): 75 | add_entry_to_market(market, bid_price, ask_price) 76 | place_order_if_eligible_sell(market, bid_price, 0) 77 | place_order_if_eligble_buy(market, ask_price, 0) 78 | 79 | def add_entry_to_market(market, bid_price, ask_price): 80 | market.addEntry(bid_price, ask_price) 81 | 82 | """ 83 | Handles an entry message from the server [Either type: PRICE or TRADE] 84 | Receives message and a reference to marketStateESX and marketStateSP 85 | On message received from server its adding the entry to the appropriate market and checks if eiligble to open position. 86 | If yes, it opens a position using open_position(). 87 | """ 88 | def handle_message(message, marketStateESX, marketStateSP): 89 | comps = message.split("|") 90 | 91 | if len(comps) == 0: 92 | print(f"Invalid message received: {message}") 93 | return 94 | 95 | type = comps[0] 96 | 97 | if type == "TYPE=PRICE": 98 | 99 | feedcode = comps[1].split("=")[1] 100 | bid_price = float(comps[2].split("=")[1]) 101 | bid_volume = int(comps[3].split("=")[1]) 102 | ask_price = float(comps[4].split("=")[1]) 103 | ask_volume = int(comps[5].split("=")[1]) 104 | 105 | if feedcode == "SP-FUTURE": 106 | handle_server_message_for_marketState(marketStateSP, bid_price, ask_price) 107 | 108 | if feedcode == "ESX-FUTURE": 109 | handle_server_message_for_marketState(marketStateESX, bid_price, ask_price) 110 | 111 | if type == "TYPE=TRADE": 112 | 113 | feedcode = comps[1].split("=")[1] 114 | side = comps[2].split("=")[1] 115 | traded_price = float(comps[3].split("=")[1]) 116 | traded_volume = int(comps[4].split("=")[1]) 117 | 118 | if type == "TYPE=ORDER_ACK": 119 | 120 | if comps[1].split("=")[0] == "ERROR": 121 | error_message = comps[1].split("=")[1] 122 | print(f"Order was rejected because of error {error_message}.") 123 | return 124 | 125 | feedcode = comps[1].split("=")[1] 126 | traded_price = float(comps[2].split("=")[1]) 127 | 128 | # This is only 0 if price is not there, and volume became 0 instead. 129 | # Possible cause: someone else got the trade instead of you. 130 | if traded_price == 0: 131 | print(f"Unable to get trade on: {feedcode}") 132 | return 133 | 134 | traded_volume = int(comps[3].split("=")[1]) 135 | 136 | 137 | 138 | def send_order(target_feedcode, action, target_price, volume): 139 | """ 140 | Send an order to the exchange. 141 | :param target_feedcode: The feedcode, either "SP-FUTURE" or "ESX-FUTURE" 142 | :param action: "BUY" or "SELL" 143 | :param target_price: Price you want to trade at 144 | :param volume: Volume you want to trade at. Please start with 10 and go from there. Don't go crazy! 145 | :return: 146 | Example: 147 | If you want to buy 100 SP-FUTURES at a price of 3000: 148 | - send_order("SP-FUTURE", "BUY", 3000, 100) 149 | """ 150 | order_message = f"TYPE=ORDER|USERNAME={USERNAME}|PASSWORD={PASSWORD}|FEEDCODE={target_feedcode}|ACTION={action}|PRICE={target_price}|VOLUME={volume}" 151 | print(f"[SENDING ORDER] {order_message}") 152 | eml_sock.sendto(order_message.encode(), (REMOTE_IP, EML_UDP_PORT_REMOTE)) 153 | 154 | 155 | # ------------------------------------- 156 | # Main 157 | # ------------------------------------- 158 | 159 | if __name__ == "__main__": 160 | start_autotrader() 161 | --------------------------------------------------------------------------------