Files
POTE/tests/test_monitoring.py
T
ilia 5df21d82f4
CI / skip-ci-check (pull_request) Successful in 19s
CI / secret-scan (pull_request) Successful in 19s
CI / python-ci (pull_request) Successful in 2m38s
Fix lint debt and make CI gates honest
- ruff: fix all 328 errors (autofix + manual); move config to [tool.ruff.lint]
- mypy: fix all errors (annotations, nullable-column guards, stale kwargs
  in weekly report caller)
- black: reformat src/tests so black --check passes
- CI: remove `|| true` from ruff/black/mypy/pytest steps in both
  .gitea and .github workflows; install project deps and add mypy
  step in gitea python-ci so gates actually run
- docs: move 14 root status/guide markdown files into docs/, update links
2026-07-26 15:41:07 -04:00

438 lines
13 KiB
Python

"""Tests for market monitoring module."""
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
import pytest
from pote.db.models import MarketAlert, Official, Security, Trade
from pote.monitoring.alert_manager import AlertManager
from pote.monitoring.market_monitor import MarketMonitor
@pytest.fixture
def sample_congressional_trades(test_db_session):
"""Create sample congressional trades for watchlist building."""
session = test_db_session
# Create officials
pelosi = Official(name="Nancy Pelosi", chamber="House", party="Democrat", state="CA")
tuberville = Official(name="Tommy Tuberville", chamber="Senate", party="Republican", state="AL")
session.add_all([pelosi, tuberville])
session.flush()
# Create securities
nvda = Security(ticker="NVDA", name="NVIDIA Corporation", sector="Technology")
msft = Security(ticker="MSFT", name="Microsoft Corporation", sector="Technology")
aapl = Security(ticker="AAPL", name="Apple Inc.", sector="Technology")
tsla = Security(ticker="TSLA", name="Tesla, Inc.", sector="Automotive")
spy = Security(ticker="SPY", name="SPDR S&P 500 ETF", sector="Financial")
session.add_all([nvda, msft, aapl, tsla, spy])
session.flush()
# Create multiple trades (NVDA is most traded)
trades = [
Trade(
official_id=pelosi.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 1, 15),
side="buy",
value_min=Decimal("15001"),
value_max=Decimal("50000"),
),
Trade(
official_id=pelosi.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 2, 1),
side="buy",
value_min=Decimal("15001"),
value_max=Decimal("50000"),
),
Trade(
official_id=tuberville.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 2, 15),
side="buy",
value_min=Decimal("50001"),
value_max=Decimal("100000"),
),
Trade(
official_id=pelosi.id,
security_id=msft.id,
source="test",
transaction_date=date(2024, 1, 20),
side="sell",
value_min=Decimal("15001"),
value_max=Decimal("50000"),
),
Trade(
official_id=tuberville.id,
security_id=aapl.id,
source="test",
transaction_date=date(2024, 2, 10),
side="buy",
value_min=Decimal("15001"),
value_max=Decimal("50000"),
),
]
session.add_all(trades)
session.commit()
return {
"officials": [pelosi, tuberville],
"securities": [nvda, msft, aapl, tsla, spy],
"trades": trades,
}
@pytest.fixture
def sample_alerts(test_db_session):
"""Create sample market alerts."""
session = test_db_session
now = datetime.now(UTC)
alerts = [
MarketAlert(
ticker="NVDA",
alert_type="unusual_volume",
timestamp=now - timedelta(hours=2),
details={"current_volume": 100000000, "avg_volume": 30000000, "multiplier": 3.33},
price=Decimal("495.50"),
volume=100000000,
change_pct=Decimal("2.5"),
severity=7,
),
MarketAlert(
ticker="NVDA",
alert_type="price_spike",
timestamp=now - timedelta(hours=1),
details={"current_price": 505.00, "prev_price": 495.50, "change_pct": 1.92},
price=Decimal("505.00"),
volume=85000000,
change_pct=Decimal("5.5"),
severity=4,
),
MarketAlert(
ticker="MSFT",
alert_type="high_volatility",
timestamp=now - timedelta(hours=3),
details={"recent_volatility": 4.5, "avg_volatility": 2.0, "multiplier": 2.25},
price=Decimal("380.25"),
volume=50000000,
change_pct=Decimal("1.2"),
severity=5,
),
]
session.add_all(alerts)
session.commit()
return alerts
def test_get_congressional_watchlist(test_db_session, sample_congressional_trades):
"""Test building watchlist from congressional trades."""
session = test_db_session
monitor = MarketMonitor(session)
watchlist = monitor.get_congressional_watchlist(limit=10)
assert len(watchlist) > 0
assert "NVDA" in watchlist # Most traded
assert watchlist[0] == "NVDA" # Should be first (3 trades)
def test_check_ticker_basic(test_db_session):
"""Test basic ticker checking (may not find alerts with real data)."""
session = test_db_session
monitor = MarketMonitor(session)
# This uses real yfinance data, so alerts depend on current market
# We test that it doesn't crash
alerts = monitor.check_ticker("AAPL", lookback_days=5)
assert isinstance(alerts, list)
# Each alert should have required fields
for alert in alerts:
assert "ticker" in alert
assert "alert_type" in alert
assert "timestamp" in alert
assert "severity" in alert
def test_scan_watchlist_with_mock(test_db_session, sample_congressional_trades, monkeypatch):
"""Test scanning watchlist with mocked data."""
session = test_db_session
monitor = MarketMonitor(session)
# Mock the check_ticker method to return controlled data
def mock_check_ticker(ticker, lookback_days=5):
if ticker == "NVDA":
return [
{
"ticker": ticker,
"alert_type": "unusual_volume",
"timestamp": datetime.now(UTC),
"details": {"multiplier": 3.5},
"price": Decimal("500.00"),
"volume": 100000000,
"change_pct": Decimal("2.5"),
"severity": 7,
}
]
return []
monkeypatch.setattr(monitor, "check_ticker", mock_check_ticker)
# Scan with limited watchlist
alerts = monitor.scan_watchlist(tickers=["NVDA", "MSFT"], lookback_days=5)
assert len(alerts) == 1
assert alerts[0]["ticker"] == "NVDA"
assert alerts[0]["alert_type"] == "unusual_volume"
def test_save_alerts(test_db_session):
"""Test saving alerts to database."""
session = test_db_session
monitor = MarketMonitor(session)
alerts_data = [
{
"ticker": "TSLA",
"alert_type": "price_spike",
"timestamp": datetime.now(UTC),
"details": {"change_pct": 7.5},
"price": Decimal("250.00"),
"volume": 75000000,
"change_pct": Decimal("7.5"),
"severity": 8,
},
{
"ticker": "TSLA",
"alert_type": "unusual_volume",
"timestamp": datetime.now(UTC),
"details": {"multiplier": 4.0},
"price": Decimal("250.00"),
"volume": 120000000,
"change_pct": Decimal("7.5"),
"severity": 9,
},
]
saved_count = monitor.save_alerts(alerts_data)
assert saved_count == 2
# Verify in database
alerts = session.query(MarketAlert).filter_by(ticker="TSLA").all()
assert len(alerts) == 2
def test_get_recent_alerts(test_db_session, sample_alerts):
"""Test querying recent alerts."""
session = test_db_session
monitor = MarketMonitor(session)
# Get all alerts
all_alerts = monitor.get_recent_alerts(days=1)
assert len(all_alerts) >= 3
# Filter by ticker
nvda_alerts = monitor.get_recent_alerts(ticker="NVDA", days=1)
assert len(nvda_alerts) == 2
assert all(a.ticker == "NVDA" for a in nvda_alerts)
# Filter by alert type
volume_alerts = monitor.get_recent_alerts(alert_type="unusual_volume", days=1)
assert len(volume_alerts) == 1
assert volume_alerts[0].alert_type == "unusual_volume"
# Filter by severity
high_sev_alerts = monitor.get_recent_alerts(min_severity=6, days=1)
assert all(a.severity >= 6 for a in high_sev_alerts)
def test_get_ticker_alert_summary(test_db_session, sample_alerts):
"""Test alert summary by ticker."""
session = test_db_session
monitor = MarketMonitor(session)
summary = monitor.get_ticker_alert_summary(days=1)
assert "NVDA" in summary
assert "MSFT" in summary
nvda_summary = summary["NVDA"]
assert nvda_summary["alert_count"] == 2
assert nvda_summary["max_severity"] == 7
assert 4 <= nvda_summary["avg_severity"] <= 7
def test_alert_manager_format_text(test_db_session, sample_alerts):
"""Test text formatting of alerts."""
session = test_db_session
alert_mgr = AlertManager(session)
alert = sample_alerts[0] # NVDA unusual volume
text = alert_mgr.format_alert_text(alert)
assert "NVDA" in text
assert "UNUSUAL VOLUME" in text
assert "Severity" in text
assert "$495.50" in text
def test_alert_manager_format_html(test_db_session, sample_alerts):
"""Test HTML formatting of alerts."""
session = test_db_session
alert_mgr = AlertManager(session)
alert = sample_alerts[0]
html = alert_mgr.format_alert_html(alert)
assert "<div" in html
assert "NVDA" in html
assert "unusual_volume" in html or "Unusual Volume" in html
def test_alert_manager_filter_alerts(test_db_session, sample_alerts):
"""Test filtering alerts."""
session = test_db_session
alert_mgr = AlertManager(session)
# Filter by severity
high_sev = alert_mgr.filter_alerts(sample_alerts, min_severity=6)
assert len(high_sev) == 1
assert high_sev[0].ticker == "NVDA"
assert high_sev[0].severity == 7
# Filter by ticker
nvda_only = alert_mgr.filter_alerts(sample_alerts, min_severity=0, tickers=["NVDA"])
assert len(nvda_only) == 2
assert all(a.ticker == "NVDA" for a in nvda_only)
# Filter by alert type
volume_only = alert_mgr.filter_alerts(sample_alerts, alert_types=["unusual_volume"])
assert len(volume_only) == 1
assert volume_only[0].alert_type == "unusual_volume"
# Combined filters
filtered = alert_mgr.filter_alerts(
sample_alerts,
min_severity=4,
tickers=["NVDA"],
alert_types=["unusual_volume", "price_spike"],
)
assert len(filtered) == 2
def test_alert_manager_generate_summary_text(test_db_session, sample_alerts):
"""Test generating text summary report."""
session = test_db_session
alert_mgr = AlertManager(session)
report = alert_mgr.generate_summary_report(sample_alerts, output_format="text")
assert "MARKET ACTIVITY ALERTS" in report
assert "3 Alerts" in report
assert "NVDA" in report
assert "MSFT" in report
assert "SUMMARY" in report
def test_alert_manager_generate_summary_html(test_db_session, sample_alerts):
"""Test generating HTML summary report."""
session = test_db_session
alert_mgr = AlertManager(session)
report = alert_mgr.generate_summary_report(sample_alerts, output_format="html")
assert "<html>" in report
assert "<head>" in report
assert "Market Activity Alerts" in report
assert "NVDA" in report
def test_alert_manager_empty_alerts(test_db_session):
"""Test handling empty alert list."""
session = test_db_session
alert_mgr = AlertManager(session)
report = alert_mgr.generate_summary_report([], output_format="text")
assert "No alerts" in report
def test_market_alert_model(test_db_session):
"""Test MarketAlert model creation and retrieval."""
session = test_db_session
alert = MarketAlert(
ticker="GOOGL",
alert_type="price_spike",
timestamp=datetime.now(UTC),
details={"test": "data"},
price=Decimal("140.50"),
volume=25000000,
change_pct=Decimal("6.2"),
severity=7,
source="test",
)
session.add(alert)
session.commit()
# Retrieve
retrieved = session.query(MarketAlert).filter_by(ticker="GOOGL").first()
assert retrieved is not None
assert retrieved.ticker == "GOOGL"
assert retrieved.alert_type == "price_spike"
assert retrieved.severity == 7
assert retrieved.details == {"test": "data"}
assert float(retrieved.price) == 140.50
def test_alert_timestamp_filtering(test_db_session):
"""Test filtering alerts by timestamp."""
session = test_db_session
now = datetime.now(UTC)
# Create alerts at different times
old_alert = MarketAlert(
ticker="TEST1",
alert_type="test",
timestamp=now - timedelta(days=10),
severity=5,
)
recent_alert = MarketAlert(
ticker="TEST2",
alert_type="test",
timestamp=now - timedelta(hours=2),
severity=5,
)
session.add_all([old_alert, recent_alert])
session.commit()
monitor = MarketMonitor(session)
# Should only get recent alert
alerts_1_day = monitor.get_recent_alerts(days=1)
test_alerts = [a for a in alerts_1_day if a.ticker.startswith("TEST")]
assert len(test_alerts) == 1
assert test_alerts[0].ticker == "TEST2"
# Should get both with longer lookback
alerts_30_days = monitor.get_recent_alerts(days=30)
test_alerts = [a for a in alerts_30_days if a.ticker.startswith("TEST")]
assert len(test_alerts) == 2