P2SAMAPA commited on
Commit
f1ea46e
Β·
unverified Β·
1 Parent(s): e96882a

Update strategy.py

Browse files
Files changed (1) hide show
  1. strategy.py +47 -32
strategy.py CHANGED
@@ -24,55 +24,70 @@ 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
  arrays_to_filter = [proba, y_fwd_test]
28
  if daily_ret_override is not None:
29
  arrays_to_filter.append(daily_ret_override)
30
 
31
  filtered_dates, filtered_arrays = filter_to_trading_days(test_dates, arrays_to_filter)
 
 
 
 
 
 
32
  test_dates = filtered_dates
33
  proba = filtered_arrays[0]
34
  y_fwd_test = filtered_arrays[1]
35
  if daily_ret_override is not None:
36
  daily_ret_override = filtered_arrays[2]
37
 
38
- strat_rets = []
39
- audit_trail = []
40
- daily_rf = sofr / 252
41
- stop_active = False
42
- recent_rets = [] # rolling 2-day buffer for stop check
43
- consec_loss_rets = [] # rolling 5-day buffer for consecutive loss check
44
- rotated_etf_idx = None # set when we've rotated away from top pick
45
- today = datetime.now().date()
 
46
 
47
  for i in range(len(proba)):
48
  day_scores = np.array(proba[i], dtype=float)
49
- ranked_indices = np.argsort(day_scores)[::-1] # best β†’ worst
50
  best_idx = int(ranked_indices[0])
51
  second_idx = int(ranked_indices[1]) if len(ranked_indices) > 1 else best_idx
52
 
 
 
 
 
 
53
  # ── 5-day consecutive loss rotation ──────────────────────────────────
54
- # If top ETF has lost money every single day for 5 days β†’ rotate to #2
55
- # Once top ETF has a positive day β†’ rotate back
 
56
  if rotated_etf_idx is not None:
57
- top_daily = (float(daily_ret_override[i][best_idx])
58
- if daily_ret_override is not None
59
- else float(y_fwd_test[i][best_idx]))
60
- if top_daily > 0:
61
- rotated_etf_idx = None # top pick recovering β€” return to it
62
-
63
- if rotated_etf_idx is None and len(consec_loss_rets) >= 5:
64
- if all(r < 0 for r in consec_loss_rets[-5:]):
65
- rotated_etf_idx = second_idx # rotate to model's #2 pick
66
 
67
- active_idx = rotated_etf_idx if rotated_etf_idx is not None else best_idx
68
- _, day_z, _ = compute_signal_conviction(day_scores)
69
- signal_etf = target_etfs[active_idx].replace('_Ret', '')
 
70
 
71
  if daily_ret_override is not None:
72
  realized_ret = float(daily_ret_override[i][active_idx])
73
  else:
74
  realized_ret = float(y_fwd_test[i][active_idx])
75
 
 
76
  if stop_active:
77
  if day_z >= z_reentry and day_z >= z_min_entry:
78
  stop_active = False
@@ -100,25 +115,23 @@ def execute_strategy(proba, y_fwd_test, test_dates, target_etfs, fee_bps,
100
  trade_signal = signal_etf
101
 
102
  strat_rets.append(net_ret)
 
 
103
  recent_rets.append(net_ret)
104
  if len(recent_rets) > 2:
105
  recent_rets.pop(0)
106
 
107
- # Track top-pick's actual daily return for consecutive loss detection
108
- top_actual = (float(daily_ret_override[i][best_idx])
109
- if daily_ret_override is not None
110
- else float(y_fwd_test[i][best_idx]))
111
- consec_loss_rets.append(top_actual)
112
- if len(consec_loss_rets) > 5:
113
- consec_loss_rets.pop(0)
114
 
 
115
  trade_date = test_dates[i]
116
  if trade_date.date() <= today:
117
  audit_trail.append({
118
  'Date': trade_date.strftime('%Y-%m-%d'),
119
  'Signal': trade_signal,
120
  'Conviction_Z': round(day_z, 2),
121
- 'Realized': round(realized_ret, 5),
122
  'Net_Return': round(net_ret, 5),
123
  'Stop_Active': stop_active,
124
  'Rotated': rotated_etf_idx is not None
@@ -126,11 +139,13 @@ def execute_strategy(proba, y_fwd_test, test_dates, target_etfs, fee_bps,
126
 
127
  strat_rets = np.array(strat_rets)
128
 
 
129
  if len(test_dates) > 0 and len(proba) > 0:
130
  last_date = test_dates[-1]
131
  next_trading_date = get_next_trading_day(last_date)
132
  last_scores = np.array(proba[-1], dtype=float)
133
- next_best_idx, conviction_zscore, conviction_label = compute_signal_conviction(last_scores)
 
134
  next_signal = target_etfs[next_best_idx].replace('_Ret', '')
135
  all_etf_scores = last_scores
136
  else:
 
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:
92
  if day_z >= z_reentry and day_z >= z_min_entry:
93
  stop_active = False
 
115
  trade_signal = signal_etf
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)
123
 
124
+ top_pick_rets.append(top_actual)
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),
136
  'Stop_Active': stop_active,
137
  'Rotated': rotated_etf_idx is not None
 
139
 
140
  strat_rets = np.array(strat_rets)
141
 
142
+ # ── Next trading day signal ───────────────────────────────────────────────
143
  if len(test_dates) > 0 and len(proba) > 0:
144
  last_date = test_dates[-1]
145
  next_trading_date = get_next_trading_day(last_date)
146
  last_scores = np.array(proba[-1], dtype=float)
147
+ next_best_idx, conviction_zscore, conviction_label = \
148
+ compute_signal_conviction(last_scores)
149
  next_signal = target_etfs[next_best_idx].replace('_Ret', '')
150
  all_etf_scores = last_scores
151
  else: