Use actual plan edge for Entry PM training
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -12,7 +13,7 @@ if str(TRAINING_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(TRAINING_ROOT))
|
||||
|
||||
from trader_training.labels import DEFAULT_LABEL_CONFIG, _path_stats_for_group
|
||||
from trader_training.pm import _probability_implied_edge, _simulate_open_trades, _threshold_candidates, default_pm_config
|
||||
from trader_training.pm import _pm_frame, _probability_implied_edge, _simulate_open_trades, _threshold_candidates, default_pm_config
|
||||
|
||||
|
||||
class RiskPmFixTest(unittest.TestCase):
|
||||
@@ -44,13 +45,13 @@ class RiskPmFixTest(unittest.TestCase):
|
||||
self.assertEqual(80.0, DEFAULT_LABEL_CONFIG["risk"]["spike_bps"])
|
||||
self.assertEqual(1.8, DEFAULT_LABEL_CONFIG["risk"]["vol_expansion_ratio"])
|
||||
|
||||
def test_pm_search_covers_low_entry_probability_without_allowing_negative_edge(self) -> None:
|
||||
def test_pm_search_uses_strict_entry_probability_and_positive_edge(self) -> None:
|
||||
candidates = _threshold_candidates()
|
||||
|
||||
self.assertTrue(candidates)
|
||||
self.assertLessEqual(max(item["max_market_risk_prob"] for item in candidates), 0.98)
|
||||
self.assertLessEqual(min(item["min_entry_prob"] for item in candidates), 0.03)
|
||||
self.assertGreaterEqual(min(item["min_expected_edge_bps"] for item in candidates), 0.0)
|
||||
self.assertLessEqual(max(item["max_market_risk_prob"] for item in candidates), 0.65)
|
||||
self.assertGreaterEqual(min(item["min_entry_prob"] for item in candidates), 0.30)
|
||||
self.assertGreaterEqual(min(item["min_expected_edge_bps"] for item in candidates), 3.0)
|
||||
|
||||
def test_probability_implied_edge_uses_price_plan_payoff(self) -> None:
|
||||
edge = _probability_implied_edge(
|
||||
@@ -61,6 +62,71 @@ class RiskPmFixTest(unittest.TestCase):
|
||||
self.assertAlmostEqual(3.7, float(edge.iloc[0]), places=6)
|
||||
self.assertAlmostEqual(52.5, float(edge.iloc[1]), places=6)
|
||||
|
||||
def test_pm_frame_reads_actual_plan_edge_not_old_opportunity_edge(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
(root / "model" / "direction").mkdir(parents=True)
|
||||
(root / "model" / "entry").mkdir(parents=True)
|
||||
(root / "model" / "risk").mkdir(parents=True)
|
||||
(root / "dataset").mkdir(parents=True)
|
||||
(root / "label").mkdir(parents=True)
|
||||
common = {
|
||||
"sample_id": ["s0"],
|
||||
"symbol": ["BTC-USDT-PERP"],
|
||||
"event_time": pd.to_datetime(["2026-01-01T00:00:00Z"]),
|
||||
"split_id": ["tune_inner"],
|
||||
}
|
||||
pd.DataFrame({**common, "long_prob": [0.70], "short_prob": [0.10], "neutral_prob": [0.20]}).to_parquet(
|
||||
root / "model" / "direction" / "tune_predictions.parquet",
|
||||
index=False,
|
||||
)
|
||||
pd.DataFrame(
|
||||
{
|
||||
**common,
|
||||
"long_entry_prob": [0.80],
|
||||
"short_entry_prob": [0.20],
|
||||
"long_expected_net_edge_bps": [12.0],
|
||||
"short_expected_net_edge_bps": [1.0],
|
||||
}
|
||||
).to_parquet(root / "model" / "entry" / "tune_predictions.parquet", index=False)
|
||||
pd.DataFrame(
|
||||
{
|
||||
**common,
|
||||
"market_risk_prob": [0.20],
|
||||
"long_position_risk_prob": [0.10],
|
||||
"short_position_risk_prob": [0.10],
|
||||
}
|
||||
).to_parquet(root / "model" / "risk" / "tune_predictions.parquet", index=False)
|
||||
pd.DataFrame(
|
||||
{
|
||||
"sample_id": ["s0"],
|
||||
"long_entry_target": [0],
|
||||
"short_entry_target": [0],
|
||||
"long_expected_net_edge_bps": [99.0],
|
||||
"short_expected_net_edge_bps": [88.0],
|
||||
"long_actual_plan_net_edge_bps": [-6.5],
|
||||
"short_actual_plan_net_edge_bps": [-6.5],
|
||||
}
|
||||
).to_parquet(root / "dataset" / "entry_train.parquet", index=False)
|
||||
pd.DataFrame(
|
||||
{
|
||||
"sample_id": ["s0", "s0"],
|
||||
"side": ["LONG", "SHORT"],
|
||||
"gross_edge_bps": [0.0, 0.0],
|
||||
"cost_bps": [6.5, 6.5],
|
||||
"target_hit": [0, 0],
|
||||
"stop_hit": [0, 0],
|
||||
"time_to_target_ms": [-1, -1],
|
||||
"time_to_stop_ms": [-1, -1],
|
||||
"time_to_exit_ms": [2_700_000, 2_700_000],
|
||||
}
|
||||
).to_parquet(root / "label" / "entry_labels.parquet", index=False)
|
||||
|
||||
frame = _pm_frame(root, "tune_inner")
|
||||
|
||||
self.assertAlmostEqual(-6.5, float(frame.loc[0, "actual_long_plan_edge_bps"]))
|
||||
self.assertAlmostEqual(-6.5, float(frame.loc[0, "actual_short_plan_edge_bps"]))
|
||||
|
||||
def test_pm_backtest_sizing_uses_position_manager_formula_not_fixed_floor(self) -> None:
|
||||
frame = pd.DataFrame(
|
||||
{
|
||||
@@ -78,8 +144,8 @@ class RiskPmFixTest(unittest.TestCase):
|
||||
"short_position_risk_prob": [0.10],
|
||||
"pred_long_expected_net_edge_bps": [40.0],
|
||||
"pred_short_expected_net_edge_bps": [1.0],
|
||||
"actual_long_expected_net_edge_bps": [30.0],
|
||||
"actual_short_expected_net_edge_bps": [-10.0],
|
||||
"actual_long_plan_edge_bps": [30.0],
|
||||
"actual_short_plan_edge_bps": [-10.0],
|
||||
"long_trade_net_edge_bps": [11.0],
|
||||
"short_trade_net_edge_bps": [-14.5],
|
||||
"long_target_hit": [1],
|
||||
@@ -112,7 +178,7 @@ class RiskPmFixTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(trades))
|
||||
self.assertAlmostEqual(11.0, float(trades.iloc[0]["actual_edge_bps"]))
|
||||
self.assertAlmostEqual(30.0, float(trades.iloc[0]["label_max_edge_bps"]))
|
||||
self.assertAlmostEqual(30.0, float(trades.iloc[0]["label_actual_plan_edge_bps"]))
|
||||
self.assertGreater(float(trades.iloc[0]["planned_ratio"]), 0.05)
|
||||
self.assertLessEqual(float(trades.iloc[0]["planned_ratio"]), 0.20)
|
||||
|
||||
@@ -133,8 +199,8 @@ class RiskPmFixTest(unittest.TestCase):
|
||||
"short_position_risk_prob": [0.10, 0.10],
|
||||
"pred_long_expected_net_edge_bps": [40.0, 42.0],
|
||||
"pred_short_expected_net_edge_bps": [1.0, 1.0],
|
||||
"actual_long_expected_net_edge_bps": [30.0, 31.0],
|
||||
"actual_short_expected_net_edge_bps": [-10.0, -10.0],
|
||||
"actual_long_plan_edge_bps": [30.0, 31.0],
|
||||
"actual_short_plan_edge_bps": [-10.0, -10.0],
|
||||
"long_trade_net_edge_bps": [11.0, 12.0],
|
||||
"short_trade_net_edge_bps": [-14.5, -14.5],
|
||||
"long_target_hit": [1, 1],
|
||||
|
||||
@@ -22,6 +22,7 @@ from trader_training.ofi_feature_experiment import _load_entry_dataset, l1_snaps
|
||||
from trader_training.promote import promote_artifact_bundle
|
||||
from trader_training.replay import build_splits
|
||||
from trader_training.schemas import FEATURE_ORDER, LATEST_STRESS_SPLIT, MODEL_OUTPUTS, OUTPUT_MAPPING, TRAINING_SPLITS, VALIDATION_LOCKED_SPLIT
|
||||
from trader_training.training import TARGETS
|
||||
|
||||
|
||||
class TrainingContractTest(unittest.TestCase):
|
||||
@@ -49,6 +50,12 @@ class TrainingContractTest(unittest.TestCase):
|
||||
self.assertEqual("long_actual_plan_net_edge_bps", _screen_edge_column(dataset, "LONG"))
|
||||
self.assertEqual("short_actual_plan_net_edge_bps", _screen_edge_column(dataset, "SHORT"))
|
||||
|
||||
def test_entry_regression_heads_train_on_actual_plan_edge(self) -> None:
|
||||
heads = {head[0]: head[2] for head in TARGETS["ENTRY"]["heads"]}
|
||||
|
||||
self.assertEqual("long_actual_plan_net_edge_bps", heads["long_expected_net_edge_bps"])
|
||||
self.assertEqual("short_actual_plan_net_edge_bps", heads["short_expected_net_edge_bps"])
|
||||
|
||||
def test_entry_feature_screen_keeps_zero_inflated_event_features(self) -> None:
|
||||
values = np.concatenate((np.zeros(5000), np.linspace(1.0, 100.0, 500)))
|
||||
edges = _bucket_edges(values)
|
||||
|
||||
Reference in New Issue
Block a user