Files
stonks-oracle/tests/test_pbt_micro_trading.py
Celes Renata c85c0068a2 fix: clean up utcnow deprecation warnings, fix 12 failing tests, add CI/CD pipeline manifests
- 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
2026-04-18 03:59:28 +00:00

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"