Add doctrine time session kernel

This commit is contained in:
Codex
2026-05-22 08:21:24 +08:00
parent 4f96904a2f
commit 990900e638
16 changed files with 928 additions and 4 deletions
+20
View File
@@ -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",
+16
View File
@@ -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",
+41
View File
@@ -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",
+105
View File
@@ -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()