Files
momentum devandClaude Fable 5 f6999c7f6a dilution(016): unbreak nightly EDGAR refresh + pre-register filing-veto experiment
premarket_eval's shadow enrichment call site queried session_date = current_date
(wrong on UTC-server Postgres at night, so it never matched any symbols) and
shared one try-block for float_cache + dilution filings (a float_cache failure
silently skipped the dilution refresh too). Fix: compute the ET session date
in Python, split into two independent try-blocks, lift into a testable
_refresh_shadow_enrichment(con, session_date). float_cache now applies its own
DDL unconditionally on use instead of only when an API key is present.

Adds iv_dilution_gate (additive, mirrors iv_regime_gate's keep-or-zero shape
keyed on symbol instead of session_date, fail-open on unfetched symbols) plus
its once-per-run preload, and registers the pre-registered dilution_veto_v1
experiment via the existing experiments/*.json self-registering cohort. Live
veto stays OFF; this only wires the shadow measurement.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 09:54:49 -05:00

110 lines
4.2 KiB
Python

"""Float cache: FMP shares-float -> float_cache table (SHADOW DATA ONLY).
Float is the #1 selection number the source trader reads that the system has
been blind to. The 2026-06-29 audit REFUTED float as a live gate (low float is
anti-predictive after RVOL) — so this feeds shadow analysis, board display, and
future runner-grade features, and must never gate entries without clearing a
pre-registered out-of-sample bar first.
Free-plan budget: ~250 calls/day. We fetch only symbols with no row or a row
older than REFRESH_DAYS, capped at MAX_CALLS_PER_RUN per invocation (the
nightly premarket_eval calls this for the day's tracked names).
"""
from __future__ import annotations
import json
import os
import urllib.parse
import urllib.request
from datetime import datetime, timezone
FMP_URL = "https://financialmodelingprep.com/stable/shares-float?symbol={sym}&apikey={key}"
REFRESH_DAYS = 7
MAX_CALLS_PER_RUN = 200
_SCHEMA = """
CREATE TABLE IF NOT EXISTS float_cache (
symbol TEXT PRIMARY KEY,
float_shares BIGINT,
outstanding_shares BIGINT,
free_float_pct DOUBLE PRECISION,
source_date TEXT,
fetched_at TIMESTAMP NOT NULL
)
"""
def ensure_schema(con) -> None:
con.execute(_SCHEMA)
def fetch_float(symbol: str, api_key: str, timeout: float = 10.0) -> dict | None:
url = FMP_URL.format(sym=urllib.parse.quote(symbol), key=api_key)
req = urllib.request.Request(url, headers={"User-Agent": "momentum-research"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
rows = json.loads(resp.read().decode())
except Exception: # noqa: BLE001 — enrichment must never break callers
return None
if not isinstance(rows, list) or not rows:
return None
r = rows[0]
if not r.get("floatShares"):
return None
return {
"symbol": symbol.upper(),
"float_shares": int(r["floatShares"]),
"outstanding_shares": int(r.get("outstandingShares") or 0) or None,
"free_float_pct": float(r.get("freeFloat") or 0.0) or None,
"source_date": str(r.get("date") or ""),
}
def refresh_float_cache(con, symbols: list[str], api_key: str | None = None) -> dict:
"""Fetch float for symbols missing/stale in the cache. Returns a summary."""
ensure_schema(con) # apply-on-use: table must exist even on a no-key/no-op call
api_key = api_key or os.environ.get("FMP_API_KEY") or os.environ.get(
"Financial_Modeling_Prep_Api_Key")
out = {"fetched": 0, "skipped_fresh": 0, "failed": 0, "no_key": False}
if not api_key:
out["no_key"] = True
return out
now = datetime.now(timezone.utc).replace(tzinfo=None)
calls = 0
for sym in dict.fromkeys(s.upper() for s in symbols if s):
if calls >= MAX_CALLS_PER_RUN:
break
row = con.execute(
"SELECT fetched_at FROM float_cache WHERE symbol=?", [sym]).fetchall()
if row and (now - row[0][0]).days < REFRESH_DAYS:
out["skipped_fresh"] += 1
continue
calls += 1
data = fetch_float(sym, api_key)
if not data:
out["failed"] += 1
continue
con.execute(
"INSERT INTO float_cache (symbol, float_shares, outstanding_shares, "
"free_float_pct, source_date, fetched_at) VALUES (?,?,?,?,?,?) "
"ON CONFLICT (symbol) DO UPDATE SET float_shares=EXCLUDED.float_shares, "
"outstanding_shares=EXCLUDED.outstanding_shares, "
"free_float_pct=EXCLUDED.free_float_pct, source_date=EXCLUDED.source_date, "
"fetched_at=EXCLUDED.fetched_at",
[data["symbol"], data["float_shares"], data["outstanding_shares"],
data["free_float_pct"], data["source_date"], now])
out["fetched"] += 1
return out
def get_float(con, symbol: str) -> int | None:
"""Cached float shares for a symbol, or None."""
try:
ensure_schema(con) # apply-on-use: readable even if nothing has written yet
r = con.execute("SELECT float_shares FROM float_cache WHERE symbol=?",
[symbol.upper()]).fetchall()
return int(r[0][0]) if r and r[0][0] else None
except Exception: # noqa: BLE001
return None