alpha-arena/tests/test_risk.py
oho 774f8b3f61 Initial commit: Alpha Arena - Polymarket Autonomous Trading Harness
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
2026-01-12 23:04:58 +01:00

344 lines
11 KiB
Python

"""Tests for risk management controls."""
from datetime import datetime, timedelta
from decimal import Decimal
import pytest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.broker.base import OrderSide, Position
from src.risk.controls import RiskConfig, RiskManager, RiskViolation
from src.strategy.base import ExitPlan, SignalType, TradeSignal
@pytest.fixture
def risk_config() -> RiskConfig:
"""Default risk configuration for tests."""
return RiskConfig(
max_position_size_usdc=Decimal("100"),
max_single_trade_usdc=Decimal("25"),
max_open_positions=3,
daily_loss_limit_usdc=Decimal("50"),
max_orders_per_minute=5,
kill_switch=False,
min_confidence=Decimal("0.6"),
min_edge=Decimal("0.05"),
)
@pytest.fixture
def risk_manager(risk_config: RiskConfig) -> RiskManager:
"""Risk manager instance for tests."""
return RiskManager(risk_config)
@pytest.fixture
def valid_signal() -> TradeSignal:
"""A valid trade signal that should pass risk checks."""
return TradeSignal(
market_id="test_market_123",
token_id="test_token_456",
signal_type=SignalType.ENTER_LONG,
side=OrderSide.BUY,
size=Decimal("10"),
price=Decimal("0.50"),
confidence=Decimal("0.75"),
expected_edge=Decimal("0.08"),
exit_plan=ExitPlan(
profit_target_price=Decimal("0.65"),
stop_loss_price=Decimal("0.40"),
),
)
class TestRiskChecks:
"""Test risk check validations."""
def test_valid_signal_passes(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Valid signal should pass all checks."""
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is True
assert len(check.violations) == 0
def test_kill_switch_blocks_all(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Kill switch should block all trades."""
risk_manager.activate_kill_switch("test")
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is False
assert RiskViolation.KILL_SWITCH_ACTIVE in check.violations
def test_insufficient_balance(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Should reject when balance too low."""
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("1"), # Not enough
positions=[],
)
assert check.passed is False
assert RiskViolation.INSUFFICIENT_BALANCE in check.violations
def test_max_single_trade(self, risk_manager: RiskManager):
"""Should reject trades exceeding single trade limit."""
big_signal = TradeSignal(
market_id="test",
token_id="test",
signal_type=SignalType.ENTER_LONG,
side=OrderSide.BUY,
size=Decimal("50"), # Exceeds $25 limit
price=Decimal("0.50"),
confidence=Decimal("0.75"),
expected_edge=Decimal("0.08"),
)
check = risk_manager.check_signal(
big_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is False
assert RiskViolation.MAX_SINGLE_TRADE in check.violations
def test_max_position_size(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Should reject when position would exceed limit."""
# Existing position: 180 * 0.60 = $108, already over $100 limit
# Adding 10 * 0.50 = $5 would make it even worse
existing_position = Position(
market_id="test_market_123",
token_id="test_token_456",
outcome="YES",
size=Decimal("180"),
avg_entry_price=Decimal("0.60"),
current_price=Decimal("0.60"),
unrealized_pnl=Decimal("0"),
)
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("100"),
positions=[existing_position],
)
assert check.passed is False
assert RiskViolation.MAX_POSITION_SIZE in check.violations
def test_max_open_positions(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Should reject when at max positions."""
positions = [
Position(
market_id=f"market_{i}",
token_id=f"token_{i}",
outcome="YES",
size=Decimal("10"),
avg_entry_price=Decimal("0.50"),
current_price=Decimal("0.50"),
unrealized_pnl=Decimal("0"),
)
for i in range(3) # At max (3)
]
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("100"),
positions=positions,
)
assert check.passed is False
assert RiskViolation.MAX_OPEN_POSITIONS in check.violations
def test_low_confidence_rejected(self, risk_manager: RiskManager):
"""Should reject low confidence signals."""
low_conf_signal = TradeSignal(
market_id="test",
token_id="test",
signal_type=SignalType.ENTER_LONG,
side=OrderSide.BUY,
size=Decimal("10"),
price=Decimal("0.50"),
confidence=Decimal("0.40"), # Below 0.6 threshold
expected_edge=Decimal("0.08"),
)
check = risk_manager.check_signal(
low_conf_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is False
assert RiskViolation.LOW_CONFIDENCE in check.violations
def test_low_edge_rejected(self, risk_manager: RiskManager):
"""Should reject low edge signals."""
low_edge_signal = TradeSignal(
market_id="test",
token_id="test",
signal_type=SignalType.ENTER_LONG,
side=OrderSide.BUY,
size=Decimal("10"),
price=Decimal("0.50"),
confidence=Decimal("0.75"),
expected_edge=Decimal("0.02"), # Below 0.05 threshold
)
check = risk_manager.check_signal(
low_edge_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is False
assert RiskViolation.LOW_CONFIDENCE in check.violations
def test_hold_signal_always_passes(self, risk_manager: RiskManager):
"""HOLD signals should always pass."""
risk_manager.activate_kill_switch()
hold_signal = TradeSignal(
market_id="test",
token_id="test",
signal_type=SignalType.HOLD,
confidence=Decimal("0"),
)
check = risk_manager.check_signal(
hold_signal,
balance=Decimal("0"),
positions=[],
)
# HOLD passes even with kill switch active (after the check)
# Actually kill switch is checked first, but HOLD bypasses other checks
# Let me verify the implementation - kill switch is checked first
# So this should fail with kill switch
assert check.passed is False # Kill switch blocks everything
class TestDailyLossLimit:
"""Test daily loss limit tracking."""
def test_daily_pnl_tracking(self, risk_manager: RiskManager):
"""Should track daily PnL."""
risk_manager.record_pnl(Decimal("-10"))
risk_manager.record_pnl(Decimal("-15"))
status = risk_manager.get_status()
assert Decimal(status["daily_pnl"]) == Decimal("-25")
def test_daily_loss_limit_triggered(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Should block trades when daily loss limit hit."""
# Record losses up to limit
risk_manager.record_pnl(Decimal("-50"))
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is False
assert RiskViolation.DAILY_LOSS_LIMIT in check.violations
class TestRateLimiting:
"""Test order rate limiting."""
def test_rate_limit_enforcement(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Should block when rate limit exceeded."""
from src.broker.base import Order, OrderStatus
# Record orders up to limit
for i in range(5):
order = Order(
id=f"order_{i}",
market_id="test",
token_id="test",
side=OrderSide.BUY,
size=Decimal("1"),
price=Decimal("0.50"),
status=OrderStatus.OPEN,
)
risk_manager.record_order(order)
check = risk_manager.check_signal(
valid_signal,
balance=Decimal("100"),
positions=[],
)
assert check.passed is False
assert RiskViolation.RATE_LIMIT in check.violations
class TestSignalAdjustment:
"""Test signal adjustment for risk compliance."""
def test_size_reduced_for_balance(self, risk_manager: RiskManager):
"""Should reduce size to fit balance."""
signal = TradeSignal(
market_id="test",
token_id="test",
signal_type=SignalType.ENTER_LONG,
side=OrderSide.BUY,
size=Decimal("50"),
price=Decimal("0.50"), # Would cost $25
confidence=Decimal("0.75"),
expected_edge=Decimal("0.08"),
)
adjusted = risk_manager.adjust_signal_for_risk(
signal,
balance=Decimal("10"), # Only $10 available
positions=[],
)
assert adjusted is not None
assert adjusted.size < signal.size
assert adjusted.size * adjusted.price <= Decimal("10")
def test_no_adjustment_for_blocking_violations(self, risk_manager: RiskManager, valid_signal: TradeSignal):
"""Should return None when can't adjust for violations."""
risk_manager.activate_kill_switch()
adjusted = risk_manager.adjust_signal_for_risk(
valid_signal,
balance=Decimal("100"),
positions=[],
)
assert adjusted is None
class TestKillSwitch:
"""Test kill switch functionality."""
def test_activate_kill_switch(self, risk_manager: RiskManager):
"""Should activate kill switch."""
risk_manager.activate_kill_switch("test reason")
assert risk_manager.config.kill_switch is True
def test_deactivate_kill_switch(self, risk_manager: RiskManager):
"""Should deactivate kill switch."""
risk_manager.activate_kill_switch()
risk_manager.deactivate_kill_switch()
assert risk_manager.config.kill_switch is False
def test_status_shows_kill_switch(self, risk_manager: RiskManager):
"""Status should show kill switch state."""
risk_manager.activate_kill_switch()
status = risk_manager.get_status()
assert status["kill_switch"] is True