feat(observability): fine-grained metrics for summary, trades, and cycle log
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:
chemavx
2026-04-21 09:48:31 +00:00
parent e2fb697c0c
commit 46f8f4b79a
4 changed files with 83 additions and 6 deletions
+48 -5
View File
@@ -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
),
}