edgar-pit-clock 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 Biwada LLC (Starling Quantitative — https://starlingquant.com)
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,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: edgar-pit-clock
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency point-in-time SEC EDGAR acceptanceDateTime to NYSE trading-session router, One-Document-One-Witness accession normalizer, and Purged Walk-Forward splitter.
5
+ Author-email: Starling Quantitative Research <founder@starlingquant.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://starlingquant.com
8
+ Project-URL: Repository, https://github.com/starlingquant/edgar-pit-clock
9
+ Project-URL: Documentation, https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels
10
+ Project-URL: Scorecard, https://starlingquant.com/scorecard
11
+ Project-URL: MachineReference, https://starlingquant.com/llms-full.txt
12
+ Keywords: quantitative-finance,sec-edgar,point-in-time,alternative-data,lookahead-bias,walk-forward,market-microstructure
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ # `edgar-pit-clock` — Point-in-Time SEC EDGAR Timestamp & Session Router
29
+
30
+ **Zero-dependency Python utility for eliminating the `4:15 PM` SEC `filingDate` lookahead trap, normalizing `One-Document-One-Witness` EDGAR accession provenance, and enforcing Purged Walk-Forward Cross-Validation embargoes.**
31
+
32
+ Built and maintained by **[Starling Quantitative Research](https://starlingquant.com)** (*"They Trade the Headlines. We Trade the Fine Print."*).
33
+
34
+ ---
35
+
36
+ ## Why This Exists: The `4:15 PM` Filing Trap
37
+
38
+ Naive equity backtests align corporate disclosures (`10-K`, `10-Q`, `8-K`, `Form 4`, `Schedule 13D`) by fiscal period-end (`fp`) or calendar `filingDate`. Even `filingDate` leaks a full trading session of lookahead bias whenever a company files after the `16:00:00 ET` closing bell (for example, at `16:15:04 ET`), because a daily close-to-close backtest assumes execution at `16:00:00 ET`—fifteen minutes before the filing hit EDGAR.
39
+
40
+ `edgar-pit-clock` enforces strict point-in-time filtration using down-to-the-second SEC server `acceptanceDateTime` across the full NYSE holiday calendar:
41
+
42
+ | Filing `acceptanceDateTime` (`ET`) | `MarketSession` | Intraday Daemon | Daily Batch Replay |
43
+ | :--- | :--- | :--- | :--- |
44
+ | `< 09:30:00 ET` (NYSE Trading Day) | `PRE_MARKET` | Wait for `Open(T_0)` | `Open(T_0)` |
45
+ | `09:30:00 – 15:59:59 ET` | `REGULAR_SESSION` | Live `ArrivalMid` | `Open(T+1)` *(Conservative)* |
46
+ | `>= 16:00:00 ET` | `AFTER_HOURS` | Next Session `Open(T+1)` | `Open(T+1)` |
47
+ | Weekend / NYSE Holiday | `NON_TRADING_DAY` | Next Session `Open(T+1)` | `Open(T+1)` |
48
+
49
+ ---
50
+
51
+ ## Quickstart
52
+
53
+ ```bash
54
+ pip install edgar-pit-clock
55
+ ```
56
+
57
+ ```python
58
+ from edgar_pit_clock import (
59
+ align_filing_to_market_bar,
60
+ normalize_cik10,
61
+ normalize_sec_artifact_id,
62
+ purged_walk_forward_splits,
63
+ )
64
+
65
+ # 1. Friday 16:15:04 ET Form 8-K -> Routes cleanly to Monday Open (never Friday Close)
66
+ bar = align_filing_to_market_bar("2026-11-13T16:15:04-05:00")
67
+ print(bar.market_session, bar.daily_batch_bar_date, bar.daily_batch_price_field)
68
+ # -> MarketSession.AFTER_HOURS 2026-11-16 OPEN
69
+
70
+ # 2. One-Document-One-Witness provenance key (prevents multi-parser double-counting)
71
+ doc_id = normalize_sec_artifact_id("0000320193-26-000077")
72
+ cik = normalize_cik10(320193)
73
+ # -> doc_id == "SEC:000032019326000077", cik == "0000320193"
74
+ ```
75
+
76
+ ---
77
+
78
+ ## About Starling Quantitative
79
+
80
+ **[Starling Quantitative](https://starlingquant.com)** is the systematic U.S. equity research and capital allocation desk of Biwada LLC. Starling runs two layers of **100% deterministic, zero-LLM** U.S. equity scouts under a single non-parametric calibration and risk engine:
81
+
82
+ 1. **The Proven Foundation (Point-in-Time Filing Scouts):** Rule-based scouts extracting factor-neutralized alpha from primary SEC, FDA, and patent disclosures across `3,607` survivorship-free Russell 3000 CIKs (`555` delisted retained; `+61.8%` OOS `2022–2026` unlevered vs. `SPY +54.3%`).
83
+ 2. **The Experiment Layer (Darwinian Alt-Data Scouts):** A live survival-of-the-fittest ecosystem tracking ephemeral, differentiated alternative data feeds behind a One-Way Shadow/Canary Glass Wall—starting at `$0` in shadow and earning or losing capital automatically on live out-of-sample attribution.
84
+
85
+ ### Open Research Monographs & Verification
86
+ - **Research Note RN-01:** [Point-in-Time Alternative Data Ingestion, Entity Resolution & Survivorship-Free Panel Construction](https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels) ([PDF](https://starlingquant.com/papers/RN-01-Point-in-Time-Alternative-Data-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222468](https://doi.org/10.5281/zenodo.24222468))
87
+ - **Research Note RN-02:** [Non-Parametric Signal Calibration, Orthogonal Pillar Pooling & One-Way Statutory Distress Vetoes](https://starlingquant.com/guides/non-parametric-signal-calibration-and-orthogonal-pillar-pooling) ([PDF](https://starlingquant.com/papers/RN-02-Non-Parametric-Signal-Calibration-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222814](https://doi.org/10.5281/zenodo.24222814))
88
+ - **Research Note RN-03:** [Six-Factor Residual Attribution, Purged Walk-Forward Validation & Volatility-Parity Risk Governance](https://starlingquant.com/guides/multi-agent-quorum-and-systematic-factor-consensus) ([PDF](https://starlingquant.com/papers/RN-03-Six-Factor-Residual-Attribution-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222816](https://doi.org/10.5281/zenodo.24222816))
89
+ - **Live Forward Attribution Scorecard:** [https://starlingquant.com/scorecard](https://starlingquant.com/scorecard)
90
+ - **Machine-Readable Specification:** [https://starlingquant.com/llms-full.txt](https://starlingquant.com/llms-full.txt)
91
+
92
+ *Disclosure: Starling Quantitative trades proprietary capital and conducts selective institutional research evaluation under mutual NDA. Nothing in this repository constitutes investment advice or an offer to solicit retail capital.*
@@ -0,0 +1,65 @@
1
+ # `edgar-pit-clock` — Point-in-Time SEC EDGAR Timestamp & Session Router
2
+
3
+ **Zero-dependency Python utility for eliminating the `4:15 PM` SEC `filingDate` lookahead trap, normalizing `One-Document-One-Witness` EDGAR accession provenance, and enforcing Purged Walk-Forward Cross-Validation embargoes.**
4
+
5
+ Built and maintained by **[Starling Quantitative Research](https://starlingquant.com)** (*"They Trade the Headlines. We Trade the Fine Print."*).
6
+
7
+ ---
8
+
9
+ ## Why This Exists: The `4:15 PM` Filing Trap
10
+
11
+ Naive equity backtests align corporate disclosures (`10-K`, `10-Q`, `8-K`, `Form 4`, `Schedule 13D`) by fiscal period-end (`fp`) or calendar `filingDate`. Even `filingDate` leaks a full trading session of lookahead bias whenever a company files after the `16:00:00 ET` closing bell (for example, at `16:15:04 ET`), because a daily close-to-close backtest assumes execution at `16:00:00 ET`—fifteen minutes before the filing hit EDGAR.
12
+
13
+ `edgar-pit-clock` enforces strict point-in-time filtration using down-to-the-second SEC server `acceptanceDateTime` across the full NYSE holiday calendar:
14
+
15
+ | Filing `acceptanceDateTime` (`ET`) | `MarketSession` | Intraday Daemon | Daily Batch Replay |
16
+ | :--- | :--- | :--- | :--- |
17
+ | `< 09:30:00 ET` (NYSE Trading Day) | `PRE_MARKET` | Wait for `Open(T_0)` | `Open(T_0)` |
18
+ | `09:30:00 – 15:59:59 ET` | `REGULAR_SESSION` | Live `ArrivalMid` | `Open(T+1)` *(Conservative)* |
19
+ | `>= 16:00:00 ET` | `AFTER_HOURS` | Next Session `Open(T+1)` | `Open(T+1)` |
20
+ | Weekend / NYSE Holiday | `NON_TRADING_DAY` | Next Session `Open(T+1)` | `Open(T+1)` |
21
+
22
+ ---
23
+
24
+ ## Quickstart
25
+
26
+ ```bash
27
+ pip install edgar-pit-clock
28
+ ```
29
+
30
+ ```python
31
+ from edgar_pit_clock import (
32
+ align_filing_to_market_bar,
33
+ normalize_cik10,
34
+ normalize_sec_artifact_id,
35
+ purged_walk_forward_splits,
36
+ )
37
+
38
+ # 1. Friday 16:15:04 ET Form 8-K -> Routes cleanly to Monday Open (never Friday Close)
39
+ bar = align_filing_to_market_bar("2026-11-13T16:15:04-05:00")
40
+ print(bar.market_session, bar.daily_batch_bar_date, bar.daily_batch_price_field)
41
+ # -> MarketSession.AFTER_HOURS 2026-11-16 OPEN
42
+
43
+ # 2. One-Document-One-Witness provenance key (prevents multi-parser double-counting)
44
+ doc_id = normalize_sec_artifact_id("0000320193-26-000077")
45
+ cik = normalize_cik10(320193)
46
+ # -> doc_id == "SEC:000032019326000077", cik == "0000320193"
47
+ ```
48
+
49
+ ---
50
+
51
+ ## About Starling Quantitative
52
+
53
+ **[Starling Quantitative](https://starlingquant.com)** is the systematic U.S. equity research and capital allocation desk of Biwada LLC. Starling runs two layers of **100% deterministic, zero-LLM** U.S. equity scouts under a single non-parametric calibration and risk engine:
54
+
55
+ 1. **The Proven Foundation (Point-in-Time Filing Scouts):** Rule-based scouts extracting factor-neutralized alpha from primary SEC, FDA, and patent disclosures across `3,607` survivorship-free Russell 3000 CIKs (`555` delisted retained; `+61.8%` OOS `2022–2026` unlevered vs. `SPY +54.3%`).
56
+ 2. **The Experiment Layer (Darwinian Alt-Data Scouts):** A live survival-of-the-fittest ecosystem tracking ephemeral, differentiated alternative data feeds behind a One-Way Shadow/Canary Glass Wall—starting at `$0` in shadow and earning or losing capital automatically on live out-of-sample attribution.
57
+
58
+ ### Open Research Monographs & Verification
59
+ - **Research Note RN-01:** [Point-in-Time Alternative Data Ingestion, Entity Resolution & Survivorship-Free Panel Construction](https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels) ([PDF](https://starlingquant.com/papers/RN-01-Point-in-Time-Alternative-Data-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222468](https://doi.org/10.5281/zenodo.24222468))
60
+ - **Research Note RN-02:** [Non-Parametric Signal Calibration, Orthogonal Pillar Pooling & One-Way Statutory Distress Vetoes](https://starlingquant.com/guides/non-parametric-signal-calibration-and-orthogonal-pillar-pooling) ([PDF](https://starlingquant.com/papers/RN-02-Non-Parametric-Signal-Calibration-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222814](https://doi.org/10.5281/zenodo.24222814))
61
+ - **Research Note RN-03:** [Six-Factor Residual Attribution, Purged Walk-Forward Validation & Volatility-Parity Risk Governance](https://starlingquant.com/guides/multi-agent-quorum-and-systematic-factor-consensus) ([PDF](https://starlingquant.com/papers/RN-03-Six-Factor-Residual-Attribution-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222816](https://doi.org/10.5281/zenodo.24222816))
62
+ - **Live Forward Attribution Scorecard:** [https://starlingquant.com/scorecard](https://starlingquant.com/scorecard)
63
+ - **Machine-Readable Specification:** [https://starlingquant.com/llms-full.txt](https://starlingquant.com/llms-full.txt)
64
+
65
+ *Disclosure: Starling Quantitative trades proprietary capital and conducts selective institutional research evaluation under mutual NDA. Nothing in this repository constitutes investment advice or an offer to solicit retail capital.*
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "edgar-pit-clock"
7
+ version = "0.1.0"
8
+ description = "Zero-dependency point-in-time SEC EDGAR acceptanceDateTime to NYSE trading-session router, One-Document-One-Witness accession normalizer, and Purged Walk-Forward splitter."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Starling Quantitative Research", email = "founder@starlingquant.com" }
14
+ ]
15
+ keywords = [
16
+ "quantitative-finance",
17
+ "sec-edgar",
18
+ "point-in-time",
19
+ "alternative-data",
20
+ "lookahead-bias",
21
+ "walk-forward",
22
+ "market-microstructure"
23
+ ]
24
+ classifiers = [
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Financial and Insurance Industry",
27
+ "Intended Audience :: Science/Research",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Topic :: Office/Business :: Financial :: Investment",
34
+ "Typing :: Typed"
35
+ ]
36
+
37
+ [project.urls]
38
+ Homepage = "https://starlingquant.com"
39
+ Repository = "https://github.com/starlingquant/edgar-pit-clock"
40
+ Documentation = "https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels"
41
+ Scorecard = "https://starlingquant.com/scorecard"
42
+ MachineReference = "https://starlingquant.com/llms-full.txt"
43
+
44
+ [tool.setuptools.packages.find]
45
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,36 @@
1
+ """
2
+ edgar_pit_clock — Deterministic Point-in-Time SEC EDGAR Timestamp & Session Router.
3
+
4
+ Maintained by Starling Quantitative Research (https://starlingquant.com).
5
+ Zero external dependencies; 100% deterministic Python standard library.
6
+ """
7
+
8
+ from .clock import (
9
+ MarketSession,
10
+ PointInTimeAlignment,
11
+ PurgedFold,
12
+ add_nyse_trading_days,
13
+ align_filing_to_market_bar,
14
+ is_nyse_trading_day,
15
+ next_nyse_trading_day,
16
+ normalize_cik10,
17
+ normalize_sec_artifact_id,
18
+ parse_edgar_acceptance,
19
+ purged_walk_forward_splits,
20
+ )
21
+
22
+ __version__ = "0.1.0"
23
+
24
+ __all__ = [
25
+ "MarketSession",
26
+ "PointInTimeAlignment",
27
+ "PurgedFold",
28
+ "add_nyse_trading_days",
29
+ "align_filing_to_market_bar",
30
+ "is_nyse_trading_day",
31
+ "next_nyse_trading_day",
32
+ "normalize_cik10",
33
+ "normalize_sec_artifact_id",
34
+ "parse_edgar_acceptance",
35
+ "purged_walk_forward_splits",
36
+ ]
@@ -0,0 +1,329 @@
1
+ """
2
+ Core point-in-time SEC EDGAR timestamp alignment, NYSE holiday calendar,
3
+ One-Document-One-Witness accession normalization, and Purged Walk-Forward CV splitting.
4
+
5
+ Reference:
6
+ Starling Quantitative Research Note RN-01:
7
+ "Point-in-Time Alternative Data Ingestion, Entity Resolution & Survivorship-Free Panel Construction"
8
+ https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass
15
+ from datetime import date, datetime, time, timedelta, timezone
16
+ from enum import Enum
17
+ from typing import Iterable, Sequence
18
+ from zoneinfo import ZoneInfo
19
+
20
+ ET = ZoneInfo("America/New_York")
21
+ MARKET_OPEN = time(9, 30, 0)
22
+ MARKET_CLOSE = time(16, 0, 0)
23
+
24
+ _DIGITS_14_RE = re.compile(r"^\d{14}$")
25
+ _ACCESSION_RE = re.compile(r"(\d{10})-?(\d{2})-?(\d{6})")
26
+
27
+
28
+ class MarketSession(str, Enum):
29
+ PRE_MARKET = "PRE_MARKET"
30
+ REGULAR_SESSION = "REGULAR_SESSION"
31
+ AFTER_HOURS = "AFTER_HOURS"
32
+ NON_TRADING_DAY = "NON_TRADING_DAY"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class PointInTimeAlignment:
37
+ acceptance_et: datetime
38
+ acceptance_utc: datetime
39
+ market_session: MarketSession
40
+ daily_batch_bar_date: date
41
+ daily_batch_price_field: str
42
+ intraday_eligible: bool
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class PurgedFold:
47
+ fold_index: int
48
+ train_indices: tuple[int, ...]
49
+ test_indices: tuple[int, ...]
50
+ train_end_date: date
51
+ test_start_date: date
52
+ test_end_date: date
53
+ purged_count: int
54
+
55
+
56
+ def _easter_sunday(year: int) -> date:
57
+ """Anonymous Gregorian computus for Western Easter Sunday."""
58
+ a = year % 19
59
+ b = year // 100
60
+ c = year % 100
61
+ d = b // 4
62
+ e = b % 4
63
+ f = (b + 8) // 25
64
+ g = (b - f + 1) // 3
65
+ h = (19 * a + b - d - g + 15) % 30
66
+ i = c // 4
67
+ k = c % 4
68
+ l_val = (32 + 2 * e + 2 * i - h - k) % 7
69
+ m = (a + 11 * h + 22 * l_val) // 451
70
+ month = (h + l_val - 7 * m + 114) // 31
71
+ day = ((h + l_val - 7 * m + 114) % 31) + 1
72
+ return date(year, month, day)
73
+
74
+
75
+ def _nth_weekday(year: int, month: int, weekday: int, n: int) -> date:
76
+ first = date(year, month, 1)
77
+ offset = (weekday - first.weekday()) % 7
78
+ return first + timedelta(days=offset + 7 * (n - 1))
79
+
80
+
81
+ def _last_weekday(year: int, month: int, weekday: int) -> date:
82
+ if month == 12:
83
+ nxt = date(year + 1, 1, 1)
84
+ else:
85
+ nxt = date(year, month + 1, 1)
86
+ last = nxt - timedelta(days=1)
87
+ offset = (last.weekday() - weekday) % 7
88
+ return last - timedelta(days=offset)
89
+
90
+
91
+ def _observed_fixed_holiday(d: date) -> date:
92
+ if d.weekday() == 5: # Saturday -> Friday
93
+ return d - timedelta(days=1)
94
+ if d.weekday() == 6: # Sunday -> Monday
95
+ return d + timedelta(days=1)
96
+ return d
97
+
98
+
99
+ def _nyse_holidays_for_year(year: int) -> set[date]:
100
+ holidays: set[date] = set()
101
+ for fixed in (
102
+ date(year, 1, 1),
103
+ date(year + 1, 1, 1), # Handles Jan 1 observed on Dec 31
104
+ date(year, 7, 4),
105
+ date(year, 12, 25),
106
+ ):
107
+ obs = _observed_fixed_holiday(fixed)
108
+ if obs.year == year:
109
+ holidays.add(obs)
110
+
111
+ if year >= 2022:
112
+ obs_june = _observed_fixed_holiday(date(year, 6, 19))
113
+ holidays.add(obs_june)
114
+
115
+ holidays.add(_nth_weekday(year, 1, 0, 3)) # MLK Jr. Day (3rd Mon Jan)
116
+ holidays.add(_nth_weekday(year, 2, 0, 3)) # Presidents' Day (3rd Mon Feb)
117
+ holidays.add(_easter_sunday(year) - timedelta(days=2)) # Good Friday
118
+ holidays.add(_last_weekday(year, 5, 0)) # Memorial Day (Last Mon May)
119
+ holidays.add(_nth_weekday(year, 9, 0, 1)) # Labor Day (1st Mon Sep)
120
+ holidays.add(_nth_weekday(year, 11, 3, 4)) # Thanksgiving (4th Thu Nov)
121
+ return holidays
122
+
123
+
124
+ def is_nyse_trading_day(d: date) -> bool:
125
+ """Returns True if `d` is a regular weekday NYSE session."""
126
+ if isinstance(d, datetime):
127
+ d = d.date()
128
+ if d.weekday() >= 5:
129
+ return False
130
+ return d not in _nyse_holidays_for_year(d.year)
131
+
132
+
133
+ def next_nyse_trading_day(d: date, *, inclusive: bool = False) -> date:
134
+ """Advances `d` to the next valid NYSE trading session."""
135
+ if isinstance(d, datetime):
136
+ d = d.date()
137
+ cur = d if inclusive else d + timedelta(days=1)
138
+ while not is_nyse_trading_day(cur):
139
+ cur += timedelta(days=1)
140
+ return cur
141
+
142
+
143
+ def add_nyse_trading_days(d: date, n_days: int) -> date:
144
+ """Adds `n_days >= 0` NYSE trading sessions to `d`."""
145
+ if n_days < 0:
146
+ raise ValueError("n_days must be >= 0")
147
+ cur = next_nyse_trading_day(d, inclusive=True)
148
+ for _ in range(n_days):
149
+ cur = next_nyse_trading_day(cur, inclusive=False)
150
+ return cur
151
+
152
+
153
+ def parse_edgar_acceptance(raw_ts: str | datetime) -> datetime:
154
+ """
155
+ Parses SEC EDGAR `acceptanceDateTime` into timezone-aware America/New_York (`ET`).
156
+
157
+ Supports:
158
+ - 14-digit EDGAR submission strings (`"20261113161504"`), which SEC EDGAR stamps in Eastern Time
159
+ - ISO-8601 strings with `Z` or explicit offset (`"2026-11-13T21:15:04.000Z"` or `"-05:00"`)
160
+ - Naive ISO-8601 strings from EDGAR `submissions` JSON (`"2026-11-13T16:15:04"`), interpreted as `ET`
161
+ """
162
+ if isinstance(raw_ts, datetime):
163
+ if raw_ts.tzinfo is None:
164
+ return raw_ts.replace(tzinfo=ET)
165
+ return raw_ts.astimezone(ET)
166
+
167
+ s = str(raw_ts).strip()
168
+ if not s:
169
+ raise ValueError("Empty acceptanceDateTime")
170
+
171
+ if _DIGITS_14_RE.match(s):
172
+ dt_naive = datetime.strptime(s, "%Y%m%d%H%M%S")
173
+ return dt_naive.replace(tzinfo=ET)
174
+
175
+ if s.endswith("Z") or s.endswith("z"):
176
+ s = s[:-1] + "+00:00"
177
+
178
+ parsed = datetime.fromisoformat(s)
179
+ if parsed.tzinfo is None:
180
+ return parsed.replace(tzinfo=ET)
181
+ return parsed.astimezone(ET)
182
+
183
+
184
+ def align_filing_to_market_bar(raw_acceptance_ts: str | datetime) -> PointInTimeAlignment:
185
+ """
186
+ Routes an SEC filing's `acceptanceDateTime` to its earliest lookahead-free executable bar:
187
+ - `< 09:30:00 ET` on a trading day -> `PRE_MARKET`, daily batch `Open(T_0)`
188
+ - `09:30:00 <= t < 16:00:00 ET` -> `REGULAR_SESSION`, intraday `ArrivalMid` eligible, daily batch `Open(T+1)`
189
+ - `>= 16:00:00 ET` -> `AFTER_HOURS`, strictly `Open(T+1)` (eliminates the 4:15 PM filing trap)
190
+ - Weekend / NYSE Holiday -> `NON_TRADING_DAY`, strictly `Open(next_trading_day)`
191
+ """
192
+ accept_et = parse_edgar_acceptance(raw_acceptance_ts)
193
+ accept_utc = accept_et.astimezone(timezone.utc)
194
+ cal_date = accept_et.date()
195
+ tod = accept_et.timetz().replace(tzinfo=None)
196
+
197
+ if not is_nyse_trading_day(cal_date):
198
+ return PointInTimeAlignment(
199
+ acceptance_et=accept_et,
200
+ acceptance_utc=accept_utc,
201
+ market_session=MarketSession.NON_TRADING_DAY,
202
+ daily_batch_bar_date=next_nyse_trading_day(cal_date, inclusive=False),
203
+ daily_batch_price_field="OPEN",
204
+ intraday_eligible=False,
205
+ )
206
+
207
+ if tod < MARKET_OPEN:
208
+ return PointInTimeAlignment(
209
+ acceptance_et=accept_et,
210
+ acceptance_utc=accept_utc,
211
+ market_session=MarketSession.PRE_MARKET,
212
+ daily_batch_bar_date=cal_date,
213
+ daily_batch_price_field="OPEN",
214
+ intraday_eligible=False,
215
+ )
216
+
217
+ if tod < MARKET_CLOSE:
218
+ return PointInTimeAlignment(
219
+ acceptance_et=accept_et,
220
+ acceptance_utc=accept_utc,
221
+ market_session=MarketSession.REGULAR_SESSION,
222
+ daily_batch_bar_date=next_nyse_trading_day(cal_date, inclusive=False),
223
+ daily_batch_price_field="OPEN",
224
+ intraday_eligible=True,
225
+ )
226
+
227
+ return PointInTimeAlignment(
228
+ acceptance_et=accept_et,
229
+ acceptance_utc=accept_utc,
230
+ market_session=MarketSession.AFTER_HOURS,
231
+ daily_batch_bar_date=next_nyse_trading_day(cal_date, inclusive=False),
232
+ daily_batch_price_field="OPEN",
233
+ intraday_eligible=False,
234
+ )
235
+
236
+
237
+ def normalize_cik10(cik: str | int) -> str:
238
+ """Formats an SEC Central Index Key (`CIK`) to canonical 10-digit zero-padded string."""
239
+ digits = re.sub(r"\D", "", str(cik).strip())
240
+ if not digits or len(digits) > 10 or int(digits) == 0:
241
+ raise ValueError(f"Invalid SEC CIK: {cik!r}")
242
+ return digits.zfill(10)
243
+
244
+
245
+ def normalize_sec_artifact_id(accession_or_url: str) -> str:
246
+ """
247
+ Canonicalizes any SEC accession string or EDGAR archive URL into a
248
+ One-Document-One-Witness identifier (`SEC:<18-digit-accession>`) so
249
+ multiple parsers reading the same filing cannot manufacture false confluence.
250
+ """
251
+ m = _ACCESSION_RE.search(str(accession_or_url))
252
+ if not m:
253
+ raise ValueError(f"Could not extract 18-digit SEC accession from {accession_or_url!r}")
254
+ return f"SEC:{m.group(1)}{m.group(2)}{m.group(3)}"
255
+
256
+
257
+ def purged_walk_forward_splits(
258
+ entry_dates: Sequence[date],
259
+ *,
260
+ n_splits: int = 5,
261
+ horizon_trading_days: int = 10,
262
+ settlement_embargo_days: int = 2,
263
+ min_train_events: int = 30,
264
+ ) -> list[PurgedFold]:
265
+ """
266
+ Anchored Purged Walk-Forward Cross-Validation splitter (Lopez de Prado, 2018)
267
+ enforcing a trailing purge + settlement embargo gap so zero overlapping forward
268
+ return windows leak from the training set into the out-of-sample fold.
269
+ """
270
+ if n_splits < 2:
271
+ raise ValueError("n_splits must be >= 2")
272
+ n = len(entry_dates)
273
+ if n <= min_train_events:
274
+ raise ValueError(f"Need > {min_train_events} events, got {n}")
275
+
276
+ order = sorted(range(n), key=lambda idx: entry_dates[idx])
277
+ sorted_dates = [entry_dates[idx] for idx in order]
278
+ total_purge_days = horizon_trading_days + settlement_embargo_days
279
+
280
+ unique_dates = sorted(set(sorted_dates))
281
+ if len(unique_dates) < n_splits + 1:
282
+ raise ValueError("Insufficient distinct trading dates for requested n_splits")
283
+
284
+ # Reserve Chunk 0 as initial calibration warmup; evaluate Folds 1..n_splits out-of-sample
285
+ chunk_count = n_splits + 1
286
+ chunk_size = max(1, len(unique_dates) // chunk_count)
287
+ folds: list[PurgedFold] = []
288
+
289
+ for fold_idx in range(1, chunk_count):
290
+ test_start_dt = unique_dates[fold_idx * chunk_size]
291
+ test_end_dt = (
292
+ unique_dates[-1]
293
+ if fold_idx == chunk_count - 1
294
+ else unique_dates[(fold_idx + 1) * chunk_size - 1]
295
+ )
296
+
297
+ test_indices = tuple(
298
+ orig_i for orig_i, dt in zip(order, sorted_dates) if test_start_dt <= dt <= test_end_dt
299
+ )
300
+ if not test_indices:
301
+ continue
302
+
303
+ train_candidates: list[int] = []
304
+ purged_count = 0
305
+ for orig_i, dt in zip(order, sorted_dates):
306
+ if dt >= test_start_dt:
307
+ break
308
+ matured_dt = add_nyse_trading_days(dt, total_purge_days)
309
+ if matured_dt <= test_start_dt:
310
+ train_candidates.append(orig_i)
311
+ else:
312
+ purged_count += 1
313
+
314
+ if len(train_candidates) < min_train_events:
315
+ continue
316
+
317
+ train_end_dt = max(entry_dates[i] for i in train_candidates)
318
+ folds.append(
319
+ PurgedFold(
320
+ fold_index=fold_idx,
321
+ train_indices=tuple(train_candidates),
322
+ test_indices=test_indices,
323
+ train_end_date=train_end_dt,
324
+ test_start_date=test_start_dt,
325
+ test_end_date=test_end_dt,
326
+ purged_count=purged_count,
327
+ )
328
+ )
329
+ return folds
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: edgar-pit-clock
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency point-in-time SEC EDGAR acceptanceDateTime to NYSE trading-session router, One-Document-One-Witness accession normalizer, and Purged Walk-Forward splitter.
5
+ Author-email: Starling Quantitative Research <founder@starlingquant.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://starlingquant.com
8
+ Project-URL: Repository, https://github.com/starlingquant/edgar-pit-clock
9
+ Project-URL: Documentation, https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels
10
+ Project-URL: Scorecard, https://starlingquant.com/scorecard
11
+ Project-URL: MachineReference, https://starlingquant.com/llms-full.txt
12
+ Keywords: quantitative-finance,sec-edgar,point-in-time,alternative-data,lookahead-bias,walk-forward,market-microstructure
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Office/Business :: Financial :: Investment
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Dynamic: license-file
27
+
28
+ # `edgar-pit-clock` — Point-in-Time SEC EDGAR Timestamp & Session Router
29
+
30
+ **Zero-dependency Python utility for eliminating the `4:15 PM` SEC `filingDate` lookahead trap, normalizing `One-Document-One-Witness` EDGAR accession provenance, and enforcing Purged Walk-Forward Cross-Validation embargoes.**
31
+
32
+ Built and maintained by **[Starling Quantitative Research](https://starlingquant.com)** (*"They Trade the Headlines. We Trade the Fine Print."*).
33
+
34
+ ---
35
+
36
+ ## Why This Exists: The `4:15 PM` Filing Trap
37
+
38
+ Naive equity backtests align corporate disclosures (`10-K`, `10-Q`, `8-K`, `Form 4`, `Schedule 13D`) by fiscal period-end (`fp`) or calendar `filingDate`. Even `filingDate` leaks a full trading session of lookahead bias whenever a company files after the `16:00:00 ET` closing bell (for example, at `16:15:04 ET`), because a daily close-to-close backtest assumes execution at `16:00:00 ET`—fifteen minutes before the filing hit EDGAR.
39
+
40
+ `edgar-pit-clock` enforces strict point-in-time filtration using down-to-the-second SEC server `acceptanceDateTime` across the full NYSE holiday calendar:
41
+
42
+ | Filing `acceptanceDateTime` (`ET`) | `MarketSession` | Intraday Daemon | Daily Batch Replay |
43
+ | :--- | :--- | :--- | :--- |
44
+ | `< 09:30:00 ET` (NYSE Trading Day) | `PRE_MARKET` | Wait for `Open(T_0)` | `Open(T_0)` |
45
+ | `09:30:00 – 15:59:59 ET` | `REGULAR_SESSION` | Live `ArrivalMid` | `Open(T+1)` *(Conservative)* |
46
+ | `>= 16:00:00 ET` | `AFTER_HOURS` | Next Session `Open(T+1)` | `Open(T+1)` |
47
+ | Weekend / NYSE Holiday | `NON_TRADING_DAY` | Next Session `Open(T+1)` | `Open(T+1)` |
48
+
49
+ ---
50
+
51
+ ## Quickstart
52
+
53
+ ```bash
54
+ pip install edgar-pit-clock
55
+ ```
56
+
57
+ ```python
58
+ from edgar_pit_clock import (
59
+ align_filing_to_market_bar,
60
+ normalize_cik10,
61
+ normalize_sec_artifact_id,
62
+ purged_walk_forward_splits,
63
+ )
64
+
65
+ # 1. Friday 16:15:04 ET Form 8-K -> Routes cleanly to Monday Open (never Friday Close)
66
+ bar = align_filing_to_market_bar("2026-11-13T16:15:04-05:00")
67
+ print(bar.market_session, bar.daily_batch_bar_date, bar.daily_batch_price_field)
68
+ # -> MarketSession.AFTER_HOURS 2026-11-16 OPEN
69
+
70
+ # 2. One-Document-One-Witness provenance key (prevents multi-parser double-counting)
71
+ doc_id = normalize_sec_artifact_id("0000320193-26-000077")
72
+ cik = normalize_cik10(320193)
73
+ # -> doc_id == "SEC:000032019326000077", cik == "0000320193"
74
+ ```
75
+
76
+ ---
77
+
78
+ ## About Starling Quantitative
79
+
80
+ **[Starling Quantitative](https://starlingquant.com)** is the systematic U.S. equity research and capital allocation desk of Biwada LLC. Starling runs two layers of **100% deterministic, zero-LLM** U.S. equity scouts under a single non-parametric calibration and risk engine:
81
+
82
+ 1. **The Proven Foundation (Point-in-Time Filing Scouts):** Rule-based scouts extracting factor-neutralized alpha from primary SEC, FDA, and patent disclosures across `3,607` survivorship-free Russell 3000 CIKs (`555` delisted retained; `+61.8%` OOS `2022–2026` unlevered vs. `SPY +54.3%`).
83
+ 2. **The Experiment Layer (Darwinian Alt-Data Scouts):** A live survival-of-the-fittest ecosystem tracking ephemeral, differentiated alternative data feeds behind a One-Way Shadow/Canary Glass Wall—starting at `$0` in shadow and earning or losing capital automatically on live out-of-sample attribution.
84
+
85
+ ### Open Research Monographs & Verification
86
+ - **Research Note RN-01:** [Point-in-Time Alternative Data Ingestion, Entity Resolution & Survivorship-Free Panel Construction](https://starlingquant.com/guides/point-in-time-alternative-data-and-survivorship-free-panels) ([PDF](https://starlingquant.com/papers/RN-01-Point-in-Time-Alternative-Data-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222468](https://doi.org/10.5281/zenodo.24222468))
87
+ - **Research Note RN-02:** [Non-Parametric Signal Calibration, Orthogonal Pillar Pooling & One-Way Statutory Distress Vetoes](https://starlingquant.com/guides/non-parametric-signal-calibration-and-orthogonal-pillar-pooling) ([PDF](https://starlingquant.com/papers/RN-02-Non-Parametric-Signal-Calibration-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222814](https://doi.org/10.5281/zenodo.24222814))
88
+ - **Research Note RN-03:** [Six-Factor Residual Attribution, Purged Walk-Forward Validation & Volatility-Parity Risk Governance](https://starlingquant.com/guides/multi-agent-quorum-and-systematic-factor-consensus) ([PDF](https://starlingquant.com/papers/RN-03-Six-Factor-Residual-Attribution-Starling-Quantitative.pdf) | [DOI: 10.5281/zenodo.24222816](https://doi.org/10.5281/zenodo.24222816))
89
+ - **Live Forward Attribution Scorecard:** [https://starlingquant.com/scorecard](https://starlingquant.com/scorecard)
90
+ - **Machine-Readable Specification:** [https://starlingquant.com/llms-full.txt](https://starlingquant.com/llms-full.txt)
91
+
92
+ *Disclosure: Starling Quantitative trades proprietary capital and conducts selective institutional research evaluation under mutual NDA. Nothing in this repository constitutes investment advice or an offer to solicit retail capital.*
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/edgar_pit_clock/__init__.py
5
+ src/edgar_pit_clock/clock.py
6
+ src/edgar_pit_clock.egg-info/PKG-INFO
7
+ src/edgar_pit_clock.egg-info/SOURCES.txt
8
+ src/edgar_pit_clock.egg-info/dependency_links.txt
9
+ src/edgar_pit_clock.egg-info/top_level.txt
10
+ tests/test_edgar_pit_clock.py
@@ -0,0 +1 @@
1
+ edgar_pit_clock
@@ -0,0 +1,84 @@
1
+ from datetime import date
2
+
3
+ import pytest
4
+
5
+ from edgar_pit_clock import (
6
+ MarketSession,
7
+ align_filing_to_market_bar,
8
+ is_nyse_trading_day,
9
+ normalize_cik10,
10
+ normalize_sec_artifact_id,
11
+ purged_walk_forward_splits,
12
+ )
13
+
14
+
15
+ def test_415pm_after_hours_trap_routes_to_t_plus_1_open():
16
+ # Friday 2026-11-13 at 16:15:04 ET must NOT trade Friday Close; routes to Monday 2026-11-16 Open
17
+ aligned = align_filing_to_market_bar("20261113161504")
18
+ assert aligned.market_session == MarketSession.AFTER_HOURS
19
+ assert aligned.daily_batch_bar_date == date(2026, 11, 16)
20
+ assert aligned.daily_batch_price_field == "OPEN"
21
+ assert aligned.intraday_eligible is False
22
+
23
+
24
+ def test_pre_market_filing_routes_to_same_day_open():
25
+ aligned = align_filing_to_market_bar("2026-11-13T08:05:12-05:00")
26
+ assert aligned.market_session == MarketSession.PRE_MARKET
27
+ assert aligned.daily_batch_bar_date == date(2026, 11, 13)
28
+ assert aligned.intraday_eligible is False
29
+
30
+
31
+ def test_regular_session_intraday_vs_daily_batch_conservative_open():
32
+ # 14:00 UTC in November (EST = UTC-5) -> 09:00 ET (Pre-market)
33
+ pre = align_filing_to_market_bar("2026-11-13T14:00:00Z")
34
+ assert pre.market_session == MarketSession.PRE_MARKET
35
+
36
+ # 18:30 UTC in November -> 13:30 ET (Regular session)
37
+ reg = align_filing_to_market_bar("2026-11-13T18:30:00Z")
38
+ assert reg.market_session == MarketSession.REGULAR_SESSION
39
+ assert reg.intraday_eligible is True
40
+ assert reg.daily_batch_bar_date == date(2026, 11, 16)
41
+
42
+
43
+ def test_nyse_holiday_routing_good_friday_and_observed_july_4th():
44
+ # July 4, 2026 falls on Saturday -> NYSE observes Friday July 3, 2026
45
+ assert is_nyse_trading_day(date(2026, 7, 3)) is False
46
+ aligned = align_filing_to_market_bar("2026-07-02T17:01:00-04:00")
47
+ assert aligned.daily_batch_bar_date == date(2026, 7, 6)
48
+
49
+ # Good Friday 2026 is April 3, 2026
50
+ assert is_nyse_trading_day(date(2026, 4, 3)) is False
51
+
52
+
53
+ def test_cik_and_one_document_one_witness_normalization():
54
+ assert normalize_cik10(320193) == "0000320193"
55
+ assert (
56
+ normalize_sec_artifact_id(
57
+ "https://www.sec.gov/Archives/edgar/data/320193/000032019326000077/aapl-20260926.htm"
58
+ )
59
+ == "SEC:000032019326000077"
60
+ )
61
+ assert normalize_sec_artifact_id("0000320193-26-000077") == "SEC:000032019326000077"
62
+
63
+
64
+ def test_purged_walk_forward_enforces_embargo_without_leakage():
65
+ # Generate 240 sequential NYSE trading dates
66
+ d = date(2024, 1, 2)
67
+ dates = []
68
+ while len(dates) < 240:
69
+ if is_nyse_trading_day(d):
70
+ dates.append(d)
71
+ d = date.fromordinal(d.toordinal() + 1)
72
+
73
+ folds = purged_walk_forward_splits(
74
+ dates,
75
+ n_splits=4,
76
+ horizon_trading_days=10,
77
+ settlement_embargo_days=2,
78
+ min_train_events=20,
79
+ )
80
+ assert len(folds) == 4
81
+ for fold in folds:
82
+ assert fold.purged_count > 0
83
+ assert fold.train_end_date < fold.test_start_date
84
+ assert set(fold.train_indices).isdisjoint(set(fold.test_indices))