feat: add daily budget, active window, trade snapshots, and price impact monitor

- New migrations for daily_budget, active_window, and bottrade snapshot
- Add trumpstruth scraper and price_impact_monitor service
- Expand bot_engine, hyperliquid, recovery, and tp_sl_monitor logic
- Update API/schemas/models for new features

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-04-25 16:04:49 +08:00
parent a2c68e2939
commit 4ffcb442fe
20 changed files with 1110 additions and 114 deletions
+94 -1
View File
@@ -10,6 +10,7 @@ from app.config import settings
from app.database import AsyncSessionLocal, engine
from app.models import Base
from app.scrapers.truth_social import poll_truth_social, backfill_history
from app.scrapers.trumpstruth import poll_trumpstruth
from app.services.binance import run_binance_ws
from app.ws.manager import manager
@@ -64,9 +65,24 @@ async def lifespan(app: FastAPI):
max_instances=1,
coalesce=True,
)
# Fallback poller — trumpstruth.org RSS. Same Truth Social originalId =
# same external_id hash, so dedup is automatic. Whoever sees the post
# first wins. Offset by half the interval so the two pollers don't
# hammer the network at the same instant.
_scheduler.add_job(
poll_trumpstruth,
"interval",
seconds=settings.truth_social_poll_seconds,
args=[AsyncSessionLocal],
id="trumpstruth_poll",
max_instances=1,
coalesce=True,
next_run_time=None, # APScheduler will pick a fresh slot
)
_scheduler.start()
logger.info(
"Truth Social poller scheduled every %ds.", settings.truth_social_poll_seconds
"Truth Social pollers scheduled every %ds (CNN + trumpstruth.org).",
settings.truth_social_poll_seconds,
)
yield
@@ -117,8 +133,85 @@ app.include_router(user_router, prefix="/api")
@app.get("/api/health")
async def health():
"""Shallow liveness — used by load balancers / Docker healthcheck.
Returns 200 as long as the process is alive and the event loop responds."""
return {"status": "ok"}
@app.get("/api/health/deep")
async def health_deep():
"""Deep healthcheck — used by external uptime monitors (UptimeRobot, etc).
Returns 503 if any of:
- DB query fails
- Scraper hasn't completed a successful poll in > 90s
(poll runs every 15s; allow 6 misses before alarm)
Body always includes diagnostic fields so the alarm payload is actionable.
"""
from datetime import datetime, timezone
from fastapi import Response
from sqlalchemy import text
from app.scrapers import truth_social as ts_scraper
from app.scrapers import trumpstruth as ts_fallback
now = datetime.now(timezone.utc)
problems: list[str] = []
# 1. DB ping
db_ok = False
db_error: Optional[str] = None
try:
async with AsyncSessionLocal() as db:
await db.execute(text("SELECT 1"))
db_ok = True
except Exception as exc:
db_error = str(exc)
problems.append(f"db: {db_error}")
# 2. Scraper liveness — system is healthy if AT LEAST ONE source polled
# recently. Both stale = real problem (network down, or both upstreams dead).
sources = [
("cnn", ts_scraper.last_successful_poll_at, ts_scraper.last_poll_error),
("trumpstruth", ts_fallback.last_successful_poll_at, ts_fallback.last_poll_error),
]
source_status = []
freshest_age: Optional[int] = None
for name, last, err in sources:
age = int((now - last).total_seconds()) if last else None
source_status.append({
"name": name,
"last_poll": last.isoformat() if last else None,
"age_sec": age,
"last_error": err,
})
if age is not None and (freshest_age is None or age < freshest_age):
freshest_age = age
if freshest_age is None:
problems.append("scrapers: no source has ever completed a poll")
elif freshest_age > 90:
# 6× the 15s interval — both sources are silent
problems.append(f"scrapers: all stale (freshest={freshest_age}s)")
body = {
"status": "ok" if not problems else "degraded",
"now": now.isoformat(),
"db_ok": db_ok,
"db_error": db_error,
"scrapers": source_status,
"freshest_age_sec": freshest_age,
"problems": problems,
}
if problems:
return Response(
content=__import__("json").dumps(body),
status_code=503,
media_type="application/json",
)
return body
if settings.environment == "development":
from app.api.dev import router as dev_router
app.include_router(dev_router, prefix="/api")