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
164 lines
5.3 KiB
Python
164 lines
5.3 KiB
Python
"""Notification service for the autonomous trading engine.
|
|
|
|
Pure computation module for notification logic. Actual delivery (AWS SNS,
|
|
Gmail API) is deferred to integration code — this module handles formatting,
|
|
rate-limit decisions, and record creation.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
|
|
from services.trading.models import PerformanceMetrics
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Supported event types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SUPPORTED_EVENT_TYPES: frozenset[str] = frozenset(
|
|
{
|
|
"circuit_breaker_triggered",
|
|
"circuit_breaker_resumed",
|
|
"risk_tier_changed",
|
|
"emergency_liquidation",
|
|
"large_trade_pnl",
|
|
"daily_summary",
|
|
"weekly_digest",
|
|
}
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class NotificationRecord:
|
|
"""A single notification record for audit persistence."""
|
|
|
|
channel: str
|
|
event_type: str
|
|
message: str
|
|
delivery_status: str = "pending"
|
|
retry_count: int = 0
|
|
created_at: datetime = field(default_factory=datetime.utcnow)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Notification service (pure computation)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class NotificationService:
|
|
"""Pure-computation notification service.
|
|
|
|
Handles formatting, rate-limit checking, and record creation.
|
|
Actual delivery is the caller's responsibility.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
sms_enabled: bool = False,
|
|
email_enabled: bool = False,
|
|
rate_limit_sms_per_hour: int = 10,
|
|
rate_limit_email_per_hour: int = 20,
|
|
) -> None:
|
|
self.sms_enabled = sms_enabled
|
|
self.email_enabled = email_enabled
|
|
self.rate_limit_sms_per_hour = rate_limit_sms_per_hour
|
|
self.rate_limit_email_per_hour = rate_limit_email_per_hour
|
|
|
|
# ------------------------------------------------------------------
|
|
# Rate limiting
|
|
# ------------------------------------------------------------------
|
|
|
|
def should_send(self, channel: str, current_hour_count: int) -> bool:
|
|
"""Check whether a notification on *channel* is within the rate limit.
|
|
|
|
Args:
|
|
channel: ``"sms"`` or ``"email"``.
|
|
current_hour_count: Number of notifications already sent on this
|
|
channel during the current hour window.
|
|
|
|
Returns:
|
|
``True`` if the notification may be sent, ``False`` if it should
|
|
be rate-limited.
|
|
"""
|
|
if channel == "sms":
|
|
if not self.sms_enabled:
|
|
return False
|
|
return current_hour_count < self.rate_limit_sms_per_hour
|
|
elif channel == "email":
|
|
if not self.email_enabled:
|
|
return False
|
|
return current_hour_count < self.rate_limit_email_per_hour
|
|
return False
|
|
|
|
# ------------------------------------------------------------------
|
|
# Formatting helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def format_daily_summary(self, metrics: PerformanceMetrics) -> str:
|
|
"""Format a daily performance summary message.
|
|
|
|
Args:
|
|
metrics: Current performance metrics snapshot.
|
|
|
|
Returns:
|
|
Human-readable summary string.
|
|
"""
|
|
total_trades = metrics.win_count + metrics.loss_count
|
|
return (
|
|
f"Daily Summary — "
|
|
f"P&L: ${metrics.daily_pnl:+.2f} | "
|
|
f"Portfolio: ${metrics.total_portfolio_value:,.2f} | "
|
|
f"Active: ${metrics.active_pool:,.2f} | "
|
|
f"Reserve: ${metrics.reserve_pool:,.2f} | "
|
|
f"Trades: {total_trades} | "
|
|
f"Win Rate: {metrics.win_rate:.0%} | "
|
|
f"Heat: {metrics.portfolio_heat:.2%}"
|
|
)
|
|
|
|
def format_alert(self, event_type: str, details: str) -> str:
|
|
"""Format an alert message for a specific event type.
|
|
|
|
Args:
|
|
event_type: One of the supported event types.
|
|
details: Free-form detail string.
|
|
|
|
Returns:
|
|
Formatted alert string.
|
|
"""
|
|
label = event_type.replace("_", " ").title()
|
|
return f"[Stonks Alert] {label}: {details}"
|
|
|
|
# ------------------------------------------------------------------
|
|
# Record creation
|
|
# ------------------------------------------------------------------
|
|
|
|
def create_notification(
|
|
self,
|
|
channel: str,
|
|
event_type: str,
|
|
message: str,
|
|
) -> NotificationRecord:
|
|
"""Create a ``NotificationRecord`` ready for persistence.
|
|
|
|
Args:
|
|
channel: ``"sms"`` or ``"email"``.
|
|
event_type: One of the supported event types.
|
|
message: The formatted message body.
|
|
|
|
Returns:
|
|
A new ``NotificationRecord`` with ``delivery_status="pending"``.
|
|
"""
|
|
return NotificationRecord(
|
|
channel=channel,
|
|
event_type=event_type,
|
|
message=message,
|
|
delivery_status="pending",
|
|
retry_count=0,
|
|
)
|