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.
- pit_adjuster-0.1.0/LICENSE +21 -0
- pit_adjuster-0.1.0/PKG-INFO +210 -0
- pit_adjuster-0.1.0/README.md +189 -0
- pit_adjuster-0.1.0/pyproject.toml +34 -0
- pit_adjuster-0.1.0/setup.cfg +4 -0
- pit_adjuster-0.1.0/src/pit_adjuster/__init__.py +23 -0
- pit_adjuster-0.1.0/src/pit_adjuster/__main__.py +4 -0
- pit_adjuster-0.1.0/src/pit_adjuster/chain.py +184 -0
- pit_adjuster-0.1.0/src/pit_adjuster/cli.py +146 -0
- pit_adjuster-0.1.0/src/pit_adjuster/io.py +62 -0
- pit_adjuster-0.1.0/src/pit_adjuster/validation.py +178 -0
- pit_adjuster-0.1.0/src/pit_adjuster.egg-info/PKG-INFO +210 -0
- pit_adjuster-0.1.0/src/pit_adjuster.egg-info/SOURCES.txt +17 -0
- pit_adjuster-0.1.0/src/pit_adjuster.egg-info/dependency_links.txt +1 -0
- pit_adjuster-0.1.0/src/pit_adjuster.egg-info/entry_points.txt +3 -0
- pit_adjuster-0.1.0/src/pit_adjuster.egg-info/top_level.txt +1 -0
- pit_adjuster-0.1.0/tests/test_chain.py +143 -0
- pit_adjuster-0.1.0/tests/test_cli.py +111 -0
- pit_adjuster-0.1.0/tests/test_validation.py +114 -0
|
@@ -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
|
+

|
|
30
|
+

|
|
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
|
+

|
|
9
|
+

|
|
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,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,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
|