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:
Celes Renata
2026-04-15 16:12:22 +00:00
parent da86132f0c
commit 4ffde8cc06
58 changed files with 14168 additions and 1 deletions
+169
View File
@@ -0,0 +1,169 @@
"""Property-based tests for the Notification Service.
Feature: autonomous-trading-engine
Property 30: Notification rate limiting.
"""
from __future__ import annotations
from hypothesis import given, settings
from hypothesis import strategies as st
from services.trading.notifications import NotificationService
# ---------------------------------------------------------------------------
# Property 30: Notification rate limiting
# **Validates: Requirements 19.7**
# ---------------------------------------------------------------------------
class TestProperty30NotificationRateLimiting:
"""Property 30: Notification rate limiting.
Generate random sequences of notification requests within a one-hour
window. Verify at most 10 SMS and 20 emails allowed per hour.
Verify excess notifications blocked (should_send returns False).
**Validates: Requirements 19.7**
"""
@settings(max_examples=100)
@given(
sms_limit=st.integers(min_value=1, max_value=50),
email_limit=st.integers(min_value=1, max_value=50),
sms_requests=st.integers(min_value=0, max_value=100),
email_requests=st.integers(min_value=0, max_value=100),
)
def test_sms_rate_limit_enforced(
self,
sms_limit: int,
email_limit: int,
sms_requests: int,
email_requests: int,
) -> None:
"""At most sms_limit SMS notifications are allowed per hour."""
svc = NotificationService(
sms_enabled=True,
email_enabled=True,
rate_limit_sms_per_hour=sms_limit,
rate_limit_email_per_hour=email_limit,
)
sent_sms = 0
for i in range(sms_requests):
if svc.should_send("sms", current_hour_count=i):
sent_sms += 1
assert sent_sms <= sms_limit
@settings(max_examples=100)
@given(
sms_limit=st.integers(min_value=1, max_value=50),
email_limit=st.integers(min_value=1, max_value=50),
sms_requests=st.integers(min_value=0, max_value=100),
email_requests=st.integers(min_value=0, max_value=100),
)
def test_email_rate_limit_enforced(
self,
sms_limit: int,
email_limit: int,
sms_requests: int,
email_requests: int,
) -> None:
"""At most email_limit email notifications are allowed per hour."""
svc = NotificationService(
sms_enabled=True,
email_enabled=True,
rate_limit_sms_per_hour=sms_limit,
rate_limit_email_per_hour=email_limit,
)
sent_email = 0
for i in range(email_requests):
if svc.should_send("email", current_hour_count=i):
sent_email += 1
assert sent_email <= email_limit
@settings(max_examples=100)
@given(
sms_limit=st.integers(min_value=1, max_value=50),
email_limit=st.integers(min_value=1, max_value=50),
)
def test_excess_sms_blocked(
self,
sms_limit: int,
email_limit: int,
) -> None:
"""Notifications beyond the limit are blocked."""
svc = NotificationService(
sms_enabled=True,
email_enabled=True,
rate_limit_sms_per_hour=sms_limit,
rate_limit_email_per_hour=email_limit,
)
# At the limit, should_send returns False
assert svc.should_send("sms", current_hour_count=sms_limit) is False
# One past the limit, still False
assert svc.should_send("sms", current_hour_count=sms_limit + 1) is False
@settings(max_examples=100)
@given(
sms_limit=st.integers(min_value=1, max_value=50),
email_limit=st.integers(min_value=1, max_value=50),
)
def test_excess_email_blocked(
self,
sms_limit: int,
email_limit: int,
) -> None:
"""Email notifications beyond the limit are blocked."""
svc = NotificationService(
sms_enabled=True,
email_enabled=True,
rate_limit_sms_per_hour=sms_limit,
rate_limit_email_per_hour=email_limit,
)
assert svc.should_send("email", current_hour_count=email_limit) is False
assert svc.should_send("email", current_hour_count=email_limit + 1) is False
@settings(max_examples=100)
@given(
count=st.integers(min_value=0, max_value=100),
)
def test_default_limits_10_sms_20_email(
self,
count: int,
) -> None:
"""Default limits are 10 SMS and 20 emails per hour."""
svc = NotificationService(sms_enabled=True, email_enabled=True)
sms_allowed = svc.should_send("sms", current_hour_count=count)
email_allowed = svc.should_send("email", current_hour_count=count)
if count < 10:
assert sms_allowed is True
else:
assert sms_allowed is False
if count < 20:
assert email_allowed is True
else:
assert email_allowed is False
@settings(max_examples=100)
@given(
count=st.integers(min_value=0, max_value=50),
)
def test_disabled_channel_always_blocked(
self,
count: int,
) -> None:
"""Disabled channels always return False regardless of count."""
svc = NotificationService(sms_enabled=False, email_enabled=False)
assert svc.should_send("sms", current_hour_count=count) is False
assert svc.should_send("email", current_hour_count=count) is False