feat(observability): fine-grained metrics for summary, trades, and cycle log
CI/CD / build-and-push (push) Successful in 1m51s
CI/CD / build-and-push (push) Successful in 1m51s
api/summary — new fields:
open_trades_count, closed_trades_count, cash_available (bankroll−deployed),
legacy_incomplete_count, reentry_guard_blocks_24h
parallel fetch via asyncio.gather for sub-ms overhead
api/trades?status=open — trade enrichment:
days_open (float, rounded to 1 decimal)
signal_components {fg, mom, news, mfld} parsed from reasoning via regex
Old trades without feat_str in reasoning return signal_components: null
bayesian.py — reasoning now embeds feat_str:
"fg=+0.0600 mom=+0.0000 news=+0.0000 mfld=-0.7483 |"
Manifold counters: _manifold_fetched / _manifold_on_trade per cycle
get_cycle_stats() exposes manifold_matches_accepted / manifold_matches_rejected
bot/main.py — CYCLE SUMMARY 4 new fields:
reentry_guard_blocked, legacy_incomplete_seen,
family_conflicts_prevented, manifold_matches_accepted/rejected
legacy_incomplete_count queried from DB once per cycle
db.py — get_legacy_incomplete_count(): open trades with NULL edge_net
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+48
-5
@@ -1,13 +1,44 @@
|
||||
"""
|
||||
FastAPI Backend — serves metrics and trade data to the React dashboard.
|
||||
"""
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
import re
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from bot.data.db import Database
|
||||
|
||||
# Matches the feat_str embedded in reasoning for trades from bayesian.py v2+:
|
||||
# "fg=+0.0600 mom=+0.0000 news=+0.0000 mfld=-0.7483"
|
||||
_FEAT_RE = re.compile(
|
||||
r"fg=([+-]?[\d.]+).*?mom=([+-]?[\d.]+).*?news=([+-]?[\d.]+).*?mfld=([+-]?[\d.]+)"
|
||||
)
|
||||
|
||||
|
||||
def _enrich_trade(trade: dict) -> dict:
|
||||
"""Add days_open and signal_components to an open trade dict."""
|
||||
ts = trade.get("timestamp")
|
||||
if ts is not None:
|
||||
now = datetime.now(timezone.utc)
|
||||
if getattr(ts, "tzinfo", None) is None:
|
||||
ts = ts.replace(tzinfo=timezone.utc)
|
||||
trade["days_open"] = round((now - ts).total_seconds() / 86400, 1)
|
||||
else:
|
||||
trade["days_open"] = None
|
||||
|
||||
reasoning = trade.get("reasoning") or ""
|
||||
m = _FEAT_RE.search(reasoning)
|
||||
trade["signal_components"] = (
|
||||
{"fg": float(m.group(1)), "mom": float(m.group(2)),
|
||||
"news": float(m.group(3)), "mfld": float(m.group(4))}
|
||||
if m else None
|
||||
)
|
||||
return trade
|
||||
|
||||
db = Database()
|
||||
|
||||
|
||||
@@ -45,12 +76,14 @@ async def get_metrics():
|
||||
async def get_trades(limit: int = 50, status: str = "open"):
|
||||
"""
|
||||
status: "open" (default) | "closed" | "all"
|
||||
Each trade includes a computed "status" field.
|
||||
Open trades include days_open and signal_components {fg, mom, news, mfld}.
|
||||
"""
|
||||
if status not in ("open", "closed", "all"):
|
||||
status = "open"
|
||||
filter_status = None if status == "all" else status
|
||||
trades = await db.get_recent_trades(limit=limit, status=filter_status)
|
||||
if filter_status == "open":
|
||||
trades = [_enrich_trade(t) for t in trades]
|
||||
return {"trades": trades, "count": len(trades), "status_filter": status}
|
||||
|
||||
|
||||
@@ -58,17 +91,27 @@ async def get_trades(limit: int = 50, status: str = "open"):
|
||||
async def get_summary():
|
||||
"""Dashboard summary card data."""
|
||||
history = await db.get_metrics_history(days=1)
|
||||
trades = await db.get_recent_trades(limit=500)
|
||||
open_trades, all_trades, inverted, legacy_count = await asyncio.gather(
|
||||
db.get_recent_trades(limit=500, status="open"),
|
||||
db.get_recent_trades(limit=500),
|
||||
db.get_recently_closed_inverted(hours=24),
|
||||
db.get_legacy_incomplete_count(),
|
||||
)
|
||||
|
||||
latest = history[0] if history else {}
|
||||
paper_bankroll = float(os.getenv("PAPER_BANKROLL", "10000"))
|
||||
total_deployed = sum(t.get("net_cost", 0) for t in trades)
|
||||
total_deployed = sum(t.get("net_cost", 0) for t in open_trades)
|
||||
|
||||
return {
|
||||
"paper_mode": os.getenv("PAPER_MODE", "true") == "true",
|
||||
"paper_bankroll": paper_bankroll,
|
||||
"total_trades": len(trades),
|
||||
"total_trades": len(all_trades),
|
||||
"open_trades_count": len(open_trades),
|
||||
"closed_trades_count": len(all_trades) - len(open_trades),
|
||||
"total_deployed": total_deployed,
|
||||
"cash_available": max(0.0, paper_bankroll - total_deployed),
|
||||
"legacy_incomplete_count": legacy_count,
|
||||
"reentry_guard_blocks_24h": len(inverted),
|
||||
"total_pnl": latest.get("total_pnl", 0),
|
||||
"win_rate": latest.get("win_rate", 0),
|
||||
"sharpe_ratio": latest.get("sharpe_ratio", 0),
|
||||
@@ -77,6 +120,6 @@ async def get_summary():
|
||||
latest.get("sharpe_ratio", 0) >= 0.5
|
||||
and latest.get("win_rate", 0) >= 0.52
|
||||
and latest.get("calibration_score", 0) >= 0.7
|
||||
and len(trades) >= 50
|
||||
and len(all_trades) >= 50
|
||||
),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user