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:
+94
-1
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user