4ffde8cc06
- 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
170 lines
5.4 KiB
Python
170 lines
5.4 KiB
Python
"""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
|