Make live signal reads non-mutating
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
|
||||
Computes dimension scores (technical, sr_quality, sentiment, fundamental,
|
||||
momentum) each 0-100, composite score as weighted average of available
|
||||
dimensions with re-normalized weights, staleness marking/recomputation
|
||||
on demand, and weight update triggers full recomputation.
|
||||
dimensions with re-normalized weights, staleness marking, explicit refresh
|
||||
paths, and weight update triggers full recomputation.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -765,73 +765,37 @@ async def compute_composite_score(
|
||||
async def get_score(
|
||||
db: AsyncSession, symbol: str
|
||||
) -> dict:
|
||||
"""Get composite + all dimension scores for a ticker.
|
||||
"""Read composite + dimension scores for a ticker without recomputing.
|
||||
|
||||
Recomputes stale dimensions on demand, then recomputes composite.
|
||||
Returns a dict suitable for ScoreResponse, including dimension breakdowns
|
||||
and composite breakdown with re-normalization info.
|
||||
GET endpoints use this path, so it must not mutate persisted score context.
|
||||
Scheduled/manual write paths are responsible for refreshing scores.
|
||||
"""
|
||||
ticker = await _get_ticker(db, symbol)
|
||||
weights = await _get_weights(db)
|
||||
|
||||
# Check for stale dimension scores and recompute them
|
||||
result = await db.execute(
|
||||
select(DimensionScore).where(DimensionScore.ticker_id == ticker.id)
|
||||
)
|
||||
dim_scores = {ds.dimension: ds for ds in result.scalars().all()}
|
||||
|
||||
for dim in DIMENSIONS:
|
||||
ds = dim_scores.get(dim)
|
||||
if ds is None or ds.is_stale:
|
||||
await compute_dimension_score(db, symbol, dim)
|
||||
|
||||
# Check composite staleness
|
||||
comp_result = await db.execute(
|
||||
select(CompositeScore).where(CompositeScore.ticker_id == ticker.id)
|
||||
)
|
||||
comp = comp_result.scalar_one_or_none()
|
||||
|
||||
if comp is None or comp.is_stale:
|
||||
await compute_composite_score(db, symbol, weights)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# Re-fetch everything fresh
|
||||
result = await db.execute(
|
||||
select(DimensionScore).where(DimensionScore.ticker_id == ticker.id)
|
||||
)
|
||||
dim_scores_list = list(result.scalars().all())
|
||||
dim_scores = {ds.dimension: ds for ds in dim_scores_list}
|
||||
|
||||
comp_result = await db.execute(
|
||||
select(CompositeScore).where(CompositeScore.ticker_id == ticker.id)
|
||||
)
|
||||
comp = comp_result.scalar_one_or_none()
|
||||
|
||||
# Compute breakdowns for each dimension by calling the dimension computers
|
||||
breakdowns: dict[str, dict | None] = {}
|
||||
for dim in DIMENSIONS:
|
||||
try:
|
||||
raw_result = await _DIMENSION_COMPUTERS[dim](db, symbol)
|
||||
if isinstance(raw_result, tuple) and len(raw_result) == 2:
|
||||
breakdowns[dim] = raw_result[1]
|
||||
else:
|
||||
breakdowns[dim] = None
|
||||
except Exception:
|
||||
breakdowns[dim] = None
|
||||
|
||||
# Build dimension entries with breakdowns
|
||||
dimensions = []
|
||||
missing = []
|
||||
available_dims: list[str] = []
|
||||
for dim in DIMENSIONS:
|
||||
found = next((ds for ds in dim_scores_list if ds.dimension == dim), None)
|
||||
found = dim_scores.get(dim)
|
||||
if found is not None and not found.is_stale and found.score is not None:
|
||||
dimensions.append({
|
||||
"dimension": found.dimension,
|
||||
"score": found.score,
|
||||
"is_stale": found.is_stale,
|
||||
"computed_at": found.computed_at,
|
||||
"breakdown": breakdowns.get(dim),
|
||||
"breakdown": None,
|
||||
})
|
||||
w = weights.get(dim, 0.0)
|
||||
if w > 0:
|
||||
@@ -845,7 +809,7 @@ async def get_score(
|
||||
"score": found.score,
|
||||
"is_stale": found.is_stale,
|
||||
"computed_at": found.computed_at,
|
||||
"breakdown": breakdowns.get(dim),
|
||||
"breakdown": None,
|
||||
})
|
||||
|
||||
# Build composite breakdown: the non-sentiment base (re-normalized weighted
|
||||
@@ -925,31 +889,13 @@ async def get_rankings(db: AsyncSession) -> dict:
|
||||
dims[ds.ticker_id][ds.dimension] = ds
|
||||
return comps, dims
|
||||
|
||||
# Two bulk reads instead of ~4 queries per ticker.
|
||||
comps, dims_by_ticker = await _load_scores()
|
||||
|
||||
# Lazily recompute any stale/missing scores (kept fresh by the daily scan;
|
||||
# this self-heals tickers that aged out between scans), committing once.
|
||||
recomputed = False
|
||||
for ticker in tickers:
|
||||
comp = comps.get(ticker.id)
|
||||
if comp is None or comp.is_stale:
|
||||
dim_scores = dims_by_ticker.get(ticker.id, {})
|
||||
for dim in DIMENSIONS:
|
||||
ds = dim_scores.get(dim)
|
||||
if ds is None or ds.is_stale:
|
||||
await compute_dimension_score(db, ticker.symbol, dim)
|
||||
await compute_composite_score(db, ticker.symbol, weights)
|
||||
recomputed = True
|
||||
|
||||
if recomputed:
|
||||
await db.commit()
|
||||
comps, dims_by_ticker = await _load_scores()
|
||||
|
||||
rankings = [
|
||||
{
|
||||
"symbol": ticker.symbol,
|
||||
"composite_score": comp.score,
|
||||
"composite_stale": comp.is_stale,
|
||||
"dimensions": [
|
||||
{
|
||||
"dimension": ds.dimension,
|
||||
|
||||
Reference in New Issue
Block a user