def harmony_search( func, bounds: np.ndarray, n_harmonies: int, hm_considering_rate: float, bandwidth: float, pitch_adjusting_rate: float, iters: int, starting_xs: [np.ndarray] = [], post_processing_func = None): # hm == harmony memory n_harmonies = max(n_harmonies, len(starting_xs)) seen = set() hm = numpyize([[np.random.uniform(bounds[0][i], bounds[1][i]) for i in range(len(bounds[0]))] for _ in range(n_harmonies)]) for i in range(len(starting_xs)): assert len(starting_xs[i]) == len(bounds[0]) harmony = np.array(starting_xs[i]) for z in range(len(bounds[0])): harmony[z] = max(bounds[0][z], min(bounds[1][z], harmony[z])) tpl = tuple(harmony) if tpl not in seen: hm[i] = harmony seen.add(tpl) print('evaluating initial harmonies...') hm_evals = numpyize([func(h) for h in hm]) print('best harmony') print(round_values(denumpyize(hm[hm_evals.argmin()]), 5), f'{hm_evals.min():.8f}') if post_processing_func is not None: post_processing_func(hm[hm_evals.argmin()]) print('starting search...') worst_eval_i = hm_evals.argmax() for itr in range(iters): new_harmony = np.zeros(len(bounds[0])) for note_i in range(len(bounds[0])): if np.random.random() < hm_considering_rate: new_note = hm[np.random.randint(0, len(hm))][note_i] if np.random.random() < pitch_adjusting_rate: new_note = new_note + bandwidth * (np.random.random() - 0.5) * abs(bounds[0][note_i] - bounds[1][note_i]) new_note = max(bounds[0][note_i], min(bounds[1][note_i], new_note)) else: new_note = np.random.uniform(bounds[0][note_i], bounds[1][note_i]) new_harmony[note_i] = new_note h_eval = func(new_harmony) if h_eval < hm_evals[worst_eval_i]: hm[worst_eval_i] = new_harmony hm_evals[worst_eval_i] = h_eval worst_eval_i = hm_evals.argmax() print('improved harmony') print(round_values(denumpyize(new_harmony), 5), f'{h_eval:.8f}') print('best harmony') print(round_values(denumpyize(hm[hm_evals.argmin()]), 5), f'{hm_evals.min():.8f}') print('iteration', itr, 'of', iters) if post_processing_func is not None: post_processing_func(hm[hm_evals.argmin()]) return hm[hm_evals.argmin()]
def process(self, result): score, analysis, config = result score = -score best_score = self.all_backtest_analyses[0][0] if self.all_backtest_analyses else 9e9 analysis['score'] = score idx = bisect([e[0] for e in self.all_backtest_analyses], score) self.all_backtest_analyses.insert(idx, (score, analysis)) to_dump = denumpyize({**analysis, **pack_config(config)}) f"{len(self.all_backtest_analyses): <5}" table = PrettyTable() table.field_names = ['adg', 'bkr_dist', 'eqbal_ratio', 'shrp', 'hrs_no_fills', 'hrs_no_fills_ss', 'mean_hrs_btwn_fills', 'n_slices', 'score'] for elm in self.all_backtest_analyses[:20] + [(score, analysis)]: row = [round_dynamic(e, 6) for e in [elm[1]['average_daily_gain'], elm[1]['closest_bkr'], elm[1]['lowest_eqbal_ratio'], elm[1]['sharpe_ratio'], elm[1]['max_hrs_no_fills'], elm[1]['max_hrs_no_fills_same_side'], elm[1]['mean_hrs_between_fills'], elm[1]['completed_slices'], elm[1]['score']]] table.add_row(row) output = table.get_string(border=True, padding_width=1) print(f'\n\n{len(self.all_backtest_analyses)}') print(output) with open(config['optimize_dirpath'] + 'results.txt', 'a') as f: f.write(json.dumps(to_dump) + '\n') if score < best_score: dump_live_config(to_dump, config['optimize_dirpath'] + 'current_best.json') return score
def backtest_single_wrap(config_: dict): config = config_.copy() exchange_name = config['exchange'] + ('_spot' if config['market_type'] == 'spot' else '') cache_filepath = f"backtests/{exchange_name}/{config['symbol']}/caches/" ticks_filepath = cache_filepath + f"{config['start_date']}_{config['end_date']}_ticks_cache.npy" mss = json.load(open(cache_filepath + 'market_specific_settings.json')) ticks = np.load(ticks_filepath) config.update(mss) try: fills, stats = backtest(config, ticks) fdf, sdf, analysis = analyze_fills(fills, stats, config) pa_closeness_long = analysis['pa_closeness_mean_long'] pa_closeness_shrt = analysis['pa_closeness_mean_shrt'] adg = analysis['average_daily_gain'] print(f"backtested {config['symbol']: <12} pa closeness long {pa_closeness_long:.6f} " f"pa closeness shrt {pa_closeness_shrt:.6f} adg {adg:.6f}") except Exception as e: print(f'error with {config["symbol"]} {e}') print('config') traceback.print_exc() adg = 0.0 pa_closeness_long = pa_closeness_shrt = 100.0 with open(make_get_filepath('tmp/harmony_search_errors.txt'), 'a') as f: f.write(json.dumps([time(), 'error', str(e), denumpyize(config)]) + '\n') return (pa_closeness_long, pa_closeness_shrt, adg)
async def main() -> None: parser = argparse.ArgumentParser(prog='passivbot', description='run passivbot') parser.add_argument('user', type=str, help='user/account_name defined in api-keys.json') parser.add_argument('symbol', type=str, help='symbol to trade') parser.add_argument('live_config_path', type=str, help='live config to use') parser.add_argument( '-m', '--market_type', type=str, required=False, dest='market_type', default=None, help= 'specify whether spot or futures (default), overriding value from backtest config' ) parser.add_argument('-gs', '--graceful_stop', action='store_true', help='if true, disable long and short') 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( "-sm", "--short_mode", "--short-mode", type=str, required=False, dest="short_mode", default=None, help= "specify one of following short modes: [n (normal), m (manual), gs (graceful_stop), p (panic), t (tp_only)]" ) parser.add_argument( "-lm", "--long_mode", "--long-mode", type=str, required=False, dest="long_mode", default=None, help= "specify one of following long modes: [n (normal), m (manual), gs (graceful_stop), p (panic), t (tp_only)]" ) parser.add_argument('-ab', '--assigned_balance', type=float, required=False, dest='assigned_balance', default=None, help='add assigned_balance to live config') args = parser.parse_args() try: accounts = json.load(open('api-keys.json')) except Exception as e: print(e, 'failed to load api-keys.json file') return try: account = accounts[args.user] except Exception as e: print('unrecognized account name', args.user, e) return try: config = load_live_config(args.live_config_path) except Exception as e: print(e, 'failed to load config', args.live_config_path) return config['user'] = args.user config['exchange'] = account['exchange'] config['symbol'] = args.symbol config['live_config_path'] = args.live_config_path config[ 'market_type'] = args.market_type if args.market_type is not None else 'futures' if args.assigned_balance is not None: print(f'\nassigned balance set to {args.assigned_balance}\n') config['assigned_balance'] = args.assigned_balance if args.long_mode is not None: if args.long_mode in ['gs', 'graceful_stop', 'graceful-stop']: print( '\n\nlong graceful stop enabled; will not make new entries once existing positions are closed\n' ) config['long']['enabled'] = config['do_long'] = False elif args.long_mode in ['m', 'manual']: print( '\n\nlong manual mode enabled; will neither cancel nor create long orders' ) config['long_mode'] = 'manual' elif args.long_mode in ['n', 'normal']: print('\n\nlong normal mode') config['long']['enabled'] = config['do_long'] = True elif args.long_mode in ['p', 'panic']: print('\nlong panic mode enabled') config['long_mode'] = 'panic' config['long']['enabled'] = config['do_long'] = False elif args.long_mode.lower() in ['t', 'tp_only', 'tp-only']: print('\nlong tp only mode enabled') config['long_mode'] = 'tp_only' if args.short_mode is not None: if args.short_mode in ['gs', 'graceful_stop', 'graceful-stop']: print( '\n\nshrt graceful stop enabled; will not make new entries once existing positions are closed\n' ) config['shrt']['enabled'] = config['do_shrt'] = False elif args.short_mode in ['m', 'manual']: print( '\n\nshrt manual mode enabled; will neither cancel nor create shrt orders' ) config['shrt_mode'] = 'manual' elif args.short_mode in ['n', 'normal']: print('\n\nshrt normal mode') config['shrt']['enabled'] = config['do_shrt'] = True elif args.short_mode in ['p', 'panic']: print('\nshort panic mode enabled') config['shrt_mode'] = 'panic' config['shrt']['enabled'] = config['do_shrt'] = False elif args.short_mode.lower() in ['t', 'tp_only', 'tp-only']: print('\nshort tp only mode enabled') config['shrt_mode'] = 'tp_only' if args.graceful_stop: print( '\n\ngraceful stop enabled for both long and short; will not make new entries once existing positions are closed\n' ) config['long']['enabled'] = config['do_long'] = False config['shrt']['enabled'] = config['do_shrt'] = False 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']: config = spotify_config(config) if account['exchange'] == 'binance': if 'spot' in config['market_type']: from procedures import create_binance_bot_spot bot = await create_binance_bot_spot(config) else: from procedures import create_binance_bot bot = await create_binance_bot(config) elif account['exchange'] == 'bybit': from procedures import create_bybit_bot bot = await create_bybit_bot(config) else: raise Exception('unknown exchange', account['exchange']) print('using config') pprint.pprint(denumpyize(config)) signal.signal(signal.SIGINT, bot.stop) signal.signal(signal.SIGTERM, bot.stop) await start_bot(bot) await bot.session.close()
def backtest_wrap(config_: dict, ticks_caches: dict): """ loads historical data from disk, runs backtest and returns relevant metrics """ config = { **{ "long": deepcopy(config_["long"]), "short": deepcopy(config_["short"]) }, **{ k: config_[k] for k in [ "starting_balance", "latency_simulation_ms", "symbol", "market_type", "config_no", ] }, **{k: v for k, v in config_["market_specific_settings"].items()}, } if config["symbol"] in ticks_caches: ticks = ticks_caches[config["symbol"]] else: ticks = np.load(config_["ticks_cache_fname"]) try: fills_long, fills_short, stats = backtest(config, ticks) longs, shorts, sdf, analysis = analyze_fills(fills_long, fills_short, stats, config) pa_distance_mean_long = analysis["pa_distance_mean_long"] pa_distance_mean_short = analysis["pa_distance_mean_short"] PAD_std_long = analysis["pa_distance_std_long"] PAD_std_short = analysis["pa_distance_std_short"] adg_long = analysis["adg_long"] adg_short = analysis["adg_short"] adg_DGstd_ratio_long = analysis["adg_DGstd_ratio_long"] adg_DGstd_ratio_short = analysis["adg_DGstd_ratio_short"] """ with open("logs/debug_harmonysearch.txt", "a") as f: f.write(json.dumps({"config": denumpyize(config), "analysis": analysis}) + "\n") """ logging.debug( f"backtested {config['symbol']: <12} pa distance long {pa_distance_mean_long:.6f} " + f"pa distance short {pa_distance_mean_short:.6f} adg long {adg_long:.6f} " + f"adg short {adg_short:.6f} std long {PAD_std_long:.5f} " + f"std short {PAD_std_short:.5f}") except Exception as e: logging.error(f'error with {config["symbol"]} {e}') logging.error("config") traceback.print_exc() adg_long = adg_short = adg_DGstd_ratio_long = adg_DGstd_ratio_short = 0.0 pa_distance_mean_long = pa_distance_mean_short = PAD_std_long = PAD_std_short = 100.0 with open(make_get_filepath("tmp/harmony_search_errors.txt"), "a") as f: f.write( json.dumps([time(), "error", str(e), denumpyize(config)]) + "\n") return { "pa_distance_mean_long": pa_distance_mean_long, "pa_distance_mean_short": pa_distance_mean_short, "adg_DGstd_ratio_long": adg_DGstd_ratio_long, "adg_DGstd_ratio_short": adg_DGstd_ratio_short, "pa_distance_std_long": PAD_std_long, "pa_distance_std_short": PAD_std_short, "adg_long": adg_long, "adg_short": adg_short, }
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())
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)
def dump_plots(result: dict, fdf: pd.DataFrame, sdf: pd.DataFrame, df: pd.DataFrame): init(autoreset=True) plt.rcParams['figure.figsize'] = [29, 18] pd.set_option('precision', 10) table = PrettyTable(["Metric", "Value"]) table.align['Metric'] = 'l' table.align['Value'] = 'l' table.title = 'Summary' table.add_row(['Exchange', result['exchange'] if 'exchange' in result else 'unknown']) table.add_row(['Market type', result['market_type'] if 'market_type' in result else 'unknown']) table.add_row(['Symbol', result['symbol'] if 'symbol' in result else 'unknown']) table.add_row(['No. days', round_dynamic(result['result']['n_days'], 6)]) table.add_row(['Starting balance', round_dynamic(result['result']['starting_balance'], 6)]) profit_color = Fore.RED if result['result']['final_balance'] < result['result']['starting_balance'] else Fore.RESET table.add_row(['Final balance', f"{profit_color}{round_dynamic(result['result']['final_balance'], 6)}{Fore.RESET}"]) table.add_row(['Final equity', f"{profit_color}{round_dynamic(result['result']['final_equity'], 6)}{Fore.RESET}"]) table.add_row(['Net PNL + fees', f"{profit_color}{round_dynamic(result['result']['net_pnl_plus_fees'], 6)}{Fore.RESET}"]) table.add_row(['Total gain percentage', f"{profit_color}{round_dynamic(result['result']['gain'] * 100, 4)}%{Fore.RESET}"]) table.add_row(['Average daily gain percentage', f"{profit_color}{round_dynamic((result['result']['average_daily_gain']) * 100, 3)}%{Fore.RESET}"]) table.add_row(['Adjusted daily gain', f"{profit_color}{round_dynamic(result['result']['adjusted_daily_gain'], 6)}{Fore.RESET}"]) bankruptcy_color = Fore.RED if result['result']['closest_bkr'] < 0.4 else Fore.YELLOW if result['result']['closest_bkr'] < 0.8 else Fore.RESET table.add_row(['Closest bankruptcy percentage', f"{bankruptcy_color}{round_dynamic(result['result']['closest_bkr'] * 100, 4)}%{Fore.RESET}"]) table.add_row([' ', ' ']) table.add_row(['Profit sum', f"{profit_color}{round_dynamic(result['result']['profit_sum'], 6)}{Fore.RESET}"]) table.add_row(['Loss sum', f"{Fore.RED}{round_dynamic(result['result']['loss_sum'], 6)}{Fore.RESET}"]) table.add_row(['Fee sum', round_dynamic(result['result']['fee_sum'], 6)]) table.add_row(['Lowest equity/balance ratio', round_dynamic(result['result']['eqbal_ratio_min'], 6)]) table.add_row(['Biggest psize', round_dynamic(result['result']['biggest_psize'], 6)]) table.add_row(['Price action distance mean long', round_dynamic(result['result']['pa_closeness_mean_long'], 6)]) table.add_row(['Price action distance median long', round_dynamic(result['result']['pa_closeness_median_long'], 6)]) table.add_row(['Price action distance max long', round_dynamic(result['result']['pa_closeness_max_long'], 6)]) table.add_row(['Price action distance mean short', round_dynamic(result['result']['pa_closeness_mean_shrt'], 6)]) table.add_row(['Price action distance median short', round_dynamic(result['result']['pa_closeness_median_shrt'], 6)]) table.add_row(['Price action distance max short', round_dynamic(result['result']['pa_closeness_max_shrt'], 6)]) table.add_row(['Average n fills per day', round_dynamic(result['result']['avg_fills_per_day'], 6)]) table.add_row([' ', ' ']) table.add_row(['No. fills', round_dynamic(result['result']['n_fills'], 6)]) table.add_row(['No. entries', round_dynamic(result['result']['n_entries'], 6)]) table.add_row(['No. closes', round_dynamic(result['result']['n_closes'], 6)]) table.add_row(['No. initial entries', round_dynamic(result['result']['n_ientries'], 6)]) table.add_row(['No. reentries', round_dynamic(result['result']['n_rentries'], 6)]) table.add_row([' ', ' ']) table.add_row(['Mean hours between fills', round_dynamic(result['result']['hrs_stuck_avg_long'], 6)]) table.add_row(['Max hours no fills (same side)', round_dynamic(result['result']['hrs_stuck_max_long'], 6)]) table.add_row(['Max hours no fills', round_dynamic(result['result']['hrs_stuck_max_long'], 6)]) longs = fdf[fdf.type.str.contains('long')] shrts = fdf[fdf.type.str.contains('shrt')] if result['long']['enabled']: table.add_row([' ', ' ']) table.add_row(['Long', result['long']['enabled']]) table.add_row(["No. inital entries", len(longs[longs.type.str.contains('ientry')])]) table.add_row(["No. reentries", len(longs[longs.type.str.contains('rentry')])]) table.add_row(["No. normal closes", len(longs[longs.type.str.contains('nclose')])]) table.add_row(['Mean hours stuck (long)', round_dynamic(result['result']['hrs_stuck_avg_long'], 6)]) table.add_row(['Max hours stuck (long)', round_dynamic(result['result']['hrs_stuck_max_long'], 6)]) profit_color = Fore.RED if longs.pnl.sum() < 0 else Fore.RESET table.add_row(["PNL sum", f"{profit_color}{longs.pnl.sum()}{Fore.RESET}"]) if result['shrt']['enabled']: table.add_row([' ', ' ']) table.add_row(['Short', result['shrt']['enabled']]) table.add_row(["No. inital entries", len(shrts[shrts.type.str.contains('ientry')])]) table.add_row(["No. reentries", len(shrts[shrts.type.str.contains('rentry')])]) table.add_row(["No. normal closes", len(shrts[shrts.type.str.contains('nclose')])]) table.add_row(['Mean hours stuck (shrt)', round_dynamic(result['result']['hrs_stuck_avg_shrt'], 6)]) table.add_row(['Max hours stuck (shrt)', round_dynamic(result['result']['hrs_stuck_max_shrt'], 6)]) profit_color = Fore.RED if shrts.pnl.sum() < 0 else Fore.RESET table.add_row(["PNL sum", f"{profit_color}{shrts.pnl.sum()}{Fore.RESET}"]) dump_live_config(result, result['plots_dirpath'] + 'live_config.json') json.dump(denumpyize(result), open(result['plots_dirpath'] + 'result.json', 'w'), indent=4) print('writing backtest_result.txt...\n') with open(f"{result['plots_dirpath']}backtest_result.txt", 'w') as f: output = table.get_string(border=True, padding_width=1) print(output) f.write(re.sub('\033\\[([0-9]+)(;[0-9]+)*m', '', output)) print('\nplotting balance and equity...') plt.clf() sdf.balance.plot() sdf.equity.plot(title="Balance and equity", xlabel="Time", ylabel="Balance") plt.savefig(f"{result['plots_dirpath']}balance_and_equity_sampled.png") plt.clf() longs.pnl.cumsum().plot(title="PNL cumulated sum - Long", xlabel="Time", ylabel="PNL") plt.savefig(f"{result['plots_dirpath']}pnl_cumsum_long.png") plt.clf() shrts.pnl.cumsum().plot(title="PNL cumulated sum - Short", xlabel="Time", ylabel="PNL") plt.savefig(f"{result['plots_dirpath']}pnl_cumsum_shrt.png") adg = (sdf.equity / sdf.equity.iloc[0]) ** (1 / ((sdf.timestamp - sdf.timestamp.iloc[0]) / (1000 * 60 * 60 * 24))) plt.clf() adg.plot(title="Average daily gain", xlabel="Time", ylabel="Average daily gain") plt.savefig(f"{result['plots_dirpath']}adg.png") print('plotting backtest whole and in chunks...') n_parts = max(3, int(round_up(result['n_days'] / 14, 1.0))) for z in range(n_parts): start_ = z / n_parts end_ = (z + 1) / n_parts print(f'{z} of {n_parts} {start_ * 100:.2f}% to {end_ * 100:.2f}%') fig = plot_fills(df, fdf.iloc[int(len(fdf) * start_):int(len(fdf) * end_)], bkr_thr=0.1, title=f'Fills {z+1} of {n_parts}') if fig is not None: fig.savefig(f"{result['plots_dirpath']}backtest_{z + 1}of{n_parts}.png") else: print('no fills...') fig = plot_fills(df, fdf, bkr_thr=0.1, plot_whole_df=True, title='Overview Fills') fig.savefig(f"{result['plots_dirpath']}whole_backtest.png") print('plotting pos sizes...') plt.clf() longs.psize.plot() shrts.psize.plot(title="Position size in terms of contracts", xlabel="Time", ylabel="Position size") plt.savefig(f"{result['plots_dirpath']}psizes_plot.png")
def dump_plots( result: dict, longs: pd.DataFrame, shorts: pd.DataFrame, sdf: pd.DataFrame, df: pd.DataFrame, n_parts: int = None, ): init(autoreset=True) plt.rcParams["figure.figsize"] = [29, 18] try: pd.set_option("display.precision", 10) except Exception as e: print("error setting pandas precision", e) table = PrettyTable(["Metric", "Value"]) table.align["Metric"] = "l" table.align["Value"] = "l" table.title = "Summary" table.add_row([ "Exchange", result["exchange"] if "exchange" in result else "unknown" ]) table.add_row([ "Market type", result["market_type"] if "market_type" in result else "unknown" ]) table.add_row( ["Symbol", result["symbol"] if "symbol" in result else "unknown"]) table.add_row(["No. days", round_dynamic(result["result"]["n_days"], 6)]) table.add_row([ "Starting balance", round_dynamic(result["result"]["starting_balance"], 6) ]) for side in ["long", "short"]: if result[side]["enabled"]: table.add_row([" ", " "]) table.add_row([side.capitalize(), result[side]["enabled"]]) adg_per_exp = result["result"][f"adg_{side}"] / result[side][ "wallet_exposure_limit"] table.add_row([ "ADG per exposure", f"{round_dynamic(adg_per_exp * 100, 3)}%" ]) profit_color = ( Fore.RED if result["result"][f"final_balance_{side}"] < result["result"]["starting_balance"] else Fore.RESET) table.add_row([ "Final balance", f"{profit_color}{round_dynamic(result['result'][f'final_balance_{side}'], 6)}{Fore.RESET}", ]) table.add_row([ "Final equity", f"{profit_color}{round_dynamic(result['result'][f'final_equity_{side}'], 6)}{Fore.RESET}", ]) table.add_row([ "Net PNL + fees", f"{profit_color}{round_dynamic(result['result'][f'net_pnl_plus_fees_{side}'], 6)}{Fore.RESET}", ]) table.add_row([ "Total gain", f"{profit_color}{round_dynamic(result['result'][f'gain_{side}'] * 100, 4)}%{Fore.RESET}", ]) table.add_row([ "Average daily gain", f"{profit_color}{round_dynamic((result['result'][f'adg_{side}']) * 100, 3)}%{Fore.RESET}", ]) gain_per_exp = result["result"][f"gain_{side}"] / result[side][ "wallet_exposure_limit"] table.add_row([ "Gain per exposure", f"{round_dynamic(gain_per_exp * 100, 3)}%" ]) table.add_row([ "DG mean std ratio", f"{round_dynamic(result['result'][f'adg_DGstd_ratio_{side}'], 4)}", ]) table.add_row([ f"Price action distance mean", round_dynamic(result["result"][f"pa_distance_mean_{side}"], 6), ]) table.add_row([ f"Price action distance std", round_dynamic(result["result"][f"pa_distance_std_{side}"], 6), ]) table.add_row([ f"Price action distance max", round_dynamic(result["result"][f"pa_distance_max_{side}"], 6), ]) table.add_row([ "Closest bankruptcy", f'{round_dynamic(result["result"][f"closest_bkr_{side}"] * 100, 4)}%', ]) table.add_row([ "Lowest equity/balance ratio", f'{round_dynamic(result["result"][f"eqbal_ratio_min_{side}"], 4)}', ]) table.add_row(["No. fills", result["result"][f"n_fills_{side}"]]) table.add_row( ["No. entries", result["result"][f"n_entries_{side}"]]) table.add_row(["No. closes", result["result"][f"n_closes_{side}"]]) table.add_row([ "No. initial entries", result["result"][f"n_ientries_{side}"] ]) table.add_row( ["No. reentries", result["result"][f"n_rentries_{side}"]]) table.add_row([ "No. unstuck entries", result["result"][f"n_unstuck_entries_{side}"] ]) table.add_row([ "No. unstuck closes", result["result"][f"n_unstuck_closes_{side}"] ]) table.add_row([ "No. normal closes", result["result"][f"n_normal_closes_{side}"] ]) table.add_row([ "Average n fills per day", round_dynamic(result["result"][f"avg_fills_per_day_{side}"], 3), ]) table.add_row([ "Mean hours stuck", round_dynamic(result["result"][f"hrs_stuck_avg_{side}"], 6), ]) table.add_row([ "Max hours stuck", round_dynamic(result["result"][f"hrs_stuck_max_{side}"], 6), ]) profit_color = Fore.RED if result["result"][ f"pnl_sum_{side}"] < 0 else Fore.RESET table.add_row([ "PNL sum", f"{profit_color}{round_dynamic(result['result'][f'pnl_sum_{side}'], 4)}{Fore.RESET}", ]) table.add_row([ "Profit sum", round_dynamic(result["result"][f"profit_sum_{side}"], 4) ]) table.add_row([ "Loss sum", round_dynamic(result["result"][f"loss_sum_{side}"], 4) ]) table.add_row([ "Fee sum", round_dynamic(result["result"][f"fee_sum_{side}"], 4) ]) table.add_row([ "Biggest pos size", result["result"][f"biggest_psize_{side}"] ]) table.add_row([ "Biggest pos cost", round_dynamic(result["result"][f"biggest_psize_quote_{side}"], 4), ]) table.add_row([ "Volume quote", round_dynamic(result["result"][f"volume_quote_{side}"], 6) ]) dump_live_config(result, result["plots_dirpath"] + "live_config.json") json.dump(denumpyize(result), open(result["plots_dirpath"] + "result.json", "w"), indent=4) print("writing backtest_result.txt...\n") with open(f"{result['plots_dirpath']}backtest_result.txt", "w") as f: output = table.get_string(border=True, padding_width=1) print(output) f.write(re.sub("\033\\[([0-9]+)(;[0-9]+)*m", "", output)) n_parts = n_parts if n_parts is not None else max( 3, int(round_up(result["n_days"] / 14, 1.0))) for side, fdf in [("long", longs), ("short", shorts)]: if result[side]["enabled"]: plt.clf() fig = plot_fills(df, fdf, plot_whole_df=True, title=f"Overview Fills {side.capitalize()}") fig.savefig(f"{result['plots_dirpath']}whole_backtest_{side}.png") print(f"\nplotting balance and equity {side}...") plt.clf() sdf[f"balance_{side}"].plot() sdf[f"equity_{side}"].plot( title=f"Balance and equity {side.capitalize()}", xlabel="Time", ylabel="Balance") plt.savefig( f"{result['plots_dirpath']}balance_and_equity_sampled_{side}.png" ) for z in range(n_parts): start_ = z / n_parts end_ = (z + 1) / n_parts print( f"{side} {z} of {n_parts} {start_ * 100:.2f}% to {end_ * 100:.2f}%" ) fig = plot_fills( df, fdf.iloc[int(len(fdf) * start_):int(len(fdf) * end_)], title=f"Fills {side} {z+1} of {n_parts}", ) if fig is not None: fig.savefig( f"{result['plots_dirpath']}backtest_{side}{z + 1}of{n_parts}.png" ) else: print(f"no {side} fills...") print(f"plotting {side} initial entry band") spans_multiplier = 60 / ((df.index[1] - df.index[0]) / 1000) spans = [ result[side]["ema_span_0"] * spans_multiplier, ((result[side]["ema_span_0"] * result[side]["ema_span_1"])** 0.5) * spans_multiplier, result[side]["ema_span_1"] * spans_multiplier, ] emas = pd.DataFrame({ str(span): df.iloc[::100].price.ewm(span=max(1.0, span / 100), adjust=False).mean() for span in spans }) ema_band_lower = emas.min(axis=1) ema_band_upper = emas.max(axis=1) if side == "long": ientry_band = ema_band_lower * ( 1 - result[side]["initial_eprice_ema_dist"]) else: ientry_band = ema_band_upper * ( 1 + result[side]["initial_eprice_ema_dist"]) plt.clf() df.price.iloc[::100].plot( style="y-", title=f"{side.capitalize()} Initial Entry Band") ientry_band.plot(style=f"{('b' if side == 'long' else 'r')}-.") plt.savefig( f"{result['plots_dirpath']}initial_entry_band_{side}.png") if result[side]["auto_unstuck_wallet_exposure_threshold"] != 0.0: print(f"plotting {side} unstucking bands...") unstucking_band_lower = ema_band_lower * ( 1 - result[side]["auto_unstuck_ema_dist"]) unstucking_band_upper = ema_band_upper * ( 1 + result[side]["auto_unstuck_ema_dist"]) plt.clf() df.price.iloc[::100].plot( style="y-", title=f"{side.capitalize()} Auto Unstucking Bands") unstucking_band_lower.plot(style="b-.") unstucking_band_upper.plot(style="r-.") plt.savefig( f"{result['plots_dirpath']}auto_unstuck_bands_{side}.png") print("plotting pos sizes...") plt.clf() sdf[["psize_long", "psize_short"]].plot( title="Position size in terms of contracts", xlabel="Time", ylabel="Position size", ) plt.savefig(f"{result['plots_dirpath']}psizes_plot.png")
async def main() -> None: parser = argparse.ArgumentParser(prog="passivbot", description="run passivbot") parser.add_argument("user", type=str, help="user/account_name defined in api-keys.json") parser.add_argument("symbol", type=str, help="symbol to trade") parser.add_argument("live_config_path", type=str, help="live config to use") parser.add_argument( "-m", "--market_type", type=str, required=False, dest="market_type", default=None, help= "specify whether spot or futures (default), overriding value from backtest config", ) parser.add_argument( "-gs", "--graceful_stop", action="store_true", help="if true, disable long and short", ) 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( "-sm", "--short_mode", "--short-mode", type=str, required=False, dest="short_mode", default=None, help= "specify one of following short modes: [n (normal), m (manual), gs (graceful_stop), p (panic), t (tp_only)]", ) parser.add_argument( "-lm", "--long_mode", "--long-mode", type=str, required=False, dest="long_mode", default=None, help= "specify one of following long modes: [n (normal), m (manual), gs (graceful_stop), p (panic), t (tp_only)]", ) parser.add_argument( "-ab", "--assigned_balance", type=float, required=False, dest="assigned_balance", default=None, help="add assigned_balance to live config", ) args = parser.parse_args() try: accounts = json.load(open("api-keys.json")) except Exception as e: print(e, "failed to load api-keys.json file") return try: account = accounts[args.user] except Exception as e: print("unrecognized account name", args.user, e) return try: config = load_live_config(args.live_config_path) except Exception as e: print(e, "failed to load config", args.live_config_path) return config["user"] = args.user config["exchange"] = account["exchange"] config["symbol"] = args.symbol config[ "market_type"] = args.market_type if args.market_type is not None else "futures" config["passivbot_mode"] = determine_passivbot_mode(config) if args.assigned_balance is not None: print(f"\nassigned balance set to {args.assigned_balance}\n") config["assigned_balance"] = args.assigned_balance if args.long_mode is None: if not config["long"]["enabled"]: config["long_mode"] = "manual" else: if args.long_mode in ["gs", "graceful_stop", "graceful-stop"]: print( "\n\nlong graceful stop enabled; will not make new entries once existing positions are closed\n" ) config["long"]["enabled"] = config["do_long"] = False elif args.long_mode in ["m", "manual"]: print( "\n\nlong manual mode enabled; will neither cancel nor create long orders" ) config["long_mode"] = "manual" elif args.long_mode in ["n", "normal"]: print("\n\nlong normal mode") config["long"]["enabled"] = config["do_long"] = True elif args.long_mode in ["p", "panic"]: print("\nlong panic mode enabled") config["long_mode"] = "panic" config["long"]["enabled"] = config["do_long"] = False elif args.long_mode.lower() in ["t", "tp_only", "tp-only"]: print("\nlong tp only mode enabled") config["long_mode"] = "tp_only" if args.short_mode is None: if not config["short"]["enabled"]: config["short_mode"] = "manual" else: if args.short_mode in ["gs", "graceful_stop", "graceful-stop"]: print( "\n\nshort graceful stop enabled; will not make new entries once existing positions are closed\n" ) config["short"]["enabled"] = config["do_short"] = False elif args.short_mode in ["m", "manual"]: print( "\n\nshort manual mode enabled; will neither cancel nor create short orders" ) config["short_mode"] = "manual" elif args.short_mode in ["n", "normal"]: print("\n\nshort normal mode") config["short"]["enabled"] = config["do_short"] = True elif args.short_mode in ["p", "panic"]: print("\nshort panic mode enabled") config["short_mode"] = "panic" config["short"]["enabled"] = config["do_short"] = False elif args.short_mode.lower() in ["t", "tp_only", "tp-only"]: print("\nshort tp only mode enabled") config["short_mode"] = "tp_only" if args.graceful_stop: print( "\n\ngraceful stop enabled for both long and short; will not make new entries once existing positions are closed\n" ) config["long"]["enabled"] = config["do_long"] = False config["short"]["enabled"] = config["do_short"] = False config["long_mode"] = None config["short_mode"] = None 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 "spot" in config["market_type"]: config = spotify_config(config) if account["exchange"] == "binance": if "spot" in config["market_type"]: from procedures import create_binance_bot_spot bot = await create_binance_bot_spot(config) else: from procedures import create_binance_bot bot = await create_binance_bot(config) elif account["exchange"] == "binance_us": from procedures import create_binance_bot_spot bot = await create_binance_bot_spot(config) elif account["exchange"] == "bybit": from procedures import create_bybit_bot bot = await create_bybit_bot(config) else: raise Exception("unknown exchange", account["exchange"]) print("using config") pprint.pprint(denumpyize(config)) signal.signal(signal.SIGINT, bot.stop) signal.signal(signal.SIGTERM, bot.stop) await start_bot(bot) await bot.session.close()