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
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(ticks: np.ndarray, settings: dict): # ticks formatting [price: float, buyer_maker: bool, timestamp: float] ss = settings pos_size, pos_price, reentry_price, reentry_qty, liq_price = 0.0, 0.0, 0.0, 0.0, 0.0 closest_long_liq, closest_shrt_liq = 1.0, 1.0 stop_loss_liq_diff_price, stop_loss_pos_price_diff_price, stop_loss_price = 0.0, 0.0, 0.0 actual_balance = ss['starting_balance'] apparent_balance = actual_balance * ss['balance_pct'] pnl_plus_fees_cumsum, loss_cumsum, profit_cumsum, fee_paid_cumsum = 0.0, 0.0, 0.0, 0.0 if ss['inverse']: min_qty_f = calc_min_qty_inverse long_pnl_f = calc_long_pnl_inverse shrt_pnl_f = calc_shrt_pnl_inverse cost_f = calc_cost_inverse pos_margin_f = calc_margin_cost_inverse max_pos_size_f = calc_max_pos_size_inverse min_entry_qty_f = calc_min_entry_qty_inverse long_liq_price_f = lambda bal, psize, pprice: \ bybit_calc_cross_long_liq_price(bal, psize, pprice, ss['max_leverage']) shrt_liq_price_f = lambda bal, psize, pprice: \ bybit_calc_cross_shrt_liq_price(bal, psize, pprice, ss['max_leverage']) else: min_qty_f = calc_min_qty_linear long_pnl_f = calc_long_pnl_linear shrt_pnl_f = calc_shrt_pnl_linear cost_f = calc_cost_linear pos_margin_f = calc_margin_cost_linear max_pos_size_f = calc_max_pos_size_linear min_entry_qty_f = calc_min_entry_qty_linear long_liq_price_f = lambda bal, psize, pprice: \ binance_calc_cross_long_liq_price(bal, psize, pprice, ss['leverage']) shrt_liq_price_f = lambda bal, psize, pprice: \ binance_calc_cross_shrt_liq_price(bal, psize, pprice, ss['leverage']) break_on = {e[0]: eval(e[1]) for e in settings['break_on'] if e[0].startswith('ON:')} ema = ticks[0][0] ema_alpha = 2 / (ss['ema_span'] + 1) ema_alpha_ = 1 - ema_alpha prev_trade_ts = 0 min_trade_delay_millis = ss['latency_simulation_ms'] if 'latency_simulation_ms' in ss else 1000 trades = [] ob = [min(ticks[0][0], ticks[1][0]), max(ticks[0][0], ticks[1][0])] for k, t in enumerate(ticks): did_trade = False if t[1]: # maker buy, taker sel if pos_size == 0.0: # create long pos if ss['do_long']: price = calc_no_pos_bid_price(ss['price_step'], ss['ema_spread'], ema, ob[0]) if t[0] < price and ss['do_long']: did_trade = True qty = min_entry_qty_f(ss['qty_step'], ss['min_qty'], ss['min_cost'], ss['entry_qty_pct'], ss['leverage'], apparent_balance, price) trade_type, trade_side = 'entry', 'long' pnl = 0.0 fee_paid = -cost_f(qty, price) * ss['maker_fee'] elif pos_size > 0.0: closest_long_liq = min(calc_diff(liq_price, t[0]), closest_long_liq) if t[0] <= liq_price and closest_long_liq < 0.2: # long liquidation print('\nlong liquidation') return [] if t[0] < reentry_price: # add to long pos did_trade, qty, price = True, reentry_qty, reentry_price trade_type, trade_side = 'reentry', 'long' pnl = 0.0 fee_paid = -cost_f(qty, price) * ss['maker_fee'] # check if long stop loss triggered if t[0] <= stop_loss_liq_diff_price: stop_loss_price = ob[1] stop_loss_type = 'stop_loss_liq_diff' elif t[0] <= stop_loss_pos_price_diff_price: stop_loss_price = ob[1] stop_loss_type = 'stop_loss_pos_price_diff' else: stop_loss_price = 0.0 else: if t[0] <= pos_price: # close shrt pos min_close_qty = calc_min_close_qty( ss['qty_step'], ss['min_qty'], ss['min_close_qty_multiplier'], min_entry_qty_f(ss['qty_step'], ss['min_qty'], ss['min_cost'], ss['entry_qty_pct'], ss['leverage'], apparent_balance, t[0]) ) qtys, prices = calc_shrt_closes(ss['price_step'], ss['qty_step'], min_close_qty, ss['min_markup'], ss['max_markup'], pos_size, pos_price, ob[0], ss['n_close_orders']) if t[0] < prices[0]: did_trade, qty, price = True, qtys[0], prices[0] trade_type, trade_side = 'close', 'shrt' pnl = shrt_pnl_f(pos_price, price, qty) fee_paid = -cost_f(qty, price) * ss['maker_fee'] elif t[0] < stop_loss_price: # shrt stop loss did_trade = True qty = calc_pos_reduction_qty(ss['qty_step'], ss['stop_loss_pos_reduction'], pos_size) price = stop_loss_price trade_type, trade_side = stop_loss_type, 'shrt' pnl = shrt_pnl_f(pos_price, price, qty) fee_paid = -cost_f(qty, price) * ss['maker_fee'] ob[0] = t[0] else: # maker sel, taker buy if pos_size == 0.0: # create shrt pos if ss['do_shrt']: price = calc_no_pos_ask_price(ss['price_step'], ss['ema_spread'], ema, ob[1]) if t[0] > price: did_trade = True qty = -min_entry_qty_f(ss['qty_step'], ss['min_qty'], ss['min_cost'], ss['entry_qty_pct'], ss['leverage'], apparent_balance, price) trade_type, trade_side = 'entry', 'shrt' pnl = 0.0 fee_paid = -cost_f(-qty, price) * ss['maker_fee'] elif pos_size < 0.0: closest_shrt_liq = min(calc_diff(liq_price, t[0]), closest_shrt_liq) if t[0] >= liq_price and closest_shrt_liq < 0.2: # shrt liquidation print('\nshrt liquidation') return [] if t[0] > reentry_price: # add to shrt pos did_trade, qty, price = True, reentry_qty, reentry_price trade_type, trade_side = 'reentry', 'shrt' pnl = 0.0 fee_paid = -cost_f(-qty, price) * ss['maker_fee'] # check if shrt stop loss triggered if t[0] >= stop_loss_liq_diff_price: stop_loss_price = ob[0] stop_loss_type = 'stop_loss_liq_diff' elif t[0] >= stop_loss_pos_price_diff_price: stop_loss_price = ob[0] stop_loss_type = 'stop_loss_pos_price_diff' else: stop_loss_price = 0.0 else: # close long pos if t[0] >= pos_price: min_close_qty = calc_min_close_qty( ss['qty_step'], ss['min_qty'], ss['min_close_qty_multiplier'], min_entry_qty_f(ss['qty_step'], ss['min_qty'], ss['min_cost'], ss['entry_qty_pct'], ss['leverage'], apparent_balance, t[0]) ) qtys, prices = calc_long_closes(ss['price_step'], ss['qty_step'], min_close_qty, ss['min_markup'], ss['max_markup'], pos_size, pos_price, ob[1], ss['n_close_orders']) if t[0] > prices[0]: did_trade, qty, price = True, qtys[0], prices[0] trade_type, trade_side = 'close', 'long' pnl = long_pnl_f(pos_price, price, -qty) fee_paid = - cost_f(-qty, price) * ss['maker_fee'] elif stop_loss_price > 0.0 and t[0] > stop_loss_price: # long stop loss did_trade = True qty = -calc_pos_reduction_qty(ss['qty_step'], ss['stop_loss_pos_reduction'], pos_size) price = stop_loss_price trade_type, trade_side = stop_loss_type, 'long' pnl = long_pnl_f(pos_price, price, qty) fee_paid = -cost_f(-qty, price) * ss['maker_fee'] ob[1] = t[0] ema = ema * ema_alpha_ + t[0] * ema_alpha if did_trade: if t[2] - prev_trade_ts < min_trade_delay_millis: if trade_type == 'reentry': # because of live bot's multiple open orders, # allow consecutive reentries whose timestamp diff < min delay if trades[-1]['type'] != 'reentry': continue else: continue prev_trade_ts = t[2] new_pos_size = round_(pos_size + qty, 0.0000000001) if 'entry' in trade_type: pos_price = pos_price * abs(pos_size / new_pos_size) + \ price * abs(qty / new_pos_size) if new_pos_size else np.nan pos_size = new_pos_size actual_balance = max(ss['starting_balance'], actual_balance + pnl + fee_paid) apparent_balance = actual_balance * ss['balance_pct'] if pos_size == 0.0: liq_price = 0.0 elif pos_size > 0.0: liq_price = long_liq_price_f(actual_balance, pos_size, pos_price) else: liq_price = shrt_liq_price_f(actual_balance, pos_size, pos_price) if liq_price < 0.0: liq_price = 0.0 progress = k / len(ticks) pnl_plus_fee = pnl + fee_paid pnl_plus_fees_cumsum += pnl_plus_fee if trade_type.startswith('stop_loss'): loss_cumsum += pnl else: profit_cumsum += pnl fee_paid_cumsum += fee_paid total_gain = (pnl_plus_fees_cumsum + settings['starting_balance']) / settings['starting_balance'] n_days_ = (t[2] - ticks[0][2]) / (1000 * 60 * 60 * 24) try: adg = total_gain ** (1 / n_days_) if (n_days_ > 0.0 and total_gain > 0.0) else 0.0 except: adg = 0.0 avg_gain_per_tick = \ (actual_balance / settings['starting_balance']) ** (1 / (len(trades) + 1)) millis_since_prev_trade = t[2] - trades[-1]['timestamp'] if trades else 0.0 trades.append({'trade_id': k, 'side': trade_side, 'type': trade_type, 'price': price, 'qty': qty, 'pos_price': pos_price, 'pos_size': pos_size, 'liq_price': liq_price, 'pnl': pnl, 'fee_paid': fee_paid, 'pnl_plus_fee': pnl_plus_fee, 'fee_paid_cumsum': fee_paid_cumsum, 'apparent_balance': apparent_balance, 'actual_balance': actual_balance, 'profit_cumsum': profit_cumsum, 'loss_cumsum': loss_cumsum, 'pnl_plus_fees_cumsum': pnl_plus_fees_cumsum, 'average_daily_gain': adg, 'timestamp': t[2], 'closest_long_liq': closest_long_liq, 'closest_shrt_liq': closest_shrt_liq, 'closest_liq': min(closest_long_liq, closest_shrt_liq), 'avg_gain_per_tick': avg_gain_per_tick, 'millis_since_prev_trade': millis_since_prev_trade, 'progress': progress}) closest_long_liq, closest_shrt_liq = 1.0, 1.0 for key, condition in break_on.items(): if condition(trades, ticks, k): print('break on', key) return [] if pos_size > 0.0: stop_loss_liq_diff_price = liq_price * (1 + ss['stop_loss_liq_diff']) stop_loss_pos_price_diff_price = pos_price * (1 - ss['stop_loss_pos_price_diff']) stop_loss_price = 0.0 reentry_price = min( ob[0], calc_long_reentry_price(ss['price_step'], ss['grid_spacing'], ss['grid_coefficient'], apparent_balance, pos_margin_f(ss['leverage'], pos_size, pos_price), pos_price) ) reentry_price = max(ss['price_step'], reentry_price) min_qty_ = min_qty_f(ss['qty_step'], ss['min_qty'], ss['min_cost'], reentry_price) reentry_qty = calc_reentry_qty(ss['qty_step'], ss['ddown_factor'], min_qty_, max_pos_size_f(ss['leverage'], apparent_balance, reentry_price), pos_size) if reentry_qty < min_qty_: reentry_price = ss['price_step'] trades[-1]['reentry_price'] = reentry_price elif pos_size < 0.0: stop_loss_liq_diff_price = liq_price * (1 - ss['stop_loss_liq_diff']) \ if liq_price > 0.0 else pos_price * 10000 stop_loss_pos_price_diff_price = pos_price * (1 + ss['stop_loss_pos_price_diff']) stop_loss_price = 0.0 reentry_price = max([ ss['price_step'], ob[1], calc_shrt_reentry_price(ss['price_step'], ss['grid_spacing'], ss['grid_coefficient'], apparent_balance, pos_margin_f(ss['leverage'], pos_size, pos_price), pos_price) ]) min_qty_ = min_qty_f(ss['qty_step'], ss['min_qty'], ss['min_cost'], reentry_price) reentry_qty = -calc_reentry_qty(ss['qty_step'], ss['ddown_factor'], min_qty_, max_pos_size_f(ss['leverage'], apparent_balance, reentry_price), pos_size) if -reentry_qty < min_qty_: reentry_price = 9e12 trades[-1]['reentry_price'] = reentry_price else: trades[-1]['reentry_price'] = np.nan line = f"\r{progress:.3f} pnl plus fees cumsum {pnl_plus_fees_cumsum:.8f} " line += f"profit cumsum {profit_cumsum:.5f} " line += f"loss cumsum {loss_cumsum:.5f} " line += f"actual_bal {actual_balance:.4f} " line += f"apparent_bal {apparent_balance:.4f} " #line += f"qty {calc_min_entry_qty_(apparent_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(apparent_balance, t[0]):.3f} " line += f"pos size {pos_size:.4f} " print(line, end=' ') return trades