Add dynamic exit plan search diagnostics

This commit is contained in:
Codex
2026-06-28 06:51:39 +08:00
parent 1fd46ff3c9
commit 340e220b28
3 changed files with 455 additions and 0 deletions
+60
View File
@@ -14,6 +14,7 @@ if str(TRAINING_ROOT) not in sys.path:
sys.path.insert(0, str(TRAINING_ROOT))
from trader_training.onnx_export import LinearHead, export_heads
from trader_training.dynamic_exit_search import search_dynamic_exit_plans
from trader_training.entry_feature_screen import _bucket_edges, _screen_edge_column
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
@@ -56,6 +57,65 @@ class TrainingContractTest(unittest.TestCase):
self.assertEqual(-np.inf, edges[0])
self.assertEqual(np.inf, edges[-1])
def test_dynamic_exit_search_writes_plan_diagnostics(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
data_root = Path(tmp)
run_root = data_root / "trader-v4" / "runs" / "unit-dynamic-exit"
feature_path = run_root / "feature" / "feature_frame.parquet"
replay_path = run_root / "replay" / "replay_1m.parquet"
config_path = data_root / "label_config.json"
feature_path.parent.mkdir(parents=True)
replay_path.parent.mkdir(parents=True)
times = pd.date_range("2026-01-01", periods=7, freq="min", tz="UTC")
pd.DataFrame(
{
"sample_id": [f"s{i}" for i in range(4)],
"symbol": ["BTC-USDT-PERP"] * 4,
"event_time": times[:4],
"open_time_ms": [0, 60_000, 120_000, 180_000],
"split_id": ["tune_inner", "validation_locked", "latest_stress", "fit_inner"],
"walk_forward_fold": [0, 0, 0, 0],
"data_quality_flag": ["OK", "OK", "OK", "OK"],
}
).to_parquet(feature_path, index=False)
pd.DataFrame(
{
"event_time": times,
"open_time_ms": [0, 60_000, 120_000, 180_000, 240_000, 300_000, 360_000],
"symbol": ["BTC-USDT-PERP"] * 7,
"open": [100.0] * 7,
"high": [100.0, 100.12, 100.22, 100.24, 100.24, 100.24, 100.24],
"low": [100.0, 100.00, 100.00, 100.00, 100.00, 100.00, 100.00],
"close": [100.0, 100.10, 100.18, 100.20, 100.20, 100.20, 100.20],
"spread_bps": [1.0] * 7,
}
).to_parquet(replay_path, index=False)
write_json(config_path, {"entry": {"min_expected_net_edge_bps": 3.0}})
search_dynamic_exit_plans(
Namespace(
data_root=data_root,
run_id="unit-dynamic-exit",
feature_path=feature_path,
replay_path=replay_path,
label_config_path=config_path,
cost_config_path=None,
horizons=(3,),
targets=(10.0,),
stops=(5.0,),
trailing_stops=(4.0,),
second_target_multipliers=(2.0,),
take1_ratios=(0.5,),
take2_ratios=(0.25,),
)
)
result = read_json(run_root / "dynamic-exit-search" / "dynamic_exit_search_result.json")
self.assertEqual("DYNAMIC_TRAILING_V1", result["best_plan"]["plan_method"])
self.assertEqual(1, result["candidate_count"])
self.assertTrue((run_root / "dynamic-exit-search" / "dynamic_exit_search_report.md").is_file())
def test_split_builder_uses_locked_validation_contract(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
data_root = Path(tmp)