Add multi-factor conviction gate to activation
Make "qualified" mean an edge candidate, not just R:R + confidence. The gate now also requires (all admin-configurable, defaults on): - high conviction: recommended_action LONG_HIGH / SHORT_HIGH only - clean read: risk_level Low (no contradicting signals) - probable primary target: best target probability >= min (default 60) - Shared predicate: app/services/qualification.py + frontend/src/lib/qualification.ts (mirrored) - Activation config extended (min_target_probability, require_high_conviction, exclude_conflicts) with bool-aware get/update + validation - /trades/performance switched to ?qualified_only=true, applying the full gate server-side; confidence breakdown stays unfiltered - Dashboard "Qualified", Signals "Qualified only" toggle, and Track Record all use the one gate; Admin gains the new controls Sentiment provider runtime config (prior change) included. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,14 +31,29 @@ RECOMMENDATION_CONFIG_DEFAULTS: dict[str, float] = {
|
||||
DEFAULT_TICKER_UNIVERSE = "sp500"
|
||||
SUPPORTED_TICKER_UNIVERSES = {"sp500", "nasdaq100", "nasdaq_all"}
|
||||
|
||||
# Activation thresholds: what counts as a signal worth acting on.
|
||||
# Used as Signals-page default filters, the Dashboard's qualified-setup
|
||||
# metrics, and the Track Record's "qualified only" view. The outcome
|
||||
# evaluator deliberately ignores these — every setup gets evaluated so the
|
||||
# thresholds themselves can be validated against outcomes.
|
||||
ACTIVATION_DEFAULTS: dict[str, float] = {
|
||||
"activation_min_rr": 2.0,
|
||||
"activation_min_confidence": 70.0,
|
||||
# Activation gate: what counts as a signal worth acting on. Used by the
|
||||
# Dashboard's "Qualified" metric, the Signals "Qualified only" view, and the
|
||||
# Track Record's qualified stats. The outcome evaluator deliberately ignores
|
||||
# these — every setup is evaluated so the gate itself can be validated.
|
||||
#
|
||||
# Beyond raw R:R and confidence, the gate demands conviction: a high-conviction
|
||||
# action (LONG_HIGH / SHORT_HIGH), a clean read (risk Low / no conflicts), and a
|
||||
# probable primary target.
|
||||
_ACTIVATION_FLOAT_KEYS: dict[str, str] = {
|
||||
"min_rr": "activation_min_rr",
|
||||
"min_confidence": "activation_min_confidence",
|
||||
"min_target_probability": "activation_min_target_probability",
|
||||
}
|
||||
_ACTIVATION_BOOL_KEYS: dict[str, str] = {
|
||||
"require_high_conviction": "activation_require_high_conviction",
|
||||
"exclude_conflicts": "activation_exclude_conflicts",
|
||||
}
|
||||
ACTIVATION_DEFAULTS: dict[str, float | bool] = {
|
||||
"min_rr": 2.0,
|
||||
"min_confidence": 70.0,
|
||||
"min_target_probability": 60.0,
|
||||
"require_high_conviction": True,
|
||||
"exclude_conflicts": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -157,40 +172,43 @@ async def update_setting(db: AsyncSession, key: str, value: str) -> SystemSettin
|
||||
# Activation thresholds
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
async def get_activation_config(db: AsyncSession) -> dict[str, float]:
|
||||
"""Return activation thresholds with public keys (min_rr, min_confidence)."""
|
||||
async def get_activation_config(db: AsyncSession) -> dict[str, float | bool]:
|
||||
"""Return the activation gate config with public keys."""
|
||||
result = await db.execute(
|
||||
select(SystemSetting).where(SystemSetting.key.like("activation_%"))
|
||||
)
|
||||
config = dict(ACTIVATION_DEFAULTS)
|
||||
for setting in result.scalars().all():
|
||||
if setting.key in config:
|
||||
stored = {s.key: s.value for s in result.scalars().all()}
|
||||
|
||||
config: dict[str, float | bool] = dict(ACTIVATION_DEFAULTS)
|
||||
for public_key, storage_key in _ACTIVATION_FLOAT_KEYS.items():
|
||||
if storage_key in stored:
|
||||
try:
|
||||
config[setting.key] = float(setting.value)
|
||||
config[public_key] = float(stored[storage_key])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return {
|
||||
"min_rr": config["activation_min_rr"],
|
||||
"min_confidence": config["activation_min_confidence"],
|
||||
}
|
||||
for public_key, storage_key in _ACTIVATION_BOOL_KEYS.items():
|
||||
if storage_key in stored:
|
||||
config[public_key] = str(stored[storage_key]).strip().lower() == "true"
|
||||
return config
|
||||
|
||||
|
||||
async def update_activation_config(
|
||||
db: AsyncSession, updates: dict[str, float]
|
||||
) -> dict[str, float]:
|
||||
"""Update activation thresholds. Accepts public keys min_rr / min_confidence."""
|
||||
db: AsyncSession, updates: dict[str, float | bool]
|
||||
) -> dict[str, float | bool]:
|
||||
"""Update the activation gate. Accepts public keys; only supplied keys change."""
|
||||
if "min_rr" in updates and updates["min_rr"] < 0:
|
||||
raise ValidationError("min_rr must be >= 0")
|
||||
if "min_confidence" in updates and not 0 <= updates["min_confidence"] <= 100:
|
||||
raise ValidationError("min_confidence must be between 0 and 100")
|
||||
if "min_target_probability" in updates and not 0 <= updates["min_target_probability"] <= 100:
|
||||
raise ValidationError("min_target_probability must be between 0 and 100")
|
||||
|
||||
key_map = {
|
||||
"min_rr": "activation_min_rr",
|
||||
"min_confidence": "activation_min_confidence",
|
||||
}
|
||||
for public_key, storage_key in key_map.items():
|
||||
if public_key in updates:
|
||||
for public_key, storage_key in _ACTIVATION_FLOAT_KEYS.items():
|
||||
if public_key in updates and updates[public_key] is not None:
|
||||
await update_setting(db, storage_key, str(float(updates[public_key])))
|
||||
for public_key, storage_key in _ACTIVATION_BOOL_KEYS.items():
|
||||
if public_key in updates and updates[public_key] is not None:
|
||||
await update_setting(db, storage_key, "true" if updates[public_key] else "false")
|
||||
|
||||
return await get_activation_config(db)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user