├── README.md ├── orderbook_monitors.py ├── orderb_imbalance.py └── api_files.py /README.md: -------------------------------------------------------------------------------- 1 | # An orderbook based trading algorithm for bitfinex 2 | 3 | Orderbook based trading algorithm for Bitfinex, based on open source from jndok and dawsbot. Please use at own risk ! 4 | 5 | Best used in a jupyter notebook, the python requests.get is used with a proxyDict. You might need to modify that if you don't need to use a proxy. You will also need to enter your own api keys to use Bitfinex. 6 | 7 | The api files are based on the interface from work by jndok and dawsbot. bitfinex/FinexAPI/FinexAPI.py 8 | 9 | In order to take it further, a few other functions were added and a working trading algorithm was implemented. 10 | 11 | trim_orderbook : This trims the orderbook to a % of the current price. So that bids and offers that are far away can be removed. 12 | 13 | simple_strategy: This relies on the calculation of imbalance in the trimmed orderbook to decide if a trade needs to be entered into. 14 | 15 | run_algo: This implements the trading algorithm. It works in the following way. 16 | 17 | A loop runs every n+(computation time) times to check if the imbalance is beyond a certain threshold. For e.g, if the amount of bids - amount of offers > 60.0, then enter a buy order. 18 | 19 | Once a buy order is triggered, the trade is monitored for a certain amount of time. lag (=2s sat) * 60 seconds. Every 2s, the pnl is calculated and if it exceeds the gain or the loss, it's closed out. If at the end of the period, pnl is within threshold, the position is still closed. 20 | 21 | A better strategy would be based on monitoring the orderbook changes. In order to measure the change, as the price changes, the only meaningful measure is splitting the prices into ranges (bins). These functions are implemented in orderbook_monitor. 22 | 23 | The code can be used to trade any cryptoccy pair by changing the symbol. 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /orderbook_monitors.py: -------------------------------------------------------------------------------- 1 | def orderbook_to_df(trim_ob): # converts orderbook to binned ranges. 2 | 3 | bids_amount = [] 4 | bids_px = [] 5 | asks_amount = [] 6 | asks_px = [] 7 | 8 | for i in trim_ob['bids']: 9 | bids_amount.append(i[0]) 10 | bids_px.append(i[1]) 11 | 12 | bids_dict = {'amount': bids_amount, 'px' : bids_px} 13 | 14 | for j in trim_ob['asks']: 15 | asks_amount.append(j[0]) 16 | asks_px.append(j[1]) 17 | 18 | asks_dict = {'amount': asks_amount, 'px' : asks_px} 19 | 20 | asks_df = pd.DataFrame.from_dict(asks_dict) 21 | bids_df = pd.DataFrame.from_dict(bids_dict) 22 | 23 | return [asks_df,bids_df] 24 | 25 | def orderbook_tobins(symbol = 'btcusd', price = 6500.0, percent = 0.01, lag = 3.0): 26 | 27 | px_before = ticker(symbol)['last_price'] 28 | trim_ob = trim_orderbook(symbol = symbol, current_price = float(px_before), percent = percent) 29 | 30 | time.sleep(lag) 31 | 32 | px_after = ticker(symbol)['last_price'] 33 | trim_ob_after = trim_orderbook(symbol = symbol, current_price = float(px_after), percent = percent) #poll orderbook after 2s 34 | 35 | 36 | asks_df = orderbook_to_df(trim_ob)[0] 37 | bids_df = orderbook_to_df(trim_ob)[1] 38 | 39 | asks_df_after = orderbook_to_df(trim_ob_after)[0] 40 | bids_df_after = orderbook_to_df(trim_ob_after)[1] 41 | 42 | asks_bins = np.linspace(asks_df.px.min(),asks_df.px.max(),5) # use same bins 43 | bids_bins = np.linspace(bids_df.px.min(),bids_df.px.max(),5) # use same bins 44 | 45 | grouped_asks = asks_df.groupby(pd.cut(asks_df.px,asks_bins)) 46 | grouped_bids = bids_df.groupby(pd.cut(bids_df.px,bids_bins)) 47 | 48 | grouped_asks_after = asks_df_after.groupby(pd.cut(asks_df_after.px,asks_bins)) #use same bins 49 | grouped_bids_after = bids_df_after.groupby(pd.cut(bids_df_after.px,bids_bins)) #use same bins 50 | 51 | binned_asks = grouped_asks.sum().amount 52 | binned_bids = grouped_bids.sum().amount.sort_index(ascending = False) 53 | 54 | binned_asks_after = grouped_asks_after.sum().amount 55 | binned_bids_after = grouped_bids_after.sum().amount.sort_index(ascending = False) 56 | 57 | change_asks = (binned_asks_after - binned_asks).sum() # change the return statement to change_asks-change_bids 58 | change_bids = (binned_bids_after - binned_bids).sum() # if you need a simple signal 59 | 60 | return [grouped_asks.sum().amount, grouped_bids.sum().amount] 61 | -------------------------------------------------------------------------------- /orderb_imbalance.py: -------------------------------------------------------------------------------- 1 | def trades(symbol='btcusd'): # get a list of the most recent trades for the given symbol. 2 | 3 | r = requests.get(URL + "/trades/" + symbol, verify=True, proxies = proxyDict ) 4 | rep = r.json() 5 | 6 | return rep 7 | 8 | def trim_orderbook(symbol = 'BTCUSD',current_price = 7200.0, percent = 0.02) : 9 | orig = orderbook(symbol) 10 | trim_bids = [] 11 | for k in orig['bids']: 12 | if float(k['price']) > current_price*(1-percent): 13 | trim_bids.append([float(k['amount']),float(k['price']),float(k['timestamp'])]) 14 | 15 | trim_asks = [] 16 | for t in orig['asks']: 17 | if float(t['price']) < current_price*(1+percent): 18 | trim_asks.append([float(t['amount']),float(t['price']),float(t['timestamp'])]) 19 | 20 | trim_book = {'bids': trim_bids, 'asks': trim_asks} 21 | 22 | return trim_book 23 | 24 | 25 | def current_relevant_price(symbol = 'BTCUSD', threshold = 100.0): 26 | 27 | trades_list = trades(symbol) 28 | time_now = datetime.datetime.now().timestamp() 29 | 30 | volume = 0.0 31 | price_volume = 0.0 32 | 33 | for j in trades_list: 34 | if time_now - float(j['timestamp'])< threshold : #1 min from now# 35 | volume = volume + float(j['amount']) 36 | price_volume = price_volume + float(j['amount'])* float(j['price']) 37 | result = price_volume/volume 38 | if volume == 0: 39 | return "volume zero error" 40 | else: 41 | return [result, volume] 42 | 43 | def simple_strategy(price = 7150.0, symbol = 'BTCUSD'): 44 | book = trim_orderbook(current_price = price, percent = 0.01, symbol = symbol) 45 | bid_list = book['bids'] #get a list of bids and size 46 | ask_list = book['asks'] # get a list of asks and sizes 47 | 48 | sum_bids = 0 49 | sum_asks = 0 50 | 51 | for m in bid_list: 52 | sum_bids = sum_bids + m[0] 53 | for n in ask_list: 54 | sum_asks = sum_asks + n[0] 55 | 56 | return sum_bids - sum_asks 57 | 58 | def opp_traded(side = 'buy'): 59 | if side == 'buy': 60 | return 'sell' 61 | elif side == 'sell': 62 | return 'buy' 63 | 64 | def active_position(symbol = 'btcusd'): 65 | act = active_positions() 66 | if act != []: 67 | for j in act: 68 | if j['symbol'] == symbol: 69 | return j['amount'] 70 | else: 71 | return 0 72 | else : 73 | return 0 74 | 75 | def run_algo(): # if condition is satisifed runs a trade 10 times, 76 | 77 | for t in range(0,10): # run the strategy 10 times 78 | 79 | time.sleep(3) #wait for 3s before starting the next strategy - arbitrary 80 | 81 | symbol = 'btcusd' 82 | size = '0.01' # size in coins 83 | 84 | imbalance = 30.0 # orderbook imbalance to act on 85 | pnl_gain_threshold = 6.00 # pnl threshold to close +ve position 86 | pnl_loss_threshold = -2.00 # # pnl threshold to close -ve position 87 | 88 | 89 | init_price = float(ticker(symbol)['last_price']) 90 | init_weight = simple_strategy(init_price,symbol) # relies on the imbalance of bids/offers as defined in function above. 91 | 92 | if init_weight > imbalance and active_position(symbol) == 0: 93 | p = place_order(size,str(init_price),'buy','market', symbol = symbol) # Initiate buy position 94 | elif init_weight < imbalance*-1.0 and active_position(symbol) == 0: 95 | p = place_order(size,str(init_price),'sell','market', symbol = symbol) # Or initiate sell position 96 | else: 97 | p = [] 98 | print('thresholds not satisfied to initiate position ,' + '{0:.0f}'.format(init_weight) + ',' + '{0:.7f}'.format(init_price)) 99 | 100 | 101 | if p != []: 102 | 103 | time.sleep(5) # wait 10s before trade is executed. 104 | 105 | status = status_order(p['id']) 106 | 107 | #not_executed = status['is_live'] #check if order is executed 108 | traded = status['side'] 109 | 110 | print(status['side']+' ' + status['executed_amount'] +' ' + status['avg_execution_price']) 111 | 112 | if status['is_live'] == False: 113 | start_price = status['avg_execution_price'] 114 | size_start = status['executed_amount'] 115 | sign = [-1,1][traded == 'buy'] 116 | 117 | for i in range(0,60): # start monitoring every (4 + computing times) for 60 times 118 | time.sleep(2) 119 | price = float(ticker(symbol)['last_price']) 120 | pnl = float(size_start)*(price-float(start_price))*sign 121 | print('{0:.7f}'.format(pnl) + ',' + '{0:.7f}'.format(price)) 122 | if pnl > pnl_gain_threshold: 123 | if active_position('symbol') == size: 124 | t1 = place_order(size,str(price),opp_traded(traded),'market',symbol = symbol) 125 | print(t1) 126 | print(status_order(t1['id'])) 127 | break 128 | elif pnl < pnl_loss_threshold: 129 | if active_position('symbol') == size: 130 | t2 = place_order(size,str(price),opp_traded(traded),'market',symbol = symbol) 131 | print(t2) 132 | print(status_order(t2['id'])) 133 | break 134 | 135 | # if time passes with pnl within range exit position. 136 | time.sleep(5) 137 | live_position = float(active_position(symbol)) 138 | if live_position != 0: 139 | order_to_close = ['buy', 'sell'][live_position > 0.0] 140 | final_order = place_order(str(abs(live_position)),str(price),order_to_close,'market', symbol = symbol) 141 | 142 | print(active_position(symbol)) 143 | 144 | 145 | else: 146 | print('no orders executed') 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /api_files.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import base64 4 | import hmac 5 | import hashlib 6 | import time 7 | import datetime 8 | 9 | proxyDict = { 10 | 'http' : 'http://user:password@proxy:80', 11 | 'https' : 'https://user:password@proxy:80', 12 | } 13 | 14 | __all__ = ['ticker', 'today', 'orderbook', 'lendbook', 'stats', 'trades', 'lends', 'symbols', 'place_order', 'delete_order', 'delete_all_order', 'status_order', 'active_orders', 'active_positions', 'place_offer', 'cancel_offer', 'status_offer', 'active_offers', 'past_trades', 'balances', 'claim_position', 'close_position', 'withdraw'] 15 | 16 | URL = "https://api.bitfinex.com/v1" 17 | 18 | 19 | API_KEY = 'ddddddddddddddd' # put your API public key here. 20 | API_SECRET = b'ddddddddddd' # put your API private key here. 21 | 22 | print ("Your pub: " + str(API_KEY)) 23 | #print "Your priv: " + str(API_SECRET) 24 | 25 | # unauthenticated 26 | 27 | def ticker(symbol='btcusd'): # gets the innermost bid and asks and information on the most recent trade. 28 | 29 | r = requests.get(URL + "/pubticker/" + symbol, verify=True, proxies = proxyDict) # <== UPDATED TO LATEST VERSION OF BFX! 30 | rep = r.json() 31 | 32 | try: 33 | rep['last_price'] 34 | except KeyError: 35 | return rep['message'] 36 | 37 | return rep 38 | 39 | def stats(symbol='btcusd'): # Various statistics about the requested pairs. 40 | 41 | r = requests.get(URL + "/stats/" + symbol, verify=True, proxies = proxyDict) # <== UPDATED TO LATEST VERSION OF BFX! 42 | rep = r.json() 43 | return rep 44 | 45 | try: 46 | rep['volume'] 47 | except KeyError: 48 | return rep['message'] 49 | 50 | def today(symbol='btcusd'): # today's low, high and volume. 51 | 52 | r = requests.get(URL + "/today/" + symbol, verify=True, proxies = proxyDict) 53 | rep = r.json() 54 | 55 | try: 56 | rep['volume'] 57 | except KeyError: 58 | return rep['message'] 59 | 60 | return rep 61 | 62 | def genNonce(): # generates a nonce, used for authentication. 63 | return str((time.time() * 1000000)) 64 | 65 | def payloadPacker(payload): # packs and signs the payload of the request. 66 | 67 | j = json.dumps(payload) 68 | #data = base64.standard_b64encode(j) 69 | data = base64.b64encode(bytes(j, "utf-8")) 70 | 71 | h = hmac.new(API_SECRET, data, hashlib.sha384) 72 | signature = h.hexdigest() 73 | 74 | return { 75 | "X-BFX-APIKEY": API_KEY, 76 | "X-BFX-SIGNATURE": signature, 77 | "X-BFX-PAYLOAD": data 78 | } 79 | def place_order(amount, price, side, ord_type, symbol='btcusd', exchange='bitfinex'): # submit a new order. 80 | 81 | payload = { 82 | 83 | "request":"/v1/order/new", 84 | "nonce":genNonce(), 85 | "symbol":symbol, 86 | "amount":amount, 87 | "price":price, 88 | "exchange":exchange, 89 | "side":side, 90 | "type":ord_type 91 | 92 | } 93 | 94 | signed_payload = payloadPacker(payload) 95 | r = requests.post(URL + "/order/new", headers=signed_payload, verify=True, proxies = proxyDict) 96 | rep = r.json() 97 | 98 | try: 99 | rep['order_id'] 100 | except: 101 | return rep['message'] 102 | 103 | return rep 104 | 105 | def past_trades(timestamp=0, symbol='btcusd'): # view your past trades 106 | 107 | payload = { 108 | 109 | "request":"/v1/mytrades", 110 | "nonce":genNonce(), 111 | "symbol":symbol, 112 | "timestamp":timestamp 113 | 114 | } 115 | 116 | signed_payload = payloadPacker(payload) 117 | r = requests.post(URL + "/mytrades", headers=signed_payload, verify=True,proxies = proxyDict) 118 | rep = r.json() 119 | 120 | return rep 121 | 122 | def status_order(order_id): # get the status of an order. Is it active? Was it cancelled? To what extent has it been executed? etc. 123 | 124 | payload = { 125 | 126 | "request":"/v1/order/status", 127 | "nonce":genNonce(), 128 | "order_id":order_id 129 | 130 | } 131 | 132 | signed_payload = payloadPacker(payload) 133 | r = requests.post(URL + "/order/status", headers=signed_payload, verify=True,proxies = proxyDict) 134 | rep = r.json() 135 | 136 | try: 137 | rep['avg_execution_price'] 138 | except: 139 | return rep['message'] 140 | 141 | return rep 142 | 143 | def balances(): # see your balances. 144 | 145 | payload = { 146 | 147 | "request":"/v1/balances", 148 | "nonce":genNonce() 149 | 150 | } 151 | 152 | signed_payload = payloadPacker(payload) 153 | r = requests.post(URL + "/balances", headers=signed_payload, verify=True,proxies = proxyDict) 154 | rep = r.json() 155 | 156 | return rep 157 | 158 | def orderbook(symbol='btcusd'): # get the full order book. 159 | 160 | r = requests.get(URL + "/book/" + symbol, verify=True,proxies = proxyDict) 161 | rep = r.json() 162 | 163 | return rep 164 | 165 | def delete_order(order_id): # cancel an order. 166 | 167 | payload = { 168 | 169 | "request":"/v1/order/cancel", 170 | "nonce":genNonce(), 171 | "order_id":order_id 172 | 173 | } 174 | 175 | signed_payload = payloadPacker(payload) 176 | r = requests.post(URL + "/order/cancel", headers=signed_payload, verify=True,proxies = proxyDict) 177 | rep = r.json() 178 | 179 | try: 180 | rep['avg_execution_price'] 181 | except: 182 | return rep['message'] 183 | 184 | return rep 185 | 186 | def delete_all_order(): # cancel an order. 187 | 188 | payload = { 189 | 190 | "request":"/v1/order/cancel/all", 191 | "nonce":genNonce(), 192 | 193 | } 194 | 195 | signed_payload = payloadPacker(payload) 196 | r = requests.post(URL + "/order/cancel/all", headers=signed_payload, verify=True,proxies = proxyDict) 197 | rep = r.json() 198 | return rep 199 | ''' 200 | try: 201 | rep['avg_execution_price'] 202 | except: 203 | return rep['message'] 204 | ''' 205 | 206 | def status_order(order_id): # get the status of an order. Is it active? Was it cancelled? To what extent has it been executed? etc. 207 | 208 | payload = { 209 | 210 | "request":"/v1/order/status", 211 | "nonce":genNonce(), 212 | "order_id":order_id 213 | 214 | } 215 | 216 | signed_payload = payloadPacker(payload) 217 | r = requests.post(URL + "/order/status", headers=signed_payload, verify=True,proxies = proxyDict) 218 | rep = r.json() 219 | 220 | try: 221 | rep['avg_execution_price'] 222 | except: 223 | return rep['message'] 224 | 225 | return rep 226 | 227 | def active_orders(): # view your active orders. 228 | 229 | payload = { 230 | 231 | "request":"/v1/orders", 232 | "nonce":genNonce() 233 | 234 | } 235 | 236 | signed_payload = payloadPacker(payload) 237 | r = requests.post(URL + "/orders", headers=signed_payload, verify=True,proxies = proxyDict) 238 | rep = r.json() 239 | 240 | return rep 241 | 242 | def active_positions(): # view your active positions. 243 | 244 | payload = { 245 | 246 | "request":"/v1/positions", 247 | "nonce":genNonce() 248 | 249 | } 250 | 251 | signed_payload = payloadPacker(payload) 252 | r = requests.post(URL + "/positions", headers=signed_payload, verify=True,proxies = proxyDict) 253 | rep = r.json() 254 | 255 | return rep 256 | 257 | def claim_position(position_id): # Claim a position. 258 | 259 | payload = { 260 | 261 | "request":"/v1/position/claim", 262 | "nonce":genNonce(), 263 | "position_id":position_id 264 | 265 | } 266 | 267 | signed_payload = payloadPacker(payload) 268 | r = requests.post(URL + "/position/claim", headers=signed_payload, verify=True,proxies = proxyDict) 269 | rep = r.json() 270 | 271 | return rep 272 | 273 | def close_position(position_id): # Claim a position. 274 | 275 | payload = { 276 | 277 | "request":"/v1/position/close", 278 | "nonce":genNonce(), 279 | "position_id":position_id 280 | 281 | } 282 | 283 | signed_payload = payloadPacker(payload) 284 | r = requests.post(URL + "/position/close", headers=signed_payload, verify=True,proxies = proxyDict) 285 | rep = r.json() 286 | 287 | return rep 288 | 289 | 290 | def place_offer(currency, amount, rate, period, direction): 291 | 292 | payload = { 293 | 294 | "request":"/v1/offer/new", 295 | "nonce":genNonce(), 296 | "currency":currency, 297 | "amount":amount, 298 | "rate":rate, 299 | "period":period, 300 | "direction":direction 301 | 302 | } 303 | 304 | signed_payload = payloadPacker(payload) 305 | r = requests.post(URL + "/offer/new", headers=signed_payload, verify=True,proxies = proxyDict) 306 | rep = r.json() 307 | 308 | return rep 309 | 310 | def cancel_offer(offer_id): 311 | 312 | payload = { 313 | 314 | "request":"/v1/offer/cancel", 315 | "nonce":genNonce(), 316 | "offer_id":offer_id 317 | 318 | } 319 | 320 | signed_payload = payloadPacker(payload) 321 | r = requests.post(URL + "/offer/cancel", headers=signed_payload, verify=True,proxies = proxyDict) 322 | rep = r.json() 323 | 324 | return rep 325 | 326 | def status_offer(offer_id): 327 | 328 | payload = { 329 | 330 | "request":"/v1/offer/status", 331 | "nonce":genNonce(), 332 | "offer_id":offer_id 333 | 334 | } 335 | 336 | signed_payload = payloadPacker(payload) 337 | r = requests.post(URL + "/offer/status", headers=signed_payload, verify=True,proxies = proxyDict) 338 | rep = r.json() 339 | 340 | return rep 341 | 342 | def active_offers(): 343 | 344 | payload = { 345 | 346 | "request":"/v1/offers", 347 | "nonce":genNonce() 348 | 349 | } 350 | 351 | signed_payload = payloadPacker(payload) 352 | r = requests.post(URL + "/offers", headers=signed_payload, verify=True,proxies = proxyDict) 353 | rep = r.json() 354 | 355 | return rep 356 | 357 | --------------------------------------------------------------------------------