summaryrefslogtreecommitdiff
path: root/scripts/stock_screener.py
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:11:59 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-02 10:11:59 +0900
commit47465828d839c460a6af12894451539908d76c26 (patch)
tree4d4c43a26d87c243dcffc2ec2bba197c2fd8601f /scripts/stock_screener.py
parent71a01fb5577ae8326072020a8de49361f16bd3de (diff)
feat: add Claude-powered daily stock screener for MOC strategy
Diffstat (limited to 'scripts/stock_screener.py')
-rwxr-xr-xscripts/stock_screener.py253
1 files changed, 253 insertions, 0 deletions
diff --git a/scripts/stock_screener.py b/scripts/stock_screener.py
new file mode 100755
index 0000000..387bfea
--- /dev/null
+++ b/scripts/stock_screener.py
@@ -0,0 +1,253 @@
+#!/usr/bin/env python3
+"""Claude-powered daily stock screener for MOC strategy.
+
+Analyzes market conditions and recommends stocks to buy at close.
+Uses Anthropic Claude API for fundamental/news analysis.
+
+Usage:
+ python scripts/stock_screener.py
+ python scripts/stock_screener.py --top 5
+
+Requires: ANTHROPIC_API_KEY environment variable
+"""
+import argparse
+import asyncio
+import json
+import os
+import sys
+from datetime import datetime, timezone
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT / "shared" / "src"))
+
+from shared.alpaca import AlpacaClient # noqa: E402
+
+
+async def get_market_data(alpaca: AlpacaClient, symbols: list[str]) -> list[dict]:
+ """Fetch latest market data for screening."""
+ results = []
+ for symbol in symbols:
+ try:
+ bars = await alpaca.get_bars(symbol, timeframe="1Day", limit=5)
+ if not bars:
+ continue
+
+ latest = bars[-1]
+ prev = bars[-2] if len(bars) > 1 else latest
+
+ # Calculate basic metrics
+ close = float(latest["c"])
+ prev_close = float(prev["c"])
+ change_pct = (close - prev_close) / prev_close * 100 if prev_close else 0
+ volume = int(latest["v"])
+
+ # 5-day average volume
+ avg_vol = sum(int(b["v"]) for b in bars) / len(bars)
+ vol_ratio = volume / avg_vol if avg_vol > 0 else 1.0
+
+ # 5-day price range
+ highs = [float(b["h"]) for b in bars]
+ lows = [float(b["l"]) for b in bars]
+ range_pct = (max(highs) - min(lows)) / close * 100 if close > 0 else 0
+
+ results.append({
+ "symbol": symbol,
+ "close": close,
+ "change_pct": round(change_pct, 2),
+ "volume": volume,
+ "vol_ratio": round(vol_ratio, 2),
+ "range_5d_pct": round(range_pct, 2),
+ "is_bullish": close > float(latest["o"]), # Today bullish?
+ })
+ except Exception as exc:
+ print(f" Warning: Failed to fetch {symbol}: {exc}")
+
+ return results
+
+
+def build_claude_prompt(market_data: list[dict]) -> str:
+ """Build a prompt for Claude to analyze stocks."""
+ data_str = json.dumps(market_data, indent=2)
+
+ return f"""You are a stock analyst. Analyze these US stocks for a Market-on-Close (MOC) overnight trading strategy.
+
+The strategy buys stocks at market close (3:50-4:00 PM ET) and sells at market open next day (9:30-10:00 AM ET).
+
+We want stocks that are likely to gap UP overnight. Good candidates have:
+1. Bullish daily candle (closed above open)
+2. Volume above average (vol_ratio > 1.0)
+3. Positive momentum (change_pct > 0)
+4. Not overextended (change_pct < 5% — avoid chasing)
+5. Moderate volatility (range_5d_pct between 3-15%)
+
+Market data:
+{data_str}
+
+Analyze each stock and return a JSON array of your top recommendations, ranked by confidence.
+Format:
+```json
+[
+ {{
+ "symbol": "AAPL",
+ "score": 85,
+ "reason": "Bullish candle on above-average volume, moderate momentum"
+ }}
+]
+```
+
+Only include stocks with score >= 60. If no stocks qualify, return an empty array.
+Return ONLY the JSON array, no other text."""
+
+
+async def analyze_with_claude(prompt: str, api_key: str) -> list[dict]:
+ """Send prompt to Claude and parse response."""
+ try:
+ import anthropic
+
+ client = anthropic.Anthropic(api_key=api_key)
+ message = client.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=1024,
+ messages=[{"role": "user", "content": prompt}],
+ )
+
+ response_text = message.content[0].text.strip()
+
+ # Extract JSON from response (handle markdown code blocks)
+ if "```json" in response_text:
+ response_text = response_text.split("```json")[1].split("```")[0].strip()
+ elif "```" in response_text:
+ response_text = response_text.split("```")[1].split("```")[0].strip()
+
+ return json.loads(response_text)
+
+ except ImportError:
+ print("Error: anthropic package not installed. Run: pip install anthropic")
+ return []
+ except json.JSONDecodeError as e:
+ print(f"Error: Failed to parse Claude response: {e}")
+ print(f"Response was: {response_text[:500]}")
+ return []
+ except Exception as e:
+ print(f"Error: Claude API call failed: {e}")
+ return []
+
+
+# Default universe of liquid US stocks
+DEFAULT_UNIVERSE = [
+ # Tech
+ "AAPL", "MSFT", "GOOGL", "AMZN", "META", "NVDA", "TSLA", "AMD", "INTC", "CRM",
+ # Finance
+ "JPM", "BAC", "GS", "MS", "V", "MA",
+ # Healthcare
+ "JNJ", "UNH", "PFE", "ABBV",
+ # Consumer
+ "WMT", "KO", "PEP", "MCD", "NKE",
+ # Energy
+ "XOM", "CVX",
+ # Industrial
+ "CAT", "BA", "GE",
+ # ETFs
+ "SPY", "QQQ", "IWM",
+]
+
+
+async def main_async(top_n: int = 5, universe: list[str] | None = None):
+ api_key = os.environ.get("ANTHROPIC_API_KEY", "")
+ alpaca_key = os.environ.get("ALPACA_API_KEY", "")
+ alpaca_secret = os.environ.get("ALPACA_API_SECRET", "")
+
+ if not alpaca_key:
+ print("Error: ALPACA_API_KEY not set")
+ sys.exit(1)
+
+ symbols = universe or DEFAULT_UNIVERSE
+
+ print("=" * 60)
+ print("Daily Stock Screener — MOC Strategy")
+ print(f"Date: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}")
+ print(f"Universe: {len(symbols)} stocks")
+ print("=" * 60)
+
+ # Fetch market data
+ print("\nFetching market data from Alpaca...")
+ alpaca = AlpacaClient(api_key=alpaca_key, api_secret=alpaca_secret, paper=True)
+
+ try:
+ market_data = await get_market_data(alpaca, symbols)
+ finally:
+ await alpaca.close()
+
+ if not market_data:
+ print("No market data available. Market may be closed.")
+ return
+
+ print(f"Got data for {len(market_data)} stocks\n")
+
+ # Pre-filter obvious rejects
+ candidates = [
+ s for s in market_data
+ if s["is_bullish"] and s["vol_ratio"] >= 0.8 and s["change_pct"] > -2
+ ]
+ print(f"Pre-filter: {len(candidates)} candidates (bullish, decent volume)\n")
+
+ if not candidates:
+ print("No candidates passed pre-filter.")
+ return
+
+ # Claude analysis
+ if api_key:
+ print("Analyzing with Claude...")
+ prompt = build_claude_prompt(candidates)
+ recommendations = await analyze_with_claude(prompt, api_key)
+
+ if recommendations:
+ print(f"\nClaude's Top {min(top_n, len(recommendations))} Picks:")
+ print("-" * 60)
+ for i, rec in enumerate(recommendations[:top_n], 1):
+ print(f"\n#{i} {rec['symbol']} (Score: {rec['score']})")
+ print(f" {rec['reason']}")
+ # Find market data for this symbol
+ md = next((m for m in market_data if m["symbol"] == rec["symbol"]), None)
+ if md:
+ print(f" Close: ${md['close']:.2f} | Change: {md['change_pct']:+.2f}% | Vol Ratio: {md['vol_ratio']:.1f}x")
+ else:
+ print("Claude found no qualifying stocks today.")
+ else:
+ print("ANTHROPIC_API_KEY not set — showing technical screening only\n")
+ # Fallback: simple technical screening
+ scored = []
+ for s in candidates:
+ score = 0
+ if s["change_pct"] > 0:
+ score += 30
+ if s["vol_ratio"] > 1.2:
+ score += 25
+ if s["is_bullish"]:
+ score += 20
+ if 3 < s["range_5d_pct"] < 15:
+ score += 15
+ if s["change_pct"] < 5:
+ score += 10
+ scored.append((s, score))
+
+ scored.sort(key=lambda x: x[1], reverse=True)
+ print(f"Technical Screening Top {top_n}:")
+ print("-" * 60)
+ for i, (s, score) in enumerate(scored[:top_n], 1):
+ print(f"#{i} {s['symbol']} (Score: {score})")
+ print(f" Close: ${s['close']:.2f} | Change: {s['change_pct']:+.2f}% | Vol: {s['vol_ratio']:.1f}x")
+
+ print("\n" + "=" * 60)
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Daily stock screener for MOC strategy")
+ parser.add_argument("--top", type=int, default=5, help="Number of top picks")
+ args = parser.parse_args()
+ asyncio.run(main_async(top_n=args.top))
+
+
+if __name__ == "__main__":
+ main()