Add activation thresholds: qualified-signal defaults and views
Admin-configurable thresholds (min R:R, default 2.0; min confidence, default 70%) defining what counts as an actionable signal: - Admin Settings: new Activation Thresholds panel (GET/PUT /admin/settings/activation) - GET /trades/activation exposes values to all users with access - Signals/Setups: filters initialize from activation values - Track Record: "Qualified signals only" toggle (default on) via min_rr/min_confidence params on /trades/performance; the confidence breakdown always covers the full population so the thresholds can be validated against outcomes - Dashboard: "Qualified" metric and qualified-first Top Setups - Outcome evaluator unchanged: every setup is still evaluated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -178,12 +178,20 @@ def _confidence_bucket(score: float | None) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
async def get_performance_stats(db: AsyncSession) -> dict:
|
||||
async def get_performance_stats(
|
||||
db: AsyncSession,
|
||||
min_rr: float | None = None,
|
||||
min_confidence: float | None = None,
|
||||
) -> dict:
|
||||
"""Aggregate outcome statistics over all evaluated trade setups.
|
||||
|
||||
avg_r is the expectancy per trade in R-multiples (win = +rr_ratio,
|
||||
loss = -1R, expired = 0R). A positive avg_r means the signals have
|
||||
been profitable on a risk-adjusted basis.
|
||||
|
||||
min_rr / min_confidence filter the overall, direction and action
|
||||
breakdowns. The confidence breakdown deliberately stays unfiltered:
|
||||
it is the instrument for validating the thresholds themselves.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(TradeSetup).where(TradeSetup.actual_outcome.is_not(None))
|
||||
@@ -195,14 +203,26 @@ async def get_performance_stats(db: AsyncSession) -> dict:
|
||||
)
|
||||
pending_count = len(pending_result.scalars().all())
|
||||
|
||||
def qualifies(setup: TradeSetup) -> bool:
|
||||
if min_rr is not None and setup.rr_ratio < min_rr:
|
||||
return False
|
||||
if min_confidence is not None and (setup.confidence_score or 0.0) < min_confidence:
|
||||
return False
|
||||
return True
|
||||
|
||||
qualified = [s for s in evaluated if qualifies(s)]
|
||||
|
||||
by_direction: dict[str, list[TradeSetup]] = {}
|
||||
by_action: dict[str, list[TradeSetup]] = {}
|
||||
by_confidence: dict[str, list[TradeSetup]] = {}
|
||||
|
||||
for setup in evaluated:
|
||||
for setup in qualified:
|
||||
by_direction.setdefault(setup.direction, []).append(setup)
|
||||
action = setup.recommended_action or "NONE"
|
||||
by_action.setdefault(action, []).append(setup)
|
||||
|
||||
# Confidence buckets always cover the full evaluated population
|
||||
for setup in evaluated:
|
||||
bucket = _confidence_bucket(setup.confidence_score)
|
||||
if bucket is not None:
|
||||
by_confidence.setdefault(bucket, []).append(setup)
|
||||
@@ -210,7 +230,7 @@ async def get_performance_stats(db: AsyncSession) -> dict:
|
||||
bucket_order = [label for label, _, _ in _CONFIDENCE_BUCKETS]
|
||||
|
||||
return {
|
||||
"overall": _bucket_stats(evaluated),
|
||||
"overall": _bucket_stats(qualified),
|
||||
"pending": pending_count,
|
||||
"by_direction": {k: _bucket_stats(v) for k, v in sorted(by_direction.items())},
|
||||
"by_action": {k: _bucket_stats(v) for k, v in sorted(by_action.items())},
|
||||
|
||||
Reference in New Issue
Block a user