POTE/tests/test_analytics_integration.py
ilia b4e6a7c340 Fix analytics tests and add comprehensive testing guide
Critical Fixes:
- Fixed Price model query to use security_id join with Security
- Added Security import to returns.py module
- Fixed all test fixtures to use test_db_session correctly
- Added AAPL price data to sample_prices fixture

New Tests:
- tests/test_analytics_integration.py: 10 comprehensive integration tests
  * Real-world scenarios with synthetic price data
  * Return calculations, benchmark comparisons, performance metrics
  * Edge cases: missing data, sell trades, disclosure timing

Documentation:
- LOCAL_TEST_GUIDE.md: Complete guide for local testing
  * How to test before deploying
  * Current data status (live vs fixtures)
  * Multiple options for getting real data
  * Common issues and fixes

Test Results:
 All 55 tests passing
 Analytics fully functional
 Ready for deployment

Live Data Status:
 House Stock Watcher API still down (external issue)
 Manual CSV import works
 yfinance for prices works
 Can use system NOW with manual data
2025-12-15 14:42:20 -05:00

332 lines
11 KiB
Python

"""Integration tests for analytics with real-ish data."""
import pytest
from datetime import date, timedelta
from decimal import Decimal
from pote.analytics.returns import ReturnCalculator
from pote.analytics.benchmarks import BenchmarkComparison
from pote.analytics.metrics import PerformanceMetrics
from pote.db.models import Official, Security, Trade, Price
@pytest.fixture
def full_test_data(test_db_session):
"""Create complete test dataset with prices."""
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")
spy = Security(ticker="SPY", name="SPDR S&P 500 ETF", sector="Financial")
session.add_all([nvda, spy])
session.flush()
# Create price data for NVDA (upward trend)
base_date = date(2024, 1, 1)
nvda_base_price = Decimal("495.00")
for i in range(120):
current_date = base_date + timedelta(days=i)
# Simulate upward trend: +0.5% per day on average
price_change = Decimal(i) * Decimal("2.50") # ~50% gain over 120 days
current_price = nvda_base_price + price_change
price = Price(
security_id=nvda.id,
date=current_date,
open=current_price - Decimal("5.00"),
high=current_price + Decimal("5.00"),
low=current_price - Decimal("8.00"),
close=current_price,
volume=50000000,
)
session.add(price)
# Create price data for SPY (slower upward trend - ~10% over 120 days)
spy_base_price = Decimal("450.00")
for i in range(120):
current_date = base_date + timedelta(days=i)
price_change = Decimal(i) * Decimal("0.35")
current_price = spy_base_price + price_change
price = Price(
security_id=spy.id,
date=current_date,
open=current_price - Decimal("2.00"),
high=current_price + Decimal("2.00"),
low=current_price - Decimal("3.00"),
close=current_price,
volume=100000000,
)
session.add(price)
# Create trades
# Pelosi buys NVDA early (should show good returns)
trade1 = Trade(
official_id=pelosi.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 1, 15), # Buy at ~495
filing_date=date(2024, 2, 1),
side="buy",
value_min=Decimal("15001"),
value_max=Decimal("50000"),
)
# Tuberville buys NVDA later (still good but less alpha)
trade2 = Trade(
official_id=tuberville.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 2, 1), # Buy at ~540
filing_date=date(2024, 2, 15),
side="buy",
value_min=Decimal("50001"),
value_max=Decimal("100000"),
)
session.add_all([trade1, trade2])
session.commit()
return {
"officials": [pelosi, tuberville],
"securities": [nvda, spy],
"trades": [trade1, trade2],
}
def test_return_calculation_with_real_data(test_db_session, full_test_data):
session = test_db_session
"""Test return calculation with realistic price data."""
calculator = ReturnCalculator(session)
# Get Pelosi's NVDA trade
trade = full_test_data["trades"][0]
# Calculate 90-day return
result = calculator.calculate_trade_return(trade, window_days=90)
assert result is not None, "Should calculate return with available data"
assert result["ticker"] == "NVDA"
assert result["window_days"] == 90
assert result["return_pct"] > 0, "NVDA should have positive return"
# Entry around day 15, exit around day 105
# Expected return: (720 - 532.5) / 532.5 = ~35%
assert 30 < float(result["return_pct"]) < 50, f"Expected ~35% return, got {result['return_pct']}"
print(f"\n✅ NVDA 90-day return: {result['return_pct']:.2f}%")
print(f" Entry: ${result['entry_price']} on {result['transaction_date']}")
print(f" Exit: ${result['exit_price']} on {result['exit_date']}")
def test_benchmark_comparison_with_real_data(test_db_session, full_test_data):
session = test_db_session
"""Test benchmark comparison with SPY."""
benchmark = BenchmarkComparison(session)
# Get Pelosi's trade
trade = full_test_data["trades"][0]
# Compare to SPY
result = benchmark.compare_trade_to_benchmark(trade, window_days=90, benchmark="SPY")
assert result is not None
assert result["ticker"] == "NVDA"
assert result["benchmark"] == "SPY"
# NVDA should beat SPY significantly
assert result["beat_market"] is True
assert float(result["abnormal_return"]) > 10, "NVDA should have strong alpha vs SPY"
print(f"\n✅ Benchmark Comparison:")
print(f" NVDA Return: {result['trade_return']:.2f}%")
print(f" SPY Return: {result['benchmark_return']:.2f}%")
print(f" Alpha: {result['abnormal_return']:+.2f}%")
def test_official_performance_summary(test_db_session, full_test_data):
session = test_db_session
"""Test official performance aggregation."""
metrics = PerformanceMetrics(session)
pelosi = full_test_data["officials"][0]
# Get performance summary
perf = metrics.official_performance(pelosi.id, window_days=90)
assert perf["name"] == "Nancy Pelosi"
assert perf["total_trades"] >= 1
if perf.get("trades_analyzed", 0) > 0:
assert "avg_return" in perf
assert "avg_alpha" in perf
assert "win_rate" in perf
assert perf["win_rate"] >= 0 and perf["win_rate"] <= 1
print(f"\n{perf['name']} Performance:")
print(f" Total Trades: {perf['total_trades']}")
print(f" Average Return: {perf['avg_return']:.2f}%")
print(f" Alpha: {perf['avg_alpha']:+.2f}%")
print(f" Win Rate: {perf['win_rate']:.1%}")
def test_multiple_windows(test_db_session, full_test_data):
session = test_db_session
"""Test calculating multiple time windows."""
calculator = ReturnCalculator(session)
trade = full_test_data["trades"][0]
# Calculate for 30, 60, 90 days
results = calculator.calculate_multiple_windows(trade, windows=[30, 60, 90])
assert len(results) == 3, "Should calculate all three windows"
# Returns should generally increase with longer windows (given upward trend)
if 30 in results and 90 in results:
print(f"\n✅ Multiple Windows:")
for window in [30, 60, 90]:
if window in results:
print(f" {window:3d} days: {results[window]['return_pct']:+7.2f}%")
def test_top_performers(test_db_session, full_test_data):
session = test_db_session
"""Test top performer ranking."""
metrics = PerformanceMetrics(session)
top = metrics.top_performers(window_days=90, limit=5)
assert isinstance(top, list)
assert len(top) > 0
print(f"\n✅ Top Performers:")
for i, perf in enumerate(top, 1):
if perf.get("trades_analyzed", 0) > 0:
print(f" {i}. {perf['name']:20s} | Alpha: {perf['avg_alpha']:+6.2f}%")
def test_system_statistics(test_db_session, full_test_data):
session = test_db_session
"""Test system-wide statistics."""
metrics = PerformanceMetrics(session)
stats = metrics.summary_statistics(window_days=90)
assert stats["total_officials"] >= 2
assert stats["total_trades"] >= 2
assert stats["total_securities"] >= 2
print(f"\n✅ System Statistics:")
print(f" Officials: {stats['total_officials']}")
print(f" Trades: {stats['total_trades']}")
print(f" Securities: {stats['total_securities']}")
if stats.get("avg_alpha") is not None:
print(f" Avg Alpha: {stats['avg_alpha']:+.2f}%")
print(f" Beat Market: {stats['beat_market_rate']:.1%}")
def test_disclosure_timing(test_db_session, full_test_data):
session = test_db_session
"""Test disclosure lag analysis."""
metrics = PerformanceMetrics(session)
timing = metrics.timing_analysis()
assert "avg_disclosure_lag_days" in timing
assert timing["avg_disclosure_lag_days"] > 0
print(f"\n✅ Disclosure Timing:")
print(f" Average Lag: {timing['avg_disclosure_lag_days']:.1f} days")
print(f" Median Lag: {timing['median_disclosure_lag_days']} days")
def test_sector_analysis(test_db_session, full_test_data):
session = test_db_session
"""Test sector-level analysis."""
metrics = PerformanceMetrics(session)
sectors = metrics.sector_analysis(window_days=90)
assert isinstance(sectors, list)
if sectors:
print(f"\n✅ Sector Analysis:")
for s in sectors:
print(f" {s['sector']:15s} | {s['trade_count']} trades | Alpha: {s['avg_alpha']:+6.2f}%")
def test_edge_case_missing_exit_price(test_db_session, full_test_data):
session = test_db_session
"""Test handling of trade with no exit price available."""
calculator = ReturnCalculator(session)
nvda = session.query(Security).filter_by(ticker="NVDA").first()
pelosi = session.query(Official).filter_by(name="Nancy Pelosi").first()
# Create trade with transaction date far in future (no exit price)
future_trade = Trade(
official_id=pelosi.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 12, 1), # No prices available this far out
side="buy",
value_min=Decimal("10000"),
value_max=Decimal("50000"),
)
session.add(future_trade)
session.commit()
result = calculator.calculate_trade_return(future_trade, window_days=90)
assert result is None, "Should return None when price data unavailable"
print("\n✅ Correctly handles missing price data")
def test_sell_trade_logic(test_db_session, full_test_data):
session = test_db_session
"""Test that sell trades have inverted return logic."""
calculator = ReturnCalculator(session)
nvda = session.query(Security).filter_by(ticker="NVDA").first()
pelosi = session.query(Official).filter_by(name="Nancy Pelosi").first()
# Create sell trade during uptrend (should show negative return)
sell_trade = Trade(
official_id=pelosi.id,
security_id=nvda.id,
source="test",
transaction_date=date(2024, 1, 15),
side="sell",
value_min=Decimal("10000"),
value_max=Decimal("50000"),
)
session.add(sell_trade)
session.commit()
result = calculator.calculate_trade_return(sell_trade, window_days=90)
if result:
# Selling during uptrend = negative return
assert result["return_pct"] < 0, "Sell during uptrend should show negative return"
print(f"\n✅ Sell trade return correctly inverted: {result['return_pct']:.2f}%")