Add Entry opportunity training diagnostics

This commit is contained in:
Codex
2026-06-28 09:27:59 +08:00
parent e8420f76fe
commit 6be4bb976a
5 changed files with 468 additions and 10 deletions
+42
View File
@@ -20,6 +20,7 @@ from trader_training.dynamic_exit_search import search_dynamic_exit_plans
from trader_training.entry_condition_pair_screen import screen_entry_condition_pairs
from trader_training.entry_feature_screen import _bucket_edges, _screen_edge_column
from trader_training.entry_mae_label_diagnostic import diagnose_entry_mae_labels
from trader_training.good_trade_structure import _side_frame, _top_fraction_metrics
from trader_training.io_utils import read_json, write_json
from trader_training.labels import ENTRY_LABEL_METHOD, _path_stats_for_group, build_entry_labels
from trader_training.nonlinear_pm_probe import _expanded_threshold_candidates
@@ -97,6 +98,27 @@ class TrainingContractTest(unittest.TestCase):
self.assertEqual("ALL_FIT_ROWS", default_filter)
self.assertEqual([True, True, True, True], default_mask.tolist())
def test_conditional_entry_training_can_use_side_opportunity_rows(self) -> None:
train = pd.DataFrame(
{
"long_max_achievable_net_edge_bps": [45.0, 10.0, 60.0, 39.0],
"short_max_achievable_net_edge_bps": [8.0, 42.0, 15.0, 80.0],
}
)
args = Namespace(
conditional_entry_source="side_opportunity",
conditional_entry_direction_labels=False,
conditional_entry_opportunity_bps=40.0,
)
long_mask, long_filter = _head_train_mask("ENTRY", "long_entry_prob", train, args)
short_mask, short_filter = _head_train_mask("ENTRY", "short_expected_net_edge_bps", train, args)
self.assertEqual("SIDE_OPPORTUNITY_LONG_GE_40_BPS_FIT_ROWS", long_filter)
self.assertEqual([True, False, True, False], long_mask.tolist())
self.assertEqual("SIDE_OPPORTUNITY_SHORT_GE_40_BPS_FIT_ROWS", short_filter)
self.assertEqual([False, True, False, True], short_mask.tolist())
def test_direction_opportunity_labels_choose_clear_path_opportunity(self) -> None:
labels = _opportunity_labels(
np.array([45.0, 10.0, 45.0, 42.0, np.nan]),
@@ -142,6 +164,26 @@ class TrainingContractTest(unittest.TestCase):
self.assertEqual("dataset/entry_train.parquet", summary["fit_inner"]["entry"]["source"])
self.assertEqual(0.5, summary["fit_inner"]["entry"]["target_rate_by_side"]["LONG"])
def test_good_trade_structure_builds_side_frame_and_top_metrics(self) -> None:
dataset = pd.DataFrame(
{
"sample_id": ["s1", "s2", "s3"],
"split_id": ["fit_inner", "fit_inner", "fit_inner"],
"long_actual_plan_net_edge_bps": [4.0, -5.0, 1.0],
"short_actual_plan_net_edge_bps": [-5.0, 6.0, -1.0],
**{feature: [0.1, 0.2, 0.3] for feature in FEATURE_ORDER},
}
)
frame = _side_frame(dataset, "LONG", min_good_edge_bps=3.0, bad_edge_bps=-3.0)
metrics = _top_fraction_metrics(frame, np.array([0.9, 0.1, 0.2]), 1 / 3)
self.assertEqual([1, 0, 0], frame["good_trade"].tolist())
self.assertEqual([0, 1, 0], frame["bad_trade"].tolist())
self.assertEqual(1, metrics["rows"])
self.assertEqual(1.0, metrics["good_rate"])
self.assertEqual(4.0, metrics["avg_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)