async def fetch_position(self) -> dict: position = {} if 'linear_perpetual' in self.market_type: 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 'inverse_perpetual' in self.market_type: if fetched['result']['side'] == 'Buy': long_pos = fetched['result'] shrt_pos = { 'size': 0.0, 'entry_price': 0.0, 'liq_price': 0.0 } else: long_pos = { 'size': 0.0, 'entry_price': 0.0, 'liq_price': 0.0 } shrt_pos = fetched['result'] elif 'inverse_futures' in self.market_type: 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] else: raise Exception('unknown market type') position['long'] = { 'size': round_(float(long_pos['size']), self.qty_step), 'price': float(long_pos['entry_price']), 'liquidation_price': float(long_pos['liq_price']) } position['shrt'] = { 'size': -round_(float(shrt_pos['size']), self.qty_step), 'price': float(shrt_pos['entry_price']), 'liquidation_price': float(shrt_pos['liq_price']) } return position
def plot_wrap(config, data): print("n_days", round_(config["n_days"], 0.1)) print("starting_balance", config["starting_balance"]) print("backtesting...") sts = time() fills_long, fills_short, stats = backtest(config, data, do_print=True) print(f"{time() - sts:.2f} seconds elapsed") if not fills_long and not fills_short: print("no fills") return longs, shorts, sdf, result = analyze_fills(fills_long, fills_short, stats, config) config["result"] = result config["plots_dirpath"] = make_get_filepath( os.path.join(config["plots_dirpath"], f"{ts_to_date(time())[:19].replace(':', '')}", "")) longs.to_csv(config["plots_dirpath"] + "fills_long.csv") shorts.to_csv(config["plots_dirpath"] + "fills_short.csv") sdf.to_csv(config["plots_dirpath"] + "stats.csv") df = pd.DataFrame({ **{ "timestamp": data[:, 0], "qty": data[:, 1], "price": data[:, 2] }, **{} }) print("dumping plots...") dump_plots(config, longs, shorts, sdf, df, n_parts=config["n_parts"])
def plot_wrap(config, data): print('n_days', round_(config['n_days'], 0.1)) print('starting_balance', config['starting_balance']) print('backtesting...') sts = time() fills, stats = backtest(config, data, do_print=True) print(f'{time() - sts:.2f} seconds elapsed') if not fills: print('no fills') return fdf, sdf, result = analyze_fills(fills, stats, config) config['result'] = result config['plots_dirpath'] = make_get_filepath(os.path.join( config['plots_dirpath'], f"{ts_to_date(time())[:19].replace(':', '')}", '') ) fdf.to_csv(config['plots_dirpath'] + "fills.csv") sdf.to_csv(config['plots_dirpath'] + "stats.csv") df = pd.DataFrame({**{'timestamp': data[:, 0], 'qty': data[:, 1], 'price': data[:, 2]}, **{}}) print('dumping plots...') dump_plots(config, fdf, sdf, df)
if args.long_wallet_exposure_limit is not None: print( f"overriding long wallet exposure limit ({config['long']['pbr_limit']}) " f"with new value: {args.long_wallet_exposure_limit}" ) config["long"]["pbr_limit"] = args.long_wallet_exposure_limit if args.short_wallet_exposure_limit is not None: print( f"overriding short wallet exposure limit ({config['shrt']['pbr_limit']}) " f"with new value: {args.short_wallet_exposure_limit}" ) config["shrt"]["pbr_limit"] = args.short_wallet_exposure_limit if 'spot' in config['market_type']: live_config = spotify_config(live_config) downloader = Downloader(config) print() for k in (keys := ['exchange', 'spot', 'symbol', 'market_type', 'config_type', 'starting_balance', 'start_date', 'end_date', 'latency_simulation_ms']): if k in config: print(f"{k: <{max(map(len, keys)) + 2}} {config[k]}") print() data = await downloader.get_sampled_ticks() config['n_days'] = round_((data[-1][0] - data[0][0]) / (1000 * 60 * 60 * 24), 0.1) pprint.pprint(denumpyize(live_config)) plot_wrap(config, data) if __name__ == '__main__': asyncio.run(main())
def standardize_user_stream_event( self, event: Union[List[Dict], Dict]) -> Union[List[Dict], Dict]: events = [] if 'topic' in event: if event['topic'] == 'order': for elm in event['data']: if elm['symbol'] == self.symbol: if elm['order_status'] == 'Created': pass elif elm['order_status'] == 'Rejected': pass elif elm['order_status'] == 'New': new_open_order = { 'order_id': elm['order_id'], 'symbol': elm['symbol'], 'price': float(elm['price']), 'qty': float(elm['qty']), 'type': elm['order_type'].lower(), 'side': (side := elm['side'].lower()), 'timestamp': date_to_ts(elm['timestamp' if self. inverse else 'update_time']) } if 'inverse_perpetual' in self.market_type: if self.position['long']['size'] == 0.0: if self.position['shrt']['size'] == 0.0: new_open_order[ 'position_side'] = 'long' if new_open_order[ 'side'] == 'buy' else 'shrt' else: new_open_order[ 'position_side'] = 'shrt' else: new_open_order['position_side'] = 'long' elif 'inverse_futures' in self.market_type: new_open_order[ 'position_side'] = determine_pos_side(elm) else: new_open_order['position_side'] = ('long' if ( (new_open_order['side'] == 'buy' and elm['create_type'] == 'CreateByUser') or (new_open_order['side'] == 'sell' and elm['create_type'] == 'CreateByClosing')) else 'shrt') events.append({'new_open_order': new_open_order}) elif elm['order_status'] == 'PartiallyFilled': events.append({ 'deleted_order_id': elm['order_id'], 'partially_filled': True }) elif elm['order_status'] == 'Filled': events.append({ 'deleted_order_id': elm['order_id'], 'filled': True }) elif elm['order_status'] == 'Cancelled': events.append( {'deleted_order_id': elm['order_id']}) elif elm['order_status'] == 'PendingCancel': pass else: events.append({ 'other_symbol': elm['symbol'], 'other_type': event['topic'] }) elif event['topic'] == 'execution': for elm in event['data']: if elm['symbol'] == self.symbol: if elm['exec_type'] == 'Trade': # already handled by "order" pass else: events.append({ 'other_symbol': elm['symbol'], 'other_type': event['topic'] }) elif event['topic'] == 'position': for elm in event['data']: if elm['symbol'] == self.symbol: standardized = {} if elm['side'] == 'Buy': standardized['long_psize'] = round_( float(elm['size']), self.qty_step) standardized['long_pprice'] = float( elm['entry_price']) elif elm['side'] == 'Sell': standardized['shrt_psize'] = -round_( abs(float(elm['size'])), self.qty_step) standardized['shrt_pprice'] = float( elm['entry_price']) events.append(standardized) if self.inverse: events.append({ 'wallet_balance': float(elm['wallet_balance']) }) else: events.append({ 'other_symbol': elm['symbol'], 'other_type': event['topic'] }) elif not self.inverse and event['topic'] == 'wallet': for elm in event['data']: events.append( {'wallet_balance': float(elm['wallet_balance'])}) return events
async def main(): parser = argparse.ArgumentParser( prog="Backtest", description="Backtest given passivbot config.") parser.add_argument("live_config_path", type=str, help="path to live config to test") parser = add_argparse_args(parser) parser.add_argument( "-lw", "--long_wallet_exposure_limit", "--long-wallet-exposure-limit", type=float, required=False, dest="long_wallet_exposure_limit", default=None, help= "specify long wallet exposure limit, overriding value from live config", ) parser.add_argument( "-sw", "--short_wallet_exposure_limit", "--short-wallet-exposure-limit", type=float, required=False, dest="short_wallet_exposure_limit", default=None, help= "specify short wallet exposure limit, overriding value from live config", ) parser.add_argument( "-le", "--long_enabled", "--long-enabled", type=str, required=False, dest="long_enabled", default=None, help="specify long enabled [y/n], overriding value from live config", ) parser.add_argument( "-se", "--short_enabled", "--short-enabled", type=str, required=False, dest="short_enabled", default=None, help="specify short enabled [y/n], overriding value from live config", ) parser.add_argument( "-np", "--n_parts", "--n-parts", type=int, required=False, dest="n_parts", default=None, help="set n backtest slices to plot", ) parser.add_argument( "-oh", "--ohlcv", help="use 1m ohlcv instead of 1s ticks", action="store_true", ) args = parser.parse_args() if args.symbol is None: tmp_cfg = load_hjson_config(args.backtest_config_path) symbols = (tmp_cfg["symbol"] if type(tmp_cfg["symbol"]) == list else tmp_cfg["symbol"].split(",")) else: symbols = args.symbol.split(",") for symbol in symbols: args = parser.parse_args() args.symbol = symbol config = await prepare_backtest_config(args) config["n_parts"] = args.n_parts live_config = load_live_config(args.live_config_path) config.update(live_config) if args.long_wallet_exposure_limit is not None: print( f"overriding long wallet exposure limit ({config['long']['wallet_exposure_limit']}) " f"with new value: {args.long_wallet_exposure_limit}") config["long"][ "wallet_exposure_limit"] = args.long_wallet_exposure_limit if args.short_wallet_exposure_limit is not None: print( f"overriding short wallet exposure limit ({config['short']['wallet_exposure_limit']}) " f"with new value: {args.short_wallet_exposure_limit}") config["short"][ "wallet_exposure_limit"] = args.short_wallet_exposure_limit if args.long_enabled is not None: config["long"]["enabled"] = "y" in args.long_enabled.lower() if args.short_enabled is not None: config["short"]["enabled"] = "y" in args.short_enabled.lower() if "spot" in config["market_type"]: live_config = spotify_config(live_config) config["ohlcv"] = args.ohlcv print() for k in (keys := [ "exchange", "spot", "symbol", "market_type", "passivbot_mode", "config_type", "starting_balance", "start_date", "end_date", "latency_simulation_ms", "base_dir", ]): if k in config: print(f"{k: <{max(map(len, keys)) + 2}} {config[k]}") print() if config["ohlcv"]: data = load_hlc_cache( symbol, config["start_date"], config["end_date"], base_dir=config["base_dir"], spot=config["spot"], ) else: downloader = Downloader(config) data = await downloader.get_sampled_ticks() config["n_days"] = round_( (data[-1][0] - data[0][0]) / (1000 * 60 * 60 * 24), 0.1) pprint.pprint(denumpyize(live_config)) plot_wrap(config, data)
async def fetch_position(self) -> dict: position = {} if "linear_perpetual" in self.market_type: 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] short_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 "inverse_perpetual" in self.market_type: if fetched["result"]["side"] == "Buy": long_pos = fetched["result"] short_pos = { "size": 0.0, "entry_price": 0.0, "liq_price": 0.0 } else: long_pos = { "size": 0.0, "entry_price": 0.0, "liq_price": 0.0 } short_pos = fetched["result"] elif "inverse_futures" in self.market_type: long_pos = [ e["data"] for e in fetched["result"] if e["data"]["position_idx"] == 1 ][0] short_pos = [ e["data"] for e in fetched["result"] if e["data"]["position_idx"] == 2 ][0] else: raise Exception("unknown market type") position["long"] = { "size": round_(float(long_pos["size"]), self.qty_step), "price": float(long_pos["entry_price"]), "liquidation_price": float(long_pos["liq_price"]), } position["short"] = { "size": -round_(float(short_pos["size"]), self.qty_step), "price": float(short_pos["entry_price"]), "liquidation_price": float(short_pos["liq_price"]), } return position
for elm in event["data"]: if elm["symbol"] == self.symbol: if elm["exec_type"] == "Trade": # already handled by "order" pass else: events.append({ "other_symbol": elm["symbol"], "other_type": event["topic"], }) elif event["topic"] == "position": for elm in event["data"]: if elm["symbol"] == self.symbol: standardized = {} if elm["side"] == "Buy": standardized["psize_long"] = round_( float(elm["size"]), self.qty_step) standardized["pprice_long"] = float( elm["entry_price"]) elif elm["side"] == "Sell": standardized["psize_short"] = -round_( abs(float(elm["size"])), self.qty_step) standardized["pprice_short"] = float( elm["entry_price"]) events.append(standardized) if self.inverse: events.append({ "wallet_balance": float(elm["wallet_balance"]) }) else:
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_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"