Spaces:
Running
Running
File size: 8,273 Bytes
620004a 3ac23cd 620004a c3d6bbe 620004a 496032f 3ac23cd 496032f 3ac23cd b0c5d87 3ac23cd 7605749 fb162a4 3ac23cd fb162a4 496032f f1ea46e fb162a4 f1ea46e 3ac23cd 721b886 fb162a4 721b886 fb162a4 f1ea46e 99ab553 721b886 f1ea46e fb162a4 99ab553 fb162a4 99ab553 f1ea46e 721b886 fb162a4 f1ea46e 3ac23cd fb162a4 b0c5d87 f1ea46e b0c5d87 3ac23cd b0c5d87 3ac23cd b0c5d87 a9781e5 3ac23cd a9781e5 3ac23cd a9781e5 3ac23cd a9781e5 b0c5d87 3ac23cd b0c5d87 496032f 620004a f1ea46e fb162a4 b0c5d87 496032f f1ea46e 721b886 8cb7f5b 94d99f9 fb162a4 8cb7f5b c3d6bbe fb162a4 3ac23cd 27c2334 3ac23cd 721b886 c3d6bbe 496032f 620004a 496032f f1ea46e 3ac23cd 620004a 3ac23cd f1ea46e 3ac23cd 620004a 3ac23cd 496032f 3ac23cd 496032f 620004a 3ac23cd 620004a 3ac23cd 620004a 3ac23cd 620004a 3ac23cd 620004a 3ac23cd 620004a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | """
Backtesting and strategy execution logic β TFT model only.
"""
import numpy as np
import pandas as pd
from datetime import datetime
from utils import filter_to_trading_days, get_next_trading_day
def compute_signal_conviction(raw_scores):
best_idx = int(np.argmax(raw_scores))
mean = np.mean(raw_scores)
std = np.std(raw_scores)
z = 0.0 if std < 1e-9 else (raw_scores[best_idx] - mean) / std
if z >= 2.0: label = "Very High"
elif z >= 1.0: label = "High"
elif z >= 0.0: label = "Moderate"
else: label = "Low"
return best_idx, z, label
def execute_strategy(proba, y_fwd_test, test_dates, target_etfs, fee_bps,
stop_loss_pct=-0.12, z_reentry=1.0,
sofr=0.045, z_min_entry=0.5,
daily_ret_override=None):
"""
Execute strategy on test set.
NOTE: We do NOT filter by NYSE calendar here β test_dates already come
from the dataset which only contains trading days. Calendar filtering
was dropping recent dates due to calendar library lag.
"""
# Convert to pandas DatetimeIndex if needed
if not isinstance(test_dates, pd.DatetimeIndex):
test_dates = pd.DatetimeIndex(test_dates)
# Convert arrays to numpy
proba = np.array(proba)
y_fwd_test = np.array(y_fwd_test)
if daily_ret_override is not None:
daily_ret_override = np.array(daily_ret_override)
strat_rets = []
audit_trail = []
daily_rf = sofr / 252
stop_active = False
recent_rets = [] # last 2 net returns for stop-loss check
top_pick_rets = [] # last 5 TOP-PICK actual daily returns for rotation
rotated_etf_idx = None
today = datetime.now().date()
for i in range(len(proba)):
day_scores = np.array(proba[i], dtype=float)
ranked_indices = np.argsort(day_scores)[::-1]
best_idx = int(ranked_indices[0])
second_idx = int(ranked_indices[1]) if len(ranked_indices) > 1 else best_idx
# Top pick's actual daily return (always track #1 pick for rotation)
top_actual = (float(daily_ret_override[i][best_idx])
if daily_ret_override is not None
else float(y_fwd_test[i][best_idx]))
# ββ 5-day cumulative loss rotation βββββββββββββββββββββββββββββββββββ
# Rule: if top pick's 5-day cumulative return < 0 β rotate to #2
# Recovery: as soon as top pick has a positive day β rotate back to #1
if rotated_etf_idx is not None:
if top_actual > 0:
rotated_etf_idx = None
if rotated_etf_idx is None and len(top_pick_rets) >= 5:
cum_5d = 1.0
for r in top_pick_rets[-5:]:
cum_5d *= (1 + r)
cum_5d -= 1.0
if cum_5d < 0:
rotated_etf_idx = second_idx
active_idx = rotated_etf_idx if rotated_etf_idx is not None else best_idx
_, day_z, _ = compute_signal_conviction(day_scores)
signal_etf = target_etfs[active_idx].replace('_Ret', '')
realized_ret = (float(daily_ret_override[i][active_idx])
if daily_ret_override is not None
else float(y_fwd_test[i][active_idx]))
# ββ Stop-loss + conviction gate βββββββββββββββββββββββββββββββββββββββ
if stop_active:
if day_z >= z_reentry and day_z >= z_min_entry:
stop_active = False
net_ret = realized_ret - (fee_bps / 10000)
trade_signal = signal_etf
else:
net_ret = daily_rf
trade_signal = "CASH"
else:
if day_z < z_min_entry:
net_ret = daily_rf
trade_signal = "CASH"
else:
if len(recent_rets) >= 2:
cum_2d = (1 + recent_rets[-2]) * (1 + recent_rets[-1]) - 1
if cum_2d <= stop_loss_pct:
stop_active = True
net_ret = daily_rf
trade_signal = "CASH"
else:
net_ret = realized_ret - (fee_bps / 10000)
trade_signal = signal_etf
else:
net_ret = realized_ret - (fee_bps / 10000)
trade_signal = signal_etf
strat_rets.append(net_ret)
# ββ Update rolling buffers AFTER decision ββββββββββββββββββββββββββββ
recent_rets.append(net_ret)
if len(recent_rets) > 2:
recent_rets.pop(0)
top_pick_rets.append(top_actual)
if len(top_pick_rets) > 5:
top_pick_rets.pop(0)
# Audit trail: only show CLOSED trading days (exclude today β market not yet closed)
trade_date = test_dates[i]
if hasattr(trade_date, 'date'):
trade_date_val = trade_date.date()
else:
trade_date_val = trade_date
if trade_date_val < today:
audit_trail.append({
'Date': trade_date_val.strftime('%Y-%m-%d'),
'Signal': trade_signal,
'Top_Pick': target_etfs[best_idx].replace('_Ret', ''),
'Conviction_Z': round(day_z, 2),
'Net_Return': round(net_ret, 5),
'Stop_Active': stop_active,
'Rotated': rotated_etf_idx is not None
})
strat_rets = np.array(strat_rets)
# ββ Next trading day signal βββββββββββββββββββββββββββββββββββββββββββββββ
if len(test_dates) > 0 and len(proba) > 0:
last_date = test_dates[-1]
next_trading_date = get_next_trading_day(last_date)
last_scores = np.array(proba[-1], dtype=float)
next_best_idx, conviction_zscore, conviction_label = \
compute_signal_conviction(last_scores)
next_signal = target_etfs[next_best_idx].replace('_Ret', '')
all_etf_scores = last_scores
else:
next_trading_date = datetime.now().date()
next_signal = "CASH"
conviction_zscore = 0.0
conviction_label = "Low"
all_etf_scores = np.zeros(len(target_etfs))
return (strat_rets, audit_trail, next_signal, next_trading_date,
conviction_zscore, conviction_label, all_etf_scores)
def calculate_metrics(strat_rets, sofr_rate=0.045):
cum_returns = np.cumprod(1 + strat_rets)
ann_return = (cum_returns[-1] ** (252 / len(strat_rets))) - 1
sharpe = ((np.mean(strat_rets) - sofr_rate / 252) /
(np.std(strat_rets) + 1e-9) * np.sqrt(252))
recent_rets = strat_rets[-15:]
hit_ratio = np.mean(recent_rets > 0)
cum_max = np.maximum.accumulate(cum_returns)
drawdown = (cum_returns - cum_max) / cum_max
max_dd = np.min(drawdown)
max_daily_dd = np.min(strat_rets)
return {
'cum_returns': cum_returns,
'ann_return': ann_return,
'sharpe': sharpe,
'hit_ratio': hit_ratio,
'max_dd': max_dd,
'max_daily_dd': max_daily_dd,
'cum_max': cum_max
}
def calculate_benchmark_metrics(benchmark_returns, sofr_rate=0.045):
cum_returns = np.cumprod(1 + benchmark_returns)
ann_return = (cum_returns[-1] ** (252 / len(benchmark_returns))) - 1
sharpe = ((np.mean(benchmark_returns) - sofr_rate / 252) /
(np.std(benchmark_returns) + 1e-9) * np.sqrt(252))
cum_max = np.maximum.accumulate(cum_returns)
dd = (cum_returns - cum_max) / cum_max
max_dd = np.min(dd)
max_daily_dd = np.min(benchmark_returns)
return {
'cum_returns': cum_returns,
'ann_return': ann_return,
'sharpe': sharpe,
'max_dd': max_dd,
'max_daily_dd': max_daily_dd
}
|