def add_pbrs_to_pos(self, position_: dict): position = position_.copy() position['long']['pbr'] = (qty_to_cost(position['long']['size'], position['long']['price'], self.xk['inverse'], self.xk['c_mult']) / position['wallet_balance']) if position['wallet_balance'] else 0.0 position['shrt']['pbr'] = (qty_to_cost(position['shrt']['size'], position['shrt']['price'], self.xk['inverse'], self.xk['c_mult']) / position['wallet_balance']) if position['wallet_balance'] else 0.0 return position
def add_wallet_exposures_to_pos(self, position_: dict): position = position_.copy() position["long"]["wallet_exposure"] = ((qty_to_cost( position["long"]["size"], position["long"]["price"], self.xk["inverse"], self.xk["c_mult"], ) / position["wallet_balance"]) if position["wallet_balance"] else 0.0) position["short"]["wallet_exposure"] = ((qty_to_cost( position["short"]["size"], position["short"]["price"], self.xk["inverse"], self.xk["c_mult"], ) / position["wallet_balance"]) if position["wallet_balance"] else 0.0) return position
def calc_orders(self): default_orders = super().calc_orders() orders = [] remaining_cost = self.balance[self.quot]["onhand"] for order in sorted(default_orders, key=lambda x: calc_diff(x["price"], self.price)): if order["price"] > min( self.max_price, round_dn(self.price * self.price_multiplier_up, self.price_step), ): print(f'price {order["price"]} too high') continue if order["price"] < max( self.min_price, round_up(self.price * self.price_multiplier_dn, self.price_step), ): print(f'price {order["price"]} too low') continue if order["side"] == "buy": cost = qty_to_cost(order["qty"], order["price"], self.inverse, self.c_mult) if cost > remaining_cost: adjusted_qty = round_dn(remaining_cost / order["price"], self.qty_step) min_entry_qty = calc_min_entry_qty( order["price"], self.inverse, self.qty_step, self.min_qty, self.min_cost, ) if adjusted_qty >= min_entry_qty: orders.append({**order, **{"qty": adjusted_qty}}) remaining_cost = 0.0 else: orders.append(order) remaining_cost -= cost else: orders.append(order) return orders
def analyze_fills(fills_long: list, fills_short: list, stats: list, config: dict) -> (pd.DataFrame, pd.DataFrame, dict): sdf = pd.DataFrame( stats, columns=[ "timestamp", "bkr_price_long", "bkr_price_short", "psize_long", "pprice_long", "psize_short", "pprice_short", "price", "closest_bkr_long", "closest_bkr_short", "balance_long", "balance_short", "equity_long", "equity_short", ], ) longs = pd.DataFrame( fills_long, columns=[ "trade_id", "timestamp", "pnl", "fee_paid", "balance", "equity", "qty", "price", "psize", "pprice", "type", ], ) longs.index = longs.timestamp shorts = pd.DataFrame( fills_short, columns=[ "trade_id", "timestamp", "pnl", "fee_paid", "balance", "equity", "qty", "price", "psize", "pprice", "type", ], ) shorts.index = shorts.timestamp longs.loc[:, "wallet_exposure"] = [ qty_to_cost(x.psize, x.pprice, config["inverse"], config["c_mult"]) / x.balance if x.balance > 0.0 else 0.0 for x in longs.itertuples() ] shorts.loc[:, "wallet_exposure"] = [ qty_to_cost(x.psize, x.pprice, config["inverse"], config["c_mult"]) / x.balance if x.balance > 0.0 else 0.0 for x in shorts.itertuples() ] sdf.loc[:, "wallet_exposure_long"] = [ qty_to_cost(x.psize_long, x.pprice_long, config["inverse"], config["c_mult"]) / x.balance_long if x.balance_long > 0.0 else 0.0 for x in sdf.itertuples() ] sdf.loc[:, "wallet_exposure_short"] = [ qty_to_cost(x.psize_short, x.pprice_short, config["inverse"], config["c_mult"]) / x.balance_short if x.balance_short > 0.0 else 0.0 for x in sdf.itertuples() ] n_days = (sdf.timestamp.iloc[-1] - sdf.timestamp.iloc[0]) / (1000 * 60 * 60 * 24) pos_changes_long = sdf[sdf.psize_long != sdf.psize_long.shift()] pos_changes_long_ms_diff = np.diff( list(pos_changes_long.timestamp) + [sdf.timestamp.iloc[-1]]) hrs_stuck_max_long = pos_changes_long_ms_diff.max() / (1000 * 60 * 60) hrs_stuck_avg_long = pos_changes_long_ms_diff.mean() / (1000 * 60 * 60) pos_changes_short = sdf[sdf.psize_short != sdf.psize_short.shift()] pos_changes_short_ms_diff = np.diff( list(pos_changes_short.timestamp) + [sdf.timestamp.iloc[-1]]) hrs_stuck_max_short = pos_changes_short_ms_diff.max() / (1000 * 60 * 60) hrs_stuck_avg_short = pos_changes_short_ms_diff.mean() / (1000 * 60 * 60) lpprices = sdf[sdf.psize_long != 0.0] spprices = sdf[sdf.psize_short != 0.0] pa_distance_long = (((lpprices.pprice_long - lpprices.price).abs() / lpprices.price) if len(lpprices) > 0 else pd.Series([100.0])) pa_distance_short = (((spprices.pprice_short - spprices.price).abs() / spprices.price) if len(spprices) > 0 else pd.Series([100.0])) gain_long = longs.pnl.sum() / sdf.balance_long.iloc[0] gain_short = shorts.pnl.sum() / sdf.balance_short.iloc[0] ms2d = 1000 * 60 * 60 * 24 if len(longs) > 0: daily_equity_long = sdf.groupby(sdf.timestamp // ms2d).equity_long.last() daily_gains_long = daily_equity_long / daily_equity_long.shift(1) - 1 adg_long = daily_gains_long.mean() DGstd_long = daily_gains_long.std() adg_DGstd_ratio_long = adg_long / DGstd_long if len( daily_gains_long) > 0 else 0.0 if any("bankruptcy" in e for e in longs.type.unique()): adg_long = 0.01**( 1 / n_days) - 1 # reward bankrupt runs lasting longer else: adg_long = adg_DGstd_ratio_long = 0.0 DGstd_long = 100.0 if len(shorts) > 0: daily_equity_short = sdf.groupby(sdf.timestamp // ms2d).equity_short.last() daily_gains_short = daily_equity_short / daily_equity_short.shift( 1) - 1 adg_short = daily_gains_short.mean() DGstd_short = daily_gains_short.std() adg_DGstd_ratio_short = adg_short / DGstd_short if len( daily_gains_short) > 0 else 0.0 if any("bankruptcy" in e for e in shorts.type.unique()): adg_short = 0.01**( 1 / n_days) - 1 # reward bankrupt runs lasting longer else: adg_short = adg_DGstd_ratio_short = 0.0 DGstd_short = 100.0 pos_costs_long = longs.apply( lambda x: qty_to_cost(x["psize"], x["pprice"], config["inverse"], config["c_mult"]), axis=1, ) pos_costs_short = shorts.apply( lambda x: qty_to_cost(x["psize"], x["pprice"], config["inverse"], config["c_mult"]), axis=1, ) biggest_pos_cost_long = pos_costs_long.max() if len(longs) > 0 else 0.0 biggest_pos_cost_short = pos_costs_short.max() if len(shorts) > 0 else 0.0 volume_quote_long = (longs.apply( lambda x: qty_to_cost(x["qty"], x["price"], config["inverse"], config[ "c_mult"]), axis=1, ).sum() if len(longs) > 0 else 0.0) volume_quote_short = (shorts.apply( lambda x: qty_to_cost(x["qty"], x["price"], config["inverse"], config[ "c_mult"]), axis=1, ).sum() if len(shorts) > 0 else 0.0) analysis = { "exchange": config["exchange"] if "exchange" in config else "unknown", "symbol": config["symbol"] if "symbol" in config else "unknown", "starting_balance": sdf.balance_long.iloc[0], "pa_distance_mean_long": pa_distance_long.mean(), "pa_distance_max_long": pa_distance_long.max(), "pa_distance_std_long": pa_distance_long.std(), "pa_distance_mean_short": pa_distance_short.mean(), "pa_distance_max_short": pa_distance_short.max(), "pa_distance_std_short": pa_distance_short.std(), "gain_long": gain_long, "adg_long": adg_long if adg_long == adg_long else -1.0, "adg_per_exposure_long": adg_long / config["long"]["wallet_exposure_limit"] if config["long"]["enabled"] and config["long"]["wallet_exposure_limit"] > 0.0 else 0.0, "gain_short": gain_short, "adg_short": adg_short if adg_short == adg_short else -1.0, "adg_per_exposure_short": adg_short / config["short"]["wallet_exposure_limit"] if config["short"]["enabled"] and config["short"]["wallet_exposure_limit"] > 0.0 else 0.0, "adg_DGstd_ratio_long": adg_DGstd_ratio_long, "adg_DGstd_ratio_short": adg_DGstd_ratio_short, "DGstd_long": DGstd_long, "DGstd_short": DGstd_short, "n_days": n_days, "n_fills_long": len(fills_long), "n_fills_short": len(fills_short), "n_closes_long": len(longs[longs.type.str.contains("close")]), "n_closes_short": len(shorts[shorts.type.str.contains("close")]), "n_normal_closes_long": len(longs[longs.type.str.contains("nclose")]), "n_normal_closes_short": len(shorts[shorts.type.str.contains("nclose")]), "n_entries_long": len(longs[longs.type.str.contains("entry")]), "n_entries_short": len(shorts[shorts.type.str.contains("entry")]), "n_ientries_long": len(longs[longs.type.str.contains("ientry")]), "n_ientries_short": len(shorts[shorts.type.str.contains("ientry")]), "n_rentries_long": len(longs[longs.type.str.contains("rentry")]), "n_rentries_short": len(shorts[shorts.type.str.contains("rentry")]), "n_unstuck_closes_long": len(longs[longs.type.str.contains("unstuck_close")]), "n_unstuck_closes_short": len(shorts[shorts.type.str.contains("unstuck_close")]), "n_unstuck_entries_long": len(longs[longs.type.str.contains("unstuck_entry")]), "n_unstuck_entries_short": len(shorts[shorts.type.str.contains("unstuck_entry")]), "avg_fills_per_day_long": len(longs) / n_days, "avg_fills_per_day_short": len(shorts) / n_days, "hrs_stuck_max_long": hrs_stuck_max_long, "hrs_stuck_avg_long": hrs_stuck_avg_long, "hrs_stuck_max_short": hrs_stuck_max_short, "hrs_stuck_avg_short": hrs_stuck_avg_short, "hrs_stuck_max": max(hrs_stuck_max_long, hrs_stuck_max_short), "hrs_stuck_avg": max(hrs_stuck_avg_long, hrs_stuck_avg_short), "loss_sum_long": longs[longs.pnl < 0.0].pnl.sum(), "loss_sum_short": shorts[shorts.pnl < 0.0].pnl.sum(), "profit_sum_long": longs[longs.pnl > 0.0].pnl.sum(), "profit_sum_short": shorts[shorts.pnl > 0.0].pnl.sum(), "pnl_sum_long": (pnl_sum_long := longs.pnl.sum()), "pnl_sum_short": (pnl_sum_short := shorts.pnl.sum()), "fee_sum_long": (fee_sum_long := longs.fee_paid.sum()), "fee_sum_short": (fee_sum_short := shorts.fee_paid.sum()), "net_pnl_plus_fees_long": pnl_sum_long + fee_sum_long, "net_pnl_plus_fees_short": pnl_sum_short + fee_sum_short, "final_equity_long": sdf.equity_long.iloc[-1], "final_balance_long": sdf.balance_long.iloc[-1], "final_equity_short": sdf.equity_short.iloc[-1], "final_balance_short": sdf.balance_short.iloc[-1], "closest_bkr_long": sdf.closest_bkr_long.min(), "closest_bkr_short": sdf.closest_bkr_short.min(), "eqbal_ratio_min_long": (eqbal_ratios_long := sdf.equity_long / sdf.balance_long).min(), "eqbal_ratio_mean_long": eqbal_ratios_long.mean(), "eqbal_ratio_min_short": (eqbal_ratios_short := sdf.equity_short / sdf.balance_short).min(), "eqbal_ratio_mean_short": eqbal_ratios_short.mean(), "biggest_psize_long": longs.psize.abs().max(), "biggest_psize_short": shorts.psize.abs().max(), "biggest_psize_quote_long": biggest_pos_cost_long, "biggest_psize_quote_short": biggest_pos_cost_short, "volume_quote_long": volume_quote_long, "volume_quote_short": volume_quote_short, }
def analyze_fills(fills: list, stats: list, config: dict) -> (pd.DataFrame, pd.DataFrame, dict): sdf = pd.DataFrame(stats, columns=['timestamp', 'balance', 'equity', 'bkr_price', 'long_psize', 'long_pprice', 'shrt_psize', 'shrt_pprice', 'price', 'closest_bkr']) fdf = pd.DataFrame(fills, columns=['trade_id', 'timestamp', 'pnl', 'fee_paid', 'balance', 'equity', 'qty', 'price', 'psize', 'pprice', 'type']) fdf.loc[:, 'pbr'] = [qty_to_cost(x.psize, x.pprice, config['inverse'], config['c_mult']) / x.balance if x.balance > 0.0 else 0.0 for x in fdf.itertuples()] sdf.loc[:, 'long_pbr'] = [qty_to_cost(x.long_psize, x.long_pprice, config['inverse'], config['c_mult']) / x.balance if x.balance > 0.0 else 0.0 for x in sdf.itertuples()] sdf.loc[:, 'shrt_pbr'] = [qty_to_cost(x.shrt_psize, x.shrt_pprice, config['inverse'], config['c_mult']) / x.balance if x.balance > 0.0 else 0.0 for x in sdf.itertuples()] gain = sdf.balance.iloc[-1] / sdf.balance.iloc[0] n_days = (sdf.timestamp.iloc[-1] - sdf.timestamp.iloc[0]) / (1000 * 60 * 60 * 24) adg = gain ** (1 / n_days) - 1 gain -= 1 fills_per_day = len(fills) / n_days long_pos_changes = sdf[sdf.long_psize != sdf.long_psize.shift()] long_pos_changes_ms_diff = np.diff([sdf.timestamp.iloc[0]] + list(long_pos_changes.timestamp) + [sdf.timestamp.iloc[-1]]) hrs_stuck_max_long = long_pos_changes_ms_diff.max() / (1000 * 60 * 60) hrs_stuck_avg_long = long_pos_changes_ms_diff.mean() / (1000 * 60 * 60) shrt_pos_changes = sdf[sdf.shrt_psize != sdf.shrt_psize.shift()] shrt_pos_changes_ms_diff = np.diff([sdf.timestamp.iloc[0]] + list(shrt_pos_changes.timestamp) + [sdf.timestamp.iloc[-1]]) hrs_stuck_max_shrt = shrt_pos_changes_ms_diff.max() / (1000 * 60 * 60) hrs_stuck_avg_shrt = shrt_pos_changes_ms_diff.mean() / (1000 * 60 * 60) lpprices = sdf[sdf.long_pprice != 0.0] spprices = sdf[sdf.shrt_pprice != 0.0] pa_closeness_long = ((lpprices.long_pprice - lpprices.price).abs() / lpprices.price) if len(lpprices) > 0 else pd.Series([100.0]) pa_closeness_shrt = ((spprices.shrt_pprice - spprices.price).abs() / spprices.price) if len(spprices) > 0 else pd.Series([100.0]) analysis = { 'exchange': config['exchange'] if 'exchange' in config else 'unknown', 'symbol': config['symbol'] if 'symbol' in config else 'unknown', 'starting_balance': sdf.balance.iloc[0], 'pa_closeness_mean_long': pa_closeness_long.mean(), 'pa_closeness_median_long': pa_closeness_long.median(), 'pa_closeness_max_long': pa_closeness_long.max(), 'pa_closeness_mean_shrt': pa_closeness_shrt.mean(), 'pa_closeness_median_shrt': pa_closeness_shrt.median(), 'pa_closeness_max_shrt': pa_closeness_shrt.max(), 'average_daily_gain': adg, 'adjusted_daily_gain': np.tanh(20 * adg) / 20, 'gain': gain, 'n_days': n_days, 'n_fills': len(fills), 'n_entries': len(fdf[fdf.type.str.contains('entry')]), 'n_closes': len(fdf[fdf.type.str.contains('close')]), 'n_ientries': len(fdf[fdf.type.str.contains('ientry')]), 'n_rentries': len(fdf[fdf.type.str.contains('rentry')]), 'avg_fills_per_day': fills_per_day, 'hrs_stuck_max_long': hrs_stuck_max_long, 'hrs_stuck_avg_long': hrs_stuck_avg_long, 'hrs_stuck_max': hrs_stuck_max_long, 'hrs_stuck_avg': hrs_stuck_avg_long, 'hrs_stuck_max_shrt': hrs_stuck_max_shrt, 'hrs_stuck_avg_shrt': hrs_stuck_avg_shrt, 'hrs_stuck_max': hrs_stuck_max_shrt, 'hrs_stuck_avg': hrs_stuck_avg_shrt, 'loss_sum': fdf[fdf.pnl < 0.0].pnl.sum(), 'profit_sum': fdf[fdf.pnl > 0.0].pnl.sum(), 'pnl_sum': (pnl_sum := fdf.pnl.sum()), 'fee_sum': (fee_sum := fdf.fee_paid.sum()), 'net_pnl_plus_fees': pnl_sum + fee_sum, 'final_equity': sdf.equity.iloc[-1], 'final_balance': sdf.balance.iloc[-1], 'closest_bkr': sdf.closest_bkr.min(), 'eqbal_ratio_min': (eqbal_ratios := sdf.equity / sdf.balance).min(), 'eqbal_ratio_mean': eqbal_ratios.mean(), 'eqbal_ratio_median': eqbal_ratios.median(), 'biggest_psize': fdf.psize.abs().max(), }
def calc_recursive_entry_long( balance, psize, pprice, highest_bid, ema_band_lower, inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct, initial_eprice_ema_dist, ddown_factor, rentry_pprice_dist, rentry_pprice_dist_wallet_exposure_weighting, wallet_exposure_limit, auto_unstuck_ema_dist, auto_unstuck_wallet_exposure_threshold, ): ientry_price = max( price_step, min( highest_bid, round_dn(ema_band_lower * (1 - initial_eprice_ema_dist), price_step)), ) min_entry_qty = calc_min_entry_qty(ientry_price, inverse, qty_step, min_qty, min_cost) ientry_qty = max( min_entry_qty, round_( cost_to_qty(balance, ientry_price, inverse, c_mult) * wallet_exposure_limit * initial_qty_pct, qty_step, ), ) if psize == 0.0: # normal ientry return ientry_qty, ientry_price, "long_ientry_normal" elif psize < ientry_qty * 0.8: # partial ientry entry_qty = max(min_entry_qty, round_(ientry_qty - psize, qty_step)) return entry_qty, ientry_price, "long_ientry_partial" else: wallet_exposure = qty_to_cost(psize, pprice, inverse, c_mult) / balance if wallet_exposure >= wallet_exposure_limit * 1.001: # no entry if wallet_exposure within 0.1% of limit return 0.0, 0.0, "" threshold = wallet_exposure_limit * ( 1 - auto_unstuck_wallet_exposure_threshold) if auto_unstuck_wallet_exposure_threshold != 0.0 and wallet_exposure > threshold * 0.99: # auto unstuck mode entry_price = round_dn( min([ highest_bid, pprice, ema_band_lower * (1 - auto_unstuck_ema_dist) ]), price_step) entry_qty = find_entry_qty_bringing_wallet_exposure_to_target( balance, psize, pprice, wallet_exposure_limit, entry_price, inverse, qty_step, c_mult) min_entry_qty = calc_min_entry_qty(entry_price, inverse, qty_step, min_qty, min_cost) return (max(entry_qty, min_entry_qty), entry_price, "long_unstuck_entry") else: # normal reentry ratio = wallet_exposure / wallet_exposure_limit entry_price = round_dn( pprice * (1 - rentry_pprice_dist * (1 + ratio * rentry_pprice_dist_wallet_exposure_weighting)), price_step, ) entry_price = min(highest_bid, entry_price) min_entry_qty = calc_min_entry_qty(entry_price, inverse, qty_step, min_qty, min_cost) entry_qty = max(min_entry_qty, round_(psize * ddown_factor, qty_step)) wallet_exposure_if_filled = calc_wallet_exposure_if_filled( balance, psize, pprice, entry_qty, entry_price, inverse, c_mult, qty_step) if wallet_exposure_if_filled > wallet_exposure_limit * 1.01: entry_qty = find_entry_qty_bringing_wallet_exposure_to_target( balance, psize, pprice, wallet_exposure_limit, entry_price, inverse, qty_step, c_mult, ) entry_qty = max(entry_qty, min_entry_qty) return entry_qty, entry_price, "long_rentry"
def backtest_recursive_grid( ticks, starting_balance, latency_simulation_ms, maker_fee, inverse, do_long, do_short, qty_step, price_step, min_qty, min_cost, c_mult, ema_span_0, ema_span_1, initial_qty_pct, initial_eprice_ema_dist, wallet_exposure_limit, ddown_factor, rentry_pprice_dist, rentry_pprice_dist_wallet_exposure_weighting, min_markup, markup_range, n_close_orders, auto_unstuck_wallet_exposure_threshold, auto_unstuck_ema_dist, ): if len(ticks[0]) == 3: timestamps = ticks[:, 0] closes = ticks[:, 2] lows = closes highs = closes else: timestamps = ticks[:, 0] highs = ticks[:, 1] lows = ticks[:, 2] closes = ticks[:, 3] balance_long = balance_short = equity_long = equity_short = starting_balance psize_long, pprice_long, psize_short, pprice_short = 0.0, 0.0, 0.0, 0.0 fills_long, fills_short, stats = [], [], [] entry_long, entry_short = (0.0, 0.0, ""), (0.0, 0.0, "") closes_long, closes_short = [(0.0, 0.0, "")], [(0.0, 0.0, "")] bkr_price_long = bkr_price_short = 0.0 next_entry_update_ts_long = 0 next_entry_update_ts_short = 0 next_close_grid_update_ts_long = 0 next_close_grid_update_ts_short = 0 next_stats_update = 0 closest_bkr_long = closest_bkr_short = 1.0 spans_multiplier = 60 / ((timestamps[1] - timestamps[0]) / 1000) spans_long = [ ema_span_0[0], (ema_span_0[0] * ema_span_1[0])**0.5, ema_span_1[0] ] spans_long = np.array( sorted(spans_long)) * spans_multiplier if do_long else np.ones(3) spans_short = [ ema_span_0[1], (ema_span_0[1] * ema_span_1[1])**0.5, ema_span_1[1] ] spans_short = np.array( sorted(spans_short)) * spans_multiplier if do_short else np.ones(3) assert max(spans_long) < len( ticks), "ema_span_1 long larger than len(prices)" assert max(spans_short) < len( ticks), "ema_span_1 short larger than len(prices)" spans_long = np.where(spans_long < 1.0, 1.0, spans_long) spans_short = np.where(spans_short < 1.0, 1.0, spans_short) max_span_long = int(round(max(spans_long))) max_span_short = int(round(max(spans_short))) emas_long, emas_short = np.repeat(closes[0], 3), np.repeat(closes[0], 3) alphas_long = 2.0 / (spans_long + 1.0) alphas__long = 1.0 - alphas_long alphas_short = 2.0 / (spans_short + 1.0) alphas__short = 1.0 - alphas_short long_wallet_exposure = 0.0 short_wallet_exposure = 0.0 long_wallet_exposure_auto_unstuck_threshold = ( (wallet_exposure_limit[0] * (1 - auto_unstuck_wallet_exposure_threshold[0])) if auto_unstuck_wallet_exposure_threshold[0] != 0.0 else wallet_exposure_limit[0] * 10) short_wallet_exposure_auto_unstuck_threshold = ( (wallet_exposure_limit[1] * (1 - auto_unstuck_wallet_exposure_threshold[1])) if auto_unstuck_wallet_exposure_threshold[1] != 0.0 else wallet_exposure_limit[1] * 10) for k in range(0, len(ticks)): if do_long: emas_long = calc_ema(alphas_long, alphas__long, emas_long, closes[k]) if k >= max_span_long: # check bankruptcy bkr_diff_long = calc_diff(bkr_price_long, closes[k]) closest_bkr_long = min(closest_bkr_long, bkr_diff_long) if closest_bkr_long < 0.06: # consider bankruptcy within 6% as liquidation if psize_long != 0.0: fee_paid = -qty_to_cost(psize_long, pprice_long, inverse, c_mult) * maker_fee pnl = calc_pnl_long(pprice_long, closes[k], -psize_long, inverse, c_mult) balance_long = 0.0 equity_long = 0.0 psize_long, pprice_long = 0.0, 0.0 fills_long.append(( k, timestamps[k], pnl, fee_paid, balance_long, equity_long, -psize_long, closes[k], 0.0, 0.0, "long_bankruptcy", )) do_long = False if not do_short: return fills_long, fills_short, stats # check if long entry order should be updated if timestamps[k] >= next_entry_update_ts_long: entry_long = calc_recursive_entry_long( balance_long, psize_long, pprice_long, closes[k - 1], min(emas_long), inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct[0], initial_eprice_ema_dist[0], ddown_factor[0], rentry_pprice_dist[0], rentry_pprice_dist_wallet_exposure_weighting[0], wallet_exposure_limit[0], auto_unstuck_ema_dist[0], auto_unstuck_wallet_exposure_threshold[0], ) next_entry_update_ts_long = timestamps[ k] + 1000 * 60 * 5 # five mins delay # check if close grid should be updated if timestamps[k] >= next_close_grid_update_ts_long: closes_long = calc_close_grid_long( balance_long, psize_long, pprice_long, closes[k - 1], max(emas_long), inverse, qty_step, price_step, min_qty, min_cost, c_mult, wallet_exposure_limit[0], min_markup[0], markup_range[0], n_close_orders[0], auto_unstuck_wallet_exposure_threshold[0], auto_unstuck_ema_dist[0], ) next_close_grid_update_ts_long = timestamps[ k] + 1000 * 60 * 5 # five mins delay # check if long entry filled while entry_long[0] != 0.0 and lows[k] < entry_long[1]: next_entry_update_ts_long = min( next_entry_update_ts_long, timestamps[k] + latency_simulation_ms) next_close_grid_update_ts_long = min( next_close_grid_update_ts_long, timestamps[k] + latency_simulation_ms) psize_long, pprice_long = calc_new_psize_pprice( psize_long, pprice_long, entry_long[0], entry_long[1], qty_step, ) fee_paid = -qty_to_cost(entry_long[0], entry_long[1], inverse, c_mult) * maker_fee balance_long += fee_paid equity_long = balance_long + calc_pnl_long( pprice_long, closes[k], psize_long, inverse, c_mult) fills_long.append(( k, timestamps[k], 0.0, fee_paid, balance_long, equity_long, entry_long[0], entry_long[1], psize_long, pprice_long, entry_long[2], )) bkr_price_long = calc_bankruptcy_price( balance_long, psize_long, pprice_long, 0.0, 0.0, inverse, c_mult, ) long_wallet_exposure = ( qty_to_cost(psize_long, pprice_long, inverse, c_mult) / balance_long) entry_long = calc_recursive_entry_long( balance_long, psize_long, pprice_long, closes[k - 1], min(emas_long), inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct[0], initial_eprice_ema_dist[0], ddown_factor[0], rentry_pprice_dist[0], rentry_pprice_dist_wallet_exposure_weighting[0], wallet_exposure_limit[0], auto_unstuck_ema_dist[0], auto_unstuck_wallet_exposure_threshold[0], ) # check if long closes filled while (psize_long > 0.0 and closes_long and closes_long[0][0] < 0.0 and highs[k] > closes_long[0][1]): next_entry_update_ts_long = min( next_entry_update_ts_long, timestamps[k] + latency_simulation_ms) next_close_grid_update_ts_long = min( next_close_grid_update_ts_long, timestamps[k] + latency_simulation_ms) close_qty_long = closes_long[0][0] new_psize_long = round_(psize_long + close_qty_long, qty_step) if new_psize_long < 0.0: print( "warning: long close qty greater than long psize") print("psize_long", psize_long) print("pprice_long", pprice_long) print("closes_long[0]", closes_long[0]) close_qty_long = -psize_long new_psize_long, pprice_long = 0.0, 0.0 psize_long = new_psize_long fee_paid = (-qty_to_cost(close_qty_long, closes_long[0][1], inverse, c_mult) * maker_fee) pnl = calc_pnl_long(pprice_long, closes_long[0][1], close_qty_long, inverse, c_mult) balance_long += fee_paid + pnl equity_long = balance_long + calc_pnl_long( pprice_long, closes[k], psize_long, inverse, c_mult) fills_long.append(( k, timestamps[k], pnl, fee_paid, balance_long, equity_long, close_qty_long, closes_long[0][1], psize_long, pprice_long, closes_long[0][2], )) closes_long = closes_long[1:] bkr_price_long = calc_bankruptcy_price( balance_long, psize_long, pprice_long, 0.0, 0.0, inverse, c_mult, ) long_wallet_exposure = ( qty_to_cost(psize_long, pprice_long, inverse, c_mult) / balance_long) if psize_long == 0.0: # update entry order next_entry_update_ts_long = min( next_entry_update_ts_long, timestamps[k] + latency_simulation_ms, ) else: if closes[k] > pprice_long: # update closes after 2.5 secs next_close_grid_update_ts_long = min( next_close_grid_update_ts_long, timestamps[k] + latency_simulation_ms + 2500, ) elif long_wallet_exposure >= long_wallet_exposure_auto_unstuck_threshold: # update both entry and closes after 15 secs next_close_grid_update_ts_long = min( next_close_grid_update_ts_long, timestamps[k] + latency_simulation_ms + 15000, ) next_entry_update_ts_long = min( next_entry_update_ts_long, timestamps[k] + latency_simulation_ms + 15000, ) if do_short: emas_short = calc_ema(alphas_short, alphas__short, emas_short, closes[k]) if k >= max_span_short: # check bankruptcy bkr_diff_short = calc_diff(bkr_price_short, closes[k]) closest_bkr_short = min(closest_bkr_short, bkr_diff_short) if closest_bkr_short < 0.06: # consider bankruptcy within 6% as liquidation if psize_short != 0.0: fee_paid = (-qty_to_cost(psize_short, pprice_short, inverse, c_mult) * maker_fee) pnl = calc_pnl_short(pprice_short, closes[k], -psize_short, inverse, c_mult) balance_short = 0.0 equity_short = 0.0 psize_short, pprice_short = 0.0, 0.0 fills_short.append(( k, timestamps[k], pnl, fee_paid, balance_short, equity_short, -psize_short, closes[k], 0.0, 0.0, "short_bankruptcy", )) do_short = False if not do_long: return fills_long, fills_short, stats # check if entry order should be updated if timestamps[k] >= next_entry_update_ts_short: entry_short = calc_recursive_entry_short( balance_short, psize_short, pprice_short, closes[k - 1], max(emas_short), inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct[1], initial_eprice_ema_dist[1], ddown_factor[1], rentry_pprice_dist[1], rentry_pprice_dist_wallet_exposure_weighting[1], wallet_exposure_limit[1], auto_unstuck_ema_dist[1], auto_unstuck_wallet_exposure_threshold[1], ) next_entry_update_ts_short = timestamps[ k] + 1000 * 60 * 5 # five mins delay # check if close grid should be updated if timestamps[k] >= next_close_grid_update_ts_short: closes_short = calc_close_grid_short( balance_short, psize_short, pprice_short, closes[k - 1], min(emas_short), inverse, qty_step, price_step, min_qty, min_cost, c_mult, wallet_exposure_limit[1], min_markup[1], markup_range[1], n_close_orders[1], auto_unstuck_wallet_exposure_threshold[1], auto_unstuck_ema_dist[1], ) next_close_grid_update_ts_short = timestamps[ k] + 1000 * 60 * 5 # five mins delay # check if short entry filled while entry_short[0] != 0.0 and highs[k] > entry_short[1]: next_entry_update_ts_short = min( next_entry_update_ts_short, timestamps[k] + latency_simulation_ms) next_close_grid_update_ts_short = min( next_close_grid_update_ts_short, timestamps[k] + latency_simulation_ms) psize_short, pprice_short = calc_new_psize_pprice( psize_short, pprice_short, entry_short[0], entry_short[1], qty_step, ) fee_paid = (-qty_to_cost(entry_short[0], entry_short[1], inverse, c_mult) * maker_fee) balance_short += fee_paid equity_short = balance_short + calc_pnl_short( pprice_short, closes[k], psize_short, inverse, c_mult) fills_short.append(( k, timestamps[k], 0.0, fee_paid, balance_short, equity_short, entry_short[0], entry_short[1], psize_short, pprice_short, entry_short[2], )) bkr_price_short = calc_bankruptcy_price( balance_short, 0.0, 0.0, psize_short, pprice_short, inverse, c_mult, ) short_wallet_exposure = (qty_to_cost( psize_short, pprice_short, inverse, c_mult) / balance_short) entry_short = calc_recursive_entry_short( balance_short, psize_short, pprice_short, closes[k - 1], max(emas_short), inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct[1], initial_eprice_ema_dist[1], ddown_factor[1], rentry_pprice_dist[1], rentry_pprice_dist_wallet_exposure_weighting[1], wallet_exposure_limit[1], auto_unstuck_ema_dist[1], auto_unstuck_wallet_exposure_threshold[1], ) # check if short closes filled while (psize_short < 0.0 and closes_short and closes_short[0][0] > 0.0 and lows[k] < closes_short[0][1]): next_entry_update_ts_short = min( next_entry_update_ts_short, timestamps[k] + latency_simulation_ms) next_close_grid_update_ts_short = min( next_close_grid_update_ts_short, timestamps[k] + latency_simulation_ms) close_qty_short = closes_short[0][0] new_psize_short = round_(psize_short + close_qty_short, qty_step) if new_psize_short > 0.0: print( "warning: short close qty greater than short psize" ) print("psize_short", psize_short) print("pprice_short", pprice_short) print("closes_short[0]", closes_short[0]) close_qty_short = abs(psize_short) new_psize_short, pprice_short = 0.0, 0.0 psize_short = new_psize_short fee_paid = (-qty_to_cost( close_qty_short, closes_short[0][1], inverse, c_mult) * maker_fee) pnl = calc_pnl_short(pprice_short, closes_short[0][1], close_qty_short, inverse, c_mult) balance_short += fee_paid + pnl equity_short = balance_short + calc_pnl_short( pprice_short, closes[k], psize_short, inverse, c_mult) fills_short.append(( k, timestamps[k], pnl, fee_paid, balance_short, equity_short, close_qty_short, closes_short[0][1], psize_short, pprice_short, closes_short[0][2], )) closes_short = closes_short[1:] bkr_price_short = calc_bankruptcy_price( balance_short, 0.0, 0.0, psize_short, pprice_short, inverse, c_mult, ) short_wallet_exposure = (qty_to_cost( psize_short, pprice_short, inverse, c_mult) / balance_short) if psize_short == 0.0: # update entry order now next_entry_update_ts_short = min( next_entry_update_ts_short, timestamps[k] + latency_simulation_ms, ) else: if closes[k] > pprice_short: # update closes after 2.5 secs next_close_grid_update_ts_short = min( next_close_grid_update_ts_short, timestamps[k] + latency_simulation_ms + 2500, ) elif short_wallet_exposure >= short_wallet_exposure_auto_unstuck_threshold: # update both entry and closes after 15 secs next_close_grid_update_ts_short = min( next_close_grid_update_ts_short, timestamps[k] + latency_simulation_ms + 15000, ) next_entry_update_ts_short = min( next_entry_update_ts_short, timestamps[k] + latency_simulation_ms + 15000, ) # process stats if timestamps[k] >= next_stats_update: equity_long = balance_long + calc_pnl_long( pprice_long, closes[k], psize_long, inverse, c_mult) equity_short = balance_short + calc_pnl_short( pprice_short, closes[k], psize_short, inverse, c_mult) stats.append(( timestamps[k], bkr_price_long, bkr_price_short, psize_long, pprice_long, psize_short, pprice_short, closes[k], closest_bkr_long, closest_bkr_short, balance_long, balance_short, equity_long, equity_short, )) next_stats_update = timestamps[k] + 60 * 1000 return fills_long, fills_short, stats
def calc_recursive_entries_short( balance, psize, pprice, lowest_ask, ema_band_upper, inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct, initial_eprice_ema_dist, ddown_factor, rentry_pprice_dist, rentry_pprice_dist_wallet_exposure_weighting, wallet_exposure_limit, auto_unstuck_ema_dist, auto_unstuck_wallet_exposure_threshold, whole_grid=False, ): entries = [] psize_ = psize pprice_ = pprice lowest_ask_ = lowest_ask i = 0 infinite_loop_break = 30 while True: i += 1 if i > infinite_loop_break: break entry_qty, entry_price, entry_type = calc_recursive_entry_short( balance, psize_, pprice_, lowest_ask_, ema_band_upper, inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct, initial_eprice_ema_dist, ddown_factor, rentry_pprice_dist, rentry_pprice_dist_wallet_exposure_weighting, wallet_exposure_limit, auto_unstuck_ema_dist, auto_unstuck_wallet_exposure_threshold, ) if entry_qty == 0.0: break if entries and entry_price == entries[-1][1]: break psize_, pprice_ = calc_new_psize_pprice(psize_, pprice_, entry_qty, entry_price, qty_step) lowest_ask_ = max(lowest_ask, entry_price) wallet_exposure = qty_to_cost(psize_, pprice_, inverse, c_mult) / balance if "unstuck" in entry_type: if len(entries) == 0: # return unstucking entry only if it's the only one return [(entry_qty, entry_price, entry_type, psize_, pprice_, wallet_exposure)] else: entries.append((entry_qty, entry_price, entry_type, psize_, pprice_, wallet_exposure)) if not whole_grid and psize == 0.0: break return entries
def calc_recursive_entry_short( balance, psize, pprice, lowest_ask, ema_band_upper, inverse, qty_step, price_step, min_qty, min_cost, c_mult, initial_qty_pct, initial_eprice_ema_dist, ddown_factor, rentry_pprice_dist, rentry_pprice_dist_wallet_exposure_weighting, wallet_exposure_limit, auto_unstuck_ema_dist, auto_unstuck_wallet_exposure_threshold, ): abs_psize = abs(psize) ientry_price = max( lowest_ask, round_up(ema_band_upper * (1 + initial_eprice_ema_dist), price_step)) min_entry_qty = calc_min_entry_qty(ientry_price, inverse, qty_step, min_qty, min_cost) ientry_qty = max( min_entry_qty, round_( cost_to_qty(balance, ientry_price, inverse, c_mult) * wallet_exposure_limit * initial_qty_pct, qty_step, ), ) if abs_psize == 0.0: # normal ientry return -ientry_qty, ientry_price, "short_ientry_normal" elif abs_psize < ientry_qty * 0.8: # partial ientry entry_qty = max(min_entry_qty, round_(ientry_qty - abs_psize, qty_step)) return -entry_qty, ientry_price, "short_ientry_partial" else: wallet_exposure = qty_to_cost(abs_psize, pprice, inverse, c_mult) / balance if wallet_exposure >= wallet_exposure_limit * 1.001: # no entry if wallet_exposure within 0.1% of limit return 0.0, 0.0, "" threshold = wallet_exposure_limit * ( 1 - auto_unstuck_wallet_exposure_threshold) if auto_unstuck_wallet_exposure_threshold != 0.0 and wallet_exposure > threshold * 0.99: # auto unstuck mode entry_price = round_up( max([ lowest_ask, pprice, ema_band_upper * (1 + auto_unstuck_ema_dist) ]), price_step) entry_qty = find_entry_qty_bringing_wallet_exposure_to_target( balance, abs_psize, pprice, wallet_exposure_limit, entry_price, inverse, qty_step, c_mult, ) min_entry_qty = calc_min_entry_qty(entry_price, inverse, qty_step, min_qty, min_cost) return (-max(entry_qty, min_entry_qty), entry_price, "short_unstuck_entry") else: # normal reentry ratio = wallet_exposure / wallet_exposure_limit entry_price = round_up( pprice * (1 + rentry_pprice_dist * (1 + ratio * rentry_pprice_dist_wallet_exposure_weighting)), price_step, ) entry_price = max(entry_price, lowest_ask) min_entry_qty = calc_min_entry_qty(entry_price, inverse, qty_step, min_qty, min_cost) entry_qty = max(min_entry_qty, round_(abs_psize * ddown_factor, qty_step)) wallet_exposure_if_filled = calc_wallet_exposure_if_filled( balance, abs_psize, pprice, entry_qty, entry_price, inverse, c_mult, qty_step) if wallet_exposure_if_filled > wallet_exposure_limit * 1.01: # crop qty entry_qty = find_entry_qty_bringing_wallet_exposure_to_target( balance, abs_psize, pprice, wallet_exposure_limit, entry_price, inverse, qty_step, c_mult, ) entry_qty = max(entry_qty, min_entry_qty) return -entry_qty, entry_price, "short_rentry"