P2SAMAPA commited on
Commit
fb162a4
Β·
unverified Β·
1 Parent(s): f0300e9

Update strategy.py

Browse files
Files changed (1) hide show
  1. 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
- # ── Filter to NYSE trading days ──────────────────────────────────────────
29
- arrays_to_filter = [proba, y_fwd_test]
30
- if daily_ret_override is not None:
31
- arrays_to_filter.append(daily_ret_override)
32
-
33
- filtered_dates, filtered_arrays = filter_to_trading_days(test_dates, arrays_to_filter)
34
-
35
- # Safety: if filter dropped too many dates (calendar issue), use original
36
- if len(filtered_dates) < len(test_dates) * 0.8:
37
- filtered_dates = test_dates
38
- filtered_arrays = arrays_to_filter
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 = filtered_arrays[2]
45
 
46
  strat_rets = []
47
  audit_trail = []
48
  daily_rf = sofr / 252
49
  stop_active = False
50
- recent_rets = [] # rolling 2-day buffer for stop-loss check
51
- top_pick_rets = [] # rolling 5-day buffer of TOP PICK's actual daily returns
52
- # (always tracks #1 ranked ETF regardless of rotation)
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] # best β†’ worst
59
  best_idx = int(ranked_indices[0])
60
  second_idx = int(ranked_indices[1]) if len(ranked_indices) > 1 else best_idx
61
 
62
- # ── Get top pick's actual return for this day (for rotation tracking) ─
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
- # Evaluate BEFORE appending today β€” buffer contains last 5 completed days
69
- # Rule: if top pick lost every day for 5 consecutive days β†’ rotate to #2
70
- # Recovery: as soon as top pick has a positive day β†’ rotate back to #1
 
71
  if rotated_etf_idx is not None:
72
- # Already rotated β€” check if top pick recovered
73
  if top_actual > 0:
74
- rotated_etf_idx = None # top pick positive β†’ return to it
75
- else:
76
- # Not rotated β€” check if 5 consecutive losses warrant rotation
77
- if len(top_pick_rets) >= 5 and all(r < 0 for r in top_pick_rets[-5:]):
 
78
  rotated_etf_idx = second_idx
79
 
80
- # ── Active ETF for today ──────────────────────────────────────────────
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
- if daily_ret_override is not None:
86
- realized_ret = float(daily_ret_override[i][active_idx])
87
- else:
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 trade decision
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 β€” include all dates up to and including today ────────
129
  trade_date = test_dates[i]
130
- if trade_date.date() <= today:
 
 
 
 
 
131
  audit_trail.append({
132
- 'Date': trade_date.strftime('%Y-%m-%d'),
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),