diff --git a/SESSION.md b/SESSION.md index 84e202d..48d63b4 100644 --- a/SESSION.md +++ b/SESSION.md @@ -19,7 +19,7 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. ## Last Updated -2026-05-21 UTC +2026-05-22 UTC ## Automation Status @@ -848,10 +848,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen. - D1 Doctrine Market Language Kernel is complete locally: added `src/services/ict_mainline/doctrine.py`, `GET /trading/doctrine-rules`, Strategy Backtest `doctrine_research_status` / `doctrine_rule_gap_summary`, and Trader Cockpit watchlist/readout `research_only` gating. Old sweep/MSS/FVG/retrace candidates now expose `missing_doctrine_setup_playbook` or the specific missing Doctrine rule reason unless mapped to a full setup playbook. No execution/readiness/risk/trust/operator-confirmation gate was changed. - D2 Doctrine Playbook / Backtest Cohort is complete locally: added the first registered setup playbook `ICT-CH1-6-LONDON-DISCOUNT-REVERSAL`, `GET /trading/doctrine-playbooks`, `doctrine_backtest_cohort` / `doctrine_cohort_summary`, `BACKTEST_SETUP_PLAYBOOK_ID`, `BACKTEST_BATCH_SETUP_PLAYBOOK_IDS`, and a Review Now gate that requires Doctrine playbook, active Trading Plan, and Strategy Backtest quality evidence to all pass. No execution/readiness/risk/trust/operator-confirmation gate was changed. - D3 Liquidity Kernel is complete locally: added `DOCTRINE_LIQUIDITY_PHASE=D3_LIQUIDITY_KERNEL`, `LiquidityTargetRule`, `GET /trading/liquidity-kernel`, Strategy Backtest `liquidity_backtest_features` / `liquidity_kernel_status` / `liquidity_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`. BSL / SSL / liquidity pool / stop run / sweep / raid are now target-ranking and no-trade/wait rules; `sweep_is_not_setup`, `missing_liquidity_target`, `missing_liquidity_pool`, `missing_working_draw`, and `manipulation_window_wait` block promotion until the Working Draw, ranked pool, post-take reaction, and objective reorder evidence are present. No execution/readiness/risk/trust/operator-confirmation gate was changed. +- D4 PD Array / Reaction Kernel is complete, deployed, and remote-verified: added `DOCTRINE_REACTION_PHASE=D4_PD_ARRAY_REACTION_KERNEL`, `PdArrayReactionRule`, `GET /trading/pd-array-kernel`, Strategy Backtest `pd_array_backtest_features` / `pd_array_reaction_status` / `pd_array_reaction_summary`, and `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`. FVG / OB / Breaker / Mitigation / BPR/Rebalancing now stay reaction-location quality rankings rather than direction tags; `missing_pd_array`, `missing_pd_array_role`, `missing_array_location`, `fvg_is_not_setup`, `order_block_not_authorized`, and `breaker_without_failure_precondition` block promotion until role, rank, location, freshness, origin, reaction/retest, and invalidation are explicit. Validation passed focused D4/API/backtest/docs tests, full unit discovery (`839` tests), script static checks (`79` scripts), JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/pd-array-kernel` field smoke, `/trading/backtests` D4 field smoke, and Playwright screenshot evidence at `output/playwright/d4-pd-array-strategy-backtest-1366x768.png`. The read-only smoke initially exposed the expected external proxy blocker; starting the local v2rayN/sing-box proxy restored `192.168.3.73:10808` and the gate passed. No execution/readiness/risk/trust/operator-confirmation gate was changed. ## Exact Next Step -Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`, `docs/ICT交易全书_出版终校版.md`, `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md`, `SESSION.md`, and `TASKS.md` as the active anchors. Start D4 PD Array / Reaction Kernel: extract FVG / Order Block / Breaker / Mitigation / Rebalance / Void into reaction-location quality ranking rules; keep PD Array as a reaction/entry-location layer, not a direction source; wire the playbook `pd_array_rule` toward measurable backtest fields. Do not restart external-context/archive/digest long-tail work, do not add order-submission features, do not physically delete S1 deprecated surfaces yet, and do not fabricate R2 paper samples from blocked candidates. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged. +Use `docs/AI_ICT_DoctrineKernel全书规则内核重建_D0_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel市场语言规则内核_D1_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PlaybookBacktestCohort_D2_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_LiquidityKernel_D3_2026-05-21_v1.md`, `docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md`, `docs/ICT交易全书_出版终校版.md`, `docs/AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md`, `docs/AI_ICT_批量历史回测验证_S16_2026-05-21_v1.md`, `SESSION.md`, and `TASKS.md` as the active anchors. Start D5 Time / Session Kernel: rebuild Asia / London / NY AM / NY PM, killzone, Judas/manipulation window, opening range, weekly profile, and news/no-trade time into measurable session-quality rules; a setup must bind to a verifiable session window and outside-session evidence stays study-only/wait. Do not restart external-context/archive/digest long-tail work, do not add order-submission features, do not physically delete S1 deprecated surfaces yet, and do not fabricate R2 paper samples from blocked candidates. Keep `execution_allowed`, `decision_support_ready`, `real_live_ready`, risk governor, trust bucket, and operator confirmation protocol unchanged. ## Validation Commands Used Recently diff --git a/TASKS.md b/TASKS.md index 1768aac..11e3ad5 100644 --- a/TASKS.md +++ b/TASKS.md @@ -955,7 +955,8 @@ - [x] D1 Doctrine Market Language Kernel: built the first `Range / Draw / Location / Context / Time / Invalidation / IRL-ERL` registry, added `/trading/doctrine-rules`, Strategy Backtest Doctrine gap aggregation, and Trader Cockpit `research_only_reason` gating for old surface-shape candidates; no live order submission and no fabricated samples - [x] D2 Doctrine Playbook / Backtest Cohort: defined first registered setup playbook `ICT-CH1-6-LONDON-DISCOUNT-REVERSAL`, added `/trading/doctrine-playbooks`, `doctrine_backtest_cohort` / `doctrine_cohort_summary`, setup-playbook batch envs, Strategy Backtest UI cohort display, and Review Now gating that requires playbook mapping + historical quality gate + active Trading Plan - [x] D3 Liquidity Kernel: rebuilt BSL / SSL / liquidity pool / stop run / sweep / raid from shape labels into target-ranking and no-trade/wait rules; sweep remains evidence component only, Strategy Backtest exposes liquidity features/status/summary, and `/trading/liquidity-kernel` exposes the D3 registry -- [ ] D4 next product gate: rebuild PD Array / Reaction Kernel so FVG / OB / Breaker / Mitigation / Rebalance / Void become reaction-location quality rankings instead of standalone direction tags +- [x] D4 PD Array / Reaction Kernel: rebuilt FVG / OB / Breaker / Mitigation / Rebalance / Void into reaction-location quality ranking and no-trade/wait rules; PD Array remains location/reaction evidence only, Strategy Backtest exposes PD Array features/status/summary, `/trading/pd-array-kernel` exposes the D4 registry, and remote deploy/ops/read-only smoke passed after the local proxy was restarted +- [ ] D5 next product gate: rebuild Time / Session Kernel so Asia / London / NY AM / NY PM / killzone / Judas window / opening range / weekly profile / news time become measurable session-quality gates instead of loose time labels - [x] R3 OKX sandbox isolated smoke tooling: guarded script and service can call OKX demo submit/query/cancel/query/fills only when `OKX_SANDBOX_SMOKE_EXECUTE=1`; local validation, deploy smoke, and remote ops smoke passed - [ ] R3 sandbox credential permission gate deferred: current phase is assisted analysis / decision support only, so do not request trade-permission credentials or make submit/cancel smoke the next gate - [ ] Long-term target: complete `Real-time Assisted Trading V1` without enabling default live auto-trading diff --git a/docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md b/docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md new file mode 100644 index 0000000..7daa7d9 --- /dev/null +++ b/docs/AI_ICT_DoctrineKernel_PDArrayReactionKernel_D4_2026-05-22_v1.md @@ -0,0 +1,95 @@ +# AI_ICT Doctrine Kernel PD Array / Reaction Kernel D4 2026-05-22 v1 + +## Scope + +D4 rebuilds PD Arrays from visible shape labels into a reaction-location quality layer. This slice follows `docs/ICT交易全书_出版终校版.md` chapters 13-17 and keeps the hard mainline chain: + +`Range / Draw / Location -> Liquidity / Sweep / Inducement / Path -> Displacement / MSS / FVG / Retest / Session -> backtest evidence -> pre-market plan -> intraday unique focus or no-trade reason -> journal / study-notes feedback`. + +## Product Rule + +PD Array is not a direction source. FVG, Order Block, Breaker, Mitigation, BPR/Rebalancing, and Void can only support a setup after the system can state: + +- what reaction location it is, +- whether it is target, entry, repair, expansion, or observation, +- where it sits relative to the active Range / Draw / Location / Liquidity chain, +- how fresh and high-rank it is, +- what displacement or failed-order-flow premise created it, +- what reaction/retest behavior occurred after price arrived, +- where the array-specific invalidation is. + +If those items are missing, Strategy Backtest and Trader Cockpit must keep the setup as `study_only` or `wait`. + +## Implemented Kernel + +Code anchor: `src/services/ict_mainline/doctrine.py` + +New phase: + +- `DOCTRINE_REACTION_PHASE = D4_PD_ARRAY_REACTION_KERNEL` + +New registry/API: + +- `GET /trading/pd-array-kernel` +- `build_pd_array_reaction_registry` +- `list_pd_array_rules` + +New backtest surface fields: + +- `pd_array_backtest_features` +- `pd_array_reaction_status` +- `pd_array_reaction_summary` +- `pd_array_reaction_status_counts` +- `pd_array_reaction_reason_counts` +- `by_pd_array_type` + +## D4 Rules + +1. `pd_array_relevance_matrix` + PD Array must be ranked as reaction-location quality. It is not a buy/sell or bias signal. + +2. `fvg_behavior_after_rebalance` + FVG is not a trade. It requires role, displacement origin, retrace/retest, and reaction quality. + +3. `bpr_rebalancing_classifier` + BPR/Rebalancing is path state. It must classify internal repair versus external expansion and require structure response. + +4. `order_block_authorization` + Order Block needs displacement origin, validation, structure context, draw relationship, and parent/subordinate alignment. + +5. `breaker_mitigation_failure_reuse` + Breaker/Mitigation requires objective failed order flow plus revisit reaction before review. + +## Promotion Gate + +The D2 playbook now requires D4 PD Array fields before promotion: + +- `pd_array_type` +- `pd_array_id` +- `pd_array_role` +- `pd_array_rank` +- `array_location` +- `array_timeframe` +- `array_freshness` +- `formation_event` +- `displacement_origin` +- `fvg_role` +- `fvg_retrace_confirmed` +- `reaction_quality` +- `invalidation` + +The Doctrine Backtest Cohort now blocks promotion unless: + +- D2 playbook schema is complete, +- D3 Liquidity Kernel passes, +- D4 PD Array / Reaction Kernel passes, +- Strategy Backtest quality gate passes, +- active Trading Plan gate passes. + +## Read-Only Boundary + +D4 does not add order submission, does not change `execution_allowed`, does not change `decision_support_ready`, does not change `real_live_ready`, and does not loosen risk governor / trust bucket / operator confirmation protocol. + +## Next Slice + +D5 should rebuild the Time / Session Kernel: Asia, London, NY AM, NY PM, killzones, Judas/manipulation windows, opening range, weekly profile, news/time-based no-trade rules, and measurable session fields in Strategy Backtest. diff --git a/output/playwright/d4-pd-array-strategy-backtest-1366x768.png b/output/playwright/d4-pd-array-strategy-backtest-1366x768.png new file mode 100644 index 0000000..f0dfb8d Binary files /dev/null and b/output/playwright/d4-pd-array-strategy-backtest-1366x768.png differ diff --git a/src/api/backtest.py b/src/api/backtest.py index 336edb5..737c373 100644 --- a/src/api/backtest.py +++ b/src/api/backtest.py @@ -8,13 +8,16 @@ from src.services.backtest import evaluate_backtest_quality from src.services.ict_mainline import ( build_doctrine_backtest_cohort, build_liquidity_backtest_features, + build_pd_array_backtest_features, build_backtest_quality_gate, build_ict_rulebook_mapping, evaluate_doctrine_research_status, evaluate_liquidity_kernel_status, + evaluate_pd_array_reaction_status, summarize_doctrine_backtest_cohorts, summarize_doctrine_rule_gaps, summarize_liquidity_kernel_statuses, + summarize_pd_array_reaction_statuses, ) @@ -37,6 +40,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in doctrine_rule_gap_summary = summarize_doctrine_rule_gaps(items) doctrine_cohort_summary = summarize_doctrine_backtest_cohorts(items) liquidity_kernel_summary = summarize_liquidity_kernel_statuses(items) + pd_array_reaction_summary = summarize_pd_array_reaction_statuses(items) return { "instrument_id": instrument_id, "item_count": len(items), @@ -47,6 +51,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in "doctrine_rule_gap_summary": doctrine_rule_gap_summary, "doctrine_cohort_summary": doctrine_cohort_summary, "liquidity_kernel_summary": liquidity_kernel_summary, + "pd_array_reaction_summary": pd_array_reaction_summary, "ict_rulebook_mapping": ict_rulebook_mapping, "items": items, } @@ -131,6 +136,27 @@ def normalize_backtest_summary_row(row: dict) -> dict: "liquidity_backtest_features": liquidity_backtest_features, } ) + pd_array_backtest_features = build_pd_array_backtest_features( + { + "subject_type": row.get("subject_type"), + "model_code": row.get("model_code"), + "candidate_model_code": row.get("candidate_model_code"), + "timeframe_set": timeframe_set, + "config": config, + "summary": summary, + } + ) + pd_array_reaction_status = evaluate_pd_array_reaction_status( + { + "subject_type": row.get("subject_type"), + "model_code": row.get("model_code"), + "candidate_model_code": row.get("candidate_model_code"), + "timeframe_set": timeframe_set, + "config": config, + "summary": summary, + "pd_array_backtest_features": pd_array_backtest_features, + } + ) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { "subject_type": row.get("subject_type"), @@ -140,6 +166,7 @@ def normalize_backtest_summary_row(row: dict) -> dict: "config": config, "summary": summary, "liquidity_backtest_features": liquidity_backtest_features, + "pd_array_backtest_features": pd_array_backtest_features, }, quality_gate=dict(summary.get("quality_gate") or {}), ) @@ -152,6 +179,8 @@ def normalize_backtest_summary_row(row: dict) -> dict: "doctrine_research_status": doctrine_research_status, "liquidity_backtest_features": liquidity_backtest_features, "liquidity_kernel_status": liquidity_kernel_status, + "pd_array_backtest_features": pd_array_backtest_features, + "pd_array_reaction_status": pd_array_reaction_status, "doctrine_backtest_cohort": doctrine_backtest_cohort, } @@ -189,6 +218,13 @@ def _build_operator_summary(latest: Optional[dict]) -> dict: status = "liquidity_kernel_blocked" decision = "do_not_promote_strategy" reason = str(liquidity_reasons[0]) + pd_array_status = dict(latest.get("pd_array_reaction_status") or {}) + if not pd_array_status.get("passed"): + pd_array_reasons = list(pd_array_status.get("reasons") or []) + if pd_array_reasons: + status = "pd_array_reaction_blocked" + decision = "do_not_promote_strategy" + reason = str(pd_array_reasons[0]) if not (doctrine_cohort.get("promotion_gate") or {}).get("passed"): cohort_reasons = list((doctrine_cohort.get("promotion_gate") or {}).get("reasons") or []) if cohort_reasons: diff --git a/src/api/server.py b/src/api/server.py index 5e9fa8d..95228d4 100644 --- a/src/api/server.py +++ b/src/api/server.py @@ -122,7 +122,12 @@ from src.api.training import ( parse_quality_sort_query, submit_training_attempt, ) -from src.services.ict_mainline import build_doctrine_playbook_registry, build_doctrine_rule_matrix, build_liquidity_kernel_registry +from src.services.ict_mainline import ( + build_doctrine_playbook_registry, + build_doctrine_rule_matrix, + build_liquidity_kernel_registry, + build_pd_array_reaction_registry, +) from src.services.execution import ExecutionService from src.services.market_state import start_market_truth_runtime from src.services.runtime import start_trading_runtime_service @@ -225,6 +230,7 @@ def build_api_dependencies() -> dict: "trading_doctrine_rules": build_doctrine_rule_matrix, "trading_doctrine_playbooks": build_doctrine_playbook_registry, "trading_liquidity_kernel": build_liquidity_kernel_registry, + "trading_pd_array_kernel": build_pd_array_reaction_registry, "training_cases": get_training_cases, "training_case": get_training_case, "training_export_cases": export_training_case_package, @@ -852,6 +858,8 @@ def route_request( return 200, deps["trading_doctrine_playbooks"]() if method == "GET" and path == "/trading/liquidity-kernel": return 200, deps["trading_liquidity_kernel"]() + if method == "GET" and path == "/trading/pd-array-kernel": + return 200, deps["trading_pd_array_kernel"]() if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "candidates"]: candidate = deps["trading_candidate_by_id"](_parse_path_int(path_parts[2], "candidate_id")) if candidate is None: diff --git a/src/services/backtest/backtest_service.py b/src/services/backtest/backtest_service.py index 1ed26d6..8585e32 100644 --- a/src/services/backtest/backtest_service.py +++ b/src/services/backtest/backtest_service.py @@ -7,7 +7,9 @@ from src.services.backtest.replay import ReplaySwing, iter_historical_replay from src.services.ict_mainline import ( build_doctrine_backtest_cohort, build_liquidity_backtest_features, + build_pd_array_backtest_features, evaluate_liquidity_kernel_status, + evaluate_pd_array_reaction_status, extract_setup_playbook_id, ) @@ -142,6 +144,16 @@ class BacktestService: 'active_trading_plan': active_trading_plan, } ) + pd_array_backtest_features = build_pd_array_backtest_features( + { + 'subject_type': subject_type, + 'model_code': 'SWEEP_MSS_FVG', + 'candidate_model_code': candidate_model_code, + 'timeframe_set': timeframe_set, + 'run_metadata': run_metadata, + 'active_trading_plan': active_trading_plan, + } + ) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -150,6 +162,7 @@ class BacktestService: 'timeframe_set': timeframe_set, 'run_metadata': run_metadata, 'liquidity_backtest_features': liquidity_backtest_features, + 'pd_array_backtest_features': pd_array_backtest_features, } ) run_id = self.repository.insert_backtest_run( @@ -174,6 +187,8 @@ class BacktestService: 'setup_playbook_id': setup_playbook_id, 'liquidity_backtest_features': liquidity_backtest_features, 'liquidity_kernel_status': evaluate_liquidity_kernel_status(liquidity_backtest_features), + 'pd_array_backtest_features': pd_array_backtest_features, + 'pd_array_reaction_status': evaluate_pd_array_reaction_status(pd_array_backtest_features), 'doctrine_backtest_cohort': doctrine_backtest_cohort, 'run_metadata': run_metadata, }, @@ -194,8 +209,11 @@ class BacktestService: summary['quality_gate'] = evaluate_backtest_quality(summary, gate=quality_gate, sample_basis='executed') outcome_setup_playbook_id = setup_playbook_id or _first_outcome_meta_value(outcomes, 'setup_playbook_id') outcome_liquidity_features = _first_outcome_meta_value(outcomes, 'liquidity_backtest_features') or liquidity_backtest_features + outcome_pd_array_features = _first_outcome_meta_value(outcomes, 'pd_array_backtest_features') or pd_array_backtest_features summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {}) summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features']) + summary['pd_array_backtest_features'] = dict(outcome_pd_array_features or {}) + summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features']) summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -205,6 +223,7 @@ class BacktestService: 'run_metadata': {**run_metadata, 'setup_playbook_id': outcome_setup_playbook_id}, 'summary': summary, 'liquidity_backtest_features': summary['liquidity_backtest_features'], + 'pd_array_backtest_features': summary['pd_array_backtest_features'], }, quality_gate=summary['quality_gate'], ) @@ -242,6 +261,13 @@ class BacktestService: 'run_metadata': run_metadata, } ) + signal_pd_array_features = build_pd_array_backtest_features( + { + 'signal': signal, + 'target_plan': target_plan, + 'run_metadata': run_metadata, + } + ) error_tags = ['no_future_candles'] if len(sessions) == 0: error_tags.append('outside_session_window') @@ -280,6 +306,8 @@ class BacktestService: 'management_rules': management_rules, 'liquidity_backtest_features': signal_liquidity_features, 'liquidity_kernel_status': evaluate_liquidity_kernel_status(signal_liquidity_features), + 'pd_array_backtest_features': signal_pd_array_features, + 'pd_array_reaction_status': evaluate_pd_array_reaction_status(signal_pd_array_features), 'cost_r': 0.0, 'cost_model': cost_config.__dict__, 'backtest_mode': mode, @@ -363,10 +391,18 @@ class BacktestService: 'run_metadata': run_metadata, } ), + 'pd_array_backtest_features': build_pd_array_backtest_features( + { + 'signal': signal, + 'target_plan': extract_target_plan_from_signal_row(signal), + 'run_metadata': run_metadata, + } + ), 'weekday': _extract_weekday(signal['created_ts']), } ) record.meta['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(record.meta['liquidity_backtest_features']) + record.meta['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(record.meta['pd_array_backtest_features']) outcomes.append(record) self.repository.insert_backtest_result( backtest_run_id=run_id, @@ -386,8 +422,11 @@ class BacktestService: summary = summarize_backtest_outcomes(outcomes, subject_type=subject_type) summary['quality_gate'] = evaluate_backtest_quality(summary, gate=quality_gate) outcome_liquidity_features = _first_outcome_meta_value(outcomes, 'liquidity_backtest_features') or liquidity_backtest_features + outcome_pd_array_features = _first_outcome_meta_value(outcomes, 'pd_array_backtest_features') or pd_array_backtest_features summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {}) summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features']) + summary['pd_array_backtest_features'] = dict(outcome_pd_array_features or {}) + summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features']) summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort( { 'subject_type': subject_type, @@ -397,6 +436,7 @@ class BacktestService: 'run_metadata': run_metadata, 'summary': summary, 'liquidity_backtest_features': summary['liquidity_backtest_features'], + 'pd_array_backtest_features': summary['pd_array_backtest_features'], }, quality_gate=summary['quality_gate'], ) @@ -963,6 +1003,17 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: } ) liquidity_kernel_status = evaluate_liquidity_kernel_status(liquidity_backtest_features) + pd_array_backtest_features = build_pd_array_backtest_features( + { + 'first_candidate': first, + 'latest_candidate': latest, + 'confirmed_candidate': confirmed_row or {}, + 'entry_payload': entry_payload, + 'target_plan': target_plan, + 'setup_playbook_id': setup_playbook_id, + } + ) + pd_array_reaction_status = evaluate_pd_array_reaction_status(pd_array_backtest_features) doctrine_backtest_cohort = build_doctrine_backtest_cohort( { 'setup_playbook_id': setup_playbook_id, @@ -972,6 +1023,7 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: 'trigger_tf': first.get('trigger_timeframe'), }, 'liquidity_backtest_features': liquidity_backtest_features, + 'pd_array_backtest_features': pd_array_backtest_features, } ) units.append( @@ -982,6 +1034,8 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]: 'doctrine_backtest_cohort': doctrine_backtest_cohort, 'liquidity_backtest_features': liquidity_backtest_features, 'liquidity_kernel_status': liquidity_kernel_status, + 'pd_array_backtest_features': pd_array_backtest_features, + 'pd_array_reaction_status': pd_array_reaction_status, 'candidate_model_code': first.get('candidate_model_code'), 'side': first.get('side'), 'session_code': first.get('session_code'), @@ -1075,6 +1129,8 @@ def build_plan_replay_meta( 'doctrine_completeness': (unit.get('doctrine_backtest_cohort') or {}).get('doctrine_completeness'), 'liquidity_backtest_features': dict(unit.get('liquidity_backtest_features') or {}), 'liquidity_kernel_status': dict(unit.get('liquidity_kernel_status') or {}), + 'pd_array_backtest_features': dict(unit.get('pd_array_backtest_features') or {}), + 'pd_array_reaction_status': dict(unit.get('pd_array_reaction_status') or {}), 'plan_activated_ts': _normalize_meta_ts(unit.get('plan_activated_ts')), 'plan_created_ts': _normalize_meta_ts(unit.get('plan_created_ts')), 'candidate_id': unit.get('candidate_id'), @@ -1262,6 +1318,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s grouped_by_timeframe: dict[str, dict] = {} grouped_by_doctrine_cohort: dict[str, dict] = {} grouped_by_liquidity_target_side: dict[str, dict] = {} + grouped_by_pd_array_type: dict[str, dict] = {} r_multiples: list[float] = [] cost_values: list[float] = [] mfe_r_values: list[float] = [] @@ -1274,6 +1331,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s failure_reason_counts: dict[str, int] = {} liquidity_kernel_reason_counts: dict[str, int] = {} liquidity_kernel_status_counts: dict[str, int] = {} + pd_array_reaction_reason_counts: dict[str, int] = {} + pd_array_reaction_status_counts: dict[str, int] = {} plan_total_r: dict[int, float] = {} plan_seen: set[int] = set() @@ -1316,6 +1375,13 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s liquidity_kernel_status_counts[liquidity_status_key] = liquidity_kernel_status_counts.get(liquidity_status_key, 0) + 1 for reason in list(liquidity_status.get('reasons') or []): liquidity_kernel_reason_counts[str(reason)] = liquidity_kernel_reason_counts.get(str(reason), 0) + 1 + pd_array_features = dict(outcome.meta.get('pd_array_backtest_features') or {}) + pd_array_type = str(pd_array_features.get('pd_array_type') or 'missing_pd_array') + pd_array_status = dict(outcome.meta.get('pd_array_reaction_status') or evaluate_pd_array_reaction_status(pd_array_features)) + pd_array_status_key = str(pd_array_status.get('status') or 'unknown') + pd_array_reaction_status_counts[pd_array_status_key] = pd_array_reaction_status_counts.get(pd_array_status_key, 0) + 1 + for reason in list(pd_array_status.get('reasons') or []): + pd_array_reaction_reason_counts[str(reason)] = pd_array_reaction_reason_counts.get(str(reason), 0) + 1 r_multiples.append(float(outcome.meta.get('r_multiple') or 0.0)) cost_values.append(float(outcome.meta.get('cost_r') or 0.0)) mfe_r_values.append(float(outcome.meta.get('mfe_r') or 0.0)) @@ -1327,6 +1393,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s _accumulate_bucket(grouped_by_timeframe, timeframe_bucket, outcome) _accumulate_bucket(grouped_by_doctrine_cohort, doctrine_cohort_key, outcome) _accumulate_bucket(grouped_by_liquidity_target_side, liquidity_target_side, outcome) + _accumulate_bucket(grouped_by_pd_array_type, pd_array_type, outcome) for session_code in session_codes: _accumulate_bucket(grouped_by_session, str(session_code), outcome) @@ -1359,6 +1426,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s 'failure_reasons': failure_reason_counts, 'liquidity_kernel_status_counts': liquidity_kernel_status_counts, 'liquidity_kernel_reason_counts': liquidity_kernel_reason_counts, + 'pd_array_reaction_status_counts': pd_array_reaction_status_counts, + 'pd_array_reaction_reason_counts': pd_array_reaction_reason_counts, 'avg_mfe': _average([outcome.mfe for outcome in outcomes]), 'avg_mae': _average([outcome.mae for outcome in outcomes]), 'avg_mfe_r': _average(mfe_r_values), @@ -1378,6 +1447,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s 'by_timeframe': grouped_by_timeframe, 'by_doctrine_cohort': grouped_by_doctrine_cohort, 'by_liquidity_target_side': grouped_by_liquidity_target_side, + 'by_pd_array_type': grouped_by_pd_array_type, } result['session_performance'] = result['by_session'] return result diff --git a/src/services/ict_mainline/__init__.py b/src/services/ict_mainline/__init__.py index 162b33e..e76ffcd 100644 --- a/src/services/ict_mainline/__init__.py +++ b/src/services/ict_mainline/__init__.py @@ -21,35 +21,44 @@ from src.services.ict_mainline.doctrine 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_rule_matrix, build_doctrine_playbook_gate, build_doctrine_playbook_registry, build_liquidity_kernel_registry, + build_pd_array_reaction_registry, evaluate_doctrine_research_status, evaluate_liquidity_kernel_status, + evaluate_pd_array_reaction_status, extract_setup_playbook_id, get_doctrine_setup_playbook, list_doctrine_setup_playbooks, list_doctrine_rules, list_liquidity_rules, + list_pd_array_rules, summarize_doctrine_backtest_cohorts, summarize_doctrine_rule_gaps, summarize_liquidity_kernel_statuses, + summarize_pd_array_reaction_statuses, ) __all__ = [ "DOCTRINE_KERNEL_PHASE", "DOCTRINE_LIQUIDITY_PHASE", "DOCTRINE_PLAYBOOK_PHASE", + "DOCTRINE_REACTION_PHASE", "ICT_MAINLINE_CHAIN", "build_doctrine_backtest_cohort", "build_liquidity_backtest_features", + "build_pd_array_backtest_features", "build_doctrine_rule_matrix", "build_doctrine_playbook_gate", "build_doctrine_playbook_registry", "build_liquidity_kernel_registry", + "build_pd_array_reaction_registry", "build_ict_rulebook_mapping", "build_ict_rulebook_summary", "build_backtest_quality_gate", @@ -61,15 +70,18 @@ __all__ = [ "classify_ict_concepts", "evaluate_doctrine_research_status", "evaluate_liquidity_kernel_status", + "evaluate_pd_array_reaction_status", "extract_setup_playbook_id", "get_ict_concept", "get_doctrine_setup_playbook", "list_doctrine_setup_playbooks", "list_doctrine_rules", "list_liquidity_rules", + "list_pd_array_rules", "list_ict_concepts", "summarize_doctrine_backtest_cohorts", "summarize_doctrine_rule_gaps", "summarize_liquidity_kernel_statuses", + "summarize_pd_array_reaction_statuses", "summarize_book_rule_errors", ] diff --git a/src/services/ict_mainline/doctrine.py b/src/services/ict_mainline/doctrine.py index e813023..9d766d5 100644 --- a/src/services/ict_mainline/doctrine.py +++ b/src/services/ict_mainline/doctrine.py @@ -9,6 +9,7 @@ from typing import Iterable DOCTRINE_KERNEL_PHASE = "D1_MARKET_LANGUAGE_KERNEL" DOCTRINE_PLAYBOOK_PHASE = "D2_PLAYBOOK_BACKTEST_COHORT" DOCTRINE_LIQUIDITY_PHASE = "D3_LIQUIDITY_KERNEL" +DOCTRINE_REACTION_PHASE = "D4_PD_ARRAY_REACTION_KERNEL" @dataclass(frozen=True) @@ -105,6 +106,34 @@ class LiquidityTargetRule: return payload +@dataclass(frozen=True) +class PdArrayReactionRule: + rule_id: str + stage: str + book_anchor: str + array_type: str + ranking_weight: int + required_inputs: tuple[str, ...] + measurable_fields: tuple[str, ...] + no_trade_when: tuple[str, ...] + wait_when: tuple[str, ...] + backtest_observable: str + operator_rule: str + aliases: tuple[str, ...] + + def to_public_dict(self) -> dict: + payload = asdict(self) + for key in ( + "required_inputs", + "measurable_fields", + "no_trade_when", + "wait_when", + "aliases", + ): + payload[key] = list(payload[key]) + return payload + + D1_DOCTRINE_RULES: tuple[DoctrineRule, ...] = ( DoctrineRule( rule_id="range_defined", @@ -256,8 +285,26 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( "operator_rule": "Displacement and MSS must confirm the post-sweep direction before review.", }, pd_array_rule={ - "required_fields": ("pd_array_type", "pd_array_quality", "fvg_retrace_confirmed"), - "operator_rule": "FVG is usable only when it serves the range/draw/location chain.", + "required_fields": ( + "pd_array_type", + "pd_array_id", + "pd_array_role", + "pd_array_rank", + "array_location", + "array_timeframe", + "array_freshness", + "formation_event", + "displacement_origin", + "fvg_role", + "fvg_retrace_confirmed", + "reaction_quality", + "invalidation", + ), + "ranking_rule_id": "pd_array_relevance_matrix", + "operator_rule": ( + "FVG/OB/Breaker/Mitigation is a reaction-location layer only; it must serve the " + "Range/Draw/Location/Liquidity chain and show post-arrival reaction before review." + ), }, session_timing_rule={ "required_fields": ("session_code", "allowed_sessions", "path_context", "sequence_validity"), @@ -284,6 +331,10 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( "missing_liquidity_pool", "sweep_is_not_setup", "manipulation_window_wait", + "missing_pd_array", + "missing_pd_array_role", + "pd_array_not_direction_source", + "fvg_is_not_setup", "doctrine_missing_time_path", "doctrine_missing_invalidation", "doctrine_missing_exit_architecture", @@ -320,6 +371,9 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = ( "traded_sweep_as_setup", "traded_without_liquidity_target_rank", "traded_during_manipulation_phase", + "traded_fvg_as_direction", + "traded_without_pd_array_context", + "traded_unvalidated_order_block", "traded_without_time_or_path", "traded_without_invalidation", "no_exit_architecture", @@ -477,6 +531,122 @@ D3_LIQUIDITY_TARGET_RULES: tuple[LiquidityTargetRule, ...] = ( ) +D4_PD_ARRAY_REACTION_RULES: tuple[PdArrayReactionRule, ...] = ( + PdArrayReactionRule( + rule_id="pd_array_relevance_matrix", + stage="Array Relevance", + book_anchor="ICT交易全书 chapter 13: PD Array relevance depends on narrative, layer, time, and structure", + array_type="matrix", + ranking_weight=110, + required_inputs=("pd_array_type", "pd_array_role", "pd_array_rank", "array_location", "array_timeframe"), + measurable_fields=("pd_array_type", "pd_array_id", "pd_array_role", "pd_array_rank", "array_location", "array_timeframe", "array_freshness"), + no_trade_when=("missing_pd_array", "missing_pd_array_role", "pd_array_not_direction_source"), + wait_when=("pd_array_overload_wait", "array_quality_conflict_unresolved"), + backtest_observable="setup_without_ranked_relevant_pd_array", + operator_rule="Rank only a few relevant arrays; PD Array is a reaction location, not a direction source.", + aliases=( + "pd_array", + "pd_array_type", + "pd_array_role", + "pd_array_rank", + "array_location", + "array_timeframe", + "array_freshness", + ), + ), + PdArrayReactionRule( + rule_id="fvg_behavior_after_rebalance", + stage="FVG / Imbalance", + book_anchor="ICT交易全书 chapter 14: trade the behavior after imbalance, not the FVG label", + array_type="fvg", + ranking_weight=100, + required_inputs=("fvg_role", "fvg_retrace_confirmed", "fvg_fill_depth", "displacement_origin", "reaction_quality"), + measurable_fields=("fvg_role", "fvg_state", "fvg_retrace_confirmed", "fvg_fill_depth", "fvg_ce_reached", "displacement_origin", "reaction_quality"), + no_trade_when=("fvg_is_not_setup", "missing_fvg_role", "fvg_without_displacement_origin"), + wait_when=("awaiting_fvg_retrace_confirmation", "awaiting_reaction_quality"), + backtest_observable="fvg_label_without_retrace_reaction_context", + operator_rule="Define whether FVG is target or entry; first touch without reaction stays study-only.", + aliases=( + "fvg", + "fair_value_gap", + "imbalance", + "fair_valuation", + "fvg_role", + "fvg_retrace_confirmed", + "fvg_fill_depth", + "fvg_ce_reached", + ), + ), + PdArrayReactionRule( + rule_id="bpr_rebalancing_classifier", + stage="BPR / Rebalancing", + book_anchor="ICT交易全书 chapter 15: rebalancing is path state, not reversal promise", + array_type="bpr", + ranking_weight=80, + required_inputs=("bpr_id", "rebalancing_state", "rebalancing_depth", "trade_classification", "structure_response"), + measurable_fields=("bpr_id", "rebalancing_state", "rebalancing_depth", "trade_classification", "structure_response", "irl_erl_priority"), + no_trade_when=("rebalancing_without_structure_response", "bpr_without_imbalance_origin"), + wait_when=("awaiting_rebalance_response", "trade_classification_unresolved"), + backtest_observable="rebalancing_used_as_reversal_without_structure_response", + operator_rule="Classify internal repair versus external expansion before using BPR/Rebalancing.", + aliases=( + "bpr", + "balanced_price_range", + "rebalancing", + "return_to_fair_value", + "rebalancing_depth", + "trade_classification", + "structure_response", + ), + ), + PdArrayReactionRule( + rule_id="order_block_authorization", + stage="Order Block", + book_anchor="ICT交易全书 chapter 16: Order Block needs displacement, validation, structure, time, and draw", + array_type="order_block", + ranking_weight=90, + required_inputs=("order_block_role", "order_block_validated", "parent_ob_id", "displacement_origin", "fvg_ob_pairing"), + measurable_fields=("order_block_role", "order_block_validated", "parent_ob_id", "subordinate_ob_id", "fvg_ob_pairing", "ob_invalidation"), + no_trade_when=("order_block_not_authorized", "ob_without_displacement", "ob_without_draw"), + wait_when=("awaiting_ob_return_confirmation", "awaiting_parent_subordinate_alignment"), + backtest_observable="order_block_label_without_validation", + operator_rule="OB is authorized only when it serves current draw, has displacement/validation, and respects parent/subordinate alignment.", + aliases=( + "order_block", + "ob", + "parent_ob", + "subordinate_ob", + "order_block_role", + "order_block_validated", + "fvg_ob_pairing", + ), + ), + PdArrayReactionRule( + rule_id="breaker_mitigation_failure_reuse", + stage="Breaker / Mitigation", + book_anchor="ICT交易全书 chapter 17: Breaker/Mitigation reuses failed order flow only after objective failure", + array_type="breaker_mitigation", + ranking_weight=85, + required_inputs=("breaker_mitigation_state", "failure_precondition", "trapped_side", "mitigation_inventory_context", "reaction_quality"), + measurable_fields=("breaker_mitigation_state", "failure_precondition", "trapped_side", "mitigation_inventory_context", "breaker_retest_confirmed"), + no_trade_when=("breaker_without_failure_precondition", "mitigation_without_inventory_context"), + wait_when=("awaiting_breaker_retest_confirmation", "awaiting_mitigation_reaction"), + backtest_observable="breaker_or_mitigation_without_failure_precondition", + operator_rule="No objective failure, no Breaker/Mitigation semantics; wait for revisit reaction before review.", + aliases=( + "breaker", + "breaker_block", + "mitigation", + "mitigation_block", + "breaker_mitigation_state", + "failure_precondition", + "trapped_side", + "mitigation_inventory_context", + ), + ), +) + + def list_doctrine_rules() -> list[dict]: return [rule.to_public_dict() for rule in D1_DOCTRINE_RULES] @@ -519,6 +689,10 @@ def list_liquidity_rules() -> list[dict]: return [rule.to_public_dict() for rule in D3_LIQUIDITY_TARGET_RULES] +def list_pd_array_rules() -> list[dict]: + return [rule.to_public_dict() for rule in D4_PD_ARRAY_REACTION_RULES] + + def build_liquidity_kernel_registry() -> dict: return { "phase": DOCTRINE_LIQUIDITY_PHASE, @@ -542,6 +716,29 @@ def build_liquidity_kernel_registry() -> dict: } +def build_pd_array_reaction_registry() -> dict: + return { + "phase": DOCTRINE_REACTION_PHASE, + "source": "docs/ICT浜ゆ槗鍏ㄤ功_鍑虹増缁堟牎鐗?md chapters 13-17", + "status": "active", + "rule_count": len(D4_PD_ARRAY_REACTION_RULES), + "hard_rules": [ + "PD Array is reaction-location quality, not a direction source.", + "FVG is not a trade; only behavior after imbalance can become evidence.", + "BPR / Rebalancing is path state, not a reversal promise.", + "Order Block requires displacement, validation, structure, time, and draw context.", + "Breaker / Mitigation requires an objective failure premise before reuse.", + ], + "required_backtest_feature_fields": _dedupe( + field + for rule in D4_PD_ARRAY_REACTION_RULES + for field in rule.measurable_fields + ), + "rules": list_pd_array_rules(), + "execution_boundary": "read_only_analysis_no_order_submission", + } + + def get_doctrine_setup_playbook(setup_playbook_id: str | None) -> dict | None: key = str(setup_playbook_id or "").strip() if not key: @@ -626,6 +823,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None setup_playbook_id = extract_setup_playbook_id(payload) playbook_gate = build_doctrine_playbook_gate(payload) liquidity_status = evaluate_liquidity_kernel_status(payload) + pd_array_status = evaluate_pd_array_reaction_status(payload) quality = dict(quality_gate or _extract_quality_gate(payload) or {}) quality_reasons = [str(item) for item in list(quality.get("reasons") or []) if item] quality_passed = bool(quality.get("passed")) @@ -639,6 +837,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None promotion_reasons = [ *[str(item) for item in list(playbook_gate.get("reasons") or []) if item], *([] if liquidity_status.get("passed") else [str(item) for item in list(liquidity_status.get("reasons") or []) if item]), + *([] if pd_array_status.get("passed") else [str(item) for item in list(pd_array_status.get("reasons") or []) if item]), *([] if quality_passed else quality_reasons), ] promotion_reasons = _dedupe(promotion_reasons) @@ -654,11 +853,14 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None "playbook_gate": playbook_gate, "liquidity_kernel_status": liquidity_status, "liquidity_backtest_features": dict(liquidity_status.get("features") or {}), + "pd_array_reaction_status": pd_array_status, + "pd_array_backtest_features": dict(pd_array_status.get("features") or {}), "promotion_gate": { "passed": not promotion_reasons, "reasons": promotion_reasons, "requires_playbook_schema_complete": True, "requires_liquidity_kernel_complete": True, + "requires_pd_array_reaction_complete": True, "requires_backtest_quality_gate": True, "requires_active_trading_plan": True, "execution_boundary": "read_only_analysis_no_order_submission", @@ -688,6 +890,7 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: "promotable_count": 0, "promotion_blocker_counts": {}, "liquidity_status_counts": {}, + "pd_array_status_counts": {}, }, ) bucket["run_count"] += 1 @@ -697,6 +900,8 @@ def summarize_doctrine_backtest_cohorts(items: Iterable[dict]) -> dict: bucket["promotable_count"] += 1 liquidity_status = str((cohort.get("liquidity_kernel_status") or {}).get("status") or "unknown") bucket["liquidity_status_counts"][liquidity_status] = int(bucket["liquidity_status_counts"].get(liquidity_status) or 0) + 1 + pd_array_status = str((cohort.get("pd_array_reaction_status") or {}).get("status") or "unknown") + bucket["pd_array_status_counts"][pd_array_status] = int(bucket["pd_array_status_counts"].get(pd_array_status) or 0) + 1 for reason in list((cohort.get("promotion_gate") or {}).get("reasons") or []): reason_key = str(reason) bucket["promotion_blocker_counts"][reason_key] = int(bucket["promotion_blocker_counts"].get(reason_key) or 0) + 1 @@ -852,6 +1057,231 @@ def summarize_liquidity_kernel_statuses(items: Iterable[dict]) -> dict: } +def build_pd_array_backtest_features(payload: object) -> dict: + features = { + "pd_array_type": _string_or_empty( + _extract_first_present_value(payload, keys=("pd_array_type", "array_type", "pd_array_kind", "imbalance_type")) + ), + "pd_array_id": _string_or_empty( + _extract_first_present_value(payload, keys=("pd_array_id", "array_id", "fvg_id", "order_block_id", "breaker_id", "mitigation_id", "bpr_id")) + ), + "pd_array_role": _string_or_empty( + _extract_first_present_value(payload, keys=("pd_array_role", "array_role", "reaction_role", "entry_role")) + ), + "pd_array_rank": _string_or_empty( + _extract_first_present_value(payload, keys=("pd_array_rank", "array_rank", "reaction_rank", "pd_array_quality_rank")) + ), + "pd_array_quality_score": _extract_first_present_value(payload, keys=("pd_array_quality_score", "pd_array_quality", "array_quality_score")), + "array_location": _string_or_empty( + _extract_first_present_value(payload, keys=("array_location", "pd_array_location", "premium_discount_zone", "location")) + ), + "array_timeframe": _string_or_empty( + _extract_first_present_value(payload, keys=("array_timeframe", "pd_array_timeframe", "setup_tf", "setup_timeframe")) + ), + "array_freshness": _string_or_empty( + _extract_first_present_value(payload, keys=("array_freshness", "pd_array_freshness", "fvg_freshness", "ob_freshness")) + ), + "formation_event": _string_or_empty( + _extract_first_present_value(payload, keys=("formation_event", "array_formation_event", "origin_event")) + ), + "displacement_origin": _string_or_empty( + _extract_first_present_value(payload, keys=("displacement_origin", "related_displacement_id", "displacement_event_id", "imbalance_origin")) + ), + "reaction_quality": _string_or_empty( + _extract_first_present_value(payload, keys=("reaction_quality", "post_arrival_reaction", "post_retest_reaction", "rejection_quality")) + ), + "retest_confirmed": _extract_first_present_value(payload, keys=("retest_confirmed", "array_retest_confirmed", "breaker_retest_confirmed")), + "fvg_role": _string_or_empty(_extract_first_present_value(payload, keys=("fvg_role", "fvg_usage", "imbalance_role"))), + "fvg_state": _string_or_empty(_extract_first_present_value(payload, keys=("fvg_state", "fvg_status", "imbalance_state"))), + "fvg_retrace_confirmed": _extract_first_present_value(payload, keys=("fvg_retrace_confirmed", "fvg_retest_confirmed")), + "fvg_fill_depth": _extract_first_present_value(payload, keys=("fvg_fill_depth", "fvg_fill_pct", "fvg_fill_percent")), + "fvg_ce_reached": _extract_first_present_value(payload, keys=("fvg_ce_reached", "ce_reached")), + "bpr_id": _string_or_empty(_extract_first_present_value(payload, keys=("bpr_id", "balanced_price_range_id", "rebalancing_id"))), + "rebalancing_state": _string_or_empty(_extract_first_present_value(payload, keys=("rebalancing_state", "rebalance_state", "bpr_state"))), + "rebalancing_depth": _extract_first_present_value(payload, keys=("rebalancing_depth", "return_depth", "bpr_depth")), + "structure_response": _string_or_empty(_extract_first_present_value(payload, keys=("structure_response", "mss_state", "market_structure_response"))), + "trade_classification": _string_or_empty( + _extract_first_present_value(payload, keys=("trade_classification", "repair_or_expansion", "internal_external_classification")) + ), + "order_block_role": _string_or_empty(_extract_first_present_value(payload, keys=("order_block_role", "ob_role", "order_block_type"))), + "order_block_validated": _extract_first_present_value(payload, keys=("order_block_validated", "ob_validated", "ob_validation_state")), + "parent_ob_id": _string_or_empty(_extract_first_present_value(payload, keys=("parent_ob_id", "parent_order_block_id"))), + "subordinate_ob_id": _string_or_empty(_extract_first_present_value(payload, keys=("subordinate_ob_id", "child_ob_id"))), + "fvg_ob_pairing": _string_or_empty(_extract_first_present_value(payload, keys=("fvg_ob_pairing", "paired_fvg_id", "ob_fvg_pairing"))), + "breaker_mitigation_state": _string_or_empty( + _extract_first_present_value(payload, keys=("breaker_mitigation_state", "breaker_state", "mitigation_state")) + ), + "failure_precondition": _string_or_empty(_extract_first_present_value(payload, keys=("failure_precondition", "failed_order_flow", "objective_failure"))), + "trapped_side": _string_or_empty(_extract_first_present_value(payload, keys=("trapped_side", "trapped_traders_side"))), + "mitigation_inventory_context": _string_or_empty( + _extract_first_present_value(payload, keys=("mitigation_inventory_context", "inventory_context")) + ), + "pd_array_invalidation": _string_or_empty( + _extract_first_present_value(payload, keys=("pd_array_invalidation", "array_invalidation", "ob_invalidation", "invalidation")) + ), + } + lower_type = str(features["pd_array_type"] or "").lower() + features["has_pd_array"] = bool( + features["pd_array_id"] + or features["pd_array_type"] + or features["fvg_role"] + or features["fvg_state"] + or features["fvg_retrace_confirmed"] is not None + or features["bpr_id"] + or features["rebalancing_state"] + or features["order_block_role"] + or features["parent_ob_id"] + or features["breaker_mitigation_state"] + or features["failure_precondition"] + or features["mitigation_inventory_context"] + ) + features["is_fvg_like"] = bool( + any(token in lower_type for token in ("fvg", "fair_value_gap", "imbalance")) + or features["fvg_role"] + or features["fvg_state"] + or features["fvg_retrace_confirmed"] is not None + ) + features["is_bpr_like"] = bool( + any(token in lower_type for token in ("bpr", "rebalancing", "balanced_price_range")) + or features["bpr_id"] + or features["rebalancing_state"] + ) + features["is_order_block_like"] = bool( + any(token in lower_type for token in ("order_block", "ob")) + or features["order_block_role"] + or features["parent_ob_id"] + ) + features["is_breaker_mitigation_like"] = bool( + any(token in lower_type for token in ("breaker", "mitigation")) + or features["breaker_mitigation_state"] + or features["failure_precondition"] + or features["mitigation_inventory_context"] + ) + return features + + +def evaluate_pd_array_reaction_status(payload: object) -> dict: + features = build_pd_array_backtest_features(payload) + no_trade_reasons: list[str] = [] + wait_reasons: list[str] = [] + if not features["has_pd_array"]: + no_trade_reasons.append("missing_pd_array") + if features["has_pd_array"] and not features["pd_array_role"]: + no_trade_reasons.append("missing_pd_array_role") + if features["has_pd_array"] and not features["array_location"]: + no_trade_reasons.append("missing_array_location") + if features["has_pd_array"] and not features["pd_array_rank"]: + wait_reasons.append("unranked_pd_array") + if features["has_pd_array"] and not features["array_timeframe"]: + wait_reasons.append("array_timeframe_unresolved") + if features["has_pd_array"] and not features["array_freshness"]: + wait_reasons.append("array_freshness_unresolved") + if _looks_like_direction_source(features): + no_trade_reasons.append("pd_array_not_direction_source") + + if features["is_fvg_like"]: + if not features["fvg_role"]: + no_trade_reasons.append("missing_fvg_role") + if not features["displacement_origin"]: + no_trade_reasons.append("fvg_without_displacement_origin") + if not _truthy_feature(features["fvg_retrace_confirmed"]): + wait_reasons.append("awaiting_fvg_retrace_confirmation") + if not features["reaction_quality"] and not _truthy_feature(features["retest_confirmed"]): + wait_reasons.append("awaiting_reaction_quality") + if any(reason in no_trade_reasons or reason in wait_reasons for reason in ( + "missing_fvg_role", + "fvg_without_displacement_origin", + "awaiting_fvg_retrace_confirmation", + "awaiting_reaction_quality", + )): + no_trade_reasons.append("fvg_is_not_setup") + + if features["is_bpr_like"]: + if not features["structure_response"]: + no_trade_reasons.append("rebalancing_without_structure_response") + if not features["displacement_origin"]: + no_trade_reasons.append("bpr_without_imbalance_origin") + if not features["trade_classification"]: + wait_reasons.append("trade_classification_unresolved") + + if features["is_order_block_like"]: + if not _truthy_feature(features["order_block_validated"]): + no_trade_reasons.append("order_block_not_authorized") + if not features["displacement_origin"]: + no_trade_reasons.append("ob_without_displacement") + if not features["parent_ob_id"] and not features["subordinate_ob_id"]: + wait_reasons.append("awaiting_parent_subordinate_alignment") + + if features["is_breaker_mitigation_like"]: + if not features["failure_precondition"]: + no_trade_reasons.append("breaker_without_failure_precondition") + if "mitigation" in str(features["breaker_mitigation_state"] or "").lower() and not features["mitigation_inventory_context"]: + no_trade_reasons.append("mitigation_without_inventory_context") + if not _truthy_feature(features["retest_confirmed"]) and not features["reaction_quality"]: + wait_reasons.append("awaiting_breaker_retest_confirmation") + + reasons = _dedupe([*no_trade_reasons, *wait_reasons]) + passed = not reasons + if passed: + status = "pd_array_mapped" + elif no_trade_reasons: + status = "study_only" + else: + status = "wait" + return { + "phase": DOCTRINE_REACTION_PHASE, + "status": status, + "passed": passed, + "reason": "" if passed else reasons[0], + "reasons": reasons, + "no_trade_reasons": _dedupe(no_trade_reasons), + "wait_reasons": _dedupe(wait_reasons), + "features": features, + "can_promote_to_playbook": passed, + "operator_summary": ( + "PD Array reaction location, role, rank, freshness, reaction, and invalidation are mapped." + if passed + else "Treat FVG/OB/Breaker/Mitigation as study-only/wait until the reaction location is ranked and behavior after arrival is explicit." + ), + "execution_boundary": "read_only_analysis_no_order_submission", + } + + +def summarize_pd_array_reaction_statuses(items: Iterable[dict]) -> dict: + reason_counts: dict[str, int] = {} + status_counts: dict[str, int] = {} + array_type_counts: dict[str, int] = {} + total = 0 + passed_count = 0 + for item in items: + total += 1 + status = dict(item.get("pd_array_reaction_status") or evaluate_pd_array_reaction_status(item)) + status_key = str(status.get("status") or "unknown") + status_counts[status_key] = int(status_counts.get(status_key) or 0) + 1 + if status.get("passed"): + passed_count += 1 + features = dict(status.get("features") or {}) + array_type = str(features.get("pd_array_type") or "missing_pd_array") + array_type_counts[array_type] = int(array_type_counts.get(array_type) or 0) + 1 + for reason in list(status.get("reasons") or []): + key = str(reason) + reason_counts[key] = int(reason_counts.get(key) or 0) + 1 + return { + "phase": DOCTRINE_REACTION_PHASE, + "item_count": total, + "passed_count": passed_count, + "blocked_count": max(0, total - passed_count), + "status_counts": status_counts, + "reason_counts": reason_counts, + "array_type_counts": array_type_counts, + "operator_summary": ( + "No backtest items available for PD Array Reaction review." + if not total + else "Review PD Array reaction blockers before treating any FVG/OB/Breaker/Mitigation as usable evidence." + ), + } + + def evaluate_doctrine_research_status( payload: object, *, @@ -1082,3 +1512,22 @@ def _has_liquidity_action(features: dict) -> bool: def _looks_like_equal_priority(features: dict) -> bool: text = " ".join(str(features.get(key) or "").lower() for key in ("working_draw", "liquidity_pool_rank", "reorder_trigger_type")) return any(token in text for token in ("equal_priority", "two_sided_equal", "tied", "both_sides_equal")) + + +def _truthy_feature(value: object) -> bool: + if isinstance(value, bool): + return value + if value is None: + return False + text = str(value).strip().lower() + if text in {"", "0", "false", "no", "none", "null", "missing", "unconfirmed", "not_confirmed"}: + return False + return text in {"1", "true", "yes", "confirmed", "valid", "validated", "accepted", "retest_confirmed", "reaction_confirmed"} + + +def _looks_like_direction_source(features: dict) -> bool: + role_text = str(features.get("pd_array_role") or "").lower() + type_text = str(features.get("pd_array_type") or "").lower() + return any(token in role_text for token in ("direction", "bias", "signal", "buy_sell")) or any( + token in type_text for token in ("direction_source", "signal_source") + ) diff --git a/src/web/app.js b/src/web/app.js index 6f7269e..c77a0cd 100644 --- a/src/web/app.js +++ b/src/web/app.js @@ -5539,6 +5539,17 @@ function renderStrategyBacktests(payload) { doctrineCohort.liquidity_backtest_features || liquidityStatus.features || {}; + const pdArrayStatus = + latest.pd_array_reaction_status || + summary.pd_array_reaction_status || + doctrineCohort.pd_array_reaction_status || + {}; + const pdArrayFeatures = + latest.pd_array_backtest_features || + summary.pd_array_backtest_features || + doctrineCohort.pd_array_backtest_features || + pdArrayStatus.features || + {}; strategyBacktestPanel.appendChild( buildInfoGrid([ @@ -5553,6 +5564,10 @@ function renderStrategyBacktests(payload) { ['Liquidity Reason', formatBacktestDecision(liquidityStatus.reason || 'missing_liquidity_target')], ['Liquidity Target', liquidityFeatures.liquidity_target_side || 'missing_liquidity_target'], ['Liquidity Pool', liquidityFeatures.liquidity_pool_id || 'missing_liquidity_pool'], + ['PD Array Kernel', formatBacktestDecision(pdArrayStatus.status || 'study_only')], + ['PD Array Reason', formatBacktestDecision(pdArrayStatus.reason || 'missing_pd_array')], + ['PD Array', pdArrayFeatures.pd_array_type || 'missing_pd_array'], + ['PD Array Role', pdArrayFeatures.pd_array_role || 'missing_pd_array_role'], ['Promotion Gate', doctrinePromotionGate.passed ? 'Passed' : 'Blocked'], ['Instrument ID', latest.instrument_id ?? 'n/a'], ['Timeframe', formatJsonInline(latest.timeframe_set || {})], @@ -5588,6 +5603,13 @@ function renderStrategyBacktests(payload) { formatList((liquidityStatus.reasons || []).map(formatBacktestDecision), 'Liquidity chain mapped') ) ); + cards.appendChild( + buildMetricCard( + 'PD Array Kernel', + pdArrayStatus.passed ? 'Mapped' : 'Blocked', + formatList((pdArrayStatus.reasons || []).map(formatBacktestDecision), 'PD Array chain mapped') + ) + ); strategyBacktestPanel.appendChild(cards); strategyBacktestPanel.appendChild(buildBacktestCurve(summary.cumulative_r_curve || [])); @@ -5629,6 +5651,15 @@ function renderStrategyBacktests(payload) { .map((item) => [formatBacktestDecision(item.key), String(item.value)]) ) ); + detailGrid.appendChild( + buildItemCard( + 'PD Array Kernel', + `${payload?.pd_array_reaction_summary?.passed_count ?? 0} mapped / ${payload?.pd_array_reaction_summary?.blocked_count ?? 0} blocked`, + topCountEntries(payload?.pd_array_reaction_summary?.reason_counts || summary.pd_array_reaction_reason_counts || {}) + .slice(0, 5) + .map((item) => [formatBacktestDecision(item.key), String(item.value)]) + ) + ); detailGrid.appendChild( buildItemCard( 'Recent Backtest Runs', diff --git a/src/web/i18n.js b/src/web/i18n.js index 7e8ca8e..e48650f 100644 --- a/src/web/i18n.js +++ b/src/web/i18n.js @@ -82,6 +82,11 @@ 'Liquidity Target': '流动性目标', 'Liquidity Pool': '流动性池', 'Liquidity chain mapped': '流动性链路已映射', + 'PD Array Kernel': 'PD Array 反应内核', + 'PD Array Reason': 'PD Array 原因', + 'PD Array': 'PD Array 位置', + 'PD Array Role': 'PD Array 角色', + 'PD Array chain mapped': 'PD Array 反应链路已映射', 'Mapped': '已映射', 'mapped': '已映射', 'Eligible For Trading Plan Review': '可进入交易计划复核', @@ -762,6 +767,29 @@ ['liquidity_targets_tied_wait', '两侧流动性同权等待'], ['void_without_trigger_pool', 'Void 缺少触发池'], ['reorder_trigger_pending', '等待目标重排证据'], + ['pd_array_mapped', 'PD Array 反应位已映射'], + ['pd_array_reaction_blocked', 'PD Array 反应内核阻塞'], + ['missing_pd_array', '缺少 PD Array 反应位'], + ['missing_pd_array_role', '缺少 PD Array 角色'], + ['missing_array_location', '缺少 PD Array 所在位置'], + ['unranked_pd_array', 'PD Array 未排序'], + ['array_timeframe_unresolved', 'PD Array 周期未明确'], + ['array_freshness_unresolved', 'PD Array 新鲜度未明确'], + ['pd_array_not_direction_source', 'PD Array 不能当方向来源'], + ['missing_fvg_role', '缺少 FVG 角色'], + ['fvg_without_displacement_origin', 'FVG 缺少位移来源'], + ['fvg_is_not_setup', 'FVG 标签不是完整 setup'], + ['awaiting_fvg_retrace_confirmation', '等待 FVG 回撤确认'], + ['awaiting_reaction_quality', '等待到位后的反应质量'], + ['rebalancing_without_structure_response', 'Rebalancing 缺少结构响应'], + ['bpr_without_imbalance_origin', 'BPR 缺少 imbalance 来源'], + ['trade_classification_unresolved', '内部修复/外部扩展未分类'], + ['order_block_not_authorized', 'Order Block 未被授权'], + ['ob_without_displacement', 'Order Block 缺少位移来源'], + ['awaiting_parent_subordinate_alignment', '等待父/子 OB 对齐'], + ['breaker_without_failure_precondition', 'Breaker 缺少失败前提'], + ['mitigation_without_inventory_context', 'Mitigation 缺少库存上下文'], + ['awaiting_breaker_retest_confirmation', '等待 Breaker 回踩确认'], ['missing_playbook', '缺少 playbook'], ['Doctrine Cohort', 'Doctrine 回测分组'], ['Doctrine Cohorts', 'Doctrine 回测分组'], diff --git a/tests/unit/test_api_and_main.py b/tests/unit/test_api_and_main.py index d5522d9..4ea1a5e 100644 --- a/tests/unit/test_api_and_main.py +++ b/tests/unit/test_api_and_main.py @@ -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", diff --git a/tests/unit/test_api_server.py b/tests/unit/test_api_server.py index 6de4b44..a95983b 100644 --- a/tests/unit/test_api_server.py +++ b/tests/unit/test_api_server.py @@ -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", diff --git a/tests/unit/test_backtest_service.py b/tests/unit/test_backtest_service.py index 508f589..48df6e2 100644 --- a/tests/unit/test_backtest_service.py +++ b/tests/unit/test_backtest_service.py @@ -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"}) diff --git a/tests/unit/test_documentation_consistency.py b/tests/unit/test_documentation_consistency.py index 5547b59..ffe1924 100644 --- a/tests/unit/test_documentation_consistency.py +++ b/tests/unit/test_documentation_consistency.py @@ -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", diff --git a/tests/unit/test_ict_doctrine.py b/tests/unit/test_ict_doctrine.py index 8bb4fbc..bdced11 100644 --- a/tests/unit/test_ict_doctrine.py +++ b/tests/unit/test_ict_doctrine.py @@ -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()