Volume 4 · research notes
Research notes — the factor model, and the insider signal is “dead”
Five research notes written between 29 and 31 August 2026. They test whether the insider score from Volumes 1–3 carries any edge: first as one factor in a US quality/value/momentum long-short factor model, then on its own. The answer was no. The notes are reproduced as written; they are working documents, not a recommendation.
Generated (committed): 2026-08-29 06:51 UTC – 2026-08-31 10:55 UTC.
Design doc — a Sarto-style systematic factor model on the docka engine
Status: Reviewed proposal, M-S0 built. The plan to build a cross-sectional
quality/value/momentum long-short factor model on top of the existing
insider-signal-bot plumbing, with the insider signal folded in as one factor.
The concrete form of the direction in PROJECT.md.
Revision: v2 — revised after a two-critic review (architect + tester). v2
fixed four critical items the review caught: (1) balance-sheet factors need a
new instant-fact as-of reader — the annual dedup reader silently drops instant
facts; (2) TTM is not computable today, so v1 uses latest-annual and a real
TTM is deferred to M-S8; (3) the market-cap wording no longer implies editing the
scorer's stub (a new reader is used); (4) the consumer-layer boundary is now
enforced by a sink guard in test_import_graph.py, and the rebalanced
backtest moved to portfolio/backtest.py. Momentum now carries a split-refusal
guard, missing factors are excluded (never zero-filled), and degenerate values
(negative equity, zero denominators) are handled. M-S0 (scaffolding) is built and
green; M-S1 is next.
Companion reading: PROJECT.md (why), README.md (what already exists and what
it measured), SPEC §5–§9 (the raw/derived split, the entry rule, the holdout
discipline, the stop-after-M9 posture — all of which this design inherits).
1. Objective and success criterion
Build a US-listed systematic long-short factor model that ranks the investable universe on quality, value, momentum (and the existing insider signal), holds the top and shorts the bottom, and is judged the way a real systematic fund is judged.
Success criterion (Sarto's bar, not the hype thread's): over a multi-year point-in-time backtest, the book beats a matched benchmark (SPY total return) by a small margin at equal or lower risk — a positive information ratio and a lower max drawdown than the benchmark — and the result survives a single holdout read. Not spectacular return. Not an in-sample number. A modest, risk-adjusted, out-of-sample edge.
Explicit non-goals
- Not live trading. This stays research + alert/paper only under SPEC §9 and the two-key paper lock. No capital until an observation window passes.
- Not a global fund. US-listed only (see §3).
- Not a claim of alpha. The deliverable is a measurement apparatus and the first honest number it produces — which may well be "no edge."
2. What Sarto does, decomposed
From the Ärileht profile (18.08.2026), the process is:
- Universe: developed markets (~10k names) → drop illiquid and sub-$300M market cap → ~5–6k ranked names.
- Three academically-grounded pillars — quality (low leverage, strong balance sheet, stable cash flow), value (low multiples), momentum (price trend). Only factors "the academy has validated."
- Rank the whole universe; act on the top and the bottom, never on a single-name thesis ("we don't hunt for needles; we buy the part of the haystack where they hide"). The edge is statistical — the law of large numbers.
- 200:100 book — 200% long the good names, 100% short the weak, short proceeds funding extra longs; net ~100% long, gross ~300%.
- Risk structural, not discretionary — 1,000–2,000 names, position cap 2.5% long / ~1.25% short; the short leg cushions crashes.
- Point-in-time data — tested on 30 years using the values as known then, not as later restated.
- The model decides; humans build and test it, never override a pick.
3. Scope decision — US-listed subset (and why)
Sarto is global; docka's reachable, point-in-time-clean, free data is US SEC + a free price feed. Ex-US fundamentals are neither free nor reliably point-in-time. So the faithful copy is of the method, on a US-listed common stock universe. This is stated as a limitation, not hidden: the model is "a US systematic quality/value/momentum long-short," a subset of Sarto's mandate. If it works, an ex-US data source is a later, separate project.
4. Architecture — a new layer, not a rewrite
~70% of the hard machinery exists. The new work is a factor/portfolio layer that consumes the existing raw tables through the existing point-in-time readers.
EXISTING (reuse as-is) NEW (this design)
───────────────────── ────────────────
edgar/tickers.py ── listed CIK↔ticker↔exch ─▶ universe/ (eligible set, as-of)
edgar/xbrl.py ── companyfacts ───────────▶ (expand WANTED_CONCEPTS)
db/xbrl_facts ── point-in-time facts ────▶ factors/quality.py
db/repo_xbrl.facts_for(as_of) factors/value.py
prices/ + prices/lookup.bar_on_or_before ─────▶ factors/momentum.py
score/history.market_cap_usd (STUB, D5) ─────▶ (left untouched; new factors/marketcap.py::market_cap_as_of)
prices/liquidity.passes_liquidity ────────────▶ universe/ liquidity leg
backtest/entry.py (entry_date_for, factors/rank.py (winsorize, z, composite)
adjusted_open) ──────────────────────────▶ portfolio/construct.py (200:100, caps)
backtest/engine.py, backtest_runs holdout ───▶ portfolio/backtest.py (rebalanced L/S, in the SINK)
score/scorer.py (insider score) ─────────────▶ factors/insider.py (insider as a factor)
Key existing hooks confirmed in the code:
db/repo_xbrl.py::facts_for(conn, cik, concept, unit, *, as_of)— the point-in-time clamp (filed_date <= as_of, required kwarg, newest-first).score/history.py::_annual_dedupedalready collapses restated periods to the newest disclosure ≤ as_of. This is Sarto's "data as known then" firewall, already built.prices/lookup.py::bar_on_or_before(conn, ticker, day, *, as_of, source, column)— as-of clamped price read; used for momentum and the market-cap price leg.score/history.py::market_cap_usd(conn, cik, as_of)— a documented stub returningNone(D5), pre-clamped so "the day this is implemented it is already clamped." We implement it.backtest/entry.py::entry_date_for/adjusted_open— next-session entry and split-safe pricing (open * adj_close / close), reused for portfolio fills.prices/liquidity.py::passes_liquidity— the singleMIN_ADV_USD = 500_000gate, reused for the universe's liquidity leg.backtest_runsparam-hash + holdout refusal (exit 4 without--i-am-overfitting) — reused unchanged for the portfolio backtest.
Isolation rule: the factor engine is a new consumer of the raw tables. It
does not alter score/scorer.py, the insider signals table, or any existing
backtest number. The insider factor (factors/insider.py) reads the existing
score read-only. This keeps every current README measurement reproducible and
respects the repo's "never silently change scoring" discipline.
One consequence, called out because it is easy to get wrong:
score/history.py::market_cap_usd is a D5 stub returning None. It is wired to
be called by the −25 small/penny penalty, but today score/rules.py's
_small_or_penny_hit short-circuits and records {"market_cap": "unknown"}
without calling it — so the stub is dormant. Implementing that stub would
therefore change scoring the moment _small_or_penny_hit starts consuming it. To
avoid any coupling, the factor layer gets its own market-cap reader
(factors/marketcap.py::market_cap_as_of) and leaves market_cap_usd exactly as
it is. Retiring the D5 stub and wiring the −25 penalty to a real market cap is a
separate, later, ablation-gated change (tracked at M-S7), never a side effect
of this work.
Boundary is enforced, not asserted. factors/, universe/ and portfolio/
are registered as a guarded sink set (CONSUMER_SINK_PACKAGES) in
tests/unit/test_import_graph.py, with a reverse-arrow test
(test_no_stage_imports_a_consumer_sink) mirroring the report/ guard. So "no
stage depends on this layer" is a test, not a comment. The rebalanced long-short
backtest therefore lives in the sink (portfolio/backtest.py), not inside the
backtest stage — a stage placing it would import the sink and fail that test.
5. Schema additions
All additive (CREATE TABLE IF NOT EXISTS), following the v3/v4 overlay pattern.
Raw stays append-only; the factor/portfolio tables are DERIVED (drop & rebuild).
xbrl_facts— no schema change; we add concepts to what the fetch stores (§7). Existing rows are a subset, re-fetch is idempotent (INSERT OR IGNORE).factor_scores(DERIVED) — one row per (cik, ticker, as_of_date, factor, raw_value, z_value, pillar), plus the composite. Rebuildable from raw + prices.universe_membership(DERIVED) — one row per (as_of_date, cik, ticker, eligible, reason) so a dropped name is an auditable skip, not a silent absence.portfolio_positions(DERIVED) — one row per (rebalance_date, ticker, side, weight) — the constructed book at each rebalance.factor_backtest_runs— param-hash + split, mirroringbacktest_runs, for holdout accounting on the portfolio backtest.
A schema_version bump (5 → 6 — v5 already exists) with a forward-only
migration, matching the existing pattern in db/connection.py. The three derived
tables are added to _DERIVED_SHAPE_REQUIREMENTS; factor_backtest_runs (ops) is
not. A reopen-preservation test pins that each required_column really exists in
schema.sql, so the migration can never silently drop-and-wipe a derived table.
6. Point-in-time and anti-look-ahead guarantees
This is the section that decides whether any number is real. Every one of these reuses machinery that already exists here.
- Fundamentals as known then. All fundamental reads go through
facts_for(..., as_of=rebalance_date), which clampsfiled_date <= as_of— so a restatement filed later is invisible. This clamp is reused; the dedup is not fully reused, and the doc must not pretend it is:_annual_dedupedhandles annual duration facts only (350–380-day band) and drops every instant fact (period_start IS NULL→continue). Every balance-sheet concept (Assets, StockholdersEquity, Liabilities, debt legs, shares outstanding) is instant, so reusing_annual_dedupedfor them returns nothing and silently skips the name.- Therefore M-S1 builds a new instant-fact as-of reader (newest disclosure ≤ as_of per
period_end, no duration band), with its own property test, and a guard test that the duration path returns empty on instant facts so the two can never be swapped. (Sarto's central discipline; the clamp is built, the instant-fact dedup is new work.)
- Prices as known then. Momentum and market-cap price legs read
bar_on_or_before(..., as_of=rebalance_date).
2b. Momentum is not immune to the split disease. This corpus's free-feed
adj_close is not reliably split-adjusted (README "the split question";
penny_price's C54 note). A MOM_12_1 ratio spanning a split the feed never
applied invents a ±60–200% return and rockets the name into a decile on a
fabricated signal — the same defect entry.py refuses via unadjusted_split.
So the momentum factor reuses prices.crosscheck's unadjusted-split
detection and excludes (flags, never scores) any name with an unapplied split
inside [t−252, t−21]. Momentum offsets are trading-day counts via the
calendar, never timedelta days.
- Fills you could actually get. Entry/exit at each rebalance use
entry_date_for(next session open, split-safeadjusted_open) — never the rebalance-day close. - Coverage is a skip, not a miss. A name whose XBRL was never fetched returns the 4-state
overlay_fetch_logstatus (_fundamentals_unavailable), so a half-populated corpus reports pending names rather than scoring them as "cheap/low-quality = 0". - Survivorship. The historical universe must include names later delisted, or the long side inflates. The backtest already has
DELISTEDhandling to build on; the universe builder keys off the historical ticker/exchange map, not just currently-listed names. Open risk — see §12. - Holdout. A second distinct parameter set on the holdout split is refused (exit 4) unless
--i-am-overfitting, logged. Factor definitions are pre-registered in this doc so tuning cannot masquerade as discovery.
7. Factor definitions (pre-registered, academically grounded)
Each factor is a named hypothesis, ablatable, with a citation — Sarto's "science
only" rule. Concepts marked (new) are added to WANTED_CONCEPTS.
Fundamentals to add to the fetch:
Assets, Liabilities, StockholdersEquity, LongTermDebtNoncurrent,
DebtCurrent, NetCashProvidedByUsedInOperatingActivities, GrossProfit
(fallback Revenues − CostOfGoodsAndServicesSold), and shares outstanding
dei:EntityCommonStockSharesOutstanding (fallback CommonStockSharesOutstanding).
Revenues and NetIncomeLoss already fetched.
v1 uses LATEST ANNUAL figures, not TTM. The critics were right: there is no point-in-time TTM assembler in the repo, and a correct one (stitch Q1–Q3 10-Qs + derive Q4, dedup each quarter to newest disclosure ≤ as_of, avoid the YTD-vs-discrete double-count) is its own milestone. v1 therefore uses the latest annual (FY) figure disclosed ≤ as_of for every flow concept (earnings, revenue, operating cash flow), read through the instant/annual as-of readers. A proper TTM is a deferred enhancement (M-S8), gated by its own restated-quarter and no-double-count property tests. Flow = latest FY; stock = latest instant.
Quality (higher = better)
GROSS_PROFITABILITY= GrossProfit / Assets — Novy-Marx (2013).LOW_LEVERAGE= − (LongTermDebt + DebtCurrent) / StockholdersEquity.LOW_ACCRUALS= − (NetIncome(FY) − OperatingCashFlow(FY)) / Assets — Sloan (1996).
Value (higher = cheaper = better) — all vs market cap
EARNINGS_YIELD= NetIncome(FY) / MarketCap.BOOK_TO_PRICE= StockholdersEquity / MarketCap — Fama–French (1992).CF_TO_PRICE= OperatingCashFlow(FY) / MarketCap.
Momentum (higher = better)
MOM_12_1= adj_close(t−21) / adj_close(t−252) − 1 — Jegadeesh–Titman (1993), skip the most recent month; trading-day offsets; split-refusal guard per §6.2b.
Insider (higher = better) — the docka edge as a factor
INSIDER_SCORE= the latestis_latestinsider cluster score for the name withactionable_at ≤ as_of(never a signal disclosed after the date). Most names have no cluster, so the factor is sparse: a name with no signal ≤ as_of is absent from this factor (not 0), handled by the missing-data rule below. A staleness cap (ignore signals older than N months) is a param, defaulted and recorded. Deferred to M-S7.
Degenerate values (required, not optional) — mirror adjusted_open's guards:
- Any denominator that is
0,None, or missing → the factor is a skip for that name (neverinf, neverZeroDivisionErrormid cross-section). - Negative StockholdersEquity →
LOW_LEVERAGEandBOOK_TO_PRICEare excluded for that name (a negative denominator flips the sign and would rank a distressed firm as "great quality / deep value" — the Fama–French convention drops these), not scored. EARNINGS_YIELDkeeps the sign of negative earnings (a loss-maker is genuinely low-yield); this is a recorded decision, not an accident.
Missing data — a skip, never a zero. A name missing a factor is removed from that factor's cross-section (its mean/std/rank), never assigned raw 0 or z 0 — zero-filling silently plants a no-data name mid-pack (or, for value, makes it look "average cheap"). Explicit coverage rules, pinned and tested:
- A pillar is computed from the sub-factors a name actually has, requiring a minimum coverage (≥ 2 of 3 sub-factors) or the pillar is itself absent.
- The composite requires a minimum number of pillars (≥ 2 of {quality, value, momentum}); a name below that is universe-ineligible, not neutral-filled.
- This keeps the repo's "coverage is a skip, not a miss" firewall alive through the winsorize/z step, where it would otherwise evaporate.
Combination
- Per rebalance date, cross-sectionally, on the present-only values: winsorize at 1/99% first, then z-score (order pinned — the two orders give different numbers). Average z within a pillar → pillar score; average pillars (equal weight v1) → composite. A zero-variance or n<2 cross-section → defined output (skip/neutral), never a std-division NaN storm.
- Optional sector-neutralization: demean z by 2-digit SIC (
issuers.sicexists). - Insider enters as its own pillar in v2 (M-S7), after quality/value/momentum are validated alone — so its marginal contribution is measurable by ablation, which is the whole point.
Market cap is computed by a new reader in the factor layer,
factors/marketcap.py::market_cap_as_of(conn, cik, as_of) =
shares_outstanding(as_of, XBRL, unit shares) × close(as_of, bar_on_or_before,
unit USD). It deliberately does not touch score/history.py::market_cap_usd
(the scorer's D5 stub) — see the isolation note in §4. Three traps the tests must
pin: (a) facts_for defaults unit="USD", so the shares leg must pass
unit="shares" or the fact is invisible and the name drops no_market_cap; (b)
both legs read on the same as_of — the as-of property test asserts neither
the shares leg nor the price leg reads anything dated/filed after as_of; (c) a
dual-class issuer reporting multiple EntityCommonStockSharesOutstanding rows
needs a defined aggregation against the single priced ticker — recorded, not
guessed. Also verify at M-S1 that edgar/xbrl.py actually walks the dei
taxonomy block of companyfacts and stores the shares concept under a name
facts_for matches; if the fetcher only iterates us-gaap, market cap has no
shares leg and the whole universe drops.
8. Universe construction (insider universe --as-of DATE)
- Start from the historical listed set (ticker/exchange map).
- Common stock on a major exchange (NYSE/Nasdaq/NYSE American) — reuse the exchange gate;
exchange_unknownis a meaningful exclusion (C32). Caveat (see §12):issuer_exchangeis a current-state, one-row-per-CIK table, so this leg is not truly point-in-time; the as-of eligibility is anchored instead on price-series existence + theissuer_tickersfirst_seen/last_seenwindow, and the current-exchange snapshot is a supplementary filter, not the authority. - Market cap ≥ $300M as-of (
market_cap_as_of(D), not today's) — Sarto's floor. - Liquidity leg —
passes_liquidity(ADV ≥ $500k). ADV-only,spread_applied = Falserecorded (as the existing gate documents). - Emit
universe_membershipwith an explicitreasonfor every drop.
Every leg is evaluated as of D, never on today's state: a name sub-$300M or not-yet-listed at D but large/listed now must be dropped at D, and a name listed at D but delisted since must be eligible at D (the survivorship-inclusion case, tested at M-S2 — see §12). Target eligible set: on US-listed common stock this yields low thousands — a decile is hundreds of names, inside the caps.
9. Portfolio construction (portfolio/construct.py)
- Rank composite; long the top quantile, short the bottom (quantile a param, default deciles). A NaN/absent composite is excluded from the ranking entirely — never sorted to the bottom and shorted as "worst" (pandas puts NaN last; that would be a silent short on a no-data name). Ties at a quantile edge are broken deterministically by
cikso the book is reproducible. - Scale to the 200:100 gross structure; position cap 2.5% long / 1.25% short; target name count 1,000–2,000 (params). When the quantile is too thin for the caps to reach the gross target (Σ caps < 200%), the shortfall is recorded, not silently re-weighted past the cap (or the quantile widens per a stated rule).
- Turnover control: carry names that stay in-quantile; per-rebalance turnover capped. Costs are charged on the traded delta only (a carried, unchanged name incurs zero turnover cost — see §10).
- Sector caps optional (v2).
- Output
portfolio_positions.
10. Rebalanced long-short backtest (insider port-backtest)
Distinct from the existing event-study backtest. Lives in the sink
(portfolio/backtest.py), per §4.
- Monthly rebalance (param). At each date: build universe as-of, score factors as-of, construct book, hold to next rebalance.
- Fills strictly after the rebalance. Entry and exit price via
entry_date_for(next session open, split-safeadjusted_open) — never the rebalance-day close. "No fill on or before D" is too weak (D is not before D and a same-day close fill is the exact look-aheadentry.pyexists to prevent); the property test asserts every fill is strictly after its rebalance date. - Delisting mid-hold is a return event, not just a universe event: a name that delists between rebalances exits at delisting, never carried to the next rebalance at a stale price (that books a survivorship gain). Mirrors the halt-vs-holiday trap in
entry.py. - Returns from adjusted prices; transaction cost charged on the traded delta each rebalance (0.5% round-trip assumption, documented as a guess) + a flat borrow cost on shorts (free data has no real borrow — modelled + hard-to-borrow exclusions, never pretended).
- Benchmark-relative metrics (because the success test is relative): ann. return, vol, Sharpe, beta to SPY, tracking error, information ratio, max DD, max DD vs SPY, monthly excess hit rate. CAGR refused below the existing 30-trade / 1-year guards. (SPY total-return bars must be ingested — a dependency.)
- Holdout hash — the full knob set.
factor_backtest_runs.param_hashmust fold in every researcher knob, or two different strategies collide (one silently overwrites the other's holdout claim) or a variant evades the guard: rebalance frequency, long-quantile, gross-long/gross-short, cost-bps, borrow-bps, sector-neutral flag, pillar weights, the ablation set, and the price snapshot + scorer version. Second distinct set on the holdout → exit 4 unless--i-am-overfitting, logged. Likewisescorer_versiononfactor_scores/portfolio_positionsmust encode the ablation set + sector-neutral + weights, or two composites collide on the PK. - Per-factor ablation:
--ablate MOM_12_1,LOW_ACCRUALSzeroes a factor's z in the composite and rebuilds — dev-split measurement of marginal contribution, stamped so it can never masquerade as a validated number (mirrors the existing--ablate).
11. CLI surface (new subcommands, same insider entry point)
insider fundamentals --universe # fetch expanded concepts for all listed CIKs
insider universe --as-of DATE [--min-mcap 300e6]
insider factors --as-of DATE [--ablate CODE,...] [--sector-neutral]
insider port-backtest --from DATE --to DATE --rebalance 1m \
[--long-quantile 0.1] [--gross-long 2.0 --gross-short 1.0] \
[--cost-bps 50 --borrow-bps 50] [--i-am-overfitting]
12. Risks and open questions
- Survivorship (highest risk). Need delisted names in the historical universe; the free ticker map is currently-listed only. Mitigation: build the historical universe from names that ever appeared in filings/prices, and treat a disappeared name as delisted (reuse
DELISTEDhandling), not as absent. This still cannot recover names that delisted before the corpus starts and never filed a Form 4 — a proper point-in-time listing history (CRSP-grade) is not in the free data. So the honest framing is that any long-side result is an optimistic upper bound until survivorship coverage is measured (M-S6), and the number is reported as such — never as clean. - Exchange gate is current-state (a second survivorship leak).
issuer_exchangeis one row per CIK (today's listing), so it cannot answer "listed at D": a since-delisted name readsexchange_unknownat every historical rebalance and a recently-listed name reads listed even before it was. So the exchange leg is supplementary; as-of eligibility is anchored on price-series existence +issuer_tickersfirst_seen/last_seen(§8). Another source of long-side optimism until measured. - Fundamentals coverage. Small filers have thin/absent XBRL; value/quality factors will be missing for a chunk of the universe. Handled as skips (and the missing-data rule in §7 excludes rather than zero-fills), but if coverage is low the universe shrinks. Measure coverage first (M-S1), with a non-trivial-count guard so a 0-name coverage run cannot read green.
- Shorting realism. No borrow data on free feeds — flat cost + exclusions, flagged as an upper bound on real shortability.
- Point-in-time shares outstanding. Cover-page shares (
dei:EntityCommonStockSharesOutstanding) are as-of the filing date, good; but buybacks/issuance between filings drift market cap. Acceptable at monthly rebalance; documented. - Data volume/time. Full-universe XBRL ≈ 8–10k companyfacts requests (~40 min at 5 req/s); full-universe prices via yfinance is hours and rate-limited. This is the real wall-clock cost, and it is one-time + incremental.
- Multiple-testing. Four pillars, several sub-factors — pre-registration here
- single holdout read + ablation stamping are the defence, exactly as SPEC §8's holdout discipline intends.
13. Build plan to green
"Green" per milestone = pytest -q (offline suite) + ruff check . + mypy
pass, and the milestone's own command runs. Overall green = M-S6: the rebalanced
long-short backtest runs end-to-end under holdout discipline and prints its first
benchmark-relative number — with no strategy verdict claimed. Each milestone is
a separate PR off main, small enough to review.
Every milestone is built the repo's way: offline unit tests with the existing
fakes/builders (tests/builders.py, tests/conftest.py), point-in-time reads
only, and a property test where a look-ahead could hide.
The green criteria below are sharpened from the plan-critic review: for the factor layer, "green" is not "a test passes" but "the look-ahead / sign / missing- data trap has a property or numeric-pin test that would fail if the bug were present." A milestone that only tests the happy path is not green.
| # | Milestone | Deliverable | Green when |
|---|---|---|---|
| M-S0 ✅ | Scaffolding | factors//universe//portfolio/ packages; schema v5→6; the four tables; _DERIVED_SHAPE_REQUIREMENTS updated; enforced sink guard in test_import_graph.py | Done. migration applies on a fresh DB; reopen-preservation test; sink-boundary tests; pytest(+9)/ruff/mypy green; no behaviour |
| M-S1 | Market cap + fundamentals depth | expand WANTED_CONCEPTS; new instant-fact as-of reader; factors/marketcap.py::market_cap_as_of (stub untouched); coverage report | instant-fact reader pin + guard that the annual path returns empty on instant facts (C2); mcap unit="shares" pin + both-legs as-of property test (C6); dei-block fetch verified; coverage command asserts a non-trivial count |
| M-S2 | Universe builder | insider universe --as-of (exchange/mcap/liquidity legs, as-of); universe_membership with reasons | the four drop reasons plus the survivorship-inclusion test (a since-delisted name is eligible at D) and as-of eligibility (sub-$300M-at-D-but-large-now → dropped at D) |
| M-S3 | Factor engine | factors/{quality,value,momentum}.py + rank.py; insider factors --as-of | per-factor numeric pins including negative-equity/zero-denominator/negative-earnings (C5); missing-factor → excluded-not-zero (C4); momentum split-refusal (C3); winsor-then-z order + degenerate/thin cross-section pins; ablation zeroes exactly one factor and changes scorer_version |
| M-S4 | Portfolio construction | portfolio/construct.py — 200:100, caps, quantiles, turnover | NaN composite excluded (never shorted as worst) + deterministic tie-break; caps-vs-gross conflict on a thin quantile is recorded, not silently re-weighted; same input → byte-identical book |
| M-S5 | Rebalanced backtest | portfolio/backtest.py + insider port-backtest; costs, borrow, benchmark-relative metrics; holdout | fill is strictly after the rebalance (property, mirrors entry.py) + symmetric exit + delist-mid-hold exits at delisting; turnover charged on the traded delta only; holdout second-set → exit 4; 2-rebalance toy corpus pinned to a known book and known return |
| M-S6 | First honest number | run M-S5 on the real corpus once history is deep; write results with "tiny sample, not a verdict" honesty | reuses the 30-trade/1-year CAGR refusal; prints benchmark-relative metrics + holdout stamp; every §12 caveat (survivorship both legs) restated beside the number |
| M-S7 | Insider as a factor | factors/insider.py; ablation on dev split, validate once on holdout | insider factor as-of property test (reads no signal with actionable_at > D) + defined sparsity/staleness fill; incremental IR stated, not assumed |
| M-S8 (deferred) | Real TTM assembler | replace latest-FY flow figures with point-in-time TTM | restated-quarter pin (newest disclosure ≤ as_of per quarter); YTD-vs-discrete no-double-count pin; missing-quarter → skip, never a 3-of-4 partial |
Precondition running through all of it (from PROJECT.md): the EDGAR backfill and
full-universe fundamentals/prices ingest. M-S0..M-S5 are offline, test-driven,
and independent of that ingest (they use fixtures); only M-S6 needs the deep
real corpus. So we can build and green the entire apparatus before the long ingest
finishes.
14. Test strategy
- Offline by default (
-m 'not live'), fixtures viatests/builders.py. - A property test wherever look-ahead could hide (mcap as-of, factor as-of, fill date ≥ rebalance) — the repo's pattern from
entry.py. - Numeric pins for every factor formula (hand-computed expected values).
- Determinism tests (same corpus → byte-identical factor rows and book), matching the existing "two rebuilds identical apart from built_at" guarantee.
- Holdout-refusal and ablation-stamping tests, mirroring the existing backtest.
mypyclean;ruffclean; lockfile respected (no new heavy deps — pandas is already present for the cross-sectional maths).
Sarto factor model — the first honest number (M-S6)
Run date: 2026-08-29. Branch: sarto/fix-dualclass-preferreds (@ 85762f7 engine).
Corpus: the research corpus database (read-only), measured on a WAL-safe backup-API snapshot.
Verdict: RESEARCH-ONLY — not worth real money. The strategy fails 3 of the 5
go/no-go gates (2, 3, 5). The headline long-short return is large but it is (a) not
risk-adjusted-better than SPY on drawdown, (b) statistically indistinguishable from
repackaged market + momentum exposure (FF-alpha t = 0.61), and (c) resting on a
long side that is a survivorship upper bound (0 % delisted-name inclusion).
This is the plan's explicitly-anticipated outcome: "a modest net information ratio or none — quite possibly no edge after costs and borrow." The deliverable is the measurement apparatus and its first defensible number, not a claim of alpha.
How it was run (discipline)
| Item | Value |
|---|---|
| Snapshot mechanism | SQLite backup API (sqlite3.Connection.backup), source opened mode=ro. Corpus never mutated. (VACUUM INTO / wal_checkpoint(TRUNCATE) were the alternatives; backup API chosen.) |
| Backtest window | 2023-01-01 → 2026-08-01, monthly rebalance; ~2021-12 price burn-in for 12-1 momentum |
| Evaluable rebalance-months | 42 (≥ MIN_MONTHS_FOR_VERDICT = 36 ✓) |
| Rebalances | 43 |
| Params | Pre-registered defaults: long_quantile 0.1, gross_long 2.0 / gross_short 1.0, cap_long 0.025 / cap_short 0.0125, cost 50 bps, borrow 50 bps, equal-weight pillars, NOT sector-neutral, no ablation |
| Dev/holdout split | Entire window is "holdout" by the engine's date rule (HOLDOUT_START = 2022-01-01; any window ending ≥ then is holdout). There is no separate dev window: fundamentals only reach back to 2023-01 and prices to 2021-12, so a date_to < 2022-01-01 "tune" window is not constructible. Discipline was therefore honored by running the single pre-registered config once — no tuning, no exploration on the measured series, --i-am-overfitting never used. |
DSR trial count (n_trials) | 1 (one distinct logged param hash; logged_floor = 1). The survivorship-subset rerun shares the identical param hash (same window/params) so it is not a second trial. Ledger started empty; the count is provably complete and honest. |
| Ablation / IC / French-validation live wiring | DEFERRED (see "What was not run" below) |
Pre-verdict guards all passed on the real data before any number was computed:
FF series is decimal (RF 0.0028–0.0048 per month, ÷100 applied once), monthly
cadence (42 consecutive months), coverage floor passes (5,053 computable market caps at
2026-06 vs a 1,200 floor), SPY total-return present, FF present. The scale/alignment trap
tests (test_ff_series_scale_is_decimal, test_excess_sharpe_matched_scale,
test_ff_alpha_port_ff_month_alignment, test_ir_port_spy_month_alignment) pass.
The five go/no-go gates
| # | Gate | Value | Verdict |
|---|---|---|---|
| 1 | Deflated Sharpe ≥ 0.95 (deflated by n_trials = 1) | DSR ≈ 1.000 (excess Sharpe 1.04) | PASS ⚠️ (see DSR caveat) |
| 2 | IR vs SPY > 0 AND maxDD ≤ SPY's | IR +0.330 > 0 ✓, but book maxDD −12.9 % vs SPY −7.4 % ✗ | FAIL |
| 3 | FF alpha with HAC t > 3 | alpha +0.56 %/mo, HAC t = 0.61 | FAIL |
| 4 | Long-only variant sane (edge not only in shorts) | long-only ann +15.6 %, mean > 0 | PASS |
| 5 | Survivorship subset: IR ≥ 50 % of full AND t > 3 AND same sign AND delisted-inclusion ≥ 2 % | retention ✓ (subset IR 0.360 ≥ 0.5·0.330), same sign ✓, but delisted inclusion 0.00 % ✗ and subset FF-t 0.37 ✗ | FAIL |
worth_real_money = FALSE. Fails gates 2, 3, and 5.
Why each failing gate fails
- Gate 2 — the book beats SPY on tracking-error-adjusted excess return (IR +0.33) but is riskier: its worst drawdown (−12.9 %) is nearly double SPY's (−7.4 %). It does not clear "positive IR at equal or lower drawdown."
- Gate 3 — the return is almost entirely factor beta, not alpha. FF loadings: mkt-RF 0.91, UMD (momentum) 0.90, RMW 0.59, HML 0.24, SMB 0.15, CMA −0.06. The intercept t-stat is 0.61 — no statistically significant idiosyncratic alpha. This is a strategy that is largely repackaged market + momentum + profitability exposure, exactly the "repackaged HML/UMD/RMW is not an edge" failure the gate is built to catch.
- Gate 5 — the survivorship retention sub-gate is fine (subset IR even slightly higher than full, same sign), but the delisted-name inclusion rate is 0.00 % (0 of 15,106 held name-months). The free price feed is currently-listed-only (see caveat 1). The long side is therefore an optimistic upper bound, and gate 5 fails outright per §1 ("a rate ≈ 0 → long side upper-bound-only → research-only"). The subset FF-t (0.37) also stays below 3.
Full diagnostics (reported regardless of pass/fail)
Long-short book (net of 50 bps cost + 50 bps borrow):
| Metric | Value |
|---|---|
| Annualized return | +29.1 % |
| Annualized vol | 22.8 % |
| Raw Sharpe | 1.24 |
| Excess Sharpe (RF-subtracted; the gate-1 DSR input) | 1.04 |
| DSR (n_trials = 1) | ≈ 1.000 |
| Total return over window | +144.4 % (vs SPY +104.0 %) |
| Max drawdown (book / SPY) | −12.9 % / −7.4 % |
| IR vs SPY | +0.330 |
| Beta vs SPY | 0.757 |
| Tracking error vs SPY | 20.9 % |
| Monthly excess hit-rate (beat SPY) | 52.4 % |
| Mean one-rebalance turnover | 1.68 |
Long-only book (net): ann +15.6 %, Sharpe 0.87, total +66.1 % — trails SPY (+104 %). So the long leg on its own does not beat buy-and-hold; the long-short excess comes substantially from the short leg (which is the structurally-optimistic, borrow-cost- modelled, hard-to-borrow-screened leg).
Fama-French regression: alpha +0.56 %/mo, HAC t = 0.61 (Newey-West, maxlags by the pinned ⌊4·(T/100)^(2/9)⌋ rule at T = 42). Betas above.
Survivorship-sensitivity pair: full IR 0.330 → subset IR 0.360 (retention 109 %, same sign); full alpha +0.56 %/mo → subset alpha +0.34 %/mo (same sign); delisted inclusion rate 0.00 % (0 / 15,106 name-months).
Equity curves (book / long-only / SPY, growth of 1.0 over 42 months): 2.44 / 1.66 / 2.04. The full monthly return series for both the book and SPY are in the run JSON.
Every §6 caveat, restated beside the number
- Survivorship = optimistic UPPER BOUND (the #1 threat, measured). The delisted-name inclusion rate is 0.00 %. Of 9,945 tickers with price bars, 9,861 (99.2 %) still trade at 2026-08; only ~56 delisted before mid-2026. The free ticker map / yfinance feed is effectively currently-listed-only — it does not recover names that delisted inside the window. Both legs, especially the long side, are survivorship- inflated upper bounds. This alone forces research-only (gate 5b).
- Exchange-gate leak (second survivorship leak).
issuer_exchangeis current-state, one-row-per-CIK, demoted to supplementary; eligibility anchors on price-series existenceissuer_tickersbounds. Residual: those bounds are ingest-derived, bounded by the 2021-12 backfill / 2023-01 start.
- Fundamentals coverage %. At 2023-01: 2,315 eligible of 8,004 considered (~29 %); at 2026-06, 5,053 computable market caps. The universe is what has XBRL coverage — small filers with thin XBRL are skipped (never zero-filled). A stated shrinkage, above the decile-stability floor (top decile ≈ 230 names ≫ the 80 needed to fill the long book).
- Borrow cost is MODELLED, not real. 50 bps annual on surviving shorts by size bucket
- a hard-to-borrow screen. No real borrow feed. The short leg — which carries most of the long-short excess — is an upper bound on real shortability.
- Fundamentals are latest-FY, not TTM (M-S8 deferred). Flow figures (earnings, cash flow, gross profit) use the latest annual disclosure ≤ as_of, not a point-in-time trailing-twelve-month stitch. An explicit, revisitable approximation.
- ~42-month sample is SHORT. Barely above the 36-month floor; sparse residual dof for the FF regression and the DSR. A 3–5y bar is the Sarto minimum, and this is the low end.
- Universe / coverage. ~29 % of considered names are eligible (caveat 3); the number is computed on that covered subset, not the full US market.
- Dual-class handling is PARTIAL (two cohorts, one stated bias). 423 names were excluded
dual_class_unpriceableat 2023-01 (multi-listed classes caught byCOUNT(DISTINCT ticker) > 1, plus a 5-name denylist: META, SNAP, F, PLTR, NKE). Single-listed-class dual-class names (second class unlisted) that escape both the ticker-count query and the denylist are priced off one class → understated market cap → overstated value ratios → a spurious long/value pull. A stated residual bias, not a clean exclusion. - These are self-found PUBLIC factors. V/M/Q are the most-published factors in existence; McLean–Pontiff decay says only ⅓–½ of naive backtest edge survives publication, and Muravyev-style critique says an unmodelled short leg is structurally optimistic. Here there is no significant alpha to decay in the first place (gate 3), and the return is dominated by a 0.90 momentum beta — so the live-survival expectation is worse than the ⅓–½ rule of thumb, not better.
Real-data issues hit (reported, not worked around — for orchestrator triage)
- Gate-1 DSR input-Sharpe scale looks anti-conservatively saturated.
excess_sharpereturns an annualized Sharpe (×√12 = 1.04), butdeflated_sharpe's z-statisticz = (sharpe − sr0)·√(n_obs−1)/√vartreatssharpeas a per-observation (monthly) Sharpe. Feeding the annualized 1.04 with n_obs = 42 gives z ≈ 6.0 → DSR ≈ 1.000. The BLdP-correct per-monthly Sharpe (1.04/√12 ≈ 0.30) gives z ≈ 1.92 → DSR ≈ 0.97. Gate 1 passes either way (0.97 and 1.00 both ≥ 0.95), so the overall verdict is unchanged (gates 2/3/5 fail), but this is a latent scale mismatch that would matter near the DSR boundary and should be triaged in the apparatus (it saturates, i.e. understates deflation). Not fixed here — this is an execution milestone. - Survivorship-free price backfill did not actually capture delisted names. The Track-B price ingest was intended to be survivorship-free, but in practice the free feed returns almost exclusively currently-listed tickers (caveat 1). This is a data-ingest gap, not an engine bug, and it is precisely what gate 5 is designed to catch — but it means the long-side number cannot be de-biased from within this corpus. Recovering delisted-name prices (a paid feed, or CRSP-style delisting returns) is the prerequisite to a non-upper-bound long side.
What was NOT run, and why (honest trial budget)
- Ablation (drop each pillar): DEFERRED. Each pillar-drop is a distinct param hash → each would add a trial to the DSR count and each is a full ~40-minute run (~2 h for three). Since gate 3 already shows the return is repackaged factor beta with no significant alpha, and the FF betas above already expose the pillar exposures (momentum dominates at UMD-beta 0.90), the marginal-IR ablation is not verdict-changing. Deferred to keep the single pre-registered measurement's trial count honest at 1.
- Live French-validation correlations & IC: DEFERRED. Both need per-pillar single-factor return / cross-sectional forward-return series that the port backtest does not emit; producing them is additional construction beyond minimal M-S6 glue. The validator and IC definitions are fixture-pinned at M-S5b; wiring live series is future work.
Bottom line
A large gross long-short return (+29 %/yr, +144 % cumulative) that is not worth real money: it draws down deeper than SPY, has no statistically significant Fama-French alpha (t = 0.61) — it is repackaged market + momentum beta — and its long side sits on a survivorship upper bound (0 % delisted inclusion). Per SPEC §9 / plan §1, anything short of all five gates is research-only, no capital. The apparatus works, the number is honest, and the honest number says: no demonstrated edge after costs, factors, and survivorship.
Runner: scripts/ms6_first_number.py (the only new code — it calls the built
run_port_backtest and evaluate_go_no_go, changes no engine code). Raw result block:
the run's --out JSON.
Sarto factor model — does docka's insider signal add alpha over QVM? (M-S7)
Run date: 2026-08-29. Branch: sarto/m-s7-insider-factor (@ d3710ea engine + insider factor).
Corpus: the research corpus database (read-only), measured on a WAL-safe backup-API snapshot.
Verdict: INSIDER WASHES OUT — it adds no alpha, and marginally DEGRADES the book.
This milestone answers the decisive question the QVM-only result (M-S6) left open: does folding docka's insider signal in as a fourth pillar inject real, independent alpha that the quality/value/momentum book lacked? M-S6 produced a large long-short return (+29.1 %/yr) that was not worth real money — repackaged momentum (Fama-French alpha HAC t = 0.61), drawing down deeper than SPY, on a survivorship upper bound. The test here: BASE (QVM only) vs WITH_INSIDER, head to head, same window, same pre-registered book.
The answer, up front. Adding the insider factor moves the Fama-French alpha HAC t-stat from 0.61 to 0.62 — i.e. essentially not at all, and nowhere near the t > 3 bar a real edge would need. On every other axis it is flat-to-slightly-worse: information ratio vs SPY 0.330 → 0.320, annualized return 29.1 % → 28.7 %, max drawdown −12.9 % → −15.6 % (deeper), deflated Sharpe 0.949 → 0.940. The isolated marginal series (WITH − BASE) has an annualized return of −0.4 % with an FF-alpha HAC t = −0.18 (indistinguishable from zero, faintly negative). docka's insider signal, folded in this way over this window, carries no independent alpha; M-S6's research-only verdict stands unchanged.
Integrity disclosure — read this before the numbers
This is a controlled A/B on a corpus that is entirely post-2022, which forces two honest disclosures:
- There is NO separate untouched holdout on this data. The engine's split rule keys on a fixed
HOLDOUT_START = 2022-01-01: any window ending after that date is labelledholdout. The Sarto corpus's fundamentals only reach back to 2023-01 (prices to 2021-12 for the momentum burn-in), so adate_to < 2022-01-01"dev/tune" window is not constructible (established at M-S6). Both configs therefore share the same 2023-01 → 2026-08 window. There is no out-of-sample split here. --i-am-overfittingwas used deliberately, and disclosed. BASE was the first (allowed) holdout run. The engine's anti-p-hacking guard then refuses a second distinct parameter set on the holdout split — exactly correct for iterative tuning, but it would block a legitimate pre-registered A/B. WITH_INSIDER is 1 of 2 pre-registered configs (base / with-insider), not an iterative search, so the flag was set to permit it. The flag only lifts the refusal and recordsi_am_overfitting: trueinargs_json; it changes NO computed metric. Both runs are logged infactor_backtest_runs, so the DSR is deflated by the full trial count (n_trials = 2), not by a laundered count of 1.
How to read the result. The honest statistical test here is the Fama-French alpha HAC t-stat comparison (a controlled marginal test, BASE vs WITH_INSIDER on the same window), not a clean out-of-sample holdout claim.
Staging the rigor (stated in advance, not after seeing the number):
- IF the insider factor had shown a materially better, significant FF-alpha (WITH_INSIDER t crossing the t > 3 bar QVM missed), that would be promising but not yet trustworthy — the correct next step would be a rigorous dev/holdout validation (a longer corpus with a genuine pre-2023 dev span, or a purged cross-validation split), run BEFORE any capital claim.
- IF it washed out (FF-alpha staying insignificant, like QVM's t = 0.61), no further validation is warranted — the answer is "insider adds nothing after factors."
We are in the second case. FF-alpha t went 0.61 → 0.62. No further validation is warranted; the answer is that the insider signal adds nothing after factors.
Note — ABLATION not run (deliberate). The plan also specifies an
--ablate INSIDER_SCORErun (insider pillar present, its z zeroed). Zeroing the insider z is ≈ BASE, so BASE already serves as the ablation reference for the marginal test (WITH − BASE). The explicit ablation run is a third distinct holdout hash that added ~20 min and no verdict-relevant information; it was skipped to keep the honest trial count at 2. The marginal contribution is measured directly as WITH_INSIDER − BASE.
How it was run (discipline)
| Item | Value |
|---|---|
| Snapshot mechanism | SQLite backup API (sqlite3.Connection.backup), source opened mode=ro. Corpus never mutated; WAL-safe (committed frames included). |
| Backtest window | 2023-01-01 → 2026-08-01, monthly rebalance; ~2021-12 price burn-in for 12-1 momentum |
| Evaluable rebalance-months | 42 (≥ MIN_MONTHS_FOR_VERDICT = 36 ✓); 43 rebalances |
| Configs (pre-registered) | BASE with_insider=off (reproduces M-S6, 3-pillar QVM, ≥2-of-3); WITH_INSIDER with_insider=on, 6-month staleness (4th pillar, composite ≥2-of-4) |
| Shared params | long_quantile 0.1, gross 2.0/1.0, caps 2.5 %/1.25 %, cost 50 bps, borrow 50 bps, equal-weight pillars, NOT sector-neutral |
| Param hashes | BASE ceafecb5f524 (scorer sarto-factors-v1), WITH_INSIDER d82722e1c0a9 (scorer sarto-factors-v1+insider=6m) — two distinct scorer_versions |
DSR trial count (n_trials) | 2 (two distinct logged holdout hashes; logged_floor = 2). DSR deflated by 2. |
| Insider factor | Reads the signals table read-only: latest non-superseded cluster with actionable_at ≤ as_of, staleness cap 6 months, sparse (a name with no cluster is absent, never 0). Does not alter score/scorer.py. |
The decisive comparison — BASE vs WITH_INSIDER
| Metric | BASE (QVM only) | WITH_INSIDER (4 pillars) | Change |
|---|---|---|---|
| Fama-French alpha (monthly) | +0.560 %/mo | +0.531 %/mo | −0.029 pp |
| FF-alpha HAC t-stat ⭐ | 0.607 | 0.621 | +0.014 (both ≪ 3) |
| Annualized long-short return | +29.09 % | +28.75 % | −0.35 pp |
| Annualized vol | 22.76 % | 22.22 % | −0.54 pp |
| Raw Sharpe | 1.239 | 1.253 | +0.014 |
| Excess Sharpe (DSR input) | 1.040 | 1.050 | +0.010 |
| DSR (deflated by 2) | 0.949 | 0.940 | −0.009 |
| Information ratio vs SPY | 0.330 | 0.320 | −0.010 |
| Max drawdown (book / SPY) | −12.95 % / −7.42 % | −15.61 % / −7.42 % | deeper |
| Monthly excess hit-rate | 52.38 % | 52.38 % | 0 |
| Mean turnover | 1.678 | 1.726 | +0.048 |
⭐ The load-bearing number. The whole point of M-S7 was whether the insider signal injects an alpha the QVM book lacked. It does not: the FF-alpha HAC t-stat goes 0.607 → 0.621 — a rounding- error move that leaves it an order of magnitude short of the t > 3 significance bar. The return is still overwhelmingly factor beta, not idiosyncratic alpha. Adding insider does not reduce the momentum dependence either: the UMD (momentum) loading stays dominant at 0.897 → 0.845.
Marginal contribution (WITH − BASE return series, FF-tested). Annualized-return difference −0.42 %/yr, monthly FF-alpha −0.029 %/mo at HAC t = −0.177. The marginal series delivered by the insider factor is statistically indistinguishable from zero and, if anything, faintly negative. Over this window and construction, the insider pillar's net effect on the book is nil-to-slightly-harmful: it trims a hair of return, deepens the worst drawdown, and lowers the deflated Sharpe and information ratio, while leaving the (absent) alpha absent.
Full metric blocks
BASE (QVM only — M-S6 reproduction)
| Metric | Value |
|---|---|
| Annualized return / vol | +29.09 % / 22.76 % |
| Raw / excess Sharpe | 1.239 / 1.040 |
| DSR (n_trials 2) | 0.949 |
| Total return (book / SPY) | +144.4 % / +104.0 % |
| Max drawdown (book / SPY) | −12.95 % / −7.42 % |
| IR / beta / TE vs SPY | 0.330 / 0.757 / 20.88 % |
| Monthly excess hit-rate | 52.38 % |
| Mean turnover | 1.678 |
| Long-only ann / Sharpe / total | +15.60 % / 0.867 / +66.1 % |
| FF alpha / HAC t | +0.560 %/mo / 0.607 |
| FF betas | mkt-RF 0.914, UMD 0.897, RMW 0.589, HML 0.236, SMB 0.147, CMA −0.061 |
WITH_INSIDER (4 pillars, 6-month staleness)
| Metric | Value |
|---|---|
| Annualized return / vol | +28.75 % / 22.22 % |
| Raw / excess Sharpe | 1.253 / 1.050 |
| DSR (n_trials 2) | 0.940 |
| Total return (book / SPY) | +142.1 % / +104.0 % |
| Max drawdown (book / SPY) | −15.61 % / −7.42 % |
| IR / beta / TE vs SPY | 0.320 / 0.736 / 20.46 % |
| Monthly excess hit-rate | 52.38 % |
| Mean turnover | 1.726 |
| Long-only ann / Sharpe / total | +15.03 % / 0.855 / +63.2 % |
| FF alpha / HAC t | +0.531 %/mo / 0.621 |
| FF betas | mkt-RF 0.957, UMD 0.845, RMW 0.576, HML 0.282, SMB 0.095, CMA 0.044 |
The two books are near-twins. The insider pillar reshuffles the top decile slightly (turnover ticks up, betas shift marginally) but does not change the character of the return: momentum-dominated, no significant alpha.
The five go/no-go gates on WITH_INSIDER
| # | Gate | Value | Verdict |
|---|---|---|---|
| 1 | Deflated Sharpe ≥ 0.95 (deflated by n_trials = 2) | DSR 0.940 | FAIL |
| 2 | IR vs SPY > 0 AND maxDD ≤ SPY's | IR +0.320 > 0 ✓, but book maxDD −15.6 % vs SPY −7.4 % ✗ | FAIL |
| 3 | FF alpha HAC t > 3 | alpha +0.531 %/mo, t = 0.621 | FAIL |
| 4 | Long-only variant sane | long-only ann +15.03 %, mean > 0 | PASS |
| 5 | Survivorship subset (only if 1-4 pass) | not evaluated — WITH failed gates 1-4, so per plan §5 the ~20-min subset rerun was correctly skipped | n/a |
worth_real_money = FALSE. WITH_INSIDER fails gates 1, 2, and 3.
On gate 1 (a note on trial-count sensitivity). At M-S6's n_trials = 1, BASE's DSR saturated near 1.000 and passed gate 1. At M-S7's honest n_trials = 2, both BASE (0.949) and WITH_INSIDER (0.940) fall just under the 0.95 bar — the deflation by two trials is what pushes them under. This does not change any verdict (both already fail gates 2 and 3), but it is the honest reading: with the insider trial declared, gate 1 no longer passes either. (The M-S6-flagged DSR scale quirk — the annualized excess Sharpe fed where a per-observation Sharpe is expected, which understates deflation — still applies; if anything the true DSR is lower, reinforcing the FAIL.)
Gate 5 was not reached: the plan (§5) runs the survivorship subset only if the candidate clears gates 1-4, precisely to avoid spending ~20 min on a book that has already failed. WITH_INSIDER failed at gate 1, so the subset rerun was skipped.
Every caveat, restated beside the number
- These signals are the box's PRODUCTION scorer v15, pre-computed — not a fresh re-score. The
signalstable was populated by docka's live v15 scorer (reflecting the box's price/catalyst snapshots up to ~2026-08-10), reused here read-only. A fresh local re-score could differ; this is a replay of production scores, not an independent scoring. A materially different scorer could in principle carry alpha this one does not — but on the production signal as it stands, the answer is a wash. - The insider factor is SPARSE and spans only 2023-08 → 2026-08. 15,300 signals over 3,191 issuers; a name without a cluster ≤ as_of simply uses its QVM composite (absent from the insider cross-section, never zero-filled). So the insider factor contributes on ~36 of the 42 evaluable months, and only for the subset of names carrying a signal — expected sparsity. This sparsity is also why the marginal effect is small: on most name-months the composite is unchanged.
- Survivorship = optimistic UPPER BOUND (the #1 threat). The free price feed is effectively currently-listed-only; the delisted-name inclusion rate at M-S6 was 0.00 %. Both legs, especially the long side, are survivorship-inflated. Unchanged by adding the insider factor.
- Borrow cost is MODELLED, not real (50 bps annual + hard-to-borrow screen; no real borrow feed). The short leg — which carries most of the long-short excess — is an upper bound on real shortability.
- Fundamentals are latest-FY, not TTM (M-S8 deferred) — an explicit, revisitable approximation.
- ~42-month sample is SHORT — barely above the 36-month floor; sparse residual degrees of freedom for the FF regression and the DSR. The low end of the Sarto 3-5y minimum.
- Dual-class handling is PARTIAL — 423 names excluded
dual_class_unpriceable; a stated residual single-listed-class market-cap-understatement bias remains. - These are self-found PUBLIC factors (V/M/Q); McLean-Pontiff decay says only ⅓-½ of naive backtest edge survives publication. The insider signal was the one candidate for a non-public edge here — which is exactly why this test mattered, and it did not deliver one.
Bottom line
docka's insider signal does not add real, statistically-significant alpha on top of the QVM factors — it washes out. Head to head on the identical 42-month window and pre-registered book, folding the insider score in as a fourth pillar moved the Fama-French alpha HAC t-stat from 0.61 to 0.62 (both an order of magnitude below the t > 3 bar), and marginally worsened the book on information ratio (0.330 → 0.320), annualized return (29.1 % → 28.7 %), max drawdown (−12.9 % → −15.6 %) and deflated Sharpe (0.949 → 0.940). The isolated marginal series (WITH − BASE) is a statistically-zero −0.4 %/yr at FF-alpha t = −0.18. WITH_INSIDER fails go/no-go gates 1, 2, and 3; worth_real_money = FALSE.
Per the pre-stated rigor ladder, a wash-out is the terminal answer — no further validation is warranted, and no dev/holdout escalation is triggered. M-S6's RESEARCH-ONLY verdict stands: this book, with or without the insider pillar, shows no demonstrated edge after costs, factors, and survivorship. The insider signal, as scored by production v15 and folded in this way over this window, is not the missing alpha.
Runner: scripts/ms7_insider_factor.py (staged + resumable; the only new code — it calls the built
run_port_backtest and evaluate_go_no_go, changes no engine code). Raw result block: the run's
--out JSON.
Sarto — standalone insider-signal IC test (the audit's highest-powered re-test)
Run date: 2026-08-31. Branch: sarto/fix-audit-defects (bug-fixed seams @ 3e66101).
Corpus: the research corpus database (opened read-only: URI mode=ro + PRAGMA query_only=1).
Script: scripts/insider_ic_test.py (pinned by tests/unit/test_insider_ic_test.py).
Verdict up front: the insider score is DEAD as a positive return predictor at 1, 3 and 6 months on this sample — and the one statistically significant pattern in the family runs the WRONG way (the top-scored decile underperforms).
The prior test blended the score as a ¼-weight sparse pillar into a factor composite and measured book-level differences; the audit showed that design had ~no statistical power (70–88 % of the measured difference was structural churn), so the signal had been "shown nothing," not "shown dead." This test removes all portfolio machinery — no pillars, no dilution, no book construction — and asks the signal directly: does the docka insider score rank forward stock returns at all? The answer this time is not "nothing shown." It is: ~zero, measured with enough power to exclude the entire real-world-good band.
Pre-registered spec (implemented exactly; the multiplicity budget is THIS family)
- Observation dates: last XNYS trading day of each month, 2023-08 → 2026-05 (34 months).
- Signal per name per date: the latest non-superseded insider event with
actionable_at ≤ dateand inside a 6-month staleness window — the factor's exact convention (insider.factors.insider.insider_score_as_of:superseded_by IS NULL, neveris_latest). The rawscorevalue is used. - Universe per date: every name with a usable signal AND a resolvable ticker (the FIXED
resolve_ticker, C31) AND an entry bar within 5 sessions on-or-before the date AND a forward bar at the horizon. Names delisting mid-horizon exit at their last available bar — counted, never dropped silently. A month enters a horizon's series only when the horizon's exit session is on or before the corpus's last SPY session (data-end truncation is not delisting). - Forward returns: 21 / 63 / 126 trading sessions (via the XNYS calendar), from adjusted closes, PIT-clean (the unit test pins the PIT clamp, the staleness cap, the delisted-exit fallback, and a hand-computed IC).
- Statistics per horizon: (a) monthly cross-sectional Spearman rank-IC of score vs forward return; time-series mean IC with a Newey-West t (Bartlett kernel, lag = months of horizon overlap: 0 / 2 / 5). (b) equal-weight top-minus-bottom decile spread per month (quintiles in any month with < 100 covered names — none occurred; every month used deciles), mean + NW t; annualized arithmetically (×12 / ×4 / ×2). (c) the market-hedged legs (each leg minus the covered-universe mean); the hedged top-minus-bottom spread is arithmetically identical to the raw spread (the universe mean cancels), so the hedged legs are reported as the decomposition, not as an extra statistic.
- Multiplicity: 3 horizons × 2 statistic families = 6 headline numbers. No others were computed first, and no variant was shopped.
The 6 headline numbers
| Horizon | Months | Mean IC | IC NW-t | IC>0 hit rate | Decile spread / period | Spread NW-t | Spread ann. |
|---|---|---|---|---|---|---|---|
| 1m (21 sess, lag 0) | 34 | +0.0010 | +0.13 | 50.0 % | −2.50 % | −3.31 | −30.0 %/yr |
| 3m (63 sess, lag 2) | 33 | −0.0043 | −0.37 | 42.4 % | −7.15 % | −3.01 | −28.6 %/yr |
| 6m (126 sess, lag 5) | 30 | −0.0045 | −0.54 | 53.3 % | −8.18 % | −2.99 | −16.4 %/yr |
Hedged decomposition (mean per period, vs the covered-universe mean): 1m top −1.31 % / bottom +1.19 %; 3m top −4.23 % / bottom +2.92 %; 6m top −6.60 % / bottom +1.57 %. The negative spread is not one bad month: the monthly spread is negative in 24/34 (1m), 24/33 (3m) and 23/30 (6m) months, medians −2.4 % / −5.8 % / −5.4 %, and the hedged top leg is below the universe mean in 24/34, 31/33 and 27/30 months — the effect lives mostly in the top-scored decile, not the bottom.
Detectable-effect math (the pre-stated power claim, then the actuals)
Pre-stated: with ~34 monthly observations the test can detect a true mean IC ≈ 0.05 at t ≈ 2–3. Actuals are better, because the realized cross-sections are wide (247–1,256 names/month) and the monthly IC series is tight (sd 0.042–0.054): the NW standard errors of the mean IC came out 0.0078 (1m), 0.0117 (3m), 0.0083 (6m), so a true mean IC of 0.016 / 0.023 / 0.017 would have shown t ≥ 2 at 1m / 3m / 6m. Equivalently, the 95 % CIs on the mean IC are [−0.015, +0.017] (1m), [−0.028, +0.019] (3m), [−0.021, +0.012] (6m): every horizon's CI excludes the entire |IC| 0.02–0.05 "real-world-good" band on the positive side. This is the upgrade over the audited pillar test: not "no power, nothing shown" but "power enough, and ~zero is what's there."
Coverage
Per-month coverage below. candidates = ciks with a live in-window signal; unresolved = no
resolvable ticker (12–20 %/month, mostly names with no price join in the corpus); no_entry =
no bar within 5 sessions of the date; dup = second cik on an already-taken ticker (never
occurred); usable = the covered cross-section.
| Obs date | Candidates | Usable | Unresolved | No entry bar |
|---|---|---|---|---|
| 2023-08-31 | 308 | 247 | 58 | 3 |
| 2023-09-29 | 493 | 389 | 102 | 2 |
| 2023-10-31 | 595 | 461 | 129 | 5 |
| 2023-11-30 | 803 | 624 | 173 | 6 |
| 2023-12-29 | 923 | 717 | 199 | 7 |
| 2024-01-31 | 969 | 749 | 216 | 4 |
| 2024-02-29 | 1015 | 798 | 214 | 3 |
| 2024-03-28 | 1031 | 814 | 213 | 4 |
| 2024-04-30 | 1037 | 820 | 212 | 5 |
| 2024-05-31 | 1025 | 828 | 194 | 3 |
| 2024-06-28 | 1019 | 825 | 190 | 4 |
| 2024-07-31 | 1010 | 824 | 182 | 4 |
| 2024-08-30 | 1063 | 873 | 187 | 3 |
| 2024-09-30 | 997 | 817 | 176 | 4 |
| 2024-10-31 | 985 | 813 | 168 | 4 |
| 2024-11-29 | 992 | 818 | 168 | 6 |
| 2024-12-31 | 970 | 801 | 165 | 4 |
| 2025-01-31 | 954 | 782 | 168 | 4 |
| 2025-02-28 | 984 | 809 | 170 | 5 |
| 2025-03-31 | 1116 | 938 | 172 | 6 |
| 2025-04-30 | 1156 | 972 | 176 | 8 |
| 2025-05-30 | 1258 | 1063 | 188 | 7 |
| 2025-06-30 | 1258 | 1070 | 181 | 7 |
| 2025-07-31 | 1244 | 1066 | 172 | 6 |
| 2025-08-29 | 1319 | 1138 | 177 | 4 |
| 2025-09-30 | 1192 | 1027 | 160 | 5 |
| 2025-10-31 | 1154 | 1004 | 146 | 4 |
| 2025-11-28 | 1130 | 1004 | 121 | 5 |
| 2025-12-31 | 1104 | 978 | 121 | 5 |
| 2026-01-30 | 1093 | 969 | 120 | 4 |
| 2026-02-27 | 1123 | 1002 | 116 | 5 |
| 2026-03-31 | 1277 | 1152 | 122 | 3 |
| 2026-04-30 | 1322 | 1200 | 119 | 3 |
| 2026-05-29 | 1377 | 1256 | 119 | 2 |
Name-month totals: 29,648 (1m), 28,392 (3m), 25,038 (6m). Horizon month counts: the 6m window runs out of data after the 2026-01 observation (30 months), and the 3m window for 2026-05-29 lands one session past SPY's last bar (2026-08-27), so 3m has 33 months, not the pre-stated 34 — disclosed rather than patched, per the "exit on or before the last benchmark session" rule.
Delisted names, and a data surprise that matters
- Delisted-mid-horizon name-months (exit at last available bar): 0 (1m), 2 (3m), 1 (6m).
- That near-absence is itself a finding about the corpus: only 56 of 9,945 tickers in the
pricestable stop trading before 2026-07. Three years of a ~1,000-name small-cap cross-section should shed far more names than that, so the price panel is effectively survivors-only — names that delisted during the window mostly have no price history in the corpus at all (they fall out upstream asunresolved/no_entry, not mid-horizon). The delisting-handling machinery in the spec was implemented and tested, but on this corpus it had almost nothing to count. - Direction of the bias: missing delistings among insider-buy names would, if anything, make the top-scored decile look better than reality (its worst outcomes are absent). Survivorship therefore cannot explain the negative spread away — it more plausibly understates it. It DOES mean the absolute levels here are upper-bound-flavored.
- Also noted: the
signalsscore range in this corpus is −37…+130 (not 0–100 as commonly described), andsuperseded_byis NULL on all 15,300 rows, so the non-superseded filter — while implemented per spec — is vacuous on this data.
Caveats
- Box-scorer v15, one generation. Every score is the production box scorer's output as captured in this corpus. This test evaluates that artifact — not insider-filing information content in general, and not any recalibrated scorer.
- 34 monthly observations, one regime. 2023-08 → 2026-05 is a single market stretch; the 3m and 6m series are overlapping (NW lags 2 and 5 correct the t-stats but cannot add independent information — effective samples are nearer ~11 and ~5 independent windows).
- No costs, equal weight, extreme deciles. The spread numbers are frictionless equal-weight top/bottom-decile constructs on names that include small, illiquid tickers; they are a measurement of association, not a tradable return.
- Survivors-only price panel (above): levels are optimistic for the covered names; the cross-sectional ranking claim is less affected but not immune.
Honest verdict (per the pre-stated bands)
Dead. The pre-registered band said IC t < ~1 means the signal is dead at that horizon on this sample: measured IC t = +0.13 / −0.37 / −0.54, with |mean IC| ≤ 0.005 everywhere — an order of magnitude below the 0.02–0.05 real-world-good band, and with confidence intervals tight enough to exclude that band outright at every horizon. As a positive return predictor — the signal's claim, and the direction the bot trades — the docka insider score does not predict forward returns at 1, 3 or 6 months on this corpus, and this time the test had the power to say so. The one band-crossing statistic (decile spread t ≈ −3.0 to −3.3 at all three horizons) has the wrong sign: the top-scored decile underperforms the bottom by ~25–30 %/yr equal-weight, concentrated in the top leg, while the IC sits at zero — i.e., a tail phenomenon (the most heavily insider-bought names do badly; ranks in the middle carry no information), not a monotone inverse signal. That inverted-tail pattern was not pre-registered as a directional hypothesis and is reported as an observation, not a discovery: if anyone wants to pursue "short the top insider decile," that is a NEW pre-registered study on fresh data with the small-cap/illiquidity/cost confounds controlled — this write-up makes no such claim. For the roadmap: the standalone read is strictly worse than "unvalidated"; there is no positive signal here to rescue with more sources at these horizons under scorer v15.
Sarto factor model — CORRECTED record (M-S6/M-S7 rerun on audit-fixed code)
Run date: 2026-08-31. Branch: sarto/fix-audit-defects (@ 3e66101).
Corpus: the research corpus database (read-only), measured on a fresh WAL-safe backup-API snapshot.
SUPERSEDES-NOTE. This document supersedes the numeric records in
docs/sarto-first-number.md(M-S6, BASE numbers) anddocs/sarto-insider-factor-result.md(M-S7, BASE/WITH numbers + the WITH−BASE marginal). Those runs were made on code carrying four confirmed defects (below); their numbers are retracted in favor of the blocks here. Their verdicts — RESEARCH-ONLY, worth_real_money = FALSE, insider adds no alpha — are unchanged by the correction, and their methodology and integrity-disclosure text still applies except where restated here.
Verdict, corrected: STILL RESEARCH-ONLY — not worth real money — and the insider signal still adds nothing. Every headline number improved materially (the bugs deflated returns), but both configs still fail gates 2 and 3, the survivorship upper bound stands, and the newly completed ablation control isolates the insider signal's clean marginal contribution at exactly zero (FF-alpha HAC t = −0.009).
Audit context and the fix list
A 17-agent audit of the M-S6/M-S7 record confirmed four defects in the measurement code (fixed and green at 3e66101, 2688 tests; audit numbering retained):
| # | Defect | Fix |
|---|---|---|
| D1 | Cost double-charge — cost_bps (50bp, a round-trip guess) was charged on every one-way traded delta, i.e. 100bp round-trip | Each one-way delta now pays cost_bps/2; establish + liquidate totals exactly 50bp. At ~1.7×/mo turnover this alone returns ≈ +5%/yr to the book |
| D3 | Preferred-ticker resolution — bar-count tie-break let a preferred fetched one session earlier beat the common (MS → MS-PF) | Dollar volume ranks before bar count, and non-common listings (preferred/warrant/unit/right, bare fifth-letter, curated note/ETN set) are dropped from the candidate pool, with a fallback so a units-only issuer still resolves |
| D4 | False dual-class exclusion — bare-ticker baby bonds/ETNs/depositary listings under the same CIK branded ~81 single-common mega-cap financials/utilities (JPM, WFC, BAC, GS, T, NEE, DUK, SO, …) dual_class_unpriceable | Shared repo_meta.non_common_listings filter; the cohort is back in the universe |
| D5 | Rounding-triggered exclusions — exact-equality reconciliation of the two shares concepts excluded names over millions-rounding hairs (CAT: 0.009%) | 0.5% relative tolerance; genuine divergence still fires |
Measured on this corpus (old vs new flag logic diffed head-to-head at as-of 2025-06-30): 118 CIKs un-flagged (the D4 cohort plus D5 tolerance restorations such as CAT), 0 newly flagged, 1,178 still flagged. GOOG/GOOGL, BRK-A/B and the genuine dual-class set are still detected.
How it was rerun (discipline)
| Item | Value |
|---|---|
| Snapshot | Fresh SQLite backup-API copy of the corpus (source mode=ro); all three runs + the ledger on the snapshot; corpus never mutated |
| Window / params | Identical to the pre-registered record: 2023-01-01 → 2026-08-01, monthly; long_quantile 0.1, gross 2.0/1.0, caps 2.5%/1.25%, cost 50bp now correctly round-trip, borrow 50bp, equal pillars, not sector-neutral |
| Evaluable months | 42 (43 rebalances; FF ends 2026-06) — same as the superseded record |
| Configs | BASE (3-pillar QVM), WITH_INSIDER (4th pillar, 6-mo staleness), ABLATION (--ablate INSIDER_SCORE — insider pillar present, z zeroed) |
| Param hashes | BASE ceafecb5f524… and WITH d82722e1c0a9… — identical to the superseded record (the fixes change no pre-registered knob, so these are identical-hash reruns, always allowed on holdout). ABLATION 5a3fbe906576… is a new, third distinct hash |
| Overfitting flag | BASE ran first on the empty ledger (allowed). WITH and ABLATION carry i_am_overfitting — WITH as the pre-registered A/B arm (disclosure unchanged from M-S7), ABLATION as the audit-ordered, pre-registered control completing the run M-S7 deliberately skipped. The flag lifts the second-distinct-hash refusal and changes no computed metric |
| DSR trial count | n_trials = 3 (three distinct logged holdout hashes; floor = 3) — the honest count. DSR is reported at n_trials=3 (primary) and n_trials=1 (side-computation, both disclosed below) |
Why the ablation matters. WITH−BASE mixes two effects: the insider signal and the 3→4-pillar structural churn (the ≥2-of-4 composite coverage rule reshuffles the eligible cross-section even where no insider z exists). ABLATION holds the 4-pillar structure and eligible sets identical to WITH but zeroes the insider z — so WITH − ABLATION is the only clean isolation of the signal, and ABLATION − BASE prices the structural churn.
THE KEY NEW NUMBER — the clean insider-signal isolation
| Marginal series (42 mo) | Mean monthly | Ann. return diff | FF alpha (mo) | HAC t |
|---|---|---|---|---|
| WITH − ABLATION (signal, clean) ⭐ | +0.001% | −0.10%/yr | −0.0012% | −0.009 |
| WITH − BASE (continuity w/ old record) | +0.019% | +0.14%/yr | −0.0051% | −0.035 |
| ABLATION − BASE (structural churn) | +0.018% | +0.17%/yr | −0.0039% | −0.021 |
⭐ The audit-ordered control, now completed. The insider signal's clean marginal contribution is zero to the fourth decimal: +0.1bp/mo mean, FF-alpha t = −0.009. And the decomposition shows ~95% of the (tiny) WITH−BASE mean difference is structural churn, not signal (+1.78bp/mo of the +1.88bp/mo total). M-S7's shortcut of reading WITH−BASE as the marginal was directionally right but conflated: the honest split is churn ≈ everything, signal ≈ nothing. On the corrected code, over this window, docka's insider signal carries no independent alpha — same verdict as the buggy record, now on a defensible control.
Corrected metric blocks
| Metric | BASE (QVM) | WITH_INSIDER | ABLATION |
|---|---|---|---|
| Annualized L-S return | +35.58% | +35.92% | +35.77% |
| Annualized vol | 22.43% | 22.05% | 22.76% |
| Raw Sharpe | 1.477 | 1.513 | 1.465 |
| Excess Sharpe (DSR input) | 1.276 | 1.308 | 1.267 |
| DSR @ n_trials=3 (primary) | 0.967 | 0.956 | 0.963 |
| DSR @ n_trials=1 (disclosed side-calc) | 0.997 | 0.995 | 0.996 |
| IR vs SPY | 0.576 | 0.596 | 0.587 |
| Beta / TE vs SPY | 0.754 / 20.54% | 0.744 / 20.21% | 0.805 / 20.51% |
| Max drawdown (book / SPY) | −10.31% / −7.42% | −12.79% / −7.42% | −14.43% / −7.42% |
| Monthly excess hit-rate | 52.4% | 54.8% | 57.1% |
| Mean turnover | 1.674 | 1.723 | 1.689 |
| Total return (book / SPY +104.0%) | +190.2% | +192.8% | +191.6% |
| Long-only ann / Sharpe / total | +17.73% / 0.968 / +77.1% | +17.13% / 0.958 / +73.9% | +17.00% / 0.930 / +73.2% |
| FF alpha (monthly) / HAC t | +0.999% / t=1.092 | +0.994% / t=1.150 | +0.995% / t=1.119 |
| FF betas (mkt-RF, SMB, HML, RMW, CMA, UMD) | 0.89, 0.18, 0.19, 0.57, −0.08, 0.88 | 0.96, 0.16, 0.26, 0.63, −0.03, 0.83 | 0.92, 0.17, 0.13, 0.46, 0.04, 0.90 |
The return is still overwhelmingly factor beta — UMD loads 0.83–0.90 in all three configs — and the alpha t-stat, though roughly doubled by the corrections, is still nowhere near the bar.
The corrected five-gate table (BASE and WITH_INSIDER)
| # | Gate | BASE | WITH_INSIDER |
|---|---|---|---|
| 1 | DSR ≥ 0.95 (deflated by the honest n_trials = 3) | 0.967 — PASS | 0.956 — PASS |
| 2 | IR vs SPY > 0 and maxDD ≤ SPY's | IR 0.576 ✓ but −10.31% vs −7.42% ✗ — FAIL | IR 0.596 ✓ but −12.79% vs −7.42% ✗ — FAIL |
| 3 | FF alpha HAC t > 3 | t = 1.09 — FAIL | t = 1.15 — FAIL |
| 4 | Long-only sane (mean > 0) | +17.7%/yr — PASS | +17.1%/yr — PASS |
| 5 | Survivorship subset (formally evaluated only if 1–4 pass) | not reached (plan §5) — and the 5b evidence, measured directly: delisted-name inclusion 0.00% (0 of 15,183 name-months) → would FAIL the 2% floor; the long side remains an upper bound | same: 0.00% (0 of 15,178) → would FAIL |
worth_real_money = FALSE for both. Gate 1 now passes under both DSR conventions (the cost fix raised the Sharpe; note the M-S6 DSR-scale quirk is also fixed — the per-observation de-annualisation is pinned by trap test, so these DSRs are on the correct scale). That changes nothing: gates 2 and 3 fail exactly as before, and gate 5's measured 0% delisted inclusion means every one of these return numbers is a survivorship-optimistic ceiling.
Sanity checks on the corrected cross-section
- The falsely-excluded mega-caps are back and get used. JPM, WFC, BAC, GS, T are eligible in
universe_membershipat all 43 rebalance dates. Of the 118 restored CIKs, 47 distinct restored names actually enter the BASE book (50 in WITH) — JPM, WFC, BAC, T, CAT, HBAN, ZION, ARM, SMCI, NOW, CHTR, … — for 422 long-short name-months in BASE (~2.8% of all book name-months). GS is eligible throughout but never ranks into the top/bottom decile. - No preferred tickers in the book, with one measured, de-minimis residual. Scanning every book name in all three configs against the non-common filter finds only AHL-PE (5 name-months) and TRTN-PC (1 name-month) — 6 of ~15,183 name-months (0.04%). These are the documented D3 fallback, not a regression: both issuers' common was delisted (acquisitions) and the preferred is the CIK's only live listing, so the resolver's "nothing else survives" branch keeps them rather than dropping the issuer entirely.
- Universe scale: 108,399 eligible name-dates across 43 builds; 17,443
dual_class_unpriceablename-dates remain (the genuine cohort).
What changed vs the buggy record — honest read
| Number (BASE) | Buggy (superseded) | Corrected | Driver |
|---|---|---|---|
| Ann. L-S return | +29.1% | +35.6% | D1 halved effective costs (≈ +5%/yr at 1.67 turnover) + restored cross-section |
| Excess Sharpe / DSR | 1.04 / 0.949 @n=2 | 1.28 / 0.967 @n=3 | same; gate 1 flips to PASS despite the larger trial deflation |
| IR vs SPY | 0.33 | 0.58 | same |
| Max drawdown | −12.9% | −10.3% | costs + the restored (lower-vol, mega-cap financial) longs |
| FF alpha / HAC t | +0.56%/mo, t=0.61 | +1.00%/mo, t=1.09 | roughly doubled — but still ⅓ of the t>3 bar |
| WITH−BASE marginal | −0.42%/yr, t=−0.18 | +0.14%/yr, t=−0.035 | noise around zero in both records |
| WITH−ABLATION (new) | never run | −0.10%/yr, t=−0.009 | the clean control: insider = zero |
The bugs were material and conservative-biased (they suppressed returns ≈6.5%/yr and corrupted the cross-section), and fixing them makes the book look meaningfully better — but they were not verdict-relevant in either direction: the strategy remains a momentum-dominated, survivorship-capped factor book with no significant alpha, and the insider pillar's contribution, now cleanly isolated for the first time, is statistically and economically nil.
Every carried caveat, restated
- Survivorship = optimistic UPPER BOUND — 0.00% delisted-name inclusion, measured (the #1 threat, unchanged). Every return/Sharpe/IR number here is a ceiling, not an estimate.
- 42-month sample — barely above the 36-month verdict floor; thin FF residual dof.
- Gate-3 power limit — at this run's HAC SE (≈0.86–0.91%/mo), t > 3 needs ≈ 2.6–2.7%/mo alpha (~35%+/yr idiosyncratic); the test can only reject enormous alphas at this T.
- Insider signals are the box's production scorer v15, replayed read-only — not a fresh re-score; sparse (2023-08→2026-08, ~36 of 42 months, signal-carrying subset only).
- Whole-window holdout — no pre-2023 dev split is constructible on this corpus; the honest reading is the controlled A/B + ablation on one window, not out-of-sample validation.
- Borrow is modelled (50bp + HTB screen), fundamentals are latest-FY not TTM, and V/M/Q are self-found public factors (publication decay applies).
- AHL-PE / TRTN-PC residual (0.04% of name-months) via the documented resolver fallback.
Bottom line
The 17-agent audit was right that the numbers were wrong, and wrong about nothing else: on fixed code the book earns +35.6%/yr (BASE) at a 0.58 IR with a passing deflated Sharpe — and it is still repackaged momentum (UMD 0.88, FF-alpha t 1.09 ≪ 3) on a survivorship ceiling, still drawing down deeper than SPY, still not worth real money. The previously-skipped ablation control, now run, settles M-S7's question at the cleanest level available: WITH − ABLATION = −0.10%/yr at t = −0.009 — docka's insider signal, isolated from structural churn, contributes nothing. RESEARCH-ONLY stands.
Runner: scripts/ms7_insider_factor.py (staged, checkpoint-resumable; base → with →
ablation → aggregate, each stage its own process). Raw blocks: the run's --out JSON;
restored-cohort diff + DSR side-calcs: session analysis scripts against the same snapshot.