Initial commit with translated description

This commit is contained in:
2026-03-29 14:34:25 +08:00
commit 7d03078316
17 changed files with 7879 additions and 0 deletions

442
App-Plan.md Normal file
View File

@@ -0,0 +1,442 @@
# StockPulse - Commercial Product Roadmap
## Vision
Transform the stock-analysis skill into **StockPulse**, a commercial mobile app for retail investors with AI-powered stock and crypto analysis, portfolio tracking, and personalized alerts.
## Technical Decisions
- **Mobile:** Flutter (iOS + Android cross-platform)
- **Backend:** Python FastAPI on AWS (ECS/Lambda)
- **Database:** PostgreSQL (RDS) + Redis (ElastiCache)
- **Auth:** AWS Cognito or Firebase Auth
- **Monetization:** Freemium + Subscription ($9.99/mo or $79.99/yr)
---
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────┐
│ MOBILE APP (Flutter) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Dashboard │ │Portfolio │ │ Analysis │ │ Alerts │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
│ HTTPS/REST
┌─────────────────────────────────────────────────────────────────┐
│ API GATEWAY (AWS) │
│ Rate Limiting, Auth, Caching │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ BACKEND (FastAPI on ECS) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Auth Service │ │ Analysis API │ │ Portfolio API│ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Alerts Svc │ │ Subscription │ │ User Service │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ PostgreSQL │ │ Redis │ │ S3 │
│ (RDS) │ │ (ElastiCache)│ │ (Reports) │
└──────────────┘ └──────────────┘ └──────────────┘
BACKGROUND WORKERS (Lambda/ECS)
┌─────────────────────────────────────────────────────────────────┐
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │Price Updater │ │Alert Checker │ │Daily Reports │ │
│ │ (5 min) │ │ (1 min) │ │ (Daily) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
## Feature Tiers
### Free Tier
- 1 portfolio (max 10 assets)
- Basic stock/crypto analysis
- Daily market summary
- Limited to 5 analyses/day
- Ads displayed
### Premium ($9.99/mo)
- Unlimited portfolios & assets
- Full 8-dimension analysis
- Real-time price alerts
- Push notifications
- Period reports (daily/weekly/monthly)
- No ads
- Priority support
### Pro ($19.99/mo) - Future
- API access
- Custom watchlists
- Advanced screeners
- Export to CSV/PDF
- Portfolio optimization suggestions
---
## Development Phases
### Phase 1: Backend API
**Goal:** Convert Python scripts to production REST API
#### Tasks:
1. **Project Setup**
- FastAPI project structure
- Docker containerization
- CI/CD pipeline (GitHub Actions)
- AWS infrastructure (Terraform)
2. **Core API Endpoints**
```
POST /auth/register
POST /auth/login
POST /auth/refresh
GET /analysis/{ticker}
POST /analysis/batch
GET /portfolios
POST /portfolios
PUT /portfolios/{id}
DELETE /portfolios/{id}
GET /portfolios/{id}/assets
POST /portfolios/{id}/assets
PUT /portfolios/{id}/assets/{ticker}
DELETE /portfolios/{id}/assets/{ticker}
GET /portfolios/{id}/performance?period=weekly
GET /portfolios/{id}/summary
GET /alerts
POST /alerts
DELETE /alerts/{id}
GET /user/subscription
POST /user/subscription/upgrade
```
3. **Database Schema**
```sql
users (id, email, password_hash, created_at, subscription_tier)
portfolios (id, user_id, name, created_at, updated_at)
assets (id, portfolio_id, ticker, asset_type, quantity, cost_basis)
alerts (id, user_id, ticker, condition, threshold, enabled)
analysis_cache (ticker, data, expires_at)
subscriptions (id, user_id, stripe_id, status, expires_at)
```
4. **Refactor Existing Code**
- Extract `analyze_stock.py` into modules:
- `analysis/earnings.py`
- `analysis/fundamentals.py`
- `analysis/sentiment.py`
- `analysis/crypto.py`
- `analysis/market_context.py`
- Add async support throughout
- Implement proper caching (Redis)
- Rate limiting per user tier
#### Files to Create:
```
backend/
├── app/
│ ├── main.py # FastAPI app
│ ├── config.py # Settings
│ ├── models/ # SQLAlchemy models
│ ├── schemas/ # Pydantic schemas
│ ├── routers/ # API routes
│ │ ├── auth.py
│ │ ├── analysis.py
│ │ ├── portfolios.py
│ │ └── alerts.py
│ ├── services/ # Business logic
│ │ ├── analysis/ # Refactored from analyze_stock.py
│ │ ├── portfolio.py
│ │ └── alerts.py
│ └── workers/ # Background tasks
├── tests/
├── Dockerfile
├── docker-compose.yml
└── requirements.txt
```
---
### Phase 2: Flutter Mobile App
**Goal:** Build polished cross-platform mobile app
#### Screens:
1. **Onboarding** - Welcome, feature highlights, sign up/login
2. **Dashboard** - Market overview, portfolio summary, alerts
3. **Analysis** - Search ticker, view full analysis, save to portfolio
4. **Portfolio** - List portfolios, asset breakdown, P&L chart
5. **Alerts** - Manage price alerts, notification settings
6. **Settings** - Account, subscription, preferences
#### Key Flutter Packages:
```yaml
dependencies:
flutter_bloc: ^8.0.0 # State management
dio: ^5.0.0 # HTTP client
go_router: ^12.0.0 # Navigation
fl_chart: ^0.65.0 # Charts
firebase_messaging: ^14.0.0 # Push notifications
in_app_purchase: ^3.0.0 # Subscriptions
shared_preferences: ^2.0.0
flutter_secure_storage: ^9.0.0
```
#### App Structure:
```
lib/
├── main.dart
├── app/
│ ├── routes.dart
│ └── theme.dart
├── features/
│ ├── auth/
│ │ ├── bloc/
│ │ ├── screens/
│ │ └── widgets/
│ ├── dashboard/
│ ├── analysis/
│ ├── portfolio/
│ ├── alerts/
│ └── settings/
├── core/
│ ├── api/
│ ├── models/
│ └── utils/
└── shared/
└── widgets/
```
---
### Phase 3: Infrastructure & DevOps
**Goal:** Production-ready cloud infrastructure
#### AWS Services:
- **ECS Fargate** - Backend containers
- **RDS PostgreSQL** - Database
- **ElastiCache Redis** - Caching
- **S3** - Static assets, reports
- **CloudFront** - CDN
- **Cognito** - Authentication
- **SES** - Email notifications
- **SNS** - Push notifications
- **CloudWatch** - Monitoring
- **WAF** - Security
#### Terraform Modules:
```
infrastructure/
├── main.tf
├── variables.tf
├── modules/
│ ├── vpc/
│ ├── ecs/
│ ├── rds/
│ ├── elasticache/
│ └── cognito/
└── environments/
├── dev/
├── staging/
└── prod/
```
#### Estimated Monthly Costs (Production):
| Service | Est. Cost |
|---------|-----------|
| ECS Fargate (2 tasks) | $50-100 |
| RDS (db.t3.small) | $30-50 |
| ElastiCache (cache.t3.micro) | $15-25 |
| S3 + CloudFront | $10-20 |
| Other (Cognito, SES, etc.) | $20-30 |
| **Total** | **$125-225/mo** |
---
### Phase 4: Payments & Subscriptions
**Goal:** Integrate Stripe for subscriptions
#### Implementation:
1. Stripe subscription products (Free, Premium, Pro)
2. In-app purchase for iOS/Android
3. Webhook handlers for subscription events
4. Grace period handling
5. Receipt validation
#### Stripe Integration:
```python
# Backend webhook handler
@router.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
event = stripe.Webhook.construct_event(...)
if event.type == "customer.subscription.updated":
update_user_tier(event.data.object)
elif event.type == "customer.subscription.deleted":
downgrade_to_free(event.data.object)
```
---
### Phase 5: Push Notifications & Alerts
**Goal:** Real-time price alerts and notifications
#### Alert Types:
- Price above/below threshold
- Percentage change (daily)
- Earnings announcement
- Breaking news (geopolitical)
- Portfolio performance
#### Implementation:
- Firebase Cloud Messaging (FCM)
- Background worker checks alerts every minute
- Rate limit: max 10 alerts/day per free user
---
### Phase 6: Analytics & Monitoring
**Goal:** Track usage, errors, business metrics
#### Tools:
- **Mixpanel/Amplitude** - Product analytics
- **Sentry** - Error tracking
- **CloudWatch** - Infrastructure metrics
- **Custom dashboard** - Business KPIs
#### Key Metrics:
- DAU/MAU
- Conversion rate (free → premium)
- Churn rate
- API response times
- Analysis accuracy feedback
---
## Security Considerations
1. **Authentication**
- JWT tokens with refresh rotation
- OAuth2 (Google, Apple Sign-In)
- 2FA optional for premium users
2. **Data Protection**
- Encrypt PII at rest (RDS encryption)
- TLS 1.3 for all API traffic
- No plaintext passwords
3. **API Security**
- Rate limiting per tier
- Input validation (Pydantic)
- SQL injection prevention (SQLAlchemy ORM)
- CORS configuration
4. **Compliance**
- Privacy policy
- Terms of service
- GDPR data export/deletion
- Financial disclaimer (not investment advice)
---
## Risks & Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Yahoo Finance rate limits | High | Implement caching, use paid API fallback |
| App store rejection | Medium | Follow guidelines, proper disclaimers |
| Data accuracy issues | High | Clear disclaimers, data validation |
| Security breach | Critical | Security audit, penetration testing |
| Low conversion rate | Medium | A/B testing, feature gating |
---
## Success Metrics (Year 1)
| Metric | Target |
|--------|--------|
| App downloads | 10,000+ |
| DAU | 1,000+ |
| Premium subscribers | 500+ |
| Monthly revenue | $5,000+ |
| App store rating | 4.5+ stars |
| Churn rate | <5%/month |
---
## Next Steps (Immediate)
1. **Validate idea** - User interviews, landing page
2. **Design** - Figma mockups for key screens
3. **Backend MVP** - Core API endpoints
4. **Flutter prototype** - Basic app with analysis feature
5. **Beta testing** - TestFlight/Google Play beta
---
## Repository Structure (Final)
```
stockpulse/
├── backend/ # FastAPI backend
│ ├── app/
│ ├── tests/
│ ├── Dockerfile
│ └── requirements.txt
├── mobile/ # Flutter app
│ ├── lib/
│ ├── test/
│ ├── ios/
│ ├── android/
│ └── pubspec.yaml
├── infrastructure/ # Terraform
│ ├── modules/
│ └── environments/
├── docs/ # Documentation
│ ├── api/
│ └── architecture/
└── scripts/ # Utility scripts
```
---
## Timeline Summary (Planning Only)
| Phase | Duration | Dependencies |
|-------|----------|--------------|
| 1. Backend API | 4-6 weeks | - |
| 2. Flutter App | 6-8 weeks | Phase 1 |
| 3. Infrastructure | 2-3 weeks | Phase 1 |
| 4. Payments | 2 weeks | Phase 2, 3 |
| 5. Notifications | 2 weeks | Phase 2, 3 |
| 6. Analytics | 1 week | Phase 2 |
| **Total** | **17-22 weeks** | |
This is a planning document. No fixed timeline - execute phases as resources allow.
---
**Disclaimer:** This tool is for informational purposes only and does NOT constitute financial advice.

214
README.md Normal file
View File

@@ -0,0 +1,214 @@
# 📈 Stock Analysis v6.1
> AI-powered stock & crypto analysis with portfolio management, watchlists, dividend analysis, and **viral trend detection**.
[![ClawHub Downloads](https://img.shields.io/badge/ClawHub-1500%2B%20downloads-blue)](https://clawhub.ai)
[![OpenClaw Skill](https://img.shields.io/badge/OpenClaw-Skill-green)](https://openclaw.ai)
## What's New in v6.1
- 🔥 **Hot Scanner** — Find viral stocks & crypto across multiple sources
- 🐦 **Twitter/X Integration** — Social sentiment via bird CLI
- 📰 **Multi-Source Aggregation** — CoinGecko, Google News, Yahoo Finance
-**Cron Support** — Daily trend reports
## What's New in v6.0
- 🆕 **Watchlist + Alerts** — Price targets, stop losses, signal change notifications
- 🆕 **Dividend Analysis** — Yield, payout ratio, growth rate, safety score
- 🆕 **Fast Mode** — Skip slow analyses for quick checks
- 🆕 **Improved Commands** — Better OpenClaw/Telegram integration
- 🆕 **Test Suite** — Unit tests for core functionality
## Features
| Feature | Description |
|---------|-------------|
| **8-Dimension Analysis** | Earnings, fundamentals, analysts, momentum, sentiment, sector, market, history |
| **Crypto Support** | Top 20 cryptos with market cap, BTC correlation, momentum |
| **Portfolio Management** | Track holdings, P&L, concentration warnings |
| **Watchlist + Alerts** | Price targets, stop losses, signal changes |
| **Dividend Analysis** | Yield, payout, growth, safety score |
| **Risk Detection** | Geopolitical, earnings timing, overbought, risk-off |
| **Breaking News** | Crisis keyword scanning (last 24h) |
## Quick Start
### Analyze Stocks
```bash
uv run scripts/analyze_stock.py AAPL
uv run scripts/analyze_stock.py AAPL MSFT GOOGL
uv run scripts/analyze_stock.py AAPL --fast # Skip slow analyses
```
### Analyze Crypto
```bash
uv run scripts/analyze_stock.py BTC-USD
uv run scripts/analyze_stock.py ETH-USD SOL-USD
```
### Dividend Analysis
```bash
uv run scripts/dividends.py JNJ PG KO
```
### Watchlist
```bash
uv run scripts/watchlist.py add AAPL --target 200 --stop 150
uv run scripts/watchlist.py list
uv run scripts/watchlist.py check --notify
```
### Portfolio
```bash
uv run scripts/portfolio.py create "My Portfolio"
uv run scripts/portfolio.py add AAPL --quantity 100 --cost 150
uv run scripts/portfolio.py show
```
### 🔥 Hot Scanner (NEW)
```bash
# Full scan with all sources
python3 scripts/hot_scanner.py
# Fast scan (skip social media)
python3 scripts/hot_scanner.py --no-social
# JSON output for automation
python3 scripts/hot_scanner.py --json
```
## Analysis Dimensions
### Stocks (8 dimensions)
1. **Earnings Surprise** (30%) — EPS beat/miss
2. **Fundamentals** (20%) — P/E, margins, growth, debt
3. **Analyst Sentiment** (20%) — Ratings, price targets
4. **Historical Patterns** (10%) — Past earnings reactions
5. **Market Context** (10%) — VIX, SPY/QQQ trends
6. **Sector Performance** (15%) — Relative strength
7. **Momentum** (15%) — RSI, 52-week range
8. **Sentiment** (10%) — Fear/Greed, shorts, insiders
### Crypto (3 dimensions)
- Market Cap & Category
- BTC Correlation (30-day)
- Momentum (RSI, range)
## Dividend Metrics
| Metric | Description |
|--------|-------------|
| Yield | Annual dividend / price |
| Payout Ratio | Dividend / EPS |
| 5Y Growth | CAGR of dividend |
| Consecutive Years | Years of increases |
| Safety Score | 0-100 composite |
| Income Rating | Excellent → Poor |
## 🔥 Hot Scanner
Find what's trending RIGHT NOW across stocks & crypto.
### Data Sources
| Source | What it finds |
|--------|---------------|
| **CoinGecko Trending** | Top 15 trending coins |
| **CoinGecko Movers** | Biggest gainers/losers (>3%) |
| **Google News** | Breaking finance & crypto news |
| **Yahoo Finance** | Top gainers, losers, most active |
| **Twitter/X** | Social sentiment (requires auth) |
### Output
```
📊 TOP TRENDING (by buzz):
1. BTC (6 pts) [CoinGecko, Google News] 📉 bearish (-2.5%)
2. ETH (5 pts) [CoinGecko, Twitter] 📉 bearish (-7.2%)
3. NVDA (3 pts) [Google News, Yahoo] 📰 Earnings beat...
🪙 CRYPTO HIGHLIGHTS:
🚀 RIVER River +14.0%
📉 BTC Bitcoin -2.5%
📈 STOCK MOVERS:
🟢 NVDA (gainers)
🔴 TSLA (losers)
📰 BREAKING NEWS:
[BTC, ETH] Crypto crash: $2.5B liquidated...
```
### Twitter/X Setup (Optional)
1. Install bird CLI: `npm install -g @steipete/bird`
2. Login to x.com in Safari/Chrome
3. Create `.env` file:
```
AUTH_TOKEN=your_auth_token
CT0=your_ct0_token
```
Get tokens from browser DevTools → Application → Cookies → x.com
### Automation
Set up a daily cron job for morning reports:
```bash
# Run at 8 AM daily
0 8 * * * python3 /path/to/hot_scanner.py --no-social >> /var/log/hot_scanner.log
```
## Risk Detection
- ⚠️ Pre-earnings warning (< 14 days)
- ⚠️ Post-earnings spike (> 15% in 5 days)
- ⚠️ Overbought (RSI > 70 + near 52w high)
- ⚠️ Risk-off mode (GLD/TLT/UUP rising)
- ⚠️ Geopolitical keywords (Taiwan, China, etc.)
- ⚠️ Breaking news alerts
## Performance Options
| Flag | Speed | Description |
|------|-------|-------------|
| (default) | 5-10s | Full analysis |
| `--no-insider` | 3-5s | Skip SEC EDGAR |
| `--fast` | 2-3s | Skip insider + news |
## Data Sources
- [Yahoo Finance](https://finance.yahoo.com) — Prices, fundamentals, movers
- [CoinGecko](https://coingecko.com) — Crypto trending, market data
- [CNN Fear & Greed](https://money.cnn.com/data/fear-and-greed/) — Sentiment
- [SEC EDGAR](https://www.sec.gov/edgar) — Insider trading
- [Google News RSS](https://news.google.com) — Breaking news
- [Twitter/X](https://x.com) — Social sentiment (via bird CLI)
## Storage
| Data | Location |
|------|----------|
| Portfolios | `~/.clawdbot/skills/stock-analysis/portfolios.json` |
| Watchlist | `~/.clawdbot/skills/stock-analysis/watchlist.json` |
## Testing
```bash
uv run pytest scripts/test_stock_analysis.py -v
```
## Limitations
- Yahoo Finance may lag 15-20 minutes
- Short interest lags ~2 weeks (FINRA)
- US markets only
## Disclaimer
⚠️ **NOT FINANCIAL ADVICE.** For informational purposes only. Consult a licensed financial advisor before making investment decisions.
---
Built for [OpenClaw](https://openclaw.ai) 🦞 | [ClawHub](https://clawhub.ai)

248
SKILL.md Normal file
View File

@@ -0,0 +1,248 @@
---
name: stock-analysis
description: "使用Yahoo Finance数据股票和加密货币分析。"
version: 6.2.0
homepage: https://finance.yahoo.com
commands:
- /stock - Analyze a stock or crypto (e.g., /stock AAPL)
- /stock_compare - Compare multiple tickers
- /stock_dividend - Analyze dividend metrics
- /stock_watch - Add/remove from watchlist
- /stock_alerts - Check triggered alerts
- /stock_hot - Find trending stocks & crypto (Hot Scanner)
- /stock_rumors - Find early signals, M&A rumors, insider activity (Rumor Scanner)
- /portfolio - Show portfolio summary
- /portfolio_add - Add asset to portfolio
metadata: {"clawdbot":{"emoji":"📈","requires":{"bins":["uv"],"env":[]},"install":[{"id":"uv-brew","kind":"brew","formula":"uv","bins":["uv"],"label":"Install uv (brew)"}]}}
---
# Stock Analysis v6.1
Analyze US stocks and cryptocurrencies with 8-dimension analysis, portfolio management, watchlists, alerts, dividend analysis, and **viral trend detection**.
## What's New in v6.2
- 🔮 **Rumor Scanner** — Early signals before mainstream news
- M&A rumors and takeover bids
- Insider buying/selling activity
- Analyst upgrades/downgrades
- Twitter/X "hearing that...", "sources say..." detection
- 🎯 **Impact Scoring** — Rumors ranked by potential market impact
## What's in v6.1
- 🔥 **Hot Scanner** — Find viral stocks & crypto across multiple sources
- 🐦 **Twitter/X Integration** — Social sentiment via bird CLI
- 📰 **Multi-Source Aggregation** — CoinGecko, Google News, Yahoo Finance
-**Cron Support** — Daily trend reports
## What's in v6.0
- 🆕 **Watchlist + Alerts** — Price targets, stop losses, signal changes
- 🆕 **Dividend Analysis** — Yield, payout ratio, growth, safety score
- 🆕 **Fast Mode**`--fast` skips slow analyses (insider, news)
- 🆕 **Improved Performance**`--no-insider` for faster runs
## Quick Commands
### Stock Analysis
```bash
# Basic analysis
uv run {baseDir}/scripts/analyze_stock.py AAPL
# Fast mode (skips insider trading & breaking news)
uv run {baseDir}/scripts/analyze_stock.py AAPL --fast
# Compare multiple
uv run {baseDir}/scripts/analyze_stock.py AAPL MSFT GOOGL
# Crypto
uv run {baseDir}/scripts/analyze_stock.py BTC-USD ETH-USD
```
### Dividend Analysis (NEW v6.0)
```bash
# Analyze dividends
uv run {baseDir}/scripts/dividends.py JNJ
# Compare dividend stocks
uv run {baseDir}/scripts/dividends.py JNJ PG KO MCD --output json
```
**Dividend Metrics:**
- Dividend Yield & Annual Payout
- Payout Ratio (safe/moderate/high/unsustainable)
- 5-Year Dividend Growth (CAGR)
- Consecutive Years of Increases
- Safety Score (0-100)
- Income Rating (excellent/good/moderate/poor)
### Watchlist + Alerts (NEW v6.0)
```bash
# Add to watchlist
uv run {baseDir}/scripts/watchlist.py add AAPL
# With price target alert
uv run {baseDir}/scripts/watchlist.py add AAPL --target 200
# With stop loss alert
uv run {baseDir}/scripts/watchlist.py add AAPL --stop 150
# Alert on signal change (BUY→SELL)
uv run {baseDir}/scripts/watchlist.py add AAPL --alert-on signal
# View watchlist
uv run {baseDir}/scripts/watchlist.py list
# Check for triggered alerts
uv run {baseDir}/scripts/watchlist.py check
uv run {baseDir}/scripts/watchlist.py check --notify # Telegram format
# Remove from watchlist
uv run {baseDir}/scripts/watchlist.py remove AAPL
```
**Alert Types:**
- 🎯 **Target Hit** — Price >= target
- 🛑 **Stop Hit** — Price <= stop
- 📊 **Signal Change** — BUY/HOLD/SELL changed
### Portfolio Management
```bash
# Create portfolio
uv run {baseDir}/scripts/portfolio.py create "Tech Portfolio"
# Add assets
uv run {baseDir}/scripts/portfolio.py add AAPL --quantity 100 --cost 150
uv run {baseDir}/scripts/portfolio.py add BTC-USD --quantity 0.5 --cost 40000
# View portfolio
uv run {baseDir}/scripts/portfolio.py show
# Analyze with period returns
uv run {baseDir}/scripts/analyze_stock.py --portfolio "Tech Portfolio" --period weekly
```
### 🔥 Hot Scanner (NEW v6.1)
```bash
# Full scan - find what's trending NOW
python3 {baseDir}/scripts/hot_scanner.py
# Fast scan (skip social media)
python3 {baseDir}/scripts/hot_scanner.py --no-social
# JSON output for automation
python3 {baseDir}/scripts/hot_scanner.py --json
```
**Data Sources:**
- 📊 CoinGecko Trending — Top 15 trending coins
- 📈 CoinGecko Movers — Biggest gainers/losers
- 📰 Google News — Finance & crypto headlines
- 📉 Yahoo Finance — Gainers, losers, most active
- 🐦 Twitter/X — Social sentiment (requires auth)
**Output:**
- Top trending by mention count
- Crypto highlights with 24h changes
- Stock movers by category
- Breaking news with tickers
**Twitter Setup (Optional):**
1. Install bird: `npm install -g @steipete/bird`
2. Login to x.com in Safari/Chrome
3. Create `.env` with `AUTH_TOKEN` and `CT0`
### 🔮 Rumor Scanner (NEW v6.2)
```bash
# Find early signals, M&A rumors, insider activity
python3 {baseDir}/scripts/rumor_scanner.py
```
**What it finds:**
- 🏢 **M&A Rumors** — Merger, acquisition, takeover bids
- 👔 **Insider Activity** — CEO/Director buying/selling
- 📊 **Analyst Actions** — Upgrades, downgrades, price target changes
- 🐦 **Twitter Whispers** — "hearing that...", "sources say...", "rumor"
- ⚖️ **SEC Activity** — Investigations, filings
**Impact Scoring:**
- Each rumor is scored by potential market impact (1-10)
- M&A/Takeover: +5 points
- Insider buying: +4 points
- Upgrade/Downgrade: +3 points
- "Hearing"/"Sources say": +2 points
- High engagement: +2 bonus
**Best Practice:** Run at 07:00 before US market open to catch pre-market signals.
## Analysis Dimensions (8 for stocks, 3 for crypto)
### Stocks
| Dimension | Weight | Description |
|-----------|--------|-------------|
| Earnings Surprise | 30% | EPS beat/miss |
| Fundamentals | 20% | P/E, margins, growth |
| Analyst Sentiment | 20% | Ratings, price targets |
| Historical | 10% | Past earnings reactions |
| Market Context | 10% | VIX, SPY/QQQ trends |
| Sector | 15% | Relative strength |
| Momentum | 15% | RSI, 52-week range |
| Sentiment | 10% | Fear/Greed, shorts, insiders |
### Crypto
- Market Cap & Category
- BTC Correlation (30-day)
- Momentum (RSI, range)
## Sentiment Sub-Indicators
| Indicator | Source | Signal |
|-----------|--------|--------|
| Fear & Greed | CNN | Contrarian (fear=buy) |
| Short Interest | Yahoo | Squeeze potential |
| VIX Structure | Futures | Stress detection |
| Insider Trades | SEC EDGAR | Smart money |
| Put/Call Ratio | Options | Sentiment extreme |
## Risk Detection
- ⚠️ **Pre-Earnings** — Warns if < 14 days to earnings
- ⚠️ **Post-Spike** — Flags if up >15% in 5 days
- ⚠️ **Overbought** — RSI >70 + near 52w high
- ⚠️ **Risk-Off** — GLD/TLT/UUP rising together
- ⚠️ **Geopolitical** — Taiwan, China, Russia, Middle East keywords
- ⚠️ **Breaking News** — Crisis keywords in last 24h
## Performance Options
| Flag | Effect | Speed |
|------|--------|-------|
| (default) | Full analysis | 5-10s |
| `--no-insider` | Skip SEC EDGAR | 3-5s |
| `--fast` | Skip insider + news | 2-3s |
## Supported Cryptos (Top 20)
BTC, ETH, BNB, SOL, XRP, ADA, DOGE, AVAX, DOT, MATIC, LINK, ATOM, UNI, LTC, BCH, XLM, ALGO, VET, FIL, NEAR
(Use `-USD` suffix: `BTC-USD`, `ETH-USD`)
## Data Storage
| File | Location |
|------|----------|
| Portfolios | `~/.clawdbot/skills/stock-analysis/portfolios.json` |
| Watchlist | `~/.clawdbot/skills/stock-analysis/watchlist.json` |
## Limitations
- Yahoo Finance may lag 15-20 minutes
- Short interest lags ~2 weeks (FINRA)
- Insider trades lag 2-3 days (SEC filing)
- US markets only (non-US incomplete)
- Breaking news: 1h cache, keyword-based
## Disclaimer
⚠️ **NOT FINANCIAL ADVICE.** For informational purposes only. Consult a licensed financial advisor before making investment decisions.

394
TODO.md Normal file
View File

@@ -0,0 +1,394 @@
# Stock Analysis - Future Enhancements
## Roadmap Overview
### v4.0.0 (Current) - Geopolitical Risk & News Sentiment
✅ 8 analysis dimensions with Fear/Greed, short interest, VIX structure, put/call ratio
✅ Safe-haven indicators (GLD, TLT, UUP) with risk-off detection
✅ Breaking news alerts via Google News RSS
✅ Geopolitical risk mapping (Taiwan, China, Russia, Middle East, Banking)
✅ Sector-specific crisis flagging with confidence penalties
✅ 1h caching for shared indicators (Fear/Greed, VIX structure, breaking news)
✅ Async parallel sentiment fetching (5 indicators with 10s timeouts)
### v5.0.0 (Current) - Portfolio & Crypto
✅ Portfolio management (create, add, remove, show assets)
✅ Cryptocurrency support (Top 20 by market cap)
✅ Portfolio analysis with --portfolio flag
✅ Periodic returns (--period daily/weekly/monthly/quarterly/yearly)
✅ Concentration warnings (>30% single asset)
✅ Crypto fundamentals (market cap, category, BTC correlation)
### v4.1.0 - Performance & Completeness
✅ Full insider trading parsing via edgartools (Task #1)
✅ Market context caching with 1h TTL (Task #3b)
🔧 SEC EDGAR rate limit monitoring (Task #4 - low priority)
### Future (v6.0+)
💡 Research phase: Social sentiment, fund flows, on-chain metrics
---
## Sentiment Analysis Improvements
### 1. Implement Full Insider Trading Parsing
**Status**: ✅ DONE
**Priority**: Medium
**Effort**: 2-3 hours
**Current State**:
-`get_insider_activity()` fetches Form 4 filings via edgartools
- ✅ SEC identity configured (`stock-analysis@clawd.bot`)
- ✅ Aggregates buys/sells over 90-day window
- ✅ Scoring logic: strong buying (+0.8), moderate (+0.4), neutral (0), moderate selling (-0.4), strong (-0.8)
**Tasks**:
- [ ] Research edgartools API for Form 4 parsing
- [ ] Implement transaction aggregation (90-day window)
- [ ] Calculate net shares bought/sold
- [ ] Calculate net value in millions USD
- [ ] Apply scoring logic:
- Strong buying (>100K shares or >$1M): +0.8
- Moderate buying (>10K shares or >$0.1M): +0.4
- Neutral: 0
- Moderate selling: -0.4
- Strong selling: -0.8
- [ ] Add error handling for missing/incomplete filings
- [ ] Test with multiple tickers (BAC, TSLA, AAPL)
- [ ] Verify SEC rate limit compliance (10 req/s)
**Expected Impact**:
- Insider activity detection for 4th sentiment indicator
- Increase from 3/5 to 4/5 indicators typically available
---
### 2. Add Parallel Async Fetching
**Status**: ✅ DONE (sentiment indicators)
**Priority**: High
**Effort**: 4-6 hours
**Current State**:
- ✅ Sentiment indicators fetched in parallel via `asyncio.gather()`
- ✅ 10s timeout per indicator
- Main data fetches (yfinance) still sequential (acceptable)
**Tasks**:
- [ ] Convert sentiment helper functions to async
- [ ] `async def get_fear_greed_index()`
- [ ] `async def get_short_interest(data)`
- [ ] `async def get_vix_term_structure()`
- [ ] `async def get_insider_activity(ticker)`
- [ ] `async def get_put_call_ratio(data)`
- [ ] Update `analyze_sentiment()` to use `asyncio.gather()`
- [ ] Handle yfinance thread safety (may need locks)
- [ ] Add timeout per indicator (10s max)
- [ ] Test with multiple stocks in sequence
- [ ] Measure actual runtime improvement
- [ ] Update SKILL.md with new runtime (target: 3-4s)
**Expected Impact**:
- Reduce runtime from 6-10s to 3-4s per stock
- Better user experience for multi-stock analysis
---
### 3. Add Caching for Shared Indicators
**Status**: ✅ DONE (sentiment + breaking news)
**Priority**: Medium
**Effort**: 2-3 hours
**Current State**:
- ✅ Fear & Greed Index cached (1h TTL)
- ✅ VIX term structure cached (1h TTL)
- ✅ Breaking news cached (1h TTL)
- ✅ Market context (VIX/SPY/QQQ/GLD/TLT/UUP) cached (1h TTL)
**Tasks**:
- [ ] Design cache structure (simple dict or functools.lru_cache)
- [ ] Implement TTL (time-to-live):
- Fear & Greed: 1 hour
- VIX structure: 1 hour
- Short interest: No cache (per-stock)
- Insider activity: No cache (per-stock)
- Put/Call ratio: No cache (per-stock)
- [ ] Add cache invalidation logic
- [ ] Add verbose logging for cache hits/misses
- [ ] Test multi-stock analysis (e.g., `BAC TSLA AAPL`)
- [ ] Measure performance improvement
- [ ] Document caching behavior in SKILL.md
**Expected Impact**:
- Multi-stock analysis faster (e.g., 3 stocks: 18-30s → 10-15s)
- Reduced API calls to Fear/Greed and VIX data sources
- Same-session analysis efficiency
---
### 4. Monitor SEC EDGAR Rate Limits
**Status**: Not Started
**Priority**: Low (until insider trading implemented)
**Effort**: 1-2 hours
**Current State**:
- SEC EDGAR API has 10 requests/second rate limit
- No rate limit tracking or logging
- edgartools may handle rate limiting internally
**Tasks**:
- [ ] Research edgartools rate limit handling
- [ ] Add request counter/tracker if needed
- [ ] Implement exponential backoff on 429 errors
- [ ] Add logging for rate limit hits
- [ ] Test with high-volume scenarios (10+ stocks in quick succession)
- [ ] Document rate limit behavior
- [ ] Add error message if rate limited: "SEC API rate limited, try again in 1 minute"
**Expected Impact**:
- Robust handling of SEC API limits in production
- Clear user feedback if limits hit
- Prevent API blocking/banning
---
## Stock Analysis 4.0: Geopolitical Risk & News Sentiment
### What's Currently Missing
The current implementation captures:
- ✅ VIX (general market fear)
- ✅ SPY/QQQ trends (market direction)
- ✅ Sector performance
What we **don't** have yet:
- ❌ Geopolitical risk indicators
- ❌ News sentiment analysis
- ❌ Sector-specific crisis flags
---
### 7. Geopolitical Risk Index
**Status**: ✅ DONE (keyword-based)
**Priority**: High
**Effort**: 8-12 hours
**Proposed Approach**:
Option A: Use GPRD (Geopolitical Risk Daily Index) from policyuncertainty.com
Option B: Scan news APIs (NewsAPI, GDELT) for geopolitical keywords
**Tasks**:
- [ ] Research free geopolitical risk data sources
- [ ] Check policyuncertainty.com API availability
- [ ] Evaluate NewsAPI free tier limits
- [ ] Consider GDELT Project (free, comprehensive)
- [ ] Design risk scoring system (0-100 scale)
- [ ] Implement data fetching with caching (4-hour TTL)
- [ ] Map risk levels to sentiment scores:
- Low risk (0-30): +0.2 (bullish)
- Moderate risk (30-60): 0 (neutral)
- High risk (60-80): -0.3 (caution)
- Extreme risk (80-100): -0.5 (bearish)
- [ ] Add to sentiment analysis as 6th indicator
- [ ] Test with historical crisis periods
- [ ] Update SKILL.md with geopolitical indicator
**Expected Impact**:
- Early warning for market-wide risk events
- Better context for earnings-season volatility
- Complement to VIX (VIX is reactive, geopolitical is predictive)
**Example Output**:
```
⚠️ GEOPOLITICAL RISK: HIGH (72/100)
Context: Elevated Taiwan tensions detected
Market Impact: Risk-off sentiment likely
```
---
### 8. Sector-Specific Crisis Mapping
**Status**: ✅ DONE
**Priority**: High
**Effort**: 6-8 hours
**Current Gap**:
- No mapping between geopolitical events and affected sectors
- No automatic flagging of at-risk holdings
**Proposed Risk Mapping**:
| Geopolitical Event | Affected Sectors | Example Tickers |
|-------------------|------------------|-----------------|
| Taiwan conflict | Semiconductors | NVDA, AMD, TSM, INTC |
| Russia-Ukraine | Energy, Agriculture | XLE, MOS, CF, NTR |
| Middle East escalation | Oil, Defense | XOM, CVX, LMT, RTX |
| China tensions | Tech supply chain, Retail | AAPL, QCOM, NKE, SBUX |
| Banking crisis | Financials | JPM, BAC, WFC, C |
**Tasks**:
- [ ] Build event → sector → ticker mapping database
- [ ] Implement keyword detection in news feeds:
- "Taiwan" + "military" → Semiconductors ⚠️
- "Russia" + "sanctions" → Energy ⚠️
- "Iran" + "attack" → Oil, Defense ⚠️
- "China" + "tariffs" → Tech, Consumer ⚠️
- [ ] Add sector exposure check to analysis
- [ ] Generate automatic warnings in output
- [ ] Apply confidence penalty for high-risk sectors
- [ ] Test with historical crisis events
- [ ] Document in SKILL.md
**Expected Impact**:
- Automatic detection of sector-specific risks
- Clear warnings for exposed holdings
- Reduced false positives (only flag relevant sectors)
**Example Output**:
```
⚠️ SECTOR RISK ALERT: Semiconductors
Event: Taiwan military exercises (elevated tensions)
Impact: NVDA HIGH RISK - supply chain exposure
Recommendation: HOLD → downgraded from BUY
```
---
### 9. Breaking News Check
**Status**: ✅ DONE
**Priority**: Medium
**Effort**: 4-6 hours
**Current Gap**:
- No real-time news scanning before analysis
- User might get stale recommendation during breaking events
**Proposed Solution**:
- Scan Google News or Reuters RSS before analysis
- Flag high-impact keywords within last 24 hours
**Tasks**:
- [ ] Choose news source (Google News RSS, Reuters API, or NewsAPI)
- [ ] Implement news fetching with 24-hour lookback
- [ ] Define crisis keywords:
- **War/Conflict**: "war", "invasion", "military strike", "attack"
- **Economic**: "recession", "crisis", "collapse", "default"
- **Regulatory**: "sanctions", "embargo", "ban", "investigation"
- **Natural disaster**: "earthquake", "hurricane", "pandemic"
- [ ] Add ticker-specific news check (company name + keywords)
- [ ] Generate automatic caveat in output
- [ ] Cache news check results (1 hour TTL)
- [ ] Add `--skip-news` flag for offline mode
- [ ] Test with historical crisis dates
- [ ] Document in SKILL.md
**Expected Impact**:
- Real-time awareness of breaking events
- Automatic caveats during high volatility
- User protection from stale recommendations
**Example Output**:
```
⚠️ BREAKING NEWS ALERT (last 6 hours):
"Fed announces emergency rate hike"
Impact: Market-wide volatility expected
Caveat: Analysis may be outdated - rerun in 24h
```
---
### 10. Safe-Haven Indicators
**Status**: ✅ DONE
**Priority**: Medium
**Effort**: 3-4 hours
**Current Gap**:
- No detection of "risk-off" market regime
- VIX alone is insufficient (measures implied volatility, not capital flows)
**Proposed Indicators**:
- Gold (GLD) - Flight to safety
- US Treasuries (TLT) - Bond market fear
- USD Index (UUP) - Dollar strength during crisis
**Risk-Off Detection Logic**:
```
IF GLD +2% AND TLT +1% AND UUP +1% (all rising together)
THEN Market Regime = RISK-OFF
```
**Tasks**:
- [ ] Fetch GLD, TLT, UUP price data (5-day change)
- [ ] Implement risk-off detection algorithm
- [ ] Add to market context analysis
- [ ] Apply broad risk penalty:
- Risk-off detected → Reduce all BUY confidence by 30%
- Add caveat: "Market in risk-off mode - defensive positioning recommended"
- [ ] Test with historical crisis periods (2008, 2020, 2022)
- [ ] Add verbose output for safe-haven movements
- [ ] Document in SKILL.md
**Expected Impact**:
- Detect market-wide flight to safety
- Automatic risk reduction during panics
- Complement geopolitical risk scoring
**Example Output**:
```
🛡️ SAFE-HAVEN ALERT: Risk-off mode detected
- Gold (GLD): +3.2% (5d)
- Treasuries (TLT): +2.1% (5d)
- USD Index: +1.8% (5d)
Recommendation: Reduce equity exposure, favor defensives
```
---
## General Improvements
### 11. Add Social Sentiment (Future Phase)
**Status**: Deferred
**Priority**: Low
**Effort**: 8-12 hours
**Notes**:
- Requires free API (Twitter/Reddit alternatives?)
- Most sentiment APIs are paid (StockTwits, etc.)
- Research needed for viable free sources
### 12. Add Fund Flows (Future Phase)
**Status**: Deferred
**Priority**: Low
**Effort**: 6-8 hours
**Notes**:
- Requires ETF flow data
- May need paid data source
- Research free alternatives
---
## Implementation Priorities
### v4.1.0 Complete
- ✅ Task #1 - Insider trading parsing via edgartools
- ✅ Task #3b - Market context caching (1h TTL)
- 🔧 Task #4 - SEC EDGAR rate limits (low priority, only if hitting limits)
### Completed in v4.0.0
- ✅ Task #2 - Async parallel fetching (sentiment)
- ✅ Task #3 - Caching for shared indicators (sentiment + news)
- ✅ Task #7 - Geopolitical risk (keyword-based)
- ✅ Task #8 - Sector-specific crisis mapping
- ✅ Task #9 - Breaking news check
- ✅ Task #10 - Safe-haven indicators
---
## Version History
- **v5.0.0** (2026-01-16): Portfolio management, cryptocurrency support (Top 20), periodic analysis
- **v4.1.0** (2026-01-16): Full insider trading parsing via edgartools, market context caching
- **v4.0.0** (2026-01-15): Geopolitical risk, breaking news, safe-haven detection, sector crisis mapping
- **v3.0.0** (2026-01-15): Sentiment analysis added with 5 indicators (3-4 typically working)
- **v2.0.0**: Market context, sector performance, earnings timing, momentum
- **v1.0.0**: Initial release with earnings, fundamentals, analysts, historical

6
_meta.json Normal file
View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn77fv9851hjcqe52zqx0bhhbx7z680h",
"slug": "stock-analysis",
"version": "6.2.0",
"publishedAt": 1770041353575
}

408
docs/ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,408 @@
# Technical Architecture
How Stock Analysis v6.0 works under the hood.
## System Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ Stock Analysis v6.0 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ CLI Interface │ │
│ │ analyze_stock.py | dividends.py | watchlist.py | portfolio.py│ │
│ └────────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────▼─────────────────────────────────┐ │
│ │ Analysis Engine │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │Earnings │ │Fundmtls │ │Analysts │ │Historical│ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ │ │
│ │ │ Market │ │ Sector │ │Momentum │ │Sentiment│ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ │ │ │ │ │ │ │
│ │ └───────────┴───────────┴───────────┘ │ │
│ │ │ │ │
│ │ [Synthesizer] │ │
│ │ │ │ │
│ │ [Signal Output] │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────▼─────────────────────────────────┐ │
│ │ Data Sources │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Yahoo │ │ CNN │ │ SEC │ │ Google │ │ │
│ │ │ Finance │ │Fear/Grd │ │ EDGAR │ │ News │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Core Components
### 1. Data Fetching (`fetch_stock_data`)
```python
def fetch_stock_data(ticker: str, verbose: bool = False) -> StockData | None:
"""Fetch stock data from Yahoo Finance with retry logic."""
```
**Features:**
- 3 retries with exponential backoff
- Graceful handling of missing data
- Asset type detection (stock vs crypto)
**Returns:** `StockData` dataclass with:
- `info`: Company fundamentals
- `earnings_history`: Past earnings
- `analyst_info`: Ratings and targets
- `price_history`: 1-year OHLCV
### 2. Analysis Modules
Each dimension has its own analyzer:
| Module | Function | Returns |
|--------|----------|---------|
| Earnings | `analyze_earnings_surprise()` | `EarningsSurprise` |
| Fundamentals | `analyze_fundamentals()` | `Fundamentals` |
| Analysts | `analyze_analyst_sentiment()` | `AnalystSentiment` |
| Historical | `analyze_historical_patterns()` | `HistoricalPatterns` |
| Market | `analyze_market_context()` | `MarketContext` |
| Sector | `analyze_sector_performance()` | `SectorComparison` |
| Momentum | `analyze_momentum()` | `MomentumAnalysis` |
| Sentiment | `analyze_sentiment()` | `SentimentAnalysis` |
### 3. Sentiment Sub-Analyzers
Sentiment runs 5 parallel async tasks:
```python
results = await asyncio.gather(
get_fear_greed_index(), # CNN Fear & Greed
get_short_interest(data), # Yahoo Finance
get_vix_term_structure(), # VIX Futures
get_insider_activity(), # SEC EDGAR
get_put_call_ratio(data), # Options Chain
return_exceptions=True
)
```
**Timeout:** 10 seconds per indicator
**Minimum:** 2 of 5 indicators required
### 4. Signal Synthesis
```python
def synthesize_signal(
ticker, company_name,
earnings, fundamentals, analysts, historical,
market_context, sector, earnings_timing,
momentum, sentiment,
breaking_news, geopolitical_risk_warning, geopolitical_risk_penalty
) -> Signal:
```
**Scoring:**
1. Collect available component scores
2. Apply normalized weights
3. Calculate weighted average → `final_score`
4. Apply adjustments (timing, overbought, risk-off)
5. Determine recommendation threshold
**Thresholds:**
```python
if final_score > 0.33:
recommendation = "BUY"
elif final_score < -0.33:
recommendation = "SELL"
else:
recommendation = "HOLD"
```
---
## Caching Strategy
### What's Cached
| Data | TTL | Key |
|------|-----|-----|
| Market Context | 1 hour | `market_context` |
| Fear & Greed | 1 hour | `fear_greed` |
| VIX Structure | 1 hour | `vix_structure` |
| Breaking News | 1 hour | `breaking_news` |
### Cache Implementation
```python
_SENTIMENT_CACHE = {}
_CACHE_TTL_SECONDS = 3600 # 1 hour
def _get_cached(key: str):
if key in _SENTIMENT_CACHE:
value, timestamp = _SENTIMENT_CACHE[key]
if time.time() - timestamp < _CACHE_TTL_SECONDS:
return value
return None
def _set_cache(key: str, value):
_SENTIMENT_CACHE[key] = (value, time.time())
```
### Why This Matters
- First stock: ~8 seconds (full fetch)
- Second stock: ~4 seconds (reuses market data)
- Same stock again: ~4 seconds (no stock-level cache)
---
## Data Flow
### Single Stock Analysis
```
User Input: "AAPL"
┌─────────────────────────────────────────────────────────────┐
│ 1. FETCH DATA (yfinance) │
│ - Stock info, earnings, price history │
│ - ~2 seconds │
└────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. PARALLEL ANALYSIS │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Earnings │ │Fundmtls │ │ Analysts │ ... (sync) │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Market Context (cached or fetch) │ ~1 second │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ Sentiment (5 async tasks) │ ~3-5 seconds │
│ │ - Fear/Greed (cached) │ │
│ │ - Short Interest │ │
│ │ - VIX Structure (cached) │ │
│ │ - Insider Trading (slow!) │ │
│ │ - Put/Call Ratio │ │
│ └────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. SYNTHESIZE SIGNAL │
│ - Combine scores with weights │
│ - Apply adjustments │
│ - Generate caveats │
│ - ~10 ms │
└────────────────────────┬────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. OUTPUT │
│ - Text or JSON format │
│ - Include disclaimer │
└─────────────────────────────────────────────────────────────┘
```
---
## Risk Detection
### Geopolitical Risk
```python
GEOPOLITICAL_RISK_MAP = {
"taiwan": {
"keywords": ["taiwan", "tsmc", "strait"],
"sectors": ["Technology", "Communication Services"],
"affected_tickers": ["NVDA", "AMD", "TSM", ...],
"impact": "Semiconductor supply chain disruption",
},
# ... china, russia_ukraine, middle_east, banking_crisis
}
```
**Process:**
1. Check breaking news for keywords
2. If keyword found, check if ticker in affected list
3. Apply confidence penalty (30% direct, 15% sector)
### Breaking News
```python
def check_breaking_news(verbose: bool = False) -> list[str] | None:
"""Scan Google News RSS for crisis keywords (last 24h)."""
```
**Crisis Keywords:**
```python
CRISIS_KEYWORDS = {
"war": ["war", "invasion", "military strike", ...],
"economic": ["recession", "crisis", "collapse", ...],
"regulatory": ["sanctions", "embargo", "ban", ...],
"disaster": ["earthquake", "hurricane", "pandemic", ...],
"financial": ["emergency rate", "bailout", ...],
}
```
---
## File Structure
```
stock-analysis/
├── scripts/
│ ├── analyze_stock.py # Main analysis engine (2500+ lines)
│ ├── portfolio.py # Portfolio management
│ ├── dividends.py # Dividend analysis
│ ├── watchlist.py # Watchlist + alerts
│ └── test_stock_analysis.py # Unit tests
├── docs/
│ ├── CONCEPT.md # Philosophy & ideas
│ ├── USAGE.md # Practical guide
│ └── ARCHITECTURE.md # This file
├── SKILL.md # OpenClaw skill definition
├── README.md # Project overview
└── .clawdhub/ # ClawHub metadata
```
---
## Data Storage
### Portfolio (`portfolios.json`)
```json
{
"portfolios": [
{
"name": "Retirement",
"created_at": "2024-01-01T00:00:00Z",
"assets": [
{
"ticker": "AAPL",
"quantity": 100,
"cost_basis": 150.00,
"type": "stock",
"added_at": "2024-01-01T00:00:00Z"
}
]
}
]
}
```
### Watchlist (`watchlist.json`)
```json
[
{
"ticker": "NVDA",
"added_at": "2024-01-15T10:30:00Z",
"price_at_add": 700.00,
"target_price": 800.00,
"stop_price": 600.00,
"alert_on_signal": true,
"last_signal": "BUY",
"last_check": "2024-01-20T08:00:00Z"
}
]
```
---
## Dependencies
```python
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "yfinance>=0.2.40", # Stock data
# "pandas>=2.0.0", # Data manipulation
# "fear-and-greed>=0.4", # CNN Fear & Greed
# "edgartools>=2.0.0", # SEC EDGAR filings
# "feedparser>=6.0.0", # RSS parsing
# ]
# ///
```
**Why These:**
- `yfinance`: Most reliable free stock API
- `pandas`: Industry standard for financial data
- `fear-and-greed`: Simple CNN F&G wrapper
- `edgartools`: Clean SEC EDGAR access
- `feedparser`: Robust RSS parsing
---
## Performance Optimization
### Current
| Operation | Time |
|-----------|------|
| yfinance fetch | ~2s |
| Market context | ~1s (cached after) |
| Insider trading | ~3-5s (slowest!) |
| Sentiment (parallel) | ~3-5s |
| Synthesis | ~10ms |
| **Total** | **5-10s** |
### Fast Mode (`--fast`)
Skips:
- Insider trading (SEC EDGAR)
- Breaking news scan
**Result:** 2-3 seconds
### Future Optimizations
1. **Stock-level caching** — Cache fundamentals for 24h
2. **Batch API calls** — yfinance supports multiple tickers
3. **Background refresh** — Pre-fetch watchlist data
4. **Local SEC data** — Avoid EDGAR API calls
---
## Error Handling
### Retry Strategy
```python
max_retries = 3
for attempt in range(max_retries):
try:
# fetch data
except Exception as e:
wait_time = 2 ** attempt # Exponential backoff: 1, 2, 4 seconds
time.sleep(wait_time)
```
### Graceful Degradation
- Missing earnings → Skip dimension, reweight
- Missing analysts → Skip dimension, reweight
- Missing sentiment → Skip dimension, reweight
- API failure → Return None, continue with partial data
### Minimum Requirements
- At least 2 of 8 dimensions required
- At least 2 of 5 sentiment indicators required
- Otherwise → HOLD with low confidence

233
docs/CONCEPT.md Normal file
View File

@@ -0,0 +1,233 @@
# Concept & Philosophy
## The Problem
Making investment decisions is hard. There's too much data, too many opinions, and too much noise. Most retail investors either:
1. **Over-simplify** — Buy based on headlines or tips
2. **Over-complicate** — Get lost in endless research
3. **Freeze** — Analysis paralysis, never act
## The Solution
Stock Analysis provides a **structured, multi-dimensional framework** that:
- Aggregates data from multiple sources
- Weighs different factors objectively
- Produces a clear **BUY / HOLD / SELL** signal
- Explains the reasoning with bullet points
- Flags risks and caveats
Think of it as a **second opinion** — not a replacement for your judgment, but a systematic check.
---
## Core Philosophy
### 1. Multiple Perspectives Beat Single Metrics
No single metric tells the whole story:
- A low P/E might mean "cheap" or "dying business"
- High analyst ratings might mean "priced in" or "genuine upside"
- Strong momentum might mean "trend" or "overbought"
By combining **8 dimensions**, we get a more complete picture.
### 2. Contrarian Signals Matter
Some of our best signals are **contrarian**:
| Indicator | Crowd Says | We Interpret |
|-----------|------------|--------------|
| Extreme Fear (Fear & Greed < 25) | "Sell everything!" | Potential buy opportunity |
| Extreme Greed (> 75) | "Easy money!" | Caution, reduce exposure |
| High Short Interest + Days to Cover | "Stock is doomed" | Squeeze potential |
| Insider Buying | (often ignored) | Smart money signal |
### 3. Timing Matters
A good stock at the wrong time is a bad trade:
- **Pre-earnings** — Even strong stocks can gap down 10%+
- **Post-spike** — Buying after a 20% run often means buying the top
- **Overbought** — RSI > 70 + near 52-week high = high-risk entry
We detect these timing issues and adjust recommendations accordingly.
### 4. Context Changes Everything
The same stock behaves differently in different market regimes:
| Regime | Characteristics | Impact |
|--------|-----------------|--------|
| **Bull** | VIX < 20, SPY up | BUY signals more reliable |
| **Bear** | VIX > 30, SPY down | Even good stocks fall |
| **Risk-Off** | GLD/TLT/UUP rising | Flight to safety, reduce equity |
| **Geopolitical** | Crisis keywords | Sector-specific penalties |
### 5. Dividends Are Different
Income investors have different priorities than growth investors:
| Growth Investor | Income Investor |
|-----------------|-----------------|
| Price appreciation | Dividend yield |
| Revenue growth | Payout sustainability |
| Market share | Dividend growth rate |
| P/E ratio | Safety of payment |
That's why we have a **separate dividend analysis** module.
---
## The 8 Dimensions
### Why These 8?
Each dimension captures a different aspect of investment quality:
```
┌─────────────────────────────────────────────────────────────┐
│ FUNDAMENTAL VALUE │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Earnings │ │ Fundamentals │ │
│ │ Surprise │ │ (P/E, etc.) │ │
│ │ (30%) │ │ (20%) │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ EXTERNAL VALIDATION │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Analyst │ │ Historical │ │
│ │ Sentiment │ │ Patterns │ │
│ │ (20%) │ │ (10%) │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ MARKET ENVIRONMENT │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Market │ │ Sector │ │
│ │ Context │ │ Performance │ │
│ │ (10%) │ │ (15%) │ │
│ └─────────────────┘ └─────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ TECHNICAL & SENTIMENT │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Momentum │ │ Sentiment │ │
│ │ (RSI, range) │ │ (Fear, shorts) │ │
│ │ (15%) │ │ (10%) │ │
│ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Weight Rationale
| Weight | Dimension | Rationale |
|--------|-----------|-----------|
| 30% | Earnings | Most direct measure of company performance |
| 20% | Fundamentals | Long-term value indicators |
| 20% | Analysts | Professional consensus (with skepticism) |
| 15% | Sector | Relative performance matters |
| 15% | Momentum | Trend is your friend (until it isn't) |
| 10% | Market | Rising tide lifts all boats |
| 10% | Sentiment | Contrarian edge |
| 10% | Historical | Past behavior predicts future reactions |
**Note:** Weights auto-normalize when data is missing.
---
## Risk Detection Philosophy
### "Don't Lose Money"
Warren Buffett's Rule #1. Our risk detection is designed to **prevent bad entries**:
1. **Pre-Earnings Hold** — Don't buy right before a binary event
2. **Post-Spike Caution** — Don't chase a run-up
3. **Overbought Warning** — Technical exhaustion
4. **Risk-Off Mode** — When even good stocks fall
5. **Geopolitical Flags** — Sector-specific event risk
### False Positive vs False Negative
We err on the side of **caution**:
- Missing a 10% gain is annoying
- Catching a 30% loss is devastating
That's why our caveats are prominent, and we downgrade BUY → HOLD liberally.
---
## Crypto Adaptation
Crypto is fundamentally different from stocks:
| Stocks | Crypto |
|--------|--------|
| Earnings | No earnings |
| P/E Ratio | Market cap tiers |
| Sector ETFs | BTC correlation |
| Dividends | Staking yields (not tracked) |
| SEC Filings | No filings |
We adapted the framework:
- **3 dimensions** instead of 8
- **BTC correlation** as a key metric
- **Category classification** (L1, DeFi, etc.)
- **No sentiment** (no insider data for crypto)
---
## Why Not Just Use [X]?
### vs. Stock Screeners (Finviz, etc.)
- Screeners show data, we provide **recommendations**
- We combine fundamental + technical + sentiment
- We flag timing and risk issues
### vs. Analyst Reports
- Analysts have conflicts of interest
- Reports are often stale
- We aggregate multiple signals
### vs. Trading Bots
- Bots execute, we advise
- We explain reasoning
- Human stays in control
### vs. ChatGPT/AI Chat
- We have **structured scoring**, not just conversation
- Real-time data fetching
- Consistent methodology
---
## Limitations We Acknowledge
1. **Data Lag** — Yahoo Finance is 15-20 min delayed
2. **US Focus** — International stocks have incomplete data
3. **No Execution** — We advise, you decide and execute
4. **Past ≠ Future** — All models have limits
5. **Black Swans** — Can't predict unpredictable events
**This is a tool, not a crystal ball.**
---
## The Bottom Line
Stock Analysis v6.0 is designed to be your **systematic second opinion**:
- ✅ Multi-dimensional analysis
- ✅ Clear recommendations
- ✅ Risk detection
- ✅ Explained reasoning
- ✅ Fast and automated
**NOT:**
- ❌ Financial advice
- ❌ Guaranteed returns
- ❌ Replacement for research
- ❌ Trading signals
Use it wisely. 📈

288
docs/HOT_SCANNER.md Normal file
View File

@@ -0,0 +1,288 @@
# 🔥 Hot Scanner
Find viral stocks & crypto trends in real-time by aggregating multiple data sources.
## Overview
The Hot Scanner answers one question: **"What's hot right now?"**
It aggregates data from:
- CoinGecko (trending coins, biggest movers)
- Google News (finance & crypto headlines)
- Yahoo Finance (gainers, losers, most active)
- Twitter/X (social sentiment, optional)
## Quick Start
```bash
# Full scan with all sources
python3 scripts/hot_scanner.py
# Skip social media (faster)
python3 scripts/hot_scanner.py --no-social
# JSON output for automation
python3 scripts/hot_scanner.py --json
```
## Output Format
### Console Output
```
============================================================
🔥 HOT SCANNER v2 - What's Trending Right Now?
📅 2026-02-02 10:45:30 UTC
============================================================
📊 TOP TRENDING (by buzz):
1. BTC (6 pts) [CoinGecko, Google News] 📉 bearish (-2.5%)
2. ETH (5 pts) [CoinGecko, Twitter] 📉 bearish (-7.2%)
3. NVDA (3 pts) [Google News, Yahoo] 📰 Earnings beat...
🪙 CRYPTO HIGHLIGHTS:
🚀 RIVER River +14.0%
📉 BTC Bitcoin -2.5%
📉 ETH Ethereum -7.2%
📈 STOCK MOVERS:
🟢 NVDA (gainers)
🔴 TSLA (losers)
📊 AAPL (most active)
🐦 SOCIAL BUZZ:
[twitter] Bitcoin to $100k prediction...
[reddit_wsb] GME yolo update...
📰 BREAKING NEWS:
[BTC, ETH] Crypto crash: $2.5B liquidated...
[NVDA] Nvidia beats earnings expectations...
```
### JSON Output
```json
{
"scan_time": "2026-02-02T10:45:30+00:00",
"top_trending": [
{
"symbol": "BTC",
"mentions": 6,
"sources": ["CoinGecko Trending", "Google News"],
"signals": ["📉 bearish (-2.5%)"]
}
],
"crypto_highlights": [...],
"stock_highlights": [...],
"social_buzz": [...],
"breaking_news": [...]
}
```
## Data Sources
### CoinGecko (No Auth Required)
| Endpoint | Data |
|----------|------|
| `/search/trending` | Top 15 trending coins |
| `/coins/markets` | Top 100 by market cap with 24h changes |
**Scoring:** Trending coins get 2 points, movers with >3% change get 1 point.
### Google News RSS (No Auth Required)
| Feed | Content |
|------|---------|
| Business News | General finance headlines |
| Crypto Search | Bitcoin, Ethereum, crypto keywords |
**Ticker Extraction:** Uses regex patterns and company name mappings.
### Yahoo Finance (No Auth Required)
| Page | Data |
|------|------|
| `/gainers` | Top gaining stocks |
| `/losers` | Top losing stocks |
| `/most-active` | Highest volume stocks |
**Note:** Requires gzip decompression.
### Twitter/X (Auth Required)
Uses [bird CLI](https://github.com/steipete/bird) for Twitter search.
**Searches:**
- `stock OR $SPY OR $QQQ OR earnings`
- `bitcoin OR ethereum OR crypto OR $BTC`
## Twitter/X Setup
### 1. Install bird CLI
```bash
# macOS
brew install steipete/tap/bird
# npm
npm install -g @steipete/bird
```
### 2. Get Auth Tokens
**Option A: Browser cookies (macOS)**
1. Login to x.com in Safari/Chrome
2. Grant Terminal "Full Disk Access" in System Settings
3. Run `bird whoami` to verify
**Option B: Manual extraction**
1. Open x.com in Chrome
2. DevTools (F12) → Application → Cookies → x.com
3. Copy `auth_token` and `ct0` values
### 3. Configure
Create `.env` file in the skill directory:
```bash
# /path/to/stock-analysis/.env
AUTH_TOKEN=your_auth_token_here
CT0=your_ct0_token_here
```
Or export as environment variables:
```bash
export AUTH_TOKEN="..."
export CT0="..."
```
### 4. Verify
```bash
bird whoami
# Should show: 🙋 @YourUsername
```
## Scoring System
Each mention from a source adds points:
| Source | Points |
|--------|--------|
| CoinGecko Trending | 2 |
| CoinGecko Movers | 1 |
| Google News | 1 |
| Yahoo Finance | 1 |
| Twitter/X | 1 |
| Reddit (high score) | 2 |
| Reddit (normal) | 1 |
Symbols are ranked by total points across all sources.
## Ticker Extraction
### Patterns
```python
# Cashtag: $AAPL
r'\$([A-Z]{1,5})\b'
# Parentheses: (AAPL)
r'\(([A-Z]{2,5})\)'
# Stock mentions: AAPL stock, AAPL shares
r'\b([A-Z]{2,5})(?:\'s|:|\s+stock|\s+shares)'
```
### Company Mappings
```python
{
"Apple": "AAPL",
"Microsoft": "MSFT",
"Tesla": "TSLA",
"Nvidia": "NVDA",
"Bitcoin": "BTC",
"Ethereum": "ETH",
# ... etc
}
```
### Crypto Keywords
```python
{
"bitcoin": "BTC",
"ethereum": "ETH",
"solana": "SOL",
"dogecoin": "DOGE",
# ... etc
}
```
## Automation
### Cron Job
```bash
# Daily at 8 AM
0 8 * * * cd /path/to/stock-analysis && python3 scripts/hot_scanner.py --json > cache/daily_scan.json
```
### OpenClaw Integration
```yaml
# Cron job config
name: "🔥 Daily Hot Scanner"
schedule:
kind: cron
expr: "0 8 * * *"
tz: "Europe/Berlin"
payload:
kind: agentTurn
message: "Run hot scanner and summarize results"
deliver: true
sessionTarget: isolated
```
## Caching
Results are saved to:
- `cache/hot_scan_latest.json` — Most recent scan
## Limitations
- **Reddit:** Blocked without OAuth (403). Requires API application.
- **Twitter:** Requires auth tokens, may expire.
- **Yahoo:** Sometimes rate-limited.
- **Google News:** RSS URLs may change.
## Future Enhancements
- [ ] Reddit API integration (PRAW)
- [ ] StockTwits integration
- [ ] Google Trends
- [ ] Historical trend tracking
- [ ] Alert thresholds (notify when score > X)
## Troubleshooting
### Twitter not working
```bash
# Check auth
bird whoami
# Should see your username
# If not, re-export tokens
```
### Yahoo 403 or gzip errors
The scanner handles gzip automatically. If issues persist, Yahoo may be rate-limiting.
### No tickers found
Check that news headlines contain recognizable patterns. The scanner uses conservative extraction to avoid false positives.

95
docs/README.md Normal file
View File

@@ -0,0 +1,95 @@
# Documentation
## Stock Analysis v6.1
This folder contains detailed documentation for the Stock Analysis skill.
## Contents
| Document | Description |
|----------|-------------|
| [CONCEPT.md](./CONCEPT.md) | Philosophy, ideas, and design rationale |
| [USAGE.md](./USAGE.md) | Practical usage guide with examples |
| [ARCHITECTURE.md](./ARCHITECTURE.md) | Technical implementation details |
| [HOT_SCANNER.md](./HOT_SCANNER.md) | 🔥 Viral trend detection (NEW) |
## Quick Links
### For Users
Start with **[USAGE.md](./USAGE.md)** — it has practical examples for:
- Basic stock analysis
- Comparing stocks
- Crypto analysis
- Dividend investing
- Portfolio management
- Watchlist & alerts
### For Understanding
Read **[CONCEPT.md](./CONCEPT.md)** to understand:
- Why 8 dimensions?
- How scoring works
- Contrarian signals
- Risk detection philosophy
- Limitations we acknowledge
### For Developers
Check **[ARCHITECTURE.md](./ARCHITECTURE.md)** for:
- System overview diagram
- Data flow
- Caching strategy
- File structure
- Performance optimization
## Quick Start
```bash
# Analyze a stock
uv run scripts/analyze_stock.py AAPL
# Fast mode (2-3 seconds)
uv run scripts/analyze_stock.py AAPL --fast
# Dividend analysis
uv run scripts/dividends.py JNJ
# Watchlist
uv run scripts/watchlist.py add AAPL --target 200
uv run scripts/watchlist.py check
```
## Key Concepts
### The 8 Dimensions
1. **Earnings Surprise** (30%) — Did they beat expectations?
2. **Fundamentals** (20%) — P/E, margins, growth, debt
3. **Analyst Sentiment** (20%) — Professional consensus
4. **Historical Patterns** (10%) — Past earnings reactions
5. **Market Context** (10%) — VIX, SPY/QQQ trends
6. **Sector Performance** (15%) — Relative strength
7. **Momentum** (15%) — RSI, 52-week range
8. **Sentiment** (10%) — Fear/Greed, shorts, insiders
### Signal Thresholds
| Score | Recommendation |
|-------|----------------|
| > +0.33 | **BUY** |
| -0.33 to +0.33 | **HOLD** |
| < -0.33 | **SELL** |
### Risk Flags
- ⚠️ Pre-earnings (< 14 days)
- ⚠️ Post-spike (> 15% in 5 days)
- ⚠️ Overbought (RSI > 70 + near 52w high)
- ⚠️ Risk-off mode (GLD/TLT/UUP rising)
- ⚠️ Geopolitical keywords
- ⚠️ Breaking news alerts
## Disclaimer
⚠️ **NOT FINANCIAL ADVICE.** For informational purposes only. Always do your own research and consult a licensed financial advisor.

465
docs/USAGE.md Normal file
View File

@@ -0,0 +1,465 @@
# Usage Guide
Practical examples for using Stock Analysis v6.0 in real scenarios.
## Table of Contents
1. [Basic Stock Analysis](#basic-stock-analysis)
2. [Comparing Stocks](#comparing-stocks)
3. [Crypto Analysis](#crypto-analysis)
4. [Dividend Investing](#dividend-investing)
5. [Portfolio Management](#portfolio-management)
6. [Watchlist & Alerts](#watchlist--alerts)
7. [Performance Tips](#performance-tips)
8. [Interpreting Results](#interpreting-results)
---
## Basic Stock Analysis
### Single Stock
```bash
uv run scripts/analyze_stock.py AAPL
```
**Output:**
```
===========================================================================
STOCK ANALYSIS: AAPL (Apple Inc.)
Generated: 2024-02-01T10:30:00
===========================================================================
RECOMMENDATION: BUY (Confidence: 72%)
SUPPORTING POINTS:
• Beat by 8.2% - EPS $2.18 vs $2.01 expected
• Strong margin: 24.1%
• Analyst consensus: Buy with 12.3% upside (42 analysts)
• Momentum: RSI 58 (neutral)
• Sector: Technology uptrend (+5.2% 1m)
CAVEATS:
• Earnings in 12 days - high volatility expected
• High market volatility (VIX 24)
===========================================================================
DISCLAIMER: NOT FINANCIAL ADVICE.
===========================================================================
```
### JSON Output
For programmatic use:
```bash
uv run scripts/analyze_stock.py AAPL --output json | jq '.recommendation, .confidence'
```
### Verbose Mode
See what's happening under the hood:
```bash
uv run scripts/analyze_stock.py AAPL --verbose
```
---
## Comparing Stocks
### Side-by-Side Analysis
```bash
uv run scripts/analyze_stock.py AAPL MSFT GOOGL
```
Each stock gets a full analysis. Compare recommendations and confidence levels.
### Sector Comparison
Compare stocks in the same sector:
```bash
# Banks
uv run scripts/analyze_stock.py JPM BAC WFC GS
# Tech
uv run scripts/analyze_stock.py AAPL MSFT GOOGL AMZN META
```
---
## Crypto Analysis
### Basic Crypto
```bash
uv run scripts/analyze_stock.py BTC-USD
```
**Crypto-Specific Output:**
- Market cap classification (large/mid/small)
- Category (Smart Contract L1, DeFi, etc.)
- BTC correlation (30-day)
- Momentum (RSI, price range)
### Compare Cryptos
```bash
uv run scripts/analyze_stock.py BTC-USD ETH-USD SOL-USD
```
### Supported Cryptos
```
BTC, ETH, BNB, SOL, XRP, ADA, DOGE, AVAX, DOT, MATIC,
LINK, ATOM, UNI, LTC, BCH, XLM, ALGO, VET, FIL, NEAR
```
Use `-USD` suffix: `BTC-USD`, `ETH-USD`, etc.
---
## Dividend Investing
### Analyze Dividend Stock
```bash
uv run scripts/dividends.py JNJ
```
**Output:**
```
============================================================
DIVIDEND ANALYSIS: JNJ (Johnson & Johnson)
============================================================
Current Price: $160.50
Annual Dividend: $4.76
Dividend Yield: 2.97%
Payment Freq: quarterly
Ex-Dividend: 2024-02-15
Payout Ratio: 65.0% (moderate)
5Y Div Growth: +5.8%
Consecutive Yrs: 62
SAFETY SCORE: 78/100
INCOME RATING: GOOD
Safety Factors:
• Moderate payout ratio (65%)
• Good dividend growth (+5.8% CAGR)
• Dividend Aristocrat (62+ years)
Dividend History:
2023: $4.52
2022: $4.36
2021: $4.24
2020: $4.04
2019: $3.80
============================================================
```
### Compare Dividend Stocks
```bash
uv run scripts/dividends.py JNJ PG KO MCD VZ T
```
### Dividend Aristocrats Screen
Look for stocks with:
- Yield > 2%
- Payout < 60%
- Growth > 5%
- Consecutive years > 25
---
## Portfolio Management
### Create Portfolio
```bash
uv run scripts/portfolio.py create "Retirement"
```
### Add Holdings
```bash
# Stocks
uv run scripts/portfolio.py add AAPL --quantity 100 --cost 150.00
# Crypto
uv run scripts/portfolio.py add BTC-USD --quantity 0.5 --cost 40000
```
### View Portfolio
```bash
uv run scripts/portfolio.py show
```
**Output:**
```
Portfolio: Retirement
====================
Assets:
AAPL 100 shares @ $150.00 = $15,000.00
Current: $185.00 = $18,500.00 (+23.3%)
BTC-USD 0.5 @ $40,000 = $20,000.00
Current: $45,000 = $22,500.00 (+12.5%)
Total Cost: $35,000.00
Current Value: $41,000.00
Total P&L: +$6,000.00 (+17.1%)
```
### Analyze Portfolio
```bash
# Full analysis of all holdings
uv run scripts/analyze_stock.py --portfolio "Retirement"
# With period returns
uv run scripts/analyze_stock.py --portfolio "Retirement" --period monthly
```
### Rebalance Check
The analysis flags concentration warnings:
```
⚠️ CONCENTRATION WARNINGS:
• AAPL: 45.1% (>30% of portfolio)
```
---
## Watchlist & Alerts
### Add to Watchlist
```bash
# Basic watch
uv run scripts/watchlist.py add NVDA
# With price target
uv run scripts/watchlist.py add NVDA --target 800
# With stop loss
uv run scripts/watchlist.py add NVDA --stop 600
# Alert on signal change
uv run scripts/watchlist.py add NVDA --alert-on signal
# All options
uv run scripts/watchlist.py add NVDA --target 800 --stop 600 --alert-on signal
```
### View Watchlist
```bash
uv run scripts/watchlist.py list
```
**Output:**
```json
{
"success": true,
"items": [
{
"ticker": "NVDA",
"current_price": 725.50,
"price_at_add": 700.00,
"change_pct": 3.64,
"target_price": 800.00,
"to_target_pct": 10.27,
"stop_price": 600.00,
"to_stop_pct": -17.30,
"alert_on_signal": true,
"last_signal": "BUY",
"added_at": "2024-01-15"
}
],
"count": 1
}
```
### Check Alerts
```bash
# Check for triggered alerts
uv run scripts/watchlist.py check
# Format for notification (Telegram)
uv run scripts/watchlist.py check --notify
```
**Alert Example:**
```
📢 Stock Alerts
🎯 NVDA hit target! $802.50 >= $800.00
🛑 TSLA hit stop! $195.00 <= $200.00
📊 AAPL signal changed: HOLD → BUY
```
### Remove from Watchlist
```bash
uv run scripts/watchlist.py remove NVDA
```
---
## Performance Tips
### Fast Mode
Skip slow analyses for quick checks:
```bash
# Skip insider trading + breaking news
uv run scripts/analyze_stock.py AAPL --fast
```
**Speed comparison:**
| Mode | Time | What's Skipped |
|------|------|----------------|
| Default | 5-10s | Nothing |
| `--no-insider` | 3-5s | SEC EDGAR |
| `--fast` | 2-3s | Insider + News |
### Batch Analysis
Analyze multiple stocks in one command:
```bash
uv run scripts/analyze_stock.py AAPL MSFT GOOGL AMZN META
```
### Caching
Market context is cached for 1 hour:
- VIX, SPY, QQQ trends
- Fear & Greed Index
- VIX term structure
- Breaking news
Second analysis of different stock reuses cached data.
---
## Interpreting Results
### Recommendation Thresholds
| Score | Recommendation |
|-------|----------------|
| > +0.33 | BUY |
| -0.33 to +0.33 | HOLD |
| < -0.33 | SELL |
### Confidence Levels
| Confidence | Meaning |
|------------|---------|
| > 80% | Strong conviction |
| 60-80% | Moderate conviction |
| 40-60% | Mixed signals |
| < 40% | Low conviction |
### Reading Caveats
**Always read the caveats!** They often contain critical information:
```
CAVEATS:
• Earnings in 5 days - high volatility expected ← Timing risk
• RSI 78 (overbought) + near 52w high ← Technical risk
• ⚠️ BREAKING NEWS: Fed emergency rate discussion ← External risk
• ⚠️ SECTOR RISK: China tensions affect tech ← Geopolitical
```
### When to Ignore the Signal
- **Pre-earnings:** Even BUY → wait until after
- **Overbought:** Consider smaller position
- **Risk-off:** Reduce overall exposure
- **Low confidence:** Do more research
### When to Trust the Signal
- **High confidence + no major caveats**
- **Multiple supporting points align**
- **Sector is strong**
- **Market regime is favorable**
---
## Common Workflows
### Morning Check
```bash
# Check watchlist alerts
uv run scripts/watchlist.py check --notify
# Quick portfolio update
uv run scripts/analyze_stock.py --portfolio "Main" --fast
```
### Research New Stock
```bash
# Full analysis
uv run scripts/analyze_stock.py XYZ
# If dividend stock
uv run scripts/dividends.py XYZ
# Add to watchlist for monitoring
uv run scripts/watchlist.py add XYZ --alert-on signal
```
### Weekly Review
```bash
# Full portfolio analysis
uv run scripts/analyze_stock.py --portfolio "Main" --period weekly
# Check dividend holdings
uv run scripts/dividends.py JNJ PG KO
```
---
## Troubleshooting
### "Invalid ticker"
- Check spelling
- For crypto, use `-USD` suffix
- Non-US stocks may not work
### "Insufficient data"
- Stock might be too new
- ETFs have limited data
- OTC stocks often fail
### Slow Performance
- Use `--fast` for quick checks
- Insider trading is slowest
- Breaking news adds ~2s
### Missing Data
- Not all stocks have analyst coverage
- Some metrics require options chains
- Crypto has no sentiment data

2532
scripts/analyze_stock.py Normal file

File diff suppressed because it is too large Load Diff

365
scripts/dividends.py Normal file
View File

@@ -0,0 +1,365 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "yfinance>=0.2.40",
# "pandas>=2.0.0",
# ]
# ///
"""
Dividend Analysis Module.
Analyzes dividend metrics for income investors:
- Dividend Yield
- Payout Ratio
- Dividend Growth Rate (5Y CAGR)
- Dividend Safety Score
- Ex-Dividend Date
Usage:
uv run dividends.py AAPL
uv run dividends.py JNJ PG KO --output json
"""
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from datetime import datetime
import pandas as pd
import yfinance as yf
@dataclass
class DividendAnalysis:
ticker: str
company_name: str
# Basic metrics
dividend_yield: float | None # Annual yield %
annual_dividend: float | None # Annual dividend per share
current_price: float | None
# Payout analysis
payout_ratio: float | None # Dividend / EPS
payout_status: str # "safe", "moderate", "high", "unsustainable"
# Growth
dividend_growth_5y: float | None # 5-year CAGR %
consecutive_years: int | None # Years of consecutive increases
dividend_history: list[dict] | None # Last 5 years
# Timing
ex_dividend_date: str | None
payment_frequency: str | None # "quarterly", "monthly", "annual"
# Safety score (0-100)
safety_score: int
safety_factors: list[str]
# Verdict
income_rating: str # "excellent", "good", "moderate", "poor", "no_dividend"
summary: str
def analyze_dividends(ticker: str, verbose: bool = False) -> DividendAnalysis | None:
"""Analyze dividend metrics for a stock."""
try:
stock = yf.Ticker(ticker)
info = stock.info
company_name = info.get("longName") or info.get("shortName") or ticker
current_price = info.get("regularMarketPrice") or info.get("currentPrice")
# Basic dividend info
dividend_yield = info.get("dividendYield")
if dividend_yield:
dividend_yield = dividend_yield * 100 # Convert to percentage
annual_dividend = info.get("dividendRate")
# No dividend
if not annual_dividend or annual_dividend == 0:
return DividendAnalysis(
ticker=ticker,
company_name=company_name,
dividend_yield=None,
annual_dividend=None,
current_price=current_price,
payout_ratio=None,
payout_status="no_dividend",
dividend_growth_5y=None,
consecutive_years=None,
dividend_history=None,
ex_dividend_date=None,
payment_frequency=None,
safety_score=0,
safety_factors=["No dividend paid"],
income_rating="no_dividend",
summary=f"{ticker} does not pay a dividend.",
)
# Payout ratio
trailing_eps = info.get("trailingEps")
payout_ratio = None
payout_status = "unknown"
if trailing_eps and trailing_eps > 0 and annual_dividend:
payout_ratio = (annual_dividend / trailing_eps) * 100
if payout_ratio < 40:
payout_status = "safe"
elif payout_ratio < 60:
payout_status = "moderate"
elif payout_ratio < 80:
payout_status = "high"
else:
payout_status = "unsustainable"
# Dividend history (for growth calculation)
dividends = stock.dividends
dividend_history = None
dividend_growth_5y = None
consecutive_years = None
if dividends is not None and len(dividends) > 0:
# Group by year
dividends_df = dividends.reset_index()
dividends_df["Year"] = pd.to_datetime(dividends_df["Date"]).dt.year
yearly = dividends_df.groupby("Year")["Dividends"].sum().sort_index(ascending=False)
# Last 5 years history
dividend_history = []
for year in yearly.head(5).index:
dividend_history.append({
"year": int(year),
"total": round(float(yearly[year]), 4),
})
# Calculate 5-year CAGR
if len(yearly) >= 5:
current_div = yearly.iloc[0]
div_5y_ago = yearly.iloc[4]
if div_5y_ago > 0 and current_div > 0:
dividend_growth_5y = ((current_div / div_5y_ago) ** (1/5) - 1) * 100
# Count consecutive years of increases
consecutive_years = 0
prev_div = None
for div in yearly.values:
if prev_div is not None:
if div >= prev_div:
consecutive_years += 1
else:
break
prev_div = div
# Ex-dividend date
ex_dividend_date = info.get("exDividendDate")
if ex_dividend_date:
ex_dividend_date = datetime.fromtimestamp(ex_dividend_date).strftime("%Y-%m-%d")
# Payment frequency
payment_frequency = None
if dividends is not None and len(dividends) >= 4:
# Count dividends in last year
one_year_ago = pd.Timestamp.now() - pd.DateOffset(years=1)
recent_divs = dividends[dividends.index > one_year_ago]
count = len(recent_divs)
if count >= 10:
payment_frequency = "monthly"
elif count >= 3:
payment_frequency = "quarterly"
elif count >= 1:
payment_frequency = "annual"
# Safety score calculation (0-100)
safety_score = 50 # Base score
safety_factors = []
# Payout ratio factor (+/- 20)
if payout_ratio:
if payout_ratio < 40:
safety_score += 20
safety_factors.append(f"Low payout ratio ({payout_ratio:.0f}%)")
elif payout_ratio < 60:
safety_score += 10
safety_factors.append(f"Moderate payout ratio ({payout_ratio:.0f}%)")
elif payout_ratio < 80:
safety_score -= 10
safety_factors.append(f"High payout ratio ({payout_ratio:.0f}%)")
else:
safety_score -= 20
safety_factors.append(f"Unsustainable payout ratio ({payout_ratio:.0f}%)")
# Growth factor (+/- 15)
if dividend_growth_5y:
if dividend_growth_5y > 10:
safety_score += 15
safety_factors.append(f"Strong dividend growth ({dividend_growth_5y:.1f}% CAGR)")
elif dividend_growth_5y > 5:
safety_score += 10
safety_factors.append(f"Good dividend growth ({dividend_growth_5y:.1f}% CAGR)")
elif dividend_growth_5y > 0:
safety_score += 5
safety_factors.append(f"Positive dividend growth ({dividend_growth_5y:.1f}% CAGR)")
else:
safety_score -= 15
safety_factors.append(f"Dividend declining ({dividend_growth_5y:.1f}% CAGR)")
# Consecutive years factor (+/- 15)
if consecutive_years:
if consecutive_years >= 25:
safety_score += 15
safety_factors.append(f"Dividend Aristocrat ({consecutive_years}+ years)")
elif consecutive_years >= 10:
safety_score += 10
safety_factors.append(f"Long dividend history ({consecutive_years} years)")
elif consecutive_years >= 5:
safety_score += 5
safety_factors.append(f"Consistent dividend ({consecutive_years} years)")
# Yield factor (high yield can be risky)
if dividend_yield:
if dividend_yield > 8:
safety_score -= 10
safety_factors.append(f"Very high yield ({dividend_yield:.1f}%) - verify sustainability")
elif dividend_yield < 1:
safety_factors.append(f"Low yield ({dividend_yield:.2f}%)")
# Clamp score
safety_score = max(0, min(100, safety_score))
# Income rating
if safety_score >= 80:
income_rating = "excellent"
elif safety_score >= 60:
income_rating = "good"
elif safety_score >= 40:
income_rating = "moderate"
else:
income_rating = "poor"
# Summary
summary_parts = []
if dividend_yield:
summary_parts.append(f"{dividend_yield:.2f}% yield")
if payout_ratio:
summary_parts.append(f"{payout_ratio:.0f}% payout")
if dividend_growth_5y:
summary_parts.append(f"{dividend_growth_5y:+.1f}% 5Y growth")
if consecutive_years and consecutive_years >= 5:
summary_parts.append(f"{consecutive_years}Y streak")
summary = f"{ticker}: {', '.join(summary_parts)}. Rating: {income_rating.upper()}"
return DividendAnalysis(
ticker=ticker,
company_name=company_name,
dividend_yield=round(dividend_yield, 2) if dividend_yield else None,
annual_dividend=round(annual_dividend, 4) if annual_dividend else None,
current_price=current_price,
payout_ratio=round(payout_ratio, 1) if payout_ratio else None,
payout_status=payout_status,
dividend_growth_5y=round(dividend_growth_5y, 2) if dividend_growth_5y else None,
consecutive_years=consecutive_years,
dividend_history=dividend_history,
ex_dividend_date=ex_dividend_date,
payment_frequency=payment_frequency,
safety_score=safety_score,
safety_factors=safety_factors,
income_rating=income_rating,
summary=summary,
)
except Exception as e:
if verbose:
print(f"Error analyzing {ticker}: {e}", file=sys.stderr)
return None
def format_text(analysis: DividendAnalysis) -> str:
"""Format dividend analysis as text."""
lines = [
"=" * 60,
f"DIVIDEND ANALYSIS: {analysis.ticker} ({analysis.company_name})",
"=" * 60,
"",
]
if analysis.income_rating == "no_dividend":
lines.append("This stock does not pay a dividend.")
lines.append("=" * 60)
return "\n".join(lines)
# Yield & Price
lines.append(f"Current Price: ${analysis.current_price:.2f}")
lines.append(f"Annual Dividend: ${analysis.annual_dividend:.2f}")
lines.append(f"Dividend Yield: {analysis.dividend_yield:.2f}%")
lines.append(f"Payment Freq: {analysis.payment_frequency or 'Unknown'}")
if analysis.ex_dividend_date:
lines.append(f"Ex-Dividend: {analysis.ex_dividend_date}")
lines.append("")
# Payout & Safety
lines.append(f"Payout Ratio: {analysis.payout_ratio:.1f}% ({analysis.payout_status})")
lines.append(f"5Y Div Growth: {analysis.dividend_growth_5y:+.1f}%" if analysis.dividend_growth_5y else "5Y Div Growth: N/A")
if analysis.consecutive_years:
lines.append(f"Consecutive Yrs: {analysis.consecutive_years}")
lines.append("")
lines.append(f"SAFETY SCORE: {analysis.safety_score}/100")
lines.append(f"INCOME RATING: {analysis.income_rating.upper()}")
lines.append("")
lines.append("Safety Factors:")
for factor in analysis.safety_factors:
lines.append(f"{factor}")
# History
if analysis.dividend_history:
lines.append("")
lines.append("Dividend History:")
for h in analysis.dividend_history[:5]:
lines.append(f" {h['year']}: ${h['total']:.2f}")
lines.append("")
lines.append("=" * 60)
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(description="Dividend Analysis")
parser.add_argument("tickers", nargs="+", help="Stock ticker(s)")
parser.add_argument("--output", choices=["text", "json"], default="text")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()
results = []
for ticker in args.tickers:
analysis = analyze_dividends(ticker.upper(), verbose=args.verbose)
if analysis:
results.append(analysis)
else:
print(f"Error: Could not analyze {ticker}", file=sys.stderr)
if args.output == "json":
if len(results) == 1:
print(json.dumps(asdict(results[0]), indent=2))
else:
print(json.dumps([asdict(r) for r in results], indent=2))
else:
for i, analysis in enumerate(results):
if i > 0:
print("\n")
print(format_text(analysis))
if __name__ == "__main__":
main()

582
scripts/hot_scanner.py Normal file
View File

@@ -0,0 +1,582 @@
#!/usr/bin/env python3
"""
🔥 HOT SCANNER v2 - Find viral stocks & crypto trends
Now with Twitter/X, Reddit, and improved Yahoo Finance
"""
import json
import urllib.request
import urllib.error
import xml.etree.ElementTree as ET
import gzip
import io
import subprocess
import os
from datetime import datetime, timezone
from pathlib import Path
import re
import ssl
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
# Load .env file if exists
ENV_FILE = Path(__file__).parent.parent / ".env"
if ENV_FILE.exists():
with open(ENV_FILE) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, value = line.split("=", 1)
os.environ[key] = value
# Cache directory
CACHE_DIR = Path(__file__).parent.parent / "cache"
CACHE_DIR.mkdir(exist_ok=True)
# SSL context
SSL_CONTEXT = ssl.create_default_context()
class HotScanner:
def __init__(self, include_social=True):
self.include_social = include_social
self.results = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"crypto": [],
"stocks": [],
"news": [],
"movers": [],
"social": []
}
self.mentions = defaultdict(lambda: {"count": 0, "sources": [], "sentiment_hints": []})
self.headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
}
def _fetch(self, url, timeout=15):
"""Fetch URL with gzip support."""
req = urllib.request.Request(url, headers=self.headers)
with urllib.request.urlopen(req, timeout=timeout, context=SSL_CONTEXT) as resp:
data = resp.read()
# Handle gzip
if resp.info().get('Content-Encoding') == 'gzip' or data[:2] == b'\x1f\x8b':
data = gzip.decompress(data)
return data.decode('utf-8', errors='replace')
def _fetch_json(self, url, timeout=15):
"""Fetch and parse JSON."""
return json.loads(self._fetch(url, timeout))
def scan_all(self):
"""Run all scans in parallel."""
print("🔍 Scanning for hot trends...\n")
tasks = [
("CoinGecko Trending", self.scan_coingecko_trending),
("CoinGecko Movers", self.scan_coingecko_gainers_losers),
("Google News Finance", self.scan_google_news_finance),
("Google News Crypto", self.scan_google_news_crypto),
("Yahoo Movers", self.scan_yahoo_movers),
]
if self.include_social:
tasks.extend([
("Reddit WSB", self.scan_reddit_wsb),
("Reddit Crypto", self.scan_reddit_crypto),
("Twitter/X", self.scan_twitter),
])
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {executor.submit(task[1]): task[0] for task in tasks}
for future in as_completed(futures):
name = futures[future]
try:
future.result()
except Exception as e:
print(f"{name}: {str(e)[:50]}")
return self.results
def scan_coingecko_trending(self):
"""Get trending crypto from CoinGecko."""
print(" 📊 CoinGecko Trending...")
try:
url = "https://api.coingecko.com/api/v3/search/trending"
data = self._fetch_json(url)
for item in data.get("coins", [])[:10]:
coin = item.get("item", {})
price_data = coin.get("data", {})
price_change = price_data.get("price_change_percentage_24h", {}).get("usd", 0)
entry = {
"symbol": coin.get("symbol", "").upper(),
"name": coin.get("name", ""),
"rank": coin.get("market_cap_rank"),
"price_change_24h": round(price_change, 2) if price_change else None,
"source": "coingecko_trending"
}
self.results["crypto"].append(entry)
sym = entry["symbol"]
self.mentions[sym]["count"] += 2 # Trending gets extra weight
self.mentions[sym]["sources"].append("CoinGecko Trending")
if price_change:
direction = "🚀 bullish" if price_change > 0 else "📉 bearish"
self.mentions[sym]["sentiment_hints"].append(f"{direction} ({price_change:+.1f}%)")
print(f"{len(data.get('coins', []))} trending coins")
except Exception as e:
print(f" ❌ CoinGecko trending: {e}")
def scan_coingecko_gainers_losers(self):
"""Get top gainers/losers."""
print(" 📈 CoinGecko Movers...")
try:
url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=100&page=1&price_change_percentage=24h"
data = self._fetch_json(url)
sorted_data = sorted(data, key=lambda x: abs(x.get("price_change_percentage_24h") or 0), reverse=True)
count = 0
for coin in sorted_data[:20]:
change = coin.get("price_change_percentage_24h", 0)
if abs(change or 0) > 3:
entry = {
"symbol": coin.get("symbol", "").upper(),
"name": coin.get("name", ""),
"price": coin.get("current_price"),
"change_24h": round(change, 2) if change else None,
"volume": coin.get("total_volume"),
"source": "coingecko_movers"
}
self.results["movers"].append(entry)
count += 1
sym = entry["symbol"]
self.mentions[sym]["count"] += 1
self.mentions[sym]["sources"].append("CoinGecko Movers")
direction = "🚀 pumping" if change > 0 else "📉 dumping"
self.mentions[sym]["sentiment_hints"].append(f"{direction} ({change:+.1f}%)")
print(f"{count} significant movers")
except Exception as e:
print(f" ❌ CoinGecko movers: {e}")
def scan_google_news_finance(self):
"""Get finance news from Google News RSS."""
print(" 📰 Google News Finance...")
try:
# Business news topic
url = "https://news.google.com/rss/topics/CAAqJggKIiBDQkFTRWdvSUwyMHZNRGx6TVdZU0FtVnVHZ0pWVXlnQVAB?hl=en-US&gl=US&ceid=US:en"
text = self._fetch(url)
root = ET.fromstring(text)
items = root.findall(".//item")
for item in items[:15]:
title_elem = item.find("title")
title = title_elem.text if title_elem is not None else ""
tickers = self._extract_tickers(title)
news_entry = {
"title": title,
"tickers_mentioned": tickers,
"source": "google_news_finance"
}
self.results["news"].append(news_entry)
for ticker in tickers:
self.mentions[ticker]["count"] += 1
self.mentions[ticker]["sources"].append("Google News")
self.mentions[ticker]["sentiment_hints"].append(f"📰 {title[:40]}...")
print(f"{len(items)} news items")
except Exception as e:
print(f" ❌ Google News Finance: {e}")
def scan_google_news_crypto(self):
"""Search for crypto news."""
print(" 📰 Google News Crypto...")
try:
url = "https://news.google.com/rss/search?q=bitcoin+OR+ethereum+OR+crypto+crash+OR+crypto+pump&hl=en-US&gl=US&ceid=US:en"
text = self._fetch(url)
root = ET.fromstring(text)
items = root.findall(".//item")
crypto_keywords = {
"bitcoin": "BTC", "btc": "BTC", "ethereum": "ETH", "eth": "ETH",
"solana": "SOL", "xrp": "XRP", "ripple": "XRP", "dogecoin": "DOGE",
"cardano": "ADA", "polkadot": "DOT", "avalanche": "AVAX",
}
for item in items[:12]:
title_elem = item.find("title")
title = title_elem.text if title_elem is not None else ""
tickers = self._extract_tickers(title)
for word, ticker in crypto_keywords.items():
if word in title.lower():
tickers.append(ticker)
tickers = list(set(tickers))
if tickers:
news_entry = {
"title": title,
"tickers_mentioned": tickers,
"source": "google_news_crypto"
}
self.results["news"].append(news_entry)
for ticker in tickers:
self.mentions[ticker]["count"] += 1
self.mentions[ticker]["sources"].append("Google News Crypto")
print(f" ✅ Processed crypto news")
except Exception as e:
print(f" ❌ Google News Crypto: {e}")
def scan_yahoo_movers(self):
"""Scrape Yahoo Finance movers with gzip support."""
print(" 📈 Yahoo Finance Movers...")
categories = [
("gainers", "https://finance.yahoo.com/gainers/"),
("losers", "https://finance.yahoo.com/losers/"),
("most_active", "https://finance.yahoo.com/most-active/")
]
for category, url in categories:
try:
text = self._fetch(url, timeout=12)
# Multiple patterns for ticker extraction
tickers = []
# Pattern 1: data-symbol attribute
tickers.extend(re.findall(r'data-symbol="([A-Z]{1,5})"', text))
# Pattern 2: ticker in URL
tickers.extend(re.findall(r'/quote/([A-Z]{1,5})[/"\?]', text))
# Pattern 3: fin-streamer
tickers.extend(re.findall(r'fin-streamer[^>]*symbol="([A-Z]{1,5})"', text))
unique_tickers = list(dict.fromkeys(tickers))[:15]
for ticker in unique_tickers:
# Skip common false positives
if ticker in ['USA', 'CEO', 'IPO', 'ETF', 'SEC', 'FDA', 'NYSE', 'API']:
continue
self.results["stocks"].append({
"symbol": ticker,
"category": category,
"source": f"yahoo_{category}"
})
self.mentions[ticker]["count"] += 1
self.mentions[ticker]["sources"].append(f"Yahoo {category.replace('_', ' ').title()}")
if unique_tickers:
print(f" ✅ Yahoo {category}: {len(unique_tickers)} tickers")
except Exception as e:
print(f" ⚠️ Yahoo {category}: {str(e)[:30]}")
def scan_reddit_wsb(self):
"""Scrape r/wallstreetbets for hot stocks."""
print(" 🦍 Reddit r/wallstreetbets...")
try:
# Use old.reddit.com (more scrape-friendly)
url = "https://old.reddit.com/r/wallstreetbets/hot/.json"
headers = {**self.headers, "Accept": "application/json"}
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=15, context=SSL_CONTEXT) as resp:
data = resp.read()
if data[:2] == b'\x1f\x8b':
data = gzip.decompress(data)
posts = json.loads(data.decode('utf-8'))
tickers_found = []
for post in posts.get("data", {}).get("children", [])[:25]:
title = post.get("data", {}).get("title", "")
score = post.get("data", {}).get("score", 0)
# Extract tickers
tickers = self._extract_tickers(title)
for ticker in tickers:
if ticker not in ['USA', 'CEO', 'IPO', 'DD', 'WSB', 'YOLO', 'FD']:
weight = 2 if score > 1000 else 1
self.mentions[ticker]["count"] += weight
self.mentions[ticker]["sources"].append("Reddit WSB")
self.mentions[ticker]["sentiment_hints"].append(f"🦍 WSB: {title[:35]}...")
tickers_found.append(ticker)
self.results["social"].append({
"platform": "reddit_wsb",
"title": title[:100],
"score": score,
"tickers": tickers
})
print(f" ✅ WSB: {len(set(tickers_found))} tickers mentioned")
except Exception as e:
print(f" ❌ Reddit WSB: {str(e)[:40]}")
def scan_reddit_crypto(self):
"""Scrape r/cryptocurrency for hot coins."""
print(" 💎 Reddit r/cryptocurrency...")
try:
url = "https://old.reddit.com/r/cryptocurrency/hot/.json"
headers = {**self.headers, "Accept": "application/json"}
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req, timeout=15, context=SSL_CONTEXT) as resp:
data = resp.read()
if data[:2] == b'\x1f\x8b':
data = gzip.decompress(data)
posts = json.loads(data.decode('utf-8'))
crypto_keywords = {
"bitcoin": "BTC", "btc": "BTC", "ethereum": "ETH", "eth": "ETH",
"solana": "SOL", "sol": "SOL", "xrp": "XRP", "cardano": "ADA",
"dogecoin": "DOGE", "doge": "DOGE", "shiba": "SHIB", "pepe": "PEPE",
"avalanche": "AVAX", "polkadot": "DOT", "chainlink": "LINK",
}
tickers_found = []
for post in posts.get("data", {}).get("children", [])[:20]:
title = post.get("data", {}).get("title", "").lower()
score = post.get("data", {}).get("score", 0)
for word, ticker in crypto_keywords.items():
if word in title:
weight = 2 if score > 500 else 1
self.mentions[ticker]["count"] += weight
self.mentions[ticker]["sources"].append("Reddit Crypto")
tickers_found.append(ticker)
print(f" ✅ r/crypto: {len(set(tickers_found))} coins mentioned")
except Exception as e:
print(f" ❌ Reddit Crypto: {str(e)[:40]}")
def scan_twitter(self):
"""Use bird CLI to get trending finance/crypto tweets."""
print(" 🐦 Twitter/X...")
try:
# Find bird binary
bird_paths = [
"/home/clawdbot/.nvm/versions/node/v24.12.0/bin/bird",
"/usr/local/bin/bird",
"bird"
]
bird_bin = None
for p in bird_paths:
if Path(p).exists() or p == "bird":
bird_bin = p
break
if not bird_bin:
print(" ⚠️ Twitter: bird not found")
return
# Search for finance tweets
searches = [
("stocks", "stock OR $SPY OR $QQQ OR earnings"),
("crypto", "bitcoin OR ethereum OR crypto OR $BTC"),
]
for category, query in searches:
try:
env = os.environ.copy()
result = subprocess.run(
[bird_bin, "search", query, "-n", "15", "--json"],
capture_output=True, text=True, timeout=30, env=env
)
if result.returncode == 0 and result.stdout.strip():
tweets = json.loads(result.stdout)
for tweet in tweets[:10]:
text = tweet.get("text", "")
tickers = self._extract_tickers(text)
# Add crypto keywords
crypto_map = {"bitcoin": "BTC", "ethereum": "ETH", "solana": "SOL"}
for word, ticker in crypto_map.items():
if word in text.lower():
tickers.append(ticker)
for ticker in set(tickers):
self.mentions[ticker]["count"] += 1
self.mentions[ticker]["sources"].append("Twitter/X")
self.mentions[ticker]["sentiment_hints"].append(f"🐦 {text[:35]}...")
self.results["social"].append({
"platform": "twitter",
"text": text[:100],
"tickers": list(set(tickers))
})
print(f" ✅ Twitter {category}: processed")
except subprocess.TimeoutExpired:
print(f" ⚠️ Twitter {category}: timeout")
except json.JSONDecodeError:
print(f" ⚠️ Twitter {category}: no auth?")
except FileNotFoundError:
print(" ⚠️ Twitter: bird CLI not found")
except Exception as e:
print(f" ❌ Twitter: {str(e)[:40]}")
def _extract_tickers(self, text):
"""Extract stock/crypto tickers from text."""
patterns = [
r'\$([A-Z]{1,5})\b', # $AAPL
r'\(([A-Z]{2,5})\)', # (AAPL)
r'(?:^|\s)([A-Z]{2,4})(?:\s|$|[,.])', # Standalone caps
]
tickers = []
for pattern in patterns:
matches = re.findall(pattern, text)
tickers.extend(matches)
# Company mappings
companies = {
"Apple": "AAPL", "Microsoft": "MSFT", "Google": "GOOGL", "Alphabet": "GOOGL",
"Amazon": "AMZN", "Tesla": "TSLA", "Nvidia": "NVDA", "Meta": "META",
"Netflix": "NFLX", "GameStop": "GME", "AMD": "AMD", "Intel": "INTC",
"Palantir": "PLTR", "Coinbase": "COIN", "MicroStrategy": "MSTR",
}
for company, ticker in companies.items():
if company.lower() in text.lower():
tickers.append(ticker)
# Filter out common words
skip = {'USA', 'CEO', 'IPO', 'ETF', 'SEC', 'FDA', 'NYSE', 'API', 'USD', 'EU',
'UK', 'US', 'AI', 'IT', 'AT', 'TO', 'IN', 'ON', 'IS', 'IF', 'OR', 'AN',
'DD', 'WSB', 'YOLO', 'FD', 'OP', 'PM', 'AM'}
return list(set(t for t in tickers if t not in skip and len(t) >= 2))
def get_hot_summary(self):
"""Generate summary."""
sorted_mentions = sorted(
self.mentions.items(),
key=lambda x: x[1]["count"],
reverse=True
)
summary = {
"scan_time": self.results["timestamp"],
"top_trending": [],
"crypto_highlights": [],
"stock_highlights": [],
"social_buzz": [],
"breaking_news": []
}
for symbol, data in sorted_mentions[:20]:
summary["top_trending"].append({
"symbol": symbol,
"mentions": data["count"],
"sources": list(set(data["sources"])),
"signals": data["sentiment_hints"][:3]
})
# Crypto
seen = set()
for coin in self.results["crypto"] + self.results["movers"]:
if coin["symbol"] not in seen:
summary["crypto_highlights"].append(coin)
seen.add(coin["symbol"])
# Stocks
seen = set()
for stock in self.results["stocks"]:
if stock["symbol"] not in seen:
summary["stock_highlights"].append(stock)
seen.add(stock["symbol"])
# Social
for item in self.results["social"][:15]:
summary["social_buzz"].append(item)
# News
for news in self.results["news"][:10]:
if news.get("tickers_mentioned"):
summary["breaking_news"].append({
"title": news["title"],
"tickers": news["tickers_mentioned"]
})
return summary
def main():
import argparse
parser = argparse.ArgumentParser(description="🔥 Hot Scanner - Find trending stocks & crypto")
parser.add_argument("--no-social", action="store_true", help="Skip social media scans")
parser.add_argument("--json", action="store_true", help="Output only JSON")
args = parser.parse_args()
scanner = HotScanner(include_social=not args.no_social)
if not args.json:
print("=" * 60)
print("🔥 HOT SCANNER v2 - What's Trending Right Now?")
print(f"📅 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC")
print("=" * 60)
print()
scanner.scan_all()
summary = scanner.get_hot_summary()
# Save
output_file = CACHE_DIR / "hot_scan_latest.json"
with open(output_file, "w") as f:
json.dump(summary, f, indent=2, default=str)
if args.json:
print(json.dumps(summary, indent=2, default=str))
return
print()
print("=" * 60)
print("🔥 RESULTS")
print("=" * 60)
print("\n📊 TOP TRENDING (by buzz):\n")
for i, item in enumerate(summary["top_trending"][:12], 1):
sources = ", ".join(item["sources"][:2])
signal = item["signals"][0][:30] if item["signals"] else ""
print(f" {i:2}. {item['symbol']:8} ({item['mentions']:2} pts) [{sources}] {signal}")
print("\n🪙 CRYPTO:\n")
for coin in summary["crypto_highlights"][:8]:
change = coin.get("change_24h") or coin.get("price_change_24h")
change_str = f"{change:+.1f}%" if change else "🔥"
emoji = "🚀" if (change or 0) > 0 else "📉" if (change or 0) < 0 else "🔥"
print(f" {emoji} {coin.get('symbol', '?'):8} {coin.get('name', '')[:16]:16} {change_str:>8}")
print("\n📈 STOCKS:\n")
cat_emoji = {"gainers": "🟢", "losers": "🔴", "most_active": "📊"}
for stock in summary["stock_highlights"][:10]:
emoji = cat_emoji.get(stock.get("category"), "")
print(f" {emoji} {stock['symbol']:6} ({stock.get('category', 'N/A').replace('_', ' ')})")
if summary["social_buzz"]:
print("\n🐦 SOCIAL BUZZ:\n")
for item in summary["social_buzz"][:5]:
platform = item.get("platform", "?")
text = item.get("title") or item.get("text", "")
text = text[:55] + "..." if len(text) > 55 else text
print(f" [{platform}] {text}")
print("\n📰 NEWS:\n")
for news in summary["breaking_news"][:5]:
tickers = ", ".join(news["tickers"][:3])
title = news["title"][:55] + "..." if len(news["title"]) > 55 else news["title"]
print(f" [{tickers}] {title}")
print(f"\n💾 Saved: {output_file}\n")
if __name__ == "__main__":
main()

548
scripts/portfolio.py Normal file
View File

@@ -0,0 +1,548 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["yfinance>=0.2.40"]
# ///
"""
Portfolio management for stock-analysis skill.
Usage:
uv run portfolio.py create "Portfolio Name"
uv run portfolio.py list
uv run portfolio.py show [--portfolio NAME]
uv run portfolio.py delete "Portfolio Name"
uv run portfolio.py rename "Old Name" "New Name"
uv run portfolio.py add TICKER --quantity 100 --cost 150.00 [--portfolio NAME]
uv run portfolio.py update TICKER --quantity 150 [--portfolio NAME]
uv run portfolio.py remove TICKER [--portfolio NAME]
"""
import argparse
import json
import os
import sys
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from typing import Literal
import yfinance as yf
# Top 20 supported cryptocurrencies
SUPPORTED_CRYPTOS = {
"BTC-USD", "ETH-USD", "BNB-USD", "SOL-USD", "XRP-USD",
"ADA-USD", "DOGE-USD", "AVAX-USD", "DOT-USD", "MATIC-USD",
"LINK-USD", "ATOM-USD", "UNI-USD", "LTC-USD", "BCH-USD",
"XLM-USD", "ALGO-USD", "VET-USD", "FIL-USD", "NEAR-USD",
}
def get_storage_path() -> Path:
"""Get the portfolio storage path."""
# Use ~/.clawdbot/skills/stock-analysis/portfolios.json
state_dir = os.environ.get("CLAWDBOT_STATE_DIR", os.path.expanduser("~/.clawdbot"))
portfolio_dir = Path(state_dir) / "skills" / "stock-analysis"
portfolio_dir.mkdir(parents=True, exist_ok=True)
return portfolio_dir / "portfolios.json"
def detect_asset_type(ticker: str) -> Literal["stock", "crypto"]:
"""Detect asset type from ticker format."""
ticker_upper = ticker.upper()
if ticker_upper.endswith("-USD"):
base = ticker_upper[:-4]
if base.isalpha() and f"{base}-USD" in SUPPORTED_CRYPTOS:
return "crypto"
# Allow any *-USD ticker as crypto (flexible)
if base.isalpha():
return "crypto"
return "stock"
@dataclass
class Asset:
ticker: str
type: Literal["stock", "crypto"]
quantity: float
cost_basis: float
added_at: str
@dataclass
class Portfolio:
name: str
created_at: str
updated_at: str
assets: list[Asset]
class PortfolioStore:
"""Manages portfolio storage with atomic writes."""
def __init__(self, path: Path | None = None):
self.path = path or get_storage_path()
self._data: dict | None = None
def _load(self) -> dict:
"""Load portfolios from disk."""
if self._data is not None:
return self._data
if not self.path.exists():
self._data = {"version": 1, "portfolios": {}}
return self._data
try:
with open(self.path, "r", encoding="utf-8") as f:
self._data = json.load(f)
return self._data
except (json.JSONDecodeError, IOError):
self._data = {"version": 1, "portfolios": {}}
return self._data
def _save(self) -> None:
"""Save portfolios to disk with atomic write."""
if self._data is None:
return
# Ensure directory exists
self.path.parent.mkdir(parents=True, exist_ok=True)
# Atomic write: write to temp file, then rename
tmp_path = self.path.with_suffix(".tmp")
try:
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(self._data, f, indent=2)
tmp_path.replace(self.path)
except Exception:
if tmp_path.exists():
tmp_path.unlink()
raise
def _get_portfolio_key(self, name: str) -> str:
"""Convert portfolio name to storage key."""
return name.lower().replace(" ", "-")
def list_portfolios(self) -> list[str]:
"""List all portfolio names."""
data = self._load()
return [p["name"] for p in data["portfolios"].values()]
def get_portfolio(self, name: str) -> Portfolio | None:
"""Get a portfolio by name."""
data = self._load()
key = self._get_portfolio_key(name)
if key not in data["portfolios"]:
# Try case-insensitive match
for k, v in data["portfolios"].items():
if v["name"].lower() == name.lower():
key = k
break
else:
return None
p = data["portfolios"][key]
assets = [
Asset(
ticker=a["ticker"],
type=a["type"],
quantity=a["quantity"],
cost_basis=a["cost_basis"],
added_at=a["added_at"],
)
for a in p.get("assets", [])
]
return Portfolio(
name=p["name"],
created_at=p["created_at"],
updated_at=p["updated_at"],
assets=assets,
)
def create_portfolio(self, name: str) -> Portfolio:
"""Create a new portfolio."""
data = self._load()
key = self._get_portfolio_key(name)
if key in data["portfolios"]:
raise ValueError(f"Portfolio '{name}' already exists")
now = datetime.now().isoformat()
portfolio = {
"name": name,
"created_at": now,
"updated_at": now,
"assets": [],
}
data["portfolios"][key] = portfolio
self._save()
return Portfolio(name=name, created_at=now, updated_at=now, assets=[])
def delete_portfolio(self, name: str) -> bool:
"""Delete a portfolio."""
data = self._load()
key = self._get_portfolio_key(name)
# Try case-insensitive match
if key not in data["portfolios"]:
for k, v in data["portfolios"].items():
if v["name"].lower() == name.lower():
key = k
break
else:
return False
del data["portfolios"][key]
self._save()
return True
def rename_portfolio(self, old_name: str, new_name: str) -> bool:
"""Rename a portfolio."""
data = self._load()
old_key = self._get_portfolio_key(old_name)
new_key = self._get_portfolio_key(new_name)
# Find old portfolio
if old_key not in data["portfolios"]:
for k, v in data["portfolios"].items():
if v["name"].lower() == old_name.lower():
old_key = k
break
else:
return False
if new_key in data["portfolios"] and new_key != old_key:
raise ValueError(f"Portfolio '{new_name}' already exists")
portfolio = data["portfolios"].pop(old_key)
portfolio["name"] = new_name
portfolio["updated_at"] = datetime.now().isoformat()
data["portfolios"][new_key] = portfolio
self._save()
return True
def add_asset(
self,
portfolio_name: str,
ticker: str,
quantity: float,
cost_basis: float,
) -> Asset:
"""Add an asset to a portfolio."""
data = self._load()
key = self._get_portfolio_key(portfolio_name)
# Find portfolio
if key not in data["portfolios"]:
for k, v in data["portfolios"].items():
if v["name"].lower() == portfolio_name.lower():
key = k
break
else:
raise ValueError(f"Portfolio '{portfolio_name}' not found")
portfolio = data["portfolios"][key]
ticker = ticker.upper()
# Check if asset already exists
for asset in portfolio["assets"]:
if asset["ticker"] == ticker:
raise ValueError(f"Asset '{ticker}' already in portfolio. Use 'update' to modify.")
# Validate ticker
asset_type = detect_asset_type(ticker)
try:
stock = yf.Ticker(ticker)
info = stock.info
if "regularMarketPrice" not in info:
raise ValueError(f"Invalid ticker: {ticker}")
except Exception as e:
raise ValueError(f"Could not validate ticker '{ticker}': {e}")
now = datetime.now().isoformat()
asset = {
"ticker": ticker,
"type": asset_type,
"quantity": quantity,
"cost_basis": cost_basis,
"added_at": now,
}
portfolio["assets"].append(asset)
portfolio["updated_at"] = now
self._save()
return Asset(**asset)
def update_asset(
self,
portfolio_name: str,
ticker: str,
quantity: float | None = None,
cost_basis: float | None = None,
) -> Asset | None:
"""Update an asset in a portfolio."""
data = self._load()
key = self._get_portfolio_key(portfolio_name)
# Find portfolio
if key not in data["portfolios"]:
for k, v in data["portfolios"].items():
if v["name"].lower() == portfolio_name.lower():
key = k
break
else:
return None
portfolio = data["portfolios"][key]
ticker = ticker.upper()
for asset in portfolio["assets"]:
if asset["ticker"] == ticker:
if quantity is not None:
asset["quantity"] = quantity
if cost_basis is not None:
asset["cost_basis"] = cost_basis
portfolio["updated_at"] = datetime.now().isoformat()
self._save()
return Asset(**asset)
return None
def remove_asset(self, portfolio_name: str, ticker: str) -> bool:
"""Remove an asset from a portfolio."""
data = self._load()
key = self._get_portfolio_key(portfolio_name)
# Find portfolio
if key not in data["portfolios"]:
for k, v in data["portfolios"].items():
if v["name"].lower() == portfolio_name.lower():
key = k
break
else:
return False
portfolio = data["portfolios"][key]
ticker = ticker.upper()
original_len = len(portfolio["assets"])
portfolio["assets"] = [a for a in portfolio["assets"] if a["ticker"] != ticker]
if len(portfolio["assets"]) < original_len:
portfolio["updated_at"] = datetime.now().isoformat()
self._save()
return True
return False
def get_default_portfolio_name(self) -> str | None:
"""Get the default (first) portfolio name, or None if empty."""
portfolios = self.list_portfolios()
return portfolios[0] if portfolios else None
def format_currency(value: float) -> str:
"""Format a value as currency."""
if abs(value) >= 1_000_000:
return f"${value/1_000_000:.2f}M"
elif abs(value) >= 1_000:
return f"${value/1_000:.2f}K"
else:
return f"${value:.2f}"
def show_portfolio(portfolio: Portfolio, verbose: bool = False) -> None:
"""Display portfolio details with current prices."""
print(f"\n{'='*60}")
print(f"PORTFOLIO: {portfolio.name}")
print(f"Created: {portfolio.created_at[:10]} | Updated: {portfolio.updated_at[:10]}")
print(f"{'='*60}\n")
if not portfolio.assets:
print(" No assets in portfolio. Use 'add' to add assets.\n")
return
total_cost = 0.0
total_value = 0.0
print(f"{'Ticker':<12} {'Type':<8} {'Qty':>10} {'Cost':>12} {'Current':>12} {'Value':>14} {'P&L':>12}")
print("-" * 82)
for asset in portfolio.assets:
try:
stock = yf.Ticker(asset.ticker)
current_price = stock.info.get("regularMarketPrice", 0) or 0
except Exception:
current_price = 0
cost_total = asset.quantity * asset.cost_basis
current_value = asset.quantity * current_price
pnl = current_value - cost_total
pnl_pct = (pnl / cost_total * 100) if cost_total > 0 else 0
total_cost += cost_total
total_value += current_value
pnl_str = f"{'+' if pnl >= 0 else ''}{format_currency(pnl)} ({pnl_pct:+.1f}%)"
print(f"{asset.ticker:<12} {asset.type:<8} {asset.quantity:>10.4f} "
f"{format_currency(asset.cost_basis):>12} {format_currency(current_price):>12} "
f"{format_currency(current_value):>14} {pnl_str:>12}")
print("-" * 82)
total_pnl = total_value - total_cost
total_pnl_pct = (total_pnl / total_cost * 100) if total_cost > 0 else 0
print(f"{'TOTAL':<12} {'':<8} {'':<10} {format_currency(total_cost):>12} {'':<12} "
f"{format_currency(total_value):>14} {'+' if total_pnl >= 0 else ''}{format_currency(total_pnl)} ({total_pnl_pct:+.1f}%)")
print()
def main():
parser = argparse.ArgumentParser(description="Portfolio management for stock-analysis")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# create
create_parser = subparsers.add_parser("create", help="Create a new portfolio")
create_parser.add_argument("name", help="Portfolio name")
# list
subparsers.add_parser("list", help="List all portfolios")
# show
show_parser = subparsers.add_parser("show", help="Show portfolio details")
show_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
# delete
delete_parser = subparsers.add_parser("delete", help="Delete a portfolio")
delete_parser.add_argument("name", help="Portfolio name")
# rename
rename_parser = subparsers.add_parser("rename", help="Rename a portfolio")
rename_parser.add_argument("old_name", help="Current portfolio name")
rename_parser.add_argument("new_name", help="New portfolio name")
# add
add_parser = subparsers.add_parser("add", help="Add an asset to portfolio")
add_parser.add_argument("ticker", help="Stock/crypto ticker (e.g., AAPL, BTC-USD)")
add_parser.add_argument("--quantity", "-q", type=float, required=True, help="Quantity")
add_parser.add_argument("--cost", "-c", type=float, required=True, help="Cost basis per unit")
add_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
# update
update_parser = subparsers.add_parser("update", help="Update an asset in portfolio")
update_parser.add_argument("ticker", help="Stock/crypto ticker")
update_parser.add_argument("--quantity", "-q", type=float, help="New quantity")
update_parser.add_argument("--cost", "-c", type=float, help="New cost basis per unit")
update_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
# remove
remove_parser = subparsers.add_parser("remove", help="Remove an asset from portfolio")
remove_parser.add_argument("ticker", help="Stock/crypto ticker")
remove_parser.add_argument("--portfolio", "-p", help="Portfolio name (default: first portfolio)")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
store = PortfolioStore()
try:
if args.command == "create":
portfolio = store.create_portfolio(args.name)
print(f"Created portfolio: {portfolio.name}")
elif args.command == "list":
portfolios = store.list_portfolios()
if not portfolios:
print("No portfolios found. Use 'create' to create one.")
else:
print("\nPortfolios:")
for name in portfolios:
p = store.get_portfolio(name)
asset_count = len(p.assets) if p else 0
print(f" - {name} ({asset_count} assets)")
print()
elif args.command == "show":
portfolio_name = args.portfolio or store.get_default_portfolio_name()
if not portfolio_name:
print("No portfolios found. Use 'create' to create one.")
sys.exit(1)
portfolio = store.get_portfolio(portfolio_name)
if not portfolio:
print(f"Portfolio '{portfolio_name}' not found.")
sys.exit(1)
show_portfolio(portfolio)
elif args.command == "delete":
if store.delete_portfolio(args.name):
print(f"Deleted portfolio: {args.name}")
else:
print(f"Portfolio '{args.name}' not found.")
sys.exit(1)
elif args.command == "rename":
if store.rename_portfolio(args.old_name, args.new_name):
print(f"Renamed portfolio: {args.old_name} -> {args.new_name}")
else:
print(f"Portfolio '{args.old_name}' not found.")
sys.exit(1)
elif args.command == "add":
portfolio_name = args.portfolio or store.get_default_portfolio_name()
if not portfolio_name:
print("No portfolios found. Use 'create' to create one first.")
sys.exit(1)
asset = store.add_asset(portfolio_name, args.ticker, args.quantity, args.cost)
print(f"Added {asset.ticker} ({asset.type}) to {portfolio_name}: "
f"{asset.quantity} units @ {format_currency(asset.cost_basis)}")
elif args.command == "update":
portfolio_name = args.portfolio or store.get_default_portfolio_name()
if not portfolio_name:
print("No portfolios found.")
sys.exit(1)
if args.quantity is None and args.cost is None:
print("Must specify --quantity and/or --cost to update.")
sys.exit(1)
asset = store.update_asset(portfolio_name, args.ticker, args.quantity, args.cost)
if asset:
print(f"Updated {asset.ticker} in {portfolio_name}: "
f"{asset.quantity} units @ {format_currency(asset.cost_basis)}")
else:
print(f"Asset '{args.ticker}' not found in portfolio '{portfolio_name}'.")
sys.exit(1)
elif args.command == "remove":
portfolio_name = args.portfolio or store.get_default_portfolio_name()
if not portfolio_name:
print("No portfolios found.")
sys.exit(1)
if store.remove_asset(portfolio_name, args.ticker):
print(f"Removed {args.ticker.upper()} from {portfolio_name}")
else:
print(f"Asset '{args.ticker}' not found in portfolio '{portfolio_name}'.")
sys.exit(1)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

342
scripts/rumor_scanner.py Normal file
View File

@@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
🔮 RUMOR & BUZZ SCANNER
Scans for early signals, rumors, and whispers before they become mainstream news.
Sources:
- Twitter/X: "hearing", "rumor", "sources say", unusual buzz
- Google News: M&A, insider, upgrade/downgrade
- Unusual keywords detection
Usage: python3 rumor_scanner.py
"""
import json
import os
import subprocess
import sys
import re
from datetime import datetime, timezone
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.parse import quote_plus
import gzip
CACHE_DIR = Path(__file__).parent.parent / "cache"
CACHE_DIR.mkdir(exist_ok=True)
# Bird CLI path
BIRD_CLI = "/home/clawdbot/.nvm/versions/node/v24.12.0/bin/bird"
BIRD_ENV = Path(__file__).parent.parent / ".env"
def load_env():
"""Load environment variables from .env file."""
if BIRD_ENV.exists():
for line in BIRD_ENV.read_text().splitlines():
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip().strip('"').strip("'")
def fetch_url(url, timeout=15):
"""Fetch URL with headers."""
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'en-US,en;q=0.9',
}
req = Request(url, headers=headers)
try:
with urlopen(req, timeout=timeout) as resp:
data = resp.read()
if resp.info().get('Content-Encoding') == 'gzip':
data = gzip.decompress(data)
return data.decode('utf-8', errors='ignore')
except Exception as e:
return None
def search_twitter_rumors():
"""Search Twitter for rumors and early signals."""
results = []
# Rumor-focused search queries
queries = [
'"hearing that" stock OR $',
'"sources say" stock OR company',
'"rumor" merger OR acquisition',
'insider buying stock',
'"upgrade" OR "downgrade" stock tomorrow',
'$AAPL OR $TSLA OR $NVDA rumor',
'"breaking" stock market',
'M&A rumor',
]
load_env()
for query in queries[:4]: # Limit to avoid rate limits
try:
cmd = [BIRD_CLI, 'search', query, '-n', '10', '--json']
env = os.environ.copy()
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=env)
if result.returncode == 0 and result.stdout:
try:
tweets = json.loads(result.stdout)
for tweet in tweets:
text = tweet.get('text', '')
# Filter for actual rumors/signals
if any(kw in text.lower() for kw in ['hearing', 'rumor', 'source', 'insider', 'upgrade', 'downgrade', 'breaking', 'M&A', 'merger', 'acquisition']):
results.append({
'source': 'twitter',
'type': 'rumor',
'text': text[:300],
'author': tweet.get('author', {}).get('username', 'unknown'),
'likes': tweet.get('likes', 0),
'retweets': tweet.get('retweets', 0),
'query': query
})
except json.JSONDecodeError:
pass
except Exception as e:
pass
# Dedupe by text similarity
seen = set()
unique = []
for r in results:
key = r['text'][:100]
if key not in seen:
seen.add(key)
unique.append(r)
return unique
def search_twitter_buzz():
"""Search Twitter for general stock buzz - what are people talking about?"""
results = []
queries = [
'$SPY OR $QQQ',
'stock to buy',
'calls OR puts expiring',
'earnings play',
'short squeeze',
]
load_env()
for query in queries[:3]:
try:
cmd = [BIRD_CLI, 'search', query, '-n', '15', '--json']
env = os.environ.copy()
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, env=env)
if result.returncode == 0 and result.stdout:
try:
tweets = json.loads(result.stdout)
for tweet in tweets:
text = tweet.get('text', '')
# Extract stock symbols
symbols = re.findall(r'\$([A-Z]{1,5})\b', text)
if symbols:
results.append({
'source': 'twitter',
'type': 'buzz',
'text': text[:300],
'symbols': symbols,
'author': tweet.get('author', {}).get('username', 'unknown'),
'engagement': tweet.get('likes', 0) + tweet.get('retweets', 0) * 2
})
except json.JSONDecodeError:
pass
except Exception as e:
pass
# Sort by engagement
results.sort(key=lambda x: x.get('engagement', 0), reverse=True)
return results[:20]
def search_news_rumors():
"""Search Google News for M&A, insider, upgrade news."""
results = []
queries = [
'merger acquisition rumor',
'insider buying stock',
'analyst upgrade stock',
'takeover bid company',
'SEC investigation company',
]
for query in queries:
url = f"https://news.google.com/rss/search?q={quote_plus(query)}&hl=en-US&gl=US&ceid=US:en"
content = fetch_url(url)
if content:
import xml.etree.ElementTree as ET
try:
root = ET.fromstring(content)
for item in root.findall('.//item')[:5]:
title = item.find('title')
link = item.find('link')
pub_date = item.find('pubDate')
if title is not None:
title_text = title.text or ''
# Extract company names or symbols
results.append({
'source': 'google_news',
'type': 'news_rumor',
'title': title_text,
'link': link.text if link is not None else '',
'date': pub_date.text if pub_date is not None else '',
'query': query
})
except ET.ParseError:
pass
return results
def extract_symbols_from_text(text):
"""Extract stock symbols from text."""
# $SYMBOL pattern
dollar_symbols = re.findall(r'\$([A-Z]{1,5})\b', text)
# Common company name to symbol mapping
company_map = {
'apple': 'AAPL', 'tesla': 'TSLA', 'nvidia': 'NVDA', 'microsoft': 'MSFT',
'google': 'GOOGL', 'amazon': 'AMZN', 'meta': 'META', 'netflix': 'NFLX',
'coinbase': 'COIN', 'robinhood': 'HOOD', 'disney': 'DIS', 'intel': 'INTC',
'amd': 'AMD', 'palantir': 'PLTR', 'gamestop': 'GME', 'amc': 'AMC',
}
text_lower = text.lower()
company_symbols = [sym for name, sym in company_map.items() if name in text_lower]
return list(set(dollar_symbols + company_symbols))
def calculate_rumor_score(item):
"""Score a rumor by potential impact."""
score = 0
text = (item.get('text', '') + item.get('title', '')).lower()
# High impact keywords
if any(kw in text for kw in ['merger', 'acquisition', 'takeover', 'buyout']):
score += 5
if any(kw in text for kw in ['insider', 'ceo buying', 'director buying']):
score += 4
if any(kw in text for kw in ['upgrade', 'price target raised']):
score += 3
if any(kw in text for kw in ['downgrade', 'sec investigation', 'fraud']):
score += 3
if any(kw in text for kw in ['hearing', 'sources say', 'rumor']):
score += 2
if any(kw in text for kw in ['breaking', 'just in', 'alert']):
score += 2
# Engagement boost
if item.get('engagement', 0) > 100:
score += 2
if item.get('likes', 0) > 50:
score += 1
return score
def main():
print("=" * 60)
print("🔮 RUMOR & BUZZ SCANNER")
print(f"📅 {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC")
print("=" * 60)
print()
print("🔍 Scanning for early signals...")
print()
all_rumors = []
all_buzz = []
# Twitter Rumors
print(" 🐦 Twitter rumors...")
rumors = search_twitter_rumors()
print(f"{len(rumors)} potential rumors")
all_rumors.extend(rumors)
# Twitter Buzz
print(" 🐦 Twitter buzz...")
buzz = search_twitter_buzz()
print(f"{len(buzz)} buzz items")
all_buzz.extend(buzz)
# News Rumors
print(" 📰 News rumors...")
news = search_news_rumors()
print(f"{len(news)} news items")
all_rumors.extend(news)
# Score and sort rumors
for item in all_rumors:
item['score'] = calculate_rumor_score(item)
item['symbols'] = extract_symbols_from_text(item.get('text', '') + item.get('title', ''))
all_rumors.sort(key=lambda x: x['score'], reverse=True)
# Count symbol mentions in buzz
symbol_counts = {}
for item in all_buzz:
for sym in item.get('symbols', []):
symbol_counts[sym] = symbol_counts.get(sym, 0) + 1
# Output
print()
print("=" * 60)
print("🔮 RESULTS")
print("=" * 60)
print()
# Top Rumors
print("🚨 TOP RUMORS (by potential impact):")
print()
for item in all_rumors[:10]:
if item['score'] > 0:
source = item['source']
symbols = ', '.join(item.get('symbols', [])) or 'N/A'
text = item.get('text', item.get('title', ''))[:80]
print(f" [{item['score']}] [{source}] {symbols}")
print(f" {text}...")
print()
# Buzz Leaderboard
print("📊 BUZZ LEADERBOARD (most discussed):")
print()
sorted_symbols = sorted(symbol_counts.items(), key=lambda x: x[1], reverse=True)
for symbol, count in sorted_symbols[:15]:
bar = "" * min(count, 20)
print(f" ${symbol:5} {bar} ({count})")
print()
# Recent Buzz Snippets
print("💬 WHAT PEOPLE ARE SAYING:")
print()
for item in all_buzz[:8]:
author = item.get('author', 'anon')
text = item.get('text', '')[:120]
engagement = item.get('engagement', 0)
print(f" @{author} ({engagement}♥): {text}...")
print()
# Save results
output = {
'timestamp': datetime.now(timezone.utc).isoformat(),
'rumors': all_rumors[:20],
'buzz': all_buzz[:30],
'symbol_counts': symbol_counts,
}
output_file = CACHE_DIR / 'rumor_scan_latest.json'
output_file.write_text(json.dumps(output, indent=2, default=str))
print(f"💾 Saved: {output_file}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,381 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pytest>=8.0.0",
# "yfinance>=0.2.40",
# "pandas>=2.0.0",
# ]
# ///
"""
Tests for Stock Analysis Skill v6.0
Run with: uv run pytest test_stock_analysis.py -v
"""
import json
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timezone
import pandas as pd
# Import modules to test
from analyze_stock import (
detect_asset_type,
calculate_rsi,
fetch_stock_data,
analyze_earnings_surprise,
analyze_fundamentals,
analyze_momentum,
synthesize_signal,
EarningsSurprise,
Fundamentals,
MomentumAnalysis,
MarketContext,
StockData,
)
from dividends import analyze_dividends
from watchlist import (
add_to_watchlist,
remove_from_watchlist,
list_watchlist,
WatchlistItem,
)
from portfolio import PortfolioStore
class TestAssetTypeDetection:
"""Test asset type detection."""
def test_stock_detection(self):
assert detect_asset_type("AAPL") == "stock"
assert detect_asset_type("MSFT") == "stock"
assert detect_asset_type("googl") == "stock"
def test_crypto_detection(self):
assert detect_asset_type("BTC-USD") == "crypto"
assert detect_asset_type("ETH-USD") == "crypto"
assert detect_asset_type("sol-usd") == "crypto"
def test_edge_cases(self):
# Ticker ending in USD but not crypto format
assert detect_asset_type("MUSD") == "stock"
# Numbers in ticker
assert detect_asset_type("BRK.B") == "stock"
class TestRSICalculation:
"""Test RSI calculation."""
def test_rsi_overbought(self):
"""Test RSI > 70 (overbought)."""
# Create rising prices
prices = pd.Series([100 + i * 2 for i in range(20)])
rsi = calculate_rsi(prices, period=14)
assert rsi is not None
assert rsi > 70
def test_rsi_oversold(self):
"""Test RSI < 30 (oversold)."""
# Create falling prices
prices = pd.Series([100 - i * 2 for i in range(20)])
rsi = calculate_rsi(prices, period=14)
assert rsi is not None
assert rsi < 30
def test_rsi_insufficient_data(self):
"""Test RSI with insufficient data."""
prices = pd.Series([100, 101, 102]) # Too few points
rsi = calculate_rsi(prices, period=14)
assert rsi is None
class TestEarningsSurprise:
"""Test earnings surprise analysis."""
def test_earnings_beat(self):
"""Test positive earnings surprise."""
# Mock StockData with earnings beat
mock_earnings = pd.DataFrame({
"Reported EPS": [1.50],
"EPS Estimate": [1.20],
}, index=[pd.Timestamp("2024-01-15")])
mock_data = Mock(spec=StockData)
mock_data.earnings_history = mock_earnings
result = analyze_earnings_surprise(mock_data)
assert result is not None
assert result.score > 0
assert result.surprise_pct > 0
assert "Beat" in result.explanation
def test_earnings_miss(self):
"""Test negative earnings surprise."""
mock_earnings = pd.DataFrame({
"Reported EPS": [0.80],
"EPS Estimate": [1.00],
}, index=[pd.Timestamp("2024-01-15")])
mock_data = Mock(spec=StockData)
mock_data.earnings_history = mock_earnings
result = analyze_earnings_surprise(mock_data)
assert result is not None
assert result.score < 0
assert result.surprise_pct < 0
assert "Missed" in result.explanation
class TestFundamentals:
"""Test fundamentals analysis."""
def test_strong_fundamentals(self):
"""Test stock with strong fundamentals."""
mock_data = Mock(spec=StockData)
mock_data.info = {
"trailingPE": 15,
"operatingMargins": 0.25,
"revenueGrowth": 0.30,
"debtToEquity": 30,
}
result = analyze_fundamentals(mock_data)
assert result is not None
assert result.score > 0
assert "pe_ratio" in result.key_metrics
def test_weak_fundamentals(self):
"""Test stock with weak fundamentals."""
mock_data = Mock(spec=StockData)
mock_data.info = {
"trailingPE": 50,
"operatingMargins": 0.02,
"revenueGrowth": -0.10,
"debtToEquity": 300,
}
result = analyze_fundamentals(mock_data)
assert result is not None
assert result.score < 0
class TestMomentum:
"""Test momentum analysis."""
def test_overbought_momentum(self):
"""Test overbought conditions."""
# Create mock price history with rising prices near 52w high
dates = pd.date_range(end=datetime.now(), periods=100)
prices = pd.DataFrame({
"Close": [100 + i * 0.5 for i in range(100)],
"Volume": [1000000] * 100,
}, index=dates)
mock_data = Mock(spec=StockData)
mock_data.price_history = prices
mock_data.info = {
"fiftyTwoWeekHigh": 150,
"fiftyTwoWeekLow": 80,
"regularMarketPrice": 148,
}
result = analyze_momentum(mock_data)
assert result is not None
assert result.rsi_status == "overbought"
assert result.near_52w_high == True
assert result.score < 0 # Overbought = negative score
class TestSignalSynthesis:
"""Test signal synthesis."""
def test_buy_signal(self):
"""Test BUY recommendation synthesis."""
earnings = EarningsSurprise(score=0.8, explanation="Beat by 20%", actual_eps=1.2, expected_eps=1.0, surprise_pct=20)
fundamentals = Fundamentals(score=0.6, key_metrics={"pe_ratio": 15}, explanation="Strong margins")
signal = synthesize_signal(
ticker="TEST",
company_name="Test Corp",
earnings=earnings,
fundamentals=fundamentals,
analysts=None,
historical=None,
market_context=None,
sector=None,
earnings_timing=None,
momentum=None,
sentiment=None,
)
assert signal.recommendation == "BUY"
assert signal.confidence > 0.5
def test_sell_signal(self):
"""Test SELL recommendation synthesis."""
earnings = EarningsSurprise(score=-0.8, explanation="Missed by 20%", actual_eps=0.8, expected_eps=1.0, surprise_pct=-20)
fundamentals = Fundamentals(score=-0.6, key_metrics={"pe_ratio": 50}, explanation="Weak margins")
signal = synthesize_signal(
ticker="TEST",
company_name="Test Corp",
earnings=earnings,
fundamentals=fundamentals,
analysts=None,
historical=None,
market_context=None,
sector=None,
earnings_timing=None,
momentum=None,
sentiment=None,
)
assert signal.recommendation == "SELL"
def test_risk_off_penalty(self):
"""Test risk-off mode reduces BUY confidence."""
earnings = EarningsSurprise(score=0.8, explanation="Beat", actual_eps=1.2, expected_eps=1.0, surprise_pct=20)
fundamentals = Fundamentals(score=0.6, key_metrics={}, explanation="Strong")
market = MarketContext(
vix_level=25,
vix_status="elevated",
spy_trend_10d=2.0,
qqq_trend_10d=1.5,
market_regime="choppy",
score=-0.2,
explanation="Risk-off",
gld_change_5d=3.0,
tlt_change_5d=2.0,
uup_change_5d=1.5,
risk_off_detected=True,
)
signal = synthesize_signal(
ticker="TEST",
company_name="Test Corp",
earnings=earnings,
fundamentals=fundamentals,
analysts=None,
historical=None,
market_context=market,
sector=None,
earnings_timing=None,
momentum=None,
sentiment=None,
)
# Should still be BUY but with reduced confidence
assert signal.recommendation in ["BUY", "HOLD"]
assert any("RISK-OFF" in c for c in signal.caveats)
class TestWatchlist:
"""Test watchlist functionality."""
@patch('watchlist.get_current_price')
@patch('watchlist.save_watchlist')
@patch('watchlist.load_watchlist')
def test_add_to_watchlist(self, mock_load, mock_save, mock_price):
"""Test adding ticker to watchlist."""
mock_load.return_value = []
mock_price.return_value = 150.0
mock_save.return_value = None
result = add_to_watchlist("AAPL", target_price=200.0)
assert result["success"] == True
assert result["action"] == "added"
assert result["ticker"] == "AAPL"
assert result["target_price"] == 200.0
@patch('watchlist.save_watchlist')
@patch('watchlist.load_watchlist')
def test_remove_from_watchlist(self, mock_load, mock_save):
"""Test removing ticker from watchlist."""
mock_load.return_value = [
WatchlistItem(ticker="AAPL", added_at="2024-01-01T00:00:00+00:00")
]
mock_save.return_value = None
result = remove_from_watchlist("AAPL")
assert result["success"] == True
assert result["removed"] == "AAPL"
class TestDividendAnalysis:
"""Test dividend analysis."""
@patch('yfinance.Ticker')
def test_dividend_stock(self, mock_ticker):
"""Test analysis of dividend-paying stock."""
mock_stock = Mock()
mock_stock.info = {
"longName": "Johnson & Johnson",
"regularMarketPrice": 160.0,
"dividendYield": 0.03,
"dividendRate": 4.80,
"trailingEps": 6.00,
}
mock_stock.dividends = pd.Series(
[1.2, 1.2, 1.2, 1.2] * 5, # 5 years of quarterly dividends
index=pd.date_range(start="2019-01-01", periods=20, freq="Q")
)
mock_ticker.return_value = mock_stock
result = analyze_dividends("JNJ")
assert result is not None
assert result.dividend_yield == 3.0
assert result.payout_ratio == 80.0
assert result.income_rating != "no_dividend"
@patch('yfinance.Ticker')
def test_no_dividend_stock(self, mock_ticker):
"""Test analysis of non-dividend stock."""
mock_stock = Mock()
mock_stock.info = {
"longName": "Amazon",
"regularMarketPrice": 180.0,
"dividendYield": None,
"dividendRate": None,
}
mock_ticker.return_value = mock_stock
result = analyze_dividends("AMZN")
assert result is not None
assert result.income_rating == "no_dividend"
class TestIntegration:
"""Integration tests (require network)."""
@pytest.mark.integration
def test_real_stock_analysis(self):
"""Test real stock analysis (AAPL)."""
data = fetch_stock_data("AAPL", verbose=False)
assert data is not None
assert data.ticker == "AAPL"
assert data.info is not None
assert "regularMarketPrice" in data.info
@pytest.mark.integration
def test_real_crypto_analysis(self):
"""Test real crypto analysis (BTC-USD)."""
data = fetch_stock_data("BTC-USD", verbose=False)
assert data is not None
assert data.asset_type == "crypto"
# Run tests
if __name__ == "__main__":
pytest.main([__file__, "-v", "--ignore-glob=*integration*"])

336
scripts/watchlist.py Normal file
View File

@@ -0,0 +1,336 @@
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "yfinance>=0.2.40",
# ]
# ///
"""
Stock Watchlist with Price Alerts.
Usage:
uv run watchlist.py add AAPL # Add to watchlist
uv run watchlist.py add AAPL --target 200 # With price target
uv run watchlist.py add AAPL --stop 150 # With stop loss
uv run watchlist.py add AAPL --alert-on signal # Alert on signal change
uv run watchlist.py remove AAPL # Remove from watchlist
uv run watchlist.py list # Show watchlist
uv run watchlist.py check # Check for triggered alerts
uv run watchlist.py check --notify # Check and format for notification
"""
import argparse
import json
import sys
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Literal
import yfinance as yf
# Storage
WATCHLIST_DIR = Path.home() / ".clawdbot" / "skills" / "stock-analysis"
WATCHLIST_FILE = WATCHLIST_DIR / "watchlist.json"
@dataclass
class WatchlistItem:
ticker: str
added_at: str
price_at_add: float | None = None
target_price: float | None = None # Alert when price >= target
stop_price: float | None = None # Alert when price <= stop
alert_on_signal: bool = False # Alert when recommendation changes
last_signal: str | None = None # BUY/HOLD/SELL
last_check: str | None = None
notes: str | None = None
@dataclass
class Alert:
ticker: str
alert_type: Literal["target_hit", "stop_hit", "signal_change"]
message: str
current_price: float
trigger_value: float | str
timestamp: str
def ensure_dirs():
"""Create storage directories."""
WATCHLIST_DIR.mkdir(parents=True, exist_ok=True)
def load_watchlist() -> list[WatchlistItem]:
"""Load watchlist from file."""
if WATCHLIST_FILE.exists():
data = json.loads(WATCHLIST_FILE.read_text())
return [WatchlistItem(**item) for item in data]
return []
def save_watchlist(items: list[WatchlistItem]):
"""Save watchlist to file."""
ensure_dirs()
data = [asdict(item) for item in items]
WATCHLIST_FILE.write_text(json.dumps(data, indent=2))
def get_current_price(ticker: str) -> float | None:
"""Get current price for a ticker."""
try:
stock = yf.Ticker(ticker)
price = stock.info.get("regularMarketPrice") or stock.info.get("currentPrice")
return float(price) if price else None
except Exception:
return None
def add_to_watchlist(
ticker: str,
target_price: float | None = None,
stop_price: float | None = None,
alert_on_signal: bool = False,
notes: str | None = None,
) -> dict:
"""Add ticker to watchlist."""
ticker = ticker.upper()
# Validate ticker
current_price = get_current_price(ticker)
if current_price is None:
return {"success": False, "error": f"Invalid ticker: {ticker}"}
# Load existing watchlist
watchlist = load_watchlist()
# Check if already exists
for item in watchlist:
if item.ticker == ticker:
# Update existing
item.target_price = target_price or item.target_price
item.stop_price = stop_price or item.stop_price
item.alert_on_signal = alert_on_signal or item.alert_on_signal
item.notes = notes or item.notes
save_watchlist(watchlist)
return {
"success": True,
"action": "updated",
"ticker": ticker,
"current_price": current_price,
"target_price": item.target_price,
"stop_price": item.stop_price,
"alert_on_signal": item.alert_on_signal,
}
# Add new
item = WatchlistItem(
ticker=ticker,
added_at=datetime.now(timezone.utc).isoformat(),
price_at_add=current_price,
target_price=target_price,
stop_price=stop_price,
alert_on_signal=alert_on_signal,
notes=notes,
)
watchlist.append(item)
save_watchlist(watchlist)
return {
"success": True,
"action": "added",
"ticker": ticker,
"current_price": current_price,
"target_price": target_price,
"stop_price": stop_price,
"alert_on_signal": alert_on_signal,
}
def remove_from_watchlist(ticker: str) -> dict:
"""Remove ticker from watchlist."""
ticker = ticker.upper()
watchlist = load_watchlist()
original_len = len(watchlist)
watchlist = [item for item in watchlist if item.ticker != ticker]
if len(watchlist) == original_len:
return {"success": False, "error": f"{ticker} not in watchlist"}
save_watchlist(watchlist)
return {"success": True, "removed": ticker}
def list_watchlist() -> dict:
"""List all watchlist items with current prices."""
watchlist = load_watchlist()
if not watchlist:
return {"success": True, "items": [], "count": 0}
items = []
for item in watchlist:
current_price = get_current_price(item.ticker)
# Calculate change since added
change_pct = None
if current_price and item.price_at_add:
change_pct = ((current_price - item.price_at_add) / item.price_at_add) * 100
# Distance to target/stop
to_target = None
to_stop = None
if current_price:
if item.target_price:
to_target = ((item.target_price - current_price) / current_price) * 100
if item.stop_price:
to_stop = ((item.stop_price - current_price) / current_price) * 100
items.append({
"ticker": item.ticker,
"current_price": current_price,
"price_at_add": item.price_at_add,
"change_pct": round(change_pct, 2) if change_pct else None,
"target_price": item.target_price,
"to_target_pct": round(to_target, 2) if to_target else None,
"stop_price": item.stop_price,
"to_stop_pct": round(to_stop, 2) if to_stop else None,
"alert_on_signal": item.alert_on_signal,
"last_signal": item.last_signal,
"added_at": item.added_at[:10],
"notes": item.notes,
})
return {"success": True, "items": items, "count": len(items)}
def check_alerts(notify_format: bool = False) -> dict:
"""Check watchlist for triggered alerts."""
watchlist = load_watchlist()
alerts: list[Alert] = []
now = datetime.now(timezone.utc).isoformat()
for item in watchlist:
current_price = get_current_price(item.ticker)
if current_price is None:
continue
# Check target price
if item.target_price and current_price >= item.target_price:
alerts.append(Alert(
ticker=item.ticker,
alert_type="target_hit",
message=f"🎯 {item.ticker} hit target! ${current_price:.2f} >= ${item.target_price:.2f}",
current_price=current_price,
trigger_value=item.target_price,
timestamp=now,
))
# Check stop price
if item.stop_price and current_price <= item.stop_price:
alerts.append(Alert(
ticker=item.ticker,
alert_type="stop_hit",
message=f"🛑 {item.ticker} hit stop! ${current_price:.2f} <= ${item.stop_price:.2f}",
current_price=current_price,
trigger_value=item.stop_price,
timestamp=now,
))
# Check signal change (requires running analyze_stock)
if item.alert_on_signal:
try:
import subprocess
result = subprocess.run(
["uv", "run", str(Path(__file__).parent / "analyze_stock.py"), item.ticker, "--output", "json"],
capture_output=True,
text=True,
timeout=60,
)
if result.returncode == 0:
analysis = json.loads(result.stdout)
new_signal = analysis.get("recommendation")
if item.last_signal and new_signal and new_signal != item.last_signal:
alerts.append(Alert(
ticker=item.ticker,
alert_type="signal_change",
message=f"📊 {item.ticker} signal changed: {item.last_signal}{new_signal}",
current_price=current_price,
trigger_value=f"{item.last_signal}{new_signal}",
timestamp=now,
))
# Update last signal
item.last_signal = new_signal
except Exception:
pass
item.last_check = now
# Save updated watchlist (with last_signal updates)
save_watchlist(watchlist)
# Format output
if notify_format and alerts:
# Format for Telegram notification
lines = ["📢 **Stock Alerts**\n"]
for alert in alerts:
lines.append(alert.message)
return {"success": True, "alerts": [asdict(a) for a in alerts], "notification": "\n".join(lines)}
return {"success": True, "alerts": [asdict(a) for a in alerts], "count": len(alerts)}
def main():
parser = argparse.ArgumentParser(description="Stock Watchlist with Alerts")
subparsers = parser.add_subparsers(dest="command", required=True)
# Add
add_parser = subparsers.add_parser("add", help="Add ticker to watchlist")
add_parser.add_argument("ticker", help="Stock ticker")
add_parser.add_argument("--target", type=float, help="Target price for alert")
add_parser.add_argument("--stop", type=float, help="Stop loss price for alert")
add_parser.add_argument("--alert-on", choices=["signal"], help="Alert on signal change")
add_parser.add_argument("--notes", help="Notes")
# Remove
remove_parser = subparsers.add_parser("remove", help="Remove ticker from watchlist")
remove_parser.add_argument("ticker", help="Stock ticker")
# List
subparsers.add_parser("list", help="List watchlist")
# Check
check_parser = subparsers.add_parser("check", help="Check for triggered alerts")
check_parser.add_argument("--notify", action="store_true", help="Format for notification")
args = parser.parse_args()
if args.command == "add":
result = add_to_watchlist(
args.ticker,
target_price=args.target,
stop_price=args.stop,
alert_on_signal=(args.alert_on == "signal"),
notes=args.notes,
)
print(json.dumps(result, indent=2))
elif args.command == "remove":
result = remove_from_watchlist(args.ticker)
print(json.dumps(result, indent=2))
elif args.command == "list":
result = list_watchlist()
print(json.dumps(result, indent=2))
elif args.command == "check":
result = check_alerts(notify_format=args.notify)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()