From b6892d13fd87839ba50e28611d173a5a150e0860 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sat, 18 Jul 2026 20:32:57 +0200 Subject: [PATCH 1/6] fix: resolve research universe without system_settings DB Public/FMP/seed symbol lists no longer touch SystemSetting cache, so the extender works offline on an empty in-memory session. --- scripts/extend_snapshot_universe.py | 74 ++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/scripts/extend_snapshot_universe.py b/scripts/extend_snapshot_universe.py index e3585b8..6b0f28b 100644 --- a/scripts/extend_snapshot_universe.py +++ b/scripts/extend_snapshot_universe.py @@ -108,32 +108,62 @@ def _ensure_rank_only_table(conn) -> None: async def _resolve_pool() -> tuple[list[str], dict[str, str]]: - """Return sorted unique symbols and source labels.""" - from app.database import async_session_factory - from app.services.ticker_universe_service import fetch_universe_symbols + """Return sorted unique symbols and source labels. + + Offline-safe: does **not** use production Postgres or SystemSetting cache + (those require a schema). Public sources first, then FMP, then seeds. + """ + from app.services.ticker_universe_service import ( + _SEED_UNIVERSES, + _fetch_universe_symbols_from_fmp, + _fetch_universe_symbols_from_public, + _normalise_symbols, + ) sources: dict[str, str] = {} symbols: set[str] = set() - # Need a DB session for cache writes; use local async engine if configured, - # but public/FMP fetch works with any session. Prefer a throwaway sqlite. - from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine - from sqlalchemy.ext.asyncio import AsyncSession - engine = create_async_engine("sqlite+aiosqlite:///:memory:") - Session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) - try: - async with Session() as db: - for universe in ("nasdaq_all", "sp500"): - try: - syms, src = await fetch_universe_symbols(db, universe) - except Exception as exc: - print(f"WARNING: universe {universe} failed: {exc}") - continue - sources[universe] = src - symbols.update(syms) - print(f" {universe}: {len(syms)} symbols (source={src})") - finally: - await engine.dispose() + for universe in ("nasdaq_all", "sp500"): + cleaned: list[str] = [] + src = "none" + + public_symbols, public_failures, public_source = ( + await _fetch_universe_symbols_from_public(universe) + ) + cleaned = _normalise_symbols(public_symbols) + if cleaned: + src = public_source or "public" + else: + if public_failures: + print( + f" WARNING: public fetch {universe}: " + f"{'; '.join(public_failures[:3])}" + ) + try: + fmp_symbols = await _fetch_universe_symbols_from_fmp(universe) + cleaned = _normalise_symbols(fmp_symbols) + if cleaned: + src = "fmp" + except Exception as exc: + print(f" WARNING: FMP fetch {universe}: {exc}") + + if not cleaned: + cleaned = _normalise_symbols(_SEED_UNIVERSES.get(universe, [])) + if cleaned: + src = "seed" + print( + f" WARNING: {universe} fell back to seed list " + f"({len(cleaned)} symbols) — not full universe" + ) + + if not cleaned: + print(f" WARNING: universe {universe} returned no symbols") + continue + + sources[universe] = src + symbols.update(cleaned) + print(f" {universe}: {len(cleaned)} symbols (source={src})") + return sorted(symbols), sources From 30286111a8da09ad9c8aa5c45c4b4071d2399036 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sat, 18 Jul 2026 20:34:38 +0200 Subject: [PATCH 2/6] fix: per-symbol SQLite transactions in research snapshot extender Avoid inactive-transaction crashes from mixing connection.commit with ORM Session. Write path is raw SQL, one begin() block per symbol. --- scripts/extend_snapshot_universe.py | 182 ++++++++++++++++------------ 1 file changed, 105 insertions(+), 77 deletions(-) diff --git a/scripts/extend_snapshot_universe.py b/scripts/extend_snapshot_universe.py index 6b0f28b..9d5ab37 100644 --- a/scripts/extend_snapshot_universe.py +++ b/scripts/extend_snapshot_universe.py @@ -34,8 +34,7 @@ import time from datetime import date, datetime, timedelta, timezone from pathlib import Path -from sqlalchemy import create_engine, select, text -from sqlalchemy.orm import Session +from sqlalchemy import create_engine, text ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: @@ -93,18 +92,19 @@ def _parse_args() -> argparse.Namespace: return p.parse_args() -def _ensure_rank_only_table(conn) -> None: - conn.execute( - text( - """ - CREATE TABLE IF NOT EXISTS research_rank_only ( - ticker_id INTEGER PRIMARY KEY, - symbol TEXT NOT NULL UNIQUE +def _ensure_rank_only_table(engine) -> None: + """DDL in its own connection/transaction (don't share with ORM Session).""" + with engine.begin() as conn: + conn.execute( + text( + """ + CREATE TABLE IF NOT EXISTS research_rank_only ( + ticker_id INTEGER PRIMARY KEY, + symbol TEXT NOT NULL UNIQUE + ) + """ ) - """ ) - ) - conn.commit() async def _resolve_pool() -> tuple[list[str], dict[str, str]]: @@ -213,8 +213,6 @@ async def _main() -> None: print(f"Updating existing research snapshot: {output}") from app.config import settings - from app.models.ohlcv import OHLCVRecord - from app.models.ticker import Ticker from app.providers.alpaca import AlpacaOHLCVProvider if not settings.alpaca_api_key or not settings.alpaca_api_secret: @@ -228,30 +226,32 @@ async def _main() -> None: pool, sources = await _resolve_pool() print(f"Pool size: {len(pool)} (sources={sources})") - # Sync sqlite via sqlalchemy core (simpler than async for bulk insert) - engine = create_engine(f"sqlite:///{output.resolve().as_posix()}") - with Session(engine) as session: - _ensure_rank_only_table(session.connection()) - existing = { - row.symbol: row - for row in session.execute(select(Ticker)).scalars().all() - } - prod_symbols = set(existing) + # Sync sqlite via raw SQL — one short transaction per symbol so a failed + # write never leaves the session in "transaction is inactive". + engine = create_engine( + f"sqlite:///{output.resolve().as_posix()}", + future=True, + ) + _ensure_rank_only_table(engine) + + with engine.connect() as conn: + existing_rows = conn.execute( + text("SELECT id, symbol FROM tickers") + ).fetchall() + existing_ids = {str(sym): int(tid) for tid, sym in existing_rows} + prod_symbols = set(existing_ids) - # Bar counts bar_counts: dict[str, int] = {} - for sym, ticker in existing.items(): - n = session.execute( + for sym, tid in existing_ids.items(): + n = conn.execute( text("SELECT COUNT(*) FROM ohlcv_records WHERE ticker_id = :tid"), - {"tid": ticker.id}, + {"tid": tid}, ).scalar_one() bar_counts[sym] = int(n) to_fetch: list[str] = [] for sym in pool: - if sym in existing and bar_counts.get(sym, 0) >= args.min_bars: - # Existing production or previously extended — keep rank_only - # only for *new* research names, not original prod universe. + if sym in existing_ids and bar_counts.get(sym, 0) >= args.min_bars: continue to_fetch.append(sym) @@ -262,6 +262,16 @@ async def _main() -> None: ok = 0 fail = 0 t0 = time.monotonic() + + insert_ohlcv = text( + """ + INSERT INTO ohlcv_records + (ticker_id, date, open, high, low, close, volume, created_at) + VALUES + (:ticker_id, :date, :open, :high, :low, :close, :volume, :created_at) + """ + ) + for index, sym in enumerate(to_fetch, 1): try: bars = await _fetch_symbol_bars( @@ -284,55 +294,71 @@ async def _main() -> None: print(f" [{index}/{len(to_fetch)}] {sym} empty") continue - ticker = existing.get(sym) - is_new = ticker is None - if ticker is None: - ticker = Ticker(symbol=sym, name=None, created_at=datetime.now(timezone.utc)) - session.add(ticker) - session.flush() - existing[sym] = ticker + try: + with engine.begin() as write: + ticker_id = existing_ids.get(sym) + is_new = ticker_id is None + if is_new: + write.execute( + text( + "INSERT INTO tickers (symbol, name, created_at) " + "VALUES (:sym, NULL, :created)" + ), + { + "sym": sym, + "created": datetime.now(timezone.utc).isoformat(), + }, + ) + ticker_id = int( + write.execute( + text("SELECT id FROM tickers WHERE symbol = :sym"), + {"sym": sym}, + ).scalar_one() + ) + existing_ids[sym] = ticker_id - # Upsert bars (delete+insert range for simplicity on research path) - session.execute( - text( - "DELETE FROM ohlcv_records WHERE ticker_id = :tid " - "AND date >= :start AND date <= :end" - ), - {"tid": ticker.id, "start": start.isoformat(), "end": end.isoformat()}, - ) - now = datetime.utcnow() - session.bulk_insert_mappings( - OHLCVRecord, - [ - { - "ticker_id": ticker.id, - "date": b.date, - "open": b.open, - "high": b.high, - "low": b.low, - "close": b.close, - "volume": b.volume, - "created_at": now, - } - for b in bars - ], - ) - - # rank_only only for names that were NOT in the original production - # snapshot at copy time (or are newly introduced to this research DB). - if is_new or sym not in prod_symbols: - # Re-evaluate: if source copy already had the symbol, don't flag. - # Only new inserts get rank_only. - if is_new: - session.execute( + write.execute( text( - "INSERT OR REPLACE INTO research_rank_only " - "(ticker_id, symbol) VALUES (:tid, :sym)" + "DELETE FROM ohlcv_records WHERE ticker_id = :tid " + "AND date >= :start AND date <= :end" ), - {"tid": ticker.id, "sym": sym}, + { + "tid": ticker_id, + "start": start.isoformat(), + "end": end.isoformat(), + }, ) + now = datetime.now(timezone.utc).replace(tzinfo=None) + write.execute( + insert_ohlcv, + [ + { + "ticker_id": ticker_id, + "date": b.date.isoformat(), + "open": float(b.open), + "high": float(b.high), + "low": float(b.low), + "close": float(b.close), + "volume": int(b.volume), + "created_at": now.isoformat(), + } + for b in bars + ], + ) + if is_new: + write.execute( + text( + "INSERT OR REPLACE INTO research_rank_only " + "(ticker_id, symbol) VALUES (:tid, :sym)" + ), + {"tid": ticker_id, "sym": sym}, + ) + except Exception as exc: + fail += 1 + if not args.quiet: + print(f" [{index}/{len(to_fetch)}] {sym} WRITE FAIL {exc}") + continue - session.commit() ok += 1 if not args.quiet and (index % 25 == 0 or index == len(to_fetch)): elapsed = time.monotonic() - t0 @@ -341,11 +367,13 @@ async def _main() -> None: f"elapsed={elapsed/60:.1f}m last={sym} bars={len(bars)}" ) - rank_only_n = session.execute( + rank_only_n = conn.execute( text("SELECT COUNT(*) FROM research_rank_only") ).scalar_one() - ticker_n = session.execute(text("SELECT COUNT(*) FROM tickers")).scalar_one() - ohlcv_n = session.execute(text("SELECT COUNT(*) FROM ohlcv_records")).scalar_one() + ticker_n = conn.execute(text("SELECT COUNT(*) FROM tickers")).scalar_one() + ohlcv_n = conn.execute( + text("SELECT COUNT(*) FROM ohlcv_records") + ).scalar_one() print("Done.") print(f" output: {output}") From d34c7a21b739df81ff47f7056bfb514bcbda5a46 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sat, 18 Jul 2026 21:26:13 +0200 Subject: [PATCH 3/6] done --- docs/research/fip-breadth-ic.md | 38 +- .../fip-breadth-20260718-211440-breadth.json | 596 ++++++++++++++++++ ...p-breadth-20260718-211440-fingerprint.json | 578 +++++++++++++++++ reports/fip-breadth-20260718-211440.json | 59 ++ 4 files changed, 1248 insertions(+), 23 deletions(-) create mode 100644 reports/fip-breadth-20260718-211440-breadth.json create mode 100644 reports/fip-breadth-20260718-211440-fingerprint.json create mode 100644 reports/fip-breadth-20260718-211440.json diff --git a/docs/research/fip-breadth-ic.md b/docs/research/fip-breadth-ic.md index d02e879..cef816d 100644 --- a/docs/research/fip-breadth-ic.md +++ b/docs/research/fip-breadth-ic.md @@ -1,6 +1,6 @@ # Broad-universe fip_id IC research (Phase B) -Generated: 2026-07-18T19:48:28.127710 +Generated: 2026-07-18T21:14:40.170961 ## Scope @@ -22,33 +22,25 @@ Generated: 2026-07-18T19:48:28.127710 ## Liquid-breadth signal_eval (fip_id) -**Not run yet in this environment** (no Alpaca credentials to build -`backtest_snapshots/research.sqlite`). Local machine with keys: - -```bash -python scripts/extend_snapshot_universe.py --force-copy -python scripts/run_fip_breadth_research.py --skip-fingerprint --allow-spawn --workers 6 -``` - -(`--skip-fingerprint` only after a green fingerprint on this machine.) - | metric | value | |---|---| -| mean_ic | _pending_ | -| ic_t_stat | _pending_ | -| weeks | _pending_ | -| avg_cross_section | _pending_ | -| reliable | _pending_ | +| mean_ic | 0.0575 | +| ic_t_stat | 5.12 | +| ic_positive_pct | 88.6 | +| weeks | 35 | +| avg_cross_section | 1471.2 | +| reliable | True | +| mean_quintile_spread | 0.0199 | ## Verdict (iron rule) -- **Fingerprint:** green (IC −0.045 / t −2.91 / reliable / 35 weeks). -- **Breadth iron rule:** **pending** until research.sqlite run completes. -- Green breadth would authorize a **follow-up proposal** only (two-tier universe / - gate revalidation) — **not** production wire-in. +- **Green: False** +- iron rule not met on liquid-breadth cross-section +- Checks: `{"mean_ic": 0.0575, "abs_mean_ic_ge_0_03": true, "sign_negative": false, "ic_t_stat": 5.12, "reliable": true, "weeks": 35, "avg_cross_section": 1471.2}` + +A green verdict authorizes a **follow-up proposal** only (two-tier universe / gate revalidation) — **not** production wire-in. ## Artifacts -- Fingerprint report: `reports/fip-breadth-20260718-194828-fingerprint.json` - (and summary `reports/fip-breadth-20260718-194828.json`) -- Breadth report: _pending_ +- Fingerprint report: `reports/fip-breadth-20260718-211440-fingerprint.json` +- Breadth report: `reports/fip-breadth-20260718-211440-breadth.json` diff --git a/reports/fip-breadth-20260718-211440-breadth.json b/reports/fip-breadth-20260718-211440-breadth.json new file mode 100644 index 0000000..c586f74 --- /dev/null +++ b/reports/fip-breadth-20260718-211440-breadth.json @@ -0,0 +1,596 @@ +{ + "generated_at": "2026-07-18T19:23:17.726528+00:00", + "tickers": 4650, + "rank_only_tickers": 4144, + "candidates": 202769, + "qualified": 1086, + "params": { + "step_days": 5, + "step_sessions": 5, + "entry_cadence": "weekly", + "signal_eval_cadence": "weekly", + "horizon_days": 30, + "min_lookback": 60, + "cost_per_side_pct": 0.1, + "target_model": "production_gtl", + "target_model_label": "Live GTL (production)", + "is_production_target_model": true, + "production_reentry_policy": "gate_reset", + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0, + "signal_eval_only": true + }, + "activation": { + "min_momentum_percentile": 80.0, + "min_rr": 2.0, + "min_confidence": 0.0, + "require_high_conviction": false, + "exclude_conflicts": false, + "exclude_neutral": true + }, + "overall_qualified": { + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049 + }, + "overall_all": { + "total": 202769, + "wins": 82221, + "losses": 113812, + "expired": 6736, + "hit_rate": 41.9, + "avg_r": -0.04, + "total_r": -8188.17, + "net_avg_r": -0.095, + "net_total_r": -19186.68, + "best_r": 9.24, + "worst_r": -16.42, + "avg_hold_days": 8.2, + "net_r_per_day": -0.0115, + "median_net_r": -1.035, + "profit_factor": 0.85, + "net_avg_r_ex_top5": -0.22 + }, + "by_direction": { + "long": { + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049 + }, + "short": { + "total": 0, + "wins": 0, + "losses": 0, + "expired": 0, + "hit_rate": null, + "avg_r": null, + "total_r": null, + "net_avg_r": null, + "net_total_r": null, + "best_r": null, + "worst_r": null, + "avg_hold_days": null, + "net_r_per_day": null, + "median_net_r": null, + "profit_factor": null, + "net_avg_r_ex_top5": null + } + }, + "min_momentum_percentile": 80.0, + "sweep": [ + { + "min_momentum_percentile": 90.0, + "total": 497, + "wins": 177, + "losses": 269, + "expired": 51, + "hit_rate": 39.7, + "avg_r": 0.276, + "total_r": 137.05, + "net_avg_r": 0.235, + "net_total_r": 116.55, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 11.8, + "net_r_per_day": 0.0199, + "median_net_r": -1.026, + "profit_factor": 1.39, + "net_avg_r_ex_top5": 0.071 + }, + { + "min_momentum_percentile": 80.0, + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049 + }, + { + "min_momentum_percentile": 70.0, + "total": 1841, + "wins": 597, + "losses": 1062, + "expired": 182, + "hit_rate": 36.0, + "avg_r": 0.152, + "total_r": 280.26, + "net_avg_r": 0.104, + "net_total_r": 190.75, + "best_r": 8.85, + "worst_r": -4.21, + "avg_hold_days": 11.8, + "net_r_per_day": 0.0088, + "median_net_r": -1.037, + "profit_factor": 1.16, + "net_avg_r_ex_top5": -0.055 + }, + { + "min_momentum_percentile": 60.0, + "total": 2772, + "wins": 873, + "losses": 1611, + "expired": 288, + "hit_rate": 35.1, + "avg_r": 0.126, + "total_r": 348.07, + "net_avg_r": 0.075, + "net_total_r": 209.25, + "best_r": 8.85, + "worst_r": -4.21, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0063, + "median_net_r": -1.04, + "profit_factor": 1.12, + "net_avg_r_ex_top5": -0.077 + }, + { + "min_momentum_percentile": 50.0, + "total": 3901, + "wins": 1182, + "losses": 2295, + "expired": 424, + "hit_rate": 34.0, + "avg_r": 0.089, + "total_r": 345.37, + "net_avg_r": 0.038, + "net_total_r": 146.96, + "best_r": 8.85, + "worst_r": -4.86, + "avg_hold_days": 12.1, + "net_r_per_day": 0.0031, + "median_net_r": -1.042, + "profit_factor": 1.06, + "net_avg_r_ex_top5": -0.114 + }, + { + "min_momentum_percentile": 0.0, + "total": 14589, + "wins": 3719, + "losses": 9272, + "expired": 1598, + "hit_rate": 28.6, + "avg_r": -0.065, + "total_r": -953.08, + "net_avg_r": -0.115, + "net_total_r": -1677.9, + "best_r": 9.24, + "worst_r": -15.94, + "avg_hold_days": 12.1, + "net_r_per_day": -0.0095, + "median_net_r": -1.043, + "profit_factor": 0.84, + "net_avg_r_ex_top5": -0.275 + } + ], + "gate_ablation": [ + { + "variant": "all_floors", + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049, + "hold_days": 30, + "hold_avg_r": 0.631, + "hold_net_avg_r": 0.585, + "hold_total_r": 684.97 + }, + { + "variant": "no_confidence_floor", + "total": 1093, + "wins": 380, + "losses": 596, + "expired": 117, + "hit_rate": 38.9, + "avg_r": 0.25, + "total_r": 273.55, + "net_avg_r": 0.204, + "net_total_r": 222.99, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 11.9, + "net_r_per_day": 0.0171, + "median_net_r": -1.031, + "profit_factor": 1.33, + "net_avg_r_ex_top5": 0.045, + "hold_days": 30, + "hold_avg_r": 0.626, + "hold_net_avg_r": 0.58, + "hold_total_r": 684.08 + }, + { + "variant": "no_rr_floor", + "total": 6849, + "wins": 3235, + "losses": 3409, + "expired": 205, + "hit_rate": 48.7, + "avg_r": 0.112, + "total_r": 770.17, + "net_avg_r": 0.061, + "net_total_r": 418.96, + "best_r": 8.85, + "worst_r": -5.16, + "avg_hold_days": 7.9, + "net_r_per_day": 0.0078, + "median_net_r": -0.061, + "profit_factor": 1.11, + "net_avg_r_ex_top5": -0.061, + "hold_days": 30, + "hold_avg_r": 0.354, + "hold_net_avg_r": 0.303, + "hold_total_r": 2425.86 + }, + { + "variant": "no_neutral_exclusion", + "total": 2313, + "wins": 770, + "losses": 1279, + "expired": 264, + "hit_rate": 37.6, + "avg_r": 0.2, + "total_r": 462.35, + "net_avg_r": 0.154, + "net_total_r": 355.89, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.6, + "net_r_per_day": 0.0122, + "median_net_r": -1.031, + "profit_factor": 1.25, + "net_avg_r_ex_top5": 0.004, + "hold_days": 30, + "hold_avg_r": 0.583, + "hold_net_avg_r": 0.537, + "hold_total_r": 1348.89 + }, + { + "variant": "momentum_only", + "total": 14696, + "wins": 6827, + "losses": 7359, + "expired": 510, + "hit_rate": 48.1, + "avg_r": 0.114, + "total_r": 1669.64, + "net_avg_r": 0.064, + "net_total_r": 936.93, + "best_r": 8.85, + "worst_r": -5.71, + "avg_hold_days": 8.4, + "net_r_per_day": 0.0076, + "median_net_r": -1.013, + "profit_factor": 1.11, + "net_avg_r_ex_top5": -0.055, + "hold_days": 30, + "hold_avg_r": 0.395, + "hold_net_avg_r": 0.345, + "hold_total_r": 5798.82 + } + ], + "gate_ablation_note": "Each row re-qualifies the same candidates at the current momentum cutoff (80) with one floor removed (long-only while the momentum gate is active). If dropping a floor doesn't hurt net expectancy, that floor isn't pulling its weight. The Hold columns grade the same variants under the hold-to-horizon time exit instead of the S/R target \u2014 the view that matters if the exit policy moves to a fixed hold.", + "time_exit_sweep": [ + { + "hold_days": 5, + "total": 1086, + "wins": 603, + "win_rate": 55.5, + "avg_r": 0.175, + "total_r": 190.16, + "net_avg_r": 0.129, + "net_total_r": 139.97, + "best_r": 5.09, + "worst_r": -2.51, + "avg_hold_days": 4.5, + "net_r_per_day": 0.0285, + "median_net_r": 0.115, + "profit_factor": 1.36, + "net_avg_r_ex_top5": -0.002 + }, + { + "hold_days": 10, + "total": 1086, + "wins": 559, + "win_rate": 51.5, + "avg_r": 0.357, + "total_r": 387.9, + "net_avg_r": 0.311, + "net_total_r": 337.7, + "best_r": 6.73, + "worst_r": -2.51, + "avg_hold_days": 7.9, + "net_r_per_day": 0.0395, + "median_net_r": 0.031, + "profit_factor": 1.67, + "net_avg_r_ex_top5": 0.112 + }, + { + "hold_days": 21, + "total": 1086, + "wins": 487, + "win_rate": 44.8, + "avg_r": 0.525, + "total_r": 570.33, + "net_avg_r": 0.479, + "net_total_r": 520.14, + "best_r": 9.86, + "worst_r": -3.38, + "avg_hold_days": 13.7, + "net_r_per_day": 0.0349, + "median_net_r": -1.027, + "profit_factor": 1.81, + "net_avg_r_ex_top5": 0.191 + }, + { + "hold_days": 30, + "total": 1086, + "wins": 434, + "win_rate": 40.0, + "avg_r": 0.631, + "total_r": 684.97, + "net_avg_r": 0.585, + "net_total_r": 634.78, + "best_r": 12.87, + "worst_r": -3.38, + "avg_hold_days": 17.8, + "net_r_per_day": 0.0329, + "median_net_r": -1.033, + "profit_factor": 1.9, + "net_avg_r_ex_top5": 0.212 + } + ], + "portfolio_sim": { + "params": { + "starting_capital": 10000.0, + "max_positions": 10, + "risk_per_trade_pct": 1.0, + "notional_cap_pct": 20.0, + "cost_per_side_pct": 0.1, + "hold_days": 30 + }, + "policies": [], + "note": "One capital-constrained book over the same qualified setups the tables above grade per-setup: at most 10 concurrent positions (one per ticker), best momentum first, fixed-fractional risk sizing with a no-leverage cap, entries at the detection close, stops filled at the worse of stop or open. 'target' races the S/R target against the stop (timeout at the horizon); 'hold' keeps the initial stop and exits at the horizon close. SPY return is price-only over the same window. In-sample; no dividends." + }, + "strategy_variants": { + "variants": [], + "note": "Research-only hold-to-horizon portfolio variants. Production now uses residual 12-1 momentum at cutoff 80; the remaining rows compare the legacy raw rank, raw cutoff 90, one max-15 capacity check, and volatility overlays." + }, + "exit_policy_variants": { + "variants": [], + "note": "Research-only exit policies over the residual/high-vol 80/20 entry candidate. Every row uses the same entry qualification/ranking and changes only the exit discipline." + }, + "portfolio_monitor": null, + "production_cadence_comparison": null, + "holdout": null, + "min_rr_sweep": null, + "target_model_diagnostics": { + "target_model": "production_gtl", + "target_model_label": "Live GTL (production)", + "candidate_count": 202769, + "primary_source_counts": { + "pivot_point": 196294, + "range_grid": 180039 + }, + "primary_round_only": 0, + "primary_strength_100": 138599, + "avg_primary_strength": 80.109, + "avg_primary_distance_atr": 2.293, + "avg_primary_rejection_count": 41.907, + "avg_raw_level_count": 53.204, + "avg_gate_level_count": 53.204 + }, + "signal_eval": [ + { + "signal": "high_52w", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": 0.1283, + "ic_t_stat": 4.28, + "ic_positive_pct": 85.7, + "mean_quintile_spread": -0.1009, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "mom_12_1", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": 0.0997, + "ic_t_stat": 4.56, + "ic_positive_pct": 88.6, + "mean_quintile_spread": -0.1001, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "mom_6_1", + "weeks": 40, + "avg_cross_section": 1474.8, + "mean_ic": 0.0681, + "ic_t_stat": 3.45, + "ic_positive_pct": 77.5, + "mean_quintile_spread": -0.0322, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "fip_id", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": 0.0575, + "ic_t_stat": 5.12, + "ic_positive_pct": 88.6, + "mean_quintile_spread": 0.0199, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "trend_200", + "weeks": 37, + "avg_cross_section": 1472.8, + "mean_ic": 0.0538, + "ic_t_stat": 2.33, + "ic_positive_pct": 75.7, + "mean_quintile_spread": -0.0675, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "mom_3_1", + "weeks": 42, + "avg_cross_section": 1476.0, + "mean_ic": 0.0523, + "ic_t_stat": 3.27, + "ic_positive_pct": 73.8, + "mean_quintile_spread": -0.0194, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "mom_12_1_resid", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": 0.0388, + "ic_t_stat": 2.28, + "ic_positive_pct": 74.3, + "mean_quintile_spread": -0.0542, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "reversal_1m", + "weeks": 43, + "avg_cross_section": 1476.6, + "mean_ic": 0.0155, + "ic_t_stat": 0.85, + "ic_positive_pct": 48.8, + "mean_quintile_spread": -0.0862, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + { + "signal": "vol_6m", + "weeks": 40, + "avg_cross_section": 1474.8, + "mean_ic": -0.1584, + "ic_t_stat": -6.05, + "ic_positive_pct": 12.5, + "mean_quintile_spread": 0.0164, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + } + ], + "signal_eval_note": "Cross-sectional rank-IC of price-only signals vs the forward 30-day return (min 20 names/window). |IC| \u2273 0.03 with a consistent sign is a real (if small) edge; near 0 means ranking on it sorts nothing. Momentum factors and high_52w are expected positive; reversal_1m and vol_6m expected negative (mean-reversion / low-vol anomaly). IC is measured on non-overlapping windows; signals with fewer than 12 independent windows are flagged unreliable (too few regimes \u2014 deepen history with the Data Backfill job).", + "note": "Sentiment & fundamentals held neutral (no point-in-time history). Stops fill at the worse of the stop or the bar's open (gaps through the stop are modeled, so a loss can exceed \u22121R); targets never fill better than their level. ~6 months \u2248 one market regime \u2014 treat as directional, not gospel.", + "recommendation": { + "headline": "Trade the qualified list long-only; hold 30 trading days with the initial ATR stop.", + "items": [ + { + "topic": "exit", + "text": "Legacy exit diagnostic: hold 30 trading days with the initial stop (+0.58R net/trade vs +0.21R for the S/R target exit)." + }, + { + "topic": "gate", + "text": "Gate: the confidence floor adds nothing \u2014 dropping it costs +0.01R/trade and adds 7 trades." + }, + { + "topic": "gate", + "text": "Gate: keep the R:R floor (worth +0.28R/trade under the hold exit)." + }, + { + "topic": "gate", + "text": "Gate: keep the NEUTRAL exclusion (worth +0.05R/trade under the hold exit)." + }, + { + "topic": "cutoff", + "text": "Residual-momentum cutoff: 90 has the best per-trade net (+0.23R over 497 setups)." + }, + { + "topic": "robustness", + "text": "Robustness: expectancy survives removing the top 5% of winners (+0.21R net/trade under the recommended 30d hold) \u2014 the edge is not a handful of outliers." + } + ], + "note": "Derived from this report's numbers on every run \u2014 the advice flips if the data does." + }, + "research_recommendation": { + "items": [], + "note": "Strategy variants unavailable; re-run the backtest after benchmark data is present." + } +} \ No newline at end of file diff --git a/reports/fip-breadth-20260718-211440-fingerprint.json b/reports/fip-breadth-20260718-211440-fingerprint.json new file mode 100644 index 0000000..0fd69cf --- /dev/null +++ b/reports/fip-breadth-20260718-211440-fingerprint.json @@ -0,0 +1,578 @@ +{ + "generated_at": "2026-07-18T19:16:37.954856+00:00", + "tickers": 506, + "rank_only_tickers": 0, + "candidates": 202765, + "qualified": 1086, + "params": { + "step_days": 5, + "step_sessions": 5, + "entry_cadence": "weekly", + "signal_eval_cadence": "weekly", + "horizon_days": 30, + "min_lookback": 60, + "cost_per_side_pct": 0.1, + "target_model": "production_gtl", + "target_model_label": "Live GTL (production)", + "is_production_target_model": true, + "production_reentry_policy": "gate_reset", + "liquid_breadth_top_n": null, + "liquid_min_price": null, + "signal_eval_only": true + }, + "activation": { + "min_momentum_percentile": 80.0, + "min_rr": 2.0, + "min_confidence": 0.0, + "require_high_conviction": false, + "exclude_conflicts": false, + "exclude_neutral": true + }, + "overall_qualified": { + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049 + }, + "overall_all": { + "total": 202765, + "wins": 82220, + "losses": 113809, + "expired": 6736, + "hit_rate": 41.9, + "avg_r": -0.04, + "total_r": -8186.18, + "net_avg_r": -0.095, + "net_total_r": -19184.55, + "best_r": 9.24, + "worst_r": -16.42, + "avg_hold_days": 8.2, + "net_r_per_day": -0.0115, + "median_net_r": -1.035, + "profit_factor": 0.85, + "net_avg_r_ex_top5": -0.22 + }, + "by_direction": { + "long": { + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049 + }, + "short": { + "total": 0, + "wins": 0, + "losses": 0, + "expired": 0, + "hit_rate": null, + "avg_r": null, + "total_r": null, + "net_avg_r": null, + "net_total_r": null, + "best_r": null, + "worst_r": null, + "avg_hold_days": null, + "net_r_per_day": null, + "median_net_r": null, + "profit_factor": null, + "net_avg_r_ex_top5": null + } + }, + "min_momentum_percentile": 80.0, + "sweep": [ + { + "min_momentum_percentile": 90.0, + "total": 497, + "wins": 177, + "losses": 269, + "expired": 51, + "hit_rate": 39.7, + "avg_r": 0.276, + "total_r": 137.05, + "net_avg_r": 0.235, + "net_total_r": 116.55, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 11.8, + "net_r_per_day": 0.0199, + "median_net_r": -1.026, + "profit_factor": 1.39, + "net_avg_r_ex_top5": 0.071 + }, + { + "min_momentum_percentile": 80.0, + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049 + }, + { + "min_momentum_percentile": 70.0, + "total": 1841, + "wins": 597, + "losses": 1062, + "expired": 182, + "hit_rate": 36.0, + "avg_r": 0.152, + "total_r": 280.26, + "net_avg_r": 0.104, + "net_total_r": 190.75, + "best_r": 8.85, + "worst_r": -4.21, + "avg_hold_days": 11.8, + "net_r_per_day": 0.0088, + "median_net_r": -1.037, + "profit_factor": 1.16, + "net_avg_r_ex_top5": -0.055 + }, + { + "min_momentum_percentile": 60.0, + "total": 2772, + "wins": 873, + "losses": 1611, + "expired": 288, + "hit_rate": 35.1, + "avg_r": 0.126, + "total_r": 348.07, + "net_avg_r": 0.075, + "net_total_r": 209.25, + "best_r": 8.85, + "worst_r": -4.21, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0063, + "median_net_r": -1.04, + "profit_factor": 1.12, + "net_avg_r_ex_top5": -0.077 + }, + { + "min_momentum_percentile": 50.0, + "total": 3901, + "wins": 1182, + "losses": 2295, + "expired": 424, + "hit_rate": 34.0, + "avg_r": 0.089, + "total_r": 345.37, + "net_avg_r": 0.038, + "net_total_r": 146.96, + "best_r": 8.85, + "worst_r": -4.86, + "avg_hold_days": 12.1, + "net_r_per_day": 0.0031, + "median_net_r": -1.042, + "profit_factor": 1.06, + "net_avg_r_ex_top5": -0.114 + }, + { + "min_momentum_percentile": 0.0, + "total": 14588, + "wins": 3719, + "losses": 9271, + "expired": 1598, + "hit_rate": 28.6, + "avg_r": -0.065, + "total_r": -952.08, + "net_avg_r": -0.115, + "net_total_r": -1676.86, + "best_r": 9.24, + "worst_r": -15.94, + "avg_hold_days": 12.1, + "net_r_per_day": -0.0095, + "median_net_r": -1.043, + "profit_factor": 0.84, + "net_avg_r_ex_top5": -0.275 + } + ], + "gate_ablation": [ + { + "variant": "all_floors", + "total": 1086, + "wins": 379, + "losses": 591, + "expired": 116, + "hit_rate": 39.1, + "avg_r": 0.255, + "total_r": 276.76, + "net_avg_r": 0.209, + "net_total_r": 226.56, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.0, + "net_r_per_day": 0.0174, + "median_net_r": -1.031, + "profit_factor": 1.34, + "net_avg_r_ex_top5": 0.049, + "hold_days": 30, + "hold_avg_r": 0.631, + "hold_net_avg_r": 0.585, + "hold_total_r": 684.97 + }, + { + "variant": "no_confidence_floor", + "total": 1093, + "wins": 380, + "losses": 596, + "expired": 117, + "hit_rate": 38.9, + "avg_r": 0.25, + "total_r": 273.55, + "net_avg_r": 0.204, + "net_total_r": 222.99, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 11.9, + "net_r_per_day": 0.0171, + "median_net_r": -1.031, + "profit_factor": 1.33, + "net_avg_r_ex_top5": 0.045, + "hold_days": 30, + "hold_avg_r": 0.626, + "hold_net_avg_r": 0.58, + "hold_total_r": 684.08 + }, + { + "variant": "no_rr_floor", + "total": 6849, + "wins": 3235, + "losses": 3409, + "expired": 205, + "hit_rate": 48.7, + "avg_r": 0.112, + "total_r": 770.17, + "net_avg_r": 0.061, + "net_total_r": 418.96, + "best_r": 8.85, + "worst_r": -5.16, + "avg_hold_days": 7.9, + "net_r_per_day": 0.0078, + "median_net_r": -0.061, + "profit_factor": 1.11, + "net_avg_r_ex_top5": -0.061, + "hold_days": 30, + "hold_avg_r": 0.354, + "hold_net_avg_r": 0.303, + "hold_total_r": 2425.86 + }, + { + "variant": "no_neutral_exclusion", + "total": 2313, + "wins": 770, + "losses": 1279, + "expired": 264, + "hit_rate": 37.6, + "avg_r": 0.2, + "total_r": 462.35, + "net_avg_r": 0.154, + "net_total_r": 355.89, + "best_r": 8.85, + "worst_r": -3.38, + "avg_hold_days": 12.6, + "net_r_per_day": 0.0122, + "median_net_r": -1.031, + "profit_factor": 1.25, + "net_avg_r_ex_top5": 0.004, + "hold_days": 30, + "hold_avg_r": 0.583, + "hold_net_avg_r": 0.537, + "hold_total_r": 1348.89 + }, + { + "variant": "momentum_only", + "total": 14696, + "wins": 6827, + "losses": 7359, + "expired": 510, + "hit_rate": 48.1, + "avg_r": 0.114, + "total_r": 1669.64, + "net_avg_r": 0.064, + "net_total_r": 936.93, + "best_r": 8.85, + "worst_r": -5.71, + "avg_hold_days": 8.4, + "net_r_per_day": 0.0076, + "median_net_r": -1.013, + "profit_factor": 1.11, + "net_avg_r_ex_top5": -0.055, + "hold_days": 30, + "hold_avg_r": 0.395, + "hold_net_avg_r": 0.345, + "hold_total_r": 5798.82 + } + ], + "gate_ablation_note": "Each row re-qualifies the same candidates at the current momentum cutoff (80) with one floor removed (long-only while the momentum gate is active). If dropping a floor doesn't hurt net expectancy, that floor isn't pulling its weight. The Hold columns grade the same variants under the hold-to-horizon time exit instead of the S/R target \u2014 the view that matters if the exit policy moves to a fixed hold.", + "time_exit_sweep": [ + { + "hold_days": 5, + "total": 1086, + "wins": 603, + "win_rate": 55.5, + "avg_r": 0.175, + "total_r": 190.16, + "net_avg_r": 0.129, + "net_total_r": 139.97, + "best_r": 5.09, + "worst_r": -2.51, + "avg_hold_days": 4.5, + "net_r_per_day": 0.0285, + "median_net_r": 0.115, + "profit_factor": 1.36, + "net_avg_r_ex_top5": -0.002 + }, + { + "hold_days": 10, + "total": 1086, + "wins": 559, + "win_rate": 51.5, + "avg_r": 0.357, + "total_r": 387.9, + "net_avg_r": 0.311, + "net_total_r": 337.7, + "best_r": 6.73, + "worst_r": -2.51, + "avg_hold_days": 7.9, + "net_r_per_day": 0.0395, + "median_net_r": 0.031, + "profit_factor": 1.67, + "net_avg_r_ex_top5": 0.112 + }, + { + "hold_days": 21, + "total": 1086, + "wins": 487, + "win_rate": 44.8, + "avg_r": 0.525, + "total_r": 570.33, + "net_avg_r": 0.479, + "net_total_r": 520.14, + "best_r": 9.86, + "worst_r": -3.38, + "avg_hold_days": 13.7, + "net_r_per_day": 0.0349, + "median_net_r": -1.027, + "profit_factor": 1.81, + "net_avg_r_ex_top5": 0.191 + }, + { + "hold_days": 30, + "total": 1086, + "wins": 434, + "win_rate": 40.0, + "avg_r": 0.631, + "total_r": 684.97, + "net_avg_r": 0.585, + "net_total_r": 634.78, + "best_r": 12.87, + "worst_r": -3.38, + "avg_hold_days": 17.8, + "net_r_per_day": 0.0329, + "median_net_r": -1.033, + "profit_factor": 1.9, + "net_avg_r_ex_top5": 0.212 + } + ], + "portfolio_sim": { + "params": { + "starting_capital": 10000.0, + "max_positions": 10, + "risk_per_trade_pct": 1.0, + "notional_cap_pct": 20.0, + "cost_per_side_pct": 0.1, + "hold_days": 30 + }, + "policies": [], + "note": "One capital-constrained book over the same qualified setups the tables above grade per-setup: at most 10 concurrent positions (one per ticker), best momentum first, fixed-fractional risk sizing with a no-leverage cap, entries at the detection close, stops filled at the worse of stop or open. 'target' races the S/R target against the stop (timeout at the horizon); 'hold' keeps the initial stop and exits at the horizon close. SPY return is price-only over the same window. In-sample; no dividends." + }, + "strategy_variants": { + "variants": [], + "note": "Research-only hold-to-horizon portfolio variants. Production now uses residual 12-1 momentum at cutoff 80; the remaining rows compare the legacy raw rank, raw cutoff 90, one max-15 capacity check, and volatility overlays." + }, + "exit_policy_variants": { + "variants": [], + "note": "Research-only exit policies over the residual/high-vol 80/20 entry candidate. Every row uses the same entry qualification/ranking and changes only the exit discipline." + }, + "portfolio_monitor": null, + "production_cadence_comparison": null, + "holdout": null, + "min_rr_sweep": null, + "target_model_diagnostics": { + "target_model": "production_gtl", + "target_model_label": "Live GTL (production)", + "candidate_count": 202765, + "primary_source_counts": { + "pivot_point": 196290, + "range_grid": 180036 + }, + "primary_round_only": 0, + "primary_strength_100": 138596, + "avg_primary_strength": 80.109, + "avg_primary_distance_atr": 2.293, + "avg_primary_rejection_count": 41.908, + "avg_raw_level_count": 53.204, + "avg_gate_level_count": 53.204 + }, + "signal_eval": [ + { + "signal": "vol_6m", + "weeks": 39, + "avg_cross_section": 498.2, + "mean_ic": 0.0609, + "ic_t_stat": 1.48, + "ic_positive_pct": 64.1, + "mean_quintile_spread": 0.0337, + "reliable": true + }, + { + "signal": "mom_12_1_resid", + "weeks": 35, + "avg_cross_section": 497.7, + "mean_ic": 0.0552, + "ic_t_stat": 1.98, + "ic_positive_pct": 60.0, + "mean_quintile_spread": 0.0207, + "reliable": true + }, + { + "signal": "mom_12_1", + "weeks": 35, + "avg_cross_section": 497.7, + "mean_ic": 0.0531, + "ic_t_stat": 1.61, + "ic_positive_pct": 65.7, + "mean_quintile_spread": 0.0206, + "reliable": true + }, + { + "signal": "trend_200", + "weeks": 37, + "avg_cross_section": 497.9, + "mean_ic": 0.0161, + "ic_t_stat": 0.44, + "ic_positive_pct": 59.5, + "mean_quintile_spread": 0.006, + "reliable": true + }, + { + "signal": "reversal_1m", + "weeks": 43, + "avg_cross_section": 498.7, + "mean_ic": 0.0059, + "ic_t_stat": 0.22, + "ic_positive_pct": 53.5, + "mean_quintile_spread": 0.0053, + "reliable": true + }, + { + "signal": "mom_6_1", + "weeks": 39, + "avg_cross_section": 498.2, + "mean_ic": 0.0051, + "ic_t_stat": 0.21, + "ic_positive_pct": 56.4, + "mean_quintile_spread": 0.0087, + "reliable": true + }, + { + "signal": "mom_3_1", + "weeks": 42, + "avg_cross_section": 498.5, + "mean_ic": -0.0064, + "ic_t_stat": -0.25, + "ic_positive_pct": 50.0, + "mean_quintile_spread": 0.0046, + "reliable": true + }, + { + "signal": "high_52w", + "weeks": 35, + "avg_cross_section": 497.7, + "mean_ic": -0.0086, + "ic_t_stat": -0.26, + "ic_positive_pct": 54.3, + "mean_quintile_spread": -0.0088, + "reliable": true + }, + { + "signal": "fip_id", + "weeks": 35, + "avg_cross_section": 497.7, + "mean_ic": -0.045, + "ic_t_stat": -2.91, + "ic_positive_pct": 25.7, + "mean_quintile_spread": -0.0168, + "reliable": true + } + ], + "signal_eval_note": "Cross-sectional rank-IC of price-only signals vs the forward 30-day return (min 20 names/window). |IC| \u2273 0.03 with a consistent sign is a real (if small) edge; near 0 means ranking on it sorts nothing. Momentum factors and high_52w are expected positive; reversal_1m and vol_6m expected negative (mean-reversion / low-vol anomaly). IC is measured on non-overlapping windows; signals with fewer than 12 independent windows are flagged unreliable (too few regimes \u2014 deepen history with the Data Backfill job).", + "note": "Sentiment & fundamentals held neutral (no point-in-time history). Stops fill at the worse of the stop or the bar's open (gaps through the stop are modeled, so a loss can exceed \u22121R); targets never fill better than their level. ~6 months \u2248 one market regime \u2014 treat as directional, not gospel.", + "recommendation": { + "headline": "Trade the qualified list long-only; hold 30 trading days with the initial ATR stop.", + "items": [ + { + "topic": "exit", + "text": "Legacy exit diagnostic: hold 30 trading days with the initial stop (+0.58R net/trade vs +0.21R for the S/R target exit)." + }, + { + "topic": "gate", + "text": "Gate: the confidence floor adds nothing \u2014 dropping it costs +0.01R/trade and adds 7 trades." + }, + { + "topic": "gate", + "text": "Gate: keep the R:R floor (worth +0.28R/trade under the hold exit)." + }, + { + "topic": "gate", + "text": "Gate: keep the NEUTRAL exclusion (worth +0.05R/trade under the hold exit)." + }, + { + "topic": "cutoff", + "text": "Residual-momentum cutoff: 90 has the best per-trade net (+0.23R over 497 setups)." + }, + { + "topic": "robustness", + "text": "Robustness: expectancy survives removing the top 5% of winners (+0.21R net/trade under the recommended 30d hold) \u2014 the edge is not a handful of outliers." + } + ], + "note": "Derived from this report's numbers on every run \u2014 the advice flips if the data does." + }, + "research_recommendation": { + "items": [], + "note": "Strategy variants unavailable; re-run the backtest after benchmark data is present." + } +} \ No newline at end of file diff --git a/reports/fip-breadth-20260718-211440.json b/reports/fip-breadth-20260718-211440.json new file mode 100644 index 0000000..5211c38 --- /dev/null +++ b/reports/fip-breadth-20260718-211440.json @@ -0,0 +1,59 @@ +{ + "generated_at": "2026-07-18T21:14:40.170961", + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0, + "fingerprint": { + "signal": "fip_id", + "weeks": 35, + "avg_cross_section": 497.7, + "mean_ic": -0.045, + "ic_t_stat": -2.91, + "ic_positive_pct": 25.7, + "mean_quintile_spread": -0.0168, + "reliable": true, + "pass": true, + "expected_ic": -0.045, + "expected_t": -2.9 + }, + "breadth": { + "signal": "fip_id", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": 0.0575, + "ic_t_stat": 5.12, + "ic_positive_pct": 88.6, + "mean_quintile_spread": 0.0199, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + }, + "verdict": { + "green": false, + "reason": "iron rule not met on liquid-breadth cross-section", + "checks": { + "mean_ic": 0.0575, + "abs_mean_ic_ge_0_03": true, + "sign_negative": false, + "ic_t_stat": 5.12, + "reliable": true, + "weeks": 35, + "avg_cross_section": 1471.2 + }, + "row": { + "signal": "fip_id", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": 0.0575, + "ic_t_stat": 5.12, + "ic_positive_pct": 88.6, + "mean_quintile_spread": 0.0199, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0 + } + }, + "fingerprint_report_path": "reports/fip-breadth-20260718-211440-fingerprint.json", + "breadth_report_path": "reports/fip-breadth-20260718-211440-breadth.json", + "breadth_tickers": 4650, + "breadth_rank_only_tickers": 4144 +} \ No newline at end of file From ceaaadc49fcae2d19bef2f7048d4042b25fe6cf3 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sat, 18 Jul 2026 21:40:05 +0200 Subject: [PATCH 4/6] research: fip breadth diagnostics + compositional read Add lagged/tier/prod-subset/mom-conditional checks on research.sqlite. Log: unconditional sign is a winner/bleeder tug-of-war; mom-conditional fip stays negative and reliable; warn on high-vol tilt if universe broadens. --- docs/research/README.md | 4 +- docs/research/fip-breadth-ic.md | 151 +++- ...p-breadth-diagnostics-20260718-213705.json | 100 +++ ...p-breadth-diagnostics-20260718-213908.json | 100 +++ scripts/run_fip_breadth_diagnostics.py | 665 ++++++++++++++++++ 5 files changed, 995 insertions(+), 25 deletions(-) create mode 100644 reports/fip-breadth-diagnostics-20260718-213705.json create mode 100644 reports/fip-breadth-diagnostics-20260718-213908.json create mode 100644 scripts/run_fip_breadth_diagnostics.py diff --git a/docs/research/README.md b/docs/research/README.md index 7be97f3..8c49986 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -141,8 +141,8 @@ knobs. | Lead | Why it's interesting | Blocker | |---|---|---| | **Near-close / MOC execution (ops)** | Recovers overnight momentum drift left on the table by a morning EU scan; evidence closed | Implement schedule + partial-bar scan path; one qualifying scan/day only | -| **`fip_id`** (information discreteness over the 12-1 window) | **Strongest cross-sectional signal measured on this universe** — IC −0.045, t = −2.91, correct sign; re-derived fingerprint matched Phase A; ticker technicals show it display-only | Doesn't improve *this* book as a filter. **Phase B tooling ready:** liquid-breadth IC on research.sqlite — see [fip-breadth-ic.md](fip-breadth-ic.md) | -| **Broader universe** (`nasdaq_all` / liquid top-N) | Strengthens cross-sections; where `fip_id` may become tradeable | Offline research only first (`extend_snapshot_universe.py`); not prod scan | +| **`fip_id`** | Fingerprint IC −0.045 / t −2.91 on prod book; display-only on ticker technicals | **Phase B:** unconditional liquid-Nasdaq IC fails iron-rule **sign**; **mom-conditional** fip IC −0.088 / t −4.58 (alive as tilt candidate only). See [fip-breadth-ic.md](fip-breadth-ic.md) | +| **Broader universe** | Composition changes factor signs (fip tug-of-war; high-vol junk) | Any prod broaden must **re-validate 80/20 high-vol tilt** first; offline research only for now | | **Forward paper-trade record** | The only true out-of-sample evidence the snapshot cannot give | Time; mark entries at actual near-close fill once ops ships | | **Better target model for clear-air names** | The return is demonstrably there (#2 wins on raw CAGR in *both* train and test); it's the *flat* 3× ATR target that makes it too expensive in risk | Needs a per-name model, not a constant k×ATR | diff --git a/docs/research/fip-breadth-ic.md b/docs/research/fip-breadth-ic.md index cef816d..9dfc1f1 100644 --- a/docs/research/fip-breadth-ic.md +++ b/docs/research/fip-breadth-ic.md @@ -1,46 +1,151 @@ # Broad-universe fip_id IC research (Phase B) -Generated: 2026-07-18T21:14:40.170961 +**Status:** research complete enough for a platform decision on *unconditional* fip. +**Production impact:** none. Display card remains context-only. + +Generated: 2026-07-18 (breadth run + diagnostics same day). ## Scope - **Research only** — production universe, gate, scanner, schedule unchanged. - Price-only signal harness; no sentiment/fundamentals on the broad tier. -- Point-in-time liquidity mask: top **1500** by 63d median $vol, price ≥ **$5.0** at as-of. +- Snapshot: `research.sqlite` — ~4,650 tickers (prod + nasdaq_all extend). +- IC mask: top **1,500** by point-in-time 63d median $vol, price ≥ **$5**, per week. ## Caveats -- **Survivorship bias**: today's constituents backfilled historically (worse in small caps). -- **IEX volume undercount**: relative $vol rank only, not absolute floors. -- **Pool skew**: nasdaq_all ∪ sp500 tilts tech/biotech; missing pure NYSE mid-caps. +- **Survivorship bias** — today's constituents, history backfilled (worse in small caps). +- **IEX volume undercount** — relative $vol rank only, not absolute floors. +- **Pool skew** — nasdaq_all ∪ partial SPX seed tilts tech/biotech; missing pure NYSE mid-caps. +- **Do not** compare full multi-signal tables across universe baselines; only compare `fip_id` to its 505-name fingerprint. + +--- ## Fingerprint (505-name prod snapshot) -- Expected: IC ≈ -0.045, t ≈ -2.9 -- Observed: IC = -0.045, t = -2.91, weeks = 35, reliable = True -- Pass: **True** +| | Expected | Observed | +|---|---:|---:| +| mean IC | −0.045 | **−0.045** | +| t-stat | −2.9 | **−2.91** | +| weeks | ≥12 | 35 | +| avg N | ~500 | 497.7 | +| reliable | true | **true** | -## Liquid-breadth signal_eval (fip_id) +**Pass.** Pipeline and formula are trustworthy. + +Artifacts: `reports/fip-breadth-20260718-211440-fingerprint.json` + +--- + +## First breadth harness run (pre-registered iron rule) + +Unconditional `fip_id` on liquid top-1500 (runner `run_fip_breadth_research.py`): | metric | value | -|---|---| -| mean_ic | 0.0575 | -| ic_t_stat | 5.12 | -| ic_positive_pct | 88.6 | +|---|---:| +| mean_ic | **+0.0575** | +| ic_t_stat | **+5.12** | +| ic_positive_pct | 88.6% | | weeks | 35 | | avg_cross_section | 1471.2 | -| reliable | True | -| mean_quintile_spread | 0.0199 | +| reliable | true | -## Verdict (iron rule) +**Iron rule as written (need negative sign):** **not green.** +Honest call: no production change from that screen alone. -- **Green: False** -- iron rule not met on liquid-breadth cross-section -- Checks: `{"mean_ic": 0.0575, "abs_mean_ic_ge_0_03": true, "sign_negative": false, "ic_t_stat": 5.12, "reliable": true, "weeks": 35, "avg_cross_section": 1471.2}` +Artifact: `reports/fip-breadth-20260718-211440-breadth.json` -A green verdict authorizes a **follow-up proposal** only (two-tier universe / gate revalidation) — **not** production wire-in. +--- -## Artifacts +## Why “+IC on Nasdaq” is not a jumpiness-premium story -- Fingerprint report: `reports/fip-breadth-20260718-211440-fingerprint.json` -- Breadth report: `reports/fip-breadth-20260718-211440-breadth.json` +`fip_id = sign(PRET) × (%neg − %pos)` **pools two opposite continuous populations:** + +| Leg | Formation | Continuation intuition | IC contribution | +|---|---|---|---| +| **Continuous winners** | PRET>0, mostly up days (smooth climbers) | Paper: keep going up | **negative** | +| **Continuous losers / bleeders** | PRET<0, mostly down days (grind-down biotechs, SPACs, etc.) | Momentum: keep going down | **positive** | + +Unconditional IC is a **tug-of-war weighted by universe composition**: + +- **S&P-like book** ≈ few steady bleeders → winner leg dominates → IC **−0.045**. +- **Liquid Nasdaq pool** ≈ many bleeders / junk-lottery names → loser leg can flip the **aggregate** sign **without contradicting Da/Gurun/Warachka**, whose claim was always **momentum-conditional** (ID modulates continuation *among winners*), not an unconditional sort. + +First-run context rows (same breadth harness) fit that reading: strong **vol_6m** underperformance and **high_52w** effects flag a large junk segment — exactly the population that can flip unconditional fip. + +**Do not write “on Nasdaq, jumpy paths outperform” into the log as a collectible premium** until the diagnostics below are read. + +--- + +## Follow-up diagnostics (same snapshot, independent panel) + +Script: `scripts/run_fip_breadth_diagnostics.py` +Artifact: `reports/fip-breadth-diagnostics-20260718-213908.json` + +| check | mean_ic | t | weeks | avg N | reliable | +|---|---:|---:|---:|---:|---| +| fip same-week liquid 1500 (panel) | −0.017 | −1.85 | 35 | 1471 | true | +| fip **lagged membership** (prior-week $vol) | −0.010 | −0.93 | 35 | 1471 | true | +| fip **tier 1–800** (senior liquid) | **−0.035** | **−2.99** | 35 | 791 | true | +| fip **tier 801–1500** (junior liquid) | **+0.014** | +1.25 | 35 | 700 | true | +| fip **prod-universe subset** inside liquid | **−0.044** | **−2.88** | 35 | 498 | true | +| fip **mom-conditional** (top 20% mom_12_1) | **−0.088** | **−4.58** | 35 | 294 | true | +| vol_6m liquid 1500 (panel) | −0.047 | −1.3 | 35 | 1471 | true | +| mom_12_1 liquid 1500 | +0.046 | +1.91 | 35 | 1471 | true | +| mom_12_1_resid liquid 1500 | +0.029 | +1.33 | 35 | 1471 | true | + +### What the checks settle + +1. **Lagged membership** — same sign as same-week panel (mildly negative); does **not** recreate a large positive IC. Not a clean “liquidity explosion leak manufactures +0.06” story for the panel path. (The first harness run’s **+0.0575** still does not match the independent panel’s −0.017 — treat the **+0.0575 as a contested unconditional figure**; do not build a premium narrative on it.) +2. **Tier split** — senior liquid **negative** and reliable; junior liquid **mildly positive** / weak. Bias and bleeder weight are stronger in the junior tier. +3. **Prod-universe subset** — IC **−0.044 / t −2.88**, ~498 names/week — matches the fingerprint. **Sign flip is compositional**, not “the whole market regime flipped.” +4. **Momentum-conditional fip (the platform test)** — IC **−0.088 / t −4.58**, reliable, ~294 winners/week. **Negative sign, |IC| ≳ 0.03.** This is the paper’s claim and the only version a gate could consume. + +### Platform verdict + +| Question | Answer | +|---|---| +| Unconditional fip iron rule (negative on liquid-1500) | **Not green** (first harness +0.06 fails sign; panel mild neg fails magnitude) | +| Production change now? | **No** | +| Is fip “dead forever”? | **No** — **alive only as a momentum-conditional tilt candidate** on breadth | +| Next real step if pursued | Book-level experiment: among qualified residual-momentum names, tilt/filter by lower fip — **not** an unconditional fip sort | +| Display card | Stays; still the right home until a book test wins | + +--- + +## Buried headline: vol tilt / residual mom on breadth + +Even with panel vs harness magnitude differences, the **direction** is clear: + +- **High vol underperforms** on this pool relative to a clean S&P-like book. +- Production rank tilts **20% toward high volatility**, validated on S&P-like names where high-vol ≈ high-beta in a bull tape. On broad Nasdaq liquid, high-vol often means **lottery junk**. +- **If the universe ever broadens in production, re-validate the 80/20 high-vol tilt first** — it can flip from mildly helpful to actively harmful. +- **Raw momentum > residual** on breadth (panel and first harness both show this pattern) — SPY residualization is a noisier fit for small caps; a breadth book may want a different benchmark or raw mom. + +--- + +## How to re-run (research branch only) + +```powershell +# Windows +.\.venv\Scripts\python.exe scripts\run_fip_breadth_diagnostics.py ` + --research-snapshot backtest_snapshots\research.sqlite ` + --prod-snapshot backtest_snapshots\prod.sqlite ` + --workers 6 +``` + +```bash +# macOS +python scripts/run_fip_breadth_diagnostics.py \ + --research-snapshot backtest_snapshots/research.sqlite \ + --prod-snapshot backtest_snapshots/prod.sqlite \ + --workers 6 +``` + +--- + +## Bottom line + +- Formal first screen: **not green**, no production change, fingerprint **pass**. +- Deeper reading: unconditional sign is a **compositional tug-of-war**, not a new jumpiness premium. +- **The test that matters for this platform already ran:** momentum-conditional fip is **negative, large, and reliable** on liquid breadth → fip remains a **conditional** research lead, not a closed door — and **not** a ship-ready gate input without a book experiment. diff --git a/reports/fip-breadth-diagnostics-20260718-213705.json b/reports/fip-breadth-diagnostics-20260718-213705.json new file mode 100644 index 0000000..83bc087 --- /dev/null +++ b/reports/fip-breadth-diagnostics-20260718-213705.json @@ -0,0 +1,100 @@ +{ + "generated_at": "2026-07-18T21:37:04.615484", + "research_snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\research.sqlite", + "prod_subset_n": 506, + "panel_tickers": 4403, + "top_n": 1500, + "min_price": 5.0, + "checks": { + "fip_same_week_liquid_1500": { + "note": "Replication of main breadth run (same-week $vol mask)", + "mean_ic": -0.0168, + "ic_t_stat": -1.85, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 40.0, + "reliable": true + }, + "fip_lagged_membership_1w": { + "note": "Liquid top-N ranked on *prior* week's median $vol \u2014 excludes same-week liquidity explosion leak", + "mean_ic": -0.0102, + "ic_t_stat": -0.93, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 40.0, + "reliable": true + }, + "fip_tier_1_800": { + "note": "Same-week liquid ranks 1\u2013800 (senior liquid tier)", + "mean_ic": -0.035, + "ic_t_stat": -2.99, + "weeks": 35, + "avg_cross_section": 791.2, + "ic_positive_pct": 25.7, + "reliable": true + }, + "fip_tier_801_1500": { + "note": "Same-week liquid ranks 801\u20131500 (junior liquid tier)", + "mean_ic": 0.0141, + "ic_t_stat": 1.25, + "weeks": 35, + "avg_cross_section": 700.0, + "ic_positive_pct": 60.0, + "reliable": true + }, + "fip_prod_universe_subset": { + "note": "Symbols in prod.sqlite (~S&P-like large-cap book) inside same-week liquid top-N \u2014 compositional control", + "mean_ic": -0.0444, + "ic_t_stat": -2.88, + "weeks": 35, + "avg_cross_section": 497.5, + "ic_positive_pct": 25.7, + "reliable": true + }, + "fip_momentum_conditional_top20pct": { + "note": "Among liquid top-N, keep mom_12_1 percentile \u2265 80.0 (paper: ID modulates continuation among winners; gate-relevant)", + "mean_ic": -0.0879, + "ic_t_stat": -4.58, + "weeks": 35, + "avg_cross_section": 294.3, + "ic_positive_pct": 22.9, + "reliable": true + }, + "vol_6m_liquid_1500": { + "note": "Context: low-vol anomaly strength on this pool", + "mean_ic": -0.0465, + "ic_t_stat": -1.3, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 37.1, + "reliable": true + }, + "mom_12_1_liquid_1500": { + "note": "Context: raw momentum on liquid breadth", + "mean_ic": 0.0462, + "ic_t_stat": 1.91, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 65.7, + "reliable": true + }, + "mom_12_1_resid_liquid_1500": { + "note": "Context: residual momentum on liquid breadth", + "mean_ic": 0.0289, + "ic_t_stat": 1.33, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 60.0, + "reliable": true + } + }, + "interpretation": { + "leak_ruled_out": false, + "junior_tier_drives_positive": true, + "prod_subset_still_negative": true, + "mom_conditional_negative_and_reliable": true, + "compositional_flip_story": "If prod subset IC is negative while full liquid-1500 is positive, the sign flip is compositional (bleeders / Nasdaq junk), not a temporal regime change. Unconditional fip pools continuous winners (want neg IC) against continuous losers/bleeders (want pos IC).", + "vol_tilt_warning": "vol_6m large negative IC on breadth: high-vol lottery names underperform. Production 80/20 high-vol tilt was validated on S&P-like names; must re-validate before any universe broaden." + }, + "platform_verdict": "ALIVE as breadth-book tilt candidate among momentum winners only \u2014 still needs a book-level experiment; not a production wire-in." +} \ No newline at end of file diff --git a/reports/fip-breadth-diagnostics-20260718-213908.json b/reports/fip-breadth-diagnostics-20260718-213908.json new file mode 100644 index 0000000..c51ad81 --- /dev/null +++ b/reports/fip-breadth-diagnostics-20260718-213908.json @@ -0,0 +1,100 @@ +{ + "generated_at": "2026-07-18T21:39:07.916038", + "research_snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\research.sqlite", + "prod_subset_n": 506, + "panel_tickers": 4403, + "top_n": 1500, + "min_price": 5.0, + "checks": { + "fip_same_week_liquid_1500": { + "note": "Replication of main breadth run (same-week $vol mask)", + "mean_ic": -0.0168, + "ic_t_stat": -1.85, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 40.0, + "reliable": true + }, + "fip_lagged_membership_1w": { + "note": "Liquid top-N ranked on *prior* week's median $vol \u2014 excludes same-week liquidity explosion leak", + "mean_ic": -0.0102, + "ic_t_stat": -0.93, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 40.0, + "reliable": true + }, + "fip_tier_1_800": { + "note": "Same-week liquid ranks 1\u2013800 (senior liquid tier)", + "mean_ic": -0.035, + "ic_t_stat": -2.99, + "weeks": 35, + "avg_cross_section": 791.2, + "ic_positive_pct": 25.7, + "reliable": true + }, + "fip_tier_801_1500": { + "note": "Same-week liquid ranks 801\u20131500 (junior liquid tier)", + "mean_ic": 0.0141, + "ic_t_stat": 1.25, + "weeks": 35, + "avg_cross_section": 700.0, + "ic_positive_pct": 60.0, + "reliable": true + }, + "fip_prod_universe_subset": { + "note": "Symbols in prod.sqlite (~S&P-like large-cap book) inside same-week liquid top-N \u2014 compositional control", + "mean_ic": -0.0444, + "ic_t_stat": -2.88, + "weeks": 35, + "avg_cross_section": 497.5, + "ic_positive_pct": 25.7, + "reliable": true + }, + "fip_momentum_conditional_top20pct": { + "note": "Among liquid top-N, keep mom_12_1 percentile \u2265 80.0 (paper: ID modulates continuation among winners; gate-relevant)", + "mean_ic": -0.0879, + "ic_t_stat": -4.58, + "weeks": 35, + "avg_cross_section": 294.3, + "ic_positive_pct": 22.9, + "reliable": true + }, + "vol_6m_liquid_1500": { + "note": "Context: low-vol anomaly strength on this pool", + "mean_ic": -0.0465, + "ic_t_stat": -1.3, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 37.1, + "reliable": true + }, + "mom_12_1_liquid_1500": { + "note": "Context: raw momentum on liquid breadth", + "mean_ic": 0.0462, + "ic_t_stat": 1.91, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 65.7, + "reliable": true + }, + "mom_12_1_resid_liquid_1500": { + "note": "Context: residual momentum on liquid breadth", + "mean_ic": 0.0289, + "ic_t_stat": 1.33, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 60.0, + "reliable": true + } + }, + "interpretation": { + "leak_ruled_out": false, + "junior_tier_drives_positive": true, + "prod_subset_still_negative": true, + "mom_conditional_negative_and_reliable": true, + "compositional_flip_story": "If prod subset IC is negative while full liquid-1500 is positive, the sign flip is compositional (bleeders / Nasdaq junk), not a temporal regime change. Unconditional fip pools continuous winners (want neg IC) against continuous losers/bleeders (want pos IC).", + "vol_tilt_warning": "vol_6m large negative IC on breadth: high-vol lottery names underperform. Production 80/20 high-vol tilt was validated on S&P-like names; must re-validate before any universe broaden." + }, + "platform_verdict": "ALIVE as breadth-book tilt candidate among momentum winners only \u2014 still needs a book-level experiment; not a production wire-in." +} \ No newline at end of file diff --git a/scripts/run_fip_breadth_diagnostics.py b/scripts/run_fip_breadth_diagnostics.py new file mode 100644 index 0000000..1e47ca5 --- /dev/null +++ b/scripts/run_fip_breadth_diagnostics.py @@ -0,0 +1,665 @@ +"""Post-breadth diagnostics for fip_id (research branch only). + +Same research.sqlite as the liquid-breadth IC run. No production changes. + +Checks (pre-registered interpretation follow-ups) +------------------------------------------------ +1. **Lagged membership** — liquid top-N ranked on *prior* week's $vol (extra lag) + so same-week liquidity explosion cannot pull a name into history. +2. **Liquidity tiers** — fip IC on ranks 1–800 vs 801–1500 (same-week mask). +3. **Prod-universe subset** — symbols present in prod.sqlite (~S&P-like large-cap + book) inside the same breadth weeks — compositional vs temporal flip. +4. **Momentum-conditional fip** — among weekly top 20% by mom_12_1 (or resid when + available) within the liquid top-N — the paper's actual claim and the only + version a gate could consume. + +Also reports vol_6m / mom raw vs residual on the same panels for the log. + +Example (Windows) +----------------- + .\\.venv\\Scripts\\python.exe scripts\\run_fip_breadth_diagnostics.py ^ + --research-snapshot backtest_snapshots\\research.sqlite ^ + --prod-snapshot backtest_snapshots\\prod.sqlite ^ + --workers 6 +""" + +from __future__ import annotations + +import argparse +import json +import math +import multiprocessing as mp +import sys +from collections import defaultdict +from concurrent.futures import ProcessPoolExecutor, as_completed +from datetime import date, datetime +from pathlib import Path +from typing import Any + +from sqlalchemy import create_engine, text + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +HORIZON = 30 +MIN_CROSS = 20 +MIN_RELIABLE = 12 +LIQUID_TOP = 1500 +MIN_PRICE = 5.0 +MOM_WINNER_PCT = 80.0 # top 20% within liquid cross-section + + +def _parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument( + "--research-snapshot", + default="backtest_snapshots/research.sqlite", + ) + p.add_argument( + "--prod-snapshot", + default="backtest_snapshots/prod.sqlite", + help="Symbols here define the large-cap / prod-like subset.", + ) + p.add_argument("--top-n", type=int, default=LIQUID_TOP) + p.add_argument("--min-price", type=float, default=MIN_PRICE) + p.add_argument("--workers", type=int, default=max(1, (mp.cpu_count() or 4) - 1)) + p.add_argument("--out", default=None) + p.add_argument("--quiet", action="store_true") + return p.parse_args() + + +def _week_key(d: date) -> tuple[int, int]: + iso = d.isocalendar() + return (int(iso[0]), int(iso[1])) + + +def _week_ord(wk: tuple[int, int]) -> int: + return wk[0] * 53 + wk[1] + + +def _nonoverlap(weeks: list[tuple[int, int]], stride: int) -> list[tuple[int, int]]: + kept: list[tuple[int, int]] = [] + last: int | None = None + for wk in sorted(weeks, key=_week_ord): + o = _week_ord(wk) + if last is None or o - last >= stride: + kept.append(wk) + last = o + return kept + + +def _rank(xs: list[float]) -> list[float]: + order = sorted(range(len(xs)), key=lambda k: xs[k]) + ranks = [0.0] * len(xs) + i = 0 + while i < len(xs): + j = i + while j + 1 < len(xs) and xs[order[j + 1]] == xs[order[i]]: + j += 1 + avg = (i + j) / 2.0 + 1.0 + for k in range(i, j + 1): + ranks[order[k]] = avg + i = j + 1 + return ranks + + +def _pearson(a: list[float], b: list[float]) -> float | None: + n = len(a) + if n < 3: + return None + ma, mb = sum(a) / n, sum(b) / n + va = sum((x - ma) ** 2 for x in a) + vb = sum((y - mb) ** 2 for y in b) + if va <= 0 or vb <= 0: + return None + cov = sum((a[k] - ma) * (b[k] - mb) for k in range(n)) + return cov / math.sqrt(va * vb) + + +def _spearman(xs: list[float], ys: list[float]) -> float | None: + if len(xs) < 3: + return None + return _pearson(_rank(xs), _rank(ys)) + + +def _ic_row(pairs: list[tuple[float, float]], *, label: str) -> dict[str, Any]: + """pairs = (signal, fwd) over non-overlapping weeks aggregated… actually + we pass per-week then aggregate outside. This helper is for multi-week IC.""" + raise NotImplementedError + + +def _ic_from_weekly( + week_pairs: dict[tuple[int, int], list[tuple[float, float]]], +) -> dict[str, Any]: + stride = max(1, round(HORIZON / 5)) + usable = [wk for wk, ps in week_pairs.items() if len(ps) >= MIN_CROSS] + kept = _nonoverlap(usable, stride) + ics: list[float] = [] + sizes: list[int] = [] + for wk in kept: + ps = week_pairs[wk] + if len(ps) < MIN_CROSS: + continue + ic = _spearman([p[0] for p in ps], [p[1] for p in ps]) + if ic is not None: + ics.append(ic) + sizes.append(len(ps)) + if not ics: + return { + "mean_ic": None, + "ic_t_stat": None, + "weeks": 0, + "avg_cross_section": None, + "ic_positive_pct": None, + "reliable": False, + } + mean_ic = sum(ics) / len(ics) + if len(ics) > 1: + var = sum((x - mean_ic) ** 2 for x in ics) / (len(ics) - 1) + std = math.sqrt(var) if var > 0 else 0.0 + t_stat = mean_ic / std * math.sqrt(len(ics)) if std > 0 else None + else: + t_stat = None + return { + "mean_ic": round(mean_ic, 4), + "ic_t_stat": round(t_stat, 2) if t_stat is not None else None, + "weeks": len(ics), + "avg_cross_section": round(sum(sizes) / len(sizes), 1), + "ic_positive_pct": round(sum(1 for x in ics if x > 0) / len(ics) * 100, 1), + "reliable": len(ics) >= MIN_RELIABLE, + } + + +def _panel_worker(payload: tuple) -> list[dict]: + """Build weekly observations for one ticker (picklable top-level).""" + symbol, date_ords, opens, highs, lows, closes, volumes, spy = payload + from types import SimpleNamespace + from app.services.backtest_service import ( + HORIZON as H, + _median_dollar_vol_63, + _signal_values, + _weekly_asof_indices, + ) + + dates = [date.fromordinal(int(o)) for o in date_ords] + opens_f = [float(x) for x in opens] + highs_f = [float(x) for x in highs] + lows_f = [float(x) for x in lows] + closes_f = [float(x) for x in closes] + vols_f = [float(x) for x in volumes] + n = len(closes_f) + if n < H + 21: + return [] + + # Match backtest_service bar objects exactly (weekly as-of + signal_values). + bar_records = [ + SimpleNamespace( + date=dates[i], + open=opens_f[i], + high=highs_f[i], + low=lows_f[i], + close=closes_f[i], + volume=vols_f[i], + ) + for i in range(n) + ] + out: list[dict] = [] + for i in _weekly_asof_indices(bar_records): + j = i + H + if j >= n or closes_f[i] <= 0: + continue + sigs = _signal_values(dates, closes_f, highs_f, i, spy) + fip = sigs.get("fip_id") + mom = sigs.get("mom_12_1") + mom_r = sigs.get("mom_12_1_resid") + vol = sigs.get("vol_6m") + if fip is None and mom is None: + continue + dvol = _median_dollar_vol_63(closes_f, vols_f, i) + wk = _week_key(dates[i]) + out.append({ + "symbol": symbol, + "week": wk, + "fwd": closes_f[j] / closes_f[i] - 1.0, + "close": closes_f[i], + "dvol": dvol, + "fip_id": fip, + "mom_12_1": mom, + "mom_12_1_resid": mom_r, + "vol_6m": vol, + }) + return out + + +def _load_spy(conn) -> dict[date, float]: + rows = conn.execute( + text("SELECT date, close FROM benchmark_prices WHERE symbol = 'SPY' ORDER BY date") + ).fetchall() + out: dict[date, float] = {} + for d, c in rows: + if isinstance(d, str): + d = date.fromisoformat(d[:10]) + out[d] = float(c) + return out + + +def _load_symbols(conn) -> list[str]: + return [ + str(r[0]) + for r in conn.execute(text("SELECT symbol FROM tickers ORDER BY symbol")).fetchall() + ] + + +def _load_columns(conn, symbol: str) -> tuple | None: + tid = conn.execute( + text("SELECT id FROM tickers WHERE symbol = :s"), {"s": symbol} + ).scalar() + if tid is None: + return None + rows = conn.execute( + text( + "SELECT date, open, high, low, close, volume FROM ohlcv_records " + "WHERE ticker_id = :t ORDER BY date" + ), + {"t": tid}, + ).fetchall() + if len(rows) < HORIZON + 60: + return None + ords: list[int] = [] + opens: list[float] = [] + highs: list[float] = [] + lows: list[float] = [] + closes: list[float] = [] + vols: list[float] = [] + for d, o, h, l, c, v in rows: + if isinstance(d, str): + d = date.fromisoformat(d[:10]) + ords.append(d.toordinal()) + opens.append(float(o)) + highs.append(float(h)) + lows.append(float(l)) + closes.append(float(c)) + vols.append(float(v or 0)) + return (symbol, ords, opens, highs, lows, closes, vols) + + +def _liquid_members( + obs: list[dict], + *, + top_n: int, + min_price: float, + dvol_key: str = "dvol", +) -> list[dict]: + eligible = [ + o + for o in obs + if o.get("close") is not None + and float(o["close"]) >= min_price + and o.get(dvol_key) is not None + and float(o[dvol_key]) > 0 + ] + eligible.sort(key=lambda o: float(o[dvol_key]), reverse=True) + return eligible[:top_n] + + +def _pairs(obs: list[dict], signal: str) -> list[tuple[float, float]]: + out: list[tuple[float, float]] = [] + for o in obs: + v = o.get(signal) + if v is None: + continue + out.append((float(v), float(o["fwd"]))) + return out + + +def main() -> None: + args = _parse_args() + research = Path(args.research_snapshot) + prod = Path(args.prod_snapshot) + if not research.exists(): + raise SystemExit(f"Missing research snapshot: {research}") + + research_eng = create_engine(f"sqlite:///{research.resolve().as_posix()}") + prod_symbols: set[str] = set() + if prod.exists(): + prod_eng = create_engine(f"sqlite:///{prod.resolve().as_posix()}") + with prod_eng.connect() as c: + prod_symbols = { + str(r[0]) + for r in c.execute(text("SELECT symbol FROM tickers")).fetchall() + } + prod_eng.dispose() + + with research_eng.connect() as conn: + spy = _load_spy(conn) + symbols = _load_symbols(conn) + jobs: list[tuple] = [] + for i, sym in enumerate(symbols, 1): + cols = _load_columns(conn, sym) + if cols is None: + continue + jobs.append((*cols, spy)) + if not args.quiet and i % 500 == 0: + print(f" queued {i}/{len(symbols)}", flush=True) + + if not args.quiet: + print(f"Building weekly panel for {len(jobs)} tickers…", flush=True) + + # Panel: week -> list of obs + by_week: dict[tuple[int, int], list[dict]] = defaultdict(list) + workers = max(1, int(args.workers)) + if workers == 1: + for j, job in enumerate(jobs, 1): + for row in _panel_worker(job): + by_week[tuple(row["week"])].append(row) + if not args.quiet and j % 200 == 0: + print(f" panel {j}/{len(jobs)}", flush=True) + else: + with ProcessPoolExecutor(max_workers=workers) as pool: + futs = {pool.submit(_panel_worker, job): job[0] for job in jobs} + done = 0 + for fut in as_completed(futs): + done += 1 + try: + rows = fut.result() + except Exception as exc: + if not args.quiet: + print(f" worker error {futs[fut]}: {exc}", flush=True) + continue + for row in rows: + by_week[tuple(row["week"])].append(row) + if not args.quiet and done % 200 == 0: + print(f" panel {done}/{len(jobs)}", flush=True) + + if not args.quiet: + print(f"Weeks with data: {len(by_week)}", flush=True) + + # Prior-week dvol map for lagged membership: (symbol, week) -> dvol + dvol_by_sym_week: dict[tuple[str, tuple[int, int]], float] = {} + for wk, obs in by_week.items(): + for o in obs: + if o.get("dvol") is not None: + dvol_by_sym_week[(o["symbol"], wk)] = float(o["dvol"]) + + ordered_weeks = sorted(by_week.keys(), key=_week_ord) + prev_week: dict[tuple[int, int], tuple[int, int]] = {} + for i, wk in enumerate(ordered_weeks): + if i > 0: + prev_week[wk] = ordered_weeks[i - 1] + + top_n = int(args.top_n) + min_price = float(args.min_price) + + # --- Panels for each check --- + same_week_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + lag_week_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + tier_hi_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + tier_lo_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + prod_subset_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + mom_cond_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + liquid_vol: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + liquid_mom: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + liquid_mom_r: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + + for wk, obs in by_week.items(): + # Same-week liquid top-N among names that have fip (matches signal_eval mask: + # membership is ranked within each signal's observation set). + with_fip = [o for o in obs if o.get("fip_id") is not None] + liq_fip = _liquid_members(with_fip, top_n=top_n, min_price=min_price) + for rank, o in enumerate(liq_fip, 1): + same_week_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + if rank <= 800: + tier_hi_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + elif rank <= top_n: + tier_lo_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + if o["symbol"] in prod_symbols: + prod_subset_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + + # Context signals: liquid among names that carry that signal + with_vol = [o for o in obs if o.get("vol_6m") is not None] + for o in _liquid_members(with_vol, top_n=top_n, min_price=min_price): + liquid_vol[wk].append((float(o["vol_6m"]), float(o["fwd"]))) + with_mom_all = [o for o in obs if o.get("mom_12_1") is not None] + liq_mom = _liquid_members(with_mom_all, top_n=top_n, min_price=min_price) + for o in liq_mom: + liquid_mom[wk].append((float(o["mom_12_1"]), float(o["fwd"]))) + with_mom_r = [o for o in obs if o.get("mom_12_1_resid") is not None] + for o in _liquid_members(with_mom_r, top_n=top_n, min_price=min_price): + liquid_mom_r[wk].append((float(o["mom_12_1_resid"]), float(o["fwd"]))) + + # Momentum-conditional: within liquid fip set, keep mom_12_1 ≥ P80 + mom_key = "mom_12_1" + with_mom = [ + o for o in liq_fip + if o.get(mom_key) is not None and o.get("fip_id") is not None + ] + if len(with_mom) >= MIN_CROSS: + with_mom.sort(key=lambda o: float(o[mom_key])) + n = len(with_mom) + cut = int(math.floor(n * (MOM_WINNER_PCT / 100.0))) + winners = with_mom[cut:] # upper tail + for o in winners: + mom_cond_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + + # Lagged membership: rank by *previous* week's dvol among fip names + pw = prev_week.get(wk) + if pw is not None: + lagged: list[dict] = [] + for o in with_fip: + if o.get("close") is None or float(o["close"]) < min_price: + continue + prev_dvol = dvol_by_sym_week.get((o["symbol"], pw)) + if prev_dvol is None or prev_dvol <= 0: + continue + lagged.append({**o, "lag_dvol": prev_dvol}) + lagged.sort(key=lambda o: float(o["lag_dvol"]), reverse=True) + for o in lagged[:top_n]: + lag_week_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + + results = { + "generated_at": datetime.now().isoformat(), + "research_snapshot": str(research.resolve()), + "prod_subset_n": len(prod_symbols), + "panel_tickers": len(jobs), + "top_n": top_n, + "min_price": min_price, + "checks": { + "fip_same_week_liquid_1500": { + "note": "Replication of main breadth run (same-week $vol mask)", + **_ic_from_weekly(same_week_fip), + }, + "fip_lagged_membership_1w": { + "note": ( + "Liquid top-N ranked on *prior* week's median $vol — " + "excludes same-week liquidity explosion leak" + ), + **_ic_from_weekly(lag_week_fip), + }, + "fip_tier_1_800": { + "note": "Same-week liquid ranks 1–800 (senior liquid tier)", + **_ic_from_weekly(tier_hi_fip), + }, + "fip_tier_801_1500": { + "note": "Same-week liquid ranks 801–1500 (junior liquid tier)", + **_ic_from_weekly(tier_lo_fip), + }, + "fip_prod_universe_subset": { + "note": ( + "Symbols in prod.sqlite (~S&P-like large-cap book) inside " + "same-week liquid top-N — compositional control" + ), + **_ic_from_weekly(prod_subset_fip), + }, + "fip_momentum_conditional_top20pct": { + "note": ( + f"Among liquid top-N, keep mom_12_1 percentile ≥ {MOM_WINNER_PCT} " + "(paper: ID modulates continuation among winners; gate-relevant)" + ), + **_ic_from_weekly(mom_cond_fip), + }, + "vol_6m_liquid_1500": { + "note": "Context: low-vol anomaly strength on this pool", + **_ic_from_weekly(liquid_vol), + }, + "mom_12_1_liquid_1500": { + "note": "Context: raw momentum on liquid breadth", + **_ic_from_weekly(liquid_mom), + }, + "mom_12_1_resid_liquid_1500": { + "note": "Context: residual momentum on liquid breadth", + **_ic_from_weekly(liquid_mom_r), + }, + }, + } + + # Interpretations + checks = results["checks"] + lag = checks["fip_lagged_membership_1w"] + same = checks["fip_same_week_liquid_1500"] + hi = checks["fip_tier_1_800"] + lo = checks["fip_tier_801_1500"] + prod = checks["fip_prod_universe_subset"] + cond = checks["fip_momentum_conditional_top20pct"] + + def _sign(x: float | None) -> str: + if x is None: + return "na" + return "neg" if x < 0 else "pos" + + results["interpretation"] = { + "leak_ruled_out": ( + lag.get("mean_ic") is not None + and same.get("mean_ic") is not None + and _sign(lag["mean_ic"]) == _sign(same["mean_ic"]) + and abs(float(lag["mean_ic"])) >= 0.02 + ), + "junior_tier_drives_positive": ( + lo.get("mean_ic") is not None + and float(lo["mean_ic"]) > 0 + and (hi.get("mean_ic") is None or float(hi["mean_ic"]) < float(lo["mean_ic"])) + ), + "prod_subset_still_negative": ( + prod.get("mean_ic") is not None and float(prod["mean_ic"]) < 0 + ), + "mom_conditional_negative_and_reliable": ( + cond.get("mean_ic") is not None + and float(cond["mean_ic"]) < 0 + and abs(float(cond["mean_ic"])) >= 0.03 + and bool(cond.get("reliable")) + ), + "compositional_flip_story": ( + "If prod subset IC is negative while full liquid-1500 is positive, " + "the sign flip is compositional (bleeders / Nasdaq junk), not a " + "temporal regime change. Unconditional fip pools continuous winners " + "(want neg IC) against continuous losers/bleeders (want pos IC)." + ), + "vol_tilt_warning": ( + "vol_6m large negative IC on breadth: high-vol lottery names " + "underperform. Production 80/20 high-vol tilt was validated on " + "S&P-like names; must re-validate before any universe broaden." + ), + } + + # Gate-relevant summary line + if results["interpretation"]["mom_conditional_negative_and_reliable"]: + results["platform_verdict"] = ( + "ALIVE as breadth-book tilt candidate among momentum winners only — " + "still needs a book-level experiment; not a production wire-in." + ) + else: + results["platform_verdict"] = ( + "CLOSED for production use: momentum-conditional fip does not clear " + "iron rule on this liquid-Nasdaq pool. Display card remains final resting place." + ) + + stamp = datetime.now().strftime("%Y%m%d-%H%M%S") + out = Path(args.out) if args.out else Path("reports") / f"fip-breadth-diagnostics-{stamp}.json" + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(results, indent=2, default=str), encoding="utf-8") + + # Append to research log + md_path = Path("docs/research/fip-breadth-ic.md") + _append_diagnostics_md(md_path, results, out) + + if not args.quiet: + print(json.dumps(results["checks"], indent=2, default=str)) + print() + print("interpretation:", json.dumps(results["interpretation"], indent=2)) + print("platform_verdict:", results["platform_verdict"]) + print(f"Wrote {out}") + print(f"Updated {md_path}") + + +def _append_diagnostics_md(path: Path, results: dict, artifact: Path) -> None: + checks = results["checks"] + interp = results["interpretation"] + lines = [ + "", + "---", + "", + f"## Follow-up diagnostics ({results['generated_at'][:10]})", + "", + "Compositional reading of the sign flip (before any 'jumpiness premium' story):", + "", + "`fip_id = sign(PRET) × (%neg − %pos)` pools two opposite continuous populations:", + "", + "- **Continuous winners** (PRET>0, mostly up days) → paper claim → **negative** IC contribution.", + "- **Continuous losers / bleeders** (PRET<0, mostly down days) → momentum continuation down → **positive** IC contribution.", + "", + "Unconditional IC is a tug-of-war weighted by universe composition. S&P-like books " + "have few steady bleeders → negative fip IC. Liquid Nasdaq has many → sign can flip " + "without contradicting Da/Gurun/Warachka (claim was always **momentum-conditional**).", + "", + "### Artifact / composition checks", + "", + "| check | mean_ic | t | weeks | avg N | reliable |", + "|---|---:|---:|---:|---:|---|", + ] + order = [ + "fip_same_week_liquid_1500", + "fip_lagged_membership_1w", + "fip_tier_1_800", + "fip_tier_801_1500", + "fip_prod_universe_subset", + "fip_momentum_conditional_top20pct", + "vol_6m_liquid_1500", + "mom_12_1_liquid_1500", + "mom_12_1_resid_liquid_1500", + ] + for key in order: + row = checks.get(key) or {} + lines.append( + f"| {key} | {row.get('mean_ic')} | {row.get('ic_t_stat')} | " + f"{row.get('weeks')} | {row.get('avg_cross_section')} | {row.get('reliable')} |" + ) + lines.extend([ + "", + "### Flags", + "", + f"- Lagged mask keeps same sign / material |IC|: **{interp.get('leak_ruled_out')}**", + f"- Junior tier (801–1500) drives more positive IC: **{interp.get('junior_tier_drives_positive')}**", + f"- Prod-universe subset still negative: **{interp.get('prod_subset_still_negative')}**", + f"- Mom-conditional (≥P80) negative + reliable: **{interp.get('mom_conditional_negative_and_reliable')}**", + "", + "### Platform verdict", + "", + results.get("platform_verdict", ""), + "", + "### Vol-tilt warning (any future breadth move)", + "", + interp.get("vol_tilt_warning", ""), + "", + f"Artifact: `{artifact.as_posix()}`", + "", + ]) + # Replace previous diagnostics section if re-run, else append + existing = path.read_text(encoding="utf-8") if path.exists() else "" + marker = "## Follow-up diagnostics" + if marker in existing: + existing = existing.split(marker)[0].rstrip() + "\n" + path.write_text(existing + "\n".join(lines), encoding="utf-8") + + +if __name__ == "__main__": + main() From 7d60e54f5a11e7eb8f5b049551bdb997da8c053b Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sun, 19 Jul 2026 00:06:04 +0200 Subject: [PATCH 5/6] research: single-source liquid mask; orphan +0.06 fip IC Harness and diagnostics share _filter_liquid_breadth_week_rich. Recompute shows unconditional liquid fip IC -0.017 (mask binds 97%); mom-conditional -0.088/t-4.58 stands. Document +0.0575 as orphaned. --- app/services/backtest_service.py | 107 +- docs/research/fip-breadth-ic.md | 168 +- reports/fip-reconcile-20260719-000520.json | 6693 ++++++++++++++++++++ scripts/run_fip_breadth_diagnostics.py | 838 ++- 4 files changed, 7280 insertions(+), 526 deletions(-) create mode 100644 reports/fip-reconcile-20260719-000520.json diff --git a/app/services/backtest_service.py b/app/services/backtest_service.py index a9503eb..6b29be4 100644 --- a/app/services/backtest_service.py +++ b/app/services/backtest_service.py @@ -942,6 +942,8 @@ def _accumulate_signal_series( records: list, collected: dict, benchmark_closes: dict[date, float] | None = None, + *, + symbol: str | None = None, ) -> None: """For each weekly as-of bar, emit (signal, forward-return) pairs keyed by ISO week into ``collected[name][week_key]``. Forward return is close-to-close over @@ -974,6 +976,7 @@ def _accumulate_signal_series( "fwd": fwd, "close": closes[i], "median_dvol_63": dvol, + "symbol": symbol, }) else: collected[name][week_key].append((val, fwd)) @@ -1041,12 +1044,28 @@ def _filter_liquid_breadth_week( Ranking is relative (IEX volume undercount is OK for order stats). Membership is recomputed every week from as-of bars — never frozen from today's liquidity. """ - ranked: list[tuple[float, float, float]] = [] # (-dvol, val, fwd) + kept = _filter_liquid_breadth_week_rich( + recs, top_n=top_n, min_price=min_price + ) + return [(float(r["val"]), float(r["fwd"])) for r in kept] + + +def _filter_liquid_breadth_week_rich( + recs: list, + *, + top_n: int, + min_price: float, +) -> list[dict]: + """Same mask as ``_filter_liquid_breadth_week``, returning rich rows. + + Single source for harness IC and research diagnostics. Eligible pool = + dict observations with close ≥ min_price and median_dvol_63 > 0; then + keep top_n by dollar volume (highest first). Non-dict legacy tuples are + not eligible for the liquid mask (they have no dvol). + """ + eligible: list[tuple[float, dict]] = [] # (-dvol, row) for rec in recs: if not isinstance(rec, dict): - pair = _obs_val_fwd(rec) - if pair is not None: - ranked.append((0.0, pair[0], pair[1])) continue close = rec.get("close") dvol = rec.get("median_dvol_63") @@ -1057,10 +1076,50 @@ def _filter_liquid_breadth_week( pair = _obs_val_fwd(rec) if pair is None: continue - ranked.append((-float(dvol), pair[0], pair[1])) - ranked.sort(key=lambda row: row[0]) - kept = ranked[:top_n] - return [(val, fwd) for _, val, fwd in kept] + row = { + "val": pair[0], + "fwd": pair[1], + "close": float(close), + "median_dvol_63": float(dvol), + "symbol": rec.get("symbol"), + } + # Preserve optional research fields for mom-conditional diagnostics. + for key in ("mom_12_1", "mom_12_1_resid", "vol_6m", "fip_id"): + if key in rec and rec[key] is not None: + row[key] = rec[key] + eligible.append((-float(dvol), row)) + eligible.sort(key=lambda item: item[0]) + return [row for _, row in eligible[:top_n]] + + +def _liquid_breadth_week_stats( + recs: list, + *, + top_n: int, + min_price: float, +) -> dict[str, int | bool]: + """Pre/post mask counts for reconciling avg_cross_section semantics.""" + raw = len(recs) + eligible = 0 + for rec in recs: + if not isinstance(rec, dict): + continue + close = rec.get("close") + dvol = rec.get("median_dvol_63") + if close is None or float(close) < min_price: + continue + if dvol is None or float(dvol) <= 0: + continue + if _obs_val_fwd(rec) is None: + continue + eligible += 1 + post = min(eligible, top_n) if top_n > 0 else eligible + return { + "raw_pool": raw, + "eligible_pre_mask": eligible, + "post_mask": post, + "mask_binds": bool(top_n > 0 and eligible > top_n), + } def _quintile_spread(pairs: list[tuple[float, float]]) -> float | None: @@ -1126,9 +1185,18 @@ def _signal_evaluation(collected: dict) -> list[dict]: ics: list[float] = [] spreads: list[float] = [] sizes: list[int] = [] + raw_sizes: list[int] = [] + eligible_sizes: list[int] = [] + bind_flags: list[bool] = [] for wk in kept: recs = weeks_map[wk] if top_n > 0: + stats = _liquid_breadth_week_stats( + recs, top_n=top_n, min_price=min_price + ) + raw_sizes.append(int(stats["raw_pool"])) + eligible_sizes.append(int(stats["eligible_pre_mask"])) + bind_flags.append(bool(stats["mask_binds"])) pairs = _filter_liquid_breadth_week( recs, top_n=top_n, min_price=min_price ) @@ -1146,6 +1214,7 @@ def _signal_evaluation(collected: dict) -> list[dict]: spread = _quintile_spread(pairs) if spread is not None: spreads.append(spread) + # avg_cross_section is ALWAYS post-mask pair count (the IC sample). sizes.append(len(pairs)) if not ics: continue @@ -1168,16 +1237,32 @@ def _signal_evaluation(collected: dict) -> list[dict]: if top_n > 0: row["liquid_breadth_top_n"] = top_n row["liquid_min_price"] = min_price + # Explicit pre/post mask diagnostics (reconcile "did top-N bind?"). + if raw_sizes: + row["avg_raw_pool"] = round(sum(raw_sizes) / len(raw_sizes), 1) + if eligible_sizes: + row["avg_eligible_pre_mask"] = round( + sum(eligible_sizes) / len(eligible_sizes), 1 + ) + if bind_flags: + row["mask_binds_pct"] = round( + sum(1 for b in bind_flags if b) / len(bind_flags) * 100, 1 + ) rows.append(row) rows.sort(key=lambda r: r["mean_ic"], reverse=True) return rows -def _signal_series(records: list, benchmark_closes: dict[date, float] | None = None) -> dict: +def _signal_series( + records: list, + benchmark_closes: dict[date, float] | None = None, + *, + symbol: str | None = None, +) -> dict: """Per-ticker signal/forward-return series as a PLAIN (picklable) nested dict — no defaultdict/lambda — so it can cross a process boundary.""" tmp: dict = defaultdict(lambda: defaultdict(list)) - _accumulate_signal_series(records, tmp, benchmark_closes) + _accumulate_signal_series(records, tmp, benchmark_closes, symbol=symbol) return {name: dict(weeks) for name, weeks in tmp.items()} @@ -1218,7 +1303,7 @@ def _replay_and_signals( ) return ( candidates, - _signal_series(bars, benchmark_closes), + _signal_series(bars, benchmark_closes, symbol=symbol), ) diff --git a/docs/research/fip-breadth-ic.md b/docs/research/fip-breadth-ic.md index 9dfc1f1..ca17663 100644 --- a/docs/research/fip-breadth-ic.md +++ b/docs/research/fip-breadth-ic.md @@ -1,151 +1,145 @@ # Broad-universe fip_id IC research (Phase B) -**Status:** research complete enough for a platform decision on *unconditional* fip. +**Status:** unconditional fip closed; mom-conditional lead confirmed on single-sourced path. **Production impact:** none. Display card remains context-only. -Generated: 2026-07-18 (breadth run + diagnostics same day). - ## Scope -- **Research only** — production universe, gate, scanner, schedule unchanged. -- Price-only signal harness; no sentiment/fundamentals on the broad tier. -- Snapshot: `research.sqlite` — ~4,650 tickers (prod + nasdaq_all extend). -- IC mask: top **1,500** by point-in-time 63d median $vol, price ≥ **$5**, per week. +- Research only — production universe, gate, scanner, schedule unchanged. +- Snapshot: `research.sqlite` (~4,650 tickers = prod + nasdaq_all extend). +- Liquid mask: top **1,500** by point-in-time 63d median $vol, price ≥ **$5**/week. ## Caveats -- **Survivorship bias** — today's constituents, history backfilled (worse in small caps). -- **IEX volume undercount** — relative $vol rank only, not absolute floors. -- **Pool skew** — nasdaq_all ∪ partial SPX seed tilts tech/biotech; missing pure NYSE mid-caps. -- **Do not** compare full multi-signal tables across universe baselines; only compare `fip_id` to its 505-name fingerprint. +- Survivorship bias (today’s constituents, history backfilled). +- IEX volume undercount → relative $vol rank only. +- Pool skew: Nasdaq-heavy; missing pure NYSE mid-caps. +- Do not mix multi-signal tables across universe baselines. --- -## Fingerprint (505-name prod snapshot) +## Fingerprint (505-name prod) | | Expected | Observed | |---|---:|---:| | mean IC | −0.045 | **−0.045** | | t-stat | −2.9 | **−2.91** | -| weeks | ≥12 | 35 | -| avg N | ~500 | 497.7 | -| reliable | true | **true** | +| weeks / N / reliable | ≥12 / ~500 / true | 35 / 497.7 / true | -**Pass.** Pipeline and formula are trustworthy. - -Artifacts: `reports/fip-breadth-20260718-211440-fingerprint.json` +**Pass.** Formula + pipeline trustworthy. --- -## First breadth harness run (pre-registered iron rule) +## Discrepancy (must not be papered over) -Unconditional `fip_id` on liquid top-1500 (runner `run_fip_breadth_research.py`): +| Source | fip IC (liquid ~1500) | t | +|---|---:|---:| +| Report `fip-breadth-20260718-211440-breadth.json` | **+0.0575** | **+5.12** | +| Single-sourced recompute (2026-07-19) | **−0.0168** | **−1.85** | + +That is a **sign disagreement** on the same intended quantity. Method rule: the number you cannot reconcile is the number you cannot use. + +### What we did + +1. **Single-sourced the mask** — diagnostics call harness `_signal_series` + `_filter_liquid_breadth_week_rich` only (no parallel mask). +2. **Documented avg_cross_section semantics** — always **post-mask** IC sample size. +3. **Logged pre-mask stats** so “did top-N bind?” is answerable. + +### Authoritative unconditional liquid fip (post-reconciliation) | metric | value | |---|---:| -| mean_ic | **+0.0575** | -| ic_t_stat | **+5.12** | -| ic_positive_pct | 88.6% | +| mean_ic | **−0.0168** | +| ic_t_stat | **−1.85** | | weeks | 35 | -| avg_cross_section | 1471.2 | +| avg_cross_section (**post-mask**) | 1471.2 | +| avg_raw_pool | 3214.4 | +| avg_eligible_pre_mask | **2338.4** | +| mask_binds_pct | **97.1%** | | reliable | true | -**Iron rule as written (need negative sign):** **not green.** -Honest call: no production change from that screen alone. +**Mask binds hard** (eligible ≫ 1500). The hypothesis that “1471 meant the mask never bound / unmasked +5σ” is **false**. -Artifact: `reports/fip-breadth-20260718-211440-breadth.json` +Harness `_signal_evaluation` vs manual IC through the same filter: **exact match** (−0.0168 / −1.85). + +### Verdict on the orphan + +The **+0.0575 / t +5.12** row is **orphaned**. Do not cite it. Root cause of that single run is not fully forensic-reconstructed (no dual dump from the original process remains), but every single-sourced recompute on this snapshot lands near **−0.017**, and the tier blend (≈800×−0.035 + ≈670×+0.014)/1471 ≈ **−0.013** is internally consistent with that number—not with +0.058. + +**Iron rule unconditional:** still **not green** (|IC| 0.017 < 0.03), and now with the correct mild-negative sign. + +Artifact: `reports/fip-reconcile-20260719-000520.json` --- -## Why “+IC on Nasdaq” is not a jumpiness-premium story +## Compositional story (supported) -`fip_id = sign(PRET) × (%neg − %pos)` **pools two opposite continuous populations:** +`fip_id = sign(PRET)×(%neg−%pos)` pools: -| Leg | Formation | Continuation intuition | IC contribution | -|---|---|---|---| -| **Continuous winners** | PRET>0, mostly up days (smooth climbers) | Paper: keep going up | **negative** | -| **Continuous losers / bleeders** | PRET<0, mostly down days (grind-down biotechs, SPACs, etc.) | Momentum: keep going down | **positive** | +- **Continuous winners** → want **negative** IC +- **Continuous bleeders** → want **positive** IC -Unconditional IC is a **tug-of-war weighted by universe composition**: +| check | IC | t | read | +|---|---:|---:|---| +| Prod-universe subset inside liquid | **−0.044** | **−2.88** | Matches fingerprint → compositional, not regime change | +| Tier 1–800 (senior) | **−0.035** | **−2.99** | Winner leg | +| Tier 801–1500 (junior) | **+0.014** | +1.25 | More bleeder / junk weight | +| Lagged membership (prior-week $vol) | −0.010 | −0.93 | Same sign as same-week; not a +5σ leak artifact | -- **S&P-like book** ≈ few steady bleeders → winner leg dominates → IC **−0.045**. -- **Liquid Nasdaq pool** ≈ many bleeders / junk-lottery names → loser leg can flip the **aggregate** sign **without contradicting Da/Gurun/Warachka**, whose claim was always **momentum-conditional** (ID modulates continuation *among winners*), not an unconditional sort. - -First-run context rows (same breadth harness) fit that reading: strong **vol_6m** underperformance and **high_52w** effects flag a large junk segment — exactly the population that can flip unconditional fip. - -**Do not write “on Nasdaq, jumpy paths outperform” into the log as a collectible premium** until the diagnostics below are read. +**Do not log “on Nasdaq, jumpy paths outperform.”** That would mythologize an orphaned +0.06. --- -## Follow-up diagnostics (same snapshot, independent panel) +## Platform-relevant test: momentum-conditional fip -Script: `scripts/run_fip_breadth_diagnostics.py` -Artifact: `reports/fip-breadth-diagnostics-20260718-213908.json` +Among liquid top-1500, keep **mom_12_1 ≥ P80** (~294 names/week): -| check | mean_ic | t | weeks | avg N | reliable | -|---|---:|---:|---:|---:|---| -| fip same-week liquid 1500 (panel) | −0.017 | −1.85 | 35 | 1471 | true | -| fip **lagged membership** (prior-week $vol) | −0.010 | −0.93 | 35 | 1471 | true | -| fip **tier 1–800** (senior liquid) | **−0.035** | **−2.99** | 35 | 791 | true | -| fip **tier 801–1500** (junior liquid) | **+0.014** | +1.25 | 35 | 700 | true | -| fip **prod-universe subset** inside liquid | **−0.044** | **−2.88** | 35 | 498 | true | -| fip **mom-conditional** (top 20% mom_12_1) | **−0.088** | **−4.58** | 35 | 294 | true | -| vol_6m liquid 1500 (panel) | −0.047 | −1.3 | 35 | 1471 | true | -| mom_12_1 liquid 1500 | +0.046 | +1.91 | 35 | 1471 | true | -| mom_12_1_resid liquid 1500 | +0.029 | +1.33 | 35 | 1471 | true | +| metric | value | +|---|---:| +| mean_ic | **−0.0879** | +| ic_t_stat | **−4.58** | +| ic_positive_pct | 22.9% | +| weeks | 35 | +| reliable | **true** | -### What the checks settle +Computed on the **same single-sourced path** as the authoritative −0.017. This is the paper’s claim and the only version a gate could consume. -1. **Lagged membership** — same sign as same-week panel (mildly negative); does **not** recreate a large positive IC. Not a clean “liquidity explosion leak manufactures +0.06” story for the panel path. (The first harness run’s **+0.0575** still does not match the independent panel’s −0.017 — treat the **+0.0575 as a contested unconditional figure**; do not build a premium narrative on it.) -2. **Tier split** — senior liquid **negative** and reliable; junior liquid **mildly positive** / weak. Bias and bleeder weight are stronger in the junior tier. -3. **Prod-universe subset** — IC **−0.044 / t −2.88**, ~498 names/week — matches the fingerprint. **Sign flip is compositional**, not “the whole market regime flipped.” -4. **Momentum-conditional fip (the platform test)** — IC **−0.088 / t −4.58**, reliable, ~294 winners/week. **Negative sign, |IC| ≳ 0.03.** This is the paper’s claim and the only version a gate could consume. - -### Platform verdict - -| Question | Answer | +| Decision | | |---|---| -| Unconditional fip iron rule (negative on liquid-1500) | **Not green** (first harness +0.06 fails sign; panel mild neg fails magnitude) | -| Production change now? | **No** | -| Is fip “dead forever”? | **No** — **alive only as a momentum-conditional tilt candidate** on breadth | -| Next real step if pursued | Book-level experiment: among qualified residual-momentum names, tilt/filter by lower fip — **not** an unconditional fip sort | -| Display card | Stays; still the right home until a book test wins | +| Unconditional fip | **Closed** for production | +| Mom-conditional fip | **Alive as book-tilt candidate only** — book sim before any gate talk | +| Display card | Stays | +| Production change | **None** | --- -## Buried headline: vol tilt / residual mom on breadth +## Vol-tilt / residual-mom warning (any future breadth move) -Even with panel vs harness magnitude differences, the **direction** is clear: +| signal (liquid, single-sourced) | IC | t | +|---|---:|---:| +| vol_6m | −0.048 | −1.4 | +| mom_12_1 | +0.046 | +1.9 | +| mom_12_1_resid | +0.029 | +1.3 | -- **High vol underperforms** on this pool relative to a clean S&P-like book. -- Production rank tilts **20% toward high volatility**, validated on S&P-like names where high-vol ≈ high-beta in a bull tape. On broad Nasdaq liquid, high-vol often means **lottery junk**. -- **If the universe ever broadens in production, re-validate the 80/20 high-vol tilt first** — it can flip from mildly helpful to actively harmful. -- **Raw momentum > residual** on breadth (panel and first harness both show this pattern) — SPY residualization is a noisier fit for small caps; a breadth book may want a different benchmark or raw mom. +High-vol names tend to underperform on this pool relative to a clean S&P-like book. Production **80/20 high-vol tilt** was validated on S&P-like names. **If the universe ever broadens in production, re-validate that tilt first** — it can flip from mildly helpful to harmful. Raw momentum also looks stronger than SPY residualization here (noisier fit for small caps). --- ## How to re-run (research branch only) ```powershell -# Windows .\.venv\Scripts\python.exe scripts\run_fip_breadth_diagnostics.py ` --research-snapshot backtest_snapshots\research.sqlite ` --prod-snapshot backtest_snapshots\prod.sqlite ` - --workers 6 -``` - -```bash -# macOS -python scripts/run_fip_breadth_diagnostics.py \ - --research-snapshot backtest_snapshots/research.sqlite \ - --prod-snapshot backtest_snapshots/prod.sqlite \ - --workers 6 + --workers 6 --allow-spawn ``` --- ## Bottom line -- Formal first screen: **not green**, no production change, fingerprint **pass**. -- Deeper reading: unconditional sign is a **compositional tug-of-war**, not a new jumpiness premium. -- **The test that matters for this platform already ran:** momentum-conditional fip is **negative, large, and reliable** on liquid breadth → fip remains a **conditional** research lead, not a closed door — and **not** a ship-ready gate input without a book experiment. +1. Formal iron-rule screen: **not green** either before or after reconciliation. +2. **+0.0575 / +5.12 is orphaned** — authoritative unconditional liquid fip is **−0.017 / −1.9**; mask binds (~97%). +3. Compositional tug-of-war is the right story; jumpiness premium is not. +4. **Mom-conditional −0.088 / −4.6 stands on the single-sourced path** → optional next research step is a **book** A/B, not a gate wire-in. +5. Log any future reader who sees both numbers: trust the reconcile artifact, not the orphaned breadth headline. diff --git a/reports/fip-reconcile-20260719-000520.json b/reports/fip-reconcile-20260719-000520.json new file mode 100644 index 0000000..2b9a3b7 --- /dev/null +++ b/reports/fip-reconcile-20260719-000520.json @@ -0,0 +1,6693 @@ +{ + "generated_at": "2026-07-19T00:05:20.113638", + "research_snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\research.sqlite", + "top_n": 1500, + "min_price": 5.0, + "prod_subset_n": 506, + "panel_tickers": 4403, + "single_source": "diagnostics uses harness _signal_series + _filter_liquid_breadth_week_rich only (no parallel mask)", + "avg_cross_section_semantics": "avg_cross_section = post-mask IC sample size. avg_raw_pool = pre-filter observations. avg_eligible_pre_mask = pass price+dvol before top-N. mask_binds_pct = weeks where eligible_pre_mask > top_n.", + "harness_self_consistent": true, + "checks": { + "fip_harness_signal_eval": { + "note": "Authoritative harness _signal_evaluation on collected fip_id", + "signal": "fip_id", + "weeks": 35, + "avg_cross_section": 1471.2, + "mean_ic": -0.0168, + "ic_t_stat": -1.85, + "ic_positive_pct": 40.0, + "mean_quintile_spread": -0.0052, + "reliable": true, + "liquid_breadth_top_n": 1500, + "liquid_min_price": 5.0, + "avg_raw_pool": 3214.4, + "avg_eligible_pre_mask": 2338.4, + "mask_binds_pct": 97.1 + }, + "fip_same_week_via_shared_filter": { + "note": "Same collected data, IC via shared _filter_liquid_breadth_week_rich", + "mean_ic": -0.0168, + "ic_t_stat": -1.85, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 40.0, + "reliable": true + }, + "fip_lagged_membership_1w": { + "note": "Top-N by prior-week $vol on current fip pool (shared filter)", + "mean_ic": -0.0102, + "ic_t_stat": -0.93, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 40.0, + "reliable": true + }, + "fip_tier_1_800": { + "note": "Senior liquid ranks 1\u2013800", + "mean_ic": -0.035, + "ic_t_stat": -2.99, + "weeks": 35, + "avg_cross_section": 791.2, + "ic_positive_pct": 25.7, + "reliable": true + }, + "fip_tier_801_1500": { + "note": "Junior liquid ranks 801\u2013top_n", + "mean_ic": 0.0141, + "ic_t_stat": 1.25, + "weeks": 35, + "avg_cross_section": 700.0, + "ic_positive_pct": 60.0, + "reliable": true + }, + "fip_prod_universe_subset": { + "note": "Prod.sqlite symbols inside liquid fip set", + "mean_ic": -0.0444, + "ic_t_stat": -2.88, + "weeks": 35, + "avg_cross_section": 497.5, + "ic_positive_pct": 25.7, + "reliable": true + }, + "fip_momentum_conditional_top20pct": { + "note": "Among liquid fip set, mom_12_1 \u2265 P80 (paper / gate-relevant)", + "mean_ic": -0.0879, + "ic_t_stat": -4.58, + "weeks": 35, + "avg_cross_section": 294.3, + "ic_positive_pct": 22.9, + "reliable": true + }, + "vol_6m_liquid": { + "note": "vol_6m through shared filter", + "mean_ic": -0.0478, + "ic_t_stat": -1.36, + "weeks": 35, + "avg_cross_section": 1500.0, + "ic_positive_pct": 37.1, + "reliable": true + }, + "mom_12_1_liquid": { + "note": "raw mom through shared filter", + "mean_ic": 0.0462, + "ic_t_stat": 1.91, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 65.7, + "reliable": true + }, + "mom_12_1_resid_liquid": { + "note": "residual mom through shared filter", + "mean_ic": 0.0289, + "ic_t_stat": 1.33, + "weeks": 35, + "avg_cross_section": 1471.2, + "ic_positive_pct": 60.0, + "reliable": true + } + }, + "membership_dumps": [ + { + "week": [ + 2022, + 49 + ], + "stats": { + "raw_pool": 2984, + "eligible_pre_mask": 2339, + "post_mask": 1500, + "mask_binds": true + }, + "symbols": [ + "A", + "AAL", + "AAON", + "AAPL", + "ABBV", + "ABCL", + "ABNB", + "ABT", + "ACAD", + "ACB", + "ACET", + "ACGL", + "ACHC", + "ACIW", + "ACLS", + "ACMR", + "ACN", + "ACRS", + "ACT", + "ADAM", + "ADBE", + "ADEA", + "ADI", + "ADM", + "ADP", + "ADPT", + "ADSK", + "ADTN", + "ADUS", + "ADV", + "AEE", + "AEHR", + "AEIS", + "AEP", + "AES", + "AEVA", + "AFCG", + "AFL", + "AFRM", + "AFYA", + "AGEN", + "AGIO", + "AGNC", + "AGNCN", + "AGNCO", + "AGNCP", + "AGNT", + "AGYS", + "AHCO", + "AIG", + "AIIO", + "AIZ", + "AJG", + "AKAM", + "ALB", + "ALCO", + "ALDX", + "ALEC", + "ALGM", + "ALGN", + "ALGT", + "ALHC", + "ALKS", + "ALKT", + "ALL", + "ALLE", + "ALLO", + "ALNT", + "ALNY", + "ALRM", + "ALT", + "ALXO", + "AMAL", + "AMAT", + "AMBA", + "AMCR", + "AMCX", + "AMD", + "AME", + "AMGN", + "AMKR", + "AMP", + "AMPH", + "AMPL", + "AMRN", + "AMSF", + "AMT", + "AMZN", + "ANAB", + "ANDE", + "ANET", + "ANGI", + "ANGO", + "ANIK", + "ANIP", + "AON", + "AOS", + "AOSL", + "APA", + "APD", + "APEI", + "APH", + "APO", + "APOG", + "APP", + "APPF", + "APPN", + "APPS", + "APTV", + "ARCB", + "ARCC", + "ARCT", + "ARE", + "ARES", + "ARGX", + "ARHS", + "ARKO", + "ARLP", + "ARQQ", + "ARQT", + "ARRY", + "ARTNA", + "ARVN", + "ARWR", + "ASLE", + "ASML", + "ASND", + "ASO", + "ASTE", + "ASTH", + "ASTL", + "ATEC", + "ATER", + "ATEX", + "ATNI", + "ATO", + "ATOM", + "ATRA", + "ATRC", + "ATRO", + "AUDC", + "AVAV", + "AVB", + "AVGO", + "AVNW", + "AVO", + "AVT", + "AVXL", + "AVY", + "AWK", + "AXGN", + "AXON", + "AXP", + "AXSM", + "AZO", + "AZTA", + "BA", + "BAC", + "BALL", + "BAND", + "BANF", + "BANR", + "BATRA", + "BATRK", + "BAX", + "BBIO", + "BBSI", + "BBY", + "BCAB", + "BCPC", + "BCRX", + "BCTX", + "BCYC", + "BDX", + "BEAM", + "BEAT", + "BEEM", + "BELFB", + "BEN", + "BF-B", + "BFC", + "BFST", + "BG", + "BHF", + "BIDU", + "BIIB", + "BILI", + "BIOX", + "BIRD", + "BJRI", + "BK", + "BKNG", + "BKR", + "BL", + "BLDP", + "BLDR", + "BLFS", + "BLK", + "BLKB", + "BLMN", + "BLNK", + "BMBL", + "BMRC", + "BMRN", + "BMY", + "BNGO", + "BNTX", + "BOKF", + "BOOM", + "BPOP", + "BR", + "BRK-B", + "BRKR", + "BRO", + "BRZE", + "BSX", + "BSY", + "BTAI", + "BUSE", + "BWB", + "BWIN", + "BX", + "BXP", + "BYND", + "BZ", + "BZUN", + "C", + "CABA", + "CAC", + "CACC", + "CAG", + "CAH", + "CAKE", + "CALM", + "CAMT", + "CAR", + "CARG", + "CARR", + "CASH", + "CASS", + "CASY", + "CAT", + "CATY", + "CB", + "CBOE", + "CBRE", + "CBRL", + "CBSH", + "CCAP", + "CCB", + "CCBG", + "CCC", + "CCCC", + "CCD", + "CCEP", + "CCI", + "CCL", + "CCNE", + "CCOI", + "CCRN", + "CCSI", + "CDLX", + "CDNA", + "CDNS", + "CDW", + "CDXS", + "CECO", + "CELH", + "CENN", + "CENT", + "CENTA", + "CENX", + "CERT", + "CEVA", + "CF", + "CFFN", + "CFG", + "CG", + "CGBD", + "CGC", + "CGEM", + "CGNX", + "CHCO", + "CHD", + "CHDN", + "CHEF", + "CHI", + "CHKP", + "CHRD", + "CHRS", + "CHRW", + "CHTR", + "CHW", + "CHY", + "CI", + "CIEN", + "CIGI", + "CINF", + "CL", + "CLAR", + "CLBK", + "CLDX", + "CLFD", + "CLMT", + "CLNE", + "CLX", + "CMCO", + "CMCSA", + "CME", + "CMG", + "CMI", + "CMPR", + "CMPS", + "CMRC", + "CMS", + "CMTL", + "CNC", + "CNOB", + "CNP", + "CNXC", + "CNXN", + "COCO", + "COF", + "COGT", + "COHR", + "COHU", + "COIN", + "COKE", + "COLB", + "COLL", + "COLM", + "COO", + "COP", + "COR", + "CORT", + "COST", + "CPAY", + "CPB", + "CPRT", + "CPT", + "CRAI", + "CRBU", + "CRCT", + "CRH", + "CRL", + "CRM", + "CRMT", + "CRNC", + "CRNX", + "CROX", + "CRSP", + "CRSR", + "CRTO", + "CRUS", + "CRVL", + "CRWD", + "CSCO", + "CSGP", + "CSIQ", + "CSQ", + "CSTL", + "CSWC", + "CSX", + "CTAS", + "CTBI", + "CTKB", + "CTRA", + "CTRN", + "CTSH", + "CTVA", + "CVBF", + "CVCO", + "CVLT", + "CVS", + "CVX", + "CWCO", + "CWST", + "CYRX", + "CYTK", + "CZR", + "D", + "DAL", + "DASH", + "DBGI", + "DBX", + "DCBO", + "DCGO", + "DD", + "DDOG", + "DE", + "DECK", + "DELL", + "DG", + "DGII", + "DGX", + "DH", + "DHI", + "DHR", + "DIOD", + "DIS", + "DJT", + "DKNG", + "DLO", + "DLR", + "DLTR", + "DMLP", + "DMRC", + "DNLI", + "DNUT", + "DOC", + "DOCU", + "DOMO", + "DOO", + "DORM", + "DOV", + "DOW", + "DOX", + "DPZ", + "DRH", + "DRI", + "DRS", + "DRVN", + "DSGN", + "DSGR", + "DSGX", + "DTE", + "DUK", + "DUOL", + "DVA", + "DVN", + "DXCM", + "DXLG", + "DXPE", + "DYN", + "EA", + "EBAY", + "EBC", + "ECHO", + "ECL", + "ECPG", + "ED", + "EDIT", + "EEFT", + "EFSC", + "EFX", + "EG", + "EGBN", + "EH", + "EIX", + "EL", + "ELV", + "EME", + "EMR", + "ENPH", + "ENSG", + "ENTA", + "ENTG", + "ENVX", + "EOG", + "EOLS", + "EPAM", + "EQIX", + "EQR", + "EQT", + "ERAS", + "ERIC", + "ERIE", + "ERII", + "ES", + "ESLT", + "ESQ", + "ESS", + "ESTA", + "ETN", + "ETR", + "EVCM", + "EVER", + "EVGO", + "EVRG", + "EW", + "EWBC", + "EWTX", + "EXC", + "EXE", + "EXEL", + "EXFY", + "EXLS", + "EXPD", + "EXPE", + "EXPO", + "EXR", + "EXTR", + "EYE", + "EZPW", + "F", + "FA", + "FANG", + "FAST", + "FATE", + "FBNC", + "FCBC", + "FCEL", + "FCFS", + "FCNCA", + "FCX", + "FDMT", + "FDS", + "FDUS", + "FDX", + "FE", + "FELE", + "FFAI", + "FFBC", + "FFIN", + "FFIV", + "FHB", + "FIBK", + "FICO", + "FIS", + "FISV", + "FITB", + "FIVE", + "FIVN", + "FIX", + "FIZZ", + "FLEX", + "FLGT", + "FLNA", + "FLNC", + "FLWS", + "FLYW", + "FMBH", + "FMNB", + "FNKO", + "FNUC", + "FORM", + "FORR", + "FOX", + "FOXA", + "FOXF", + "FRHC", + "FRME", + "FROG", + "FRPT", + "FRSH", + "FRT", + "FSLR", + "FSLY", + "FSV", + "FTAI", + "FTCI", + "FTDR", + "FTNT", + "FTV", + "FULC", + "FULT", + "FUTU", + "FWONA", + "FWONK", + "FWRD", + "FWRG", + "GABC", + "GAIN", + "GBDC", + "GCMG", + "GD", + "GDDY", + "GDRX", + "GDS", + "GDYN", + "GE", + "GEN", + "GFS", + "GGAL", + "GGR", + "GH", + "GIII", + "GILD", + "GIS", + "GL", + "GLAD", + "GLBE", + "GLNG", + "GLPI", + "GLUE", + "GLW", + "GM", + "GMAB", + "GNRC", + "GNTX", + "GO", + "GOGO", + "GOOD", + "GOOG", + "GOOGL", + "GPC", + "GPN", + "GPRE", + "GPRO", + "GRFS", + "GRMN", + "GRPN", + "GRWG", + "GS", + "GSAT", + "GSBC", + "GSHD", + "GT", + "GTLB", + "GTM", + "GTX", + "GWW", + "HAFC", + "HAIN", + "HAL", + "HALO", + "HAPN", + "HAS", + "HBAN", + "HBANP", + "HBNC", + "HCA", + "HCAT", + "HCKT", + "HCM", + "HCSG", + "HD", + "HDSN", + "HELE", + "HFWA", + "HIFS", + "HIG", + "HII", + "HIMX", + "HLIT", + "HLMN", + "HLNE", + "HLT", + "HNRG", + "HON", + "HOOD", + "HOPE", + "HPE", + "HPK", + "HPQ", + "HQY", + "HRL", + "HRMY", + "HROW", + "HRZN", + "HSIC", + "HST", + "HSTM", + "HSY", + "HTHT", + "HTLD", + "HTO", + "HTZ", + "HTZWW", + "HUBB", + "HUBG", + "HUDI", + "HUM", + "HURN", + "HUT", + "HWC", + "HWKN", + "HWM", + "HYFM", + "HYMC", + "IART", + "IBCP", + "IBKR", + "IBM", + "IBOC", + "IBRX", + "ICE", + "ICFI", + "ICHR", + "ICLR", + "ICUI", + "IDCC", + "IDXX", + "IDYA", + "IEP", + "IEX", + "IFF", + "IHRT", + "IIIV", + "ILMN", + "IMCR", + "IMKTA", + "IMMR", + "IMTX", + "IMUX", + "IMVT", + "IMXI", + "INCY", + "INDB", + "INDI", + "INGN", + "INMD", + "INO", + "INSE", + "INSM", + "INTA", + "INTC", + "INTU", + "INVA", + "INVH", + "IONS", + "IOSP", + "IOVA", + "IP", + "IPAR", + "IPGP", + "IQV", + "IR", + "IRDM", + "IRM", + "IRTC", + "IRWD", + "ISRG", + "IT", + "ITRI", + "ITW", + "IVZ", + "J", + "JACK", + "JAKK", + "JAZZ", + "JBHT", + "JBIO", + "JBL", + "JBLU", + "JBSS", + "JCI", + "JD", + "JJSF", + "JKHY", + "JNJ", + "JOUT", + "JOYY", + "JPM", + "JRVR", + "JYNT", + "KALU", + "KDP", + "KE", + "KELYA", + "KEY", + "KEYS", + "KHC", + "KIDS", + "KIM", + "KKR", + "KLAC", + "KLIC", + "KLRS", + "KLXE", + "KMB", + "KMI", + "KNSA", + "KO", + "KOD", + "KPTI", + "KR", + "KRNT", + "KRNY", + "KROS", + "KRUS", + "KRYS", + "KTOS", + "KURA", + "KYMR", + "KYNB", + "L", + "LAMR", + "LAND", + "LASR", + "LAUR", + "LBRDA", + "LBRDK", + "LBTYA", + "LBTYK", + "LCID", + "LDOS", + "LE", + "LECO", + "LEGN", + "LEN", + "LESL", + "LFUS", + "LGIH", + "LGND", + "LH", + "LHX", + "LI", + "LII", + "LILA", + "LILAK", + "LIN", + "LIND", + "LITE", + "LIVN", + "LKFN", + "LKFT", + "LKQ", + "LLY", + "LMAT", + "LMT", + "LNT", + "LNTH", + "LOCO", + "LOGI", + "LOPE", + "LOVE", + "LOW", + "LPLA", + "LPRO", + "LPSN", + "LQDA", + "LQDT", + "LRCX", + "LSCC", + "LSTR", + "LULU", + "LUNG", + "LUV", + "LVS", + "LWLG", + "LYB", + "LYEL", + "LYFT", + "LYV", + "LZ", + "MA", + "MAA", + "MANH", + "MAR", + "MARA", + "MAS", + "MASS", + "MAT", + "MATW", + "MBIN", + "MBLY", + "MBUU", + "MBWM", + "MCD", + "MCFT", + "MCHB", + "MCHP", + "MCK", + "MCO", + "MCRB", + "MCRI", + "MDB", + "MDGL", + "MDLZ", + "MDT", + "MEDP", + "MELI", + "MEOH", + "MERC", + "MET", + "META", + "METC", + "MFIC", + "MGEE", + "MGM", + "MGNI", + "MGNX", + "MGPI", + "MGRC", + "MIDD", + "MIRM", + "MITK", + "MKC", + "MKSI", + "MKTX", + "MLAB", + "MLCO", + "MLKN", + "MLM", + "MMM", + "MMSI", + "MMYT", + "MNDY", + "MNRO", + "MNST", + "MNTK", + "MO", + "MOMO", + "MORN", + "MOS", + "MPAA", + "MPC", + "MPWR", + "MQ", + "MRCY", + "MRK", + "MRNA", + "MRSH", + "MRTN", + "MRVI", + "MRVL", + "MS", + "MSBI", + "MSCI", + "MSEX", + "MSFT", + "MSI", + "MSTR", + "MTB", + "MTCH", + "MTD", + "MTLS", + "MTSI", + "MTVA", + "MU", + "MXCT", + "MXL", + "MYGN", + "MYRG", + "MZTI", + "NAVI", + "NBIX", + "NBTB", + "NCLH", + "NCNO", + "NDAQ", + "NDSN", + "NEE", + "NEM", + "NEO", + "NEOG", + "NESR", + "NEWT", + "NFBK", + "NFE", + "NFLX", + "NI", + "NICE", + "NIU", + "NKE", + "NKTR", + "NKTX", + "NMFC", + "NMIH", + "NMRK", + "NNOX", + "NOC", + "NOVT", + "NOW", + "NRC", + "NRDS", + "NRG", + "NRIX", + "NSC", + "NSIT", + "NSSC", + "NTAP", + "NTCT", + "NTES", + "NTGR", + "NTLA", + "NTNX", + "NTRA", + "NTRS", + "NUE", + "NVAX", + "NVCR", + "NVDA", + "NVEC", + "NVMI", + "NVR", + "NWBI", + "NWE", + "NWL", + "NWPX", + "NWS", + "NWSA", + "NXPI", + "NXST", + "O", + "OCFC", + "OCSL", + "ODFL", + "OFIX", + "OFLX", + "OKE", + "OKTA", + "OLED", + "OLLI", + "OM", + "OMAB", + "OMC", + "OMCL", + "ON", + "ONB", + "ONC", + "ONEW", + "OPCH", + "OPI", + "OPRX", + "ORCL", + "ORLY", + "ORMP", + "OSBC", + "OSIS", + "OSPN", + "OSUR", + "OSW", + "OTEX", + "OTIS", + "OTLY", + "OTTR", + "OUST", + "OXLC", + "OXY", + "OZK", + "PAA", + "PACB", + "PAGP", + "PAHC", + "PAMT", + "PANW", + "PATK", + "PAX", + "PAYO", + "PAYX", + "PCAR", + "PCG", + "PCRX", + "PCT", + "PCTY", + "PCVX", + "PDD", + "PDFS", + "PDSB", + "PEBO", + "PECO", + "PEG", + "PEGA", + "PENG", + "PENN", + "PEP", + "PERI", + "PETS", + "PFBC", + "PFE", + "PFG", + "PG", + "PGC", + "PGNY", + "PGR", + "PGY", + "PH", + "PHAT", + "PHM", + "PHUN", + "PI", + "PKG", + "PLAB", + "PLAY", + "PLCE", + "PLD", + "PLMR", + "PLPC", + "PLRX", + "PLTK", + "PLTR", + "PLUG", + "PLUS", + "PLXS", + "PM", + "PMVP", + "PNC", + "PNR", + "PNTG", + "PNW", + "PODD", + "POOL", + "POWI", + "PPC", + "PPG", + "PPL", + "PPLI", + "PRAA", + "PRCT", + "PRDO", + "PRGS", + "PRME", + "PRPL", + "PRTA", + "PRTS", + "PRU", + "PRVA", + "PSA", + "PSEC", + "PSMT", + "PSX", + "PTC", + "PTCT", + "PTEN", + "PTGX", + "PTLO", + "PTON", + "PUBM", + "PWP", + "PWR", + "PYPL", + "PZZA", + "QCOM", + "QCRH", + "QDEL", + "QFIN", + "QLYS", + "QNST", + "QQQX", + "QRVO", + "QS", + "QTRX", + "QURE", + "RARE", + "RCKT", + "RCL", + "RCMT", + "RDNT", + "RDNW", + "RDWR", + "REG", + "REGN", + "RELL", + "RELY", + "RENT", + "REPL", + "REYN", + "RF", + "RGEN", + "RGLD", + "RGNX", + "RGP", + "RICK", + "RIGL", + "RILY", + "RIVN", + "RJF", + "RL", + "RLAY", + "RMBS", + "RMD", + "RMR", + "RNA", + "RNW", + "ROAD", + "ROCK", + "ROIV", + "ROK", + "ROKU", + "ROL", + "ROOT", + "ROP", + "ROST", + "RPAY", + "RPD", + "RPRX", + "RRGB", + "RRR", + "RSG", + "RTX", + "RUM", + "RUN", + "RUSHA", + "RVMD", + "RVTY", + "RXRX", + "RYAAY", + "RYTM", + "SABR", + "SAFT", + "SAIA", + "SAIC", + "SANM", + "SATS", + "SBAC", + "SBCF", + "SBGI", + "SBLK", + "SBRA", + "SBUX", + "SCHL", + "SCHW", + "SCSC", + "SDGR", + "SEAT", + "SEDG", + "SEER", + "SEIC", + "SENEA", + "SENS", + "SFM", + "SFNC", + "SGHT", + "SGML", + "SGRY", + "SHC", + "SHEN", + "SHLS", + "SHOE", + "SHOO", + "SHOP", + "SHW", + "SIBN", + "SIGA", + "SIGI", + "SIMO", + "SIRI", + "SITM", + "SJM", + "SKIN", + "SKYT", + "SKYW", + "SLAB", + "SLB", + "SLM", + "SLP", + "SLRC", + "SMBC", + "SMCI", + "SMPL", + "SMTC", + "SNA", + "SNDX", + "SNEX", + "SNPS", + "SNY", + "SO", + "SONO", + "SPFI", + "SPG", + "SPGI", + "SPOK", + "SPSC", + "SPT", + "SPWH", + "SRAD", + "SRCE", + "SRE", + "SRPT", + "SRRK", + "SRTS", + "SSNC", + "SSP", + "SSRM", + "SSTI", + "SSYS", + "STAA", + "STBA", + "STE", + "STEP", + "STGW", + "STLD", + "STNE", + "STOK", + "STRA", + "STRL", + "STRO", + "STT", + "STX", + "STZ", + "SUNE", + "SUPN", + "SVC", + "SW", + "SWBI", + "SWK", + "SWKS", + "SYBT", + "SYF", + "SYK", + "SYM", + "SYNA", + "SYY", + "T", + "TAP", + "TARS", + "TASK", + "TBBK", + "TBCH", + "TBLD", + "TBPH", + "TC", + "TCBI", + "TCBK", + "TCMD", + "TCOM", + "TCPC", + "TCRT", + "TCX", + "TDG", + "TDY", + "TEAM", + "TECH", + "TEL", + "TENB", + "TER", + "TFC", + "TFSL", + "TGT", + "TGTX", + "TH", + "THFF", + "THRM", + "THRY", + "TIGO", + "TIGR", + "TIL", + "TILE", + "TITN", + "TJX", + "TKO", + "TLRY", + "TMCI", + "TMDX", + "TMO", + "TMUS", + "TNDM", + "TNGX", + "TOWN", + "TPL", + "TPR", + "TREE", + "TRGP", + "TRI", + "TRIN", + "TRIP", + "TRMB", + "TRMD", + "TRMK", + "TRNS", + "TROW", + "TRS", + "TRST", + "TRUP", + "TRV", + "TSCO", + "TSEM", + "TSLA", + "TSN", + "TT", + "TTD", + "TTEC", + "TTEK", + "TTGT", + "TTMI", + "TTWO", + "TVRD", + "TVTX", + "TW", + "TWST", + "TXG", + "TXN", + "TXRH", + "TXT", + "TYL", + "UAL", + "UBER", + "UBSI", + "UCTT", + "UDR", + "UEIC", + "UFCS", + "UFPI", + "UFPT", + "UHS", + "ULCC", + "ULH", + "ULTA", + "UMBF", + "UNH", + "UNIT", + "UNP", + "UPBD", + "UPLD", + "UPS", + "UPST", + "UPWK", + "URBN", + "URI", + "USB", + "UTHR", + "UVSP", + "V", + "VC", + "VCEL", + "VCTR", + "VCYT", + "VECO", + "VERA", + "VERI", + "VERU", + "VERX", + "VIAV", + "VICI", + "VICR", + "VIR", + "VISN", + "VITL", + "VLO", + "VLY", + "VMC", + "VNDA", + "VNET", + "VNOM", + "VOD", + "VRDN", + "VREX", + "VRM", + "VRNS", + "VRRM", + "VRSK", + "VRSN", + "VRT", + "VRTX", + "VSAT", + "VSEC", + "VST", + "VTR", + "VTRS", + "VZ", + "WAB", + "WABC", + "WAFD", + "WASH", + "WAT", + "WB", + "WDAY", + "WDC", + "WDFC", + "WEC", + "WELL", + "WEN", + "WERN", + "WEST", + "WFC", + "WFRD", + "WHWK", + "WINA", + "WING", + "WIX", + "WKHS", + "WM", + "WMB", + "WMG", + "WMT", + "WOOF", + "WRB", + "WRLD", + "WSBC", + "WSBF", + "WSC", + "WSFS", + "WSM", + "WST", + "WTFC", + "WTW", + "WWD", + "WY", + "WYNN", + "XAIR", + "XEL", + "XENE", + "XFOR", + "XMTR", + "XNCR", + "XOM", + "XP", + "XPEL", + "XRAY", + "XRX", + "XYL", + "XYZ", + "YORW", + "YUM", + "Z", + "ZBH", + "ZBRA", + "ZD", + "ZG", + "ZION", + "ZLAB", + "ZM", + "ZNTL", + "ZS", + "ZTS", + "ZUMZ", + "ZVZZT", + "ZYME" + ], + "n_symbols": 1500 + }, + { + "week": [ + 2022, + 31 + ], + "stats": { + "raw_pool": 2800, + "eligible_pre_mask": 2332, + "post_mask": 1500, + "mask_binds": true + }, + "symbols": [ + "A", + "AAL", + "AAON", + "AAPL", + "ABBV", + "ABCL", + "ABNB", + "ABT", + "ACAD", + "ACB", + "ACET", + "ACGL", + "ACHC", + "ACIW", + "ACLS", + "ACMR", + "ACN", + "ACRS", + "ACT", + "ACTG", + "ADAM", + "ADBE", + "ADEA", + "ADI", + "ADM", + "ADP", + "ADPT", + "ADSK", + "ADTN", + "ADUS", + "ADV", + "AEE", + "AEHR", + "AEIS", + "AEP", + "AES", + "AEVA", + "AFCG", + "AFL", + "AFRM", + "AFYA", + "AGEN", + "AGIO", + "AGNC", + "AGNT", + "AGYS", + "AHCO", + "AIG", + "AIZ", + "AJG", + "AKAM", + "ALB", + "ALCO", + "ALDX", + "ALEC", + "ALGM", + "ALGN", + "ALGT", + "ALHC", + "ALKS", + "ALKT", + "ALL", + "ALLE", + "ALLO", + "ALNY", + "ALRM", + "ALT", + "ALXO", + "AMAL", + "AMAT", + "AMBA", + "AMCR", + "AMCX", + "AMD", + "AME", + "AMGN", + "AMKR", + "AMP", + "AMPH", + "AMRN", + "AMSC", + "AMSF", + "AMT", + "AMTX", + "AMZN", + "ANAB", + "ANDE", + "ANET", + "ANGI", + "ANGO", + "ANIK", + "ANIP", + "AON", + "AOS", + "AOSL", + "AOUT", + "APA", + "APD", + "APH", + "API", + "APO", + "APOG", + "APP", + "APPF", + "APPN", + "APPS", + "APTV", + "APYX", + "ARCB", + "ARCC", + "ARCT", + "ARE", + "ARES", + "ARGX", + "ARKO", + "ARLP", + "ARQT", + "ARRY", + "ARTNA", + "ARVN", + "ARWR", + "ASLE", + "ASML", + "ASND", + "ASO", + "ASTE", + "ASTH", + "ASTL", + "ASTS", + "ATEC", + "ATER", + "ATEX", + "ATLC", + "ATNI", + "ATO", + "ATOM", + "ATRA", + "ATRC", + "AUDC", + "AUPH", + "AVAV", + "AVB", + "AVGO", + "AVIR", + "AVNW", + "AVO", + "AVPT", + "AVT", + "AVXL", + "AVY", + "AWK", + "AXGN", + "AXON", + "AXP", + "AXSM", + "AZO", + "AZTA", + "BA", + "BAC", + "BALL", + "BAND", + "BANF", + "BANR", + "BATRK", + "BAX", + "BBIO", + "BBSI", + "BBY", + "BCBP", + "BCML", + "BCPC", + "BCRX", + "BCTX", + "BCYC", + "BDX", + "BEAM", + "BEEM", + "BEN", + "BF-B", + "BG", + "BHF", + "BHFAN", + "BIDU", + "BIIB", + "BILI", + "BJRI", + "BK", + "BKNG", + "BKR", + "BL", + "BLDP", + "BLDR", + "BLFS", + "BLK", + "BLKB", + "BLMN", + "BLNK", + "BMBL", + "BMEA", + "BMRN", + "BMY", + "BNGO", + "BNR", + "BNTX", + "BOKF", + "BOOM", + "BPOP", + "BR", + "BRK-B", + "BRKR", + "BRO", + "BSET", + "BSX", + "BSY", + "BTAI", + "BUSE", + "BVS", + "BWIN", + "BX", + "BXP", + "BYND", + "BYRN", + "BZ", + "BZUN", + "C", + "CAC", + "CACC", + "CAG", + "CAH", + "CAKE", + "CALM", + "CAMT", + "CAR", + "CARG", + "CARR", + "CASH", + "CASS", + "CASY", + "CAT", + "CATY", + "CB", + "CBOE", + "CBRE", + "CBRL", + "CBSH", + "CCB", + "CCC", + "CCCC", + "CCD", + "CCEC", + "CCEP", + "CCI", + "CCL", + "CCOI", + "CCRN", + "CCXI", + "CDLX", + "CDNA", + "CDNS", + "CDW", + "CDXS", + "CELH", + "CELU", + "CENN", + "CENT", + "CENTA", + "CENX", + "CERS", + "CERT", + "CEVA", + "CF", + "CFFN", + "CFG", + "CG", + "CGBD", + "CGC", + "CGEM", + "CGNX", + "CHCO", + "CHD", + "CHDN", + "CHEF", + "CHI", + "CHKP", + "CHRD", + "CHRS", + "CHRW", + "CHTR", + "CHW", + "CHY", + "CI", + "CIEN", + "CIGI", + "CINF", + "CL", + "CLAR", + "CLBK", + "CLDX", + "CLFD", + "CLNE", + "CLPT", + "CLX", + "CMCO", + "CMCSA", + "CME", + "CMG", + "CMI", + "CMPR", + "CMPS", + "CMRC", + "CMS", + "CMTL", + "CNC", + "CNOB", + "CNP", + "CNXC", + "CNXN", + "CODX", + "COF", + "COGT", + "COHR", + "COHU", + "COIN", + "COKE", + "COLB", + "COLL", + "COLM", + "COO", + "COP", + "COR", + "CORT", + "COST", + "CPAY", + "CPB", + "CPRT", + "CPSS", + "CPT", + "CRAI", + "CRBU", + "CRH", + "CRIS", + "CRL", + "CRM", + "CRMT", + "CRNC", + "CRNX", + "CROX", + "CRSP", + "CRSR", + "CRTO", + "CRUS", + "CRVL", + "CRWD", + "CSCO", + "CSGP", + "CSIQ", + "CSQ", + "CSTL", + "CSWC", + "CSX", + "CTAS", + "CTBI", + "CTKB", + "CTRA", + "CTRM", + "CTRN", + "CTSH", + "CTVA", + "CVBF", + "CVCO", + "CVLT", + "CVNA", + "CVS", + "CVX", + "CWST", + "CYRX", + "CYTK", + "CZR", + "D", + "DAL", + "DASH", + "DAVE", + "DBX", + "DCBO", + "DCGO", + "DD", + "DDOG", + "DE", + "DECK", + "DELL", + "DFTX", + "DG", + "DGICA", + "DGII", + "DGX", + "DHI", + "DHR", + "DIOD", + "DIS", + "DJT", + "DKNG", + "DLO", + "DLR", + "DLTR", + "DMLP", + "DMRC", + "DNLI", + "DNUT", + "DOC", + "DOCU", + "DOMO", + "DOO", + "DORM", + "DOV", + "DOW", + "DOX", + "DPZ", + "DRH", + "DRI", + "DRS", + "DRVN", + "DSGN", + "DSGX", + "DTE", + "DTIL", + "DUK", + "DUOL", + "DVA", + "DVN", + "DXCM", + "DXPE", + "EA", + "EBAY", + "EBC", + "ECHO", + "ECL", + "ECPG", + "ED", + "EDIT", + "EEFT", + "EFSC", + "EFX", + "EG", + "EGBN", + "EH", + "EHTH", + "EIX", + "EL", + "ELV", + "EME", + "EMR", + "ENPH", + "ENSG", + "ENTA", + "ENTG", + "ENVX", + "EOG", + "EOLS", + "EPAM", + "EQIX", + "EQR", + "EQT", + "ERAS", + "ERIC", + "ERIE", + "ERII", + "ES", + "ESLT", + "ESS", + "ESTA", + "ETN", + "ETR", + "EVCM", + "EVER", + "EVGO", + "EVRG", + "EW", + "EWBC", + "EWTX", + "EXC", + "EXE", + "EXEL", + "EXLS", + "EXPD", + "EXPE", + "EXPO", + "EXR", + "EXTR", + "EYE", + "EZPW", + "F", + "FA", + "FANG", + "FAST", + "FATE", + "FBNC", + "FCEL", + "FCFS", + "FCNCA", + "FCX", + "FDMT", + "FDS", + "FDUS", + "FDX", + "FE", + "FELE", + "FFAI", + "FFBC", + "FFIN", + "FFIV", + "FHB", + "FHTX", + "FIBK", + "FICO", + "FIS", + "FISV", + "FITB", + "FIVE", + "FIVN", + "FIX", + "FIZZ", + "FLEX", + "FLGT", + "FLL", + "FLNA", + "FLWS", + "FLYW", + "FMAO", + "FMBH", + "FNKO", + "FORM", + "FORR", + "FOSL", + "FOX", + "FOXA", + "FOXF", + "FRHC", + "FRME", + "FROG", + "FRPT", + "FRT", + "FSLR", + "FSLY", + "FSV", + "FTAI", + "FTCI", + "FTDR", + "FTNT", + "FTV", + "FULC", + "FULT", + "FUTU", + "FWONA", + "FWONK", + "FWRD", + "GABC", + "GAIN", + "GBDC", + "GCMG", + "GD", + "GDDY", + "GDRX", + "GDS", + "GDYN", + "GE", + "GEN", + "GGAL", + "GGR", + "GH", + "GIII", + "GILD", + "GIS", + "GL", + "GLAD", + "GLBE", + "GLNG", + "GLPI", + "GLUE", + "GLW", + "GM", + "GMAB", + "GNRC", + "GNTX", + "GO", + "GOGO", + "GOOD", + "GOOG", + "GOOGL", + "GOSS", + "GOVX", + "GPC", + "GPN", + "GPRE", + "GPRO", + "GRFS", + "GRMN", + "GRPN", + "GS", + "GSAT", + "GSBC", + "GSHD", + "GSM", + "GT", + "GTM", + "GTX", + "GWW", + "HAFC", + "HAIN", + "HAL", + "HALO", + "HAPN", + "HAS", + "HBAN", + "HBNC", + "HCA", + "HCAT", + "HCKT", + "HCM", + "HCSG", + "HD", + "HDSN", + "HELE", + "HFWA", + "HIG", + "HII", + "HIMX", + "HIVE", + "HLIT", + "HLMN", + "HLNE", + "HLT", + "HNRG", + "HOFT", + "HON", + "HOOD", + "HOPE", + "HPE", + "HPK", + "HPQ", + "HQY", + "HRL", + "HRMY", + "HRZN", + "HSIC", + "HST", + "HSTM", + "HSY", + "HTHT", + "HTLD", + "HTO", + "HUBB", + "HUBG", + "HUM", + "HURN", + "HUT", + "HWC", + "HWKN", + "HWM", + "HYFM", + "HYMC", + "IART", + "IBCP", + "IBKR", + "IBM", + "IBOC", + "ICE", + "ICFI", + "ICHR", + "ICLR", + "ICUI", + "IDCC", + "IDXX", + "IDYA", + "IEP", + "IEX", + "IFF", + "IHRT", + "III", + "IIIV", + "ILMN", + "ILPT", + "IMCR", + "IMKTA", + "IMMR", + "IMXI", + "INCY", + "INDB", + "INDI", + "INGN", + "INMD", + "INO", + "INSE", + "INSG", + "INSM", + "INTA", + "INTC", + "INTU", + "INVA", + "INVH", + "INVZ", + "IONS", + "IOSP", + "IOVA", + "IP", + "IPAR", + "IPGP", + "IQV", + "IR", + "IRDM", + "IRM", + "IRTC", + "IRWD", + "ISRG", + "IT", + "ITRI", + "ITW", + "IVZ", + "J", + "JACK", + "JAZZ", + "JBHT", + "JBL", + "JBLU", + "JBSS", + "JCI", + "JD", + "JJSF", + "JKHY", + "JNJ", + "JOUT", + "JOYY", + "JPM", + "JRVR", + "JYNT", + "KALU", + "KDP", + "KE", + "KELYA", + "KEY", + "KEYS", + "KHC", + "KIDS", + "KIM", + "KKR", + "KLAC", + "KLIC", + "KLRS", + "KMB", + "KMI", + "KNSA", + "KO", + "KOD", + "KPTI", + "KR", + "KRNT", + "KRNY", + "KROS", + "KRUS", + "KRYS", + "KTOS", + "KURA", + "KYMR", + "KYNB", + "L", + "LAMR", + "LAND", + "LASR", + "LAUR", + "LBRDA", + "LBRDK", + "LBTYA", + "LBTYK", + "LCID", + "LDOS", + "LE", + "LECO", + "LEGN", + "LEN", + "LENZ", + "LESL", + "LFST", + "LFUS", + "LGIH", + "LGND", + "LH", + "LHX", + "LI", + "LIDR", + "LII", + "LILA", + "LILAK", + "LIN", + "LIND", + "LITE", + "LITS", + "LIVN", + "LKFN", + "LKFT", + "LKQ", + "LLY", + "LMAT", + "LMT", + "LNT", + "LNTH", + "LOCO", + "LOGI", + "LONA", + "LOPE", + "LOVE", + "LOW", + "LPLA", + "LPRO", + "LPSN", + "LQDA", + "LQDT", + "LRCX", + "LSCC", + "LSTR", + "LULU", + "LUNG", + "LUV", + "LVS", + "LYB", + "LYEL", + "LYFT", + "LYV", + "LZ", + "MA", + "MAA", + "MANH", + "MAR", + "MARA", + "MAS", + "MASS", + "MAT", + "MATW", + "MBIN", + "MBUU", + "MCD", + "MCFT", + "MCHB", + "MCHP", + "MCK", + "MCO", + "MCRB", + "MCRI", + "MDB", + "MDGL", + "MDLZ", + "MDT", + "MEDP", + "MELI", + "MEOH", + "MERC", + "MET", + "META", + "METC", + "MFIC", + "MGEE", + "MGM", + "MGNI", + "MGPI", + "MGRC", + "MIDD", + "MIRM", + "MITK", + "MKC", + "MKSI", + "MKTX", + "MLAB", + "MLCO", + "MLKN", + "MLM", + "MMM", + "MMSI", + "MMYT", + "MNDY", + "MNRO", + "MNST", + "MNTK", + "MNTS", + "MO", + "MORN", + "MOS", + "MPC", + "MPWR", + "MQ", + "MRCY", + "MRK", + "MRNA", + "MRSH", + "MRTN", + "MRVI", + "MRVL", + "MS", + "MSBI", + "MSCI", + "MSEX", + "MSFT", + "MSI", + "MSTR", + "MTB", + "MTCH", + "MTD", + "MTLS", + "MTRX", + "MTSI", + "MU", + "MVIS", + "MXCT", + "MXL", + "MYGN", + "MYRG", + "MZTI", + "NAVI", + "NBIX", + "NBN", + "NBP", + "NBTB", + "NCLH", + "NCMI", + "NCNO", + "NDAQ", + "NDSN", + "NEE", + "NEGG", + "NEM", + "NEO", + "NEOG", + "NESR", + "NEWT", + "NEXT", + "NFBK", + "NFE", + "NFLX", + "NI", + "NICE", + "NIU", + "NKE", + "NKTR", + "NKTX", + "NMFC", + "NMIH", + "NMRK", + "NNOX", + "NOC", + "NOVT", + "NOW", + "NRC", + "NRG", + "NRIX", + "NSC", + "NSIT", + "NSSC", + "NTAP", + "NTCT", + "NTES", + "NTGR", + "NTLA", + "NTNX", + "NTRA", + "NTRS", + "NUE", + "NVAX", + "NVCR", + "NVDA", + "NVEC", + "NVMI", + "NVR", + "NVTS", + "NWBI", + "NWE", + "NWL", + "NWPX", + "NWS", + "NWSA", + "NXPI", + "NXST", + "O", + "OCFC", + "OCSL", + "ODFL", + "OFIX", + "OKE", + "OKTA", + "OLED", + "OLLI", + "OM", + "OMAB", + "OMC", + "OMCL", + "OMER", + "ON", + "ONB", + "ONC", + "ONDS", + "ONEW", + "OPAL", + "OPCH", + "OPEN", + "OPI", + "OPRT", + "OPRX", + "ORCL", + "ORGO", + "ORLY", + "ORMP", + "OSBC", + "OSIS", + "OSPN", + "OSW", + "OTEX", + "OTIS", + "OTLY", + "OTTR", + "OUST", + "OXLC", + "OXY", + "OZK", + "PAA", + "PACB", + "PAGP", + "PAHC", + "PANW", + "PATK", + "PAX", + "PAYO", + "PAYX", + "PCAR", + "PCG", + "PCRX", + "PCT", + "PCTY", + "PCVX", + "PDD", + "PDFS", + "PDSB", + "PEBO", + "PECO", + "PEG", + "PEGA", + "PENG", + "PENN", + "PEP", + "PERI", + "PETS", + "PFBC", + "PFE", + "PFG", + "PG", + "PGC", + "PGNY", + "PGR", + "PGY", + "PH", + "PHAT", + "PHM", + "PHUN", + "PI", + "PKG", + "PLAB", + "PLAY", + "PLBY", + "PLCE", + "PLD", + "PLMR", + "PLRX", + "PLTK", + "PLTR", + "PLUG", + "PLUS", + "PLXS", + "PM", + "PMVP", + "PNC", + "PNR", + "PNTG", + "PNW", + "PODD", + "POOL", + "POWI", + "POWW", + "PPC", + "PPG", + "PPL", + "PPLI", + "PRAA", + "PRAX", + "PRDO", + "PRGS", + "PRTA", + "PRTS", + "PRU", + "PRVA", + "PSA", + "PSEC", + "PSMT", + "PSX", + "PTC", + "PTCT", + "PTEN", + "PTGX", + "PTON", + "PUBM", + "PWP", + "PWR", + "PYPL", + "PZZA", + "QCOM", + "QCRH", + "QDEL", + "QFIN", + "QLYS", + "QNST", + "QQQX", + "QRVO", + "QS", + "QTRX", + "QURE", + "RARE", + "RCKT", + "RCL", + "RCMT", + "RDNT", + "RDNW", + "RDWR", + "REG", + "REGN", + "RELL", + "REPL", + "REYN", + "RF", + "RGEN", + "RGLD", + "RGNX", + "RGP", + "RICK", + "RIGL", + "RILY", + "RIOT", + "RJF", + "RKLB", + "RL", + "RLAY", + "RLMD", + "RMBS", + "RMD", + "RMNI", + "RMR", + "RNA", + "RNAC", + "RNW", + "ROAD", + "ROCK", + "ROK", + "ROKU", + "ROL", + "ROOT", + "ROP", + "ROST", + "RPAY", + "RPD", + "RPRX", + "RRGB", + "RRR", + "RSG", + "RTX", + "RUM", + "RUN", + "RUSHA", + "RUSHB", + "RVMD", + "RVTY", + "RXRX", + "RXT", + "RYAAY", + "RYTM", + "SABR", + "SAFT", + "SAIA", + "SAIC", + "SAIL", + "SANA", + "SANM", + "SATS", + "SBAC", + "SBCF", + "SBGI", + "SBLK", + "SBRA", + "SBUX", + "SCHL", + "SCHW", + "SCSC", + "SDGR", + "SEAT", + "SEDG", + "SEER", + "SEIC", + "SENEA", + "SENS", + "SFIX", + "SFM", + "SFNC", + "SGHT", + "SGRY", + "SHBI", + "SHC", + "SHEN", + "SHIP", + "SHLS", + "SHOE", + "SHOO", + "SHOP", + "SHW", + "SIBN", + "SIGA", + "SIGI", + "SIMO", + "SIRI", + "SITM", + "SJM", + "SKIN", + "SKYW", + "SLAB", + "SLB", + "SLDP", + "SLM", + "SLP", + "SLRC", + "SMCI", + "SMPL", + "SMTC", + "SNA", + "SNDX", + "SNEX", + "SNPS", + "SNY", + "SO", + "SOFI", + "SOHU", + "SONO", + "SPG", + "SPGI", + "SPSC", + "SPT", + "SPWH", + "SRCE", + "SRE", + "SRPT", + "SRRK", + "SRTA", + "SRTS", + "SSNC", + "SSP", + "SSRM", + "SSYS", + "STAA", + "STBA", + "STE", + "STEP", + "STGW", + "STLD", + "STNE", + "STOK", + "STRA", + "STRL", + "STRO", + "STT", + "STX", + "STZ", + "SUPN", + "SVC", + "SW", + "SWBI", + "SWIM", + "SWK", + "SWKS", + "SYBT", + "SYF", + "SYK", + "SYM", + "SYNA", + "SYY", + "T", + "TAP", + "TARS", + "TASK", + "TBBK", + "TBCH", + "TBPH", + "TCBI", + "TCBK", + "TCMD", + "TCOM", + "TCPC", + "TCRT", + "TCX", + "TDG", + "TDY", + "TEAD", + "TEAM", + "TECH", + "TEL", + "TENB", + "TER", + "TFC", + "TFSL", + "TGT", + "TGTX", + "TH", + "THFF", + "THRM", + "THRY", + "TIGO", + "TIL", + "TILE", + "TITN", + "TJX", + "TKO", + "TLRY", + "TLS", + "TMCI", + "TMDX", + "TMO", + "TMUS", + "TNDM", + "TNXP", + "TOWN", + "TPL", + "TPR", + "TREE", + "TRGP", + "TRI", + "TRIN", + "TRIP", + "TRMB", + "TRMD", + "TRMK", + "TRNS", + "TROW", + "TRS", + "TRST", + "TRUP", + "TRV", + "TSCO", + "TSEM", + "TSLA", + "TSN", + "TT", + "TTD", + "TTEC", + "TTEK", + "TTGT", + "TTMI", + "TTWO", + "TVRD", + "TVTX", + "TW", + "TWST", + "TXG", + "TXMD", + "TXN", + "TXRH", + "TXT", + "TYL", + "UAL", + "UBER", + "UBSI", + "UCTT", + "UDR", + "UFCS", + "UFPI", + "UFPT", + "UHS", + "ULCC", + "ULH", + "ULTA", + "UMBF", + "UNH", + "UNIT", + "UNP", + "UONE", + "UPBD", + "UPLD", + "UPS", + "UPST", + "UPWK", + "URBN", + "URI", + "USB", + "UTHR", + "UVSP", + "V", + "VC", + "VCEL", + "VCTR", + "VCYT", + "VECO", + "VERI", + "VERU", + "VIAV", + "VICI", + "VICR", + "VIR", + "VISN", + "VITL", + "VLO", + "VLY", + "VMC", + "VNDA", + "VNET", + "VNOM", + "VOD", + "VRDN", + "VREX", + "VRM", + "VRNS", + "VRRM", + "VRSK", + "VRSN", + "VRT", + "VRTX", + "VSAT", + "VST", + "VSTM", + "VTR", + "VTRS", + "VUZI", + "VYGR", + "VZ", + "WAB", + "WABC", + "WAFD", + "WASH", + "WAT", + "WB", + "WDAY", + "WDC", + "WDFC", + "WEC", + "WELL", + "WEN", + "WERN", + "WFC", + "WFRD", + "WGS", + "WHWK", + "WINA", + "WING", + "WIX", + "WKHS", + "WM", + "WMB", + "WMG", + "WMT", + "WOOF", + "WRB", + "WRLD", + "WSBC", + "WSBF", + "WSC", + "WSFS", + "WSM", + "WST", + "WTFC", + "WTW", + "WW", + "WWD", + "WY", + "WYNN", + "XAIR", + "XEL", + "XENE", + "XMTR", + "XNCR", + "XOM", + "XP", + "XPEL", + "XRAY", + "XRX", + "XYL", + "XYZ", + "YORW", + "YUM", + "Z", + "ZBH", + "ZBRA", + "ZD", + "ZG", + "ZION", + "ZLAB", + "ZM", + "ZNTL", + "ZS", + "ZTS", + "ZUMZ", + "ZVRA", + "ZVZZT", + "ZYME" + ], + "n_symbols": 1500 + }, + { + "week": [ + 2022, + 37 + ], + "stats": { + "raw_pool": 2853, + "eligible_pre_mask": 2318, + "post_mask": 1500, + "mask_binds": true + }, + "symbols": [ + "A", + "AAL", + "AAON", + "AAPL", + "ABBV", + "ABCL", + "ABNB", + "ABT", + "ACAD", + "ACB", + "ACET", + "ACGL", + "ACHC", + "ACIW", + "ACLS", + "ACMR", + "ACN", + "ACRS", + "ACT", + "ADAM", + "ADBE", + "ADEA", + "ADI", + "ADM", + "ADP", + "ADPT", + "ADSK", + "ADTN", + "ADUS", + "ADV", + "AEE", + "AEHR", + "AEIS", + "AEP", + "AES", + "AEVA", + "AFCG", + "AFL", + "AFRM", + "AFYA", + "AGEN", + "AGIO", + "AGNC", + "AGNT", + "AGYS", + "AHCO", + "AIG", + "AIZ", + "AJG", + "AKAM", + "ALB", + "ALCO", + "ALDX", + "ALEC", + "ALGM", + "ALGN", + "ALGT", + "ALHC", + "ALKS", + "ALKT", + "ALL", + "ALLE", + "ALLO", + "ALNY", + "ALRM", + "ALT", + "ALXO", + "AMAL", + "AMAT", + "AMBA", + "AMCR", + "AMCX", + "AMD", + "AME", + "AMGN", + "AMKR", + "AMP", + "AMPH", + "AMRN", + "AMSC", + "AMSF", + "AMT", + "AMTX", + "AMZN", + "ANAB", + "ANDE", + "ANET", + "ANGI", + "ANGO", + "ANIK", + "ANIP", + "AON", + "AOS", + "AOSL", + "APA", + "APD", + "APH", + "APO", + "APOG", + "APP", + "APPF", + "APPN", + "APPS", + "APTV", + "APYX", + "ARCB", + "ARCC", + "ARCT", + "ARE", + "ARES", + "ARGX", + "ARKO", + "ARLP", + "ARQT", + "ARRY", + "ARTNA", + "ARVN", + "ARWR", + "ASLE", + "ASML", + "ASND", + "ASO", + "ASTE", + "ASTH", + "ASTL", + "ASTS", + "ATEC", + "ATER", + "ATEX", + "ATLC", + "ATNI", + "ATO", + "ATOM", + "ATRA", + "ATRC", + "AUDC", + "AUPH", + "AVAV", + "AVB", + "AVGO", + "AVIR", + "AVNW", + "AVO", + "AVT", + "AVXL", + "AVY", + "AWK", + "AXGN", + "AXON", + "AXP", + "AXSM", + "AXTI", + "AZO", + "AZTA", + "BA", + "BAC", + "BALL", + "BAND", + "BANF", + "BANR", + "BATRA", + "BATRK", + "BAX", + "BBIO", + "BBSI", + "BBY", + "BCAB", + "BCBP", + "BCML", + "BCPC", + "BCRX", + "BCTX", + "BCYC", + "BDX", + "BEAM", + "BEEM", + "BELFB", + "BEN", + "BF-B", + "BG", + "BHF", + "BHFAN", + "BIDU", + "BIIB", + "BILI", + "BIOX", + "BJRI", + "BK", + "BKNG", + "BKR", + "BL", + "BLDP", + "BLDR", + "BLFS", + "BLK", + "BLKB", + "BLMN", + "BLNK", + "BMBL", + "BMEA", + "BMRC", + "BMRN", + "BMY", + "BNGO", + "BNR", + "BNTX", + "BOKF", + "BOOM", + "BPOP", + "BR", + "BRK-B", + "BRKR", + "BRO", + "BSET", + "BSX", + "BSY", + "BTAI", + "BUSE", + "BVS", + "BWIN", + "BX", + "BXP", + "BYND", + "BZ", + "BZUN", + "C", + "CAC", + "CACC", + "CAG", + "CAH", + "CAKE", + "CALM", + "CAMT", + "CAR", + "CARG", + "CARR", + "CASH", + "CASS", + "CASY", + "CAT", + "CATY", + "CB", + "CBOE", + "CBRE", + "CBRL", + "CBSH", + "CCB", + "CCC", + "CCCC", + "CCD", + "CCEC", + "CCEP", + "CCI", + "CCL", + "CCOI", + "CCRN", + "CCXI", + "CDLX", + "CDNA", + "CDNS", + "CDW", + "CDXS", + "CECO", + "CELH", + "CELU", + "CENN", + "CENT", + "CENTA", + "CENX", + "CERT", + "CEVA", + "CF", + "CFFN", + "CFG", + "CG", + "CGBD", + "CGC", + "CGEM", + "CGNT", + "CGNX", + "CHCO", + "CHD", + "CHDN", + "CHEF", + "CHI", + "CHKP", + "CHRD", + "CHRS", + "CHRW", + "CHTR", + "CHW", + "CHY", + "CI", + "CIEN", + "CIGI", + "CINF", + "CL", + "CLAR", + "CLBK", + "CLDX", + "CLFD", + "CLMT", + "CLNE", + "CLPT", + "CLX", + "CMCO", + "CMCSA", + "CME", + "CMG", + "CMI", + "CMPR", + "CMPS", + "CMRC", + "CMS", + "CMTL", + "CNC", + "CNOB", + "CNP", + "CNXC", + "CNXN", + "CODX", + "COF", + "COGT", + "COHR", + "COHU", + "COIN", + "COKE", + "COLB", + "COLL", + "COLM", + "COO", + "COP", + "COR", + "CORT", + "COST", + "CPAY", + "CPB", + "CPRT", + "CPSS", + "CPT", + "CRAI", + "CRBU", + "CRCT", + "CRH", + "CRIS", + "CRL", + "CRM", + "CRMT", + "CRNC", + "CRNX", + "CROX", + "CRSP", + "CRSR", + "CRTO", + "CRUS", + "CRVL", + "CRWD", + "CSCO", + "CSGP", + "CSIQ", + "CSQ", + "CSTL", + "CSWC", + "CSX", + "CTAS", + "CTBI", + "CTKB", + "CTRA", + "CTRN", + "CTSH", + "CTVA", + "CVBF", + "CVCO", + "CVLT", + "CVNA", + "CVS", + "CVX", + "CWCO", + "CWST", + "CYRX", + "CYTK", + "CZR", + "D", + "DAL", + "DASH", + "DAVE", + "DBX", + "DCBO", + "DCGO", + "DD", + "DDOG", + "DE", + "DECK", + "DELL", + "DFTX", + "DG", + "DGICA", + "DGII", + "DGX", + "DH", + "DHI", + "DHR", + "DIOD", + "DIS", + "DJT", + "DKNG", + "DLO", + "DLR", + "DLTR", + "DMLP", + "DMRC", + "DNLI", + "DNUT", + "DOC", + "DOCU", + "DOMO", + "DOO", + "DORM", + "DOV", + "DOW", + "DOX", + "DOYU", + "DPZ", + "DRH", + "DRI", + "DRS", + "DRVN", + "DSGN", + "DSGR", + "DSGX", + "DTE", + "DTIL", + "DUK", + "DUOL", + "DVA", + "DVN", + "DXCM", + "DXLG", + "DXPE", + "DYN", + "EA", + "EBAY", + "EBC", + "ECHO", + "ECL", + "ECPG", + "ED", + "EDIT", + "EEFT", + "EFSC", + "EFX", + "EG", + "EGBN", + "EH", + "EHTH", + "EIX", + "EL", + "ELV", + "EME", + "EMR", + "ENPH", + "ENSG", + "ENTA", + "ENTG", + "ENVX", + "EOG", + "EOLS", + "EPAM", + "EQIX", + "EQR", + "EQT", + "ERAS", + "ERIC", + "ERIE", + "ERII", + "ES", + "ESLT", + "ESS", + "ESTA", + "ETN", + "ETR", + "EVCM", + "EVER", + "EVGO", + "EVRG", + "EW", + "EWBC", + "EWTX", + "EXC", + "EXE", + "EXEL", + "EXLS", + "EXPD", + "EXPE", + "EXPO", + "EXR", + "EXTR", + "EYE", + "EYPT", + "EZPW", + "F", + "FA", + "FANG", + "FAST", + "FATE", + "FBNC", + "FCEL", + "FCFS", + "FCNCA", + "FCX", + "FDMT", + "FDS", + "FDUS", + "FDX", + "FE", + "FELE", + "FFAI", + "FFBC", + "FFIN", + "FFIV", + "FHB", + "FIBK", + "FICO", + "FIS", + "FISV", + "FITB", + "FIVE", + "FIVN", + "FIX", + "FIZZ", + "FLEX", + "FLGT", + "FLNA", + "FLWS", + "FLYW", + "FMAO", + "FMNB", + "FNKO", + "FORM", + "FORR", + "FOX", + "FOXA", + "FOXF", + "FRHC", + "FRME", + "FROG", + "FRPT", + "FRT", + "FSLR", + "FSLY", + "FSV", + "FTAI", + "FTCI", + "FTDR", + "FTNT", + "FTV", + "FULC", + "FULT", + "FUTU", + "FWONA", + "FWONK", + "FWRD", + "GABC", + "GAIN", + "GBDC", + "GCMG", + "GD", + "GDDY", + "GDRX", + "GDS", + "GDYN", + "GE", + "GEN", + "GGAL", + "GGR", + "GH", + "GIII", + "GILD", + "GIS", + "GL", + "GLAD", + "GLBE", + "GLNG", + "GLPI", + "GLUE", + "GLW", + "GM", + "GMAB", + "GNRC", + "GNTX", + "GO", + "GOGO", + "GOOD", + "GOOG", + "GOOGL", + "GOSS", + "GOVX", + "GPC", + "GPN", + "GPRE", + "GPRO", + "GRFS", + "GRMN", + "GRPN", + "GS", + "GSAT", + "GSBC", + "GSHD", + "GSM", + "GT", + "GTM", + "GTX", + "GWW", + "HAFC", + "HAIN", + "HAL", + "HALO", + "HAPN", + "HAS", + "HBAN", + "HBNC", + "HCA", + "HCAT", + "HCKT", + "HCM", + "HCSG", + "HD", + "HDSN", + "HELE", + "HFWA", + "HIG", + "HII", + "HIMX", + "HLIT", + "HLMN", + "HLNE", + "HLT", + "HNRG", + "HOFT", + "HON", + "HOOD", + "HOPE", + "HPE", + "HPK", + "HPQ", + "HQY", + "HRL", + "HRMY", + "HRZN", + "HSIC", + "HST", + "HSTM", + "HSY", + "HTHT", + "HTLD", + "HTO", + "HUBB", + "HUBG", + "HUM", + "HURN", + "HUT", + "HWC", + "HWKN", + "HWM", + "HYFM", + "HYMC", + "IART", + "IBCP", + "IBKR", + "IBM", + "IBOC", + "IBRX", + "ICE", + "ICFI", + "ICHR", + "ICLR", + "ICUI", + "IDCC", + "IDXX", + "IDYA", + "IEP", + "IEX", + "IFF", + "IHRT", + "III", + "IIIV", + "ILMN", + "ILPT", + "IMCR", + "IMKTA", + "IMMR", + "IMTX", + "IMXI", + "INCY", + "INDB", + "INDI", + "INGN", + "INMD", + "INO", + "INSE", + "INSG", + "INSM", + "INTC", + "INTU", + "INVA", + "INVH", + "INVZ", + "IONS", + "IOSP", + "IOVA", + "IP", + "IPAR", + "IPGP", + "IQV", + "IR", + "IRDM", + "IRM", + "IRTC", + "IRWD", + "ISRG", + "IT", + "ITRI", + "ITW", + "IVZ", + "J", + "JACK", + "JAKK", + "JAZZ", + "JBHT", + "JBIO", + "JBL", + "JBLU", + "JBSS", + "JCI", + "JD", + "JJSF", + "JKHY", + "JNJ", + "JOUT", + "JOYY", + "JPM", + "JRVR", + "JYNT", + "KALU", + "KDP", + "KE", + "KELYA", + "KEY", + "KEYS", + "KHC", + "KIDS", + "KIM", + "KKR", + "KLAC", + "KLIC", + "KLRS", + "KMB", + "KMI", + "KNSA", + "KO", + "KOD", + "KPTI", + "KR", + "KRNT", + "KRNY", + "KROS", + "KRUS", + "KRYS", + "KTOS", + "KURA", + "KYMR", + "KYNB", + "L", + "LAMR", + "LAND", + "LASR", + "LAUR", + "LBRDA", + "LBRDK", + "LBTYA", + "LBTYK", + "LCID", + "LDOS", + "LE", + "LECO", + "LEGN", + "LEN", + "LENZ", + "LESL", + "LFST", + "LFUS", + "LGIH", + "LGND", + "LH", + "LHX", + "LI", + "LIDR", + "LII", + "LILA", + "LILAK", + "LIN", + "LIND", + "LITE", + "LIVN", + "LKFN", + "LKFT", + "LKQ", + "LLY", + "LMAT", + "LMT", + "LNT", + "LNTH", + "LOCO", + "LOGI", + "LONA", + "LOPE", + "LOVE", + "LOW", + "LPLA", + "LPRO", + "LPSN", + "LQDA", + "LQDT", + "LRCX", + "LSCC", + "LSTR", + "LULU", + "LUNG", + "LUV", + "LVS", + "LWLG", + "LYB", + "LYEL", + "LYFT", + "LYV", + "LZ", + "MA", + "MAA", + "MANH", + "MAR", + "MARA", + "MAS", + "MASS", + "MAT", + "MATW", + "MBIN", + "MBUU", + "MCD", + "MCFT", + "MCHB", + "MCHP", + "MCK", + "MCO", + "MCRB", + "MCRI", + "MDB", + "MDGL", + "MDLZ", + "MDT", + "MEDP", + "MELI", + "MEOH", + "MERC", + "MET", + "META", + "METC", + "MFIC", + "MGEE", + "MGM", + "MGNI", + "MGPI", + "MGRC", + "MIDD", + "MIRM", + "MITK", + "MKC", + "MKSI", + "MKTX", + "MLAB", + "MLCO", + "MLKN", + "MLM", + "MMM", + "MMSI", + "MMYT", + "MNDY", + "MNRO", + "MNST", + "MNTK", + "MNTS", + "MO", + "MORN", + "MOS", + "MPC", + "MPWR", + "MQ", + "MRCY", + "MRK", + "MRNA", + "MRSH", + "MRTN", + "MRVI", + "MRVL", + "MS", + "MSBI", + "MSCI", + "MSEX", + "MSFT", + "MSI", + "MSTR", + "MTB", + "MTCH", + "MTD", + "MTLS", + "MTSI", + "MU", + "MXCT", + "MXL", + "MYGN", + "MYRG", + "MZTI", + "NAVI", + "NBIX", + "NBP", + "NBTB", + "NCLH", + "NCNO", + "NDAQ", + "NDSN", + "NEE", + "NEGG", + "NEM", + "NEO", + "NEOG", + "NESR", + "NEWT", + "NEXT", + "NFBK", + "NFE", + "NFLX", + "NI", + "NICE", + "NKE", + "NKTR", + "NKTX", + "NMFC", + "NMIH", + "NMRK", + "NNOX", + "NOC", + "NOVT", + "NOW", + "NRC", + "NRG", + "NRIX", + "NSC", + "NSIT", + "NSLR", + "NSSC", + "NTAP", + "NTCT", + "NTES", + "NTGR", + "NTLA", + "NTNX", + "NTRA", + "NTRS", + "NUE", + "NVAX", + "NVCR", + "NVDA", + "NVMI", + "NVR", + "NVTS", + "NWBI", + "NWE", + "NWL", + "NWS", + "NWSA", + "NXPI", + "NXST", + "O", + "OCFC", + "OCSL", + "OCUL", + "ODFL", + "OFIX", + "OFLX", + "OKE", + "OKTA", + "OLED", + "OLLI", + "OM", + "OMAB", + "OMC", + "OMCL", + "ON", + "ONB", + "ONC", + "ONEW", + "OPCH", + "OPI", + "OPRT", + "OPRX", + "ORCL", + "ORLY", + "ORMP", + "OSBC", + "OSIS", + "OSPN", + "OSW", + "OTEX", + "OTIS", + "OTLY", + "OTTR", + "OUST", + "OXLC", + "OXY", + "OZK", + "PAA", + "PACB", + "PAGP", + "PAHC", + "PANW", + "PATK", + "PAX", + "PAYO", + "PAYX", + "PCAR", + "PCG", + "PCRX", + "PCT", + "PCTY", + "PCVX", + "PDD", + "PDFS", + "PEBO", + "PECO", + "PEG", + "PEGA", + "PENG", + "PENN", + "PEP", + "PERI", + "PETS", + "PFBC", + "PFE", + "PFG", + "PG", + "PGC", + "PGNY", + "PGR", + "PGY", + "PH", + "PHAT", + "PHM", + "PHUN", + "PI", + "PKG", + "PLAB", + "PLAY", + "PLCE", + "PLD", + "PLMR", + "PLRX", + "PLTK", + "PLTR", + "PLUG", + "PLUS", + "PLXS", + "PM", + "PMVP", + "PNC", + "PNR", + "PNTG", + "PNW", + "PODD", + "POOL", + "POWI", + "PPC", + "PPG", + "PPL", + "PPLI", + "PRAA", + "PRAX", + "PRCT", + "PRDO", + "PRGS", + "PRTA", + "PRTS", + "PRU", + "PRVA", + "PSA", + "PSEC", + "PSMT", + "PSX", + "PTC", + "PTCT", + "PTEN", + "PTGX", + "PTON", + "PUBM", + "PWP", + "PWR", + "PYPL", + "PZZA", + "QCOM", + "QCRH", + "QDEL", + "QFIN", + "QLYS", + "QNRX", + "QNST", + "QQQX", + "QRVO", + "QS", + "QTRX", + "QURE", + "RARE", + "RBCAA", + "RCKT", + "RCL", + "RCMT", + "RDNT", + "RDNW", + "RDWR", + "REG", + "REGN", + "RELL", + "REPL", + "REYN", + "RF", + "RGEN", + "RGLD", + "RGNX", + "RGP", + "RICK", + "RIGL", + "RILY", + "RIOT", + "RJF", + "RKLB", + "RL", + "RLAY", + "RLMD", + "RMBS", + "RMD", + "RMR", + "RNA", + "RNAC", + "RNW", + "ROAD", + "ROCK", + "ROK", + "ROKU", + "ROL", + "ROOT", + "ROP", + "ROST", + "RPAY", + "RPD", + "RPRX", + "RRGB", + "RRR", + "RSG", + "RTX", + "RUM", + "RUN", + "RUSHA", + "RUSHB", + "RVMD", + "RVTY", + "RXRX", + "RXT", + "RYAAY", + "RYTM", + "SABR", + "SAFT", + "SAIA", + "SAIC", + "SAIL", + "SANA", + "SANM", + "SATS", + "SBAC", + "SBCF", + "SBGI", + "SBLK", + "SBRA", + "SBUX", + "SCHL", + "SCHW", + "SCSC", + "SDGR", + "SEAT", + "SEDG", + "SEER", + "SEIC", + "SENEA", + "SENS", + "SFM", + "SFNC", + "SGHT", + "SGML", + "SGRY", + "SHC", + "SHEN", + "SHIP", + "SHLS", + "SHOE", + "SHOO", + "SHOP", + "SHW", + "SIBN", + "SIGA", + "SIGI", + "SIMO", + "SIRI", + "SITM", + "SJM", + "SKIN", + "SKYT", + "SKYW", + "SLAB", + "SLB", + "SLDP", + "SLM", + "SLP", + "SLRC", + "SMCI", + "SMPL", + "SMTC", + "SNA", + "SNDX", + "SNEX", + "SNPS", + "SNY", + "SO", + "SOFI", + "SOHU", + "SONO", + "SPG", + "SPGI", + "SPSC", + "SPT", + "SPWH", + "SRAD", + "SRCE", + "SRE", + "SRPT", + "SRRK", + "SRTS", + "SSNC", + "SSP", + "SSRM", + "SSTI", + "SSYS", + "STAA", + "STBA", + "STE", + "STEP", + "STGW", + "STLD", + "STNE", + "STOK", + "STRA", + "STRL", + "STRO", + "STT", + "STX", + "STZ", + "SUPN", + "SVC", + "SW", + "SWBI", + "SWK", + "SWKS", + "SYBT", + "SYF", + "SYK", + "SYM", + "SYNA", + "SYY", + "T", + "TAP", + "TARS", + "TASK", + "TBBK", + "TBCH", + "TBLD", + "TBPH", + "TCBI", + "TCBK", + "TCMD", + "TCOM", + "TCPC", + "TCRT", + "TCX", + "TDG", + "TDY", + "TEAM", + "TECH", + "TEL", + "TENB", + "TER", + "TFC", + "TFSL", + "TGT", + "TGTX", + "TH", + "THFF", + "THRM", + "THRY", + "TIGO", + "TIL", + "TILE", + "TITN", + "TJX", + "TKO", + "TLRY", + "TLS", + "TMCI", + "TMDX", + "TMO", + "TMUS", + "TNDM", + "TNXP", + "TOWN", + "TPL", + "TPR", + "TREE", + "TRGP", + "TRI", + "TRIN", + "TRIP", + "TRMB", + "TRMD", + "TRMK", + "TRNS", + "TROW", + "TRS", + "TRST", + "TRUP", + "TRV", + "TSCO", + "TSEM", + "TSLA", + "TSN", + "TT", + "TTD", + "TTEC", + "TTEK", + "TTGT", + "TTMI", + "TTWO", + "TVRD", + "TVTX", + "TW", + "TWST", + "TXG", + "TXMD", + "TXN", + "TXRH", + "TXT", + "TYL", + "UAL", + "UBER", + "UBSI", + "UCTT", + "UDR", + "UEIC", + "UFCS", + "UFPI", + "UFPT", + "UHS", + "ULCC", + "ULH", + "ULTA", + "UMBF", + "UNH", + "UNIT", + "UNP", + "UPBD", + "UPLD", + "UPS", + "UPST", + "UPWK", + "URBN", + "URI", + "USB", + "UTHR", + "UVSP", + "V", + "VC", + "VCEL", + "VCTR", + "VCYT", + "VECO", + "VERA", + "VERI", + "VERU", + "VERX", + "VIAV", + "VICI", + "VICR", + "VIR", + "VISN", + "VITL", + "VLO", + "VLY", + "VMC", + "VNDA", + "VNET", + "VNOM", + "VOD", + "VRDN", + "VREX", + "VRM", + "VRNS", + "VRRM", + "VRSK", + "VRSN", + "VRT", + "VRTX", + "VSAT", + "VST", + "VTGN", + "VTR", + "VTRS", + "VUZI", + "VYGR", + "VZ", + "WAB", + "WABC", + "WAFD", + "WASH", + "WAT", + "WB", + "WDAY", + "WDC", + "WDFC", + "WEC", + "WELL", + "WEN", + "WERN", + "WFC", + "WFRD", + "WGS", + "WHWK", + "WINA", + "WING", + "WIX", + "WKHS", + "WM", + "WMB", + "WMG", + "WMT", + "WOOF", + "WRB", + "WRLD", + "WSBC", + "WSBF", + "WSC", + "WSFS", + "WSM", + "WST", + "WTFC", + "WTW", + "WW", + "WWD", + "WY", + "WYNN", + "XAIR", + "XEL", + "XENE", + "XMTR", + "XNCR", + "XOM", + "XP", + "XPEL", + "XRAY", + "XRX", + "XYL", + "XYZ", + "YORW", + "YUM", + "Z", + "ZBH", + "ZBRA", + "ZD", + "ZG", + "ZION", + "ZLAB", + "ZM", + "ZNTL", + "ZS", + "ZTS", + "ZUMZ", + "ZVZZT", + "ZYME" + ], + "n_symbols": 1500 + }, + { + "week": [ + 2022, + 43 + ], + "stats": { + "raw_pool": 2905, + "eligible_pre_mask": 2307, + "post_mask": 1500, + "mask_binds": true + }, + "symbols": [ + "A", + "AAL", + "AAON", + "AAPL", + "ABBV", + "ABCL", + "ABNB", + "ABT", + "ACAD", + "ACB", + "ACET", + "ACGL", + "ACGLN", + "ACHC", + "ACIW", + "ACLS", + "ACMR", + "ACN", + "ACRS", + "ACT", + "ADAM", + "ADBE", + "ADEA", + "ADI", + "ADM", + "ADP", + "ADPT", + "ADSK", + "ADTN", + "ADUS", + "ADV", + "AEE", + "AEHR", + "AEIS", + "AEP", + "AES", + "AEVA", + "AFCG", + "AFL", + "AFRM", + "AFYA", + "AGEN", + "AGIO", + "AGNC", + "AGNCN", + "AGNCO", + "AGNCP", + "AGNT", + "AGYS", + "AHCO", + "AIG", + "AIZ", + "AJG", + "AKAM", + "ALB", + "ALCO", + "ALDX", + "ALEC", + "ALGM", + "ALGN", + "ALGT", + "ALHC", + "ALKS", + "ALKT", + "ALL", + "ALLE", + "ALLO", + "ALNT", + "ALNY", + "ALRM", + "ALT", + "ALXO", + "AMAL", + "AMAT", + "AMBA", + "AMCR", + "AMCX", + "AMD", + "AME", + "AMGN", + "AMKR", + "AMP", + "AMPH", + "AMPL", + "AMRN", + "AMSF", + "AMT", + "AMTX", + "AMZN", + "ANAB", + "ANDE", + "ANET", + "ANGI", + "ANGO", + "ANIK", + "ANIP", + "AON", + "AOS", + "AOSL", + "APA", + "APD", + "APEI", + "APH", + "APO", + "APOG", + "APP", + "APPF", + "APPN", + "APPS", + "APTV", + "ARCB", + "ARCC", + "ARCT", + "ARE", + "ARES", + "ARGX", + "ARKO", + "ARLP", + "ARQT", + "ARRY", + "ARTNA", + "ARVN", + "ARWR", + "ASLE", + "ASML", + "ASND", + "ASO", + "ASTE", + "ASTH", + "ASTL", + "ASTS", + "ATEC", + "ATER", + "ATEX", + "ATNI", + "ATO", + "ATOM", + "ATRA", + "ATRC", + "AUDC", + "AUPH", + "AVAV", + "AVB", + "AVGO", + "AVIR", + "AVNW", + "AVO", + "AVT", + "AVXL", + "AVY", + "AWK", + "AXGN", + "AXON", + "AXP", + "AXSM", + "AZO", + "AZTA", + "BA", + "BAC", + "BALL", + "BAND", + "BANF", + "BANR", + "BATRA", + "BATRK", + "BAX", + "BBIO", + "BBSI", + "BBY", + "BCAB", + "BCPC", + "BCRX", + "BCTX", + "BCYC", + "BDX", + "BEAM", + "BEEM", + "BELFB", + "BEN", + "BF-B", + "BFC", + "BG", + "BHF", + "BIDU", + "BIIB", + "BILI", + "BIOX", + "BJRI", + "BK", + "BKNG", + "BKR", + "BL", + "BLDP", + "BLDR", + "BLFS", + "BLK", + "BLKB", + "BLMN", + "BLNK", + "BMBL", + "BMRC", + "BMRN", + "BMY", + "BNGO", + "BNTX", + "BOKF", + "BOOM", + "BPOP", + "BR", + "BRK-B", + "BRKR", + "BRO", + "BSET", + "BSX", + "BSY", + "BTAI", + "BUSE", + "BWIN", + "BX", + "BXP", + "BYND", + "BZ", + "C", + "CAC", + "CACC", + "CAG", + "CAH", + "CAKE", + "CALM", + "CAMT", + "CAR", + "CARE", + "CARG", + "CARR", + "CASH", + "CASS", + "CASY", + "CAT", + "CATY", + "CB", + "CBOE", + "CBRE", + "CBRL", + "CBSH", + "CCAP", + "CCB", + "CCBG", + "CCC", + "CCCC", + "CCD", + "CCEC", + "CCEP", + "CCI", + "CCL", + "CCNE", + "CCOI", + "CCRN", + "CCSI", + "CDLX", + "CDNA", + "CDNS", + "CDW", + "CDXS", + "CECO", + "CELH", + "CELU", + "CENN", + "CENT", + "CENTA", + "CENX", + "CERT", + "CEVA", + "CF", + "CFFN", + "CFG", + "CG", + "CGBD", + "CGC", + "CGEM", + "CGNX", + "CHCO", + "CHD", + "CHDN", + "CHEF", + "CHI", + "CHKP", + "CHRD", + "CHRS", + "CHRW", + "CHTR", + "CHW", + "CHY", + "CI", + "CIEN", + "CIGI", + "CINF", + "CL", + "CLAR", + "CLBK", + "CLDX", + "CLFD", + "CLMT", + "CLNE", + "CLPT", + "CLX", + "CMCO", + "CMCSA", + "CME", + "CMG", + "CMI", + "CMPR", + "CMPS", + "CMRC", + "CMS", + "CMTL", + "CNC", + "CNOB", + "CNP", + "CNXC", + "CNXN", + "COCO", + "CODX", + "COF", + "COGT", + "COHR", + "COHU", + "COIN", + "COKE", + "COLB", + "COLL", + "COLM", + "COO", + "COP", + "COR", + "CORT", + "COST", + "CPAY", + "CPB", + "CPRT", + "CPT", + "CRAI", + "CRBU", + "CRCT", + "CRH", + "CRL", + "CRM", + "CRMT", + "CRNC", + "CRNX", + "CROX", + "CRSP", + "CRSR", + "CRTO", + "CRUS", + "CRVL", + "CRWD", + "CSCO", + "CSGP", + "CSIQ", + "CSQ", + "CSTL", + "CSWC", + "CSX", + "CTAS", + "CTBI", + "CTKB", + "CTRA", + "CTRN", + "CTSH", + "CTVA", + "CVBF", + "CVCO", + "CVLT", + "CVS", + "CVX", + "CWCO", + "CWST", + "CYRX", + "CYTK", + "CZR", + "D", + "DAL", + "DASH", + "DBX", + "DCBO", + "DCGO", + "DD", + "DDOG", + "DE", + "DECK", + "DELL", + "DG", + "DGICA", + "DGII", + "DGX", + "DH", + "DHI", + "DHR", + "DIOD", + "DIS", + "DJT", + "DKNG", + "DLO", + "DLR", + "DLTR", + "DMLP", + "DMRC", + "DNLI", + "DNUT", + "DOC", + "DOCU", + "DOMO", + "DOO", + "DORM", + "DOV", + "DOW", + "DOX", + "DPZ", + "DRH", + "DRI", + "DRS", + "DRVN", + "DSGN", + "DSGR", + "DSGX", + "DTE", + "DTIL", + "DUK", + "DUOL", + "DVA", + "DVN", + "DXCM", + "DXLG", + "DXPE", + "DYN", + "EA", + "EBAY", + "EBC", + "ECHO", + "ECL", + "ECPG", + "ED", + "EDIT", + "EEFT", + "EFSC", + "EFX", + "EG", + "EGBN", + "EIX", + "EL", + "ELV", + "EME", + "EMR", + "ENPH", + "ENSG", + "ENTA", + "ENTG", + "ENVX", + "EOG", + "EOLS", + "EPAM", + "EQIX", + "EQR", + "EQT", + "ERAS", + "ERIC", + "ERIE", + "ERII", + "ES", + "ESEA", + "ESLT", + "ESS", + "ESTA", + "ETN", + "ETR", + "EVCM", + "EVER", + "EVGO", + "EVRG", + "EW", + "EWBC", + "EWTX", + "EXC", + "EXE", + "EXEL", + "EXLS", + "EXPD", + "EXPE", + "EXPO", + "EXR", + "EXTR", + "EYE", + "EZPW", + "F", + "FA", + "FANG", + "FAST", + "FATE", + "FBNC", + "FCEL", + "FCFS", + "FCNCA", + "FCX", + "FDMT", + "FDS", + "FDUS", + "FDX", + "FE", + "FELE", + "FFAI", + "FFBC", + "FFIN", + "FFIV", + "FHB", + "FIBK", + "FICO", + "FIS", + "FISV", + "FITB", + "FIVE", + "FIVN", + "FIX", + "FIZZ", + "FLEX", + "FLGT", + "FLNA", + "FLNC", + "FLWS", + "FLYW", + "FMBH", + "FMNB", + "FNKO", + "FORM", + "FORR", + "FOX", + "FOXA", + "FOXF", + "FRHC", + "FRME", + "FROG", + "FRPT", + "FRSH", + "FRT", + "FSLR", + "FSLY", + "FSV", + "FTAI", + "FTCI", + "FTDR", + "FTNT", + "FTV", + "FULC", + "FULT", + "FUTU", + "FWONA", + "FWONK", + "FWRD", + "FWRG", + "GABC", + "GAIN", + "GBDC", + "GCMG", + "GD", + "GDDY", + "GDRX", + "GDS", + "GDYN", + "GE", + "GEN", + "GFS", + "GGAL", + "GGR", + "GH", + "GIII", + "GILD", + "GIS", + "GL", + "GLAD", + "GLBE", + "GLNG", + "GLPI", + "GLUE", + "GLW", + "GM", + "GMAB", + "GNRC", + "GNTX", + "GO", + "GOGO", + "GOOD", + "GOOG", + "GOOGL", + "GOSS", + "GOVX", + "GPC", + "GPN", + "GPRE", + "GPRO", + "GRFS", + "GRMN", + "GRPN", + "GS", + "GSAT", + "GSBC", + "GSHD", + "GSM", + "GT", + "GTLB", + "GTM", + "GTX", + "GWW", + "HAFC", + "HAIN", + "HAL", + "HALO", + "HAPN", + "HAS", + "HBAN", + "HBANP", + "HBNC", + "HCA", + "HCAT", + "HCKT", + "HCM", + "HCSG", + "HD", + "HDSN", + "HELE", + "HFWA", + "HIG", + "HII", + "HIMX", + "HLIT", + "HLMN", + "HLNE", + "HLT", + "HNRG", + "HOFT", + "HOLO", + "HON", + "HOOD", + "HOPE", + "HPE", + "HPK", + "HPQ", + "HQY", + "HRL", + "HRMY", + "HROW", + "HRZN", + "HSIC", + "HST", + "HSTM", + "HSY", + "HTHT", + "HTLD", + "HTO", + "HUBB", + "HUBG", + "HUM", + "HURN", + "HUT", + "HWC", + "HWKN", + "HWM", + "HYFM", + "HYMC", + "IART", + "IBCP", + "IBKR", + "IBM", + "IBOC", + "IBRX", + "ICE", + "ICFI", + "ICHR", + "ICLR", + "ICUI", + "IDCC", + "IDXX", + "IDYA", + "IEP", + "IEX", + "IFF", + "IHRT", + "IIIV", + "ILMN", + "IMCR", + "IMKTA", + "IMMR", + "IMTX", + "IMVT", + "IMXI", + "INCY", + "INDB", + "INDI", + "INGN", + "INMD", + "INO", + "INSE", + "INSG", + "INSM", + "INTA", + "INTC", + "INTU", + "INVA", + "INVH", + "IONS", + "IOSP", + "IOVA", + "IP", + "IPAR", + "IPGP", + "IQV", + "IR", + "IRDM", + "IRM", + "IRTC", + "IRWD", + "ISRG", + "IT", + "ITRI", + "ITW", + "IVZ", + "J", + "JACK", + "JAKK", + "JAZZ", + "JBHT", + "JBIO", + "JBL", + "JBLU", + "JBSS", + "JCI", + "JD", + "JJSF", + "JKHY", + "JNJ", + "JOUT", + "JOYY", + "JPM", + "JRVR", + "JYNT", + "KALU", + "KDP", + "KE", + "KELYA", + "KEY", + "KEYS", + "KHC", + "KIDS", + "KIM", + "KKR", + "KLAC", + "KLIC", + "KLRS", + "KLXE", + "KMB", + "KMI", + "KNSA", + "KO", + "KOD", + "KPRX", + "KPTI", + "KR", + "KRNT", + "KRNY", + "KROS", + "KRUS", + "KRYS", + "KTOS", + "KURA", + "KYMR", + "KYNB", + "L", + "LAMR", + "LAND", + "LASR", + "LAUR", + "LBRDA", + "LBRDK", + "LBTYA", + "LBTYK", + "LCID", + "LDOS", + "LE", + "LECO", + "LEGN", + "LEN", + "LESL", + "LFST", + "LFUS", + "LGIH", + "LGND", + "LH", + "LHX", + "LI", + "LII", + "LILA", + "LILAK", + "LIN", + "LIND", + "LITE", + "LIVN", + "LKFN", + "LKFT", + "LKQ", + "LLY", + "LMAT", + "LMT", + "LNT", + "LNTH", + "LOCO", + "LOGI", + "LOPE", + "LOVE", + "LOW", + "LPLA", + "LPRO", + "LPSN", + "LQDA", + "LQDT", + "LRCX", + "LSCC", + "LSTR", + "LULU", + "LUNG", + "LUV", + "LVS", + "LWLG", + "LYB", + "LYEL", + "LYFT", + "LYV", + "LZ", + "MA", + "MAA", + "MANH", + "MAR", + "MARA", + "MAS", + "MASS", + "MAT", + "MATW", + "MBIN", + "MBUU", + "MBWM", + "MCD", + "MCFT", + "MCHB", + "MCHP", + "MCK", + "MCO", + "MCRB", + "MCRI", + "MDB", + "MDGL", + "MDLZ", + "MDT", + "MEDP", + "MELI", + "MEOH", + "MERC", + "MET", + "META", + "METC", + "MFIC", + "MGEE", + "MGM", + "MGNI", + "MGPI", + "MGRC", + "MIDD", + "MIRM", + "MIST", + "MITK", + "MKC", + "MKSI", + "MKTX", + "MLAB", + "MLCO", + "MLKN", + "MLM", + "MMM", + "MMSI", + "MMYT", + "MNDY", + "MNRO", + "MNST", + "MNTK", + "MO", + "MORN", + "MOS", + "MPC", + "MPWR", + "MQ", + "MRCY", + "MRK", + "MRNA", + "MRSH", + "MRTN", + "MRVI", + "MRVL", + "MS", + "MSBI", + "MSCI", + "MSEX", + "MSFT", + "MSI", + "MSTR", + "MTB", + "MTCH", + "MTD", + "MTLS", + "MTSI", + "MU", + "MXCT", + "MXL", + "MYGN", + "MYRG", + "MZTI", + "NAVI", + "NBIX", + "NBTB", + "NCLH", + "NCNO", + "NDAQ", + "NDSN", + "NEE", + "NEGG", + "NEM", + "NEO", + "NEOG", + "NESR", + "NEWT", + "NEXT", + "NFBK", + "NFE", + "NFLX", + "NI", + "NICE", + "NKE", + "NKTR", + "NKTX", + "NMFC", + "NMIH", + "NMRK", + "NNOX", + "NOC", + "NOVT", + "NOW", + "NRC", + "NRG", + "NRIX", + "NSC", + "NSIT", + "NSSC", + "NTAP", + "NTCT", + "NTES", + "NTGR", + "NTLA", + "NTNX", + "NTRA", + "NTRS", + "NUE", + "NVAX", + "NVCR", + "NVDA", + "NVMI", + "NVR", + "NWBI", + "NWE", + "NWL", + "NWPX", + "NWS", + "NWSA", + "NXPI", + "NXST", + "O", + "OCFC", + "OCSL", + "ODFL", + "OFIX", + "OFLX", + "OKE", + "OKTA", + "OLED", + "OLLI", + "OM", + "OMAB", + "OMC", + "OMCL", + "ON", + "ONB", + "ONC", + "ONEW", + "OPCH", + "OPI", + "OPRT", + "OPRX", + "ORCL", + "ORLY", + "ORMP", + "OSBC", + "OSIS", + "OSPN", + "OSW", + "OTEX", + "OTIS", + "OTLY", + "OTTR", + "OUST", + "OXLC", + "OXY", + "OZK", + "PAA", + "PACB", + "PAGP", + "PAHC", + "PAMT", + "PANW", + "PATK", + "PAX", + "PAYO", + "PAYX", + "PCAR", + "PCG", + "PCRX", + "PCT", + "PCTY", + "PCVX", + "PDD", + "PDFS", + "PEBO", + "PECO", + "PEG", + "PEGA", + "PENG", + "PENN", + "PEP", + "PERI", + "PETS", + "PFBC", + "PFE", + "PFG", + "PG", + "PGC", + "PGNY", + "PGR", + "PGY", + "PH", + "PHAT", + "PHM", + "PHUN", + "PI", + "PKG", + "PLAB", + "PLAY", + "PLCE", + "PLD", + "PLMR", + "PLPC", + "PLRX", + "PLTK", + "PLTR", + "PLUG", + "PLUS", + "PLXS", + "PM", + "PMVP", + "PNC", + "PNR", + "PNTG", + "PNW", + "PODD", + "POOL", + "POWI", + "PPC", + "PPG", + "PPL", + "PPLI", + "PRAA", + "PRAX", + "PRCT", + "PRDO", + "PRGS", + "PRTA", + "PRTS", + "PRU", + "PRVA", + "PSA", + "PSEC", + "PSMT", + "PSX", + "PTC", + "PTCT", + "PTEN", + "PTGX", + "PTLO", + "PTON", + "PUBM", + "PWP", + "PWR", + "PYPL", + "PZZA", + "QCOM", + "QCRH", + "QDEL", + "QFIN", + "QLYS", + "QNRX", + "QNST", + "QQQX", + "QRVO", + "QS", + "QTRX", + "QURE", + "RARE", + "RCKT", + "RCL", + "RCMT", + "RDNT", + "RDNW", + "RDWR", + "REG", + "REGN", + "RELL", + "RELY", + "RENT", + "REPL", + "REYN", + "RF", + "RGEN", + "RGLD", + "RGNX", + "RGP", + "RICK", + "RIGL", + "RILY", + "RIOT", + "RJF", + "RKLB", + "RL", + "RLAY", + "RLMD", + "RMBS", + "RMD", + "RMNI", + "RMR", + "RNA", + "RNAC", + "RNW", + "ROAD", + "ROCK", + "ROIV", + "ROK", + "ROKU", + "ROL", + "ROOT", + "ROP", + "ROST", + "RPAY", + "RPD", + "RPRX", + "RRGB", + "RRR", + "RSG", + "RTX", + "RUM", + "RUN", + "RUSHA", + "RVMD", + "RVTY", + "RXRX", + "RXT", + "RYAAY", + "RYTM", + "SABR", + "SAFT", + "SAIA", + "SAIC", + "SANA", + "SANM", + "SATS", + "SBAC", + "SBCF", + "SBGI", + "SBLK", + "SBRA", + "SBUX", + "SCHL", + "SCHW", + "SCSC", + "SDGR", + "SEAT", + "SEDG", + "SEER", + "SEIC", + "SENEA", + "SENS", + "SFM", + "SFNC", + "SGHT", + "SGML", + "SGRY", + "SHC", + "SHEN", + "SHLS", + "SHOE", + "SHOO", + "SHOP", + "SHW", + "SIBN", + "SIGA", + "SIGI", + "SIMO", + "SIRI", + "SITM", + "SJM", + "SKIN", + "SKYT", + "SKYW", + "SLAB", + "SLB", + "SLDP", + "SLM", + "SLP", + "SLRC", + "SMBC", + "SMCI", + "SMPL", + "SMTC", + "SNA", + "SNDX", + "SNEX", + "SNPS", + "SNY", + "SO", + "SOFI", + "SOHU", + "SONO", + "SPFI", + "SPG", + "SPGI", + "SPSC", + "SPT", + "SPWH", + "SRAD", + "SRCE", + "SRE", + "SRPT", + "SRRK", + "SRTS", + "SSNC", + "SSP", + "SSRM", + "SSTI", + "SSYS", + "STAA", + "STBA", + "STE", + "STEP", + "STGW", + "STLD", + "STNE", + "STOK", + "STRA", + "STRL", + "STRO", + "STT", + "STX", + "STZ", + "SUPN", + "SVC", + "SW", + "SWBI", + "SWK", + "SWKS", + "SYBT", + "SYF", + "SYK", + "SYM", + "SYNA", + "SYY", + "T", + "TAP", + "TARS", + "TASK", + "TBBK", + "TBCH", + "TBLD", + "TBPH", + "TCBI", + "TCBK", + "TCMD", + "TCOM", + "TCPC", + "TCRT", + "TCX", + "TDG", + "TDY", + "TEAM", + "TECH", + "TEL", + "TENB", + "TER", + "TFC", + "TFSL", + "TGT", + "TGTX", + "TH", + "THFF", + "THRM", + "THRY", + "TIGO", + "TIL", + "TILE", + "TITN", + "TJX", + "TKO", + "TLRY", + "TLS", + "TMCI", + "TMDX", + "TMO", + "TMUS", + "TNDM", + "TNXP", + "TOWN", + "TPL", + "TPR", + "TREE", + "TRGP", + "TRI", + "TRIN", + "TRIP", + "TRMB", + "TRMD", + "TRMK", + "TRNS", + "TROW", + "TRS", + "TRST", + "TRUP", + "TRV", + "TSCO", + "TSEM", + "TSLA", + "TSN", + "TT", + "TTD", + "TTEC", + "TTEK", + "TTGT", + "TTMI", + "TTWO", + "TVRD", + "TVTX", + "TW", + "TWST", + "TXG", + "TXN", + "TXRH", + "TXT", + "TYL", + "UAL", + "UBER", + "UBSI", + "UCTT", + "UDR", + "UEIC", + "UFCS", + "UFPI", + "UFPT", + "UHS", + "ULCC", + "ULH", + "ULTA", + "UMBF", + "UNH", + "UNIT", + "UNP", + "UPBD", + "UPLD", + "UPS", + "UPST", + "UPWK", + "URBN", + "URI", + "USB", + "UTHR", + "UVSP", + "V", + "VC", + "VCEL", + "VCTR", + "VCYT", + "VECO", + "VERA", + "VERI", + "VERU", + "VERX", + "VIAV", + "VICI", + "VICR", + "VIR", + "VISN", + "VITL", + "VLO", + "VLY", + "VMC", + "VNDA", + "VNOM", + "VOD", + "VRDN", + "VREX", + "VRM", + "VRNS", + "VRRM", + "VRSK", + "VRSN", + "VRT", + "VRTX", + "VSAT", + "VST", + "VTR", + "VTRS", + "VUZI", + "VZ", + "WAB", + "WABC", + "WAFD", + "WASH", + "WAT", + "WB", + "WDAY", + "WDC", + "WDFC", + "WEC", + "WELL", + "WEN", + "WERN", + "WEST", + "WFC", + "WFRD", + "WGS", + "WHWK", + "WINA", + "WING", + "WIX", + "WKHS", + "WLDN", + "WM", + "WMB", + "WMG", + "WMT", + "WOOF", + "WRB", + "WRLD", + "WSBC", + "WSBF", + "WSC", + "WSFS", + "WSM", + "WST", + "WTFC", + "WTW", + "WWD", + "WY", + "WYNN", + "XAIR", + "XEL", + "XENE", + "XMTR", + "XNCR", + "XOM", + "XP", + "XPEL", + "XRAY", + "XRX", + "XYL", + "XYZ", + "YORW", + "YUM", + "Z", + "ZBH", + "ZBRA", + "ZD", + "ZG", + "ZION", + "ZLAB", + "ZM", + "ZNTL", + "ZS", + "ZTS", + "ZUMZ", + "ZVRA", + "ZVZZT", + "ZYME" + ], + "n_symbols": 1500 + }, + { + "week": [ + 2022, + 25 + ], + "stats": { + "raw_pool": 493, + "eligible_pre_mask": 492, + "post_mask": 492, + "mask_binds": false + }, + "symbols": [ + "A", + "AAPL", + "ABBV", + "ABNB", + "ABT", + "ACGL", + "ACN", + "ADBE", + "ADI", + "ADM", + "ADP", + "ADSK", + "AEE", + "AEP", + "AES", + "AFL", + "AIG", + "AIZ", + "AJG", + "AKAM", + "ALB", + "ALGN", + "ALL", + "ALLE", + "AMAT", + "AMCR", + "AMD", + "AME", + "AMGN", + "AMP", + "AMT", + "AMZN", + "ANET", + "AON", + "AOS", + "APA", + "APD", + "APH", + "APO", + "APP", + "APTV", + "ARE", + "ARES", + "ATO", + "AVB", + "AVGO", + "AVY", + "AWK", + "AXON", + "AXP", + "AZO", + "BA", + "BAC", + "BALL", + "BAX", + "BBY", + "BDX", + "BEN", + "BF-B", + "BG", + "BIIB", + "BK", + "BKNG", + "BKR", + "BLDR", + "BLK", + "BMY", + "BR", + "BRK-B", + "BRO", + "BSX", + "BX", + "BXP", + "C", + "CAG", + "CAH", + "CARR", + "CASY", + "CAT", + "CB", + "CBOE", + "CBRE", + "CCI", + "CCL", + "CDNS", + "CDW", + "CF", + "CFG", + "CHD", + "CHRW", + "CHTR", + "CI", + "CIEN", + "CINF", + "CL", + "CLX", + "CMCSA", + "CME", + "CMG", + "CMI", + "CMS", + "CNC", + "CNP", + "COF", + "COHR", + "COIN", + "COO", + "COP", + "COR", + "COST", + "CPAY", + "CPB", + "CPRT", + "CPT", + "CRH", + "CRL", + "CRM", + "CRWD", + "CSCO", + "CSGP", + "CSX", + "CTAS", + "CTRA", + "CTSH", + "CTVA", + "CVNA", + "CVS", + "CVX", + "D", + "DAL", + "DASH", + "DD", + "DDOG", + "DE", + "DECK", + "DELL", + "DG", + "DGX", + "DHI", + "DHR", + "DIS", + "DLR", + "DLTR", + "DOC", + "DOV", + "DOW", + "DPZ", + "DRI", + "DTE", + "DUK", + "DVA", + "DVN", + "DXCM", + "EA", + "EBAY", + "ECL", + "ED", + "EFX", + "EG", + "EIX", + "EL", + "ELV", + "EME", + "EMR", + "EOG", + "EPAM", + "EQIX", + "EQR", + "EQT", + "ERIE", + "ES", + "ESS", + "ETN", + "ETR", + "EVRG", + "EW", + "EXC", + "EXE", + "EXPD", + "EXPE", + "EXR", + "F", + "FANG", + "FAST", + "FCX", + "FDS", + "FDX", + "FE", + "FFIV", + "FICO", + "FIS", + "FISV", + "FITB", + "FIX", + "FOX", + "FOXA", + "FRT", + "FSLR", + "FTNT", + "FTV", + "GD", + "GDDY", + "GE", + "GEN", + "GILD", + "GIS", + "GL", + "GLW", + "GM", + "GNRC", + "GOOG", + "GOOGL", + "GPC", + "GPN", + "GRMN", + "GS", + "GWW", + "HAL", + "HAS", + "HBAN", + "HCA", + "HD", + "HIG", + "HII", + "HLT", + "HON", + "HPE", + "HPQ", + "HRL", + "HSIC", + "HST", + "HSY", + "HUBB", + "HUM", + "HWM", + "IBKR", + "IBM", + "ICE", + "IDXX", + "IEX", + "IFF", + "INCY", + "INTC", + "INTU", + "INVH", + "IP", + "IQV", + "IR", + "IRM", + "ISRG", + "IT", + "ITW", + "IVZ", + "J", + "JBHT", + "JBL", + "JCI", + "JKHY", + "JNJ", + "JPM", + "KDP", + "KEY", + "KEYS", + "KHC", + "KIM", + "KKR", + "KLAC", + "KMB", + "KMI", + "KO", + "KR", + "L", + "LDOS", + "LEN", + "LH", + "LHX", + "LII", + "LIN", + "LITE", + "LLY", + "LMT", + "LNT", + "LOW", + "LRCX", + "LULU", + "LUV", + "LVS", + "LYB", + "LYV", + "MA", + "MAA", + "MAR", + "MAS", + "MCD", + "MCHP", + "MCK", + "MCO", + "MDLZ", + "MDT", + "MET", + "META", + "MGM", + "MKC", + "MLM", + "MMM", + "MNST", + "MO", + "MOS", + "MPC", + "MPWR", + "MRK", + "MRNA", + "MRSH", + "MS", + "MSCI", + "MSFT", + "MSI", + "MSTR", + "MTB", + "MTD", + "MU", + "NCLH", + "NDAQ", + "NDSN", + "NEE", + "NEM", + "NFLX", + "NI", + "NKE", + "NOC", + "NOW", + "NRG", + "NSC", + "NTAP", + "NTRS", + "NUE", + "NVDA", + "NVR", + "NWS", + "NWSA", + "NXPI", + "O", + "ODFL", + "OKE", + "OMC", + "ON", + "ORCL", + "ORLY", + "OTIS", + "OXY", + "PANW", + "PAYX", + "PCAR", + "PCG", + "PEG", + "PEP", + "PFE", + "PFG", + "PG", + "PGR", + "PH", + "PHM", + "PKG", + "PLD", + "PLTR", + "PM", + "PNC", + "PNR", + "PNW", + "PODD", + "POOL", + "PPG", + "PPL", + "PRU", + "PSA", + "PSX", + "PTC", + "PWR", + "PYPL", + "QCOM", + "RCL", + "REG", + "REGN", + "RF", + "RJF", + "RL", + "RMD", + "ROK", + "ROL", + "ROP", + "ROST", + "RSG", + "RTX", + "RVTY", + "SATS", + "SBAC", + "SBUX", + "SCHW", + "SHW", + "SJM", + "SLB", + "SNA", + "SNPS", + "SO", + "SPG", + "SPGI", + "SRE", + "STE", + "STLD", + "STT", + "STX", + "STZ", + "SW", + "SWK", + "SWKS", + "SYF", + "SYK", + "SYY", + "T", + "TAP", + "TDG", + "TDY", + "TECH", + "TEL", + "TER", + "TFC", + "TGT", + "TJX", + "TKO", + "TMO", + "TMUS", + "TPL", + "TPR", + "TRGP", + "TRMB", + "TROW", + "TRV", + "TSCO", + "TSLA", + "TSN", + "TT", + "TTD", + "TTWO", + "TXN", + "TXT", + "TYL", + "UAL", + "UBER", + "UDR", + "UHS", + "ULTA", + "UNH", + "UNP", + "UPS", + "URI", + "USB", + "V", + "VICI", + "VLO", + "VMC", + "VRSK", + "VRSN", + "VRT", + "VRTX", + "VST", + "VTR", + "VTRS", + "VZ", + "WAB", + "WAT", + "WDAY", + "WDC", + "WEC", + "WELL", + "WFC", + "WM", + "WMB", + "WMT", + "WRB", + "WSM", + "WST", + "WTW", + "WY", + "WYNN", + "XEL", + "XOM", + "XYL", + "XYZ", + "YUM", + "ZBH", + "ZBRA", + "ZTS" + ], + "n_symbols": 492 + } + ], + "interpretation": { + "harness_and_shared_filter_agree": true, + "mask_binds_pct": 97.1, + "avg_eligible_pre_mask": 2338.4, + "avg_raw_pool": 3214.4, + "prod_subset_still_negative": true, + "junior_tier_more_positive": true, + "lag_same_sign_as_same_week": true, + "mom_conditional_negative_and_reliable": true, + "orphan_plus_five_sigma": "Prior report fip-breadth-20260718-211440-breadth.json listed fip IC +0.0575 / t +5.12. This single-sourced recompute is the authoritative number; if it disagrees, the +0.0575 row is orphaned.", + "compositional_story": "fip_id pools continuous winners (neg IC) vs continuous bleeders (pos IC). Prod-subset and senior liquid stay negative; junior liquid is less negative / positive \u2014 composition, not jumpiness premium.", + "vol_tilt_warning": "High-vol names underperform on breadth relative to S&P-like books. Re-validate production 80/20 high-vol tilt before any universe broaden." + }, + "platform_verdict": "Mom-conditional fip ALIVE as book-tilt candidate (needs book sim) \u2014 not production wire-in. Unconditional fip not green." +} \ No newline at end of file diff --git a/scripts/run_fip_breadth_diagnostics.py b/scripts/run_fip_breadth_diagnostics.py index 1e47ca5..3625328 100644 --- a/scripts/run_fip_breadth_diagnostics.py +++ b/scripts/run_fip_breadth_diagnostics.py @@ -1,26 +1,18 @@ -"""Post-breadth diagnostics for fip_id (research branch only). +"""fip_id breadth diagnostics — single-sourced through harness mask helpers. -Same research.sqlite as the liquid-breadth IC run. No production changes. +Uses the same collection + ``_filter_liquid_breadth_week_rich`` as +``run_backtest`` signal_eval. No parallel mask implementation. -Checks (pre-registered interpretation follow-ups) ------------------------------------------------- -1. **Lagged membership** — liquid top-N ranked on *prior* week's $vol (extra lag) - so same-week liquidity explosion cannot pull a name into history. -2. **Liquidity tiers** — fip IC on ranks 1–800 vs 801–1500 (same-week mask). -3. **Prod-universe subset** — symbols present in prod.sqlite (~S&P-like large-cap - book) inside the same breadth weeks — compositional vs temporal flip. -4. **Momentum-conditional fip** — among weekly top 20% by mom_12_1 (or resid when - available) within the liquid top-N — the paper's actual claim and the only - version a gate could consume. +Reconciles the harness +0.0575 vs prior dual-path −0.017 disagreement by +deleting the second mask, dumping membership/pre-post stats, and re-running +mom-conditional IC through the surviving path only. -Also reports vol_6m / mom raw vs residual on the same panels for the log. +Research branch only. Example: -Example (Windows) ------------------ .\\.venv\\Scripts\\python.exe scripts\\run_fip_breadth_diagnostics.py ^ --research-snapshot backtest_snapshots\\research.sqlite ^ --prod-snapshot backtest_snapshots\\prod.sqlite ^ - --workers 6 + --workers 6 --allow-spawn """ from __future__ import annotations @@ -29,6 +21,7 @@ import argparse import json import math import multiprocessing as mp +import os import sys from collections import defaultdict from concurrent.futures import ProcessPoolExecutor, as_completed @@ -42,96 +35,41 @@ ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) -HORIZON = 30 +# Match production signal_eval cadence / reliability bars. MIN_CROSS = 20 MIN_RELIABLE = 12 -LIQUID_TOP = 1500 -MIN_PRICE = 5.0 -MOM_WINNER_PCT = 80.0 # top 20% within liquid cross-section +MOM_WINNER_PCT = 80.0 def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description=__doc__) - p.add_argument( - "--research-snapshot", - default="backtest_snapshots/research.sqlite", - ) - p.add_argument( - "--prod-snapshot", - default="backtest_snapshots/prod.sqlite", - help="Symbols here define the large-cap / prod-like subset.", - ) - p.add_argument("--top-n", type=int, default=LIQUID_TOP) - p.add_argument("--min-price", type=float, default=MIN_PRICE) + p.add_argument("--research-snapshot", default="backtest_snapshots/research.sqlite") + p.add_argument("--prod-snapshot", default="backtest_snapshots/prod.sqlite") + p.add_argument("--top-n", type=int, default=1500) + p.add_argument("--min-price", type=float, default=5.0) p.add_argument("--workers", type=int, default=max(1, (mp.cpu_count() or 4) - 1)) + p.add_argument("--allow-spawn", action="store_true") + p.add_argument("--dump-weeks", type=int, default=5, help="How many weeks to dump membership for") p.add_argument("--out", default=None) p.add_argument("--quiet", action="store_true") return p.parse_args() -def _week_key(d: date) -> tuple[int, int]: - iso = d.isocalendar() - return (int(iso[0]), int(iso[1])) - - def _week_ord(wk: tuple[int, int]) -> int: - return wk[0] * 53 + wk[1] + return int(wk[0]) * 53 + int(wk[1]) def _nonoverlap(weeks: list[tuple[int, int]], stride: int) -> list[tuple[int, int]]: - kept: list[tuple[int, int]] = [] - last: int | None = None - for wk in sorted(weeks, key=_week_ord): - o = _week_ord(wk) - if last is None or o - last >= stride: - kept.append(wk) - last = o - return kept + from app.services.backtest_service import _nonoverlapping_weeks - -def _rank(xs: list[float]) -> list[float]: - order = sorted(range(len(xs)), key=lambda k: xs[k]) - ranks = [0.0] * len(xs) - i = 0 - while i < len(xs): - j = i - while j + 1 < len(xs) and xs[order[j + 1]] == xs[order[i]]: - j += 1 - avg = (i + j) / 2.0 + 1.0 - for k in range(i, j + 1): - ranks[order[k]] = avg - i = j + 1 - return ranks - - -def _pearson(a: list[float], b: list[float]) -> float | None: - n = len(a) - if n < 3: - return None - ma, mb = sum(a) / n, sum(b) / n - va = sum((x - ma) ** 2 for x in a) - vb = sum((y - mb) ** 2 for y in b) - if va <= 0 or vb <= 0: - return None - cov = sum((a[k] - ma) * (b[k] - mb) for k in range(n)) - return cov / math.sqrt(va * vb) - - -def _spearman(xs: list[float], ys: list[float]) -> float | None: - if len(xs) < 3: - return None - return _pearson(_rank(xs), _rank(ys)) - - -def _ic_row(pairs: list[tuple[float, float]], *, label: str) -> dict[str, Any]: - """pairs = (signal, fwd) over non-overlapping weeks aggregated… actually - we pass per-week then aggregate outside. This helper is for multi-week IC.""" - raise NotImplementedError + return _nonoverlapping_weeks(weeks, stride) def _ic_from_weekly( week_pairs: dict[tuple[int, int], list[tuple[float, float]]], ) -> dict[str, Any]: + from app.services.backtest_service import HORIZON, _spearman + stride = max(1, round(HORIZON / 5)) usable = [wk for wk, ps in week_pairs.items() if len(ps) >= MIN_CROSS] kept = _nonoverlap(usable, stride) @@ -171,70 +109,29 @@ def _ic_from_weekly( } -def _panel_worker(payload: tuple) -> list[dict]: - """Build weekly observations for one ticker (picklable top-level).""" - symbol, date_ords, opens, highs, lows, closes, volumes, spy = payload +def _worker(payload: tuple) -> dict: + """Return harness-style signal series for one ticker (liquid-mode dicts).""" + symbol, ords, opens, highs, lows, closes, volumes, spy = payload from types import SimpleNamespace - from app.services.backtest_service import ( - HORIZON as H, - _median_dollar_vol_63, - _signal_values, - _weekly_asof_indices, - ) + from app.services.backtest_service import _signal_series - dates = [date.fromordinal(int(o)) for o in date_ords] - opens_f = [float(x) for x in opens] - highs_f = [float(x) for x in highs] - lows_f = [float(x) for x in lows] - closes_f = [float(x) for x in closes] - vols_f = [float(x) for x in volumes] - n = len(closes_f) - if n < H + 21: - return [] - - # Match backtest_service bar objects exactly (weekly as-of + signal_values). - bar_records = [ + bars = [ SimpleNamespace( - date=dates[i], - open=opens_f[i], - high=highs_f[i], - low=lows_f[i], - close=closes_f[i], - volume=vols_f[i], + date=date.fromordinal(int(o)), + open=float(op), + high=float(hi), + low=float(lo), + close=float(cl), + volume=float(vo), ) - for i in range(n) + for o, op, hi, lo, cl, vo in zip(ords, opens, highs, lows, closes, volumes) ] - out: list[dict] = [] - for i in _weekly_asof_indices(bar_records): - j = i + H - if j >= n or closes_f[i] <= 0: - continue - sigs = _signal_values(dates, closes_f, highs_f, i, spy) - fip = sigs.get("fip_id") - mom = sigs.get("mom_12_1") - mom_r = sigs.get("mom_12_1_resid") - vol = sigs.get("vol_6m") - if fip is None and mom is None: - continue - dvol = _median_dollar_vol_63(closes_f, vols_f, i) - wk = _week_key(dates[i]) - out.append({ - "symbol": symbol, - "week": wk, - "fwd": closes_f[j] / closes_f[i] - 1.0, - "close": closes_f[i], - "dvol": dvol, - "fip_id": fip, - "mom_12_1": mom, - "mom_12_1_resid": mom_r, - "vol_6m": vol, - }) - return out + return _signal_series(bars, spy, symbol=symbol) def _load_spy(conn) -> dict[date, float]: rows = conn.execute( - text("SELECT date, close FROM benchmark_prices WHERE symbol = 'SPY' ORDER BY date") + text("SELECT date, close FROM benchmark_prices WHERE symbol='SPY' ORDER BY date") ).fetchall() out: dict[date, float] = {} for d, c in rows: @@ -244,34 +141,22 @@ def _load_spy(conn) -> dict[date, float]: return out -def _load_symbols(conn) -> list[str]: - return [ - str(r[0]) - for r in conn.execute(text("SELECT symbol FROM tickers ORDER BY symbol")).fetchall() - ] - - -def _load_columns(conn, symbol: str) -> tuple | None: +def _load_job(conn, symbol: str, spy: dict) -> tuple | None: tid = conn.execute( - text("SELECT id FROM tickers WHERE symbol = :s"), {"s": symbol} + text("SELECT id FROM tickers WHERE symbol=:s"), {"s": symbol} ).scalar() if tid is None: return None rows = conn.execute( text( "SELECT date, open, high, low, close, volume FROM ohlcv_records " - "WHERE ticker_id = :t ORDER BY date" + "WHERE ticker_id=:t ORDER BY date" ), {"t": tid}, ).fetchall() - if len(rows) < HORIZON + 60: + if len(rows) < 90: return None - ords: list[int] = [] - opens: list[float] = [] - highs: list[float] = [] - lows: list[float] = [] - closes: list[float] = [] - vols: list[float] = [] + ords, opens, highs, lows, closes, vols = [], [], [], [], [], [] for d, o, h, l, c, v in rows: if isinstance(d, str): d = date.fromisoformat(d[:10]) @@ -281,36 +166,7 @@ def _load_columns(conn, symbol: str) -> tuple | None: lows.append(float(l)) closes.append(float(c)) vols.append(float(v or 0)) - return (symbol, ords, opens, highs, lows, closes, vols) - - -def _liquid_members( - obs: list[dict], - *, - top_n: int, - min_price: float, - dvol_key: str = "dvol", -) -> list[dict]: - eligible = [ - o - for o in obs - if o.get("close") is not None - and float(o["close"]) >= min_price - and o.get(dvol_key) is not None - and float(o[dvol_key]) > 0 - ] - eligible.sort(key=lambda o: float(o[dvol_key]), reverse=True) - return eligible[:top_n] - - -def _pairs(obs: list[dict], signal: str) -> list[tuple[float, float]]: - out: list[tuple[float, float]] = [] - for o in obs: - v = o.get(signal) - if v is None: - continue - out.append((float(v), float(o["fwd"]))) - return out + return (symbol, ords, opens, highs, lows, closes, vols, spy) def main() -> None: @@ -318,316 +174,440 @@ def main() -> None: research = Path(args.research_snapshot) prod = Path(args.prod_snapshot) if not research.exists(): - raise SystemExit(f"Missing research snapshot: {research}") + raise SystemExit(f"Missing {research}") - research_eng = create_engine(f"sqlite:///{research.resolve().as_posix()}") + # Force harness liquid-mode collection (same env as breadth run). + os.environ["BACKTEST_LIQUID_BREADTH"] = str(int(args.top_n)) + os.environ["BACKTEST_LIQUID_MIN_PRICE"] = str(float(args.min_price)) + if args.allow_spawn: + os.environ["BACKTEST_ALLOW_SPAWN"] = "1" + + from app.services.backtest_service import ( + HORIZON, + _filter_liquid_breadth_week_rich, + _liquid_breadth_week_stats, + _signal_evaluation, + ) + + eng = create_engine(f"sqlite:///{research.resolve().as_posix()}") prod_symbols: set[str] = set() if prod.exists(): - prod_eng = create_engine(f"sqlite:///{prod.resolve().as_posix()}") - with prod_eng.connect() as c: + peng = create_engine(f"sqlite:///{prod.resolve().as_posix()}") + with peng.connect() as c: prod_symbols = { - str(r[0]) - for r in c.execute(text("SELECT symbol FROM tickers")).fetchall() + str(r[0]) for r in c.execute(text("SELECT symbol FROM tickers")) } - prod_eng.dispose() + peng.dispose() - with research_eng.connect() as conn: + with eng.connect() as conn: spy = _load_spy(conn) - symbols = _load_symbols(conn) - jobs: list[tuple] = [] + symbols = [ + str(r[0]) + for r in conn.execute(text("SELECT symbol FROM tickers ORDER BY symbol")) + ] + jobs = [] for i, sym in enumerate(symbols, 1): - cols = _load_columns(conn, sym) - if cols is None: - continue - jobs.append((*cols, spy)) + job = _load_job(conn, sym, spy) + if job is not None: + jobs.append(job) if not args.quiet and i % 500 == 0: print(f" queued {i}/{len(symbols)}", flush=True) if not args.quiet: - print(f"Building weekly panel for {len(jobs)} tickers…", flush=True) + print(f"Collecting harness signal series for {len(jobs)} tickers…", flush=True) - # Panel: week -> list of obs - by_week: dict[tuple[int, int], list[dict]] = defaultdict(list) + collected: dict = defaultdict(lambda: defaultdict(list)) workers = max(1, int(args.workers)) + + def _merge(series: dict) -> None: + for name, weeks in series.items(): + for wk, recs in weeks.items(): + # week keys may arrive as lists after JSON; normalize to tuple + key = tuple(wk) if not isinstance(wk, tuple) else wk + collected[name][key].extend(recs) + if workers == 1: for j, job in enumerate(jobs, 1): - for row in _panel_worker(job): - by_week[tuple(row["week"])].append(row) + _merge(_worker(job)) if not args.quiet and j % 200 == 0: - print(f" panel {j}/{len(jobs)}", flush=True) + print(f" series {j}/{len(jobs)}", flush=True) else: - with ProcessPoolExecutor(max_workers=workers) as pool: - futs = {pool.submit(_panel_worker, job): job[0] for job in jobs} - done = 0 - for fut in as_completed(futs): - done += 1 + ctx = mp.get_context("spawn") if args.allow_spawn or sys.platform == "win32" else None + with ProcessPoolExecutor(max_workers=workers, mp_context=ctx) as pool: + futs = [pool.submit(_worker, job) for job in jobs] + for j, fut in enumerate(as_completed(futs), 1): try: - rows = fut.result() + _merge(fut.result()) except Exception as exc: if not args.quiet: - print(f" worker error {futs[fut]}: {exc}", flush=True) - continue - for row in rows: - by_week[tuple(row["week"])].append(row) - if not args.quiet and done % 200 == 0: - print(f" panel {done}/{len(jobs)}", flush=True) + print(f" worker error: {exc}", flush=True) + if not args.quiet and j % 200 == 0: + print(f" series {j}/{len(jobs)}", flush=True) - if not args.quiet: - print(f"Weeks with data: {len(by_week)}", flush=True) - - # Prior-week dvol map for lagged membership: (symbol, week) -> dvol - dvol_by_sym_week: dict[tuple[str, tuple[int, int]], float] = {} - for wk, obs in by_week.items(): - for o in obs: - if o.get("dvol") is not None: - dvol_by_sym_week[(o["symbol"], wk)] = float(o["dvol"]) - - ordered_weeks = sorted(by_week.keys(), key=_week_ord) - prev_week: dict[tuple[int, int], tuple[int, int]] = {} - for i, wk in enumerate(ordered_weeks): - if i > 0: - prev_week[wk] = ordered_weeks[i - 1] + # --- Harness signal_eval (authoritative unconditional ICs) --- + harness_rows = _signal_evaluation(dict(collected)) + harness_by_name = {r["signal"]: r for r in harness_rows} top_n = int(args.top_n) min_price = float(args.min_price) + fip_weeks = collected.get("fip_id") or {} + mom_weeks = collected.get("mom_12_1") or {} + vol_weeks = collected.get("vol_6m") or {} + momr_weeks = collected.get("mom_12_1_resid") or {} - # --- Panels for each check --- - same_week_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - lag_week_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - tier_hi_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - tier_lo_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - prod_subset_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - mom_cond_fip: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - liquid_vol: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - liquid_mom: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) - liquid_mom_r: dict[tuple[int, int], list[tuple[float, float]]] = defaultdict(list) + # Index mom/vol by (week, symbol) for joins + def _index(weeks_map: dict) -> dict[tuple, dict]: + out: dict[tuple, dict] = {} + for wk, recs in weeks_map.items(): + key_wk = tuple(wk) if not isinstance(wk, tuple) else wk + for rec in recs: + if not isinstance(rec, dict): + continue + sym = rec.get("symbol") + if not sym: + continue + out[(key_wk, str(sym))] = rec + return out - for wk, obs in by_week.items(): - # Same-week liquid top-N among names that have fip (matches signal_eval mask: - # membership is ranked within each signal's observation set). - with_fip = [o for o in obs if o.get("fip_id") is not None] - liq_fip = _liquid_members(with_fip, top_n=top_n, min_price=min_price) - for rank, o in enumerate(liq_fip, 1): - same_week_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + mom_ix = _index(mom_weeks) + vol_ix = _index(vol_weeks) + momr_ix = _index(momr_weeks) + + # Per-week membership + extended checks via shared rich filter + same_week: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + lag_week: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + tier_hi: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + tier_lo: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + prod_sub: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + mom_cond: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + vol_pairs: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + mom_pairs: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + momr_pairs: dict[tuple, list[tuple[float, float]]] = defaultdict(list) + + ordered = sorted((tuple(w) for w in fip_weeks.keys()), key=_week_ord) + prev: dict[tuple, tuple] = {} + for i, wk in enumerate(ordered): + if i: + prev[wk] = ordered[i - 1] + + # Prior-week dvol for lag: (symbol, week) from fip recs + dvol_sw: dict[tuple[str, tuple], float] = {} + for wk, recs in fip_weeks.items(): + key_wk = tuple(wk) if not isinstance(wk, tuple) else wk + for rec in recs: + if isinstance(rec, dict) and rec.get("symbol") and rec.get("median_dvol_63"): + dvol_sw[(str(rec["symbol"]), key_wk)] = float(rec["median_dvol_63"]) + + membership_dumps: list[dict] = [] + dump_count = 0 + stride = max(1, round(HORIZON / 5)) + dump_weeks = _nonoverlap(ordered, stride)[: max(0, int(args.dump_weeks))] + + for wk_raw, recs in fip_weeks.items(): + wk = tuple(wk_raw) if not isinstance(wk_raw, tuple) else wk_raw + stats = _liquid_breadth_week_stats(recs, top_n=top_n, min_price=min_price) + rich = _filter_liquid_breadth_week_rich( + recs, top_n=top_n, min_price=min_price + ) + for rank, row in enumerate(rich, 1): + same_week[wk].append((float(row["val"]), float(row["fwd"]))) if rank <= 800: - tier_hi_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + tier_hi[wk].append((float(row["val"]), float(row["fwd"]))) elif rank <= top_n: - tier_lo_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) - if o["symbol"] in prod_symbols: - prod_subset_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + tier_lo[wk].append((float(row["val"]), float(row["fwd"]))) + sym = row.get("symbol") + if sym and str(sym) in prod_symbols: + prod_sub[wk].append((float(row["val"]), float(row["fwd"]))) + # Join mom for conditional + mrec = mom_ix.get((wk, str(sym))) if sym else None + if mrec is not None: + row["mom_12_1"] = mrec.get("val") - # Context signals: liquid among names that carry that signal - with_vol = [o for o in obs if o.get("vol_6m") is not None] - for o in _liquid_members(with_vol, top_n=top_n, min_price=min_price): - liquid_vol[wk].append((float(o["vol_6m"]), float(o["fwd"]))) - with_mom_all = [o for o in obs if o.get("mom_12_1") is not None] - liq_mom = _liquid_members(with_mom_all, top_n=top_n, min_price=min_price) - for o in liq_mom: - liquid_mom[wk].append((float(o["mom_12_1"]), float(o["fwd"]))) - with_mom_r = [o for o in obs if o.get("mom_12_1_resid") is not None] - for o in _liquid_members(with_mom_r, top_n=top_n, min_price=min_price): - liquid_mom_r[wk].append((float(o["mom_12_1_resid"]), float(o["fwd"]))) - - # Momentum-conditional: within liquid fip set, keep mom_12_1 ≥ P80 - mom_key = "mom_12_1" + # Mom-conditional among liquid fip set with_mom = [ - o for o in liq_fip - if o.get(mom_key) is not None and o.get("fip_id") is not None + r for r in rich + if r.get("mom_12_1") is not None or mom_ix.get((wk, str(r.get("symbol")))) ] + # ensure mom filled + for r in with_mom: + if r.get("mom_12_1") is None and r.get("symbol"): + m = mom_ix.get((wk, str(r["symbol"]))) + if m is not None: + r["mom_12_1"] = m["val"] + with_mom = [r for r in rich if r.get("mom_12_1") is not None] if len(with_mom) >= MIN_CROSS: - with_mom.sort(key=lambda o: float(o[mom_key])) - n = len(with_mom) - cut = int(math.floor(n * (MOM_WINNER_PCT / 100.0))) - winners = with_mom[cut:] # upper tail - for o in winners: - mom_cond_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + with_mom.sort(key=lambda r: float(r["mom_12_1"])) + cut = int(math.floor(len(with_mom) * (MOM_WINNER_PCT / 100.0))) + for r in with_mom[cut:]: + mom_cond[wk].append((float(r["val"]), float(r["fwd"]))) - # Lagged membership: rank by *previous* week's dvol among fip names - pw = prev_week.get(wk) + # Context signals via same shared filter on their own pools + for r in _filter_liquid_breadth_week_rich( + vol_weeks.get(wk_raw) or vol_weeks.get(wk) or [], + top_n=top_n, + min_price=min_price, + ): + vol_pairs[wk].append((float(r["val"]), float(r["fwd"]))) + for r in _filter_liquid_breadth_week_rich( + mom_weeks.get(wk_raw) or mom_weeks.get(wk) or [], + top_n=top_n, + min_price=min_price, + ): + mom_pairs[wk].append((float(r["val"]), float(r["fwd"]))) + for r in _filter_liquid_breadth_week_rich( + momr_weeks.get(wk_raw) or momr_weeks.get(wk) or [], + top_n=top_n, + min_price=min_price, + ): + momr_pairs[wk].append((float(r["val"]), float(r["fwd"]))) + + # Lagged membership using prior week dvol on current fip pool + pw = prev.get(wk) if pw is not None: - lagged: list[dict] = [] - for o in with_fip: - if o.get("close") is None or float(o["close"]) < min_price: + lagged_recs = [] + for rec in recs: + if not isinstance(rec, dict) or not rec.get("symbol"): continue - prev_dvol = dvol_by_sym_week.get((o["symbol"], pw)) - if prev_dvol is None or prev_dvol <= 0: + pdv = dvol_sw.get((str(rec["symbol"]), pw)) + if pdv is None or pdv <= 0: continue - lagged.append({**o, "lag_dvol": prev_dvol}) - lagged.sort(key=lambda o: float(o["lag_dvol"]), reverse=True) - for o in lagged[:top_n]: - lag_week_fip[wk].append((float(o["fip_id"]), float(o["fwd"]))) + # Clone with lag dvol for ranking + lagged_recs.append({ + **rec, + "median_dvol_63": pdv, + }) + for r in _filter_liquid_breadth_week_rich( + lagged_recs, top_n=top_n, min_price=min_price + ): + lag_week[wk].append((float(r["val"]), float(r["fwd"]))) + + if wk in dump_weeks and dump_count < args.dump_weeks: + membership_dumps.append({ + "week": list(wk), + "stats": stats, + "symbols": sorted( + str(r["symbol"]) for r in rich if r.get("symbol") + ), + "n_symbols": len(rich), + }) + dump_count += 1 + + # IC rows + checks = { + "fip_harness_signal_eval": { + "note": "Authoritative harness _signal_evaluation on collected fip_id", + **(harness_by_name.get("fip_id") or {}), + }, + "fip_same_week_via_shared_filter": { + "note": "Same collected data, IC via shared _filter_liquid_breadth_week_rich", + **_ic_from_weekly(same_week), + }, + "fip_lagged_membership_1w": { + "note": "Top-N by prior-week $vol on current fip pool (shared filter)", + **_ic_from_weekly(lag_week), + }, + "fip_tier_1_800": { + "note": "Senior liquid ranks 1–800", + **_ic_from_weekly(tier_hi), + }, + "fip_tier_801_1500": { + "note": "Junior liquid ranks 801–top_n", + **_ic_from_weekly(tier_lo), + }, + "fip_prod_universe_subset": { + "note": "Prod.sqlite symbols inside liquid fip set", + **_ic_from_weekly(prod_sub), + }, + "fip_momentum_conditional_top20pct": { + "note": ( + f"Among liquid fip set, mom_12_1 ≥ P{MOM_WINNER_PCT:.0f} " + "(paper / gate-relevant)" + ), + **_ic_from_weekly(mom_cond), + }, + "vol_6m_liquid": { + "note": "vol_6m through shared filter", + **_ic_from_weekly(vol_pairs), + }, + "mom_12_1_liquid": { + "note": "raw mom through shared filter", + **_ic_from_weekly(mom_pairs), + }, + "mom_12_1_resid_liquid": { + "note": "residual mom through shared filter", + **_ic_from_weekly(momr_pairs), + }, + } + + h = checks["fip_harness_signal_eval"] + s = checks["fip_same_week_via_shared_filter"] + cond = checks["fip_momentum_conditional_top20pct"] + prod = checks["fip_prod_universe_subset"] + hi = checks["fip_tier_1_800"] + lo = checks["fip_tier_801_1500"] + lag = checks["fip_lagged_membership_1w"] + + # Self-consistency: harness eval vs manual IC on same filter must match + harness_ic = h.get("mean_ic") + shared_ic = s.get("mean_ic") + consistent = ( + harness_ic is not None + and shared_ic is not None + and abs(float(harness_ic) - float(shared_ic)) < 0.005 + ) + + mom_alive = ( + cond.get("mean_ic") is not None + and float(cond["mean_ic"]) < 0 + and abs(float(cond["mean_ic"])) >= 0.03 + and bool(cond.get("reliable")) + ) results = { "generated_at": datetime.now().isoformat(), "research_snapshot": str(research.resolve()), - "prod_subset_n": len(prod_symbols), - "panel_tickers": len(jobs), "top_n": top_n, "min_price": min_price, - "checks": { - "fip_same_week_liquid_1500": { - "note": "Replication of main breadth run (same-week $vol mask)", - **_ic_from_weekly(same_week_fip), - }, - "fip_lagged_membership_1w": { - "note": ( - "Liquid top-N ranked on *prior* week's median $vol — " - "excludes same-week liquidity explosion leak" - ), - **_ic_from_weekly(lag_week_fip), - }, - "fip_tier_1_800": { - "note": "Same-week liquid ranks 1–800 (senior liquid tier)", - **_ic_from_weekly(tier_hi_fip), - }, - "fip_tier_801_1500": { - "note": "Same-week liquid ranks 801–1500 (junior liquid tier)", - **_ic_from_weekly(tier_lo_fip), - }, - "fip_prod_universe_subset": { - "note": ( - "Symbols in prod.sqlite (~S&P-like large-cap book) inside " - "same-week liquid top-N — compositional control" - ), - **_ic_from_weekly(prod_subset_fip), - }, - "fip_momentum_conditional_top20pct": { - "note": ( - f"Among liquid top-N, keep mom_12_1 percentile ≥ {MOM_WINNER_PCT} " - "(paper: ID modulates continuation among winners; gate-relevant)" - ), - **_ic_from_weekly(mom_cond_fip), - }, - "vol_6m_liquid_1500": { - "note": "Context: low-vol anomaly strength on this pool", - **_ic_from_weekly(liquid_vol), - }, - "mom_12_1_liquid_1500": { - "note": "Context: raw momentum on liquid breadth", - **_ic_from_weekly(liquid_mom), - }, - "mom_12_1_resid_liquid_1500": { - "note": "Context: residual momentum on liquid breadth", - **_ic_from_weekly(liquid_mom_r), - }, + "prod_subset_n": len(prod_symbols), + "panel_tickers": len(jobs), + "single_source": ( + "diagnostics uses harness _signal_series + " + "_filter_liquid_breadth_week_rich only (no parallel mask)" + ), + "avg_cross_section_semantics": ( + "avg_cross_section = post-mask IC sample size. " + "avg_raw_pool = pre-filter observations. " + "avg_eligible_pre_mask = pass price+dvol before top-N. " + "mask_binds_pct = weeks where eligible_pre_mask > top_n." + ), + "harness_self_consistent": consistent, + "checks": checks, + "membership_dumps": membership_dumps, + "interpretation": { + "harness_and_shared_filter_agree": consistent, + "mask_binds_pct": h.get("mask_binds_pct"), + "avg_eligible_pre_mask": h.get("avg_eligible_pre_mask"), + "avg_raw_pool": h.get("avg_raw_pool"), + "prod_subset_still_negative": ( + prod.get("mean_ic") is not None and float(prod["mean_ic"]) < 0 + ), + "junior_tier_more_positive": ( + lo.get("mean_ic") is not None + and hi.get("mean_ic") is not None + and float(lo["mean_ic"]) > float(hi["mean_ic"]) + ), + "lag_same_sign_as_same_week": ( + lag.get("mean_ic") is not None + and s.get("mean_ic") is not None + and (float(lag["mean_ic"]) < 0) == (float(s["mean_ic"]) < 0) + ), + "mom_conditional_negative_and_reliable": mom_alive, + "orphan_plus_five_sigma": ( + "Prior report fip-breadth-20260718-211440-breadth.json listed " + "fip IC +0.0575 / t +5.12. This single-sourced recompute is the " + "authoritative number; if it disagrees, the +0.0575 row is orphaned." + ), + "compositional_story": ( + "fip_id pools continuous winners (neg IC) vs continuous bleeders " + "(pos IC). Prod-subset and senior liquid stay negative; junior " + "liquid is less negative / positive — composition, not jumpiness premium." + ), + "vol_tilt_warning": ( + "High-vol names underperform on breadth relative to S&P-like books. " + "Re-validate production 80/20 high-vol tilt before any universe broaden." + ), }, - } - - # Interpretations - checks = results["checks"] - lag = checks["fip_lagged_membership_1w"] - same = checks["fip_same_week_liquid_1500"] - hi = checks["fip_tier_1_800"] - lo = checks["fip_tier_801_1500"] - prod = checks["fip_prod_universe_subset"] - cond = checks["fip_momentum_conditional_top20pct"] - - def _sign(x: float | None) -> str: - if x is None: - return "na" - return "neg" if x < 0 else "pos" - - results["interpretation"] = { - "leak_ruled_out": ( - lag.get("mean_ic") is not None - and same.get("mean_ic") is not None - and _sign(lag["mean_ic"]) == _sign(same["mean_ic"]) - and abs(float(lag["mean_ic"])) >= 0.02 - ), - "junior_tier_drives_positive": ( - lo.get("mean_ic") is not None - and float(lo["mean_ic"]) > 0 - and (hi.get("mean_ic") is None or float(hi["mean_ic"]) < float(lo["mean_ic"])) - ), - "prod_subset_still_negative": ( - prod.get("mean_ic") is not None and float(prod["mean_ic"]) < 0 - ), - "mom_conditional_negative_and_reliable": ( - cond.get("mean_ic") is not None - and float(cond["mean_ic"]) < 0 - and abs(float(cond["mean_ic"])) >= 0.03 - and bool(cond.get("reliable")) - ), - "compositional_flip_story": ( - "If prod subset IC is negative while full liquid-1500 is positive, " - "the sign flip is compositional (bleeders / Nasdaq junk), not a " - "temporal regime change. Unconditional fip pools continuous winners " - "(want neg IC) against continuous losers/bleeders (want pos IC)." - ), - "vol_tilt_warning": ( - "vol_6m large negative IC on breadth: high-vol lottery names " - "underperform. Production 80/20 high-vol tilt was validated on " - "S&P-like names; must re-validate before any universe broaden." + "platform_verdict": ( + "Mom-conditional fip ALIVE as book-tilt candidate (needs book sim) — " + "not production wire-in. Unconditional fip not green." + if mom_alive + else ( + "fip CLOSED for production: mom-conditional does not clear iron rule " + "on single-sourced path. Display card is the resting place." + ) ), } - # Gate-relevant summary line - if results["interpretation"]["mom_conditional_negative_and_reliable"]: - results["platform_verdict"] = ( - "ALIVE as breadth-book tilt candidate among momentum winners only — " - "still needs a book-level experiment; not a production wire-in." - ) - else: - results["platform_verdict"] = ( - "CLOSED for production use: momentum-conditional fip does not clear " - "iron rule on this liquid-Nasdaq pool. Display card remains final resting place." - ) - stamp = datetime.now().strftime("%Y%m%d-%H%M%S") - out = Path(args.out) if args.out else Path("reports") / f"fip-breadth-diagnostics-{stamp}.json" + out = Path(args.out) if args.out else Path("reports") / f"fip-reconcile-{stamp}.json" out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(results, indent=2, default=str), encoding="utf-8") - # Append to research log - md_path = Path("docs/research/fip-breadth-ic.md") - _append_diagnostics_md(md_path, results, out) + # Update research log + _update_md(Path("docs/research/fip-breadth-ic.md"), results, out) if not args.quiet: - print(json.dumps(results["checks"], indent=2, default=str)) - print() - print("interpretation:", json.dumps(results["interpretation"], indent=2)) + print("=== Harness fip_id (authoritative) ===") + print(json.dumps(h, indent=2, default=str)) + print("=== Shared-filter same-week (must match) ===") + print(json.dumps(s, indent=2, default=str)) + print("=== Mom-conditional ===") + print(json.dumps(cond, indent=2, default=str)) + print("self_consistent:", consistent) print("platform_verdict:", results["platform_verdict"]) print(f"Wrote {out}") - print(f"Updated {md_path}") -def _append_diagnostics_md(path: Path, results: dict, artifact: Path) -> None: +def _update_md(path: Path, results: dict, artifact: Path) -> None: checks = results["checks"] interp = results["interpretation"] + h = checks.get("fip_harness_signal_eval") or {} lines = [ "", "---", "", - f"## Follow-up diagnostics ({results['generated_at'][:10]})", + f"## Reconciliation ({results['generated_at'][:10]})", "", - "Compositional reading of the sign flip (before any 'jumpiness premium' story):", + "### Problem", "", - "`fip_id = sign(PRET) × (%neg − %pos)` pools two opposite continuous populations:", + "Two implementations of the liquid-1500 fip IC disagreed on **sign**:", "", - "- **Continuous winners** (PRET>0, mostly up days) → paper claim → **negative** IC contribution.", - "- **Continuous losers / bleeders** (PRET<0, mostly down days) → momentum continuation down → **positive** IC contribution.", + "- Harness report `fip-breadth-20260718-211440-breadth.json`: **+0.0575 / t +5.12**", + "- Dual-path diagnostics (since deleted): **−0.017 / t −1.9**", "", - "Unconditional IC is a tug-of-war weighted by universe composition. S&P-like books " - "have few steady bleeders → negative fip IC. Liquid Nasdaq has many → sign can flip " - "without contradicting Da/Gurun/Warachka (claim was always **momentum-conditional**).", + "A static read cannot decide which is right without single-sourcing the mask.", "", - "### Artifact / composition checks", + "### Resolution", + "", + f"- **Single source:** {results.get('single_source')}", + f"- **avg_cross_section semantics:** {results.get('avg_cross_section_semantics')}", + f"- Harness `_signal_evaluation` vs shared-filter recompute agree: " + f"**{interp.get('harness_and_shared_filter_agree')}**", + "", + "### Authoritative unconditional fip (liquid top-N, post-mask)", + "", + f"| metric | value |", + f"|---|---|", + f"| mean_ic | {h.get('mean_ic')} |", + f"| ic_t_stat | {h.get('ic_t_stat')} |", + f"| weeks | {h.get('weeks')} |", + f"| avg_cross_section (post-mask) | {h.get('avg_cross_section')} |", + f"| avg_raw_pool | {h.get('avg_raw_pool')} |", + f"| avg_eligible_pre_mask | {h.get('avg_eligible_pre_mask')} |", + f"| mask_binds_pct | {h.get('mask_binds_pct')} |", + f"| reliable | {h.get('reliable')} |", + "", + "The **+0.0575 / +5.12** row is **orphaned** if the authoritative recompute " + "disagrees; do not cite it. Iron-rule unconditional green still requires " + "negative sign and |IC| ≳ 0.03 on this row.", + "", + "### Checks (single-sourced)", "", "| check | mean_ic | t | weeks | avg N | reliable |", "|---|---:|---:|---:|---:|---|", ] - order = [ - "fip_same_week_liquid_1500", + for key in [ + "fip_harness_signal_eval", + "fip_same_week_via_shared_filter", "fip_lagged_membership_1w", "fip_tier_1_800", "fip_tier_801_1500", "fip_prod_universe_subset", "fip_momentum_conditional_top20pct", - "vol_6m_liquid_1500", - "mom_12_1_liquid_1500", - "mom_12_1_resid_liquid_1500", - ] - for key in order: + "vol_6m_liquid", + "mom_12_1_liquid", + "mom_12_1_resid_liquid", + ]: row = checks.get(key) or {} lines.append( f"| {key} | {row.get('mean_ic')} | {row.get('ic_t_stat')} | " @@ -637,28 +617,30 @@ def _append_diagnostics_md(path: Path, results: dict, artifact: Path) -> None: "", "### Flags", "", - f"- Lagged mask keeps same sign / material |IC|: **{interp.get('leak_ruled_out')}**", - f"- Junior tier (801–1500) drives more positive IC: **{interp.get('junior_tier_drives_positive')}**", - f"- Prod-universe subset still negative: **{interp.get('prod_subset_still_negative')}**", - f"- Mom-conditional (≥P80) negative + reliable: **{interp.get('mom_conditional_negative_and_reliable')}**", + f"- Prod subset still negative: **{interp.get('prod_subset_still_negative')}**", + f"- Junior tier more positive than senior: **{interp.get('junior_tier_more_positive')}**", + f"- Lag same sign as same-week: **{interp.get('lag_same_sign_as_same_week')}**", + f"- Mom-conditional negative + reliable: **{interp.get('mom_conditional_negative_and_reliable')}**", "", - "### Platform verdict", + "### Platform verdict (post-reconciliation)", "", results.get("platform_verdict", ""), "", - "### Vol-tilt warning (any future breadth move)", + "### Vol-tilt warning", "", interp.get("vol_tilt_warning", ""), "", f"Artifact: `{artifact.as_posix()}`", "", ]) - # Replace previous diagnostics section if re-run, else append existing = path.read_text(encoding="utf-8") if path.exists() else "" - marker = "## Follow-up diagnostics" + marker = "## Reconciliation" if marker in existing: existing = existing.split(marker)[0].rstrip() + "\n" - path.write_text(existing + "\n".join(lines), encoding="utf-8") + # Also strip old dual-path diagnostics section if present after reconciliation + if "## Follow-up diagnostics" in existing and marker not in path.read_text(encoding="utf-8") if path.exists() else "": + pass + path.write_text(existing.rstrip() + "\n" + "\n".join(lines), encoding="utf-8") if __name__ == "__main__": From 2311999e5768462ee354cfa5e161f89d292dcdb6 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sun, 19 Jul 2026 00:32:20 +0200 Subject: [PATCH 6/6] research: park Phase B fip breadth; race guard and compact evidence Log the 21:14 orphan as a snapshot-build race, rewrite the context table to authoritative ICs only, and soften the vol-tilt warning. Add extender completion manifest + breadth refuse guard; strip intermediate/orphaned reports; park the thread (no book sim, no deploy). --- README.md | 4 +- docs/research/README.md | 11 +- docs/research/fip-breadth-ic.md | 123 +- reports/README.md | 20 + ...p-breadth-20260718-194828-fingerprint.json | 578 -- reports/fip-breadth-20260718-194828.json | 21 - .../fip-breadth-20260718-211440-breadth.json | 596 -- reports/fip-breadth-20260718-211440.json | 59 - ...p-breadth-diagnostics-20260718-213705.json | 100 - ...p-breadth-diagnostics-20260718-213908.json | 100 - reports/fip-reconcile-20260719-000520.json | 6574 +---------------- scripts/extend_snapshot_universe.py | 42 + scripts/research_snapshot_manifest.py | 172 + scripts/run_fip_breadth_diagnostics.py | 98 +- scripts/run_fip_breadth_research.py | 34 +- tests/unit/test_research_snapshot_manifest.py | 133 + 16 files changed, 562 insertions(+), 8103 deletions(-) delete mode 100644 reports/fip-breadth-20260718-194828-fingerprint.json delete mode 100644 reports/fip-breadth-20260718-194828.json delete mode 100644 reports/fip-breadth-20260718-211440-breadth.json delete mode 100644 reports/fip-breadth-20260718-211440.json delete mode 100644 reports/fip-breadth-diagnostics-20260718-213705.json delete mode 100644 reports/fip-breadth-diagnostics-20260718-213908.json create mode 100644 scripts/research_snapshot_manifest.py create mode 100644 tests/unit/test_research_snapshot_manifest.py diff --git a/README.md b/README.md index 2e3b901..d875a8e 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ A systematic single-variable sweep (offline prod snapshot, production gate/rank/ Two findings future sessions must not re-litigate: - **The "inverse-vol sizing win" (July 2026) was mis-attributed — do not resurrect.** The diagnostic sized `notional = equity × 1% / vol_6m`, and the 20% notional cap bound on 95% of entries, so it actually measured "~5 positions × 20% notional each" — a concentration/risk-appetite bump economically equivalent to raising risk to 1.5%, not vol-managed sizing. Genuine inverse-vol sizing (risk budget × median-vol/vol) cuts max drawdown to −18.2% but costs ~58pp total return at flat Sharpe: a risk-preference trade, not edge. -- **`fip_id` — Da/Gurun/Warachka information discreteness over the 12-1 formation window — is the strongest cross-sectional signal measured on this universe: IC −0.045, t = −2.91, correct sign (continuous-information winners outperform).** It clears the iron-rule bar in isolation but does not improve this book (the momentum gate already captures the effect in-sample). It is the prime ranking/gate candidate **if the universe broadens** (e.g. `nasdaq_all`). +- **`fip_id` — Da/Gurun/Warachka information discreteness over the 12-1 formation window — is the strongest cross-sectional signal on the *production* universe: IC −0.045, t = −2.91, correct sign (continuous-information winners outperform).** It clears the iron-rule bar in isolation but does not improve this book (the momentum gate already captures the effect in-sample). **Phase B (liquid-1500, research branch only):** unconditional fip fails iron rule (−0.017 / t −1.85); mom-conditional fip (−0.088 / t −4.58) is a *book-tilt candidate only* after a baseline breadth mom book is proven. Do **not** cite the orphaned 21:14 row (+0.0575) — it raced a partial `research.sqlite`. See `docs/research/fip-breadth-ic.md`. ### The iron rule for strategy changes @@ -281,7 +281,7 @@ Corollaries: never let an unvalidated score gate setups; the outcome evaluator m 1. **Forward monitor the promoted strategy** — the production UI now behaves like a portfolio monitor for the current strategy, with selectable lookbacks and SPY comparison. Forward paper-trade months are the only evidence the snapshot cannot provide; the July 2026 tuning pass closed every in-sample lead. (Trailing-stop sensitivity and the max-15 capacity check are done — see the tuning table above.) 2. **Signal context snapshots** — accumulate point-in-time composite/sentiment/fundamental context for every new setup so the discretionary overlay can be tested forward-only. -3. **More breadth, not more history** — widening the ranked universe (e.g. `nasdaq_all`) strengthens each week's cross-section and the IC t-stat, even if only the top slice is traded. Now doubly motivated: it is also where the strong `fip_id` signal (see tuning findings) could become tradeable. (Deeper history was considered and declined.) +3. **Breadth is no longer free leverage** — Phase B found residual-mom t-stat *fell* on liquid-1500 vs the 505-name fingerprint (0.055/1.98 → 0.029/1.33). Any breadth book must clear a pre-registered baseline arm before fip tilts mean anything. (Deeper history was considered and declined.) ## Key Use Cases diff --git a/docs/research/README.md b/docs/research/README.md index 8c49986..47e5add 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -140,9 +140,9 @@ knobs. | Lead | Why it's interesting | Blocker | |---|---|---| -| **Near-close / MOC execution (ops)** | Recovers overnight momentum drift left on the table by a morning EU scan; evidence closed | Implement schedule + partial-bar scan path; one qualifying scan/day only | -| **`fip_id`** | Fingerprint IC −0.045 / t −2.91 on prod book; display-only on ticker technicals | **Phase B:** unconditional liquid-Nasdaq IC fails iron-rule **sign**; **mom-conditional** fip IC −0.088 / t −4.58 (alive as tilt candidate only). See [fip-breadth-ic.md](fip-breadth-ic.md) | -| **Broader universe** | Composition changes factor signs (fip tug-of-war; high-vol junk) | Any prod broaden must **re-validate 80/20 high-vol tilt** first; offline research only for now | +| **Near-close / MOC execution (ops)** | Recovers overnight momentum drift left on the table by a morning EU scan; evidence closed | Schedule + fill_mode shipped; live paper validation ongoing | +| **`fip_id` / liquid breadth** | Fingerprint −0.045 / t −2.91; liquid unconditional **−0.017 / t −1.85** (not green); mom-conditional **−0.088 / t −4.58** | **Parked.** Orphan +0.0575 died (snapshot race). Breadth did not strengthen resid-mom t-stat. Optional reopen = pre-registered two-arm liquid-1500 book first. See [fip-breadth-ic.md](fip-breadth-ic.md) | +| **Broader universe** | Composition changes factor signs (fip tug-of-war); vol-tilt on breadth is only a **directional hypothesis** (auth. −0.048 / t −1.36) | Any prod broaden must re-validate 80/20 tilt; offline research only; research.sqlite requires completion manifest | | **Forward paper-trade record** | The only true out-of-sample evidence the snapshot cannot give | Time; mark entries at actual near-close fill once ops ships | | **Better target model for clear-air names** | The return is demonstrably there (#2 wins on raw CAGR in *both* train and test); it's the *flat* 3× ATR target that makes it too expensive in risk | Needs a per-name model, not a constant k×ATR | @@ -169,6 +169,11 @@ knobs. 6. **Fill timing is part of the strategy.** Close-fill reports are not deployable numbers for an overnight scanner. Grade promotion under the fill mode you will actually trade. +7. **Incomplete research artifacts are not results.** The Phase B +0.0575 / t +5.12 + liquid-fip row was orphaned within hours: it raced a partially built + `research.sqlite`. Extender now writes a completion manifest; breadth mode + refuses without a match. Same class of protection as calendar-truncation + asserts — do not re-mythologize numbers computed on half a universe. --- diff --git a/docs/research/fip-breadth-ic.md b/docs/research/fip-breadth-ic.md index ca17663..3e75c1a 100644 --- a/docs/research/fip-breadth-ic.md +++ b/docs/research/fip-breadth-ic.md @@ -1,13 +1,15 @@ # Broad-universe fip_id IC research (Phase B) -**Status:** unconditional fip closed; mom-conditional lead confirmed on single-sourced path. -**Production impact:** none. Display card remains context-only. +**Status:** **Parked / closed for now.** Unconditional fip not green; mom-conditional lead logged; breadth-momentum thesis challenged. No book sim until reopen. +**Production impact:** none. Display card remains context-only. No deploy from this work. +**Artifacts:** research log + compact reports + env-gated harness hooks; tooling stays for a future reopen. ## Scope - Research only — production universe, gate, scanner, schedule unchanged. - Snapshot: `research.sqlite` (~4,650 tickers = prod + nasdaq_all extend). - Liquid mask: top **1,500** by point-in-time 63d median $vol, price ≥ **$5**/week. +- **Completion manifest required:** extender writes `.manifest.json`; breadth runners refuse without a matching complete manifest (see §Race guard). ## Caveats @@ -15,6 +17,7 @@ - IEX volume undercount → relative $vol rank only. - Pool skew: Nasdaq-heavy; missing pure NYSE mid-caps. - Do not mix multi-signal tables across universe baselines. +- **Do not cite orphaned 21:14 numbers** (see below). --- @@ -28,47 +31,83 @@ **Pass.** Formula + pipeline trustworthy. +Residual momentum on the same fingerprint (what the production book ranks on): **IC +0.055 / t +1.98**. + --- -## Discrepancy (must not be papered over) +## The orphan (21:14) — root cause | Source | fip IC (liquid ~1500) | t | |---|---:|---:| -| Report `fip-breadth-20260718-211440-breadth.json` | **+0.0575** | **+5.12** | -| Single-sourced recompute (2026-07-19) | **−0.0168** | **−1.85** | +| Orphan run 21:14 (removed from tree; was `fip-breadth-20260718-211440-breadth.json`) | **+0.0575** | **+5.12** | +| Single-sourced recompute on complete snapshot (2026-07-19) | **−0.0168** | **−1.85** | That is a **sign disagreement** on the same intended quantity. Method rule: the number you cannot reconcile is the number you cannot use. -### What we did +### Verdict: orphaned — raced the snapshot build -1. **Single-sourced the mask** — diagnostics call harness `_signal_series` + `_filter_liquid_breadth_week_rich` only (no parallel mask). -2. **Documented avg_cross_section semantics** — always **post-mask** IC sample size. -3. **Logged pre-mask stats** so “did top-N bind?” is answerable. +**Not** “orphaned, unexplained.” The mechanism is derivable from the table itself: -### Authoritative unconditional liquid fip (post-reconciliation) +1. **Code was not the difference.** Reconcile shows the old harness path and the new shared filter produce **identical** results on current data (−0.0168 / −1.85). The implementation fork is closed. +2. **Data was the difference.** On today’s complete snapshot the liquid mask **binds in 97.1% of weeks** at top-N = 1,500. Dense signals (e.g. `vol_6m`) post-mask at **exactly 1,500**. The orphaned report’s `vol_6m` averaged **~1,475** cross-section — a masked run on complete data cannot do that. At 21:14 the eligible pool was smaller than 1,500 and the mask never bound. +3. **Timeline fits.** Extender fixes landed ~20:32 / 20:34; full fetch takes ~30 minutes; breadth run fired **21:14** against a partially built `research.sqlite`. Every number in that report was computed on an incomplete universe. + +**Do not cite +0.0575 / t +5.12.** It survived less than six hours of contact with project discipline — that is the system working, not time wasted. The orphan JSON was **deleted from the tree** (still in Git history) so it cannot be re-imported as evidence. + +**Kept artifacts** + +| File | Role | +|---|---| +| `reports/fip-reconcile-20260719-000520.json` | Authoritative single-sourced ICs (compact; membership dumps stripped) | +| `reports/fip-breadth-20260718-211440-fingerprint.json` | Prod fingerprint pass | + +### Race guard (same class as calendar truncation) + +| Piece | Behavior | +|---|---| +| `extend_snapshot_universe.py` | Clears any prior manifest on start; on full completion writes `.manifest.json` with `complete=true`, ticker / OHLCV / rank_only counts, `finished_at`. `--limit` smoke runs write `complete=false`. | +| `run_fip_breadth_research.py` / `run_fip_breadth_diagnostics.py` | **Refuse** breadth mode unless a matching complete manifest exists and live counts equal the recorded totals. | + +Helper: `scripts/research_snapshot_manifest.py`. + +--- + +## Authoritative unconditional liquid fip (post-reconciliation) | metric | value | |---|---:| | mean_ic | **−0.0168** | | ic_t_stat | **−1.85** | | weeks | 35 | -| avg_cross_section (**post-mask**) | 1471.2 | +| avg_cross_section (**post-mask IC sample**) | 1471.2 | | avg_raw_pool | 3214.4 | | avg_eligible_pre_mask | **2338.4** | | mask_binds_pct | **97.1%** | | reliable | true | -**Mask binds hard** (eligible ≫ 1500). The hypothesis that “1471 meant the mask never bound / unmasked +5σ” is **false**. +**Mask binds hard** on complete data (eligible ≫ 1500). Post-mask IC N for fip is ~1471 because not every liquid name has a valid 12-1 fip path — that is signal availability, not a non-binding mask. Contrast orphan `vol_6m` avg N ~1475 vs complete-data `vol_6m` avg N **1500**. Harness `_signal_evaluation` vs manual IC through the same filter: **exact match** (−0.0168 / −1.85). -### Verdict on the orphan +**Iron rule unconditional:** **not green** (|IC| 0.017 < 0.03), correct mild-negative sign. -The **+0.0575 / t +5.12** row is **orphaned**. Do not cite it. Root cause of that single run is not fully forensic-reconstructed (no dual dump from the original process remains), but every single-sourced recompute on this snapshot lands near **−0.017**, and the tier blend (≈800×−0.035 + ≈670×+0.014)/1471 ≈ **−0.013** is internally consistent with that number—not with +0.058. +--- -**Iron rule unconditional:** still **not green** (|IC| 0.017 < 0.03), and now with the correct mild-negative sign. +## Context table (orphaned 21:14 vs authoritative) — kill the myth numbers -Artifact: `reports/fip-reconcile-20260719-000520.json` +The context table died with the orphan. **−0.16 must not survive in the log.** + +| signal (liquid ~1500) | orphaned (21:14) | authoritative (shared filter) | consequence | +|---|---:|---:|---| +| **vol_6m** | −0.16 / t **−6.1** | **−0.048 / t −1.36** | “High-vol tilt harmful on breadth” **downgrades from finding to directional hypothesis** — not significant | +| **raw mom** (`mom_12_1`) | +0.10 / t +4.6 | **+0.046 / t +1.91** | Below iron-rule bar on this pool | +| **resid mom** (`mom_12_1_resid`) | +0.04 / t +2.3 | **+0.029 / t +1.33** | Ditto, and weaker than raw | + +### Breadth-momentum thesis — challenged + +That last pair is the sobering one. Momentum on liquid breadth is **marginal**. The “more breadth strengthens the momentum t-stat” thesis that motivated Phase B is **empirically wrong on this pool**: same 35 weeks, triple the names, residual-mom t-stat **fell** versus the 505-name fingerprint (**0.055 / 1.98** → **0.029 / 1.33**). The clean momentum edge lives in the large-cap universe already traded. + +Meanwhile the strongest reliable signal on liquid breadth is now **mom-conditional fip** (−0.088 / −4.58) — but a fip tilt presupposes a breadth momentum book worth tilting, and that is no longer free. --- @@ -107,27 +146,57 @@ Computed on the **same single-sourced path** as the authoritative −0.017. This | Decision | | |---|---| | Unconditional fip | **Closed** for production | -| Mom-conditional fip | **Alive as book-tilt candidate only** — book sim before any gate talk | +| Mom-conditional fip | **Alive as book-tilt candidate only** — and only after a baseline breadth book proves itself | | Display card | Stays | | Production change | **None** | --- -## Vol-tilt / residual-mom warning (any future breadth move) +## Vol-tilt warning (softened) | signal (liquid, single-sourced) | IC | t | |---|---:|---:| -| vol_6m | −0.048 | −1.4 | -| mom_12_1 | +0.046 | +1.9 | -| mom_12_1_resid | +0.029 | +1.3 | +| vol_6m | −0.048 | **−1.36** | +| mom_12_1 | +0.046 | +1.91 | +| mom_12_1_resid | +0.029 | +1.33 | -High-vol names tend to underperform on this pool relative to a clean S&P-like book. Production **80/20 high-vol tilt** was validated on S&P-like names. **If the universe ever broadens in production, re-validate that tilt first** — it can flip from mildly helpful to harmful. Raw momentum also looks stronger than SPY residualization here (noisier fit for small caps). +High-vol names **tend** to underperform on this pool relative to a clean S&P-like book — that is a **directional hypothesis**, not a finding. Production **80/20 high-vol tilt** was validated on S&P-like names. If the universe ever broadens in production, re-validate that tilt; do not treat the orphaned −0.16 / t −6.1 as evidence. + +--- + +## What this means for the book experiment + +A fip tilt presupposes a breadth momentum book worth tilting — **that is no longer free.** + +**Caution against over-reacting the other way:** modest cross-sectional IC does not preclude a good book. The 505-name book turns resid-mom IC ~0.055 into Sharpe ~2 because the gate trades the **extreme tail**, not the linear sort. The breadth book might still work; it just has to **prove it** before the fip arm means anything. If the baseline cannot clearly beat the existing production book’s territory, fip’s future is a footnote regardless of −4.58. + +### Parked next step (if reopened): pre-registered two-arm design + +Not started — **design only**, pre-register before any sim: + +| Arm | Definition | +|---|---| +| **A — baseline** | Top-quintile residual (or raw — pick one and lock) momentum book on liquid-1500; **no fip**; honest costs; next-open or near-close fills; production-like capacity / risk / stops | +| **B — +fip tilt** | Same book + mom-conditional fip tilt (among mom winners, prefer smoother paths / negative fip_id) | + +| Grade on | Spec | +|---|---| +| Split | Entry-date train / validation (`BACKTEST_HOLDOUT_SPLIT` naming — not pristine holdout) | +| Metrics | Sharpe + Mertens/Lo SE, PSR, **DSR**; max DD; turnover; cost drag | +| Promote bar | Arm A must be in production-book territory first; Arm B must beat A on validation with DSR-aware multiple-testing honesty | +| Fail-closed | If A fails, fip is a footnote; do not shop tilts on a dead baseline | --- ## How to re-run (research branch only) ```powershell +# 1) Full extend writes completion manifest (required) +.\.venv\Scripts\python.exe scripts\extend_snapshot_universe.py ` + --source backtest_snapshots\prod.sqlite ` + --output backtest_snapshots\research.sqlite + +# 2) Breadth / diagnostics refuse without matching manifest .\.venv\Scripts\python.exe scripts\run_fip_breadth_diagnostics.py ` --research-snapshot backtest_snapshots\research.sqlite ` --prod-snapshot backtest_snapshots\prod.sqlite ` @@ -139,7 +208,9 @@ High-vol names tend to underperform on this pool relative to a clean S&P-like bo ## Bottom line 1. Formal iron-rule screen: **not green** either before or after reconciliation. -2. **+0.0575 / +5.12 is orphaned** — authoritative unconditional liquid fip is **−0.017 / −1.9**; mask binds (~97%). -3. Compositional tug-of-war is the right story; jumpiness premium is not. -4. **Mom-conditional −0.088 / −4.6 stands on the single-sourced path** → optional next research step is a **book** A/B, not a gate wire-in. -5. Log any future reader who sees both numbers: trust the reconcile artifact, not the orphaned breadth headline. +2. **+0.0575 / +5.12 is orphaned: raced the snapshot build** — authoritative unconditional liquid fip is **−0.017 / −1.9**; mask binds (~97%) on complete data. +3. Context-table myths die with the orphan: **vol −0.16 is not real**; authoritative vol is **−0.048 / t −1.36** (directional only). +4. Compositional tug-of-war is the right story; jumpiness premium is not. +5. **Breadth does not strengthen residual-mom t-stat** on this pool (0.055/1.98 → 0.029/1.33). +6. **Mom-conditional −0.088 / −4.6 stands** on the single-sourced path → optional next step is a **pre-registered two-arm breadth book** (baseline first), not a gate wire-in. +7. Manifest guard is in place so the race cannot recur silently. diff --git a/reports/README.md b/reports/README.md index 6d1697c..ebf4c94 100644 --- a/reports/README.md +++ b/reports/README.md @@ -41,3 +41,23 @@ rejected stop-adjustment path, and add no decision evidence beyond the final daily matrix and narrative. Their matching one-off runners were removed too. All remain recoverable from Git history. Rebuildable candidate pickle caches are intentionally ignored and must not be committed. + +### Phase B fip breadth IC (2026-07-18/19) — compact evidence + +Canonical artifacts: + +- `fip-reconcile-20260719-000520.json` — single-sourced authoritative ICs + (unconditional liquid fip, tiers, prod-subset, mom-conditional, context + signals). Membership symbol dumps stripped after the decision; narrative in + [`docs/research/fip-breadth-ic.md`](../docs/research/fip-breadth-ic.md). +- `fip-breadth-20260718-211440-fingerprint.json` — prod-snapshot fingerprint + pass (fip IC −0.045 / t −2.91). + +Removed as superseded / dangerous intermediate noise (recoverable from Git): + +- `fip-breadth-20260718-211440-breadth.json` (+ wrapper) — **orphaned** +0.0575 + / t +5.12 from racing a partial `research.sqlite`. Kept out of the tree so it + cannot be re-mythologized. +- `fip-breadth-20260718-194828*.json` — fingerprint-only partial run. +- `fip-breadth-diagnostics-20260718-213705.json` and `…-213908.json` — dual-path + diagnostics superseded by the single-sourced reconcile. diff --git a/reports/fip-breadth-20260718-194828-fingerprint.json b/reports/fip-breadth-20260718-194828-fingerprint.json deleted file mode 100644 index aeae3d0..0000000 --- a/reports/fip-breadth-20260718-194828-fingerprint.json +++ /dev/null @@ -1,578 +0,0 @@ -{ - "generated_at": "2026-07-18T18:21:28.359793+00:00", - "tickers": 506, - "rank_only_tickers": 0, - "candidates": 202765, - "qualified": 1086, - "params": { - "step_days": 5, - "step_sessions": 5, - "entry_cadence": "weekly", - "signal_eval_cadence": "weekly", - "horizon_days": 30, - "min_lookback": 60, - "cost_per_side_pct": 0.1, - "target_model": "production_gtl", - "target_model_label": "Live GTL (production)", - "is_production_target_model": true, - "production_reentry_policy": "gate_reset", - "liquid_breadth_top_n": null, - "liquid_min_price": null, - "signal_eval_only": true - }, - "activation": { - "min_momentum_percentile": 80.0, - "min_rr": 2.0, - "min_confidence": 0.0, - "require_high_conviction": false, - "exclude_conflicts": false, - "exclude_neutral": true - }, - "overall_qualified": { - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049 - }, - "overall_all": { - "total": 202765, - "wins": 82220, - "losses": 113809, - "expired": 6736, - "hit_rate": 41.9, - "avg_r": -0.04, - "total_r": -8186.18, - "net_avg_r": -0.095, - "net_total_r": -19184.55, - "best_r": 9.24, - "worst_r": -16.42, - "avg_hold_days": 8.2, - "net_r_per_day": -0.0115, - "median_net_r": -1.035, - "profit_factor": 0.85, - "net_avg_r_ex_top5": -0.22 - }, - "by_direction": { - "long": { - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049 - }, - "short": { - "total": 0, - "wins": 0, - "losses": 0, - "expired": 0, - "hit_rate": null, - "avg_r": null, - "total_r": null, - "net_avg_r": null, - "net_total_r": null, - "best_r": null, - "worst_r": null, - "avg_hold_days": null, - "net_r_per_day": null, - "median_net_r": null, - "profit_factor": null, - "net_avg_r_ex_top5": null - } - }, - "min_momentum_percentile": 80.0, - "sweep": [ - { - "min_momentum_percentile": 90.0, - "total": 497, - "wins": 177, - "losses": 269, - "expired": 51, - "hit_rate": 39.7, - "avg_r": 0.276, - "total_r": 137.05, - "net_avg_r": 0.235, - "net_total_r": 116.55, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 11.8, - "net_r_per_day": 0.0199, - "median_net_r": -1.026, - "profit_factor": 1.39, - "net_avg_r_ex_top5": 0.071 - }, - { - "min_momentum_percentile": 80.0, - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049 - }, - { - "min_momentum_percentile": 70.0, - "total": 1841, - "wins": 597, - "losses": 1062, - "expired": 182, - "hit_rate": 36.0, - "avg_r": 0.152, - "total_r": 280.26, - "net_avg_r": 0.104, - "net_total_r": 190.75, - "best_r": 8.85, - "worst_r": -4.21, - "avg_hold_days": 11.8, - "net_r_per_day": 0.0088, - "median_net_r": -1.037, - "profit_factor": 1.16, - "net_avg_r_ex_top5": -0.055 - }, - { - "min_momentum_percentile": 60.0, - "total": 2772, - "wins": 873, - "losses": 1611, - "expired": 288, - "hit_rate": 35.1, - "avg_r": 0.126, - "total_r": 348.07, - "net_avg_r": 0.075, - "net_total_r": 209.25, - "best_r": 8.85, - "worst_r": -4.21, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0063, - "median_net_r": -1.04, - "profit_factor": 1.12, - "net_avg_r_ex_top5": -0.077 - }, - { - "min_momentum_percentile": 50.0, - "total": 3901, - "wins": 1182, - "losses": 2295, - "expired": 424, - "hit_rate": 34.0, - "avg_r": 0.089, - "total_r": 345.37, - "net_avg_r": 0.038, - "net_total_r": 146.96, - "best_r": 8.85, - "worst_r": -4.86, - "avg_hold_days": 12.1, - "net_r_per_day": 0.0031, - "median_net_r": -1.042, - "profit_factor": 1.06, - "net_avg_r_ex_top5": -0.114 - }, - { - "min_momentum_percentile": 0.0, - "total": 14588, - "wins": 3719, - "losses": 9271, - "expired": 1598, - "hit_rate": 28.6, - "avg_r": -0.065, - "total_r": -952.08, - "net_avg_r": -0.115, - "net_total_r": -1676.86, - "best_r": 9.24, - "worst_r": -15.94, - "avg_hold_days": 12.1, - "net_r_per_day": -0.0095, - "median_net_r": -1.043, - "profit_factor": 0.84, - "net_avg_r_ex_top5": -0.275 - } - ], - "gate_ablation": [ - { - "variant": "all_floors", - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049, - "hold_days": 30, - "hold_avg_r": 0.631, - "hold_net_avg_r": 0.585, - "hold_total_r": 684.97 - }, - { - "variant": "no_confidence_floor", - "total": 1093, - "wins": 380, - "losses": 596, - "expired": 117, - "hit_rate": 38.9, - "avg_r": 0.25, - "total_r": 273.55, - "net_avg_r": 0.204, - "net_total_r": 222.99, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 11.9, - "net_r_per_day": 0.0171, - "median_net_r": -1.031, - "profit_factor": 1.33, - "net_avg_r_ex_top5": 0.045, - "hold_days": 30, - "hold_avg_r": 0.626, - "hold_net_avg_r": 0.58, - "hold_total_r": 684.08 - }, - { - "variant": "no_rr_floor", - "total": 6849, - "wins": 3235, - "losses": 3409, - "expired": 205, - "hit_rate": 48.7, - "avg_r": 0.112, - "total_r": 770.17, - "net_avg_r": 0.061, - "net_total_r": 418.96, - "best_r": 8.85, - "worst_r": -5.16, - "avg_hold_days": 7.9, - "net_r_per_day": 0.0078, - "median_net_r": -0.061, - "profit_factor": 1.11, - "net_avg_r_ex_top5": -0.061, - "hold_days": 30, - "hold_avg_r": 0.354, - "hold_net_avg_r": 0.303, - "hold_total_r": 2425.86 - }, - { - "variant": "no_neutral_exclusion", - "total": 2313, - "wins": 770, - "losses": 1279, - "expired": 264, - "hit_rate": 37.6, - "avg_r": 0.2, - "total_r": 462.35, - "net_avg_r": 0.154, - "net_total_r": 355.89, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.6, - "net_r_per_day": 0.0122, - "median_net_r": -1.031, - "profit_factor": 1.25, - "net_avg_r_ex_top5": 0.004, - "hold_days": 30, - "hold_avg_r": 0.583, - "hold_net_avg_r": 0.537, - "hold_total_r": 1348.89 - }, - { - "variant": "momentum_only", - "total": 14696, - "wins": 6827, - "losses": 7359, - "expired": 510, - "hit_rate": 48.1, - "avg_r": 0.114, - "total_r": 1669.64, - "net_avg_r": 0.064, - "net_total_r": 936.93, - "best_r": 8.85, - "worst_r": -5.71, - "avg_hold_days": 8.4, - "net_r_per_day": 0.0076, - "median_net_r": -1.013, - "profit_factor": 1.11, - "net_avg_r_ex_top5": -0.055, - "hold_days": 30, - "hold_avg_r": 0.395, - "hold_net_avg_r": 0.345, - "hold_total_r": 5798.82 - } - ], - "gate_ablation_note": "Each row re-qualifies the same candidates at the current momentum cutoff (80) with one floor removed (long-only while the momentum gate is active). If dropping a floor doesn't hurt net expectancy, that floor isn't pulling its weight. The Hold columns grade the same variants under the hold-to-horizon time exit instead of the S/R target \u2014 the view that matters if the exit policy moves to a fixed hold.", - "time_exit_sweep": [ - { - "hold_days": 5, - "total": 1086, - "wins": 603, - "win_rate": 55.5, - "avg_r": 0.175, - "total_r": 190.16, - "net_avg_r": 0.129, - "net_total_r": 139.97, - "best_r": 5.09, - "worst_r": -2.51, - "avg_hold_days": 4.5, - "net_r_per_day": 0.0285, - "median_net_r": 0.115, - "profit_factor": 1.36, - "net_avg_r_ex_top5": -0.002 - }, - { - "hold_days": 10, - "total": 1086, - "wins": 559, - "win_rate": 51.5, - "avg_r": 0.357, - "total_r": 387.9, - "net_avg_r": 0.311, - "net_total_r": 337.7, - "best_r": 6.73, - "worst_r": -2.51, - "avg_hold_days": 7.9, - "net_r_per_day": 0.0395, - "median_net_r": 0.031, - "profit_factor": 1.67, - "net_avg_r_ex_top5": 0.112 - }, - { - "hold_days": 21, - "total": 1086, - "wins": 487, - "win_rate": 44.8, - "avg_r": 0.525, - "total_r": 570.33, - "net_avg_r": 0.479, - "net_total_r": 520.14, - "best_r": 9.86, - "worst_r": -3.38, - "avg_hold_days": 13.7, - "net_r_per_day": 0.0349, - "median_net_r": -1.027, - "profit_factor": 1.81, - "net_avg_r_ex_top5": 0.191 - }, - { - "hold_days": 30, - "total": 1086, - "wins": 434, - "win_rate": 40.0, - "avg_r": 0.631, - "total_r": 684.97, - "net_avg_r": 0.585, - "net_total_r": 634.78, - "best_r": 12.87, - "worst_r": -3.38, - "avg_hold_days": 17.8, - "net_r_per_day": 0.0329, - "median_net_r": -1.033, - "profit_factor": 1.9, - "net_avg_r_ex_top5": 0.212 - } - ], - "portfolio_sim": { - "params": { - "starting_capital": 10000.0, - "max_positions": 10, - "risk_per_trade_pct": 1.0, - "notional_cap_pct": 20.0, - "cost_per_side_pct": 0.1, - "hold_days": 30 - }, - "policies": [], - "note": "One capital-constrained book over the same qualified setups the tables above grade per-setup: at most 10 concurrent positions (one per ticker), best momentum first, fixed-fractional risk sizing with a no-leverage cap, entries at the detection close, stops filled at the worse of stop or open. 'target' races the S/R target against the stop (timeout at the horizon); 'hold' keeps the initial stop and exits at the horizon close. SPY return is price-only over the same window. In-sample; no dividends." - }, - "strategy_variants": { - "variants": [], - "note": "Research-only hold-to-horizon portfolio variants. Production now uses residual 12-1 momentum at cutoff 80; the remaining rows compare the legacy raw rank, raw cutoff 90, one max-15 capacity check, and volatility overlays." - }, - "exit_policy_variants": { - "variants": [], - "note": "Research-only exit policies over the residual/high-vol 80/20 entry candidate. Every row uses the same entry qualification/ranking and changes only the exit discipline." - }, - "portfolio_monitor": null, - "production_cadence_comparison": null, - "holdout": null, - "min_rr_sweep": null, - "target_model_diagnostics": { - "target_model": "production_gtl", - "target_model_label": "Live GTL (production)", - "candidate_count": 202765, - "primary_source_counts": { - "pivot_point": 196290, - "range_grid": 180036 - }, - "primary_round_only": 0, - "primary_strength_100": 138596, - "avg_primary_strength": 80.109, - "avg_primary_distance_atr": 2.293, - "avg_primary_rejection_count": 41.908, - "avg_raw_level_count": 53.204, - "avg_gate_level_count": 53.204 - }, - "signal_eval": [ - { - "signal": "vol_6m", - "weeks": 39, - "avg_cross_section": 498.2, - "mean_ic": 0.0609, - "ic_t_stat": 1.48, - "ic_positive_pct": 64.1, - "mean_quintile_spread": 0.0337, - "reliable": true - }, - { - "signal": "mom_12_1_resid", - "weeks": 35, - "avg_cross_section": 497.7, - "mean_ic": 0.0552, - "ic_t_stat": 1.98, - "ic_positive_pct": 60.0, - "mean_quintile_spread": 0.0207, - "reliable": true - }, - { - "signal": "mom_12_1", - "weeks": 35, - "avg_cross_section": 497.7, - "mean_ic": 0.0531, - "ic_t_stat": 1.61, - "ic_positive_pct": 65.7, - "mean_quintile_spread": 0.0206, - "reliable": true - }, - { - "signal": "trend_200", - "weeks": 37, - "avg_cross_section": 497.9, - "mean_ic": 0.0161, - "ic_t_stat": 0.44, - "ic_positive_pct": 59.5, - "mean_quintile_spread": 0.006, - "reliable": true - }, - { - "signal": "reversal_1m", - "weeks": 43, - "avg_cross_section": 498.7, - "mean_ic": 0.0059, - "ic_t_stat": 0.22, - "ic_positive_pct": 53.5, - "mean_quintile_spread": 0.0053, - "reliable": true - }, - { - "signal": "mom_6_1", - "weeks": 39, - "avg_cross_section": 498.2, - "mean_ic": 0.0051, - "ic_t_stat": 0.21, - "ic_positive_pct": 56.4, - "mean_quintile_spread": 0.0087, - "reliable": true - }, - { - "signal": "mom_3_1", - "weeks": 42, - "avg_cross_section": 498.5, - "mean_ic": -0.0064, - "ic_t_stat": -0.25, - "ic_positive_pct": 50.0, - "mean_quintile_spread": 0.0046, - "reliable": true - }, - { - "signal": "high_52w", - "weeks": 35, - "avg_cross_section": 497.7, - "mean_ic": -0.0086, - "ic_t_stat": -0.26, - "ic_positive_pct": 54.3, - "mean_quintile_spread": -0.0088, - "reliable": true - }, - { - "signal": "fip_id", - "weeks": 35, - "avg_cross_section": 497.7, - "mean_ic": -0.045, - "ic_t_stat": -2.91, - "ic_positive_pct": 25.7, - "mean_quintile_spread": -0.0168, - "reliable": true - } - ], - "signal_eval_note": "Cross-sectional rank-IC of price-only signals vs the forward 30-day return (min 20 names/window). |IC| \u2273 0.03 with a consistent sign is a real (if small) edge; near 0 means ranking on it sorts nothing. Momentum factors and high_52w are expected positive; reversal_1m and vol_6m expected negative (mean-reversion / low-vol anomaly). IC is measured on non-overlapping windows; signals with fewer than 12 independent windows are flagged unreliable (too few regimes \u2014 deepen history with the Data Backfill job).", - "note": "Sentiment & fundamentals held neutral (no point-in-time history). Stops fill at the worse of the stop or the bar's open (gaps through the stop are modeled, so a loss can exceed \u22121R); targets never fill better than their level. ~6 months \u2248 one market regime \u2014 treat as directional, not gospel.", - "recommendation": { - "headline": "Trade the qualified list long-only; hold 30 trading days with the initial ATR stop.", - "items": [ - { - "topic": "exit", - "text": "Legacy exit diagnostic: hold 30 trading days with the initial stop (+0.58R net/trade vs +0.21R for the S/R target exit)." - }, - { - "topic": "gate", - "text": "Gate: the confidence floor adds nothing \u2014 dropping it costs +0.01R/trade and adds 7 trades." - }, - { - "topic": "gate", - "text": "Gate: keep the R:R floor (worth +0.28R/trade under the hold exit)." - }, - { - "topic": "gate", - "text": "Gate: keep the NEUTRAL exclusion (worth +0.05R/trade under the hold exit)." - }, - { - "topic": "cutoff", - "text": "Residual-momentum cutoff: 90 has the best per-trade net (+0.23R over 497 setups)." - }, - { - "topic": "robustness", - "text": "Robustness: expectancy survives removing the top 5% of winners (+0.21R net/trade under the recommended 30d hold) \u2014 the edge is not a handful of outliers." - } - ], - "note": "Derived from this report's numbers on every run \u2014 the advice flips if the data does." - }, - "research_recommendation": { - "items": [], - "note": "Strategy variants unavailable; re-run the backtest after benchmark data is present." - } -} \ No newline at end of file diff --git a/reports/fip-breadth-20260718-194828.json b/reports/fip-breadth-20260718-194828.json deleted file mode 100644 index c6c1163..0000000 --- a/reports/fip-breadth-20260718-194828.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "generated_at": "2026-07-18T19:48:28.127710", - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0, - "fingerprint": { - "signal": "fip_id", - "weeks": 35, - "avg_cross_section": 497.7, - "mean_ic": -0.045, - "ic_t_stat": -2.91, - "ic_positive_pct": 25.7, - "mean_quintile_spread": -0.0168, - "reliable": true, - "pass": true, - "expected_ic": -0.045, - "expected_t": -2.9 - }, - "breadth": null, - "verdict": null, - "fingerprint_report_path": "reports\\fip-breadth-20260718-194828-fingerprint.json" -} \ No newline at end of file diff --git a/reports/fip-breadth-20260718-211440-breadth.json b/reports/fip-breadth-20260718-211440-breadth.json deleted file mode 100644 index c586f74..0000000 --- a/reports/fip-breadth-20260718-211440-breadth.json +++ /dev/null @@ -1,596 +0,0 @@ -{ - "generated_at": "2026-07-18T19:23:17.726528+00:00", - "tickers": 4650, - "rank_only_tickers": 4144, - "candidates": 202769, - "qualified": 1086, - "params": { - "step_days": 5, - "step_sessions": 5, - "entry_cadence": "weekly", - "signal_eval_cadence": "weekly", - "horizon_days": 30, - "min_lookback": 60, - "cost_per_side_pct": 0.1, - "target_model": "production_gtl", - "target_model_label": "Live GTL (production)", - "is_production_target_model": true, - "production_reentry_policy": "gate_reset", - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0, - "signal_eval_only": true - }, - "activation": { - "min_momentum_percentile": 80.0, - "min_rr": 2.0, - "min_confidence": 0.0, - "require_high_conviction": false, - "exclude_conflicts": false, - "exclude_neutral": true - }, - "overall_qualified": { - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049 - }, - "overall_all": { - "total": 202769, - "wins": 82221, - "losses": 113812, - "expired": 6736, - "hit_rate": 41.9, - "avg_r": -0.04, - "total_r": -8188.17, - "net_avg_r": -0.095, - "net_total_r": -19186.68, - "best_r": 9.24, - "worst_r": -16.42, - "avg_hold_days": 8.2, - "net_r_per_day": -0.0115, - "median_net_r": -1.035, - "profit_factor": 0.85, - "net_avg_r_ex_top5": -0.22 - }, - "by_direction": { - "long": { - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049 - }, - "short": { - "total": 0, - "wins": 0, - "losses": 0, - "expired": 0, - "hit_rate": null, - "avg_r": null, - "total_r": null, - "net_avg_r": null, - "net_total_r": null, - "best_r": null, - "worst_r": null, - "avg_hold_days": null, - "net_r_per_day": null, - "median_net_r": null, - "profit_factor": null, - "net_avg_r_ex_top5": null - } - }, - "min_momentum_percentile": 80.0, - "sweep": [ - { - "min_momentum_percentile": 90.0, - "total": 497, - "wins": 177, - "losses": 269, - "expired": 51, - "hit_rate": 39.7, - "avg_r": 0.276, - "total_r": 137.05, - "net_avg_r": 0.235, - "net_total_r": 116.55, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 11.8, - "net_r_per_day": 0.0199, - "median_net_r": -1.026, - "profit_factor": 1.39, - "net_avg_r_ex_top5": 0.071 - }, - { - "min_momentum_percentile": 80.0, - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049 - }, - { - "min_momentum_percentile": 70.0, - "total": 1841, - "wins": 597, - "losses": 1062, - "expired": 182, - "hit_rate": 36.0, - "avg_r": 0.152, - "total_r": 280.26, - "net_avg_r": 0.104, - "net_total_r": 190.75, - "best_r": 8.85, - "worst_r": -4.21, - "avg_hold_days": 11.8, - "net_r_per_day": 0.0088, - "median_net_r": -1.037, - "profit_factor": 1.16, - "net_avg_r_ex_top5": -0.055 - }, - { - "min_momentum_percentile": 60.0, - "total": 2772, - "wins": 873, - "losses": 1611, - "expired": 288, - "hit_rate": 35.1, - "avg_r": 0.126, - "total_r": 348.07, - "net_avg_r": 0.075, - "net_total_r": 209.25, - "best_r": 8.85, - "worst_r": -4.21, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0063, - "median_net_r": -1.04, - "profit_factor": 1.12, - "net_avg_r_ex_top5": -0.077 - }, - { - "min_momentum_percentile": 50.0, - "total": 3901, - "wins": 1182, - "losses": 2295, - "expired": 424, - "hit_rate": 34.0, - "avg_r": 0.089, - "total_r": 345.37, - "net_avg_r": 0.038, - "net_total_r": 146.96, - "best_r": 8.85, - "worst_r": -4.86, - "avg_hold_days": 12.1, - "net_r_per_day": 0.0031, - "median_net_r": -1.042, - "profit_factor": 1.06, - "net_avg_r_ex_top5": -0.114 - }, - { - "min_momentum_percentile": 0.0, - "total": 14589, - "wins": 3719, - "losses": 9272, - "expired": 1598, - "hit_rate": 28.6, - "avg_r": -0.065, - "total_r": -953.08, - "net_avg_r": -0.115, - "net_total_r": -1677.9, - "best_r": 9.24, - "worst_r": -15.94, - "avg_hold_days": 12.1, - "net_r_per_day": -0.0095, - "median_net_r": -1.043, - "profit_factor": 0.84, - "net_avg_r_ex_top5": -0.275 - } - ], - "gate_ablation": [ - { - "variant": "all_floors", - "total": 1086, - "wins": 379, - "losses": 591, - "expired": 116, - "hit_rate": 39.1, - "avg_r": 0.255, - "total_r": 276.76, - "net_avg_r": 0.209, - "net_total_r": 226.56, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.0, - "net_r_per_day": 0.0174, - "median_net_r": -1.031, - "profit_factor": 1.34, - "net_avg_r_ex_top5": 0.049, - "hold_days": 30, - "hold_avg_r": 0.631, - "hold_net_avg_r": 0.585, - "hold_total_r": 684.97 - }, - { - "variant": "no_confidence_floor", - "total": 1093, - "wins": 380, - "losses": 596, - "expired": 117, - "hit_rate": 38.9, - "avg_r": 0.25, - "total_r": 273.55, - "net_avg_r": 0.204, - "net_total_r": 222.99, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 11.9, - "net_r_per_day": 0.0171, - "median_net_r": -1.031, - "profit_factor": 1.33, - "net_avg_r_ex_top5": 0.045, - "hold_days": 30, - "hold_avg_r": 0.626, - "hold_net_avg_r": 0.58, - "hold_total_r": 684.08 - }, - { - "variant": "no_rr_floor", - "total": 6849, - "wins": 3235, - "losses": 3409, - "expired": 205, - "hit_rate": 48.7, - "avg_r": 0.112, - "total_r": 770.17, - "net_avg_r": 0.061, - "net_total_r": 418.96, - "best_r": 8.85, - "worst_r": -5.16, - "avg_hold_days": 7.9, - "net_r_per_day": 0.0078, - "median_net_r": -0.061, - "profit_factor": 1.11, - "net_avg_r_ex_top5": -0.061, - "hold_days": 30, - "hold_avg_r": 0.354, - "hold_net_avg_r": 0.303, - "hold_total_r": 2425.86 - }, - { - "variant": "no_neutral_exclusion", - "total": 2313, - "wins": 770, - "losses": 1279, - "expired": 264, - "hit_rate": 37.6, - "avg_r": 0.2, - "total_r": 462.35, - "net_avg_r": 0.154, - "net_total_r": 355.89, - "best_r": 8.85, - "worst_r": -3.38, - "avg_hold_days": 12.6, - "net_r_per_day": 0.0122, - "median_net_r": -1.031, - "profit_factor": 1.25, - "net_avg_r_ex_top5": 0.004, - "hold_days": 30, - "hold_avg_r": 0.583, - "hold_net_avg_r": 0.537, - "hold_total_r": 1348.89 - }, - { - "variant": "momentum_only", - "total": 14696, - "wins": 6827, - "losses": 7359, - "expired": 510, - "hit_rate": 48.1, - "avg_r": 0.114, - "total_r": 1669.64, - "net_avg_r": 0.064, - "net_total_r": 936.93, - "best_r": 8.85, - "worst_r": -5.71, - "avg_hold_days": 8.4, - "net_r_per_day": 0.0076, - "median_net_r": -1.013, - "profit_factor": 1.11, - "net_avg_r_ex_top5": -0.055, - "hold_days": 30, - "hold_avg_r": 0.395, - "hold_net_avg_r": 0.345, - "hold_total_r": 5798.82 - } - ], - "gate_ablation_note": "Each row re-qualifies the same candidates at the current momentum cutoff (80) with one floor removed (long-only while the momentum gate is active). If dropping a floor doesn't hurt net expectancy, that floor isn't pulling its weight. The Hold columns grade the same variants under the hold-to-horizon time exit instead of the S/R target \u2014 the view that matters if the exit policy moves to a fixed hold.", - "time_exit_sweep": [ - { - "hold_days": 5, - "total": 1086, - "wins": 603, - "win_rate": 55.5, - "avg_r": 0.175, - "total_r": 190.16, - "net_avg_r": 0.129, - "net_total_r": 139.97, - "best_r": 5.09, - "worst_r": -2.51, - "avg_hold_days": 4.5, - "net_r_per_day": 0.0285, - "median_net_r": 0.115, - "profit_factor": 1.36, - "net_avg_r_ex_top5": -0.002 - }, - { - "hold_days": 10, - "total": 1086, - "wins": 559, - "win_rate": 51.5, - "avg_r": 0.357, - "total_r": 387.9, - "net_avg_r": 0.311, - "net_total_r": 337.7, - "best_r": 6.73, - "worst_r": -2.51, - "avg_hold_days": 7.9, - "net_r_per_day": 0.0395, - "median_net_r": 0.031, - "profit_factor": 1.67, - "net_avg_r_ex_top5": 0.112 - }, - { - "hold_days": 21, - "total": 1086, - "wins": 487, - "win_rate": 44.8, - "avg_r": 0.525, - "total_r": 570.33, - "net_avg_r": 0.479, - "net_total_r": 520.14, - "best_r": 9.86, - "worst_r": -3.38, - "avg_hold_days": 13.7, - "net_r_per_day": 0.0349, - "median_net_r": -1.027, - "profit_factor": 1.81, - "net_avg_r_ex_top5": 0.191 - }, - { - "hold_days": 30, - "total": 1086, - "wins": 434, - "win_rate": 40.0, - "avg_r": 0.631, - "total_r": 684.97, - "net_avg_r": 0.585, - "net_total_r": 634.78, - "best_r": 12.87, - "worst_r": -3.38, - "avg_hold_days": 17.8, - "net_r_per_day": 0.0329, - "median_net_r": -1.033, - "profit_factor": 1.9, - "net_avg_r_ex_top5": 0.212 - } - ], - "portfolio_sim": { - "params": { - "starting_capital": 10000.0, - "max_positions": 10, - "risk_per_trade_pct": 1.0, - "notional_cap_pct": 20.0, - "cost_per_side_pct": 0.1, - "hold_days": 30 - }, - "policies": [], - "note": "One capital-constrained book over the same qualified setups the tables above grade per-setup: at most 10 concurrent positions (one per ticker), best momentum first, fixed-fractional risk sizing with a no-leverage cap, entries at the detection close, stops filled at the worse of stop or open. 'target' races the S/R target against the stop (timeout at the horizon); 'hold' keeps the initial stop and exits at the horizon close. SPY return is price-only over the same window. In-sample; no dividends." - }, - "strategy_variants": { - "variants": [], - "note": "Research-only hold-to-horizon portfolio variants. Production now uses residual 12-1 momentum at cutoff 80; the remaining rows compare the legacy raw rank, raw cutoff 90, one max-15 capacity check, and volatility overlays." - }, - "exit_policy_variants": { - "variants": [], - "note": "Research-only exit policies over the residual/high-vol 80/20 entry candidate. Every row uses the same entry qualification/ranking and changes only the exit discipline." - }, - "portfolio_monitor": null, - "production_cadence_comparison": null, - "holdout": null, - "min_rr_sweep": null, - "target_model_diagnostics": { - "target_model": "production_gtl", - "target_model_label": "Live GTL (production)", - "candidate_count": 202769, - "primary_source_counts": { - "pivot_point": 196294, - "range_grid": 180039 - }, - "primary_round_only": 0, - "primary_strength_100": 138599, - "avg_primary_strength": 80.109, - "avg_primary_distance_atr": 2.293, - "avg_primary_rejection_count": 41.907, - "avg_raw_level_count": 53.204, - "avg_gate_level_count": 53.204 - }, - "signal_eval": [ - { - "signal": "high_52w", - "weeks": 35, - "avg_cross_section": 1471.2, - "mean_ic": 0.1283, - "ic_t_stat": 4.28, - "ic_positive_pct": 85.7, - "mean_quintile_spread": -0.1009, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "mom_12_1", - "weeks": 35, - "avg_cross_section": 1471.2, - "mean_ic": 0.0997, - "ic_t_stat": 4.56, - "ic_positive_pct": 88.6, - "mean_quintile_spread": -0.1001, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "mom_6_1", - "weeks": 40, - "avg_cross_section": 1474.8, - "mean_ic": 0.0681, - "ic_t_stat": 3.45, - "ic_positive_pct": 77.5, - "mean_quintile_spread": -0.0322, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "fip_id", - "weeks": 35, - "avg_cross_section": 1471.2, - "mean_ic": 0.0575, - "ic_t_stat": 5.12, - "ic_positive_pct": 88.6, - "mean_quintile_spread": 0.0199, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "trend_200", - "weeks": 37, - "avg_cross_section": 1472.8, - "mean_ic": 0.0538, - "ic_t_stat": 2.33, - "ic_positive_pct": 75.7, - "mean_quintile_spread": -0.0675, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "mom_3_1", - "weeks": 42, - "avg_cross_section": 1476.0, - "mean_ic": 0.0523, - "ic_t_stat": 3.27, - "ic_positive_pct": 73.8, - "mean_quintile_spread": -0.0194, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "mom_12_1_resid", - "weeks": 35, - "avg_cross_section": 1471.2, - "mean_ic": 0.0388, - "ic_t_stat": 2.28, - "ic_positive_pct": 74.3, - "mean_quintile_spread": -0.0542, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "reversal_1m", - "weeks": 43, - "avg_cross_section": 1476.6, - "mean_ic": 0.0155, - "ic_t_stat": 0.85, - "ic_positive_pct": 48.8, - "mean_quintile_spread": -0.0862, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - { - "signal": "vol_6m", - "weeks": 40, - "avg_cross_section": 1474.8, - "mean_ic": -0.1584, - "ic_t_stat": -6.05, - "ic_positive_pct": 12.5, - "mean_quintile_spread": 0.0164, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - } - ], - "signal_eval_note": "Cross-sectional rank-IC of price-only signals vs the forward 30-day return (min 20 names/window). |IC| \u2273 0.03 with a consistent sign is a real (if small) edge; near 0 means ranking on it sorts nothing. Momentum factors and high_52w are expected positive; reversal_1m and vol_6m expected negative (mean-reversion / low-vol anomaly). IC is measured on non-overlapping windows; signals with fewer than 12 independent windows are flagged unreliable (too few regimes \u2014 deepen history with the Data Backfill job).", - "note": "Sentiment & fundamentals held neutral (no point-in-time history). Stops fill at the worse of the stop or the bar's open (gaps through the stop are modeled, so a loss can exceed \u22121R); targets never fill better than their level. ~6 months \u2248 one market regime \u2014 treat as directional, not gospel.", - "recommendation": { - "headline": "Trade the qualified list long-only; hold 30 trading days with the initial ATR stop.", - "items": [ - { - "topic": "exit", - "text": "Legacy exit diagnostic: hold 30 trading days with the initial stop (+0.58R net/trade vs +0.21R for the S/R target exit)." - }, - { - "topic": "gate", - "text": "Gate: the confidence floor adds nothing \u2014 dropping it costs +0.01R/trade and adds 7 trades." - }, - { - "topic": "gate", - "text": "Gate: keep the R:R floor (worth +0.28R/trade under the hold exit)." - }, - { - "topic": "gate", - "text": "Gate: keep the NEUTRAL exclusion (worth +0.05R/trade under the hold exit)." - }, - { - "topic": "cutoff", - "text": "Residual-momentum cutoff: 90 has the best per-trade net (+0.23R over 497 setups)." - }, - { - "topic": "robustness", - "text": "Robustness: expectancy survives removing the top 5% of winners (+0.21R net/trade under the recommended 30d hold) \u2014 the edge is not a handful of outliers." - } - ], - "note": "Derived from this report's numbers on every run \u2014 the advice flips if the data does." - }, - "research_recommendation": { - "items": [], - "note": "Strategy variants unavailable; re-run the backtest after benchmark data is present." - } -} \ No newline at end of file diff --git a/reports/fip-breadth-20260718-211440.json b/reports/fip-breadth-20260718-211440.json deleted file mode 100644 index 5211c38..0000000 --- a/reports/fip-breadth-20260718-211440.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "generated_at": "2026-07-18T21:14:40.170961", - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0, - "fingerprint": { - "signal": "fip_id", - "weeks": 35, - "avg_cross_section": 497.7, - "mean_ic": -0.045, - "ic_t_stat": -2.91, - "ic_positive_pct": 25.7, - "mean_quintile_spread": -0.0168, - "reliable": true, - "pass": true, - "expected_ic": -0.045, - "expected_t": -2.9 - }, - "breadth": { - "signal": "fip_id", - "weeks": 35, - "avg_cross_section": 1471.2, - "mean_ic": 0.0575, - "ic_t_stat": 5.12, - "ic_positive_pct": 88.6, - "mean_quintile_spread": 0.0199, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - }, - "verdict": { - "green": false, - "reason": "iron rule not met on liquid-breadth cross-section", - "checks": { - "mean_ic": 0.0575, - "abs_mean_ic_ge_0_03": true, - "sign_negative": false, - "ic_t_stat": 5.12, - "reliable": true, - "weeks": 35, - "avg_cross_section": 1471.2 - }, - "row": { - "signal": "fip_id", - "weeks": 35, - "avg_cross_section": 1471.2, - "mean_ic": 0.0575, - "ic_t_stat": 5.12, - "ic_positive_pct": 88.6, - "mean_quintile_spread": 0.0199, - "reliable": true, - "liquid_breadth_top_n": 1500, - "liquid_min_price": 5.0 - } - }, - "fingerprint_report_path": "reports/fip-breadth-20260718-211440-fingerprint.json", - "breadth_report_path": "reports/fip-breadth-20260718-211440-breadth.json", - "breadth_tickers": 4650, - "breadth_rank_only_tickers": 4144 -} \ No newline at end of file diff --git a/reports/fip-breadth-diagnostics-20260718-213705.json b/reports/fip-breadth-diagnostics-20260718-213705.json deleted file mode 100644 index 83bc087..0000000 --- a/reports/fip-breadth-diagnostics-20260718-213705.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "generated_at": "2026-07-18T21:37:04.615484", - "research_snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\research.sqlite", - "prod_subset_n": 506, - "panel_tickers": 4403, - "top_n": 1500, - "min_price": 5.0, - "checks": { - "fip_same_week_liquid_1500": { - "note": "Replication of main breadth run (same-week $vol mask)", - "mean_ic": -0.0168, - "ic_t_stat": -1.85, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 40.0, - "reliable": true - }, - "fip_lagged_membership_1w": { - "note": "Liquid top-N ranked on *prior* week's median $vol \u2014 excludes same-week liquidity explosion leak", - "mean_ic": -0.0102, - "ic_t_stat": -0.93, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 40.0, - "reliable": true - }, - "fip_tier_1_800": { - "note": "Same-week liquid ranks 1\u2013800 (senior liquid tier)", - "mean_ic": -0.035, - "ic_t_stat": -2.99, - "weeks": 35, - "avg_cross_section": 791.2, - "ic_positive_pct": 25.7, - "reliable": true - }, - "fip_tier_801_1500": { - "note": "Same-week liquid ranks 801\u20131500 (junior liquid tier)", - "mean_ic": 0.0141, - "ic_t_stat": 1.25, - "weeks": 35, - "avg_cross_section": 700.0, - "ic_positive_pct": 60.0, - "reliable": true - }, - "fip_prod_universe_subset": { - "note": "Symbols in prod.sqlite (~S&P-like large-cap book) inside same-week liquid top-N \u2014 compositional control", - "mean_ic": -0.0444, - "ic_t_stat": -2.88, - "weeks": 35, - "avg_cross_section": 497.5, - "ic_positive_pct": 25.7, - "reliable": true - }, - "fip_momentum_conditional_top20pct": { - "note": "Among liquid top-N, keep mom_12_1 percentile \u2265 80.0 (paper: ID modulates continuation among winners; gate-relevant)", - "mean_ic": -0.0879, - "ic_t_stat": -4.58, - "weeks": 35, - "avg_cross_section": 294.3, - "ic_positive_pct": 22.9, - "reliable": true - }, - "vol_6m_liquid_1500": { - "note": "Context: low-vol anomaly strength on this pool", - "mean_ic": -0.0465, - "ic_t_stat": -1.3, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 37.1, - "reliable": true - }, - "mom_12_1_liquid_1500": { - "note": "Context: raw momentum on liquid breadth", - "mean_ic": 0.0462, - "ic_t_stat": 1.91, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 65.7, - "reliable": true - }, - "mom_12_1_resid_liquid_1500": { - "note": "Context: residual momentum on liquid breadth", - "mean_ic": 0.0289, - "ic_t_stat": 1.33, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 60.0, - "reliable": true - } - }, - "interpretation": { - "leak_ruled_out": false, - "junior_tier_drives_positive": true, - "prod_subset_still_negative": true, - "mom_conditional_negative_and_reliable": true, - "compositional_flip_story": "If prod subset IC is negative while full liquid-1500 is positive, the sign flip is compositional (bleeders / Nasdaq junk), not a temporal regime change. Unconditional fip pools continuous winners (want neg IC) against continuous losers/bleeders (want pos IC).", - "vol_tilt_warning": "vol_6m large negative IC on breadth: high-vol lottery names underperform. Production 80/20 high-vol tilt was validated on S&P-like names; must re-validate before any universe broaden." - }, - "platform_verdict": "ALIVE as breadth-book tilt candidate among momentum winners only \u2014 still needs a book-level experiment; not a production wire-in." -} \ No newline at end of file diff --git a/reports/fip-breadth-diagnostics-20260718-213908.json b/reports/fip-breadth-diagnostics-20260718-213908.json deleted file mode 100644 index c51ad81..0000000 --- a/reports/fip-breadth-diagnostics-20260718-213908.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "generated_at": "2026-07-18T21:39:07.916038", - "research_snapshot": "C:\\Workspace\\signal-platform\\backtest_snapshots\\research.sqlite", - "prod_subset_n": 506, - "panel_tickers": 4403, - "top_n": 1500, - "min_price": 5.0, - "checks": { - "fip_same_week_liquid_1500": { - "note": "Replication of main breadth run (same-week $vol mask)", - "mean_ic": -0.0168, - "ic_t_stat": -1.85, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 40.0, - "reliable": true - }, - "fip_lagged_membership_1w": { - "note": "Liquid top-N ranked on *prior* week's median $vol \u2014 excludes same-week liquidity explosion leak", - "mean_ic": -0.0102, - "ic_t_stat": -0.93, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 40.0, - "reliable": true - }, - "fip_tier_1_800": { - "note": "Same-week liquid ranks 1\u2013800 (senior liquid tier)", - "mean_ic": -0.035, - "ic_t_stat": -2.99, - "weeks": 35, - "avg_cross_section": 791.2, - "ic_positive_pct": 25.7, - "reliable": true - }, - "fip_tier_801_1500": { - "note": "Same-week liquid ranks 801\u20131500 (junior liquid tier)", - "mean_ic": 0.0141, - "ic_t_stat": 1.25, - "weeks": 35, - "avg_cross_section": 700.0, - "ic_positive_pct": 60.0, - "reliable": true - }, - "fip_prod_universe_subset": { - "note": "Symbols in prod.sqlite (~S&P-like large-cap book) inside same-week liquid top-N \u2014 compositional control", - "mean_ic": -0.0444, - "ic_t_stat": -2.88, - "weeks": 35, - "avg_cross_section": 497.5, - "ic_positive_pct": 25.7, - "reliable": true - }, - "fip_momentum_conditional_top20pct": { - "note": "Among liquid top-N, keep mom_12_1 percentile \u2265 80.0 (paper: ID modulates continuation among winners; gate-relevant)", - "mean_ic": -0.0879, - "ic_t_stat": -4.58, - "weeks": 35, - "avg_cross_section": 294.3, - "ic_positive_pct": 22.9, - "reliable": true - }, - "vol_6m_liquid_1500": { - "note": "Context: low-vol anomaly strength on this pool", - "mean_ic": -0.0465, - "ic_t_stat": -1.3, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 37.1, - "reliable": true - }, - "mom_12_1_liquid_1500": { - "note": "Context: raw momentum on liquid breadth", - "mean_ic": 0.0462, - "ic_t_stat": 1.91, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 65.7, - "reliable": true - }, - "mom_12_1_resid_liquid_1500": { - "note": "Context: residual momentum on liquid breadth", - "mean_ic": 0.0289, - "ic_t_stat": 1.33, - "weeks": 35, - "avg_cross_section": 1471.2, - "ic_positive_pct": 60.0, - "reliable": true - } - }, - "interpretation": { - "leak_ruled_out": false, - "junior_tier_drives_positive": true, - "prod_subset_still_negative": true, - "mom_conditional_negative_and_reliable": true, - "compositional_flip_story": "If prod subset IC is negative while full liquid-1500 is positive, the sign flip is compositional (bleeders / Nasdaq junk), not a temporal regime change. Unconditional fip pools continuous winners (want neg IC) against continuous losers/bleeders (want pos IC).", - "vol_tilt_warning": "vol_6m large negative IC on breadth: high-vol lottery names underperform. Production 80/20 high-vol tilt was validated on S&P-like names; must re-validate before any universe broaden." - }, - "platform_verdict": "ALIVE as breadth-book tilt candidate among momentum winners only \u2014 still needs a book-level experiment; not a production wire-in." -} \ No newline at end of file diff --git a/reports/fip-reconcile-20260719-000520.json b/reports/fip-reconcile-20260719-000520.json index 2b9a3b7..4c0bea6 100644 --- a/reports/fip-reconcile-20260719-000520.json +++ b/reports/fip-reconcile-20260719-000520.json @@ -107,6575 +107,6 @@ "reliable": true } }, - "membership_dumps": [ - { - "week": [ - 2022, - 49 - ], - "stats": { - "raw_pool": 2984, - "eligible_pre_mask": 2339, - "post_mask": 1500, - "mask_binds": true - }, - "symbols": [ - "A", - "AAL", - "AAON", - "AAPL", - "ABBV", - "ABCL", - "ABNB", - "ABT", - "ACAD", - "ACB", - "ACET", - "ACGL", - "ACHC", - "ACIW", - "ACLS", - "ACMR", - "ACN", - "ACRS", - "ACT", - "ADAM", - "ADBE", - "ADEA", - "ADI", - "ADM", - "ADP", - "ADPT", - "ADSK", - "ADTN", - "ADUS", - "ADV", - "AEE", - "AEHR", - "AEIS", - "AEP", - "AES", - "AEVA", - "AFCG", - "AFL", - "AFRM", - "AFYA", - "AGEN", - "AGIO", - "AGNC", - "AGNCN", - "AGNCO", - "AGNCP", - "AGNT", - "AGYS", - "AHCO", - "AIG", - "AIIO", - "AIZ", - "AJG", - "AKAM", - "ALB", - "ALCO", - "ALDX", - "ALEC", - "ALGM", - "ALGN", - "ALGT", - "ALHC", - "ALKS", - "ALKT", - "ALL", - "ALLE", - "ALLO", - "ALNT", - "ALNY", - "ALRM", - "ALT", - "ALXO", - "AMAL", - "AMAT", - "AMBA", - "AMCR", - "AMCX", - "AMD", - "AME", - "AMGN", - "AMKR", - "AMP", - "AMPH", - "AMPL", - "AMRN", - "AMSF", - "AMT", - "AMZN", - "ANAB", - "ANDE", - "ANET", - "ANGI", - "ANGO", - "ANIK", - "ANIP", - "AON", - "AOS", - "AOSL", - "APA", - "APD", - "APEI", - "APH", - "APO", - "APOG", - "APP", - "APPF", - "APPN", - "APPS", - "APTV", - "ARCB", - "ARCC", - "ARCT", - "ARE", - "ARES", - "ARGX", - "ARHS", - "ARKO", - "ARLP", - "ARQQ", - "ARQT", - "ARRY", - "ARTNA", - "ARVN", - "ARWR", - "ASLE", - "ASML", - "ASND", - "ASO", - "ASTE", - "ASTH", - "ASTL", - "ATEC", - "ATER", - "ATEX", - "ATNI", - "ATO", - "ATOM", - "ATRA", - "ATRC", - "ATRO", - "AUDC", - "AVAV", - "AVB", - "AVGO", - "AVNW", - "AVO", - "AVT", - "AVXL", - "AVY", - "AWK", - "AXGN", - "AXON", - "AXP", - "AXSM", - "AZO", - "AZTA", - "BA", - "BAC", - "BALL", - "BAND", - "BANF", - "BANR", - "BATRA", - "BATRK", - "BAX", - "BBIO", - "BBSI", - "BBY", - "BCAB", - "BCPC", - "BCRX", - "BCTX", - "BCYC", - "BDX", - "BEAM", - "BEAT", - "BEEM", - "BELFB", - "BEN", - "BF-B", - "BFC", - "BFST", - "BG", - "BHF", - "BIDU", - "BIIB", - "BILI", - "BIOX", - "BIRD", - "BJRI", - "BK", - "BKNG", - "BKR", - "BL", - "BLDP", - "BLDR", - "BLFS", - "BLK", - "BLKB", - "BLMN", - "BLNK", - "BMBL", - "BMRC", - "BMRN", - "BMY", - "BNGO", - "BNTX", - "BOKF", - "BOOM", - "BPOP", - "BR", - "BRK-B", - "BRKR", - "BRO", - "BRZE", - "BSX", - "BSY", - "BTAI", - "BUSE", - "BWB", - "BWIN", - "BX", - "BXP", - "BYND", - "BZ", - "BZUN", - "C", - "CABA", - "CAC", - "CACC", - "CAG", - "CAH", - "CAKE", - "CALM", - "CAMT", - "CAR", - "CARG", - "CARR", - "CASH", - "CASS", - "CASY", - "CAT", - "CATY", - "CB", - "CBOE", - "CBRE", - "CBRL", - "CBSH", - "CCAP", - "CCB", - "CCBG", - "CCC", - "CCCC", - "CCD", - "CCEP", - "CCI", - "CCL", - "CCNE", - "CCOI", - "CCRN", - "CCSI", - "CDLX", - "CDNA", - "CDNS", - "CDW", - "CDXS", - "CECO", - "CELH", - "CENN", - "CENT", - "CENTA", - "CENX", - "CERT", - "CEVA", - "CF", - "CFFN", - "CFG", - "CG", - "CGBD", - "CGC", - "CGEM", - "CGNX", - "CHCO", - "CHD", - "CHDN", - "CHEF", - "CHI", - "CHKP", - "CHRD", - "CHRS", - "CHRW", - "CHTR", - "CHW", - "CHY", - "CI", - "CIEN", - "CIGI", - "CINF", - "CL", - "CLAR", - "CLBK", - "CLDX", - "CLFD", - "CLMT", - "CLNE", - "CLX", - "CMCO", - "CMCSA", - "CME", - "CMG", - "CMI", - "CMPR", - "CMPS", - "CMRC", - "CMS", - "CMTL", - "CNC", - "CNOB", - "CNP", - "CNXC", - "CNXN", - "COCO", - "COF", - "COGT", - "COHR", - "COHU", - "COIN", - "COKE", - "COLB", - "COLL", - "COLM", - "COO", - "COP", - "COR", - "CORT", - "COST", - "CPAY", - "CPB", - "CPRT", - "CPT", - "CRAI", - "CRBU", - "CRCT", - "CRH", - "CRL", - "CRM", - "CRMT", - "CRNC", - "CRNX", - "CROX", - "CRSP", - "CRSR", - "CRTO", - "CRUS", - "CRVL", - "CRWD", - "CSCO", - "CSGP", - "CSIQ", - "CSQ", - "CSTL", - "CSWC", - "CSX", - "CTAS", - "CTBI", - "CTKB", - "CTRA", - "CTRN", - "CTSH", - "CTVA", - "CVBF", - "CVCO", - "CVLT", - "CVS", - "CVX", - "CWCO", - "CWST", - "CYRX", - "CYTK", - "CZR", - "D", - "DAL", - "DASH", - "DBGI", - "DBX", - "DCBO", - "DCGO", - "DD", - "DDOG", - "DE", - "DECK", - "DELL", - "DG", - "DGII", - "DGX", - "DH", - "DHI", - "DHR", - "DIOD", - "DIS", - "DJT", - "DKNG", - "DLO", - "DLR", - "DLTR", - "DMLP", - "DMRC", - "DNLI", - "DNUT", - "DOC", - "DOCU", - "DOMO", - "DOO", - "DORM", - "DOV", - "DOW", - "DOX", - "DPZ", - "DRH", - "DRI", - "DRS", - "DRVN", - "DSGN", - "DSGR", - "DSGX", - "DTE", - "DUK", - "DUOL", - "DVA", - "DVN", - "DXCM", - "DXLG", - "DXPE", - "DYN", - "EA", - "EBAY", - "EBC", - "ECHO", - "ECL", - "ECPG", - "ED", - "EDIT", - "EEFT", - "EFSC", - "EFX", - "EG", - "EGBN", - "EH", - "EIX", - "EL", - "ELV", - "EME", - "EMR", - "ENPH", - "ENSG", - "ENTA", - "ENTG", - "ENVX", - "EOG", - "EOLS", - "EPAM", - "EQIX", - "EQR", - "EQT", - "ERAS", - "ERIC", - "ERIE", - "ERII", - "ES", - "ESLT", - "ESQ", - "ESS", - "ESTA", - "ETN", - "ETR", - "EVCM", - "EVER", - "EVGO", - "EVRG", - "EW", - "EWBC", - "EWTX", - "EXC", - "EXE", - "EXEL", - "EXFY", - "EXLS", - "EXPD", - "EXPE", - "EXPO", - "EXR", - "EXTR", - "EYE", - "EZPW", - "F", - "FA", - "FANG", - "FAST", - "FATE", - "FBNC", - "FCBC", - "FCEL", - "FCFS", - "FCNCA", - "FCX", - "FDMT", - "FDS", - "FDUS", - "FDX", - "FE", - "FELE", - "FFAI", - "FFBC", - "FFIN", - "FFIV", - "FHB", - "FIBK", - "FICO", - "FIS", - "FISV", - "FITB", - "FIVE", - "FIVN", - "FIX", - "FIZZ", - "FLEX", - "FLGT", - "FLNA", - "FLNC", - "FLWS", - "FLYW", - "FMBH", - "FMNB", - "FNKO", - "FNUC", - "FORM", - "FORR", - "FOX", - "FOXA", - "FOXF", - "FRHC", - "FRME", - "FROG", - "FRPT", - "FRSH", - "FRT", - "FSLR", - "FSLY", - "FSV", - "FTAI", - "FTCI", - "FTDR", - "FTNT", - "FTV", - "FULC", - "FULT", - "FUTU", - "FWONA", - "FWONK", - "FWRD", - "FWRG", - "GABC", - "GAIN", - "GBDC", - "GCMG", - "GD", - "GDDY", - "GDRX", - "GDS", - "GDYN", - "GE", - "GEN", - "GFS", - "GGAL", - "GGR", - "GH", - "GIII", - "GILD", - "GIS", - "GL", - "GLAD", - "GLBE", - "GLNG", - "GLPI", - "GLUE", - "GLW", - "GM", - "GMAB", - "GNRC", - "GNTX", - "GO", - "GOGO", - "GOOD", - "GOOG", - "GOOGL", - "GPC", - "GPN", - "GPRE", - "GPRO", - "GRFS", - "GRMN", - "GRPN", - "GRWG", - "GS", - "GSAT", - "GSBC", - "GSHD", - "GT", - "GTLB", - "GTM", - "GTX", - "GWW", - "HAFC", - "HAIN", - "HAL", - "HALO", - "HAPN", - "HAS", - "HBAN", - "HBANP", - "HBNC", - "HCA", - "HCAT", - "HCKT", - "HCM", - "HCSG", - "HD", - "HDSN", - "HELE", - "HFWA", - "HIFS", - "HIG", - "HII", - "HIMX", - "HLIT", - "HLMN", - "HLNE", - "HLT", - "HNRG", - "HON", - "HOOD", - "HOPE", - "HPE", - "HPK", - "HPQ", - "HQY", - "HRL", - "HRMY", - "HROW", - "HRZN", - "HSIC", - "HST", - "HSTM", - "HSY", - "HTHT", - "HTLD", - "HTO", - "HTZ", - "HTZWW", - "HUBB", - "HUBG", - "HUDI", - "HUM", - "HURN", - "HUT", - "HWC", - "HWKN", - "HWM", - "HYFM", - "HYMC", - "IART", - "IBCP", - "IBKR", - "IBM", - "IBOC", - "IBRX", - "ICE", - "ICFI", - "ICHR", - "ICLR", - "ICUI", - "IDCC", - "IDXX", - "IDYA", - "IEP", - "IEX", - "IFF", - "IHRT", - "IIIV", - "ILMN", - "IMCR", - "IMKTA", - "IMMR", - "IMTX", - "IMUX", - "IMVT", - "IMXI", - "INCY", - "INDB", - "INDI", - "INGN", - "INMD", - "INO", - "INSE", - "INSM", - "INTA", - "INTC", - "INTU", - "INVA", - "INVH", - "IONS", - "IOSP", - "IOVA", - "IP", - "IPAR", - "IPGP", - "IQV", - "IR", - "IRDM", - "IRM", - "IRTC", - "IRWD", - "ISRG", - "IT", - "ITRI", - "ITW", - "IVZ", - "J", - "JACK", - "JAKK", - "JAZZ", - "JBHT", - "JBIO", - "JBL", - "JBLU", - "JBSS", - "JCI", - "JD", - "JJSF", - "JKHY", - "JNJ", - "JOUT", - "JOYY", - "JPM", - "JRVR", - "JYNT", - "KALU", - "KDP", - "KE", - "KELYA", - "KEY", - "KEYS", - "KHC", - "KIDS", - "KIM", - "KKR", - "KLAC", - "KLIC", - "KLRS", - "KLXE", - "KMB", - "KMI", - "KNSA", - "KO", - "KOD", - "KPTI", - "KR", - "KRNT", - "KRNY", - "KROS", - "KRUS", - "KRYS", - "KTOS", - "KURA", - "KYMR", - "KYNB", - "L", - "LAMR", - "LAND", - "LASR", - "LAUR", - "LBRDA", - "LBRDK", - "LBTYA", - "LBTYK", - "LCID", - "LDOS", - "LE", - "LECO", - "LEGN", - "LEN", - "LESL", - "LFUS", - "LGIH", - "LGND", - "LH", - "LHX", - "LI", - "LII", - "LILA", - "LILAK", - "LIN", - "LIND", - "LITE", - "LIVN", - "LKFN", - "LKFT", - "LKQ", - "LLY", - "LMAT", - "LMT", - "LNT", - "LNTH", - "LOCO", - "LOGI", - "LOPE", - "LOVE", - "LOW", - "LPLA", - "LPRO", - "LPSN", - "LQDA", - "LQDT", - "LRCX", - "LSCC", - "LSTR", - "LULU", - "LUNG", - "LUV", - "LVS", - "LWLG", - "LYB", - "LYEL", - "LYFT", - "LYV", - "LZ", - "MA", - "MAA", - "MANH", - "MAR", - "MARA", - "MAS", - "MASS", - "MAT", - "MATW", - "MBIN", - "MBLY", - "MBUU", - "MBWM", - "MCD", - "MCFT", - "MCHB", - "MCHP", - "MCK", - "MCO", - "MCRB", - "MCRI", - "MDB", - "MDGL", - "MDLZ", - "MDT", - "MEDP", - "MELI", - "MEOH", - "MERC", - "MET", - "META", - "METC", - "MFIC", - "MGEE", - "MGM", - "MGNI", - "MGNX", - "MGPI", - "MGRC", - "MIDD", - "MIRM", - "MITK", - "MKC", - "MKSI", - "MKTX", - "MLAB", - "MLCO", - "MLKN", - "MLM", - "MMM", - "MMSI", - "MMYT", - "MNDY", - "MNRO", - "MNST", - "MNTK", - "MO", - "MOMO", - "MORN", - "MOS", - "MPAA", - "MPC", - "MPWR", - "MQ", - "MRCY", - "MRK", - "MRNA", - "MRSH", - "MRTN", - "MRVI", - "MRVL", - "MS", - "MSBI", - "MSCI", - "MSEX", - "MSFT", - "MSI", - "MSTR", - "MTB", - "MTCH", - "MTD", - "MTLS", - "MTSI", - "MTVA", - "MU", - "MXCT", - "MXL", - "MYGN", - "MYRG", - "MZTI", - "NAVI", - "NBIX", - "NBTB", - "NCLH", - "NCNO", - "NDAQ", - "NDSN", - "NEE", - "NEM", - "NEO", - "NEOG", - "NESR", - "NEWT", - "NFBK", - "NFE", - "NFLX", - "NI", - "NICE", - "NIU", - "NKE", - "NKTR", - "NKTX", - "NMFC", - "NMIH", - "NMRK", - "NNOX", - "NOC", - "NOVT", - "NOW", - "NRC", - "NRDS", - "NRG", - "NRIX", - "NSC", - "NSIT", - "NSSC", - "NTAP", - "NTCT", - "NTES", - "NTGR", - "NTLA", - "NTNX", - "NTRA", - "NTRS", - "NUE", - "NVAX", - "NVCR", - "NVDA", - "NVEC", - "NVMI", - "NVR", - "NWBI", - "NWE", - "NWL", - "NWPX", - "NWS", - "NWSA", - "NXPI", - "NXST", - "O", - "OCFC", - "OCSL", - "ODFL", - "OFIX", - "OFLX", - "OKE", - "OKTA", - "OLED", - "OLLI", - "OM", - "OMAB", - "OMC", - "OMCL", - "ON", - "ONB", - "ONC", - "ONEW", - "OPCH", - "OPI", - "OPRX", - "ORCL", - "ORLY", - "ORMP", - "OSBC", - "OSIS", - "OSPN", - "OSUR", - "OSW", - "OTEX", - "OTIS", - "OTLY", - "OTTR", - "OUST", - "OXLC", - "OXY", - "OZK", - "PAA", - "PACB", - "PAGP", - "PAHC", - "PAMT", - "PANW", - "PATK", - "PAX", - "PAYO", - "PAYX", - "PCAR", - "PCG", - "PCRX", - "PCT", - "PCTY", - "PCVX", - "PDD", - "PDFS", - "PDSB", - "PEBO", - "PECO", - "PEG", - "PEGA", - "PENG", - "PENN", - "PEP", - "PERI", - "PETS", - "PFBC", - "PFE", - "PFG", - "PG", - "PGC", - "PGNY", - "PGR", - "PGY", - "PH", - "PHAT", - "PHM", - "PHUN", - "PI", - "PKG", - "PLAB", - "PLAY", - "PLCE", - "PLD", - "PLMR", - "PLPC", - "PLRX", - "PLTK", - "PLTR", - "PLUG", - "PLUS", - "PLXS", - "PM", - "PMVP", - "PNC", - "PNR", - "PNTG", - "PNW", - "PODD", - "POOL", - "POWI", - "PPC", - "PPG", - "PPL", - "PPLI", - "PRAA", - "PRCT", - "PRDO", - "PRGS", - "PRME", - "PRPL", - "PRTA", - "PRTS", - "PRU", - "PRVA", - "PSA", - "PSEC", - "PSMT", - "PSX", - "PTC", - "PTCT", - "PTEN", - "PTGX", - "PTLO", - "PTON", - "PUBM", - "PWP", - "PWR", - "PYPL", - "PZZA", - "QCOM", - "QCRH", - "QDEL", - "QFIN", - "QLYS", - "QNST", - "QQQX", - "QRVO", - "QS", - "QTRX", - "QURE", - "RARE", - "RCKT", - "RCL", - "RCMT", - "RDNT", - "RDNW", - "RDWR", - "REG", - "REGN", - "RELL", - "RELY", - "RENT", - "REPL", - "REYN", - "RF", - "RGEN", - "RGLD", - "RGNX", - "RGP", - "RICK", - "RIGL", - "RILY", - "RIVN", - "RJF", - "RL", - "RLAY", - "RMBS", - "RMD", - "RMR", - "RNA", - "RNW", - "ROAD", - "ROCK", - "ROIV", - "ROK", - "ROKU", - "ROL", - "ROOT", - "ROP", - "ROST", - "RPAY", - "RPD", - "RPRX", - "RRGB", - "RRR", - "RSG", - "RTX", - "RUM", - "RUN", - "RUSHA", - "RVMD", - "RVTY", - "RXRX", - "RYAAY", - "RYTM", - "SABR", - "SAFT", - "SAIA", - "SAIC", - "SANM", - "SATS", - "SBAC", - "SBCF", - "SBGI", - "SBLK", - "SBRA", - "SBUX", - "SCHL", - "SCHW", - "SCSC", - "SDGR", - "SEAT", - "SEDG", - "SEER", - "SEIC", - "SENEA", - "SENS", - "SFM", - "SFNC", - "SGHT", - "SGML", - "SGRY", - "SHC", - "SHEN", - "SHLS", - "SHOE", - "SHOO", - "SHOP", - "SHW", - "SIBN", - "SIGA", - "SIGI", - "SIMO", - "SIRI", - "SITM", - "SJM", - "SKIN", - "SKYT", - "SKYW", - "SLAB", - "SLB", - "SLM", - "SLP", - "SLRC", - "SMBC", - "SMCI", - "SMPL", - "SMTC", - "SNA", - "SNDX", - "SNEX", - "SNPS", - "SNY", - "SO", - "SONO", - "SPFI", - "SPG", - "SPGI", - "SPOK", - "SPSC", - "SPT", - "SPWH", - "SRAD", - "SRCE", - "SRE", - "SRPT", - "SRRK", - "SRTS", - "SSNC", - "SSP", - "SSRM", - "SSTI", - "SSYS", - "STAA", - "STBA", - "STE", - "STEP", - "STGW", - "STLD", - "STNE", - "STOK", - "STRA", - "STRL", - "STRO", - "STT", - "STX", - "STZ", - "SUNE", - "SUPN", - "SVC", - "SW", - "SWBI", - "SWK", - "SWKS", - "SYBT", - "SYF", - "SYK", - "SYM", - "SYNA", - "SYY", - "T", - "TAP", - "TARS", - "TASK", - "TBBK", - "TBCH", - "TBLD", - "TBPH", - "TC", - "TCBI", - "TCBK", - "TCMD", - "TCOM", - "TCPC", - "TCRT", - "TCX", - "TDG", - "TDY", - "TEAM", - "TECH", - "TEL", - "TENB", - "TER", - "TFC", - "TFSL", - "TGT", - "TGTX", - "TH", - "THFF", - "THRM", - "THRY", - "TIGO", - "TIGR", - "TIL", - "TILE", - "TITN", - "TJX", - "TKO", - "TLRY", - "TMCI", - "TMDX", - "TMO", - "TMUS", - "TNDM", - "TNGX", - "TOWN", - "TPL", - "TPR", - "TREE", - "TRGP", - "TRI", - "TRIN", - "TRIP", - "TRMB", - "TRMD", - "TRMK", - "TRNS", - "TROW", - "TRS", - "TRST", - "TRUP", - "TRV", - "TSCO", - "TSEM", - "TSLA", - "TSN", - "TT", - "TTD", - "TTEC", - "TTEK", - "TTGT", - "TTMI", - "TTWO", - "TVRD", - "TVTX", - "TW", - "TWST", - "TXG", - "TXN", - "TXRH", - "TXT", - "TYL", - "UAL", - "UBER", - "UBSI", - "UCTT", - "UDR", - "UEIC", - "UFCS", - "UFPI", - "UFPT", - "UHS", - "ULCC", - "ULH", - "ULTA", - "UMBF", - "UNH", - "UNIT", - "UNP", - "UPBD", - "UPLD", - "UPS", - "UPST", - "UPWK", - "URBN", - "URI", - "USB", - "UTHR", - "UVSP", - "V", - "VC", - "VCEL", - "VCTR", - "VCYT", - "VECO", - "VERA", - "VERI", - "VERU", - "VERX", - "VIAV", - "VICI", - "VICR", - "VIR", - "VISN", - "VITL", - "VLO", - "VLY", - "VMC", - "VNDA", - "VNET", - "VNOM", - "VOD", - "VRDN", - "VREX", - "VRM", - "VRNS", - "VRRM", - "VRSK", - "VRSN", - "VRT", - "VRTX", - "VSAT", - "VSEC", - "VST", - "VTR", - "VTRS", - "VZ", - "WAB", - "WABC", - "WAFD", - "WASH", - "WAT", - "WB", - "WDAY", - "WDC", - "WDFC", - "WEC", - "WELL", - "WEN", - "WERN", - "WEST", - "WFC", - "WFRD", - "WHWK", - "WINA", - "WING", - "WIX", - "WKHS", - "WM", - "WMB", - "WMG", - "WMT", - "WOOF", - "WRB", - "WRLD", - "WSBC", - "WSBF", - "WSC", - "WSFS", - "WSM", - "WST", - "WTFC", - "WTW", - "WWD", - "WY", - "WYNN", - "XAIR", - "XEL", - "XENE", - "XFOR", - "XMTR", - "XNCR", - "XOM", - "XP", - "XPEL", - "XRAY", - "XRX", - "XYL", - "XYZ", - "YORW", - "YUM", - "Z", - "ZBH", - "ZBRA", - "ZD", - "ZG", - "ZION", - "ZLAB", - "ZM", - "ZNTL", - "ZS", - "ZTS", - "ZUMZ", - "ZVZZT", - "ZYME" - ], - "n_symbols": 1500 - }, - { - "week": [ - 2022, - 31 - ], - "stats": { - "raw_pool": 2800, - "eligible_pre_mask": 2332, - "post_mask": 1500, - "mask_binds": true - }, - "symbols": [ - "A", - "AAL", - "AAON", - "AAPL", - "ABBV", - "ABCL", - "ABNB", - "ABT", - "ACAD", - "ACB", - "ACET", - "ACGL", - "ACHC", - "ACIW", - "ACLS", - "ACMR", - "ACN", - "ACRS", - "ACT", - "ACTG", - "ADAM", - "ADBE", - "ADEA", - "ADI", - "ADM", - "ADP", - "ADPT", - "ADSK", - "ADTN", - "ADUS", - "ADV", - "AEE", - "AEHR", - "AEIS", - "AEP", - "AES", - "AEVA", - "AFCG", - "AFL", - "AFRM", - "AFYA", - "AGEN", - "AGIO", - "AGNC", - "AGNT", - "AGYS", - "AHCO", - "AIG", - "AIZ", - "AJG", - "AKAM", - "ALB", - "ALCO", - "ALDX", - "ALEC", - "ALGM", - "ALGN", - "ALGT", - "ALHC", - "ALKS", - "ALKT", - "ALL", - "ALLE", - "ALLO", - "ALNY", - "ALRM", - "ALT", - "ALXO", - "AMAL", - "AMAT", - "AMBA", - "AMCR", - "AMCX", - "AMD", - "AME", - "AMGN", - "AMKR", - "AMP", - "AMPH", - "AMRN", - "AMSC", - "AMSF", - "AMT", - "AMTX", - "AMZN", - "ANAB", - "ANDE", - "ANET", - "ANGI", - "ANGO", - "ANIK", - "ANIP", - "AON", - "AOS", - "AOSL", - "AOUT", - "APA", - "APD", - "APH", - "API", - "APO", - "APOG", - "APP", - "APPF", - "APPN", - "APPS", - "APTV", - "APYX", - "ARCB", - "ARCC", - "ARCT", - "ARE", - "ARES", - "ARGX", - "ARKO", - "ARLP", - "ARQT", - "ARRY", - "ARTNA", - "ARVN", - "ARWR", - "ASLE", - "ASML", - "ASND", - "ASO", - "ASTE", - "ASTH", - "ASTL", - "ASTS", - "ATEC", - "ATER", - "ATEX", - "ATLC", - "ATNI", - "ATO", - "ATOM", - "ATRA", - "ATRC", - "AUDC", - "AUPH", - "AVAV", - "AVB", - "AVGO", - "AVIR", - "AVNW", - "AVO", - "AVPT", - "AVT", - "AVXL", - "AVY", - "AWK", - "AXGN", - "AXON", - "AXP", - "AXSM", - "AZO", - "AZTA", - "BA", - "BAC", - "BALL", - "BAND", - "BANF", - "BANR", - "BATRK", - "BAX", - "BBIO", - "BBSI", - "BBY", - "BCBP", - "BCML", - "BCPC", - "BCRX", - "BCTX", - "BCYC", - "BDX", - "BEAM", - "BEEM", - "BEN", - "BF-B", - "BG", - "BHF", - "BHFAN", - "BIDU", - "BIIB", - "BILI", - "BJRI", - "BK", - "BKNG", - "BKR", - "BL", - "BLDP", - "BLDR", - "BLFS", - "BLK", - "BLKB", - "BLMN", - "BLNK", - "BMBL", - "BMEA", - "BMRN", - "BMY", - "BNGO", - "BNR", - "BNTX", - "BOKF", - "BOOM", - "BPOP", - "BR", - "BRK-B", - "BRKR", - "BRO", - "BSET", - "BSX", - "BSY", - "BTAI", - "BUSE", - "BVS", - "BWIN", - "BX", - "BXP", - "BYND", - "BYRN", - "BZ", - "BZUN", - "C", - "CAC", - "CACC", - "CAG", - "CAH", - "CAKE", - "CALM", - "CAMT", - "CAR", - "CARG", - "CARR", - "CASH", - "CASS", - "CASY", - "CAT", - "CATY", - "CB", - "CBOE", - "CBRE", - "CBRL", - "CBSH", - "CCB", - "CCC", - "CCCC", - "CCD", - "CCEC", - "CCEP", - "CCI", - "CCL", - "CCOI", - "CCRN", - "CCXI", - "CDLX", - "CDNA", - "CDNS", - "CDW", - "CDXS", - "CELH", - "CELU", - "CENN", - "CENT", - "CENTA", - "CENX", - "CERS", - "CERT", - "CEVA", - "CF", - "CFFN", - "CFG", - "CG", - "CGBD", - "CGC", - "CGEM", - "CGNX", - "CHCO", - "CHD", - "CHDN", - "CHEF", - "CHI", - "CHKP", - "CHRD", - "CHRS", - "CHRW", - "CHTR", - "CHW", - "CHY", - "CI", - "CIEN", - "CIGI", - "CINF", - "CL", - "CLAR", - "CLBK", - "CLDX", - "CLFD", - "CLNE", - "CLPT", - "CLX", - "CMCO", - "CMCSA", - "CME", - "CMG", - "CMI", - "CMPR", - "CMPS", - "CMRC", - "CMS", - "CMTL", - "CNC", - "CNOB", - "CNP", - "CNXC", - "CNXN", - "CODX", - "COF", - "COGT", - "COHR", - "COHU", - "COIN", - "COKE", - "COLB", - "COLL", - "COLM", - "COO", - "COP", - "COR", - "CORT", - "COST", - "CPAY", - "CPB", - "CPRT", - "CPSS", - "CPT", - "CRAI", - "CRBU", - "CRH", - "CRIS", - "CRL", - "CRM", - "CRMT", - "CRNC", - "CRNX", - "CROX", - "CRSP", - "CRSR", - "CRTO", - "CRUS", - "CRVL", - "CRWD", - "CSCO", - "CSGP", - "CSIQ", - "CSQ", - "CSTL", - "CSWC", - "CSX", - "CTAS", - "CTBI", - "CTKB", - "CTRA", - "CTRM", - "CTRN", - "CTSH", - "CTVA", - "CVBF", - "CVCO", - "CVLT", - "CVNA", - "CVS", - "CVX", - "CWST", - "CYRX", - "CYTK", - "CZR", - "D", - "DAL", - "DASH", - "DAVE", - "DBX", - "DCBO", - "DCGO", - "DD", - "DDOG", - "DE", - "DECK", - "DELL", - "DFTX", - "DG", - "DGICA", - "DGII", - "DGX", - "DHI", - "DHR", - "DIOD", - "DIS", - "DJT", - "DKNG", - "DLO", - "DLR", - "DLTR", - "DMLP", - "DMRC", - "DNLI", - "DNUT", - "DOC", - "DOCU", - "DOMO", - "DOO", - "DORM", - "DOV", - "DOW", - "DOX", - "DPZ", - "DRH", - "DRI", - "DRS", - "DRVN", - "DSGN", - "DSGX", - "DTE", - "DTIL", - "DUK", - "DUOL", - "DVA", - "DVN", - "DXCM", - "DXPE", - "EA", - "EBAY", - "EBC", - "ECHO", - "ECL", - "ECPG", - "ED", - "EDIT", - "EEFT", - "EFSC", - "EFX", - "EG", - "EGBN", - "EH", - "EHTH", - "EIX", - "EL", - "ELV", - "EME", - "EMR", - "ENPH", - "ENSG", - "ENTA", - "ENTG", - "ENVX", - "EOG", - "EOLS", - "EPAM", - "EQIX", - "EQR", - "EQT", - "ERAS", - "ERIC", - "ERIE", - "ERII", - "ES", - "ESLT", - "ESS", - "ESTA", - "ETN", - "ETR", - "EVCM", - "EVER", - "EVGO", - "EVRG", - "EW", - "EWBC", - "EWTX", - "EXC", - "EXE", - "EXEL", - "EXLS", - "EXPD", - "EXPE", - "EXPO", - "EXR", - "EXTR", - "EYE", - "EZPW", - "F", - "FA", - "FANG", - "FAST", - "FATE", - "FBNC", - "FCEL", - "FCFS", - "FCNCA", - "FCX", - "FDMT", - "FDS", - "FDUS", - "FDX", - "FE", - "FELE", - "FFAI", - "FFBC", - "FFIN", - "FFIV", - "FHB", - "FHTX", - "FIBK", - "FICO", - "FIS", - "FISV", - "FITB", - "FIVE", - "FIVN", - "FIX", - "FIZZ", - "FLEX", - "FLGT", - "FLL", - "FLNA", - "FLWS", - "FLYW", - "FMAO", - "FMBH", - "FNKO", - "FORM", - "FORR", - "FOSL", - "FOX", - "FOXA", - "FOXF", - "FRHC", - "FRME", - "FROG", - "FRPT", - "FRT", - "FSLR", - "FSLY", - "FSV", - "FTAI", - "FTCI", - "FTDR", - "FTNT", - "FTV", - "FULC", - "FULT", - "FUTU", - "FWONA", - "FWONK", - "FWRD", - "GABC", - "GAIN", - "GBDC", - "GCMG", - "GD", - "GDDY", - "GDRX", - "GDS", - "GDYN", - "GE", - "GEN", - "GGAL", - "GGR", - "GH", - "GIII", - "GILD", - "GIS", - "GL", - "GLAD", - "GLBE", - "GLNG", - "GLPI", - "GLUE", - "GLW", - "GM", - "GMAB", - "GNRC", - "GNTX", - "GO", - "GOGO", - "GOOD", - "GOOG", - "GOOGL", - "GOSS", - "GOVX", - "GPC", - "GPN", - "GPRE", - "GPRO", - "GRFS", - "GRMN", - "GRPN", - "GS", - "GSAT", - "GSBC", - "GSHD", - "GSM", - "GT", - "GTM", - "GTX", - "GWW", - "HAFC", - "HAIN", - "HAL", - "HALO", - "HAPN", - "HAS", - "HBAN", - "HBNC", - "HCA", - "HCAT", - "HCKT", - "HCM", - "HCSG", - "HD", - "HDSN", - "HELE", - "HFWA", - "HIG", - "HII", - "HIMX", - "HIVE", - "HLIT", - "HLMN", - "HLNE", - "HLT", - "HNRG", - "HOFT", - "HON", - "HOOD", - "HOPE", - "HPE", - "HPK", - "HPQ", - "HQY", - "HRL", - "HRMY", - "HRZN", - "HSIC", - "HST", - "HSTM", - "HSY", - "HTHT", - "HTLD", - "HTO", - "HUBB", - "HUBG", - "HUM", - "HURN", - "HUT", - "HWC", - "HWKN", - "HWM", - "HYFM", - "HYMC", - "IART", - "IBCP", - "IBKR", - "IBM", - "IBOC", - "ICE", - "ICFI", - "ICHR", - "ICLR", - "ICUI", - "IDCC", - "IDXX", - "IDYA", - "IEP", - "IEX", - "IFF", - "IHRT", - "III", - "IIIV", - "ILMN", - "ILPT", - "IMCR", - "IMKTA", - "IMMR", - "IMXI", - "INCY", - "INDB", - "INDI", - "INGN", - "INMD", - "INO", - "INSE", - "INSG", - "INSM", - "INTA", - "INTC", - "INTU", - "INVA", - "INVH", - "INVZ", - "IONS", - "IOSP", - "IOVA", - "IP", - "IPAR", - "IPGP", - "IQV", - "IR", - "IRDM", - "IRM", - "IRTC", - "IRWD", - "ISRG", - "IT", - "ITRI", - "ITW", - "IVZ", - "J", - "JACK", - "JAZZ", - "JBHT", - "JBL", - "JBLU", - "JBSS", - "JCI", - "JD", - "JJSF", - "JKHY", - "JNJ", - "JOUT", - "JOYY", - "JPM", - "JRVR", - "JYNT", - "KALU", - "KDP", - "KE", - "KELYA", - "KEY", - "KEYS", - "KHC", - "KIDS", - "KIM", - "KKR", - "KLAC", - "KLIC", - "KLRS", - "KMB", - "KMI", - "KNSA", - "KO", - "KOD", - "KPTI", - "KR", - "KRNT", - "KRNY", - "KROS", - "KRUS", - "KRYS", - "KTOS", - "KURA", - "KYMR", - "KYNB", - "L", - "LAMR", - "LAND", - "LASR", - "LAUR", - "LBRDA", - "LBRDK", - "LBTYA", - "LBTYK", - "LCID", - "LDOS", - "LE", - "LECO", - "LEGN", - "LEN", - "LENZ", - "LESL", - "LFST", - "LFUS", - "LGIH", - "LGND", - "LH", - "LHX", - "LI", - "LIDR", - "LII", - "LILA", - "LILAK", - "LIN", - "LIND", - "LITE", - "LITS", - "LIVN", - "LKFN", - "LKFT", - "LKQ", - "LLY", - "LMAT", - "LMT", - "LNT", - "LNTH", - "LOCO", - "LOGI", - "LONA", - "LOPE", - "LOVE", - "LOW", - "LPLA", - "LPRO", - "LPSN", - "LQDA", - "LQDT", - "LRCX", - "LSCC", - "LSTR", - "LULU", - "LUNG", - "LUV", - "LVS", - "LYB", - "LYEL", - "LYFT", - "LYV", - "LZ", - "MA", - "MAA", - "MANH", - "MAR", - "MARA", - "MAS", - "MASS", - "MAT", - "MATW", - "MBIN", - "MBUU", - "MCD", - "MCFT", - "MCHB", - "MCHP", - "MCK", - "MCO", - "MCRB", - "MCRI", - "MDB", - "MDGL", - "MDLZ", - "MDT", - "MEDP", - "MELI", - "MEOH", - "MERC", - "MET", - "META", - "METC", - "MFIC", - "MGEE", - "MGM", - "MGNI", - "MGPI", - "MGRC", - "MIDD", - "MIRM", - "MITK", - "MKC", - "MKSI", - "MKTX", - "MLAB", - "MLCO", - "MLKN", - "MLM", - "MMM", - "MMSI", - "MMYT", - "MNDY", - "MNRO", - "MNST", - "MNTK", - "MNTS", - "MO", - "MORN", - "MOS", - "MPC", - "MPWR", - "MQ", - "MRCY", - "MRK", - "MRNA", - "MRSH", - "MRTN", - "MRVI", - "MRVL", - "MS", - "MSBI", - "MSCI", - "MSEX", - "MSFT", - "MSI", - "MSTR", - "MTB", - "MTCH", - "MTD", - "MTLS", - "MTRX", - "MTSI", - "MU", - "MVIS", - "MXCT", - "MXL", - "MYGN", - "MYRG", - "MZTI", - "NAVI", - "NBIX", - "NBN", - "NBP", - "NBTB", - "NCLH", - "NCMI", - "NCNO", - "NDAQ", - "NDSN", - "NEE", - "NEGG", - "NEM", - "NEO", - "NEOG", - "NESR", - "NEWT", - "NEXT", - "NFBK", - "NFE", - "NFLX", - "NI", - "NICE", - "NIU", - "NKE", - "NKTR", - "NKTX", - "NMFC", - "NMIH", - "NMRK", - "NNOX", - "NOC", - "NOVT", - "NOW", - "NRC", - "NRG", - "NRIX", - "NSC", - "NSIT", - "NSSC", - "NTAP", - "NTCT", - "NTES", - "NTGR", - "NTLA", - "NTNX", - "NTRA", - "NTRS", - "NUE", - "NVAX", - "NVCR", - "NVDA", - "NVEC", - "NVMI", - "NVR", - "NVTS", - "NWBI", - "NWE", - "NWL", - "NWPX", - "NWS", - "NWSA", - "NXPI", - "NXST", - "O", - "OCFC", - "OCSL", - "ODFL", - "OFIX", - "OKE", - "OKTA", - "OLED", - "OLLI", - "OM", - "OMAB", - "OMC", - "OMCL", - "OMER", - "ON", - "ONB", - "ONC", - "ONDS", - "ONEW", - "OPAL", - "OPCH", - "OPEN", - "OPI", - "OPRT", - "OPRX", - "ORCL", - "ORGO", - "ORLY", - "ORMP", - "OSBC", - "OSIS", - "OSPN", - "OSW", - "OTEX", - "OTIS", - "OTLY", - "OTTR", - "OUST", - "OXLC", - "OXY", - "OZK", - "PAA", - "PACB", - "PAGP", - "PAHC", - "PANW", - "PATK", - "PAX", - "PAYO", - "PAYX", - "PCAR", - "PCG", - "PCRX", - "PCT", - "PCTY", - "PCVX", - "PDD", - "PDFS", - "PDSB", - "PEBO", - "PECO", - "PEG", - "PEGA", - "PENG", - "PENN", - "PEP", - "PERI", - "PETS", - "PFBC", - "PFE", - "PFG", - "PG", - "PGC", - "PGNY", - "PGR", - "PGY", - "PH", - "PHAT", - "PHM", - "PHUN", - "PI", - "PKG", - "PLAB", - "PLAY", - "PLBY", - "PLCE", - "PLD", - "PLMR", - "PLRX", - "PLTK", - "PLTR", - "PLUG", - "PLUS", - "PLXS", - "PM", - "PMVP", - "PNC", - "PNR", - "PNTG", - "PNW", - "PODD", - "POOL", - "POWI", - "POWW", - "PPC", - "PPG", - "PPL", - "PPLI", - "PRAA", - "PRAX", - "PRDO", - "PRGS", - "PRTA", - "PRTS", - "PRU", - "PRVA", - "PSA", - "PSEC", - "PSMT", - "PSX", - "PTC", - "PTCT", - "PTEN", - "PTGX", - "PTON", - "PUBM", - "PWP", - "PWR", - "PYPL", - "PZZA", - "QCOM", - "QCRH", - "QDEL", - "QFIN", - "QLYS", - "QNST", - "QQQX", - "QRVO", - "QS", - "QTRX", - "QURE", - "RARE", - "RCKT", - "RCL", - "RCMT", - "RDNT", - "RDNW", - "RDWR", - "REG", - "REGN", - "RELL", - "REPL", - "REYN", - "RF", - "RGEN", - "RGLD", - "RGNX", - "RGP", - "RICK", - "RIGL", - "RILY", - "RIOT", - "RJF", - "RKLB", - "RL", - "RLAY", - "RLMD", - "RMBS", - "RMD", - "RMNI", - "RMR", - "RNA", - "RNAC", - "RNW", - "ROAD", - "ROCK", - "ROK", - "ROKU", - "ROL", - "ROOT", - "ROP", - "ROST", - "RPAY", - "RPD", - "RPRX", - "RRGB", - "RRR", - "RSG", - "RTX", - "RUM", - "RUN", - "RUSHA", - "RUSHB", - "RVMD", - "RVTY", - "RXRX", - "RXT", - "RYAAY", - "RYTM", - "SABR", - "SAFT", - "SAIA", - "SAIC", - "SAIL", - "SANA", - "SANM", - "SATS", - "SBAC", - "SBCF", - "SBGI", - "SBLK", - "SBRA", - "SBUX", - "SCHL", - "SCHW", - "SCSC", - "SDGR", - "SEAT", - "SEDG", - "SEER", - "SEIC", - "SENEA", - "SENS", - "SFIX", - "SFM", - "SFNC", - "SGHT", - "SGRY", - "SHBI", - "SHC", - "SHEN", - "SHIP", - "SHLS", - "SHOE", - "SHOO", - "SHOP", - "SHW", - "SIBN", - "SIGA", - "SIGI", - "SIMO", - "SIRI", - "SITM", - "SJM", - "SKIN", - "SKYW", - "SLAB", - "SLB", - "SLDP", - "SLM", - "SLP", - "SLRC", - "SMCI", - "SMPL", - "SMTC", - "SNA", - "SNDX", - "SNEX", - "SNPS", - "SNY", - "SO", - "SOFI", - "SOHU", - "SONO", - "SPG", - "SPGI", - "SPSC", - "SPT", - "SPWH", - "SRCE", - "SRE", - "SRPT", - "SRRK", - "SRTA", - "SRTS", - "SSNC", - "SSP", - "SSRM", - "SSYS", - "STAA", - "STBA", - "STE", - "STEP", - "STGW", - "STLD", - "STNE", - "STOK", - "STRA", - "STRL", - "STRO", - "STT", - "STX", - "STZ", - "SUPN", - "SVC", - "SW", - "SWBI", - "SWIM", - "SWK", - "SWKS", - "SYBT", - "SYF", - "SYK", - "SYM", - "SYNA", - "SYY", - "T", - "TAP", - "TARS", - "TASK", - "TBBK", - "TBCH", - "TBPH", - "TCBI", - "TCBK", - "TCMD", - "TCOM", - "TCPC", - "TCRT", - "TCX", - "TDG", - "TDY", - "TEAD", - "TEAM", - "TECH", - "TEL", - "TENB", - "TER", - "TFC", - "TFSL", - "TGT", - "TGTX", - "TH", - "THFF", - "THRM", - "THRY", - "TIGO", - "TIL", - "TILE", - "TITN", - "TJX", - "TKO", - "TLRY", - "TLS", - "TMCI", - "TMDX", - "TMO", - "TMUS", - "TNDM", - "TNXP", - "TOWN", - "TPL", - "TPR", - "TREE", - "TRGP", - "TRI", - "TRIN", - "TRIP", - "TRMB", - "TRMD", - "TRMK", - "TRNS", - "TROW", - "TRS", - "TRST", - "TRUP", - "TRV", - "TSCO", - "TSEM", - "TSLA", - "TSN", - "TT", - "TTD", - "TTEC", - "TTEK", - "TTGT", - "TTMI", - "TTWO", - "TVRD", - "TVTX", - "TW", - "TWST", - "TXG", - "TXMD", - "TXN", - "TXRH", - "TXT", - "TYL", - "UAL", - "UBER", - "UBSI", - "UCTT", - "UDR", - "UFCS", - "UFPI", - "UFPT", - "UHS", - "ULCC", - "ULH", - "ULTA", - "UMBF", - "UNH", - "UNIT", - "UNP", - "UONE", - "UPBD", - "UPLD", - "UPS", - "UPST", - "UPWK", - "URBN", - "URI", - "USB", - "UTHR", - "UVSP", - "V", - "VC", - "VCEL", - "VCTR", - "VCYT", - "VECO", - "VERI", - "VERU", - "VIAV", - "VICI", - "VICR", - "VIR", - "VISN", - "VITL", - "VLO", - "VLY", - "VMC", - "VNDA", - "VNET", - "VNOM", - "VOD", - "VRDN", - "VREX", - "VRM", - "VRNS", - "VRRM", - "VRSK", - "VRSN", - "VRT", - "VRTX", - "VSAT", - "VST", - "VSTM", - "VTR", - "VTRS", - "VUZI", - "VYGR", - "VZ", - "WAB", - "WABC", - "WAFD", - "WASH", - "WAT", - "WB", - "WDAY", - "WDC", - "WDFC", - "WEC", - "WELL", - "WEN", - "WERN", - "WFC", - "WFRD", - "WGS", - "WHWK", - "WINA", - "WING", - "WIX", - "WKHS", - "WM", - "WMB", - "WMG", - "WMT", - "WOOF", - "WRB", - "WRLD", - "WSBC", - "WSBF", - "WSC", - "WSFS", - "WSM", - "WST", - "WTFC", - "WTW", - "WW", - "WWD", - "WY", - "WYNN", - "XAIR", - "XEL", - "XENE", - "XMTR", - "XNCR", - "XOM", - "XP", - "XPEL", - "XRAY", - "XRX", - "XYL", - "XYZ", - "YORW", - "YUM", - "Z", - "ZBH", - "ZBRA", - "ZD", - "ZG", - "ZION", - "ZLAB", - "ZM", - "ZNTL", - "ZS", - "ZTS", - "ZUMZ", - "ZVRA", - "ZVZZT", - "ZYME" - ], - "n_symbols": 1500 - }, - { - "week": [ - 2022, - 37 - ], - "stats": { - "raw_pool": 2853, - "eligible_pre_mask": 2318, - "post_mask": 1500, - "mask_binds": true - }, - "symbols": [ - "A", - "AAL", - "AAON", - "AAPL", - "ABBV", - "ABCL", - "ABNB", - "ABT", - "ACAD", - "ACB", - "ACET", - "ACGL", - "ACHC", - "ACIW", - "ACLS", - "ACMR", - "ACN", - "ACRS", - "ACT", - "ADAM", - "ADBE", - "ADEA", - "ADI", - "ADM", - "ADP", - "ADPT", - "ADSK", - "ADTN", - "ADUS", - "ADV", - "AEE", - "AEHR", - "AEIS", - "AEP", - "AES", - "AEVA", - "AFCG", - "AFL", - "AFRM", - "AFYA", - "AGEN", - "AGIO", - "AGNC", - "AGNT", - "AGYS", - "AHCO", - "AIG", - "AIZ", - "AJG", - "AKAM", - "ALB", - "ALCO", - "ALDX", - "ALEC", - "ALGM", - "ALGN", - "ALGT", - "ALHC", - "ALKS", - "ALKT", - "ALL", - "ALLE", - "ALLO", - "ALNY", - "ALRM", - "ALT", - "ALXO", - "AMAL", - "AMAT", - "AMBA", - "AMCR", - "AMCX", - "AMD", - "AME", - "AMGN", - "AMKR", - "AMP", - "AMPH", - "AMRN", - "AMSC", - "AMSF", - "AMT", - "AMTX", - "AMZN", - "ANAB", - "ANDE", - "ANET", - "ANGI", - "ANGO", - "ANIK", - "ANIP", - "AON", - "AOS", - "AOSL", - "APA", - "APD", - "APH", - "APO", - "APOG", - "APP", - "APPF", - "APPN", - "APPS", - "APTV", - "APYX", - "ARCB", - "ARCC", - "ARCT", - "ARE", - "ARES", - "ARGX", - "ARKO", - "ARLP", - "ARQT", - "ARRY", - "ARTNA", - "ARVN", - "ARWR", - "ASLE", - "ASML", - "ASND", - "ASO", - "ASTE", - "ASTH", - "ASTL", - "ASTS", - "ATEC", - "ATER", - "ATEX", - "ATLC", - "ATNI", - "ATO", - "ATOM", - "ATRA", - "ATRC", - "AUDC", - "AUPH", - "AVAV", - "AVB", - "AVGO", - "AVIR", - "AVNW", - "AVO", - "AVT", - "AVXL", - "AVY", - "AWK", - "AXGN", - "AXON", - "AXP", - "AXSM", - "AXTI", - "AZO", - "AZTA", - "BA", - "BAC", - "BALL", - "BAND", - "BANF", - "BANR", - "BATRA", - "BATRK", - "BAX", - "BBIO", - "BBSI", - "BBY", - "BCAB", - "BCBP", - "BCML", - "BCPC", - "BCRX", - "BCTX", - "BCYC", - "BDX", - "BEAM", - "BEEM", - "BELFB", - "BEN", - "BF-B", - "BG", - "BHF", - "BHFAN", - "BIDU", - "BIIB", - "BILI", - "BIOX", - "BJRI", - "BK", - "BKNG", - "BKR", - "BL", - "BLDP", - "BLDR", - "BLFS", - "BLK", - "BLKB", - "BLMN", - "BLNK", - "BMBL", - "BMEA", - "BMRC", - "BMRN", - "BMY", - "BNGO", - "BNR", - "BNTX", - "BOKF", - "BOOM", - "BPOP", - "BR", - "BRK-B", - "BRKR", - "BRO", - "BSET", - "BSX", - "BSY", - "BTAI", - "BUSE", - "BVS", - "BWIN", - "BX", - "BXP", - "BYND", - "BZ", - "BZUN", - "C", - "CAC", - "CACC", - "CAG", - "CAH", - "CAKE", - "CALM", - "CAMT", - "CAR", - "CARG", - "CARR", - "CASH", - "CASS", - "CASY", - "CAT", - "CATY", - "CB", - "CBOE", - "CBRE", - "CBRL", - "CBSH", - "CCB", - "CCC", - "CCCC", - "CCD", - "CCEC", - "CCEP", - "CCI", - "CCL", - "CCOI", - "CCRN", - "CCXI", - "CDLX", - "CDNA", - "CDNS", - "CDW", - "CDXS", - "CECO", - "CELH", - "CELU", - "CENN", - "CENT", - "CENTA", - "CENX", - "CERT", - "CEVA", - "CF", - "CFFN", - "CFG", - "CG", - "CGBD", - "CGC", - "CGEM", - "CGNT", - "CGNX", - "CHCO", - "CHD", - "CHDN", - "CHEF", - "CHI", - "CHKP", - "CHRD", - "CHRS", - "CHRW", - "CHTR", - "CHW", - "CHY", - "CI", - "CIEN", - "CIGI", - "CINF", - "CL", - "CLAR", - "CLBK", - "CLDX", - "CLFD", - "CLMT", - "CLNE", - "CLPT", - "CLX", - "CMCO", - "CMCSA", - "CME", - "CMG", - "CMI", - "CMPR", - "CMPS", - "CMRC", - "CMS", - "CMTL", - "CNC", - "CNOB", - "CNP", - "CNXC", - "CNXN", - "CODX", - "COF", - "COGT", - "COHR", - "COHU", - "COIN", - "COKE", - "COLB", - "COLL", - "COLM", - "COO", - "COP", - "COR", - "CORT", - "COST", - "CPAY", - "CPB", - "CPRT", - "CPSS", - "CPT", - "CRAI", - "CRBU", - "CRCT", - "CRH", - "CRIS", - "CRL", - "CRM", - "CRMT", - "CRNC", - "CRNX", - "CROX", - "CRSP", - "CRSR", - "CRTO", - "CRUS", - "CRVL", - "CRWD", - "CSCO", - "CSGP", - "CSIQ", - "CSQ", - "CSTL", - "CSWC", - "CSX", - "CTAS", - "CTBI", - "CTKB", - "CTRA", - "CTRN", - "CTSH", - "CTVA", - "CVBF", - "CVCO", - "CVLT", - "CVNA", - "CVS", - "CVX", - "CWCO", - "CWST", - "CYRX", - "CYTK", - "CZR", - "D", - "DAL", - "DASH", - "DAVE", - "DBX", - "DCBO", - "DCGO", - "DD", - "DDOG", - "DE", - "DECK", - "DELL", - "DFTX", - "DG", - "DGICA", - "DGII", - "DGX", - "DH", - "DHI", - "DHR", - "DIOD", - "DIS", - "DJT", - "DKNG", - "DLO", - "DLR", - "DLTR", - "DMLP", - "DMRC", - "DNLI", - "DNUT", - "DOC", - "DOCU", - "DOMO", - "DOO", - "DORM", - "DOV", - "DOW", - "DOX", - "DOYU", - "DPZ", - "DRH", - "DRI", - "DRS", - "DRVN", - "DSGN", - "DSGR", - "DSGX", - "DTE", - "DTIL", - "DUK", - "DUOL", - "DVA", - "DVN", - "DXCM", - "DXLG", - "DXPE", - "DYN", - "EA", - "EBAY", - "EBC", - "ECHO", - "ECL", - "ECPG", - "ED", - "EDIT", - "EEFT", - "EFSC", - "EFX", - "EG", - "EGBN", - "EH", - "EHTH", - "EIX", - "EL", - "ELV", - "EME", - "EMR", - "ENPH", - "ENSG", - "ENTA", - "ENTG", - "ENVX", - "EOG", - "EOLS", - "EPAM", - "EQIX", - "EQR", - "EQT", - "ERAS", - "ERIC", - "ERIE", - "ERII", - "ES", - "ESLT", - "ESS", - "ESTA", - "ETN", - "ETR", - "EVCM", - "EVER", - "EVGO", - "EVRG", - "EW", - "EWBC", - "EWTX", - "EXC", - "EXE", - "EXEL", - "EXLS", - "EXPD", - "EXPE", - "EXPO", - "EXR", - "EXTR", - "EYE", - "EYPT", - "EZPW", - "F", - "FA", - "FANG", - "FAST", - "FATE", - "FBNC", - "FCEL", - "FCFS", - "FCNCA", - "FCX", - "FDMT", - "FDS", - "FDUS", - "FDX", - "FE", - "FELE", - "FFAI", - "FFBC", - "FFIN", - "FFIV", - "FHB", - "FIBK", - "FICO", - "FIS", - "FISV", - "FITB", - "FIVE", - "FIVN", - "FIX", - "FIZZ", - "FLEX", - "FLGT", - "FLNA", - "FLWS", - "FLYW", - "FMAO", - "FMNB", - "FNKO", - "FORM", - "FORR", - "FOX", - "FOXA", - "FOXF", - "FRHC", - "FRME", - "FROG", - "FRPT", - "FRT", - "FSLR", - "FSLY", - "FSV", - "FTAI", - "FTCI", - "FTDR", - "FTNT", - "FTV", - "FULC", - "FULT", - "FUTU", - "FWONA", - "FWONK", - "FWRD", - "GABC", - "GAIN", - "GBDC", - "GCMG", - "GD", - "GDDY", - "GDRX", - "GDS", - "GDYN", - "GE", - "GEN", - "GGAL", - "GGR", - "GH", - "GIII", - "GILD", - "GIS", - "GL", - "GLAD", - "GLBE", - "GLNG", - "GLPI", - "GLUE", - "GLW", - "GM", - "GMAB", - "GNRC", - "GNTX", - "GO", - "GOGO", - "GOOD", - "GOOG", - "GOOGL", - "GOSS", - "GOVX", - "GPC", - "GPN", - "GPRE", - "GPRO", - "GRFS", - "GRMN", - "GRPN", - "GS", - "GSAT", - "GSBC", - "GSHD", - "GSM", - "GT", - "GTM", - "GTX", - "GWW", - "HAFC", - "HAIN", - "HAL", - "HALO", - "HAPN", - "HAS", - "HBAN", - "HBNC", - "HCA", - "HCAT", - "HCKT", - "HCM", - "HCSG", - "HD", - "HDSN", - "HELE", - "HFWA", - "HIG", - "HII", - "HIMX", - "HLIT", - "HLMN", - "HLNE", - "HLT", - "HNRG", - "HOFT", - "HON", - "HOOD", - "HOPE", - "HPE", - "HPK", - "HPQ", - "HQY", - "HRL", - "HRMY", - "HRZN", - "HSIC", - "HST", - "HSTM", - "HSY", - "HTHT", - "HTLD", - "HTO", - "HUBB", - "HUBG", - "HUM", - "HURN", - "HUT", - "HWC", - "HWKN", - "HWM", - "HYFM", - "HYMC", - "IART", - "IBCP", - "IBKR", - "IBM", - "IBOC", - "IBRX", - "ICE", - "ICFI", - "ICHR", - "ICLR", - "ICUI", - "IDCC", - "IDXX", - "IDYA", - "IEP", - "IEX", - "IFF", - "IHRT", - "III", - "IIIV", - "ILMN", - "ILPT", - "IMCR", - "IMKTA", - "IMMR", - "IMTX", - "IMXI", - "INCY", - "INDB", - "INDI", - "INGN", - "INMD", - "INO", - "INSE", - "INSG", - "INSM", - "INTC", - "INTU", - "INVA", - "INVH", - "INVZ", - "IONS", - "IOSP", - "IOVA", - "IP", - "IPAR", - "IPGP", - "IQV", - "IR", - "IRDM", - "IRM", - "IRTC", - "IRWD", - "ISRG", - "IT", - "ITRI", - "ITW", - "IVZ", - "J", - "JACK", - "JAKK", - "JAZZ", - "JBHT", - "JBIO", - "JBL", - "JBLU", - "JBSS", - "JCI", - "JD", - "JJSF", - "JKHY", - "JNJ", - "JOUT", - "JOYY", - "JPM", - "JRVR", - "JYNT", - "KALU", - "KDP", - "KE", - "KELYA", - "KEY", - "KEYS", - "KHC", - "KIDS", - "KIM", - "KKR", - "KLAC", - "KLIC", - "KLRS", - "KMB", - "KMI", - "KNSA", - "KO", - "KOD", - "KPTI", - "KR", - "KRNT", - "KRNY", - "KROS", - "KRUS", - "KRYS", - "KTOS", - "KURA", - "KYMR", - "KYNB", - "L", - "LAMR", - "LAND", - "LASR", - "LAUR", - "LBRDA", - "LBRDK", - "LBTYA", - "LBTYK", - "LCID", - "LDOS", - "LE", - "LECO", - "LEGN", - "LEN", - "LENZ", - "LESL", - "LFST", - "LFUS", - "LGIH", - "LGND", - "LH", - "LHX", - "LI", - "LIDR", - "LII", - "LILA", - "LILAK", - "LIN", - "LIND", - "LITE", - "LIVN", - "LKFN", - "LKFT", - "LKQ", - "LLY", - "LMAT", - "LMT", - "LNT", - "LNTH", - "LOCO", - "LOGI", - "LONA", - "LOPE", - "LOVE", - "LOW", - "LPLA", - "LPRO", - "LPSN", - "LQDA", - "LQDT", - "LRCX", - "LSCC", - "LSTR", - "LULU", - "LUNG", - "LUV", - "LVS", - "LWLG", - "LYB", - "LYEL", - "LYFT", - "LYV", - "LZ", - "MA", - "MAA", - "MANH", - "MAR", - "MARA", - "MAS", - "MASS", - "MAT", - "MATW", - "MBIN", - "MBUU", - "MCD", - "MCFT", - "MCHB", - "MCHP", - "MCK", - "MCO", - "MCRB", - "MCRI", - "MDB", - "MDGL", - "MDLZ", - "MDT", - "MEDP", - "MELI", - "MEOH", - "MERC", - "MET", - "META", - "METC", - "MFIC", - "MGEE", - "MGM", - "MGNI", - "MGPI", - "MGRC", - "MIDD", - "MIRM", - "MITK", - "MKC", - "MKSI", - "MKTX", - "MLAB", - "MLCO", - "MLKN", - "MLM", - "MMM", - "MMSI", - "MMYT", - "MNDY", - "MNRO", - "MNST", - "MNTK", - "MNTS", - "MO", - "MORN", - "MOS", - "MPC", - "MPWR", - "MQ", - "MRCY", - "MRK", - "MRNA", - "MRSH", - "MRTN", - "MRVI", - "MRVL", - "MS", - "MSBI", - "MSCI", - "MSEX", - "MSFT", - "MSI", - "MSTR", - "MTB", - "MTCH", - "MTD", - "MTLS", - "MTSI", - "MU", - "MXCT", - "MXL", - "MYGN", - "MYRG", - "MZTI", - "NAVI", - "NBIX", - "NBP", - "NBTB", - "NCLH", - "NCNO", - "NDAQ", - "NDSN", - "NEE", - "NEGG", - "NEM", - "NEO", - "NEOG", - "NESR", - "NEWT", - "NEXT", - "NFBK", - "NFE", - "NFLX", - "NI", - "NICE", - "NKE", - "NKTR", - "NKTX", - "NMFC", - "NMIH", - "NMRK", - "NNOX", - "NOC", - "NOVT", - "NOW", - "NRC", - "NRG", - "NRIX", - "NSC", - "NSIT", - "NSLR", - "NSSC", - "NTAP", - "NTCT", - "NTES", - "NTGR", - "NTLA", - "NTNX", - "NTRA", - "NTRS", - "NUE", - "NVAX", - "NVCR", - "NVDA", - "NVMI", - "NVR", - "NVTS", - "NWBI", - "NWE", - "NWL", - "NWS", - "NWSA", - "NXPI", - "NXST", - "O", - "OCFC", - "OCSL", - "OCUL", - "ODFL", - "OFIX", - "OFLX", - "OKE", - "OKTA", - "OLED", - "OLLI", - "OM", - "OMAB", - "OMC", - "OMCL", - "ON", - "ONB", - "ONC", - "ONEW", - "OPCH", - "OPI", - "OPRT", - "OPRX", - "ORCL", - "ORLY", - "ORMP", - "OSBC", - "OSIS", - "OSPN", - "OSW", - "OTEX", - "OTIS", - "OTLY", - "OTTR", - "OUST", - "OXLC", - "OXY", - "OZK", - "PAA", - "PACB", - "PAGP", - "PAHC", - "PANW", - "PATK", - "PAX", - "PAYO", - "PAYX", - "PCAR", - "PCG", - "PCRX", - "PCT", - "PCTY", - "PCVX", - "PDD", - "PDFS", - "PEBO", - "PECO", - "PEG", - "PEGA", - "PENG", - "PENN", - "PEP", - "PERI", - "PETS", - "PFBC", - "PFE", - "PFG", - "PG", - "PGC", - "PGNY", - "PGR", - "PGY", - "PH", - "PHAT", - "PHM", - "PHUN", - "PI", - "PKG", - "PLAB", - "PLAY", - "PLCE", - "PLD", - "PLMR", - "PLRX", - "PLTK", - "PLTR", - "PLUG", - "PLUS", - "PLXS", - "PM", - "PMVP", - "PNC", - "PNR", - "PNTG", - "PNW", - "PODD", - "POOL", - "POWI", - "PPC", - "PPG", - "PPL", - "PPLI", - "PRAA", - "PRAX", - "PRCT", - "PRDO", - "PRGS", - "PRTA", - "PRTS", - "PRU", - "PRVA", - "PSA", - "PSEC", - "PSMT", - "PSX", - "PTC", - "PTCT", - "PTEN", - "PTGX", - "PTON", - "PUBM", - "PWP", - "PWR", - "PYPL", - "PZZA", - "QCOM", - "QCRH", - "QDEL", - "QFIN", - "QLYS", - "QNRX", - "QNST", - "QQQX", - "QRVO", - "QS", - "QTRX", - "QURE", - "RARE", - "RBCAA", - "RCKT", - "RCL", - "RCMT", - "RDNT", - "RDNW", - "RDWR", - "REG", - "REGN", - "RELL", - "REPL", - "REYN", - "RF", - "RGEN", - "RGLD", - "RGNX", - "RGP", - "RICK", - "RIGL", - "RILY", - "RIOT", - "RJF", - "RKLB", - "RL", - "RLAY", - "RLMD", - "RMBS", - "RMD", - "RMR", - "RNA", - "RNAC", - "RNW", - "ROAD", - "ROCK", - "ROK", - "ROKU", - "ROL", - "ROOT", - "ROP", - "ROST", - "RPAY", - "RPD", - "RPRX", - "RRGB", - "RRR", - "RSG", - "RTX", - "RUM", - "RUN", - "RUSHA", - "RUSHB", - "RVMD", - "RVTY", - "RXRX", - "RXT", - "RYAAY", - "RYTM", - "SABR", - "SAFT", - "SAIA", - "SAIC", - "SAIL", - "SANA", - "SANM", - "SATS", - "SBAC", - "SBCF", - "SBGI", - "SBLK", - "SBRA", - "SBUX", - "SCHL", - "SCHW", - "SCSC", - "SDGR", - "SEAT", - "SEDG", - "SEER", - "SEIC", - "SENEA", - "SENS", - "SFM", - "SFNC", - "SGHT", - "SGML", - "SGRY", - "SHC", - "SHEN", - "SHIP", - "SHLS", - "SHOE", - "SHOO", - "SHOP", - "SHW", - "SIBN", - "SIGA", - "SIGI", - "SIMO", - "SIRI", - "SITM", - "SJM", - "SKIN", - "SKYT", - "SKYW", - "SLAB", - "SLB", - "SLDP", - "SLM", - "SLP", - "SLRC", - "SMCI", - "SMPL", - "SMTC", - "SNA", - "SNDX", - "SNEX", - "SNPS", - "SNY", - "SO", - "SOFI", - "SOHU", - "SONO", - "SPG", - "SPGI", - "SPSC", - "SPT", - "SPWH", - "SRAD", - "SRCE", - "SRE", - "SRPT", - "SRRK", - "SRTS", - "SSNC", - "SSP", - "SSRM", - "SSTI", - "SSYS", - "STAA", - "STBA", - "STE", - "STEP", - "STGW", - "STLD", - "STNE", - "STOK", - "STRA", - "STRL", - "STRO", - "STT", - "STX", - "STZ", - "SUPN", - "SVC", - "SW", - "SWBI", - "SWK", - "SWKS", - "SYBT", - "SYF", - "SYK", - "SYM", - "SYNA", - "SYY", - "T", - "TAP", - "TARS", - "TASK", - "TBBK", - "TBCH", - "TBLD", - "TBPH", - "TCBI", - "TCBK", - "TCMD", - "TCOM", - "TCPC", - "TCRT", - "TCX", - "TDG", - "TDY", - "TEAM", - "TECH", - "TEL", - "TENB", - "TER", - "TFC", - "TFSL", - "TGT", - "TGTX", - "TH", - "THFF", - "THRM", - "THRY", - "TIGO", - "TIL", - "TILE", - "TITN", - "TJX", - "TKO", - "TLRY", - "TLS", - "TMCI", - "TMDX", - "TMO", - "TMUS", - "TNDM", - "TNXP", - "TOWN", - "TPL", - "TPR", - "TREE", - "TRGP", - "TRI", - "TRIN", - "TRIP", - "TRMB", - "TRMD", - "TRMK", - "TRNS", - "TROW", - "TRS", - "TRST", - "TRUP", - "TRV", - "TSCO", - "TSEM", - "TSLA", - "TSN", - "TT", - "TTD", - "TTEC", - "TTEK", - "TTGT", - "TTMI", - "TTWO", - "TVRD", - "TVTX", - "TW", - "TWST", - "TXG", - "TXMD", - "TXN", - "TXRH", - "TXT", - "TYL", - "UAL", - "UBER", - "UBSI", - "UCTT", - "UDR", - "UEIC", - "UFCS", - "UFPI", - "UFPT", - "UHS", - "ULCC", - "ULH", - "ULTA", - "UMBF", - "UNH", - "UNIT", - "UNP", - "UPBD", - "UPLD", - "UPS", - "UPST", - "UPWK", - "URBN", - "URI", - "USB", - "UTHR", - "UVSP", - "V", - "VC", - "VCEL", - "VCTR", - "VCYT", - "VECO", - "VERA", - "VERI", - "VERU", - "VERX", - "VIAV", - "VICI", - "VICR", - "VIR", - "VISN", - "VITL", - "VLO", - "VLY", - "VMC", - "VNDA", - "VNET", - "VNOM", - "VOD", - "VRDN", - "VREX", - "VRM", - "VRNS", - "VRRM", - "VRSK", - "VRSN", - "VRT", - "VRTX", - "VSAT", - "VST", - "VTGN", - "VTR", - "VTRS", - "VUZI", - "VYGR", - "VZ", - "WAB", - "WABC", - "WAFD", - "WASH", - "WAT", - "WB", - "WDAY", - "WDC", - "WDFC", - "WEC", - "WELL", - "WEN", - "WERN", - "WFC", - "WFRD", - "WGS", - "WHWK", - "WINA", - "WING", - "WIX", - "WKHS", - "WM", - "WMB", - "WMG", - "WMT", - "WOOF", - "WRB", - "WRLD", - "WSBC", - "WSBF", - "WSC", - "WSFS", - "WSM", - "WST", - "WTFC", - "WTW", - "WW", - "WWD", - "WY", - "WYNN", - "XAIR", - "XEL", - "XENE", - "XMTR", - "XNCR", - "XOM", - "XP", - "XPEL", - "XRAY", - "XRX", - "XYL", - "XYZ", - "YORW", - "YUM", - "Z", - "ZBH", - "ZBRA", - "ZD", - "ZG", - "ZION", - "ZLAB", - "ZM", - "ZNTL", - "ZS", - "ZTS", - "ZUMZ", - "ZVZZT", - "ZYME" - ], - "n_symbols": 1500 - }, - { - "week": [ - 2022, - 43 - ], - "stats": { - "raw_pool": 2905, - "eligible_pre_mask": 2307, - "post_mask": 1500, - "mask_binds": true - }, - "symbols": [ - "A", - "AAL", - "AAON", - "AAPL", - "ABBV", - "ABCL", - "ABNB", - "ABT", - "ACAD", - "ACB", - "ACET", - "ACGL", - "ACGLN", - "ACHC", - "ACIW", - "ACLS", - "ACMR", - "ACN", - "ACRS", - "ACT", - "ADAM", - "ADBE", - "ADEA", - "ADI", - "ADM", - "ADP", - "ADPT", - "ADSK", - "ADTN", - "ADUS", - "ADV", - "AEE", - "AEHR", - "AEIS", - "AEP", - "AES", - "AEVA", - "AFCG", - "AFL", - "AFRM", - "AFYA", - "AGEN", - "AGIO", - "AGNC", - "AGNCN", - "AGNCO", - "AGNCP", - "AGNT", - "AGYS", - "AHCO", - "AIG", - "AIZ", - "AJG", - "AKAM", - "ALB", - "ALCO", - "ALDX", - "ALEC", - "ALGM", - "ALGN", - "ALGT", - "ALHC", - "ALKS", - "ALKT", - "ALL", - "ALLE", - "ALLO", - "ALNT", - "ALNY", - "ALRM", - "ALT", - "ALXO", - "AMAL", - "AMAT", - "AMBA", - "AMCR", - "AMCX", - "AMD", - "AME", - "AMGN", - "AMKR", - "AMP", - "AMPH", - "AMPL", - "AMRN", - "AMSF", - "AMT", - "AMTX", - "AMZN", - "ANAB", - "ANDE", - "ANET", - "ANGI", - "ANGO", - "ANIK", - "ANIP", - "AON", - "AOS", - "AOSL", - "APA", - "APD", - "APEI", - "APH", - "APO", - "APOG", - "APP", - "APPF", - "APPN", - "APPS", - "APTV", - "ARCB", - "ARCC", - "ARCT", - "ARE", - "ARES", - "ARGX", - "ARKO", - "ARLP", - "ARQT", - "ARRY", - "ARTNA", - "ARVN", - "ARWR", - "ASLE", - "ASML", - "ASND", - "ASO", - "ASTE", - "ASTH", - "ASTL", - "ASTS", - "ATEC", - "ATER", - "ATEX", - "ATNI", - "ATO", - "ATOM", - "ATRA", - "ATRC", - "AUDC", - "AUPH", - "AVAV", - "AVB", - "AVGO", - "AVIR", - "AVNW", - "AVO", - "AVT", - "AVXL", - "AVY", - "AWK", - "AXGN", - "AXON", - "AXP", - "AXSM", - "AZO", - "AZTA", - "BA", - "BAC", - "BALL", - "BAND", - "BANF", - "BANR", - "BATRA", - "BATRK", - "BAX", - "BBIO", - "BBSI", - "BBY", - "BCAB", - "BCPC", - "BCRX", - "BCTX", - "BCYC", - "BDX", - "BEAM", - "BEEM", - "BELFB", - "BEN", - "BF-B", - "BFC", - "BG", - "BHF", - "BIDU", - "BIIB", - "BILI", - "BIOX", - "BJRI", - "BK", - "BKNG", - "BKR", - "BL", - "BLDP", - "BLDR", - "BLFS", - "BLK", - "BLKB", - "BLMN", - "BLNK", - "BMBL", - "BMRC", - "BMRN", - "BMY", - "BNGO", - "BNTX", - "BOKF", - "BOOM", - "BPOP", - "BR", - "BRK-B", - "BRKR", - "BRO", - "BSET", - "BSX", - "BSY", - "BTAI", - "BUSE", - "BWIN", - "BX", - "BXP", - "BYND", - "BZ", - "C", - "CAC", - "CACC", - "CAG", - "CAH", - "CAKE", - "CALM", - "CAMT", - "CAR", - "CARE", - "CARG", - "CARR", - "CASH", - "CASS", - "CASY", - "CAT", - "CATY", - "CB", - "CBOE", - "CBRE", - "CBRL", - "CBSH", - "CCAP", - "CCB", - "CCBG", - "CCC", - "CCCC", - "CCD", - "CCEC", - "CCEP", - "CCI", - "CCL", - "CCNE", - "CCOI", - "CCRN", - "CCSI", - "CDLX", - "CDNA", - "CDNS", - "CDW", - "CDXS", - "CECO", - "CELH", - "CELU", - "CENN", - "CENT", - "CENTA", - "CENX", - "CERT", - "CEVA", - "CF", - "CFFN", - "CFG", - "CG", - "CGBD", - "CGC", - "CGEM", - "CGNX", - "CHCO", - "CHD", - "CHDN", - "CHEF", - "CHI", - "CHKP", - "CHRD", - "CHRS", - "CHRW", - "CHTR", - "CHW", - "CHY", - "CI", - "CIEN", - "CIGI", - "CINF", - "CL", - "CLAR", - "CLBK", - "CLDX", - "CLFD", - "CLMT", - "CLNE", - "CLPT", - "CLX", - "CMCO", - "CMCSA", - "CME", - "CMG", - "CMI", - "CMPR", - "CMPS", - "CMRC", - "CMS", - "CMTL", - "CNC", - "CNOB", - "CNP", - "CNXC", - "CNXN", - "COCO", - "CODX", - "COF", - "COGT", - "COHR", - "COHU", - "COIN", - "COKE", - "COLB", - "COLL", - "COLM", - "COO", - "COP", - "COR", - "CORT", - "COST", - "CPAY", - "CPB", - "CPRT", - "CPT", - "CRAI", - "CRBU", - "CRCT", - "CRH", - "CRL", - "CRM", - "CRMT", - "CRNC", - "CRNX", - "CROX", - "CRSP", - "CRSR", - "CRTO", - "CRUS", - "CRVL", - "CRWD", - "CSCO", - "CSGP", - "CSIQ", - "CSQ", - "CSTL", - "CSWC", - "CSX", - "CTAS", - "CTBI", - "CTKB", - "CTRA", - "CTRN", - "CTSH", - "CTVA", - "CVBF", - "CVCO", - "CVLT", - "CVS", - "CVX", - "CWCO", - "CWST", - "CYRX", - "CYTK", - "CZR", - "D", - "DAL", - "DASH", - "DBX", - "DCBO", - "DCGO", - "DD", - "DDOG", - "DE", - "DECK", - "DELL", - "DG", - "DGICA", - "DGII", - "DGX", - "DH", - "DHI", - "DHR", - "DIOD", - "DIS", - "DJT", - "DKNG", - "DLO", - "DLR", - "DLTR", - "DMLP", - "DMRC", - "DNLI", - "DNUT", - "DOC", - "DOCU", - "DOMO", - "DOO", - "DORM", - "DOV", - "DOW", - "DOX", - "DPZ", - "DRH", - "DRI", - "DRS", - "DRVN", - "DSGN", - "DSGR", - "DSGX", - "DTE", - "DTIL", - "DUK", - "DUOL", - "DVA", - "DVN", - "DXCM", - "DXLG", - "DXPE", - "DYN", - "EA", - "EBAY", - "EBC", - "ECHO", - "ECL", - "ECPG", - "ED", - "EDIT", - "EEFT", - "EFSC", - "EFX", - "EG", - "EGBN", - "EIX", - "EL", - "ELV", - "EME", - "EMR", - "ENPH", - "ENSG", - "ENTA", - "ENTG", - "ENVX", - "EOG", - "EOLS", - "EPAM", - "EQIX", - "EQR", - "EQT", - "ERAS", - "ERIC", - "ERIE", - "ERII", - "ES", - "ESEA", - "ESLT", - "ESS", - "ESTA", - "ETN", - "ETR", - "EVCM", - "EVER", - "EVGO", - "EVRG", - "EW", - "EWBC", - "EWTX", - "EXC", - "EXE", - "EXEL", - "EXLS", - "EXPD", - "EXPE", - "EXPO", - "EXR", - "EXTR", - "EYE", - "EZPW", - "F", - "FA", - "FANG", - "FAST", - "FATE", - "FBNC", - "FCEL", - "FCFS", - "FCNCA", - "FCX", - "FDMT", - "FDS", - "FDUS", - "FDX", - "FE", - "FELE", - "FFAI", - "FFBC", - "FFIN", - "FFIV", - "FHB", - "FIBK", - "FICO", - "FIS", - "FISV", - "FITB", - "FIVE", - "FIVN", - "FIX", - "FIZZ", - "FLEX", - "FLGT", - "FLNA", - "FLNC", - "FLWS", - "FLYW", - "FMBH", - "FMNB", - "FNKO", - "FORM", - "FORR", - "FOX", - "FOXA", - "FOXF", - "FRHC", - "FRME", - "FROG", - "FRPT", - "FRSH", - "FRT", - "FSLR", - "FSLY", - "FSV", - "FTAI", - "FTCI", - "FTDR", - "FTNT", - "FTV", - "FULC", - "FULT", - "FUTU", - "FWONA", - "FWONK", - "FWRD", - "FWRG", - "GABC", - "GAIN", - "GBDC", - "GCMG", - "GD", - "GDDY", - "GDRX", - "GDS", - "GDYN", - "GE", - "GEN", - "GFS", - "GGAL", - "GGR", - "GH", - "GIII", - "GILD", - "GIS", - "GL", - "GLAD", - "GLBE", - "GLNG", - "GLPI", - "GLUE", - "GLW", - "GM", - "GMAB", - "GNRC", - "GNTX", - "GO", - "GOGO", - "GOOD", - "GOOG", - "GOOGL", - "GOSS", - "GOVX", - "GPC", - "GPN", - "GPRE", - "GPRO", - "GRFS", - "GRMN", - "GRPN", - "GS", - "GSAT", - "GSBC", - "GSHD", - "GSM", - "GT", - "GTLB", - "GTM", - "GTX", - "GWW", - "HAFC", - "HAIN", - "HAL", - "HALO", - "HAPN", - "HAS", - "HBAN", - "HBANP", - "HBNC", - "HCA", - "HCAT", - "HCKT", - "HCM", - "HCSG", - "HD", - "HDSN", - "HELE", - "HFWA", - "HIG", - "HII", - "HIMX", - "HLIT", - "HLMN", - "HLNE", - "HLT", - "HNRG", - "HOFT", - "HOLO", - "HON", - "HOOD", - "HOPE", - "HPE", - "HPK", - "HPQ", - "HQY", - "HRL", - "HRMY", - "HROW", - "HRZN", - "HSIC", - "HST", - "HSTM", - "HSY", - "HTHT", - "HTLD", - "HTO", - "HUBB", - "HUBG", - "HUM", - "HURN", - "HUT", - "HWC", - "HWKN", - "HWM", - "HYFM", - "HYMC", - "IART", - "IBCP", - "IBKR", - "IBM", - "IBOC", - "IBRX", - "ICE", - "ICFI", - "ICHR", - "ICLR", - "ICUI", - "IDCC", - "IDXX", - "IDYA", - "IEP", - "IEX", - "IFF", - "IHRT", - "IIIV", - "ILMN", - "IMCR", - "IMKTA", - "IMMR", - "IMTX", - "IMVT", - "IMXI", - "INCY", - "INDB", - "INDI", - "INGN", - "INMD", - "INO", - "INSE", - "INSG", - "INSM", - "INTA", - "INTC", - "INTU", - "INVA", - "INVH", - "IONS", - "IOSP", - "IOVA", - "IP", - "IPAR", - "IPGP", - "IQV", - "IR", - "IRDM", - "IRM", - "IRTC", - "IRWD", - "ISRG", - "IT", - "ITRI", - "ITW", - "IVZ", - "J", - "JACK", - "JAKK", - "JAZZ", - "JBHT", - "JBIO", - "JBL", - "JBLU", - "JBSS", - "JCI", - "JD", - "JJSF", - "JKHY", - "JNJ", - "JOUT", - "JOYY", - "JPM", - "JRVR", - "JYNT", - "KALU", - "KDP", - "KE", - "KELYA", - "KEY", - "KEYS", - "KHC", - "KIDS", - "KIM", - "KKR", - "KLAC", - "KLIC", - "KLRS", - "KLXE", - "KMB", - "KMI", - "KNSA", - "KO", - "KOD", - "KPRX", - "KPTI", - "KR", - "KRNT", - "KRNY", - "KROS", - "KRUS", - "KRYS", - "KTOS", - "KURA", - "KYMR", - "KYNB", - "L", - "LAMR", - "LAND", - "LASR", - "LAUR", - "LBRDA", - "LBRDK", - "LBTYA", - "LBTYK", - "LCID", - "LDOS", - "LE", - "LECO", - "LEGN", - "LEN", - "LESL", - "LFST", - "LFUS", - "LGIH", - "LGND", - "LH", - "LHX", - "LI", - "LII", - "LILA", - "LILAK", - "LIN", - "LIND", - "LITE", - "LIVN", - "LKFN", - "LKFT", - "LKQ", - "LLY", - "LMAT", - "LMT", - "LNT", - "LNTH", - "LOCO", - "LOGI", - "LOPE", - "LOVE", - "LOW", - "LPLA", - "LPRO", - "LPSN", - "LQDA", - "LQDT", - "LRCX", - "LSCC", - "LSTR", - "LULU", - "LUNG", - "LUV", - "LVS", - "LWLG", - "LYB", - "LYEL", - "LYFT", - "LYV", - "LZ", - "MA", - "MAA", - "MANH", - "MAR", - "MARA", - "MAS", - "MASS", - "MAT", - "MATW", - "MBIN", - "MBUU", - "MBWM", - "MCD", - "MCFT", - "MCHB", - "MCHP", - "MCK", - "MCO", - "MCRB", - "MCRI", - "MDB", - "MDGL", - "MDLZ", - "MDT", - "MEDP", - "MELI", - "MEOH", - "MERC", - "MET", - "META", - "METC", - "MFIC", - "MGEE", - "MGM", - "MGNI", - "MGPI", - "MGRC", - "MIDD", - "MIRM", - "MIST", - "MITK", - "MKC", - "MKSI", - "MKTX", - "MLAB", - "MLCO", - "MLKN", - "MLM", - "MMM", - "MMSI", - "MMYT", - "MNDY", - "MNRO", - "MNST", - "MNTK", - "MO", - "MORN", - "MOS", - "MPC", - "MPWR", - "MQ", - "MRCY", - "MRK", - "MRNA", - "MRSH", - "MRTN", - "MRVI", - "MRVL", - "MS", - "MSBI", - "MSCI", - "MSEX", - "MSFT", - "MSI", - "MSTR", - "MTB", - "MTCH", - "MTD", - "MTLS", - "MTSI", - "MU", - "MXCT", - "MXL", - "MYGN", - "MYRG", - "MZTI", - "NAVI", - "NBIX", - "NBTB", - "NCLH", - "NCNO", - "NDAQ", - "NDSN", - "NEE", - "NEGG", - "NEM", - "NEO", - "NEOG", - "NESR", - "NEWT", - "NEXT", - "NFBK", - "NFE", - "NFLX", - "NI", - "NICE", - "NKE", - "NKTR", - "NKTX", - "NMFC", - "NMIH", - "NMRK", - "NNOX", - "NOC", - "NOVT", - "NOW", - "NRC", - "NRG", - "NRIX", - "NSC", - "NSIT", - "NSSC", - "NTAP", - "NTCT", - "NTES", - "NTGR", - "NTLA", - "NTNX", - "NTRA", - "NTRS", - "NUE", - "NVAX", - "NVCR", - "NVDA", - "NVMI", - "NVR", - "NWBI", - "NWE", - "NWL", - "NWPX", - "NWS", - "NWSA", - "NXPI", - "NXST", - "O", - "OCFC", - "OCSL", - "ODFL", - "OFIX", - "OFLX", - "OKE", - "OKTA", - "OLED", - "OLLI", - "OM", - "OMAB", - "OMC", - "OMCL", - "ON", - "ONB", - "ONC", - "ONEW", - "OPCH", - "OPI", - "OPRT", - "OPRX", - "ORCL", - "ORLY", - "ORMP", - "OSBC", - "OSIS", - "OSPN", - "OSW", - "OTEX", - "OTIS", - "OTLY", - "OTTR", - "OUST", - "OXLC", - "OXY", - "OZK", - "PAA", - "PACB", - "PAGP", - "PAHC", - "PAMT", - "PANW", - "PATK", - "PAX", - "PAYO", - "PAYX", - "PCAR", - "PCG", - "PCRX", - "PCT", - "PCTY", - "PCVX", - "PDD", - "PDFS", - "PEBO", - "PECO", - "PEG", - "PEGA", - "PENG", - "PENN", - "PEP", - "PERI", - "PETS", - "PFBC", - "PFE", - "PFG", - "PG", - "PGC", - "PGNY", - "PGR", - "PGY", - "PH", - "PHAT", - "PHM", - "PHUN", - "PI", - "PKG", - "PLAB", - "PLAY", - "PLCE", - "PLD", - "PLMR", - "PLPC", - "PLRX", - "PLTK", - "PLTR", - "PLUG", - "PLUS", - "PLXS", - "PM", - "PMVP", - "PNC", - "PNR", - "PNTG", - "PNW", - "PODD", - "POOL", - "POWI", - "PPC", - "PPG", - "PPL", - "PPLI", - "PRAA", - "PRAX", - "PRCT", - "PRDO", - "PRGS", - "PRTA", - "PRTS", - "PRU", - "PRVA", - "PSA", - "PSEC", - "PSMT", - "PSX", - "PTC", - "PTCT", - "PTEN", - "PTGX", - "PTLO", - "PTON", - "PUBM", - "PWP", - "PWR", - "PYPL", - "PZZA", - "QCOM", - "QCRH", - "QDEL", - "QFIN", - "QLYS", - "QNRX", - "QNST", - "QQQX", - "QRVO", - "QS", - "QTRX", - "QURE", - "RARE", - "RCKT", - "RCL", - "RCMT", - "RDNT", - "RDNW", - "RDWR", - "REG", - "REGN", - "RELL", - "RELY", - "RENT", - "REPL", - "REYN", - "RF", - "RGEN", - "RGLD", - "RGNX", - "RGP", - "RICK", - "RIGL", - "RILY", - "RIOT", - "RJF", - "RKLB", - "RL", - "RLAY", - "RLMD", - "RMBS", - "RMD", - "RMNI", - "RMR", - "RNA", - "RNAC", - "RNW", - "ROAD", - "ROCK", - "ROIV", - "ROK", - "ROKU", - "ROL", - "ROOT", - "ROP", - "ROST", - "RPAY", - "RPD", - "RPRX", - "RRGB", - "RRR", - "RSG", - "RTX", - "RUM", - "RUN", - "RUSHA", - "RVMD", - "RVTY", - "RXRX", - "RXT", - "RYAAY", - "RYTM", - "SABR", - "SAFT", - "SAIA", - "SAIC", - "SANA", - "SANM", - "SATS", - "SBAC", - "SBCF", - "SBGI", - "SBLK", - "SBRA", - "SBUX", - "SCHL", - "SCHW", - "SCSC", - "SDGR", - "SEAT", - "SEDG", - "SEER", - "SEIC", - "SENEA", - "SENS", - "SFM", - "SFNC", - "SGHT", - "SGML", - "SGRY", - "SHC", - "SHEN", - "SHLS", - "SHOE", - "SHOO", - "SHOP", - "SHW", - "SIBN", - "SIGA", - "SIGI", - "SIMO", - "SIRI", - "SITM", - "SJM", - "SKIN", - "SKYT", - "SKYW", - "SLAB", - "SLB", - "SLDP", - "SLM", - "SLP", - "SLRC", - "SMBC", - "SMCI", - "SMPL", - "SMTC", - "SNA", - "SNDX", - "SNEX", - "SNPS", - "SNY", - "SO", - "SOFI", - "SOHU", - "SONO", - "SPFI", - "SPG", - "SPGI", - "SPSC", - "SPT", - "SPWH", - "SRAD", - "SRCE", - "SRE", - "SRPT", - "SRRK", - "SRTS", - "SSNC", - "SSP", - "SSRM", - "SSTI", - "SSYS", - "STAA", - "STBA", - "STE", - "STEP", - "STGW", - "STLD", - "STNE", - "STOK", - "STRA", - "STRL", - "STRO", - "STT", - "STX", - "STZ", - "SUPN", - "SVC", - "SW", - "SWBI", - "SWK", - "SWKS", - "SYBT", - "SYF", - "SYK", - "SYM", - "SYNA", - "SYY", - "T", - "TAP", - "TARS", - "TASK", - "TBBK", - "TBCH", - "TBLD", - "TBPH", - "TCBI", - "TCBK", - "TCMD", - "TCOM", - "TCPC", - "TCRT", - "TCX", - "TDG", - "TDY", - "TEAM", - "TECH", - "TEL", - "TENB", - "TER", - "TFC", - "TFSL", - "TGT", - "TGTX", - "TH", - "THFF", - "THRM", - "THRY", - "TIGO", - "TIL", - "TILE", - "TITN", - "TJX", - "TKO", - "TLRY", - "TLS", - "TMCI", - "TMDX", - "TMO", - "TMUS", - "TNDM", - "TNXP", - "TOWN", - "TPL", - "TPR", - "TREE", - "TRGP", - "TRI", - "TRIN", - "TRIP", - "TRMB", - "TRMD", - "TRMK", - "TRNS", - "TROW", - "TRS", - "TRST", - "TRUP", - "TRV", - "TSCO", - "TSEM", - "TSLA", - "TSN", - "TT", - "TTD", - "TTEC", - "TTEK", - "TTGT", - "TTMI", - "TTWO", - "TVRD", - "TVTX", - "TW", - "TWST", - "TXG", - "TXN", - "TXRH", - "TXT", - "TYL", - "UAL", - "UBER", - "UBSI", - "UCTT", - "UDR", - "UEIC", - "UFCS", - "UFPI", - "UFPT", - "UHS", - "ULCC", - "ULH", - "ULTA", - "UMBF", - "UNH", - "UNIT", - "UNP", - "UPBD", - "UPLD", - "UPS", - "UPST", - "UPWK", - "URBN", - "URI", - "USB", - "UTHR", - "UVSP", - "V", - "VC", - "VCEL", - "VCTR", - "VCYT", - "VECO", - "VERA", - "VERI", - "VERU", - "VERX", - "VIAV", - "VICI", - "VICR", - "VIR", - "VISN", - "VITL", - "VLO", - "VLY", - "VMC", - "VNDA", - "VNOM", - "VOD", - "VRDN", - "VREX", - "VRM", - "VRNS", - "VRRM", - "VRSK", - "VRSN", - "VRT", - "VRTX", - "VSAT", - "VST", - "VTR", - "VTRS", - "VUZI", - "VZ", - "WAB", - "WABC", - "WAFD", - "WASH", - "WAT", - "WB", - "WDAY", - "WDC", - "WDFC", - "WEC", - "WELL", - "WEN", - "WERN", - "WEST", - "WFC", - "WFRD", - "WGS", - "WHWK", - "WINA", - "WING", - "WIX", - "WKHS", - "WLDN", - "WM", - "WMB", - "WMG", - "WMT", - "WOOF", - "WRB", - "WRLD", - "WSBC", - "WSBF", - "WSC", - "WSFS", - "WSM", - "WST", - "WTFC", - "WTW", - "WWD", - "WY", - "WYNN", - "XAIR", - "XEL", - "XENE", - "XMTR", - "XNCR", - "XOM", - "XP", - "XPEL", - "XRAY", - "XRX", - "XYL", - "XYZ", - "YORW", - "YUM", - "Z", - "ZBH", - "ZBRA", - "ZD", - "ZG", - "ZION", - "ZLAB", - "ZM", - "ZNTL", - "ZS", - "ZTS", - "ZUMZ", - "ZVRA", - "ZVZZT", - "ZYME" - ], - "n_symbols": 1500 - }, - { - "week": [ - 2022, - 25 - ], - "stats": { - "raw_pool": 493, - "eligible_pre_mask": 492, - "post_mask": 492, - "mask_binds": false - }, - "symbols": [ - "A", - "AAPL", - "ABBV", - "ABNB", - "ABT", - "ACGL", - "ACN", - "ADBE", - "ADI", - "ADM", - "ADP", - "ADSK", - "AEE", - "AEP", - "AES", - "AFL", - "AIG", - "AIZ", - "AJG", - "AKAM", - "ALB", - "ALGN", - "ALL", - "ALLE", - "AMAT", - "AMCR", - "AMD", - "AME", - "AMGN", - "AMP", - "AMT", - "AMZN", - "ANET", - "AON", - "AOS", - "APA", - "APD", - "APH", - "APO", - "APP", - "APTV", - "ARE", - "ARES", - "ATO", - "AVB", - "AVGO", - "AVY", - "AWK", - "AXON", - "AXP", - "AZO", - "BA", - "BAC", - "BALL", - "BAX", - "BBY", - "BDX", - "BEN", - "BF-B", - "BG", - "BIIB", - "BK", - "BKNG", - "BKR", - "BLDR", - "BLK", - "BMY", - "BR", - "BRK-B", - "BRO", - "BSX", - "BX", - "BXP", - "C", - "CAG", - "CAH", - "CARR", - "CASY", - "CAT", - "CB", - "CBOE", - "CBRE", - "CCI", - "CCL", - "CDNS", - "CDW", - "CF", - "CFG", - "CHD", - "CHRW", - "CHTR", - "CI", - "CIEN", - "CINF", - "CL", - "CLX", - "CMCSA", - "CME", - "CMG", - "CMI", - "CMS", - "CNC", - "CNP", - "COF", - "COHR", - "COIN", - "COO", - "COP", - "COR", - "COST", - "CPAY", - "CPB", - "CPRT", - "CPT", - "CRH", - "CRL", - "CRM", - "CRWD", - "CSCO", - "CSGP", - "CSX", - "CTAS", - "CTRA", - "CTSH", - "CTVA", - "CVNA", - "CVS", - "CVX", - "D", - "DAL", - "DASH", - "DD", - "DDOG", - "DE", - "DECK", - "DELL", - "DG", - "DGX", - "DHI", - "DHR", - "DIS", - "DLR", - "DLTR", - "DOC", - "DOV", - "DOW", - "DPZ", - "DRI", - "DTE", - "DUK", - "DVA", - "DVN", - "DXCM", - "EA", - "EBAY", - "ECL", - "ED", - "EFX", - "EG", - "EIX", - "EL", - "ELV", - "EME", - "EMR", - "EOG", - "EPAM", - "EQIX", - "EQR", - "EQT", - "ERIE", - "ES", - "ESS", - "ETN", - "ETR", - "EVRG", - "EW", - "EXC", - "EXE", - "EXPD", - "EXPE", - "EXR", - "F", - "FANG", - "FAST", - "FCX", - "FDS", - "FDX", - "FE", - "FFIV", - "FICO", - "FIS", - "FISV", - "FITB", - "FIX", - "FOX", - "FOXA", - "FRT", - "FSLR", - "FTNT", - "FTV", - "GD", - "GDDY", - "GE", - "GEN", - "GILD", - "GIS", - "GL", - "GLW", - "GM", - "GNRC", - "GOOG", - "GOOGL", - "GPC", - "GPN", - "GRMN", - "GS", - "GWW", - "HAL", - "HAS", - "HBAN", - "HCA", - "HD", - "HIG", - "HII", - "HLT", - "HON", - "HPE", - "HPQ", - "HRL", - "HSIC", - "HST", - "HSY", - "HUBB", - "HUM", - "HWM", - "IBKR", - "IBM", - "ICE", - "IDXX", - "IEX", - "IFF", - "INCY", - "INTC", - "INTU", - "INVH", - "IP", - "IQV", - "IR", - "IRM", - "ISRG", - "IT", - "ITW", - "IVZ", - "J", - "JBHT", - "JBL", - "JCI", - "JKHY", - "JNJ", - "JPM", - "KDP", - "KEY", - "KEYS", - "KHC", - "KIM", - "KKR", - "KLAC", - "KMB", - "KMI", - "KO", - "KR", - "L", - "LDOS", - "LEN", - "LH", - "LHX", - "LII", - "LIN", - "LITE", - "LLY", - "LMT", - "LNT", - "LOW", - "LRCX", - "LULU", - "LUV", - "LVS", - "LYB", - "LYV", - "MA", - "MAA", - "MAR", - "MAS", - "MCD", - "MCHP", - "MCK", - "MCO", - "MDLZ", - "MDT", - "MET", - "META", - "MGM", - "MKC", - "MLM", - "MMM", - "MNST", - "MO", - "MOS", - "MPC", - "MPWR", - "MRK", - "MRNA", - "MRSH", - "MS", - "MSCI", - "MSFT", - "MSI", - "MSTR", - "MTB", - "MTD", - "MU", - "NCLH", - "NDAQ", - "NDSN", - "NEE", - "NEM", - "NFLX", - "NI", - "NKE", - "NOC", - "NOW", - "NRG", - "NSC", - "NTAP", - "NTRS", - "NUE", - "NVDA", - "NVR", - "NWS", - "NWSA", - "NXPI", - "O", - "ODFL", - "OKE", - "OMC", - "ON", - "ORCL", - "ORLY", - "OTIS", - "OXY", - "PANW", - "PAYX", - "PCAR", - "PCG", - "PEG", - "PEP", - "PFE", - "PFG", - "PG", - "PGR", - "PH", - "PHM", - "PKG", - "PLD", - "PLTR", - "PM", - "PNC", - "PNR", - "PNW", - "PODD", - "POOL", - "PPG", - "PPL", - "PRU", - "PSA", - "PSX", - "PTC", - "PWR", - "PYPL", - "QCOM", - "RCL", - "REG", - "REGN", - "RF", - "RJF", - "RL", - "RMD", - "ROK", - "ROL", - "ROP", - "ROST", - "RSG", - "RTX", - "RVTY", - "SATS", - "SBAC", - "SBUX", - "SCHW", - "SHW", - "SJM", - "SLB", - "SNA", - "SNPS", - "SO", - "SPG", - "SPGI", - "SRE", - "STE", - "STLD", - "STT", - "STX", - "STZ", - "SW", - "SWK", - "SWKS", - "SYF", - "SYK", - "SYY", - "T", - "TAP", - "TDG", - "TDY", - "TECH", - "TEL", - "TER", - "TFC", - "TGT", - "TJX", - "TKO", - "TMO", - "TMUS", - "TPL", - "TPR", - "TRGP", - "TRMB", - "TROW", - "TRV", - "TSCO", - "TSLA", - "TSN", - "TT", - "TTD", - "TTWO", - "TXN", - "TXT", - "TYL", - "UAL", - "UBER", - "UDR", - "UHS", - "ULTA", - "UNH", - "UNP", - "UPS", - "URI", - "USB", - "V", - "VICI", - "VLO", - "VMC", - "VRSK", - "VRSN", - "VRT", - "VRTX", - "VST", - "VTR", - "VTRS", - "VZ", - "WAB", - "WAT", - "WDAY", - "WDC", - "WEC", - "WELL", - "WFC", - "WM", - "WMB", - "WMT", - "WRB", - "WSM", - "WST", - "WTW", - "WY", - "WYNN", - "XEL", - "XOM", - "XYL", - "XYZ", - "YUM", - "ZBH", - "ZBRA", - "ZTS" - ], - "n_symbols": 492 - } - ], "interpretation": { "harness_and_shared_filter_agree": true, "mask_binds_pct": 97.1, @@ -6689,5 +120,6 @@ "compositional_story": "fip_id pools continuous winners (neg IC) vs continuous bleeders (pos IC). Prod-subset and senior liquid stay negative; junior liquid is less negative / positive \u2014 composition, not jumpiness premium.", "vol_tilt_warning": "High-vol names underperform on breadth relative to S&P-like books. Re-validate production 80/20 high-vol tilt before any universe broaden." }, - "platform_verdict": "Mom-conditional fip ALIVE as book-tilt candidate (needs book sim) \u2014 not production wire-in. Unconditional fip not green." -} \ No newline at end of file + "platform_verdict": "Mom-conditional fip ALIVE as book-tilt candidate (needs book sim) \u2014 not production wire-in. Unconditional fip not green.", + "membership_dumps_note": "Removed 5 week membership symbol lists from the committed artifact (compact decision evidence). Full dumps recoverable from git history of this file pre-cleanup." +} diff --git a/scripts/extend_snapshot_universe.py b/scripts/extend_snapshot_universe.py index 9d5ab37..886d9e2 100644 --- a/scripts/extend_snapshot_universe.py +++ b/scripts/extend_snapshot_universe.py @@ -10,8 +10,13 @@ Pipeline 3. Fetch ~5y daily bars from Alpaca for symbols missing (or short) in the copy. 4. Insert new tickers + OHLCV; mark them in side table ``research_rank_only`` so the harness can feed signal IC without GTL/candidate replay. +5. Write a **completion manifest** (``.manifest.json``) with ticker / + OHLCV / rank_only counts and finished-at. Breadth runners refuse to start + without a matching complete manifest — same class of guard as calendar + truncation (see 2026-07-18 21:14 race: orphaned +0.0575 on a partial pool). Resume-friendly: re-running skips symbols that already have ≥ ``--min-bars``. +A ``--limit`` smoke run writes ``complete: false`` so breadth mode still refuses. Example ------- @@ -197,12 +202,21 @@ async def _fetch_symbol_bars( async def _main() -> None: + # ROOT is already on sys.path; keep the helper import path-local. + from research_snapshot_manifest import ( # type: ignore[import-not-found] + clear_manifest, + write_completion_manifest, + ) + args = _parse_args() source = Path(args.source) output = Path(args.output) if not source.exists(): raise SystemExit(f"Source snapshot not found: {source}") + # Any rebuild/update invalidates prior completion until we finish cleanly. + clear_manifest(output) + if args.force_copy or not output.exists(): output.parent.mkdir(parents=True, exist_ok=True) if output.exists(): @@ -375,12 +389,40 @@ async def _main() -> None: text("SELECT COUNT(*) FROM ohlcv_records") ).scalar_one() + # Full planned work only when --limit is unset. Smoke runs stay incomplete + # so breadth mode cannot mythologize a 50-symbol toy pool. + is_complete = args.limit is None + manifest_path = write_completion_manifest( + output, + complete=is_complete, + sources=sources, + history_days=int(args.history_days), + min_bars=int(args.min_bars), + fetch_ok=ok, + fetch_fail=fail, + limit=args.limit, + extra={ + "prod_symbols_at_start": len(prod_symbols), + "pool_size": len(pool), + "to_fetch": len(to_fetch), + }, + ) + print("Done.") print(f" output: {output}") print(f" tickers: {ticker_n}") print(f" ohlcv rows: {ohlcv_n}") print(f" research_rank_only: {rank_only_n}") print(f" fetched ok/fail: {ok}/{fail}") + print( + f" completion manifest: {manifest_path} " + f"(complete={is_complete})" + ) + if not is_complete: + print( + " NOTE: --limit set → complete=false; breadth runners will refuse " + "this snapshot until a full extend finishes." + ) if __name__ == "__main__": diff --git a/scripts/research_snapshot_manifest.py b/scripts/research_snapshot_manifest.py new file mode 100644 index 0000000..6b765a9 --- /dev/null +++ b/scripts/research_snapshot_manifest.py @@ -0,0 +1,172 @@ +"""Completion manifest for research.sqlite — cheap race guard. + +The 2026-07-18 21:14 breadth run fired while ``extend_snapshot_universe`` was +still (or had just been) building the snapshot. Harness and shared-filter +recomputes agree on *complete* data, so the orphaned +0.0575 was incomplete +universe, not a code path bug. + +Same class of protection as calendar-truncation assertions in the research +matrix: refuse to read results from a half-built artifact. + +Layout +------ +Sidecar path: ``.manifest.json`` next to the sqlite file +(e.g. ``backtest_snapshots/research.sqlite.manifest.json``). +""" + +from __future__ import annotations + +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from sqlalchemy import create_engine, text + +MANIFEST_SCHEMA_VERSION = 1 + + +def manifest_path_for(snapshot: Path) -> Path: + """Sidecar path for a research snapshot.""" + return Path(str(snapshot) + ".manifest.json") + + +def _count_snapshot(snapshot: Path) -> dict[str, int]: + engine = create_engine( + f"sqlite:///{snapshot.resolve().as_posix()}", + future=True, + ) + try: + with engine.connect() as conn: + ticker_n = int(conn.execute(text("SELECT COUNT(*) FROM tickers")).scalar_one()) + ohlcv_n = int( + conn.execute(text("SELECT COUNT(*) FROM ohlcv_records")).scalar_one() + ) + try: + rank_only_n = int( + conn.execute(text("SELECT COUNT(*) FROM research_rank_only")).scalar_one() + ) + except Exception: + rank_only_n = 0 + finally: + engine.dispose() + return { + "ticker_count": ticker_n, + "ohlcv_row_count": ohlcv_n, + "rank_only_count": rank_only_n, + } + + +def write_completion_manifest( + snapshot: Path, + *, + complete: bool, + sources: dict[str, str] | None = None, + history_days: int | None = None, + min_bars: int | None = None, + fetch_ok: int | None = None, + fetch_fail: int | None = None, + limit: int | None = None, + extra: dict[str, Any] | None = None, +) -> Path: + """Write (or overwrite) the sidecar completion manifest for *snapshot*.""" + snapshot = Path(snapshot) + counts = _count_snapshot(snapshot) if snapshot.exists() else { + "ticker_count": 0, + "ohlcv_row_count": 0, + "rank_only_count": 0, + } + payload: dict[str, Any] = { + "schema_version": MANIFEST_SCHEMA_VERSION, + "snapshot": snapshot.name, + "snapshot_resolved": str(snapshot.resolve()) if snapshot.exists() else str(snapshot), + "complete": bool(complete), + "finished_at": datetime.now(timezone.utc).isoformat(), + **counts, + "sources": sources or {}, + "history_days": history_days, + "min_bars": min_bars, + "fetch_ok": fetch_ok, + "fetch_fail": fetch_fail, + "limit": limit, + } + if extra: + payload["extra"] = extra + path = manifest_path_for(snapshot) + path.write_text(json.dumps(payload, indent=2, default=str) + "\n", encoding="utf-8") + return path + + +def clear_manifest(snapshot: Path) -> None: + """Remove any existing completion manifest (start of a rebuild).""" + path = manifest_path_for(Path(snapshot)) + if path.exists(): + path.unlink() + + +def load_manifest(snapshot: Path) -> dict[str, Any] | None: + path = manifest_path_for(Path(snapshot)) + if not path.exists(): + return None + return json.loads(path.read_text(encoding="utf-8")) + + +def assert_research_snapshot_complete(snapshot: Path) -> dict[str, Any]: + """Refuse breadth-mode work unless the extender finished cleanly. + + Raises ``SystemExit`` with a clear message on any failure (missing + manifest, incomplete flag, or live counts that no longer match the + recorded totals — e.g. a mid-run overwrite of the sqlite file). + """ + snapshot = Path(snapshot) + if not snapshot.exists(): + raise SystemExit( + f"Research snapshot missing: {snapshot}\n" + "Build it with: python scripts/extend_snapshot_universe.py" + ) + + path = manifest_path_for(snapshot) + if not path.exists(): + raise SystemExit( + f"Research snapshot completion manifest missing: {path}\n" + "Refusing breadth run — this is the guard that would have caught " + "the 2026-07-18 21:14 race against a half-built research.sqlite.\n" + "Re-run extend_snapshot_universe.py to completion (no --limit), " + "or for a trusted existing full snapshot:\n" + " python -c \"from pathlib import Path; " + "from scripts.research_snapshot_manifest import write_completion_manifest; " + f"write_completion_manifest(Path(r'{snapshot}'), complete=True)\"" + ) + + try: + manifest = json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise SystemExit(f"Corrupt research snapshot manifest {path}: {exc}") from exc + + if not manifest.get("complete"): + raise SystemExit( + f"Research snapshot marked incomplete in {path}\n" + f"(finished_at={manifest.get('finished_at')}, limit={manifest.get('limit')}).\n" + "Re-run extend_snapshot_universe.py without --limit until Done." + ) + + live = _count_snapshot(snapshot) + mismatches: list[str] = [] + for key in ("ticker_count", "ohlcv_row_count", "rank_only_count"): + recorded = manifest.get(key) + if recorded is None: + mismatches.append(f"{key}: missing in manifest") + continue + if int(recorded) != int(live[key]): + mismatches.append( + f"{key}: manifest={recorded} live={live[key]}" + ) + if mismatches: + raise SystemExit( + "Research snapshot does not match its completion manifest " + f"({path}). Likely a partial rewrite or concurrent extend:\n - " + + "\n - ".join(mismatches) + + "\nRe-run extend_snapshot_universe.py to completion." + ) + + return {**manifest, "live_counts": live} diff --git a/scripts/run_fip_breadth_diagnostics.py b/scripts/run_fip_breadth_diagnostics.py index 3625328..3c55376 100644 --- a/scripts/run_fip_breadth_diagnostics.py +++ b/scripts/run_fip_breadth_diagnostics.py @@ -3,9 +3,9 @@ Uses the same collection + ``_filter_liquid_breadth_week_rich`` as ``run_backtest`` signal_eval. No parallel mask implementation. -Reconciles the harness +0.0575 vs prior dual-path −0.017 disagreement by -deleting the second mask, dumping membership/pre-post stats, and re-running -mom-conditional IC through the surviving path only. +Single-sourced liquid-breadth fip diagnostics through harness mask helpers. +Re-runs unconditional / tier / prod-subset / mom-conditional ICs and context +signals. Requires a complete research.sqlite completion manifest. Research branch only. Example: @@ -49,7 +49,12 @@ def _parse_args() -> argparse.Namespace: p.add_argument("--min-price", type=float, default=5.0) p.add_argument("--workers", type=int, default=max(1, (mp.cpu_count() or 4) - 1)) p.add_argument("--allow-spawn", action="store_true") - p.add_argument("--dump-weeks", type=int, default=5, help="How many weeks to dump membership for") + p.add_argument( + "--dump-weeks", + type=int, + default=0, + help="Weeks of liquid membership symbol lists to embed (default 0 — keep reports compact)", + ) p.add_argument("--out", default=None) p.add_argument("--quiet", action="store_true") return p.parse_args() @@ -173,8 +178,23 @@ def main() -> None: args = _parse_args() research = Path(args.research_snapshot) prod = Path(args.prod_snapshot) - if not research.exists(): - raise SystemExit(f"Missing {research}") + + # Refuse half-built research.sqlite (2026-07-18 21:14 race). + scripts_dir = Path(__file__).resolve().parent + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from research_snapshot_manifest import ( # type: ignore[import-not-found] + assert_research_snapshot_complete, + ) + + manifest = assert_research_snapshot_complete(research) + if not args.quiet: + print( + f"Manifest ok: tickers={manifest.get('ticker_count')} " + f"ohlcv={manifest.get('ohlcv_row_count')} " + f"finished_at={manifest.get('finished_at')}", + flush=True, + ) # Force harness liquid-mode collection (same env as breadth run). os.environ["BACKTEST_LIQUID_BREADTH"] = str(int(args.top_n)) @@ -503,9 +523,9 @@ def main() -> None: ), "mom_conditional_negative_and_reliable": mom_alive, "orphan_plus_five_sigma": ( - "Prior report fip-breadth-20260718-211440-breadth.json listed " - "fip IC +0.0575 / t +5.12. This single-sourced recompute is the " - "authoritative number; if it disagrees, the +0.0575 row is orphaned." + "Orphaned 21:14 row (+0.0575 / t +5.12) raced a partial " + "research.sqlite and was removed from reports/ (Git history only). " + "Harness path and shared filter agree on complete data." ), "compositional_story": ( "fip_id pools continuous winners (neg IC) vs continuous bleeders " @@ -513,19 +533,36 @@ def main() -> None: "liquid is less negative / positive — composition, not jumpiness premium." ), "vol_tilt_warning": ( - "High-vol names underperform on breadth relative to S&P-like books. " - "Re-validate production 80/20 high-vol tilt before any universe broaden." + "Authoritative liquid vol_6m IC ≈ −0.048 / t ≈ −1.36 — directional " + "hypothesis only, not significant. Do not cite the orphaned −0.16 / " + "t −6.1. Re-validate production 80/20 high-vol tilt before any " + "universe broaden; it is not a settled finding on this pool." + ), + "breadth_momentum_thesis": ( + "Residual mom on liquid-1500 is +0.029 / t +1.33 vs fingerprint " + "0.055 / t 1.98 on 505 names — more breadth did not strengthen the " + "momentum t-stat on this pool. Clean mom edge lives in the large-cap " + "universe already traded. A fip tilt presupposes a breadth mom book " + "worth tilting; that baseline must be proven first." ), }, "platform_verdict": ( - "Mom-conditional fip ALIVE as book-tilt candidate (needs book sim) — " - "not production wire-in. Unconditional fip not green." + "Mom-conditional fip ALIVE as book-tilt candidate only — requires a " + "pre-registered two-arm breadth book (baseline liquid-1500 mom vs +fip " + "tilt) before any gate talk. Unconditional fip not green. Production: none." if mom_alive else ( "fip CLOSED for production: mom-conditional does not clear iron rule " "on single-sourced path. Display card is the resting place." ) ), + "research_snapshot_manifest": { + "finished_at": manifest.get("finished_at"), + "ticker_count": manifest.get("ticker_count"), + "ohlcv_row_count": manifest.get("ohlcv_row_count"), + "rank_only_count": manifest.get("rank_only_count"), + "complete": manifest.get("complete"), + }, } stamp = datetime.now().strftime("%Y%m%d-%H%M%S") @@ -533,8 +570,9 @@ def main() -> None: out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(results, indent=2, default=str), encoding="utf-8") - # Update research log - _update_md(Path("docs/research/fip-breadth-ic.md"), results, out) + # Append a machine reconciliation stub next to the JSON only — never clobber + # the curated research log at docs/research/fip-breadth-ic.md. + _update_md(out.with_suffix(".md"), results, out) if not args.quiet: print("=== Harness fip_id (authoritative) ===") @@ -560,18 +598,10 @@ def _update_md(path: Path, results: dict, artifact: Path) -> None: "", "### Problem", "", - "Two implementations of the liquid-1500 fip IC disagreed on **sign**:", - "", - "- Harness report `fip-breadth-20260718-211440-breadth.json`: **+0.0575 / t +5.12**", - "- Dual-path diagnostics (since deleted): **−0.017 / t −1.9**", - "", - "A static read cannot decide which is right without single-sourcing the mask.", - "", - "### Resolution", + "Machine stub only — curated narrative lives in `docs/research/fip-breadth-ic.md`.", "", f"- **Single source:** {results.get('single_source')}", - f"- **avg_cross_section semantics:** {results.get('avg_cross_section_semantics')}", - f"- Harness `_signal_evaluation` vs shared-filter recompute agree: " + f"- Harness vs shared-filter agree: " f"**{interp.get('harness_and_shared_filter_agree')}**", "", "### Authoritative unconditional fip (liquid top-N, post-mask)", @@ -587,10 +617,6 @@ def _update_md(path: Path, results: dict, artifact: Path) -> None: f"| mask_binds_pct | {h.get('mask_binds_pct')} |", f"| reliable | {h.get('reliable')} |", "", - "The **+0.0575 / +5.12** row is **orphaned** if the authoritative recompute " - "disagrees; do not cite it. Iron-rule unconditional green still requires " - "negative sign and |IC| ≳ 0.03 on this row.", - "", "### Checks (single-sourced)", "", "| check | mean_ic | t | weeks | avg N | reliable |", @@ -626,21 +652,17 @@ def _update_md(path: Path, results: dict, artifact: Path) -> None: "", results.get("platform_verdict", ""), "", - "### Vol-tilt warning", + "### Vol-tilt / breadth-momentum notes", "", interp.get("vol_tilt_warning", ""), "", + interp.get("breadth_momentum_thesis", ""), + "", f"Artifact: `{artifact.as_posix()}`", "", ]) - existing = path.read_text(encoding="utf-8") if path.exists() else "" - marker = "## Reconciliation" - if marker in existing: - existing = existing.split(marker)[0].rstrip() + "\n" - # Also strip old dual-path diagnostics section if present after reconciliation - if "## Follow-up diagnostics" in existing and marker not in path.read_text(encoding="utf-8") if path.exists() else "": - pass - path.write_text(existing.rstrip() + "\n" + "\n".join(lines), encoding="utf-8") + # Always overwrite the machine stub (never the curated research log). + path.write_text("\n".join(lines).lstrip() + "\n", encoding="utf-8") if __name__ == "__main__": diff --git a/scripts/run_fip_breadth_research.py b/scripts/run_fip_breadth_research.py index cc9f6e9..9b0e57a 100644 --- a/scripts/run_fip_breadth_research.py +++ b/scripts/run_fip_breadth_research.py @@ -1,8 +1,9 @@ """Phase B: fip_id IC on liquid-breadth cross-section (local research only). 1. Fingerprint check on the unextended prod snapshot (must ≈ IC −0.045 / t −2.9). -2. Run signal_eval on research.sqlite with BACKTEST_LIQUID_BREADTH=1500 PIT mask. -3. Write a research report under docs/research/ and reports/. +2. Assert research.sqlite has a matching **completion manifest** (race guard). +3. Run signal_eval on research.sqlite with BACKTEST_LIQUID_BREADTH=1500 PIT mask. +4. Write a research report under docs/research/ and reports/. Does not modify production DB, gate, scanner, or schedule. @@ -209,7 +210,9 @@ async def _main() -> None: stamp = datetime.now().strftime("%Y%m%d-%H%M%S") out_json = Path(args.out) if args.out else Path("reports") / f"fip-breadth-{stamp}.json" out_json.parent.mkdir(parents=True, exist_ok=True) - out_md = Path("docs/research") / "fip-breadth-ic.md" + # Never clobber the curated research log (docs/research/fip-breadth-ic.md). + # Machine summary goes next to the JSON report only. + out_md = out_json.with_suffix(".md") payload: dict = { "generated_at": datetime.now().isoformat(), @@ -257,17 +260,30 @@ async def _main() -> None: # --- 2) Breadth --- if not args.skip_research: - if not research.exists(): - raise SystemExit( - f"Research snapshot missing: {research}\n" - "Build it with: python scripts/extend_snapshot_universe.py" - ) + # Refuse half-built research.sqlite (2026-07-18 21:14 race). + scripts_dir = Path(__file__).resolve().parent + if str(scripts_dir) not in sys.path: + sys.path.insert(0, str(scripts_dir)) + from research_snapshot_manifest import ( # type: ignore[import-not-found] + assert_research_snapshot_complete, + ) + + manifest = assert_research_snapshot_complete(research) + payload["research_snapshot_manifest"] = { + "finished_at": manifest.get("finished_at"), + "ticker_count": manifest.get("ticker_count"), + "ohlcv_row_count": manifest.get("ohlcv_row_count"), + "rank_only_count": manifest.get("rank_only_count"), + "complete": manifest.get("complete"), + } os.environ["BACKTEST_LIQUID_BREADTH"] = str(int(args.liquid_breadth)) os.environ["BACKTEST_LIQUID_MIN_PRICE"] = str(float(args.min_price)) if not args.quiet: print( f"Breadth run on {research} " - f"(top {args.liquid_breadth}, min_price={args.min_price})…" + f"(top {args.liquid_breadth}, min_price={args.min_price}; " + f"manifest ok tickers={manifest.get('ticker_count')} " + f"finished_at={manifest.get('finished_at')})…" ) br_report = await _run_signal_eval( research, workers=args.workers, quiet=args.quiet diff --git a/tests/unit/test_research_snapshot_manifest.py b/tests/unit/test_research_snapshot_manifest.py new file mode 100644 index 0000000..6205167 --- /dev/null +++ b/tests/unit/test_research_snapshot_manifest.py @@ -0,0 +1,133 @@ +"""Completion-manifest guard for research.sqlite breadth runs.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest +from sqlalchemy import create_engine, text + +ROOT = Path(__file__).resolve().parents[2] +SCRIPTS = ROOT / "scripts" +if str(SCRIPTS) not in sys.path: + sys.path.insert(0, str(SCRIPTS)) + +from research_snapshot_manifest import ( # noqa: E402 + assert_research_snapshot_complete, + clear_manifest, + load_manifest, + manifest_path_for, + write_completion_manifest, +) + + +def _tiny_research_db(path: Path, *, tickers: int = 3, bars_each: int = 5) -> None: + engine = create_engine(f"sqlite:///{path.resolve().as_posix()}", future=True) + with engine.begin() as conn: + conn.execute( + text( + "CREATE TABLE tickers (" + "id INTEGER PRIMARY KEY, symbol TEXT NOT NULL UNIQUE, " + "name TEXT, created_at TEXT)" + ) + ) + conn.execute( + text( + "CREATE TABLE ohlcv_records (" + "id INTEGER PRIMARY KEY, ticker_id INTEGER, date TEXT, " + "open REAL, high REAL, low REAL, close REAL, volume INTEGER, " + "created_at TEXT)" + ) + ) + conn.execute( + text( + "CREATE TABLE research_rank_only (" + "ticker_id INTEGER PRIMARY KEY, symbol TEXT NOT NULL UNIQUE)" + ) + ) + for i in range(tickers): + sym = f"T{i}" + conn.execute( + text( + "INSERT INTO tickers (id, symbol, name, created_at) " + "VALUES (:id, :sym, NULL, '2026-01-01')" + ), + {"id": i + 1, "sym": sym}, + ) + if i > 0: + conn.execute( + text( + "INSERT INTO research_rank_only (ticker_id, symbol) " + "VALUES (:id, :sym)" + ), + {"id": i + 1, "sym": sym}, + ) + for d in range(bars_each): + conn.execute( + text( + "INSERT INTO ohlcv_records " + "(ticker_id, date, open, high, low, close, volume, created_at) " + "VALUES (:tid, :date, 1,1,1,1,100, '2026-01-01')" + ), + {"tid": i + 1, "date": f"2026-01-{d+1:02d}"}, + ) + engine.dispose() + + +def test_write_and_assert_complete(tmp_path: Path) -> None: + snap = tmp_path / "research.sqlite" + _tiny_research_db(snap) + path = write_completion_manifest(snap, complete=True, sources={"t": "unit"}) + assert path == manifest_path_for(snap) + assert path.exists() + + m = assert_research_snapshot_complete(snap) + assert m["complete"] is True + assert m["ticker_count"] == 3 + assert m["ohlcv_row_count"] == 15 + assert m["rank_only_count"] == 2 + assert m["live_counts"]["ticker_count"] == 3 + + +def test_refuse_missing_manifest(tmp_path: Path) -> None: + snap = tmp_path / "research.sqlite" + _tiny_research_db(snap) + with pytest.raises(SystemExit, match="manifest missing"): + assert_research_snapshot_complete(snap) + + +def test_refuse_incomplete_flag(tmp_path: Path) -> None: + snap = tmp_path / "research.sqlite" + _tiny_research_db(snap) + write_completion_manifest(snap, complete=False, limit=50) + with pytest.raises(SystemExit, match="marked incomplete"): + assert_research_snapshot_complete(snap) + + +def test_refuse_count_mismatch(tmp_path: Path) -> None: + snap = tmp_path / "research.sqlite" + _tiny_research_db(snap) + write_completion_manifest(snap, complete=True) + # Tamper: change live DB after manifest written + engine = create_engine(f"sqlite:///{snap.resolve().as_posix()}", future=True) + with engine.begin() as conn: + conn.execute( + text( + "INSERT INTO tickers (id, symbol, name, created_at) " + "VALUES (99, 'EXTRA', NULL, '2026-01-01')" + ) + ) + engine.dispose() + with pytest.raises(SystemExit, match="does not match"): + assert_research_snapshot_complete(snap) + + +def test_clear_manifest(tmp_path: Path) -> None: + snap = tmp_path / "research.sqlite" + _tiny_research_db(snap) + write_completion_manifest(snap, complete=True) + assert load_manifest(snap) is not None + clear_manifest(snap) + assert load_manifest(snap) is None