Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions config/phase_i5f_intraday_liquidity_diagnostics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Phase I5f — intraday execution liquidity diagnostics (REPORT-ONLY, NOT research).
#
# Same intraday tail-rebalance smoke as I5a/I5b (decision 14:50, execute at the
# first valid 1min close in [14:51, 14:56:59], exec-to-exec holding returns,
# PIT-safe I3 score, raw stk_limit execution-time feasibility ON) PLUS an opt-in,
# REPORT-ONLY execution liquidity diagnostic. For each desired rebalance trade
# (|target_weight - current_weight|) it sizes the trade against the SELECTED
# execution-minute 1min bar's traded amount (RMB) at max_participation_rate and
# reports a capacity ratio. It NEVER changes fills, can_buy/can_sell, blocked
# reasons, target weights, achieved holdings, turnover, cost, or NAV — the backtest
# numbers are identical to running this config with the diagnostic disabled.
#
# Minute bars are read from the EXISTING intraday cache only (read-only; a miss is a
# hard blocker -> zero stk_mins live calls). Raw stk_limit flows through the same P4
# read-through cache as the daily endpoints. SSE50 (000016.SH) over the same short,
# recent window as I5b matches the available minute-cache coverage.
#
# portfolio_notional is an ILLUSTRATIVE diagnostic sizing (10,000,000 RMB), not a
# performance or tradability claim. No new factors, no tuning. The report writes to
# its own name so it never overwrites the accepted I5a/I5b artifacts.

project:
name: quantitative_trading_phase_i5f_intraday_liquidity_diagnostics
timezone: Asia/Shanghai
data:
source: tushare
freq: D
start: '2026-03-03'
end: '2026-06-12'
external_secret_file: /home/shaofl/Projects/financial_projects/.config.json
tushare_token_key: tushare.token
output_name: i5f_daily
cache:
enabled: true
root_dir: artifacts/cache/tushare/v1
refresh_recent_days: 14
refresh_dimension_days: 30
force_refresh: []
universe:
type: index
index_code: 000016.SH
symbols: []
min_listing_days: 60
filters:
# Selection-level daily tradability flags stay DISABLED (as in I5b): the daily
# close limit flag is an EOD value not known at 14:50. The honest direction-aware
# blocking is execution-time raw stk_limit vs the raw execution-minute close.
missing_close: true
suspended: false
st: false
limit_up_down: false
# factors is required by the schema but the runner ignores it: the score is the I3
# intraday_ret feature, not a config factor. Minimal valid placeholder.
factors:
- name: momentum_20
enabled: true
params:
window: 20
price_col: close
processing:
drop_missing: true
standardize:
enabled: true
method: zscore
winsorize:
enabled: false
method: mad
n: 3.0
neutralize:
enabled: false
industry_col: industry
size_col: market_cap
industry_level: L1
alpha:
model: equal_weight
params: {}
portfolio:
constructor: topn_equal_weight
top_n: 10
long_only: true
max_weight: null
turnover_cap: null
backtest:
initial_nav: 1.0
rebalance: monthly
event_order: intraday_tail_rebalance
cash_return: 0.0
intraday:
enabled: true
decision_time: '14:50:00'
data_lag: 1min
session_open: '09:30:00'
execution_model: next_minute_close
execution_window:
- '14:51:00'
- '14:56:59'
require_cache_coverage: true
missing_execution: block
# I5b execution-time raw price-limit feasibility stays ON.
price_limit_check: true
require_price_limit_coverage: true
limit_tolerance: 1.0e-06
# I5f: opt-in, report-only execution liquidity diagnostics.
liquidity_diagnostics:
enabled: true
portfolio_notional: 10000000
max_participation_rate: 0.05
mode: report_only
cost:
fee_rate: 0.001
slippage_rate: 0.0
turnover_formula: l1
analytics:
forward_return_periods:
- 1
- 5
- 20
quantiles: 5
benchmark: null
output:
root_dir: artifacts
data_dir: artifacts/data
factor_dir: artifacts/factors
report_dir: artifacts/reports
log_dir: artifacts/logs
overwrite: true
intraday_report_name: phase_i5f_intraday_liquidity_diagnostics
intraday_report_title: Phase I5f -- Intraday Execution Liquidity Diagnostics (report-only)
48 changes: 48 additions & 0 deletions qt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,48 @@ def _parse_hms(value: str) -> int:
return h * 3600 + m * 60 + s


class LiquidityDiagnosticsCfg(_Strict):
"""Opt-in, report-only intraday execution liquidity diagnostics (I5f).

OFF by default, so every existing config validates and behaves unchanged.
When enabled, the intraday-tail report estimates — per desired rebalance trade
— whether the SELECTED execution-minute 1min bar's traded ``amount`` (RMB) can
absorb the trade at ``max_participation_rate``. This is REPORT-ONLY: it never
changes fills, can_buy/can_sell, blocked reasons, target weights, achieved
holdings, turnover, cost, NAV, factor scores, MMP grouping, alpha, or portfolio
construction. Only ``mode='report_only'`` is supported; an enforcement mode
fails readably (this layer must not move a real trade).
"""

enabled: bool = False
portfolio_notional: float | None = None
max_participation_rate: float = 0.05
mode: str = "report_only"

@model_validator(mode="after")
def _check(self) -> "LiquidityDiagnosticsCfg":
if not self.enabled:
return self
if self.mode != "report_only":
raise ValueError(
"intraday.liquidity_diagnostics.mode only supports 'report_only' "
"(report-only diagnostics never change fills/NAV; no enforcement "
f"mode is implemented); got {self.mode!r}."
)
if self.portfolio_notional is None or float(self.portfolio_notional) <= 0.0:
raise ValueError(
"intraday.liquidity_diagnostics.portfolio_notional must be a "
"positive RMB number when enabled (it scales the desired trade "
f"notional); got {self.portfolio_notional!r}."
)
if not (0.0 < float(self.max_participation_rate) <= 1.0):
raise ValueError(
"intraday.liquidity_diagnostics.max_participation_rate must be in "
f"(0, 1]; got {self.max_participation_rate!r}."
)
return self


class IntradayCfg(_Strict):
"""Opt-in intraday tail-rebalance event model declaration (I5a).

Expand Down Expand Up @@ -280,6 +322,12 @@ class IntradayCfg(_Strict):
price_limit_check: bool = False
require_price_limit_coverage: bool = True
limit_tolerance: float = 1e-6
# I5f: opt-in, report-only execution liquidity diagnostics. Default-off nested
# block; with the default it changes nothing (existing configs validate and
# behave byte-identically).
liquidity_diagnostics: LiquidityDiagnosticsCfg = Field(
default_factory=LiquidityDiagnosticsCfg
)

@field_validator("limit_tolerance")
@classmethod
Expand Down
111 changes: 111 additions & 0 deletions qt/intraday_tail_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
from runtime.backtest.event_models import IntradayTailEventModel
from runtime.backtest.sim_execution import SimExecution
from runtime.intraday_execution import IntradayExecutionConfig
from runtime.intraday_liquidity import (
LiquidityDiagnostics,
build_liquidity_diagnostics,
)

_LOGGER_NAME = "qt.intraday_tail_framework"
_DEFAULT_REPORT_NAME = "phase_i5a_intraday_tail_framework"
Expand Down Expand Up @@ -140,6 +144,8 @@ class I5aResult:
score_coverage: dict[str, int] # {rows, valid, nan} over the daily score panel
minute_count_summary: dict[str, float] | None # valid-MMP-minutes-per-(date,symbol) distribution
factor_diagnostics: tuple[dict, ...] # per settled rebalance: n / stats / Spearman IC
# I5f execution liquidity diagnostics (report-only). None unless enabled.
liquidity_diagnostics: LiquidityDiagnostics | None
report_path: Path
log_path: Path

Expand Down Expand Up @@ -423,6 +429,7 @@ def run_phase_i5a_intraday(config_path: str) -> I5aResult:
require_price_limit_coverage=ic.require_price_limit_coverage,
)
execution = SimExecution(fee_rate=cfg.cost.fee_rate)
ld_cfg = ic.liquidity_diagnostics
engine = BacktestEngine(
model=model,
universe=universe,
Expand All @@ -432,10 +439,38 @@ def run_phase_i5a_intraday(config_path: str) -> I5aResult:
selection_panel=panel,
initial_nav=cfg.backtest.initial_nav,
cash_return=cfg.backtest.cash_return,
# I5f: record the per-rebalance (target, current) plan ONLY when the
# report-only liquidity diagnostics are enabled. Default-off keeps the
# engine loop byte-identical (no plan rows, no behaviour change).
record_rebalance_plan=ld_cfg.enabled,
)
nav_table = engine.run()
logger.info("intraday backtest: %d settled periods", len(nav_table))

# I5f execution liquidity diagnostics — REPORT-ONLY, computed AFTER settlement
# from logs the backtest already produced; it never altered fills or NAV.
liquidity_diagnostics = None
if ld_cfg.enabled:
liquidity_diagnostics = build_liquidity_diagnostics(
plan_log=engine.rebalance_plan_log(),
fills=model.fills(),
up_blocked_buy_keys=model.up_limit_blocked_buy_keys(),
down_blocked_sell_keys=model.down_limit_blocked_sell_keys(),
bars=bars,
portfolio_notional=ld_cfg.portfolio_notional,
max_participation_rate=ld_cfg.max_participation_rate,
)
logger.info(
"intraday liquidity diagnostics (report-only): %d desired trades, "
"%d feasibility-blocked, %d missing-capacity, %d inspected, %d below 1.0x "
"capacity (did NOT alter fills/NAV)",
liquidity_diagnostics.total_desired_trades,
liquidity_diagnostics.feasibility_blocked,
liquidity_diagnostics.missing_capacity_rows,
liquidity_diagnostics.inspected,
liquidity_diagnostics.below_capacity,
)

blocked = model.blocked_fills()
blocked_counts: dict[str, int] = {}
for f in blocked:
Expand Down Expand Up @@ -490,6 +525,7 @@ def run_phase_i5a_intraday(config_path: str) -> I5aResult:
score_coverage=score_coverage,
minute_count_summary=minute_count_summary,
factor_diagnostics=tuple(factor_diag),
liquidity_diagnostics=liquidity_diagnostics,
report_path=report_path,
log_path=log_path,
)
Expand Down Expand Up @@ -598,6 +634,80 @@ def _append_factor_section(lines: list[str], result: I5aResult) -> None:
lines.append("")


def _append_liquidity_section(lines: list[str], result: I5aResult) -> None:
"""Report-only execution liquidity diagnostics (I5f). No-op if disabled."""
ld = result.liquidity_diagnostics
if ld is None:
return
lines.append("## Execution liquidity diagnostics (I5f, report-only)")
lines.append("")
lines.append(
"**Report-only: these diagnostics did NOT alter fills, can_buy/can_sell, "
"blocked reasons, target weights, achieved holdings, turnover, cost, or NAV.** "
"They size each desired rebalance trade against the SELECTED execution-minute "
"1min bar's traded `amount` (RMB) at a participation cap — no future bar, no "
"daily amount/volume/close, no EOD proxy."
)
lines.append("")
lines.append(
f"- desired trade notional = `|target_weight - current_weight| * "
f"portfolio_notional`; portfolio_notional = `{ld.portfolio_notional:,.0f}` RMB "
"(illustrative diagnostic sizing, NOT a performance claim)."
)
lines.append(
f"- bar capacity = `execution_minute_amount * max_participation_rate`; "
f"max_participation_rate = `{ld.max_participation_rate}`."
)
lines.append(
"- capacity_ratio = bar_capacity / desired_notional; `>= 1` ⇒ the capped bar "
"covers the trade, `< 1` ⇒ potentially liquidity-constrained."
)
lines.append("")
lines.append(f"- total desired trades inspected: {ld.total_desired_trades}")
lines.append(
f"- excluded — existing execution feasibility already blocked them "
f"(missing bar / missing price / raw stk_limit; original reason kept, NOT "
f"reclassified as liquidity): {ld.feasibility_blocked}"
)
lines.append(f"- missing capacity-data rows (amount missing/NaN/≤0): {ld.missing_capacity_rows}")
lines.append(f"- trades with a usable capacity ratio: {ld.inspected}")
lines.append(f"- trades below 100% capacity (ratio < 1.0): {ld.below_capacity}")
rs = ld.ratio_stats
if ld.inspected > 0:
lines.append(
f"- capacity ratio distribution — min `{rs['min']:.3f}` / p10 "
f"`{rs['p10']:.3f}` / median `{rs['median']:.3f}` / p90 `{rs['p90']:.3f}`."
)
else:
lines.append("- capacity ratio distribution: _no inspected trade with a usable ratio._")
lines.append("")
if ld.top_constrained:
lines.append(
"Top constrained trades (lowest capacity ratio first; report-only):"
)
lines.append("")
lines.append(
"| date | symbol | direction | desired_notional | bar_capacity_notional | capacity_ratio |"
)
lines.append("|---|---|---|---|---|---|")
for t in ld.top_constrained:
lines.append(
f"| {pd.Timestamp(t.date).date()} | {t.symbol} | {t.direction} | "
f"{t.desired_notional:,.0f} | {t.bar_capacity_notional:,.0f} | "
f"{t.capacity_ratio:.3f} |"
)
else:
lines.append("_no constrained trade to list._")
lines.append("")
lines.append(
"- **No alpha / performance / tradability claim** is made from this "
"diagnostic: it only flags where a single execution minute may be too thin "
"for the sized trade; partial-fill / volume-cap enforcement is explicitly "
"out of scope here."
)
lines.append("")


def _write_report(result: I5aResult, *, elapsed: float) -> None:
"""Write the I5a architecture-smoke markdown report (auditable event basis)."""
cfg = result.config
Expand Down Expand Up @@ -770,6 +880,7 @@ def _write_report(result: I5aResult, *, elapsed: float) -> None:
"rule; strict mode fails before any result — never a silent passed check)"
)
lines.append("")
_append_liquidity_section(lines, result)
lines.append("## Achieved holdings (sample)")
lines.append("")
h = result.holdings_log
Expand Down
Loading