· 5 years ago · Nov 21, 2020, 01:54 PM
1# This code is for sample purposes only, comes as is and with no warranty or guarantee of performance
2
3pair = 'BTC/USDT'
4binApi = "7b213be861b47912231576cfcca7c19164cb650d1a2a82787654f8f6186616ac"
5binSecret = "6a8e92b373268c108168db8466b353e75856886b14d16dadcb455b159400db8f"
6from collections import OrderedDict
7from datetime import datetime
8from os.path import getmtime
9from time import sleep
10from utils import ( get_logger, lag, print_dict, print_dict_of_dicts, sort_by_key,
11 ticksize_ceil, ticksize_floor, ticksize_round )
12import json
13import random, string
14import copy as cp
15import argparse, logging, math, os, pathlib, sys, time, traceback
16import ccxt
17try:
18 from deribit_api import RestClient
19except ImportError:
20 print("Please install the deribit_api pacakge", file=sys.stderr)
21 print(" pip3 install deribit_api", file=sys.stderr)
22 exit(1)
23
24# Add command line switches
25parser = argparse.ArgumentParser( description = 'Bot' )
26
27# Use production platform/account
28parser.add_argument( '-p',
29 dest = 'use_prod',
30 action = 'store_true' )
31
32# Do not display regular status updates to terminal
33parser.add_argument( '--no-output',
34 dest = 'output',
35 action = 'store_false' )
36
37# Monitor account only, do not send trades
38parser.add_argument( '-m',
39 dest = 'monitor',
40 action = 'store_true' )
41
42# Do not restart bot on errors
43parser.add_argument( '--no-restart',
44 dest = 'restart',
45 action = 'store_false' )
46
47args = parser.parse_args()
48
49if not args.use_prod:
50 KEY = ''
51 SECRET = ''
52 URL = 'https://test.deribit.com'
53else:
54 KEY = ''
55 SECRET = ''
56 URL = 'https://www.deribit.com'
57
58BP = 1e-4 # one basis point
59BTC_SYMBOL = 'btc'
60CONTRACT_SIZE = 10 # USD
61COV_RETURN_CAP = 100 # cap on variance for vol estimate
62DECAY_POS_LIM = 0.1 # position lim decay factor toward expiry
63EWMA_WGT_COV = 4 # parameter in % points for EWMA volatility estimate
64EWMA_WGT_LOOPTIME = 0.1 # parameter for EWMA looptime estimate
65FORECAST_RETURN_CAP = 20 # cap on returns for vol estimate
66LOG_LEVEL = logging.INFO
67MIN_ORDER_SIZE = 75
68MAX_LAYERS = 5 # max orders to layer the ob with on each side
69MKT_IMPACT = 0.01 # base 1-sided spread between bid/offer
70NLAGS = 2 # number of lags in time series
71PCT = 100 * BP # one percentage point
72PCT_LIM_LONG = 200 # % position limit long
73PCT_LIM_SHORT = 100 # % position limit short
74PCT_QTY_BASE = 0.05 # pct order qty in bps as pct of acct on each order
75MIN_LOOP_TIME = 14.6 # Minimum time between loops
76RISK_CHARGE_VOL = 1.5 # vol risk charge in bps per 100 vol
77SECONDS_IN_DAY = 3600 * 24
78SECONDS_IN_YEAR = 365 * SECONDS_IN_DAY
79WAVELEN_MTIME_CHK = 15 # time in seconds between check for file change
80WAVELEN_OUT = 15 # time in seconds between output to terminal
81WAVELEN_TS = 15 # time in seconds between time series update
82VOL_PRIOR = 100 # vol estimation starting level in percentage pts
83
84
85
86
87 #DECAY_POS_LIM = data['RISK_CHARGE_VOL']['current']
88
89EWMA_WGT_COV *= PCT
90MKT_IMPACT *= BP
91PCT_LIM_LONG *= PCT
92PCT_LIM_SHORT *= PCT
93PCT_QTY_BASE *= BP
94VOL_PRIOR *= PCT
95
96
97class MarketMaker( object ):
98
99 def __init__( self, monitor = True, output = True ):
100 self.equity_usd = None
101 self.equity_btc = None
102 self.equity_usd_init = None
103 self.equity_btc_init = None
104 self.con_size = float( CONTRACT_SIZE )
105 self.client = None
106 self.client2 = None
107 self.deltas = OrderedDict()
108 self.futures = OrderedDict()
109 self.futures_prv = OrderedDict()
110 self.logger = None
111 self.mean_looptime = 1
112 self.monitor = monitor
113 self.output = output or monitor
114 self.positions = OrderedDict()
115 self.spread_data = None
116 self.this_mtime = None
117 self.ts = None
118 self.vols = OrderedDict()
119
120 def create_client( self ):
121 #self.client = RestClient( KEY, SECRET, URL )
122 #print(binApi)
123 binance_futures = ccxt.binance(
124 {"apiKey": binApi,
125 "secret": binSecret,
126 "options":{"defaultMarket":"futures"},
127 'urls': {'api': {
128 'public': 'https://testnet.binancefuture.com/fapi/v1',
129 'private': 'https://testnet.binancefuture.com/fapi/v1',},}
130 })
131 self.client2 = ccxt.binance({ "apiKey": binApi,
132 "secret": binSecret})
133 self.client = binance_futures
134 #print(dir(self.client))
135 m = binance_futures.fetchMarkets()
136
137
138
139 def randomword(self, length):
140 letters = string.ascii_lowercase
141 return ''.join(random.choice(letters) for i in range(length))
142
143 def get_bbo( self, contract ): # Get best b/o excluding own orders
144
145 # Get orderbook
146 ob = self.client.fetchOrderBook( contract )
147 bids = ob[ 'bids' ]
148 asks = ob[ 'asks' ]
149
150 ords = self.client.fetchOpenOrders( contract )
151 #print(ords)
152 bid_ords = [ o for o in ords if o ['info'] [ 'side' ] == 'buy' ]
153 ask_ords = [ o for o in ords if o ['info'] [ 'side' ] == 'sell' ]
154 best_bid = None
155 best_ask = None
156
157 err = 10 ** -( self.get_precision( contract ) + 1 )
158
159 for b in bids:
160 match_qty = sum( [
161 o[1] for o in bid_ords
162 if math.fabs( b[0] - o[0] ) < err
163 ] )
164 if match_qty < b[1]:
165 best_bid = b[0]
166 break
167
168 for a in asks:
169 match_qty = sum( [
170 o[1] for o in ask_ords
171 if math.fabs( a[0] - o[0] ) < err
172 ] )
173 if match_qty < a[1]:
174 best_ask = a[0]
175 break
176
177 return { 'bid': best_bid, 'ask': best_ask }
178
179
180 def get_futures( self ): # Get all current futures instruments
181
182 self.futures_prv = cp.deepcopy( self.futures )
183 insts = self.client.fetchMarkets()
184 #print(insts[0])
185 self.futures = sort_by_key( {
186 i[ 'symbol' ]: i for i in insts
187 } )
188 #print(self.futures)
189 #for k, v in self.futures.items():
190 #self.futures[ k ][ 'expi_dt' ] = datetime.strptime(
191 # v[ 'expiration' ][ : -4 ],
192 # '%Y-%m-%d %H:%M:%S' )
193
194
195 def get_pct_delta( self ):
196 self.update_status()
197 return sum( self.deltas.values()) / float(self.equity_btc)
198
199
200 def get_spot( self ):
201 #print(self.client2.fetchTicker( pair )['bid'])
202 return self.client2.fetchTicker( pair )['bid']
203
204
205 def get_precision( self, contract ):
206 return self.futures[ contract ] ['info'] [ 'pricePrecision' ]
207
208
209 def get_ticksize( self, contract ):
210 return self.futures[ contract ] ['info'] ['filters'] [ 0 ] [ 'tickSize' ]
211
212
213 def output_status( self ):
214
215 if not self.output:
216 return None
217
218 self.update_status()
219
220 now = datetime.utcnow()
221 days = ( now - self.start_time ).total_seconds() / SECONDS_IN_DAY
222 print( '********************************************************************' )
223 print( 'Start Time: %s' % self.start_time.strftime( '%Y-%m-%d %H:%M:%S' ))
224 print( 'Current Time: %s' % now.strftime( '%Y-%m-%d %H:%M:%S' ))
225 print( 'Days: %s' % round( days, 1 ))
226 print( 'Hours: %s' % round( days * 24, 1 ))
227 print( 'Spot Price: %s' % self.get_spot())
228
229
230 pnl_usd = self.equity_usd - self.equity_usd_init
231 pnl_btc = self.equity_btc - self.equity_btc_init
232
233 print( 'Equity ($): %7.2f' % self.equity_usd)
234 print( 'P&L ($) %7.2f' % pnl_usd)
235 print( 'Equity (BTC): %7.4f' % self.equity_btc)
236 print( 'P&L (BTC) %7.4f' % pnl_btc)
237 print( '%% Delta: %s%%'% round( self.get_pct_delta() / PCT, 1 ))
238 print( 'Total Delta (BTC): %s' % round( sum( self.deltas.values()), 2 ))
239 print_dict_of_dicts( {
240 k: {
241 'BTC': self.deltas[ k ]
242 } for k in self.deltas.keys()
243 },
244 roundto = 2, title = 'Deltas' )
245
246 #print(self.positions)
247 print_dict_of_dicts( {
248 k: {
249 'Contracts': self.positions[ k ][ 'positionAmt' ]
250 } for k in self.positions.keys()
251 },
252 title = 'Positions' )
253
254 if not self.monitor:
255 print_dict_of_dicts( {
256 k: {
257 '%': self.vols[ k ]
258 } for k in self.vols.keys()
259 },
260 multiple = 100, title = 'Vols' )
261 print( '\nMean Loop Time: %s' % round( self.mean_looptime, 2 ))
262 self.cancelall()
263 print( '' )
264
265
266 def place_orders( self ):
267
268 if self.monitor:
269 return None
270
271 con_sz = self.con_size
272
273 for fut in self.futures.keys():
274
275 account = self.client.fetchBalance()
276 spot = self.get_spot()
277 bal_btc = float(account[ 'info' ] [ 'totalMarginBalance' ]) / spot
278 pos = float(self.positions[ fut ][ 'positionAmt' ])
279 pos_lim_long = bal_btc * PCT_LIM_LONG * 125 #/ len(self.futures)
280 pos_lim_short = bal_btc * PCT_LIM_SHORT * 125 #/ len(self.futures)
281 #print(pos_lim_long)
282 #expi = self.futures[ fut ][ 'expi_dt' ]
283 #tte = max( 0, ( expi - datetime.utcnow()).total_seconds() / SECONDS_IN_DAY )
284 pos_decay = 1.0 - math.exp( -DECAY_POS_LIM * 8035200 )
285 pos_lim_long *= pos_decay
286 pos_lim_short *= pos_decay
287 pos_lim_long -= pos
288 pos_lim_short += pos
289 pos_lim_long = max( 0, pos_lim_long )
290 pos_lim_short = max( 0, pos_lim_short )
291
292 min_order_size_btc = (MIN_ORDER_SIZE * CONTRACT_SIZE) / spot
293 print(min_order_size_btc) #0.0006833471711135484 0.08546200188472201
294 qtybtc = bal_btc * 125 / 25
295
296 nbids = min( math.trunc( pos_lim_long / qtybtc ), MAX_LAYERS )
297 nasks = min( math.trunc( pos_lim_short / qtybtc ), MAX_LAYERS )
298
299 place_bids = nbids > 0
300 place_asks = nasks > 0
301
302 if not place_bids and not place_asks:
303 print( 'No bid no offer for %s' % fut, min_order_size_btc )
304 continue
305
306 tsz = float(self.get_ticksize( fut ))
307 # Perform pricing
308 vol = max( self.vols[ BTC_SYMBOL ], self.vols[ fut ] )
309
310 eps = BP * vol * RISK_CHARGE_VOL
311 riskfac = math.exp( eps )
312
313 bbo = self.get_bbo( fut )
314 bid_mkt = bbo[ 'bid' ]
315 ask_mkt = bbo[ 'ask' ]
316
317 if bid_mkt is None and ask_mkt is None:
318 bid_mkt = ask_mkt = spot
319 elif bid_mkt is None:
320 bid_mkt = min( spot, ask_mkt )
321 elif ask_mkt is None:
322 ask_mkt = max( spot, bid_mkt )
323 mid_mkt = 0.5 * ( bid_mkt + ask_mkt )
324
325 ords = self.client.fetchOpenOrders( fut )
326 cancel_oids = []
327 bid_ords = ask_ords = []
328
329 if place_bids:
330
331 bid_ords = [ o for o in ords if o['info']['side'] == 'buy' ]
332 len_bid_ords = min( len( bid_ords ), nbids )
333 bid0 = mid_mkt * math.exp( -MKT_IMPACT )
334
335 bids = [ bid0 * riskfac ** -i for i in range( 1, nbids + 1 ) ]
336
337 bids[ 0 ] = ticksize_floor( bids[ 0 ], tsz )
338
339 if place_asks:
340
341 ask_ords = [ o for o in ords if o['info']['side'] == 'sell' ]
342 len_ask_ords = min( len( ask_ords ), nasks )
343 ask0 = mid_mkt * math.exp( MKT_IMPACT )
344
345 asks = [ ask0 * riskfac ** i for i in range( 1, nasks + 1 ) ]
346
347 asks[ 0 ] = ticksize_ceil( asks[ 0 ], tsz )
348
349 for i in range( max( nbids, nasks )):
350 # BIDS
351 if place_bids and i < nbids:
352
353 if i > 0:
354 prc = ticksize_floor( min( bids[ i ], bids[ i - 1 ] - tsz ), tsz )
355 else:
356 prc = bids[ 0 ]
357
358 qty = round( prc * qtybtc / con_sz ) / spot
359
360 if i < len_bid_ords:
361
362 oid = bid_ords[ i ]['info']['side']['orderId']
363 print(oid)
364 try:
365 self.client.editOrder( oid, qty, prc )
366 except (SystemExit, KeyboardInterrupt):
367 raise
368 except Excetion as e:
369 print(e)
370 else:
371 try:
372 self.client.createOrder( fut, "Limit", 'buy', qty, prc, {"newClientOrderId": "x-GYswxDoF-" + self.randomword(20)})
373 except (SystemExit, KeyboardInterrupt):
374 raise
375 except Exception as e:
376 print(e)
377 self.logger.warn( 'Bid order failed: %s bid for %s'
378 % ( prc, qty ))
379
380 # OFFERS
381
382 if place_asks and i < nasks:
383
384 if i > 0:
385 prc = ticksize_ceil( max( asks[ i ], asks[ i - 1 ] + tsz ), tsz )
386 else:
387 prc = asks[ 0 ]
388
389 qty = round( prc * qtybtc / con_sz ) / spot
390
391 if i < len_ask_ords:
392 oid = ask_ords[ i ]['info']['side']['orderId']
393 print(oid)
394 try:
395 self.client.editOrder( oid, qty, prc )
396 except (SystemExit, KeyboardInterrupt):
397 raise
398 except Exeption as e:
399 print(e)
400
401 else:
402 try:
403 self.client.createOrder( fut, "Limit", 'sell', qty, prc, {"newClientOrderId": "x-GYswxDoF-" + self.randomword(20)})
404 except (SystemExit, KeyboardInterrupt):
405 raise
406 except Exception as e:
407 self.logger.warn( 'Offer order failed: %s at %s'
408 % ( qty, prc ))
409
410
411 if nbids < len( bid_ords ):
412 cancel_oids += [ o['info']['side']['orderId'] for o in bid_ords[ nbids : ]]
413 if nasks < len( ask_ords ):
414 cancel_oids += [ o['info']['side']['orderId'] for o in ask_ords[ nasks : ]]
415 for oid in cancel_oids:
416 try:
417 self.client.cancelOrder( oid , pair )
418 except:
419 self.logger.warn( 'Order cancellations failed: %s' % oid )
420
421 def cancelall(self):
422 ords = self.client.fetchOpenOrders( pair )
423 for order in ords:
424 #print(order)
425 oid = order ['info'] ['orderId']
426 # print(order)
427 try:
428 self.client.cancelOrder( oid , pair )
429 except Exception as e:
430 print(e)
431 def restart( self ):
432 try:
433 strMsg = 'RESTARTING'
434 print( strMsg )
435 self.cancelall()
436 strMsg += ' '
437 for i in range( 0, 5 ):
438 strMsg += '.'
439 print( strMsg )
440 sleep( 1 )
441 except:
442 pass
443 finally:
444 os.execv( sys.executable, [ sys.executable ] + sys.argv )
445
446
447 def run( self ):
448
449 self.run_first()
450 self.output_status()
451
452 t_ts = t_out = t_loop = t_mtime = datetime.utcnow()
453
454 while True:
455
456 self.get_futures()
457
458 # Restart if a new contract is listed
459 if len( self.futures ) != len( self.futures_prv ):
460 self.restart()
461
462 self.update_positions()
463
464 t_now = datetime.utcnow()
465
466 # Update time series and vols
467 if ( t_now - t_ts ).total_seconds() >= WAVELEN_TS:
468 t_ts = t_now
469 self.update_timeseries()
470 self.update_vols()
471
472 self.place_orders()
473
474 # Display status to terminal
475 if self.output:
476 t_now = datetime.utcnow()
477 if ( t_now - t_out ).total_seconds() >= WAVELEN_OUT:
478 self.output_status(); t_out = t_now
479
480 # Restart if file change detected
481 t_now = datetime.utcnow()
482 if ( t_now - t_mtime ).total_seconds() > WAVELEN_MTIME_CHK:
483 t_mtime = t_now
484 if getmtime( __file__ ) > self.this_mtime:
485 self.restart()
486
487 t_now = datetime.utcnow()
488 looptime = ( t_now - t_loop ).total_seconds()
489
490 # Estimate mean looptime
491 w1 = EWMA_WGT_LOOPTIME
492 w2 = 1.0 - w1
493 t1 = looptime
494 t2 = self.mean_looptime
495
496 self.mean_looptime = w1 * t1 + w2 * t2
497
498 t_loop = t_now
499 sleep_time = MIN_LOOP_TIME - looptime
500 if sleep_time > 0:
501 time.sleep( sleep_time )
502 if self.monitor:
503 time.sleep( WAVELEN_OUT )
504
505
506 def run_first( self ):
507
508 self.create_client()
509 self.cancelall()
510 self.logger = get_logger( 'root', LOG_LEVEL )
511 # Get all futures contracts
512 self.get_futures()
513 self.this_mtime = getmtime( __file__ )
514 self.symbols = [ BTC_SYMBOL ] + list( self.futures.keys()); self.symbols.sort()
515 self.deltas = OrderedDict( { s: None for s in self.symbols } )
516
517 # Create historical time series data for estimating vol
518 ts_keys = self.symbols + [ 'timestamp' ]; ts_keys.sort()
519
520 self.ts = [
521 OrderedDict( { f: None for f in ts_keys } ) for i in range( NLAGS + 1 )
522 ]
523
524 self.vols = OrderedDict( { s: VOL_PRIOR for s in self.symbols } )
525
526 self.start_time = datetime.utcnow()
527 self.update_status()
528 self.equity_usd_init = self.equity_usd
529 self.equity_btc_init = self.equity_btc
530
531
532 def update_status( self ):
533
534 account = self.client.fetchBalance()
535 spot = self.get_spot()
536
537 #print(account)
538 self.equity_btc = float(account[ 'info' ] [ 'totalMarginBalance' ]) / spot
539 self.equity_usd = self.equity_btc * spot
540
541 self.update_positions()
542
543 self.deltas = OrderedDict(
544 { k: float(self.positions[ k ][ 'positionAmt' ]) for k in self.futures.keys()}
545 )
546 self.deltas[ BTC_SYMBOL ] = float(account[ 'info' ] [ 'totalMarginBalance' ])
547
548
549 def update_positions( self ):
550
551 self.positions = OrderedDict( { f: {
552 'size': 0,
553 'positionAmt': 0,
554 'indexPrice': None,
555 'markPrice': None
556 } for f in self.futures.keys() } )
557 positions = self.client.fapiPrivateGetPositionRisk()
558 #print('lala')
559 #print(positions)
560
561 for pos in positions:
562 if pair in self.futures:
563 self.positions[ pair] = pos
564
565
566 def update_timeseries( self ):
567
568 if self.monitor:
569 return None
570
571 for t in range( NLAGS, 0, -1 ):
572 self.ts[ t ] = cp.deepcopy( self.ts[ t - 1 ] )
573
574 spot = self.get_spot()
575 self.ts[ 0 ][ BTC_SYMBOL ] = spot
576
577 for c in self.futures.keys():
578
579 bbo = self.get_bbo( c )
580 bid = bbo[ 'bid' ]
581 ask = bbo[ 'ask' ]
582
583 if not bid is None and not ask is None:
584 mid = 0.5 * ( bbo[ 'bid' ] + bbo[ 'ask' ] )
585 else:
586 continue
587 self.ts[ 0 ][ c ] = mid
588
589 self.ts[ 0 ][ 'timestamp' ] = datetime.utcnow()
590
591
592 def update_vols( self ):
593
594 if self.monitor:
595 return None
596
597 w = EWMA_WGT_COV
598 ts = self.ts
599
600 t = [ ts[ i ][ 'timestamp' ] for i in range( NLAGS + 1 ) ]
601 p = { c: None for c in self.vols.keys() }
602 for c in ts[ 0 ].keys():
603 p[ c ] = [ ts[ i ][ c ] for i in range( NLAGS + 1 ) ]
604
605 if any( x is None for x in t ):
606 return None
607 for c in self.vols.keys():
608 if any( x is None for x in p[ c ] ):
609 return None
610
611 NSECS = SECONDS_IN_YEAR
612 cov_cap = COV_RETURN_CAP / NSECS
613
614 for s in self.vols.keys():
615
616 x = p[ s ]
617 dx = x[ 0 ] / x[ 1 ] - 1
618 dt = ( t[ 0 ] - t[ 1 ] ).total_seconds()
619 v = min( dx ** 2 / dt, cov_cap ) * NSECS
620 v = w * v + ( 1 - w ) * self.vols[ s ] ** 2
621
622 self.vols[ s ] = math.sqrt( v )
623
624
625if __name__ == '__main__':
626
627 try:
628 mmbot = MarketMaker( monitor = args.monitor, output = args.output )
629 mmbot.run()
630 except( KeyboardInterrupt, SystemExit ):
631 print( "Cancelling open orders" )
632 mmbot.cancelall()
633 sys.exit()
634 except:
635 print( traceback.format_exc())
636 if args.restart:
637 mmbot.restart()
638
639