Add doctrine time session kernel
This commit is contained in:
@@ -97,6 +97,8 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertIn("missing_liquidity_target", result[0]["liquidity_kernel_status"]["reasons"])
|
||||
self.assertEqual(result[0]["pd_array_reaction_status"]["status"], "study_only")
|
||||
self.assertIn("missing_pd_array", result[0]["pd_array_reaction_status"]["reasons"])
|
||||
self.assertEqual(result[0]["time_session_kernel_status"]["status"], "study_only")
|
||||
self.assertIn("missing_session_window", result[0]["time_session_kernel_status"]["reasons"])
|
||||
|
||||
def test_normalize_backtest_summary_row_preserves_existing_quality_gate(self) -> None:
|
||||
row = {"id": 10, "summary": {"total_signals": 3, "quality_gate": {"passed": True}, "by_symbol": {"BTC-USDT": {"count": 3}}}}
|
||||
@@ -130,6 +132,8 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertIn("missing_liquidity_target", result["liquidity_kernel_summary"]["reason_counts"])
|
||||
self.assertEqual(result["pd_array_reaction_summary"]["item_count"], 1)
|
||||
self.assertIn("missing_pd_array", result["pd_array_reaction_summary"]["reason_counts"])
|
||||
self.assertEqual(result["time_session_kernel_summary"]["item_count"], 1)
|
||||
self.assertIn("missing_session_window", result["time_session_kernel_summary"]["reason_counts"])
|
||||
|
||||
def test_route_request_trading_doctrine_rules_returns_d1_matrix(self) -> None:
|
||||
status, payload = route_request(
|
||||
@@ -190,6 +194,22 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
||||
self.assertEqual(payload["rules"][0]["rule_id"], "pd_array_relevance_matrix")
|
||||
|
||||
def test_route_request_trading_time_session_kernel_returns_d5_registry(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/time-session-kernel",
|
||||
{},
|
||||
dependencies={
|
||||
"trading_time_session_kernel": lambda: {
|
||||
"phase": "D5_TIME_SESSION_KERNEL",
|
||||
"rules": [{"rule_id": "session_window_verifiable"}],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload["phase"], "D5_TIME_SESSION_KERNEL")
|
||||
self.assertEqual(payload["rules"][0]["rule_id"], "session_window_verifiable")
|
||||
|
||||
def test_route_request_trading_candidates_returns_items_payload(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/candidates",
|
||||
|
||||
@@ -150,6 +150,22 @@ class ApiServerRoutingTests(unittest.TestCase):
|
||||
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
||||
self.assertEqual(payload["rule_count"], 5)
|
||||
|
||||
def test_routes_trading_time_session_kernel(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/time-session-kernel",
|
||||
{},
|
||||
dependencies={
|
||||
"trading_time_session_kernel": lambda: {
|
||||
"phase": "D5_TIME_SESSION_KERNEL",
|
||||
"rule_count": 6,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload["phase"], "D5_TIME_SESSION_KERNEL")
|
||||
self.assertEqual(payload["rule_count"], 6)
|
||||
|
||||
def test_routes_trading_alert_quality_summary(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/alerts/quality-summary",
|
||||
|
||||
@@ -321,6 +321,9 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
self.assertEqual(summary["pd_array_reaction_status_counts"]["study_only"], 4)
|
||||
self.assertEqual(summary["pd_array_reaction_reason_counts"]["missing_pd_array"], 4)
|
||||
self.assertEqual(summary["by_pd_array_type"]["missing_pd_array"]["count"], 4)
|
||||
self.assertEqual(summary["time_session_kernel_status_counts"]["study_only"], 4)
|
||||
self.assertEqual(summary["time_session_kernel_reason_counts"]["missing_session_window"], 4)
|
||||
self.assertEqual(summary["by_time_session_code"]["LONDON"]["count"], 3)
|
||||
self.assertEqual(summary["by_session"]["LONDON"]["max_consecutive_losses"], 2)
|
||||
self.assertEqual(summary["by_session"]["LONDON"]["executed"], 3)
|
||||
self.assertAlmostEqual(summary["by_session"]["LONDON"]["executed_expectancy_r"], 0.2 / 3)
|
||||
@@ -418,6 +421,8 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
"entry": {},
|
||||
"candidate_meta": {},
|
||||
"candidate_status": "awaiting_confirmation",
|
||||
"session_code": "LONDON",
|
||||
"allowed_sessions": ["LONDON"],
|
||||
},
|
||||
{
|
||||
"candidate_id": 2,
|
||||
@@ -472,6 +477,22 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
"management_rules": [{"code": "tp1_partial", "fraction": 0.5}],
|
||||
},
|
||||
"candidate_status": "executable",
|
||||
"session_code": "LONDON",
|
||||
"session_window_state": "inside",
|
||||
"allowed_sessions": ["LONDON"],
|
||||
"killzone_name": "LONDON_KILLZONE",
|
||||
"killzone_state": "active",
|
||||
"judas_window_state": "cleared",
|
||||
"opening_range_state": "defined",
|
||||
"opening_range_id": "or-london",
|
||||
"opening_range_high": 101.0,
|
||||
"opening_range_low": 99.0,
|
||||
"weekly_profile": "midweek_expansion",
|
||||
"weekly_bias": "bullish_draw_to_bsl",
|
||||
"news_window_state": "clear",
|
||||
"high_impact_news": False,
|
||||
"low_liquidity_window": False,
|
||||
"session_invalidation": "london_close_or_reclaim",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -487,6 +508,8 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
self.assertEqual(units[0]["liquidity_kernel_status"]["status"], "liquidity_mapped")
|
||||
self.assertEqual(units[0]["pd_array_backtest_features"]["pd_array_type"], "fvg")
|
||||
self.assertEqual(units[0]["pd_array_reaction_status"]["status"], "pd_array_mapped")
|
||||
self.assertEqual(units[0]["time_session_backtest_features"]["session_code"], "LONDON")
|
||||
self.assertEqual(units[0]["time_session_kernel_status"]["status"], "time_session_mapped")
|
||||
|
||||
|
||||
class BacktestServiceRunTests(unittest.TestCase):
|
||||
@@ -739,6 +762,7 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
"entry": {},
|
||||
"invalidations": [],
|
||||
"candidate_meta": {},
|
||||
"session_code": "LONDON",
|
||||
"allowed_sessions": ["LONDON"],
|
||||
"primary_draw": {"side": "buy_side"},
|
||||
},
|
||||
@@ -786,6 +810,8 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
],
|
||||
},
|
||||
"invalidations": [],
|
||||
"session_code": "LONDON",
|
||||
"session_window_state": "inside",
|
||||
"candidate_meta": {
|
||||
"management_rules": [
|
||||
{"code": "tp1_partial", "fraction": 0.5},
|
||||
@@ -793,6 +819,19 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
]
|
||||
},
|
||||
"allowed_sessions": ["LONDON"],
|
||||
"killzone_name": "LONDON_KILLZONE",
|
||||
"killzone_state": "active",
|
||||
"judas_window_state": "cleared",
|
||||
"opening_range_state": "defined",
|
||||
"opening_range_id": "or-london",
|
||||
"opening_range_high": 101.0,
|
||||
"opening_range_low": 99.0,
|
||||
"weekly_profile": "midweek_expansion",
|
||||
"weekly_bias": "bullish_draw_to_bsl",
|
||||
"news_window_state": "clear",
|
||||
"high_impact_news": False,
|
||||
"low_liquidity_window": False,
|
||||
"session_invalidation": "london_close_or_reclaim",
|
||||
"primary_draw": {"side": "buy_side"},
|
||||
},
|
||||
]
|
||||
@@ -842,6 +881,8 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(repository.summary["pd_array_reaction_status"]["status"], "pd_array_mapped")
|
||||
self.assertEqual(repository.summary["by_pd_array_type"]["fvg"]["count"], 1)
|
||||
self.assertEqual(repository.summary["time_session_kernel_status"]["status"], "time_session_mapped")
|
||||
self.assertEqual(repository.summary["by_time_session_code"]["LONDON"]["count"], 1)
|
||||
|
||||
def test_normalize_timeframe_set_and_bucket_helpers(self) -> None:
|
||||
normalized = normalize_timeframe_set({"bias_tf": "1h", "setup_tf": "15m"})
|
||||
|
||||
@@ -22,6 +22,7 @@ ACTIVE_TOP_LEVEL_DOCS = {
|
||||
"AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md",
|
||||
"AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md",
|
||||
"AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md",
|
||||
"AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md",
|
||||
"AI_ICT_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md",
|
||||
"AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md",
|
||||
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
|
||||
|
||||
@@ -5,19 +5,24 @@ from src.services.ict_mainline import (
|
||||
DOCTRINE_LIQUIDITY_PHASE,
|
||||
DOCTRINE_PLAYBOOK_PHASE,
|
||||
DOCTRINE_REACTION_PHASE,
|
||||
DOCTRINE_SESSION_PHASE,
|
||||
build_doctrine_backtest_cohort,
|
||||
build_liquidity_backtest_features,
|
||||
build_pd_array_backtest_features,
|
||||
build_time_session_backtest_features,
|
||||
build_doctrine_playbook_gate,
|
||||
build_doctrine_playbook_registry,
|
||||
build_doctrine_rule_matrix,
|
||||
build_liquidity_kernel_registry,
|
||||
build_pd_array_reaction_registry,
|
||||
build_time_session_kernel_registry,
|
||||
evaluate_doctrine_research_status,
|
||||
evaluate_liquidity_kernel_status,
|
||||
evaluate_pd_array_reaction_status,
|
||||
evaluate_time_session_kernel_status,
|
||||
summarize_liquidity_kernel_statuses,
|
||||
summarize_pd_array_reaction_statuses,
|
||||
summarize_time_session_kernel_statuses,
|
||||
summarize_doctrine_rule_gaps,
|
||||
)
|
||||
|
||||
@@ -48,6 +53,8 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertIn("post_take_reaction", playbook["liquidity_event_rule"]["required_fields"])
|
||||
self.assertIn("pd_array_role", playbook["pd_array_rule"]["required_fields"])
|
||||
self.assertIn("reaction_quality", playbook["pd_array_rule"]["required_fields"])
|
||||
self.assertIn("killzone_state", playbook["session_timing_rule"]["required_fields"])
|
||||
self.assertIn("weekly_profile", playbook["session_timing_rule"]["required_fields"])
|
||||
self.assertTrue(playbook["promotion_gate"]["requires_backtest_quality_gate"])
|
||||
|
||||
def test_d3_liquidity_kernel_registry_exposes_target_ranking_rules(self) -> None:
|
||||
@@ -163,6 +170,50 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertGreater(summary["reason_counts"]["fvg_is_not_setup"], 0)
|
||||
self.assertEqual(summary["array_type_counts"]["fvg"], 2)
|
||||
|
||||
def test_d5_time_session_kernel_registry_exposes_session_quality_rules(self) -> None:
|
||||
registry = build_time_session_kernel_registry()
|
||||
|
||||
self.assertEqual(registry["phase"], DOCTRINE_SESSION_PHASE)
|
||||
self.assertEqual(registry["status"], "active")
|
||||
self.assertGreaterEqual(registry["rule_count"], 6)
|
||||
self.assertIn("session_window_state", registry["required_backtest_feature_fields"])
|
||||
self.assertIn("Asia / London / NY AM / NY PM are context windows, not entry signals.", registry["hard_rules"])
|
||||
|
||||
def test_outside_session_stays_study_only(self) -> None:
|
||||
status = evaluate_time_session_kernel_status(
|
||||
{
|
||||
"session_code": "OFF_HOURS",
|
||||
"session_window_state": "outside_window",
|
||||
"allowed_sessions": ["LONDON"],
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(status["passed"])
|
||||
self.assertEqual(status["phase"], DOCTRINE_SESSION_PHASE)
|
||||
self.assertEqual(status["status"], "study_only")
|
||||
self.assertIn("outside_session_window", status["reasons"])
|
||||
self.assertIn("session_not_allowed", status["reasons"])
|
||||
|
||||
def test_complete_time_session_payload_maps_without_execution_permission(self) -> None:
|
||||
status = evaluate_time_session_kernel_status(_complete_time_session_payload())
|
||||
|
||||
self.assertTrue(status["passed"])
|
||||
self.assertEqual(status["status"], "time_session_mapped")
|
||||
self.assertEqual(status["execution_boundary"], "read_only_analysis_no_order_submission")
|
||||
|
||||
def test_time_session_summary_counts_blockers(self) -> None:
|
||||
summary = summarize_time_session_kernel_statuses(
|
||||
[
|
||||
{"session_code": "OFF_HOURS", "session_window_state": "outside_window"},
|
||||
_complete_time_session_payload(),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(summary["item_count"], 2)
|
||||
self.assertEqual(summary["passed_count"], 1)
|
||||
self.assertGreater(summary["reason_counts"]["outside_session_window"], 0)
|
||||
self.assertEqual(summary["session_code_counts"]["LONDON"], 1)
|
||||
|
||||
def test_playbook_gate_blocks_missing_and_passes_registered_schema(self) -> None:
|
||||
missing = build_doctrine_playbook_gate({"model_code": "SWEEP_MSS_FVG"})
|
||||
|
||||
@@ -251,6 +302,7 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
"sweep_confirmed": True,
|
||||
"post_take_reaction": "reclaim_displacement",
|
||||
**_complete_pd_array_payload(),
|
||||
**_complete_time_session_payload(),
|
||||
"summary": {"quality_gate": {"passed": True, "reasons": []}},
|
||||
}
|
||||
)
|
||||
@@ -260,6 +312,7 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertTrue(cohort["promotion_gate"]["passed"])
|
||||
self.assertEqual(cohort["liquidity_kernel_status"]["status"], "liquidity_mapped")
|
||||
self.assertEqual(cohort["pd_array_reaction_status"]["status"], "pd_array_mapped")
|
||||
self.assertEqual(cohort["time_session_kernel_status"]["status"], "time_session_mapped")
|
||||
|
||||
missing = build_doctrine_backtest_cohort({"model_code": "SWEEP_MSS_FVG"})
|
||||
|
||||
@@ -267,6 +320,7 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertIn("missing_doctrine_setup_playbook", missing["promotion_gate"]["reasons"])
|
||||
self.assertIn("missing_liquidity_target", missing["promotion_gate"]["reasons"])
|
||||
self.assertIn("missing_pd_array", missing["promotion_gate"]["reasons"])
|
||||
self.assertIn("missing_session_window", missing["promotion_gate"]["reasons"])
|
||||
|
||||
def test_liquidity_backtest_features_extract_nested_candidate_fields(self) -> None:
|
||||
features = build_liquidity_backtest_features(
|
||||
@@ -310,6 +364,27 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertEqual(features["pd_array_rank"], "primary")
|
||||
self.assertEqual(features["array_location"], "discount")
|
||||
|
||||
def test_time_session_backtest_features_extract_nested_candidate_fields(self) -> None:
|
||||
features = build_time_session_backtest_features(
|
||||
{
|
||||
"candidate": {
|
||||
"session": {
|
||||
"session_code": "NY_AM",
|
||||
"killzone_name": "NY_AM_KILLZONE",
|
||||
"opening_range_state": "defined",
|
||||
},
|
||||
"confirmation_state": {"session_invalidation": "ny_lunch_cutoff"},
|
||||
},
|
||||
"session_filter": "covered",
|
||||
"weekly_profile": "tuesday_expansion",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(features["session_code"], "NY_AM")
|
||||
self.assertEqual(features["session_window_state"], "inside")
|
||||
self.assertEqual(features["killzone_name"], "NY_AM_KILLZONE")
|
||||
self.assertEqual(features["opening_range_state"], "defined")
|
||||
|
||||
|
||||
def _complete_pd_array_payload() -> dict:
|
||||
return {
|
||||
@@ -330,5 +405,35 @@ def _complete_pd_array_payload() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _complete_time_session_payload() -> dict:
|
||||
return {
|
||||
"session_code": "LONDON",
|
||||
"session_window_state": "inside",
|
||||
"allowed_sessions": ["LONDON", "NY_AM"],
|
||||
"session_start_ts": "2026-05-22T06:00:00Z",
|
||||
"session_end_ts": "2026-05-22T09:00:00Z",
|
||||
"killzone_name": "LONDON_KILLZONE",
|
||||
"killzone_state": "active",
|
||||
"killzone_start_ts": "2026-05-22T07:00:00Z",
|
||||
"killzone_end_ts": "2026-05-22T09:00:00Z",
|
||||
"judas_window_state": "cleared",
|
||||
"inducement_side": "sell_side",
|
||||
"post_manipulation_confirmation": "mss_after_sweep",
|
||||
"opening_range_state": "defined",
|
||||
"opening_range_id": "or-london-20260522",
|
||||
"opening_range_high": 101.0,
|
||||
"opening_range_low": 99.0,
|
||||
"opening_range_reaction": "rejected_low",
|
||||
"weekly_profile": "midweek_expansion",
|
||||
"weekly_bias": "bearish_draw_to_ssl",
|
||||
"day_of_week": "Friday",
|
||||
"weekly_range_state": "inside_weekly_range",
|
||||
"news_window_state": "clear",
|
||||
"high_impact_news": False,
|
||||
"low_liquidity_window": False,
|
||||
"session_invalidation": "london_close_or_reclaim",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user