research: clean up closed Tier-1 scaffolding from branch

Drop intermediate history-depth reports, sector-residual runners/map/code hooks
(evidence stays in final reports + docs), and slim MacBook helper to ssl/earnings/
prod-book-matrix only. SSL bootstrap and archived research conclusions retained.
This commit is contained in:
2026-07-19 14:41:52 +02:00
parent 1c38a94dd0
commit bb8aa655a1
22 changed files with 88 additions and 5450 deletions
+28 -235
View File
@@ -791,86 +791,34 @@ def _residual_momentum_12_1(
with an intercept estimated over the same window, the arithmetic residuals
sum to ~zero by construction, which would destroy the signal.
"""
return _multi_factor_residual_momentum_12_1(
dates, closes, i, [benchmark_closes] if benchmark_closes else None
)
def _multi_factor_residual_momentum_12_1(
dates: list[date],
closes: list[float],
i: int,
factor_closes: list[dict[date, float]] | None,
) -> float | None:
"""12-1 residual momentum vs one or more factors (OLS, no intercept).
Same formation window as raw / single-factor residual momentum:
daily returns from close[i-252] → close[i-21], require ≥100 paired obs.
Factors are stacked as columns; betas are OLS without intercept so the
cumulative residual is not forced to zero.
"""
if not factor_closes or i - 252 < 0:
return None
n_factors = len(factor_closes)
if n_factors < 1:
if not benchmark_closes or i - 252 < 0:
return None
stock_rets: list[float] = []
factor_rets: list[list[float]] = [[] for _ in range(n_factors)]
market_rets: list[float] = []
# Same daily intervals as mom_12_1: close[i-252] -> close[i-21].
for k in range(i - 251, i - 20):
prev_close = closes[k - 1]
if prev_close <= 0:
continue
f_day: list[float] = []
ok = True
for fc in factor_closes:
f_prev = fc.get(dates[k - 1])
f_cur = fc.get(dates[k])
if f_prev is None or f_cur is None or f_prev <= 0:
ok = False
break
f_day.append(f_cur / f_prev - 1.0)
if not ok:
bench_prev = benchmark_closes.get(dates[k - 1])
bench_cur = benchmark_closes.get(dates[k])
if prev_close <= 0 or bench_prev is None or bench_cur is None or bench_prev <= 0:
continue
stock_rets.append(closes[k] / prev_close - 1.0)
for j, r in enumerate(f_day):
factor_rets[j].append(r)
market_rets.append(bench_cur / bench_prev - 1.0)
n = len(stock_rets)
if n < 100:
if len(stock_rets) < 100:
return None
if n_factors == 1:
# Fast path: identical algebra to the historical single-factor form.
market_rets = factor_rets[0]
mean_market = sum(market_rets) / n
mean_stock = sum(stock_rets) / n
var_market = sum((x - mean_market) ** 2 for x in market_rets)
if var_market <= 0:
return None
cov = sum(
(stock_rets[k] - mean_stock) * (market_rets[k] - mean_market)
for k in range(n)
)
beta = cov / var_market
return sum(stock_rets[k] - beta * market_rets[k] for k in range(n))
# OLS without intercept: β = (X'X)^{-1} X'y for X columns = factor returns.
# Implemented for exactly two factors (market + sector); refuse larger.
if n_factors != 2:
mean_market = sum(market_rets) / len(market_rets)
mean_stock = sum(stock_rets) / len(stock_rets)
var_market = sum((x - mean_market) ** 2 for x in market_rets)
if var_market <= 0:
return None
f1, f2 = factor_rets[0], factor_rets[1]
s11 = sum(a * a for a in f1)
s22 = sum(a * a for a in f2)
s12 = sum(f1[k] * f2[k] for k in range(n))
sy1 = sum(stock_rets[k] * f1[k] for k in range(n))
sy2 = sum(stock_rets[k] * f2[k] for k in range(n))
det = s11 * s22 - s12 * s12
if abs(det) < 1e-18:
return None
b1 = (s22 * sy1 - s12 * sy2) / det
b2 = (s11 * sy2 - s12 * sy1) / det
return sum(stock_rets[k] - b1 * f1[k] - b2 * f2[k] for k in range(n))
cov = sum(
(stock_rets[k] - mean_stock) * (market_rets[k] - mean_market)
for k in range(len(stock_rets))
)
beta = cov / var_market
return sum(stock_rets[k] - beta * market_rets[k] for k in range(len(stock_rets)))
def _realized_vol_6m(closes: list[float], i: int) -> float | None:
@@ -895,7 +843,6 @@ def _signal_values(
highs: list[float],
i: int,
benchmark_closes: dict[date, float] | None = None,
sector_etf_closes: dict[date, float] | None = None,
) -> dict[str, float]:
"""Point-in-time candidate signals at as-of index ``i`` (price-only).
@@ -907,11 +854,6 @@ def _signal_values(
higher = nearer the high, expect positive IC). ``vol_6m`` is 126-day realized
volatility (expect negative IC if the low-volatility anomaly holds).
``fip_id`` is Da/Gurun/Warachka information discreteness (expect negative IC).
When ``sector_etf_closes`` is supplied (research path), also emit
``mom_12_1_sector_resid``: two-factor residual vs SPY + sector ETF.
Cross-sectional ``mom_12_1_sector_demeaned`` is injected later from the
full weekly cross-section (cannot be computed per-ticker alone).
"""
out: dict[str, float] = {}
if i - 252 >= 0 and closes[i - 252] > 0:
@@ -919,12 +861,6 @@ def _signal_values(
residual = _residual_momentum_12_1(dates, closes, i, benchmark_closes)
if residual is not None:
out["mom_12_1_resid"] = residual
if benchmark_closes and sector_etf_closes:
sector_resid = _multi_factor_residual_momentum_12_1(
dates, closes, i, [benchmark_closes, sector_etf_closes]
)
if sector_resid is not None:
out["mom_12_1_sector_resid"] = sector_resid
fip = _fip_id(closes, i)
if fip is not None:
out["fip_id"] = fip
@@ -1011,16 +947,14 @@ def _accumulate_signal_series(
benchmark_closes: dict[date, float] | None = None,
*,
symbol: str | None = None,
sector_etf_closes: dict[date, float] | 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
HORIZON trading days. Mutates ``collected`` (a dict of dict of list).
When ``BACKTEST_LIQUID_BREADTH`` is set, observations are dicts with PIT
liquidity fields for the mask. When ``symbol`` is provided, observations are
also dicts (so sector demeaning can group by name); otherwise plain
``(val, fwd)`` tuples keep the production path unchanged.
liquidity fields for the mask; otherwise plain ``(val, fwd)`` tuples so the
production signal path stays unchanged.
"""
n = len(records)
if n < HORIZON + 21:
@@ -1030,7 +964,6 @@ def _accumulate_signal_series(
volumes = [float(getattr(r, "volume", 0) or 0) for r in records]
dates = [r.date for r in records]
liquid_mode = _liquid_breadth_top_n() > 0
rich = liquid_mode or symbol is not None
for i in _weekly_asof_indices(records):
j = i + HORIZON
if j >= n or closes[i] <= 0:
@@ -1039,79 +972,19 @@ def _accumulate_signal_series(
iso = records[i].date.isocalendar()
week_key = (iso[0], iso[1])
dvol = _median_dollar_vol_63(closes, volumes, i) if liquid_mode else None
for name, val in _signal_values(
dates, closes, highs, i, benchmark_closes, sector_etf_closes
).items():
if rich:
row = {
for name, val in _signal_values(dates, closes, highs, i, benchmark_closes).items():
if liquid_mode:
collected[name][week_key].append({
"val": val,
"fwd": fwd,
"close": closes[i],
"median_dvol_63": dvol,
"symbol": symbol,
}
if liquid_mode:
row["close"] = closes[i]
row["median_dvol_63"] = dvol
collected[name][week_key].append(row)
})
else:
collected[name][week_key].append((val, fwd))
def _inject_sector_demeaned_momentum(
collected: dict,
symbol_to_sector: dict[str, str],
*,
min_sector_names: int = 2,
) -> None:
"""Cross-sectional demean of ``mom_12_1`` within GICS sector per week.
``mom_12_1_sector_demeaned[i] = mom_12_1[i] mean(mom_12_1 | sector_i)``.
Requires rich observations with a ``symbol`` field (research path). Names
without a sector label, or sectors with fewer than ``min_sector_names``
members that week, are dropped from the demeaned series.
"""
if not symbol_to_sector or "mom_12_1" not in collected:
return
from app.services.sector_map import normalise_symbol
demeaned: dict = defaultdict(list)
for week_key, recs in collected["mom_12_1"].items():
parsed: list[tuple[str, float, float, object]] = []
by_sector: dict[str, list[float]] = defaultdict(list)
for rec in recs:
pair = _obs_val_fwd(rec)
if pair is None:
continue
val, fwd = pair
if isinstance(rec, dict):
sym = rec.get("symbol")
else:
sym = None
if not sym:
continue
sector = symbol_to_sector.get(normalise_symbol(str(sym)))
if not sector:
continue
parsed.append((sector, val, fwd, rec))
by_sector[sector].append(val)
means = {
sec: sum(vs) / len(vs)
for sec, vs in by_sector.items()
if len(vs) >= min_sector_names
}
for sector, val, fwd, rec in parsed:
if sector not in means:
continue
dval = val - means[sector]
if isinstance(rec, dict):
row = dict(rec)
row["val"] = dval
demeaned[week_key].append(row)
else:
demeaned[week_key].append((dval, fwd))
if demeaned:
collected["mom_12_1_sector_demeaned"] = demeaned
def _rank(xs: list[float]) -> list[float]:
"""Average (tie-corrected) ranks, 1-based."""
order = sorted(range(len(xs)), key=lambda k: xs[k])
@@ -1388,38 +1261,14 @@ def _signal_series(
benchmark_closes: dict[date, float] | None = None,
*,
symbol: str | None = None,
sector_etf_closes: dict[date, float] | 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,
symbol=symbol,
sector_etf_closes=sector_etf_closes,
)
_accumulate_signal_series(records, tmp, benchmark_closes, symbol=symbol)
return {name: dict(weeks) for name, weeks in tmp.items()}
def _sector_etf_closes_for_symbol(
symbol: str,
symbol_to_sector: dict[str, str] | None,
sector_etf_closes: dict[str, dict[date, float]] | None,
) -> dict[date, float] | None:
"""Resolve the sector-ETF close series for one ticker, or None."""
if not symbol_to_sector or not sector_etf_closes:
return None
from app.services.sector_map import etf_for_symbol
etf = etf_for_symbol(symbol, symbol_to_sector)
if not etf:
return None
series = sector_etf_closes.get(etf)
return series or None
def _replay_and_signals(
symbol: str,
columns: tuple,
@@ -1429,8 +1278,6 @@ def _replay_and_signals(
target_model: str = PRODUCTION_GTL_TARGET_MODEL,
cadence: str = DEFAULT_BACKTEST_CADENCE,
signal_only: bool = False,
sector_etf_closes: dict[str, dict[date, float]] | None = None,
symbol_to_sector: dict[str, str] | None = None,
) -> tuple[list[dict], dict]:
"""The CPU-bound per-ticker work, as a top-level (picklable) function so it can
run in a worker process. Takes primitive column arrays (cheap to pickle),
@@ -1457,17 +1304,9 @@ def _replay_and_signals(
target_model,
cadence,
)
etf_closes = _sector_etf_closes_for_symbol(
symbol, symbol_to_sector, sector_etf_closes
)
return (
candidates,
_signal_series(
bars,
benchmark_closes,
symbol=symbol,
sector_etf_closes=etf_closes,
),
_signal_series(bars, benchmark_closes, symbol=symbol),
)
@@ -4218,41 +4057,6 @@ async def run_backtest(
except Exception:
logger.exception("Benchmark load for residual momentum failed")
# Optional sector residualisation (research): local ticker→sector map + sector
# ETF closes stored in benchmark_prices. Absent map/series → no sector signals.
symbol_to_sector: dict[str, str] = {}
sector_etf_closes: dict[str, dict[date, float]] = {}
try:
from app.services.sector_map import (
SECTOR_ETFS,
load_ticker_sector_map,
normalise_symbol,
)
from app.services.benchmark_service import load_benchmark_closes
map_path = os.getenv("BACKTEST_SECTOR_MAP_PATH", "").strip() or None
symbol_to_sector = {
normalise_symbol(k): v
for k, v in load_ticker_sector_map(map_path).items()
}
if symbol_to_sector:
for etf in SECTOR_ETFS:
try:
series = await load_benchmark_closes(db, etf)
except Exception:
series = {}
if series:
sector_etf_closes[etf] = series
logger.info(json.dumps({
"event": "backtest_sector_context_loaded",
"sector_map_size": len(symbol_to_sector),
"sector_etfs_loaded": sorted(sector_etf_closes),
}))
except Exception:
logger.exception("Sector residual context load failed; continuing without")
symbol_to_sector = {}
sector_etf_closes = {}
def _merge(result: tuple[list[dict], dict]) -> None:
cands, series = result
candidates.extend(cands)
@@ -4303,8 +4107,6 @@ async def run_backtest(
target_model,
cadence,
ticker.symbol in rank_only_symbols,
sector_etf_closes or None,
symbol_to_sector or None,
))
for result in await asyncio.gather(*futures, return_exceptions=True):
if isinstance(result, Exception):
@@ -4333,8 +4135,6 @@ async def run_backtest(
target_model,
cadence,
ticker.symbol in rank_only_symbols,
sector_etf_closes or None,
symbol_to_sector or None,
))
except Exception:
logger.exception("Backtest replay failed for %s", ticker.symbol)
@@ -4342,13 +4142,6 @@ async def run_backtest(
if progress_cb is not None and total:
progress_cb(total, total, "")
# Cross-sectional sector demean needs the full weekly universe.
if symbol_to_sector:
try:
_inject_sector_demeaned_momentum(collected, symbol_to_sector)
except Exception:
logger.exception("Sector demeaned momentum injection failed")
# Cross-sectional momentum: rank every week's universe, then "qualified" means
# floors + top ``min_momentum_percentile`` by promoted residual 12-1 momentum
# (raw 12-1 fallback only when benchmark data is unavailable).