Skip to content

Commit af33f9c

Browse files
committed
add binance pack prod risk controls
1 parent 61a8ba7 commit af33f9c

5 files changed

Lines changed: 166 additions & 0 deletions

scripts/run_binance_pack_rolling_validation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
"min_recent_ret_24h": -1.0,
4747
"min_recent_ret_72h": -1.0,
4848
"max_recent_vol_72h": 0.0,
49+
"regime_cs_skew_min": -1_000_000_000.0,
50+
"vol_target_ann": 0.0,
51+
"inv_vol_target_ann": 0.0,
52+
"inv_vol_floor": 0.05,
53+
"inv_vol_cap": 3.0,
4954
}
5055

5156
SUMMARY_FIELDS = [

scripts/run_binance_pack_scaling_matrix.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ def _config_json(row: dict[str, str] | None) -> str:
145145
"min_recent_ret_24h",
146146
"min_recent_ret_72h",
147147
"max_recent_vol_72h",
148+
"regime_cs_skew_min",
149+
"vol_target_ann",
150+
"inv_vol_target_ann",
151+
"inv_vol_floor",
152+
"inv_vol_cap",
148153
"max_positions",
149154
"max_pending_entries",
150155
"entry_ttl_hours",

scripts/sweep_binance_hourly_portfolio_pack.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@
7171
"cvar_loss_72h",
7272
]
7373

74+
HOURS_PER_YEAR = 24.0 * 365.25
75+
7476

7577
@dataclass(frozen=True)
7678
class PackConfig:
@@ -87,6 +89,11 @@ class PackConfig:
8789
min_recent_ret_24h: float
8890
min_recent_ret_72h: float
8991
max_recent_vol_72h: float
92+
regime_cs_skew_min: float
93+
vol_target_ann: float
94+
inv_vol_target_ann: float
95+
inv_vol_floor: float
96+
inv_vol_cap: float
9097
max_positions: int
9198
max_pending_entries: int
9299
entry_ttl_hours: int
@@ -437,6 +444,32 @@ def build_actions_and_bars(
437444
if "vol_72h" in rows.columns
438445
else np.zeros(len(rows), dtype=np.float64)
439446
)
447+
rows["_recent_ret_24h_for_regime"] = recent_ret_24h
448+
rows["_recent_vol_72h_for_scale"] = recent_vol_72h
449+
regime_cs_skew_24h = (
450+
rows.groupby("timestamp")["_recent_ret_24h_for_regime"]
451+
.transform(lambda values: values.skew())
452+
.replace([np.inf, -np.inf], np.nan)
453+
.fillna(0.0)
454+
.to_numpy(dtype=np.float64)
455+
)
456+
market_vol_72h = (
457+
rows.groupby("timestamp")["_recent_vol_72h_for_scale"]
458+
.transform("median")
459+
.replace([np.inf, -np.inf], np.nan)
460+
.fillna(0.0)
461+
.to_numpy(dtype=np.float64)
462+
)
463+
recent_vol_ann = recent_vol_72h * float(np.sqrt(HOURS_PER_YEAR))
464+
market_vol_ann = market_vol_72h * float(np.sqrt(HOURS_PER_YEAR))
465+
inv_vol_scale = np.ones(len(rows), dtype=np.float64)
466+
if float(cfg.inv_vol_target_ann) > 0.0:
467+
inv_vol_scale = float(cfg.inv_vol_target_ann) / np.maximum(recent_vol_ann, 1e-9)
468+
inv_vol_scale = np.clip(inv_vol_scale, float(cfg.inv_vol_floor), float(cfg.inv_vol_cap))
469+
market_vol_scale = np.ones(len(rows), dtype=np.float64)
470+
if float(cfg.vol_target_ann) > 0.0:
471+
market_vol_scale = float(cfg.vol_target_ann) / np.maximum(market_vol_ann, 1e-9)
472+
market_vol_scale = np.clip(market_vol_scale, float(cfg.inv_vol_floor), float(cfg.inv_vol_cap))
440473

441474
upside = np.maximum.reduce([pred_high, pred_close, np.zeros_like(pred_high)])
442475
downside = np.maximum(-pred_low, 0.0)
@@ -465,6 +498,9 @@ def build_actions_and_bars(
465498
)
466499
if float(cfg.max_recent_vol_72h) > 0.0:
467500
active &= recent_vol_72h <= float(cfg.max_recent_vol_72h)
501+
if float(cfg.regime_cs_skew_min) > -1e8:
502+
active &= regime_cs_skew_24h >= float(cfg.regime_cs_skew_min)
503+
amount = amount * inv_vol_scale * market_vol_scale
468504
amount = np.where(active, amount, 0.0)
469505

470506
rows["buy_price"] = buy_price
@@ -480,6 +516,11 @@ def build_actions_and_bars(
480516
rows["recent_ret_24h"] = recent_ret_24h
481517
rows["recent_ret_72h"] = recent_ret_72h
482518
rows["recent_vol_72h"] = recent_vol_72h
519+
rows["recent_vol_ann"] = recent_vol_ann
520+
rows["regime_cs_skew_24h"] = regime_cs_skew_24h
521+
rows["market_vol_ann"] = market_vol_ann
522+
rows["inv_vol_scale"] = inv_vol_scale
523+
rows["market_vol_scale"] = market_vol_scale
483524
rows["watch_entry_gap_bps"] = entry_gap * 10_000.0
484525
rows["watch_exit_gap_bps"] = exit_gap * 10_000.0
485526
rows[f"predicted_high_p50_h{label_horizon}"] = ref * (1.0 + pred_high)
@@ -506,6 +547,11 @@ def build_actions_and_bars(
506547
"recent_ret_24h",
507548
"recent_ret_72h",
508549
"recent_vol_72h",
550+
"recent_vol_ann",
551+
"regime_cs_skew_24h",
552+
"market_vol_ann",
553+
"inv_vol_scale",
554+
"market_vol_scale",
509555
"watch_entry_gap_bps",
510556
"watch_exit_gap_bps",
511557
f"predicted_high_p50_h{label_horizon}",
@@ -1020,6 +1066,11 @@ def iter_pack_configs(args: argparse.Namespace) -> list[PackConfig]:
10201066
_parse_float_list(args.min_recent_ret_24h_grid),
10211067
_parse_float_list(args.min_recent_ret_72h_grid),
10221068
_parse_float_list(args.max_recent_vol_72h_grid),
1069+
_parse_float_list(args.regime_cs_skew_min_grid),
1070+
_parse_float_list(args.vol_target_ann_grid),
1071+
_parse_float_list(args.inv_vol_target_ann_grid),
1072+
_parse_float_list(args.inv_vol_floor_grid),
1073+
_parse_float_list(args.inv_vol_cap_grid),
10231074
_parse_int_list(args.max_positions_grid),
10241075
_parse_int_list(args.max_pending_entries_grid),
10251076
_parse_int_list(args.entry_ttl_hours_grid),
@@ -1071,6 +1122,11 @@ def main() -> int:
10711122
parser.add_argument("--min-recent-ret-24h-grid", default="-1.0")
10721123
parser.add_argument("--min-recent-ret-72h-grid", default="-1.0")
10731124
parser.add_argument("--max-recent-vol-72h-grid", default="0.0")
1125+
parser.add_argument("--regime-cs-skew-min-grid", default="-1000000000.0")
1126+
parser.add_argument("--vol-target-ann-grid", default="0.0")
1127+
parser.add_argument("--inv-vol-target-ann-grid", default="0.0")
1128+
parser.add_argument("--inv-vol-floor-grid", default="0.05")
1129+
parser.add_argument("--inv-vol-cap-grid", default="3.0")
10741130
parser.add_argument("--max-positions-grid", default="5,8")
10751131
parser.add_argument("--max-pending-entries-grid", default="12,24")
10761132
parser.add_argument("--entry-ttl-hours-grid", default="3,6")

tests/test_binance_hourly_portfolio_pack.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ def _pack_config(**overrides) -> PackConfig:
2929
min_recent_ret_24h=-1.0,
3030
min_recent_ret_72h=-1.0,
3131
max_recent_vol_72h=0.0,
32+
regime_cs_skew_min=-1_000_000_000.0,
33+
vol_target_ann=0.0,
34+
inv_vol_target_ann=0.0,
35+
inv_vol_floor=0.05,
36+
inv_vol_cap=3.0,
3237
max_positions=2,
3338
max_pending_entries=4,
3439
entry_ttl_hours=3,
@@ -209,6 +214,86 @@ def test_build_actions_and_bars_can_gate_weak_recent_momentum():
209214
assert action["buy_amount"] == 0.0
210215

211216

217+
def test_build_actions_and_bars_can_gate_bad_cross_sectional_regime():
218+
ts = pd.Timestamp("2026-03-03T15:00:00Z")
219+
scored = pd.DataFrame(
220+
[
221+
{
222+
"timestamp": ts,
223+
"symbol": symbol,
224+
"open": 100.0,
225+
"high": 101.0,
226+
"low": 99.0,
227+
"close": 100.0,
228+
"volume": 10.0,
229+
"reference_close": 100.0,
230+
"pred_high_ret_xgb": 0.08,
231+
"pred_low_ret_xgb": -0.005,
232+
"pred_close_ret_xgb": 0.02,
233+
"cvar_loss_72h": 0.001,
234+
"ret_24h": 0.01,
235+
"ret_72h": 0.04,
236+
"vol_72h": 0.01,
237+
}
238+
for symbol in ("AAAUSDT", "BBBUSDT", "CCCUSDT")
239+
]
240+
)
241+
242+
_, actions = build_actions_and_bars(
243+
scored,
244+
cfg=_pack_config(regime_cs_skew_min=0.5),
245+
label_horizon=24,
246+
min_take_profit_bps=35.0,
247+
max_entry_gap_bps=120.0,
248+
max_exit_gap_bps=250.0,
249+
fee_rate=0.001,
250+
top_candidates_per_hour=10,
251+
)
252+
253+
assert actions["regime_cs_skew_24h"].eq(0.0).all()
254+
assert actions["buy_amount"].eq(0.0).all()
255+
256+
257+
def test_build_actions_and_bars_applies_inverse_vol_sizing_scale():
258+
ts = pd.Timestamp("2026-03-03T15:00:00Z")
259+
scored = pd.DataFrame(
260+
[
261+
{
262+
"timestamp": ts,
263+
"symbol": "BTCUSDT",
264+
"open": 100.0,
265+
"high": 101.0,
266+
"low": 99.0,
267+
"close": 100.0,
268+
"volume": 10.0,
269+
"reference_close": 100.0,
270+
"pred_high_ret_xgb": 0.08,
271+
"pred_low_ret_xgb": -0.005,
272+
"pred_close_ret_xgb": 0.02,
273+
"cvar_loss_72h": 0.001,
274+
"ret_24h": 0.01,
275+
"ret_72h": 0.04,
276+
"vol_72h": 0.01,
277+
}
278+
]
279+
)
280+
281+
_, actions = build_actions_and_bars(
282+
scored,
283+
cfg=_pack_config(inv_vol_target_ann=0.5, inv_vol_floor=0.05, inv_vol_cap=3.0),
284+
label_horizon=24,
285+
min_take_profit_bps=35.0,
286+
max_entry_gap_bps=120.0,
287+
max_exit_gap_bps=250.0,
288+
fee_rate=0.001,
289+
top_candidates_per_hour=10,
290+
)
291+
292+
action = actions.iloc[0]
293+
assert 0.05 < action["inv_vol_scale"] < 1.0
294+
assert 0.0 < action["buy_amount"] < 100.0
295+
296+
212297
def test_sample_pack_configs_spreads_across_full_grid_deterministically():
213298
args = argparse.Namespace(
214299
risk_penalties="0.2,0.5",
@@ -224,6 +309,11 @@ def test_sample_pack_configs_spreads_across_full_grid_deterministically():
224309
min_recent_ret_24h_grid="-1.0",
225310
min_recent_ret_72h_grid="-1.0",
226311
max_recent_vol_72h_grid="0.0",
312+
regime_cs_skew_min_grid="-1000000000.0",
313+
vol_target_ann_grid="0.0",
314+
inv_vol_target_ann_grid="0.0",
315+
inv_vol_floor_grid="0.05",
316+
inv_vol_cap_grid="3.0",
227317
max_positions_grid="5,8",
228318
max_pending_entries_grid="12,24",
229319
entry_ttl_hours_grid="3,6",
@@ -258,6 +348,11 @@ def test_sample_pack_configs_keeps_randomized_order():
258348
min_recent_ret_24h_grid="-1.0",
259349
min_recent_ret_72h_grid="-1.0",
260350
max_recent_vol_72h_grid="0.0",
351+
regime_cs_skew_min_grid="-1000000000.0",
352+
vol_target_ann_grid="0.0",
353+
inv_vol_target_ann_grid="0.0",
354+
inv_vol_floor_grid="0.05",
355+
inv_vol_cap_grid="3.0",
261356
max_positions_grid="5,8",
262357
max_pending_entries_grid="12",
263358
entry_ttl_hours_grid="3",

tests/test_run_binance_pack_rolling_validation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ def test_load_candidate_configs_defaults_new_regime_gates_and_dedupes(tmp_path):
6666
assert cfg.min_recent_ret_24h == -1.0
6767
assert cfg.min_recent_ret_72h == -1.0
6868
assert cfg.max_recent_vol_72h == 0.0
69+
assert cfg.regime_cs_skew_min == -1_000_000_000.0
70+
assert cfg.vol_target_ann == 0.0
71+
assert cfg.inv_vol_target_ann == 0.0
72+
assert cfg.inv_vol_floor == 0.05
73+
assert cfg.inv_vol_cap == 3.0
6974

7075

7176
def test_load_candidate_configs_applies_top_k_after_ranking(tmp_path):

0 commit comments

Comments
 (0)