Files
xau-traderbot/tests/test_risk_manager.py
T

139 lines
3.8 KiB
Python

"""
Unit tests for Risk Manager (Position sizing, daily loss limit, margin safety).
"""
from unittest.mock import MagicMock
import pytest
from execution.risk_manager import RiskManager
@pytest.fixture
def mock_mt5(monkeypatch):
"""Create a mock MT5 connector."""
monkeypatch.setattr("database.crud.get_daily_pnl", lambda: 0.0)
mock = MagicMock()
mock.get_account_info.return_value = {
"login": 12345678,
"balance": 10_000.0,
"equity": 10_000.0,
"margin": 200.0,
"margin_free": 9_800.0,
"margin_level": 5000.0,
"profit": 0.0,
"trade_allowed": True,
}
mock.get_symbol_info.return_value = {
"name": "XAUUSD",
"trade_contract_size": 100,
"point": 0.01,
"volume_step": 0.01,
"volume_min": 0.01,
"volume_max": 100.0,
"spread": 20,
}
mock.get_open_positions.return_value = []
mock.get_current_tick.return_value = {
"bid": 2650.0,
"ask": 2650.25,
"spread": 25,
}
return mock
def test_can_trade_normal_conditions(mock_mt5):
rm = RiskManager(mock_mt5)
allowed, reason = rm.can_trade("XAUUSD")
assert allowed is True
assert "passed" in reason.lower()
def test_cannot_trade_low_free_margin(mock_mt5):
mock_mt5.get_account_info.return_value = {
"balance": 10_000.0,
"equity": 4_000.0,
"margin": 2_000.0,
"margin_free": 2_000.0, # Only 20% of balance (min is 50%)
"trade_allowed": True,
}
rm = RiskManager(mock_mt5)
allowed, reason = rm.can_trade("XAUUSD")
assert allowed is False
assert "margin" in reason.lower()
def test_cannot_trade_excessive_spread(mock_mt5):
mock_mt5.get_current_tick.return_value = {
"bid": 2650.0,
"ask": 2650.95,
"spread": 95, # Exceeds max_spread
}
rm = RiskManager(mock_mt5)
allowed, reason = rm.can_trade("XAUUSD")
assert allowed is False
assert "spread" in reason.lower()
def test_calculate_lot_size(mock_mt5):
rm = RiskManager(mock_mt5)
entry_price = 2650.0
stop_loss = 2640.0 # 10 dollar / 1000 point distance
lot = rm.calculate_lot_size(entry_price, stop_loss, symbol="XAUUSD")
# Balance: 10,000, 2.5% risk = $250 risk.
# $250 / ($10 * 100) = 0.25 lots.
assert 0.01 <= lot <= rm.max_lot
assert round(lot, 2) == lot
def test_can_trade_zero_margin_positions(mock_mt5):
"""Test that account with 0 open positions (margin=0.0, margin_level=0.0 in MT5) passes risk check."""
mock_mt5.get_account_info.return_value = {
"login": 12345678,
"balance": 100_000.0,
"equity": 100_000.0,
"margin": 0.0,
"free_margin": 100_000.0,
"margin_level": 0.0, # MT5 default when margin is 0
"profit": 0.0,
"trade_allowed": True,
}
mock_mt5.get_current_tick.return_value = {
"bid": 2650.0,
"ask": 2650.25,
"spread": 25,
}
rm = RiskManager(mock_mt5)
allowed, reason = rm.can_trade("XAUUSD")
assert allowed is True
assert "passed" in reason.lower()
def test_trading_modes_switching(mock_mt5):
"""Test that switching trading modes alters risk parameters dynamically."""
from config.settings import get_settings
settings = get_settings()
# Safe Mode
settings.set_active_mode("safe")
rm = RiskManager(mock_mt5)
assert rm.max_risk_pct == 1.5
assert rm.max_spread == 45.0
assert rm.max_concurrent == 2
# Moderate Mode
settings.set_active_mode("moderate")
assert rm.max_risk_pct == 2.5
assert rm.max_spread == 70.0
assert rm.max_concurrent == 4
# Aggressive Mode
settings.set_active_mode("aggressive")
assert rm.max_risk_pct == 4.0
assert rm.max_spread == 90.0
assert rm.max_concurrent == 6
# Return to moderate
settings.set_active_mode("moderate")