Esempio n. 1
0
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()]
Esempio n. 2
0
 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
Esempio n. 3
0
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)
Esempio n. 4
0
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()
Esempio n. 5
0
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,
    }
Esempio n. 6
0
    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())
Esempio n. 7
0
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)
Esempio n. 8
0
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")
Esempio n. 9
0
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")
Esempio n. 10
0
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()