Skip to content

Commit 79aca08

Browse files
committed
Add strict drawdown controls to Binance meta eval
1 parent c2a91a1 commit 79aca08

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

scripts/search_binance33_meta_anneal.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def _load_rule_rows(path: Path, *, configs: Sequence[str], max_rules: int) -> li
132132
selected = [row for name in wanted for row in rows if row.get("config") == name]
133133
else:
134134
def key(row: dict[str, str]) -> float:
135-
dd = float(row.get("p90_dd_pct") or 100.0)
135+
dd = float(row.get("worst_dd_pct") or row.get("p90_dd_pct") or 100.0)
136136
median = float(row.get("median_monthly_pct") or -999.0)
137137
p10 = float(row.get("p10_monthly_pct") or -999.0)
138138
neg = int(float(row.get("neg_windows") or 99))
@@ -285,6 +285,21 @@ def _build_bank(
285285
return ScoreBank(names=names, scores=scores)
286286

287287

288+
def _apply_excluded_symbols(bank: ScoreBank, data: MktdData, excluded_symbols: Sequence[str]) -> ScoreBank:
289+
excluded = {str(symbol).strip().upper() for symbol in excluded_symbols if str(symbol).strip()}
290+
if not excluded:
291+
return bank
292+
symbols = [str(symbol).upper() for symbol in data.symbols]
293+
missing = sorted(excluded - set(symbols))
294+
if missing:
295+
raise ValueError(f"excluded symbols not present in data: {', '.join(missing)}")
296+
scores = np.asarray(bank.scores, dtype=np.float64).copy()
297+
for idx, symbol in enumerate(symbols):
298+
if symbol in excluded:
299+
scores[:, :, idx] = np.nan
300+
return ScoreBank(names=list(bank.names), scores=scores)
301+
302+
288303
def _combine_scores(bank: ScoreBank, candidate: Candidate) -> np.ndarray:
289304
weights = _softmax(candidate.logits)
290305
return np.tensordot(weights, bank.scores, axes=(0, 0)).astype(np.float64, copy=False)
@@ -760,6 +775,7 @@ def _summarise_results(
760775
"neg_windows": int(np.sum(returns < 0.0)),
761776
"windows": int(returns.size),
762777
"p90_dd_pct": float(100.0 * np.percentile(maxdds, 90)),
778+
"worst_dd_pct": float(100.0 * np.max(maxdds)) if maxdds.size else 0.0,
763779
"median_smooth": float(np.percentile(np.asarray(smooths, dtype=np.float64), 50)),
764780
"median_ulcer": float(np.percentile(np.asarray(ulcers, dtype=np.float64), 50)),
765781
"median_sortino": float(np.percentile(sortinos, 50)),
@@ -1479,6 +1495,7 @@ def _fieldnames() -> list[str]:
14791495
"neg_windows",
14801496
"windows",
14811497
"p90_dd_pct",
1498+
"worst_dd_pct",
14821499
"median_smooth",
14831500
"median_ulcer",
14841501
"median_sortino",
@@ -1509,6 +1526,11 @@ def main() -> int:
15091526
parser.add_argument("--xgb-rounds", type=int, default=80)
15101527
parser.add_argument("--xgb-device", default="cuda")
15111528
parser.add_argument("--xgb-model-dir", type=Path, default=None)
1529+
parser.add_argument(
1530+
"--exclude-symbols",
1531+
default="",
1532+
help="Comma-separated symbols to remove from all score channels before search/evaluation.",
1533+
)
15121534
parser.add_argument("--no-handcrafted", action="store_true")
15131535
parser.add_argument("--out", type=Path, default=Path("analysis/binance33_meta_anneal.csv"))
15141536
parser.add_argument("--eval-days", type=int, default=120)
@@ -1580,6 +1602,10 @@ def main() -> int:
15801602
)
15811603
if train_bank.names != val_bank.names:
15821604
raise RuntimeError("train/validation score banks have different channels")
1605+
excluded_symbols = _parse_str_list(args.exclude_symbols)
1606+
if excluded_symbols:
1607+
train_bank = _apply_excluded_symbols(train_bank, train_data, excluded_symbols)
1608+
val_bank = _apply_excluded_symbols(val_bank, val_data, excluded_symbols)
15831609

15841610
gross_grid = _parse_float_list(args.gross_grid)
15851611
max_weight_grid = _parse_float_list(args.max_weight_grid)

tests/test_search_binance33_meta_anneal.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
from scripts.search_binance33_meta_anneal import (
77
Candidate,
88
ScoreBank,
9+
_apply_excluded_symbols,
910
_apply_binary_fills,
1011
_apply_short_binary_fills,
1112
_combine_scores,
1213
_desired_weights,
1314
_evolve_weights_after_return,
1415
_normalize_score_matrix,
1516
_normalise_alloc,
17+
_summarise_results,
1618
)
1719

1820

@@ -180,3 +182,65 @@ def test_evolve_weights_after_return_zeros_bankrupt_candidate() -> None:
180182
)
181183

182184
assert np.all(weights == 0.0)
185+
186+
187+
def test_apply_excluded_symbols_nans_all_channels_for_symbol() -> None:
188+
data = MktdData(
189+
version=2,
190+
symbols=["AAA", "BBB"],
191+
features=np.zeros((2, 2, 16), dtype=np.float32),
192+
prices=np.ones((2, 2, 5), dtype=np.float32),
193+
tradable=None,
194+
)
195+
bank = ScoreBank(names=["a", "b"], scores=np.ones((2, 2, 2), dtype=np.float64))
196+
197+
filtered = _apply_excluded_symbols(bank, data, ["bbb"])
198+
199+
assert np.all(np.isfinite(filtered.scores[:, :, 0]))
200+
assert np.all(np.isnan(filtered.scores[:, :, 1]))
201+
202+
203+
def test_summarise_results_reports_worst_drawdown() -> None:
204+
candidate = Candidate(
205+
candidate_id="x",
206+
logits=np.zeros(1, dtype=np.float64),
207+
threshold=0.0,
208+
max_gross=1.0,
209+
max_weight=1.0,
210+
top_k=1,
211+
book_mode="portfolio",
212+
score_temp=1.0,
213+
btc_gate=-99.0,
214+
market_gate=-99.0,
215+
rebalance_days=1,
216+
)
217+
results = [
218+
{
219+
"total_return": 0.10,
220+
"max_drawdown": 0.05,
221+
"sortino": 1.0,
222+
"trades": 2,
223+
"equity_curve": np.asarray([1.0, 1.1]),
224+
},
225+
{
226+
"total_return": 0.20,
227+
"max_drawdown": 0.22,
228+
"sortino": 2.0,
229+
"trades": 3,
230+
"equity_curve": np.asarray([1.0, 1.2]),
231+
},
232+
]
233+
234+
row = _summarise_results(
235+
results,
236+
candidate=candidate,
237+
phase="test",
238+
eval_days=30,
239+
slippage_bps=20.0,
240+
fill_buffer_bps=5.0,
241+
target_monthly_pct=30.0,
242+
target_max_dd_pct=20.0,
243+
channel_weights={"a": 1.0},
244+
)
245+
246+
assert row["worst_dd_pct"] == 22.0

0 commit comments

Comments
 (0)