c85c0068a2
- Replace all datetime.utcnow() with datetime.now(tz=timezone.utc) across 8 files - Fix 12 failing tests to match current implementation behavior - Fix pytest_plugins in non-top-level conftest (moved to root conftest.py) - Auto-fix 189 lint issues (import sorting, unused imports) - Add CI/CD pipeline infrastructure (ARC, ArgoCD, Kargo manifests) - Add values-beta.yaml and values-paper.yaml for staged deployments - Update GitHub Actions workflow to use self-hosted-gremlin runners - Add integration-test job to CI pipeline Result: 1596 passed, 0 failed, 0 warnings
361 lines
11 KiB
Python
361 lines
11 KiB
Python
"""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"
|