def backtest(trades_list: [dict], settings: dict): # trades format is [{price: float, buyer_maker: bool}] # no static mode grid_spacing = settings['grid_spacing'] grid_coefficient = settings['grid_coefficient'] price_step = settings['price_step'] qty_step = settings['qty_step'] inverse = settings['inverse'] liq_diff_threshold = settings['liq_diff_threshold'] stop_loss_pos_reduction = settings['stop_loss_pos_reduction'] min_qty = settings['min_qty'] ddown_factor = settings['ddown_factor'] leverage = settings['leverage'] max_leverage = settings['max_leverage'] maker_fee = settings['maker_fee'] taker_fee = settings['taker_fee'] min_markup = settings['min_markup'] max_markup = settings['max_markup'] n_close_orders = settings['n_close_orders'] do_long = settings['do_long'] do_shrt = settings['do_shrt'] min_notional = settings[ 'min_notional'] if 'min_notional' in settings else 0.0 cross_mode = settings['cross_mode'] if 'cross_mode' in settings else True if inverse: calc_cost = lambda qty_, price_: qty_ / price_ if settings['cross_mode']: calc_liq_price = lambda balance_, pos_size_, pos_price_: \ bybit_calc_cross_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=max_leverage) \ if pos_size_ < 0.0 else \ bybit_calc_cross_long_liq_price(balance_, pos_size_, pos_price_, leverage=max_leverage) else: calc_liq_price = lambda balance_, pos_size_, pos_price_: \ bybit_calc_isolated_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \ if pos_size_ < 0.0 else \ bybit_calc_isolated_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) if settings['default_qty'] <= 0.0: calc_default_qty_ = lambda balance_, last_price: \ calc_default_qty(min_qty, qty_step, balance_ * last_price, settings['default_qty']) else: calc_default_qty_ = lambda balance_, last_price: settings[ 'default_qty'] calc_max_pos_size = lambda balance_, price_: balance_ * price_ * leverage else: calc_cost = lambda qty_, price_: qty_ * price_ if settings['cross_mode']: calc_liq_price = lambda balance_, pos_size_, pos_price_: \ binance_calc_cross_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \ if pos_size_ < 0.0 else \ binance_calc_cross_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) else: calc_liq_price = lambda balance_, pos_size_, pos_price_: \ binance_calc_isolated_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \ if pos_size_ < 0.0 else \ binance_calc_isolated_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) if settings['default_qty'] <= 0.0: calc_default_qty_ = lambda balance_, last_price: \ calc_default_qty(max(min_qty, round_up(min_notional / last_price, qty_step)), qty_step, balance_ / last_price, settings['default_qty']) else: calc_default_qty_ = lambda balance_, last_price: \ max(settings['default_qty'], round_up(min_notional / last_price, qty_step)) calc_max_pos_size = lambda balance_, price_: balance_ / price_ * leverage calc_long_reentry_price_ = lambda balance_, pos_margin_, pos_price_, highest_bid_: \ min(highest_bid_, calc_long_reentry_price(price_step, grid_spacing, grid_coefficient, balance_, pos_margin_, pos_price_)) calc_shrt_reentry_price_ = lambda balance_, pos_margin_, pos_price_, lowest_ask_: \ max(lowest_ask_, calc_shrt_reentry_price(price_step, grid_spacing, grid_coefficient, balance_, pos_margin_, pos_price_)) balance = settings['starting_balance'] trades = [] ob = [ min(trades_list[0]['price'], trades_list[1]['price']), max(trades_list[0]['price'], trades_list[1]['price']) ] pos_size = 0.0 pos_price = 0.0 bid_price = ob[0] ask_price = ob[1] liq_price = 0.0 pnl_sum = 0.0 loss_sum = 0.0 profit_sum = 0.0 ema_alpha = 2 / (settings['ema_span'] + 1) ema_alpha_ = 1 - ema_alpha ema = trades_list[0]['price'] k = 0 break_on = { e[0]: eval(e[1]) for e in settings['break_on'] if e[0].startswith('ON:') } for t in trades_list: append_trade = False if t['buyer_maker']: # buy if pos_size == 0.0: # no pos if do_long: bid_price = min(ob[0], round_dn(ema, price_step)) bid_qty = calc_default_qty_(balance, ob[0]) else: bid_price = 0.0 bid_qty = 0.0 elif pos_size > 0.0: if calc_diff( liq_price, ob[1] ) < liq_diff_threshold and t['price'] <= liq_price: # long liq print(f'break on long liquidation, liq price: {liq_price}') return [] # long reentry bid_qty = calc_entry_qty(qty_step, ddown_factor, calc_default_qty_(balance, ob[0]), calc_max_pos_size(balance, ob[0]), pos_size) if bid_qty >= max(min_qty, min_notional / t['price']): pos_margin = calc_cost(pos_size, pos_price) / leverage bid_price = calc_long_reentry_price_( balance, pos_margin, pos_price, ob[0]) else: bid_price = 0.0 else: # short pos if calc_diff(liq_price, ob[0]) < liq_diff_threshold: # short soft stop bid_price = ob[0] bid_qty = round_up(-pos_size * stop_loss_pos_reduction, qty_step) else: if t['price'] <= pos_price: # short close min_close_qty = \ max(min_qty, round_dn(calc_default_qty_(balance, ob[0]) * 0.5, qty_step)) qtys, prices = calc_shrt_closes( price_step, qty_step, min_qty, min_markup, max_markup, min_close_qty, pos_size, pos_price, ob[0], n_close_orders) if len(qtys) > 0: bid_qty = qtys[0] bid_price = prices[0] else: bid_price = 0.0 else: bid_price = 0.0 ob[0] = t['price'] if t['price'] < bid_price and bid_qty >= min_qty: # filled trade qty = bid_qty price = bid_price cost = calc_cost(bid_qty, bid_price) pnl = -cost * maker_fee if pos_size >= 0.0: # create or increase long pos trade_side = 'long' trade_type = 'entry' new_pos_size = pos_size + bid_qty pos_price = pos_price * (pos_size / new_pos_size) + \ bid_price * (bid_qty / new_pos_size) pos_size = new_pos_size roi = 0.0 else: # close short pos trade_side = 'shrt' gain = pos_price / bid_price - 1 pnl += cost * gain if gain > 0.0: trade_type = 'close' profit_sum += pnl else: trade_type = 'stop_loss' loss_sum += pnl pos_size = pos_size + bid_qty roi = gain * leverage balance += pnl pnl_sum += pnl liq_price = calc_liq_price(balance, pos_size, pos_price) append_trade = True else: # sell if pos_size == 0.0: # no pos if do_shrt: ask_price = max(ob[1], round_up(ema, price_step)) ask_qty = -calc_default_qty_(balance, ob[1]) else: ask_price = 9e9 ask_qty = 0.0 elif pos_size > 0.0: # long pos if calc_diff(liq_price, ob[1]) < liq_diff_threshold: # long soft stop ask_price = ob[1] ask_qty = -round_up(pos_size * stop_loss_pos_reduction, qty_step) else: if t['price'] >= pos_price: # long close min_close_qty = \ max(min_qty, round_dn(calc_default_qty_(balance, ob[1]) * 0.5, qty_step)) qtys, prices = calc_long_closes( price_step, qty_step, min_qty, min_markup, max_markup, min_close_qty, pos_size, pos_price, ob[1], n_close_orders) if len(qtys) > 0: ask_qty = qtys[0] ask_price = prices[0] else: ask_price = 9e9 else: ask_price = 9e9 else: if calc_diff( liq_price, ob[1] ) < liq_diff_threshold and t['price'] >= liq_price: # shrt liq print(f'break on shrt liquidation, liq price: {liq_price}') return [] # shrt reentry ask_qty = -calc_entry_qty( qty_step, ddown_factor, calc_default_qty_(balance, ob[1]), calc_max_pos_size(balance, ob[1]), pos_size) if -ask_qty >= max(min_qty, min_notional / t['price']): pos_margin = calc_cost(-pos_size, pos_price) / leverage ask_price = calc_shrt_reentry_price_( balance, pos_margin, pos_price, ob[0]) else: ask_price = 9e9 ob[1] = t['price'] if t['price'] > ask_price and abs(ask_qty) >= min_qty: # filled trade qty = ask_qty price = ask_price cost = calc_cost(-ask_qty, ask_price) pnl = -cost * maker_fee if pos_size <= 0.0: # create or increase shrt pos trade_side = 'shrt' trade_type = 'entry' new_pos_size = pos_size + ask_qty pos_price = pos_price * (pos_size / new_pos_size) + \ ask_price * (ask_qty / new_pos_size) pos_size = new_pos_size roi = 0.0 else: # close long pos trade_side = 'long' gain = ask_price / pos_price - 1 pnl += cost * gain if gain > 0.0: trade_type = 'close' profit_sum += pnl else: trade_type = 'stop_loss' loss_sum += pnl pos_size = pos_size + ask_qty roi = gain * leverage balance += pnl pnl_sum += pnl liq_price = calc_liq_price(balance, pos_size, pos_price) append_trade = True if append_trade: progress = k / len(trades_list) total_gain = (pnl_sum + settings['starting_balance'] ) / settings['starting_balance'] n_days_ = (t['timestamp'] - trades_list[0]['timestamp']) / (1000 * 60 * 60 * 24) adg = total_gain**(1 / n_days_) trades.append({ 'trade_id': k, 'side': trade_side, 'type': trade_type, 'price': price, 'qty': qty, 'pnl': pnl, 'roi': roi, 'pos_size': pos_size, 'pos_price': pos_price, 'balance': balance, 'max_pos_size': calc_max_pos_size(balance, t['price']), 'pnl_sum': pnl_sum, 'loss_sum': loss_sum, 'profit_sum': profit_sum, 'progress': progress, 'liq_price': liq_price, 'gain': total_gain, 'n_days': n_days_, 'average_daily_gain': adg, 'liq_diff': min(1.0, calc_diff(liq_price, t['price'])), 'timestamp': t['timestamp'] }) balance = max(balance, settings['starting_balance']) line = f"\r{progress:.3f} net pnl {pnl_sum:.8f} " line += f"profit sum {profit_sum:.5f} " line += f"loss sum {loss_sum:.5f} " line += f"balance {balance:.5f} qty {calc_default_qty_(balance, ob[0]):.4f} " line += f"adg {trades[-1]['average_daily_gain']:.3f} " line += f"max pos pct {abs(pos_size) / calc_max_pos_size(balance, t['price']):.3f} " line += f"liq diff {min(1.0, calc_diff(trades[-1]['liq_price'], ob[0])):.3f} " line += f"pos size {pos_size:.4f} " print(line, end=' ') for key, condition in break_on.items(): if condition(trades[-1], t): print('break on', key) return [] ema = ema * ema_alpha_ + t['price'] * ema_alpha k += 1 return trades
def backtest(trades_list: [dict], settings: dict): # trades format is [{price: float, buyer_maker: bool}] # no static mode grid_spacing = settings['grid_spacing'] grid_coefficient = settings['grid_coefficient'] price_step = settings['price_step'] qty_step = settings['qty_step'] inverse = settings['inverse'] liq_diff_threshold = settings['liq_diff_threshold'] stop_loss_pos_reduction = settings['stop_loss_pos_reduction'] min_qty = settings['min_qty'] ddown_factor = settings['ddown_factor'] leverage = settings['leverage'] starting_balance = settings['balance'] maker_fee = settings['maker_fee'] taker_fee = settings['taker_fee'] min_markup = settings['min_markup'] max_markup = settings['max_markup'] n_close_orders = settings['n_close_orders'] break_on_loss = settings['break_on_loss'] min_notional = settings[ 'min_notional'] if 'min_notional' in settings else 0.0 if inverse: calc_cost = lambda qty_, price_: qty_ / price_ calc_liq_price = lambda balance_, pos_size_, pos_price_: \ bybit_calc_cross_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \ if pos_size_ < 0.0 else \ bybit_calc_cross_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) if settings['default_qty'] <= 0.0: calc_default_qty_ = lambda balance_, last_price: \ calc_default_qty(min_qty, qty_step, balance_ * last_price, settings['default_qty']) else: calc_default_qty_ = lambda balance_, last_price: settings[ 'default_qty'] calc_max_pos_size = lambda balance_, price_: balance_ * price_ * leverage else: calc_cost = lambda qty_, price_: qty_ * price_ calc_liq_price = lambda balance_, pos_size_, pos_price_: \ binance_calc_cross_shrt_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) \ if pos_size_ < 0.0 else \ binance_calc_cross_long_liq_price(balance_, pos_size_, pos_price_, leverage=leverage) if settings['default_qty'] <= 0.0: calc_default_qty_ = lambda balance_, last_price: \ calc_default_qty(max(min_qty, round_up(min_notional / last_price, qty_step)), qty_step, balance_ / last_price, settings['default_qty']) else: calc_default_qty_ = lambda balance_, last_price: \ max(settings['default_qty'], round_up(min_notional / last_price, qty_step)) calc_max_pos_size = lambda balance_, price_: balance_ / price_ * leverage calc_long_reentry_price_ = lambda balance_, pos_margin_, pos_price_, highest_bid_: \ min(highest_bid_, calc_long_reentry_price(price_step, grid_spacing, grid_coefficient, balance_, pos_margin_, pos_price_)) calc_shrt_reentry_price_ = lambda balance_, pos_margin_, pos_price_, lowest_ask_: \ max(lowest_ask_, calc_shrt_reentry_price(price_step, grid_spacing, grid_coefficient, balance_, pos_margin_, pos_price_)) balance = starting_balance trades = [] ob = [ min(trades_list[0]['price'], trades_list[1]['price']), max(trades_list[0]['price'], trades_list[1]['price']) ] pos_size = 0.0 pos_price = 0.0 bid_price = ob[0] ask_price = ob[1] liq_price = 0.0 pnl_sum = 0.0 loss_sum = 0.0 ema_alpha = 2 / (settings['ema_span'] + 1) ema_alpha_ = 1 - ema_alpha ema = trades_list[0]['price'] k = 0 prev_len_trades = 0 for t in trades_list: if t['buyer_maker']: # buy if pos_size == 0.0: # no pos bid_price = min(ob[0], round_dn(ema, price_step)) bid_qty = calc_default_qty_(balance, ob[0]) elif pos_size > 0.0: # long pos liq_price = calc_liq_price(balance, pos_size, pos_price) if calc_diff(liq_price, ob[0]) < liq_diff_threshold: # long soft stop, no reentry bid_price = 0.0 else: # long reentry bid_qty = calc_entry_qty(qty_step, ddown_factor, calc_default_qty_(balance, ob[0]), calc_max_pos_size(balance, ob[0]), pos_size) if bid_qty >= min_qty: pos_margin = calc_cost(pos_size, pos_price) / leverage bid_price = calc_long_reentry_price_( balance, pos_margin, pos_price, ob[0]) else: bid_price = 0.0 else: # short pos liq_price = calc_liq_price(balance, pos_size, pos_price) if calc_diff(liq_price, ob[0]) < liq_diff_threshold: # short soft stop bid_price = ob[0] bid_qty = round_up(-pos_size * stop_loss_pos_reduction, qty_step) else: if t['price'] <= pos_price: # short close qtys, prices = calc_shrt_closes( price_step, qty_step, min_qty, min_markup, max_markup, pos_size, pos_price, ob[0], n_close_orders) bid_qty = qtys[0] bid_price = prices[0] else: bid_price = 0.0 ob[0] = t['price'] if t['price'] < bid_price: # filled trade cost = calc_cost(bid_qty, bid_price) pnl = -cost * maker_fee if pos_size >= 0.0: # create or increase long pos trade_side = 'long' trade_type = 'entry' new_pos_size = pos_size + bid_qty pos_price = pos_price * (pos_size / new_pos_size) + \ bid_price * (bid_qty / new_pos_size) pos_size = new_pos_size roi = 0.0 else: # close short pos trade_side = 'shrt' gain = pos_price / bid_price - 1 pnl += cost * gain trade_type = 'close' if gain > 0.0 else 'stop_loss' pos_size = pos_size + bid_qty roi = gain * leverage balance += pnl pnl_sum += pnl trades.append({ 'trade_id': k, 'side': trade_side, 'type': trade_type, 'price': bid_price, 'qty': bid_qty, 'pnl': pnl, 'roi': roi, 'pos_size': pos_size, 'pos_price': pos_price, 'liq_price': calc_liq_price(balance, pos_size, pos_price) }) else: # sell if pos_size == 0.0: # no pos ask_price = max(ob[1], round_up(ema, price_step)) ask_qty = -calc_default_qty_(balance, ob[1]) elif pos_size > 0.0: # long pos liq_price = calc_liq_price(balance, pos_size, pos_price) if calc_diff(liq_price, ob[1]) < liq_diff_threshold: # long soft stop ask_price = ob[1] ask_qty = -round_up(pos_size * stop_loss_pos_reduction, qty_step) else: if t['price'] >= pos_price: # long close qtys, prices = calc_long_closes( price_step, qty_step, min_qty, min_markup, max_markup, pos_size, pos_price, ob[1], n_close_orders) ask_qty = qtys[0] ask_price = prices[0] else: ask_price = 9e9 else: # short pos liq_price = calc_liq_price(balance, pos_size, pos_price) if calc_diff(liq_price, ob[1]) < liq_diff_threshold: # shrt soft stop, no reentry ask_price = 9e9 else: # shrt reentry ask_qty = -calc_entry_qty( qty_step, ddown_factor, calc_default_qty_(balance, ob[1]), calc_max_pos_size(balance, ob[1]), pos_size) if -ask_qty >= min_qty: pos_margin = calc_cost(-pos_size, pos_price) / leverage ask_price = calc_shrt_reentry_price_( balance, pos_margin, pos_price, ob[0]) else: ask_price = 9e9 ob[1] = t['price'] if t['price'] > ask_price: # filled trade cost = calc_cost(-ask_qty, ask_price) pnl = -cost * maker_fee if pos_size <= 0.0: # create or increase shrt pos trade_side = 'shrt' trade_type = 'entry' new_pos_size = pos_size + ask_qty pos_price = pos_price * (pos_size / new_pos_size) + \ ask_price * (ask_qty / new_pos_size) pos_size = new_pos_size roi = 0.0 else: # close long pos trade_side = 'long' gain = ask_price / pos_price - 1 pnl += cost * gain trade_type = 'close' if gain > 0.0 else 'stop_loss' pos_size = pos_size + ask_qty roi = gain * leverage balance += pnl pnl_sum += pnl trades.append({ 'trade_id': k, 'side': trade_side, 'type': trade_type, 'price': ask_price, 'qty': ask_qty, 'pnl': pnl, 'roi': roi, 'pos_size': pos_size, 'pos_price': pos_price, 'liq_price': calc_liq_price(balance, pos_size, pos_price) }) ema = ema * ema_alpha_ + t['price'] * ema_alpha k += 1 if k % 10000 == 0 or len(trades) != prev_len_trades: if trades and trades[-1]['type'] == 'stop_loss': loss_sum += pnl if break_on_loss: print('break on loss') return trades balance = max(balance, settings['balance']) prev_len_trades = len(trades) progress = k / len(trades_list) if pnl_sum < 0.0 and progress >= settings['break_on_negative_pnl']: print('break on negative pnl') return trades line = f"\r{progress:.3f} pnl sum {pnl_sum:.8f} " line += f"loss sum {loss_sum:.6f} balance {balance:.6f} " line += f"qty {calc_default_qty_(balance, ob[0]):.4f} " line += f"liq diff {min(1.0, calc_diff(trades[-1]['liq_price'], ob[0])):.3f} " line += f"pos size {pos_size:.6f} " print(line, end=' ') return trades