Spaces:
Runtime error
Runtime error
| """FastAPI application β REST endpoints and WebSocket for SENTINEL.""" | |
| from contextlib import asynccontextmanager | |
| from typing import Optional | |
| import asyncio | |
| from pydantic import BaseModel | |
| from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from .websocket import ConnectionManager | |
| from ..market.simulator import MarketSimulator, get_sandbox_presets, create_sandbox_agents | |
| from ..market.oracle import OracleConfig | |
| from ..market.latency_model import LatencyConfig, LatencyMode | |
| from ..market.market_data import fetch_stock, build_oracle_path, POPULAR_TICKERS | |
| from ..agents.market_maker import MarketMakerAgent | |
| from ..agents.hft_agent import HFTAgent | |
| from ..agents.institutional import InstitutionalAgent | |
| from ..agents.retail import RetailAgent | |
| from ..agents.informed import InformedAgent | |
| from ..agents.noise import NoiseAgent | |
| from ..agents.momentum import MomentumAgent | |
| from ..agents.mean_reversion import MeanReversionAgent | |
| from ..agents.spoofing import SpoofingAgent | |
| from ..agents.sentiment import SentimentAgent | |
| from ..agents.rl_agent import RLAgent | |
| from ..prediction.liquidity_shock import LiquidityShockPredictor | |
| from ..prediction.large_order import LargeOrderDetector | |
| from ..market.rl_policy import RLPolicyController | |
| from ..utils.logger import get_logger | |
| from ..utils.config import config | |
| try: | |
| from ..abides.simulation import AbidesSimulation | |
| from ..abides.agents.exchange import ExchangeAgent as AbidesExchangeAgent | |
| from ..abides.agents.market_maker import MarketMakerAgent as AbidesMarketMakerAgent | |
| from ..abides.agents.noise import NoiseAgent as AbidesNoiseAgent | |
| from ..abides.agents.informed import InformedAgent as AbidesInformedAgent | |
| ABIDES_AVAILABLE = True | |
| except Exception: | |
| AbidesSimulation = None | |
| AbidesExchangeAgent = None | |
| AbidesMarketMakerAgent = None | |
| AbidesNoiseAgent = None | |
| AbidesInformedAgent = None | |
| ABIDES_AVAILABLE = False | |
| logger = get_logger("api") | |
| # Global singletons | |
| simulator: Optional[MarketSimulator] = None | |
| abides_simulator: Optional["AbidesSimulation"] = None | |
| liquidity_predictor = LiquidityShockPredictor() | |
| large_order_detector = LargeOrderDetector() | |
| rl_policy = ( | |
| RLPolicyController( | |
| model_path=config.rl_model_path, | |
| policy_kind=config.rl_policy_kind, | |
| autoload=False, | |
| ) | |
| if config.rl_policy_enabled | |
| else None | |
| ) | |
| manager = ConnectionManager() | |
| # Simulation task handle | |
| _sim_task: Optional[asyncio.Task] = None | |
| _abides_task: Optional[asyncio.Task] = None | |
| async def lifespan(app: FastAPI): | |
| logger.info("SENTINEL API starting up") | |
| yield | |
| logger.info("SENTINEL API shutting down") | |
| app = FastAPI( | |
| title="SENTINEL API", | |
| description="Smart Early-warning Network for Trading, Institutional orders, and Liquidity Events", | |
| version="2.0.0", | |
| lifespan=lifespan, | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=config.allowed_origins, | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # ββ REST Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def health_check(): | |
| return { | |
| "status": "healthy", | |
| "simulation_active": simulator is not None and simulator.running, | |
| "connected_clients": manager.client_count, | |
| "mode": simulator.mode if simulator else config.simulation_mode, | |
| "rl_policy_ready": rl_policy.ready if rl_policy else False, | |
| "rl_policy_kind": rl_policy.loaded_policy_kind if rl_policy else None, | |
| } | |
| class ModeRequest(BaseModel): | |
| mode: str | |
| def _require_simulator() -> MarketSimulator: | |
| if simulator is None: | |
| raise HTTPException(status_code=409, detail="No active simulation") | |
| return simulator | |
| def _stop_abides() -> None: | |
| global abides_simulator, _abides_task | |
| if abides_simulator and abides_simulator.running: | |
| abides_simulator.running = False | |
| if _abides_task: | |
| _abides_task.cancel() | |
| _abides_task = None | |
| async def set_simulation_mode(request: ModeRequest): | |
| if request.mode not in ["SANDBOX", "LIVE_SHADOW"]: | |
| raise HTTPException(status_code=400, detail="Invalid mode") | |
| config.simulation_mode = request.mode | |
| if simulator: | |
| simulator.mode = request.mode | |
| return {"status": "mode_updated", "mode": request.mode} | |
| async def start_simulation(): | |
| global simulator, _sim_task | |
| if simulator and simulator.running: | |
| return {"status": "already_running", "step": simulator.step_count} | |
| _stop_abides() | |
| large_order_detector.reset() | |
| if rl_policy: | |
| rl_policy.reload() | |
| # Create full agent set | |
| agents = ( | |
| ([RLAgent("RL_MM", initial_capital=100000.0)] if rl_policy and rl_policy.ready else []) | |
| + [MarketMakerAgent(f"MM_{i}") for i in range(3)] | |
| + [HFTAgent(f"HFT_{i}") for i in range(2)] | |
| + [InstitutionalAgent(f"INST_{i}") for i in range(2)] | |
| + [RetailAgent(f"RET_{i}") for i in range(10)] | |
| + [InformedAgent(f"INF_{i}") for i in range(3)] | |
| + [NoiseAgent(f"NOISE_{i}") for i in range(10)] | |
| + [MomentumAgent(f"MOM_{i}") for i in range(2)] | |
| + [MeanReversionAgent(f"MR_{i}") for i in range(2)] | |
| + [SpoofingAgent(f"SPOOF_0")] | |
| + [SentimentAgent(f"SENT_{i}") for i in range(5)] | |
| ) | |
| simulator = MarketSimulator( | |
| agents, | |
| initial_price=config.initial_price, | |
| duration_seconds=config.simulation_duration, | |
| mode=config.simulation_mode, | |
| ) | |
| # Run simulation in background task | |
| _sim_task = asyncio.create_task(_run_simulation_loop()) | |
| return { | |
| "status": "started", | |
| "agents": len(agents), | |
| "initial_price": config.initial_price, | |
| "rl_policy_active": bool(rl_policy and rl_policy.ready), | |
| } | |
| async def stop_simulation(): | |
| global simulator, _sim_task | |
| if simulator: | |
| simulator.stop() | |
| if _sim_task: | |
| _sim_task.cancel() | |
| _sim_task = None | |
| _stop_abides() | |
| large_order_detector.reset() | |
| return {"status": "stopped"} | |
| async def get_liquidity_prediction(): | |
| active_simulator = _require_simulator() | |
| state = active_simulator.get_market_state() | |
| return liquidity_predictor.predict(state) | |
| async def get_large_order_detection(): | |
| active_simulator = _require_simulator() | |
| state = active_simulator.get_market_state() | |
| detection = large_order_detector.detect(state) | |
| return detection or {"pattern": None, "message": "No large orders detected"} | |
| async def get_agent_metrics(): | |
| active_simulator = _require_simulator() | |
| metrics = {} | |
| for agent in active_simulator.agents: | |
| metrics[agent.agent_id] = agent.get_metrics(active_simulator.current_price) | |
| return metrics | |
| async def get_market_snapshot(): | |
| active_simulator = _require_simulator() | |
| state = active_simulator.get_market_state() | |
| return { | |
| "price": state["current_price"], | |
| "mid_price": state["mid_price"], | |
| "spread": state["spread"], | |
| "best_bid": state["best_bid"], | |
| "best_ask": state["best_ask"], | |
| "depth": state["total_depth"], | |
| "order_book": { | |
| "bids": state["bid_levels"], | |
| "asks": state["ask_levels"], | |
| }, | |
| "volatility": state["volatility"], | |
| "step": state["step"], | |
| } | |
| # ββ Sandbox Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def list_sandbox_presets(): | |
| return get_sandbox_presets() | |
| async def get_sandbox_capabilities(): | |
| return {"abides": ABIDES_AVAILABLE} | |
| class SandboxCreateRequest(BaseModel): | |
| preset: str = "balanced" | |
| initial_price: float = 100.0 | |
| oracle_enabled: bool = False | |
| oracle_kappa: float = 0.05 | |
| oracle_sigma: float = 0.02 | |
| latency_mode: str = "deterministic" | |
| speed: float = 1.0 | |
| custom_agents: Optional[dict] = None | |
| class AbidesSandboxCreateRequest(BaseModel): | |
| initial_price: float = 100.0 | |
| oracle_enabled: bool = True | |
| oracle_kappa: float = 0.05 | |
| oracle_sigma: float = 0.02 | |
| latency_mode: str = "deterministic" | |
| speed: float = 1.0 | |
| market_makers: int = 1 | |
| noise_agents: int = 2 | |
| informed_agents: int = 1 | |
| async def create_sandbox(request: SandboxCreateRequest): | |
| global simulator, _sim_task | |
| if simulator and simulator.running: | |
| simulator.stop() | |
| if _sim_task: | |
| _sim_task.cancel() | |
| _stop_abides() | |
| large_order_detector.reset() | |
| if rl_policy: | |
| rl_policy.reload() | |
| agents = create_sandbox_agents(request.preset, request.custom_agents) | |
| if rl_policy and rl_policy.ready: | |
| agents.append(RLAgent("RL_MM", initial_capital=100000.0)) | |
| oracle_cfg = OracleConfig( | |
| r_bar=request.initial_price, kappa=request.oracle_kappa, | |
| sigma_s=request.oracle_sigma, enabled=request.oracle_enabled, | |
| ) | |
| mode_map = {"zero": LatencyMode.ZERO, "deterministic": LatencyMode.DETERMINISTIC, "cubic": LatencyMode.CUBIC} | |
| latency_cfg = LatencyConfig(mode=mode_map.get(request.latency_mode, LatencyMode.DETERMINISTIC)) | |
| simulator = MarketSimulator( | |
| agents, initial_price=request.initial_price, | |
| duration_seconds=config.simulation_duration, mode=config.simulation_mode, | |
| oracle_config=oracle_cfg, latency_config=latency_cfg, speed_multiplier=request.speed, | |
| ) | |
| _sim_task = asyncio.create_task(_run_simulation_loop()) | |
| return {"status": "started", "preset": request.preset, "agents": len(agents), | |
| "oracle_enabled": request.oracle_enabled, "speed": request.speed} | |
| async def create_abides_sandbox(request: AbidesSandboxCreateRequest): | |
| global abides_simulator, _abides_task | |
| if not ABIDES_AVAILABLE: | |
| raise HTTPException(status_code=501, detail="ABIDES module not available") | |
| if abides_simulator and abides_simulator.running: | |
| abides_simulator.running = False | |
| if _abides_task: | |
| _abides_task.cancel() | |
| if simulator and simulator.running: | |
| simulator.stop() | |
| if _sim_task: | |
| _sim_task.cancel() | |
| oracle_cfg = OracleConfig( | |
| r_bar=request.initial_price, | |
| kappa=request.oracle_kappa, | |
| sigma_s=request.oracle_sigma, | |
| enabled=request.oracle_enabled, | |
| ) | |
| mode_map = {"zero": LatencyMode.ZERO, "deterministic": LatencyMode.DETERMINISTIC, "cubic": LatencyMode.CUBIC} | |
| latency_cfg = LatencyConfig(mode=mode_map.get(request.latency_mode, LatencyMode.DETERMINISTIC)) | |
| abides_simulator = AbidesSimulation( | |
| oracle_config=oracle_cfg, | |
| latency_config=latency_cfg, | |
| speed_multiplier=request.speed, | |
| ) | |
| exchange = AbidesExchangeAgent() | |
| abides_simulator.set_exchange(exchange) | |
| for idx in range(max(0, request.market_makers)): | |
| abides_simulator.register_agent(AbidesMarketMakerAgent(f"AB_MM_{idx+1}", wakeup_interval=0.5)) | |
| for idx in range(max(0, request.noise_agents)): | |
| abides_simulator.register_agent(AbidesNoiseAgent(f"AB_NOISE_{idx+1}", wakeup_interval=0.4, order_rate=0.8)) | |
| for idx in range(max(0, request.informed_agents)): | |
| abides_simulator.register_agent(AbidesInformedAgent(f"AB_INF_{idx+1}", wakeup_interval=0.7, mispricing_threshold=0.15)) | |
| _abides_task = asyncio.create_task(_run_abides_loop()) | |
| return { | |
| "status": "started", | |
| "engine": "ABIDES", | |
| "oracle_enabled": request.oracle_enabled, | |
| "speed": request.speed, | |
| "agents": len(abides_simulator.agents), | |
| } | |
| async def stop_abides_sandbox(): | |
| _stop_abides() | |
| return {"status": "stopped"} | |
| async def abides_status(): | |
| return { | |
| "available": ABIDES_AVAILABLE, | |
| "running": bool(abides_simulator and abides_simulator.running), | |
| "step": abides_simulator.step_count if abides_simulator else 0, | |
| } | |
| class SpeedRequest(BaseModel): | |
| speed: float | |
| async def set_sandbox_speed(request: SpeedRequest): | |
| if simulator is None: | |
| return {"error": "No active simulation"} | |
| simulator.speed_multiplier = max(0.1, min(20.0, request.speed)) | |
| return {"speed": simulator.speed_multiplier} | |
| async def set_abides_speed(request: SpeedRequest): | |
| if abides_simulator is None: | |
| return {"error": "No active ABIDES simulation"} | |
| abides_simulator.speed_multiplier = max(0.1, min(20.0, request.speed)) | |
| return {"speed": abides_simulator.speed_multiplier} | |
| async def get_oracle_data(): | |
| if simulator is None: | |
| return {"error": "No active simulation"} | |
| return {**simulator.oracle.describe(), "recent_history": simulator.oracle.get_recent_history(240)} | |
| # ββ Stock Replay Endpoints ββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def list_popular_stocks(): | |
| return POPULAR_TICKERS | |
| class StockFetchRequest(BaseModel): | |
| ticker: str | |
| period: str = "3mo" | |
| interval: str = "1d" | |
| async def fetch_stock_data(request: StockFetchRequest): | |
| try: | |
| info = fetch_stock(ticker=request.ticker, period=request.period, interval=request.interval) | |
| return {"ticker": info.ticker, "name": info.name, "currency": info.currency, | |
| "last_close": info.last_close, "period_start": info.period_start, | |
| "period_end": info.period_end, "bars": info.bars, | |
| "realized_vol": info.realized_vol, "mean_return": info.mean_return, | |
| "price_preview": info.prices[-60:]} | |
| except (ValueError, Exception) as e: | |
| return {"error": str(e)} | |
| class StockReplayRequest(BaseModel): | |
| ticker: str | |
| period: str = "3mo" | |
| interval: str = "1d" | |
| preset: str = "balanced" | |
| custom_agents: Optional[dict] = None | |
| latency_mode: str = "deterministic" | |
| speed: float = 1.0 | |
| async def start_stock_replay(request: StockReplayRequest): | |
| global simulator, _sim_task | |
| try: | |
| info = fetch_stock(ticker=request.ticker, period=request.period, interval=request.interval) | |
| except (ValueError, Exception) as e: | |
| return {"error": str(e)} | |
| if simulator and simulator.running: | |
| simulator.stop() | |
| if _sim_task: | |
| _sim_task.cancel() | |
| _stop_abides() | |
| large_order_detector.reset() | |
| oracle_path = build_oracle_path(info, target_steps=500) | |
| initial_price = float(info.prices[0]) | |
| oracle_cfg = OracleConfig(r_bar=initial_price, kappa=0.05, | |
| sigma_s=max(0.001, info.realized_vol / 252), | |
| enabled=True, replay_path=oracle_path) | |
| mode_map = {"zero": LatencyMode.ZERO, "deterministic": LatencyMode.DETERMINISTIC, "cubic": LatencyMode.CUBIC} | |
| latency_cfg = LatencyConfig(mode=mode_map.get(request.latency_mode, LatencyMode.DETERMINISTIC)) | |
| agents = create_sandbox_agents(request.preset, request.custom_agents) | |
| simulator = MarketSimulator( | |
| agents, initial_price=initial_price, duration_seconds=config.simulation_duration, | |
| mode=config.simulation_mode, oracle_config=oracle_cfg, latency_config=latency_cfg, | |
| speed_multiplier=request.speed, | |
| ) | |
| _sim_task = asyncio.create_task(_run_simulation_loop()) | |
| return {"status": "started", "ticker": info.ticker, "name": info.name, | |
| "initial_price": initial_price, "bars": info.bars, | |
| "realized_vol": info.realized_vol, "agents": len(agents)} | |
| # ββ WebSocket βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def websocket_endpoint(websocket: WebSocket): | |
| await manager.connect(websocket) | |
| try: | |
| while True: | |
| # Keep connection alive, receive any client messages | |
| try: | |
| await asyncio.wait_for(websocket.receive_text(), timeout=0.1) | |
| except asyncio.TimeoutError: | |
| pass | |
| except WebSocketDisconnect: | |
| manager.disconnect(websocket) | |
| except Exception: | |
| manager.disconnect(websocket) | |
| # ββ Simulation Loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def _run_simulation_loop(): | |
| """Run the simulation and broadcast updates via WebSocket.""" | |
| global simulator | |
| if simulator is None: | |
| return | |
| simulator.running = True | |
| logger.info("Simulation loop started") | |
| try: | |
| while simulator.running and simulator.current_time < simulator.duration_seconds: | |
| if rl_policy and rl_policy.ready: | |
| try: | |
| rl_policy.prepare_step(simulator) | |
| except Exception as exc: | |
| logger.error(f"RL policy inference failed: {exc}") | |
| state = simulator.step() | |
| liquidity_pred = liquidity_predictor.predict(state) | |
| large_order_det = large_order_detector.detect(state) | |
| agent_metrics = {} | |
| for agent in simulator.agents: | |
| m = agent.get_metrics(simulator.current_price) | |
| agent_metrics[agent.agent_id] = { | |
| "total_pnl": m["total_pnl"], "realized_pnl": m["realized_pnl"], | |
| "unrealized_pnl": m["unrealized_pnl"], "sharpe_ratio": m["sharpe_ratio"], | |
| "agent_type": m["agent_type"], "position": m["position"], | |
| "num_trades": m["num_trades"], | |
| } | |
| update = { | |
| "type": "market_update", | |
| "timestamp": state["current_time"], | |
| "price": state["current_price"], | |
| "spread": state["spread"], | |
| "depth": state["total_depth"], | |
| "order_book": {"bids": state["bid_levels"][:10], "asks": state["ask_levels"][:10]}, | |
| "liquidity_prediction": liquidity_pred, | |
| "large_order_detection": large_order_det, | |
| "agent_metrics": agent_metrics, | |
| "step": state["step"], | |
| "volatility": state["volatility"], | |
| "mode": simulator.mode, | |
| "speed": getattr(simulator, 'speed_multiplier', 1.0), | |
| } | |
| if "oracle" in state: | |
| update["oracle"] = state["oracle"] | |
| if manager.client_count > 0: | |
| await manager.broadcast(update) | |
| sleep_time = max(0.02, 0.1 / getattr(simulator, 'speed_multiplier', 1.0)) | |
| await asyncio.sleep(sleep_time) | |
| except asyncio.CancelledError: | |
| logger.info("Simulation loop cancelled") | |
| except Exception as e: | |
| logger.error(f"Simulation loop error: {e}") | |
| finally: | |
| if simulator: | |
| simulator.running = False | |
| logger.info("Simulation loop ended") | |
| async def _run_abides_loop(): | |
| """Run the ABIDES simulation and broadcast updates via WebSocket.""" | |
| global abides_simulator | |
| if abides_simulator is None: | |
| return | |
| abides_simulator.running = True | |
| logger.info("ABIDES loop started") | |
| try: | |
| while abides_simulator.running: | |
| state = abides_simulator.step(step_seconds=1.0) | |
| agent_metrics = {} | |
| for agent in abides_simulator.agents.values(): | |
| m = agent.get_metrics(state.get("mid_price") or state.get("price") or 0.0) | |
| agent_metrics[agent.agent_id] = { | |
| "total_pnl": m["total_pnl"], | |
| "realized_pnl": m["realized_pnl"], | |
| "unrealized_pnl": m["unrealized_pnl"], | |
| "sharpe_ratio": m["sharpe_ratio"], | |
| "agent_type": m["agent_type"], | |
| "position": m["position"], | |
| "num_trades": m["num_trades"], | |
| } | |
| update = { | |
| "type": "abides_update", | |
| "timestamp": state.get("current_time", 0.0), | |
| "price": state.get("price"), | |
| "spread": state.get("spread"), | |
| "depth": state.get("total_depth"), | |
| "order_book": { | |
| "bids": state.get("bid_levels", [])[:10], | |
| "asks": state.get("ask_levels", [])[:10], | |
| }, | |
| "agent_metrics": agent_metrics, | |
| "step": state.get("step", 0), | |
| "volatility": 0.0, | |
| "mode": "SANDBOX", | |
| "engine": "ABIDES", | |
| "speed": abides_simulator.speed_multiplier, | |
| } | |
| if state.get("oracle"): | |
| update["oracle"] = state.get("oracle") | |
| if manager.client_count > 0: | |
| await manager.broadcast(update) | |
| sleep_time = max(0.05, 0.2 / abides_simulator.speed_multiplier) | |
| await asyncio.sleep(sleep_time) | |
| except asyncio.CancelledError: | |
| logger.info("ABIDES loop cancelled") | |
| except Exception as exc: | |
| logger.error(f"ABIDES loop error: {exc}") | |
| finally: | |
| if abides_simulator: | |
| abides_simulator.running = False | |
| logger.info("ABIDES loop ended") | |