Skip to main content

pit-adjuster

PyPI version PyPI downloads CI License

中文说明

pit-adjuster 面向 A 股等股票市场的历史行情复权和公司行为数据处理。 它根据带时间点的公司行为档案重建固定基准价格,检查复权因子链是否连续, 并识别数据供应商悄悄切换复权口径的情况。工具只处理和验证历史数据, 不预测价格、不提供交易建议;公司行为的发生日、公告时间和数据覆盖范围 必须由使用者提供并核验。

Point-in-time fixed-basis back-adjustment engine for daily price history: rebuild prices so that any day reads exactly what that day could have known —plus drift detection for vendors that silently switch adjustment conventions. Python 3.11+, zero dependencies, Windows / Linux / macOS.

In plain words: data vendors silently switch adjustment conventions — the 2019 prices you see today may not be the ones you saw yesterday, and nothing in the CSV changes shape. pit-adjuster detects that change and rebuilds history from a point-in-time corporate-action archive, so your backtest never quietly breaks because the data's meaning changed under you.

adjustment chain python

Status: v0.1.5 alpha, published on PyPI. The adjustment math is battle-tested inside a production research pipeline, but this standalone package is new: expect the CLI and schema to shift before v1.0.

Why this exists

A-share (and most equity) history arrives from vendors in current-vintage adjusted form. Two silent dangers:

  1. The convention itself is not point-in-time. Prices you see today embed every adjustment event that ever happened —including events that were announced after a historical date. A backtest that uses them reads the future.
  2. Vendors switch conventions silently. One day your data source starts serving forward-adjusted prices where it served back-adjusted prices yesterday. Nothing in the CSV changes shape; every historical signal silently changes value.

pit-adjuster rebuilds history from two ingredients —current-vintage forward-adjusted (qfq) bars plus a point-in-time corporate-action archive —into a fixed-basis back-adjusted (hfq) chain where each day's price depends only on events whose ex-date is on or before that day. Then it checks: did the rebuild invert the vendor chain correctly, and does the vendor chain still agree with live raw prices today?

Philosophy

Price history must be reversible. A research pipeline that cannot prove its prices were knowable in the past is not doing backtesting —it is doing wishful thinking. pit-adjuster treats look-ahead freedom as a verifiable property, not a style preference:

  • PIT principle —every price, factor, and calibration depends only on information available at that historical point. See Kelly et al., "Scaling Point-in-Time Language Models" (NBER w35247) and Look-Ahead-Bench (arXiv:2601.13770) for why the whole industry is converging on this.
  • Look-ahead bias is measurable —Daniel, Sornette & Wohrmann (2008), "Look-Ahead Benchmark Bias in Portfolio Performance Evaluation" (arXiv:0810.1922) quantify how ex-post benchmark construction inflates performance. A vendor that silently swaps adjustment conventions is doing exactly this, inside your price column.
  • Formal ground —Fonseca (2026), "Look-Ahead-Freedom as Temporal Non-Interference" (arXiv:2607.04958) proves look-ahead-freedom is undecidable in general (Pi-0-1-hard when availability depends on data values), but admits a linear-time decidable type-effect system on the value-independent fragment —windowing, resampling, joins, PIT and vintage reads.

Honest boundary: this package implements verifiable checks for the value-independent fragment of the problem (factor chains, ex-date ordering, snapshot equivalence, chain inversion). For the general value-dependent case we fall back to heuristic guards and say so explicitly —verifiability is claimed only where the theory allows it.

Quick start

# install the published package from PyPI
pip install pit-adjuster

# or run without installing anything:
#   PYTHONPATH=src python -m pit_adjuster --help

# try it on synthetic data (builds a fake qfq history + action archive,
# rebuilds to hfq, runs invert-check and drift-check)
python examples/demo.py

# real-world case: a one-day ×100 price-scale corruption (21 symbols) caught
# by the full-window drift default — tail-only sampling misses it
python examples/case_scale_corruption.py

# companion: a missing corporate-action event (constant ~31% pre-event
# deviation) — same default catches it, tail-only misses it again
python examples/case_missing_event.py

# real-world case: a vendor forward (qfq) chain that drifts on
# corporate-action-dense codes (6 of 8 sampled codes BROKEN in production,
# 32-424 unexplained drift days) — clean / broken / missing-event verdicts
python examples/case_forward_drift.py

# real-world case: silent history rewrites between runs (caught once, then
# quiet; legitimate ex-date re-anchors are exempt) — six-run replay
python examples/case_history_rewrite.py

Real-world case write-ups:

  • docs/case-study-daily-bar-scale-corruption.md — a one-day ×100 OHLC corruption caught by the full-window drift default, with the fix pattern (detect → correlate by source → repair with provenance → fix parser + regression test → re-verify).
  • docs/case-study-vendor-forward-drift.md — a vendored forward chain drifting on event-dense codes (spurious day returns on days with no corporate action), caught by padj forward-check and rebuilt deterministically from the corporate-action chain.
  • docs/case-study-cross-run-history-rewrite.md — silent history rewrites between runs (post-ex segment changes with no new event), caught once by the stateful padj watch, with legitimate ex-date re-anchors exempt.

Rebuild your own history:

padj rebuild \
  --bars bars.json --actions actions.json \
  --as-of 2026-08-11 --code 600000 --out hfq.json

padj invert-check --bars hfq.json --actions actions.json --as-of 2026-08-11
padj drift-check --bars hfq.json --actions actions.json \
  --as-of 2026-08-11 --live live_closes.json

# validate a vendor forward-adjusted series against the CA archive:
padj forward-check --raw raw_closes.json --forward vendor_qfq.json \
  --actions actions.json

# run the cross-run rewrite watchdog (state lives in --state-dir):
padj watch --bars vendor_600000.json --actions actions.json \
  --as-of 2026-08-11 --state-dir ./watch-state --code 600000

padj rebuild is the workhorse: it inverts the vendor qfq chain back to raw prices, then re-applies only events whose ex-date is on or before each bar date (fixed basis at the archive coverage start). Raw open/close are kept alongside adjusted prices so execution-level work can map back to nominal prices.

Commands

Command What it does
rebuild Rebuild bars to fixed-basis hfq: open/high/low/close adjusted, raw_open/raw_close nominal, adj_factor cumulative multiplier, volume normalized to shares
invert-check Ex-date continuity sanity check: is raw_{ex-1} × factor_e ≥ raw_ex? Informational —real ex-dates carry overnight returns, so violations can be false positives
drift-check Static forward-adjustment detection. Compares inverted raw closes against live raw closes; divergence above tolerance is authoritative —a vendor chain that no longer matches the archive
forward-check Vendor forward-chain validation. Reconciles a vendor forward-adjusted (qfq) series against the deterministic day returns implied by raw closes and the CA archive. Deviations on days with no corporate action are unexplained; more than --unexplained-limit of them ⇒ broken (vendor forward-chain drift — do not trust the series). A CA archive that missed an event the vendor applied surfaces as a single unexplained jump (suspicious). Read-only; exits non-zero on suspicious/broken
watch Cross-run history-rewrite detection (stateful). Fingerprints the trailing vendor closes into <state-dir>/watch_state.json; a day whose close changed since the previous run, on or after the newest archive ex-date, is a silent history rewrite → FIRED (exit 1). Pre-ex-date changes are exempt (a new ex-date legitimately re-anchors forward history — that segment is drift-check's job). Incremental: a persisting rewritten value does not re-fire on the next run
snapshot-equivalence Compare two rebuilt outputs (e.g. old and new pipeline versions) date-by-date within tolerance —the "did anything change?" gate
version Print version

Global flags: --help on every subcommand; JSON outputs via --out where supported; everything else prints a human-readable summary.

Data model

Bars —a JSON list of daily bars, each with at least date (ISO) and close; open/high/low/volume/amount/turnover are preserved through the rebuild:

{"date": "2026-06-12", "open": 95.0, "high": 96.0, "low": 94.5, "close": 95.5, "volume": 1234500}

Actions —a point-in-time corporate-action archive, one record per event, with ex_date, adjustment_factor and available_at:

{"ex_date": "2026-06-15", "adjustment_factor": 0.95, "available_at": "2026-06-14T18:00:00", "action_type": "cash_dividend_stock_distribution"}

Invalid records (missing ex-date, non-positive or non-finite factor) are dropped; only events with ex_date <= as_of_date participate. The schema lives in schema/corporate-action.schema.json.

Adjustment math

Standard A-share factor math (as documented by exchange reference-price rules):

factor_e = (prior_close - cash) / (prior_close * (1 + bonus + transfer))
qfq_t    = raw_t * prod_{e: ex_date_e > t} factor_e
hfq_t    = raw_t * prod_{e: ex_date_e <= t} (1 / factor_e)

rebuild inverts the vendor qfq chain back to raw prices, then applies the hfq chain with a fixed basis at the archive coverage start. Key property (under test): hfq and qfq yield identical adjusted returns for the same factor chain, while hfq additionally guarantees that a price at time t is untouched by events with ex-date after t.

Volume normalization follows the A-share convention: most codes store volume in lots (×100 to shares); STAR-market codes (688/689 prefixes) store native shares. Both are parameterizable —see --volume-to-shares and the native_share_prefixes argument in rebuild_bars.

Verification model

pit-adjuster never trusts its inputs:

  • invert-check —factor continuity at ex-dates (sanity, false-positive tolerant)
  • drift-check —inverted raws vs live raws (authoritative divergence detection; this is the "static forward-adjustment detector" —if a vendor swaps conventions, this fires)
  • forward-check —vendor forward series vs raw × CA-archive day returns (catches vendor forward chains that drift on event-dense codes, and CA archives missing an event)
  • watch —cross-run rewrite detection on the post-newest-ex segment (stateful, incremental: fires once per actual history change)
  • snapshot-equivalence —before/after equivalence of two rebuilds, the reproducibility gate for pipeline migrations

Every check is read-only. Nothing here trades, prices, or decides.

Development

python -m pip install -e . pytest
python -m pytest

CI runs the full test suite on Ubuntu, Windows and macOS with Python 3.11 and 3.12. Issues are handled on weekends; pull requests are welcome.

Related work

Project family

Part of Holdout — a toolchain against self-deception in quantitative research:

Sister org: Metabolism Tools — workspace-metabolism, policy-driven file lifecycle management for agentic workspaces.

License

MIT

Metadata

Release files for pit-adjuster 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pit-adjuster 0.1.5
File Size Uploaded
pit_adjuster-0.1.5.tar.gz 32.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pit-adjuster 0.1.5
File Interpreter ABI Platform
pit_adjuster-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 54.6 kB

Release files / pit_adjuster-0.1.5.tar.gz

Download URL pit_adjuster-0.1.5.tar.gz
Size 32.4 kB
Tags Source
SHA-256 checksum
How to use checksums
822262835313695a1f3eee21af571e49bec1a06bc48ba6d37abf8d346c2ddc17
BLAKE2b-256 checksum
How to use checksums
0f956ed10d1e73a94099e9e1b6653f630628e5e22a31e82cd87267998b6548da
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release files / pit_adjuster-0.1.5-py3-none-any.whl

Download URL pit_adjuster-0.1.5-py3-none-any.whl
Size 22.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e352e84ff6a81b51443cf844262dc6c24a09507ad57e33ed8da016498a7c6568
BLAKE2b-256 checksum
How to use checksums
ca850beea8b604b197cf0d1cb5d3348a3225593ddc9d914ba31126e060a782d4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.3

Release history Release notifications | RSS feed

This release

0.1.5 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page