"""Integration tests for the Trading Engine API — all 12 frontend-facing endpoints. Validates every endpoint the frontend calls against the live sandbox with deterministic seed data. Uses the ``trading_client`` and ``seed_ids`` fixtures from conftest.py. Routes: /health, /ready — probes (root level) /api/trading/status — engine status /api/trading/config — config update /api/trading/pause, /api/trading/resume — engine control /api/trading/decisions — decision audit trail /api/trading/metrics — current portfolio metrics /api/trading/metrics/history — historical snapshots /api/trading/notifications/config — notification config /api/trading/notifications/history — notification history /api/trading/override/order — manual override order """ import pytest pytestmark = pytest.mark.asyncio # --------------------------------------------------------------------------- # 1 Health Check # --------------------------------------------------------------------------- class TestTradingHealth: """Endpoint: GET /health.""" async def test_health(self, trading_client): """GET /health — returns {"status": "ok"}.""" resp = await trading_client.get("/health") assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" # --------------------------------------------------------------------------- # 2 Readiness Check # --------------------------------------------------------------------------- class TestTradingReady: """Endpoint: GET /ready.""" async def test_ready(self, trading_client): """GET /ready — returns readiness state.""" resp = await trading_client.get("/ready") assert resp.status_code == 200 data = resp.json() assert "ready" in data assert isinstance(data["ready"], bool) # --------------------------------------------------------------------------- # 3 Engine Status # --------------------------------------------------------------------------- class TestTradingStatus: """Endpoint: GET /api/trading/status.""" async def test_status(self, trading_client): """GET /api/trading/status — returns engine state with expected fields.""" resp = await trading_client.get("/api/trading/status") assert resp.status_code == 200 data = resp.json() assert "enabled" in data assert "paused" in data assert "risk_tier" in data assert "active_pool" in data assert "reserve_pool" in data assert "portfolio_heat" in data assert "open_positions" in data assert isinstance(data["enabled"], bool) assert isinstance(data["paused"], bool) # --------------------------------------------------------------------------- # 4 Update Config # --------------------------------------------------------------------------- class TestTradingConfig: """Endpoint: PUT /api/trading/config.""" async def test_update_config(self, trading_client): """PUT /api/trading/config — update risk_tier and verify response.""" payload = {"risk_tier": "conservative"} resp = await trading_client.put("/api/trading/config", json=payload) assert resp.status_code == 200 data = resp.json() assert "previous" in data assert "updated" in data assert data["updated"]["risk_tier"] == "conservative" assert "changed_at" in data # --------------------------------------------------------------------------- # 5 Pause Engine # --------------------------------------------------------------------------- class TestTradingPause: """Endpoint: POST /api/trading/pause.""" async def test_pause(self, trading_client): """POST /api/trading/pause — returns paused=True.""" resp = await trading_client.post("/api/trading/pause") assert resp.status_code == 200 data = resp.json() assert data["paused"] is True # --------------------------------------------------------------------------- # 6 Resume Engine # --------------------------------------------------------------------------- class TestTradingResume: """Endpoint: POST /api/trading/resume.""" async def test_resume(self, trading_client): """POST /api/trading/resume — returns paused=False.""" resp = await trading_client.post("/api/trading/resume") assert resp.status_code == 200 data = resp.json() assert data["paused"] is False # --------------------------------------------------------------------------- # 7 Trading Decisions # --------------------------------------------------------------------------- class TestTradingDecisions: """Endpoint: GET /api/trading/decisions.""" async def test_list_decisions(self, trading_client, seed_ids): """GET /api/trading/decisions — expect at least 1 decision from seed data.""" resp = await trading_client.get("/api/trading/decisions") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) assert len(data) >= 1 for d in data: assert "id" in d assert "decision" in d assert "ticker" in d # --------------------------------------------------------------------------- # 8 Current Metrics # --------------------------------------------------------------------------- class TestTradingMetrics: """Endpoint: GET /api/trading/metrics.""" async def test_current_metrics(self, trading_client): """GET /api/trading/metrics — returns portfolio metrics structure.""" resp = await trading_client.get("/api/trading/metrics") assert resp.status_code == 200 data = resp.json() assert "total_portfolio_value" in data assert "active_pool" in data assert "reserve_pool" in data assert "unrealized_pnl" in data assert "realized_pnl" in data assert "daily_pnl" in data assert "win_rate" in data assert "sharpe_ratio" in data assert "max_drawdown" in data assert "portfolio_heat" in data # All values should be numeric for key in data: assert isinstance(data[key], (int, float)), f"{key} should be numeric" # --------------------------------------------------------------------------- # 9 Metrics History # --------------------------------------------------------------------------- class TestTradingMetricsHistory: """Endpoint: GET /api/trading/metrics/history.""" async def test_metrics_history(self, trading_client): """GET /api/trading/metrics/history — returns a list of snapshots.""" resp = await trading_client.get("/api/trading/metrics/history") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) # Seed data includes at least 1 portfolio snapshot if len(data) > 0: snap = data[0] assert "portfolio_value" in snap assert "snapshot_date" in snap # --------------------------------------------------------------------------- # 10 Notification Config # --------------------------------------------------------------------------- class TestTradingNotificationConfig: """Endpoint: GET /api/trading/notifications/config.""" async def test_get_notification_config(self, trading_client): """GET /api/trading/notifications/config — returns notification settings.""" resp = await trading_client.get("/api/trading/notifications/config") assert resp.status_code == 200 data = resp.json() assert "sms_enabled" in data assert "email_enabled" in data assert isinstance(data["sms_enabled"], bool) assert isinstance(data["email_enabled"], bool) # --------------------------------------------------------------------------- # 11 Notification History # --------------------------------------------------------------------------- class TestTradingNotificationHistory: """Endpoint: GET /api/trading/notifications/history.""" async def test_notification_history(self, trading_client): """GET /api/trading/notifications/history — returns a list (may be empty).""" resp = await trading_client.get("/api/trading/notifications/history") assert resp.status_code == 200 data = resp.json() assert isinstance(data, list) # --------------------------------------------------------------------------- # 12 Override Order # --------------------------------------------------------------------------- class TestTradingOverride: """Endpoint: POST /api/trading/override/order.""" async def test_submit_override_order(self, trading_client): """POST /api/trading/override/order — submit a valid market order. The override endpoint may fail if the trading engine isn't fully configured (e.g. no Redis). We accept either a successful 202 or a structured error (4xx/5xx with JSON body). """ payload = { "ticker": "AAPL", "side": "buy", "quantity": 1.0, "order_type": "market", } resp = await trading_client.post( "/api/trading/override/order", json=payload, ) # Accept 202 (queued) or a structured error response assert resp.status_code in (200, 202, 400, 422, 503) data = resp.json() if resp.status_code == 202: assert "job_id" in data assert data["status"] == "queued" assert data["ticker"] == "AAPL" assert data["side"] == "buy" assert data["quantity"] == 1.0 else: # Structured error — just verify it's a dict with some info assert isinstance(data, dict)