edgar-pit-clock 0.1.0__py3-none-any.whl
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.
- edgar_pit_clock/__init__.py +36 -0
- edgar_pit_clock/clock.py +329 -0
- edgar_pit_clock-0.1.0.dist-info/METADATA +92 -0
- edgar_pit_clock-0.1.0.dist-info/RECORD +7 -0
- edgar_pit_clock-0.1.0.dist-info/WHEEL +5 -0
- edgar_pit_clock-0.1.0.dist-info/licenses/LICENSE +21 -0
- edgar_pit_clock-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|
edgar_pit_clock/clock.py
ADDED
|
@@ -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,7 @@
|
|
|
1
|
+
edgar_pit_clock/__init__.py,sha256=30hsmvCy77g2qA8K5ZaxsJAYANDe5eEgXQA9ymgKaYQ,893
|
|
2
|
+
edgar_pit_clock/clock.py,sha256=xdquce6Q_iUfKbH5vYeI9IjATryaGif0cWTI26fSrak,11434
|
|
3
|
+
edgar_pit_clock-0.1.0.dist-info/licenses/LICENSE,sha256=g3KY7aTLbD3pNwrcc0izG5PFfATNZk5yHG9SOXQaGVI,1121
|
|
4
|
+
edgar_pit_clock-0.1.0.dist-info/METADATA,sha256=JJN5RtHixAFTwVJjZP5BvCEc_AbSQSPUu84bSV5Vm9Q,6480
|
|
5
|
+
edgar_pit_clock-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
edgar_pit_clock-0.1.0.dist-info/top_level.txt,sha256=r4N9zIEGRUhOADG5Uh5mcVNsCvLoIdzCuqTn3T0YBqc,16
|
|
7
|
+
edgar_pit_clock-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
edgar_pit_clock
|