Implement A5 fundamentals cutover activation
Deploy / lint (push) Successful in 9s
Deploy / test (push) Successful in 1m46s
Deploy / deploy (push) Successful in 36s

This commit is contained in:
2026-07-24 14:19:22 +02:00
parent 3df36a9bfb
commit b0537ebe9a
9 changed files with 1023 additions and 141 deletions
+15 -115
View File
@@ -14,22 +14,17 @@ import json
import math
import os
import statistics
from collections import defaultdict
from datetime import date, datetime, timezone
from pathlib import Path
from typing import Any, Iterable
from zoneinfo import ZoneInfo
from sqlalchemy import func, select, text
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.data_import_run import DataImportRun
from app.models.earnings_event import EarningsEvent
from app.models.fundamental import FundamentalData
from app.models.fundamental_snapshot import FundamentalSnapshot
from app.models.ohlcv import OHLCVRecord
from app.models.ticker import Ticker
from app.services import fundamentals_derivation as deriv
from app.services import fundamentals_candidate_service as candidate_service
REPORT_VERSION = 1
APPROVAL_STATUS = "pending_explicit_approval"
@@ -94,33 +89,18 @@ async def build_report(
)
await connection.execute(text("SET TRANSACTION READ ONLY"))
tickers = list((await db.execute(select(Ticker).order_by(Ticker.symbol))).scalars())
ticker_ids = [ticker.id for ticker in tickers]
ciks = sorted({ticker.cik for ticker in tickers if ticker.cik})
candidates = await candidate_service.build_candidates(db, today=today)
ticker_ids = [candidate.ticker_id for candidate in candidates]
legacy_by_ticker = await _legacy_values(db, ticker_ids)
derived_by_cik = await _derived_by_cik(db, ciks)
closes_by_ticker = await _latest_closes(db, ticker_ids)
surprise_by_ticker = await _latest_surprises(db, ticker_ids, today)
source_runs = await _source_runs(db)
rows: list[dict[str, Any]] = []
for ticker in tickers:
legacy = legacy_by_ticker.get(ticker.id)
derived = derived_by_cik.get(ticker.cik) if ticker.cik else None
close = closes_by_ticker.get(ticker.id)
candidate_pe = (
_pe(close[0], derived.ttm_diluted_eps)
if close is not None and derived is not None
else None
)
growth_series = (
derived.metrics.get("revenue_growth_yoy") if derived is not None else None
)
candidate = {
"pe_ratio": candidate_pe,
"revenue_growth": growth_series.value if growth_series else None,
"earnings_surprise": surprise_by_ticker.get(ticker.id),
for candidate in candidates:
legacy = legacy_by_ticker.get(candidate.ticker_id)
candidate_values = {
"pe_ratio": candidate.pe_ratio,
"revenue_growth": candidate.revenue_growth,
"earnings_surprise": candidate.earnings_surprise,
}
legacy_values = {
"pe_ratio": legacy.pe_ratio if legacy else None,
@@ -128,17 +108,17 @@ async def build_report(
"earnings_surprise": legacy.earnings_surprise if legacy else None,
}
fields = {
key: _field_comparison(key, legacy_values[key], candidate[key])
key: _field_comparison(key, legacy_values[key], candidate_values[key])
for key in FIELD_KEYS
}
legacy_score = fundamental_score(**legacy_values)
candidate_score = fundamental_score(**candidate)
candidate_score = fundamental_score(**candidate_values)
rows.append(
{
"symbol": ticker.symbol,
"cik": ticker.cik,
"symbol": candidate.symbol,
"cik": candidate.cik,
"legacy_fetched_at": _iso(legacy.fetched_at) if legacy else None,
"price_date": _iso(close[1]) if close else None,
"price_date": _iso(candidate.price_date),
"fields": fields,
"scores": {
"legacy_fundamental": _round(legacy_score),
@@ -311,74 +291,6 @@ async def _legacy_values(
return {row.ticker_id: row for row in rows}
async def _derived_by_cik(
db: AsyncSession, ciks: list[str]
) -> dict[str, deriv.DerivedFundamentals]:
if not ciks:
return {}
grouped: dict[str, list[FundamentalSnapshot]] = defaultdict(list)
rows = (
await db.execute(
select(FundamentalSnapshot).where(FundamentalSnapshot.cik.in_(ciks))
)
).scalars()
for row in rows:
grouped[row.cik].append(row)
return {cik: deriv.derive(grouped.get(cik, [])) for cik in ciks}
async def _latest_closes(
db: AsyncSession, ticker_ids: list[int]
) -> dict[int, tuple[float, date]]:
if not ticker_ids:
return {}
latest = (
select(OHLCVRecord.ticker_id, func.max(OHLCVRecord.date).label("max_date"))
.where(OHLCVRecord.ticker_id.in_(ticker_ids))
.group_by(OHLCVRecord.ticker_id)
.subquery()
)
rows = (
await db.execute(
select(OHLCVRecord.ticker_id, OHLCVRecord.close, OHLCVRecord.date).join(
latest,
(OHLCVRecord.ticker_id == latest.c.ticker_id)
& (OHLCVRecord.date == latest.c.max_date),
)
)
).all()
return {
ticker_id: (float(close), close_date)
for ticker_id, close, close_date in rows
if _finite(close)
}
async def _latest_surprises(
db: AsyncSession, ticker_ids: list[int], today: date
) -> dict[int, float]:
if not ticker_ids:
return {}
rows = (
await db.execute(
select(EarningsEvent)
.where(
EarningsEvent.ticker_id.in_(ticker_ids),
EarningsEvent.announce_date < today,
)
.order_by(EarningsEvent.ticker_id, EarningsEvent.announce_date.desc())
)
).scalars()
out: dict[int, float] = {}
for row in rows:
if row.ticker_id in out:
continue
surprise = _surprise(row.eps_estimate, row.eps_actual)
if surprise is not None:
out[row.ticker_id] = surprise
return out
async def _source_runs(db: AsyncSession) -> dict[str, dict[str, Any] | None]:
sources = ("sec_facts", "dolt_earnings")
rows = (
@@ -526,18 +438,6 @@ def _rank_change(legacy: int | None, candidate: int | None) -> int | None:
return legacy - candidate if legacy is not None and candidate is not None else None
def _surprise(estimate: float | None, actual: float | None) -> float | None:
if not _finite(estimate) or not _finite(actual) or estimate == 0:
return None
return (actual - estimate) / abs(estimate) * 100.0
def _pe(price: float | None, ttm_eps: float | None) -> float | None:
if not _finite(price) or price <= 0 or not _finite(ttm_eps) or ttm_eps <= 0:
return None
return price / ttm_eps
def _delta(legacy: float | None, candidate: float | None) -> float | None:
if not _finite(legacy) or not _finite(candidate):
return None