feat(alerts): add Alert Manager for orchestration and routing - Issue #38 (55 tests)
Implements comprehensive alert management framework: - AlertPriority, AlertCategory, AlertStatus, ChannelType enums - AlertTemplate, RateLimitConfig, RoutingRule dataclasses - Alert, DeliveryResult, AlertStats tracking classes - LogChannel and WebhookChannel implementations - AlertManager main class Features: - Multi-channel alert routing (log, webhook, slack, sms, email, push) - Configurable routing rules based on priority and category - Rate limiting to prevent alert storms - Duplicate detection and suppression - Alert history with filtering and search - Alert acknowledgement tracking - Template-based alert formatting - Synchronous and asynchronous delivery - Statistics and metrics tracking - Convenience methods for trade/risk/execution alerts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ddb12c13fe
commit
7ab60eb321
|
|
@ -0,0 +1 @@
|
|||
# Tests for alerts module
|
||||
|
|
@ -0,0 +1,757 @@
|
|||
"""Tests for Alert Manager.
|
||||
|
||||
Issue #38: [ALERT-37] Alert manager - orchestration and routing
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from tradingagents.alerts.alert_manager import (
|
||||
# Enums
|
||||
AlertPriority,
|
||||
AlertCategory,
|
||||
AlertStatus,
|
||||
ChannelType,
|
||||
# Data Classes
|
||||
AlertTemplate,
|
||||
RateLimitConfig,
|
||||
RoutingRule,
|
||||
AlertConfig,
|
||||
Alert,
|
||||
DeliveryResult,
|
||||
AlertStats,
|
||||
# Channel Classes
|
||||
LogChannel,
|
||||
WebhookChannel,
|
||||
# Main Class
|
||||
AlertManager,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Enum Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAlertPriority:
|
||||
"""Tests for AlertPriority enum."""
|
||||
|
||||
def test_all_priorities_defined(self):
|
||||
"""Verify all priorities exist."""
|
||||
assert AlertPriority.LOW
|
||||
assert AlertPriority.MEDIUM
|
||||
assert AlertPriority.HIGH
|
||||
assert AlertPriority.CRITICAL
|
||||
|
||||
def test_priority_values(self):
|
||||
"""Verify priority values."""
|
||||
assert AlertPriority.LOW.value == "low"
|
||||
assert AlertPriority.CRITICAL.value == "critical"
|
||||
|
||||
|
||||
class TestAlertCategory:
|
||||
"""Tests for AlertCategory enum."""
|
||||
|
||||
def test_all_categories_defined(self):
|
||||
"""Verify all categories exist."""
|
||||
assert AlertCategory.TRADE
|
||||
assert AlertCategory.RISK
|
||||
assert AlertCategory.SYSTEM
|
||||
assert AlertCategory.MARKET
|
||||
assert AlertCategory.PORTFOLIO
|
||||
assert AlertCategory.EXECUTION
|
||||
assert AlertCategory.COMPLIANCE
|
||||
|
||||
|
||||
class TestAlertStatus:
|
||||
"""Tests for AlertStatus enum."""
|
||||
|
||||
def test_all_statuses_defined(self):
|
||||
"""Verify all statuses exist."""
|
||||
assert AlertStatus.PENDING
|
||||
assert AlertStatus.SENDING
|
||||
assert AlertStatus.DELIVERED
|
||||
assert AlertStatus.FAILED
|
||||
assert AlertStatus.RATE_LIMITED
|
||||
assert AlertStatus.SUPPRESSED
|
||||
|
||||
|
||||
class TestChannelType:
|
||||
"""Tests for ChannelType enum."""
|
||||
|
||||
def test_all_channels_defined(self):
|
||||
"""Verify all channels exist."""
|
||||
assert ChannelType.EMAIL
|
||||
assert ChannelType.SLACK
|
||||
assert ChannelType.SMS
|
||||
assert ChannelType.WEBHOOK
|
||||
assert ChannelType.PUSH
|
||||
assert ChannelType.LOG
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Data Class Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAlertTemplate:
|
||||
"""Tests for AlertTemplate dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating template with defaults."""
|
||||
template = AlertTemplate()
|
||||
assert template.template_id is not None
|
||||
assert template.title_template == "{category}: {title}"
|
||||
|
||||
def test_render_template(self):
|
||||
"""Test rendering template."""
|
||||
template = AlertTemplate(
|
||||
title_template="[{category}] {title}",
|
||||
body_template="Message: {message}",
|
||||
)
|
||||
title, body = template.render({
|
||||
"category": "TRADE",
|
||||
"title": "Buy Signal",
|
||||
"message": "AAPL buy detected",
|
||||
})
|
||||
assert title == "[TRADE] Buy Signal"
|
||||
assert body == "Message: AAPL buy detected"
|
||||
|
||||
|
||||
class TestRateLimitConfig:
|
||||
"""Tests for RateLimitConfig dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating config with defaults."""
|
||||
config = RateLimitConfig()
|
||||
assert config.max_alerts_per_minute == 10
|
||||
assert config.max_alerts_per_hour == 100
|
||||
assert config.enable_deduplication is True
|
||||
|
||||
|
||||
class TestRoutingRule:
|
||||
"""Tests for RoutingRule dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating rule with defaults."""
|
||||
rule = RoutingRule()
|
||||
assert rule.rule_id is not None
|
||||
assert rule.enabled is True
|
||||
assert rule.priority == AlertPriority.LOW
|
||||
|
||||
def test_matches_priority(self):
|
||||
"""Test priority matching."""
|
||||
rule = RoutingRule(priority=AlertPriority.HIGH)
|
||||
|
||||
low_alert = Alert(priority=AlertPriority.LOW)
|
||||
high_alert = Alert(priority=AlertPriority.HIGH)
|
||||
critical_alert = Alert(priority=AlertPriority.CRITICAL)
|
||||
|
||||
assert not rule.matches(low_alert)
|
||||
assert rule.matches(high_alert)
|
||||
assert rule.matches(critical_alert)
|
||||
|
||||
def test_matches_category(self):
|
||||
"""Test category matching."""
|
||||
rule = RoutingRule(
|
||||
categories=[AlertCategory.TRADE, AlertCategory.RISK],
|
||||
)
|
||||
|
||||
trade_alert = Alert(category=AlertCategory.TRADE)
|
||||
system_alert = Alert(category=AlertCategory.SYSTEM)
|
||||
|
||||
assert rule.matches(trade_alert)
|
||||
assert not rule.matches(system_alert)
|
||||
|
||||
def test_disabled_rule(self):
|
||||
"""Test disabled rule never matches."""
|
||||
rule = RoutingRule(enabled=False)
|
||||
alert = Alert()
|
||||
assert not rule.matches(alert)
|
||||
|
||||
|
||||
class TestAlertConfig:
|
||||
"""Tests for AlertConfig dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating config with defaults."""
|
||||
config = AlertConfig()
|
||||
assert config.log_all_alerts is True
|
||||
assert config.store_history is True
|
||||
assert config.max_history_size == 1000
|
||||
|
||||
|
||||
class TestAlert:
|
||||
"""Tests for Alert dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating alert with defaults."""
|
||||
alert = Alert()
|
||||
assert alert.alert_id is not None
|
||||
assert alert.status == AlertStatus.PENDING
|
||||
assert alert.timestamp is not None
|
||||
|
||||
def test_content_hash(self):
|
||||
"""Test content hash generation."""
|
||||
alert1 = Alert(
|
||||
title="Test",
|
||||
message="Message",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
alert2 = Alert(
|
||||
title="Test",
|
||||
message="Message",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
alert3 = Alert(
|
||||
title="Different",
|
||||
message="Message",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
|
||||
assert alert1.content_hash == alert2.content_hash
|
||||
assert alert1.content_hash != alert3.content_hash
|
||||
|
||||
|
||||
class TestDeliveryResult:
|
||||
"""Tests for DeliveryResult dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating result with defaults."""
|
||||
result = DeliveryResult()
|
||||
assert result.success is False
|
||||
assert result.timestamp is not None
|
||||
|
||||
|
||||
class TestAlertStats:
|
||||
"""Tests for AlertStats dataclass."""
|
||||
|
||||
def test_default_creation(self):
|
||||
"""Test creating stats with defaults."""
|
||||
stats = AlertStats()
|
||||
assert stats.total_sent == 0
|
||||
assert stats.total_failed == 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Channel Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestLogChannel:
|
||||
"""Tests for LogChannel."""
|
||||
|
||||
def test_channel_type(self):
|
||||
"""Test channel type."""
|
||||
channel = LogChannel()
|
||||
assert channel.channel_type == ChannelType.LOG
|
||||
|
||||
def test_is_available(self):
|
||||
"""Test availability."""
|
||||
channel = LogChannel()
|
||||
assert channel.is_available is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send(self):
|
||||
"""Test sending alert."""
|
||||
channel = LogChannel()
|
||||
alert = Alert(title="Test", message="Test message")
|
||||
result = await channel.send(alert)
|
||||
assert result is True
|
||||
|
||||
|
||||
class TestWebhookChannel:
|
||||
"""Tests for WebhookChannel."""
|
||||
|
||||
def test_channel_type(self):
|
||||
"""Test channel type."""
|
||||
channel = WebhookChannel("https://example.com/webhook")
|
||||
assert channel.channel_type == ChannelType.WEBHOOK
|
||||
|
||||
def test_availability_with_url(self):
|
||||
"""Test availability with URL."""
|
||||
channel = WebhookChannel("https://example.com/webhook")
|
||||
assert channel.is_available is True
|
||||
|
||||
def test_availability_without_url(self):
|
||||
"""Test availability without URL."""
|
||||
channel = WebhookChannel("")
|
||||
assert channel.is_available is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_with_url(self):
|
||||
"""Test sending with URL."""
|
||||
channel = WebhookChannel("https://example.com/webhook")
|
||||
alert = Alert(title="Test", message="Test message")
|
||||
result = await channel.send(alert)
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_without_url(self):
|
||||
"""Test sending without URL fails."""
|
||||
channel = WebhookChannel("")
|
||||
alert = Alert(title="Test", message="Test message")
|
||||
result = await channel.send(alert)
|
||||
assert result is False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# AlertManager Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAlertManager:
|
||||
"""Tests for AlertManager class."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Create default manager."""
|
||||
return AlertManager()
|
||||
|
||||
def test_initialization(self, manager):
|
||||
"""Test manager initialization."""
|
||||
assert manager.config is not None
|
||||
assert ChannelType.LOG in manager.channels
|
||||
assert len(manager.routing_rules) > 0
|
||||
|
||||
def test_register_channel(self, manager):
|
||||
"""Test registering a channel."""
|
||||
webhook = WebhookChannel("https://example.com")
|
||||
manager.register_channel(webhook)
|
||||
assert ChannelType.WEBHOOK in manager.channels
|
||||
|
||||
def test_unregister_channel(self, manager):
|
||||
"""Test unregistering a channel."""
|
||||
manager.unregister_channel(ChannelType.LOG)
|
||||
assert ChannelType.LOG not in manager.channels
|
||||
|
||||
def test_add_routing_rule(self, manager):
|
||||
"""Test adding routing rule."""
|
||||
initial_count = len(manager.routing_rules)
|
||||
rule = RoutingRule(name="test_rule")
|
||||
manager.add_routing_rule(rule)
|
||||
assert len(manager.routing_rules) == initial_count + 1
|
||||
|
||||
def test_remove_routing_rule(self, manager):
|
||||
"""Test removing routing rule."""
|
||||
rule = RoutingRule(name="test_rule")
|
||||
manager.add_routing_rule(rule)
|
||||
result = manager.remove_routing_rule(rule.rule_id)
|
||||
assert result is True
|
||||
|
||||
def test_remove_nonexistent_rule(self, manager):
|
||||
"""Test removing nonexistent rule."""
|
||||
result = manager.remove_routing_rule("nonexistent")
|
||||
assert result is False
|
||||
|
||||
def test_register_template(self, manager):
|
||||
"""Test registering template."""
|
||||
template = AlertTemplate(name="custom_template")
|
||||
manager.register_template(template)
|
||||
assert "custom_template" in manager.templates
|
||||
|
||||
def test_create_alert(self, manager):
|
||||
"""Test creating alert."""
|
||||
alert = manager.create_alert(
|
||||
title="Test Alert",
|
||||
message="This is a test",
|
||||
priority=AlertPriority.HIGH,
|
||||
category=AlertCategory.RISK,
|
||||
)
|
||||
assert alert.title == "Test Alert"
|
||||
assert alert.priority == AlertPriority.HIGH
|
||||
assert alert.category == AlertCategory.RISK
|
||||
|
||||
def test_create_alert_from_template(self, manager):
|
||||
"""Test creating alert from template."""
|
||||
alert = manager.create_alert_from_template(
|
||||
"trade_signal",
|
||||
{
|
||||
"symbol": "AAPL",
|
||||
"action": "BUY",
|
||||
"price": "150.00",
|
||||
"reason": "Momentum signal",
|
||||
},
|
||||
)
|
||||
assert alert is not None
|
||||
assert "AAPL" in alert.title
|
||||
assert "BUY" in alert.title
|
||||
|
||||
def test_create_alert_from_nonexistent_template(self, manager):
|
||||
"""Test creating alert from nonexistent template."""
|
||||
alert = manager.create_alert_from_template(
|
||||
"nonexistent",
|
||||
{},
|
||||
)
|
||||
assert alert is None
|
||||
|
||||
def test_send_alert(self, manager):
|
||||
"""Test sending alert."""
|
||||
alert = manager.create_alert(
|
||||
title="Test",
|
||||
message="Test message",
|
||||
)
|
||||
results = manager.send(alert)
|
||||
assert len(results) > 0
|
||||
assert alert.status == AlertStatus.DELIVERED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_alert_async(self, manager):
|
||||
"""Test sending alert asynchronously."""
|
||||
alert = manager.create_alert(
|
||||
title="Test",
|
||||
message="Test message",
|
||||
)
|
||||
results = await manager.send_async(alert)
|
||||
assert len(results) > 0
|
||||
assert alert.status == AlertStatus.DELIVERED
|
||||
|
||||
|
||||
class TestRateLimiting:
|
||||
"""Tests for rate limiting."""
|
||||
|
||||
@pytest.fixture
|
||||
def limited_manager(self):
|
||||
"""Create manager with tight rate limits."""
|
||||
config = AlertConfig(
|
||||
rate_limit_config=RateLimitConfig(
|
||||
max_alerts_per_minute=2,
|
||||
max_alerts_per_hour=5,
|
||||
),
|
||||
)
|
||||
return AlertManager(config=config)
|
||||
|
||||
def test_rate_limit_per_minute(self, limited_manager):
|
||||
"""Test rate limit per minute."""
|
||||
# Send 2 alerts (should succeed) - each with unique message to avoid dedup
|
||||
for i in range(2):
|
||||
alert = limited_manager.create_alert(
|
||||
title=f"Test {i}",
|
||||
message=f"Test message {i}",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
limited_manager.send(alert)
|
||||
|
||||
# 3rd alert should be rate limited
|
||||
alert = limited_manager.create_alert(
|
||||
title="Test 3",
|
||||
message="Test message 3",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
limited_manager.send(alert)
|
||||
assert alert.status == AlertStatus.RATE_LIMITED
|
||||
|
||||
|
||||
class TestDeduplication:
|
||||
"""Tests for deduplication."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Create manager with deduplication."""
|
||||
config = AlertConfig(
|
||||
rate_limit_config=RateLimitConfig(
|
||||
enable_deduplication=True,
|
||||
dedupe_window_seconds=60,
|
||||
),
|
||||
)
|
||||
return AlertManager(config=config)
|
||||
|
||||
def test_duplicate_suppressed(self, manager):
|
||||
"""Test duplicate alerts are suppressed."""
|
||||
# First alert should succeed
|
||||
alert1 = manager.create_alert(
|
||||
title="Same Title",
|
||||
message="Same Message",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
manager.send(alert1)
|
||||
assert alert1.status == AlertStatus.DELIVERED
|
||||
|
||||
# Duplicate should be suppressed
|
||||
alert2 = manager.create_alert(
|
||||
title="Same Title",
|
||||
message="Same Message",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
manager.send(alert2)
|
||||
assert alert2.status == AlertStatus.SUPPRESSED
|
||||
|
||||
def test_different_not_suppressed(self, manager):
|
||||
"""Test different alerts are not suppressed."""
|
||||
alert1 = manager.create_alert(
|
||||
title="Title 1",
|
||||
message="Message 1",
|
||||
)
|
||||
manager.send(alert1)
|
||||
|
||||
alert2 = manager.create_alert(
|
||||
title="Title 2",
|
||||
message="Message 2",
|
||||
)
|
||||
manager.send(alert2)
|
||||
assert alert2.status == AlertStatus.DELIVERED
|
||||
|
||||
|
||||
class TestHistory:
|
||||
"""Tests for alert history."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Create manager with history."""
|
||||
return AlertManager()
|
||||
|
||||
def test_history_stored(self, manager):
|
||||
"""Test alerts are stored in history."""
|
||||
alert = manager.create_alert(title="Test", message="Test")
|
||||
manager.send(alert)
|
||||
|
||||
history = manager.get_history()
|
||||
assert len(history) > 0
|
||||
assert history[0].title == "Test"
|
||||
|
||||
def test_history_filter_category(self, manager):
|
||||
"""Test filtering history by category."""
|
||||
alert1 = manager.create_alert(
|
||||
title="Trade",
|
||||
message="Trade alert",
|
||||
category=AlertCategory.TRADE,
|
||||
)
|
||||
alert2 = manager.create_alert(
|
||||
title="Risk",
|
||||
message="Risk alert",
|
||||
category=AlertCategory.RISK,
|
||||
)
|
||||
manager.send(alert1)
|
||||
manager.send(alert2)
|
||||
|
||||
trade_history = manager.get_history(category=AlertCategory.TRADE)
|
||||
assert len(trade_history) == 1
|
||||
assert trade_history[0].category == AlertCategory.TRADE
|
||||
|
||||
def test_history_filter_priority(self, manager):
|
||||
"""Test filtering history by priority."""
|
||||
alert1 = manager.create_alert(
|
||||
title="Low",
|
||||
message="Low priority",
|
||||
priority=AlertPriority.LOW,
|
||||
)
|
||||
alert2 = manager.create_alert(
|
||||
title="High",
|
||||
message="High priority",
|
||||
priority=AlertPriority.HIGH,
|
||||
)
|
||||
manager.send(alert1)
|
||||
manager.send(alert2)
|
||||
|
||||
high_history = manager.get_history(priority=AlertPriority.HIGH)
|
||||
assert len(high_history) == 1
|
||||
assert high_history[0].priority == AlertPriority.HIGH
|
||||
|
||||
def test_clear_history(self, manager):
|
||||
"""Test clearing history."""
|
||||
alert = manager.create_alert(title="Test", message="Test")
|
||||
manager.send(alert)
|
||||
|
||||
count = manager.clear_history()
|
||||
assert count > 0
|
||||
assert len(manager.get_history()) == 0
|
||||
|
||||
|
||||
class TestAcknowledgement:
|
||||
"""Tests for alert acknowledgement."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Create manager."""
|
||||
return AlertManager()
|
||||
|
||||
def test_acknowledge_alert(self, manager):
|
||||
"""Test acknowledging alert."""
|
||||
alert = manager.create_alert(title="Test", message="Test")
|
||||
manager.send(alert)
|
||||
|
||||
result = manager.acknowledge_alert(alert.alert_id, "user@example.com")
|
||||
assert result is True
|
||||
|
||||
history = manager.get_history()
|
||||
assert history[0].acknowledged is True
|
||||
assert history[0].acknowledged_by == "user@example.com"
|
||||
|
||||
def test_acknowledge_nonexistent(self, manager):
|
||||
"""Test acknowledging nonexistent alert."""
|
||||
result = manager.acknowledge_alert("nonexistent", "user@example.com")
|
||||
assert result is False
|
||||
|
||||
def test_get_unacknowledged(self, manager):
|
||||
"""Test getting unacknowledged alerts."""
|
||||
alert1 = manager.create_alert(title="Test1", message="Test1")
|
||||
alert2 = manager.create_alert(title="Test2", message="Test2")
|
||||
manager.send(alert1)
|
||||
manager.send(alert2)
|
||||
|
||||
manager.acknowledge_alert(alert1.alert_id, "user")
|
||||
|
||||
unacked = manager.get_unacknowledged()
|
||||
assert len(unacked) == 1
|
||||
assert unacked[0].alert_id == alert2.alert_id
|
||||
|
||||
|
||||
class TestStats:
|
||||
"""Tests for statistics."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Create manager."""
|
||||
return AlertManager()
|
||||
|
||||
def test_stats_updated(self, manager):
|
||||
"""Test stats are updated."""
|
||||
alert = manager.create_alert(title="Test", message="Test")
|
||||
manager.send(alert)
|
||||
|
||||
stats = manager.get_stats()
|
||||
assert stats.total_sent > 0
|
||||
|
||||
def test_reset_stats(self, manager):
|
||||
"""Test resetting stats."""
|
||||
alert = manager.create_alert(title="Test", message="Test")
|
||||
manager.send(alert)
|
||||
|
||||
manager.reset_stats()
|
||||
stats = manager.get_stats()
|
||||
assert stats.total_sent == 0
|
||||
|
||||
|
||||
class TestConvenienceMethods:
|
||||
"""Tests for convenience methods."""
|
||||
|
||||
@pytest.fixture
|
||||
def manager(self):
|
||||
"""Create manager."""
|
||||
return AlertManager()
|
||||
|
||||
def test_alert_trade(self, manager):
|
||||
"""Test trade alert convenience method."""
|
||||
alert = manager.alert_trade(
|
||||
symbol="AAPL",
|
||||
action="BUY",
|
||||
price=Decimal("150.00"),
|
||||
reason="Momentum",
|
||||
)
|
||||
assert alert is not None
|
||||
assert alert.category == AlertCategory.TRADE
|
||||
|
||||
def test_alert_risk(self, manager):
|
||||
"""Test risk alert convenience method."""
|
||||
alert = manager.alert_risk(
|
||||
risk_type="DrawdownLimit",
|
||||
current_value="15%",
|
||||
limit_value="10%",
|
||||
)
|
||||
assert alert is not None
|
||||
assert alert.category == AlertCategory.RISK
|
||||
|
||||
def test_alert_execution(self, manager):
|
||||
"""Test execution alert convenience method."""
|
||||
alert = manager.alert_execution(
|
||||
order_id="order-123",
|
||||
symbol="AAPL",
|
||||
status="FILLED",
|
||||
quantity=Decimal("100"),
|
||||
price=Decimal("150.00"),
|
||||
)
|
||||
assert alert is not None
|
||||
assert alert.category == AlertCategory.EXECUTION
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Integration Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAlertManagerIntegration:
|
||||
"""Integration tests for alert manager."""
|
||||
|
||||
def test_full_workflow(self):
|
||||
"""Test complete alert workflow."""
|
||||
# Setup manager
|
||||
manager = AlertManager()
|
||||
|
||||
# Add custom channel
|
||||
webhook = WebhookChannel("https://example.com/webhook")
|
||||
manager.register_channel(webhook)
|
||||
|
||||
# Add custom routing rule
|
||||
manager.add_routing_rule(RoutingRule(
|
||||
name="critical_webhook",
|
||||
priority=AlertPriority.CRITICAL,
|
||||
channels=[ChannelType.WEBHOOK],
|
||||
))
|
||||
|
||||
# Send alerts of different priorities
|
||||
low_alert = manager.create_alert(
|
||||
title="Info",
|
||||
message="Informational message",
|
||||
priority=AlertPriority.LOW,
|
||||
)
|
||||
manager.send(low_alert)
|
||||
|
||||
critical_alert = manager.create_alert(
|
||||
title="Critical Issue",
|
||||
message="Immediate attention required",
|
||||
priority=AlertPriority.CRITICAL,
|
||||
)
|
||||
manager.send(critical_alert)
|
||||
|
||||
# Verify stats
|
||||
stats = manager.get_stats()
|
||||
assert stats.total_sent == 2
|
||||
assert stats.by_priority.get("low", 0) >= 1
|
||||
assert stats.by_priority.get("critical", 0) >= 1
|
||||
|
||||
def test_module_imports(self):
|
||||
"""Test that all classes are exported from module."""
|
||||
from tradingagents.alerts import (
|
||||
AlertPriority,
|
||||
AlertCategory,
|
||||
AlertStatus,
|
||||
ChannelType,
|
||||
AlertTemplate,
|
||||
RateLimitConfig,
|
||||
RoutingRule,
|
||||
AlertConfig,
|
||||
Alert,
|
||||
DeliveryResult,
|
||||
AlertStats,
|
||||
LogChannel,
|
||||
WebhookChannel,
|
||||
AlertManager,
|
||||
)
|
||||
|
||||
# All imports successful
|
||||
assert AlertPriority.CRITICAL is not None
|
||||
assert AlertManager is not None
|
||||
|
||||
def test_multi_channel_delivery(self):
|
||||
"""Test delivery to multiple channels."""
|
||||
manager = AlertManager()
|
||||
|
||||
# Register webhook channel
|
||||
webhook = WebhookChannel("https://example.com/webhook")
|
||||
manager.register_channel(webhook)
|
||||
|
||||
# Add rule for multi-channel delivery
|
||||
manager.add_routing_rule(RoutingRule(
|
||||
name="all_channels",
|
||||
priority=AlertPriority.HIGH,
|
||||
channels=[ChannelType.LOG, ChannelType.WEBHOOK],
|
||||
))
|
||||
|
||||
alert = manager.create_alert(
|
||||
title="Multi-Channel Test",
|
||||
message="Should go to multiple channels",
|
||||
priority=AlertPriority.HIGH,
|
||||
)
|
||||
results = manager.send(alert)
|
||||
|
||||
# Should have results for multiple channels
|
||||
assert len(results) >= 2
|
||||
assert ChannelType.LOG in alert.channels_sent
|
||||
assert ChannelType.WEBHOOK in alert.channels_sent
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
"""Alerts module for trading alerts and notifications.
|
||||
|
||||
This module provides alert management including:
|
||||
- Alert orchestration and routing
|
||||
- Multiple alert channels (email, slack, sms, webhook)
|
||||
- Alert priorities and severity levels
|
||||
- Rate limiting to prevent alert storms
|
||||
- Alert history tracking
|
||||
|
||||
Issue #38: [ALERT-37] Alert manager - orchestration and routing
|
||||
|
||||
Submodules:
|
||||
alert_manager: Core alert management functionality
|
||||
|
||||
Classes:
|
||||
Enums:
|
||||
- AlertPriority: Alert priority levels (low, medium, high, critical)
|
||||
- AlertCategory: Alert categories (trade, risk, system, market, etc.)
|
||||
- AlertStatus: Alert delivery status
|
||||
- ChannelType: Alert channel types
|
||||
|
||||
Data Classes:
|
||||
- AlertTemplate: Template for formatting alerts
|
||||
- RateLimitConfig: Rate limiting configuration
|
||||
- RoutingRule: Rule for routing alerts to channels
|
||||
- AlertConfig: Alert manager configuration
|
||||
- Alert: An alert to be sent
|
||||
- DeliveryResult: Result of alert delivery
|
||||
- AlertStats: Statistics about alerts
|
||||
|
||||
Channel Classes:
|
||||
- LogChannel: Channel that logs to Python logging
|
||||
- WebhookChannel: Channel that sends to webhooks
|
||||
|
||||
Main Classes:
|
||||
- AlertManager: Orchestrates alert routing and delivery
|
||||
|
||||
Example:
|
||||
>>> from tradingagents.alerts import (
|
||||
... AlertManager,
|
||||
... AlertPriority,
|
||||
... AlertCategory,
|
||||
... )
|
||||
>>> from decimal import Decimal
|
||||
>>>
|
||||
>>> manager = AlertManager()
|
||||
>>>
|
||||
>>> # Create and send alert
|
||||
>>> alert = manager.create_alert(
|
||||
... title="Buy Signal",
|
||||
... message="AAPL buy signal detected",
|
||||
... priority=AlertPriority.MEDIUM,
|
||||
... category=AlertCategory.TRADE,
|
||||
... )
|
||||
>>> manager.send(alert)
|
||||
>>>
|
||||
>>> # Convenience methods
|
||||
>>> manager.alert_trade("AAPL", "BUY", Decimal("150.00"))
|
||||
>>> manager.alert_risk("DrawdownLimit", "15%", "10%")
|
||||
"""
|
||||
|
||||
from .alert_manager import (
|
||||
# Enums
|
||||
AlertPriority,
|
||||
AlertCategory,
|
||||
AlertStatus,
|
||||
ChannelType,
|
||||
# Data Classes
|
||||
AlertTemplate,
|
||||
RateLimitConfig,
|
||||
RoutingRule,
|
||||
AlertConfig,
|
||||
Alert,
|
||||
DeliveryResult,
|
||||
AlertStats,
|
||||
# Channel Classes
|
||||
LogChannel,
|
||||
WebhookChannel,
|
||||
# Main Class
|
||||
AlertManager,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Enums
|
||||
"AlertPriority",
|
||||
"AlertCategory",
|
||||
"AlertStatus",
|
||||
"ChannelType",
|
||||
# Data Classes
|
||||
"AlertTemplate",
|
||||
"RateLimitConfig",
|
||||
"RoutingRule",
|
||||
"AlertConfig",
|
||||
"Alert",
|
||||
"DeliveryResult",
|
||||
"AlertStats",
|
||||
# Channel Classes
|
||||
"LogChannel",
|
||||
"WebhookChannel",
|
||||
# Main Class
|
||||
"AlertManager",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue