mirror of
https://github.com/saymrwulf/alpha-arena.git
synced 2026-05-15 20:40:08 +00:00
A comprehensive autonomous trading system for Polymarket prediction markets featuring multi-LLM provider support, a native macOS menu bar app, and a web-based control dashboard. Key features: - Multi-agent trading system (Research, Risk, Execution, Reflection agents) - LLM provider flexibility (Anthropic, OpenAI, Google, xAI, Local models) - Automatic provider fallback chain for resilience - Native Swift/SwiftUI macOS menu bar application - FastAPI web dashboard with real-time WebSocket updates - Risk management with kill switch - Technical indicators and market analysis
423 lines
16 KiB
Python
423 lines
16 KiB
Python
"""
|
|
API Tests for Alpha Arena Web Application.
|
|
|
|
Tests all REST API endpoints for correct behavior, error handling,
|
|
and response formats.
|
|
"""
|
|
|
|
import pytest
|
|
from decimal import Decimal
|
|
|
|
pytestmark = pytest.mark.api
|
|
|
|
|
|
class TestHealthEndpoints:
|
|
"""Test system health and status endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_health_check(self, async_client):
|
|
"""GET /api/system/health should return healthy status."""
|
|
response = await async_client.get("/api/system/health")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "healthy"
|
|
# API returns trading_active and websocket_clients
|
|
assert "trading_active" in data or "timestamp" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_providers_status(self, async_client):
|
|
"""GET /api/system/providers should return provider status."""
|
|
response = await async_client.get("/api/system/providers")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# API returns provider statuses directly
|
|
assert isinstance(data, dict)
|
|
# Should have some known providers
|
|
assert any(key in data for key in ["anthropic", "openai", "polymarket", "providers"])
|
|
|
|
|
|
class TestTradingEndpoints:
|
|
"""Test trading control endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trading_status(self, async_client):
|
|
"""GET /api/trading/status should return current status."""
|
|
response = await async_client.get("/api/trading/status")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# API uses "active" instead of "is_running"
|
|
assert "active" in data or "is_running" in data
|
|
assert "mode" in data or "kill_switch" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_trading_simulation(self, async_client):
|
|
"""POST /api/trading/start should start trading loop."""
|
|
response = await async_client.post(
|
|
"/api/trading/start",
|
|
json={"mode": "simulation", "loop_interval": 60}
|
|
)
|
|
# May succeed or fail depending on state, but should return valid response
|
|
assert response.status_code in [200, 400]
|
|
data = response.json()
|
|
assert "status" in data or "detail" in data or "message" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stop_trading(self, async_client):
|
|
"""POST /api/trading/stop should stop trading loop."""
|
|
response = await async_client.post("/api/trading/stop")
|
|
# May succeed or fail depending on state
|
|
assert response.status_code in [200, 400]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_kill_switch_toggle(self, async_client):
|
|
"""POST /api/trading/kill-switch should toggle kill switch."""
|
|
# API expects a Form field "enabled"
|
|
response = await async_client.post(
|
|
"/api/trading/kill-switch",
|
|
data={"enabled": "true"}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "kill_switch" in data or "enabled" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_trading_with_mode(self, async_client):
|
|
"""POST /api/trading/start with mode parameter."""
|
|
response = await async_client.post(
|
|
"/api/trading/start",
|
|
json={"mode": "simulation"}
|
|
)
|
|
# Should accept valid modes
|
|
assert response.status_code in [200, 400]
|
|
|
|
|
|
class TestPositionsEndpoints:
|
|
"""Test positions and orders endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_positions(self, async_client):
|
|
"""GET /api/positions should return positions list."""
|
|
response = await async_client.get("/api/positions")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "positions" in data
|
|
assert isinstance(data["positions"], list)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_orders(self, async_client):
|
|
"""GET /api/orders should return orders list."""
|
|
response = await async_client.get("/api/orders")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "orders" in data
|
|
assert isinstance(data["orders"], list)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close_position_not_found(self, async_client):
|
|
"""POST /api/positions/{id}/close for non-existent position."""
|
|
response = await async_client.post("/api/positions/nonexistent_123/close")
|
|
# API may return 200 with error in body, or 404/400
|
|
assert response.status_code in [200, 404, 400]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancel_order_not_found(self, async_client):
|
|
"""POST /api/orders/{id}/cancel for non-existent order."""
|
|
response = await async_client.post("/api/orders/nonexistent_123/cancel")
|
|
# API may return 200 with error in body, or 404/400
|
|
assert response.status_code in [200, 404, 400]
|
|
|
|
|
|
class TestMarketsEndpoints:
|
|
"""Test markets browsing endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_markets(self, async_client):
|
|
"""GET /api/markets should return markets list."""
|
|
response = await async_client.get("/api/markets")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "markets" in data
|
|
assert isinstance(data["markets"], list)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_markets_with_filters(self, async_client):
|
|
"""GET /api/markets with query params should filter results."""
|
|
response = await async_client.get(
|
|
"/api/markets",
|
|
params={"category": "politics", "limit": 10}
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "markets" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_market_detail(self, async_client):
|
|
"""GET /api/markets/{id} should return market details."""
|
|
# First get a market ID
|
|
markets_response = await async_client.get("/api/markets?limit=1")
|
|
markets = markets_response.json().get("markets", [])
|
|
|
|
if markets:
|
|
market_id = markets[0].get("id", "test")
|
|
response = await async_client.get(f"/api/markets/{market_id}")
|
|
assert response.status_code in [200, 404]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_arbitrage_opportunities(self, async_client):
|
|
"""GET /api/arbitrage should return arbitrage opportunities."""
|
|
response = await async_client.get("/api/arbitrage")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "opportunities" in data
|
|
assert isinstance(data["opportunities"], list)
|
|
|
|
|
|
class TestWalletAnalysisEndpoints:
|
|
"""Test wallet analysis endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analyze_wallet_with_address(self, async_client):
|
|
"""POST /api/wallet/analyze with address."""
|
|
response = await async_client.post(
|
|
"/api/wallet/analyze",
|
|
json={"address": "invalid_address"}
|
|
)
|
|
# API may accept any string and return analysis or error
|
|
assert response.status_code in [200, 400, 422, 500]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_analyze_wallet_valid_format(self, async_client):
|
|
"""POST /api/wallet/analyze with valid address format."""
|
|
response = await async_client.post(
|
|
"/api/wallet/analyze",
|
|
json={"address": "0x1234567890123456789012345678901234567890"}
|
|
)
|
|
# May fail due to no data, but should accept the format
|
|
assert response.status_code in [200, 404, 500]
|
|
|
|
|
|
class TestAgentsEndpoints:
|
|
"""Test agent monitoring endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_agents(self, async_client):
|
|
"""GET /api/agents should return agents status."""
|
|
response = await async_client.get("/api/agents")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "agents" in data
|
|
assert isinstance(data["agents"], list)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_agent_history(self, async_client):
|
|
"""GET /api/agents/{name}/history should return agent history."""
|
|
response = await async_client.get("/api/agents/research/history")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "history" in data
|
|
|
|
|
|
class TestRiskEndpoints:
|
|
"""Test risk management endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_risk_status(self, async_client):
|
|
"""GET /api/risk/status should return risk status."""
|
|
response = await async_client.get("/api/risk/status")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "daily_pnl" in data or "status" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_risk_settings(self, async_client):
|
|
"""POST /api/risk/settings should update risk settings."""
|
|
response = await async_client.post(
|
|
"/api/risk/settings",
|
|
json={
|
|
"max_position_size_usdc": 150,
|
|
"daily_loss_limit_usdc": 75
|
|
}
|
|
)
|
|
assert response.status_code in [200, 400]
|
|
|
|
|
|
class TestConfigEndpoints:
|
|
"""Test configuration endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_config(self, async_client):
|
|
"""GET /api/config should return current configuration."""
|
|
response = await async_client.get("/api/config")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Should have some config structure
|
|
assert isinstance(data, dict)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_config(self, async_client):
|
|
"""POST /api/config should update configuration."""
|
|
response = await async_client.post(
|
|
"/api/config",
|
|
json={"loop_interval_seconds": 120}
|
|
)
|
|
assert response.status_code in [200, 400]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_llm_config(self, async_client):
|
|
"""GET /api/config/llm should return LLM configuration."""
|
|
response = await async_client.get("/api/config/llm")
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestLogsEndpoints:
|
|
"""Test logs and metrics endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_decision_logs(self, async_client):
|
|
"""GET /api/logs/decisions should return decision logs."""
|
|
response = await async_client.get("/api/logs/decisions")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "logs" in data
|
|
assert isinstance(data["logs"], list)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_decision_logs_with_limit(self, async_client):
|
|
"""GET /api/logs/decisions with limit param."""
|
|
response = await async_client.get("/api/logs/decisions?limit=5")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data.get("logs", [])) <= 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_trade_history(self, async_client):
|
|
"""GET /api/logs/trades should return trade history."""
|
|
response = await async_client.get("/api/logs/trades")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "trades" in data
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_performance_metrics(self, async_client):
|
|
"""GET /api/metrics/performance should return metrics."""
|
|
response = await async_client.get("/api/metrics/performance")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
# Should have performance metrics
|
|
assert isinstance(data, dict)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_equity_curve(self, async_client):
|
|
"""GET /api/metrics/equity-curve should return equity data."""
|
|
response = await async_client.get("/api/metrics/equity-curve")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "data" in data or "equity" in data or isinstance(data, list)
|
|
|
|
|
|
class TestPageRendering:
|
|
"""Test that HTML pages render correctly."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dashboard_page(self, async_client):
|
|
"""GET / should render dashboard page."""
|
|
response = await async_client.get("/")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trading_page(self, async_client):
|
|
"""GET /trading should render trading page."""
|
|
response = await async_client.get("/trading")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_positions_page(self, async_client):
|
|
"""GET /positions should render positions page."""
|
|
response = await async_client.get("/positions")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_markets_page(self, async_client):
|
|
"""GET /markets should render markets page."""
|
|
response = await async_client.get("/markets")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wallet_analysis_page(self, async_client):
|
|
"""GET /wallet-analysis should render wallet analysis page."""
|
|
response = await async_client.get("/wallet-analysis")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agents_page(self, async_client):
|
|
"""GET /agents should render agents page."""
|
|
response = await async_client.get("/agents")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_page(self, async_client):
|
|
"""GET /risk should render risk page."""
|
|
response = await async_client.get("/risk")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_config_page(self, async_client):
|
|
"""GET /config should render config page."""
|
|
response = await async_client.get("/config")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_logs_page(self, async_client):
|
|
"""GET /logs should render logs page."""
|
|
response = await async_client.get("/logs")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_help_page(self, async_client):
|
|
"""GET /help should render help page."""
|
|
response = await async_client.get("/help")
|
|
assert response.status_code == 200
|
|
assert "text/html" in response.headers.get("content-type", "")
|
|
|
|
|
|
class TestErrorHandling:
|
|
"""Test error handling and edge cases."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_404_for_unknown_route(self, async_client):
|
|
"""Unknown routes should return 404."""
|
|
response = await async_client.get("/api/nonexistent/endpoint")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_405_for_wrong_method(self, async_client):
|
|
"""Wrong HTTP method should return 405."""
|
|
response = await async_client.delete("/api/trading/status")
|
|
assert response.status_code == 405
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_422_for_invalid_json(self, async_client):
|
|
"""Invalid JSON should return 422."""
|
|
response = await async_client.post(
|
|
"/api/trading/start",
|
|
content="not valid json",
|
|
headers={"Content-Type": "application/json"}
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cors_headers_present(self, async_client):
|
|
"""CORS headers should be present for API routes."""
|
|
response = await async_client.options("/api/trading/status")
|
|
# FastAPI may or may not have CORS middleware configured
|
|
# This test documents expected behavior
|
|
assert response.status_code in [200, 405]
|