fix: deduplicate recommendations and widen position sizing range

- Add dedup check in recommendation worker: skip generation when latest
  rec for same ticker+window has identical action/mode/confidence
- Widen position sizing range (1-10% portfolio, 0.3-2% max loss) and
  factor in trend strength + evidence count for differentiated sizing
- API returns only latest recommendation per ticker by default (DISTINCT ON)
  to eliminate duplicate rows in the frontend list view
This commit is contained in:
Celes Renata
2026-04-17 00:15:32 +00:00
parent 29f46d387c
commit f11aa0a1ee
3 changed files with 134 additions and 35 deletions
+36 -12
View File
@@ -450,8 +450,14 @@ async def list_recommendations(
since: Optional[str] = None,
limit: int = Query(default=50, le=200),
offset: int = 0,
latest: bool = Query(default=True, description="Return only the latest recommendation per ticker"),
):
"""List recommendations with optional filters."""
"""List recommendations with optional filters.
By default (latest=true), returns only the most recent recommendation
per ticker to avoid showing duplicate/stale entries. Set latest=false
to see the full history.
"""
conditions: list[str] = []
params: list[Any] = []
idx = 1
@@ -475,17 +481,35 @@ async def list_recommendations(
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
rows = await pool.fetch(
f"""SELECT r.id, r.ticker, r.action, r.mode, r.confidence,
r.time_horizon, r.thesis, r.invalidation_conditions,
r.portfolio_pct, r.max_loss_pct, r.model_version,
r.risk_classification, r.generated_at
FROM recommendations r
{where}
ORDER BY r.generated_at DESC
LIMIT ${idx} OFFSET ${idx + 1}""",
*params, limit, offset,
)
if latest:
# Use DISTINCT ON to get only the latest recommendation per ticker
rows = await pool.fetch(
f"""SELECT DISTINCT ON (r.ticker)
r.id, r.ticker, r.action, r.mode, r.confidence,
r.time_horizon, r.thesis, r.invalidation_conditions,
r.portfolio_pct, r.max_loss_pct, r.model_version,
r.risk_classification, r.generated_at
FROM recommendations r
{where}
ORDER BY r.ticker, r.generated_at DESC""",
*params,
)
# Apply limit/offset manually after DISTINCT ON
# Sort by generated_at DESC for the final result
rows = sorted(rows, key=lambda r: r["generated_at"], reverse=True)
rows = rows[offset:offset + limit]
else:
rows = await pool.fetch(
f"""SELECT r.id, r.ticker, r.action, r.mode, r.confidence,
r.time_horizon, r.thesis, r.invalidation_conditions,
r.portfolio_pct, r.max_loss_pct, r.model_version,
r.risk_classification, r.generated_at
FROM recommendations r
{where}
ORDER BY r.generated_at DESC
LIMIT ${idx} OFFSET ${idx + 1}""",
*params, limit, offset,
)
results = []
for r in rows:
d = _row_to_dict(r)