diff --git a/configs/optimize/default.hjson b/configs/optimize/default.hjson index 45ecb2606..e8818c213 100644 --- a/configs/optimize/default.hjson +++ b/configs/optimize/default.hjson @@ -8,6 +8,8 @@ # set to 0.0 to disable breaking early break_early_factor: 0.5 + minimum_sharpe_ratio: 1.1 + sharpe_ratio_n_days: 3.0 minimum_bankruptcy_distance: 0.5 minimum_equity_balance_ratio: 0.5 minimum_slice_adg: 1.0 diff --git a/optimize.py b/optimize.py index 437a5f70e..29954236d 100644 --- a/optimize.py +++ b/optimize.py @@ -107,6 +107,7 @@ def objective_function(analysis: dict, config: dict, metric='adjusted_daily_gain * min(1.0, config['maximum_hrs_no_fills_same_side'] / analysis['max_hrs_no_fills_same_side']) * min(1.0, analysis['closest_bkr'] / config['minimum_bankruptcy_distance']) * min(1.0, analysis['lowest_eqbal_ratio'] / config['minimum_equity_balance_ratio']) + * min(1.0, analysis['sharpe_ratio'] / config['minimum_sharpe_ratio']) ) @@ -144,6 +145,7 @@ def single_sliding_window_run(config, data, do_print=False) -> (float, [dict]): line = (f'{str(z).rjust(3, " ")} adg {analysis["average_daily_gain"]:.4f}, ' f'bkr {analysis["closest_bkr"]:.4f}, ' f'eqbal {analysis["lowest_eqbal_ratio"]:.4f} n_days {analysis["n_days"]:.1f}, ' + f'sharpe_ratio {analysis["sharpe_ratio"]:.4f} , ' f'score {analysis["score"]:.4f}, objective {objective:.4f}, ' f'hrs stuck ss {str(round(analysis["max_hrs_no_fills_same_side"], 1)).zfill(4)}, ') if (bef := config['break_early_factor']) != 0.0: @@ -163,6 +165,10 @@ def single_sliding_window_run(config, data, do_print=False) -> (float, [dict]): line += f"broke on max_hrs_no_fills_ss {analysis['max_hrs_no_fills_same_side']:.4f}, {config['maximum_hrs_no_fills_same_side']}" print(line) break + if analysis['sharpe_ratio'] < config['minimum_sharpe_ratio'] * (1 - bef): + line += f"broke on low sharpe ratio {analysis['sharpe_ratio']:.4f} " + print(line) + break if analysis['average_daily_gain'] < config['minimum_slice_adg']: line += f"broke on low adg {analysis['average_daily_gain']:.4f} " print(line) @@ -188,6 +194,7 @@ def simple_sliding_window_wrap(config, data, do_print=False): daily_gain=np.mean([r['average_daily_gain'] for r in analyses]), closest_bkr=np.min([r['closest_bkr'] for r in analyses]), lowest_eqbal_r=np.min([r['lowest_eqbal_ratio'] for r in analyses]), + sharpe_ratio=np.mean([r['sharpe_ratio'] for r in analyses]), max_hrs_no_fills=np.max([r['max_hrs_no_fills'] for r in analyses]), max_hrs_no_fills_ss=np.max([r['max_hrs_no_fills_same_side'] for r in analyses])) @@ -258,6 +265,7 @@ def backtest_tune(data: np.ndarray, config: dict, current_best: Union[dict, list metric_columns=['daily_gain', 'closest_bkr', 'lowest_eqbal_r', + 'sharpe_ratio', 'max_hrs_no_fills', 'max_hrs_no_fills_ss', 'objective'], @@ -292,8 +300,10 @@ async def main(): return downloader = Downloader(config) print() - for k in (keys := ['exchange', 'symbol', 'starting_balance', 'start_date', 'end_date', 'latency_simulation_ms', - 'do_long', 'do_shrt', 'minimum_bankruptcy_distance', 'maximum_hrs_no_fills', + for k in (keys := ['exchange', 'symbol', 'starting_balance', 'start_date', + 'end_date', 'latency_simulation_ms', + 'do_long', 'do_shrt', 'minimum_sharpe_ratio', 'sharpe_ratio_n_days', + 'minimum_bankruptcy_distance', 'maximum_hrs_no_fills', 'maximum_hrs_no_fills_same_side', 'iters', 'n_particles', 'sliding_window_days', 'metric', 'min_span', 'max_span', 'n_spans']): if k in config: diff --git a/plotting.py b/plotting.py index 2d5e44258..5b55403be 100644 --- a/plotting.py +++ b/plotting.py @@ -19,6 +19,7 @@ def gain_conv(x): lines.append(f"symbol {result['symbol'] if 'symbol' in result else 'unknown'}") lines.append(f"gain percentage {round_dynamic(result['result']['gain'] * 100 - 100, 4)}%") lines.append(f"average_daily_gain percentage {round_dynamic((result['result']['average_daily_gain'] - 1) * 100, 3)}%") + lines.append(f"sharpe_ratio percentage {round_dynamic(result['result']['sharpe_ratio'] * 100, 3)}%") lines.append(f"closest_bkr percentage {round_dynamic(result['result']['closest_bkr'] * 100, 4)}%") lines.append(f"starting balance {round_dynamic(result['starting_balance'], 3)}") diff --git a/pure_funcs.py b/pure_funcs.py index 977d62782..35448226a 100644 --- a/pure_funcs.py +++ b/pure_funcs.py @@ -380,6 +380,7 @@ def get_empty_analysis(bc: dict) -> dict: 'n_days': 0.0, 'average_daily_gain': 0.0, 'adjusted_daily_gain': 0.0, + 'sharpe_ratio': 0.0, 'lowest_eqbal_ratio': 0.0, 'closest_bkr': 1.0, 'n_fills': 0.0, @@ -430,7 +431,14 @@ def analyze_fills(fills: list, bc: dict, first_ts: float, last_ts: float) -> (pd else: shrt_stuck_mean = 0.0 shrt_stuck = 0.0 - + + ms_span = 1000 * 60 * 60 * 24 * bc['sharpe_ratio_n_days'] + groups = fdf.groupby(fdf.timestamp // ms_span) + periodic_gains = groups.pnl.sum() / groups.balance.first() + periodic_gains = periodic_gains.reindex(np.arange(periodic_gains.index[0], periodic_gains.index[-1])).fillna(0.0) + periodic_gains_std = periodic_gains.std() + sharpe_ratio = periodic_gains.mean() / periodic_gains_std if periodic_gains_std != 0.0 else -20.0 + sharpe_ratio = np.nan_to_num(sharpe_ratio) result = { 'starting_balance': bc['starting_balance'], 'final_balance': fdf.iloc[-1].balance, @@ -440,6 +448,7 @@ def analyze_fills(fills: list, bc: dict, first_ts: float, last_ts: float) -> (pd 'n_days': (n_days := (last_ts - first_ts) / (1000 * 60 * 60 * 24)), 'average_daily_gain': (adg := gain ** (1 / n_days) if gain > 0.0 and n_days > 0.0 else 0.0), 'adjusted_daily_gain': np.tanh(10 * (adg - 1)) + 1, + 'sharpe_ratio': sharpe_ratio, 'profit_sum': fdf[fdf.pnl > 0.0].pnl.sum(), 'loss_sum': fdf[fdf.pnl < 0.0].pnl.sum(), 'fee_sum': fdf.fee_paid.sum(),