Add doctrine PD array reaction kernel
This commit is contained in:
@@ -95,6 +95,8 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertEqual(result[0]["doctrine_research_status"]["research_only_reason"], "missing_doctrine_setup_playbook")
|
||||
self.assertEqual(result[0]["liquidity_kernel_status"]["status"], "study_only")
|
||||
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"])
|
||||
|
||||
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}}}}
|
||||
@@ -126,6 +128,8 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertEqual(result["doctrine_rule_gap_summary"]["research_only_count"], 1)
|
||||
self.assertEqual(result["liquidity_kernel_summary"]["item_count"], 1)
|
||||
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"])
|
||||
|
||||
def test_route_request_trading_doctrine_rules_returns_d1_matrix(self) -> None:
|
||||
status, payload = route_request(
|
||||
@@ -170,6 +174,22 @@ class ApiTests(unittest.TestCase):
|
||||
self.assertEqual(payload["phase"], "D3_LIQUIDITY_KERNEL")
|
||||
self.assertEqual(payload["rules"][0]["rule_id"], "stop_run_sweep_raid_validated")
|
||||
|
||||
def test_route_request_trading_pd_array_kernel_returns_d4_registry(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/pd-array-kernel",
|
||||
{},
|
||||
dependencies={
|
||||
"trading_pd_array_kernel": lambda: {
|
||||
"phase": "D4_PD_ARRAY_REACTION_KERNEL",
|
||||
"rules": [{"rule_id": "pd_array_relevance_matrix"}],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(status, 200)
|
||||
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_candidates_returns_items_payload(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/candidates",
|
||||
|
||||
@@ -134,6 +134,22 @@ class ApiServerRoutingTests(unittest.TestCase):
|
||||
self.assertEqual(payload["phase"], "D3_LIQUIDITY_KERNEL")
|
||||
self.assertEqual(payload["rule_count"], 6)
|
||||
|
||||
def test_routes_trading_pd_array_kernel(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/pd-array-kernel",
|
||||
{},
|
||||
dependencies={
|
||||
"trading_pd_array_kernel": lambda: {
|
||||
"phase": "D4_PD_ARRAY_REACTION_KERNEL",
|
||||
"rule_count": 5,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
||||
self.assertEqual(payload["rule_count"], 5)
|
||||
|
||||
def test_routes_trading_alert_quality_summary(self) -> None:
|
||||
status, payload = route_request(
|
||||
"/trading/alerts/quality-summary",
|
||||
|
||||
@@ -318,6 +318,9 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(summary["by_session"]["LONDON"]["avg_mae_r"], 2.7 / 3)
|
||||
self.assertAlmostEqual(summary["by_session"]["LONDON"]["avg_cost_r"], 0.1)
|
||||
self.assertAlmostEqual(summary["by_session"]["LONDON"]["max_drawdown_r"], 2.2)
|
||||
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["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)
|
||||
@@ -435,6 +438,20 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
"swept_level": 103.0,
|
||||
"sweep_confirmed": True,
|
||||
"post_take_reaction": "acceptance_displacement",
|
||||
"pd_array_type": "fvg",
|
||||
"pd_array_id": "fvg-1",
|
||||
"pd_array_role": "entry",
|
||||
"pd_array_rank": "primary",
|
||||
"array_location": "discount",
|
||||
"array_timeframe": "5m",
|
||||
"array_freshness": "fresh",
|
||||
"formation_event": "post_sweep_displacement",
|
||||
"displacement_origin": "post_sweep_displacement",
|
||||
"fvg_role": "entry",
|
||||
"fvg_retrace_confirmed": True,
|
||||
"reaction_quality": "rejection_confirmed",
|
||||
"retest_confirmed": True,
|
||||
"pd_array_invalidation": "fvg_accepted_other_side",
|
||||
"entry_low": 100.0,
|
||||
"entry_high": 100.2,
|
||||
"stop_loss": 99.0,
|
||||
@@ -468,6 +485,8 @@ class BacktestOutcomeTests(unittest.TestCase):
|
||||
self.assertEqual(units[0]["target_plan"]["tp2"]["objective_layer"], "ERL")
|
||||
self.assertEqual(units[0]["liquidity_backtest_features"]["liquidity_target_side"], "buy_side_liquidity")
|
||||
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")
|
||||
|
||||
|
||||
class BacktestServiceRunTests(unittest.TestCase):
|
||||
@@ -736,6 +755,20 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
"expires_ts": datetime(2026, 5, 6, 4, 0, tzinfo=timezone.utc),
|
||||
"confirmation_state": {"executable": True},
|
||||
"missing_confirmations": [],
|
||||
"pd_array_type": "fvg",
|
||||
"pd_array_id": "fvg-12",
|
||||
"pd_array_role": "entry",
|
||||
"pd_array_rank": "primary",
|
||||
"array_location": "discount",
|
||||
"array_timeframe": "5m",
|
||||
"array_freshness": "fresh",
|
||||
"formation_event": "post_sweep_displacement",
|
||||
"displacement_origin": "post_sweep_displacement",
|
||||
"fvg_role": "entry",
|
||||
"fvg_retrace_confirmed": True,
|
||||
"reaction_quality": "rejection_confirmed",
|
||||
"retest_confirmed": True,
|
||||
"pd_array_invalidation": "fvg_accepted_other_side",
|
||||
"entry": {
|
||||
"entry_low": 100.0,
|
||||
"entry_high": 100.2,
|
||||
@@ -807,6 +840,8 @@ class BacktestServiceRunTests(unittest.TestCase):
|
||||
repository.summary["by_doctrine_cohort"]["ICT-CH1-6-LONDON-DISCOUNT-REVERSAL::complete"]["count"],
|
||||
1,
|
||||
)
|
||||
self.assertEqual(repository.summary["pd_array_reaction_status"]["status"], "pd_array_mapped")
|
||||
self.assertEqual(repository.summary["by_pd_array_type"]["fvg"]["count"], 1)
|
||||
|
||||
def test_normalize_timeframe_set_and_bucket_helpers(self) -> None:
|
||||
normalized = normalize_timeframe_set({"bias_tf": "1h", "setup_tf": "15m"})
|
||||
|
||||
@@ -21,6 +21,7 @@ ACTIVE_TOP_LEVEL_DOCS = {
|
||||
"AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md",
|
||||
"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_硬性主线约束_必须遵循ICT交易全书_2026-05-21_v1.md",
|
||||
"AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md",
|
||||
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
|
||||
|
||||
@@ -4,15 +4,20 @@ from src.services.ict_mainline import (
|
||||
DOCTRINE_KERNEL_PHASE,
|
||||
DOCTRINE_LIQUIDITY_PHASE,
|
||||
DOCTRINE_PLAYBOOK_PHASE,
|
||||
DOCTRINE_REACTION_PHASE,
|
||||
build_doctrine_backtest_cohort,
|
||||
build_liquidity_backtest_features,
|
||||
build_pd_array_backtest_features,
|
||||
build_doctrine_playbook_gate,
|
||||
build_doctrine_playbook_registry,
|
||||
build_doctrine_rule_matrix,
|
||||
build_liquidity_kernel_registry,
|
||||
build_pd_array_reaction_registry,
|
||||
evaluate_doctrine_research_status,
|
||||
evaluate_liquidity_kernel_status,
|
||||
evaluate_pd_array_reaction_status,
|
||||
summarize_liquidity_kernel_statuses,
|
||||
summarize_pd_array_reaction_statuses,
|
||||
summarize_doctrine_rule_gaps,
|
||||
)
|
||||
|
||||
@@ -41,6 +46,8 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertIn("target_rule", playbook)
|
||||
self.assertIn("liquidity_target_side", playbook["liquidity_event_rule"]["required_fields"])
|
||||
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.assertTrue(playbook["promotion_gate"]["requires_backtest_quality_gate"])
|
||||
|
||||
def test_d3_liquidity_kernel_registry_exposes_target_ranking_rules(self) -> None:
|
||||
@@ -112,6 +119,50 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertGreater(summary["reason_counts"]["sweep_is_not_setup"], 0)
|
||||
self.assertEqual(summary["target_side_counts"]["buy_side_liquidity"], 1)
|
||||
|
||||
def test_d4_pd_array_reaction_registry_exposes_location_quality_rules(self) -> None:
|
||||
registry = build_pd_array_reaction_registry()
|
||||
|
||||
self.assertEqual(registry["phase"], DOCTRINE_REACTION_PHASE)
|
||||
self.assertEqual(registry["status"], "active")
|
||||
self.assertGreaterEqual(registry["rule_count"], 5)
|
||||
self.assertIn("pd_array_role", registry["required_backtest_feature_fields"])
|
||||
self.assertIn("PD Array is reaction-location quality, not a direction source.", registry["hard_rules"])
|
||||
|
||||
def test_fvg_label_without_reaction_context_stays_study_only(self) -> None:
|
||||
status = evaluate_pd_array_reaction_status(
|
||||
{
|
||||
"pd_array_type": "fvg",
|
||||
"fvg_retrace_confirmed": True,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertFalse(status["passed"])
|
||||
self.assertEqual(status["phase"], DOCTRINE_REACTION_PHASE)
|
||||
self.assertEqual(status["status"], "study_only")
|
||||
self.assertIn("missing_pd_array_role", status["reasons"])
|
||||
self.assertIn("missing_array_location", status["reasons"])
|
||||
self.assertIn("fvg_is_not_setup", status["reasons"])
|
||||
|
||||
def test_complete_pd_array_reaction_payload_maps_without_execution_permission(self) -> None:
|
||||
status = evaluate_pd_array_reaction_status(_complete_pd_array_payload())
|
||||
|
||||
self.assertTrue(status["passed"])
|
||||
self.assertEqual(status["status"], "pd_array_mapped")
|
||||
self.assertEqual(status["execution_boundary"], "read_only_analysis_no_order_submission")
|
||||
|
||||
def test_pd_array_summary_counts_blockers(self) -> None:
|
||||
summary = summarize_pd_array_reaction_statuses(
|
||||
[
|
||||
{"pd_array_type": "fvg", "fvg_retrace_confirmed": True},
|
||||
_complete_pd_array_payload(),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(summary["item_count"], 2)
|
||||
self.assertEqual(summary["passed_count"], 1)
|
||||
self.assertGreater(summary["reason_counts"]["fvg_is_not_setup"], 0)
|
||||
self.assertEqual(summary["array_type_counts"]["fvg"], 2)
|
||||
|
||||
def test_playbook_gate_blocks_missing_and_passes_registered_schema(self) -> None:
|
||||
missing = build_doctrine_playbook_gate({"model_code": "SWEEP_MSS_FVG"})
|
||||
|
||||
@@ -199,6 +250,7 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
"swept_level": 99.5,
|
||||
"sweep_confirmed": True,
|
||||
"post_take_reaction": "reclaim_displacement",
|
||||
**_complete_pd_array_payload(),
|
||||
"summary": {"quality_gate": {"passed": True, "reasons": []}},
|
||||
}
|
||||
)
|
||||
@@ -207,12 +259,14 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertEqual(cohort["doctrine_completeness"], "complete")
|
||||
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")
|
||||
|
||||
missing = build_doctrine_backtest_cohort({"model_code": "SWEEP_MSS_FVG"})
|
||||
|
||||
self.assertEqual(missing["cohort_key"], "missing_playbook::incomplete")
|
||||
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"])
|
||||
|
||||
def test_liquidity_backtest_features_extract_nested_candidate_fields(self) -> None:
|
||||
features = build_liquidity_backtest_features(
|
||||
@@ -234,6 +288,47 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
||||
self.assertEqual(features["liquidity_pool_rank"], "primary")
|
||||
self.assertFalse(features["sweep_confirmed"])
|
||||
|
||||
def test_pd_array_backtest_features_extract_nested_candidate_fields(self) -> None:
|
||||
features = build_pd_array_backtest_features(
|
||||
{
|
||||
"candidate": {
|
||||
"entry": {
|
||||
"pd_array_type": "fvg",
|
||||
"pd_array_id": "fvg-1",
|
||||
"pd_array_role": "entry",
|
||||
},
|
||||
"confirmation_state": {"reaction_quality": "rejection_confirmed"},
|
||||
},
|
||||
"pd_array_rank": "primary",
|
||||
"array_location": "discount",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(features["pd_array_type"], "fvg")
|
||||
self.assertEqual(features["pd_array_id"], "fvg-1")
|
||||
self.assertEqual(features["pd_array_role"], "entry")
|
||||
self.assertEqual(features["pd_array_rank"], "primary")
|
||||
self.assertEqual(features["array_location"], "discount")
|
||||
|
||||
|
||||
def _complete_pd_array_payload() -> dict:
|
||||
return {
|
||||
"pd_array_type": "fvg",
|
||||
"pd_array_id": "fvg-1",
|
||||
"pd_array_role": "entry",
|
||||
"pd_array_rank": "primary",
|
||||
"array_location": "discount",
|
||||
"array_timeframe": "5m",
|
||||
"array_freshness": "fresh",
|
||||
"formation_event": "post_sweep_displacement",
|
||||
"displacement_origin": "post_sweep_displacement",
|
||||
"fvg_role": "entry",
|
||||
"fvg_retrace_confirmed": True,
|
||||
"reaction_quality": "rejection_confirmed",
|
||||
"retest_confirmed": True,
|
||||
"pd_array_invalidation": "fvg_accepted_other_side",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user