feat: autonomous trading engine — full implementation
- Database migration 018 with 13 tables for trading engine state - Trading engine service (services/trading/) with 12 pure computation modules: position sizer, stop-loss manager, reserve pool, circuit breaker, risk tier controller, correlation matrix, tax lots, trading window, gradual entry, notifications, micro-trading, backtester - Core TradingEngine with pre-trade evaluation pipeline and integration wiring - FastAPI HTTP service with 14 endpoints (health, config, decisions, metrics, backtest) - Performance tracker with Sharpe ratio, drawdown, profit factor computation - 194 Python tests (165 property-based + 29 integration) - Frontend: 13 TanStack Query hooks, 7 dashboard panels, tabbed Trading Engine page - Helm chart entry, network policy, nginx proxy, ingress for trading-engine - Shared infrastructure: enums, Redis keys, TradingConfig in AppConfig
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
"""Property-based tests for the Micro-Trading Module.
|
||||
|
||||
Feature: autonomous-trading-engine
|
||||
|
||||
Property 31: Micro-trade parameter constraints.
|
||||
Property 32: Micro-trade auto-close after max hold duration.
|
||||
Property 34: Micro-trades respect all existing constraints.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from services.trading.micro_trading import MicroTradeConfig, MicroTradingModule
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hypothesis strategies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _micro_config_strategy() -> st.SearchStrategy[MicroTradeConfig]:
|
||||
"""Generate random MicroTradeConfig objects with valid ranges."""
|
||||
return st.builds(
|
||||
MicroTradeConfig,
|
||||
enabled=st.just(True),
|
||||
allocation_cap_pct=st.floats(
|
||||
min_value=0.01, max_value=0.10, allow_nan=False, allow_infinity=False,
|
||||
),
|
||||
max_daily=st.integers(min_value=1, max_value=50),
|
||||
max_hold_minutes=st.integers(min_value=10, max_value=480),
|
||||
stop_loss_atr_multiplier=st.just(1.0),
|
||||
reward_risk_ratio=st.just(1.5),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 31: Micro-trade parameter constraints
|
||||
# **Validates: Requirements 20.3, 20.4, 20.5**
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestProperty31MicroTradeParameterConstraints:
|
||||
"""Property 31: Micro-trade parameter constraints.
|
||||
|
||||
Verify allocation does not exceed micro_trading_allocation_cap_pct.
|
||||
Verify daily count does not exceed configured maximum.
|
||||
|
||||
**Validates: Requirements 20.3, 20.4, 20.5**
|
||||
"""
|
||||
|
||||
module = MicroTradingModule()
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
active_pool=st.floats(
|
||||
min_value=100.0, max_value=100_000.0,
|
||||
allow_nan=False, allow_infinity=False,
|
||||
),
|
||||
)
|
||||
def test_allocation_does_not_exceed_cap(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
active_pool: float,
|
||||
) -> None:
|
||||
"""Micro-trade allocation never exceeds allocation_cap_pct * active_pool."""
|
||||
cap = self.module.compute_allocation_cap(config, active_pool)
|
||||
expected = config.allocation_cap_pct * active_pool
|
||||
|
||||
assert abs(cap - expected) < 1e-6
|
||||
assert cap <= config.allocation_cap_pct * active_pool + 1e-6
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
daily_count=st.integers(min_value=0, max_value=100),
|
||||
)
|
||||
def test_daily_count_enforced(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
daily_count: int,
|
||||
) -> None:
|
||||
"""should_evaluate returns False when daily count >= max_daily."""
|
||||
result = self.module.should_evaluate(config, daily_count)
|
||||
|
||||
if daily_count >= config.max_daily:
|
||||
assert result is False
|
||||
else:
|
||||
assert result is True
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
)
|
||||
def test_stop_loss_at_1x_atr(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
) -> None:
|
||||
"""Micro-trade stop-loss uses 1.0x ATR multiplier."""
|
||||
assert config.stop_loss_atr_multiplier == 1.0
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
)
|
||||
def test_take_profit_at_1_5x_stop_distance(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
) -> None:
|
||||
"""Micro-trade take-profit uses 1.5x reward-risk ratio."""
|
||||
assert config.reward_risk_ratio == 1.5
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
active_pool=st.floats(
|
||||
min_value=100.0, max_value=100_000.0,
|
||||
allow_nan=False, allow_infinity=False,
|
||||
),
|
||||
)
|
||||
def test_allocation_cap_scales_with_active_pool(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
active_pool: float,
|
||||
) -> None:
|
||||
"""Allocation cap is proportional to active pool."""
|
||||
cap = self.module.compute_allocation_cap(config, active_pool)
|
||||
# Doubling active pool should double the cap
|
||||
cap_double = self.module.compute_allocation_cap(config, active_pool * 2)
|
||||
assert abs(cap_double - cap * 2) < 1e-4
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 32: Micro-trade auto-close after max hold duration
|
||||
# **Validates: Requirements 20.6**
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestProperty32MicroTradeAutoClose:
|
||||
"""Property 32: Micro-trade auto-close after max hold duration.
|
||||
|
||||
Verify positions closed when hold exceeds max duration.
|
||||
Verify positions not closed when hold is within duration.
|
||||
|
||||
**Validates: Requirements 20.6**
|
||||
"""
|
||||
|
||||
module = MicroTradingModule()
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
hold_minutes=st.floats(
|
||||
min_value=0.0, max_value=1000.0,
|
||||
allow_nan=False, allow_infinity=False,
|
||||
),
|
||||
)
|
||||
def test_auto_close_when_exceeds_max_hold(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
hold_minutes: float,
|
||||
) -> None:
|
||||
"""Position auto-closed when hold_minutes > max_hold_minutes."""
|
||||
result = self.module.should_auto_close(config, hold_minutes)
|
||||
|
||||
if hold_minutes > config.max_hold_minutes:
|
||||
assert result is True
|
||||
else:
|
||||
assert result is False
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
)
|
||||
def test_not_closed_at_exact_max(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
) -> None:
|
||||
"""Position is NOT auto-closed at exactly max_hold_minutes."""
|
||||
result = self.module.should_auto_close(config, float(config.max_hold_minutes))
|
||||
assert result is False
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
)
|
||||
def test_closed_just_past_max(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
) -> None:
|
||||
"""Position IS auto-closed just past max_hold_minutes."""
|
||||
result = self.module.should_auto_close(
|
||||
config, float(config.max_hold_minutes) + 0.01,
|
||||
)
|
||||
assert result is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 34: Micro-trades respect all existing constraints
|
||||
# **Validates: Requirements 20.10**
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestProperty34MicroTradesRespectConstraints:
|
||||
"""Property 34: Micro-trades respect all existing constraints.
|
||||
|
||||
Verify trading window, circuit breakers, portfolio heat constraints
|
||||
all enforced for micro-trades.
|
||||
|
||||
**Validates: Requirements 20.10**
|
||||
"""
|
||||
|
||||
module = MicroTradingModule()
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
daily_count=st.integers(min_value=0, max_value=5),
|
||||
)
|
||||
def test_circuit_breaker_blocks_micro_trade(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
daily_count: int,
|
||||
) -> None:
|
||||
"""Micro-trades blocked when circuit breaker is active."""
|
||||
allowed, reason = self.module.check_constraints(
|
||||
config=config,
|
||||
daily_count=daily_count,
|
||||
is_within_window=True,
|
||||
circuit_breaker_active=True,
|
||||
portfolio_heat_pct=0.05,
|
||||
max_heat=0.20,
|
||||
)
|
||||
assert allowed is False
|
||||
assert reason == "circuit_breaker_active"
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
daily_count=st.integers(min_value=0, max_value=5),
|
||||
)
|
||||
def test_outside_trading_window_blocks_micro_trade(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
daily_count: int,
|
||||
) -> None:
|
||||
"""Micro-trades blocked outside trading window."""
|
||||
allowed, reason = self.module.check_constraints(
|
||||
config=config,
|
||||
daily_count=daily_count,
|
||||
is_within_window=False,
|
||||
circuit_breaker_active=False,
|
||||
portfolio_heat_pct=0.05,
|
||||
max_heat=0.20,
|
||||
)
|
||||
assert allowed is False
|
||||
assert reason == "outside_trading_window"
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
heat_pct=st.floats(
|
||||
min_value=0.20, max_value=1.0,
|
||||
allow_nan=False, allow_infinity=False,
|
||||
),
|
||||
)
|
||||
def test_portfolio_heat_blocks_micro_trade(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
heat_pct: float,
|
||||
) -> None:
|
||||
"""Micro-trades blocked when portfolio heat exceeds max."""
|
||||
max_heat = 0.20
|
||||
allowed, reason = self.module.check_constraints(
|
||||
config=config,
|
||||
daily_count=0,
|
||||
is_within_window=True,
|
||||
circuit_breaker_active=False,
|
||||
portfolio_heat_pct=heat_pct,
|
||||
max_heat=max_heat,
|
||||
)
|
||||
assert allowed is False
|
||||
assert reason == "portfolio_heat_exceeded"
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
)
|
||||
def test_daily_limit_blocks_micro_trade(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
) -> None:
|
||||
"""Micro-trades blocked when daily limit reached."""
|
||||
allowed, reason = self.module.check_constraints(
|
||||
config=config,
|
||||
daily_count=config.max_daily,
|
||||
is_within_window=True,
|
||||
circuit_breaker_active=False,
|
||||
portfolio_heat_pct=0.05,
|
||||
max_heat=0.20,
|
||||
)
|
||||
assert allowed is False
|
||||
assert reason == "daily_limit_reached"
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
daily_count=st.integers(min_value=0, max_value=5),
|
||||
)
|
||||
def test_disabled_blocks_micro_trade(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
daily_count: int,
|
||||
) -> None:
|
||||
"""Micro-trades blocked when module is disabled."""
|
||||
disabled_config = MicroTradeConfig(
|
||||
enabled=False,
|
||||
allocation_cap_pct=config.allocation_cap_pct,
|
||||
max_daily=config.max_daily,
|
||||
max_hold_minutes=config.max_hold_minutes,
|
||||
)
|
||||
allowed, reason = self.module.check_constraints(
|
||||
config=disabled_config,
|
||||
daily_count=daily_count,
|
||||
is_within_window=True,
|
||||
circuit_breaker_active=False,
|
||||
portfolio_heat_pct=0.05,
|
||||
max_heat=0.20,
|
||||
)
|
||||
assert allowed is False
|
||||
assert reason == "micro_trading_disabled"
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
config=_micro_config_strategy(),
|
||||
daily_count=st.integers(min_value=0, max_value=5),
|
||||
heat_pct=st.floats(
|
||||
min_value=0.0, max_value=0.15,
|
||||
allow_nan=False, allow_infinity=False,
|
||||
),
|
||||
)
|
||||
def test_all_constraints_pass_allows_trade(
|
||||
self,
|
||||
config: MicroTradeConfig,
|
||||
daily_count: int,
|
||||
heat_pct: float,
|
||||
) -> None:
|
||||
"""Micro-trade allowed when all constraints pass."""
|
||||
# Ensure daily_count is under the limit
|
||||
safe_count = min(daily_count, config.max_daily - 1)
|
||||
allowed, reason = self.module.check_constraints(
|
||||
config=config,
|
||||
daily_count=safe_count,
|
||||
is_within_window=True,
|
||||
circuit_breaker_active=False,
|
||||
portfolio_heat_pct=heat_pct,
|
||||
max_heat=0.20,
|
||||
)
|
||||
assert allowed is True
|
||||
assert reason == "ok"
|
||||
Reference in New Issue
Block a user