Spaces:
Running
Running
P2SAMAPA commited on
Update strategy.py
Browse files- strategy.py +42 -41
strategy.py
CHANGED
|
@@ -24,68 +24,64 @@ def execute_strategy(proba, y_fwd_test, test_dates, target_etfs, fee_bps,
|
|
| 24 |
stop_loss_pct=-0.12, z_reentry=1.0,
|
| 25 |
sofr=0.045, z_min_entry=0.5,
|
| 26 |
daily_ret_override=None):
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
test_dates = filtered_dates
|
| 41 |
-
proba = filtered_arrays[0]
|
| 42 |
-
y_fwd_test = filtered_arrays[1]
|
| 43 |
if daily_ret_override is not None:
|
| 44 |
-
daily_ret_override =
|
| 45 |
|
| 46 |
strat_rets = []
|
| 47 |
audit_trail = []
|
| 48 |
daily_rf = sofr / 252
|
| 49 |
stop_active = False
|
| 50 |
-
recent_rets = []
|
| 51 |
-
top_pick_rets = []
|
| 52 |
-
|
| 53 |
-
rotated_etf_idx = None # None = use top pick; int = rotated to this index
|
| 54 |
today = datetime.now().date()
|
| 55 |
|
| 56 |
for i in range(len(proba)):
|
| 57 |
day_scores = np.array(proba[i], dtype=float)
|
| 58 |
-
ranked_indices = np.argsort(day_scores)[::-1]
|
| 59 |
best_idx = int(ranked_indices[0])
|
| 60 |
second_idx = int(ranked_indices[1]) if len(ranked_indices) > 1 else best_idx
|
| 61 |
|
| 62 |
-
#
|
| 63 |
top_actual = (float(daily_ret_override[i][best_idx])
|
| 64 |
if daily_ret_override is not None
|
| 65 |
else float(y_fwd_test[i][best_idx]))
|
| 66 |
|
| 67 |
# ββ 5-day consecutive loss rotation ββββββββββββββββββββββββββββββββββ
|
| 68 |
-
#
|
| 69 |
-
#
|
| 70 |
-
#
|
|
|
|
| 71 |
if rotated_etf_idx is not None:
|
| 72 |
-
#
|
| 73 |
if top_actual > 0:
|
| 74 |
-
rotated_etf_idx = None
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
| 78 |
rotated_etf_idx = second_idx
|
| 79 |
|
| 80 |
-
|
| 81 |
-
active_idx = rotated_etf_idx if rotated_etf_idx is not None else best_idx
|
| 82 |
_, day_z, _ = compute_signal_conviction(day_scores)
|
| 83 |
signal_etf = target_etfs[active_idx].replace('_Ret', '')
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
realized_ret = float(y_fwd_test[i][active_idx])
|
| 89 |
|
| 90 |
# ββ Stop-loss + conviction gate βββββββββββββββββββββββββββββββββββββββ
|
| 91 |
if stop_active:
|
|
@@ -116,7 +112,7 @@ def execute_strategy(proba, y_fwd_test, test_dates, target_etfs, fee_bps,
|
|
| 116 |
|
| 117 |
strat_rets.append(net_ret)
|
| 118 |
|
| 119 |
-
# Update rolling buffers AFTER
|
| 120 |
recent_rets.append(net_ret)
|
| 121 |
if len(recent_rets) > 2:
|
| 122 |
recent_rets.pop(0)
|
|
@@ -125,11 +121,16 @@ def execute_strategy(proba, y_fwd_test, test_dates, target_etfs, fee_bps,
|
|
| 125 |
if len(top_pick_rets) > 5:
|
| 126 |
top_pick_rets.pop(0)
|
| 127 |
|
| 128 |
-
# ββ Audit trail β
|
| 129 |
trade_date = test_dates[i]
|
| 130 |
-
if trade_date
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
audit_trail.append({
|
| 132 |
-
'Date':
|
| 133 |
'Signal': trade_signal,
|
| 134 |
'Conviction_Z': round(day_z, 2),
|
| 135 |
'Net_Return': round(net_ret, 5),
|
|
|
|
| 24 |
stop_loss_pct=-0.12, z_reentry=1.0,
|
| 25 |
sofr=0.045, z_min_entry=0.5,
|
| 26 |
daily_ret_override=None):
|
| 27 |
+
"""
|
| 28 |
+
Execute strategy on test set.
|
| 29 |
+
NOTE: We do NOT filter by NYSE calendar here β test_dates already come
|
| 30 |
+
from the dataset which only contains trading days. Calendar filtering
|
| 31 |
+
was dropping recent dates due to calendar library lag.
|
| 32 |
+
"""
|
| 33 |
+
# Convert to pandas DatetimeIndex if needed
|
| 34 |
+
if not isinstance(test_dates, pd.DatetimeIndex):
|
| 35 |
+
test_dates = pd.DatetimeIndex(test_dates)
|
| 36 |
+
|
| 37 |
+
# Convert arrays to numpy
|
| 38 |
+
proba = np.array(proba)
|
| 39 |
+
y_fwd_test = np.array(y_fwd_test)
|
|
|
|
|
|
|
|
|
|
| 40 |
if daily_ret_override is not None:
|
| 41 |
+
daily_ret_override = np.array(daily_ret_override)
|
| 42 |
|
| 43 |
strat_rets = []
|
| 44 |
audit_trail = []
|
| 45 |
daily_rf = sofr / 252
|
| 46 |
stop_active = False
|
| 47 |
+
recent_rets = [] # last 2 net returns for stop-loss check
|
| 48 |
+
top_pick_rets = [] # last 5 TOP-PICK actual daily returns for rotation
|
| 49 |
+
rotated_etf_idx = None
|
|
|
|
| 50 |
today = datetime.now().date()
|
| 51 |
|
| 52 |
for i in range(len(proba)):
|
| 53 |
day_scores = np.array(proba[i], dtype=float)
|
| 54 |
+
ranked_indices = np.argsort(day_scores)[::-1]
|
| 55 |
best_idx = int(ranked_indices[0])
|
| 56 |
second_idx = int(ranked_indices[1]) if len(ranked_indices) > 1 else best_idx
|
| 57 |
|
| 58 |
+
# Top pick's actual daily return (always track #1 pick for rotation)
|
| 59 |
top_actual = (float(daily_ret_override[i][best_idx])
|
| 60 |
if daily_ret_override is not None
|
| 61 |
else float(y_fwd_test[i][best_idx]))
|
| 62 |
|
| 63 |
# ββ 5-day consecutive loss rotation ββββββββββββββββββββββββββββββββββ
|
| 64 |
+
# Buffer contains PREVIOUS days' returns (appended at end of loop)
|
| 65 |
+
# So on day i, top_pick_rets has returns from days [i-5 .. i-1]
|
| 66 |
+
# Check: all 5 previous days negative β rotate to #2
|
| 67 |
+
# Recovery: top pick positive today β rotate back to #1
|
| 68 |
if rotated_etf_idx is not None:
|
| 69 |
+
# Currently rotated β check if top pick has recovered today
|
| 70 |
if top_actual > 0:
|
| 71 |
+
rotated_etf_idx = None
|
| 72 |
+
|
| 73 |
+
# Only check rotation trigger if not already rotated
|
| 74 |
+
if rotated_etf_idx is None and len(top_pick_rets) >= 5:
|
| 75 |
+
if all(r < 0 for r in top_pick_rets[-5:]):
|
| 76 |
rotated_etf_idx = second_idx
|
| 77 |
|
| 78 |
+
active_idx = rotated_etf_idx if rotated_etf_idx is not None else best_idx
|
|
|
|
| 79 |
_, day_z, _ = compute_signal_conviction(day_scores)
|
| 80 |
signal_etf = target_etfs[active_idx].replace('_Ret', '')
|
| 81 |
|
| 82 |
+
realized_ret = (float(daily_ret_override[i][active_idx])
|
| 83 |
+
if daily_ret_override is not None
|
| 84 |
+
else float(y_fwd_test[i][active_idx]))
|
|
|
|
| 85 |
|
| 86 |
# ββ Stop-loss + conviction gate βββββββββββββββββββββββββββββββββββββββ
|
| 87 |
if stop_active:
|
|
|
|
| 112 |
|
| 113 |
strat_rets.append(net_ret)
|
| 114 |
|
| 115 |
+
# ββ Update rolling buffers AFTER decision ββββββββββββββββββββββββββββ
|
| 116 |
recent_rets.append(net_ret)
|
| 117 |
if len(recent_rets) > 2:
|
| 118 |
recent_rets.pop(0)
|
|
|
|
| 121 |
if len(top_pick_rets) > 5:
|
| 122 |
top_pick_rets.pop(0)
|
| 123 |
|
| 124 |
+
# ββ Audit trail β all dates up to and including today βββββββββββββββββ
|
| 125 |
trade_date = test_dates[i]
|
| 126 |
+
if hasattr(trade_date, 'date'):
|
| 127 |
+
trade_date_val = trade_date.date()
|
| 128 |
+
else:
|
| 129 |
+
trade_date_val = trade_date
|
| 130 |
+
|
| 131 |
+
if trade_date_val <= today:
|
| 132 |
audit_trail.append({
|
| 133 |
+
'Date': trade_date_val.strftime('%Y-%m-%d'),
|
| 134 |
'Signal': trade_signal,
|
| 135 |
'Conviction_Z': round(day_z, 2),
|
| 136 |
'Net_Return': round(net_ret, 5),
|