Trading-Bot-M20 / tests /test_risk.py
raghava4u's picture
Upload folder using huggingface_hub
d53dc44 verified
Raw History Blame Contribute Delete
6.2 kB
"""
tests/test_risk.py — Risk check pipeline tests.
Covers:
- Safe mode blocking
- Daily loss limit
- Max positions limit
- 3 consecutive losses → pause + half size
- Profit lock after daily target
- Position sizing with ATR
"""
from __future__ import annotations
import datetime
from unittest.mock import patch
import pytest
import config
from contracts import FinalScore
from execution import risk as risk_module
@pytest.fixture(autouse=True)
def reset_risk_state():
"""Reset risk module state before each test."""
risk_module._last_reset_date = None # force reset
risk_module.reset_daily_state(equity=100_000.0)
yield
def _make_final(symbol: str, decision: str = "BUY", score: float = 0.7) -> FinalScore:
return FinalScore(
symbol=symbol,
timestamp=datetime.datetime.now(datetime.timezone.utc),
technical=score,
sentiment=0.5,
final=score,
decision=decision,
)
class TestDailyLossLimit:
@patch("execution.broker.safe_mode_active", False)
@patch("execution.broker.cancel_all_orders")
@patch("execution.broker.close_all_positions")
def test_blocks_after_max_loss(self, mock_close, mock_cancel):
"""After exceeding daily loss limit, all trades should be blocked."""
risk_module.update_pnl(realized=-(config.MAX_DAILY_LOSS_USD + 5.0), unrealized=0.0)
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=0,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert not allowed
assert "daily_loss" in reason.lower() or "loss" in reason.lower()
@patch("execution.broker.safe_mode_active", False)
def test_allows_within_limit(self):
risk_module.update_pnl(realized=-10.0, unrealized=0.0)
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=0,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert allowed
class TestMaxPositions:
@patch("execution.broker.safe_mode_active", False)
def test_blocks_at_max(self):
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=config.MAX_OPEN_POSITIONS,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert not allowed
@patch("execution.broker.safe_mode_active", False)
def test_allows_below_max(self):
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=2,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert allowed
class TestConsecutiveLosses:
@patch("execution.broker.safe_mode_active", False)
def test_pause_after_3_losses(self):
"""3 consecutive losses → 30 min pause."""
for _ in range(3):
risk_module.record_trade_result(-100.0)
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=0,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert not allowed
def test_half_size_after_3_losses(self):
"""After 3 losses, half_size_mode should be True."""
for _ in range(3):
risk_module.record_trade_result(-100.0)
# Trigger the risk check which sets half_size_mode
with patch("execution.broker.safe_mode_active", False):
risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=0,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert risk_module.half_size_mode
class TestProfitLock:
@patch("execution.broker.safe_mode_active", False)
def test_locks_after_target(self):
"""After hitting the configured daily target, lock profits."""
risk_module.update_pnl(realized=config.TARGET_DAILY_PROFIT_USD + 5.0, unrealized=0.0)
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=0,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert not allowed
assert "profit" in reason.lower() or "target" in reason.lower()
class TestPositionSizing:
@patch("execution.broker.is_fractionable", return_value=True)
def test_size_within_risk_budget(self, mock_frac):
qty, stop_dist, is_frac = risk_module.compute_position_size(
equity=100_000.0,
atr_value=2.0,
symbol="AAPL",
)
# Risk per trade = 1% of $100k = $1000
# With ATR stop = 2.0 * 1.0 = $2.0
# Qty = 1000 / 2.0 = 500
assert qty > 0
assert stop_dist > 0
@patch("execution.broker.is_fractionable", return_value=True)
def test_fractional_flag(self, mock_frac):
qty, _, is_frac = risk_module.compute_position_size(
equity=100.0, # tiny account → fractional
atr_value=2.0,
symbol="AAPL",
)
if qty > 0 and qty < 1:
assert is_frac
class TestSafeMode:
@patch("execution.broker.safe_mode_active", True)
def test_safe_mode_blocks(self):
allowed, reason = risk_module.run_risk_checks(
symbol="AAPL",
final_score=_make_final("AAPL"),
open_position_count=0,
open_positions={},
alert_callback=lambda *a, **kw: None,
)
assert not allowed
assert "safe" in reason.lower()