├── LICENSE
├── README.md
├── core
├── botCore.py
├── static
│ ├── css
│ │ └── style.css
│ └── js
│ │ ├── charts.js
│ │ └── script.js
├── templates
│ ├── jinja_templates.html
│ ├── main_page.html
│ └── page_template.html
└── trader.py
├── patterns.py
├── requirements.txt
├── run.py
└── trader_configuration.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 John P Lennie
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Simple Binance Trader
2 |
3 | # Disclaimer
4 | I am not responsible for the trades you make with the script, this script has not been exstensivly tested on live trades.
5 |
6 | ## Please check the following:
7 | - Make sure your account uses BNB for the trade fees and that you have plenty of BNB for the trader to use for trades as if not there will be issues with the trader.
8 | - Please clear any cache files when updating the trader as there may be issues if not.
9 | - Please if any updates are also available for binance_api or the technical indicators update those also.
10 |
11 |
12 | NOTE: The current strtergy is also very weak and will mostlikley return only losses therefore I recomend you create your work or use some which would work. However the trader currently also does not support simulated trades and only support live trading, simulated trades should be added in the future so you may need to wait until then to use this to test stratergies.
13 |
14 | NOTE: Trader now supports MARGIN trades allowing the placement of short and long positions. If SPOT trading put your conditions within the "long_entry/long_exit" sections within the trader_configuration.py file.
15 |
16 |
17 | ## Description
18 | This is a simple binance trader that uses the REST api and sockets to allow automation or manipulation of account detail/data. The script which in the default configuration uses a basic MACD trading setup to trade. The script however is very customisable and you'll be able to configure it as you with via the help of the document in the usage section.
19 |
20 | ### Repository Contains:
21 | - run.py : This is used to start/setup the bot.
22 | - trader_configuration.py : Here is where you write your conditions using python logic.
23 | - patterns.py : Can be used to trade based on specific patterns.
24 | - Core
25 | - botCore.py : Is used to manage the socket and trader as well as pull data to be displayed.
26 | - trader.py : The main trader inchage or updating and watching orders.
27 | - static : Folder for static files for the website (js/css).
28 | - templates : Folder for HTML page templates.
29 |
30 | ### Setting File:
31 | - PUBLIC_KEY - Your public binanace api key
32 | - PRIVATE_KEY - Your private binanace api key
33 | - IS_TEST - Allow trader to be run in test mode (True/False)
34 | - MARKET_TYPE - Market type to be traded (SPOT/MARGIN)
35 | - UPDATE_BNB_BALANCE - Automatically update the BNB balance when low (for trading fees, only applicable to real trading)
36 | - TRADER_INTERVAL - Interval used for the trader (1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d).
37 | - TRADING_CURRENCY - The currency max the trader will use (in the quote key) also note this scales up with the number of markets i.e. 2 pairs each market will have 0.0015 as their trading currency pair.
38 | - TRADING_MARKETS - The markets that are being traded and seperate with ',' (BTC-ETH,BTC-NEO)
39 | - HOST_IP - The host IP for the web UI (if left blank default is 127.0.0.1)
40 | - HOST_PORT - The host port for the web UI (if left blank default is 5000)
41 | - MAX_CANDLES - Max candles the trader will use (if left brank default is 500)
42 | - MAX_DEPTH - Max market depth the trader will use (if left brank default is 50)
43 |
44 | ## Usage
45 | I recommend setting this all up within a virtual python enviornment:
46 | First get the base modules:
47 | - To quickly install all the required modules use 'pip3 install -r requirements'.
48 |
49 | Secondly get the required techinal indicators module adn binance api.
50 | - https://github.com/EasyAI/binance_api, This is the binance API that the trader uses.
51 | - https://github.com/EasyAI/Python-Charting-Indicators, This contains the logic to calculate technical indicators. (only the file technical_indicators.py is needed)
52 |
53 | Move them into the site-packages folder. NOTE: If you get an error saying that either the technical_indicators or binance_api is not found you can move them in to the same directory as the run.py file for the trader.
54 |
55 | Finally navigate to the trader directory.
56 |
57 | To set up the bot and for any further detail please refer to the google doc link below:
58 | https://docs.google.com/document/d/1VUx_1O5kQQxk0HfqqA8WyQpk6EbbnXcezAdqXkOMklo/edit?usp=sharing
59 |
60 | ### Contact
61 | Please if you find any bugs or issues contact me so I can improve.
62 | EMAIL: jlennie1996@gmail.com
63 |
--------------------------------------------------------------------------------
/core/botCore.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python3
2 | import os
3 | import sys
4 | import time
5 | import json
6 | import os.path
7 | import hashlib
8 | import logging
9 | import threading
10 | from decimal import Decimal
11 | from flask_socketio import SocketIO
12 | from flask import Flask, render_template, url_for, request
13 |
14 | from binance_api import api_master_rest_caller
15 | from binance_api import api_master_socket_caller
16 |
17 | from . import trader
18 |
19 |
20 | MULTI_DEPTH_INDICATORS = ['ema', 'sma', 'rma', 'order']
21 |
22 | # Initilize globals.
23 |
24 | ## Setup flask app/socket
25 | APP = Flask(__name__)
26 | SOCKET_IO = SocketIO(APP)
27 |
28 | ## Initilize base core object.
29 | core_object = None
30 |
31 | started_updater = False
32 |
33 | ## Initilize IP/port pair globals.
34 | host_ip = ''
35 | host_port = ''
36 |
37 | ## Set traders cache file name.
38 | CAHCE_FILES = 'traders.json'
39 |
40 |
41 | @APP.context_processor
42 | def override_url_for():
43 | return(dict(url_for=dated_url_for))
44 |
45 |
46 | def dated_url_for(endpoint, **values):
47 | # Override to prevent cached assets being used.
48 | if endpoint == 'static':
49 | filename = values.get('filename', None)
50 | if filename:
51 | file_path = os.path.join(APP.root_path,
52 | endpoint,
53 | filename)
54 | values['q'] = int(os.stat(file_path).st_mtime)
55 | return url_for(endpoint, **values)
56 |
57 |
58 | @APP.route('/', methods=['GET'])
59 | def control_panel():
60 | # Base control panel configuration.
61 | global started_updater
62 |
63 | ## Web updater used for live updating.
64 | if not(started_updater):
65 | started_updater = True
66 | web_updater_thread = threading.Thread(target=web_updater)
67 | web_updater_thread.start()
68 |
69 | ## Set socket ip/port.
70 | start_up_data = {
71 | 'host':{'IP': host_ip, 'Port': host_port},
72 | 'market_symbols': core_object.trading_markets
73 | }
74 |
75 | return(render_template('main_page.html', data=start_up_data))
76 |
77 |
78 | @APP.route('/rest-api/v1/trader_update', methods=['POST'])
79 | def update_trader():
80 | # Base API for managing trader interaction.
81 | data = request.get_json()
82 |
83 | ## Check if specified bot exists.
84 | current_trader = api_error_check(data)
85 |
86 | if current_trader == None:
87 | ## No trader therefore return false.
88 | return(json.dumps({'call':False, 'message':'INVALID_TRADER'}))
89 | elif data['action'] == 'start':
90 | ## Updating trader status to running.
91 | if current_trader.state_data['runtime_state'] == 'FORCE_PAUSE':
92 | current_trader.state_data['runtime_state'] = 'RUN'
93 | elif data['action'] == 'pause':
94 | ## Updating trader status to paused.
95 | if current_trader.state_data['runtime_state'] == 'RUN':
96 | current_trader.state_data['runtime_state'] = 'FORCE_PAUSE'
97 | else:
98 | ## If action was not found return false
99 | return(json.dumps({'call':False, 'message':'INVALID_ACTION'}))
100 |
101 | return(json.dumps({'call':True}))
102 |
103 |
104 | @APP.route('/rest-api/v1/get_trader_charting', methods=['GET'])
105 | def get_trader_charting():
106 | # Endpoint to pass trader indicator data.
107 | market = request.args.get('market')
108 | limit = int(request.args.get('limit'))
109 | data = {'market':market}
110 |
111 | ## Check if specified bot exists.
112 | current_trader = api_error_check(data)
113 |
114 | if current_trader == None:
115 | ## No trader therefore return false.
116 | return(json.dumps({'call':False, 'message':'INVALID_TRADER'}))
117 |
118 | candle_data = core_object.get_trader_candles(current_trader.print_pair)[:limit]
119 | indicator_data = core_object.get_trader_indicators(current_trader.print_pair)
120 | short_indicator_data = shorten_indicators(indicator_data, candle_data[-1][0])
121 |
122 | return(json.dumps({'call':True, 'data':{'market':market, 'indicators':short_indicator_data, 'candles':candle_data}}))
123 |
124 |
125 | @APP.route('/rest-api/v1/get_trader_indicators', methods=['GET'])
126 | def get_trader_indicators():
127 | # Endpoint to pass trader indicator data.
128 | market = request.args.get('market')
129 | limit = int(request.args.get('limit'))
130 | data = {'market':market}
131 |
132 | ## Check if specified bot exists.
133 | current_trader = api_error_check(data)
134 |
135 | if current_trader == None:
136 | ## No trader therefore return false.
137 | return(json.dumps({'call':False, 'message':'INVALID_TRADER'}))
138 |
139 | indicator_data = core_object.get_trader_indicators(current_trader.print_pair)
140 |
141 | return(json.dumps({'call':True, 'data':{'market':market, 'indicators':indicator_data}}))
142 |
143 |
144 | @APP.route('/rest-api/v1/get_trader_candles', methods=['GET'])
145 | def get_trader_candles():
146 | # Endpoint to pass trader candles.
147 | market = request.args.get('market')
148 | limit = int(request.args.get('limit'))
149 | data = {'market':market}
150 |
151 | ## Check if specified bot exists.
152 | current_trader = api_error_check(data)
153 |
154 | if current_trader == None:
155 | ## No trader therefore return false.
156 | return(json.dumps({'call':False, 'message':'INVALID_TRADER'}))
157 |
158 | candle_data = core_object.get_trader_candles(current_trader.print_pair)[:limit]
159 |
160 | return(json.dumps({'call':True, 'data':{'market':market, 'candles':candle_data}}))
161 |
162 |
163 | @APP.route('/rest-api/v1/test', methods=['GET'])
164 | def test_rest_call():
165 | # API endpoint test
166 | return(json.dumps({'call':True, 'message':'HELLO WORLD!'}))
167 |
168 |
169 | def shorten_indicators(indicators, end_time):
170 | base_indicators = {}
171 |
172 | for ind in indicators:
173 | if ind in MULTI_DEPTH_INDICATORS:
174 | base_indicators.update({ind:{}})
175 | for sub_ind in indicators[ind]:
176 | base_indicators[ind].update({sub_ind:[ [val[0] if ind != 'order' else val[0]*1000,val[1]] for val in indicators[ind][sub_ind] if (val[0] if ind != 'order' else val[0]*1000) > end_time ]})
177 | else:
178 | base_indicators.update({ind:[ [val[0],val[1]] for val in indicators[ind] if val[0] > end_time]})
179 |
180 | return(base_indicators)
181 |
182 |
183 | def api_error_check(data):
184 | ## Check if specified bot exists.
185 | current_trader = None
186 | for trader in core_object.trader_objects:
187 | if trader.print_pair == data['market']:
188 | current_trader = trader
189 | break
190 | return(current_trader)
191 |
192 |
193 | def web_updater():
194 | # Web updater use to update live via socket.
195 | lastHash = None
196 |
197 | while True:
198 | if core_object.coreState == 'RUN':
199 | ## Get trader data and hash it to find out if there have been any changes.
200 | traderData = core_object.get_trader_data()
201 | currHash = hashlib.md5(str(traderData).encode())
202 |
203 | if lastHash != currHash:
204 | ## Update any new changes via socket.
205 | lastHash = currHash
206 | total_bulk_data = []
207 | for trader in traderData:
208 | bulk_data = {}
209 | bulk_data.update({'market':trader['market']})
210 | bulk_data.update({'trade_recorder':trader['trade_recorder']})
211 | bulk_data.update({'wallet_pair':trader['wallet_pair']})
212 |
213 | bulk_data.update(trader['custom_conditions'])
214 | bulk_data.update(trader['market_activity'])
215 | bulk_data.update(trader['market_prices'])
216 | bulk_data.update(trader['state_data'])
217 | total_bulk_data.append(bulk_data)
218 |
219 | SOCKET_IO.emit('current_traders_data', {'data':total_bulk_data})
220 | time.sleep(.8)
221 |
222 |
223 | class BotCore():
224 |
225 | def __init__(self, settings, logs_dir, cache_dir):
226 | # Initilization for the bot core managment object.
227 | logging.info('[BotCore] Initilizing the BotCore object.')
228 |
229 | ## Setup binance REST and socket API.
230 | self.rest_api = api_master_rest_caller.Binance_REST(settings['public_key'], settings['private_key'])
231 | self.socket_api = api_master_socket_caller.Binance_SOCK()
232 |
233 | ## Setup the logs/cache dir locations.
234 | self.logs_dir = logs_dir
235 | self.cache_dir = cache_dir
236 |
237 | ## Setup run type, market type, and update bnb balance.
238 | self.run_type = settings['run_type']
239 | self.market_type = settings['market_type']
240 | self.update_bnb_balance = settings['update_bnb_balance']
241 |
242 | ## Setup max candle/depth setting.
243 | self.max_candles = settings['max_candles']
244 | self.max_depth = settings['max_depth']
245 |
246 | ## Get base quote pair (This prevents multiple different pairs from conflicting.)
247 | pair_one = settings['trading_markets'][0]
248 |
249 | self.quote_asset = pair_one[:pair_one.index('-')]
250 | self.base_currency = settings['trading_currency']
251 | self.candle_Interval = settings['trader_interval']
252 |
253 | ## Initilize base trader settings.
254 | self.trader_objects = []
255 | self.trading_markets = settings['trading_markets']
256 |
257 | ## Initilize core state
258 | self.coreState = 'READY'
259 |
260 |
261 | def start(self):
262 | # Start the core object.
263 | logging.info('[BotCore] Starting the BotCore object.')
264 | self.coreState = 'SETUP'
265 |
266 | ## check markets
267 | found_markets = []
268 | not_supported = []
269 |
270 | for market in self.rest_api.get_exchangeInfo()['symbols']:
271 | fmtMarket = '{0}-{1}'.format(market['quoteAsset'], market['baseAsset'])
272 |
273 | # If the current market is not in the trading markets list then skip.
274 | if not fmtMarket in self.trading_markets:
275 | continue
276 |
277 | found_markets.append(fmtMarket)
278 |
279 | if (self.market_type == 'MARGIN' and market['isMarginTradingAllowed'] == False) or (self.market_type == 'SPOT' and market['isSpotTradingAllowed'] == False):
280 | not_supported.append(fmtMarket)
281 | continue
282 |
283 | # This is used to setup min quantity.
284 | if float(market['filters'][2]['minQty']) < 1.0:
285 | minQuantBase = (Decimal(market['filters'][2]['minQty'])).as_tuple()
286 | lS = abs(int(len(minQuantBase.digits)+minQuantBase.exponent))+1
287 | else: lS = 0
288 |
289 | # This is used to set up the price precision for the market.
290 | tickSizeBase = (Decimal(market['filters'][0]['tickSize'])).as_tuple()
291 | tS = abs(int(len(tickSizeBase.digits)+tickSizeBase.exponent))+1
292 |
293 | # This is used to get the markets minimal notation.
294 | mN = float(market['filters'][3]['minNotional'])
295 |
296 | # Put all rules into a json object to pass to the trader.
297 | market_rules = {'LOT_SIZE':lS, 'TICK_SIZE':tS, 'MINIMUM_NOTATION':mN}
298 |
299 | # Initilize trader objecta dn also set-up its inital required data.
300 | traderObject = trader.BaseTrader(market['quoteAsset'], market['baseAsset'], self.rest_api, socket_api=self.socket_api)
301 | traderObject.setup_initial_values(self.market_type, self.run_type, market_rules)
302 | self.trader_objects.append(traderObject)
303 |
304 |
305 | ## Show markets that dont exist on the binance exchange.
306 | if len(self.trading_markets) != len(found_markets):
307 | no_market_text = ''
308 | for market in [market for market in self.trading_markets if market not in found_markets]:
309 | no_market_text+=str(market)+', '
310 | logging.warning('Following pairs dont exist: {}'.format(no_market_text[:-2]))
311 |
312 | ## Show markets that dont support the market type.
313 | if len(not_supported) > 0:
314 | not_support_text = ''
315 | for market in not_supported:
316 | not_support_text += ' '+str(market)
317 | logging.warning('[BotCore] Following market pairs are not supported for {}: {}'.format(self.market_type, not_support_text))
318 |
319 | valid_tading_markets = [market for market in found_markets if market not in not_supported]
320 |
321 | ## setup the binance socket.
322 | for market in valid_tading_markets:
323 | self.socket_api.set_candle_stream(symbol=market, interval=self.candle_Interval)
324 | self.socket_api.set_manual_depth_stream(symbol=market, update_speed='1000ms')
325 |
326 | if self.run_type == 'REAL':
327 | self.socket_api.set_userDataStream(self.rest_api, self.market_type)
328 |
329 | self.socket_api.BASE_CANDLE_LIMIT = self.max_candles
330 | self.socket_api.BASE_DEPTH_LIMIT = self.max_depth
331 |
332 | self.socket_api.build_query()
333 | self.socket_api.set_live_and_historic_combo(self.rest_api)
334 |
335 | self.socket_api.start()
336 |
337 | # Load the wallets.
338 | if self.run_type == 'REAL':
339 | user_info = self.rest_api.get_account(self.market_type)
340 | if self.market_type == 'SPOT':
341 | wallet_balances = user_info['balances']
342 | elif self.market_type == 'MARGIN':
343 | wallet_balances = user_info['userAssets']
344 | current_tokens = {}
345 |
346 | for balance in wallet_balances:
347 | total_balance = (float(balance['free']) + float(balance['locked']))
348 | if total_balance > 0:
349 | current_tokens.update({balance['asset']:[
350 | float(balance['free']),
351 | float(balance['locked'])]})
352 | else:
353 | current_tokens = {self.quote_asset:[float(self.base_currency), 0.0]}
354 |
355 | # Load cached data
356 | cached_traders_data = None
357 | if os.path.exists(self.cache_dir+CAHCE_FILES):
358 | with open(self.cache_dir+CAHCE_FILES, 'r') as f:
359 | cached_traders_data = json.load(f)['data']
360 |
361 | ## Setup the trader objects and start them.
362 | logging.info('[BotCore] Starting the trader objects.')
363 | for trader_ in self.trader_objects:
364 | currSymbol = "{0}{1}".format(trader_.base_asset, trader_.quote_asset)
365 |
366 | # Update trader with cached data (to resume trades/keep records of trades.)
367 | if cached_traders_data != '' and cached_traders_data:
368 | for cached_trader in cached_traders_data:
369 | m_split = cached_trader['market'].split('-')
370 | if (m_split[1]+m_split[0]) == currSymbol:
371 | trader_.configuration = cached_trader['configuration']
372 | trader_.custom_conditional_data = cached_trader['custom_conditions']
373 | trader_.market_activity = cached_trader['market_activity']
374 | trader_.trade_recorder = cached_trader['trade_recorder']
375 | trader_.state_data = cached_trader['state_data']
376 |
377 | wallet_pair = {}
378 |
379 | if trader_.quote_asset in current_tokens:
380 | wallet_pair.update({trader_.quote_asset:current_tokens[trader_.quote_asset]})
381 |
382 | if trader_.base_asset in current_tokens:
383 | wallet_pair.update({trader_.base_asset:current_tokens[trader_.base_asset]})
384 |
385 | trader_.start(self.base_currency, wallet_pair)
386 |
387 | logging.debug('[BotCore] Starting trader manager')
388 | TM_thread = threading.Thread(target=self._trader_manager)
389 | TM_thread.start()
390 |
391 | if self.update_bnb_balance:
392 | logging.debug('[BotCore] Starting BNB manager')
393 | BNB_thread = threading.Thread(target=self._bnb_manager)
394 | BNB_thread.start()
395 |
396 | logging.debug('[BotCore] Starting connection manager thread.')
397 | CM_thread = threading.Thread(target=self._connection_manager)
398 | CM_thread.start()
399 |
400 | logging.debug('[BotCore] Starting file manager thread.')
401 | FM_thread = threading.Thread(target=self._file_manager)
402 | FM_thread.start()
403 |
404 | logging.info('[BotCore] BotCore successfully started.')
405 | self.coreState = 'RUN'
406 |
407 |
408 | def _trader_manager(self):
409 | ''' '''
410 | while self.coreState != 'STOP':
411 | pass
412 |
413 |
414 | def _bnb_manager(self):
415 | ''' This will manage BNB balance and update if there is low BNB in account. '''
416 | last_wallet_update_time = 0
417 |
418 | while self.coreState != 'STOP':
419 | socket_buffer_global = self.socket_api.socketBuffer
420 |
421 | # If outbound postion is seen then wallet has updated.
422 | if 'outboundAccountPosition' in socket_buffer_global:
423 | if last_wallet_update_time != socket_buffer_global['outboundAccountPosition']['E']:
424 | last_wallet_update_time = socket_buffer_global['outboundAccountPosition']['E']
425 |
426 | for wallet in socket_buffer_global['outboundAccountPosition']['B']:
427 | if wallet['a'] == 'BNB':
428 | if float(wallet['f']) < 0.01:
429 | bnb_order = self.rest_api.place_order(self.market_type, symbol='BNBBTC', side='BUY', type='MARKET', quantity=0.1)
430 | time.sleep(2)
431 |
432 |
433 | def _file_manager(self):
434 | ''' This section is responsible for activly updating the traders cache files. '''
435 | while self.coreState != 'STOP':
436 | time.sleep(15)
437 |
438 | traders_data = self.get_trader_data()
439 | if os.path.exists(self.cache_dir):
440 | file_path = '{0}{1}'.format(self.cache_dir,CAHCE_FILES)
441 | with open(file_path, 'w') as f:
442 | json.dump({'lastUpdateTime':time.time() ,'data':traders_data}, f)
443 |
444 |
445 | def _connection_manager(self):
446 | ''' This section is responsible for re-testing connectiongs in the event of a disconnect. '''
447 | update_time = 0
448 | retryCounter = 1
449 | time.sleep(20)
450 |
451 | while self.coreState != 'STOP':
452 | time.sleep(1)
453 | if self.coreState != 'RUN':
454 | continue
455 | if self.socket_api.last_data_recv_time != update_time:
456 | update_time = self.socket_api.last_data_recv_time
457 | else:
458 | if (update_time + (15*retryCounter)) < time.time():
459 | retryCounter += 1
460 |
461 | try:
462 | print(self.rest_api.test_ping())
463 | except Exception as e:
464 | logging.warning('[BotCore] Connection issue: {0}.'.format(e))
465 | continue
466 |
467 | logging.info('[BotCore] Connection issue resolved.')
468 | if not(self.socket_api.socketRunning):
469 | logging.info('[BotCore] Attempting socket restart.')
470 | self.socket_api.start()
471 |
472 |
473 | def get_trader_data(self):
474 | ''' This can be called to return data for each of the active traders. '''
475 | rData = [ _trader.get_trader_data() for _trader in self.trader_objects ]
476 | return(rData)
477 |
478 |
479 | def get_trader_indicators(self, market):
480 | ''' This can be called to return the indicators that are used by the traders (Will be used to display web UI activity.) '''
481 | for _trader in self.trader_objects:
482 | if _trader.print_pair == market:
483 | indicator_data = _trader.indicators
484 | indicator_data.update({'order':{'buy':[], 'sell':[]}})
485 | indicator_data['order']['buy'] = [ [order[0],order[1]] for order in _trader.trade_recorder if order[4] == 'BUY']
486 | indicator_data['order']['sell'] = [ [order[0],order[1]] for order in _trader.trade_recorder if order[4] == 'SELL']
487 | return(indicator_data)
488 |
489 |
490 | def get_trader_candles(self, market):
491 | ''' This can be called to return the candle data for the traders (Will be used to display web UI activity.) '''
492 | for _trader in self.trader_objects:
493 | if _trader.print_pair == market:
494 | sock_symbol = str(_trader.base_asset)+str(_trader.quote_asset)
495 | return(self.socket_api.get_live_candles(sock_symbol))
496 |
497 |
498 | def start(settings, logs_dir, cache_dir):
499 | global core_object, host_ip, host_port
500 |
501 | if core_object == None:
502 | core_object = BotCore(settings, logs_dir, cache_dir)
503 | core_object.start()
504 |
505 | logging.info('[BotCore] Starting traders in {0} mode, market type is {1}.'.format(settings['run_type'], settings['market_type']))
506 | log = logging.getLogger('werkzeug')
507 | log.setLevel(logging.ERROR)
508 |
509 | host_ip = settings['host_ip']
510 | host_port = settings['host_port']
511 |
512 | SOCKET_IO.run(APP,
513 | host=settings['host_ip'],
514 | port=settings['host_port'],
515 | debug=True,
516 | use_reloader=False)
517 |
--------------------------------------------------------------------------------
/core/static/css/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Josefin+Sans&display=swap');
2 |
3 | *{
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | list-style: none;
8 | text-decoration: none;
9 | font-family: arial;
10 | }
11 |
12 | body{
13 | background-color: #f3f5f9;
14 | display: flex;
15 | position: relative;
16 | }
17 |
18 | .sidebar{
19 | width: 200px;
20 | height: 100%;
21 | background: #4b4276;
22 | padding: 30px 0px;
23 | position: fixed;
24 | color: #bdb8d7;
25 | }
26 |
27 | .sidebar ul li{
28 | padding: 15px;
29 | }
30 |
31 | .sidebar ul li a{
32 | display: block;
33 | color: #bdb8d7;
34 | }
35 |
36 | .sidebar ul li:hover{
37 | background-color: #594f8d;
38 | }
39 |
40 | .sidebar ul li:hover a{
41 | color: #fff;
42 | }
43 |
44 | main {
45 | width: 100%;
46 | margin-left: 200px;
47 | background-color: #F5F5F0;
48 | padding-left: 20px;
49 | padding-top: 10px;
50 | }
51 |
52 | .trader-panel {
53 | display: none;
54 | }
55 |
56 | .small-button {
57 | padding: 5px;
58 | font-size: 12px;
59 | display: block;
60 | display: inline-block;
61 | border: none;
62 | border-radius: 4px;
63 | cursor: pointer;
64 | color: white;
65 | }
66 |
67 | .green-button {
68 | background-color: #00802b;
69 | }
70 |
71 | .green-button:hover {
72 | background-color: #33ff77;
73 | }
74 |
75 | .amber-button {
76 | background-color: #e68a00
77 | }
78 |
79 | .amber-button:hover {
80 | background-color: #ffa31a;
81 | }
--------------------------------------------------------------------------------
/core/static/js/charts.js:
--------------------------------------------------------------------------------
1 | // Indicator meta details
2 | const indicator_chart_types_mapping = {'patterns_data_lines':'line', 'patterns_data_points':'scatter', 'tops_bottoms':'scatter', 'data_lines':'line', 'cps':'line', 'ichi':'line', 'boll':'line', 'adx':'line', 'stock':'line', 'order':'scatter', 'ema':'line', 'sma':'line', 'rma':'line', 'rsi':'line', 'mfi':'line', 'cci':'line', 'zerolagmacd':'macd', 'macd':'macd'}
3 | const indicator_home_type_mapping = {'patterns_data_lines':'MAIN', 'patterns_data_points':'MAIN', 'tops_bottoms':'MAIN', 'data_lines':'MAIN', 'ichi':'MAIN', 'cps':'MAIN', 'boll':'MAIN', 'adx':'OWN', 'stock':'OWN', 'order':'MAIN', 'ema':'MAIN', 'sma':'MAIN', 'rma':'MAIN', 'rsi':'OWN', 'mfi':'OWN', 'cci':'OWN', 'zerolagmacd':'OWN', 'macd':'OWN'}
4 | const indicator_is_single_mapping = ['patterns_data_lines', 'patterns_data_points', 'tops_bottoms', 'data_lines', 'order', 'ema', 'sma', 'rma', 'rsi', 'mfi', 'cci', 'cps']
5 | const double_depth_indicators = ['ema', 'sma', 'order', 'patterns_data_points', 'patterns_data_lines'];
6 |
7 | // Base Apex chart configuration.
8 | window.Apex = {
9 | chart: {
10 | animations: {
11 | enabled: false
12 | }
13 | },
14 | autoScaleYaxis: false
15 | };
16 |
17 | // Chart template for main chart.
18 | var base_candle_chart_configuration = {
19 | series: [],
20 | chart: {
21 | height: 800,
22 | id: 'main_Chart',
23 | type: 'line'
24 | },
25 | fill: {
26 | type:'solid',
27 | },
28 | markers: {
29 | size: []
30 | },
31 | //colors: [],
32 | stroke: {
33 | width: []
34 | },
35 | tooltip: {
36 | shared: true,
37 | custom: []
38 | },
39 | xaxis: {
40 | type: 'datetime',
41 | },
42 | yaxis: {
43 | labels: {
44 | minWidth: 40,
45 | formatter: function (value) { return Math.round(value); }
46 | },
47 | }
48 | };
49 |
50 | let loaded_candles_chart = null;
51 |
52 |
53 | function initial_build(target_element, charting_data) {
54 | // Add main chandle chart position.
55 | var candle_data = charting_data['candles'];
56 | var indicator_data = charting_data['indicators'];
57 | var main_chart_series = [];
58 |
59 | loaded_candles_chart = JSON.parse(JSON.stringify(base_candle_chart_configuration));
60 |
61 | populate_chart(indicator_data);
62 |
63 | var built_data = build_candle_data(candle_data);
64 | var built_candle_data = built_data[0];
65 |
66 | // Finally add the candle to the displayed chart.
67 | loaded_candles_chart["series"].push({
68 | name: 'candle',
69 | type: 'candlestick',
70 | data: built_candle_data
71 | });
72 | loaded_candles_chart["stroke"]["width"].push(1);
73 | loaded_candles_chart["markers"]["size"].push(0);
74 | loaded_candles_chart["tooltip"]["custom"].push(function({seriesIndex, dataPointIndex, w}) {
75 | var o = w.globals.seriesCandleO[seriesIndex][dataPointIndex]
76 | var h = w.globals.seriesCandleH[seriesIndex][dataPointIndex]
77 | var l = w.globals.seriesCandleL[seriesIndex][dataPointIndex]
78 | var c = w.globals.seriesCandleC[seriesIndex][dataPointIndex]
79 | return (`Open:${o}
5 | Total PL: | Total Trades:
6 |
22 | State: | Trades: | Overall: | Last Update: | Last Price:
High:${h}
Low:${l}
Close:${c}`)
80 | });
81 |
82 | candle_chart = new ApexCharts(target_element, loaded_candles_chart);
83 | candle_chart.render();
84 | }
85 |
86 |
87 | function build_candle_data(candle_data) {
88 | var built_candle_data = [];
89 | var built_volume_data = [];
90 |
91 | for (var i=0; i < candle_data.length; i++) {
92 | var candle = candle_data[i];
93 | built_candle_data.push({
94 | x: new Date(parseInt(candle[0])),
95 | y: [
96 | candle[1],
97 | candle[2],
98 | candle[3],
99 | candle[4]
100 | ]
101 | });
102 |
103 | built_volume_data.push({
104 | x: new Date(parseInt(candle[0])),
105 | y: Math.round(candle[5])
106 | });
107 | }
108 | return([built_candle_data, built_volume_data]);
109 | }
110 |
111 |
112 | function build_timeseries(ind_obj) {
113 | var indicator_lines = [];
114 | var keys = []
115 |
116 | // Use sorted timestamp to print out.
117 | for (var ind in ind_obj) {
118 | var current_set = ind_obj[ind]
119 |
120 | if (typeof current_set[0] == 'number') {
121 | indicator_lines.push({
122 | x: new Date(parseInt(current_set[0])),
123 | y: current_set[1].toFixed(8)
124 | });
125 | } else {
126 | for (var sub_ind in current_set) {
127 | if (!keys.includes(sub_ind)) {
128 | keys.push(sub_ind)
129 | indicator_lines[sub_ind] = []
130 | }
131 | indicator_lines[sub_ind].push({
132 | x: new Date(parseInt(current_set[sub_ind][0])),
133 | y: current_set[sub_ind][1].toFixed(8)
134 | });
135 | }
136 | }
137 | }
138 | return(indicator_lines);
139 | }
140 |
141 |
142 | function build_basic_indicator(chart_obj, ind_obj, chart_type, line_name=null, ind_name=null) {
143 | var indicator_lines = build_timeseries(ind_obj);
144 |
145 | if (!(line_name == null)) {
146 | chart_obj["series"].push({
147 | name: line_name,
148 | type: chart_type,
149 | data: indicator_lines
150 | });
151 | if (chart_type == "scatter") {
152 | chart_obj["stroke"]["width"].push(2);
153 | chart_obj["markers"]["size"].push(8);
154 | } else {
155 | chart_obj["stroke"]["width"].push(2);
156 | chart_obj["markers"]["size"].push(0);
157 | }
158 | } else {
159 | for (var sub_ind_name in indicator_lines) {
160 | chart_obj["series"].push({
161 | name: sub_ind_name,
162 | type: chart_type,
163 | data: indicator_lines[sub_ind_name]});
164 | if (chart_type == "scatter") {
165 | chart_obj["stroke"]["width"].push(2);
166 | chart_obj["markers"]["size"].push(8);
167 | } else {
168 | chart_obj["stroke"]["width"].push(2);
169 | chart_obj["markers"]["size"].push(0);
170 | }
171 | }
172 | }
173 |
174 | if ('custom' in chart_obj["tooltip"]) {
175 | if (!(line_name == null)) {
176 | chart_obj["tooltip"]["custom"].push(
177 | function({seriesIndex, dataPointIndex, w}) {
178 | return w.globals.series[seriesIndex][dataPointIndex]
179 | });
180 | } else {
181 | for (var ind in indicator_lines) {
182 | chart_obj["tooltip"]["custom"].push(
183 | function({seriesIndex, dataPointIndex, w}) {
184 | return w.globals.series[seriesIndex][dataPointIndex]
185 | });
186 | }
187 | }
188 | }
189 | }
190 |
191 |
192 | function populate_chart(indicator_data) {
193 |
194 | for (var raw_indicator in indicator_data) {
195 |
196 | var patt = /[^\d]+/i;
197 | var indicator = raw_indicator.match(patt)[0];
198 |
199 | var current_ind = indicator_data[raw_indicator];
200 | var chart_type = indicator_chart_types_mapping[indicator];
201 | var home_chart = indicator_home_type_mapping[indicator];
202 | var line_name = null;
203 | var ind_name = null;
204 |
205 | if (home_chart == "MAIN") {
206 | target_chart = loaded_candles_chart;
207 |
208 | if (double_depth_indicators.includes(indicator)) {
209 | for (var sub_ind in current_ind) {
210 | line_name = sub_ind;
211 | var built_chart = build_basic_indicator(target_chart, current_ind[sub_ind], chart_type, line_name, ind_name);
212 | }
213 | } else {
214 | if (indicator_is_single_mapping.includes(indicator)) {
215 | line_name = raw_indicator;
216 | }
217 | var built_chart = build_basic_indicator(target_chart, current_ind, chart_type, line_name, ind_name);
218 | }
219 | }
220 | }
221 | }
--------------------------------------------------------------------------------
/core/static/js/script.js:
--------------------------------------------------------------------------------
1 | //
2 | //
3 | const socket = io('http://'+ip+':'+port);
4 |
5 | const class_data_mapping = {
6 | 'trader-state':'runtime_state',
7 | 'trader-lastupdate':'last_update_time',
8 | 'trader-lastprice':'lastPrice',
9 | 'trader-markettype':'order_market_type',
10 | 'trader-orderside':'order_side',
11 | 'trader-ordertype':'order_type',
12 | 'trader-orderstatus':'order_status',
13 | 'trader-buyprice':'price',
14 | 'trader-sellprice':'price',
15 | 'trader-orderpoint':'order_point'
16 | };
17 |
18 | var current_chart = '';
19 | var update_chart = false;
20 |
21 |
22 | $(document).ready(function() {
23 | socket.on('current_traders_data', function(data) {
24 | update_trader_results(data);
25 | });
26 | });
27 |
28 |
29 | function update_trader_results(data) {
30 | //
31 | var currentTraders = data['data'];
32 |
33 | var overall_total_trades = 0;
34 | var overall_total_pl = 0;
35 |
36 | for (x = 0; x < (currentTraders.length); x++){
37 | var current = currentTraders[x];
38 | var trade_recorder = current['trade_recorder'];
39 | var update_targets = [`trader_${current['market']}`, `overview_${current['market']}`]
40 |
41 | for (i = 0; i < (update_targets.length); i++){
42 | var trader_panel = document.getElementById(update_targets[i]);
43 | var target_el = null;
44 |
45 | for (var key in class_data_mapping) {
46 | target_el = trader_panel.getElementsByClassName(key);
47 | if (target_el.length != 0) {
48 | if (current['order_side'] == 'SELL' && key == 'trader-buyprice') {
49 | show_val = trade_recorder[trade_recorder.length-1][1];
50 | } else {
51 | show_val = current[class_data_mapping[key]];
52 | }
53 |
54 | if (show_val === null) {
55 | show_val = 'Null';
56 | }
57 | target_el[0].innerText = show_val;
58 | }
59 | }
60 |
61 | target_el = trader_panel.getElementsByClassName('show-sellaction');
62 | if (target_el.length != 0) {
63 | if (current['order_side'] == 'SELL') {
64 | target_el[0].style.display = 'block';
65 | } else {
66 | target_el[0].style.display = 'none';
67 | }
68 | }
69 |
70 | var outcome = 0;
71 | var total_trades = 0;
72 | if (trade_recorder.length >= 2) {
73 | range = trade_recorder.length/2
74 | for (y = 0; y < (range); y++) {
75 | buy_order = trade_recorder[(range*2)-2]
76 | sell_order = trade_recorder[range*2-1]
77 |
78 | buy_value = buy_order[1]*buy_order[2]
79 | sell_value = sell_order[1]*buy_order[2]
80 |
81 | if (buy_order[3].includes("SHORT")) {
82 | outcome += buy_value-sell_value;
83 | } else {
84 | outcome += sell_value-buy_value;
85 | }
86 | total_trades += 1
87 | }
88 | }
89 |
90 | var r_outcome = Math.round(outcome*100000000)/100000000;
91 |
92 | target_el = trader_panel.getElementsByClassName('trader-trades')[0];
93 | target_el.innerText = total_trades;
94 | target_el = trader_panel.getElementsByClassName('trader-overall')[0];
95 | target_el.innerText = r_outcome;
96 |
97 | if (update_targets[i] != `overview_${current['market']}`) {
98 | overall_total_trades += total_trades;
99 | overall_total_pl += r_outcome;
100 | }
101 |
102 | if ((current_chart == update_targets[i]) && (update_chart == true) && current_chart != 'trader_Overview') {
103 | console.log(currentTraders);
104 | update_chart = false;
105 | target_el = trader_panel.getElementsByClassName('trader_charts')[0];
106 | build_chart(current['market'], target_el);
107 | }
108 | }
109 | }
110 | var overview_section = document.getElementById('trader_Overview');
111 |
112 | target_el = overview_section.getElementsByClassName('overview-totalpl')[0];
113 | target_el.innerText = overall_total_pl;
114 | target_el = overview_section.getElementsByClassName('overview-totaltrades')[0];
115 | target_el.innerText = overall_total_trades;
116 | }
117 |
118 |
119 | function hide_section(e, section_id){
120 |
121 | var section_el = document.getElementsByTagName('section');
122 | for (i = 0; i < (section_el.length); i++){
123 | if (section_el[i].id == `trader_${section_id}`) {
124 | current_chart = `trader_${section_id}`;
125 | update_chart = true;
126 | section_el[i].style.display = "block";
127 | } else {
128 | section_el[i].style.display = "none";
129 | }
130 | }
131 | }
132 |
133 |
134 | function start_trader(e, market_pair){
135 | e.preventDefault();
136 | rest_api('POST', 'trader_update', {'action':'start', 'market':market_pair});
137 | }
138 |
139 |
140 | function pause_trader(e, market_pair){
141 | e.preventDefault();
142 | rest_api('POST', 'trader_update', {'action':'pause', 'market':market_pair});
143 | }
144 |
145 |
146 | function build_chart(market_pair, element){
147 | rest_api('GET', `get_trader_charting?market=${market_pair}&limit=200`, null, initial_build, element);
148 | }
149 |
150 |
151 | function rest_api(method, endpoint, data=null, target_function=null, target_element=null){
152 | // if either the user has requested a force update on bot data or the user has added a new market to trade then send an update to the backend.
153 | console.log(`'M: ${method}, ULR: /rest-api/v1/${endpoint}, D:${data}`);
154 | let request = new XMLHttpRequest();
155 | request.open(method, '/rest-api/v1/'+endpoint, true);
156 |
157 | request.onload = function() {
158 | if (this.status == 200){
159 | var resp_data = JSON.parse(request.responseText);
160 | console.log(resp_data);
161 | if (target_function != null && target_element == null) {
162 | target_function(resp_data);
163 | } else if (target_function != null && target_element != null) {
164 | target_function(target_element, resp_data['data']);
165 | }
166 | } else {
167 | console.log(`error ${request.status} ${request.statusText}`);
168 | }
169 | }
170 |
171 | if (data == null){
172 | request.send();
173 | } else {
174 | request.setRequestHeader('content-type', 'application/json');
175 | request.send(JSON.stringify(data));
176 | }
177 | }
--------------------------------------------------------------------------------
/core/templates/jinja_templates.html:
--------------------------------------------------------------------------------
1 | {% macro overview_data_panel(market_symbols) %}
2 | Trader Overview
4 |
10 | {{ market_symbol }}
19 | Start
20 | Pause
21 |
23 | | Type: | Status: | Buy Price: | Sell Price: | OP:
24 |