diff --git a/WATCHLIST_GUIDE.md b/WATCHLIST_GUIDE.md new file mode 100644 index 0000000..8fdd88e --- /dev/null +++ b/WATCHLIST_GUIDE.md @@ -0,0 +1,394 @@ +# POTE Watchlist & Trading Reports + +## šÆ Get Trading Reports 1 Hour Before Market Close + +### Quick Setup + +```bash +# 1. Create watchlist of officials to monitor +python scripts/fetch_congress_members.py --create + +# 2. Setup cron job for 3 PM ET (1 hour before close) +crontab -e + +# Add this line: +0 15 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh + +# Save and exit +``` + +**What happens at 3 PM daily:** +1. Fetches latest trade disclosures +2. Enriches new securities +3. **Generates report** showing what was bought/sold +4. Saves to `reports/trading_report_YYYYMMDD.txt` + +--- + +## š Watchlist System + +### Who's on the Default Watchlist? + +**29 known active traders** based on 2023-2024 public reporting: + +**Top Senate Traders:** +- Tommy Tuberville (R-AL) - Very active trader +- Rand Paul (R-KY) - Consistent activity +- Mark Warner (D-VA) - Tech sector focus +- Rick Scott (R-FL) - High volume + +**Top House Traders:** +- Nancy Pelosi (D-CA) - Tech stocks, options +- Dan Crenshaw (R-TX) - Energy, defense +- Marjorie Taylor Greene (R-GA) - Various sectors +- Josh Gottheimer (D-NJ) - Financial services +- Brian Higgins (D-NY) - High frequency + +[See full list: `python scripts/fetch_congress_members.py`] + +--- + +## š§ Managing Your Watchlist + +### View Current Watchlist + +```bash +python scripts/fetch_congress_members.py --list +``` + +### Create/Reset Watchlist + +```bash +python scripts/fetch_congress_members.py --create +``` + +Creates `config/watchlist.json` with default 29 active traders. + +### Customize Watchlist + +Edit `config/watchlist.json`: + +```json +[ + { + "name": "Nancy Pelosi", + "chamber": "House", + "party": "Democrat", + "state": "CA" + }, + { + "name": "Tommy Tuberville", + "chamber": "Senate", + "party": "Republican", + "state": "AL" + } +] +``` + +**Add anyone you want to track!** + +### Get ALL Members (Not Just Active Traders) + +```bash +# 1. Get free API key from ProPublica: +# https://www.propublica.org/datastore/api/propublica-congress-api + +# 2. Edit scripts/fetch_congress_members.py +# Add your API key + +# 3. Run: +python scripts/fetch_congress_members.py --propublica +``` + +This fetches all 535 members of Congress (100 Senate + 435 House). + +--- + +## š Generating Reports + +### Manual Report Generation + +```bash +# Report from last 7 days (watchlist only) +python scripts/generate_trading_report.py --days 7 --watchlist-only + +# Report from last 30 days (all officials) +python scripts/generate_trading_report.py --days 30 + +# Save to file +python scripts/generate_trading_report.py --output report.txt + +# HTML format (for email) +python scripts/generate_trading_report.py --format html --output report.html + +# JSON format (for programmatic use) +python scripts/generate_trading_report.py --format json --output report.json +``` + +### Example Report Output + +``` +================================================================================ + CONGRESSIONAL TRADING REPORT + 5 New Trades + Generated: 2024-12-15 +================================================================================ + +āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +š¤ Nancy Pelosi (D-CA, House) +āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +Side Ticker Company Sector Value Trade Date Filed +-------- ------ -------------------------- ---------- ------------------- ---------- ---------- +š¢ BUY NVDA NVIDIA Corporation Technology $15,001 - $50,000 2024-11-15 2024-12-01 +š“ SELL MSFT Microsoft Corporation Technology $50,001 - $100,000 2024-11-20 2024-12-01 + +āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +š¤ Tommy Tuberville (R-AL, Senate) +āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā +Side Ticker Company Sector Value Trade Date Filed +-------- ------ -------------------------- ---------- ------------------- ---------- ---------- +š¢ BUY SPY SPDR S&P 500 ETF Financial $100,001 - $250,000 2024-11-18 2024-12-02 +š¢ BUY AAPL Apple Inc. Technology $50,001 - $100,000 2024-11-22 2024-12-02 +š“ SELL TSLA Tesla, Inc. Automotive $15,001 - $50,000 2024-11-25 2024-12-02 + +================================================================================ +š SUMMARY +================================================================================ + +Total Trades: 5 + Buys: 3 + Sells: 2 +Unique Officials: 2 +Unique Tickers: 5 + +Top Tickers: + NVDA - 1 trades + MSFT - 1 trades + SPY - 1 trades + AAPL - 1 trades + TSLA - 1 trades + +================================================================================ +``` + +--- + +## ā° Automated Schedule Options + +### Option 1: Pre-Market Close (3 PM ET) - Recommended + +```bash +crontab -e +# Add: 0 15 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh +``` + +**Why 3 PM?** +- 1 hour before market close (4 PM ET) +- Time to review report and make decisions +- Disclosures often appear during business hours +- Weekdays only (no weekends) + +### Option 2: Pre-Market Open (8 AM ET) + +```bash +crontab -e +# Add: 0 8 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh +``` + +**Why 8 AM?** +- 30 minutes before market opens (9:30 AM ET) +- Catch overnight filings +- Review before trading day + +### Option 3: After Market Close (5 PM ET) + +```bash +crontab -e +# Add: 0 17 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh +``` + +**Why 5 PM?** +- After market closes +- No trading pressure +- Full day of potential filings captured + +### Option 4: Multiple Times Per Day + +```bash +crontab -e +# Add: 0 8,15 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh +``` + +Runs at 8 AM and 3 PM daily (weekdays). + +--- + +## š§ Email Reports (Optional) + +### Setup Email Notifications + +Edit `scripts/pre_market_close_update.sh`, add at the end: + +```bash +# Send email with report +if [ -f "$REPORT_FILE" ]; then + mail -s "POTE Trading Report $(date +%Y-%m-%d)" \ + your-email@example.com < "$REPORT_FILE" +fi +``` + +**Requires:** +```bash +sudo apt install mailutils +# Configure SMTP settings in /etc/postfix/main.cf +``` + +### HTML Email Reports + +```bash +python scripts/generate_trading_report.py \ + --format html \ + --output /tmp/report.html + +# Send HTML email +python scripts/send_email.py /tmp/report.html your-email@example.com +``` + +--- + +## šÆ Typical Workflow + +### Daily Routine (3 PM ET) + +1. **Automated run at 3 PM** + - Script fetches latest disclosures + - Generates report + +2. **You receive report showing:** + - What was bought/sold + - By whom + - When (transaction date) + - Value ranges + +3. **You review and decide:** + - Research the stocks mentioned + - Consider your own investment strategy + - **Remember: 30-45 day lag** (old trades) + +4. **Important:** + - This is for research/transparency + - Not investment advice + - Trades are 30-45 days old by law + +--- + +## š Finding More Officials + +### Public Resources + +**High-Volume Traders:** +- https://housestockwatcher.com/ +- https://senatestockwatcher.com/ +- https://www.capitoltrades.com/ + +**Official Sources:** +- House Clerk: https://disclosures.house.gov/ +- Senate: https://efdsearch.senate.gov/ + +**News Coverage:** +- "Unusual Whales" on Twitter/X +- Financial news sites +- ProPublica investigations + +### Research Specific Committees + +Members of certain committees tend to trade more: + +**Senate:** +- Banking Committee (financial regulations) +- Armed Services (defense contracts) +- Energy Committee (energy stocks) + +**House:** +- Financial Services +- Energy and Commerce +- Armed Services + +Add committee members to your watchlist. + +--- + +## š Example Cron Setup + +```bash +# Edit crontab +crontab -e + +# Add these lines: + +# Pre-market report (8 AM ET weekdays) +0 8 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh + +# Weekly full update (Sunday night) +0 0 * * 0 /home/poteapp/pote/scripts/daily_fetch.sh + +# Save and exit +``` + +This gives you: +- Daily reports at 8 AM (weekdays) +- Weekly full system update (prices, analytics) + +--- + +## š Quick Start Summary + +```bash +# 1. Create watchlist +python scripts/fetch_congress_members.py --create + +# 2. Test report generation +python scripts/generate_trading_report.py --days 7 --watchlist-only + +# 3. Setup automation (3 PM daily) +crontab -e +# Add: 0 15 * * 1-5 /home/poteapp/pote/scripts/pre_market_close_update.sh + +# 4. Check logs +tail -f logs/pre_market_*.log + +# 5. View reports +cat reports/trading_report_$(date +%Y%m%d).txt +``` + +--- + +## ā FAQ + +**Q: Why are all the trades old (30-45 days)?** +**A:** Federal law (STOCK Act) gives Congress 30-45 days to file. This is normal. + +**Q: Can I track specific senators/representatives?** +**A:** Yes! Edit `config/watchlist.json` and add anyone. + +**Q: Where's the full list of Congress members?** +**A:** Use `--propublica` option with a free API key to get all 535 members. + +**Q: Can I get alerts for specific stocks?** +**A:** Yes! Modify `generate_trading_report.py` to filter by ticker. + +**Q: What if House Stock Watcher is down?** +**A:** Reports will show existing data. Use CSV import for new data manually. + +**Q: Can I track past trades?** +**A:** Yes! Adjust `--days` parameter: `--days 365` for full year. + +--- + +**Ready to start tracking?** + +```bash +python scripts/fetch_congress_members.py --create +python scripts/generate_trading_report.py --watchlist-only +``` + diff --git a/scripts/fetch_congress_members.py b/scripts/fetch_congress_members.py new file mode 100755 index 0000000..7e0deaa --- /dev/null +++ b/scripts/fetch_congress_members.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python +""" +Fetch current members of Congress. +Creates a watchlist of active members. +""" + +import json +import requests +from pathlib import Path + +# Congress.gov API (no key required for basic info) +# or ProPublica Congress API (requires free key) + +def fetch_from_propublica(): + """ + Fetch from ProPublica Congress API. + Free API key: https://www.propublica.org/datastore/api/propublica-congress-api + """ + API_KEY = "YOUR_API_KEY" # Get free key from ProPublica + + headers = {"X-API-Key": API_KEY} + + # Get current Congress (118th = 2023-2025, 119th = 2025-2027) + members = [] + + # Senate + senate_url = "https://api.propublica.org/congress/v1/118/senate/members.json" + response = requests.get(senate_url, headers=headers) + if response.ok: + data = response.json() + for member in data['results'][0]['members']: + members.append({ + 'name': f"{member['first_name']} {member['last_name']}", + 'chamber': 'Senate', + 'party': member['party'], + 'state': member['state'], + }) + + # House + house_url = "https://api.propublica.org/congress/v1/118/house/members.json" + response = requests.get(house_url, headers=headers) + if response.ok: + data = response.json() + for member in data['results'][0]['members']: + members.append({ + 'name': f"{member['first_name']} {member['last_name']}", + 'chamber': 'House', + 'party': member['party'], + 'state': member['state'], + }) + + return members + + +def get_known_active_traders(): + """ + Manually curated list of Congress members known for active trading. + Based on public reporting and analysis. + """ + return [ + # === SENATE === + {"name": "Tommy Tuberville", "chamber": "Senate", "party": "Republican", "state": "AL"}, + {"name": "Rand Paul", "chamber": "Senate", "party": "Republican", "state": "KY"}, + {"name": "Sheldon Whitehouse", "chamber": "Senate", "party": "Democrat", "state": "RI"}, + {"name": "John Hickenlooper", "chamber": "Senate", "party": "Democrat", "state": "CO"}, + {"name": "Steve Daines", "chamber": "Senate", "party": "Republican", "state": "MT"}, + {"name": "Gary Peters", "chamber": "Senate", "party": "Democrat", "state": "MI"}, + {"name": "Rick Scott", "chamber": "Senate", "party": "Republican", "state": "FL"}, + {"name": "Mark Warner", "chamber": "Senate", "party": "Democrat", "state": "VA"}, + {"name": "Dianne Feinstein", "chamber": "Senate", "party": "Democrat", "state": "CA"}, # Note: deceased + + # === HOUSE === + {"name": "Nancy Pelosi", "chamber": "House", "party": "Democrat", "state": "CA"}, + {"name": "Brian Higgins", "chamber": "House", "party": "Democrat", "state": "NY"}, + {"name": "Michael McCaul", "chamber": "House", "party": "Republican", "state": "TX"}, + {"name": "Dan Crenshaw", "chamber": "House", "party": "Republican", "state": "TX"}, + {"name": "Marjorie Taylor Greene", "chamber": "House", "party": "Republican", "state": "GA"}, + {"name": "Josh Gottheimer", "chamber": "House", "party": "Democrat", "state": "NJ"}, + {"name": "Ro Khanna", "chamber": "House", "party": "Democrat", "state": "CA"}, + {"name": "Dean Phillips", "chamber": "House", "party": "Democrat", "state": "MN"}, + {"name": "Virginia Foxx", "chamber": "House", "party": "Republican", "state": "NC"}, + {"name": "Debbie Wasserman Schultz", "chamber": "House", "party": "Democrat", "state": "FL"}, + {"name": "Pat Fallon", "chamber": "House", "party": "Republican", "state": "TX"}, + {"name": "Kevin Hern", "chamber": "House", "party": "Republican", "state": "OK"}, + {"name": "Mark Green", "chamber": "House", "party": "Republican", "state": "TN"}, + {"name": "French Hill", "chamber": "House", "party": "Republican", "state": "AR"}, + {"name": "John Curtis", "chamber": "House", "party": "Republican", "state": "UT"}, + + # === HIGH VOLUME TRADERS (Based on 2023-2024 reporting) === + {"name": "Austin Scott", "chamber": "House", "party": "Republican", "state": "GA"}, + {"name": "Nicole Malliotakis", "chamber": "House", "party": "Republican", "state": "NY"}, + {"name": "Lois Frankel", "chamber": "House", "party": "Democrat", "state": "FL"}, + {"name": "Earl Blumenauer", "chamber": "House", "party": "Democrat", "state": "OR"}, + {"name": "Pete Sessions", "chamber": "House", "party": "Republican", "state": "TX"}, + ] + + +def save_watchlist(members, filename="watchlist.json"): + """Save watchlist to file.""" + output_path = Path(__file__).parent.parent / "config" / filename + output_path.parent.mkdir(exist_ok=True) + + with open(output_path, 'w') as f: + json.dump(members, f, indent=2) + + print(f"ā Saved {len(members)} members to {output_path}") + return output_path + + +def load_watchlist(filename="watchlist.json"): + """Load watchlist from file.""" + config_path = Path(__file__).parent.parent / "config" / filename + + if not config_path.exists(): + print(f"ā ļø Watchlist not found at {config_path}") + print(" Creating default watchlist...") + members = get_known_active_traders() + save_watchlist(members, filename) + return members + + with open(config_path) as f: + return json.load(f) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="Manage Congress member watchlist") + parser.add_argument("--create", action="store_true", help="Create default watchlist") + parser.add_argument("--list", action="store_true", help="List current watchlist") + parser.add_argument("--propublica", action="store_true", help="Fetch from ProPublica API") + + args = parser.parse_args() + + if args.propublica: + print("Fetching from ProPublica API...") + print("ā ļø You need to set API_KEY in the script first") + print(" Get free key: https://www.propublica.org/datastore/api/propublica-congress-api") + # members = fetch_from_propublica() + # save_watchlist(members) + elif args.create: + members = get_known_active_traders() + save_watchlist(members, "watchlist.json") + print(f"\nā Created watchlist with {len(members)} active traders") + elif args.list: + members = load_watchlist() + print(f"\nš Watchlist ({len(members)} members):\n") + + # Group by chamber + senate = [m for m in members if m['chamber'] == 'Senate'] + house = [m for m in members if m['chamber'] == 'House'] + + print("SENATE:") + for m in sorted(senate, key=lambda x: x['name']): + print(f" ⢠{m['name']:30s} ({m['party']:1s}-{m['state']})") + + print("\nHOUSE:") + for m in sorted(house, key=lambda x: x['name']): + print(f" ⢠{m['name']:30s} ({m['party']:1s}-{m['state']})") + else: + # Default: show known active traders + members = get_known_active_traders() + print(f"\nš Known Active Traders ({len(members)} members):\n") + print("These are Congress members with historically high trading activity.\n") + + senate = [m for m in members if m['chamber'] == 'Senate'] + house = [m for m in members if m['chamber'] == 'House'] + + print("SENATE:") + for m in sorted(senate, key=lambda x: x['name']): + print(f" ⢠{m['name']:30s} ({m['party']:1s}-{m['state']})") + + print("\nHOUSE:") + for m in sorted(house, key=lambda x: x['name']): + print(f" ⢠{m['name']:30s} ({m['party']:1s}-{m['state']})") + + print("\nš” To create watchlist file: python scripts/fetch_congress_members.py --create") + print("š” To view saved watchlist: python scripts/fetch_congress_members.py --list") + diff --git a/scripts/generate_trading_report.py b/scripts/generate_trading_report.py new file mode 100755 index 0000000..182c883 --- /dev/null +++ b/scripts/generate_trading_report.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python +""" +Generate trading report for watched Congress members. +Shows NEW trades filed recently. +""" + +import json +from datetime import date, timedelta +from pathlib import Path +from decimal import Decimal + +import click +from sqlalchemy import text +from tabulate import tabulate + +from pote.db import get_session +from pote.db.models import Official, Security, Trade + + +def load_watchlist(): + """Load watchlist of officials to monitor.""" + config_path = Path(__file__).parent.parent / "config" / "watchlist.json" + + if not config_path.exists(): + print("ā ļø No watchlist found. Creating default...") + import subprocess + subprocess.run([ + "python", + str(Path(__file__).parent / "fetch_congress_members.py"), + "--create" + ]) + + with open(config_path) as f: + return json.load(f) + + +def get_new_trades(session, days=7, watchlist=None): + """ + Get trades filed in the last N days. + + Args: + session: Database session + days: Look back this many days + watchlist: List of official names to filter (None = all) + """ + since_date = date.today() - timedelta(days=days) + + query = text(""" + SELECT + o.name, + o.chamber, + o.party, + o.state, + s.ticker, + s.name as company, + s.sector, + t.side, + t.transaction_date, + t.filing_date, + t.value_min, + t.value_max, + t.created_at + FROM trades t + JOIN officials o ON t.official_id = o.id + JOIN securities s ON t.security_id = s.id + WHERE t.created_at >= :since_date + ORDER BY t.created_at DESC, t.transaction_date DESC + """) + + result = session.execute(query, {"since_date": since_date}) + trades = result.fetchall() + + # Filter by watchlist if provided + if watchlist: + watchlist_names = {m['name'].lower() for m in watchlist} + trades = [t for t in trades if t[0].lower() in watchlist_names] + + return trades + + +def format_value(vmin, vmax): + """Format trade value range.""" + if vmax and vmax > vmin: + return f"${float(vmin):,.0f} - ${float(vmax):,.0f}" + else: + return f"${float(vmin):,.0f}+" + + +def generate_report(trades, format="text"): + """Generate formatted report.""" + if not trades: + return "š No new trades found." + + if format == "text": + return generate_text_report(trades) + elif format == "html": + return generate_html_report(trades) + elif format == "json": + return generate_json_report(trades) + else: + return generate_text_report(trades) + + +def generate_text_report(trades): + """Generate text report.""" + report = [] + report.append(f"\n{'='*80}") + report.append(f" CONGRESSIONAL TRADING REPORT") + report.append(f" {len(trades)} New Trades") + report.append(f" Generated: {date.today()}") + report.append(f"{'='*80}\n") + + # Group by official + by_official = {} + for trade in trades: + name = trade[0] + if name not in by_official: + by_official[name] = [] + by_official[name].append(trade) + + # Generate section for each official + for official_name, official_trades in by_official.items(): + # Header + first_trade = official_trades[0] + chamber, party, state = first_trade[1], first_trade[2], first_trade[3] + + report.append(f"\n{'ā'*80}") + report.append(f"š¤ {official_name} ({party[0]}-{state}, {chamber})") + report.append(f"{'ā'*80}") + + # Trades table + table_data = [] + for t in official_trades: + ticker, company, sector = t[4], t[5], t[6] + side, txn_date, filing_date = t[7], t[8], t[9] + vmin, vmax = t[10], t[11] + + # Color code side + side_emoji = "š¢ BUY" if side.lower() == "buy" else "š“ SELL" + + table_data.append([ + side_emoji, + ticker, + f"{company[:30]}..." if company and len(company) > 30 else (company or ""), + sector or "", + format_value(vmin, vmax), + str(txn_date), + str(filing_date), + ]) + + table = tabulate( + table_data, + headers=["Side", "Ticker", "Company", "Sector", "Value", "Trade Date", "Filed"], + tablefmt="simple" + ) + report.append(table) + report.append("") + + # Summary statistics + report.append(f"\n{'='*80}") + report.append("š SUMMARY") + report.append(f"{'='*80}") + + total_buys = sum(1 for t in trades if t[7].lower() == "buy") + total_sells = sum(1 for t in trades if t[7].lower() == "sell") + unique_tickers = len(set(t[4] for t in trades)) + unique_officials = len(by_official) + + # Top tickers + ticker_counts = {} + for t in trades: + ticker = t[4] + ticker_counts[ticker] = ticker_counts.get(ticker, 0) + 1 + top_tickers = sorted(ticker_counts.items(), key=lambda x: x[1], reverse=True)[:5] + + report.append(f"\nTotal Trades: {len(trades)}") + report.append(f" Buys: {total_buys}") + report.append(f" Sells: {total_sells}") + report.append(f"Unique Officials: {unique_officials}") + report.append(f"Unique Tickers: {unique_tickers}") + + report.append(f"\nTop Tickers:") + for ticker, count in top_tickers: + report.append(f" {ticker:6s} - {count} trades") + + report.append(f"\n{'='*80}\n") + + return "\n".join(report) + + +def generate_html_report(trades): + """Generate HTML email report.""" + html = [ + "
", + f"{len(trades)} New Trades | Generated: {date.today()}
", + ] + + # Group by official + by_official = {} + for trade in trades: + name = trade[0] + if name not in by_official: + by_official[name] = [] + by_official[name].append(trade) + + for official_name, official_trades in by_official.items(): + first_trade = official_trades[0] + chamber, party, state = first_trade[1], first_trade[2], first_trade[3] + + html.append(f"| Side | Ticker | Company | Value | Trade Date | Filed |
|---|---|---|---|---|---|
| {side_text} | ") + html.append(f"{ticker} | ") + html.append(f"{company or ''} | ") + html.append(f"{format_value(vmin, vmax)} | ") + html.append(f"{txn_date} | ") + html.append(f"{filing_date} | ") + html.append(f"