pit-adjuster 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pit Adjuster contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: pit-adjuster
3
+ Version: 0.1.0
4
+ Summary: Point-in-time fixed-basis back-adjustment engine: adjustment factor chains, static forward-adjustment detection, snapshot equivalence checks.
5
+ License-Expression: MIT
6
+ Keywords: point-in-time,back-adjustment,hfq,qfq,corporate-actions,quant,pandas-free
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Environment :: Console
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Office/Business :: Financial :: Investment
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # pit-adjuster
23
+
24
+ Point-in-time fixed-basis back-adjustment engine for daily price history:
25
+ rebuild prices so that **any day reads exactly what that day could have
26
+ known** — plus drift detection for vendors that silently switch adjustment
27
+ conventions. Python 3.11+, **zero dependencies**, Windows / Linux / macOS.
28
+
29
+ ![adjustment chain](https://img.shields.io/badge/deps-0-brightgreen)
30
+ ![python](https://img.shields.io/badge/python-3.11%2B-blue)
31
+
32
+ **Status:** v0.1 — alpha. The adjustment math is battle-tested inside a
33
+ production research pipeline, but this standalone package is new: expect the
34
+ CLI and schema to shift before v1.0.
35
+
36
+ ## Why this exists
37
+
38
+ A-share (and most equity) history arrives from vendors in **current-vintage**
39
+ adjusted form. Two silent dangers:
40
+
41
+ 1. **The convention itself is not point-in-time.** Prices you see today
42
+ embed every adjustment event that ever happened — including events that
43
+ were announced *after* a historical date. A backtest that uses them reads
44
+ the future.
45
+ 2. **Vendors switch conventions silently.** One day your data source starts
46
+ serving forward-adjusted prices where it served back-adjusted prices
47
+ yesterday. Nothing in the CSV changes shape; every historical signal
48
+ silently changes value.
49
+
50
+ `pit-adjuster` rebuilds history from two ingredients — current-vintage
51
+ forward-adjusted (qfq) bars plus a **point-in-time corporate-action archive**
52
+ — into a fixed-basis back-adjusted (hfq) chain where each day's price depends
53
+ only on events whose ex-date is on or before that day. Then it *checks*: did
54
+ the rebuild invert the vendor chain correctly, and does the vendor chain
55
+ still agree with live raw prices today?
56
+
57
+ ## Philosophy
58
+
59
+ Price history must be reversible. A research pipeline that cannot prove its
60
+ prices were knowable in the past is not doing backtesting — it is doing
61
+ wishful thinking. `pit-adjuster` treats **look-ahead freedom as a
62
+ verifiable property**, not a style preference:
63
+
64
+ - **PIT principle** — every price, factor, and calibration depends only on
65
+ information available at that historical point. See
66
+ [Kelly et al., "Scaling Point-in-Time Language Models"](https://www.nber.org/papers/w35247)
67
+ (NBER w35247) and
68
+ [Look-Ahead-Bench](https://ar5iv.labs.arxiv.org/html/2601.13770)
69
+ (arXiv:2601.13770) for why the whole industry is converging on this.
70
+ - **Look-ahead bias is measurable** — Daniel, Sornette & Wohrmann (2008),
71
+ ["Look-Ahead Benchmark Bias in Portfolio Performance Evaluation"](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1289222)
72
+ (arXiv:0810.1922) quantify how ex-post benchmark construction inflates
73
+ performance. A vendor that silently swaps adjustment conventions is doing
74
+ exactly this, inside your price column.
75
+ - **Formal ground** — Fonseca (2026),
76
+ ["Look-Ahead-Freedom as Temporal Non-Interference"](https://econpapers.repec.org/paper/arxpapers/2607.04958.htm)
77
+ (arXiv:2607.04958) proves look-ahead-freedom is *undecidable* in general
78
+ (Π⁰₁-hard when availability depends on data values), but admits a
79
+ **linear-time decidable type-effect system on the value-independent
80
+ fragment** — windowing, resampling, joins, PIT and vintage reads.
81
+
82
+ **Honest boundary:** this package implements verifiable checks for the
83
+ value-independent fragment of the problem (factor chains, ex-date ordering,
84
+ snapshot equivalence, chain inversion). For the general value-dependent case
85
+ we fall back to heuristic guards and say so explicitly — verifiability is
86
+ claimed only where the theory allows it.
87
+
88
+ ## Quick start
89
+
90
+ ```bash
91
+ # install from PyPI (once published)
92
+ pip install pit-adjuster
93
+
94
+ # or run without installing anything:
95
+ # PYTHONPATH=src python -m pit_adjuster --help
96
+
97
+ # try it on synthetic data (builds a fake qfq history + action archive,
98
+ # rebuilds to hfq, runs invert-check and drift-check)
99
+ python examples/demo.py
100
+ ```
101
+
102
+ Rebuild your own history:
103
+
104
+ ```bash
105
+ padj rebuild \
106
+ --bars bars.json --actions actions.json \
107
+ --as-of 2026-08-11 --code 600000 --out hfq.json
108
+
109
+ padj invert-check --bars hfq.json --actions actions.json --as-of 2026-08-11
110
+ padj drift-check --bars hfq.json --actions actions.json \
111
+ --as-of 2026-08-11 --live live_closes.json
112
+ ```
113
+
114
+ `padj rebuild` is the workhorse: it inverts the vendor qfq chain back to raw
115
+ prices, then re-applies only events whose ex-date is on or before each bar
116
+ date (fixed basis at the archive coverage start). Raw open/close are kept
117
+ alongside adjusted prices so execution-level work can map back to nominal
118
+ prices.
119
+
120
+ ## Commands
121
+
122
+ | Command | What it does |
123
+ | --- | --- |
124
+ | `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 |
125
+ | `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 |
126
+ | `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 |
127
+ | `snapshot-equivalence` | Compare two rebuilt outputs (e.g. old and new pipeline versions) date-by-date within tolerance — the "did anything change?" gate |
128
+ | `version` | Print version |
129
+
130
+ Global flags: `--help` on every subcommand; JSON outputs via `--out` where
131
+ supported; everything else prints a human-readable summary.
132
+
133
+ ## Data model
134
+
135
+ **Bars** — a JSON list of daily bars, each with at least `date` (ISO) and
136
+ `close`; `open/high/low/volume/amount/turnover` are preserved through the
137
+ rebuild:
138
+
139
+ ```json
140
+ {"date": "2026-06-12", "open": 95.0, "high": 96.0, "low": 94.5, "close": 95.5, "volume": 1234500}
141
+ ```
142
+
143
+ **Actions** — a point-in-time corporate-action archive, one record per
144
+ event, with `ex_date`, `adjustment_factor` and `available_at`:
145
+
146
+ ```json
147
+ {"ex_date": "2026-06-15", "adjustment_factor": 0.95, "available_at": "2026-06-14T18:00:00", "action_type": "cash_dividend_stock_distribution"}
148
+ ```
149
+
150
+ Invalid records (missing ex-date, non-positive or non-finite factor) are
151
+ dropped; only events with `ex_date <= as_of_date` participate. The schema
152
+ lives in [schema/corporate-action.schema.json](schema/corporate-action.schema.json).
153
+
154
+ ## Adjustment math
155
+
156
+ Standard A-share factor math (as documented by exchange reference-price
157
+ rules):
158
+
159
+ ```
160
+ factor_e = (prior_close - cash) / (prior_close * (1 + bonus + transfer))
161
+ qfq_t = raw_t * prod_{e: ex_date_e > t} factor_e
162
+ hfq_t = raw_t * prod_{e: ex_date_e <= t} (1 / factor_e)
163
+ ```
164
+
165
+ `rebuild` inverts the vendor qfq chain back to raw prices, then applies the
166
+ hfq chain with a fixed basis at the archive coverage start. **Key property
167
+ (under test):** hfq and qfq yield identical adjusted *returns* for the same
168
+ factor chain, while hfq additionally guarantees that a price at time `t` is
169
+ untouched by events with ex-date after `t`.
170
+
171
+ Volume normalization follows the A-share convention: most codes store volume
172
+ in lots (×100 to shares); STAR-market codes (688/689 prefixes) store native
173
+ shares. Both are parameterizable — see `--volume-to-shares` and the
174
+ `native_share_prefixes` argument in `rebuild_bars`.
175
+
176
+ ## Verification model
177
+
178
+ `pit-adjuster` never trusts its inputs:
179
+
180
+ - `invert-check` — factor continuity at ex-dates (sanity, false-positive
181
+ tolerant)
182
+ - `drift-check` — inverted raws vs live raws (authoritative divergence
183
+ detection; this is the "static forward-adjustment detector" — if a vendor
184
+ swaps conventions, this fires)
185
+ - `snapshot-equivalence` — before/after equivalence of two rebuilds, the
186
+ reproducibility gate for pipeline migrations
187
+
188
+ Every check is read-only. Nothing here trades, prices, or decides.
189
+
190
+ ## Development
191
+
192
+ ```bash
193
+ python -m pip install -e . pytest
194
+ python -m pytest
195
+ ```
196
+
197
+ CI runs the full test suite on Ubuntu, Windows and macOS with Python 3.11 and
198
+ 3.12. Issues are handled on weekends; pull requests are welcome.
199
+
200
+ ## Related work
201
+
202
+ - [Daniel, Sornette & Wohrmann (2008)](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1289222) — look-ahead benchmark bias, quantified
203
+ - [Fonseca (2026)](https://econpapers.repec.org/paper/arxpapers/2607.04958.htm) — look-ahead-freedom as temporal non-interference (the verifiability boundary)
204
+ - [Point-in-Time Backtesting: A Formal Bias Taxonomy](https://www.mdpi.com/2227-7390/14/12/2182) (Mathematics 2026, 14(12):2182)
205
+ - [Kelly et al., Scaling Point-in-Time Language Models](https://www.nber.org/papers/w35247) (NBER w35247)
206
+ - [Look-Ahead-Bench](https://ar5iv.labs.arxiv.org/html/2601.13770) (arXiv:2601.13770) — measuring look-ahead bias in PIT LLMs
207
+
208
+ ## License
209
+
210
+ MIT
@@ -0,0 +1,189 @@
1
+ # pit-adjuster
2
+
3
+ Point-in-time fixed-basis back-adjustment engine for daily price history:
4
+ rebuild prices so that **any day reads exactly what that day could have
5
+ known** — plus drift detection for vendors that silently switch adjustment
6
+ conventions. Python 3.11+, **zero dependencies**, Windows / Linux / macOS.
7
+
8
+ ![adjustment chain](https://img.shields.io/badge/deps-0-brightgreen)
9
+ ![python](https://img.shields.io/badge/python-3.11%2B-blue)
10
+
11
+ **Status:** v0.1 — alpha. The adjustment math is battle-tested inside a
12
+ production research pipeline, but this standalone package is new: expect the
13
+ CLI and schema to shift before v1.0.
14
+
15
+ ## Why this exists
16
+
17
+ A-share (and most equity) history arrives from vendors in **current-vintage**
18
+ adjusted form. Two silent dangers:
19
+
20
+ 1. **The convention itself is not point-in-time.** Prices you see today
21
+ embed every adjustment event that ever happened — including events that
22
+ were announced *after* a historical date. A backtest that uses them reads
23
+ the future.
24
+ 2. **Vendors switch conventions silently.** One day your data source starts
25
+ serving forward-adjusted prices where it served back-adjusted prices
26
+ yesterday. Nothing in the CSV changes shape; every historical signal
27
+ silently changes value.
28
+
29
+ `pit-adjuster` rebuilds history from two ingredients — current-vintage
30
+ forward-adjusted (qfq) bars plus a **point-in-time corporate-action archive**
31
+ — into a fixed-basis back-adjusted (hfq) chain where each day's price depends
32
+ only on events whose ex-date is on or before that day. Then it *checks*: did
33
+ the rebuild invert the vendor chain correctly, and does the vendor chain
34
+ still agree with live raw prices today?
35
+
36
+ ## Philosophy
37
+
38
+ Price history must be reversible. A research pipeline that cannot prove its
39
+ prices were knowable in the past is not doing backtesting — it is doing
40
+ wishful thinking. `pit-adjuster` treats **look-ahead freedom as a
41
+ verifiable property**, not a style preference:
42
+
43
+ - **PIT principle** — every price, factor, and calibration depends only on
44
+ information available at that historical point. See
45
+ [Kelly et al., "Scaling Point-in-Time Language Models"](https://www.nber.org/papers/w35247)
46
+ (NBER w35247) and
47
+ [Look-Ahead-Bench](https://ar5iv.labs.arxiv.org/html/2601.13770)
48
+ (arXiv:2601.13770) for why the whole industry is converging on this.
49
+ - **Look-ahead bias is measurable** — Daniel, Sornette & Wohrmann (2008),
50
+ ["Look-Ahead Benchmark Bias in Portfolio Performance Evaluation"](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1289222)
51
+ (arXiv:0810.1922) quantify how ex-post benchmark construction inflates
52
+ performance. A vendor that silently swaps adjustment conventions is doing
53
+ exactly this, inside your price column.
54
+ - **Formal ground** — Fonseca (2026),
55
+ ["Look-Ahead-Freedom as Temporal Non-Interference"](https://econpapers.repec.org/paper/arxpapers/2607.04958.htm)
56
+ (arXiv:2607.04958) proves look-ahead-freedom is *undecidable* in general
57
+ (Π⁰₁-hard when availability depends on data values), but admits a
58
+ **linear-time decidable type-effect system on the value-independent
59
+ fragment** — windowing, resampling, joins, PIT and vintage reads.
60
+
61
+ **Honest boundary:** this package implements verifiable checks for the
62
+ value-independent fragment of the problem (factor chains, ex-date ordering,
63
+ snapshot equivalence, chain inversion). For the general value-dependent case
64
+ we fall back to heuristic guards and say so explicitly — verifiability is
65
+ claimed only where the theory allows it.
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ # install from PyPI (once published)
71
+ pip install pit-adjuster
72
+
73
+ # or run without installing anything:
74
+ # PYTHONPATH=src python -m pit_adjuster --help
75
+
76
+ # try it on synthetic data (builds a fake qfq history + action archive,
77
+ # rebuilds to hfq, runs invert-check and drift-check)
78
+ python examples/demo.py
79
+ ```
80
+
81
+ Rebuild your own history:
82
+
83
+ ```bash
84
+ padj rebuild \
85
+ --bars bars.json --actions actions.json \
86
+ --as-of 2026-08-11 --code 600000 --out hfq.json
87
+
88
+ padj invert-check --bars hfq.json --actions actions.json --as-of 2026-08-11
89
+ padj drift-check --bars hfq.json --actions actions.json \
90
+ --as-of 2026-08-11 --live live_closes.json
91
+ ```
92
+
93
+ `padj rebuild` is the workhorse: it inverts the vendor qfq chain back to raw
94
+ prices, then re-applies only events whose ex-date is on or before each bar
95
+ date (fixed basis at the archive coverage start). Raw open/close are kept
96
+ alongside adjusted prices so execution-level work can map back to nominal
97
+ prices.
98
+
99
+ ## Commands
100
+
101
+ | Command | What it does |
102
+ | --- | --- |
103
+ | `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 |
104
+ | `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 |
105
+ | `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 |
106
+ | `snapshot-equivalence` | Compare two rebuilt outputs (e.g. old and new pipeline versions) date-by-date within tolerance — the "did anything change?" gate |
107
+ | `version` | Print version |
108
+
109
+ Global flags: `--help` on every subcommand; JSON outputs via `--out` where
110
+ supported; everything else prints a human-readable summary.
111
+
112
+ ## Data model
113
+
114
+ **Bars** — a JSON list of daily bars, each with at least `date` (ISO) and
115
+ `close`; `open/high/low/volume/amount/turnover` are preserved through the
116
+ rebuild:
117
+
118
+ ```json
119
+ {"date": "2026-06-12", "open": 95.0, "high": 96.0, "low": 94.5, "close": 95.5, "volume": 1234500}
120
+ ```
121
+
122
+ **Actions** — a point-in-time corporate-action archive, one record per
123
+ event, with `ex_date`, `adjustment_factor` and `available_at`:
124
+
125
+ ```json
126
+ {"ex_date": "2026-06-15", "adjustment_factor": 0.95, "available_at": "2026-06-14T18:00:00", "action_type": "cash_dividend_stock_distribution"}
127
+ ```
128
+
129
+ Invalid records (missing ex-date, non-positive or non-finite factor) are
130
+ dropped; only events with `ex_date <= as_of_date` participate. The schema
131
+ lives in [schema/corporate-action.schema.json](schema/corporate-action.schema.json).
132
+
133
+ ## Adjustment math
134
+
135
+ Standard A-share factor math (as documented by exchange reference-price
136
+ rules):
137
+
138
+ ```
139
+ factor_e = (prior_close - cash) / (prior_close * (1 + bonus + transfer))
140
+ qfq_t = raw_t * prod_{e: ex_date_e > t} factor_e
141
+ hfq_t = raw_t * prod_{e: ex_date_e <= t} (1 / factor_e)
142
+ ```
143
+
144
+ `rebuild` inverts the vendor qfq chain back to raw prices, then applies the
145
+ hfq chain with a fixed basis at the archive coverage start. **Key property
146
+ (under test):** hfq and qfq yield identical adjusted *returns* for the same
147
+ factor chain, while hfq additionally guarantees that a price at time `t` is
148
+ untouched by events with ex-date after `t`.
149
+
150
+ Volume normalization follows the A-share convention: most codes store volume
151
+ in lots (×100 to shares); STAR-market codes (688/689 prefixes) store native
152
+ shares. Both are parameterizable — see `--volume-to-shares` and the
153
+ `native_share_prefixes` argument in `rebuild_bars`.
154
+
155
+ ## Verification model
156
+
157
+ `pit-adjuster` never trusts its inputs:
158
+
159
+ - `invert-check` — factor continuity at ex-dates (sanity, false-positive
160
+ tolerant)
161
+ - `drift-check` — inverted raws vs live raws (authoritative divergence
162
+ detection; this is the "static forward-adjustment detector" — if a vendor
163
+ swaps conventions, this fires)
164
+ - `snapshot-equivalence` — before/after equivalence of two rebuilds, the
165
+ reproducibility gate for pipeline migrations
166
+
167
+ Every check is read-only. Nothing here trades, prices, or decides.
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ python -m pip install -e . pytest
173
+ python -m pytest
174
+ ```
175
+
176
+ CI runs the full test suite on Ubuntu, Windows and macOS with Python 3.11 and
177
+ 3.12. Issues are handled on weekends; pull requests are welcome.
178
+
179
+ ## Related work
180
+
181
+ - [Daniel, Sornette & Wohrmann (2008)](https://papers.ssrn.com/sol3/papers.cfm?abstract_id=1289222) — look-ahead benchmark bias, quantified
182
+ - [Fonseca (2026)](https://econpapers.repec.org/paper/arxpapers/2607.04958.htm) — look-ahead-freedom as temporal non-interference (the verifiability boundary)
183
+ - [Point-in-Time Backtesting: A Formal Bias Taxonomy](https://www.mdpi.com/2227-7390/14/12/2182) (Mathematics 2026, 14(12):2182)
184
+ - [Kelly et al., Scaling Point-in-Time Language Models](https://www.nber.org/papers/w35247) (NBER w35247)
185
+ - [Look-Ahead-Bench](https://ar5iv.labs.arxiv.org/html/2601.13770) (arXiv:2601.13770) — measuring look-ahead bias in PIT LLMs
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pit-adjuster"
7
+ version = "0.1.0"
8
+ description = "Point-in-time fixed-basis back-adjustment engine: adjustment factor chains, static forward-adjustment detection, snapshot equivalence checks."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ keywords = ["point-in-time", "back-adjustment", "hfq", "qfq", "corporate-actions", "quant", "pandas-free"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Intended Audience :: Financial and Insurance Industry",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Topic :: Office/Business :: Financial :: Investment",
23
+ "Topic :: Scientific/Engineering",
24
+ ]
25
+
26
+ [project.scripts]
27
+ padj = "pit_adjuster.cli:main"
28
+ pit-adjuster = "pit_adjuster.cli:main"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """pit-adjuster: point-in-time fixed-basis back-adjustment engine.
2
+
3
+ Rebuild daily price history so that any day reads exactly what that day
4
+ could have known: fixed-basis hfq prices from current-vintage qfq bars plus
5
+ a point-in-time corporate-action archive, with verifiable inversion,
6
+ forward-adjustment drift detection and snapshot equivalence checks.
7
+
8
+ Zero dependencies. Python 3.11+.
9
+ """
10
+
11
+ from .chain import build_multipliers, events_from_actions, rebuild_bars
12
+ from .validation import compare_raw_closes, compare_snapshots, validate_inversion
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "build_multipliers",
18
+ "compare_raw_closes",
19
+ "compare_snapshots",
20
+ "events_from_actions",
21
+ "rebuild_bars",
22
+ "validate_inversion",
23
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,184 @@
1
+ """Core adjustment chain: PIT-safe fixed-basis back-adjusted (hfq) prices.
2
+
3
+ Builds a PIT-safe daily history from:
4
+
5
+ - current-vintage forward-adjusted (qfq) daily bars, and
6
+ - a point-in-time corporate-action archive (one ``adjustment_factor`` per
7
+ event).
8
+
9
+ Standard A-share adjustment math (exchange reference-price convention):
10
+
11
+ factor_e = (prior_close - cash) / (prior_close * (1 + bonus + transfer))
12
+ qfq_t = raw_t * prod_{e: ex_date_e > t} factor_e
13
+ hfq_t = raw_t * prod_{e: ex_date_e <= t} (1 / factor_e)
14
+
15
+ The rebuild inverts the vendor qfq chain back to raw prices, then re-applies
16
+ only events whose ex-date is on or before each bar date (fixed basis at the
17
+ archive coverage start). Raw open/close are kept alongside so
18
+ execution-level backtests can map adjusted prices back to nominal prices.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import bisect
24
+ import math
25
+ from typing import Any
26
+
27
+ DEFAULT_VOLUME_TO_SHARES = 100.0
28
+ DEFAULT_NATIVE_SHARE_PREFIXES = ("688", "689")
29
+
30
+
31
+ def events_from_actions(
32
+ actions: list[dict[str, Any]], *, as_of_date: str
33
+ ) -> list[dict[str, Any]]:
34
+ """Return actions sorted by ex-date, keeping only events <= as_of_date.
35
+
36
+ Each returned event carries ``ex_date``, ``factor`` and ``available_at``.
37
+ Invalid records (missing ex-date or non-positive factor) are dropped.
38
+ """
39
+ events: list[dict[str, Any]] = []
40
+ for action in actions or []:
41
+ ex_date = str(action.get("ex_date") or "")
42
+ factor = action.get("adjustment_factor")
43
+ try:
44
+ factor = float(factor)
45
+ except (TypeError, ValueError):
46
+ continue
47
+ if len(ex_date) < 10 or not math.isfinite(factor) or factor <= 0:
48
+ continue
49
+ if ex_date[:10] > as_of_date:
50
+ continue
51
+ events.append(
52
+ {
53
+ "ex_date": ex_date[:10],
54
+ "factor": factor,
55
+ "available_at": str(action.get("available_at") or ""),
56
+ }
57
+ )
58
+ events.sort(key=lambda item: item["ex_date"])
59
+ return events
60
+
61
+
62
+ def build_multipliers(
63
+ events: list[dict[str, Any]],
64
+ ) -> tuple[list[str], list[float], list[float]]:
65
+ """Return ``(ex_dates, hfq_prefix, qfq_suffix)`` cumulative multipliers.
66
+
67
+ ``hfq_prefix[k]`` = prod_{j<k} (1 / factor_j)
68
+ ``qfq_suffix[k]`` = prod_{j>=k} factor_j
69
+ Both arrays have length ``n + 1``; ``k`` is the count of events whose
70
+ ex-date is on or before a bar date (0 .. n).
71
+ """
72
+ ex_dates = [event["ex_date"] for event in events]
73
+ count = len(events)
74
+ hfq_prefix = [1.0] * (count + 1)
75
+ acc = 1.0
76
+ for index, event in enumerate(events):
77
+ acc *= 1.0 / float(event["factor"])
78
+ hfq_prefix[index + 1] = acc
79
+ qfq_suffix = [1.0] * (count + 1)
80
+ acc = 1.0
81
+ for index in range(count - 1, -1, -1):
82
+ acc *= float(events[index]["factor"])
83
+ qfq_suffix[index] = acc
84
+ return ex_dates, hfq_prefix, qfq_suffix
85
+
86
+
87
+ def _bar_date(bar: dict[str, Any]) -> str:
88
+ return str(bar.get("date") or "")[:10]
89
+
90
+
91
+ def _raw_close(bar: dict[str, Any]) -> float | None:
92
+ value = bar.get("raw_close")
93
+ try:
94
+ value = float(value)
95
+ except (TypeError, ValueError):
96
+ return None
97
+ return value if math.isfinite(value) and value > 0 else None
98
+
99
+
100
+ def rebuild_bars(
101
+ bars: list[dict[str, Any]],
102
+ events: list[dict[str, Any]],
103
+ *,
104
+ as_of_date: str,
105
+ code: str | None = None,
106
+ volume_to_shares: float | None = None,
107
+ native_share_prefixes: tuple[str, ...] = DEFAULT_NATIVE_SHARE_PREFIXES,
108
+ ) -> tuple[list[dict[str, Any]], dict[str, int]]:
109
+ """Rebuild one code's bars to fixed-basis hfq (PIT).
110
+
111
+ Returns ``(new_bars, stats)``. Every valid bar gets ``open/high/low/
112
+ close`` as hfq prices, ``raw_open/raw_close`` as nominal prices, and
113
+ ``adj_factor`` as the cumulative hfq multiplier. Volume is converted to
114
+ shares: codes whose prefix is in ``native_share_prefixes`` already store
115
+ shares (no conversion), all other codes use ``volume_to_shares``
116
+ (default: 1 lot = 100 shares). Amount, turnover and any extra fields
117
+ are preserved.
118
+
119
+ The PIT property: a price at time ``t`` is a function only of events
120
+ whose ex-date is on or before ``t`` (fixed basis at archive coverage
121
+ start).
122
+ """
123
+ effective_multiplier = (
124
+ volume_to_shares
125
+ if volume_to_shares is not None
126
+ else (
127
+ 1.0
128
+ if str(code or "").startswith(native_share_prefixes)
129
+ else DEFAULT_VOLUME_TO_SHARES
130
+ )
131
+ )
132
+ events = events_from_actions(events, as_of_date=as_of_date)
133
+ ex_dates, hfq_prefix, qfq_suffix = build_multipliers(events)
134
+ new_bars: list[dict[str, Any]] = []
135
+ stats: dict[str, int] = {"bars": 0, "invalid_bars": 0}
136
+ for bar in bars or []:
137
+ date_text = _bar_date(bar)
138
+ if not date_text:
139
+ stats["invalid_bars"] += 1
140
+ continue
141
+ close = bar.get("close")
142
+ try:
143
+ close_f = float(close)
144
+ except (TypeError, ValueError):
145
+ close_f = math.nan
146
+ valid = math.isfinite(close_f) and close_f > 0
147
+ k = bisect.bisect_right(ex_dates, date_text) if ex_dates else 0
148
+ qfq_mult = qfq_suffix[k] if ex_dates else 1.0
149
+ hfq_mult = hfq_prefix[k] if ex_dates else 1.0
150
+ rebuilt = dict(bar)
151
+ try:
152
+ volume_f = float(bar.get("volume") or 0.0)
153
+ except (TypeError, ValueError):
154
+ volume_f = math.nan
155
+ if math.isfinite(volume_f) and volume_f > 0:
156
+ rebuilt["volume"] = round(volume_f * effective_multiplier, 2)
157
+ else:
158
+ rebuilt["volume"] = volume_f if math.isfinite(volume_f) else None
159
+ if valid:
160
+ raw_close = close_f / qfq_mult
161
+ rebuilt["raw_close"] = round(raw_close, 6)
162
+ rebuilt["adj_factor"] = round(hfq_mult, 12)
163
+ for key in ("open", "high", "low"):
164
+ value = bar.get(key)
165
+ try:
166
+ value_f = float(value)
167
+ except (TypeError, ValueError):
168
+ value_f = math.nan
169
+ if math.isfinite(value_f) and value_f > 0:
170
+ raw_value = value_f / qfq_mult
171
+ rebuilt[key] = round(raw_value * hfq_mult, 6)
172
+ if key == "open":
173
+ rebuilt["raw_open"] = round(raw_value, 6)
174
+ else:
175
+ rebuilt[key] = None
176
+ rebuilt["close"] = round(raw_close * hfq_mult, 6)
177
+ stats["bars"] += 1
178
+ else:
179
+ rebuilt["raw_close"] = None
180
+ rebuilt["raw_open"] = None
181
+ rebuilt["adj_factor"] = 1.0
182
+ stats["invalid_bars"] += 1
183
+ new_bars.append(rebuilt)
184
+ return new_bars, stats