def stats_update(): upnl_l = x if (x := calc_long_pnl(long_pprice, tick[0], long_psize, xk['inverse'], xk['contract_multiplier'])) == x else 0.0 upnl_s = y if (y := calc_shrt_pnl(shrt_pprice, tick[0], shrt_psize, xk['inverse'], xk['contract_multiplier'])) == y else 0.0 stats.append({'timestamp': tick[2], 'balance': balance, # Redundant with fills, but makes plotting easier 'equity': balance + upnl_l + upnl_s})
async def fetch_position(self) -> dict: position = {} if self.market_type == 'linear_perpetual': fetched, bal = await asyncio.gather( self.private_get(self.endpoints['position'], {'symbol': self.symbol}), self.private_get(self.endpoints['balance'], {'coin': self.quot}) ) long_pos = [e for e in fetched['result'] if e['side'] == 'Buy'][0] shrt_pos = [e for e in fetched['result'] if e['side'] == 'Sell'][0] position['wallet_balance'] = float(bal['result'][self.quot]['wallet_balance']) else: fetched, bal = await asyncio.gather( self.private_get(self.endpoints['position'], {'symbol': self.symbol}), self.private_get(self.endpoints['balance'], {'coin': self.coin}) ) position['wallet_balance'] = float(bal['result'][self.coin]['wallet_balance']) if self.market_type == 'inverse_perpetual': if fetched['result']['side'] == 'Buy': long_pos = fetched['result'] shrt_pos = {'size': 0.0, 'entry_price': 0.0, 'leverage': 0.0, 'liq_price': 0.0} else: long_pos = {'size': 0.0, 'entry_price': 0.0, 'leverage': 0.0, 'liq_price': 0.0} shrt_pos = fetched['result'] elif self.market_type == 'inverse_futures': long_pos = [e['data'] for e in fetched['result'] if e['data']['position_idx'] == 1][0] shrt_pos = [e['data'] for e in fetched['result'] if e['data']['position_idx'] == 2][0] position['long'] = {'size': float(long_pos['size']), 'price': float(long_pos['entry_price']), 'leverage': float(long_pos['leverage']), 'liquidation_price': float(long_pos['liq_price'])} position['shrt'] = {'size': -float(shrt_pos['size']), 'price': float(shrt_pos['entry_price']), 'leverage': float(shrt_pos['leverage']), 'liquidation_price': float(shrt_pos['liq_price'])} position['long']['upnl'] = calc_long_pnl(position['long']['price'], self.price, position['long']['size'], self.xk['inverse'], self.xk['contract_multiplier']) \ if position['long']['price'] != 0.0 else 0.0 position['shrt']['upnl'] = calc_shrt_pnl(position['shrt']['price'], self.price, position['shrt']['size'], self.xk['inverse'], self.xk['contract_multiplier']) \ if position['shrt']['price'] != 0.0 else 0.0 upnl = position['long']['upnl'] + position['shrt']['upnl'] position['equity'] = position['wallet_balance'] + upnl return position
def backtest(config: dict, ticks: np.ndarray, do_print=False) -> (list, list, bool): if len(ticks) <= config['ema_span']: return [], [], False long_psize, long_pprice = 0.0, 0.0 shrt_psize, shrt_pprice = 0.0, 0.0 liq_price, liq_diff = 0.0, 1.0 balance = config['starting_balance'] if all(x in config for x in ['long_pprice', 'long_psize', 'shrt_pprice', 'shrt_psize']): long_pprice, long_psize, shrt_pprice, shrt_psize = ( config["long_pprice"], config["long_psize"], config["shrt_pprice"], config["shrt_psize"], ) else: long_pprice, long_psize, shrt_pprice, shrt_psize = 0.0, 0.0, 0.0, 0.0 pnl_plus_fees_cumsum, loss_cumsum, profit_cumsum, fee_paid_cumsum = 0.0, 0.0, 0.0, 0.0 xk = {k: float(config[k]) for k in get_keys()} if config['exchange'] == 'binance': calc_liq_price = calc_liq_price_binance elif config['exchange'] == 'bybit': calc_liq_price = calc_liq_price_bybit prev_long_close_ts, prev_long_entry_ts, prev_long_close_price = 0, 0, 0.0 prev_shrt_close_ts, prev_shrt_entry_ts, prev_shrt_close_price = 0, 0, 0.0 latency_simulation_ms = config['latency_simulation_ms'] \ if 'latency_simulation_ms' in config else 1000 next_stats_update = 0 stats = [] def stats_update(): upnl_l = x if (x := calc_long_pnl( long_pprice, tick[0], long_psize, xk['inverse'], xk['contract_multiplier'])) == x else 0.0 upnl_s = y if (y := calc_shrt_pnl( shrt_pprice, tick[0], shrt_psize, xk['inverse'], xk['contract_multiplier'])) == y else 0.0 stats.append({ 'timestamp': tick[2], 'balance': balance, # Redundant with fills, but makes plotting easier 'equity': balance + upnl_l + upnl_s }) all_fills = [] fills = [] bids, asks = [], [] ob = [min(ticks[0][0], ticks[1][0]), max(ticks[0][0], ticks[1][0])] ema_span = int(round(config['ema_span'])) # emas = calc_emas(ticks[:, 0], ema_span) # price_stds = calc_stds(ticks[:, 0], ema_span) # volatilities = price_stds / emas ema_std_iterator = iter_indicator_chunks(ticks[:, 0], ema_span) ema_chunk, std_chunk, z = next(ema_std_iterator) volatility_chunk = std_chunk / ema_chunk zc = 0 closest_liq = 1.0 prev_update_plus_delay = ticks[ema_span][2] + latency_simulation_ms update_triggered = False prev_update_plus_5sec = 0 tick = ticks[0] stats_update() # tick tuple: (price, buyer_maker, timestamp) for k, tick in enumerate(ticks[ema_span:], start=ema_span): chunk_i = k - zc if chunk_i >= len(ema_chunk): ema_chunk, std_chunk, z = next(ema_std_iterator) volatility_chunk = std_chunk / ema_chunk zc = z * len(ema_chunk) chunk_i = k - zc # Update the stats every 1/2 hour if tick[2] > next_stats_update: closest_liq = min(closest_liq, calc_diff(liq_price, tick[0])) stats_update() next_stats_update = tick[2] + 1000 * 60 * 30 fills = [] if tick[1]: if liq_diff < 0.05 and long_psize > -shrt_psize and tick[ 0] <= liq_price: fills.append({ 'qty': -long_psize, 'price': tick[0], 'pside': 'long', 'type': 'long_liquidation', 'side': 'sel', 'pnl': calc_long_pnl(long_pprice, tick[0], long_psize, xk['inverse'], xk['contract_multiplier']), 'fee_paid': -calc_cost(long_psize, tick[0], xk['inverse'], xk['contract_multiplier']) * config['taker_fee'], 'long_psize': 0.0, 'long_pprice': 0.0, 'shrt_psize': 0.0, 'shrt_pprice': 0.0, 'liq_price': 0.0, 'liq_diff': 1.0 }) long_psize, long_pprice, shrt_psize, shrt_pprice = 0.0, 0.0, 0.0, 0.0 else: if bids: if tick[0] <= bids[0][1]: update_triggered = True while bids: if tick[0] < bids[0][1]: bid = bids.pop(0) fill = { 'qty': bid[0], 'price': bid[1], 'side': 'buy', 'type': bid[4], 'fee_paid': -calc_cost(bid[0], bid[1], xk['inverse'], xk['contract_multiplier']) * config['maker_fee'] } if 'close' in bid[4]: fill['pnl'] = calc_shrt_pnl( shrt_pprice, bid[1], bid[0], xk['inverse'], xk['contract_multiplier']) shrt_psize = min( 0.0, round_(shrt_psize + bid[0], config['qty_step'])) fill.update({ 'pside': 'shrt', 'long_psize': long_psize, 'long_pprice': long_pprice, 'shrt_psize': shrt_psize, 'shrt_pprice': shrt_pprice }) prev_shrt_close_ts = tick[2] else: fill['pnl'] = 0.0 long_psize, long_pprice = calc_new_psize_pprice( long_psize, long_pprice, bid[0], bid[1], xk['qty_step']) if long_psize < 0.0: long_psize, long_pprice = 0.0, 0.0 fill.update({ 'pside': 'long', 'long_psize': bid[2], 'long_pprice': bid[3], 'shrt_psize': shrt_psize, 'shrt_pprice': shrt_pprice }) prev_long_entry_ts = tick[2] fills.append(fill) else: break ob[0] = tick[0] else: if liq_diff < 0.05 and -shrt_psize > long_psize and tick[ 0] >= liq_price: fills.append({ 'qty': -shrt_psize, 'price': tick[0], 'pside': 'shrt', 'type': 'shrt_liquidation', 'side': 'buy', 'pnl': calc_shrt_pnl(shrt_pprice, tick[0], shrt_psize, xk['inverse'], xk['contract_multiplier']), 'fee_paid': -calc_cost(shrt_psize, tick[0], xk['inverse'], xk['contract_multiplier']) * config['taker_fee'], 'long_psize': 0.0, 'long_pprice': 0.0, 'shrt_psize': 0.0, 'shrt_pprice': 0.0, 'liq_price': 0.0, 'liq_diff': 1.0 }) long_psize, long_pprice, shrt_psize, shrt_pprice = 0.0, 0.0, 0.0, 0.0 else: if asks: if tick[0] >= asks[0][1]: update_triggered = True while asks: if tick[0] > asks[0][1]: ask = asks.pop(0) fill = { 'qty': ask[0], 'price': ask[1], 'side': 'sel', 'type': ask[4], 'fee_paid': -calc_cost(ask[0], ask[1], xk['inverse'], xk['contract_multiplier']) * config['maker_fee'] } if 'close' in ask[4]: fill['pnl'] = calc_long_pnl( long_pprice, ask[1], ask[0], xk['inverse'], xk['contract_multiplier']) long_psize = max( 0.0, round_(long_psize + ask[0], config['qty_step'])) fill.update({ 'pside': 'long', 'long_psize': long_psize, 'long_pprice': long_pprice, 'shrt_psize': shrt_psize, 'shrt_pprice': shrt_pprice }) prev_long_close_ts = tick[2] else: fill['pnl'] = 0.0 shrt_psize, shrt_pprice = calc_new_psize_pprice( shrt_psize, shrt_pprice, ask[0], ask[1], xk['qty_step']) if shrt_psize > 0.0: shrt_psize, shrt_pprice = 0.0, 0.0 fill.update({ 'pside': 'shrt', 'long_psize': long_psize, 'long_pprice': long_pprice, 'shrt_psize': shrt_psize, 'shrt_pprice': shrt_pprice }) prev_shrt_entry_ts = tick[2] liq_diff = calc_diff(liq_price, tick[0]) fill.update({ 'liq_price': liq_price, 'liq_diff': liq_diff }) fills.append(fill) else: break ob[1] = tick[0] if tick[2] > prev_update_plus_delay and ( update_triggered or tick[2] > prev_update_plus_5sec): prev_update_plus_delay = tick[2] + latency_simulation_ms prev_update_plus_5sec = tick[2] + 5000 update_triggered = False bids, asks = [], [] liq_diff = calc_diff(liq_price, tick[0]) closest_liq = min(closest_liq, liq_diff) for tpl in iter_entries(balance, long_psize, long_pprice, shrt_psize, shrt_pprice, liq_price, ob[0], ob[1], ema_chunk[k - zc], tick[0], volatility_chunk[k - zc], **xk): if len(bids) > 2 and len(asks) > 2: break if tpl[0] > 0.0: bids.append(tpl) elif tpl[0] < 0.0: asks.append(tpl) else: break if tick[0] <= shrt_pprice and shrt_pprice > 0.0: for tpl in iter_shrt_closes(balance, shrt_psize, shrt_pprice, ob[0], **xk): bids.append(list(tpl) + [shrt_pprice, 'shrt_close']) if tick[0] >= long_pprice and long_pprice > 0.0: for tpl in iter_long_closes(balance, long_psize, long_pprice, ob[1], **xk): asks.append(list(tpl) + [long_pprice, 'long_close']) bids = sorted(bids, key=lambda x: x[1], reverse=True) asks = sorted(asks, key=lambda x: x[1]) if len(fills) > 0: for fill in fills: balance += fill['pnl'] + fill['fee_paid'] upnl_l = calc_long_pnl(long_pprice, tick[0], long_psize, xk['inverse'], xk['contract_multiplier']) upnl_s = calc_shrt_pnl(shrt_pprice, tick[0], shrt_psize, xk['inverse'], xk['contract_multiplier']) liq_price = calc_liq_price(balance, long_psize, long_pprice, shrt_psize, shrt_pprice, xk['inverse'], xk['contract_multiplier'], config['max_leverage']) liq_diff = calc_diff(liq_price, tick[0]) fill.update({'liq_price': liq_price, 'liq_diff': liq_diff}) fill['equity'] = balance + upnl_l + upnl_s fill['available_margin'] = calc_available_margin( balance, long_psize, long_pprice, shrt_psize, shrt_pprice, tick[0], xk['inverse'], xk['contract_multiplier'], xk['leverage']) for side_ in ['long', 'shrt']: if fill[f'{side_}_pprice'] == 0.0: fill[f'{side_}_pprice'] = np.nan fill['balance'] = balance fill['timestamp'] = tick[2] fill['trade_id'] = k fill['gain'] = fill['equity'] / config['starting_balance'] fill['n_days'] = (tick[2] - ticks[ema_span][2]) / (1000 * 60 * 60 * 24) fill['closest_liq'] = closest_liq try: fill['average_daily_gain'] = fill['gain'] ** (1 / fill['n_days']) \ if (fill['n_days'] > 0.5 and fill['gain'] > 0.0) else 0.0 except: fill['average_daily_gain'] = 0.0 all_fills.append(fill) if balance <= 0.0 or 'liquidation' in fill['type']: return all_fills, stats, False if do_print: line = f"\r{k / len(ticks):.3f} " line += f"adg {all_fills[-1]['average_daily_gain']:.4f} " line += f"closest_liq {closest_liq:.4f} " print(line, end=' ') tick = ticks[-1] stats_update() return all_fills, stats, True