Add trade setup outcome tracking and performance stats
Closes the feedback loop on R:R scanner signals: - Nightly outcome_evaluator job replays unresolved setups against daily OHLCV bars: target_hit / stop_hit / ambiguous (same-bar, counted as loss) / expired after OUTCOME_EVALUATION_MAX_BARS (default 30) - Migration 004: evaluated_at + outcome_date on trade_setups - GET /trades/performance: hit rate, expectancy (avg R), total R with breakdowns by direction, recommended action, and confidence bucket - New Performance page (stat cards, breakdown tables, Evaluate Now, methodology disclosure) wired into sidebar and mobile nav - 17 new unit tests for evaluation logic and stats aggregation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,7 @@ from app.providers.fundamentals_chain import build_fundamental_provider_chain
|
||||
from app.providers.openai_sentiment import OpenAISentimentProvider
|
||||
from app.providers.protocol import SentimentData
|
||||
from app.services import fundamental_service, ingestion_service, sentiment_service
|
||||
from app.services.outcome_service import evaluate_pending_setups
|
||||
from app.services.rr_scanner_service import scan_all_tickers
|
||||
from app.services.ticker_universe_service import bootstrap_universe
|
||||
|
||||
@@ -676,6 +677,52 @@ async def scan_rr() -> None:
|
||||
_runtime_finish(job_name, "error", processed=processed, total=total, message=str(exc))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job: Outcome Evaluator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def evaluate_outcomes() -> None:
|
||||
"""Evaluate unresolved trade setups against OHLCV data collected since.
|
||||
|
||||
Writes actual_outcome / outcome_date / evaluated_at on each decided setup.
|
||||
Undecided setups stay pending and are re-checked on the next run.
|
||||
"""
|
||||
job_name = "outcome_evaluator"
|
||||
logger.info(json.dumps({"event": "job_start", "job": job_name}))
|
||||
_runtime_start(job_name, total=1)
|
||||
|
||||
try:
|
||||
async with async_session_factory() as db:
|
||||
if not await _is_job_enabled(db, job_name):
|
||||
logger.info(json.dumps({"event": "job_skipped", "job": job_name, "reason": "disabled"}))
|
||||
_runtime_finish(job_name, "skipped", processed=0, total=1, message="Disabled")
|
||||
return
|
||||
|
||||
summary = await evaluate_pending_setups(
|
||||
db, max_bars=settings.outcome_evaluation_max_bars
|
||||
)
|
||||
|
||||
_runtime_progress(job_name, processed=1, total=1)
|
||||
_runtime_finish(
|
||||
job_name, "completed", processed=1, total=1,
|
||||
message=f"Evaluated {summary['evaluated']}, pending {summary['still_pending']}",
|
||||
)
|
||||
logger.info(json.dumps({
|
||||
"event": "job_complete",
|
||||
"job": job_name,
|
||||
"summary": summary,
|
||||
}))
|
||||
except Exception as exc:
|
||||
_runtime_finish(job_name, "error", processed=0, total=1, message=str(exc))
|
||||
logger.error(json.dumps({
|
||||
"event": "job_error",
|
||||
"job": job_name,
|
||||
"error_type": type(exc).__name__,
|
||||
"message": str(exc),
|
||||
}))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job: Ticker Universe Sync
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -804,6 +851,16 @@ def configure_scheduler() -> None:
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
# Outcome Evaluator — nightly, after fresh OHLCV has been collected
|
||||
scheduler.add_job(
|
||||
evaluate_outcomes,
|
||||
"interval",
|
||||
hours=24,
|
||||
id="outcome_evaluator",
|
||||
name="Outcome Evaluator",
|
||||
replace_existing=True,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
json.dumps({
|
||||
"event": "scheduler_configured",
|
||||
|
||||
Reference in New Issue
Block a user