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
198 changes: 198 additions & 0 deletions config/phase_l_valley_price_quantile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# PR-L — Tenth real factor evaluation: VALLEY WEIGHTED-PRICE-QUANTILE.
#
# Reproduces the SIXTH factor of the Kaiyuan market-microstructure series #27 (开源证券
# 《高频成交量的峰、岭、谷信息——市场微观结构研究系列(27)》, reportId 4957417, §5) — the
# "量谷加权价格分位点因子" — as a first-class ValleyPriceQuantileFactor and runs it through the
# FROZEN StandardFactorEvaluator on REAL cached A-share data. CACHE-ONLY: the minute read is
# provably live-call-free; daily / universe / covariate endpoints go through the read-through
# cache (warm -> 0 gap fetches).
#
# WHY THIS FACTOR, NOW: the five prior reproductions from this report covered a COUNT (PR-F
# volume_peak_count, weak), a TIMING moment (PR-H peak_interval_kurtosis, null), two price
# RATIOS (PR-I valley_relative_vwap and PR-J valley_ridge_vwap_ratio, both PASSING) and a
# RETURN (PR-K ridge_minute_return, sign transferred but Reject). This factor is a price
# POSITION — where inside the day's range the valley VWAP sits — so it separates two
# explanations of PR-I/PR-J's success: the ratio FORM, or price-level INFORMATION generally.
#
# SAME MACHINE as PR-F / PR-H / PR-I / PR-J / PR-K. The minute classification is REUSED from
# data/clean/intraday_volume_prv.py (not re-implemented, and not modified by this PR):
# PIT-truncate each day at 14:50, classify every visible minute against its SAME-SLOT
# strictly-prior 20-day baseline (ERUPTIVE if vol > mu + sigma, else a VALLEY 量谷). Because the
# classification is shared verbatim, a different verdict here cannot be an artefact of a
# differently implemented taxonomy.
#
# PINNED choices (all disclosed on the factor spec; the report specifies only the shape):
# (1) THE PRICE RANGE reads three prices — the visible day's high, the visible day's low,
# and the PREVIOUS trading day's close — as hi = max(visible high, prev_close) and
# lo = min(visible low, prev_close). prev_close is pinned to the LAST VISIBLE (<=14:50)
# RAW CLOSE of the previous trading day. DISCLOSED DEVIATION from the report, which uses
# the true previous daily close. NOTE this is NOT a leakage fix — the previous day's
# 15:00 close is already past at a 14:50 decision on day d — it is a
# SINGLE-VISIBILITY-DEFINITION choice, so every price entering the factor comes from one
# source under one cutoff instead of mixing the minute cache with a daily-close feed;
# (2) the daily quantile (valley VWAP - lo)/(hi - lo) is NOT CLIPPED. A VWAP outside the
# range means the range construction is wrong, and clipping to [0,1] would hide exactly
# the defect worth seeing;
# (3) THE REVERSAL NEUTRALIZATION IS TAKEN AT T-1. The report residualizes this factor
# against a 20-day reversal because the raw quantile is strongly exposed to it. The naive
# -(close_d/close_{d-20} - 1) reads day d's CLOSE, which is 15:00 information and WOULD
# be a lookahead at our 14:50 decision, so we use rev20 = -(close_{d-1}/close_{d-21} - 1)
# on FRONT-ADJUSTED daily closes taken from the panel the runner already loads — NO new
# data source. Front-adjusted closes are REQUIRED here (unlike the within-day quantities)
# because the ratio spans 20 trading days. Day d's close is therefore never an input to
# day d's factor value, and a dedicated test perturbs it to prove so;
# (4) RAW minute prices for the range and the VWAP, with ONE DISCLOSED IMPERFECTION that
# does NOT apply to PR-I/PR-J/PR-K: those factors could argue the adjustment factor
# cancels because both legs sat inside ONE trading day, but prev_close crosses the
# overnight boundary, so on an ex-dividend / split date the previous raw close sits on
# the OLD (higher) price scale while the day's own bars sit on the new one. The bias is
# ONE-SIDED, not symmetric: prev_close pulls hi UP (measured on real cache, 42
# top-widening vs 4 bottom-widening among true ex-dates), inflating the denominator
# hi - lo while the valley VWAP and lo stay on the new scale — which compresses that
# day's quantile toward the LOW END of an artificially wide range, NOT toward the
# middle. MEASURED on the 30-name prototype panel: 15.6% of valid days show ANY
# prev_close widening, but that is overwhelmingly ordinary overnight gapping, which is
# the INTENDED behaviour (the report's range includes the previous close precisely so a
# gap counts); only 0.73% of valid days are true ex-dates, and only ~0.21% are BOTH a
# true ex-date AND actually range-distorted, before the 20-day mean dilutes it further.
# DISCLOSED rather than silently corrected, because correcting it would mix an adjusted
# price into an otherwise raw, single-visibility construction;
# (5) a PRICE guard (finite, high >= low > 0) admits a bar to the range while a TRADE guard
# (finite positive volume AND amount) admits it to the VWAP — a zero-volume minute still
# quotes a real price. Both run at the summation step only, so PR-F's same-slot baseline
# — and therefore all five merged factors — stay bit-identical;
# (6) a day is VALID iff it has >= 100 classifiable bars AND >= 20 TRADABLE valley bars AND
# positive valley volume AND an available prev_close AND hi > lo, so a symbol's FIRST
# visible day never produces a value; NaN below 10 valid days;
# (7) the residualization is PER DATE on the covered cross-section (PR-G's pattern): fewer
# than 10 paired symbols, a missing rev20, or a degenerate (zero-variance) rev20 all
# yield NaN, never a zero-filled or unresidualized value passed off as neutralized. The
# runner REPORTS the before/after exposure and the realized cross-section sizes;
# (8) DEVIATION FROM THE REPORT, disclosed: everything spans the PIT-VISIBLE window
# 09:31-14:50 only, while the report uses the FULL day. Reading the closing auction would
# be lookahead at our 14:50 decision time.
#
# Pre-registered sign = +1 (report full-market RankIC +6.34% / RankICIR 4.32, long leg
# 13.1%/yr, long-short 20.22%/yr, IR 3.29, max drawdown 10.18%, monthly win rate 80.4%;
# CSI500 sub-domain long-short 11.71% / IR 1.76 — the closest comparable to our eval cell,
# quoted because the report actually gives one for this factor). Semantics per the report: a
# HIGH position of the calm-minute price within the day's range means the informed, unhurried
# part of the day traded near the top of the range -> higher future return. NOTE the report is
# a MONTHLY, market-cap + industry neutral FULL-MARKET series on Wind data — our eval cell is
# CSI500 daily with industry + size neutral, so those numbers are a LOOSE reference only and
# are NOT written in as expected values. Raw minute volume (cached as-is) has split-day
# magnitude jumps that pollute the 20-day sigma; the report (Wind) does not adjust for this
# either, so it is disclosed and NOT corrected.
#
# SIGN = +1 means this factor is NOT affected by the frozen layer's aligned-spread cost-sign
# defect (which computes sign * (gross - cost) and therefore ADDS costs back only when the sign
# is negative). Unlike PR-K, the aligned_spread_* fields may be read directly here.
# net_long_short_by_cost remains the primary tradability read, for sibling comparability.
#
# Eval CELL is IDENTICAL to PR-C .. PR-K: universe = CSI500 (000905.SH), PIT membership; window
# = 2021-07-01 .. 2026-06-30 (the project's minute-coverage window); daily rebalance; OOS split
# 2024-01-01; book = value_ep/value_bp/volatility_20; fee 0.001. The ONLY substantive difference
# is the subject factor, which is minute-derived and computed by the runner from the intraday
# cache — it is NOT a config-listed daily factor. Because the cell matches its siblings exactly,
# this run is directly comparable to them. The evaluator runs TWICE (see
# qt/eval_valley_price_quantile.py): once with NO book (Incremental NOT_ASSESSED) and once with
# the confirmed book — which matters here because a within-range price POSITION is a plausible
# cousin of a value signal, and the with-book run is what says whether this is a new bet.

project:
name: quantitative_trading_pr_l_valley_price_quantile
timezone: Asia/Shanghai
data:
source: tushare
freq: D
start: '2021-07-01'
end: '2026-06-30'
external_secret_file: /home/shaofl/Projects/financial_projects/.config.json
tushare_token_key: tushare.token
output_name: valley_price_quantile_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: 000905.SH
symbols: []
min_listing_days: 60
filters:
missing_close: true
suspended: false
st: false
limit_up_down: false
# factors = the confirmed BOOK used for the Incremental axis (value + low-vol). The SUBJECT
# factor (valley_price_quantile_20) is minute-derived and computed by the runner from the
# intraday cache — it is NOT a config-listed daily factor.
factors:
- name: value_ep
enabled: true
- name: value_bp
enabled: true
- name: volatility_20
enabled: true
params:
window: 20
price_col: close
processing:
drop_missing: true
standardize:
enabled: true
method: zscore
# winsorize is a P0 no-op in this codebase; the EvalConfig declares winsorize=None
# accordingly (nothing is clipped), so the report never overstates preprocessing.
winsorize:
enabled: false
method: mad
n: 3.0
# Industry + market-cap neutralization (SW-L1), matching the report's neutral column and
# the EvalConfig neutralization declaration. This is SEPARATE from, and applied AFTER, the
# factor's own reversal neutralization (which is part of the factor definition).
neutralize:
enabled: true
industry_col: industry
size_col: market_cap
industry_level: L1
alpha:
model: equal_weight
params: {}
portfolio:
# Required by the schema but unused: this runner evaluates a factor, it does not
# build/execute a portfolio.
constructor: topn_equal_weight
top_n: 50
long_only: true
max_weight: null
turnover_cap: null
backtest:
# Required by the schema but unused (no backtest is run). The EvalConfig uses a DAILY
# rebalance (contract default), independent of this field.
initial_nav: 1.0
rebalance: monthly
event_order: close_to_next_period
cash_return: 0.0
cost:
fee_rate: 0.001
slippage_rate: 0.0
turnover_formula: l1
analytics:
forward_return_periods:
- 1
quantiles: 5
benchmark: null
# OOS split (window midpoint) so the OOS section runs and the Predictive axis can be
# assessed (sign consistency across both holdout subperiods).
oos:
split_date: '2024-01-01'
output:
root_dir: artifacts
data_dir: artifacts/data
factor_dir: artifacts/factors
report_dir: artifacts/reports
log_dir: artifacts/logs
overwrite: true
Loading