Skip to content

feat: intraday 1min PIT pipeline (I1–I4: feed/schema, cache, aggregation, tail execution) - #29

Merged
StackOverFlow11 merged 4 commits into
mainfrom
feat/intraday-pit-pipeline
Jun 13, 2026
Merged

feat: intraday 1min PIT pipeline (I1–I4: feed/schema, cache, aggregation, tail execution)#29
StackOverFlow11 merged 4 commits into
mainfrom
feat/intraday-pit-pipeline

Conversation

@StackOverFlow11

Copy link
Copy Markdown
Owner

Summary

Adds a complete, SEPARATE intraday (1min) research track — raw feed/schema → read-through cache → minute→daily PIT aggregation → tail-rebalance execution semantics. Nothing touches the daily pipeline: factors/alpha/portfolio, the daily runtime/backtest, daily TushareFeed/TushareCache, and all config/ models are unchanged. Phase 0/2/3 behavior is identical (run-phase0 still ic_mean=0.9600, annual_return=0.8408).

Four logically-distinct stages, one commit each:

I1 — feat(data): 1min raw feed + PIT schema

  • data/clean/intraday_schema.py: minute-bar schema MultiIndex(time, symbol)time kept at minute precision, NEVER normalized to midnight (separate from the daily panel contract). PIT fields bar_end=trade_time, bar_start=bar_end-freq, available_time=bar_end+data_lag. Raw persisted freq is 1min only; coarser freqs are derived views (ensure_raw_intraday_freq).
  • data/feed/tushare_intraday.py: TushareIntradayFeed.get_minutes over stk_mins; raw field mapping only; non-1min raw freq rejected before any SDK call; token read live, never logged.

I2 — feat(cache): stk_mins 1min read-through cache

  • Timestamp-interval coverage ledger (NOT the daily date-range ledger), month-partitioned parquet (stk_mins_1min/freq=1min/symbol_prefix/symbol/year/month.parquet), trading-day gap planning (reuses intervals.py), ≤23-day window paging under the 8000-row cap. Cold writes / warm zero-call / partial-gap / empty-recorded / failed-retried; cached output is byte-identical to the direct feed after normalize. Daily TushareCache untouched (separate classes).

I3 — feat(data): minute→daily PIT aggregation (14:50)

  • asof_daily_features (default decision_time=14:50:00): filter available_time <= cutoff before grouping by day (minute timestamps never date-normalized first). Cutoff-encoded columns: intraday_ret_0930_1450, intraday_realized_vol_0930_1450, intraday_vwap_0930_1450, intraday_last30m_ret_1420_1450.
  • resample_intraday_bars: derived coarse bars carry available_time = max(source_1min.available_time).

I4 — feat(runtime): tail-rebalance execution skeleton

  • simulate_tail_rebalance keeps three boundaries distinct: signal cutoff (T 14:50) / execution timestamp (T 14:51, next_minute_close) / holding period (exec(T)→exec(T_next), never close-to-close). Missing bar / NaN price / no bar in window → explainable blocked record, never a silent daily-close fallback. Standalone — NOT wired into config/pipeline.

Invariants held

  • factors never see forward returns; daily close_to_next_period unchanged.
  • Raw intraday SoT = 1min only; 5/15/30/60min are derived; daily D stays a separate retained source.
  • No token / secret / .config.json content in any committed file (scanned: 0); cache parquet + ledger are gitignored.

Test plan

  • pytest tests -q484 passed (+56 intraday: I1 schema 14 + feed 9, I2 cache 10, I3 agg 13, I4 exec 10; 0 regressions).
  • ruff check . → clean.
  • qt validate-config --config config/example.yaml → OK.
  • qt run-phase0 --config config/example.yamlic_mean=0.9600, annual_return=0.8408 (unchanged).

Stage-by-stage acceptance evidence (PIT semantics, cold/warm/partial, leakage tests, boundary notes) lives in tmp/context/intraday_pit_checkpoints/stage_i{1,2,3,4}_acceptance.md (gitignored, local).

Adds a SEPARATE intraday data path that never touches the daily panel contract.

- data/clean/intraday_schema.py: minute-bar schema (MultiIndex(time, symbol),
  time kept at minute precision, never normalized to midnight). Derives PIT time
  fields bar_end=trade_time, bar_start=bar_end-freq, available_time=bar_end+data_lag.
  Raw persisted freq is 1min only (RAW_INTRADAY_FREQ + ensure_raw_intraday_freq);
  coarser freqs are derived views, not raw products.
- data/feed/tushare_intraday.py: TushareIntradayFeed.get_minutes over stk_mins;
  raw field mapping only (ts_code->symbol, vol->volume, trade_time->time/bar_end);
  non-1min raw freq rejected before any SDK call; token read live, never logged.
  Includes an opt-in cache=None hook (dormant until I2).
- Tests: minute-precision, PIT derivation, reverse-input sort, dedup last-wins,
  readable errors, empty frame, no-token-leak.

No daily feed/cache/pipeline change.
Endpoint-level read-through cache for raw 1min bars, separate from the daily cache
(zero change to the 540-line daily TushareCache).

- data/cache/intraday_coverage.py: timestamp-interval ledger (raw_freq/start_time/
  end_time), NOT the daily date-range ledger; ok/empty cover, failed does not.
- data/cache/intraday_parquet_store.py: month-partitioned store
  (stk_mins_1min/freq=1min/symbol_prefix/symbol/year/month.parquet); atomic
  idempotent upsert by (symbol, freq, bar_end); stores RAW only.
- data/cache/intraday_cache.py: TushareIntradayCache.stk_mins_1min read-through —
  trading-day gap planning (reuses intervals.py), <=23-day window paging under the
  8000-row cap, 1min-only guard, empty recorded / failed retried.
- Wires an optional cache into TushareIntradayFeed (direct path byte-identical).
- Tests: cold writes / warm zero-call / partial gap / empty / failed / dup-upsert /
  non-1min reject / no-secret / cached==direct after normalize.

Daily cache/feed/pipeline unchanged.
PIT-safe aggregation from normalized 1min bars to a daily (date, symbol) feature
table for the factor layer. Filter available_time <= (trade_date + decision_time)
BEFORE grouping by day — minute timestamps are never date-normalized first.

- data/clean/intraday_aggregate.py: asof_daily_features (default 14:50:00 cutoff;
  cutoff-encoded columns intraday_ret_0930_1450 / realized_vol / vwap /
  last30m_ret_1420_1450) + resample_intraday_bars (derived coarse bars carry
  available_time = max(source_1min.available_time), never bar_end+lag).
- Tests: cutoff leakage (perturb post-14:50 -> features unchanged), availability
  exclusion, daily index, multi-symbol/day isolation, empty, custom cutoff,
  derived-bar max-availability.

No factor/alpha/portfolio/runtime wiring; daily layer untouched.
Second backtest event model intraday_tail_rebalance as a standalone
runtime/execution skeleton — NOT wired into the daily pipeline or config.

- runtime/intraday_execution.py: IntradayExecutionConfig / ExecutionFill /
  TailRebalanceResult + resolve_fill / build_execution_prices /
  simulate_tail_rebalance. Three boundaries kept distinct: signal cutoff (T 14:50)
  / execution timestamp (T 14:51, next_minute_close) / holding period
  (exec(T)->exec(T_next), never close-to-close). Missing bar / NaN price / no bar
  in window -> explainable blocked record, never a silent daily-close fallback.
  Only intraday bars are read (no T-day EOD leak).
- Tests: cutoff vs exec separation, next_minute_close + window fallback, blocked
  reasons, exec-to-exec != close-to-close, blocked exclusion.

Daily close_to_next_period + Phase 0/2/3 unchanged (run-phase0 ic 0.96/annual 0.84).
@StackOverFlow11
StackOverFlow11 merged commit f565d0c into main Jun 13, 2026
@StackOverFlow11
StackOverFlow11 deleted the feat/intraday-pit-pipeline branch June 13, 2026 13:56
StackOverFlow11 added a commit that referenced this pull request Jun 13, 2026
docs: record intraday I1–I4 pipeline (PR #29) in CLAUDE.md progress
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant