POTE/scripts/analyze_disclosure_timing.py
ilia 6b62ae96f7 Phase 2: Disclosure Timing Correlation Engine
COMPLETE: Match congressional trades to prior market alerts

New Module:
- src/pote/monitoring/disclosure_correlator.py: Core correlation engine
  * get_alerts_before_trade(): Find alerts before trade date
  * calculate_timing_score(): Score suspicious timing (0-100 scale)
    - Factors: alert count, severity, recency, type
    - Thresholds: 60+ = suspicious, 80+ = highly suspicious
  * analyze_trade(): Complete trade analysis with timing
  * analyze_recent_disclosures(): Batch analysis of new filings
  * get_official_timing_pattern(): Historical pattern analysis
  * get_ticker_timing_analysis(): Per-stock timing patterns

Timing Score Algorithm:
- Base score: alert count × 5 + avg severity × 2
- Recency bonus: +10 per alert within 7 days
- Severity bonus: +15 per high-severity (7+) alert
- Total score: 0-100 (capped)
- Interpretation:
  * 80-100: Highly suspicious (likely timing advantage)
  * 60-79: Suspicious (possible timing advantage)
  * 40-59: Notable (some unusual activity)
  * 0-39: Normal (no significant pattern)

New Script:
- scripts/analyze_disclosure_timing.py: CLI analysis tool
  * Analyze recent disclosures (--days N)
  * Filter by timing score (--min-score)
  * Analyze specific official (--official NAME)
  * Analyze specific ticker (--ticker SYMBOL)
  * Text/JSON output formats
  * Detailed reports with prior alerts

Usage Examples:
  # Find suspicious trades filed recently
  python scripts/analyze_disclosure_timing.py --days 30 --min-score 60

  # Analyze specific official
  python scripts/analyze_disclosure_timing.py --official "Nancy Pelosi"

  # Analyze specific ticker
  python scripts/analyze_disclosure_timing.py --ticker NVDA

Report Includes:
- Timing score and suspicion level
- Prior alert details (count, severity, timing)
- Official name, ticker, trade details
- Assessment and reasoning
- Top suspicious trades ranked

Next: Phase 3 - Pattern Detection across officials/stocks
2025-12-15 15:17:09 -05:00

220 lines
7.8 KiB
Python
Executable File

#!/usr/bin/env python
"""
Analyze congressional trade timing vs market alerts.
Identifies suspicious timing patterns and potential insider trading.
"""
import click
from pathlib import Path
from tabulate import tabulate
from pote.db import get_session
from pote.monitoring.disclosure_correlator import DisclosureCorrelator
@click.command()
@click.option("--days", default=30, help="Analyze trades filed in last N days")
@click.option("--min-score", default=50, help="Minimum timing score to report (0-100)")
@click.option("--official", help="Analyze specific official by name")
@click.option("--ticker", help="Analyze specific ticker")
@click.option("--output", help="Save report to file")
@click.option("--format", type=click.Choice(["text", "json"]), default="text")
def main(days, min_score, official, ticker, output, format):
"""Analyze disclosure timing and detect suspicious patterns."""
session = next(get_session())
correlator = DisclosureCorrelator(session)
if official:
# Analyze specific official
from pote.db.models import Official
official_obj = session.query(Official).filter(
Official.name.ilike(f"%{official}%")
).first()
if not official_obj:
click.echo(f"❌ Official '{official}' not found")
return
click.echo(f"\n📊 Analyzing {official_obj.name}...\n")
result = correlator.get_official_timing_pattern(official_obj.id)
report = format_official_report(result)
click.echo(report)
elif ticker:
# Analyze specific ticker
click.echo(f"\n📊 Analyzing trades in {ticker.upper()}...\n")
result = correlator.get_ticker_timing_analysis(ticker.upper())
report = format_ticker_report(result)
click.echo(report)
else:
# Analyze recent disclosures
click.echo(f"\n🔍 Analyzing trades filed in last {days} days...")
click.echo(f" Minimum timing score: {min_score}\n")
suspicious_trades = correlator.analyze_recent_disclosures(
days=days,
min_timing_score=min_score
)
if not suspicious_trades:
click.echo(f"✅ No trades found with timing score >= {min_score}")
return
report = format_suspicious_trades_report(suspicious_trades)
click.echo(report)
# Save to file if requested
if output:
Path(output).write_text(report)
click.echo(f"\n💾 Report saved to {output}")
def format_suspicious_trades_report(trades):
"""Format suspicious trades as text report."""
lines = [
"=" * 100,
f" SUSPICIOUS TRADING TIMING ANALYSIS",
f" {len(trades)} Trades with Timing Advantages Detected",
"=" * 100,
"",
]
for i, trade in enumerate(trades, 1):
# Determine alert level
if trade.get("highly_suspicious"):
alert_emoji = "🚨"
level = "HIGHLY SUSPICIOUS"
elif trade["suspicious"]:
alert_emoji = "🔴"
level = "SUSPICIOUS"
else:
alert_emoji = "🟡"
level = "NOTABLE"
lines.extend([
"" * 100,
f"{alert_emoji} #{i} - {level} (Timing Score: {trade['timing_score']}/100)",
"" * 100,
f"Official: {trade['official_name']}",
f"Ticker: {trade['ticker']}",
f"Side: {trade['side'].upper()}",
f"Trade Date: {trade['transaction_date']}",
f"Filed Date: {trade['filing_date']}",
f"Value: {trade['value_range']}",
"",
f"📊 Timing Analysis:",
f" Prior Alerts: {trade['alert_count']}",
f" Recent Alerts (7d): {trade['recent_alert_count']}",
f" High Severity: {trade['high_severity_count']}",
f" Avg Severity: {trade['avg_severity']}/10",
"",
f"💡 Assessment: {trade['reason']}",
"",
])
if trade['prior_alerts']:
lines.append("🔔 Prior Market Alerts:")
alert_table = []
for alert in trade['prior_alerts'][:5]: # Top 5
alert_table.append([
alert['timestamp'],
alert['alert_type'].replace('_', ' ').title(),
f"{alert['severity']}/10",
f"{alert['days_before_trade']} days before",
])
lines.append(tabulate(
alert_table,
headers=["Timestamp", "Type", "Severity", "Timing"],
tablefmt="simple"
))
lines.append("")
lines.extend([
"=" * 100,
"📈 SUMMARY",
"=" * 100,
f"Total Suspicious Trades: {len(trades)}",
f"Highly Suspicious: {sum(1 for t in trades if t.get('highly_suspicious'))}",
f"Average Timing Score: {sum(t['timing_score'] for t in trades) / len(trades):.2f}/100",
"",
"⚠️ IMPORTANT:",
" This analysis is for research and transparency purposes only.",
" High timing scores suggest potential issues but are not definitive proof.",
" Further investigation may be warranted for highly suspicious patterns.",
"",
"=" * 100,
])
return "\n".join(lines)
def format_official_report(result):
"""Format official timing pattern report."""
lines = [
"=" * 80,
f" OFFICIAL TIMING PATTERN ANALYSIS",
"=" * 80,
"",
f"Trade Count: {result['trade_count']}",
f"With Prior Alerts: {result['trades_with_prior_alerts']} ({result['trades_with_prior_alerts']/result['trade_count']*100:.1f}%)" if result['trade_count'] > 0 else "",
f"Suspicious Trades: {result['suspicious_trade_count']}",
f"Highly Suspicious: {result['highly_suspicious_count']}",
f"Average Timing Score: {result['avg_timing_score']}/100",
"",
f"📊 Pattern: {result['pattern']}",
"",
]
if result.get('analyses'):
# Show top suspicious trades
suspicious = [a for a in result['analyses'] if a['suspicious']]
if suspicious:
lines.append("🚨 Most Suspicious Trades:")
for trade in suspicious[:5]:
lines.append(
f" {trade['ticker']:6s} {trade['side']:4s} on {trade['transaction_date']} "
f"(Score: {trade['timing_score']:.0f}/100, {trade['alert_count']} alerts)"
)
lines.append("")
lines.append("=" * 80)
return "\n".join(lines)
def format_ticker_report(result):
"""Format ticker timing analysis report."""
lines = [
"=" * 80,
f" TICKER TIMING ANALYSIS: {result['ticker']}",
"=" * 80,
"",
f"Total Trades: {result['trade_count']}",
f"With Prior Alerts: {result['trades_with_alerts']}",
f"Suspicious Count: {result['suspicious_count']}",
f"Average Timing Score: {result['avg_timing_score']}/100",
"",
]
if result.get('analyses'):
lines.append("📊 Recent Trades:")
for trade in result['analyses'][:10]:
emoji = "🚨" if trade.get('highly_suspicious') else "🔴" if trade['suspicious'] else "🟡" if trade['alert_count'] > 0 else ""
lines.append(
f" {emoji} {trade['official_name']:25s} {trade['side']:4s} on {trade['transaction_date']} "
f"(Score: {trade['timing_score']:.0f}/100)"
)
lines.append("")
lines.append("=" * 80)
return "\n".join(lines)
if __name__ == "__main__":
main()