Harden trading, training, and monitoring

This commit is contained in:
Codex
2026-07-10 15:51:53 +03:00
parent 6fb79ee2a9
commit 069d75d2f2
55 changed files with 2658 additions and 2332049 deletions
+125 -31
View File
@@ -1,13 +1,16 @@
from __future__ import annotations
import asyncio
import json
import logging
from contextlib import asynccontextmanager
from typing import Any
from fastapi import FastAPI, HTTPException, Response
from fastapi import Depends, FastAPI, HTTPException, Response
from fastapi.responses import JSONResponse, PlainTextResponse
from crypto_spot_bot.analytics import analytics_snapshot
from crypto_spot_bot.auth import ApiAuthorizer
from crypto_spot_bot.bot import CryptoSpotBot
from crypto_spot_bot.bybit import BybitClient
from crypto_spot_bot.config import Settings, load_settings, update_env_value
@@ -23,6 +26,7 @@ from crypto_spot_bot.training_coordination import TrainingCoordinator
WEB_UI_REMOVED_MESSAGE = "Web UI removed. Use the Android TradeBot AI app and /api/* endpoints."
logger = logging.getLogger(__name__)
def create_app(settings: Settings | None = None) -> FastAPI:
@@ -44,6 +48,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
forecaster = TimeSeriesForecaster(settings)
bot = CryptoSpotBot(settings, storage, market, broker, strategy, pattern_analyzer, learner, forecaster)
training = TrainingCoordinator(settings.time_series_lstm_model_path.parent)
authorizer = ApiAuthorizer(settings)
@asynccontextmanager
async def lifespan(_: FastAPI):
@@ -66,50 +71,62 @@ def create_app(settings: Settings | None = None) -> FastAPI:
@app.get("/api/health")
async def health() -> dict[str, Any]:
return {"ok": True, "running": bot.running, "mode": settings.trading_mode}
return {
"ok": True,
"running": bot.running,
"mode": settings.trading_mode,
"auth_configured": authorizer.configured(),
}
@app.get("/api/ready")
async def ready() -> JSONResponse:
payload = bot.readiness_snapshot()
return JSONResponse(payload, status_code=200 if payload["ready"] else 503)
@app.get("/api/status")
async def status() -> dict[str, Any]:
async def status(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return {
"status": bot.status().as_dict(),
"account": bot.account_snapshot(),
"positions": bot.positions_snapshot(),
"learning": bot.learning_snapshot(),
"latest_equity": storage.latest_equity(),
"latest_equity": storage.latest_equity(mode=settings.trading_mode),
"readiness": bot.readiness_snapshot(),
}
@app.get("/api/markets")
async def markets() -> dict[str, Any]:
async def markets(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return market.snapshot()
@app.get("/api/trades")
async def trades(limit: int = 80) -> dict[str, Any]:
async def trades(limit: int = 80, _: None = Depends(authorizer.require)) -> dict[str, Any]:
row_limit = _limit(limit)
return {
"items": storage.recent_trades(row_limit),
"closed_items": storage.closed_trades(row_limit),
"closed_summary": storage.closed_trade_summary(),
"items": storage.recent_trades(row_limit, mode=settings.trading_mode),
"closed_items": storage.closed_trades(row_limit, mode=settings.trading_mode),
"closed_summary": storage.closed_trade_summary(mode=settings.trading_mode),
}
@app.get("/api/signals")
async def signals(limit: int = 120) -> dict[str, Any]:
async def signals(limit: int = 120, _: None = Depends(authorizer.require)) -> dict[str, Any]:
return {"items": storage.recent_signals(_limit(limit))}
@app.get("/api/events")
async def events(limit: int = 120) -> dict[str, Any]:
async def events(limit: int = 120, _: None = Depends(authorizer.require)) -> dict[str, Any]:
return {"items": storage.recent_events(_limit(limit))}
@app.get("/api/analytics")
async def analytics() -> dict[str, Any]:
async def analytics(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return analytics_snapshot(settings, storage)
@app.get("/api/quality")
async def quality() -> dict[str, Any]:
async def quality(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return market.snapshot().get("quality", {})
@app.get("/api/reconciliation")
async def reconciliation() -> dict[str, Any]:
return reconciliation_snapshot(
async def reconciliation(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return await asyncio.to_thread(
reconciliation_snapshot,
settings=settings,
storage=storage,
client=client,
@@ -117,58 +134,113 @@ def create_app(settings: Settings | None = None) -> FastAPI:
)
@app.get("/api/backtest")
async def backtest() -> dict[str, Any]:
async def backtest(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return _runtime_json(settings, "torch_threshold_calibration.json")
@app.get("/api/retrain")
async def retrain() -> dict[str, Any]:
async def retrain(_: None = Depends(authorizer.require)) -> dict[str, Any]:
data = _runtime_json(settings, "torch_retrain_guard.json")
data["coordination"] = training.status()
return data
@app.get("/api/training/status")
async def training_status() -> dict[str, Any]:
async def training_status(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return training.status()
@app.post("/api/training/retrain")
async def training_retrain(payload: dict[str, Any] | None = None) -> dict[str, Any]:
async def training_retrain(
payload: dict[str, Any] | None = None,
_: None = Depends(authorizer.require),
) -> dict[str, Any]:
return training.request_retrain(payload)
@app.post("/api/training/heartbeat")
async def training_heartbeat(payload: dict[str, Any] | None = None) -> dict[str, Any]:
async def training_heartbeat(
payload: dict[str, Any] | None = None,
_: None = Depends(authorizer.require_training),
) -> dict[str, Any]:
return training.heartbeat(payload)
@app.post("/api/training/claim")
async def training_claim(payload: dict[str, Any] | None = None) -> dict[str, Any]:
async def training_claim(
payload: dict[str, Any] | None = None,
_: None = Depends(authorizer.require_training),
) -> dict[str, Any]:
return training.claim(payload)
@app.post("/api/training/jobs/{job_id}/artifacts/chunk")
async def training_artifact_chunk(job_id: str, payload: dict[str, Any]) -> dict[str, Any]:
async def training_artifact_chunk(
job_id: str,
payload: dict[str, Any],
_: None = Depends(authorizer.require_training),
) -> dict[str, Any]:
try:
return training.save_artifact_chunk(job_id, payload)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/training/jobs/{job_id}/progress")
async def training_progress(job_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
async def training_progress(
job_id: str,
payload: dict[str, Any] | None = None,
_: None = Depends(authorizer.require_training),
) -> dict[str, Any]:
try:
return training.progress(job_id, payload)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@app.post("/api/training/jobs/{job_id}/complete")
async def training_complete(job_id: str, payload: dict[str, Any] | None = None) -> dict[str, Any]:
async def training_complete(
job_id: str,
payload: dict[str, Any] | None = None,
_: None = Depends(authorizer.require_training),
) -> dict[str, Any]:
try:
return training.complete(job_id, payload)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/api/config")
async def config() -> dict[str, Any]:
async def config(_: None = Depends(authorizer.require)) -> dict[str, Any]:
return _safe_config(settings)
@app.get("/api/mobile/snapshot")
async def mobile_snapshot(_: None = Depends(authorizer.require)) -> dict[str, Any]:
row_limit = 220
retrain_data = _runtime_json(settings, "torch_retrain_guard.json")
retrain_data["coordination"] = training.status()
return {
"health": {
"ok": True,
"running": bot.running,
"mode": settings.trading_mode,
},
"status": {
"status": bot.status().as_dict(),
"account": bot.account_snapshot(),
"positions": bot.positions_snapshot(),
"learning": bot.learning_snapshot(),
"latest_equity": storage.latest_equity(mode=settings.trading_mode),
"readiness": bot.readiness_snapshot(),
},
"markets": market.snapshot(),
"signals": {"items": storage.recent_signals(row_limit)},
"config": _safe_config(settings),
"trades": {
"items": storage.recent_trades(10, mode=settings.trading_mode),
"closed_items": storage.closed_trades(10, mode=settings.trading_mode),
"closed_summary": storage.closed_trade_summary(mode=settings.trading_mode),
},
"retrain": retrain_data,
"backtest": _runtime_json(settings, "torch_threshold_calibration.json"),
}
@app.post("/api/config/fast-trading")
async def set_fast_trading(payload: dict[str, Any]) -> dict[str, Any]:
async def set_fast_trading(
payload: dict[str, Any],
_: None = Depends(authorizer.require),
) -> dict[str, Any]:
enabled = _enabled_from_payload(payload)
env_persisted = _apply_fast_trading(settings, storage, enabled)
response = _safe_config(settings)
@@ -176,12 +248,12 @@ def create_app(settings: Settings | None = None) -> FastAPI:
return response
@app.post("/api/control/start")
async def start() -> dict[str, Any]:
async def start(_: None = Depends(authorizer.require)) -> dict[str, Any]:
await bot.start()
return bot.status().as_dict()
@app.post("/api/control/stop")
async def stop() -> dict[str, Any]:
async def stop(_: None = Depends(authorizer.require)) -> dict[str, Any]:
await bot.stop()
return bot.status().as_dict()
@@ -207,13 +279,22 @@ def create_app(settings: Settings | None = None) -> FastAPI:
"# HELP tradebot_loop_interval_seconds Effective bot decision loop interval.",
"# TYPE tradebot_loop_interval_seconds gauge",
f"tradebot_loop_interval_seconds {settings.effective_loop_interval_seconds:.4f}",
"# HELP tradebot_ready Whether trading prerequisites are ready.",
"# TYPE tradebot_ready gauge",
f"tradebot_ready {1 if bot.readiness_snapshot()['ready'] else 0}",
"# HELP tradebot_rest_errors_total REST refresh errors observed by market data.",
"# TYPE tradebot_rest_errors_total counter",
f"tradebot_rest_errors_total {market.rest_error_count}",
]
return PlainTextResponse("\n".join(lines) + "\n")
@app.exception_handler(Exception)
async def error_handler(_, exc: Exception) -> JSONResponse:
storage.event(f"API error: {exc}", "ERROR")
return JSONResponse({"error": str(exc)}, status_code=500)
try:
storage.event(f"API error: {exc}", "ERROR")
except Exception:
logger.exception("Could not persist API error event")
return JSONResponse({"error": "internal server error"}, status_code=500)
return app
@@ -317,6 +398,11 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
"time_series_probe_min_probability_up": settings.time_series_probe_min_probability_up,
"time_series_probe_size_multiplier": settings.time_series_probe_size_multiplier,
"time_series_rebound_fallback_enabled": settings.time_series_rebound_fallback_enabled,
"time_series_require_quality_gate": settings.time_series_require_quality_gate,
"time_series_manual_quality_override": settings.time_series_manual_quality_override,
"time_series_require_fresh_model": settings.time_series_require_fresh_model,
"time_series_model_max_age_hours": settings.time_series_model_max_age_hours,
"market_ticker_max_age_seconds": settings.market_ticker_max_age_seconds,
"time_series_model_artifact": _time_series_model_artifact(settings),
"stop_loss_percent": settings.stop_loss_percent,
"stop_loss_exit_enabled": settings.stop_loss_exit_enabled,
@@ -331,6 +417,14 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
"slippage_rate": settings.slippage_rate,
"live_ready": settings.live_ready,
"live_order_max_usdt": settings.live_order_max_usdt,
"live_order_fill_timeout_seconds": settings.live_order_fill_timeout_seconds,
"live_reconciliation_interval_seconds": settings.live_reconciliation_interval_seconds,
"live_protective_stop_enabled": settings.live_protective_stop_enabled,
"api_auth_configured": bool(
settings.api_auth_token
or settings.training_worker_token
or settings.trusted_proxy_user_header
),
}