Add doctrine time session kernel
This commit is contained in:
+2
-1
@@ -849,10 +849,11 @@ Automation execution-layer PXX remains closed and its known scope stays frozen.
|
|||||||
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
- D5 Time / Session Kernel is complete, deployed, and remote-verified: added `DOCTRINE_SESSION_PHASE=D5_TIME_SESSION_KERNEL`, `TimeSessionRule`, `GET /trading/time-session-kernel`, Strategy Backtest `time_session_backtest_features` / `time_session_kernel_status` / `time_session_kernel_summary`, and `docs/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_2026-05-22_v1.md`. Asia / London / NY AM / NY PM, killzone, Judas/manipulation window, opening range, weekly profile, and news/no-trade time are now measurable session-quality gates; `missing_session_window`, `session_window_unverified`, `outside_session_window`, `session_not_allowed`, `missing_killzone`, `killzone_not_active`, `judas_window_wait`, `opening_range_unresolved`, `weekly_profile_missing`, `news_time_no_trade`, and `session_invalidation_missing` block promotion until timing is explicit. Validation passed focused D5/API/backtest/docs tests, full unit discovery (`846` tests), script static checks (`79` scripts), secret static checks, JS syntax checks, deploy smoke over 11 endpoints, remote ops smoke, read-only live analysis smoke, `/trading/time-session-kernel` field smoke, `/trading/backtests` D5 field smoke, and Playwright screenshot evidence at `output/playwright/d5-time-session-strategy-backtest-1366x768.png`. Remote latest backtest `#513` is correctly blocked by D5 as `study_only / session_window_unverified` instead of treating `ASIA` plus `mark_outside` as a verified session window. No execution/readiness/risk/trust/operator-confirmation gate was changed.
|
||||||
|
|
||||||
## Exact Next Step
|
## 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/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.
|
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/AI_ICT_DoctrineKernel_TimeSessionKernel_D5_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 D6 Model Playbook Kernel: convert the book-derived models into full setup playbooks with required Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target fields and batch-backtest cohorts. 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
|
## Validation Commands Used Recently
|
||||||
|
|
||||||
|
|||||||
@@ -956,7 +956,8 @@
|
|||||||
- [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] 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
|
- [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
|
||||||
- [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
|
- [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] D5 Time / Session Kernel: rebuilt Asia / London / NY AM / NY PM / killzone / Judas window / opening range / weekly profile / news time into measurable session-quality gates instead of loose time labels; Strategy Backtest now exposes `time_session_backtest_features`, `time_session_kernel_status`, `time_session_kernel_summary`, and `/trading/time-session-kernel` without changing execution/readiness gates
|
||||||
|
- [ ] D6 next product gate: rebuild Model Playbook Kernel so book-derived models become full setup playbooks with required Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target fields and batch-backtest cohorts
|
||||||
- [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
|
- [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
|
- [ ] 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
|
- [ ] Long-term target: complete `Real-time Assisted Trading V1` without enabling default live auto-trading
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# AI_ICT Doctrine Kernel Time / Session Kernel D5 2026-05-22 v1
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
D5 rebuilds time and session from loose labels into measurable session-quality gates. This slice follows `docs/ICT交易全书_出版终校版.md` 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
|
||||||
|
|
||||||
|
Asia, London, NY AM, NY PM, killzone, Judas/manipulation window, opening range, weekly profile, and news/no-trade time are not buy/sell signals. They can only qualify or block a setup after the system can state:
|
||||||
|
|
||||||
|
- the exact session code and whether the candidate occurred inside a verifiable session window,
|
||||||
|
- whether the active Trading Plan allows that session,
|
||||||
|
- the killzone name and state,
|
||||||
|
- whether Judas/manipulation is still in a wait-only phase,
|
||||||
|
- whether the opening range reference is resolved,
|
||||||
|
- the weekly/day profile,
|
||||||
|
- whether a news or low-liquidity no-trade window is active,
|
||||||
|
- the session-specific invalidation.
|
||||||
|
|
||||||
|
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_SESSION_PHASE = D5_TIME_SESSION_KERNEL`
|
||||||
|
|
||||||
|
New registry/API:
|
||||||
|
|
||||||
|
- `GET /trading/time-session-kernel`
|
||||||
|
- `build_time_session_kernel_registry`
|
||||||
|
- `list_time_session_rules`
|
||||||
|
|
||||||
|
New backtest surface fields:
|
||||||
|
|
||||||
|
- `time_session_backtest_features`
|
||||||
|
- `time_session_kernel_status`
|
||||||
|
- `time_session_kernel_summary`
|
||||||
|
- `time_session_kernel_status_counts`
|
||||||
|
- `time_session_kernel_reason_counts`
|
||||||
|
- `by_time_session_code`
|
||||||
|
|
||||||
|
## D5 Rules
|
||||||
|
|
||||||
|
1. `session_window_verifiable`
|
||||||
|
Session names are context windows. A setup must prove it occurred inside the allowed session window.
|
||||||
|
|
||||||
|
2. `killzone_reaction_window`
|
||||||
|
Killzone is a timing filter only. It cannot create direction or override missing Range / Draw / Location.
|
||||||
|
|
||||||
|
3. `judas_manipulation_sequence`
|
||||||
|
Judas/manipulation is wait-only until post-manipulation confirmation exists.
|
||||||
|
|
||||||
|
4. `opening_range_delivery_state`
|
||||||
|
Opening range defines early reference and delivery state. Early motion is not enough.
|
||||||
|
|
||||||
|
5. `weekly_profile_alignment`
|
||||||
|
Weekly/day profile must be declared before an intraday setup can be promoted.
|
||||||
|
|
||||||
|
6. `news_time_no_trade_guard`
|
||||||
|
High-impact news and thin-liquidity windows are explicit no-trade guards.
|
||||||
|
|
||||||
|
## Promotion Gate
|
||||||
|
|
||||||
|
The D2 playbook now requires D5 Time / Session fields before promotion:
|
||||||
|
|
||||||
|
- `session_code`
|
||||||
|
- `session_window_state`
|
||||||
|
- `allowed_sessions`
|
||||||
|
- `killzone_name`
|
||||||
|
- `killzone_state`
|
||||||
|
- `judas_window_state`
|
||||||
|
- `opening_range_state`
|
||||||
|
- `weekly_profile`
|
||||||
|
- `news_window_state`
|
||||||
|
- `session_invalidation`
|
||||||
|
|
||||||
|
The Doctrine Backtest Cohort now blocks promotion unless:
|
||||||
|
|
||||||
|
- D2 playbook schema is complete,
|
||||||
|
- D3 Liquidity Kernel passes,
|
||||||
|
- D4 PD Array / Reaction Kernel passes,
|
||||||
|
- D5 Time / Session Kernel passes,
|
||||||
|
- Strategy Backtest quality gate passes,
|
||||||
|
- active Trading Plan gate passes.
|
||||||
|
|
||||||
|
## Read-Only Boundary
|
||||||
|
|
||||||
|
D5 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
|
||||||
|
|
||||||
|
D6 should rebuild the Model Playbook Kernel: convert the book-derived models into full setup playbooks with required Range / Draw / Liquidity / PD Array / Session / Confirmation / Invalidation / Target fields and batch-backtest cohorts.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 54 KiB |
@@ -9,15 +9,18 @@ from src.services.ict_mainline import (
|
|||||||
build_doctrine_backtest_cohort,
|
build_doctrine_backtest_cohort,
|
||||||
build_liquidity_backtest_features,
|
build_liquidity_backtest_features,
|
||||||
build_pd_array_backtest_features,
|
build_pd_array_backtest_features,
|
||||||
|
build_time_session_backtest_features,
|
||||||
build_backtest_quality_gate,
|
build_backtest_quality_gate,
|
||||||
build_ict_rulebook_mapping,
|
build_ict_rulebook_mapping,
|
||||||
evaluate_doctrine_research_status,
|
evaluate_doctrine_research_status,
|
||||||
evaluate_liquidity_kernel_status,
|
evaluate_liquidity_kernel_status,
|
||||||
evaluate_pd_array_reaction_status,
|
evaluate_pd_array_reaction_status,
|
||||||
|
evaluate_time_session_kernel_status,
|
||||||
summarize_doctrine_backtest_cohorts,
|
summarize_doctrine_backtest_cohorts,
|
||||||
summarize_doctrine_rule_gaps,
|
summarize_doctrine_rule_gaps,
|
||||||
summarize_liquidity_kernel_statuses,
|
summarize_liquidity_kernel_statuses,
|
||||||
summarize_pd_array_reaction_statuses,
|
summarize_pd_array_reaction_statuses,
|
||||||
|
summarize_time_session_kernel_statuses,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -41,6 +44,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in
|
|||||||
doctrine_cohort_summary = summarize_doctrine_backtest_cohorts(items)
|
doctrine_cohort_summary = summarize_doctrine_backtest_cohorts(items)
|
||||||
liquidity_kernel_summary = summarize_liquidity_kernel_statuses(items)
|
liquidity_kernel_summary = summarize_liquidity_kernel_statuses(items)
|
||||||
pd_array_reaction_summary = summarize_pd_array_reaction_statuses(items)
|
pd_array_reaction_summary = summarize_pd_array_reaction_statuses(items)
|
||||||
|
time_session_kernel_summary = summarize_time_session_kernel_statuses(items)
|
||||||
return {
|
return {
|
||||||
"instrument_id": instrument_id,
|
"instrument_id": instrument_id,
|
||||||
"item_count": len(items),
|
"item_count": len(items),
|
||||||
@@ -52,6 +56,7 @@ def get_strategy_backtest_surface(instrument_id: Optional[int] = None, limit: in
|
|||||||
"doctrine_cohort_summary": doctrine_cohort_summary,
|
"doctrine_cohort_summary": doctrine_cohort_summary,
|
||||||
"liquidity_kernel_summary": liquidity_kernel_summary,
|
"liquidity_kernel_summary": liquidity_kernel_summary,
|
||||||
"pd_array_reaction_summary": pd_array_reaction_summary,
|
"pd_array_reaction_summary": pd_array_reaction_summary,
|
||||||
|
"time_session_kernel_summary": time_session_kernel_summary,
|
||||||
"ict_rulebook_mapping": ict_rulebook_mapping,
|
"ict_rulebook_mapping": ict_rulebook_mapping,
|
||||||
"items": items,
|
"items": items,
|
||||||
}
|
}
|
||||||
@@ -157,6 +162,27 @@ def normalize_backtest_summary_row(row: dict) -> dict:
|
|||||||
"pd_array_backtest_features": pd_array_backtest_features,
|
"pd_array_backtest_features": pd_array_backtest_features,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
time_session_backtest_features = build_time_session_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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
time_session_kernel_status = evaluate_time_session_kernel_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,
|
||||||
|
"time_session_backtest_features": time_session_backtest_features,
|
||||||
|
}
|
||||||
|
)
|
||||||
doctrine_backtest_cohort = build_doctrine_backtest_cohort(
|
doctrine_backtest_cohort = build_doctrine_backtest_cohort(
|
||||||
{
|
{
|
||||||
"subject_type": row.get("subject_type"),
|
"subject_type": row.get("subject_type"),
|
||||||
@@ -167,6 +193,7 @@ def normalize_backtest_summary_row(row: dict) -> dict:
|
|||||||
"summary": summary,
|
"summary": summary,
|
||||||
"liquidity_backtest_features": liquidity_backtest_features,
|
"liquidity_backtest_features": liquidity_backtest_features,
|
||||||
"pd_array_backtest_features": pd_array_backtest_features,
|
"pd_array_backtest_features": pd_array_backtest_features,
|
||||||
|
"time_session_backtest_features": time_session_backtest_features,
|
||||||
},
|
},
|
||||||
quality_gate=dict(summary.get("quality_gate") or {}),
|
quality_gate=dict(summary.get("quality_gate") or {}),
|
||||||
)
|
)
|
||||||
@@ -181,6 +208,8 @@ def normalize_backtest_summary_row(row: dict) -> dict:
|
|||||||
"liquidity_kernel_status": liquidity_kernel_status,
|
"liquidity_kernel_status": liquidity_kernel_status,
|
||||||
"pd_array_backtest_features": pd_array_backtest_features,
|
"pd_array_backtest_features": pd_array_backtest_features,
|
||||||
"pd_array_reaction_status": pd_array_reaction_status,
|
"pd_array_reaction_status": pd_array_reaction_status,
|
||||||
|
"time_session_backtest_features": time_session_backtest_features,
|
||||||
|
"time_session_kernel_status": time_session_kernel_status,
|
||||||
"doctrine_backtest_cohort": doctrine_backtest_cohort,
|
"doctrine_backtest_cohort": doctrine_backtest_cohort,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +254,13 @@ def _build_operator_summary(latest: Optional[dict]) -> dict:
|
|||||||
status = "pd_array_reaction_blocked"
|
status = "pd_array_reaction_blocked"
|
||||||
decision = "do_not_promote_strategy"
|
decision = "do_not_promote_strategy"
|
||||||
reason = str(pd_array_reasons[0])
|
reason = str(pd_array_reasons[0])
|
||||||
|
time_session_status = dict(latest.get("time_session_kernel_status") or {})
|
||||||
|
if not time_session_status.get("passed"):
|
||||||
|
time_session_reasons = list(time_session_status.get("reasons") or [])
|
||||||
|
if time_session_reasons:
|
||||||
|
status = "time_session_kernel_blocked"
|
||||||
|
decision = "do_not_promote_strategy"
|
||||||
|
reason = str(time_session_reasons[0])
|
||||||
if not (doctrine_cohort.get("promotion_gate") or {}).get("passed"):
|
if not (doctrine_cohort.get("promotion_gate") or {}).get("passed"):
|
||||||
cohort_reasons = list((doctrine_cohort.get("promotion_gate") or {}).get("reasons") or [])
|
cohort_reasons = list((doctrine_cohort.get("promotion_gate") or {}).get("reasons") or [])
|
||||||
if cohort_reasons:
|
if cohort_reasons:
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ from src.services.ict_mainline import (
|
|||||||
build_doctrine_rule_matrix,
|
build_doctrine_rule_matrix,
|
||||||
build_liquidity_kernel_registry,
|
build_liquidity_kernel_registry,
|
||||||
build_pd_array_reaction_registry,
|
build_pd_array_reaction_registry,
|
||||||
|
build_time_session_kernel_registry,
|
||||||
)
|
)
|
||||||
from src.services.execution import ExecutionService
|
from src.services.execution import ExecutionService
|
||||||
from src.services.market_state import start_market_truth_runtime
|
from src.services.market_state import start_market_truth_runtime
|
||||||
@@ -231,6 +232,7 @@ def build_api_dependencies() -> dict:
|
|||||||
"trading_doctrine_playbooks": build_doctrine_playbook_registry,
|
"trading_doctrine_playbooks": build_doctrine_playbook_registry,
|
||||||
"trading_liquidity_kernel": build_liquidity_kernel_registry,
|
"trading_liquidity_kernel": build_liquidity_kernel_registry,
|
||||||
"trading_pd_array_kernel": build_pd_array_reaction_registry,
|
"trading_pd_array_kernel": build_pd_array_reaction_registry,
|
||||||
|
"trading_time_session_kernel": build_time_session_kernel_registry,
|
||||||
"training_cases": get_training_cases,
|
"training_cases": get_training_cases,
|
||||||
"training_case": get_training_case,
|
"training_case": get_training_case,
|
||||||
"training_export_cases": export_training_case_package,
|
"training_export_cases": export_training_case_package,
|
||||||
@@ -860,6 +862,8 @@ def route_request(
|
|||||||
return 200, deps["trading_liquidity_kernel"]()
|
return 200, deps["trading_liquidity_kernel"]()
|
||||||
if method == "GET" and path == "/trading/pd-array-kernel":
|
if method == "GET" and path == "/trading/pd-array-kernel":
|
||||||
return 200, deps["trading_pd_array_kernel"]()
|
return 200, deps["trading_pd_array_kernel"]()
|
||||||
|
if method == "GET" and path == "/trading/time-session-kernel":
|
||||||
|
return 200, deps["trading_time_session_kernel"]()
|
||||||
if method == "GET" and len(path_parts) == 3 and path_parts[:2] == ["trading", "candidates"]:
|
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"))
|
candidate = deps["trading_candidate_by_id"](_parse_path_int(path_parts[2], "candidate_id"))
|
||||||
if candidate is None:
|
if candidate is None:
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ from src.services.ict_mainline import (
|
|||||||
build_doctrine_backtest_cohort,
|
build_doctrine_backtest_cohort,
|
||||||
build_liquidity_backtest_features,
|
build_liquidity_backtest_features,
|
||||||
build_pd_array_backtest_features,
|
build_pd_array_backtest_features,
|
||||||
|
build_time_session_backtest_features,
|
||||||
evaluate_liquidity_kernel_status,
|
evaluate_liquidity_kernel_status,
|
||||||
evaluate_pd_array_reaction_status,
|
evaluate_pd_array_reaction_status,
|
||||||
|
evaluate_time_session_kernel_status,
|
||||||
extract_setup_playbook_id,
|
extract_setup_playbook_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,6 +156,17 @@ class BacktestService:
|
|||||||
'active_trading_plan': active_trading_plan,
|
'active_trading_plan': active_trading_plan,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
time_session_backtest_features = build_time_session_backtest_features(
|
||||||
|
{
|
||||||
|
'subject_type': subject_type,
|
||||||
|
'model_code': 'SWEEP_MSS_FVG',
|
||||||
|
'candidate_model_code': candidate_model_code,
|
||||||
|
'timeframe_set': timeframe_set,
|
||||||
|
'session_codes': list(session_codes or []),
|
||||||
|
'run_metadata': run_metadata,
|
||||||
|
'active_trading_plan': active_trading_plan,
|
||||||
|
}
|
||||||
|
)
|
||||||
doctrine_backtest_cohort = build_doctrine_backtest_cohort(
|
doctrine_backtest_cohort = build_doctrine_backtest_cohort(
|
||||||
{
|
{
|
||||||
'subject_type': subject_type,
|
'subject_type': subject_type,
|
||||||
@@ -163,6 +176,7 @@ class BacktestService:
|
|||||||
'run_metadata': run_metadata,
|
'run_metadata': run_metadata,
|
||||||
'liquidity_backtest_features': liquidity_backtest_features,
|
'liquidity_backtest_features': liquidity_backtest_features,
|
||||||
'pd_array_backtest_features': pd_array_backtest_features,
|
'pd_array_backtest_features': pd_array_backtest_features,
|
||||||
|
'time_session_backtest_features': time_session_backtest_features,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
run_id = self.repository.insert_backtest_run(
|
run_id = self.repository.insert_backtest_run(
|
||||||
@@ -189,6 +203,8 @@ class BacktestService:
|
|||||||
'liquidity_kernel_status': evaluate_liquidity_kernel_status(liquidity_backtest_features),
|
'liquidity_kernel_status': evaluate_liquidity_kernel_status(liquidity_backtest_features),
|
||||||
'pd_array_backtest_features': pd_array_backtest_features,
|
'pd_array_backtest_features': pd_array_backtest_features,
|
||||||
'pd_array_reaction_status': evaluate_pd_array_reaction_status(pd_array_backtest_features),
|
'pd_array_reaction_status': evaluate_pd_array_reaction_status(pd_array_backtest_features),
|
||||||
|
'time_session_backtest_features': time_session_backtest_features,
|
||||||
|
'time_session_kernel_status': evaluate_time_session_kernel_status(time_session_backtest_features),
|
||||||
'doctrine_backtest_cohort': doctrine_backtest_cohort,
|
'doctrine_backtest_cohort': doctrine_backtest_cohort,
|
||||||
'run_metadata': run_metadata,
|
'run_metadata': run_metadata,
|
||||||
},
|
},
|
||||||
@@ -210,10 +226,13 @@ class BacktestService:
|
|||||||
outcome_setup_playbook_id = setup_playbook_id or _first_outcome_meta_value(outcomes, 'setup_playbook_id')
|
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_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
|
outcome_pd_array_features = _first_outcome_meta_value(outcomes, 'pd_array_backtest_features') or pd_array_backtest_features
|
||||||
|
outcome_time_session_features = _first_outcome_meta_value(outcomes, 'time_session_backtest_features') or time_session_backtest_features
|
||||||
summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {})
|
summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {})
|
||||||
summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features'])
|
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_backtest_features'] = dict(outcome_pd_array_features or {})
|
||||||
summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features'])
|
summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features'])
|
||||||
|
summary['time_session_backtest_features'] = dict(outcome_time_session_features or {})
|
||||||
|
summary['time_session_kernel_status'] = evaluate_time_session_kernel_status(summary['time_session_backtest_features'])
|
||||||
summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort(
|
summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort(
|
||||||
{
|
{
|
||||||
'subject_type': subject_type,
|
'subject_type': subject_type,
|
||||||
@@ -224,6 +243,7 @@ class BacktestService:
|
|||||||
'summary': summary,
|
'summary': summary,
|
||||||
'liquidity_backtest_features': summary['liquidity_backtest_features'],
|
'liquidity_backtest_features': summary['liquidity_backtest_features'],
|
||||||
'pd_array_backtest_features': summary['pd_array_backtest_features'],
|
'pd_array_backtest_features': summary['pd_array_backtest_features'],
|
||||||
|
'time_session_backtest_features': summary['time_session_backtest_features'],
|
||||||
},
|
},
|
||||||
quality_gate=summary['quality_gate'],
|
quality_gate=summary['quality_gate'],
|
||||||
)
|
)
|
||||||
@@ -268,6 +288,15 @@ class BacktestService:
|
|||||||
'run_metadata': run_metadata,
|
'run_metadata': run_metadata,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
signal_time_session_features = build_time_session_backtest_features(
|
||||||
|
{
|
||||||
|
'signal': signal,
|
||||||
|
'target_plan': target_plan,
|
||||||
|
'run_metadata': run_metadata,
|
||||||
|
'session_codes': session_codes,
|
||||||
|
'session_filter': 'outside_window' if len(sessions) == 0 else 'covered',
|
||||||
|
}
|
||||||
|
)
|
||||||
error_tags = ['no_future_candles']
|
error_tags = ['no_future_candles']
|
||||||
if len(sessions) == 0:
|
if len(sessions) == 0:
|
||||||
error_tags.append('outside_session_window')
|
error_tags.append('outside_session_window')
|
||||||
@@ -308,6 +337,8 @@ class BacktestService:
|
|||||||
'liquidity_kernel_status': evaluate_liquidity_kernel_status(signal_liquidity_features),
|
'liquidity_kernel_status': evaluate_liquidity_kernel_status(signal_liquidity_features),
|
||||||
'pd_array_backtest_features': signal_pd_array_features,
|
'pd_array_backtest_features': signal_pd_array_features,
|
||||||
'pd_array_reaction_status': evaluate_pd_array_reaction_status(signal_pd_array_features),
|
'pd_array_reaction_status': evaluate_pd_array_reaction_status(signal_pd_array_features),
|
||||||
|
'time_session_backtest_features': signal_time_session_features,
|
||||||
|
'time_session_kernel_status': evaluate_time_session_kernel_status(signal_time_session_features),
|
||||||
'cost_r': 0.0,
|
'cost_r': 0.0,
|
||||||
'cost_model': cost_config.__dict__,
|
'cost_model': cost_config.__dict__,
|
||||||
'backtest_mode': mode,
|
'backtest_mode': mode,
|
||||||
@@ -398,11 +429,21 @@ class BacktestService:
|
|||||||
'run_metadata': run_metadata,
|
'run_metadata': run_metadata,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
'time_session_backtest_features': build_time_session_backtest_features(
|
||||||
|
{
|
||||||
|
'signal': signal,
|
||||||
|
'target_plan': extract_target_plan_from_signal_row(signal),
|
||||||
|
'run_metadata': run_metadata,
|
||||||
|
'session_codes': session_codes,
|
||||||
|
'session_filter': 'outside_window' if session_filtered else 'covered',
|
||||||
|
}
|
||||||
|
),
|
||||||
'weekday': _extract_weekday(signal['created_ts']),
|
'weekday': _extract_weekday(signal['created_ts']),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
record.meta['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(record.meta['liquidity_backtest_features'])
|
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'])
|
record.meta['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(record.meta['pd_array_backtest_features'])
|
||||||
|
record.meta['time_session_kernel_status'] = evaluate_time_session_kernel_status(record.meta['time_session_backtest_features'])
|
||||||
outcomes.append(record)
|
outcomes.append(record)
|
||||||
self.repository.insert_backtest_result(
|
self.repository.insert_backtest_result(
|
||||||
backtest_run_id=run_id,
|
backtest_run_id=run_id,
|
||||||
@@ -423,10 +464,13 @@ class BacktestService:
|
|||||||
summary['quality_gate'] = evaluate_backtest_quality(summary, gate=quality_gate)
|
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_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
|
outcome_pd_array_features = _first_outcome_meta_value(outcomes, 'pd_array_backtest_features') or pd_array_backtest_features
|
||||||
|
outcome_time_session_features = _first_outcome_meta_value(outcomes, 'time_session_backtest_features') or time_session_backtest_features
|
||||||
summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {})
|
summary['liquidity_backtest_features'] = dict(outcome_liquidity_features or {})
|
||||||
summary['liquidity_kernel_status'] = evaluate_liquidity_kernel_status(summary['liquidity_backtest_features'])
|
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_backtest_features'] = dict(outcome_pd_array_features or {})
|
||||||
summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features'])
|
summary['pd_array_reaction_status'] = evaluate_pd_array_reaction_status(summary['pd_array_backtest_features'])
|
||||||
|
summary['time_session_backtest_features'] = dict(outcome_time_session_features or {})
|
||||||
|
summary['time_session_kernel_status'] = evaluate_time_session_kernel_status(summary['time_session_backtest_features'])
|
||||||
summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort(
|
summary['doctrine_backtest_cohort'] = build_doctrine_backtest_cohort(
|
||||||
{
|
{
|
||||||
'subject_type': subject_type,
|
'subject_type': subject_type,
|
||||||
@@ -437,6 +481,7 @@ class BacktestService:
|
|||||||
'summary': summary,
|
'summary': summary,
|
||||||
'liquidity_backtest_features': summary['liquidity_backtest_features'],
|
'liquidity_backtest_features': summary['liquidity_backtest_features'],
|
||||||
'pd_array_backtest_features': summary['pd_array_backtest_features'],
|
'pd_array_backtest_features': summary['pd_array_backtest_features'],
|
||||||
|
'time_session_backtest_features': summary['time_session_backtest_features'],
|
||||||
},
|
},
|
||||||
quality_gate=summary['quality_gate'],
|
quality_gate=summary['quality_gate'],
|
||||||
)
|
)
|
||||||
@@ -1014,6 +1059,19 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
pd_array_reaction_status = evaluate_pd_array_reaction_status(pd_array_backtest_features)
|
pd_array_reaction_status = evaluate_pd_array_reaction_status(pd_array_backtest_features)
|
||||||
|
time_session_backtest_features = build_time_session_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,
|
||||||
|
'session_code': first.get('session_code'),
|
||||||
|
'allowed_sessions': list(first.get('allowed_sessions') or []),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
time_session_kernel_status = evaluate_time_session_kernel_status(time_session_backtest_features)
|
||||||
doctrine_backtest_cohort = build_doctrine_backtest_cohort(
|
doctrine_backtest_cohort = build_doctrine_backtest_cohort(
|
||||||
{
|
{
|
||||||
'setup_playbook_id': setup_playbook_id,
|
'setup_playbook_id': setup_playbook_id,
|
||||||
@@ -1024,6 +1082,7 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]:
|
|||||||
},
|
},
|
||||||
'liquidity_backtest_features': liquidity_backtest_features,
|
'liquidity_backtest_features': liquidity_backtest_features,
|
||||||
'pd_array_backtest_features': pd_array_backtest_features,
|
'pd_array_backtest_features': pd_array_backtest_features,
|
||||||
|
'time_session_backtest_features': time_session_backtest_features,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
units.append(
|
units.append(
|
||||||
@@ -1036,6 +1095,8 @@ def build_plan_replay_units(candidate_rows: list[dict]) -> list[dict]:
|
|||||||
'liquidity_kernel_status': liquidity_kernel_status,
|
'liquidity_kernel_status': liquidity_kernel_status,
|
||||||
'pd_array_backtest_features': pd_array_backtest_features,
|
'pd_array_backtest_features': pd_array_backtest_features,
|
||||||
'pd_array_reaction_status': pd_array_reaction_status,
|
'pd_array_reaction_status': pd_array_reaction_status,
|
||||||
|
'time_session_backtest_features': time_session_backtest_features,
|
||||||
|
'time_session_kernel_status': time_session_kernel_status,
|
||||||
'candidate_model_code': first.get('candidate_model_code'),
|
'candidate_model_code': first.get('candidate_model_code'),
|
||||||
'side': first.get('side'),
|
'side': first.get('side'),
|
||||||
'session_code': first.get('session_code'),
|
'session_code': first.get('session_code'),
|
||||||
@@ -1120,6 +1181,15 @@ def build_plan_replay_meta(
|
|||||||
mode: str,
|
mode: str,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
session_codes = [row.get('session_code') for row in sessions if row.get('session_code')] or ['OFF_HOURS']
|
session_codes = [row.get('session_code') for row in sessions if row.get('session_code')] or ['OFF_HOURS']
|
||||||
|
time_session_backtest_features = build_time_session_backtest_features(
|
||||||
|
{
|
||||||
|
'unit': unit,
|
||||||
|
'time_session_backtest_features': dict(unit.get('time_session_backtest_features') or {}),
|
||||||
|
'session_codes': session_codes,
|
||||||
|
'session_filter': 'outside_window' if len(sessions) == 0 else 'covered',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
time_session_kernel_status = evaluate_time_session_kernel_status(time_session_backtest_features)
|
||||||
return {
|
return {
|
||||||
'subject_type': BACKTEST_SUBJECT_PLAN_REPLAY,
|
'subject_type': BACKTEST_SUBJECT_PLAN_REPLAY,
|
||||||
'trading_plan_id': unit.get('trading_plan_id'),
|
'trading_plan_id': unit.get('trading_plan_id'),
|
||||||
@@ -1131,6 +1201,8 @@ def build_plan_replay_meta(
|
|||||||
'liquidity_kernel_status': dict(unit.get('liquidity_kernel_status') 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_backtest_features': dict(unit.get('pd_array_backtest_features') or {}),
|
||||||
'pd_array_reaction_status': dict(unit.get('pd_array_reaction_status') or {}),
|
'pd_array_reaction_status': dict(unit.get('pd_array_reaction_status') or {}),
|
||||||
|
'time_session_backtest_features': time_session_backtest_features,
|
||||||
|
'time_session_kernel_status': time_session_kernel_status,
|
||||||
'plan_activated_ts': _normalize_meta_ts(unit.get('plan_activated_ts')),
|
'plan_activated_ts': _normalize_meta_ts(unit.get('plan_activated_ts')),
|
||||||
'plan_created_ts': _normalize_meta_ts(unit.get('plan_created_ts')),
|
'plan_created_ts': _normalize_meta_ts(unit.get('plan_created_ts')),
|
||||||
'candidate_id': unit.get('candidate_id'),
|
'candidate_id': unit.get('candidate_id'),
|
||||||
@@ -1319,6 +1391,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s
|
|||||||
grouped_by_doctrine_cohort: dict[str, dict] = {}
|
grouped_by_doctrine_cohort: dict[str, dict] = {}
|
||||||
grouped_by_liquidity_target_side: dict[str, dict] = {}
|
grouped_by_liquidity_target_side: dict[str, dict] = {}
|
||||||
grouped_by_pd_array_type: dict[str, dict] = {}
|
grouped_by_pd_array_type: dict[str, dict] = {}
|
||||||
|
grouped_by_time_session_code: dict[str, dict] = {}
|
||||||
r_multiples: list[float] = []
|
r_multiples: list[float] = []
|
||||||
cost_values: list[float] = []
|
cost_values: list[float] = []
|
||||||
mfe_r_values: list[float] = []
|
mfe_r_values: list[float] = []
|
||||||
@@ -1333,6 +1406,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s
|
|||||||
liquidity_kernel_status_counts: dict[str, int] = {}
|
liquidity_kernel_status_counts: dict[str, int] = {}
|
||||||
pd_array_reaction_reason_counts: dict[str, int] = {}
|
pd_array_reaction_reason_counts: dict[str, int] = {}
|
||||||
pd_array_reaction_status_counts: dict[str, int] = {}
|
pd_array_reaction_status_counts: dict[str, int] = {}
|
||||||
|
time_session_kernel_reason_counts: dict[str, int] = {}
|
||||||
|
time_session_kernel_status_counts: dict[str, int] = {}
|
||||||
plan_total_r: dict[int, float] = {}
|
plan_total_r: dict[int, float] = {}
|
||||||
plan_seen: set[int] = set()
|
plan_seen: set[int] = set()
|
||||||
|
|
||||||
@@ -1382,6 +1457,13 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s
|
|||||||
pd_array_reaction_status_counts[pd_array_status_key] = pd_array_reaction_status_counts.get(pd_array_status_key, 0) + 1
|
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 []):
|
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
|
pd_array_reaction_reason_counts[str(reason)] = pd_array_reaction_reason_counts.get(str(reason), 0) + 1
|
||||||
|
time_session_features = dict(outcome.meta.get('time_session_backtest_features') or {})
|
||||||
|
time_session_code = str(time_session_features.get('session_code') or (session_codes[0] if session_codes else '') or 'missing_session_window')
|
||||||
|
time_session_status = dict(outcome.meta.get('time_session_kernel_status') or evaluate_time_session_kernel_status(time_session_features))
|
||||||
|
time_session_status_key = str(time_session_status.get('status') or 'unknown')
|
||||||
|
time_session_kernel_status_counts[time_session_status_key] = time_session_kernel_status_counts.get(time_session_status_key, 0) + 1
|
||||||
|
for reason in list(time_session_status.get('reasons') or []):
|
||||||
|
time_session_kernel_reason_counts[str(reason)] = time_session_kernel_reason_counts.get(str(reason), 0) + 1
|
||||||
r_multiples.append(float(outcome.meta.get('r_multiple') or 0.0))
|
r_multiples.append(float(outcome.meta.get('r_multiple') or 0.0))
|
||||||
cost_values.append(float(outcome.meta.get('cost_r') 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))
|
mfe_r_values.append(float(outcome.meta.get('mfe_r') or 0.0))
|
||||||
@@ -1394,6 +1476,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s
|
|||||||
_accumulate_bucket(grouped_by_doctrine_cohort, doctrine_cohort_key, 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_liquidity_target_side, liquidity_target_side, outcome)
|
||||||
_accumulate_bucket(grouped_by_pd_array_type, pd_array_type, outcome)
|
_accumulate_bucket(grouped_by_pd_array_type, pd_array_type, outcome)
|
||||||
|
_accumulate_bucket(grouped_by_time_session_code, time_session_code, outcome)
|
||||||
for session_code in session_codes:
|
for session_code in session_codes:
|
||||||
_accumulate_bucket(grouped_by_session, str(session_code), outcome)
|
_accumulate_bucket(grouped_by_session, str(session_code), outcome)
|
||||||
|
|
||||||
@@ -1428,6 +1511,8 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s
|
|||||||
'liquidity_kernel_reason_counts': liquidity_kernel_reason_counts,
|
'liquidity_kernel_reason_counts': liquidity_kernel_reason_counts,
|
||||||
'pd_array_reaction_status_counts': pd_array_reaction_status_counts,
|
'pd_array_reaction_status_counts': pd_array_reaction_status_counts,
|
||||||
'pd_array_reaction_reason_counts': pd_array_reaction_reason_counts,
|
'pd_array_reaction_reason_counts': pd_array_reaction_reason_counts,
|
||||||
|
'time_session_kernel_status_counts': time_session_kernel_status_counts,
|
||||||
|
'time_session_kernel_reason_counts': time_session_kernel_reason_counts,
|
||||||
'avg_mfe': _average([outcome.mfe for outcome in outcomes]),
|
'avg_mfe': _average([outcome.mfe for outcome in outcomes]),
|
||||||
'avg_mae': _average([outcome.mae for outcome in outcomes]),
|
'avg_mae': _average([outcome.mae for outcome in outcomes]),
|
||||||
'avg_mfe_r': _average(mfe_r_values),
|
'avg_mfe_r': _average(mfe_r_values),
|
||||||
@@ -1448,6 +1533,7 @@ def summarize_backtest_outcomes(outcomes: list[BacktestOutcome], subject_type: s
|
|||||||
'by_doctrine_cohort': grouped_by_doctrine_cohort,
|
'by_doctrine_cohort': grouped_by_doctrine_cohort,
|
||||||
'by_liquidity_target_side': grouped_by_liquidity_target_side,
|
'by_liquidity_target_side': grouped_by_liquidity_target_side,
|
||||||
'by_pd_array_type': grouped_by_pd_array_type,
|
'by_pd_array_type': grouped_by_pd_array_type,
|
||||||
|
'by_time_session_code': grouped_by_time_session_code,
|
||||||
}
|
}
|
||||||
result['session_performance'] = result['by_session']
|
result['session_performance'] = result['by_session']
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -22,27 +22,33 @@ from src.services.ict_mainline.doctrine import (
|
|||||||
DOCTRINE_LIQUIDITY_PHASE,
|
DOCTRINE_LIQUIDITY_PHASE,
|
||||||
DOCTRINE_PLAYBOOK_PHASE,
|
DOCTRINE_PLAYBOOK_PHASE,
|
||||||
DOCTRINE_REACTION_PHASE,
|
DOCTRINE_REACTION_PHASE,
|
||||||
|
DOCTRINE_SESSION_PHASE,
|
||||||
build_doctrine_backtest_cohort,
|
build_doctrine_backtest_cohort,
|
||||||
build_liquidity_backtest_features,
|
build_liquidity_backtest_features,
|
||||||
build_pd_array_backtest_features,
|
build_pd_array_backtest_features,
|
||||||
|
build_time_session_backtest_features,
|
||||||
build_doctrine_rule_matrix,
|
build_doctrine_rule_matrix,
|
||||||
build_doctrine_playbook_gate,
|
build_doctrine_playbook_gate,
|
||||||
build_doctrine_playbook_registry,
|
build_doctrine_playbook_registry,
|
||||||
build_liquidity_kernel_registry,
|
build_liquidity_kernel_registry,
|
||||||
build_pd_array_reaction_registry,
|
build_pd_array_reaction_registry,
|
||||||
|
build_time_session_kernel_registry,
|
||||||
evaluate_doctrine_research_status,
|
evaluate_doctrine_research_status,
|
||||||
evaluate_liquidity_kernel_status,
|
evaluate_liquidity_kernel_status,
|
||||||
evaluate_pd_array_reaction_status,
|
evaluate_pd_array_reaction_status,
|
||||||
|
evaluate_time_session_kernel_status,
|
||||||
extract_setup_playbook_id,
|
extract_setup_playbook_id,
|
||||||
get_doctrine_setup_playbook,
|
get_doctrine_setup_playbook,
|
||||||
list_doctrine_setup_playbooks,
|
list_doctrine_setup_playbooks,
|
||||||
list_doctrine_rules,
|
list_doctrine_rules,
|
||||||
list_liquidity_rules,
|
list_liquidity_rules,
|
||||||
list_pd_array_rules,
|
list_pd_array_rules,
|
||||||
|
list_time_session_rules,
|
||||||
summarize_doctrine_backtest_cohorts,
|
summarize_doctrine_backtest_cohorts,
|
||||||
summarize_doctrine_rule_gaps,
|
summarize_doctrine_rule_gaps,
|
||||||
summarize_liquidity_kernel_statuses,
|
summarize_liquidity_kernel_statuses,
|
||||||
summarize_pd_array_reaction_statuses,
|
summarize_pd_array_reaction_statuses,
|
||||||
|
summarize_time_session_kernel_statuses,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -50,15 +56,18 @@ __all__ = [
|
|||||||
"DOCTRINE_LIQUIDITY_PHASE",
|
"DOCTRINE_LIQUIDITY_PHASE",
|
||||||
"DOCTRINE_PLAYBOOK_PHASE",
|
"DOCTRINE_PLAYBOOK_PHASE",
|
||||||
"DOCTRINE_REACTION_PHASE",
|
"DOCTRINE_REACTION_PHASE",
|
||||||
|
"DOCTRINE_SESSION_PHASE",
|
||||||
"ICT_MAINLINE_CHAIN",
|
"ICT_MAINLINE_CHAIN",
|
||||||
"build_doctrine_backtest_cohort",
|
"build_doctrine_backtest_cohort",
|
||||||
"build_liquidity_backtest_features",
|
"build_liquidity_backtest_features",
|
||||||
"build_pd_array_backtest_features",
|
"build_pd_array_backtest_features",
|
||||||
|
"build_time_session_backtest_features",
|
||||||
"build_doctrine_rule_matrix",
|
"build_doctrine_rule_matrix",
|
||||||
"build_doctrine_playbook_gate",
|
"build_doctrine_playbook_gate",
|
||||||
"build_doctrine_playbook_registry",
|
"build_doctrine_playbook_registry",
|
||||||
"build_liquidity_kernel_registry",
|
"build_liquidity_kernel_registry",
|
||||||
"build_pd_array_reaction_registry",
|
"build_pd_array_reaction_registry",
|
||||||
|
"build_time_session_kernel_registry",
|
||||||
"build_ict_rulebook_mapping",
|
"build_ict_rulebook_mapping",
|
||||||
"build_ict_rulebook_summary",
|
"build_ict_rulebook_summary",
|
||||||
"build_backtest_quality_gate",
|
"build_backtest_quality_gate",
|
||||||
@@ -71,6 +80,7 @@ __all__ = [
|
|||||||
"evaluate_doctrine_research_status",
|
"evaluate_doctrine_research_status",
|
||||||
"evaluate_liquidity_kernel_status",
|
"evaluate_liquidity_kernel_status",
|
||||||
"evaluate_pd_array_reaction_status",
|
"evaluate_pd_array_reaction_status",
|
||||||
|
"evaluate_time_session_kernel_status",
|
||||||
"extract_setup_playbook_id",
|
"extract_setup_playbook_id",
|
||||||
"get_ict_concept",
|
"get_ict_concept",
|
||||||
"get_doctrine_setup_playbook",
|
"get_doctrine_setup_playbook",
|
||||||
@@ -78,10 +88,12 @@ __all__ = [
|
|||||||
"list_doctrine_rules",
|
"list_doctrine_rules",
|
||||||
"list_liquidity_rules",
|
"list_liquidity_rules",
|
||||||
"list_pd_array_rules",
|
"list_pd_array_rules",
|
||||||
|
"list_time_session_rules",
|
||||||
"list_ict_concepts",
|
"list_ict_concepts",
|
||||||
"summarize_doctrine_backtest_cohorts",
|
"summarize_doctrine_backtest_cohorts",
|
||||||
"summarize_doctrine_rule_gaps",
|
"summarize_doctrine_rule_gaps",
|
||||||
"summarize_liquidity_kernel_statuses",
|
"summarize_liquidity_kernel_statuses",
|
||||||
"summarize_pd_array_reaction_statuses",
|
"summarize_pd_array_reaction_statuses",
|
||||||
|
"summarize_time_session_kernel_statuses",
|
||||||
"summarize_book_rule_errors",
|
"summarize_book_rule_errors",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ DOCTRINE_KERNEL_PHASE = "D1_MARKET_LANGUAGE_KERNEL"
|
|||||||
DOCTRINE_PLAYBOOK_PHASE = "D2_PLAYBOOK_BACKTEST_COHORT"
|
DOCTRINE_PLAYBOOK_PHASE = "D2_PLAYBOOK_BACKTEST_COHORT"
|
||||||
DOCTRINE_LIQUIDITY_PHASE = "D3_LIQUIDITY_KERNEL"
|
DOCTRINE_LIQUIDITY_PHASE = "D3_LIQUIDITY_KERNEL"
|
||||||
DOCTRINE_REACTION_PHASE = "D4_PD_ARRAY_REACTION_KERNEL"
|
DOCTRINE_REACTION_PHASE = "D4_PD_ARRAY_REACTION_KERNEL"
|
||||||
|
DOCTRINE_SESSION_PHASE = "D5_TIME_SESSION_KERNEL"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -134,6 +135,34 @@ class PdArrayReactionRule:
|
|||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TimeSessionRule:
|
||||||
|
rule_id: str
|
||||||
|
stage: str
|
||||||
|
book_anchor: str
|
||||||
|
session_scope: 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, ...] = (
|
D1_DOCTRINE_RULES: tuple[DoctrineRule, ...] = (
|
||||||
DoctrineRule(
|
DoctrineRule(
|
||||||
rule_id="range_defined",
|
rule_id="range_defined",
|
||||||
@@ -307,8 +336,25 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = (
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
session_timing_rule={
|
session_timing_rule={
|
||||||
"required_fields": ("session_code", "allowed_sessions", "path_context", "sequence_validity"),
|
"required_fields": (
|
||||||
"operator_rule": "Only London/NY timing defined by the active plan can produce a review candidate.",
|
"session_code",
|
||||||
|
"session_window_state",
|
||||||
|
"allowed_sessions",
|
||||||
|
"killzone_name",
|
||||||
|
"killzone_state",
|
||||||
|
"judas_window_state",
|
||||||
|
"opening_range_state",
|
||||||
|
"weekly_profile",
|
||||||
|
"news_window_state",
|
||||||
|
"session_invalidation",
|
||||||
|
"path_context",
|
||||||
|
"sequence_validity",
|
||||||
|
),
|
||||||
|
"ranking_rule_id": "session_window_verifiable",
|
||||||
|
"operator_rule": (
|
||||||
|
"Only a verifiable London/NY session window can produce review; killzone, Judas/manipulation, "
|
||||||
|
"opening range, weekly profile, and news/no-trade time must be explicit."
|
||||||
|
),
|
||||||
},
|
},
|
||||||
confirmation_rule={
|
confirmation_rule={
|
||||||
"required_fields": ("confirmation_state", "missing_confirmations", "reason_codes"),
|
"required_fields": ("confirmation_state", "missing_confirmations", "reason_codes"),
|
||||||
@@ -336,6 +382,17 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = (
|
|||||||
"pd_array_not_direction_source",
|
"pd_array_not_direction_source",
|
||||||
"fvg_is_not_setup",
|
"fvg_is_not_setup",
|
||||||
"doctrine_missing_time_path",
|
"doctrine_missing_time_path",
|
||||||
|
"missing_session_window",
|
||||||
|
"session_window_unverified",
|
||||||
|
"outside_session_window",
|
||||||
|
"session_not_allowed",
|
||||||
|
"missing_killzone",
|
||||||
|
"killzone_not_active",
|
||||||
|
"judas_window_wait",
|
||||||
|
"opening_range_unresolved",
|
||||||
|
"weekly_profile_missing",
|
||||||
|
"news_time_no_trade",
|
||||||
|
"session_invalidation_missing",
|
||||||
"doctrine_missing_invalidation",
|
"doctrine_missing_invalidation",
|
||||||
"doctrine_missing_exit_architecture",
|
"doctrine_missing_exit_architecture",
|
||||||
"backtest_quality_gate_failed",
|
"backtest_quality_gate_failed",
|
||||||
@@ -375,6 +432,10 @@ D2_DOCTRINE_SETUP_PLAYBOOKS: tuple[DoctrineSetupPlaybook, ...] = (
|
|||||||
"traded_without_pd_array_context",
|
"traded_without_pd_array_context",
|
||||||
"traded_unvalidated_order_block",
|
"traded_unvalidated_order_block",
|
||||||
"traded_without_time_or_path",
|
"traded_without_time_or_path",
|
||||||
|
"traded_outside_session_window",
|
||||||
|
"traded_judas_as_signal",
|
||||||
|
"traded_without_weekly_profile",
|
||||||
|
"traded_through_news_window",
|
||||||
"traded_without_invalidation",
|
"traded_without_invalidation",
|
||||||
"no_exit_architecture",
|
"no_exit_architecture",
|
||||||
),
|
),
|
||||||
@@ -647,6 +708,94 @@ D4_PD_ARRAY_REACTION_RULES: tuple[PdArrayReactionRule, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
D5_TIME_SESSION_RULES: tuple[TimeSessionRule, ...] = (
|
||||||
|
TimeSessionRule(
|
||||||
|
rule_id="session_window_verifiable",
|
||||||
|
stage="Session Window",
|
||||||
|
book_anchor="ICT trading book Time / Session model: Asia, London, NY AM, and NY PM are context windows, not signals",
|
||||||
|
session_scope="ASIA/LONDON/NY_AM/NY_PM",
|
||||||
|
ranking_weight=100,
|
||||||
|
required_inputs=("session_code", "session_window_state", "allowed_sessions", "session_invalidation"),
|
||||||
|
measurable_fields=("session_code", "session_window_state", "session_start_ts", "session_end_ts", "allowed_sessions", "session_invalidation"),
|
||||||
|
no_trade_when=("missing_session_window", "session_window_unverified", "outside_session_window", "session_not_allowed"),
|
||||||
|
wait_when=(),
|
||||||
|
backtest_observable="setup_without_verifiable_session_window",
|
||||||
|
operator_rule="A setup must name the active session and prove that the candidate occurred inside the allowed session window.",
|
||||||
|
aliases=("session_code", "session_window_state", "session_filter", "session_start_ts", "session_end_ts", "allowed_sessions"),
|
||||||
|
),
|
||||||
|
TimeSessionRule(
|
||||||
|
rule_id="killzone_reaction_window",
|
||||||
|
stage="Killzone",
|
||||||
|
book_anchor="ICT trading book Time / Session model: killzone is a timing filter after context is known",
|
||||||
|
session_scope="London/NY killzone",
|
||||||
|
ranking_weight=90,
|
||||||
|
required_inputs=("killzone_name", "killzone_state", "killzone_start_ts", "killzone_end_ts"),
|
||||||
|
measurable_fields=("killzone_name", "killzone_state", "killzone_start_ts", "killzone_end_ts"),
|
||||||
|
no_trade_when=("missing_killzone",),
|
||||||
|
wait_when=("killzone_not_active",),
|
||||||
|
backtest_observable="setup_without_active_killzone_window",
|
||||||
|
operator_rule="Killzone can only qualify timing; it cannot create direction or override missing Range/Draw/Location.",
|
||||||
|
aliases=("killzone", "kill_zone", "killzone_name", "killzone_state"),
|
||||||
|
),
|
||||||
|
TimeSessionRule(
|
||||||
|
rule_id="judas_manipulation_sequence",
|
||||||
|
stage="Judas / Manipulation",
|
||||||
|
book_anchor="ICT trading book Time / Session model: Judas/manipulation is inducement first and confirmation later",
|
||||||
|
session_scope="London/NY manipulation window",
|
||||||
|
ranking_weight=85,
|
||||||
|
required_inputs=("judas_window_state", "manipulation_window_state", "inducement_side", "post_manipulation_confirmation"),
|
||||||
|
measurable_fields=("judas_window_state", "manipulation_window_state", "inducement_side", "post_manipulation_confirmation"),
|
||||||
|
no_trade_when=("judas_traded_as_signal",),
|
||||||
|
wait_when=("judas_window_wait",),
|
||||||
|
backtest_observable="trade_attempt_during_manipulation_window",
|
||||||
|
operator_rule="Judas/manipulation is a wait condition until sweep, structure, and session transition evidence are present.",
|
||||||
|
aliases=("judas_window_state", "manipulation_window_state", "inducement_state", "post_manipulation_confirmation"),
|
||||||
|
),
|
||||||
|
TimeSessionRule(
|
||||||
|
rule_id="opening_range_delivery_state",
|
||||||
|
stage="Opening Range",
|
||||||
|
book_anchor="ICT trading book Time / Session model: opening range defines early reference and delivery state",
|
||||||
|
session_scope="session opening range",
|
||||||
|
ranking_weight=75,
|
||||||
|
required_inputs=("opening_range_state", "opening_range_id", "opening_range_high", "opening_range_low"),
|
||||||
|
measurable_fields=("opening_range_state", "opening_range_id", "opening_range_high", "opening_range_low", "opening_range_reaction"),
|
||||||
|
no_trade_when=("opening_range_invalid",),
|
||||||
|
wait_when=("opening_range_unresolved",),
|
||||||
|
backtest_observable="setup_before_opening_range_resolved",
|
||||||
|
operator_rule="Do not treat early-session motion as a setup until the opening range reference and reaction state are explicit.",
|
||||||
|
aliases=("opening_range_state", "opening_range_id", "opening_range_high", "opening_range_low", "or_high", "or_low"),
|
||||||
|
),
|
||||||
|
TimeSessionRule(
|
||||||
|
rule_id="weekly_profile_alignment",
|
||||||
|
stage="Weekly Profile",
|
||||||
|
book_anchor="ICT trading book Time / Session model: day/week profile conditions the intraday model",
|
||||||
|
session_scope="weekly profile / day model",
|
||||||
|
ranking_weight=70,
|
||||||
|
required_inputs=("weekly_profile", "weekly_bias", "day_of_week", "weekly_range_state"),
|
||||||
|
measurable_fields=("weekly_profile", "weekly_bias", "day_of_week", "weekly_range_state"),
|
||||||
|
no_trade_when=("weekly_profile_missing",),
|
||||||
|
wait_when=("weekly_profile_unresolved",),
|
||||||
|
backtest_observable="setup_without_weekly_profile_alignment",
|
||||||
|
operator_rule="A session setup must declare the weekly/day profile or stay study-only.",
|
||||||
|
aliases=("weekly_profile", "weekly_bias", "day_of_week", "weekly_range_state", "day_model", "week_model"),
|
||||||
|
),
|
||||||
|
TimeSessionRule(
|
||||||
|
rule_id="news_time_no_trade_guard",
|
||||||
|
stage="News / No-Trade Time",
|
||||||
|
book_anchor="ICT trading book Time / Session model: high-impact news and thin-liquidity windows are explicit no-trade guards",
|
||||||
|
session_scope="news / low-liquidity window",
|
||||||
|
ranking_weight=95,
|
||||||
|
required_inputs=("news_window_state", "high_impact_news", "low_liquidity_window"),
|
||||||
|
measurable_fields=("news_window_state", "high_impact_news", "low_liquidity_window", "news_event_id"),
|
||||||
|
no_trade_when=("news_time_no_trade",),
|
||||||
|
wait_when=("news_window_clearing_wait",),
|
||||||
|
backtest_observable="setup_attempted_inside_news_no_trade_window",
|
||||||
|
operator_rule="When news/no-trade time is active, freeze promotion and wait for the window to clear.",
|
||||||
|
aliases=("news_window_state", "high_impact_news", "low_liquidity_window", "news_event_id", "red_folder_news"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_doctrine_rules() -> list[dict]:
|
def list_doctrine_rules() -> list[dict]:
|
||||||
return [rule.to_public_dict() for rule in D1_DOCTRINE_RULES]
|
return [rule.to_public_dict() for rule in D1_DOCTRINE_RULES]
|
||||||
|
|
||||||
@@ -693,6 +842,10 @@ def list_pd_array_rules() -> list[dict]:
|
|||||||
return [rule.to_public_dict() for rule in D4_PD_ARRAY_REACTION_RULES]
|
return [rule.to_public_dict() for rule in D4_PD_ARRAY_REACTION_RULES]
|
||||||
|
|
||||||
|
|
||||||
|
def list_time_session_rules() -> list[dict]:
|
||||||
|
return [rule.to_public_dict() for rule in D5_TIME_SESSION_RULES]
|
||||||
|
|
||||||
|
|
||||||
def build_liquidity_kernel_registry() -> dict:
|
def build_liquidity_kernel_registry() -> dict:
|
||||||
return {
|
return {
|
||||||
"phase": DOCTRINE_LIQUIDITY_PHASE,
|
"phase": DOCTRINE_LIQUIDITY_PHASE,
|
||||||
@@ -739,6 +892,29 @@ def build_pd_array_reaction_registry() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_time_session_kernel_registry() -> dict:
|
||||||
|
return {
|
||||||
|
"phase": DOCTRINE_SESSION_PHASE,
|
||||||
|
"source": "docs/ICT交易全书_出版终校版.md Time / Session model chapters",
|
||||||
|
"status": "active",
|
||||||
|
"rule_count": len(D5_TIME_SESSION_RULES),
|
||||||
|
"hard_rules": [
|
||||||
|
"Asia / London / NY AM / NY PM are context windows, not entry signals.",
|
||||||
|
"Killzone is a timing filter only; it cannot override missing Range/Draw/Location/Liquidity.",
|
||||||
|
"Judas / manipulation window is wait-only until post-manipulation confirmation exists.",
|
||||||
|
"Opening range must be defined before early-session motion is used as evidence.",
|
||||||
|
"Weekly profile and news/no-trade windows must be explicit before promotion.",
|
||||||
|
],
|
||||||
|
"required_backtest_feature_fields": _dedupe(
|
||||||
|
field
|
||||||
|
for rule in D5_TIME_SESSION_RULES
|
||||||
|
for field in rule.measurable_fields
|
||||||
|
),
|
||||||
|
"rules": list_time_session_rules(),
|
||||||
|
"execution_boundary": "read_only_analysis_no_order_submission",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_doctrine_setup_playbook(setup_playbook_id: str | None) -> dict | None:
|
def get_doctrine_setup_playbook(setup_playbook_id: str | None) -> dict | None:
|
||||||
key = str(setup_playbook_id or "").strip()
|
key = str(setup_playbook_id or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
@@ -824,6 +1000,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None
|
|||||||
playbook_gate = build_doctrine_playbook_gate(payload)
|
playbook_gate = build_doctrine_playbook_gate(payload)
|
||||||
liquidity_status = evaluate_liquidity_kernel_status(payload)
|
liquidity_status = evaluate_liquidity_kernel_status(payload)
|
||||||
pd_array_status = evaluate_pd_array_reaction_status(payload)
|
pd_array_status = evaluate_pd_array_reaction_status(payload)
|
||||||
|
time_session_status = evaluate_time_session_kernel_status(payload)
|
||||||
quality = dict(quality_gate or _extract_quality_gate(payload) or {})
|
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_reasons = [str(item) for item in list(quality.get("reasons") or []) if item]
|
||||||
quality_passed = bool(quality.get("passed"))
|
quality_passed = bool(quality.get("passed"))
|
||||||
@@ -838,6 +1015,7 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None
|
|||||||
*[str(item) for item in list(playbook_gate.get("reasons") or []) if item],
|
*[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 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 pd_array_status.get("passed") else [str(item) for item in list(pd_array_status.get("reasons") or []) if item]),
|
||||||
|
*([] if time_session_status.get("passed") else [str(item) for item in list(time_session_status.get("reasons") or []) if item]),
|
||||||
*([] if quality_passed else quality_reasons),
|
*([] if quality_passed else quality_reasons),
|
||||||
]
|
]
|
||||||
promotion_reasons = _dedupe(promotion_reasons)
|
promotion_reasons = _dedupe(promotion_reasons)
|
||||||
@@ -855,12 +1033,15 @@ def build_doctrine_backtest_cohort(payload: object, *, quality_gate: dict | None
|
|||||||
"liquidity_backtest_features": dict(liquidity_status.get("features") or {}),
|
"liquidity_backtest_features": dict(liquidity_status.get("features") or {}),
|
||||||
"pd_array_reaction_status": pd_array_status,
|
"pd_array_reaction_status": pd_array_status,
|
||||||
"pd_array_backtest_features": dict(pd_array_status.get("features") or {}),
|
"pd_array_backtest_features": dict(pd_array_status.get("features") or {}),
|
||||||
|
"time_session_kernel_status": time_session_status,
|
||||||
|
"time_session_backtest_features": dict(time_session_status.get("features") or {}),
|
||||||
"promotion_gate": {
|
"promotion_gate": {
|
||||||
"passed": not promotion_reasons,
|
"passed": not promotion_reasons,
|
||||||
"reasons": promotion_reasons,
|
"reasons": promotion_reasons,
|
||||||
"requires_playbook_schema_complete": True,
|
"requires_playbook_schema_complete": True,
|
||||||
"requires_liquidity_kernel_complete": True,
|
"requires_liquidity_kernel_complete": True,
|
||||||
"requires_pd_array_reaction_complete": True,
|
"requires_pd_array_reaction_complete": True,
|
||||||
|
"requires_time_session_kernel_complete": True,
|
||||||
"requires_backtest_quality_gate": True,
|
"requires_backtest_quality_gate": True,
|
||||||
"requires_active_trading_plan": True,
|
"requires_active_trading_plan": True,
|
||||||
"execution_boundary": "read_only_analysis_no_order_submission",
|
"execution_boundary": "read_only_analysis_no_order_submission",
|
||||||
@@ -1282,6 +1463,194 @@ def summarize_pd_array_reaction_statuses(items: Iterable[dict]) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_time_session_backtest_features(payload: object) -> dict:
|
||||||
|
session_codes = _as_string_list(
|
||||||
|
_extract_first_present_value(payload, keys=("session_codes", "allowed_session_codes"))
|
||||||
|
)
|
||||||
|
allowed_sessions = _as_string_list(
|
||||||
|
_extract_first_present_value(payload, keys=("allowed_sessions", "plan_allowed_sessions"))
|
||||||
|
)
|
||||||
|
session_code = _string_or_empty(_extract_first_present_value(payload, keys=("session_code",)))
|
||||||
|
if not session_code:
|
||||||
|
session_value = _extract_first_present_value(payload, keys=("session",))
|
||||||
|
if not isinstance(session_value, (dict, list, tuple)):
|
||||||
|
session_code = _string_or_empty(session_value)
|
||||||
|
if not session_code and session_codes:
|
||||||
|
session_code = session_codes[0]
|
||||||
|
raw_window_state = _string_or_empty(
|
||||||
|
_extract_first_present_value(
|
||||||
|
payload,
|
||||||
|
keys=("session_window_state", "session_filter", "session_status", "window_state"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session_filter = _string_or_empty(_extract_first_present_value(payload, keys=("session_filter",)))
|
||||||
|
session_window_state = _normalize_session_window_state(
|
||||||
|
raw_window_state or session_filter,
|
||||||
|
session_code=session_code,
|
||||||
|
)
|
||||||
|
features = {
|
||||||
|
"session_code": session_code,
|
||||||
|
"session_codes": session_codes,
|
||||||
|
"allowed_sessions": allowed_sessions,
|
||||||
|
"session_window_state": session_window_state,
|
||||||
|
"session_filter": session_filter,
|
||||||
|
"session_start_ts": _extract_first_present_value(payload, keys=("session_start_ts", "window_start_ts", "session_open_ts")),
|
||||||
|
"session_end_ts": _extract_first_present_value(payload, keys=("session_end_ts", "window_end_ts", "session_close_ts")),
|
||||||
|
"killzone_name": _string_or_empty(
|
||||||
|
_extract_first_present_value(payload, keys=("killzone_name", "killzone", "kill_zone", "killzone_code"))
|
||||||
|
),
|
||||||
|
"killzone_state": _string_or_empty(
|
||||||
|
_extract_first_present_value(payload, keys=("killzone_state", "kill_zone_state", "killzone_status"))
|
||||||
|
),
|
||||||
|
"killzone_start_ts": _extract_first_present_value(payload, keys=("killzone_start_ts", "kill_zone_start_ts")),
|
||||||
|
"killzone_end_ts": _extract_first_present_value(payload, keys=("killzone_end_ts", "kill_zone_end_ts")),
|
||||||
|
"judas_window_state": _string_or_empty(
|
||||||
|
_extract_first_present_value(
|
||||||
|
payload,
|
||||||
|
keys=("judas_window_state", "judas_state", "manipulation_window_state", "inducement_state"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"inducement_side": _string_or_empty(_extract_first_present_value(payload, keys=("inducement_side", "used_side"))),
|
||||||
|
"post_manipulation_confirmation": _extract_first_present_value(
|
||||||
|
payload,
|
||||||
|
keys=("post_manipulation_confirmation", "post_judas_confirmation", "post_take_reaction", "structure_response"),
|
||||||
|
),
|
||||||
|
"opening_range_state": _string_or_empty(
|
||||||
|
_extract_first_present_value(payload, keys=("opening_range_state", "opening_range_status", "or_state"))
|
||||||
|
),
|
||||||
|
"opening_range_id": _string_or_empty(_extract_first_present_value(payload, keys=("opening_range_id", "or_id"))),
|
||||||
|
"opening_range_high": _extract_first_present_value(payload, keys=("opening_range_high", "or_high")),
|
||||||
|
"opening_range_low": _extract_first_present_value(payload, keys=("opening_range_low", "or_low")),
|
||||||
|
"opening_range_reaction": _string_or_empty(
|
||||||
|
_extract_first_present_value(payload, keys=("opening_range_reaction", "or_reaction", "opening_range_response"))
|
||||||
|
),
|
||||||
|
"weekly_profile": _string_or_empty(_extract_first_present_value(payload, keys=("weekly_profile", "week_model", "day_model"))),
|
||||||
|
"weekly_bias": _string_or_empty(_extract_first_present_value(payload, keys=("weekly_bias", "week_bias", "day_bias"))),
|
||||||
|
"day_of_week": _string_or_empty(_extract_first_present_value(payload, keys=("day_of_week", "weekday"))),
|
||||||
|
"weekly_range_state": _string_or_empty(
|
||||||
|
_extract_first_present_value(payload, keys=("weekly_range_state", "week_range_state"))
|
||||||
|
),
|
||||||
|
"news_window_state": _string_or_empty(
|
||||||
|
_extract_first_present_value(payload, keys=("news_window_state", "news_state", "event_window_state"))
|
||||||
|
),
|
||||||
|
"high_impact_news": _extract_first_present_value(payload, keys=("high_impact_news", "red_folder_news")),
|
||||||
|
"low_liquidity_window": _extract_first_present_value(payload, keys=("low_liquidity_window", "thin_liquidity_window")),
|
||||||
|
"news_event_id": _string_or_empty(_extract_first_present_value(payload, keys=("news_event_id", "event_id"))),
|
||||||
|
"session_invalidation": _string_or_empty(
|
||||||
|
_extract_first_present_value(
|
||||||
|
payload,
|
||||||
|
keys=("session_invalidation", "session_invalidation_rule", "session_invalidates_at", "invalidation"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"created_ts": _extract_first_present_value(payload, keys=("created_ts", "signal_created_ts", "candidate_created_ts")),
|
||||||
|
"expires_ts": _extract_first_present_value(payload, keys=("expires_ts", "expiry_ts", "session_expires_ts")),
|
||||||
|
}
|
||||||
|
features["has_session_window"] = bool(features["session_code"] and features["session_window_state"])
|
||||||
|
features["session_allowed"] = _session_value_allowed(features["session_code"], allowed_sessions)
|
||||||
|
features["killzone_active"] = _session_state_is_active(features["killzone_state"])
|
||||||
|
features["opening_range_resolved"] = _opening_range_resolved(features["opening_range_state"])
|
||||||
|
features["news_no_trade_active"] = _news_window_active(features)
|
||||||
|
return features
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_time_session_kernel_status(payload: object) -> dict:
|
||||||
|
features = build_time_session_backtest_features(payload)
|
||||||
|
no_trade_reasons: list[str] = []
|
||||||
|
wait_reasons: list[str] = []
|
||||||
|
session_code = str(features.get("session_code") or "").strip()
|
||||||
|
|
||||||
|
if not session_code or _session_state_is_missing(session_code):
|
||||||
|
no_trade_reasons.append("missing_session_window")
|
||||||
|
elif not features.get("session_window_state"):
|
||||||
|
no_trade_reasons.append("session_window_unverified")
|
||||||
|
elif _session_state_is_outside(session_code) or _session_state_is_outside(str(features.get("session_window_state") or "")):
|
||||||
|
no_trade_reasons.append("outside_session_window")
|
||||||
|
if not features["session_allowed"]:
|
||||||
|
no_trade_reasons.append("session_not_allowed")
|
||||||
|
|
||||||
|
if not features["killzone_name"]:
|
||||||
|
wait_reasons.append("missing_killzone")
|
||||||
|
elif not features["killzone_active"]:
|
||||||
|
wait_reasons.append("killzone_not_active")
|
||||||
|
|
||||||
|
judas_state = str(features["judas_window_state"] or "").lower()
|
||||||
|
if judas_state in {"active", "judas", "manipulation", "inducement", "conditioning", "pre_confirmation", "building"}:
|
||||||
|
wait_reasons.append("judas_window_wait")
|
||||||
|
|
||||||
|
if not features["opening_range_resolved"]:
|
||||||
|
wait_reasons.append("opening_range_unresolved")
|
||||||
|
|
||||||
|
if not features["weekly_profile"] and not features["weekly_bias"]:
|
||||||
|
no_trade_reasons.append("weekly_profile_missing")
|
||||||
|
|
||||||
|
if features["news_no_trade_active"]:
|
||||||
|
no_trade_reasons.append("news_time_no_trade")
|
||||||
|
|
||||||
|
if not features["session_invalidation"]:
|
||||||
|
no_trade_reasons.append("session_invalidation_missing")
|
||||||
|
|
||||||
|
reasons = _dedupe([*no_trade_reasons, *wait_reasons])
|
||||||
|
passed = not reasons
|
||||||
|
if passed:
|
||||||
|
status = "time_session_mapped"
|
||||||
|
elif no_trade_reasons:
|
||||||
|
status = "study_only"
|
||||||
|
else:
|
||||||
|
status = "wait"
|
||||||
|
return {
|
||||||
|
"phase": DOCTRINE_SESSION_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": (
|
||||||
|
"Session, killzone, opening range, weekly profile, news guard, and session invalidation are mapped."
|
||||||
|
if passed
|
||||||
|
else "Treat timing as study-only/wait until the session window, killzone, opening range, weekly profile, news guard, and session invalidation are explicit."
|
||||||
|
),
|
||||||
|
"execution_boundary": "read_only_analysis_no_order_submission",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_time_session_kernel_statuses(items: Iterable[dict]) -> dict:
|
||||||
|
reason_counts: dict[str, int] = {}
|
||||||
|
status_counts: dict[str, int] = {}
|
||||||
|
session_code_counts: dict[str, int] = {}
|
||||||
|
total = 0
|
||||||
|
passed_count = 0
|
||||||
|
for item in items:
|
||||||
|
total += 1
|
||||||
|
status = dict(item.get("time_session_kernel_status") or evaluate_time_session_kernel_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 {})
|
||||||
|
session_code = str(features.get("session_code") or "missing_session_window")
|
||||||
|
session_code_counts[session_code] = int(session_code_counts.get(session_code) 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_SESSION_PHASE,
|
||||||
|
"item_count": total,
|
||||||
|
"passed_count": passed_count,
|
||||||
|
"blocked_count": max(0, total - passed_count),
|
||||||
|
"status_counts": status_counts,
|
||||||
|
"reason_counts": reason_counts,
|
||||||
|
"session_code_counts": session_code_counts,
|
||||||
|
"operator_summary": (
|
||||||
|
"No backtest items available for Time / Session Kernel review."
|
||||||
|
if not total
|
||||||
|
else "Review Time / Session blockers before treating a session label as usable setup evidence."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def evaluate_doctrine_research_status(
|
def evaluate_doctrine_research_status(
|
||||||
payload: object,
|
payload: object,
|
||||||
*,
|
*,
|
||||||
@@ -1486,6 +1855,89 @@ def _dedupe(values: Iterable[str]) -> list[str]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _as_string_list(value: object) -> list[str]:
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if isinstance(value, (list, tuple, set)):
|
||||||
|
raw_items = value
|
||||||
|
else:
|
||||||
|
raw_items = [value]
|
||||||
|
result: list[str] = []
|
||||||
|
for item in raw_items:
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
text = str(item).strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
result.append(text)
|
||||||
|
return _dedupe(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_session_window_state(value: object, *, session_code: str = "") -> str:
|
||||||
|
text = str(value or "").strip().lower()
|
||||||
|
code = str(session_code or "").strip().lower()
|
||||||
|
if text in {"covered", "inside", "active", "open", "in_session", "in_session_window", "session_window_mapped"}:
|
||||||
|
return "inside"
|
||||||
|
if text in {"mark_outside", "outside_marking_policy"}:
|
||||||
|
return ""
|
||||||
|
if text in {"outside", "outside_window", "off_hours", "closed", "session_mismatch"}:
|
||||||
|
return "outside"
|
||||||
|
if code in {"off_hours", "outside", "n/a", "na", "none", "missing"}:
|
||||||
|
return "outside"
|
||||||
|
return str(value or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _session_value_allowed(session_code: str, allowed_sessions: list[str]) -> bool:
|
||||||
|
if not allowed_sessions:
|
||||||
|
return True
|
||||||
|
code = str(session_code or "").strip().lower()
|
||||||
|
allowed = {str(item or "").strip().lower() for item in allowed_sessions if str(item or "").strip()}
|
||||||
|
return "all" in allowed or code in allowed
|
||||||
|
|
||||||
|
|
||||||
|
def _session_state_is_missing(value: str) -> bool:
|
||||||
|
return str(value or "").strip().lower() in {"", "n/a", "na", "none", "null", "missing", "unknown"}
|
||||||
|
|
||||||
|
|
||||||
|
def _session_state_is_outside(value: str) -> bool:
|
||||||
|
return str(value or "").strip().lower() in {"off_hours", "outside", "outside_window", "closed", "session_mismatch"}
|
||||||
|
|
||||||
|
|
||||||
|
def _session_state_is_active(value: str) -> bool:
|
||||||
|
return str(value or "").strip().lower() in {
|
||||||
|
"active",
|
||||||
|
"inside",
|
||||||
|
"open",
|
||||||
|
"confirmed",
|
||||||
|
"mapped",
|
||||||
|
"in_session",
|
||||||
|
"in_killzone",
|
||||||
|
"killzone_active",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _opening_range_resolved(value: str) -> bool:
|
||||||
|
return str(value or "").strip().lower() in {
|
||||||
|
"defined",
|
||||||
|
"resolved",
|
||||||
|
"complete",
|
||||||
|
"completed",
|
||||||
|
"confirmed",
|
||||||
|
"after_opening_range",
|
||||||
|
"post_opening_range",
|
||||||
|
"range_set",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _news_window_active(features: dict) -> bool:
|
||||||
|
text = str(features.get("news_window_state") or "").strip().lower()
|
||||||
|
if text in {"active", "blocked", "high_impact", "red_folder", "pre_news", "post_news", "low_liquidity", "thin_liquidity"}:
|
||||||
|
return True
|
||||||
|
if _truthy_feature(features.get("high_impact_news")):
|
||||||
|
return True
|
||||||
|
return _truthy_feature(features.get("low_liquidity_window"))
|
||||||
|
|
||||||
|
|
||||||
def _string_or_empty(value: object) -> str:
|
def _string_or_empty(value: object) -> str:
|
||||||
if value is None:
|
if value is None:
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -5550,6 +5550,17 @@ function renderStrategyBacktests(payload) {
|
|||||||
doctrineCohort.pd_array_backtest_features ||
|
doctrineCohort.pd_array_backtest_features ||
|
||||||
pdArrayStatus.features ||
|
pdArrayStatus.features ||
|
||||||
{};
|
{};
|
||||||
|
const timeSessionStatus =
|
||||||
|
latest.time_session_kernel_status ||
|
||||||
|
summary.time_session_kernel_status ||
|
||||||
|
doctrineCohort.time_session_kernel_status ||
|
||||||
|
{};
|
||||||
|
const timeSessionFeatures =
|
||||||
|
latest.time_session_backtest_features ||
|
||||||
|
summary.time_session_backtest_features ||
|
||||||
|
doctrineCohort.time_session_backtest_features ||
|
||||||
|
timeSessionStatus.features ||
|
||||||
|
{};
|
||||||
|
|
||||||
strategyBacktestPanel.appendChild(
|
strategyBacktestPanel.appendChild(
|
||||||
buildInfoGrid([
|
buildInfoGrid([
|
||||||
@@ -5568,6 +5579,10 @@ function renderStrategyBacktests(payload) {
|
|||||||
['PD Array Reason', formatBacktestDecision(pdArrayStatus.reason || 'missing_pd_array')],
|
['PD Array Reason', formatBacktestDecision(pdArrayStatus.reason || 'missing_pd_array')],
|
||||||
['PD Array', pdArrayFeatures.pd_array_type || 'missing_pd_array'],
|
['PD Array', pdArrayFeatures.pd_array_type || 'missing_pd_array'],
|
||||||
['PD Array Role', pdArrayFeatures.pd_array_role || 'missing_pd_array_role'],
|
['PD Array Role', pdArrayFeatures.pd_array_role || 'missing_pd_array_role'],
|
||||||
|
['Time / Session Kernel', formatBacktestDecision(timeSessionStatus.status || 'study_only')],
|
||||||
|
['Time / Session Reason', formatBacktestDecision(timeSessionStatus.reason || 'missing_session_window')],
|
||||||
|
['Session', timeSessionFeatures.session_code || 'missing_session_window'],
|
||||||
|
['Killzone', timeSessionFeatures.killzone_name || 'missing_killzone'],
|
||||||
['Promotion Gate', doctrinePromotionGate.passed ? 'Passed' : 'Blocked'],
|
['Promotion Gate', doctrinePromotionGate.passed ? 'Passed' : 'Blocked'],
|
||||||
['Instrument ID', latest.instrument_id ?? 'n/a'],
|
['Instrument ID', latest.instrument_id ?? 'n/a'],
|
||||||
['Timeframe', formatJsonInline(latest.timeframe_set || {})],
|
['Timeframe', formatJsonInline(latest.timeframe_set || {})],
|
||||||
@@ -5610,6 +5625,13 @@ function renderStrategyBacktests(payload) {
|
|||||||
formatList((pdArrayStatus.reasons || []).map(formatBacktestDecision), 'PD Array chain mapped')
|
formatList((pdArrayStatus.reasons || []).map(formatBacktestDecision), 'PD Array chain mapped')
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
cards.appendChild(
|
||||||
|
buildMetricCard(
|
||||||
|
'Time / Session Kernel',
|
||||||
|
timeSessionStatus.passed ? 'Mapped' : 'Blocked',
|
||||||
|
formatList((timeSessionStatus.reasons || []).map(formatBacktestDecision), 'Time / Session chain mapped')
|
||||||
|
)
|
||||||
|
);
|
||||||
strategyBacktestPanel.appendChild(cards);
|
strategyBacktestPanel.appendChild(cards);
|
||||||
|
|
||||||
strategyBacktestPanel.appendChild(buildBacktestCurve(summary.cumulative_r_curve || []));
|
strategyBacktestPanel.appendChild(buildBacktestCurve(summary.cumulative_r_curve || []));
|
||||||
@@ -5660,6 +5682,15 @@ function renderStrategyBacktests(payload) {
|
|||||||
.map((item) => [formatBacktestDecision(item.key), String(item.value)])
|
.map((item) => [formatBacktestDecision(item.key), String(item.value)])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
detailGrid.appendChild(
|
||||||
|
buildItemCard(
|
||||||
|
'Time / Session Kernel',
|
||||||
|
`${payload?.time_session_kernel_summary?.passed_count ?? 0} mapped / ${payload?.time_session_kernel_summary?.blocked_count ?? 0} blocked`,
|
||||||
|
topCountEntries(payload?.time_session_kernel_summary?.reason_counts || summary.time_session_kernel_reason_counts || {})
|
||||||
|
.slice(0, 5)
|
||||||
|
.map((item) => [formatBacktestDecision(item.key), String(item.value)])
|
||||||
|
)
|
||||||
|
);
|
||||||
detailGrid.appendChild(
|
detailGrid.appendChild(
|
||||||
buildItemCard(
|
buildItemCard(
|
||||||
'Recent Backtest Runs',
|
'Recent Backtest Runs',
|
||||||
|
|||||||
@@ -87,6 +87,10 @@
|
|||||||
'PD Array': 'PD Array 位置',
|
'PD Array': 'PD Array 位置',
|
||||||
'PD Array Role': 'PD Array 角色',
|
'PD Array Role': 'PD Array 角色',
|
||||||
'PD Array chain mapped': 'PD Array 反应链路已映射',
|
'PD Array chain mapped': 'PD Array 反应链路已映射',
|
||||||
|
'Time / Session Kernel': '时间 / 时段内核',
|
||||||
|
'Time / Session Reason': '时间 / 时段原因',
|
||||||
|
'Time / Session chain mapped': '时间 / 时段链路已映射',
|
||||||
|
'Killzone': 'Killzone 时段',
|
||||||
'Mapped': '已映射',
|
'Mapped': '已映射',
|
||||||
'mapped': '已映射',
|
'mapped': '已映射',
|
||||||
'Eligible For Trading Plan Review': '可进入交易计划复核',
|
'Eligible For Trading Plan Review': '可进入交易计划复核',
|
||||||
@@ -790,6 +794,19 @@
|
|||||||
['breaker_without_failure_precondition', 'Breaker 缺少失败前提'],
|
['breaker_without_failure_precondition', 'Breaker 缺少失败前提'],
|
||||||
['mitigation_without_inventory_context', 'Mitigation 缺少库存上下文'],
|
['mitigation_without_inventory_context', 'Mitigation 缺少库存上下文'],
|
||||||
['awaiting_breaker_retest_confirmation', '等待 Breaker 回踩确认'],
|
['awaiting_breaker_retest_confirmation', '等待 Breaker 回踩确认'],
|
||||||
|
['time_session_mapped', '时间 / 时段链路已映射'],
|
||||||
|
['time_session_kernel_blocked', '时间 / 时段内核阻塞'],
|
||||||
|
['missing_session_window', '缺少可验证交易时段窗口'],
|
||||||
|
['session_window_unverified', '交易时段窗口未验证'],
|
||||||
|
['outside_session_window', '不在允许交易时段'],
|
||||||
|
['session_not_allowed', '当前时段不在计划允许范围'],
|
||||||
|
['missing_killzone', '缺少 Killzone 时段'],
|
||||||
|
['killzone_not_active', 'Killzone 未激活'],
|
||||||
|
['judas_window_wait', 'Judas / 操纵窗口等待中'],
|
||||||
|
['opening_range_unresolved', '开盘区间未确认'],
|
||||||
|
['weekly_profile_missing', '缺少周模型 / 日模型'],
|
||||||
|
['news_time_no_trade', '新闻 / 禁交易时间'],
|
||||||
|
['session_invalidation_missing', '缺少时段失效条件'],
|
||||||
['missing_playbook', '缺少 playbook'],
|
['missing_playbook', '缺少 playbook'],
|
||||||
['Doctrine Cohort', 'Doctrine 回测分组'],
|
['Doctrine Cohort', 'Doctrine 回测分组'],
|
||||||
['Doctrine Cohorts', 'Doctrine 回测分组'],
|
['Doctrine Cohorts', 'Doctrine 回测分组'],
|
||||||
@@ -800,6 +817,10 @@
|
|||||||
['Liquidity Target', '流动性目标'],
|
['Liquidity Target', '流动性目标'],
|
||||||
['Liquidity Pool', '流动性池'],
|
['Liquidity Pool', '流动性池'],
|
||||||
['Liquidity chain mapped', '流动性链路已映射'],
|
['Liquidity chain mapped', '流动性链路已映射'],
|
||||||
|
['Time / Session Kernel', '时间 / 时段内核'],
|
||||||
|
['Time / Session Reason', '时间 / 时段原因'],
|
||||||
|
['Time / Session chain mapped', '时间 / 时段链路已映射'],
|
||||||
|
['Killzone', 'Killzone 时段'],
|
||||||
['Promotion Gate', '晋级门'],
|
['Promotion Gate', '晋级门'],
|
||||||
['complete', '完整'],
|
['complete', '完整'],
|
||||||
['incomplete', '不完整'],
|
['incomplete', '不完整'],
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ class ApiTests(unittest.TestCase):
|
|||||||
self.assertIn("missing_liquidity_target", result[0]["liquidity_kernel_status"]["reasons"])
|
self.assertIn("missing_liquidity_target", result[0]["liquidity_kernel_status"]["reasons"])
|
||||||
self.assertEqual(result[0]["pd_array_reaction_status"]["status"], "study_only")
|
self.assertEqual(result[0]["pd_array_reaction_status"]["status"], "study_only")
|
||||||
self.assertIn("missing_pd_array", result[0]["pd_array_reaction_status"]["reasons"])
|
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:
|
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}}}}
|
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.assertIn("missing_liquidity_target", result["liquidity_kernel_summary"]["reason_counts"])
|
||||||
self.assertEqual(result["pd_array_reaction_summary"]["item_count"], 1)
|
self.assertEqual(result["pd_array_reaction_summary"]["item_count"], 1)
|
||||||
self.assertIn("missing_pd_array", result["pd_array_reaction_summary"]["reason_counts"])
|
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:
|
def test_route_request_trading_doctrine_rules_returns_d1_matrix(self) -> None:
|
||||||
status, payload = route_request(
|
status, payload = route_request(
|
||||||
@@ -190,6 +194,22 @@ class ApiTests(unittest.TestCase):
|
|||||||
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
||||||
self.assertEqual(payload["rules"][0]["rule_id"], "pd_array_relevance_matrix")
|
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:
|
def test_route_request_trading_candidates_returns_items_payload(self) -> None:
|
||||||
status, payload = route_request(
|
status, payload = route_request(
|
||||||
"/trading/candidates",
|
"/trading/candidates",
|
||||||
|
|||||||
@@ -150,6 +150,22 @@ class ApiServerRoutingTests(unittest.TestCase):
|
|||||||
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
self.assertEqual(payload["phase"], "D4_PD_ARRAY_REACTION_KERNEL")
|
||||||
self.assertEqual(payload["rule_count"], 5)
|
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:
|
def test_routes_trading_alert_quality_summary(self) -> None:
|
||||||
status, payload = route_request(
|
status, payload = route_request(
|
||||||
"/trading/alerts/quality-summary",
|
"/trading/alerts/quality-summary",
|
||||||
|
|||||||
@@ -321,6 +321,9 @@ class BacktestOutcomeTests(unittest.TestCase):
|
|||||||
self.assertEqual(summary["pd_array_reaction_status_counts"]["study_only"], 4)
|
self.assertEqual(summary["pd_array_reaction_status_counts"]["study_only"], 4)
|
||||||
self.assertEqual(summary["pd_array_reaction_reason_counts"]["missing_pd_array"], 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_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"]["max_consecutive_losses"], 2)
|
||||||
self.assertEqual(summary["by_session"]["LONDON"]["executed"], 3)
|
self.assertEqual(summary["by_session"]["LONDON"]["executed"], 3)
|
||||||
self.assertAlmostEqual(summary["by_session"]["LONDON"]["executed_expectancy_r"], 0.2 / 3)
|
self.assertAlmostEqual(summary["by_session"]["LONDON"]["executed_expectancy_r"], 0.2 / 3)
|
||||||
@@ -418,6 +421,8 @@ class BacktestOutcomeTests(unittest.TestCase):
|
|||||||
"entry": {},
|
"entry": {},
|
||||||
"candidate_meta": {},
|
"candidate_meta": {},
|
||||||
"candidate_status": "awaiting_confirmation",
|
"candidate_status": "awaiting_confirmation",
|
||||||
|
"session_code": "LONDON",
|
||||||
|
"allowed_sessions": ["LONDON"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"candidate_id": 2,
|
"candidate_id": 2,
|
||||||
@@ -472,6 +477,22 @@ class BacktestOutcomeTests(unittest.TestCase):
|
|||||||
"management_rules": [{"code": "tp1_partial", "fraction": 0.5}],
|
"management_rules": [{"code": "tp1_partial", "fraction": 0.5}],
|
||||||
},
|
},
|
||||||
"candidate_status": "executable",
|
"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]["liquidity_kernel_status"]["status"], "liquidity_mapped")
|
||||||
self.assertEqual(units[0]["pd_array_backtest_features"]["pd_array_type"], "fvg")
|
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]["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):
|
class BacktestServiceRunTests(unittest.TestCase):
|
||||||
@@ -739,6 +762,7 @@ class BacktestServiceRunTests(unittest.TestCase):
|
|||||||
"entry": {},
|
"entry": {},
|
||||||
"invalidations": [],
|
"invalidations": [],
|
||||||
"candidate_meta": {},
|
"candidate_meta": {},
|
||||||
|
"session_code": "LONDON",
|
||||||
"allowed_sessions": ["LONDON"],
|
"allowed_sessions": ["LONDON"],
|
||||||
"primary_draw": {"side": "buy_side"},
|
"primary_draw": {"side": "buy_side"},
|
||||||
},
|
},
|
||||||
@@ -786,6 +810,8 @@ class BacktestServiceRunTests(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
"invalidations": [],
|
"invalidations": [],
|
||||||
|
"session_code": "LONDON",
|
||||||
|
"session_window_state": "inside",
|
||||||
"candidate_meta": {
|
"candidate_meta": {
|
||||||
"management_rules": [
|
"management_rules": [
|
||||||
{"code": "tp1_partial", "fraction": 0.5},
|
{"code": "tp1_partial", "fraction": 0.5},
|
||||||
@@ -793,6 +819,19 @@ class BacktestServiceRunTests(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"allowed_sessions": ["LONDON"],
|
"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"},
|
"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["pd_array_reaction_status"]["status"], "pd_array_mapped")
|
||||||
self.assertEqual(repository.summary["by_pd_array_type"]["fvg"]["count"], 1)
|
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:
|
def test_normalize_timeframe_set_and_bucket_helpers(self) -> None:
|
||||||
normalized = normalize_timeframe_set({"bias_tf": "1h", "setup_tf": "15m"})
|
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_PlaybookBacktestCohort_D2_2026-05-21_v1.md",
|
||||||
"AI_ICT_DoctrineKernel_LiquidityKernel_D3_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_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_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md",
|
"AI_ICT_实盘辅助分析决策助手ICT主线开发方案_2026-05-21_v1.md",
|
||||||
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
|
"AI_ICT_主线裁剪审计_S1_2026-05-21_v1.md",
|
||||||
|
|||||||
@@ -5,19 +5,24 @@ from src.services.ict_mainline import (
|
|||||||
DOCTRINE_LIQUIDITY_PHASE,
|
DOCTRINE_LIQUIDITY_PHASE,
|
||||||
DOCTRINE_PLAYBOOK_PHASE,
|
DOCTRINE_PLAYBOOK_PHASE,
|
||||||
DOCTRINE_REACTION_PHASE,
|
DOCTRINE_REACTION_PHASE,
|
||||||
|
DOCTRINE_SESSION_PHASE,
|
||||||
build_doctrine_backtest_cohort,
|
build_doctrine_backtest_cohort,
|
||||||
build_liquidity_backtest_features,
|
build_liquidity_backtest_features,
|
||||||
build_pd_array_backtest_features,
|
build_pd_array_backtest_features,
|
||||||
|
build_time_session_backtest_features,
|
||||||
build_doctrine_playbook_gate,
|
build_doctrine_playbook_gate,
|
||||||
build_doctrine_playbook_registry,
|
build_doctrine_playbook_registry,
|
||||||
build_doctrine_rule_matrix,
|
build_doctrine_rule_matrix,
|
||||||
build_liquidity_kernel_registry,
|
build_liquidity_kernel_registry,
|
||||||
build_pd_array_reaction_registry,
|
build_pd_array_reaction_registry,
|
||||||
|
build_time_session_kernel_registry,
|
||||||
evaluate_doctrine_research_status,
|
evaluate_doctrine_research_status,
|
||||||
evaluate_liquidity_kernel_status,
|
evaluate_liquidity_kernel_status,
|
||||||
evaluate_pd_array_reaction_status,
|
evaluate_pd_array_reaction_status,
|
||||||
|
evaluate_time_session_kernel_status,
|
||||||
summarize_liquidity_kernel_statuses,
|
summarize_liquidity_kernel_statuses,
|
||||||
summarize_pd_array_reaction_statuses,
|
summarize_pd_array_reaction_statuses,
|
||||||
|
summarize_time_session_kernel_statuses,
|
||||||
summarize_doctrine_rule_gaps,
|
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("post_take_reaction", playbook["liquidity_event_rule"]["required_fields"])
|
||||||
self.assertIn("pd_array_role", playbook["pd_array_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("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"])
|
self.assertTrue(playbook["promotion_gate"]["requires_backtest_quality_gate"])
|
||||||
|
|
||||||
def test_d3_liquidity_kernel_registry_exposes_target_ranking_rules(self) -> None:
|
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.assertGreater(summary["reason_counts"]["fvg_is_not_setup"], 0)
|
||||||
self.assertEqual(summary["array_type_counts"]["fvg"], 2)
|
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:
|
def test_playbook_gate_blocks_missing_and_passes_registered_schema(self) -> None:
|
||||||
missing = build_doctrine_playbook_gate({"model_code": "SWEEP_MSS_FVG"})
|
missing = build_doctrine_playbook_gate({"model_code": "SWEEP_MSS_FVG"})
|
||||||
|
|
||||||
@@ -251,6 +302,7 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
|||||||
"sweep_confirmed": True,
|
"sweep_confirmed": True,
|
||||||
"post_take_reaction": "reclaim_displacement",
|
"post_take_reaction": "reclaim_displacement",
|
||||||
**_complete_pd_array_payload(),
|
**_complete_pd_array_payload(),
|
||||||
|
**_complete_time_session_payload(),
|
||||||
"summary": {"quality_gate": {"passed": True, "reasons": []}},
|
"summary": {"quality_gate": {"passed": True, "reasons": []}},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -260,6 +312,7 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
|||||||
self.assertTrue(cohort["promotion_gate"]["passed"])
|
self.assertTrue(cohort["promotion_gate"]["passed"])
|
||||||
self.assertEqual(cohort["liquidity_kernel_status"]["status"], "liquidity_mapped")
|
self.assertEqual(cohort["liquidity_kernel_status"]["status"], "liquidity_mapped")
|
||||||
self.assertEqual(cohort["pd_array_reaction_status"]["status"], "pd_array_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"})
|
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_doctrine_setup_playbook", missing["promotion_gate"]["reasons"])
|
||||||
self.assertIn("missing_liquidity_target", 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_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:
|
def test_liquidity_backtest_features_extract_nested_candidate_fields(self) -> None:
|
||||||
features = build_liquidity_backtest_features(
|
features = build_liquidity_backtest_features(
|
||||||
@@ -310,6 +364,27 @@ class IctDoctrineKernelTests(unittest.TestCase):
|
|||||||
self.assertEqual(features["pd_array_rank"], "primary")
|
self.assertEqual(features["pd_array_rank"], "primary")
|
||||||
self.assertEqual(features["array_location"], "discount")
|
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:
|
def _complete_pd_array_payload() -> dict:
|
||||||
return {
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user