Initial commit with translated description

This commit is contained in:
2026-03-29 10:21:46 +08:00
commit 18e90b0b09
67 changed files with 20609 additions and 0 deletions

155
README.md Normal file
View File

@@ -0,0 +1,155 @@
# Finance News Skill for OpenClaw
AI-powered market news briefings with configurable language output and automated delivery.
## Features
- **Multi-source aggregation:** Reuters, WSJ, FT, Bloomberg, CNBC, Yahoo Finance, Tagesschau, Handelsblatt
- **Global markets:** US (S&P, Dow, NASDAQ), Europe (DAX, STOXX, FTSE), Japan (Nikkei)
- **AI summaries:** LLM-powered analysis in German or English
- **Automated briefings:** Morning (market open) and evening (market close)
- **WhatsApp/Telegram delivery:** Send briefings via openclaw
- **Portfolio tracking:** Personalized news for your stocks with price alerts
- **Lobster workflows:** Approval gates before sending
## Quick Start
### Docker (Recommended)
```bash
# Build the Docker image
docker build -t finance-news-briefing .
# Generate a briefing
docker run --rm -v "$PWD/config:/app/config:ro" \
finance-news-briefing python3 scripts/briefing.py \
--time morning --lang de --json --fast
```
### Lobster Workflow
```bash
# Set required environment variables
export FINANCE_NEWS_TARGET="your-group-jid@g.us" # WhatsApp JID or Telegram chat ID
export FINANCE_NEWS_CHANNEL="whatsapp" # or "telegram"
# Run workflow (halts for approval before sending)
lobster run workflows/briefing.yaml --args-json '{"time":"morning","lang":"de"}'
```
### CLI (Legacy)
```bash
# Generate a briefing
finance-news briefing --morning --lang de
# Use fast mode + deadline (recommended)
finance-news briefing --morning --lang de --fast --deadline 300
```
## Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| `FINANCE_NEWS_TARGET` | Delivery target (WhatsApp JID, group name, or Telegram chat ID) | `120363421796203667@g.us` |
| `FINANCE_NEWS_CHANNEL` | Delivery channel | `whatsapp` or `telegram` |
| `SKILL_DIR` | Path to skill directory (for Lobster) | `$HOME/projects/finance-news-openclaw-skill` |
## Installation
### Option 1: Docker (Recommended)
```bash
git clone https://github.com/kesslerio/finance-news-openclaw-skill.git
cd finance-news-openclaw-skill
docker build -t finance-news-briefing .
```
### Option 2: Native Python
```bash
# Clone repository
git clone https://github.com/kesslerio/finance-news-openclaw-skill.git \
~/openclaw/skills/finance-news
# Create virtual environment
cd ~/openclaw/skills/finance-news
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Create CLI symlink
ln -sf ~/openclaw/skills/finance-news/scripts/finance-news ~/.local/bin/finance-news
```
## Configuration
Configuration is stored in `config/config.json`:
- **RSS Feeds:** Enable/disable news sources per region
- **Markets:** Choose which indices to track
- **Delivery:** WhatsApp/Telegram settings
- **Language:** German (`de`) or English (`en`) output
- **Schedule:** Cron times for morning/evening briefings
- **LLM:** Model order preference for headlines, summaries, translations
Run the setup wizard for interactive configuration:
```bash
finance-news setup
```
## Lobster Workflow
The skill includes a Lobster workflow (`workflows/briefing.yaml`) that:
1. **Generates** briefing via Docker
2. **Translates** portfolio headlines (German only, via openclaw)
3. **Halts** for approval (shows preview)
4. **Sends** macro briefing to channel
5. **Sends** portfolio briefing to channel
### Workflow Arguments
| Arg | Default | Description |
|-----|---------|-------------|
| `time` | `morning` | Briefing type: `morning` or `evening` |
| `lang` | `de` | Language: `en` or `de` |
| `channel` | env var | `whatsapp` or `telegram` |
| `target` | env var | Group JID/name or chat ID |
| `fast` | `false` | Use fast mode (shorter timeouts) |
## Portfolio
Manage your stock watchlist in `config/portfolio.csv`:
```bash
finance-news portfolio-list # View portfolio
finance-news portfolio-add NVDA # Add stock
finance-news portfolio-remove TSLA # Remove stock
finance-news portfolio-import stocks.csv # Import from CSV
```
Portfolio briefings show:
- Top gainers and losers from your holdings
- Relevant news articles with translations
- Shortened hyperlinks for easy access
## Dependencies
- Python 3.10+
- Docker (recommended)
- openclaw CLI (for message delivery and LLM)
- Lobster (for workflow automation)
### Optional
- OpenBB (`openbb-quote`) for enhanced market data
## License
Apache 2.0 - See [LICENSE](LICENSE) file for details.
## Related Skills
- **[task-tracker](https://github.com/kesslerio/task-tracker-openclaw-skill):** Personal task management with daily standups

280
SKILL.md Normal file
View File

@@ -0,0 +1,280 @@
---
name: finance-news
description: "市场新闻简报包含AI摘要。"
---
# Finance News Skill
AI-powered market news briefings with configurable language output and automated delivery.
## First-Time Setup
Run the interactive setup wizard to configure your sources, delivery channels, and schedule:
```bash
finance-news setup
```
The wizard will guide you through:
- 📰 **RSS Feeds:** Enable/disable WSJ, Barron's, CNBC, Yahoo, etc.
- 📊 **Markets:** Choose regions (US, Europe, Japan, Asia)
- 📤 **Delivery:** Configure WhatsApp/Telegram group
- 🌐 **Language:** Set default language (English/German)
-**Schedule:** Configure morning/evening cron times
You can also configure specific sections:
```bash
finance-news setup --section feeds # Just RSS feeds
finance-news setup --section delivery # Just delivery channels
finance-news setup --section schedule # Just cron schedule
finance-news setup --reset # Reset to defaults
finance-news config # Show current config
```
## Quick Start
```bash
# Generate morning briefing
finance-news briefing --morning
# View market overview
finance-news market
# Get news for your portfolio
finance-news portfolio
# Get news for specific stock
finance-news news AAPL
```
## Features
### 📊 Market Coverage
- **US Markets:** S&P 500, Dow Jones, NASDAQ
- **Europe:** DAX, STOXX 50, FTSE 100
- **Japan:** Nikkei 225
### 📰 News Sources
- **Premium:** WSJ, Barron's (RSS feeds)
- **Free:** CNBC, Yahoo Finance, Finnhub
- **Portfolio:** Ticker-specific news from Yahoo
### 🤖 AI Summaries
- Gemini-powered analysis
- Configurable language (English/German)
- Briefing styles: summary, analysis, headlines
### 📅 Automated Briefings
- **Morning:** 6:30 AM PT (US market open)
- **Evening:** 1:00 PM PT (US market close)
- **Delivery:** WhatsApp (configure group in cron scripts)
## Commands
### Briefing Generation
```bash
# Morning briefing (English is default)
finance-news briefing --morning
# Evening briefing with WhatsApp delivery
finance-news briefing --evening --send --group "Market Briefing"
# German language option
finance-news briefing --morning --lang de
# Analysis style (more detailed)
finance-news briefing --style analysis
```
### Market Data
```bash
# Market overview (indices + top headlines)
finance-news market
# JSON output for processing
finance-news market --json
```
### Portfolio Management
```bash
# List portfolio
finance-news portfolio-list
# Add stock
finance-news portfolio-add NVDA --name "NVIDIA Corporation" --category Tech
# Remove stock
finance-news portfolio-remove TSLA
# Import from CSV
finance-news portfolio-import ~/my_stocks.csv
# Interactive portfolio creation
finance-news portfolio-create
```
### Ticker News
```bash
# News for specific stock
finance-news news AAPL
finance-news news TSLA
```
## Configuration
### Portfolio CSV Format
Location: `~/clawd/skills/finance-news/config/portfolio.csv`
```csv
symbol,name,category,notes
AAPL,Apple Inc.,Tech,Core holding
NVDA,NVIDIA Corporation,Tech,AI play
MSFT,Microsoft Corporation,Tech,
```
### Sources Configuration
Location: `~/clawd/skills/finance-news/config/config.json` (legacy fallback: `config/sources.json`)
- RSS feeds for WSJ, Barron's, CNBC, Yahoo
- Market indices by region
- Language settings
## Cron Jobs
### Setup via OpenClaw
```bash
# Add morning briefing cron job
openclaw cron add --schedule "30 6 * * 1-5" \
--timezone "America/Los_Angeles" \
--command "bash ~/clawd/skills/finance-news/cron/morning.sh"
# Add evening briefing cron job
openclaw cron add --schedule "0 13 * * 1-5" \
--timezone "America/Los_Angeles" \
--command "bash ~/clawd/skills/finance-news/cron/evening.sh"
```
### Manual Cron (crontab)
```cron
# Morning briefing (6:30 AM PT, weekdays)
30 6 * * 1-5 bash ~/clawd/skills/finance-news/cron/morning.sh
# Evening briefing (1:00 PM PT, weekdays)
0 13 * * 1-5 bash ~/clawd/skills/finance-news/cron/evening.sh
```
## Sample Output
```markdown
🌅 **Börsen-Morgen-Briefing**
Dienstag, 21. Januar 2026 | 06:30 Uhr
📊 **Märkte**
• S&P 500: 5.234 (+0,3%)
• DAX: 16.890 (-0,1%)
• Nikkei: 35.678 (+0,5%)
📈 **Dein Portfolio**
• AAPL $256 (+1,2%) — iPhone-Verkäufe übertreffen Erwartungen
• NVDA $512 (+3,4%) — KI-Chip-Nachfrage steigt
🔥 **Top Stories**
• [WSJ] Fed signalisiert mögliche Zinssenkung im März
• [CNBC] Tech-Sektor führt Rally an
🤖 **Analyse**
Der S&P zeigt Stärke. Dein Portfolio profitiert von NVDA's
Momentum. Fed-Kommentare könnten Volatilität auslösen.
```
## Integration
### With OpenBB (existing skill)
```bash
# Get detailed quote, then news
openbb-quote AAPL && finance-news news AAPL
```
### With OpenClaw Agent
The agent will automatically use this skill when asked about:
- "What's the market doing?"
- "News for my portfolio"
- "Generate morning briefing"
- "What's happening with AAPL?"
### With Lobster (Workflow Engine)
Run briefings via [Lobster](https://github.com/openclaw/lobster) for approval gates and resumability:
```bash
# Run with approval before WhatsApp send
lobster "workflows.run --file workflows/briefing.yaml"
# With custom args
lobster "workflows.run --file workflows/briefing.yaml --args-json '{\"time\":\"evening\",\"lang\":\"en\"}'"
```
See `workflows/README.md` for full documentation.
## Files
```
skills/finance-news/
├── SKILL.md # This documentation
├── Dockerfile # NixOS-compatible container
├── config/
│ ├── portfolio.csv # Your watchlist
│ ├── config.json # RSS/API/language configuration
│ ├── alerts.json # Price target alerts
│ └── manual_earnings.json # Earnings calendar overrides
├── scripts/
│ ├── finance-news # Main CLI
│ ├── briefing.py # Briefing generator
│ ├── fetch_news.py # News aggregator
│ ├── portfolio.py # Portfolio CRUD
│ ├── summarize.py # AI summarization
│ ├── alerts.py # Price alert management
│ ├── earnings.py # Earnings calendar
│ ├── ranking.py # Headline ranking
│ └── stocks.py # Stock management
├── workflows/
│ ├── briefing.yaml # Lobster workflow with approval gate
│ └── README.md # Workflow documentation
├── cron/
│ ├── morning.sh # Morning cron (Docker-based)
│ └── evening.sh # Evening cron (Docker-based)
└── cache/ # 15-minute news cache
```
## Dependencies
- Python 3.10+
- `feedparser` (`pip install feedparser`)
- Gemini CLI (`brew install gemini-cli`)
- OpenBB (existing `openbb-quote` wrapper)
- OpenClaw message tool (for WhatsApp delivery)
## Troubleshooting
### Gemini not working
```bash
# Authenticate Gemini
gemini # Follow login flow
```
### RSS feeds timing out
- Check network connectivity
- WSJ/Barron's may require subscription cookies for some content
- Free feeds (CNBC, Yahoo) should always work
### WhatsApp delivery failing
- Verify WhatsApp group exists and bot has access
- Check `openclaw doctor` for WhatsApp status

6
_meta.json Normal file
View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn7fmw4ybcy50qzp1d2dvb1h517znaes",
"slug": "finance-news",
"version": "1.0.1",
"publishedAt": 1770017717268
}

242
config/config.json Normal file
View File

@@ -0,0 +1,242 @@
{
"rss_feeds": {
"wsj": {
"name": "Wall Street Journal",
"enabled": true,
"markets": "https://feeds.content.dowjones.io/public/rss/RSSMarketsMain",
"daily": "https://feeds.content.dowjones.io/public/rss/RSSWSJD"
},
"tagesschau": {
"name": "Tagesschau",
"enabled": true,
"wirtschaft": "https://www.tagesschau.de/wirtschaft/weltwirtschaft/index~rss2.xml"
},
"finanzen_net": {
"name": "Finanzen.net",
"enabled": true,
"news": "https://www.finanzen.net/rss/news"
},
"handelsblatt": {
"name": "Handelsblatt",
"enabled": true,
"finanzen": "https://feeds.cms.handelsblatt.com/finanzen"
},
"zeit": {
"name": "ZEIT Wirtschaft",
"enabled": true,
"wirtschaft": "https://newsfeed.zeit.de/wirtschaft/index"
},
"marketwatch": {
"name": "MarketWatch",
"enabled": true,
"topstories": "https://feeds.content.dowjones.io/public/rss/mw_topstories"
},
"reuters": {
"name": "Reuters",
"enabled": true,
"markets": "https://news.google.com/rss/search?q=site%3Areuters.com+markets+OR+stocks+OR+economy+OR+fed+OR+earnings&hl=en-US&gl=US&ceid=US%3Aen",
"note": "Google News RSS wrapper for Reuters - filtered for finance/markets."
},
"ft": {
"name": "Financial Times",
"enabled": true,
"markets": "https://www.ft.com/markets?format=rss"
},
"bloomberg": {
"name": "Bloomberg",
"enabled": true,
"markets": "https://feeds.bloomberg.com/markets/news.rss"
},
"barrons": {
"name": "Barron's",
"enabled": false,
"main": "https://www.barrons.com/market-data/rss/articles",
"note": "Requires subscription - enable after adding credentials"
},
"cnbc": {
"name": "CNBC",
"enabled": true,
"top": "https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=10001147",
"business": "https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=15839069",
"markets": "https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=20910258",
"world": "https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=10000664",
"tech": "https://www.cnbc.com/id/19854910/device/rss/rss.html"
},
"yahoo": {
"name": "Yahoo Finance",
"enabled": true,
"top": "https://finance.yahoo.com/rss/topstories"
}
},
"headline_sources": ["reuters", "wsj", "ft", "bloomberg", "marketwatch", "cnbc", "yahoo"],
"headline_sources_by_lang": {
"de": ["tagesschau", "handelsblatt", "zeit", "finanzen_net", "reuters", "wsj", "ft", "bloomberg", "marketwatch", "cnbc", "yahoo"],
"en": ["reuters", "wsj", "ft", "bloomberg", "marketwatch", "cnbc", "yahoo"]
},
"headline_exclude": [],
"source_weights": {
"reuters": 5,
"wsj": 4,
"ft": 4,
"bloomberg": 3,
"marketwatch": 3,
"cnbc": 2,
"tagesschau": 4,
"handelsblatt": 4,
"zeit": 4,
"finanzen_net": 3,
"yahoo": 1
},
"source_tiers": {
"paid": ["wsj", "ft", "barrons"],
"free": ["bloomberg", "marketwatch", "yahoo", "cnbc", "tagesschau", "handelsblatt", "zeit", "finanzen_net"]
},
"headline_shortlist_size_by_lang": {
"de": 30,
"en": 20
},
"portfolio_deadline_sec": 360,
"portfolio": {
"briefing_limit": 10,
"prioritization_enabled": true,
"prioritization_weights": {
"type": 0.40,
"volatility": 0.35,
"news_volume": 0.25
}
},
"markets": {
"us": {
"name": "US Markets",
"enabled": true,
"indices": ["^GSPC", "^DJI", "^IXIC"],
"index_names": {"^GSPC": "S&P 500", "^DJI": "Dow Jones", "^IXIC": "NASDAQ"}
},
"europe": {
"name": "Europe",
"enabled": true,
"indices": ["^GDAXI", "^STOXX50E", "^FTSE"],
"index_names": {"^GDAXI": "DAX", "^STOXX50E": "STOXX 50", "^FTSE": "FTSE 100"}
},
"japan": {
"name": "Japan",
"enabled": true,
"indices": ["^N225"],
"index_names": {"^N225": "Nikkei 225"}
}
},
"language": {
"default": "en",
"supported": ["en", "de"]
},
"delivery": {
"whatsapp": {
"enabled": true,
"group": ""
},
"telegram": {
"enabled": false,
"group": ""
}
},
"schedule": {
"morning": {
"enabled": true,
"cron": "30 6 * * 1-5",
"timezone": "America/Los_Angeles",
"description": "US Market Open (9:30 AM ET = 6:30 AM PT)"
},
"evening": {
"enabled": true,
"cron": "0 13 * * 1-5",
"timezone": "America/Los_Angeles",
"description": "US Market Close (4:00 PM ET = 1:00 PM PT)"
}
},
"llm": {
"headline_model_order": ["gemini", "minimax", "claude"],
"summary_model_order": ["gemini", "minimax", "claude"],
"translation_model_order": ["gemini", "minimax", "claude"]
},
"translations": {
"en": {
"title_morning": "Morning Briefing",
"title_evening": "Evening Briefing",
"title_prefix": "Market",
"time_suffix": "",
"heading_briefing": "Market Briefing",
"heading_markets": "Markets",
"heading_sentiment": "Sentiment",
"heading_top_headlines": "Top 5 Headlines",
"heading_portfolio_impact": "Portfolio Impact",
"heading_portfolio_movers": "Portfolio Movers",
"heading_watchpoints": "Watchpoints",
"no_data": "No data available",
"no_movers": "No significant moves (±1%)",
"follows_market": " -- follows market",
"no_catalyst": " -- no specific catalyst",
"rec_bullish": "Selective opportunities, keep risk management tight.",
"rec_bearish": "Reduce risk and prioritize liquidity.",
"rec_neutral": "Wait-and-see, focus on quality names.",
"rec_unknown": "No clear recommendation without reliable data.",
"sources_header": "Sources",
"sentiment_map": {
"Bullish": "Bullish",
"Bearish": "Bearish",
"Neutral": "Neutral",
"No data available": "No data available"
}
},
"de": {
"title_morning": "Morgen-Briefing",
"title_evening": "Abend-Briefing",
"title_prefix": "Börsen",
"time_suffix": "Uhr",
"heading_briefing": "Marktbriefing",
"heading_markets": "Märkte",
"heading_sentiment": "Stimmung",
"heading_top_headlines": "Top 5 Schlagzeilen",
"heading_portfolio_impact": "Portfolio-Auswirkung",
"heading_portfolio_movers": "Portfolio-Bewegungen",
"heading_watchpoints": "Beobachtungspunkte",
"no_data": "Keine Daten verfügbar",
"no_movers": "Keine deutlichen Bewegungen (±1%)",
"follows_market": " -- folgt dem Markt",
"no_catalyst": " -- kein spezifischer Katalysator",
"rec_bullish": "Chancen selektiv nutzen, aber Risikomanagement beibehalten.",
"rec_bearish": "Risiken reduzieren und Liquidität priorisieren.",
"rec_neutral": "Abwarten und Fokus auf Qualitätstitel.",
"rec_unknown": "Keine klare Empfehlung ohne belastbare Daten.",
"sources_header": "Quellen",
"sentiment_map": {
"Bullish": "Bullisch",
"Bearish": "Bärisch",
"Neutral": "Neutral",
"No data available": "Keine Daten verfügbar"
},
"months": {
"January": "Januar",
"February": "Februar",
"March": "März",
"April": "April",
"May": "Mai",
"June": "Juni",
"July": "Juli",
"August": "August",
"September": "September",
"October": "Oktober",
"November": "November",
"December": "Dezember"
},
"days": {
"Monday": "Montag",
"Tuesday": "Dienstag",
"Wednesday": "Mittwoch",
"Thursday": "Donnerstag",
"Friday": "Freitag",
"Saturday": "Samstag",
"Sunday": "Sonntag"
}
}
}
}

327
config/manual_earnings.json Normal file
View File

@@ -0,0 +1,327 @@
{
"_comment": "Manual earnings dates for stocks not covered by Finnhub API",
"_updated": "2026-01-27",
"6857.T": {
"date": "2026-01-27",
"time": "amc",
"note": "Q3 FY2025 - Advantest",
"source": "marketscreener.com"
},
"6920.T": {
"date": "2026-02-02",
"time": "amc",
"note": "Q3 FY2025 - Lasertec",
"source": "tipranks.com"
},
"8035.T": {
"date": "2026-02-05",
"time": "amc",
"note": "Q3 FY2025 - Tokyo Electron",
"source": "tipranks.com"
},
"6146.T": {
"date": "2026-02-06",
"time": "amc",
"note": "Q3 FY2025 - Disco Corp",
"source": "estimate"
},
"7741.T": {
"date": "2026-01-30",
"time": "amc",
"note": "Q3 FY2025 - Hoya",
"source": "estimate"
},
"7735.T": {
"date": "2026-01-30",
"time": "amc",
"note": "Q3 FY2025 - Screen Holdings",
"source": "estimate"
},
"4063.T": {
"date": "2026-01-31",
"time": "amc",
"note": "Q3 FY2025 - Shin-Etsu Chemical",
"source": "estimate"
},
"6861.T": {
"date": "2026-01-29",
"time": "amc",
"note": "Q3 FY2025 - Keyence",
"source": "estimate"
},
"9984.T": {
"date": "2026-02-07",
"time": "amc",
"note": "Q3 FY2025 - SoftBank Group",
"source": "estimate"
},
"9983.T": {
"date": "2026-01-09",
"time": "amc",
"note": "Q1 FY2026 - Fast Retailing (Uniqlo)",
"source": "estimate"
},
"D05.SI": {
"date": "2026-02-10",
"time": "bmo",
"note": "Q4 2025 - DBS Group",
"source": "estimate"
},
"O39.SI": {
"date": "2026-02-21",
"time": "bmo",
"note": "Q4 2025 - OCBC Bank",
"source": "estimate"
},
"S68.SI": {
"date": "2026-01-23",
"time": "bmo",
"note": "H1 FY2026 - Singapore Exchange",
"source": "estimate"
},
"AAPL": {
"date": "2026-01-30",
"time": "amc",
"note": "Q1 FY2026"
},
"MSFT": {
"date": "2026-01-29",
"time": "amc",
"note": "Q2 FY2026"
},
"META": {
"date": "2026-01-29",
"time": "amc",
"note": "Q4 2025"
},
"TSLA": {
"date": "2026-01-29",
"time": "amc",
"note": "Q4 2025"
},
"NVDA": {
"date": "2026-02-25",
"time": "amc",
"note": "Q4 FY2026"
},
"GOOGL": {
"date": "2026-02-04",
"time": "amc",
"note": "Q4 2025"
},
"AMZN": {
"date": "2026-02-06",
"time": "amc",
"note": "Q4 2025"
},
"NFLX": {
"date": "2026-01-21",
"time": "amc",
"note": "Q4 2025"
},
"V": {
"date": "2026-01-30",
"time": "amc",
"note": "Q1 FY2026"
},
"MA": {
"date": "2026-01-30",
"time": "bmo",
"note": "Q4 2025"
},
"ASML": {
"date": "2026-01-29",
"time": "bmo",
"note": "Q4 2025"
},
"NOW": {
"date": "2026-01-29",
"time": "amc",
"note": "Q4 2025"
},
"UBER": {
"date": "2026-02-05",
"time": "bmo",
"note": "Q4 2025"
},
"SHOP": {
"date": "2026-02-11",
"time": "bmo",
"note": "Q4 2025"
},
"SPOT": {
"date": "2026-02-04",
"time": "bmo",
"note": "Q4 2025"
},
"NET": {
"date": "2026-02-06",
"time": "amc",
"note": "Q4 2025"
},
"SNOW": {
"date": "2026-02-26",
"time": "amc",
"note": "Q4 FY2026"
},
"DKNG": {
"date": "2026-02-13",
"time": "bmo",
"note": "Q4 2025"
},
"SQ": {
"date": "2026-02-20",
"time": "amc",
"note": "Q4 2025"
},
"ABNB": {
"date": "2026-02-13",
"time": "amc",
"note": "Q4 2025"
},
"TEAM": {
"date": "2026-01-30",
"time": "amc",
"note": "Q2 FY2026"
},
"ZS": {
"date": "2026-02-25",
"time": "amc",
"note": "Q2 FY2026"
},
"FTNT": {
"date": "2026-02-06",
"time": "amc",
"note": "Q4 2025"
},
"WDAY": {
"date": "2026-02-27",
"time": "amc",
"note": "Q4 FY2026"
},
"TTD": {
"date": "2026-02-13",
"time": "amc",
"note": "Q4 2025"
},
"WMT": {
"date": "2026-02-19",
"time": "bmo",
"note": "Q4 FY2026"
},
"EA": {
"date": "2026-02-03",
"time": "amc",
"note": "Q3 FY2026"
},
"ADSK": {
"date": "2026-02-26",
"time": "amc",
"note": "Q4 FY2026"
},
"ROKU": {
"date": "2026-02-13",
"time": "amc",
"note": "Q4 2025"
},
"SNAP": {
"date": "2026-02-04",
"time": "amc",
"note": "Q4 2025"
},
"ETSY": {
"date": "2026-02-19",
"time": "amc",
"note": "Q4 2025"
},
"KO": {
"date": "2026-02-11",
"time": "bmo",
"note": "Q4 2025"
},
"BLK": {
"date": "2026-01-15",
"time": "bmo",
"note": "Q4 2025"
},
"PH": {
"date": "2026-01-30",
"time": "bmo",
"note": "Q2 FY2026"
},
"SYK": {
"date": "2026-01-28",
"time": "bmo",
"note": "Q4 2025"
},
"TJX": {
"date": "2026-02-26",
"time": "bmo",
"note": "Q4 FY2026"
},
"ROST": {
"date": "2026-03-04",
"time": "amc",
"note": "Q4 FY2026"
},
"ORLY": {
"date": "2026-02-05",
"time": "amc",
"note": "Q4 2025"
},
"SHW": {
"date": "2026-01-30",
"time": "bmo",
"note": "Q4 2025"
},
"FISV": {
"date": "2026-02-04",
"time": "bmo",
"note": "Q4 2025"
},
"MSI": {
"date": "2026-02-06",
"time": "bmo",
"note": "Q4 2025"
},
"APH": {
"date": "2026-01-22",
"time": "bmo",
"note": "Q4 2025"
},
"AXON": {
"date": "2026-02-25",
"time": "amc",
"note": "Q4 2025"
},
"ROP": {
"date": "2026-01-30",
"time": "bmo",
"note": "Q4 2025"
},
"RACE": {
"date": "2026-02-04",
"time": "bmo",
"note": "Q4 2025"
},
"TWLO": {
"date": "2026-02-12",
"time": "amc",
"note": "Q4 2025"
},
"ZM": {
"date": "2026-02-24",
"time": "amc",
"note": "Q4 FY2026"
},
"U": {
"date": "2026-02-20",
"time": "amc",
"note": "Q4 2025"
},
"ZI": {
"date": "2026-02-10",
"time": "amc",
"note": "Q4 2025"
}
}

19
cron/alerts.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Price Alerts Cron Job (Lobster Workflow)
# Schedule: 2:00 PM PT / 5:00 PM ET (1 hour after market close)
#
# Checks price alerts against current prices including after-hours.
# Sends triggered alerts and watchlist status to WhatsApp/Telegram.
set -e
export SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export FINANCE_NEWS_TARGET="${FINANCE_NEWS_TARGET:-120363421796203667@g.us}"
export FINANCE_NEWS_CHANNEL="${FINANCE_NEWS_CHANNEL:-whatsapp}"
echo "[$(date)] Checking price alerts via Lobster..."
lobster run --file "$SKILL_DIR/workflows/alerts-cron.yaml" \
--args-json '{"lang":"en"}'
echo "[$(date)] Price alerts check complete."

19
cron/earnings-weekly.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Weekly Earnings Alert Cron Job (Lobster Workflow)
# Schedule: Sunday 7:00 AM PT (before market week starts)
#
# Sends upcoming week's earnings calendar to WhatsApp/Telegram.
# Shows all portfolio stocks reporting Mon-Fri.
set -e
export SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export FINANCE_NEWS_TARGET="${FINANCE_NEWS_TARGET:-120363421796203667@g.us}"
export FINANCE_NEWS_CHANNEL="${FINANCE_NEWS_CHANNEL:-whatsapp}"
echo "[$(date)] Checking next week's earnings via Lobster..."
lobster run --file "$SKILL_DIR/workflows/earnings-weekly-cron.yaml" \
--args-json '{"lang":"en"}'
echo "[$(date)] Weekly earnings alert complete."

19
cron/earnings.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Earnings Alert Cron Job (Lobster Workflow)
# Schedule: 6:00 AM PT / 9:00 AM ET (30 min before market open)
#
# Sends today's earnings calendar to WhatsApp/Telegram.
# Alerts users about portfolio stocks reporting today.
set -e
export SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export FINANCE_NEWS_TARGET="${FINANCE_NEWS_TARGET:-120363421796203667@g.us}"
export FINANCE_NEWS_CHANNEL="${FINANCE_NEWS_CHANNEL:-whatsapp}"
echo "[$(date)] Checking today's earnings via Lobster..."
lobster run --file "$SKILL_DIR/workflows/earnings-cron.yaml" \
--args-json '{"lang":"en"}'
echo "[$(date)] Earnings alert complete."

19
cron/evening.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Evening Briefing Cron Job (Lobster Workflow)
# Schedule: 1:00 PM PT (US Market Close at 4:00 PM ET)
#
# Uses Lobster workflow to generate and send briefing directly,
# bypassing LLM agent reformatting that truncates output.
set -e
export SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export FINANCE_NEWS_TARGET="${FINANCE_NEWS_TARGET:-120363421796203667@g.us}"
export FINANCE_NEWS_CHANNEL="${FINANCE_NEWS_CHANNEL:-whatsapp}"
echo "[$(date)] Starting evening briefing via Lobster..."
lobster run --file "$SKILL_DIR/workflows/briefing-cron.yaml" \
--args-json '{"time":"evening","lang":"de"}'
echo "[$(date)] Evening briefing complete."

19
cron/morning.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Morning Briefing Cron Job (Lobster Workflow)
# Schedule: 6:30 AM PT (US Market Open at 9:30 AM ET)
#
# Uses Lobster workflow to generate and send briefing directly,
# bypassing LLM agent reformatting that truncates output.
set -e
export SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export FINANCE_NEWS_TARGET="${FINANCE_NEWS_TARGET:-120363421796203667@g.us}"
export FINANCE_NEWS_CHANNEL="${FINANCE_NEWS_CHANNEL:-whatsapp}"
echo "[$(date)] Starting morning briefing via Lobster..."
lobster run --file "$SKILL_DIR/workflows/briefing-cron.yaml" \
--args-json '{"time":"morning","lang":"de"}'
echo "[$(date)] Morning briefing complete."

122
docs/EQUITY_SHEET_FIXES.md Normal file
View File

@@ -0,0 +1,122 @@
# Equity Sheet Fixes
## Contents
- [NRR Column Fix](#nrr-column-column-q---range-values-fix)
- [Conversion Rules](#conversion-rules)
- [Fix Procedure](#fix-procedure)
- [Impact](#impact)
- [Related Columns](#related-columns)
- [Prevention](#prevention)
## NRR Column (Column Q) - Range Values Fix
**Problem:** Values like "115-120%", "125%+", "N/A" in NRR column cause #VALUE! errors in MSS Score formula (columns Y/Z).
**Root cause:** Excel/Sheets formulas cannot perform math operations on text ranges.
**Solution:** Convert all NRR values to single numeric percentages.
### Conversion Rules
**Standard formats:**
| Original | Fixed | Calculation | Rationale |
|----------|-------|-------------|-----------|
| 115-120% | 117.5% | (115+120)/2 | Midpoint (conservative estimate) |
| 120-125% | 122.5% | (120+125)/2 | Midpoint |
| 125%+ | 125% | Use lower bound | Conservative (actual may be higher) |
| N/A | [blank] | Leave empty | MSS formula uses IFERROR to handle blanks |
| 110% | 110% | Already valid | No change needed |
**Edge cases (normalize before converting):**
| Variant | Normalized | Notes |
|---------|------------|-------|
| 115120% (en-dash) | 115-120% | Replace en-dash with hyphen |
| 115 - 120% (spaces) | 115-120% | Remove spaces around hyphen |
| >=125% | 125%+ | Convert to standard "+" format |
| 125%+ or higher | 125%+ | Strip extra text |
### Fix Procedure
**Option A: Manual fix via browser**
1. Open sheet: https://docs.google.com/spreadsheets/d/1lTpdbDjqW40qe4YUvk_1vBzKYLUNrmLZYyQN-7HmFJg/edit#gid=0
2. **IMPORTANT:** Select column Q header → Format → Number → Percent
- This ensures values are stored as numbers, not text
- If column is set to "Plain text", entering "117.5%" stores as text → still causes errors
3. Navigate to column Q (NRR)
4. For each range value:
- Calculate midpoint (e.g., (115+120)/2 = 117.5)
- Replace with single percentage: `117.5%`
- Sheets auto-converts to numeric percentage when column is formatted correctly
5. For "N/A" → delete content (leave blank)
6. For "125%+" → replace with `125%`
7. **Verify:** After editing, click cell → formula bar should show `1.175` (not `"117.5%"` with quotes)
**Option B: Sheets API fix (requires Sheets API enabled)**
**Prerequisites:**
1. Enable Sheets API: https://console.developers.google.com/apis/api/sheets.googleapis.com/overview?project=831892255935
2. Ensure column Q is formatted as Percent (do once before any API writes):
- Via browser: Select column Q → Format → Number → Percent
- Via API: Use `batchUpdate` with `repeatCell` + `numberFormat` (see below)
**Using gog CLI:**
```bash
# gog CLI uses USER_ENTERED by default (parses "117.5%" as numeric)
gog-shapescale --account martin@shapescale.com sheets update \
1lTpdbDjqW40qe4YUvk_1vBzKYLUNrmLZYyQN-7HmFJg \
'Equity!Q5' '117.5%'
```
**Using Sheets API directly (curl/Python):**
```bash
# CRITICAL: Specify valueInputOption=USER_ENTERED explicitly
curl -X PUT \
"https://sheets.googleapis.com/v4/spreadsheets/SHEET_ID/values/Equity!Q5?valueInputOption=USER_ENTERED" \
-H "Authorization: Bearer $TOKEN" \
-d '{"values": [["117.5%"]]}'
# Python example:
service.spreadsheets().values().update(
spreadsheetId=SHEET_ID,
range='Equity!Q5',
valueInputOption='USER_ENTERED', # Parse as Sheets would
body={'values': [['117.5%']]}
).execute()
```
**Verify after writing:**
- Click cell → formula bar should show `1.175` (numeric)
- If formula bar shows `"117.5%"` with quotes → stored as text, still causes errors
### Impact
Fixing NRR ranges will:
- ✅ Eliminate #VALUE! errors in MSS Score column (Y)
- ✅ Eliminate #VALUE! errors in MSS Rating column (Z)
- ✅ Allow proper numerical analysis and sorting
- ✅ Make formulas copyable to new rows without errors
### How MSS Formula Handles Blank NRR Values
The MSS Score formula (column Y) includes `IFERROR()` wrapper to handle missing data:
- **Blank NRR cell** → Formula treats as missing data, uses available metrics only
- **Not treated as 0%** → Blank is excluded from calculation (doesn't penalize score)
- **Better than text "N/A"** → Text causes #VALUE! error, blank is handled gracefully
**Example:** If NRR is blank but other metrics exist (Rev Growth, Rule of 40, etc.), MSS Score calculates using remaining metrics without error.
### Related Columns
Other columns that need single numeric values (not ranges):
- **Column M (Rule of 40 Ops)**: Should be calculated value (Ops Margin + Rev Growth)
- **Column O (Rule of 40 FCF)**: Should be calculated value (FCF Margin + Rev Growth)
- Both can be negative for pre-profitable/turnaround companies
### Prevention
When adding new companies:
1. Always use single percentage values in NRR column
2. Test MSS Score formula immediately after adding row
3. If #VALUE! error appears → check Q column for ranges/text

212
docs/PREMIUM_SOURCES.md Normal file
View File

@@ -0,0 +1,212 @@
# Premium Source Authentication
## Contents
- [Overview](#overview)
- [Option 1: Keep It Simple (Recommended)](#option-1-keep-it-simple-recommended)
- [Option 2: Use Premium Sources (Advanced)](#option-2-use-premium-sources-advanced)
- [Troubleshooting](#troubleshooting)
- [Alternative: Use APIs Instead](#alternative-use-apis-instead)
- [Recommendation](#recommendation)
## Overview
WSJ and Barron's are premium financial news sources that require subscriptions. This guide explains how to authenticate and use premium sources with the finance-news skill.
**Recommendation:** For simplicity, we recommend using **free sources only** (Yahoo Finance, CNBC, MarketWatch). Premium sources add complexity and maintenance burden.
If you have subscriptions and want premium content, follow the steps below.
---
## Option 1: Keep It Simple (Recommended)
**Use free sources only.** They provide 90% of the value without authentication complexity:
- ✅ Yahoo Finance (free, reliable)
- ✅ CNBC (free, real-time news)
- ✅ MarketWatch (free, broad coverage)
- ✅ Reuters (free via Yahoo RSS)
**To disable premium sources:**
1. Edit `config/config.json` (legacy: `config/sources.json`)
2. Set `"enabled": false` for WSJ/Barron's entries
3. Done - no authentication needed
---
## Option 2: Use Premium Sources (Advanced)
### Prerequisites
- Active WSJ or Barron's subscription
- Browser with active login session (Chrome/Firefox)
- **Option B only:** Install `requests` library if needed:
```bash
pip install requests
```
### Step 1: Export Cookies from Browser
**Chrome:**
1. Install extension: [EditThisCookie](https://chrome.google.com/webstore/detail/editthiscookie/)
2. Navigate to wsj.com (logged in)
3. Click EditThisCookie icon → Export → Copy JSON
**Firefox:**
1. Install extension: [Cookie Quick Manager](https://addons.mozilla.org/en-US/firefox/addon/cookie-quick-manager/)
2. Navigate to wsj.com (logged in)
3. Right-click page → Inspect → Storage → Cookies
4. Copy relevant cookies (see format below)
### Step 2: Create Cookie File
Create `config/cookies.json` (this file is gitignored):
```json
{
"feeds.a.dj.com": {
"wsjgeo": "US",
"djcs_session": "YOUR_SESSION_TOKEN_HERE",
"djcs_route": "YOUR_ROUTE_HERE"
},
"www.barrons.com": {
"wsjgeo": "US",
"djcs_session": "YOUR_SESSION_TOKEN_HERE"
}
}
```
**Important:** Cookie domain must match feed URL domain:
- WSJ feeds use `feeds.a.dj.com` (not `wsj.com`)
- Barron's feeds use `www.barrons.com`
- Check `config/config.json` for actual feed URLs
**Note:** Cookie names/values vary by site. Export from browser to get actual values.
### Step 3: Pass Cookies to fetch_news.py
**Option A: Modify fetch_news.py (not officially supported)**
Add cookie loading to `fetch_rss()` function (maintains existing signature):
```python
import json
import urllib.request
from pathlib import Path
from urllib.parse import urlparse
def fetch_rss(url: str, limit: int = 10) -> list[dict]:
"""Fetch and parse RSS feed with optional cookie authentication."""
# Load cookies if they exist
cookie_file = Path(__file__).parent.parent / "config" / "cookies.json"
cookies = {}
if cookie_file.exists():
with open(cookie_file) as f:
all_cookies = json.load(f)
# Extract domain from URL (e.g., feeds.a.dj.com)
domain = urlparse(url).netloc
cookies = all_cookies.get(domain, {})
# Fetch with cookies and User-Agent
req = urllib.request.Request(url, headers={'User-Agent': 'OpenClaw/1.0'})
if cookies:
cookie_header = "; ".join([f"{k}={v}" for k, v in cookies.items()])
req.add_header("Cookie", cookie_header)
# ... rest of function (unchanged)
```
**Note:** This is a doc-only suggestion, not officially supported by the skill.
**Option B: Use requests library instead of urllib**
Replace `urllib` with `requests` for easier cookie handling (maintains API signature):
```python
import requests
def fetch_rss(url: str, limit: int = 10, cookies_dict: dict = None) -> list[dict]:
response = requests.get(url, cookies=cookies_dict, timeout=10)
response.raise_for_status()
# ... parse with feedparser
```
### Step 4: Security Considerations
**Critical: Do NOT commit cookies to git**
1. **`.gitignore` already includes cookie files:**
- `config/cookies.json`
- `*.cookie`
- No action needed (already configured)
2. **Set restrictive file permissions:**
```bash
chmod 600 config/cookies.json
```
2. **Set restrictive file permissions:**
```bash
chmod 600 config/cookies.json
```
3. **Rotate cookies regularly:**
- Browser session cookies expire (usually 7-30 days)
- Re-export cookies when authentication fails
4. **Never share cookie files:**
- Cookies grant full account access
- Treat like passwords
---
## Troubleshooting
### "HTTP 403 Forbidden" errors
**Cause:** Cookies expired or invalid
**Fix:**
1. Log in to WSJ/Barron's in browser
2. Re-export cookies
3. Update `config/cookies.json`
### "Paywall detected" in articles
**Cause:** RSS feed doesn't require auth, but full article does
**Fix:**
- Premium sources often provide headlines/snippets in RSS (no auth needed)
- Full articles require subscription + cookie auth
- If you only need headlines → no cookies needed
### Cookies not working
**Debug checklist:**
- [ ] Correct domain in cookies.json:
- WSJ: Use `feeds.a.dj.com` (not `wsj.com`)
- Barron's: Use `www.barrons.com` (not `barrons.com`)
- Check `config/config.json` for actual feed URLs
- [ ] Cookie values copied completely (no truncation)
- [ ] Browser session still active (test by visiting site)
- [ ] File permissions correct (chmod 600)
---
## Alternative: Use APIs Instead
Some premium sources offer APIs:
- **WSJ API:** Not publicly available
- **Barron's API:** Part of Dow Jones API (enterprise only)
- **Bloomberg API:** Enterprise only
**Conclusion:** Cookie-based auth is the only practical option for individual users.
---
## Recommendation
**For most users:** Stick with free sources. They're reliable, no auth needed, and provide comprehensive market coverage.
**For premium subscribers:** Follow Option 2, but be prepared to maintain cookie files and handle expiration.

281
htmlcov/class_index.html generated Normal file
View File

@@ -0,0 +1,281 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage report</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="indexfile">
<header>
<div class="content">
<h1>Coverage report:
<span class="pc_cov">48%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>f</kbd>
<kbd>n</kbd>
<kbd>s</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
<kbd>c</kbd>
&nbsp; change column sorting
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<form id="filter_container">
<input id="filter" type="text" value="" placeholder="filter...">
<div>
<input id="hide100" type="checkbox" >
<label for="hide100">hide covered</label>
</div>
</form>
<h2>
<a class="button" href="index.html">Files</a>
<a class="button" href="function_index.html">Functions</a>
<a class="button current">Classes</a>
</h2>
<p class="text">
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</header>
<main id="index">
<table class="index" data-sortable>
<thead>
<tr class="tablehead" title="Click to sort">
<th id="file" class="name" aria-sort="none" data-shortcut="f">File<span class="arrows"></span></th>
<th id="region" class="name" aria-sort="none" data-default-sort-order="ascending" data-shortcut="n">class<span class="arrows"></span></th>
<th class="spacer">&nbsp;</th>
<th id="statements" aria-sort="none" data-default-sort-order="descending" data-shortcut="s">statements<span class="arrows"></span></th>
<th id="missing" aria-sort="none" data-default-sort-order="descending" data-shortcut="m">missing<span class="arrows"></span></th>
<th id="excluded" aria-sort="none" data-default-sort-order="descending" data-shortcut="x">excluded<span class="arrows"></span></th>
<th class="spacer">&nbsp;</th>
<th id="coverage" aria-sort="none" data-shortcut="c">coverage<span class="arrows"></span></th>
</tr>
</thead>
<tbody>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_alerts_py.html">scripts&#8201;/&#8201;alerts.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_alerts_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>292</td>
<td>118</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="174 292">60%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_briefing_py.html">scripts&#8201;/&#8201;briefing.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_briefing_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>87</td>
<td>38</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="49 87">56%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_earnings_py.html#t507">scripts&#8201;/&#8201;earnings.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_earnings_py.html#t507"><data value='Args'>get_briefing_section.Args</data></a></td>
<td class="spacer">&nbsp;</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 0">100%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_earnings_py.html">scripts&#8201;/&#8201;earnings.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_earnings_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>329</td>
<td>181</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="148 329">45%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_fetch_news_py.html#t109">scripts&#8201;/&#8201;fetch_news.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_fetch_news_py.html#t109"><data value='PortfolioError'>PortfolioError</data></a></td>
<td class="spacer">&nbsp;</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 0">100%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_fetch_news_py.html">scripts&#8201;/&#8201;fetch_news.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_fetch_news_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>589</td>
<td>377</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="212 589">36%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_portfolio_py.html">scripts&#8201;/&#8201;portfolio.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_portfolio_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>183</td>
<td>124</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="59 183">32%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_ranking_py.html">scripts&#8201;/&#8201;ranking.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_ranking_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>147</td>
<td>21</td>
<td>9</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="126 147">86%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_research_py.html">scripts&#8201;/&#8201;research.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_research_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>130</td>
<td>45</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="85 130">65%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_setup_py.html">scripts&#8201;/&#8201;setup.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_setup_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>168</td>
<td>124</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="44 168">26%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_stocks_py.html">scripts&#8201;/&#8201;stocks.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_stocks_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>184</td>
<td>87</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="97 184">53%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html#t64">scripts&#8201;/&#8201;summarize.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html#t64"><data value='MoverContext'>MoverContext</data></a></td>
<td class="spacer">&nbsp;</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 0">100%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html#t76">scripts&#8201;/&#8201;summarize.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html#t76"><data value='SectorCluster'>SectorCluster</data></a></td>
<td class="spacer">&nbsp;</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 0">100%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html#t86">scripts&#8201;/&#8201;summarize.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html#t86"><data value='WatchpointsData'>WatchpointsData</data></a></td>
<td class="spacer">&nbsp;</td>
<td>0</td>
<td>0</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 0">100%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html">scripts&#8201;/&#8201;summarize.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>972</td>
<td>462</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="510 972">52%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_translate_portfolio_py.html">scripts&#8201;/&#8201;translate_portfolio.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_translate_portfolio_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>88</td>
<td>88</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 88">0%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_utils_py.html">scripts&#8201;/&#8201;utils.py</a></td>
<td class="name"><a href="z_de1a740d5dc98ffd_utils_py.html"><data value=''><span class='no-noun'>(no class)</span></data></a></td>
<td class="spacer">&nbsp;</td>
<td>34</td>
<td>10</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="24 34">71%</td>
</tr>
</tbody>
<tfoot>
<tr class="total">
<td class="name">Total</td>
<td class="name">&nbsp;</td>
<td class="spacer">&nbsp;</td>
<td>3203</td>
<td>1675</td>
<td>29</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="1528 3203">48%</td>
</tr>
</tfoot>
</table>
<p id="no_rows">
No items found using the specified filter.
</p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
<aside class="hidden">
<a id="prevFileLink" class="nav" href=""></a>
<a id="nextFileLink" class="nav" href=""></a>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</footer>
</body>
</html>

735
htmlcov/coverage_html_cb_dd2e7eb5.js generated Normal file
View File

@@ -0,0 +1,735 @@
// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
// For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
// Coverage.py HTML report browser code.
/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */
/*global coverage: true, document, window, $ */
coverage = {};
// General helpers
function debounce(callback, wait) {
let timeoutId = null;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
callback.apply(this, args);
}, wait);
};
};
function checkVisible(element) {
const rect = element.getBoundingClientRect();
const viewBottom = Math.max(document.documentElement.clientHeight, window.innerHeight);
const viewTop = 30;
return !(rect.bottom < viewTop || rect.top >= viewBottom);
}
function on_click(sel, fn) {
const elt = document.querySelector(sel);
if (elt) {
elt.addEventListener("click", fn);
}
}
// Helpers for table sorting
function getCellValue(row, column = 0) {
const cell = row.cells[column] // nosemgrep: eslint.detect-object-injection
if (cell.childElementCount == 1) {
var child = cell.firstElementChild;
if (child.tagName === "A") {
child = child.firstElementChild;
}
if (child instanceof HTMLDataElement && child.value) {
return child.value;
}
}
return cell.innerText || cell.textContent;
}
function rowComparator(rowA, rowB, column = 0) {
let valueA = getCellValue(rowA, column);
let valueB = getCellValue(rowB, column);
if (!isNaN(valueA) && !isNaN(valueB)) {
return valueA - valueB;
}
return valueA.localeCompare(valueB, undefined, {numeric: true});
}
function sortColumn(th) {
// Get the current sorting direction of the selected header,
// clear state on other headers and then set the new sorting direction.
const currentSortOrder = th.getAttribute("aria-sort");
[...th.parentElement.cells].forEach(header => header.setAttribute("aria-sort", "none"));
var direction;
if (currentSortOrder === "none") {
direction = th.dataset.defaultSortOrder || "ascending";
}
else if (currentSortOrder === "ascending") {
direction = "descending";
}
else {
direction = "ascending";
}
th.setAttribute("aria-sort", direction);
const column = [...th.parentElement.cells].indexOf(th)
// Sort all rows and afterwards append them in order to move them in the DOM.
Array.from(th.closest("table").querySelectorAll("tbody tr"))
.sort((rowA, rowB) => rowComparator(rowA, rowB, column) * (direction === "ascending" ? 1 : -1))
.forEach(tr => tr.parentElement.appendChild(tr));
// Save the sort order for next time.
if (th.id !== "region") {
let th_id = "file"; // Sort by file if we don't have a column id
let current_direction = direction;
const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE);
if (stored_list) {
({th_id, direction} = JSON.parse(stored_list))
}
localStorage.setItem(coverage.INDEX_SORT_STORAGE, JSON.stringify({
"th_id": th.id,
"direction": current_direction
}));
if (th.id !== th_id || document.getElementById("region")) {
// Sort column has changed, unset sorting by function or class.
localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({
"by_region": false,
"region_direction": current_direction
}));
}
}
else {
// Sort column has changed to by function or class, remember that.
localStorage.setItem(coverage.SORTED_BY_REGION, JSON.stringify({
"by_region": true,
"region_direction": direction
}));
}
}
// Find all the elements with data-shortcut attribute, and use them to assign a shortcut key.
coverage.assign_shortkeys = function () {
document.querySelectorAll("[data-shortcut]").forEach(element => {
document.addEventListener("keypress", event => {
if (event.target.tagName.toLowerCase() === "input") {
return; // ignore keypress from search filter
}
if (event.key === element.dataset.shortcut) {
element.click();
}
});
});
};
// Create the events for the filter box.
coverage.wire_up_filter = function () {
// Populate the filter and hide100 inputs if there are saved values for them.
const saved_filter_value = localStorage.getItem(coverage.FILTER_STORAGE);
if (saved_filter_value) {
document.getElementById("filter").value = saved_filter_value;
}
const saved_hide100_value = localStorage.getItem(coverage.HIDE100_STORAGE);
if (saved_hide100_value) {
document.getElementById("hide100").checked = JSON.parse(saved_hide100_value);
}
// Cache elements.
const table = document.querySelector("table.index");
const table_body_rows = table.querySelectorAll("tbody tr");
const no_rows = document.getElementById("no_rows");
const footer = table.tFoot.rows[0];
const ratio_columns = Array.from(footer.cells).map(cell => Boolean(cell.dataset.ratio));
// Observe filter keyevents.
const filter_handler = (event => {
// Keep running total of each metric, first index contains number of shown rows
const totals = ratio_columns.map(
is_ratio => is_ratio ? {"numer": 0, "denom": 0} : 0
);
var text = document.getElementById("filter").value;
// Store filter value
localStorage.setItem(coverage.FILTER_STORAGE, text);
const casefold = (text === text.toLowerCase());
const hide100 = document.getElementById("hide100").checked;
// Store hide value.
localStorage.setItem(coverage.HIDE100_STORAGE, JSON.stringify(hide100));
// Hide / show elements.
table_body_rows.forEach(row => {
var show = false;
// Check the text filter.
for (let column = 0; column < totals.length; column++) {
cell = row.cells[column];
if (cell.classList.contains("name")) {
var celltext = cell.textContent;
if (casefold) {
celltext = celltext.toLowerCase();
}
if (celltext.includes(text)) {
show = true;
}
}
}
// Check the "hide covered" filter.
if (show && hide100) {
const [numer, denom] = row.cells[row.cells.length - 1].dataset.ratio.split(" ");
show = (numer !== denom);
}
if (!show) {
// hide
row.classList.add("hidden");
return;
}
// show
row.classList.remove("hidden");
totals[0]++;
for (let column = 0; column < totals.length; column++) {
// Accumulate dynamic totals
cell = row.cells[column] // nosemgrep: eslint.detect-object-injection
if (cell.matches(".name, .spacer")) {
continue;
}
if (ratio_columns[column] && cell.dataset.ratio) {
// Column stores a ratio
const [numer, denom] = cell.dataset.ratio.split(" ");
totals[column]["numer"] += parseInt(numer, 10); // nosemgrep: eslint.detect-object-injection
totals[column]["denom"] += parseInt(denom, 10); // nosemgrep: eslint.detect-object-injection
}
else {
totals[column] += parseInt(cell.textContent, 10); // nosemgrep: eslint.detect-object-injection
}
}
});
// Show placeholder if no rows will be displayed.
if (!totals[0]) {
// Show placeholder, hide table.
no_rows.style.display = "block";
table.style.display = "none";
return;
}
// Hide placeholder, show table.
no_rows.style.display = null;
table.style.display = null;
// Calculate new dynamic sum values based on visible rows.
for (let column = 0; column < totals.length; column++) {
// Get footer cell element.
const cell = footer.cells[column]; // nosemgrep: eslint.detect-object-injection
if (cell.matches(".name, .spacer")) {
continue;
}
// Set value into dynamic footer cell element.
if (ratio_columns[column]) {
// Percentage column uses the numerator and denominator,
// and adapts to the number of decimal places.
const match = /\.([0-9]+)/.exec(cell.textContent);
const places = match ? match[1].length : 0;
const { numer, denom } = totals[column]; // nosemgrep: eslint.detect-object-injection
cell.dataset.ratio = `${numer} ${denom}`;
// Check denom to prevent NaN if filtered files contain no statements
cell.textContent = denom
? `${(numer * 100 / denom).toFixed(places)}%`
: `${(100).toFixed(places)}%`;
}
else {
cell.textContent = totals[column]; // nosemgrep: eslint.detect-object-injection
}
}
});
document.getElementById("filter").addEventListener("input", debounce(filter_handler));
document.getElementById("hide100").addEventListener("input", debounce(filter_handler));
// Trigger change event on setup, to force filter on page refresh
// (filter value may still be present).
document.getElementById("filter").dispatchEvent(new Event("input"));
document.getElementById("hide100").dispatchEvent(new Event("input"));
};
coverage.FILTER_STORAGE = "COVERAGE_FILTER_VALUE";
coverage.HIDE100_STORAGE = "COVERAGE_HIDE100_VALUE";
// Set up the click-to-sort columns.
coverage.wire_up_sorting = function () {
document.querySelectorAll("[data-sortable] th[aria-sort]").forEach(
th => th.addEventListener("click", e => sortColumn(e.target))
);
// Look for a localStorage item containing previous sort settings:
let th_id = "file", direction = "ascending";
const stored_list = localStorage.getItem(coverage.INDEX_SORT_STORAGE);
if (stored_list) {
({th_id, direction} = JSON.parse(stored_list));
}
let by_region = false, region_direction = "ascending";
const sorted_by_region = localStorage.getItem(coverage.SORTED_BY_REGION);
if (sorted_by_region) {
({
by_region,
region_direction
} = JSON.parse(sorted_by_region));
}
const region_id = "region";
if (by_region && document.getElementById(region_id)) {
direction = region_direction;
}
// If we are in a page that has a column with id of "region", sort on
// it if the last sort was by function or class.
let th;
if (document.getElementById(region_id)) {
th = document.getElementById(by_region ? region_id : th_id);
}
else {
th = document.getElementById(th_id);
}
th.setAttribute("aria-sort", direction === "ascending" ? "descending" : "ascending");
th.click()
};
coverage.INDEX_SORT_STORAGE = "COVERAGE_INDEX_SORT_2";
coverage.SORTED_BY_REGION = "COVERAGE_SORT_REGION";
// Loaded on index.html
coverage.index_ready = function () {
coverage.assign_shortkeys();
coverage.wire_up_filter();
coverage.wire_up_sorting();
on_click(".button_prev_file", coverage.to_prev_file);
on_click(".button_next_file", coverage.to_next_file);
on_click(".button_show_hide_help", coverage.show_hide_help);
};
// -- pyfile stuff --
coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS";
coverage.pyfile_ready = function () {
// If we're directed to a particular line number, highlight the line.
var frag = location.hash;
if (frag.length > 2 && frag[1] === "t") {
document.querySelector(frag).closest(".n").classList.add("highlight");
coverage.set_sel(parseInt(frag.substr(2), 10));
}
else {
coverage.set_sel(0);
}
on_click(".button_toggle_run", coverage.toggle_lines);
on_click(".button_toggle_mis", coverage.toggle_lines);
on_click(".button_toggle_exc", coverage.toggle_lines);
on_click(".button_toggle_par", coverage.toggle_lines);
on_click(".button_next_chunk", coverage.to_next_chunk_nicely);
on_click(".button_prev_chunk", coverage.to_prev_chunk_nicely);
on_click(".button_top_of_page", coverage.to_top);
on_click(".button_first_chunk", coverage.to_first_chunk);
on_click(".button_prev_file", coverage.to_prev_file);
on_click(".button_next_file", coverage.to_next_file);
on_click(".button_to_index", coverage.to_index);
on_click(".button_show_hide_help", coverage.show_hide_help);
coverage.filters = undefined;
try {
coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE);
} catch(err) {}
if (coverage.filters) {
coverage.filters = JSON.parse(coverage.filters);
}
else {
coverage.filters = {run: false, exc: true, mis: true, par: true};
}
for (cls in coverage.filters) {
coverage.set_line_visibilty(cls, coverage.filters[cls]); // nosemgrep: eslint.detect-object-injection
}
coverage.assign_shortkeys();
coverage.init_scroll_markers();
coverage.wire_up_sticky_header();
document.querySelectorAll("[id^=ctxs]").forEach(
cbox => cbox.addEventListener("click", coverage.expand_contexts)
);
// Rebuild scroll markers when the window height changes.
window.addEventListener("resize", coverage.build_scroll_markers);
};
coverage.toggle_lines = function (event) {
const btn = event.target.closest("button");
const category = btn.value
const show = !btn.classList.contains("show_" + category);
coverage.set_line_visibilty(category, show);
coverage.build_scroll_markers();
coverage.filters[category] = show;
try {
localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters));
} catch(err) {}
};
coverage.set_line_visibilty = function (category, should_show) {
const cls = "show_" + category;
const btn = document.querySelector(".button_toggle_" + category);
if (btn) {
if (should_show) {
document.querySelectorAll("#source ." + category).forEach(e => e.classList.add(cls));
btn.classList.add(cls);
}
else {
document.querySelectorAll("#source ." + category).forEach(e => e.classList.remove(cls));
btn.classList.remove(cls);
}
}
};
// Return the nth line div.
coverage.line_elt = function (n) {
return document.getElementById("t" + n)?.closest("p");
};
// Set the selection. b and e are line numbers.
coverage.set_sel = function (b, e) {
// The first line selected.
coverage.sel_begin = b;
// The next line not selected.
coverage.sel_end = (e === undefined) ? b+1 : e;
};
coverage.to_top = function () {
coverage.set_sel(0, 1);
coverage.scroll_window(0);
};
coverage.to_first_chunk = function () {
coverage.set_sel(0, 1);
coverage.to_next_chunk();
};
coverage.to_prev_file = function () {
window.location = document.getElementById("prevFileLink").href;
}
coverage.to_next_file = function () {
window.location = document.getElementById("nextFileLink").href;
}
coverage.to_index = function () {
location.href = document.getElementById("indexLink").href;
}
coverage.show_hide_help = function () {
const helpCheck = document.getElementById("help_panel_state")
helpCheck.checked = !helpCheck.checked;
}
// Return a string indicating what kind of chunk this line belongs to,
// or null if not a chunk.
coverage.chunk_indicator = function (line_elt) {
const classes = line_elt?.className;
if (!classes) {
return null;
}
const match = classes.match(/\bshow_\w+\b/);
if (!match) {
return null;
}
return match[0];
};
coverage.to_next_chunk = function () {
const c = coverage;
// Find the start of the next colored chunk.
var probe = c.sel_end;
var chunk_indicator, probe_line;
while (true) {
probe_line = c.line_elt(probe);
if (!probe_line) {
return;
}
chunk_indicator = c.chunk_indicator(probe_line);
if (chunk_indicator) {
break;
}
probe++;
}
// There's a next chunk, `probe` points to it.
var begin = probe;
// Find the end of this chunk.
var next_indicator = chunk_indicator;
while (next_indicator === chunk_indicator) {
probe++;
probe_line = c.line_elt(probe);
next_indicator = c.chunk_indicator(probe_line);
}
c.set_sel(begin, probe);
c.show_selection();
};
coverage.to_prev_chunk = function () {
const c = coverage;
// Find the end of the prev colored chunk.
var probe = c.sel_begin-1;
var probe_line = c.line_elt(probe);
if (!probe_line) {
return;
}
var chunk_indicator = c.chunk_indicator(probe_line);
while (probe > 1 && !chunk_indicator) {
probe--;
probe_line = c.line_elt(probe);
if (!probe_line) {
return;
}
chunk_indicator = c.chunk_indicator(probe_line);
}
// There's a prev chunk, `probe` points to its last line.
var end = probe+1;
// Find the beginning of this chunk.
var prev_indicator = chunk_indicator;
while (prev_indicator === chunk_indicator) {
probe--;
if (probe <= 0) {
return;
}
probe_line = c.line_elt(probe);
prev_indicator = c.chunk_indicator(probe_line);
}
c.set_sel(probe+1, end);
c.show_selection();
};
// Returns 0, 1, or 2: how many of the two ends of the selection are on
// the screen right now?
coverage.selection_ends_on_screen = function () {
if (coverage.sel_begin === 0) {
return 0;
}
const begin = coverage.line_elt(coverage.sel_begin);
const end = coverage.line_elt(coverage.sel_end-1);
return (
(checkVisible(begin) ? 1 : 0)
+ (checkVisible(end) ? 1 : 0)
);
};
coverage.to_next_chunk_nicely = function () {
if (coverage.selection_ends_on_screen() === 0) {
// The selection is entirely off the screen:
// Set the top line on the screen as selection.
// This will select the top-left of the viewport
// As this is most likely the span with the line number we take the parent
const line = document.elementFromPoint(0, 0).parentElement;
if (line.parentElement !== document.getElementById("source")) {
// The element is not a source line but the header or similar
coverage.select_line_or_chunk(1);
}
else {
// We extract the line number from the id
coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10));
}
}
coverage.to_next_chunk();
};
coverage.to_prev_chunk_nicely = function () {
if (coverage.selection_ends_on_screen() === 0) {
// The selection is entirely off the screen:
// Set the lowest line on the screen as selection.
// This will select the bottom-left of the viewport
// As this is most likely the span with the line number we take the parent
const line = document.elementFromPoint(document.documentElement.clientHeight-1, 0).parentElement;
if (line.parentElement !== document.getElementById("source")) {
// The element is not a source line but the header or similar
coverage.select_line_or_chunk(coverage.lines_len);
}
else {
// We extract the line number from the id
coverage.select_line_or_chunk(parseInt(line.id.substring(1), 10));
}
}
coverage.to_prev_chunk();
};
// Select line number lineno, or if it is in a colored chunk, select the
// entire chunk
coverage.select_line_or_chunk = function (lineno) {
var c = coverage;
var probe_line = c.line_elt(lineno);
if (!probe_line) {
return;
}
var the_indicator = c.chunk_indicator(probe_line);
if (the_indicator) {
// The line is in a highlighted chunk.
// Search backward for the first line.
var probe = lineno;
var indicator = the_indicator;
while (probe > 0 && indicator === the_indicator) {
probe--;
probe_line = c.line_elt(probe);
if (!probe_line) {
break;
}
indicator = c.chunk_indicator(probe_line);
}
var begin = probe + 1;
// Search forward for the last line.
probe = lineno;
indicator = the_indicator;
while (indicator === the_indicator) {
probe++;
probe_line = c.line_elt(probe);
indicator = c.chunk_indicator(probe_line);
}
coverage.set_sel(begin, probe);
}
else {
coverage.set_sel(lineno);
}
};
coverage.show_selection = function () {
// Highlight the lines in the chunk
document.querySelectorAll("#source .highlight").forEach(e => e.classList.remove("highlight"));
for (let probe = coverage.sel_begin; probe < coverage.sel_end; probe++) {
coverage.line_elt(probe).querySelector(".n").classList.add("highlight");
}
coverage.scroll_to_selection();
};
coverage.scroll_to_selection = function () {
// Scroll the page if the chunk isn't fully visible.
if (coverage.selection_ends_on_screen() < 2) {
const element = coverage.line_elt(coverage.sel_begin);
coverage.scroll_window(element.offsetTop - 60);
}
};
coverage.scroll_window = function (to_pos) {
window.scroll({top: to_pos, behavior: "smooth"});
};
coverage.init_scroll_markers = function () {
// Init some variables
coverage.lines_len = document.querySelectorAll("#source > p").length;
// Build html
coverage.build_scroll_markers();
};
coverage.build_scroll_markers = function () {
const temp_scroll_marker = document.getElementById("scroll_marker")
if (temp_scroll_marker) temp_scroll_marker.remove();
// Don't build markers if the window has no scroll bar.
if (document.body.scrollHeight <= window.innerHeight) {
return;
}
const marker_scale = window.innerHeight / document.body.scrollHeight;
const line_height = Math.min(Math.max(3, window.innerHeight / coverage.lines_len), 10);
let previous_line = -99, last_mark, last_top;
const scroll_marker = document.createElement("div");
scroll_marker.id = "scroll_marker";
document.getElementById("source").querySelectorAll(
"p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par"
).forEach(element => {
const line_top = Math.floor(element.offsetTop * marker_scale);
const line_number = parseInt(element.querySelector(".n a").id.substr(1));
if (line_number === previous_line + 1) {
// If this solid missed block just make previous mark higher.
last_mark.style.height = `${line_top + line_height - last_top}px`;
}
else {
// Add colored line in scroll_marker block.
last_mark = document.createElement("div");
last_mark.id = `m${line_number}`;
last_mark.classList.add("marker");
last_mark.style.height = `${line_height}px`;
last_mark.style.top = `${line_top}px`;
scroll_marker.append(last_mark);
last_top = line_top;
}
previous_line = line_number;
});
// Append last to prevent layout calculation
document.body.append(scroll_marker);
};
coverage.wire_up_sticky_header = function () {
const header = document.querySelector("header");
const header_bottom = (
header.querySelector(".content h2").getBoundingClientRect().top -
header.getBoundingClientRect().top
);
function updateHeader() {
if (window.scrollY > header_bottom) {
header.classList.add("sticky");
}
else {
header.classList.remove("sticky");
}
}
window.addEventListener("scroll", updateHeader);
updateHeader();
};
coverage.expand_contexts = function (e) {
var ctxs = e.target.parentNode.querySelector(".ctxs");
if (!ctxs.classList.contains("expanded")) {
var ctxs_text = ctxs.textContent;
var width = Number(ctxs_text[0]);
ctxs.textContent = "";
for (var i = 1; i < ctxs_text.length; i += width) {
key = ctxs_text.substring(i, i + width).trim();
ctxs.appendChild(document.createTextNode(contexts[key]));
ctxs.appendChild(document.createElement("br"));
}
ctxs.classList.add("expanded");
}
};
document.addEventListener("DOMContentLoaded", () => {
if (document.body.classList.contains("indexfile")) {
coverage.index_ready();
}
else {
coverage.pyfile_ready();
}
});

1851
htmlcov/function_index.html generated Normal file

File diff suppressed because it is too large Load Diff

216
htmlcov/index.html generated Normal file
View File

@@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage report</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="indexfile">
<header>
<div class="content">
<h1>Coverage report:
<span class="pc_cov">48%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>f</kbd>
<kbd>s</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
<kbd>c</kbd>
&nbsp; change column sorting
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<form id="filter_container">
<input id="filter" type="text" value="" placeholder="filter...">
<div>
<input id="hide100" type="checkbox" >
<label for="hide100">hide covered</label>
</div>
</form>
<h2>
<a class="button current">Files</a>
<a class="button" href="function_index.html">Functions</a>
<a class="button" href="class_index.html">Classes</a>
</h2>
<p class="text">
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</header>
<main id="index">
<table class="index" data-sortable>
<thead>
<tr class="tablehead" title="Click to sort">
<th id="file" class="name" aria-sort="none" data-shortcut="f">File<span class="arrows"></span></th>
<th class="spacer">&nbsp;</th>
<th id="statements" aria-sort="none" data-default-sort-order="descending" data-shortcut="s">statements<span class="arrows"></span></th>
<th id="missing" aria-sort="none" data-default-sort-order="descending" data-shortcut="m">missing<span class="arrows"></span></th>
<th id="excluded" aria-sort="none" data-default-sort-order="descending" data-shortcut="x">excluded<span class="arrows"></span></th>
<th class="spacer">&nbsp;</th>
<th id="coverage" aria-sort="none" data-shortcut="c">coverage<span class="arrows"></span></th>
</tr>
</thead>
<tbody>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_alerts_py.html">scripts&#8201;/&#8201;alerts.py</a></td>
<td class="spacer">&nbsp;</td>
<td>292</td>
<td>118</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="174 292">60%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_briefing_py.html">scripts&#8201;/&#8201;briefing.py</a></td>
<td class="spacer">&nbsp;</td>
<td>87</td>
<td>38</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="49 87">56%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_earnings_py.html">scripts&#8201;/&#8201;earnings.py</a></td>
<td class="spacer">&nbsp;</td>
<td>329</td>
<td>181</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="148 329">45%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_fetch_news_py.html">scripts&#8201;/&#8201;fetch_news.py</a></td>
<td class="spacer">&nbsp;</td>
<td>589</td>
<td>377</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="212 589">36%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_portfolio_py.html">scripts&#8201;/&#8201;portfolio.py</a></td>
<td class="spacer">&nbsp;</td>
<td>183</td>
<td>124</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="59 183">32%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_ranking_py.html">scripts&#8201;/&#8201;ranking.py</a></td>
<td class="spacer">&nbsp;</td>
<td>147</td>
<td>21</td>
<td>9</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="126 147">86%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_research_py.html">scripts&#8201;/&#8201;research.py</a></td>
<td class="spacer">&nbsp;</td>
<td>130</td>
<td>45</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="85 130">65%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_setup_py.html">scripts&#8201;/&#8201;setup.py</a></td>
<td class="spacer">&nbsp;</td>
<td>168</td>
<td>124</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="44 168">26%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_stocks_py.html">scripts&#8201;/&#8201;stocks.py</a></td>
<td class="spacer">&nbsp;</td>
<td>184</td>
<td>87</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="97 184">53%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_summarize_py.html">scripts&#8201;/&#8201;summarize.py</a></td>
<td class="spacer">&nbsp;</td>
<td>972</td>
<td>462</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="510 972">52%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_translate_portfolio_py.html">scripts&#8201;/&#8201;translate_portfolio.py</a></td>
<td class="spacer">&nbsp;</td>
<td>88</td>
<td>88</td>
<td>2</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="0 88">0%</td>
</tr>
<tr class="region">
<td class="name"><a href="z_de1a740d5dc98ffd_utils_py.html">scripts&#8201;/&#8201;utils.py</a></td>
<td class="spacer">&nbsp;</td>
<td>34</td>
<td>10</td>
<td>0</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="24 34">71%</td>
</tr>
</tbody>
<tfoot>
<tr class="total">
<td class="name">Total</td>
<td class="spacer">&nbsp;</td>
<td>3203</td>
<td>1675</td>
<td>29</td>
<td class="spacer">&nbsp;</td>
<td data-ratio="1528 3203">48%</td>
</tr>
</tfoot>
</table>
<p id="no_rows">
No items found using the specified filter.
</p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
<aside class="hidden">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_utils_py.html"></a>
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_alerts_py.html"></a>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</footer>
</body>
</html>

1
htmlcov/status.json generated Normal file
View File

@@ -0,0 +1 @@
{"note":"This file is an internal implementation detail to speed up HTML report generation. Its format can change at any time. You might be looking for the JSON report: https://coverage.rtfd.io/cmd.html#cmd-json","format":5,"version":"7.13.2","globals":"4be31ca40797e8400fa13be69cbf6b96","files":{"z_de1a740d5dc98ffd_alerts_py":{"hash":"9256045bbdf042ec8ac79100b07f6e16","index":{"url":"z_de1a740d5dc98ffd_alerts_py.html","file":"scripts/alerts.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":292,"n_excluded":2,"n_missing":118,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_briefing_py":{"hash":"8762987b6cbda4b3959d184f0fd43f44","index":{"url":"z_de1a740d5dc98ffd_briefing_py.html","file":"scripts/briefing.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":87,"n_excluded":2,"n_missing":38,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_earnings_py":{"hash":"313062c04b56cd9a2f238c0c041e795c","index":{"url":"z_de1a740d5dc98ffd_earnings_py.html","file":"scripts/earnings.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":329,"n_excluded":2,"n_missing":181,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_fetch_news_py":{"hash":"6cc00fcf9c47d99abd6109edce33ab1c","index":{"url":"z_de1a740d5dc98ffd_fetch_news_py.html","file":"scripts/fetch_news.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":589,"n_excluded":2,"n_missing":377,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_portfolio_py":{"hash":"291475985d04ed7b91150b8eb45bb333","index":{"url":"z_de1a740d5dc98ffd_portfolio_py.html","file":"scripts/portfolio.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":183,"n_excluded":2,"n_missing":124,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_ranking_py":{"hash":"1118174517ba630eb85b35f61798c37f","index":{"url":"z_de1a740d5dc98ffd_ranking_py.html","file":"scripts/ranking.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":147,"n_excluded":9,"n_missing":21,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_research_py":{"hash":"f70f5afdad459e2a82b06d76961b0502","index":{"url":"z_de1a740d5dc98ffd_research_py.html","file":"scripts/research.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":130,"n_excluded":2,"n_missing":45,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_setup_py":{"hash":"2b936e494283c91a1b0c1ac177ca3d23","index":{"url":"z_de1a740d5dc98ffd_setup_py.html","file":"scripts/setup.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":168,"n_excluded":2,"n_missing":124,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_stocks_py":{"hash":"a631cf0e894b87b0a89f70f06987e155","index":{"url":"z_de1a740d5dc98ffd_stocks_py.html","file":"scripts/stocks.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":184,"n_excluded":2,"n_missing":87,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_summarize_py":{"hash":"d04f1c3e26fe60ac2710db72a49b8e21","index":{"url":"z_de1a740d5dc98ffd_summarize_py.html","file":"scripts/summarize.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":972,"n_excluded":2,"n_missing":462,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_translate_portfolio_py":{"hash":"74687196bc47c7bcc8dd5ef4e7a118d2","index":{"url":"z_de1a740d5dc98ffd_translate_portfolio_py.html","file":"scripts/translate_portfolio.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":88,"n_excluded":2,"n_missing":88,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}},"z_de1a740d5dc98ffd_utils_py":{"hash":"fd9700472399838a648d2182ce916cd4","index":{"url":"z_de1a740d5dc98ffd_utils_py.html","file":"scripts/utils.py","description":"","nums":{"precision":0,"n_files":1,"n_statements":34,"n_excluded":0,"n_missing":10,"n_branches":0,"n_partial_branches":0,"n_missing_branches":0}}}}}

389
htmlcov/style_cb_9ff733b0.css generated Normal file
View File

@@ -0,0 +1,389 @@
@charset "UTF-8";
/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */
/* For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt */
/* Don't edit this .css file. Edit the .scss file instead! */
html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; }
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; }
@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } }
@media (prefers-color-scheme: dark) { body { color: #eee; } }
html > body { font-size: 16px; }
a:active, a:focus { outline: 2px dashed #007acc; }
p { font-size: .875em; line-height: 1.4em; }
table { border-collapse: collapse; }
td { vertical-align: top; }
table tr.hidden { display: none !important; }
p#no_rows { display: none; font-size: 1.15em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
a.nav { text-decoration: none; color: inherit; }
a.nav:hover { text-decoration: underline; color: inherit; }
.hidden { display: none; }
header { background: #f8f8f8; width: 100%; z-index: 2; border-bottom: 1px solid #ccc; }
@media (prefers-color-scheme: dark) { header { background: black; } }
@media (prefers-color-scheme: dark) { header { border-color: #333; } }
header .content { padding: 1rem 3.5rem; }
header h2 { margin-top: .5em; font-size: 1em; }
header h2 a.button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; }
@media (prefers-color-scheme: dark) { header h2 a.button { background: #333; } }
@media (prefers-color-scheme: dark) { header h2 a.button { border-color: #444; } }
header h2 a.button.current { border: 2px solid; background: #fff; border-color: #999; cursor: default; }
@media (prefers-color-scheme: dark) { header h2 a.button.current { background: #1e1e1e; } }
@media (prefers-color-scheme: dark) { header h2 a.button.current { border-color: #777; } }
header p.text { margin: .5em 0 -.5em; color: #666; font-style: italic; }
@media (prefers-color-scheme: dark) { header p.text { color: #aaa; } }
header.sticky { position: fixed; left: 0; right: 0; height: 2.5em; }
header.sticky .text { display: none; }
header.sticky h1, header.sticky h2 { font-size: 1em; margin-top: 0; display: inline-block; }
header.sticky .content { padding: 0.5rem 3.5rem; }
header.sticky .content p { font-size: 1em; }
header.sticky ~ #source { padding-top: 6.5em; }
main { position: relative; z-index: 1; }
footer { margin: 1rem 3.5rem; }
footer .content { padding: 0; color: #666; font-style: italic; }
@media (prefers-color-scheme: dark) { footer .content { color: #aaa; } }
#index { margin: 1rem 0 0 3.5rem; }
h1 { font-size: 1.25em; display: inline-block; }
#filter_container { float: right; margin: 0 2em 0 0; line-height: 1.66em; }
#filter_container #filter { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; }
@media (prefers-color-scheme: dark) { #filter_container #filter { border-color: #444; } }
@media (prefers-color-scheme: dark) { #filter_container #filter { background: #1e1e1e; } }
@media (prefers-color-scheme: dark) { #filter_container #filter { color: #eee; } }
#filter_container #filter:focus { border-color: #007acc; }
#filter_container :disabled ~ label { color: #ccc; }
@media (prefers-color-scheme: dark) { #filter_container :disabled ~ label { color: #444; } }
#filter_container label { font-size: .875em; color: #666; }
@media (prefers-color-scheme: dark) { #filter_container label { color: #aaa; } }
header button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; background: #eee; color: inherit; text-decoration: none; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; }
@media (prefers-color-scheme: dark) { header button { background: #333; } }
@media (prefers-color-scheme: dark) { header button { border-color: #444; } }
header button:active, header button:focus { outline: 2px dashed #007acc; }
header button.run { background: #eeffee; }
@media (prefers-color-scheme: dark) { header button.run { background: #373d29; } }
header button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; }
@media (prefers-color-scheme: dark) { header button.run.show_run { background: #373d29; } }
header button.mis { background: #ffeeee; }
@media (prefers-color-scheme: dark) { header button.mis { background: #4b1818; } }
header button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; }
@media (prefers-color-scheme: dark) { header button.mis.show_mis { background: #4b1818; } }
header button.exc { background: #f7f7f7; }
@media (prefers-color-scheme: dark) { header button.exc { background: #333; } }
header button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; }
@media (prefers-color-scheme: dark) { header button.exc.show_exc { background: #333; } }
header button.par { background: #ffffd5; }
@media (prefers-color-scheme: dark) { header button.par { background: #650; } }
header button.par.show_par { background: #ffa; border: 2px solid #bbbb00; margin: 0 .1em; }
@media (prefers-color-scheme: dark) { header button.par.show_par { background: #650; } }
#help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; }
#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; }
#help_panel_wrapper { float: right; position: relative; }
#keyboard_icon { margin: 5px; }
#help_panel_state { display: none; }
#help_panel { top: 25px; right: 0; padding: .75em; border: 1px solid #883; color: #333; }
#help_panel .keyhelp p { margin-top: .75em; }
#help_panel .legend { font-style: italic; margin-bottom: 1em; }
.indexfile #help_panel { width: 25em; }
.pyfile #help_panel { width: 18em; }
#help_panel_state:checked ~ #help_panel { display: block; }
kbd { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; border-radius: 3px; }
#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
#source p { position: relative; white-space: pre; }
#source p * { box-sizing: border-box; }
#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; user-select: none; }
@media (prefers-color-scheme: dark) { #source p .n { color: #777; } }
#source p .n.highlight { background: #ffdd00; }
#source p .n a { scroll-margin-top: 6em; text-decoration: none; color: #999; }
@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } }
#source p .n a:hover { text-decoration: underline; color: #999; }
@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } }
#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; }
@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } }
#source p .t:hover { background: #f2f2f2; }
@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } }
#source p .t:hover ~ .r .annotate.long { display: block; }
#source p .t .com { color: #008000; font-style: italic; line-height: 1px; }
@media (prefers-color-scheme: dark) { #source p .t .com { color: #6a9955; } }
#source p .t .key { font-weight: bold; line-height: 1px; }
#source p .t .str, #source p .t .fst { color: #0451a5; }
@media (prefers-color-scheme: dark) { #source p .t .str, #source p .t .fst { color: #9cdcfe; } }
#source p.mis .t { border-left: 0.2em solid #ff0000; }
#source p.mis.show_mis .t { background: #fdd; }
@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } }
#source p.mis.show_mis .t:hover { background: #f2d2d2; }
@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } }
#source p.mis.mis2 .t { border-left: 0.2em dotted #ff0000; }
#source p.mis.mis2.show_mis .t { background: #ffeeee; }
@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t { background: #351b1b; } }
#source p.mis.mis2.show_mis .t:hover { background: #f2d2d2; }
@media (prefers-color-scheme: dark) { #source p.mis.mis2.show_mis .t:hover { background: #532323; } }
#source p.run .t { border-left: 0.2em solid #00dd00; }
#source p.run.show_run .t { background: #dfd; }
@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } }
#source p.run.show_run .t:hover { background: #d2f2d2; }
@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } }
#source p.run.run2 .t { border-left: 0.2em dotted #00dd00; }
#source p.run.run2.show_run .t { background: #eeffee; }
@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t { background: #2b2e24; } }
#source p.run.run2.show_run .t:hover { background: #d2f2d2; }
@media (prefers-color-scheme: dark) { #source p.run.run2.show_run .t:hover { background: #404633; } }
#source p.exc .t { border-left: 0.2em solid #808080; }
#source p.exc.show_exc .t { background: #eee; }
@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } }
#source p.exc.show_exc .t:hover { background: #e2e2e2; }
@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } }
#source p.exc.exc2 .t { border-left: 0.2em dotted #808080; }
#source p.exc.exc2.show_exc .t { background: #f7f7f7; }
@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t { background: #292929; } }
#source p.exc.exc2.show_exc .t:hover { background: #e2e2e2; }
@media (prefers-color-scheme: dark) { #source p.exc.exc2.show_exc .t:hover { background: #3c3c3c; } }
#source p.par .t { border-left: 0.2em solid #bbbb00; }
#source p.par.show_par .t { background: #ffa; }
@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } }
#source p.par.show_par .t:hover { background: #f2f2a2; }
@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } }
#source p.par.par2 .t { border-left: 0.2em dotted #bbbb00; }
#source p.par.par2.show_par .t { background: #ffffd5; }
@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t { background: #423a0f; } }
#source p.par.par2.show_par .t:hover { background: #f2f2a2; }
@media (prefers-color-scheme: dark) { #source p.par.par2.show_par .t:hover { background: #6d5d0c; } }
#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; }
#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; }
@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } }
#source p .annotate.short:hover ~ .long { display: block; }
#source p .annotate.long { width: 30em; right: 2.5em; }
#source p input { display: none; }
#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; }
#source p input ~ .r label.ctx::before { content: "▶ "; }
#source p input ~ .r label.ctx:hover { background: #e8f4ff; color: #666; }
@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } }
@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } }
#source p input:checked ~ .r label.ctx { background: #d0e8ff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; }
@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } }
@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } }
#source p input:checked ~ .r label.ctx::before { content: "▼ "; }
#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; }
#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; }
@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } }
#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #d0e8ff; border-radius: .25em; margin-right: 1.75em; text-align: right; }
@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } }
#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; }
#index table.index { margin-left: -.5em; }
#index td, #index th { text-align: right; vertical-align: baseline; padding: .25em .5em; border-bottom: 1px solid #eee; }
@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } }
#index td.name, #index th.name { text-align: left; width: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; min-width: 15em; }
#index td.left, #index th.left { text-align: left; }
#index td.spacer, #index th.spacer { border: none; padding: 0; }
#index td.spacer:hover, #index th.spacer:hover { background: inherit; }
#index th { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-style: italic; color: #333; border-color: #ccc; cursor: pointer; }
@media (prefers-color-scheme: dark) { #index th { color: #ddd; } }
@media (prefers-color-scheme: dark) { #index th { border-color: #444; } }
#index th:hover { background: #eee; }
@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } }
#index th .arrows { color: #666; font-size: 85%; font-family: sans-serif; font-style: normal; pointer-events: none; }
#index th[aria-sort="ascending"], #index th[aria-sort="descending"] { white-space: nowrap; background: #eee; padding-left: .5em; }
@media (prefers-color-scheme: dark) { #index th[aria-sort="ascending"], #index th[aria-sort="descending"] { background: #333; } }
#index th[aria-sort="ascending"] .arrows::after { content: " ▲"; }
#index th[aria-sort="descending"] .arrows::after { content: " ▼"; }
#index tr.grouphead th { cursor: default; font-style: normal; border-color: #999; }
@media (prefers-color-scheme: dark) { #index tr.grouphead th { border-color: #777; } }
#index td.name { font-size: 1.15em; }
#index td.name a { text-decoration: none; color: inherit; }
#index td.name .no-noun { font-style: italic; }
#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-bottom: none; }
#index tr.region:hover { background: #eee; }
@media (prefers-color-scheme: dark) { #index tr.region:hover { background: #333; } }
#index tr.region:hover td.name { text-decoration: underline; color: inherit; }
#scroll_marker { position: fixed; z-index: 3; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; }
@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } }
@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } }
#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; }
@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } }

597
htmlcov/z_de1a740d5dc98ffd_alerts_py.html generated Normal file
View File

@@ -0,0 +1,597 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/alerts.py: 60%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;alerts.py</b>:
<span class="pc_cov">60%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">292 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">174<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">118<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="index.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_briefing_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Price Target Alerts - Track buy zone alerts for stocks.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">Features:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="str">- Set price target alerts (buy zone triggers)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="str">- Check alerts against current prices</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="str">- Snooze, update, delete alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="str">- Multi-currency support (USD, EUR, JPY, SGD, MXN)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="str">Usage:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="str"> alerts.py list # Show all alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="str"> alerts.py set CRWD 400 --note 'Kaufzone' # Set alert</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="str"> alerts.py check # Check triggered alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="str"> alerts.py delete CRWD # Delete alert</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"><span class="str"> alerts.py snooze CRWD --days 7 # Snooze for 7 days</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="str"> alerts.py update CRWD 380 # Update target price</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span><span class="op">,</span> <span class="nam">timedelta</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"><span class="key">from</span> <span class="nam">utils</span> <span class="key">import</span> <span class="nam">ensure_venv</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"><span class="nam">ensure_venv</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"><span class="com"># Lazy import to avoid numpy issues at module load</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"><span class="nam">fetch_market_data</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"><span class="key">def</span> <span class="nam">get_fetch_market_data</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"> <span class="key">global</span> <span class="nam">fetch_market_data</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="key">if</span> <span class="nam">fetch_market_data</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="key">from</span> <span class="nam">fetch_news</span> <span class="key">import</span> <span class="nam">fetch_market_data</span> <span class="key">as</span> <span class="nam">fmd</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="nam">fetch_market_data</span> <span class="op">=</span> <span class="nam">fmd</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="key">return</span> <span class="nam">fetch_market_data</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"><span class="nam">SCRIPT_DIR</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"><span class="nam">CONFIG_DIR</span> <span class="op">=</span> <span class="nam">SCRIPT_DIR</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"config"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"><span class="nam">ALERTS_FILE</span> <span class="op">=</span> <span class="nam">CONFIG_DIR</span> <span class="op">/</span> <span class="str">"alerts.json"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"><span class="nam">SUPPORTED_CURRENCIES</span> <span class="op">=</span> <span class="op">[</span><span class="str">"USD"</span><span class="op">,</span> <span class="str">"EUR"</span><span class="op">,</span> <span class="str">"JPY"</span><span class="op">,</span> <span class="str">"SGD"</span><span class="op">,</span> <span class="str">"MXN"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"><span class="key">def</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"> <span class="str">"""Load alerts from JSON file."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">ALERTS_FILE</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="str">"_meta"</span><span class="op">:</span> <span class="op">{</span><span class="str">"version"</span><span class="op">:</span> <span class="num">1</span><span class="op">,</span> <span class="str">"supported_currencies"</span><span class="op">:</span> <span class="nam">SUPPORTED_CURRENCIES</span><span class="op">}</span><span class="op">,</span> <span class="str">"alerts"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="key">return</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">ALERTS_FILE</span><span class="op">.</span><span class="nam">read_text</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"><span class="key">def</span> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"> <span class="str">"""Save alerts to JSON file."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"_meta"</span><span class="op">]</span><span class="op">[</span><span class="str">"updated_at"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"> <span class="nam">ALERTS_FILE</span><span class="op">.</span><span class="nam">write_text</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"><span class="key">def</span> <span class="nam">get_alert_by_ticker</span><span class="op">(</span><span class="nam">alerts</span><span class="op">:</span> <span class="nam">list</span><span class="op">,</span> <span class="nam">ticker</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span> <span class="op">|</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"> <span class="str">"""Find alert by ticker."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="key">for</span> <span class="nam">alert</span> <span class="key">in</span> <span class="nam">alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t"> <span class="key">if</span> <span class="nam">alert</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span> <span class="op">==</span> <span class="nam">ticker</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"> <span class="key">return</span> <span class="nam">alert</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="key">return</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t"><span class="key">def</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">price</span><span class="op">:</span> <span class="nam">float</span><span class="op">,</span> <span class="nam">currency</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t"> <span class="str">"""Format price with currency symbol."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"> <span class="nam">symbols</span> <span class="op">=</span> <span class="op">{</span><span class="str">"USD"</span><span class="op">:</span> <span class="str">"$"</span><span class="op">,</span> <span class="str">"EUR"</span><span class="op">:</span> <span class="str">"&#8364;"</span><span class="op">,</span> <span class="str">"JPY"</span><span class="op">:</span> <span class="str">"&#165;"</span><span class="op">,</span> <span class="str">"SGD"</span><span class="op">:</span> <span class="str">"S$"</span><span class="op">,</span> <span class="str">"MXN"</span><span class="op">:</span> <span class="str">"MX$"</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="nam">symbol</span> <span class="op">=</span> <span class="nam">symbols</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">currency</span><span class="op">,</span> <span class="nam">currency</span> <span class="op">+</span> <span class="str">" "</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="key">if</span> <span class="nam">currency</span> <span class="op">==</span> <span class="str">"JPY"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="key">return</span> <span class="str">f"{symbol}{price:,.0f}"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t"> <span class="key">return</span> <span class="str">f"{symbol}{price:,.2f}"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"><span class="key">def</span> <span class="nam">cmd_list</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="str">"""List all alerts."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128237; No price alerts set"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128202; Price Alerts ({len(alerts)} total)\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="nam">now</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="nam">active</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="nam">snoozed</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="key">for</span> <span class="nam">alert</span> <span class="key">in</span> <span class="nam">alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t"> <span class="nam">snooze_until</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"snooze_until"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"> <span class="key">if</span> <span class="nam">snooze_until</span> <span class="key">and</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">snooze_until</span><span class="op">)</span> <span class="op">></span> <span class="nam">now</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"> <span class="nam">snoozed</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">alert</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="nam">active</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">alert</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"> <span class="key">if</span> <span class="nam">active</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"### Active Alerts"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t"> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">active</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="nam">target</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">a</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span><span class="op">,</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"currency"</span><span class="op">,</span> <span class="str">"USD"</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="nam">note</span> <span class="op">=</span> <span class="str">f' &#8212; "{a["note"]}"'</span> <span class="key">if</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"note"</span><span class="op">)</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t"> <span class="nam">user</span> <span class="op">=</span> <span class="str">f" (by {a['set_by']})"</span> <span class="key">if</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"set_by"</span><span class="op">)</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#8226; {a['ticker']}: {target}{note}{user}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="key">if</span> <span class="nam">snoozed</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"### Snoozed"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">snoozed</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"> <span class="nam">target</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">a</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span><span class="op">,</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"currency"</span><span class="op">,</span> <span class="str">"USD"</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="nam">until</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">a</span><span class="op">[</span><span class="str">"snooze_until"</span><span class="op">]</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#8226; {a['ticker']}: {target} (until {until})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"><span class="key">def</span> <span class="nam">cmd_set</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"> <span class="str">"""Set a new alert."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="com"># Check if alert exists</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"> <span class="nam">existing</span> <span class="op">=</span> <span class="nam">get_alert_by_ticker</span><span class="op">(</span><span class="nam">alerts</span><span class="op">,</span> <span class="nam">ticker</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t"> <span class="key">if</span> <span class="nam">existing</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Alert for {ticker} already exists. Use 'update' to change target."</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t"> <span class="com"># Validate target price</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span> <span class="op">&lt;=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; Target price must be greater than 0"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="nam">currency</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">currency</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">currency</span> <span class="key">else</span> <span class="str">"USD"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"> <span class="key">if</span> <span class="nam">currency</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">SUPPORTED_CURRENCIES</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; Currency {currency} not supported. Use: {', '.join(SUPPORTED_CURRENCIES)}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="com"># Warn about currency mismatch based on ticker suffix</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"> <span class="nam">ticker_currency_map</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="str">".T"</span><span class="op">:</span> <span class="str">"JPY"</span><span class="op">,</span> <span class="com"># Tokyo</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="str">".SI"</span><span class="op">:</span> <span class="str">"SGD"</span><span class="op">,</span> <span class="com"># Singapore</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="str">".MX"</span><span class="op">:</span> <span class="str">"MXN"</span><span class="op">,</span> <span class="com"># Mexico</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="str">".DE"</span><span class="op">:</span> <span class="str">"EUR"</span><span class="op">,</span> <span class="str">".F"</span><span class="op">:</span> <span class="str">"EUR"</span><span class="op">,</span> <span class="str">".PA"</span><span class="op">:</span> <span class="str">"EUR"</span><span class="op">,</span> <span class="com"># Europe</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="nam">expected_currency</span> <span class="op">=</span> <span class="str">"USD"</span> <span class="com"># Default for US stocks</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="key">for</span> <span class="nam">suffix</span><span class="op">,</span> <span class="nam">curr</span> <span class="key">in</span> <span class="nam">ticker_currency_map</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t"> <span class="key">if</span> <span class="nam">ticker</span><span class="op">.</span><span class="nam">endswith</span><span class="op">(</span><span class="nam">suffix</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="nam">expected_currency</span> <span class="op">=</span> <span class="nam">curr</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="key">if</span> <span class="nam">currency</span> <span class="op">!=</span> <span class="nam">expected_currency</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Warning: {ticker} trades in {expected_currency}, but alert set in {currency}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"> <span class="com"># Fetch current price (optional - may fail if numpy broken)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="nam">current_price</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="nam">quotes</span> <span class="op">=</span> <span class="nam">get_fetch_market_data</span><span class="op">(</span><span class="op">)</span><span class="op">(</span><span class="op">[</span><span class="nam">ticker</span><span class="op">]</span><span class="op">,</span> <span class="nam">timeout</span><span class="op">=</span><span class="num">10</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="key">if</span> <span class="nam">ticker</span> <span class="key">in</span> <span class="nam">quotes</span> <span class="key">and</span> <span class="nam">quotes</span><span class="op">[</span><span class="nam">ticker</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"price"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="nam">current_price</span> <span class="op">=</span> <span class="nam">quotes</span><span class="op">[</span><span class="nam">ticker</span><span class="op">]</span><span class="op">[</span><span class="str">"price"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Could not fetch current price: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t"> <span class="nam">alert</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t"> <span class="str">"target_price"</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t"> <span class="str">"currency"</span><span class="op">:</span> <span class="nam">currency</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"> <span class="str">"note"</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">note</span> <span class="key">or</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="str">"set_by"</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">user</span> <span class="key">or</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t"> <span class="str">"set_date"</span><span class="op">:</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t"> <span class="str">"status"</span><span class="op">:</span> <span class="str">"active"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="str">"snooze_until"</span><span class="op">:</span> <span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t"> <span class="str">"triggered_count"</span><span class="op">:</span> <span class="num">0</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t"> <span class="str">"last_triggered"</span><span class="op">:</span> <span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="nam">alerts</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">alert</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"alerts"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t"> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t"> <span class="nam">target_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">target</span><span class="op">,</span> <span class="nam">currency</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Alert set: {ticker} under {target_str}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t"> <span class="key">if</span> <span class="nam">current_price</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"> <span class="nam">pct_diff</span> <span class="op">=</span> <span class="op">(</span><span class="op">(</span><span class="nam">current_price</span> <span class="op">-</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span><span class="op">)</span> <span class="op">/</span> <span class="nam">current_price</span><span class="op">)</span> <span class="op">*</span> <span class="num">100</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t"> <span class="nam">current_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">current_price</span><span class="op">,</span> <span class="nam">currency</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" Current: {current_str} ({pct_diff:+.1f}% to target)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"><span class="key">def</span> <span class="nam">cmd_delete</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t"> <span class="str">"""Delete an alert."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t"> <span class="nam">new_alerts</span> <span class="op">=</span> <span class="op">[</span><span class="nam">a</span> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">alerts</span> <span class="key">if</span> <span class="nam">a</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span> <span class="op">!=</span> <span class="nam">ticker</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">new_alerts</span><span class="op">)</span> <span class="op">==</span> <span class="nam">len</span><span class="op">(</span><span class="nam">alerts</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; No alert found for {ticker}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"alerts"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">new_alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128465;&#65039; Alert deleted: {ticker}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"><span class="key">def</span> <span class="nam">cmd_snooze</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t"> <span class="str">"""Snooze an alert."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t"> <span class="nam">alert</span> <span class="op">=</span> <span class="nam">get_alert_by_ticker</span><span class="op">(</span><span class="nam">alerts</span><span class="op">,</span> <span class="nam">ticker</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">alert</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; No alert found for {ticker}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t"> <span class="nam">days</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">days</span> <span class="key">or</span> <span class="num">7</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t"> <span class="nam">snooze_until</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span> <span class="op">+</span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">days</span><span class="op">=</span><span class="nam">days</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"snooze_until"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">snooze_until</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t"> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128564; Alert snoozed: {ticker} until {snooze_until.strftime('%Y-%m-%d')}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t"><span class="key">def</span> <span class="nam">cmd_update</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t"> <span class="str">"""Update alert target price."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t"> <span class="nam">alert</span> <span class="op">=</span> <span class="nam">get_alert_by_ticker</span><span class="op">(</span><span class="nam">alerts</span><span class="op">,</span> <span class="nam">ticker</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">alert</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; No alert found for {ticker}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t"> <span class="com"># Validate target price</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span> <span class="op">&lt;=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; Target price must be greater than 0"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"> <span class="nam">old_target</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">note</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"note"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">note</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t"> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t"> <span class="nam">currency</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"currency"</span><span class="op">,</span> <span class="str">"USD"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t"> <span class="nam">old_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">old_target</span><span class="op">,</span> <span class="nam">currency</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t"> <span class="nam">new_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">target</span><span class="op">,</span> <span class="nam">currency</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9999;&#65039; Alert updated: {ticker} {old_str} &#8594; {new_str}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t"><span class="key">def</span> <span class="nam">cmd_check</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t"> <span class="str">"""Check alerts against current prices."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">json</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="op">{</span><span class="str">"triggered"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span> <span class="str">"watching"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">}</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128237; No alerts to check"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"> <span class="nam">now</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t"> <span class="nam">active_alerts</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t"> <span class="key">for</span> <span class="nam">alert</span> <span class="key">in</span> <span class="nam">alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t"> <span class="nam">snooze_until</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"snooze_until"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t"> <span class="key">if</span> <span class="nam">snooze_until</span> <span class="key">and</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">snooze_until</span><span class="op">)</span> <span class="op">></span> <span class="nam">now</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"> <span class="nam">active_alerts</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">alert</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">active_alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">json</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="op">{</span><span class="str">"triggered"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span> <span class="str">"watching"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">}</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128237; All alerts snoozed"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t"> <span class="com"># Fetch prices for all active alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t"> <span class="nam">tickers</span> <span class="op">=</span> <span class="op">[</span><span class="nam">a</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">active_alerts</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t"> <span class="nam">quotes</span> <span class="op">=</span> <span class="nam">get_fetch_market_data</span><span class="op">(</span><span class="op">)</span><span class="op">(</span><span class="nam">tickers</span><span class="op">,</span> <span class="nam">timeout</span><span class="op">=</span><span class="num">30</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t"> <span class="nam">triggered</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t284" href="#t284">284</a></span><span class="t"> <span class="nam">watching</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t285" href="#t285">285</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t286" href="#t286">286</a></span><span class="t"> <span class="key">for</span> <span class="nam">alert</span> <span class="key">in</span> <span class="nam">active_alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t287" href="#t287">287</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t288" href="#t288">288</a></span><span class="t"> <span class="nam">target</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t289" href="#t289">289</a></span><span class="t"> <span class="nam">currency</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"currency"</span><span class="op">,</span> <span class="str">"USD"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t290" href="#t290">290</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t291" href="#t291">291</a></span><span class="t"> <span class="nam">quote</span> <span class="op">=</span> <span class="nam">quotes</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">ticker</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t292" href="#t292">292</a></span><span class="t"> <span class="nam">price</span> <span class="op">=</span> <span class="nam">quote</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t293" href="#t293">293</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t294" href="#t294">294</a></span><span class="t"> <span class="key">if</span> <span class="nam">price</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t295" href="#t295">295</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t296" href="#t296">296</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t297" href="#t297">297</a></span><span class="t"> <span class="com"># Divide-by-zero protection</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t298" href="#t298">298</a></span><span class="t"> <span class="key">if</span> <span class="nam">target</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t299" href="#t299">299</a></span><span class="t"> <span class="nam">pct_diff</span> <span class="op">=</span> <span class="num">0</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t300" href="#t300">300</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t301" href="#t301">301</a></span><span class="t"> <span class="nam">pct_diff</span> <span class="op">=</span> <span class="op">(</span><span class="op">(</span><span class="nam">price</span> <span class="op">-</span> <span class="nam">target</span><span class="op">)</span> <span class="op">/</span> <span class="nam">target</span><span class="op">)</span> <span class="op">*</span> <span class="num">100</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t302" href="#t302">302</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t303" href="#t303">303</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t304" href="#t304">304</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t305" href="#t305">305</a></span><span class="t"> <span class="str">"target_price"</span><span class="op">:</span> <span class="nam">target</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t306" href="#t306">306</a></span><span class="t"> <span class="str">"current_price"</span><span class="op">:</span> <span class="nam">price</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t307" href="#t307">307</a></span><span class="t"> <span class="str">"currency"</span><span class="op">:</span> <span class="nam">currency</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t308" href="#t308">308</a></span><span class="t"> <span class="str">"pct_from_target"</span><span class="op">:</span> <span class="nam">round</span><span class="op">(</span><span class="nam">pct_diff</span><span class="op">,</span> <span class="num">2</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t309" href="#t309">309</a></span><span class="t"> <span class="str">"note"</span><span class="op">:</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"note"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t310" href="#t310">310</a></span><span class="t"> <span class="str">"set_by"</span><span class="op">:</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"set_by"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t311" href="#t311">311</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t312" href="#t312">312</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t313" href="#t313">313</a></span><span class="t"> <span class="key">if</span> <span class="nam">price</span> <span class="op">&lt;=</span> <span class="nam">target</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t314" href="#t314">314</a></span><span class="t"> <span class="nam">triggered</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">result</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t315" href="#t315">315</a></span><span class="t"> <span class="com"># Update triggered count (only once per day to avoid inflation)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t316" href="#t316">316</a></span><span class="t"> <span class="nam">last_triggered</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"last_triggered"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t317" href="#t317">317</a></span><span class="t"> <span class="nam">today</span> <span class="op">=</span> <span class="nam">now</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t318" href="#t318">318</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">last_triggered</span> <span class="key">or</span> <span class="key">not</span> <span class="nam">last_triggered</span><span class="op">.</span><span class="nam">startswith</span><span class="op">(</span><span class="nam">today</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t319" href="#t319">319</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"triggered_count"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"triggered_count"</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">+</span> <span class="num">1</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t320" href="#t320">320</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"last_triggered"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">now</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t321" href="#t321">321</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t322" href="#t322">322</a></span><span class="t"> <span class="nam">watching</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">result</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t323" href="#t323">323</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t324" href="#t324">324</a></span><span class="t"> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t325" href="#t325">325</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t326" href="#t326">326</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">json</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t327" href="#t327">327</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="op">{</span><span class="str">"triggered"</span><span class="op">:</span> <span class="nam">triggered</span><span class="op">,</span> <span class="str">"watching"</span><span class="op">:</span> <span class="nam">watching</span><span class="op">}</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t328" href="#t328">328</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t329" href="#t329">329</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t330" href="#t330">330</a></span><span class="t"> <span class="com"># Translations</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t331" href="#t331">331</a></span><span class="t"> <span class="nam">lang</span> <span class="op">=</span> <span class="nam">getattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'lang'</span><span class="op">,</span> <span class="str">'en'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t332" href="#t332">332</a></span><span class="t"> <span class="key">if</span> <span class="nam">lang</span> <span class="op">==</span> <span class="str">"de"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t333" href="#t333">333</a></span><span class="t"> <span class="nam">labels</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t334" href="#t334">334</a></span><span class="t"> <span class="str">"title"</span><span class="op">:</span> <span class="str">"PREISWARNUNGEN"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t335" href="#t335">335</a></span><span class="t"> <span class="str">"in_zone"</span><span class="op">:</span> <span class="str">"IN KAUFZONE"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t336" href="#t336">336</a></span><span class="t"> <span class="str">"buy"</span><span class="op">:</span> <span class="str">"KAUFEN!"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t337" href="#t337">337</a></span><span class="t"> <span class="str">"target"</span><span class="op">:</span> <span class="str">"Ziel"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t338" href="#t338">338</a></span><span class="t"> <span class="str">"watching"</span><span class="op">:</span> <span class="str">"BEOBACHTUNG"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t339" href="#t339">339</a></span><span class="t"> <span class="str">"to_target"</span><span class="op">:</span> <span class="str">"noch"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t340" href="#t340">340</a></span><span class="t"> <span class="str">"no_data"</span><span class="op">:</span> <span class="str">"Keine Preisdaten f&#252;r Alerts verf&#252;gbar"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t341" href="#t341">341</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t342" href="#t342">342</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t343" href="#t343">343</a></span><span class="t"> <span class="nam">labels</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t344" href="#t344">344</a></span><span class="t"> <span class="str">"title"</span><span class="op">:</span> <span class="str">"PRICE ALERTS"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t345" href="#t345">345</a></span><span class="t"> <span class="str">"in_zone"</span><span class="op">:</span> <span class="str">"IN BUY ZONE"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t346" href="#t346">346</a></span><span class="t"> <span class="str">"buy"</span><span class="op">:</span> <span class="str">"BUY SIGNAL"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t347" href="#t347">347</a></span><span class="t"> <span class="str">"target"</span><span class="op">:</span> <span class="str">"target"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t348" href="#t348">348</a></span><span class="t"> <span class="str">"watching"</span><span class="op">:</span> <span class="str">"WATCHING"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t349" href="#t349">349</a></span><span class="t"> <span class="str">"to_target"</span><span class="op">:</span> <span class="str">"to target"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t350" href="#t350">350</a></span><span class="t"> <span class="str">"no_data"</span><span class="op">:</span> <span class="str">"No price data available for alerts"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t351" href="#t351">351</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t352" href="#t352">352</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t353" href="#t353">353</a></span><span class="t"> <span class="com"># Date header</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t354" href="#t354">354</a></span><span class="t"> <span class="nam">date_str</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%b %d, %Y"</span><span class="op">)</span> <span class="key">if</span> <span class="nam">lang</span> <span class="op">==</span> <span class="str">"en"</span> <span class="key">else</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%d. %b %Y"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t355" href="#t355">355</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128202; {labels['title']} &#8212; {date_str}\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t356" href="#t356">356</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t357" href="#t357">357</a></span><span class="t"> <span class="com"># Human-readable output</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t358" href="#t358">358</a></span><span class="t"> <span class="key">if</span> <span class="nam">triggered</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t359" href="#t359">359</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128994; {labels['in_zone']}:\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t360" href="#t360">360</a></span><span class="t"> <span class="key">for</span> <span class="nam">t</span> <span class="key">in</span> <span class="nam">triggered</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t361" href="#t361">361</a></span><span class="t"> <span class="nam">target_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">t</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span><span class="op">,</span> <span class="nam">t</span><span class="op">[</span><span class="str">"currency"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t362" href="#t362">362</a></span><span class="t"> <span class="nam">current_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">t</span><span class="op">[</span><span class="str">"current_price"</span><span class="op">]</span><span class="op">,</span> <span class="nam">t</span><span class="op">[</span><span class="str">"currency"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t363" href="#t363">363</a></span><span class="t"> <span class="nam">note</span> <span class="op">=</span> <span class="str">f'\n "{t["note"]}"'</span> <span class="key">if</span> <span class="nam">t</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"note"</span><span class="op">)</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t364" href="#t364">364</a></span><span class="t"> <span class="nam">user</span> <span class="op">=</span> <span class="str">f" &#8212; {t['set_by']}"</span> <span class="key">if</span> <span class="nam">t</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"set_by"</span><span class="op">)</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t365" href="#t365">365</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#8226; {t['ticker']}: {current_str} ({labels['target']}: {target_str}) &#8592; {labels['buy']}{note}{user}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t366" href="#t366">366</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t367" href="#t367">367</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t368" href="#t368">368</a></span><span class="t"> <span class="key">if</span> <span class="nam">watching</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t369" href="#t369">369</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9203; {labels['watching']}:\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t370" href="#t370">370</a></span><span class="t"> <span class="key">for</span> <span class="nam">w</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">watching</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">[</span><span class="str">"pct_from_target"</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t371" href="#t371">371</a></span><span class="t"> <span class="nam">target_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">w</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span><span class="op">,</span> <span class="nam">w</span><span class="op">[</span><span class="str">"currency"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t372" href="#t372">372</a></span><span class="t"> <span class="nam">current_str</span> <span class="op">=</span> <span class="nam">format_price</span><span class="op">(</span><span class="nam">w</span><span class="op">[</span><span class="str">"current_price"</span><span class="op">]</span><span class="op">,</span> <span class="nam">w</span><span class="op">[</span><span class="str">"currency"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t373" href="#t373">373</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#8226; {w['ticker']}: {current_str} ({labels['target']}: {target_str}) &#8212; {labels['to_target']} {abs(w['pct_from_target']):.1f}%"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t374" href="#t374">374</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t375" href="#t375">375</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t376" href="#t376">376</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">triggered</span> <span class="key">and</span> <span class="key">not</span> <span class="nam">watching</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t377" href="#t377">377</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128237; {labels['no_data']}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t378" href="#t378">378</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t379" href="#t379">379</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t380" href="#t380">380</a></span><span class="t"><span class="key">def</span> <span class="nam">check_alerts</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t381" href="#t381">381</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t382" href="#t382">382</a></span><span class="t"><span class="str"> Check alerts and return results for briefing integration.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t383" href="#t383">383</a></span><span class="t"><span class="str"> Returns: {"triggered": [...], "watching": [...]}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t384" href="#t384">384</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t385" href="#t385">385</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_alerts</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t386" href="#t386">386</a></span><span class="t"> <span class="nam">alerts</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"alerts"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t387" href="#t387">387</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t388" href="#t388">388</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t389" href="#t389">389</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="str">"triggered"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span> <span class="str">"watching"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t390" href="#t390">390</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t391" href="#t391">391</a></span><span class="t"> <span class="nam">now</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t392" href="#t392">392</a></span><span class="t"> <span class="nam">active_alerts</span> <span class="op">=</span> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t393" href="#t393">393</a></span><span class="t"> <span class="nam">a</span> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t394" href="#t394">394</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"snooze_until"</span><span class="op">)</span> <span class="key">or</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">a</span><span class="op">[</span><span class="str">"snooze_until"</span><span class="op">]</span><span class="op">)</span> <span class="op">&lt;=</span> <span class="nam">now</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t395" href="#t395">395</a></span><span class="t"> <span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t396" href="#t396">396</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t397" href="#t397">397</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">active_alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t398" href="#t398">398</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="str">"triggered"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span> <span class="str">"watching"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t399" href="#t399">399</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t400" href="#t400">400</a></span><span class="t"> <span class="nam">tickers</span> <span class="op">=</span> <span class="op">[</span><span class="nam">a</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">active_alerts</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t401" href="#t401">401</a></span><span class="t"> <span class="nam">quotes</span> <span class="op">=</span> <span class="nam">get_fetch_market_data</span><span class="op">(</span><span class="op">)</span><span class="op">(</span><span class="nam">tickers</span><span class="op">,</span> <span class="nam">timeout</span><span class="op">=</span><span class="num">30</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t402" href="#t402">402</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t403" href="#t403">403</a></span><span class="t"> <span class="nam">triggered</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t404" href="#t404">404</a></span><span class="t"> <span class="nam">watching</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t405" href="#t405">405</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t406" href="#t406">406</a></span><span class="t"> <span class="key">for</span> <span class="nam">alert</span> <span class="key">in</span> <span class="nam">active_alerts</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t407" href="#t407">407</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t408" href="#t408">408</a></span><span class="t"> <span class="nam">target</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">[</span><span class="str">"target_price"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t409" href="#t409">409</a></span><span class="t"> <span class="nam">currency</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"currency"</span><span class="op">,</span> <span class="str">"USD"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t410" href="#t410">410</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t411" href="#t411">411</a></span><span class="t"> <span class="nam">quote</span> <span class="op">=</span> <span class="nam">quotes</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">ticker</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t412" href="#t412">412</a></span><span class="t"> <span class="nam">price</span> <span class="op">=</span> <span class="nam">quote</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t413" href="#t413">413</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t414" href="#t414">414</a></span><span class="t"> <span class="key">if</span> <span class="nam">price</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t415" href="#t415">415</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t416" href="#t416">416</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t417" href="#t417">417</a></span><span class="t"> <span class="com"># Divide-by-zero protection</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t418" href="#t418">418</a></span><span class="t"> <span class="key">if</span> <span class="nam">target</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t419" href="#t419">419</a></span><span class="t"> <span class="nam">pct_diff</span> <span class="op">=</span> <span class="num">0</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t420" href="#t420">420</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t421" href="#t421">421</a></span><span class="t"> <span class="nam">pct_diff</span> <span class="op">=</span> <span class="op">(</span><span class="op">(</span><span class="nam">price</span> <span class="op">-</span> <span class="nam">target</span><span class="op">)</span> <span class="op">/</span> <span class="nam">target</span><span class="op">)</span> <span class="op">*</span> <span class="num">100</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t422" href="#t422">422</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t423" href="#t423">423</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t424" href="#t424">424</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t425" href="#t425">425</a></span><span class="t"> <span class="str">"target_price"</span><span class="op">:</span> <span class="nam">target</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t426" href="#t426">426</a></span><span class="t"> <span class="str">"current_price"</span><span class="op">:</span> <span class="nam">price</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t427" href="#t427">427</a></span><span class="t"> <span class="str">"currency"</span><span class="op">:</span> <span class="nam">currency</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t428" href="#t428">428</a></span><span class="t"> <span class="str">"pct_from_target"</span><span class="op">:</span> <span class="nam">round</span><span class="op">(</span><span class="nam">pct_diff</span><span class="op">,</span> <span class="num">2</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t429" href="#t429">429</a></span><span class="t"> <span class="str">"note"</span><span class="op">:</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"note"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t430" href="#t430">430</a></span><span class="t"> <span class="str">"set_by"</span><span class="op">:</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"set_by"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t431" href="#t431">431</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t432" href="#t432">432</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t433" href="#t433">433</a></span><span class="t"> <span class="key">if</span> <span class="nam">price</span> <span class="op">&lt;=</span> <span class="nam">target</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t434" href="#t434">434</a></span><span class="t"> <span class="nam">triggered</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">result</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t435" href="#t435">435</a></span><span class="t"> <span class="com"># Update triggered count (only once per day to avoid inflation)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t436" href="#t436">436</a></span><span class="t"> <span class="nam">last_triggered</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"last_triggered"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t437" href="#t437">437</a></span><span class="t"> <span class="nam">today</span> <span class="op">=</span> <span class="nam">now</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t438" href="#t438">438</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">last_triggered</span> <span class="key">or</span> <span class="key">not</span> <span class="nam">last_triggered</span><span class="op">.</span><span class="nam">startswith</span><span class="op">(</span><span class="nam">today</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t439" href="#t439">439</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"triggered_count"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">alert</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"triggered_count"</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">+</span> <span class="num">1</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t440" href="#t440">440</a></span><span class="t"> <span class="nam">alert</span><span class="op">[</span><span class="str">"last_triggered"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">now</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t441" href="#t441">441</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t442" href="#t442">442</a></span><span class="t"> <span class="nam">watching</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">result</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t443" href="#t443">443</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t444" href="#t444">444</a></span><span class="t"> <span class="nam">save_alerts</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t445" href="#t445">445</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="str">"triggered"</span><span class="op">:</span> <span class="nam">triggered</span><span class="op">,</span> <span class="str">"watching"</span><span class="op">:</span> <span class="nam">watching</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t446" href="#t446">446</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t447" href="#t447">447</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t448" href="#t448">448</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t449" href="#t449">449</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">"Price target alerts"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t450" href="#t450">450</a></span><span class="t"> <span class="nam">subparsers</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_subparsers</span><span class="op">(</span><span class="nam">dest</span><span class="op">=</span><span class="str">"command"</span><span class="op">,</span> <span class="nam">required</span><span class="op">=</span><span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t451" href="#t451">451</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t452" href="#t452">452</a></span><span class="t"> <span class="com"># list</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t453" href="#t453">453</a></span><span class="t"> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"list"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"List all alerts"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t454" href="#t454">454</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t455" href="#t455">455</a></span><span class="t"> <span class="com"># set</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t456" href="#t456">456</a></span><span class="t"> <span class="nam">set_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"set"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Set new alert"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t457" href="#t457">457</a></span><span class="t"> <span class="nam">set_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t458" href="#t458">458</a></span><span class="t"> <span class="nam">set_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"target"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Target price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t459" href="#t459">459</a></span><span class="t"> <span class="nam">set_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--note"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Note/reason"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t460" href="#t460">460</a></span><span class="t"> <span class="nam">set_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--user"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Who set the alert"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t461" href="#t461">461</a></span><span class="t"> <span class="nam">set_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--currency"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">"USD"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Currency (USD, EUR, JPY, SGD, MXN)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t462" href="#t462">462</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t463" href="#t463">463</a></span><span class="t"> <span class="com"># delete</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t464" href="#t464">464</a></span><span class="t"> <span class="nam">del_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"delete"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Delete alert"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t465" href="#t465">465</a></span><span class="t"> <span class="nam">del_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t466" href="#t466">466</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t467" href="#t467">467</a></span><span class="t"> <span class="com"># snooze</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t468" href="#t468">468</a></span><span class="t"> <span class="nam">snooze_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"snooze"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Snooze alert"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t469" href="#t469">469</a></span><span class="t"> <span class="nam">snooze_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t470" href="#t470">470</a></span><span class="t"> <span class="nam">snooze_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--days"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">int</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="num">7</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Days to snooze"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t471" href="#t471">471</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t472" href="#t472">472</a></span><span class="t"> <span class="com"># update</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t473" href="#t473">473</a></span><span class="t"> <span class="nam">update_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"update"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Update alert target"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t474" href="#t474">474</a></span><span class="t"> <span class="nam">update_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t475" href="#t475">475</a></span><span class="t"> <span class="nam">update_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"target"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"New target price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t476" href="#t476">476</a></span><span class="t"> <span class="nam">update_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--note"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Update note"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t477" href="#t477">477</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t478" href="#t478">478</a></span><span class="t"> <span class="com"># check</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t479" href="#t479">479</a></span><span class="t"> <span class="nam">check_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"check"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Check alerts against prices"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t480" href="#t480">480</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--json"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"JSON output"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t481" href="#t481">481</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--lang"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">"en"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Output language (en, de)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t482" href="#t482">482</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t483" href="#t483">483</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t484" href="#t484">484</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t485" href="#t485">485</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"list"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t486" href="#t486">486</a></span><span class="t"> <span class="nam">cmd_list</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t487" href="#t487">487</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"set"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t488" href="#t488">488</a></span><span class="t"> <span class="nam">cmd_set</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t489" href="#t489">489</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"delete"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t490" href="#t490">490</a></span><span class="t"> <span class="nam">cmd_delete</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t491" href="#t491">491</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"snooze"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t492" href="#t492">492</a></span><span class="t"> <span class="nam">cmd_snooze</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t493" href="#t493">493</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"update"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t494" href="#t494">494</a></span><span class="t"> <span class="nam">cmd_update</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t495" href="#t495">495</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"check"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t496" href="#t496">496</a></span><span class="t"> <span class="nam">cmd_check</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t497" href="#t497">497</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t498" href="#t498">498</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t499" href="#t499">499</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">"__main__"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t500" href="#t500">500</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="index.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_briefing_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/briefing.py: 56%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;briefing.py</b>:
<span class="pc_cov">56%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">87 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">49<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">38<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_alerts_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_earnings_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Briefing Generator - Main entry point for market briefings.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t"><span class="str">Generates and optionally sends to WhatsApp group.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="key">import</span> <span class="nam">os</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"><span class="key">import</span> <span class="nam">subprocess</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="key">from</span> <span class="nam">utils</span> <span class="key">import</span> <span class="nam">ensure_venv</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="nam">SCRIPT_DIR</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"><span class="nam">ensure_venv</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"><span class="key">def</span> <span class="nam">send_to_whatsapp</span><span class="op">(</span><span class="nam">message</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">group_name</span><span class="op">:</span> <span class="nam">str</span> <span class="op">|</span> <span class="key">None</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"> <span class="str">"""Send message to WhatsApp group via openclaw message tool."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">group_name</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"> <span class="nam">group_name</span> <span class="op">=</span> <span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'FINANCE_NEWS_TARGET'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">group_name</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#10060; No target specified. Set FINANCE_NEWS_TARGET env var or use --group"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"> <span class="key">return</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"> <span class="com"># Use openclaw message tool</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">run</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t"> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"> <span class="str">'openclaw'</span><span class="op">,</span> <span class="str">'message'</span><span class="op">,</span> <span class="str">'send'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"> <span class="str">'--channel'</span><span class="op">,</span> <span class="str">'whatsapp'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="str">'--target'</span><span class="op">,</span> <span class="nam">group_name</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="str">'--message'</span><span class="op">,</span> <span class="nam">message</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="nam">capture_output</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="nam">text</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="nam">timeout</span><span class="op">=</span><span class="num">30</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"> <span class="key">if</span> <span class="nam">result</span><span class="op">.</span><span class="nam">returncode</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Sent to WhatsApp group: {group_name}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; WhatsApp send failed: {result.stderr}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"> <span class="key">return</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; WhatsApp error: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="key">return</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"><span class="key">def</span> <span class="nam">generate_and_send</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="str">"""Generate briefing and optionally send to WhatsApp."""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="com"># Determine briefing type based on current time or args</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">time</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"> <span class="nam">briefing_time</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">time</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="nam">hour</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">hour</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="nam">briefing_time</span> <span class="op">=</span> <span class="str">'morning'</span> <span class="key">if</span> <span class="nam">hour</span> <span class="op">&lt;</span> <span class="num">12</span> <span class="key">else</span> <span class="str">'evening'</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"> <span class="com"># Generate the briefing</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="nam">cmd</span> <span class="op">=</span> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="nam">sys</span><span class="op">.</span><span class="nam">executable</span><span class="op">,</span> <span class="nam">SCRIPT_DIR</span> <span class="op">/</span> <span class="str">'summarize.py'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="str">'--time'</span><span class="op">,</span> <span class="nam">briefing_time</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t"> <span class="str">'--style'</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">style</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t"> <span class="str">'--lang'</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">lang</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"> <span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">deadline</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="nam">cmd</span><span class="op">.</span><span class="nam">extend</span><span class="op">(</span><span class="op">[</span><span class="str">'--deadline'</span><span class="op">,</span> <span class="nam">str</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">deadline</span><span class="op">)</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">fast</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"> <span class="nam">cmd</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">'--fast'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">llm</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="nam">cmd</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">'--llm'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t"> <span class="nam">cmd</span><span class="op">.</span><span class="nam">extend</span><span class="op">(</span><span class="op">[</span><span class="str">'--model'</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">model</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">debug</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="nam">cmd</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">'--debug'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t"> <span class="com"># Always use JSON for internal processing to handle splits</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t"> <span class="nam">cmd</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">'--json'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128202; Generating {briefing_time} briefing..."</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="nam">timeout</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">deadline</span> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">deadline</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span> <span class="key">else</span> <span class="num">300</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"> <span class="nam">timeout</span> <span class="op">=</span> <span class="nam">max</span><span class="op">(</span><span class="num">1</span><span class="op">,</span> <span class="nam">int</span><span class="op">(</span><span class="nam">timeout</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">deadline</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t"> <span class="nam">timeout</span> <span class="op">=</span> <span class="nam">timeout</span> <span class="op">+</span> <span class="num">5</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">run</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"> <span class="nam">cmd</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="nam">capture_output</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="nam">text</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="nam">stdin</span><span class="op">=</span><span class="nam">subprocess</span><span class="op">.</span><span class="nam">DEVNULL</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"> <span class="nam">timeout</span><span class="op">=</span><span class="nam">timeout</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="key">if</span> <span class="nam">result</span><span class="op">.</span><span class="nam">returncode</span> <span class="op">!=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; Briefing generation failed: {result.stderr}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t"> <span class="nam">sys</span><span class="op">.</span><span class="nam">exit</span><span class="op">(</span><span class="num">1</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="key">except</span> <span class="nam">json</span><span class="op">.</span><span class="nam">JSONDecodeError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"> <span class="com"># Fallback if not JSON (shouldn't happen with --json)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Failed to parse briefing JSON"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t"> <span class="com"># Output handling</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">json</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"> <span class="com"># Print for humans</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t"> <span class="key">if</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'macro_message'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">'macro_message'</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t"> <span class="key">if</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'portfolio_message'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n"</span> <span class="op">+</span> <span class="str">"="</span><span class="op">*</span><span class="num">20</span> <span class="op">+</span> <span class="str">"\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">'portfolio_message'</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t"> <span class="com"># Send to WhatsApp if requested</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">send</span> <span class="key">and</span> <span class="nam">args</span><span class="op">.</span><span class="nam">group</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="com"># Message 1: Macro</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"> <span class="nam">macro_msg</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'macro_message'</span><span class="op">)</span> <span class="key">or</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'summary'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t"> <span class="key">if</span> <span class="nam">macro_msg</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="nam">send_to_whatsapp</span><span class="op">(</span><span class="nam">macro_msg</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">group</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"> <span class="com"># Message 2: Portfolio (if exists)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"> <span class="nam">portfolio_msg</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'portfolio_message'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="key">if</span> <span class="nam">portfolio_msg</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"> <span class="nam">send_to_whatsapp</span><span class="op">(</span><span class="nam">portfolio_msg</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">group</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="key">return</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'macro_message'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">'Briefing Generator'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--time'</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">'morning'</span><span class="op">,</span> <span class="str">'evening'</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Briefing type (auto-detected if not specified)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--style'</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">'briefing'</span><span class="op">,</span> <span class="str">'analysis'</span><span class="op">,</span> <span class="str">'headlines'</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="nam">default</span><span class="op">=</span><span class="str">'briefing'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Summary style'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--lang'</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">'en'</span><span class="op">,</span> <span class="str">'de'</span><span class="op">]</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">'en'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Output language'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--send'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Send to WhatsApp group'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--group'</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'FINANCE_NEWS_TARGET'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'WhatsApp group name or JID (default: FINANCE_NEWS_TARGET env var)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--json'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Output as JSON'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--deadline'</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">int</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Overall deadline in seconds'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--llm'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Use LLM summary'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--model'</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">'claude'</span><span class="op">,</span> <span class="str">'minimax'</span><span class="op">,</span> <span class="str">'gemini'</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="nam">default</span><span class="op">=</span><span class="str">'claude'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'LLM model (only with --llm)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--fast'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Use fast mode (shorter timeouts, fewer items)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--debug'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Write debug log with sources'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t"> <span class="nam">generate_and_send</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">'__main__'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_alerts_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_earnings_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,711 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/earnings.py: 45%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;earnings.py</b>:
<span class="pc_cov">45%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">329 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">148<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">181<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_briefing_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_fetch_news_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Earnings Calendar - Track earnings dates for portfolio stocks.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">Features:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="str">- Fetch earnings dates from FMP API</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="str">- Show upcoming earnings in daily briefing</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="str">- Alert 24h before earnings release</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="str">- Cache results to avoid API spam</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="str">Usage:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="str"> earnings.py list # Show all upcoming earnings</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="str"> earnings.py check # Check what's reporting today/this week</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="str"> earnings.py refresh # Force refresh earnings data</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"><span class="key">import</span> <span class="nam">csv</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"><span class="key">import</span> <span class="nam">os</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"><span class="key">import</span> <span class="nam">shutil</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"><span class="key">import</span> <span class="nam">subprocess</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span><span class="op">,</span> <span class="nam">timedelta</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"><span class="key">from</span> <span class="nam">urllib</span><span class="op">.</span><span class="nam">request</span> <span class="key">import</span> <span class="nam">urlopen</span><span class="op">,</span> <span class="nam">Request</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t"><span class="key">from</span> <span class="nam">urllib</span><span class="op">.</span><span class="nam">error</span> <span class="key">import</span> <span class="nam">URLError</span><span class="op">,</span> <span class="nam">HTTPError</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"><span class="com"># Paths</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"><span class="nam">SCRIPT_DIR</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"><span class="nam">CONFIG_DIR</span> <span class="op">=</span> <span class="nam">SCRIPT_DIR</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"config"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t"><span class="nam">CACHE_DIR</span> <span class="op">=</span> <span class="nam">SCRIPT_DIR</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"cache"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"><span class="nam">PORTFOLIO_FILE</span> <span class="op">=</span> <span class="nam">CONFIG_DIR</span> <span class="op">/</span> <span class="str">"portfolio.csv"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"><span class="nam">EARNINGS_CACHE</span> <span class="op">=</span> <span class="nam">CACHE_DIR</span> <span class="op">/</span> <span class="str">"earnings_calendar.json"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"><span class="nam">MANUAL_EARNINGS</span> <span class="op">=</span> <span class="nam">CONFIG_DIR</span> <span class="op">/</span> <span class="str">"manual_earnings.json"</span> <span class="com"># For JP/other stocks not in Finnhub</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"><span class="com"># OpenBB binary path</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"><span class="nam">OPENBB_BINARY</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"><span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="nam">env_path</span> <span class="op">=</span> <span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'OPENBB_QUOTE_BIN'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="key">if</span> <span class="nam">env_path</span> <span class="key">and</span> <span class="nam">os</span><span class="op">.</span><span class="nam">path</span><span class="op">.</span><span class="nam">isfile</span><span class="op">(</span><span class="nam">env_path</span><span class="op">)</span> <span class="key">and</span> <span class="nam">os</span><span class="op">.</span><span class="nam">access</span><span class="op">(</span><span class="nam">env_path</span><span class="op">,</span> <span class="nam">os</span><span class="op">.</span><span class="nam">X_OK</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"> <span class="nam">OPENBB_BINARY</span> <span class="op">=</span> <span class="nam">env_path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"> <span class="nam">OPENBB_BINARY</span> <span class="op">=</span> <span class="nam">shutil</span><span class="op">.</span><span class="nam">which</span><span class="op">(</span><span class="str">'openbb-quote'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"><span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"><span class="com"># API Keys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t"><span class="key">def</span> <span class="nam">get_fmp_key</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="str">"""Get FMP API key from environment or .env file."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="nam">key</span> <span class="op">=</span> <span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"FMP_API_KEY"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">key</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="nam">env_file</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">.</span><span class="nam">home</span><span class="op">(</span><span class="op">)</span> <span class="op">/</span> <span class="str">".openclaw"</span> <span class="op">/</span> <span class="str">".env"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="key">if</span> <span class="nam">env_file</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"> <span class="key">for</span> <span class="nam">line</span> <span class="key">in</span> <span class="nam">env_file</span><span class="op">.</span><span class="nam">read_text</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">splitlines</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="key">if</span> <span class="nam">line</span><span class="op">.</span><span class="nam">startswith</span><span class="op">(</span><span class="str">"FMP_API_KEY="</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"> <span class="nam">key</span> <span class="op">=</span> <span class="nam">line</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="str">"="</span><span class="op">,</span> <span class="num">1</span><span class="op">)</span><span class="op">[</span><span class="num">1</span><span class="op">]</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t"> <span class="key">return</span> <span class="nam">key</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"><span class="key">def</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="str">"""Load portfolio from CSV."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">PORTFOLIO_FILE</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">PORTFOLIO_FILE</span><span class="op">,</span> <span class="str">'r'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="nam">reader</span> <span class="op">=</span> <span class="nam">csv</span><span class="op">.</span><span class="nam">DictReader</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="key">return</span> <span class="nam">list</span><span class="op">(</span><span class="nam">reader</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"><span class="key">def</span> <span class="nam">load_earnings_cache</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="str">"""Load cached earnings data."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="key">if</span> <span class="nam">EARNINGS_CACHE</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t"> <span class="key">return</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">EARNINGS_CACHE</span><span class="op">.</span><span class="nam">read_text</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="str">"last_updated"</span><span class="op">:</span> <span class="key">None</span><span class="op">,</span> <span class="str">"earnings"</span><span class="op">:</span> <span class="op">{</span><span class="op">}</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t"><span class="key">def</span> <span class="nam">load_manual_earnings</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"><span class="str"> Load manually-entered earnings dates (for JP stocks not in Finnhub).</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"><span class="str"> Format: {"6857.T": {"date": "2026-01-30", "time": "amc", "note": "Q3 FY2025"}, ...}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t"> <span class="key">if</span> <span class="nam">MANUAL_EARNINGS</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">MANUAL_EARNINGS</span><span class="op">.</span><span class="nam">read_text</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="com"># Filter out metadata keys (starting with _)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="nam">k</span><span class="op">:</span> <span class="nam">v</span> <span class="key">for</span> <span class="nam">k</span><span class="op">,</span> <span class="nam">v</span> <span class="key">in</span> <span class="nam">data</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span> <span class="key">if</span> <span class="key">not</span> <span class="nam">k</span><span class="op">.</span><span class="nam">startswith</span><span class="op">(</span><span class="str">"_"</span><span class="op">)</span> <span class="key">and</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">v</span><span class="op">,</span> <span class="nam">dict</span><span class="op">)</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"><span class="key">def</span> <span class="nam">save_earnings_cache</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="str">"""Save earnings data to cache."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="nam">CACHE_DIR</span><span class="op">.</span><span class="nam">mkdir</span><span class="op">(</span><span class="nam">exist_ok</span><span class="op">=</span><span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="nam">EARNINGS_CACHE</span><span class="op">.</span><span class="nam">write_text</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="nam">str</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t"><span class="key">def</span> <span class="nam">get_finnhub_key</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="str">"""Get Finnhub API key from environment or .env file."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="nam">key</span> <span class="op">=</span> <span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"FINNHUB_API_KEY"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">key</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t"> <span class="nam">env_file</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">.</span><span class="nam">home</span><span class="op">(</span><span class="op">)</span> <span class="op">/</span> <span class="str">".openclaw"</span> <span class="op">/</span> <span class="str">".env"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"> <span class="key">if</span> <span class="nam">env_file</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="key">for</span> <span class="nam">line</span> <span class="key">in</span> <span class="nam">env_file</span><span class="op">.</span><span class="nam">read_text</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">splitlines</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="key">if</span> <span class="nam">line</span><span class="op">.</span><span class="nam">startswith</span><span class="op">(</span><span class="str">"FINNHUB_API_KEY="</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"> <span class="nam">key</span> <span class="op">=</span> <span class="nam">line</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="str">"="</span><span class="op">,</span> <span class="num">1</span><span class="op">)</span><span class="op">[</span><span class="num">1</span><span class="op">]</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"> <span class="key">return</span> <span class="nam">key</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t"><span class="key">def</span> <span class="nam">fetch_all_earnings_finnhub</span><span class="op">(</span><span class="nam">days_ahead</span><span class="op">:</span> <span class="nam">int</span> <span class="op">=</span> <span class="num">60</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"><span class="str"> Fetch all earnings for the next N days from Finnhub.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"><span class="str"> Returns dict keyed by symbol: {"AAPL": {...}, ...}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t"> <span class="nam">finnhub_key</span> <span class="op">=</span> <span class="nam">get_finnhub_key</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">finnhub_key</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="nam">from_date</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"> <span class="nam">to_date</span> <span class="op">=</span> <span class="op">(</span><span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span> <span class="op">+</span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">days</span><span class="op">=</span><span class="nam">days_ahead</span><span class="op">)</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"> <span class="nam">url</span> <span class="op">=</span> <span class="str">f"https://finnhub.io/api/v1/calendar/earnings?from={from_date}&amp;to={to_date}&amp;token={finnhub_key}"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t"> <span class="nam">req</span> <span class="op">=</span> <span class="nam">Request</span><span class="op">(</span><span class="nam">url</span><span class="op">,</span> <span class="nam">headers</span><span class="op">=</span><span class="op">{</span><span class="str">"User-Agent"</span><span class="op">:</span> <span class="str">"finance-news/1.0"</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="key">with</span> <span class="nam">urlopen</span><span class="op">(</span><span class="nam">req</span><span class="op">,</span> <span class="nam">timeout</span><span class="op">=</span><span class="num">30</span><span class="op">)</span> <span class="key">as</span> <span class="nam">resp</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">resp</span><span class="op">.</span><span class="nam">read</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">decode</span><span class="op">(</span><span class="str">"utf-8"</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"> <span class="nam">earnings_by_symbol</span> <span class="op">=</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="key">for</span> <span class="nam">entry</span> <span class="key">in</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"earningsCalendar"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"> <span class="nam">symbol</span> <span class="op">=</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"symbol"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="key">if</span> <span class="nam">symbol</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="nam">earnings_by_symbol</span><span class="op">[</span><span class="nam">symbol</span><span class="op">]</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"> <span class="str">"date"</span><span class="op">:</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"date"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="str">"time"</span><span class="op">:</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"hour"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span> <span class="com"># bmo/amc</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"> <span class="str">"eps_estimate"</span><span class="op">:</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"epsEstimate"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="str">"revenue_estimate"</span><span class="op">:</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"revenueEstimate"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="str">"quarter"</span><span class="op">:</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"quarter"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="str">"year"</span><span class="op">:</span> <span class="nam">entry</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"year"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="key">return</span> <span class="nam">earnings_by_symbol</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; Finnhub error: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"><span class="key">def</span> <span class="nam">normalize_ticker_for_lookup</span><span class="op">(</span><span class="nam">ticker</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"><span class="str"> Convert portfolio ticker to possible Finnhub symbols.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t"><span class="str"> Returns list of possible formats to try.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="nam">variants</span> <span class="op">=</span> <span class="op">[</span><span class="nam">ticker</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="com"># Japanese stocks: 6857.T -> try 6857</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="key">if</span> <span class="nam">ticker</span><span class="op">.</span><span class="nam">endswith</span><span class="op">(</span><span class="str">'.T'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="nam">base</span> <span class="op">=</span> <span class="nam">ticker</span><span class="op">.</span><span class="nam">replace</span><span class="op">(</span><span class="str">'.T'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t"> <span class="nam">variants</span><span class="op">.</span><span class="nam">extend</span><span class="op">(</span><span class="op">[</span><span class="nam">base</span><span class="op">,</span> <span class="str">f"{base}.T"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t"> <span class="com"># Singapore stocks: D05.SI -> try D05</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t"> <span class="key">elif</span> <span class="nam">ticker</span><span class="op">.</span><span class="nam">endswith</span><span class="op">(</span><span class="str">'.SI'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t"> <span class="nam">base</span> <span class="op">=</span> <span class="nam">ticker</span><span class="op">.</span><span class="nam">replace</span><span class="op">(</span><span class="str">'.SI'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t"> <span class="nam">variants</span><span class="op">.</span><span class="nam">extend</span><span class="op">(</span><span class="op">[</span><span class="nam">base</span><span class="op">,</span> <span class="str">f"{base}.SI"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"> <span class="key">return</span> <span class="nam">variants</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t"><span class="key">def</span> <span class="nam">fetch_earnings_for_portfolio</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t"><span class="str"> Fetch earnings dates for portfolio stocks using Finnhub bulk API.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t"><span class="str"> More efficient than per-ticker calls.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t"> <span class="com"># Get all earnings for next 60 days</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="nam">all_earnings</span> <span class="op">=</span> <span class="nam">fetch_all_earnings_finnhub</span><span class="op">(</span><span class="nam">days_ahead</span><span class="op">=</span><span class="num">60</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">all_earnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t"> <span class="com"># Match portfolio tickers to earnings data</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t"> <span class="nam">results</span> <span class="op">=</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"> <span class="key">for</span> <span class="nam">stock</span> <span class="key">in</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t"> <span class="nam">ticker</span> <span class="op">=</span> <span class="nam">stock</span><span class="op">[</span><span class="str">"symbol"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t"> <span class="nam">variants</span> <span class="op">=</span> <span class="nam">normalize_ticker_for_lookup</span><span class="op">(</span><span class="nam">ticker</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t"> <span class="key">for</span> <span class="nam">variant</span> <span class="key">in</span> <span class="nam">variants</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"> <span class="key">if</span> <span class="nam">variant</span> <span class="key">in</span> <span class="nam">all_earnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t"> <span class="nam">results</span><span class="op">[</span><span class="nam">ticker</span><span class="op">]</span> <span class="op">=</span> <span class="nam">all_earnings</span><span class="op">[</span><span class="nam">variant</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t"> <span class="key">return</span> <span class="nam">results</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t"><span class="key">def</span> <span class="nam">refresh_earnings</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">,</span> <span class="nam">force</span><span class="op">:</span> <span class="nam">bool</span> <span class="op">=</span> <span class="key">False</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t"> <span class="str">"""Refresh earnings data for all portfolio stocks."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t"> <span class="nam">finnhub_key</span> <span class="op">=</span> <span class="nam">get_finnhub_key</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">finnhub_key</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#10060; FINNHUB_API_KEY not found"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="nam">load_earnings_cache</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"> <span class="com"># Check if cache is fresh (&lt; 6 hours old)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">force</span> <span class="key">and</span> <span class="nam">cache</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"last_updated"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t"> <span class="nam">last</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">cache</span><span class="op">[</span><span class="str">"last_updated"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t"> <span class="key">if</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span> <span class="op">-</span> <span class="nam">last</span> <span class="op">&lt;</span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">hours</span><span class="op">=</span><span class="num">6</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128230; Using cached data (updated {last.strftime('%H:%M')})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t"> <span class="key">return</span> <span class="nam">cache</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128260; Fetching earnings calendar from Finnhub..."</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t"> <span class="com"># Use bulk fetch - much more efficient</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t"> <span class="nam">earnings</span> <span class="op">=</span> <span class="nam">fetch_earnings_for_portfolio</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t"> <span class="com"># Merge manual earnings (for JP stocks not in Finnhub)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t"> <span class="nam">manual</span> <span class="op">=</span> <span class="nam">load_manual_earnings</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t"> <span class="key">if</span> <span class="nam">manual</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128221; Merging {len(manual)} manual entries..."</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t"> <span class="key">for</span> <span class="nam">ticker</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">manual</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t"> <span class="key">if</span> <span class="nam">ticker</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">earnings</span><span class="op">:</span> <span class="com"># Manual data fills gaps</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="nam">earnings</span><span class="op">[</span><span class="nam">ticker</span><span class="op">]</span> <span class="op">=</span> <span class="nam">data</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t"> <span class="nam">found</span> <span class="op">=</span> <span class="nam">len</span><span class="op">(</span><span class="nam">earnings</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t"> <span class="nam">total</span> <span class="op">=</span> <span class="nam">len</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Found earnings data for {found}/{total} stocks"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t"> <span class="key">if</span> <span class="nam">earnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t"> <span class="key">for</span> <span class="nam">ticker</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">earnings</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">[</span><span class="num">1</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"date"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#8226; {ticker}: {data.get('date', '?')}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t"> <span class="str">"last_updated"</span><span class="op">:</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t"> <span class="str">"earnings"</span><span class="op">:</span> <span class="nam">earnings</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t"> <span class="nam">save_earnings_cache</span><span class="op">(</span><span class="nam">cache</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t"> <span class="key">return</span> <span class="nam">cache</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t"><span class="key">def</span> <span class="nam">list_earnings</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t"> <span class="str">"""List all upcoming earnings for portfolio."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128194; Portfolio empty"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="nam">refresh_earnings</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">,</span> <span class="nam">force</span><span class="op">=</span><span class="nam">args</span><span class="op">.</span><span class="nam">refresh</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t"> <span class="nam">earnings</span> <span class="op">=</span> <span class="nam">cache</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"earnings"</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">earnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#10060; No earnings dates found"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"> <span class="com"># Sort by date</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t"> <span class="nam">sorted_earnings</span> <span class="op">=</span> <span class="nam">sorted</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t"> <span class="op">[</span><span class="op">(</span><span class="nam">ticker</span><span class="op">,</span> <span class="nam">data</span><span class="op">)</span> <span class="key">for</span> <span class="nam">ticker</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">earnings</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span> <span class="key">if</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"date"</span><span class="op">)</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t"> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">[</span><span class="num">1</span><span class="op">]</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"\n&#128197; Upcoming Earnings ({len(sorted_earnings)} stocks)\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t"> <span class="nam">today</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">date</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"> <span class="key">for</span> <span class="nam">ticker</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">sorted_earnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t"> <span class="nam">date_str</span> <span class="op">=</span> <span class="nam">data</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t"> <span class="nam">ed</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">strptime</span><span class="op">(</span><span class="nam">date_str</span><span class="op">,</span> <span class="str">"%Y-%m-%d"</span><span class="op">)</span><span class="op">.</span><span class="nam">date</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t"> <span class="nam">days_until</span> <span class="op">=</span> <span class="op">(</span><span class="nam">ed</span> <span class="op">-</span> <span class="nam">today</span><span class="op">)</span><span class="op">.</span><span class="nam">days</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"> <span class="com"># Emoji based on timing</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t"> <span class="key">if</span> <span class="nam">days_until</span> <span class="op">&lt;</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t"> <span class="nam">emoji</span> <span class="op">=</span> <span class="str">"&#9989;"</span> <span class="com"># Past</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t"> <span class="nam">timing</span> <span class="op">=</span> <span class="str">f"{-days_until}d ago"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t"> <span class="key">elif</span> <span class="nam">days_until</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t"> <span class="nam">emoji</span> <span class="op">=</span> <span class="str">"&#128308;"</span> <span class="com"># Today!</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t"> <span class="nam">timing</span> <span class="op">=</span> <span class="str">"TODAY"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t"> <span class="key">elif</span> <span class="nam">days_until</span> <span class="op">==</span> <span class="num">1</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t284" href="#t284">284</a></span><span class="t"> <span class="nam">emoji</span> <span class="op">=</span> <span class="str">"&#128993;"</span> <span class="com"># Tomorrow</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t285" href="#t285">285</a></span><span class="t"> <span class="nam">timing</span> <span class="op">=</span> <span class="str">"TOMORROW"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t286" href="#t286">286</a></span><span class="t"> <span class="key">elif</span> <span class="nam">days_until</span> <span class="op">&lt;=</span> <span class="num">7</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t287" href="#t287">287</a></span><span class="t"> <span class="nam">emoji</span> <span class="op">=</span> <span class="str">"&#128992;"</span> <span class="com"># This week</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t288" href="#t288">288</a></span><span class="t"> <span class="nam">timing</span> <span class="op">=</span> <span class="str">f"in {days_until}d"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t289" href="#t289">289</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t290" href="#t290">290</a></span><span class="t"> <span class="nam">emoji</span> <span class="op">=</span> <span class="str">"&#9898;"</span> <span class="com"># Later</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t291" href="#t291">291</a></span><span class="t"> <span class="nam">timing</span> <span class="op">=</span> <span class="str">f"in {days_until}d"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t292" href="#t292">292</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t293" href="#t293">293</a></span><span class="t"> <span class="com"># Time of day</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t294" href="#t294">294</a></span><span class="t"> <span class="nam">time_str</span> <span class="op">=</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t295" href="#t295">295</a></span><span class="t"> <span class="key">if</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"time"</span><span class="op">)</span> <span class="op">==</span> <span class="str">"bmo"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t296" href="#t296">296</a></span><span class="t"> <span class="nam">time_str</span> <span class="op">=</span> <span class="str">" (pre-market)"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t297" href="#t297">297</a></span><span class="t"> <span class="key">elif</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"time"</span><span class="op">)</span> <span class="op">==</span> <span class="str">"amc"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t298" href="#t298">298</a></span><span class="t"> <span class="nam">time_str</span> <span class="op">=</span> <span class="str">" (after-close)"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t299" href="#t299">299</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t300" href="#t300">300</a></span><span class="t"> <span class="com"># EPS estimate</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t301" href="#t301">301</a></span><span class="t"> <span class="nam">eps_str</span> <span class="op">=</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t302" href="#t302">302</a></span><span class="t"> <span class="key">if</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"eps_estimate"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t303" href="#t303">303</a></span><span class="t"> <span class="nam">eps_str</span> <span class="op">=</span> <span class="str">f" | Est: ${data['eps_estimate']:.2f}"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t304" href="#t304">304</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t305" href="#t305">305</a></span><span class="t"> <span class="com"># Stock name from portfolio</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t306" href="#t306">306</a></span><span class="t"> <span class="nam">stock_name</span> <span class="op">=</span> <span class="nam">next</span><span class="op">(</span><span class="op">(</span><span class="nam">s</span><span class="op">[</span><span class="str">"name"</span><span class="op">]</span> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">portfolio</span> <span class="key">if</span> <span class="nam">s</span><span class="op">[</span><span class="str">"symbol"</span><span class="op">]</span> <span class="op">==</span> <span class="nam">ticker</span><span class="op">)</span><span class="op">,</span> <span class="nam">ticker</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t307" href="#t307">307</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t308" href="#t308">308</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"{emoji} {date_str} ({timing}): **{ticker}** &#8212; {stock_name}{time_str}{eps_str}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t309" href="#t309">309</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t310" href="#t310">310</a></span><span class="t"> <span class="key">except</span> <span class="nam">ValueError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t311" href="#t311">311</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9898; {date_str}: {ticker}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t312" href="#t312">312</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t313" href="#t313">313</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t314" href="#t314">314</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t315" href="#t315">315</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t316" href="#t316">316</a></span><span class="t"><span class="key">def</span> <span class="nam">check_earnings</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t317" href="#t317">317</a></span><span class="t"> <span class="str">"""Check earnings for today and this week (briefing format)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t318" href="#t318">318</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t319" href="#t319">319</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t320" href="#t320">320</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t321" href="#t321">321</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t322" href="#t322">322</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="nam">load_earnings_cache</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t323" href="#t323">323</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t324" href="#t324">324</a></span><span class="t"> <span class="com"># Auto-refresh if cache is stale</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t325" href="#t325">325</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">cache</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"last_updated"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t326" href="#t326">326</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="nam">refresh_earnings</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">,</span> <span class="nam">force</span><span class="op">=</span><span class="key">False</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t327" href="#t327">327</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t328" href="#t328">328</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t329" href="#t329">329</a></span><span class="t"> <span class="nam">last</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">cache</span><span class="op">[</span><span class="str">"last_updated"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t330" href="#t330">330</a></span><span class="t"> <span class="key">if</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span> <span class="op">-</span> <span class="nam">last</span> <span class="op">></span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">hours</span><span class="op">=</span><span class="num">12</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t331" href="#t331">331</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="nam">refresh_earnings</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">,</span> <span class="nam">force</span><span class="op">=</span><span class="key">False</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t332" href="#t332">332</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t333" href="#t333">333</a></span><span class="t"> <span class="nam">cache</span> <span class="op">=</span> <span class="nam">refresh_earnings</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">,</span> <span class="nam">force</span><span class="op">=</span><span class="key">False</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t334" href="#t334">334</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t335" href="#t335">335</a></span><span class="t"> <span class="nam">earnings</span> <span class="op">=</span> <span class="nam">cache</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"earnings"</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t336" href="#t336">336</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">earnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t337" href="#t337">337</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t338" href="#t338">338</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t339" href="#t339">339</a></span><span class="t"> <span class="nam">today</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">date</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t340" href="#t340">340</a></span><span class="t"> <span class="nam">week_only</span> <span class="op">=</span> <span class="nam">getattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'week'</span><span class="op">,</span> <span class="key">False</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t341" href="#t341">341</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t342" href="#t342">342</a></span><span class="t"> <span class="com"># For weekly mode (Sunday cron), show Mon-Fri of upcoming week</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t343" href="#t343">343</a></span><span class="t"> <span class="com"># Calculation: weekday() returns 0=Mon, 6=Sun. (7 - weekday) % 7 gives days until next Monday.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t344" href="#t344">344</a></span><span class="t"> <span class="com"># Special case: if today is Monday (result=0), we want next Monday (7 days), not today.</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t345" href="#t345">345</a></span><span class="t"> <span class="key">if</span> <span class="nam">week_only</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t346" href="#t346">346</a></span><span class="t"> <span class="nam">days_until_monday</span> <span class="op">=</span> <span class="op">(</span><span class="num">7</span> <span class="op">-</span> <span class="nam">today</span><span class="op">.</span><span class="nam">weekday</span><span class="op">(</span><span class="op">)</span><span class="op">)</span> <span class="op">%</span> <span class="num">7</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t347" href="#t347">347</a></span><span class="t"> <span class="key">if</span> <span class="nam">days_until_monday</span> <span class="op">==</span> <span class="num">0</span> <span class="key">and</span> <span class="nam">today</span><span class="op">.</span><span class="nam">weekday</span><span class="op">(</span><span class="op">)</span> <span class="op">!=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t348" href="#t348">348</a></span><span class="t"> <span class="nam">days_until_monday</span> <span class="op">=</span> <span class="num">7</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t349" href="#t349">349</a></span><span class="t"> <span class="nam">week_start</span> <span class="op">=</span> <span class="nam">today</span> <span class="op">+</span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">days</span><span class="op">=</span><span class="nam">days_until_monday</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t350" href="#t350">350</a></span><span class="t"> <span class="nam">week_end</span> <span class="op">=</span> <span class="nam">week_start</span> <span class="op">+</span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">days</span><span class="op">=</span><span class="num">4</span><span class="op">)</span> <span class="com"># Mon-Fri</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t351" href="#t351">351</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t352" href="#t352">352</a></span><span class="t"> <span class="nam">week_end</span> <span class="op">=</span> <span class="nam">today</span> <span class="op">+</span> <span class="nam">timedelta</span><span class="op">(</span><span class="nam">days</span><span class="op">=</span><span class="num">7</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t353" href="#t353">353</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t354" href="#t354">354</a></span><span class="t"> <span class="nam">today_list</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t355" href="#t355">355</a></span><span class="t"> <span class="nam">week_list</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t356" href="#t356">356</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t357" href="#t357">357</a></span><span class="t"> <span class="key">for</span> <span class="nam">ticker</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">earnings</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t358" href="#t358">358</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"date"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t359" href="#t359">359</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t360" href="#t360">360</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t361" href="#t361">361</a></span><span class="t"> <span class="nam">ed</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">strptime</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">,</span> <span class="str">"%Y-%m-%d"</span><span class="op">)</span><span class="op">.</span><span class="nam">date</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t362" href="#t362">362</a></span><span class="t"> <span class="nam">stock</span> <span class="op">=</span> <span class="nam">next</span><span class="op">(</span><span class="op">(</span><span class="nam">s</span> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">portfolio</span> <span class="key">if</span> <span class="nam">s</span><span class="op">[</span><span class="str">"symbol"</span><span class="op">]</span> <span class="op">==</span> <span class="nam">ticker</span><span class="op">)</span><span class="op">,</span> <span class="key">None</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t363" href="#t363">363</a></span><span class="t"> <span class="nam">name</span> <span class="op">=</span> <span class="nam">stock</span><span class="op">[</span><span class="str">"name"</span><span class="op">]</span> <span class="key">if</span> <span class="nam">stock</span> <span class="key">else</span> <span class="nam">ticker</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t364" href="#t364">364</a></span><span class="t"> <span class="nam">category</span> <span class="op">=</span> <span class="nam">stock</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"category"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span> <span class="key">if</span> <span class="nam">stock</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t365" href="#t365">365</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t366" href="#t366">366</a></span><span class="t"> <span class="nam">entry</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t367" href="#t367">367</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t368" href="#t368">368</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">name</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t369" href="#t369">369</a></span><span class="t"> <span class="str">"date"</span><span class="op">:</span> <span class="nam">ed</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t370" href="#t370">370</a></span><span class="t"> <span class="str">"time"</span><span class="op">:</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"time"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t371" href="#t371">371</a></span><span class="t"> <span class="str">"eps_estimate"</span><span class="op">:</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"eps_estimate"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t372" href="#t372">372</a></span><span class="t"> <span class="str">"category"</span><span class="op">:</span> <span class="nam">category</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t373" href="#t373">373</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t374" href="#t374">374</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t375" href="#t375">375</a></span><span class="t"> <span class="key">if</span> <span class="nam">week_only</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t376" href="#t376">376</a></span><span class="t"> <span class="com"># Weekly mode: only show week range</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t377" href="#t377">377</a></span><span class="t"> <span class="key">if</span> <span class="nam">week_start</span> <span class="op">&lt;=</span> <span class="nam">ed</span> <span class="op">&lt;=</span> <span class="nam">week_end</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t378" href="#t378">378</a></span><span class="t"> <span class="nam">week_list</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">entry</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t379" href="#t379">379</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t380" href="#t380">380</a></span><span class="t"> <span class="com"># Daily mode: today + this week</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t381" href="#t381">381</a></span><span class="t"> <span class="key">if</span> <span class="nam">ed</span> <span class="op">==</span> <span class="nam">today</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t382" href="#t382">382</a></span><span class="t"> <span class="nam">today_list</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">entry</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t383" href="#t383">383</a></span><span class="t"> <span class="key">elif</span> <span class="nam">today</span> <span class="op">&lt;</span> <span class="nam">ed</span> <span class="op">&lt;=</span> <span class="nam">week_end</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t384" href="#t384">384</a></span><span class="t"> <span class="nam">week_list</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">entry</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t385" href="#t385">385</a></span><span class="t"> <span class="key">except</span> <span class="nam">ValueError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t386" href="#t386">386</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t387" href="#t387">387</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t388" href="#t388">388</a></span><span class="t"> <span class="com"># Handle JSON output</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t389" href="#t389">389</a></span><span class="t"> <span class="key">if</span> <span class="nam">getattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'json'</span><span class="op">,</span> <span class="key">False</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t390" href="#t390">390</a></span><span class="t"> <span class="key">if</span> <span class="nam">week_only</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t391" href="#t391">391</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t392" href="#t392">392</a></span><span class="t"> <span class="str">"week_start"</span><span class="op">:</span> <span class="nam">week_start</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t393" href="#t393">393</a></span><span class="t"> <span class="str">"week_end"</span><span class="op">:</span> <span class="nam">week_end</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t394" href="#t394">394</a></span><span class="t"> <span class="str">"earnings"</span><span class="op">:</span> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t395" href="#t395">395</a></span><span class="t"> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t396" href="#t396">396</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t397" href="#t397">397</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"name"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t398" href="#t398">398</a></span><span class="t"> <span class="str">"date"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t399" href="#t399">399</a></span><span class="t"> <span class="str">"time"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t400" href="#t400">400</a></span><span class="t"> <span class="str">"eps_estimate"</span><span class="op">:</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"eps_estimate"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t401" href="#t401">401</a></span><span class="t"> <span class="str">"category"</span><span class="op">:</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"category"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t402" href="#t402">402</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t403" href="#t403">403</a></span><span class="t"> <span class="key">for</span> <span class="nam">e</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">week_list</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t404" href="#t404">404</a></span><span class="t"> <span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t405" href="#t405">405</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t406" href="#t406">406</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t407" href="#t407">407</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t408" href="#t408">408</a></span><span class="t"> <span class="str">"today"</span><span class="op">:</span> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t409" href="#t409">409</a></span><span class="t"> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t410" href="#t410">410</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t411" href="#t411">411</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"name"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t412" href="#t412">412</a></span><span class="t"> <span class="str">"date"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t413" href="#t413">413</a></span><span class="t"> <span class="str">"time"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t414" href="#t414">414</a></span><span class="t"> <span class="str">"eps_estimate"</span><span class="op">:</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"eps_estimate"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t415" href="#t415">415</a></span><span class="t"> <span class="str">"category"</span><span class="op">:</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"category"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t416" href="#t416">416</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t417" href="#t417">417</a></span><span class="t"> <span class="key">for</span> <span class="nam">e</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">today_list</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"time"</span><span class="op">,</span> <span class="str">"zzz"</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t418" href="#t418">418</a></span><span class="t"> <span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t419" href="#t419">419</a></span><span class="t"> <span class="str">"this_week"</span><span class="op">:</span> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t420" href="#t420">420</a></span><span class="t"> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t421" href="#t421">421</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"ticker"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t422" href="#t422">422</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"name"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t423" href="#t423">423</a></span><span class="t"> <span class="str">"date"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t424" href="#t424">424</a></span><span class="t"> <span class="str">"time"</span><span class="op">:</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t425" href="#t425">425</a></span><span class="t"> <span class="str">"eps_estimate"</span><span class="op">:</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"eps_estimate"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t426" href="#t426">426</a></span><span class="t"> <span class="str">"category"</span><span class="op">:</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"category"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t427" href="#t427">427</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t428" href="#t428">428</a></span><span class="t"> <span class="key">for</span> <span class="nam">e</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">week_list</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t429" href="#t429">429</a></span><span class="t"> <span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t430" href="#t430">430</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t431" href="#t431">431</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">result</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t432" href="#t432">432</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t433" href="#t433">433</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t434" href="#t434">434</a></span><span class="t"> <span class="com"># Translations</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t435" href="#t435">435</a></span><span class="t"> <span class="nam">lang</span> <span class="op">=</span> <span class="nam">getattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'lang'</span><span class="op">,</span> <span class="str">'en'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t436" href="#t436">436</a></span><span class="t"> <span class="key">if</span> <span class="nam">lang</span> <span class="op">==</span> <span class="str">"de"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t437" href="#t437">437</a></span><span class="t"> <span class="nam">labels</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t438" href="#t438">438</a></span><span class="t"> <span class="str">"today"</span><span class="op">:</span> <span class="str">"EARNINGS HEUTE"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t439" href="#t439">439</a></span><span class="t"> <span class="str">"week"</span><span class="op">:</span> <span class="str">"EARNINGS DIESE WOCHE"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t440" href="#t440">440</a></span><span class="t"> <span class="str">"week_preview"</span><span class="op">:</span> <span class="str">"EARNINGS N&#196;CHSTE WOCHE"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t441" href="#t441">441</a></span><span class="t"> <span class="str">"pre"</span><span class="op">:</span> <span class="str">"vor B&#246;rsener&#246;ffnung"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t442" href="#t442">442</a></span><span class="t"> <span class="str">"post"</span><span class="op">:</span> <span class="str">"nach B&#246;rsenschluss"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t443" href="#t443">443</a></span><span class="t"> <span class="str">"pre_short"</span><span class="op">:</span> <span class="str">"vor"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t444" href="#t444">444</a></span><span class="t"> <span class="str">"post_short"</span><span class="op">:</span> <span class="str">"nach"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t445" href="#t445">445</a></span><span class="t"> <span class="str">"est"</span><span class="op">:</span> <span class="str">"Erw"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t446" href="#t446">446</a></span><span class="t"> <span class="str">"none"</span><span class="op">:</span> <span class="str">"Keine Earnings diese Woche"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t447" href="#t447">447</a></span><span class="t"> <span class="str">"none_week"</span><span class="op">:</span> <span class="str">"Keine Earnings n&#228;chste Woche"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t448" href="#t448">448</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t449" href="#t449">449</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t450" href="#t450">450</a></span><span class="t"> <span class="nam">labels</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t451" href="#t451">451</a></span><span class="t"> <span class="str">"today"</span><span class="op">:</span> <span class="str">"EARNINGS TODAY"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t452" href="#t452">452</a></span><span class="t"> <span class="str">"week"</span><span class="op">:</span> <span class="str">"EARNINGS THIS WEEK"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t453" href="#t453">453</a></span><span class="t"> <span class="str">"week_preview"</span><span class="op">:</span> <span class="str">"EARNINGS NEXT WEEK"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t454" href="#t454">454</a></span><span class="t"> <span class="str">"pre"</span><span class="op">:</span> <span class="str">"pre-market"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t455" href="#t455">455</a></span><span class="t"> <span class="str">"post"</span><span class="op">:</span> <span class="str">"after-close"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t456" href="#t456">456</a></span><span class="t"> <span class="str">"pre_short"</span><span class="op">:</span> <span class="str">"pre"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t457" href="#t457">457</a></span><span class="t"> <span class="str">"post_short"</span><span class="op">:</span> <span class="str">"post"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t458" href="#t458">458</a></span><span class="t"> <span class="str">"est"</span><span class="op">:</span> <span class="str">"Est"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t459" href="#t459">459</a></span><span class="t"> <span class="str">"none"</span><span class="op">:</span> <span class="str">"No earnings this week"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t460" href="#t460">460</a></span><span class="t"> <span class="str">"none_week"</span><span class="op">:</span> <span class="str">"No earnings next week"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t461" href="#t461">461</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t462" href="#t462">462</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t463" href="#t463">463</a></span><span class="t"> <span class="com"># Date header</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t464" href="#t464">464</a></span><span class="t"> <span class="nam">date_str</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%b %d, %Y"</span><span class="op">)</span> <span class="key">if</span> <span class="nam">lang</span> <span class="op">==</span> <span class="str">"en"</span> <span class="key">else</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%d. %b %Y"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t465" href="#t465">465</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t466" href="#t466">466</a></span><span class="t"> <span class="com"># Output for briefing</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t467" href="#t467">467</a></span><span class="t"> <span class="nam">output</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t468" href="#t468">468</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t469" href="#t469">469</a></span><span class="t"> <span class="com"># Daily mode: show today's earnings</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t470" href="#t470">470</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">week_only</span> <span class="key">and</span> <span class="nam">today_list</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t471" href="#t471">471</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"&#128197; {labels['today']} &#8212; {date_str}\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t472" href="#t472">472</a></span><span class="t"> <span class="key">for</span> <span class="nam">e</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">today_list</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"time"</span><span class="op">,</span> <span class="str">"zzz"</span><span class="op">)</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t473" href="#t473">473</a></span><span class="t"> <span class="nam">time_str</span> <span class="op">=</span> <span class="str">f" ({labels['pre']})"</span> <span class="key">if</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span> <span class="op">==</span> <span class="str">"bmo"</span> <span class="key">else</span> <span class="str">f" ({labels['post']})"</span> <span class="key">if</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span> <span class="op">==</span> <span class="str">"amc"</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t474" href="#t474">474</a></span><span class="t"> <span class="nam">eps_str</span> <span class="op">=</span> <span class="str">f" &#8212; {labels['est']}: ${e['eps_estimate']:.2f}"</span> <span class="key">if</span> <span class="nam">e</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"eps_estimate"</span><span class="op">)</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t475" href="#t475">475</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"&#8226; {e['ticker']} &#8212; {e['name']}{time_str}{eps_str}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t476" href="#t476">476</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t477" href="#t477">477</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t478" href="#t478">478</a></span><span class="t"> <span class="key">if</span> <span class="nam">week_list</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t479" href="#t479">479</a></span><span class="t"> <span class="com"># Use different header for weekly preview mode</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t480" href="#t480">480</a></span><span class="t"> <span class="nam">week_label</span> <span class="op">=</span> <span class="nam">labels</span><span class="op">[</span><span class="str">'week_preview'</span><span class="op">]</span> <span class="key">if</span> <span class="nam">week_only</span> <span class="key">else</span> <span class="nam">labels</span><span class="op">[</span><span class="str">'week'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t481" href="#t481">481</a></span><span class="t"> <span class="key">if</span> <span class="nam">week_only</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t482" href="#t482">482</a></span><span class="t"> <span class="com"># Show date range for weekly preview</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t483" href="#t483">483</a></span><span class="t"> <span class="nam">week_range</span> <span class="op">=</span> <span class="str">f"{week_start.strftime('%b %d')} - {week_end.strftime('%b %d')}"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t484" href="#t484">484</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"&#128197; {week_label} ({week_range})\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t485" href="#t485">485</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t486" href="#t486">486</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"&#128197; {week_label}\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t487" href="#t487">487</a></span><span class="t"> <span class="key">for</span> <span class="nam">e</span> <span class="key">in</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">week_list</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t488" href="#t488">488</a></span><span class="t"> <span class="nam">day_name</span> <span class="op">=</span> <span class="nam">e</span><span class="op">[</span><span class="str">"date"</span><span class="op">]</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%a %d.%m"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t489" href="#t489">489</a></span><span class="t"> <span class="nam">time_str</span> <span class="op">=</span> <span class="str">f" ({labels['pre_short']})"</span> <span class="key">if</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span> <span class="op">==</span> <span class="str">"bmo"</span> <span class="key">else</span> <span class="str">f" ({labels['post_short']})"</span> <span class="key">if</span> <span class="nam">e</span><span class="op">[</span><span class="str">"time"</span><span class="op">]</span> <span class="op">==</span> <span class="str">"amc"</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t490" href="#t490">490</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"&#8226; {day_name}: {e['ticker']} &#8212; {e['name']}{time_str}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t491" href="#t491">491</a></span><span class="t"> <span class="nam">output</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t492" href="#t492">492</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t493" href="#t493">493</a></span><span class="t"> <span class="key">if</span> <span class="nam">output</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t494" href="#t494">494</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n"</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">output</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t495" href="#t495">495</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t496" href="#t496">496</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">verbose</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t497" href="#t497">497</a></span><span class="t"> <span class="nam">no_earnings_label</span> <span class="op">=</span> <span class="nam">labels</span><span class="op">[</span><span class="str">'none_week'</span><span class="op">]</span> <span class="key">if</span> <span class="nam">week_only</span> <span class="key">else</span> <span class="nam">labels</span><span class="op">[</span><span class="str">'none'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t498" href="#t498">498</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128197; {no_earnings_label}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t499" href="#t499">499</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t500" href="#t500">500</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t501" href="#t501">501</a></span><span class="t"><span class="key">def</span> <span class="nam">get_briefing_section</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t502" href="#t502">502</a></span><span class="t"> <span class="str">"""Get earnings section for daily briefing (called by briefing.py)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t503" href="#t503">503</a></span><span class="t"> <span class="key">from</span> <span class="nam">io</span> <span class="key">import</span> <span class="nam">StringIO</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t504" href="#t504">504</a></span><span class="t"> <span class="key">import</span> <span class="nam">contextlib</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t505" href="#t505">505</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t506" href="#t506">506</a></span><span class="t"> <span class="com"># Capture check output</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t507" href="#t507">507</a></span><span class="t"> <span class="key">class</span> <span class="nam">Args</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t508" href="#t508">508</a></span><span class="t"> <span class="nam">verbose</span> <span class="op">=</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t509" href="#t509">509</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t510" href="#t510">510</a></span><span class="t"> <span class="nam">f</span> <span class="op">=</span> <span class="nam">StringIO</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t511" href="#t511">511</a></span><span class="t"> <span class="key">with</span> <span class="nam">contextlib</span><span class="op">.</span><span class="nam">redirect_stdout</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t512" href="#t512">512</a></span><span class="t"> <span class="nam">check_earnings</span><span class="op">(</span><span class="nam">Args</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t513" href="#t513">513</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t514" href="#t514">514</a></span><span class="t"> <span class="key">return</span> <span class="nam">f</span><span class="op">.</span><span class="nam">getvalue</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t515" href="#t515">515</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t516" href="#t516">516</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t517" href="#t517">517</a></span><span class="t"><span class="key">def</span> <span class="nam">get_earnings_context</span><span class="op">(</span><span class="nam">symbols</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t518" href="#t518">518</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t519" href="#t519">519</a></span><span class="t"><span class="str"> Get recent earnings data (beats/misses) for symbols using OpenBB.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t520" href="#t520">520</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t521" href="#t521">521</a></span><span class="t"><span class="str"> Returns list of dicts with: symbol, eps_actual, eps_estimate, surprise, revenue_actual, revenue_estimate</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t522" href="#t522">522</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t523" href="#t523">523</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">OPENBB_BINARY</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t524" href="#t524">524</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t525" href="#t525">525</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t526" href="#t526">526</a></span><span class="t"> <span class="nam">results</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t527" href="#t527">527</a></span><span class="t"> <span class="key">for</span> <span class="nam">symbol</span> <span class="key">in</span> <span class="nam">symbols</span><span class="op">[</span><span class="op">:</span><span class="num">10</span><span class="op">]</span><span class="op">:</span> <span class="com"># Limit to 10 symbols</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t528" href="#t528">528</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t529" href="#t529">529</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">run</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t530" href="#t530">530</a></span><span class="t"> <span class="op">[</span><span class="nam">OPENBB_BINARY</span><span class="op">,</span> <span class="nam">symbol</span><span class="op">,</span> <span class="str">'--earnings'</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t531" href="#t531">531</a></span><span class="t"> <span class="nam">capture_output</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t532" href="#t532">532</a></span><span class="t"> <span class="nam">text</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t533" href="#t533">533</a></span><span class="t"> <span class="nam">timeout</span><span class="op">=</span><span class="num">30</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t534" href="#t534">534</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t535" href="#t535">535</a></span><span class="t"> <span class="key">if</span> <span class="nam">result</span><span class="op">.</span><span class="nam">returncode</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t536" href="#t536">536</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t537" href="#t537">537</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t538" href="#t538">538</a></span><span class="t"> <span class="key">if</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">list</span><span class="op">)</span> <span class="key">and</span> <span class="nam">data</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t539" href="#t539">539</a></span><span class="t"> <span class="nam">results</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t540" href="#t540">540</a></span><span class="t"> <span class="str">'symbol'</span><span class="op">:</span> <span class="nam">symbol</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t541" href="#t541">541</a></span><span class="t"> <span class="str">'earnings'</span><span class="op">:</span> <span class="nam">data</span><span class="op">[</span><span class="num">0</span><span class="op">]</span> <span class="key">if</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="num">0</span><span class="op">]</span><span class="op">,</span> <span class="nam">dict</span><span class="op">)</span> <span class="key">else</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t542" href="#t542">542</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t543" href="#t543">543</a></span><span class="t"> <span class="key">except</span> <span class="nam">json</span><span class="op">.</span><span class="nam">JSONDecodeError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t544" href="#t544">544</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t545" href="#t545">545</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t546" href="#t546">546</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t547" href="#t547">547</a></span><span class="t"> <span class="key">return</span> <span class="nam">results</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t548" href="#t548">548</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t549" href="#t549">549</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t550" href="#t550">550</a></span><span class="t"><span class="key">def</span> <span class="nam">get_analyst_ratings</span><span class="op">(</span><span class="nam">symbols</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t551" href="#t551">551</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t552" href="#t552">552</a></span><span class="t"><span class="str"> Get analyst upgrades/downgrades for symbols using OpenBB.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t553" href="#t553">553</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t554" href="#t554">554</a></span><span class="t"><span class="str"> Returns list of dicts with: symbol, rating, target_price, firm, direction</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t555" href="#t555">555</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t556" href="#t556">556</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">OPENBB_BINARY</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t557" href="#t557">557</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t558" href="#t558">558</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t559" href="#t559">559</a></span><span class="t"> <span class="nam">results</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t560" href="#t560">560</a></span><span class="t"> <span class="key">for</span> <span class="nam">symbol</span> <span class="key">in</span> <span class="nam">symbols</span><span class="op">[</span><span class="op">:</span><span class="num">10</span><span class="op">]</span><span class="op">:</span> <span class="com"># Limit to 10 symbols</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t561" href="#t561">561</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t562" href="#t562">562</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">run</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t563" href="#t563">563</a></span><span class="t"> <span class="op">[</span><span class="nam">OPENBB_BINARY</span><span class="op">,</span> <span class="nam">symbol</span><span class="op">,</span> <span class="str">'--rating'</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t564" href="#t564">564</a></span><span class="t"> <span class="nam">capture_output</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t565" href="#t565">565</a></span><span class="t"> <span class="nam">text</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t566" href="#t566">566</a></span><span class="t"> <span class="nam">timeout</span><span class="op">=</span><span class="num">30</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t567" href="#t567">567</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t568" href="#t568">568</a></span><span class="t"> <span class="key">if</span> <span class="nam">result</span><span class="op">.</span><span class="nam">returncode</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t569" href="#t569">569</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t570" href="#t570">570</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t571" href="#t571">571</a></span><span class="t"> <span class="key">if</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">list</span><span class="op">)</span> <span class="key">and</span> <span class="nam">data</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t572" href="#t572">572</a></span><span class="t"> <span class="nam">results</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t573" href="#t573">573</a></span><span class="t"> <span class="str">'symbol'</span><span class="op">:</span> <span class="nam">symbol</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t574" href="#t574">574</a></span><span class="t"> <span class="str">'rating'</span><span class="op">:</span> <span class="nam">data</span><span class="op">[</span><span class="num">0</span><span class="op">]</span> <span class="key">if</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="num">0</span><span class="op">]</span><span class="op">,</span> <span class="nam">dict</span><span class="op">)</span> <span class="key">else</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t575" href="#t575">575</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t576" href="#t576">576</a></span><span class="t"> <span class="key">except</span> <span class="nam">json</span><span class="op">.</span><span class="nam">JSONDecodeError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t577" href="#t577">577</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t578" href="#t578">578</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t579" href="#t579">579</a></span><span class="t"> <span class="key">pass</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t580" href="#t580">580</a></span><span class="t"> <span class="key">return</span> <span class="nam">results</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t581" href="#t581">581</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t582" href="#t582">582</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t583" href="#t583">583</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t584" href="#t584">584</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">"Earnings Calendar Tracker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t585" href="#t585">585</a></span><span class="t"> <span class="nam">subparsers</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_subparsers</span><span class="op">(</span><span class="nam">dest</span><span class="op">=</span><span class="str">"command"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Commands"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t586" href="#t586">586</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t587" href="#t587">587</a></span><span class="t"> <span class="com"># list command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t588" href="#t588">588</a></span><span class="t"> <span class="nam">list_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"list"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"List all upcoming earnings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t589" href="#t589">589</a></span><span class="t"> <span class="nam">list_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--refresh"</span><span class="op">,</span> <span class="str">"-r"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Force refresh"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t590" href="#t590">590</a></span><span class="t"> <span class="nam">list_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">list_earnings</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t591" href="#t591">591</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t592" href="#t592">592</a></span><span class="t"> <span class="com"># check command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t593" href="#t593">593</a></span><span class="t"> <span class="nam">check_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"check"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Check today/this week"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t594" href="#t594">594</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--verbose"</span><span class="op">,</span> <span class="str">"-v"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t595" href="#t595">595</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--json"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"JSON output"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t596" href="#t596">596</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--lang"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">"en"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Output language (en, de)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t597" href="#t597">597</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--week"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Show full week preview (for weekly cron)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t598" href="#t598">598</a></span><span class="t"> <span class="nam">check_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">check_earnings</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t599" href="#t599">599</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t600" href="#t600">600</a></span><span class="t"> <span class="com"># refresh command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t601" href="#t601">601</a></span><span class="t"> <span class="nam">refresh_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"refresh"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Force refresh all data"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t602" href="#t602">602</a></span><span class="t"> <span class="nam">refresh_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">a</span><span class="op">:</span> <span class="nam">refresh_earnings</span><span class="op">(</span><span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span><span class="op">,</span> <span class="nam">force</span><span class="op">=</span><span class="key">True</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t603" href="#t603">603</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t604" href="#t604">604</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t605" href="#t605">605</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t606" href="#t606">606</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t607" href="#t607">607</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">print_help</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t608" href="#t608">608</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t609" href="#t609">609</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t610" href="#t610">610</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">func</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t611" href="#t611">611</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t612" href="#t612">612</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t613" href="#t613">613</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">"__main__"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t614" href="#t614">614</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_briefing_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_fetch_news_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,414 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/portfolio.py: 32%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;portfolio.py</b>:
<span class="pc_cov">32%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">183 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">59<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">124<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_fetch_news_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_ranking_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Portfolio Manager - CRUD operations for stock watchlist.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="key">import</span> <span class="nam">csv</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="nam">PORTFOLIO_FILE</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"config"</span> <span class="op">/</span> <span class="str">"portfolio.csv"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="nam">REQUIRED_COLUMNS</span> <span class="op">=</span> <span class="op">[</span><span class="str">'symbol'</span><span class="op">,</span> <span class="str">'name'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="nam">DEFAULT_COLUMNS</span> <span class="op">=</span> <span class="op">[</span><span class="str">'symbol'</span><span class="op">,</span> <span class="str">'name'</span><span class="op">,</span> <span class="str">'category'</span><span class="op">,</span> <span class="str">'notes'</span><span class="op">,</span> <span class="str">'type'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"><span class="key">def</span> <span class="nam">validate_portfolio_csv</span><span class="op">(</span><span class="nam">path</span><span class="op">:</span> <span class="nam">Path</span><span class="op">)</span> <span class="op">-></span> <span class="nam">tuple</span><span class="op">[</span><span class="nam">bool</span><span class="op">,</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"><span class="str"> Validate portfolio CSV file for common issues.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"><span class="str"> Returns:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"><span class="str"> Tuple of (is_valid, list of warnings)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"> <span class="nam">warnings</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">path</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span><span class="op">,</span> <span class="nam">warnings</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">path</span><span class="op">,</span> <span class="str">'r'</span><span class="op">,</span> <span class="nam">encoding</span><span class="op">=</span><span class="str">'utf-8'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"> <span class="com"># Check for encoding issues</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"> <span class="nam">content</span> <span class="op">=</span> <span class="nam">f</span><span class="op">.</span><span class="nam">read</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">path</span><span class="op">,</span> <span class="str">'r'</span><span class="op">,</span> <span class="nam">encoding</span><span class="op">=</span><span class="str">'utf-8'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"> <span class="nam">reader</span> <span class="op">=</span> <span class="nam">csv</span><span class="op">.</span><span class="nam">DictReader</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="com"># Check required columns</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="key">if</span> <span class="nam">reader</span><span class="op">.</span><span class="nam">fieldnames</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="nam">warnings</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">"CSV appears to be empty"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="key">return</span> <span class="key">False</span><span class="op">,</span> <span class="nam">warnings</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="nam">missing_cols</span> <span class="op">=</span> <span class="nam">set</span><span class="op">(</span><span class="nam">REQUIRED_COLUMNS</span><span class="op">)</span> <span class="op">-</span> <span class="nam">set</span><span class="op">(</span><span class="nam">reader</span><span class="op">.</span><span class="nam">fieldnames</span> <span class="key">or</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"> <span class="key">if</span> <span class="nam">missing_cols</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"> <span class="nam">warnings</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"Missing required columns: {', '.join(missing_cols)}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"> <span class="com"># Check for duplicate symbols</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"> <span class="nam">symbols</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"> <span class="key">for</span> <span class="nam">row</span> <span class="key">in</span> <span class="nam">reader</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"> <span class="nam">symbol</span> <span class="op">=</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'symbol'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t"> <span class="key">if</span> <span class="nam">symbol</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="nam">symbols</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">symbol</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="nam">duplicates</span> <span class="op">=</span> <span class="op">[</span><span class="nam">s</span> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">set</span><span class="op">(</span><span class="nam">symbols</span><span class="op">)</span> <span class="key">if</span> <span class="nam">symbols</span><span class="op">.</span><span class="nam">count</span><span class="op">(</span><span class="nam">s</span><span class="op">)</span> <span class="op">></span> <span class="num">1</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="key">if</span> <span class="nam">duplicates</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="nam">warnings</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"Duplicate symbols found: {', '.join(duplicates)}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="key">except</span> <span class="nam">UnicodeDecodeError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"> <span class="nam">warnings</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">"File encoding issue - try saving as UTF-8"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t"> <span class="nam">warnings</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"Error reading portfolio: {e}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"> <span class="key">return</span> <span class="key">False</span><span class="op">,</span> <span class="nam">warnings</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span><span class="op">,</span> <span class="nam">warnings</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"><span class="key">def</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="str">"""Load portfolio from CSV with validation."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">PORTFOLIO_FILE</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t"> <span class="com"># Validate first</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"> <span class="nam">is_valid</span><span class="op">,</span> <span class="nam">warnings</span> <span class="op">=</span> <span class="nam">validate_portfolio_csv</span><span class="op">(</span><span class="nam">PORTFOLIO_FILE</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="key">for</span> <span class="nam">warning</span> <span class="key">in</span> <span class="nam">warnings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Portfolio warning: {warning}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">is_valid</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#9888;&#65039; Portfolio has errors - returning empty"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">PORTFOLIO_FILE</span><span class="op">,</span> <span class="str">'r'</span><span class="op">,</span> <span class="nam">encoding</span><span class="op">=</span><span class="str">'utf-8'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t"> <span class="nam">reader</span> <span class="op">=</span> <span class="nam">csv</span><span class="op">.</span><span class="nam">DictReader</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"> <span class="com"># Normalize data</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"> <span class="nam">seen_symbols</span> <span class="op">=</span> <span class="nam">set</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t"> <span class="key">for</span> <span class="nam">row</span> <span class="key">in</span> <span class="nam">reader</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="nam">symbol</span> <span class="op">=</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'symbol'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">symbol</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"> <span class="com"># Skip duplicates (keep first occurrence)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="key">if</span> <span class="nam">symbol</span> <span class="key">in</span> <span class="nam">seen_symbols</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"> <span class="nam">seen_symbols</span><span class="op">.</span><span class="nam">add</span><span class="op">(</span><span class="nam">symbol</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="nam">portfolio</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="str">'symbol'</span><span class="op">:</span> <span class="nam">symbol</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="str">'name'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'name'</span><span class="op">,</span> <span class="nam">symbol</span><span class="op">)</span> <span class="key">or</span> <span class="nam">symbol</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"> <span class="str">'category'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'category'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span> <span class="key">or</span> <span class="str">''</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"> <span class="str">'notes'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'notes'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span> <span class="key">or</span> <span class="str">''</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t"> <span class="str">'type'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'type'</span><span class="op">,</span> <span class="str">'Watchlist'</span><span class="op">)</span> <span class="key">or</span> <span class="str">'Watchlist'</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t"> <span class="key">return</span> <span class="nam">portfolio</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Error loading portfolio: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"><span class="key">def</span> <span class="nam">save_portfolio</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="str">"""Save portfolio to CSV."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t"> <span class="nam">PORTFOLIO_FILE</span><span class="op">.</span><span class="nam">write_text</span><span class="op">(</span><span class="str">"symbol,name,category,notes,type\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">PORTFOLIO_FILE</span><span class="op">,</span> <span class="str">'w'</span><span class="op">,</span> <span class="nam">newline</span><span class="op">=</span><span class="str">''</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"> <span class="nam">writer</span> <span class="op">=</span> <span class="nam">csv</span><span class="op">.</span><span class="nam">DictWriter</span><span class="op">(</span><span class="nam">f</span><span class="op">,</span> <span class="nam">fieldnames</span><span class="op">=</span><span class="op">[</span><span class="str">'symbol'</span><span class="op">,</span> <span class="str">'name'</span><span class="op">,</span> <span class="str">'category'</span><span class="op">,</span> <span class="str">'notes'</span><span class="op">,</span> <span class="str">'type'</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t"> <span class="nam">writer</span><span class="op">.</span><span class="nam">writeheader</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="nam">writer</span><span class="op">.</span><span class="nam">writerows</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"><span class="key">def</span> <span class="nam">list_portfolio</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"> <span class="str">"""List all stocks in portfolio."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128194; Portfolio is empty. Use 'portfolio add &lt;SYMBOL>' to add stocks."</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"\n&#128202; Portfolio ({len(portfolio)} stocks)\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"> <span class="com"># Group by Type then Category</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="nam">by_type</span> <span class="op">=</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"> <span class="key">for</span> <span class="nam">stock</span> <span class="key">in</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="nam">t</span> <span class="op">=</span> <span class="nam">stock</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'type'</span><span class="op">,</span> <span class="str">'Watchlist'</span><span class="op">)</span> <span class="key">or</span> <span class="str">'Watchlist'</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="key">if</span> <span class="nam">t</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">by_type</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"> <span class="nam">by_type</span><span class="op">[</span><span class="nam">t</span><span class="op">]</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="nam">by_type</span><span class="op">[</span><span class="nam">t</span><span class="op">]</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">stock</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="key">for</span> <span class="nam">t</span><span class="op">,</span> <span class="nam">type_stocks</span> <span class="key">in</span> <span class="nam">by_type</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"# {t}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="nam">categories</span> <span class="op">=</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="key">for</span> <span class="nam">stock</span> <span class="key">in</span> <span class="nam">type_stocks</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="nam">cat</span> <span class="op">=</span> <span class="nam">stock</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'category'</span><span class="op">,</span> <span class="str">'Other'</span><span class="op">)</span> <span class="key">or</span> <span class="str">'Other'</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="key">if</span> <span class="nam">cat</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">categories</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="nam">categories</span><span class="op">[</span><span class="nam">cat</span><span class="op">]</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t"> <span class="nam">categories</span><span class="op">[</span><span class="nam">cat</span><span class="op">]</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">stock</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="key">for</span> <span class="nam">cat</span><span class="op">,</span> <span class="nam">stocks</span> <span class="key">in</span> <span class="nam">categories</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"### {cat}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">stocks</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="nam">notes</span> <span class="op">=</span> <span class="str">f" &#8212; {s['notes']}"</span> <span class="key">if</span> <span class="nam">s</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'notes'</span><span class="op">)</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#8226; {s['symbol']}: {s['name']}{notes}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"><span class="key">def</span> <span class="nam">add_stock</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="str">"""Add a stock to portfolio."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t"> <span class="com"># Check if already exists</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t"> <span class="key">if</span> <span class="nam">any</span><span class="op">(</span><span class="nam">s</span><span class="op">[</span><span class="str">'symbol'</span><span class="op">]</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span> <span class="op">==</span> <span class="nam">args</span><span class="op">.</span><span class="nam">symbol</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">portfolio</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; {args.symbol.upper()} already in portfolio"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t"> <span class="nam">new_stock</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"> <span class="str">'symbol'</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">symbol</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="str">'name'</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">name</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">symbol</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t"> <span class="str">'category'</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">category</span> <span class="key">or</span> <span class="str">''</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t"> <span class="str">'notes'</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">notes</span> <span class="key">or</span> <span class="str">''</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="str">'type'</span><span class="op">:</span> <span class="nam">args</span><span class="op">.</span><span class="nam">type</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t"> <span class="nam">portfolio</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">new_stock</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t"> <span class="nam">save_portfolio</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Added {args.symbol.upper()} to portfolio ({args.type})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t"><span class="key">def</span> <span class="nam">remove_stock</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t"> <span class="str">"""Remove a stock from portfolio."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"> <span class="nam">original_len</span> <span class="op">=</span> <span class="nam">len</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="op">[</span><span class="nam">s</span> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">portfolio</span> <span class="key">if</span> <span class="nam">s</span><span class="op">[</span><span class="str">'symbol'</span><span class="op">]</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span> <span class="op">!=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">symbol</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span> <span class="op">==</span> <span class="nam">original_len</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; {args.symbol.upper()} not found in portfolio"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="nam">save_portfolio</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Removed {args.symbol.upper()} from portfolio"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t"><span class="key">def</span> <span class="nam">import_csv</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t"> <span class="str">"""Import portfolio from external CSV."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t"> <span class="nam">import_path</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">file</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">import_path</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; File not found: {args.file}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="nam">sys</span><span class="op">.</span><span class="nam">exit</span><span class="op">(</span><span class="num">1</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">import_path</span><span class="op">,</span> <span class="str">'r'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t"> <span class="nam">reader</span> <span class="op">=</span> <span class="nam">csv</span><span class="op">.</span><span class="nam">DictReader</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"> <span class="nam">imported</span> <span class="op">=</span> <span class="nam">list</span><span class="op">(</span><span class="nam">reader</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="com"># Normalize fields</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t"> <span class="nam">normalized</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t"> <span class="key">for</span> <span class="nam">row</span> <span class="key">in</span> <span class="nam">imported</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t"> <span class="nam">normalized</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t"> <span class="str">'symbol'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'symbol'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'Symbol'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'ticker'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">)</span><span class="op">)</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t"> <span class="str">'name'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'name'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'Name'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'company'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">)</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="str">'category'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'category'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'Category'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'sector'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">)</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t"> <span class="str">'notes'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'notes'</span><span class="op">,</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'Notes'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t"> <span class="str">'type'</span><span class="op">:</span> <span class="nam">row</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'type'</span><span class="op">,</span> <span class="str">'Watchlist'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t"> <span class="nam">save_portfolio</span><span class="op">(</span><span class="nam">normalized</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Imported {len(normalized)} stocks from {args.file}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t"><span class="key">def</span> <span class="nam">create_interactive</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t"> <span class="str">"""Interactive portfolio creation."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#128202; Portfolio Creator\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"Enter stocks one per line (format: SYMBOL or SYMBOL,Name,Category)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"Type 'done' when finished.\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t"> <span class="key">while</span> <span class="key">True</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t"> <span class="nam">line</span> <span class="op">=</span> <span class="nam">input</span><span class="op">(</span><span class="str">"> "</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t"> <span class="key">except</span> <span class="op">(</span><span class="nam">EOFError</span><span class="op">,</span> <span class="nam">KeyboardInterrupt</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t"> <span class="key">if</span> <span class="nam">line</span><span class="op">.</span><span class="nam">lower</span><span class="op">(</span><span class="op">)</span> <span class="op">==</span> <span class="str">'done'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">line</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t"> <span class="key">continue</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t"> <span class="nam">parts</span> <span class="op">=</span> <span class="nam">line</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="str">','</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t"> <span class="nam">symbol</span> <span class="op">=</span> <span class="nam">parts</span><span class="op">[</span><span class="num">0</span><span class="op">]</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t"> <span class="nam">name</span> <span class="op">=</span> <span class="nam">parts</span><span class="op">[</span><span class="num">1</span><span class="op">]</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">parts</span><span class="op">)</span> <span class="op">></span> <span class="num">1</span> <span class="key">else</span> <span class="nam">symbol</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t"> <span class="nam">category</span> <span class="op">=</span> <span class="nam">parts</span><span class="op">[</span><span class="num">2</span><span class="op">]</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">parts</span><span class="op">)</span> <span class="op">></span> <span class="num">2</span> <span class="key">else</span> <span class="str">''</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t"> <span class="nam">portfolio</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="str">'symbol'</span><span class="op">:</span> <span class="nam">symbol</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t"> <span class="str">'name'</span><span class="op">:</span> <span class="nam">name</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t"> <span class="str">'category'</span><span class="op">:</span> <span class="nam">category</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t"> <span class="str">'notes'</span><span class="op">:</span> <span class="str">''</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t"> <span class="str">'type'</span><span class="op">:</span> <span class="str">'Watchlist'</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" Added: {symbol}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"> <span class="key">if</span> <span class="nam">portfolio</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t"> <span class="nam">save_portfolio</span><span class="op">(</span><span class="nam">portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"\n&#9989; Created portfolio with {len(portfolio)} stocks"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#9888;&#65039; No stocks added"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"><span class="key">def</span> <span class="nam">get_symbols</span><span class="op">(</span><span class="nam">args</span><span class="op">=</span><span class="key">None</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t"> <span class="str">"""Get list of symbols (for other scripts to use)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t"> <span class="nam">portfolio</span> <span class="op">=</span> <span class="nam">load_portfolio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t"> <span class="nam">symbols</span> <span class="op">=</span> <span class="op">[</span><span class="nam">s</span><span class="op">[</span><span class="str">'symbol'</span><span class="op">]</span> <span class="key">for</span> <span class="nam">s</span> <span class="key">in</span> <span class="nam">portfolio</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span> <span class="key">and</span> <span class="nam">args</span><span class="op">.</span><span class="nam">json</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"> <span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">symbols</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">','</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">symbols</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">'Portfolio Manager'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t"> <span class="nam">subparsers</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_subparsers</span><span class="op">(</span><span class="nam">dest</span><span class="op">=</span><span class="str">'command'</span><span class="op">,</span> <span class="nam">required</span><span class="op">=</span><span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t"> <span class="com"># List command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t"> <span class="nam">list_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'list'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'List portfolio'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t"> <span class="nam">list_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">list_portfolio</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t284" href="#t284">284</a></span><span class="t"> <span class="com"># Add command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t285" href="#t285">285</a></span><span class="t"> <span class="nam">add_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'add'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Add stock'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t286" href="#t286">286</a></span><span class="t"> <span class="nam">add_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'symbol'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Stock symbol'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t287" href="#t287">287</a></span><span class="t"> <span class="nam">add_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--name'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Company name'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t288" href="#t288">288</a></span><span class="t"> <span class="nam">add_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--category'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Category (e.g., Tech, Finance)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t289" href="#t289">289</a></span><span class="t"> <span class="nam">add_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--notes'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Notes'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t290" href="#t290">290</a></span><span class="t"> <span class="nam">add_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--type'</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">'Holding'</span><span class="op">,</span> <span class="str">'Watchlist'</span><span class="op">]</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">'Watchlist'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Portfolio type'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t291" href="#t291">291</a></span><span class="t"> <span class="nam">add_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">add_stock</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t292" href="#t292">292</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t293" href="#t293">293</a></span><span class="t"> <span class="com"># Remove command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t294" href="#t294">294</a></span><span class="t"> <span class="nam">remove_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'remove'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Remove stock'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t295" href="#t295">295</a></span><span class="t"> <span class="nam">remove_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'symbol'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Stock symbol'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t296" href="#t296">296</a></span><span class="t"> <span class="nam">remove_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">remove_stock</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t297" href="#t297">297</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t298" href="#t298">298</a></span><span class="t"> <span class="com"># Import command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t299" href="#t299">299</a></span><span class="t"> <span class="nam">import_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'import'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Import from CSV'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t300" href="#t300">300</a></span><span class="t"> <span class="nam">import_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'file'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'CSV file path'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t301" href="#t301">301</a></span><span class="t"> <span class="nam">import_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">import_csv</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t302" href="#t302">302</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t303" href="#t303">303</a></span><span class="t"> <span class="com"># Create command</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t304" href="#t304">304</a></span><span class="t"> <span class="nam">create_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'create'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Interactive creation'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t305" href="#t305">305</a></span><span class="t"> <span class="nam">create_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">create_interactive</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t306" href="#t306">306</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t307" href="#t307">307</a></span><span class="t"> <span class="com"># Symbols command (for other scripts)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t308" href="#t308">308</a></span><span class="t"> <span class="nam">symbols_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'symbols'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Get symbols list'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t309" href="#t309">309</a></span><span class="t"> <span class="nam">symbols_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--json'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Output as JSON'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t310" href="#t310">310</a></span><span class="t"> <span class="nam">symbols_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">get_symbols</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t311" href="#t311">311</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t312" href="#t312">312</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t313" href="#t313">313</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">func</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t314" href="#t314">314</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t315" href="#t315">315</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t316" href="#t316">316</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">'__main__'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t317" href="#t317">317</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_fetch_news_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_ranking_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

422
htmlcov/z_de1a740d5dc98ffd_ranking_py.html generated Normal file
View File

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/ranking.py: 86%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;ranking.py</b>:
<span class="pc_cov">86%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">147 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">126<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">21<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">9<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_portfolio_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_research_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Deterministic Headline Ranking - Impact-based ranking policy.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">Implements #53: Deterministic impact-based ranking for headline selection.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="str">Scoring Rubric (weights):</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="str">- Market Impact (40%): CB decisions, earnings, sanctions, oil spikes</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="str">- Novelty (20%): New vs recycled news</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"><span class="str">- Breadth (20%): Sector-wide vs single-stock</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="str">- Credibility (10%): Source reliability</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="str">- Diversity Bonus (10%): Underrepresented categories</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="str">Output:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="str">- MUST_READ: Top 5 stories</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"><span class="str">- SCAN: 3-5 additional stories (if quality threshold met)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"><span class="key">import</span> <span class="nam">re</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"><span class="key">from</span> <span class="nam">difflib</span> <span class="key">import</span> <span class="nam">SequenceMatcher</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"><span class="com"># Category keywords for classification</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"><span class="nam">CATEGORY_KEYWORDS</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"> <span class="str">"macro"</span><span class="op">:</span> <span class="op">[</span><span class="str">"fed"</span><span class="op">,</span> <span class="str">"ecb"</span><span class="op">,</span> <span class="str">"boj"</span><span class="op">,</span> <span class="str">"central bank"</span><span class="op">,</span> <span class="str">"rate"</span><span class="op">,</span> <span class="str">"inflation"</span><span class="op">,</span> <span class="str">"gdp"</span><span class="op">,</span> <span class="str">"unemployment"</span><span class="op">,</span> <span class="str">"treasury"</span><span class="op">,</span> <span class="str">"yield"</span><span class="op">,</span> <span class="str">"bond"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t"> <span class="str">"equities"</span><span class="op">:</span> <span class="op">[</span><span class="str">"earnings"</span><span class="op">,</span> <span class="str">"revenue"</span><span class="op">,</span> <span class="str">"profit"</span><span class="op">,</span> <span class="str">"eps"</span><span class="op">,</span> <span class="str">"guidance"</span><span class="op">,</span> <span class="str">"beat"</span><span class="op">,</span> <span class="str">"miss"</span><span class="op">,</span> <span class="str">"upgrade"</span><span class="op">,</span> <span class="str">"downgrade"</span><span class="op">,</span> <span class="str">"target"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"> <span class="str">"geopolitics"</span><span class="op">:</span> <span class="op">[</span><span class="str">"sanction"</span><span class="op">,</span> <span class="str">"tariff"</span><span class="op">,</span> <span class="str">"war"</span><span class="op">,</span> <span class="str">"conflict"</span><span class="op">,</span> <span class="str">"embargo"</span><span class="op">,</span> <span class="str">"trump"</span><span class="op">,</span> <span class="str">"china"</span><span class="op">,</span> <span class="str">"russia"</span><span class="op">,</span> <span class="str">"ukraine"</span><span class="op">,</span> <span class="str">"iran"</span><span class="op">,</span> <span class="str">"trade war"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"> <span class="str">"energy"</span><span class="op">:</span> <span class="op">[</span><span class="str">"oil"</span><span class="op">,</span> <span class="str">"opec"</span><span class="op">,</span> <span class="str">"crude"</span><span class="op">,</span> <span class="str">"gas"</span><span class="op">,</span> <span class="str">"energy"</span><span class="op">,</span> <span class="str">"brent"</span><span class="op">,</span> <span class="str">"wti"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"> <span class="str">"tech"</span><span class="op">:</span> <span class="op">[</span><span class="str">"ai"</span><span class="op">,</span> <span class="str">"chip"</span><span class="op">,</span> <span class="str">"semiconductor"</span><span class="op">,</span> <span class="str">"nvidia"</span><span class="op">,</span> <span class="str">"apple"</span><span class="op">,</span> <span class="str">"google"</span><span class="op">,</span> <span class="str">"microsoft"</span><span class="op">,</span> <span class="str">"meta"</span><span class="op">,</span> <span class="str">"amazon"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"><span class="com"># Source credibility scores (0-1)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"><span class="nam">SOURCE_CREDIBILITY</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="str">"Wall Street Journal"</span><span class="op">:</span> <span class="num">0.95</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="str">"WSJ"</span><span class="op">:</span> <span class="num">0.95</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="str">"Bloomberg"</span><span class="op">:</span> <span class="num">0.95</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="str">"Reuters"</span><span class="op">:</span> <span class="num">0.90</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="str">"Financial Times"</span><span class="op">:</span> <span class="num">0.90</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="str">"CNBC"</span><span class="op">:</span> <span class="num">0.80</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="str">"Yahoo Finance"</span><span class="op">:</span> <span class="num">0.70</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"> <span class="str">"MarketWatch"</span><span class="op">:</span> <span class="num">0.75</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"> <span class="str">"Barron's"</span><span class="op">:</span> <span class="num">0.85</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"> <span class="str">"Seeking Alpha"</span><span class="op">:</span> <span class="num">0.60</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"> <span class="str">"Tagesschau"</span><span class="op">:</span> <span class="num">0.85</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"> <span class="str">"Handelsblatt"</span><span class="op">:</span> <span class="num">0.80</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t"><span class="com"># Default config</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"><span class="nam">DEFAULT_CONFIG</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="str">"dedupe_threshold"</span><span class="op">:</span> <span class="num">0.7</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="str">"must_read_count"</span><span class="op">:</span> <span class="num">5</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="str">"scan_count"</span><span class="op">:</span> <span class="num">5</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="str">"must_read_min_score"</span><span class="op">:</span> <span class="num">0.4</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"> <span class="str">"scan_min_score"</span><span class="op">:</span> <span class="num">0.25</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="str">"source_cap"</span><span class="op">:</span> <span class="num">2</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"> <span class="str">"weights"</span><span class="op">:</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="str">"market_impact"</span><span class="op">:</span> <span class="num">0.40</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t"> <span class="str">"novelty"</span><span class="op">:</span> <span class="num">0.20</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"> <span class="str">"breadth"</span><span class="op">:</span> <span class="num">0.20</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"> <span class="str">"credibility"</span><span class="op">:</span> <span class="num">0.10</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="str">"diversity"</span><span class="op">:</span> <span class="num">0.10</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t"><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"><span class="key">def</span> <span class="nam">normalize_title</span><span class="op">(</span><span class="nam">title</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="str">"""Normalize title for comparison."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">title</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t"> <span class="key">return</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"> <span class="nam">cleaned</span> <span class="op">=</span> <span class="nam">re</span><span class="op">.</span><span class="nam">sub</span><span class="op">(</span><span class="str">r"[^a-z0-9\s]"</span><span class="op">,</span> <span class="str">" "</span><span class="op">,</span> <span class="nam">title</span><span class="op">.</span><span class="nam">lower</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="nam">tokens</span> <span class="op">=</span> <span class="nam">cleaned</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="key">return</span> <span class="str">" "</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">tokens</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"><span class="key">def</span> <span class="nam">title_similarity</span><span class="op">(</span><span class="nam">a</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">b</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"> <span class="str">"""Calculate title similarity using SequenceMatcher."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">a</span> <span class="key">or</span> <span class="key">not</span> <span class="nam">b</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="key">return</span> <span class="num">0.0</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="key">return</span> <span class="nam">SequenceMatcher</span><span class="op">(</span><span class="key">None</span><span class="op">,</span> <span class="nam">normalize_title</span><span class="op">(</span><span class="nam">a</span><span class="op">)</span><span class="op">,</span> <span class="nam">normalize_title</span><span class="op">(</span><span class="nam">b</span><span class="op">)</span><span class="op">)</span><span class="op">.</span><span class="nam">ratio</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"><span class="key">def</span> <span class="nam">deduplicate_headlines</span><span class="op">(</span><span class="nam">headlines</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">,</span> <span class="nam">threshold</span><span class="op">:</span> <span class="nam">float</span> <span class="op">=</span> <span class="num">0.7</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="str">"""Remove duplicate headlines by title similarity."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">headlines</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="nam">unique</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="key">for</span> <span class="nam">article</span> <span class="key">in</span> <span class="nam">headlines</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="nam">title</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"title"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="nam">is_dupe</span> <span class="op">=</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"> <span class="key">for</span> <span class="nam">existing</span> <span class="key">in</span> <span class="nam">unique</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="key">if</span> <span class="nam">title_similarity</span><span class="op">(</span><span class="nam">title</span><span class="op">,</span> <span class="nam">existing</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"title"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">)</span> <span class="op">></span> <span class="nam">threshold</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t"> <span class="nam">is_dupe</span> <span class="op">=</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">is_dupe</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="nam">unique</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">article</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="key">return</span> <span class="nam">unique</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t"><span class="key">def</span> <span class="nam">classify_category</span><span class="op">(</span><span class="nam">title</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">description</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="str">"""Classify headline into categories based on keywords."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="nam">text</span> <span class="op">=</span> <span class="str">f"{title} {description}"</span><span class="op">.</span><span class="nam">lower</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t"> <span class="nam">categories</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"> <span class="key">for</span> <span class="nam">category</span><span class="op">,</span> <span class="nam">keywords</span> <span class="key">in</span> <span class="nam">CATEGORY_KEYWORDS</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="key">for</span> <span class="nam">keyword</span> <span class="key">in</span> <span class="nam">keywords</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="key">if</span> <span class="nam">keyword</span> <span class="key">in</span> <span class="nam">text</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"> <span class="nam">categories</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">category</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="key">return</span> <span class="nam">categories</span> <span class="key">if</span> <span class="nam">categories</span> <span class="key">else</span> <span class="op">[</span><span class="str">"general"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t"><span class="key">def</span> <span class="nam">score_market_impact</span><span class="op">(</span><span class="nam">title</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">description</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"> <span class="str">"""Score market impact (0-1)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"> <span class="nam">text</span> <span class="op">=</span> <span class="str">f"{title} {description}"</span><span class="op">.</span><span class="nam">lower</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"> <span class="nam">score</span> <span class="op">=</span> <span class="num">0.3</span> <span class="com"># Base score</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="com"># High impact indicators</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t"> <span class="nam">high_impact</span> <span class="op">=</span> <span class="op">[</span><span class="str">"fed"</span><span class="op">,</span> <span class="str">"rate cut"</span><span class="op">,</span> <span class="str">"rate hike"</span><span class="op">,</span> <span class="str">"earnings"</span><span class="op">,</span> <span class="str">"guidance"</span><span class="op">,</span> <span class="str">"sanctions"</span><span class="op">,</span> <span class="str">"war"</span><span class="op">,</span> <span class="str">"oil"</span><span class="op">,</span> <span class="str">"recession"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t"> <span class="key">for</span> <span class="nam">term</span> <span class="key">in</span> <span class="nam">high_impact</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="key">if</span> <span class="nam">term</span> <span class="key">in</span> <span class="nam">text</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"> <span class="nam">score</span> <span class="op">+=</span> <span class="num">0.15</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"> <span class="com"># Medium impact</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="nam">medium_impact</span> <span class="op">=</span> <span class="op">[</span><span class="str">"profit"</span><span class="op">,</span> <span class="str">"revenue"</span><span class="op">,</span> <span class="str">"gdp"</span><span class="op">,</span> <span class="str">"inflation"</span><span class="op">,</span> <span class="str">"tariff"</span><span class="op">,</span> <span class="str">"merger"</span><span class="op">,</span> <span class="str">"acquisition"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"> <span class="key">for</span> <span class="nam">term</span> <span class="key">in</span> <span class="nam">medium_impact</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t"> <span class="key">if</span> <span class="nam">term</span> <span class="key">in</span> <span class="nam">text</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="nam">score</span> <span class="op">+=</span> <span class="num">0.1</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"> <span class="key">return</span> <span class="nam">min</span><span class="op">(</span><span class="nam">score</span><span class="op">,</span> <span class="num">1.0</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"><span class="key">def</span> <span class="nam">score_novelty</span><span class="op">(</span><span class="nam">article</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="str">"""Score novelty based on recency (0-1)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="nam">published_at</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"published_at"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">published_at</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="key">return</span> <span class="num">0.5</span> <span class="com"># Unknown = medium</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="key">if</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">published_at</span><span class="op">,</span> <span class="nam">str</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="nam">pub_time</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">fromisoformat</span><span class="op">(</span><span class="nam">published_at</span><span class="op">.</span><span class="nam">replace</span><span class="op">(</span><span class="str">"Z"</span><span class="op">,</span> <span class="str">"+00:00"</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="nam">pub_time</span> <span class="op">=</span> <span class="nam">published_at</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="nam">hours_old</span> <span class="op">=</span> <span class="op">(</span><span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="nam">pub_time</span><span class="op">.</span><span class="nam">tzinfo</span><span class="op">)</span> <span class="op">-</span> <span class="nam">pub_time</span><span class="op">)</span><span class="op">.</span><span class="nam">total_seconds</span><span class="op">(</span><span class="op">)</span> <span class="op">/</span> <span class="num">3600</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="key">if</span> <span class="nam">hours_old</span> <span class="op">&lt;</span> <span class="num">2</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="key">return</span> <span class="num">1.0</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"> <span class="key">elif</span> <span class="nam">hours_old</span> <span class="op">&lt;</span> <span class="num">6</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="key">return</span> <span class="num">0.8</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="key">elif</span> <span class="nam">hours_old</span> <span class="op">&lt;</span> <span class="num">12</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t"> <span class="key">return</span> <span class="num">0.6</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"> <span class="key">elif</span> <span class="nam">hours_old</span> <span class="op">&lt;</span> <span class="num">24</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="key">return</span> <span class="num">0.4</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="key">return</span> <span class="num">0.2</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="key">except</span> <span class="nam">Exception</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="key">return</span> <span class="num">0.5</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t"><span class="key">def</span> <span class="nam">score_breadth</span><span class="op">(</span><span class="nam">categories</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t"> <span class="str">"""Score breadth - sector-wide vs single-stock (0-1)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t"> <span class="com"># More categories = broader impact</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t"> <span class="key">if</span> <span class="str">"macro"</span> <span class="key">in</span> <span class="nam">categories</span> <span class="key">or</span> <span class="str">"geopolitics"</span> <span class="key">in</span> <span class="nam">categories</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t"> <span class="key">return</span> <span class="num">0.9</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"> <span class="key">if</span> <span class="str">"energy"</span> <span class="key">in</span> <span class="nam">categories</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="key">return</span> <span class="num">0.7</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">categories</span><span class="op">)</span> <span class="op">></span> <span class="num">1</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t"> <span class="key">return</span> <span class="num">0.6</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="key">return</span> <span class="num">0.4</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t"><span class="key">def</span> <span class="nam">score_credibility</span><span class="op">(</span><span class="nam">source</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t"> <span class="str">"""Score source credibility (0-1)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="key">return</span> <span class="nam">SOURCE_CREDIBILITY</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">source</span><span class="op">,</span> <span class="num">0.5</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t"><span class="key">def</span> <span class="nam">calculate_score</span><span class="op">(</span><span class="nam">article</span><span class="op">:</span> <span class="nam">dict</span><span class="op">,</span> <span class="nam">weights</span><span class="op">:</span> <span class="nam">dict</span><span class="op">,</span> <span class="nam">category_counts</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t"> <span class="str">"""Calculate overall score for a headline."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t"> <span class="nam">title</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"title"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t"> <span class="nam">description</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"description"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"> <span class="nam">source</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"source"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t"> <span class="nam">categories</span> <span class="op">=</span> <span class="nam">classify_category</span><span class="op">(</span><span class="nam">title</span><span class="op">,</span> <span class="nam">description</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t"> <span class="nam">article</span><span class="op">[</span><span class="str">"_categories"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">categories</span> <span class="com"># Store for later use</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t"> <span class="com"># Component scores</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"> <span class="nam">impact</span> <span class="op">=</span> <span class="nam">score_market_impact</span><span class="op">(</span><span class="nam">title</span><span class="op">,</span> <span class="nam">description</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t"> <span class="nam">novelty</span> <span class="op">=</span> <span class="nam">score_novelty</span><span class="op">(</span><span class="nam">article</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="nam">breadth</span> <span class="op">=</span> <span class="nam">score_breadth</span><span class="op">(</span><span class="nam">categories</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t"> <span class="nam">credibility</span> <span class="op">=</span> <span class="nam">score_credibility</span><span class="op">(</span><span class="nam">source</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t"> <span class="com"># Diversity bonus - boost underrepresented categories</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t"> <span class="nam">diversity</span> <span class="op">=</span> <span class="num">0.0</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t"> <span class="key">for</span> <span class="nam">cat</span> <span class="key">in</span> <span class="nam">categories</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t"> <span class="key">if</span> <span class="nam">category_counts</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">cat</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">&lt;</span> <span class="num">1</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t"> <span class="nam">diversity</span> <span class="op">=</span> <span class="num">0.5</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="key">elif</span> <span class="nam">category_counts</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">cat</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">&lt;</span> <span class="num">2</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="nam">diversity</span> <span class="op">=</span> <span class="num">0.3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t"> <span class="com"># Weighted sum</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t"> <span class="nam">score</span> <span class="op">=</span> <span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"> <span class="nam">impact</span> <span class="op">*</span> <span class="nam">weights</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"market_impact"</span><span class="op">,</span> <span class="num">0.4</span><span class="op">)</span> <span class="op">+</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t"> <span class="nam">novelty</span> <span class="op">*</span> <span class="nam">weights</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"novelty"</span><span class="op">,</span> <span class="num">0.2</span><span class="op">)</span> <span class="op">+</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="nam">breadth</span> <span class="op">*</span> <span class="nam">weights</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"breadth"</span><span class="op">,</span> <span class="num">0.2</span><span class="op">)</span> <span class="op">+</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t"> <span class="nam">credibility</span> <span class="op">*</span> <span class="nam">weights</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"credibility"</span><span class="op">,</span> <span class="num">0.1</span><span class="op">)</span> <span class="op">+</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t"> <span class="nam">diversity</span> <span class="op">*</span> <span class="nam">weights</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"diversity"</span><span class="op">,</span> <span class="num">0.1</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t"> <span class="nam">article</span><span class="op">[</span><span class="str">"_score"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">round</span><span class="op">(</span><span class="nam">score</span><span class="op">,</span> <span class="num">3</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="nam">article</span><span class="op">[</span><span class="str">"_impact"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">round</span><span class="op">(</span><span class="nam">impact</span><span class="op">,</span> <span class="num">3</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t"> <span class="nam">article</span><span class="op">[</span><span class="str">"_novelty"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">round</span><span class="op">(</span><span class="nam">novelty</span><span class="op">,</span> <span class="num">3</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t"> <span class="key">return</span> <span class="nam">score</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t"><span class="key">def</span> <span class="nam">apply_source_cap</span><span class="op">(</span><span class="nam">ranked</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">,</span> <span class="nam">cap</span><span class="op">:</span> <span class="nam">int</span> <span class="op">=</span> <span class="num">2</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t"> <span class="str">"""Apply source cap - max N items per outlet."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t"> <span class="nam">source_counts</span> <span class="op">=</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t"> <span class="key">for</span> <span class="nam">article</span> <span class="key">in</span> <span class="nam">ranked</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t"> <span class="nam">source</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"source"</span><span class="op">,</span> <span class="str">"Unknown"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="key">if</span> <span class="nam">source_counts</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">source</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">&lt;</span> <span class="nam">cap</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t"> <span class="nam">result</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">article</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t"> <span class="nam">source_counts</span><span class="op">[</span><span class="nam">source</span><span class="op">]</span> <span class="op">=</span> <span class="nam">source_counts</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">source</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">+</span> <span class="num">1</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t"><span class="key">def</span> <span class="nam">ensure_diversity</span><span class="op">(</span><span class="nam">selected</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">,</span> <span class="nam">candidates</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">,</span> <span class="nam">required</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t"> <span class="str">"""Ensure at least one headline from required categories if available."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">list</span><span class="op">(</span><span class="nam">selected</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t"> <span class="nam">covered</span> <span class="op">=</span> <span class="nam">set</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t"> <span class="key">for</span> <span class="nam">article</span> <span class="key">in</span> <span class="nam">result</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"> <span class="key">for</span> <span class="nam">cat</span> <span class="key">in</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"_categories"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t"> <span class="nam">covered</span><span class="op">.</span><span class="nam">add</span><span class="op">(</span><span class="nam">cat</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t"> <span class="key">for</span> <span class="nam">req_cat</span> <span class="key">in</span> <span class="nam">required</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t"> <span class="key">if</span> <span class="nam">req_cat</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">covered</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t"> <span class="com"># Find candidate from this category</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t"> <span class="key">for</span> <span class="nam">candidate</span> <span class="key">in</span> <span class="nam">candidates</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t"> <span class="key">if</span> <span class="nam">candidate</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">result</span> <span class="key">and</span> <span class="nam">req_cat</span> <span class="key">in</span> <span class="nam">candidate</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"_categories"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t"> <span class="nam">result</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">candidate</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="nam">covered</span><span class="op">.</span><span class="nam">add</span><span class="op">(</span><span class="nam">req_cat</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t"><span class="key">def</span> <span class="nam">rank_headlines</span><span class="op">(</span><span class="nam">headlines</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span><span class="op">,</span> <span class="nam">config</span><span class="op">:</span> <span class="nam">dict</span> <span class="op">|</span> <span class="key">None</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t"> <span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"><span class="str"> Rank headlines deterministically.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t"><span class="str"> </span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t"><span class="str"> Args:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"><span class="str"> headlines: List of headline dicts with title, source, description, etc.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t"><span class="str"> config: Optional config overrides</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t"><span class="str"> </span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t"><span class="str"> Returns:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"><span class="str"> {"must_read": [...], "scan": [...]}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t"> <span class="nam">cfg</span> <span class="op">=</span> <span class="op">{</span><span class="op">**</span><span class="nam">DEFAULT_CONFIG</span><span class="op">,</span> <span class="op">**</span><span class="op">(</span><span class="nam">config</span> <span class="key">or</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t"> <span class="nam">weights</span> <span class="op">=</span> <span class="nam">cfg</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"weights"</span><span class="op">,</span> <span class="nam">DEFAULT_CONFIG</span><span class="op">[</span><span class="str">"weights"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">headlines</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="str">"must_read"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span> <span class="str">"scan"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="com"># Step 1: Deduplicate</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t"> <span class="nam">unique</span> <span class="op">=</span> <span class="nam">deduplicate_headlines</span><span class="op">(</span><span class="nam">headlines</span><span class="op">,</span> <span class="nam">cfg</span><span class="op">[</span><span class="str">"dedupe_threshold"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t"> <span class="com"># Step 2: Score all headlines</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"> <span class="nam">category_counts</span> <span class="op">=</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t"> <span class="key">for</span> <span class="nam">article</span> <span class="key">in</span> <span class="nam">unique</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t"> <span class="nam">calculate_score</span><span class="op">(</span><span class="nam">article</span><span class="op">,</span> <span class="nam">weights</span><span class="op">,</span> <span class="nam">category_counts</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t"> <span class="key">for</span> <span class="nam">cat</span> <span class="key">in</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"_categories"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t"> <span class="nam">category_counts</span><span class="op">[</span><span class="nam">cat</span><span class="op">]</span> <span class="op">=</span> <span class="nam">category_counts</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="nam">cat</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">+</span> <span class="num">1</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t"> <span class="com"># Step 3: Sort by score</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t"> <span class="nam">ranked</span> <span class="op">=</span> <span class="nam">sorted</span><span class="op">(</span><span class="nam">unique</span><span class="op">,</span> <span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"_score"</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span><span class="op">,</span> <span class="nam">reverse</span><span class="op">=</span><span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t284" href="#t284">284</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t285" href="#t285">285</a></span><span class="t"> <span class="com"># Step 4: Apply source cap</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t286" href="#t286">286</a></span><span class="t"> <span class="nam">capped</span> <span class="op">=</span> <span class="nam">apply_source_cap</span><span class="op">(</span><span class="nam">ranked</span><span class="op">,</span> <span class="nam">cfg</span><span class="op">[</span><span class="str">"source_cap"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t287" href="#t287">287</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t288" href="#t288">288</a></span><span class="t"> <span class="com"># Step 5: Select must_read with diversity quota</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t289" href="#t289">289</a></span><span class="t"> <span class="com"># Leave room for diversity additions by taking count-1 initially</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t290" href="#t290">290</a></span><span class="t"> <span class="nam">must_read_candidates</span> <span class="op">=</span> <span class="op">[</span><span class="nam">a</span> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">capped</span> <span class="key">if</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"_score"</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">>=</span> <span class="nam">cfg</span><span class="op">[</span><span class="str">"must_read_min_score"</span><span class="op">]</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t291" href="#t291">291</a></span><span class="t"> <span class="nam">must_read_count</span> <span class="op">=</span> <span class="nam">cfg</span><span class="op">[</span><span class="str">"must_read_count"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t292" href="#t292">292</a></span><span class="t"> <span class="nam">must_read</span> <span class="op">=</span> <span class="nam">must_read_candidates</span><span class="op">[</span><span class="op">:</span><span class="nam">max</span><span class="op">(</span><span class="num">1</span><span class="op">,</span> <span class="nam">must_read_count</span> <span class="op">-</span> <span class="num">2</span><span class="op">)</span><span class="op">]</span> <span class="com"># Reserve 2 slots for diversity</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t293" href="#t293">293</a></span><span class="t"> <span class="nam">must_read</span> <span class="op">=</span> <span class="nam">ensure_diversity</span><span class="op">(</span><span class="nam">must_read</span><span class="op">,</span> <span class="nam">capped</span><span class="op">,</span> <span class="op">[</span><span class="str">"macro"</span><span class="op">,</span> <span class="str">"equities"</span><span class="op">,</span> <span class="str">"geopolitics"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t294" href="#t294">294</a></span><span class="t"> <span class="nam">must_read</span> <span class="op">=</span> <span class="nam">must_read</span><span class="op">[</span><span class="op">:</span><span class="nam">must_read_count</span><span class="op">]</span> <span class="com"># Final trim to exact count</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t295" href="#t295">295</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t296" href="#t296">296</a></span><span class="t"> <span class="com"># Step 6: Select scan (additional items)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t297" href="#t297">297</a></span><span class="t"> <span class="nam">scan_candidates</span> <span class="op">=</span> <span class="op">[</span><span class="nam">a</span> <span class="key">for</span> <span class="nam">a</span> <span class="key">in</span> <span class="nam">capped</span> <span class="key">if</span> <span class="nam">a</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">must_read</span> <span class="key">and</span> <span class="nam">a</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"_score"</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span> <span class="op">>=</span> <span class="nam">cfg</span><span class="op">[</span><span class="str">"scan_min_score"</span><span class="op">]</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t298" href="#t298">298</a></span><span class="t"> <span class="nam">scan</span> <span class="op">=</span> <span class="nam">scan_candidates</span><span class="op">[</span><span class="op">:</span><span class="nam">cfg</span><span class="op">[</span><span class="str">"scan_count"</span><span class="op">]</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t299" href="#t299">299</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t300" href="#t300">300</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t301" href="#t301">301</a></span><span class="t"> <span class="str">"must_read"</span><span class="op">:</span> <span class="nam">must_read</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t302" href="#t302">302</a></span><span class="t"> <span class="str">"scan"</span><span class="op">:</span> <span class="nam">scan</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t303" href="#t303">303</a></span><span class="t"> <span class="str">"total_processed"</span><span class="op">:</span> <span class="nam">len</span><span class="op">(</span><span class="nam">headlines</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t304" href="#t304">304</a></span><span class="t"> <span class="str">"after_dedupe"</span><span class="op">:</span> <span class="nam">len</span><span class="op">(</span><span class="nam">unique</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t305" href="#t305">305</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t306" href="#t306">306</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t307" href="#t307">307</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t308" href="#t308">308</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">"__main__"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t309" href="#t309">309</a></span><span class="t"> <span class="com"># Test with sample data</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t310" href="#t310">310</a></span><span class="t"> <span class="nam">test_headlines</span> <span class="op">=</span> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t311" href="#t311">311</a></span><span class="t"> <span class="op">{</span><span class="str">"title"</span><span class="op">:</span> <span class="str">"Fed signals rate cut in March"</span><span class="op">,</span> <span class="str">"source"</span><span class="op">:</span> <span class="str">"WSJ"</span><span class="op">,</span> <span class="str">"description"</span><span class="op">:</span> <span class="str">"Federal Reserve hints at policy shift"</span><span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t312" href="#t312">312</a></span><span class="t"> <span class="op">{</span><span class="str">"title"</span><span class="op">:</span> <span class="str">"Apple earnings beat expectations"</span><span class="op">,</span> <span class="str">"source"</span><span class="op">:</span> <span class="str">"CNBC"</span><span class="op">,</span> <span class="str">"description"</span><span class="op">:</span> <span class="str">"Revenue up 15%"</span><span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t313" href="#t313">313</a></span><span class="t"> <span class="op">{</span><span class="str">"title"</span><span class="op">:</span> <span class="str">"Oil prices surge on OPEC cuts"</span><span class="op">,</span> <span class="str">"source"</span><span class="op">:</span> <span class="str">"Reuters"</span><span class="op">,</span> <span class="str">"description"</span><span class="op">:</span> <span class="str">"Brent crude hits $90"</span><span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t314" href="#t314">314</a></span><span class="t"> <span class="op">{</span><span class="str">"title"</span><span class="op">:</span> <span class="str">"China-US trade tensions escalate"</span><span class="op">,</span> <span class="str">"source"</span><span class="op">:</span> <span class="str">"Bloomberg"</span><span class="op">,</span> <span class="str">"description"</span><span class="op">:</span> <span class="str">"New tariffs announced"</span><span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t315" href="#t315">315</a></span><span class="t"> <span class="op">{</span><span class="str">"title"</span><span class="op">:</span> <span class="str">"Tech stocks rally on AI optimism"</span><span class="op">,</span> <span class="str">"source"</span><span class="op">:</span> <span class="str">"Yahoo Finance"</span><span class="op">,</span> <span class="str">"description"</span><span class="op">:</span> <span class="str">"Nvidia leads gains"</span><span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t316" href="#t316">316</a></span><span class="t"> <span class="op">{</span><span class="str">"title"</span><span class="op">:</span> <span class="str">"Fed hints at rate reduction"</span><span class="op">,</span> <span class="str">"source"</span><span class="op">:</span> <span class="str">"MarketWatch"</span><span class="op">,</span> <span class="str">"description"</span><span class="op">:</span> <span class="str">"Same story as WSJ"</span><span class="op">}</span><span class="op">,</span> <span class="com"># Dupe</span>&nbsp;</span><span class="r"></span></p>
<p class="exc exc2 show_exc"><span class="n"><a id="t317" href="#t317">317</a></span><span class="t"> <span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t318" href="#t318">318</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t319" href="#t319">319</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">rank_headlines</span><span class="op">(</span><span class="nam">test_headlines</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t320" href="#t320">320</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"MUST_READ:"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t321" href="#t321">321</a></span><span class="t"> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">result</span><span class="op">[</span><span class="str">"must_read"</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t322" href="#t322">322</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" [{h['_score']:.2f}] {h['title']} ({h['source']})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t323" href="#t323">323</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\nSCAN:"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t324" href="#t324">324</a></span><span class="t"> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">result</span><span class="op">[</span><span class="str">"scan"</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t325" href="#t325">325</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" [{h['_score']:.2f}] {h['title']} ({h['source']})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_portfolio_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_research_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,380 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/research.py: 65%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;research.py</b>:
<span class="pc_cov">65%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">130 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">85<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">45<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_ranking_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_setup_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Research Module - Deep research using Gemini CLI.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t"><span class="str">Crawls articles, finds correlations, researches companies.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">Outputs research_report.md for later analysis.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"><span class="key">import</span> <span class="nam">os</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="key">import</span> <span class="nam">shutil</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="key">import</span> <span class="nam">subprocess</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="key">from</span> <span class="nam">utils</span> <span class="key">import</span> <span class="nam">ensure_venv</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"><span class="key">from</span> <span class="nam">fetch_news</span> <span class="key">import</span> <span class="nam">PortfolioError</span><span class="op">,</span> <span class="nam">get_market_news</span><span class="op">,</span> <span class="nam">get_portfolio_news</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"><span class="nam">SCRIPT_DIR</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"><span class="nam">CONFIG_DIR</span> <span class="op">=</span> <span class="nam">SCRIPT_DIR</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"config"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"><span class="nam">OUTPUT_DIR</span> <span class="op">=</span> <span class="nam">SCRIPT_DIR</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"research"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"><span class="nam">ensure_venv</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"><span class="key">def</span> <span class="nam">format_market_data</span><span class="op">(</span><span class="nam">market_data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"> <span class="str">"""Format market data for research prompt."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"> <span class="nam">lines</span> <span class="op">=</span> <span class="op">[</span><span class="str">"## Market Data\n"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"> <span class="key">for</span> <span class="nam">region</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">market_data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'markets'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"### {data['name']}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="key">for</span> <span class="nam">symbol</span><span class="op">,</span> <span class="nam">idx</span> <span class="key">in</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'indices'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="key">if</span> <span class="str">'data'</span> <span class="key">in</span> <span class="nam">idx</span> <span class="key">and</span> <span class="nam">idx</span><span class="op">[</span><span class="str">'data'</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="nam">price</span> <span class="op">=</span> <span class="nam">idx</span><span class="op">[</span><span class="str">'data'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'price'</span><span class="op">,</span> <span class="str">'N/A'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="nam">change_pct</span> <span class="op">=</span> <span class="nam">idx</span><span class="op">[</span><span class="str">'data'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'change_percent'</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="nam">emoji</span> <span class="op">=</span> <span class="str">'&#128200;'</span> <span class="key">if</span> <span class="nam">change_pct</span> <span class="op">>=</span> <span class="num">0</span> <span class="key">else</span> <span class="str">'&#128201;'</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"- {idx['name']}: {price} ({change_pct:+.2f}%) {emoji}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"> <span class="key">return</span> <span class="str">'\n'</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">lines</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"><span class="key">def</span> <span class="nam">format_headlines</span><span class="op">(</span><span class="nam">headlines</span><span class="op">:</span> <span class="nam">list</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"> <span class="str">"""Format headlines for research prompt."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"> <span class="nam">lines</span> <span class="op">=</span> <span class="op">[</span><span class="str">"## Current Headlines\n"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="key">for</span> <span class="nam">article</span> <span class="key">in</span> <span class="nam">headlines</span><span class="op">[</span><span class="op">:</span><span class="num">20</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="nam">source</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'source'</span><span class="op">,</span> <span class="str">'Unknown'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="nam">title</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'title'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="nam">link</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'link'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"- [{source}] {title}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"> <span class="key">if</span> <span class="nam">link</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f" URL: {link}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="key">return</span> <span class="str">'\n'</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">lines</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"><span class="key">def</span> <span class="nam">format_portfolio_news</span><span class="op">(</span><span class="nam">portfolio_data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="str">"""Format portfolio news for research prompt."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="nam">lines</span> <span class="op">=</span> <span class="op">[</span><span class="str">"## Portfolio Analysis\n"</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"> <span class="key">for</span> <span class="nam">symbol</span><span class="op">,</span> <span class="nam">data</span> <span class="key">in</span> <span class="nam">portfolio_data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'stocks'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="nam">quote</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'quote'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="nam">price</span> <span class="op">=</span> <span class="nam">quote</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'price'</span><span class="op">,</span> <span class="str">'N/A'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="nam">change_pct</span> <span class="op">=</span> <span class="nam">quote</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'change_percent'</span><span class="op">,</span> <span class="num">0</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"### {symbol} (${price}, {change_pct:+.2f}%)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="key">for</span> <span class="nam">article</span> <span class="key">in</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'articles'</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span><span class="op">[</span><span class="op">:</span><span class="num">5</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="nam">title</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'title'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="nam">link</span> <span class="op">=</span> <span class="nam">article</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'link'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f"- {title}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"> <span class="key">if</span> <span class="nam">link</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">f" URL: {link}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"> <span class="nam">lines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="key">return</span> <span class="str">'\n'</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">lines</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"><span class="key">def</span> <span class="nam">gemini_available</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="nam">bool</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="key">return</span> <span class="nam">shutil</span><span class="op">.</span><span class="nam">which</span><span class="op">(</span><span class="str">'gemini'</span><span class="op">)</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t"><span class="key">def</span> <span class="nam">research_with_gemini</span><span class="op">(</span><span class="nam">content</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">focus_areas</span><span class="op">:</span> <span class="nam">list</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="str">"""Perform deep research using Gemini CLI.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"><span class="str"> </span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"><span class="str"> Args:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"><span class="str"> content: Combined market/headlines/portfolio content</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"><span class="str"> focus_areas: Optional list of focus areas (e.g., ['earnings', 'macro', 'sectors'])</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"><span class="str"> </span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t"><span class="str"> Returns:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"><span class="str"> Research report text</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"><span class="str"> """</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="nam">focus_prompt</span> <span class="op">=</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="key">if</span> <span class="nam">focus_areas</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="nam">focus_prompt</span> <span class="op">=</span> <span class="str">f"""</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"><span class="str">Focus areas for the research:</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"><span class="str">{', '.join(f'- {area}' for area in focus_areas)}</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"><span class="str">Go deep on each area.</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t"> <span class="nam">prompt</span> <span class="op">=</span> <span class="str">f"""You are an experienced investment research analyst.</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"><span class="str">Your task is to deliver deep research on current market developments.</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"><span class="str">{focus_prompt}</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"><span class="str">Please analyze the following market data:</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"><span class="str">{content}</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t"><span class="str">## Analysis Requirements:</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"><span class="str">1. **Macro Trends**: What is driving the market today? Which economic data/decisions matter?</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"><span class="str">2. **Sector Analysis**: Which sectors are performing best/worst? Why?</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"><span class="str">3. **Company News**: Relevant earnings, M&amp;A, product launches?</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t"><span class="str">4. **Risks**: What downside risks should be noted?</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"><span class="str">5. **Opportunities**: Which positive developments offer opportunities?</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"><span class="str">6. **Correlations**: Are there links between different news items/asset classes?</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"><span class="str">7. **Trade Ideas**: Concrete setups based on the analysis (not financial advice!)</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"><span class="str">8. **Sources**: Original links for further research</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"><span class="str">Be analytical, objective, and opinionated where appropriate.</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"><span class="str">Deliver a substantial report (500-800 words).</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">run</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"> <span class="op">[</span><span class="str">'gemini'</span><span class="op">,</span> <span class="nam">prompt</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="nam">capture_output</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"> <span class="nam">text</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="nam">timeout</span><span class="op">=</span><span class="num">120</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="key">if</span> <span class="nam">result</span><span class="op">.</span><span class="nam">returncode</span> <span class="op">==</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="key">return</span> <span class="str">f"&#9888;&#65039; Gemini research error: {result.stderr}"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="key">except</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">TimeoutExpired</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="key">return</span> <span class="str">"&#9888;&#65039; Gemini research timeout"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"> <span class="key">except</span> <span class="nam">FileNotFoundError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="key">return</span> <span class="str">"&#9888;&#65039; Gemini CLI not found. Install: brew install gemini-cli"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"><span class="key">def</span> <span class="nam">format_raw_data_report</span><span class="op">(</span><span class="nam">market_data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">,</span> <span class="nam">portfolio_data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="nam">parts</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t"> <span class="key">if</span> <span class="nam">market_data</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="nam">parts</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">format_market_data</span><span class="op">(</span><span class="nam">market_data</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="key">if</span> <span class="nam">market_data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'headlines'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="nam">parts</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">format_headlines</span><span class="op">(</span><span class="nam">market_data</span><span class="op">[</span><span class="str">'headlines'</span><span class="op">]</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t"> <span class="key">if</span> <span class="nam">portfolio_data</span> <span class="key">and</span> <span class="str">'error'</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">portfolio_data</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t"> <span class="nam">parts</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">format_portfolio_news</span><span class="op">(</span><span class="nam">portfolio_data</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t"> <span class="key">return</span> <span class="str">'\n\n'</span><span class="op">.</span><span class="nam">join</span><span class="op">(</span><span class="nam">parts</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t"><span class="key">def</span> <span class="nam">generate_research_content</span><span class="op">(</span><span class="nam">market_data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">,</span> <span class="nam">portfolio_data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">,</span> <span class="nam">focus_areas</span><span class="op">:</span> <span class="nam">list</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t"> <span class="nam">raw_report</span> <span class="op">=</span> <span class="nam">format_raw_data_report</span><span class="op">(</span><span class="nam">market_data</span><span class="op">,</span> <span class="nam">portfolio_data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">raw_report</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t"> <span class="str">'report'</span><span class="op">:</span> <span class="str">''</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t"> <span class="str">'source'</span><span class="op">:</span> <span class="str">'none'</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t"> <span class="key">if</span> <span class="nam">gemini_available</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t"> <span class="str">'report'</span><span class="op">:</span> <span class="nam">research_with_gemini</span><span class="op">(</span><span class="nam">raw_report</span><span class="op">,</span> <span class="nam">focus_areas</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t"> <span class="str">'source'</span><span class="op">:</span> <span class="str">'gemini'</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t"> <span class="str">'report'</span><span class="op">:</span> <span class="nam">raw_report</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t"> <span class="str">'source'</span><span class="op">:</span> <span class="str">'raw'</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"><span class="key">def</span> <span class="nam">generate_research_report</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t"> <span class="str">"""Generate full research report."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t"> <span class="nam">OUTPUT_DIR</span><span class="op">.</span><span class="nam">mkdir</span><span class="op">(</span><span class="nam">parents</span><span class="op">=</span><span class="key">True</span><span class="op">,</span> <span class="nam">exist_ok</span><span class="op">=</span><span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t"> <span class="nam">config_path</span> <span class="op">=</span> <span class="nam">CONFIG_DIR</span> <span class="op">/</span> <span class="str">"config.json"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">config_path</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#9888;&#65039; No config found. Run 'finance-news wizard' first."</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="nam">sys</span><span class="op">.</span><span class="nam">exit</span><span class="op">(</span><span class="num">1</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t"> <span class="com"># Fetch fresh data</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128225; Fetching market data..."</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t"> <span class="com"># Get market overview</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t"> <span class="nam">market_data</span> <span class="op">=</span> <span class="nam">get_market_news</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">limit</span> <span class="key">if</span> <span class="nam">hasattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'limit'</span><span class="op">)</span> <span class="key">else</span> <span class="num">5</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t"> <span class="nam">regions</span><span class="op">=</span><span class="nam">args</span><span class="op">.</span><span class="nam">regions</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="str">','</span><span class="op">)</span> <span class="key">if</span> <span class="nam">hasattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'regions'</span><span class="op">)</span> <span class="key">else</span> <span class="op">[</span><span class="str">"us"</span><span class="op">,</span> <span class="str">"europe"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="nam">max_indices_per_region</span><span class="op">=</span><span class="num">2</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t"> <span class="com"># Get portfolio news</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"> <span class="nam">portfolio_data</span> <span class="op">=</span> <span class="nam">get_portfolio_news</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">limit</span> <span class="key">if</span> <span class="nam">hasattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'limit'</span><span class="op">)</span> <span class="key">else</span> <span class="num">5</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">max_stocks</span> <span class="key">if</span> <span class="nam">hasattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'max_stocks'</span><span class="op">)</span> <span class="key">else</span> <span class="num">10</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t"> <span class="key">except</span> <span class="nam">PortfolioError</span> <span class="key">as</span> <span class="nam">exc</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Skipping portfolio: {exc}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t"> <span class="nam">portfolio_data</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="com"># Build report</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t"> <span class="nam">focus_areas</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t"> <span class="key">if</span> <span class="nam">hasattr</span><span class="op">(</span><span class="nam">args</span><span class="op">,</span> <span class="str">'focus'</span><span class="op">)</span> <span class="key">and</span> <span class="nam">args</span><span class="op">.</span><span class="nam">focus</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t"> <span class="nam">focus_areas</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">focus</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="str">','</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t"> <span class="nam">research_result</span> <span class="op">=</span> <span class="nam">generate_research_content</span><span class="op">(</span><span class="nam">market_data</span><span class="op">,</span> <span class="nam">portfolio_data</span><span class="op">,</span> <span class="nam">focus_areas</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t"> <span class="nam">research_report</span> <span class="op">=</span> <span class="nam">research_result</span><span class="op">[</span><span class="str">'report'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t"> <span class="nam">source</span> <span class="op">=</span> <span class="nam">research_result</span><span class="op">[</span><span class="str">'source'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">research_report</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#9888;&#65039; No data available for research"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="key">if</span> <span class="nam">source</span> <span class="op">==</span> <span class="str">'gemini'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128300; Running deep research with Gemini..."</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#129534; Gemini not available; using raw data report"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t"> <span class="com"># Add metadata header</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t"> <span class="nam">timestamp</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">isoformat</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t"> <span class="nam">date_str</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d %H:%M"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t"> <span class="nam">full_report</span> <span class="op">=</span> <span class="str">f"""# Market Research Report</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t"><span class="str">**Generiert:** {date_str}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t"><span class="str">**Quelle:** Finance News Skill</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"><span class="str">---</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t"><span class="str">{research_report}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t"><span class="str">---</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t"><span class="str">*This report was generated automatically. Not financial advice.*</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="com"># Save to file</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t"> <span class="nam">output_file</span> <span class="op">=</span> <span class="nam">OUTPUT_DIR</span> <span class="op">/</span> <span class="str">f"research_{datetime.now().strftime('%Y-%m-%d')}.md"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">output_file</span><span class="op">,</span> <span class="str">'w'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t"> <span class="nam">f</span><span class="op">.</span><span class="nam">write</span><span class="op">(</span><span class="nam">full_report</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Research report saved to: {output_file}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t"> <span class="com"># Also output to stdout</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">json</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t"> <span class="str">'report'</span><span class="op">:</span> <span class="nam">research_report</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"> <span class="str">'saved_to'</span><span class="op">:</span> <span class="nam">str</span><span class="op">(</span><span class="nam">output_file</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t"> <span class="str">'timestamp'</span><span class="op">:</span> <span class="nam">timestamp</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t"> <span class="op">}</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n"</span> <span class="op">+</span> <span class="str">"="</span><span class="op">*</span><span class="num">60</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"RESEARCH REPORT"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"="</span><span class="op">*</span><span class="num">60</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">research_report</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">'Deep Market Research'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--limit'</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">int</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="num">5</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Max headlines per source'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--regions'</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">'us,europe'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Comma-separated regions'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--max-stocks'</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">int</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="num">10</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Max portfolio stocks'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--focus'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Focus areas (comma-separated)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--json'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Output as JSON'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t"> <span class="nam">generate_research_report</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">'__main__'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_ranking_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_setup_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

387
htmlcov/z_de1a740d5dc98ffd_setup_py.html generated Normal file
View File

@@ -0,0 +1,387 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/setup.py: 26%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;setup.py</b>:
<span class="pc_cov">26%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">168 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">44<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">124<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_research_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_stocks_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">Finance News Skill - Interactive Setup</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t"><span class="str">Configures RSS feeds, WhatsApp channels, and cron jobs.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="key">import</span> <span class="nam">subprocess</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="nam">SCRIPT_DIR</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="nam">CONFIG_DIR</span> <span class="op">=</span> <span class="nam">SCRIPT_DIR</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"config"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="nam">SOURCES_FILE</span> <span class="op">=</span> <span class="nam">CONFIG_DIR</span> <span class="op">/</span> <span class="str">"config.json"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"><span class="key">def</span> <span class="nam">load_sources</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"> <span class="str">"""Load current sources configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"> <span class="key">if</span> <span class="nam">SOURCES_FILE</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">SOURCES_FILE</span><span class="op">,</span> <span class="str">'r'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"> <span class="key">return</span> <span class="nam">json</span><span class="op">.</span><span class="nam">load</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"> <span class="key">return</span> <span class="nam">get_default_sources</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"><span class="key">def</span> <span class="nam">save_sources</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t"> <span class="str">"""Save sources configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"> <span class="nam">CONFIG_DIR</span><span class="op">.</span><span class="nam">mkdir</span><span class="op">(</span><span class="nam">parents</span><span class="op">=</span><span class="key">True</span><span class="op">,</span> <span class="nam">exist_ok</span><span class="op">=</span><span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">SOURCES_FILE</span><span class="op">,</span> <span class="str">'w'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"> <span class="nam">json</span><span class="op">.</span><span class="nam">dump</span><span class="op">(</span><span class="nam">sources</span><span class="op">,</span> <span class="nam">f</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Configuration saved to {SOURCES_FILE}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"><span class="key">def</span> <span class="nam">get_default_sources</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="str">"""Return default source configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="nam">config_path</span> <span class="op">=</span> <span class="nam">CONFIG_DIR</span> <span class="op">/</span> <span class="str">"config.json"</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="key">if</span> <span class="nam">config_path</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">config_path</span><span class="op">,</span> <span class="str">'r'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="key">return</span> <span class="nam">json</span><span class="op">.</span><span class="nam">load</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"><span class="key">def</span> <span class="nam">prompt</span><span class="op">(</span><span class="nam">message</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">default</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"> <span class="str">"""Prompt for input with optional default."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"> <span class="key">if</span> <span class="nam">default</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">input</span><span class="op">(</span><span class="str">f"{message} [{default}]: "</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span> <span class="key">if</span> <span class="nam">result</span> <span class="key">else</span> <span class="nam">default</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"> <span class="key">return</span> <span class="nam">input</span><span class="op">(</span><span class="str">f"{message}: "</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"><span class="key">def</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="nam">message</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">default</span><span class="op">:</span> <span class="nam">bool</span> <span class="op">=</span> <span class="key">True</span><span class="op">)</span> <span class="op">-></span> <span class="nam">bool</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="str">"""Prompt for yes/no input."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="nam">default_str</span> <span class="op">=</span> <span class="str">"Y/n"</span> <span class="key">if</span> <span class="nam">default</span> <span class="key">else</span> <span class="str">"y/N"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">input</span><span class="op">(</span><span class="str">f"{message} [{default_str}]: "</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">lower</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">result</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="key">return</span> <span class="nam">default</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span> <span class="key">in</span> <span class="op">(</span><span class="str">'y'</span><span class="op">,</span> <span class="str">'yes'</span><span class="op">,</span> <span class="str">'1'</span><span class="op">,</span> <span class="str">'true'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"><span class="key">def</span> <span class="nam">setup_rss_feeds</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"> <span class="str">"""Interactive RSS feed configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#128240; RSS Feed Configuration\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"Enable/disable news sources:\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"> <span class="key">for</span> <span class="nam">feed_id</span><span class="op">,</span> <span class="nam">feed_config</span> <span class="key">in</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'rss_feeds'</span><span class="op">]</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="nam">name</span> <span class="op">=</span> <span class="nam">feed_config</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'name'</span><span class="op">,</span> <span class="nam">feed_id</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="nam">current</span> <span class="op">=</span> <span class="nam">feed_config</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="nam">enabled</span> <span class="op">=</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">f" {name}"</span><span class="op">,</span> <span class="nam">current</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'rss_feeds'</span><span class="op">]</span><span class="op">[</span><span class="nam">feed_id</span><span class="op">]</span><span class="op">[</span><span class="str">'enabled'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">enabled</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n Add custom RSS feed? (leave blank to skip)"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="nam">custom_name</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">" Feed name"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="key">if</span> <span class="nam">custom_name</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="nam">custom_url</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">" Feed URL"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'rss_feeds'</span><span class="op">]</span><span class="op">[</span><span class="nam">custom_name</span><span class="op">.</span><span class="nam">lower</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">replace</span><span class="op">(</span><span class="str">' '</span><span class="op">,</span> <span class="str">'_'</span><span class="op">)</span><span class="op">]</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">custom_name</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"> <span class="str">"enabled"</span><span class="op">:</span> <span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"> <span class="str">"main"</span><span class="op">:</span> <span class="nam">custom_url</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#9989; Added {custom_name}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"><span class="key">def</span> <span class="nam">setup_markets</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="str">"""Interactive market configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#128202; Market Coverage\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"Enable/disable market regions:\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="key">for</span> <span class="nam">market_id</span><span class="op">,</span> <span class="nam">market_config</span> <span class="key">in</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'markets'</span><span class="op">]</span><span class="op">.</span><span class="nam">items</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="nam">name</span> <span class="op">=</span> <span class="nam">market_config</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'name'</span><span class="op">,</span> <span class="nam">market_id</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="nam">current</span> <span class="op">=</span> <span class="nam">market_config</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="nam">enabled</span> <span class="op">=</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">f" {name}"</span><span class="op">,</span> <span class="nam">current</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'markets'</span><span class="op">]</span><span class="op">[</span><span class="nam">market_id</span><span class="op">]</span><span class="op">[</span><span class="str">'enabled'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">enabled</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"><span class="key">def</span> <span class="nam">setup_delivery</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"> <span class="str">"""Interactive delivery channel configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#128228; Delivery Channels\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="com"># Ensure delivery dict exists</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"> <span class="key">if</span> <span class="str">'delivery'</span> <span class="key">not</span> <span class="key">in</span> <span class="nam">sources</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span> <span class="op">=</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t"> <span class="str">'whatsapp'</span><span class="op">:</span> <span class="op">{</span><span class="str">'enabled'</span><span class="op">:</span> <span class="key">True</span><span class="op">,</span> <span class="str">'group'</span><span class="op">:</span> <span class="str">''</span><span class="op">}</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="str">'telegram'</span><span class="op">:</span> <span class="op">{</span><span class="str">'enabled'</span><span class="op">:</span> <span class="key">False</span><span class="op">,</span> <span class="str">'group'</span><span class="op">:</span> <span class="str">''</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t"> <span class="com"># WhatsApp</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"> <span class="nam">wa_enabled</span> <span class="op">=</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">"Enable WhatsApp delivery"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="nam">sources</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'delivery'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'whatsapp'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'whatsapp'</span><span class="op">]</span><span class="op">[</span><span class="str">'enabled'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">wa_enabled</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="key">if</span> <span class="nam">wa_enabled</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"> <span class="nam">wa_group</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">" WhatsApp group name or JID"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'whatsapp'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'group'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'whatsapp'</span><span class="op">]</span><span class="op">[</span><span class="str">'group'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">wa_group</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t"> <span class="com"># Telegram</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"> <span class="nam">tg_enabled</span> <span class="op">=</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">"Enable Telegram delivery"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'telegram'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">,</span> <span class="key">False</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'telegram'</span><span class="op">]</span><span class="op">[</span><span class="str">'enabled'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">tg_enabled</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="key">if</span> <span class="nam">tg_enabled</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t"> <span class="nam">tg_group</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">" Telegram group name or ID"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'telegram'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'group'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'delivery'</span><span class="op">]</span><span class="op">[</span><span class="str">'telegram'</span><span class="op">]</span><span class="op">[</span><span class="str">'group'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">tg_group</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"><span class="key">def</span> <span class="nam">setup_language</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="str">"""Interactive language configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#127760; Language Settings\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="nam">current_lang</span> <span class="op">=</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'language'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'default'</span><span class="op">,</span> <span class="str">'de'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t"> <span class="nam">lang</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">"Default language (de/en)"</span><span class="op">,</span> <span class="nam">current_lang</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"> <span class="key">if</span> <span class="nam">lang</span> <span class="key">in</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'language'</span><span class="op">]</span><span class="op">[</span><span class="str">'supported'</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'language'</span><span class="op">]</span><span class="op">[</span><span class="str">'default'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">lang</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#9888;&#65039; Unsupported language '{lang}', keeping '{current_lang}'"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"><span class="key">def</span> <span class="nam">setup_schedule</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="str">"""Interactive schedule configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#9200; Briefing Schedule\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="com"># Morning</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="nam">morning</span> <span class="op">=</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="nam">morning_enabled</span> <span class="op">=</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">f"Enable morning briefing ({morning['description']})"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="nam">morning</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span><span class="op">[</span><span class="str">'enabled'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">morning_enabled</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t"> <span class="key">if</span> <span class="nam">morning_enabled</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="nam">morning_cron</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">" Morning cron expression"</span><span class="op">,</span> <span class="nam">morning</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'cron'</span><span class="op">,</span> <span class="str">'30 6 * * 1-5'</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span><span class="op">[</span><span class="str">'cron'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">morning_cron</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="com"># Evening</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="nam">evening</span> <span class="op">=</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'evening'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t"> <span class="nam">evening_enabled</span> <span class="op">=</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">f"Enable evening briefing ({evening['description']})"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"> <span class="nam">evening</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'evening'</span><span class="op">]</span><span class="op">[</span><span class="str">'enabled'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">evening_enabled</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="key">if</span> <span class="nam">evening_enabled</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="nam">evening_cron</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">" Evening cron expression"</span><span class="op">,</span> <span class="nam">evening</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'cron'</span><span class="op">,</span> <span class="str">'0 13 * * 1-5'</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'evening'</span><span class="op">]</span><span class="op">[</span><span class="str">'cron'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">evening_cron</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t"> <span class="com"># Timezone</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t"> <span class="nam">tz</span> <span class="op">=</span> <span class="nam">prompt</span><span class="op">(</span><span class="str">"Timezone"</span><span class="op">,</span> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'timezone'</span><span class="op">,</span> <span class="str">'America/Los_Angeles'</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span><span class="op">[</span><span class="str">'timezone'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">tz</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t"> <span class="nam">sources</span><span class="op">[</span><span class="str">'schedule'</span><span class="op">]</span><span class="op">[</span><span class="str">'evening'</span><span class="op">]</span><span class="op">[</span><span class="str">'timezone'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">tz</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"><span class="key">def</span> <span class="nam">setup_cron_jobs</span><span class="op">(</span><span class="nam">sources</span><span class="op">:</span> <span class="nam">dict</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="str">"""Set up OpenClaw cron jobs based on configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#128197; Setting up cron jobs...\n"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="nam">schedule</span> <span class="op">=</span> <span class="nam">sources</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'schedule'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t"> <span class="nam">delivery</span> <span class="op">=</span> <span class="nam">sources</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'delivery'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t"> <span class="nam">language</span> <span class="op">=</span> <span class="nam">sources</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'language'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'default'</span><span class="op">,</span> <span class="str">'de'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t"> <span class="com"># Determine delivery target</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="key">if</span> <span class="nam">delivery</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'whatsapp'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t"> <span class="nam">group</span> <span class="op">=</span> <span class="nam">delivery</span><span class="op">[</span><span class="str">'whatsapp'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'group'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t"> <span class="nam">send_cmd</span> <span class="op">=</span> <span class="str">f"--send --group '{group}'"</span> <span class="key">if</span> <span class="nam">group</span> <span class="key">else</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t"> <span class="key">elif</span> <span class="nam">delivery</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'telegram'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t"> <span class="nam">group</span> <span class="op">=</span> <span class="nam">delivery</span><span class="op">[</span><span class="str">'telegram'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'group'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t"> <span class="nam">send_cmd</span> <span class="op">=</span> <span class="str">f"--send --group '{group}'"</span> <span class="com"># Would need telegram support</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"> <span class="nam">send_cmd</span> <span class="op">=</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t"> <span class="com"># Morning job</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t"> <span class="key">if</span> <span class="nam">schedule</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'morning'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t"> <span class="nam">morning_cron</span> <span class="op">=</span> <span class="nam">schedule</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'cron'</span><span class="op">,</span> <span class="str">'30 6 * * 1-5'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"> <span class="nam">tz</span> <span class="op">=</span> <span class="nam">schedule</span><span class="op">[</span><span class="str">'morning'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'timezone'</span><span class="op">,</span> <span class="str">'America/Los_Angeles'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" Creating morning briefing job: {morning_cron} ({tz})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t"> <span class="com"># Note: Actual cron creation would happen via openclaw cron add</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#9989; Morning briefing configured"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t"> <span class="com"># Evening job</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t"> <span class="key">if</span> <span class="nam">schedule</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'evening'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'enabled'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t"> <span class="nam">evening_cron</span> <span class="op">=</span> <span class="nam">schedule</span><span class="op">[</span><span class="str">'evening'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'cron'</span><span class="op">,</span> <span class="str">'0 13 * * 1-5'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t"> <span class="nam">tz</span> <span class="op">=</span> <span class="nam">schedule</span><span class="op">[</span><span class="str">'evening'</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'timezone'</span><span class="op">,</span> <span class="str">'America/Los_Angeles'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" Creating evening briefing job: {evening_cron} ({tz})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" &#9989; Evening briefing configured"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t"><span class="key">def</span> <span class="nam">run_setup</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"> <span class="str">"""Run interactive setup wizard."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n"</span> <span class="op">+</span> <span class="str">"="</span><span class="op">*</span><span class="num">60</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#128200; Finance News Skill - Setup Wizard"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"="</span><span class="op">*</span><span class="num">60</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t"> <span class="com"># Load existing or default config</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">reset</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t"> <span class="nam">sources</span> <span class="op">=</span> <span class="nam">get_default_sources</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#9888;&#65039; Starting with fresh configuration"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t"> <span class="nam">sources</span> <span class="op">=</span> <span class="nam">load_sources</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t"> <span class="key">if</span> <span class="nam">SOURCES_FILE</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"\n&#128194; Loaded existing configuration from {SOURCES_FILE}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#128194; No existing configuration found, using defaults"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t"> <span class="com"># Run through each section</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="op">==</span> <span class="str">'feeds'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t"> <span class="nam">setup_rss_feeds</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="op">==</span> <span class="str">'markets'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="nam">setup_markets</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="op">==</span> <span class="str">'delivery'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t"> <span class="nam">setup_delivery</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="op">==</span> <span class="str">'language'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t"> <span class="nam">setup_language</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="op">==</span> <span class="str">'schedule'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t"> <span class="nam">setup_schedule</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t"> <span class="com"># Save configuration</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n"</span> <span class="op">+</span> <span class="str">"-"</span><span class="op">*</span><span class="num">60</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"> <span class="key">if</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">"Save configuration?"</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t"> <span class="nam">save_sources</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t"> <span class="com"># Set up cron jobs</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t"> <span class="key">if</span> <span class="nam">prompt_bool</span><span class="op">(</span><span class="str">"Set up cron jobs now?"</span><span class="op">,</span> <span class="key">True</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t"> <span class="nam">setup_cron_jobs</span><span class="op">(</span><span class="nam">sources</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#10060; Configuration not saved"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\n&#9989; Setup complete!"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"\nNext steps:"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">" &#8226; Run 'finance-news portfolio-list' to check your watchlist"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">" &#8226; Run 'finance-news briefing --morning' to test a briefing"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">" &#8226; Run 'finance-news market' to see market overview"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"><span class="key">def</span> <span class="nam">show_config</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t"> <span class="str">"""Show current configuration."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t"> <span class="nam">sources</span> <span class="op">=</span> <span class="nam">load_sources</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">sources</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">'Finance News Setup'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t"> <span class="nam">subparsers</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_subparsers</span><span class="op">(</span><span class="nam">dest</span><span class="op">=</span><span class="str">'command'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t"> <span class="com"># Setup command (default)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t"> <span class="nam">setup_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'wizard'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Run setup wizard'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t"> <span class="nam">setup_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--reset'</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">'store_true'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Reset to defaults'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"> <span class="nam">setup_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--section'</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">'feeds'</span><span class="op">,</span> <span class="str">'markets'</span><span class="op">,</span> <span class="str">'delivery'</span><span class="op">,</span> <span class="str">'language'</span><span class="op">,</span> <span class="str">'schedule'</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t"> <span class="nam">help</span><span class="op">=</span><span class="str">'Configure specific section only'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="nam">setup_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">run_setup</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t"> <span class="com"># Show config</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t"> <span class="nam">show_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">'show'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Show current configuration'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"> <span class="nam">show_parser</span><span class="op">.</span><span class="nam">set_defaults</span><span class="op">(</span><span class="nam">func</span><span class="op">=</span><span class="nam">show_config</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">func</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t"> <span class="com"># Default to wizard</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t284" href="#t284">284</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">reset</span> <span class="op">=</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t285" href="#t285">285</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">section</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t286" href="#t286">286</a></span><span class="t"> <span class="nam">run_setup</span><span class="op">(</span><span class="nam">args</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t287" href="#t287">287</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t288" href="#t288">288</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t289" href="#t289">289</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">'__main__'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t290" href="#t290">290</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_research_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_stocks_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

432
htmlcov/z_de1a740d5dc98ffd_stocks_py.html generated Normal file
View File

@@ -0,0 +1,432 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/stocks.py: 53%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;stocks.py</b>:
<span class="pc_cov">53%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">184 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">97<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">87<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_setup_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_summarize_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="str">stocks.py - Unified stock management for holdings and watchlist.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="str">Single source of truth for:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="str">- Holdings (stocks you own)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="str">- Watchlist (stocks you're watching to buy)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="str">Usage:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"><span class="str"> from stocks import load_stocks, save_stocks, get_holdings, get_watchlist</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="str"> from stocks import add_to_watchlist, add_to_holdings, move_to_holdings</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="str">CLI:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="str"> stocks.py list [--holdings|--watchlist]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"><span class="str"> stocks.py add-watchlist TICKER [--target 380] [--notes "Buy zone"]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"><span class="str"> stocks.py add-holding TICKER --name "Company" [--category "Tech"]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="str"> stocks.py move TICKER # watchlist &#8594; holdings (you bought it)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"><span class="str"> stocks.py remove TICKER [--from holdings|watchlist]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"><span class="key">from</span> <span class="nam">datetime</span> <span class="key">import</span> <span class="nam">datetime</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"><span class="key">from</span> <span class="nam">typing</span> <span class="key">import</span> <span class="nam">Optional</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"><span class="com"># Default path - can be overridden</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"><span class="nam">STOCKS_FILE</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"config"</span> <span class="op">/</span> <span class="str">"stocks.json"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t"><span class="key">def</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="nam">path</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">Path</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">dict</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"> <span class="str">"""Load the unified stocks file."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"> <span class="nam">path</span> <span class="op">=</span> <span class="nam">path</span> <span class="key">or</span> <span class="nam">STOCKS_FILE</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">path</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="str">"version"</span><span class="op">:</span> <span class="str">"1.0"</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t"> <span class="str">"updated"</span><span class="op">:</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="str">"holdings"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="str">"watchlist"</span><span class="op">:</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="str">"alert_definitions"</span><span class="op">:</span> <span class="op">{</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"> <span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">path</span><span class="op">,</span> <span class="str">'r'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"> <span class="key">return</span> <span class="nam">json</span><span class="op">.</span><span class="nam">load</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t"><span class="key">def</span> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">dict</span><span class="op">,</span> <span class="nam">path</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">Path</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t"> <span class="str">"""Save the unified stocks file."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="nam">path</span> <span class="op">=</span> <span class="nam">path</span> <span class="key">or</span> <span class="nam">STOCKS_FILE</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"updated"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">datetime</span><span class="op">.</span><span class="nam">now</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">strftime</span><span class="op">(</span><span class="str">"%Y-%m-%d"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">path</span><span class="op">,</span> <span class="str">'w'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="nam">json</span><span class="op">.</span><span class="nam">dump</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">f</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"><span class="key">def</span> <span class="nam">get_holdings</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="str">"""Get list of holdings."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t"> <span class="key">if</span> <span class="nam">data</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"> <span class="key">return</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"holdings"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t"><span class="key">def</span> <span class="nam">get_watchlist</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t"> <span class="str">"""Get list of watchlist items."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="key">if</span> <span class="nam">data</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="key">return</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"watchlist"</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"><span class="key">def</span> <span class="nam">get_holding_tickers</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">set</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="str">"""Get set of holding tickers for quick lookup."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="nam">holdings</span> <span class="op">=</span> <span class="nam">get_holdings</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="nam">h</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">holdings</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t"><span class="key">def</span> <span class="nam">get_watchlist_tickers</span><span class="op">(</span><span class="nam">data</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">dict</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">set</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"> <span class="str">"""Get set of watchlist tickers for quick lookup."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="nam">watchlist</span> <span class="op">=</span> <span class="nam">get_watchlist</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="key">return</span> <span class="op">{</span><span class="nam">w</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="key">for</span> <span class="nam">w</span> <span class="key">in</span> <span class="nam">watchlist</span><span class="op">}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"><span class="key">def</span> <span class="nam">add_to_watchlist</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="nam">ticker</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"> <span class="nam">target</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">float</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t"> <span class="nam">stop</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">float</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t"> <span class="nam">notes</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="nam">alerts</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">list</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"><span class="op">)</span> <span class="op">-></span> <span class="nam">bool</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="str">"""Add a stock to the watchlist."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="com"># Check if already in watchlist</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t"> <span class="key">for</span> <span class="nam">w</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"> <span class="key">if</span> <span class="nam">w</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="op">==</span> <span class="nam">ticker</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"> <span class="com"># Update existing</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="key">if</span> <span class="nam">target</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="nam">w</span><span class="op">[</span><span class="str">"target"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">target</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="key">if</span> <span class="nam">stop</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"> <span class="nam">w</span><span class="op">[</span><span class="str">"stop"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">stop</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"> <span class="key">if</span> <span class="nam">notes</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t"> <span class="nam">w</span><span class="op">[</span><span class="str">"notes"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">notes</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="key">if</span> <span class="nam">alerts</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="nam">w</span><span class="op">[</span><span class="str">"alerts"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="com"># Add new</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="str">"target"</span><span class="op">:</span> <span class="nam">target</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"> <span class="str">"stop"</span><span class="op">:</span> <span class="nam">stop</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="str">"alerts"</span><span class="op">:</span> <span class="nam">alerts</span> <span class="key">or</span> <span class="op">[</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t"> <span class="str">"notes"</span><span class="op">:</span> <span class="nam">notes</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">.</span><span class="nam">sort</span><span class="op">(</span><span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"><span class="key">def</span> <span class="nam">add_to_holdings</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t"> <span class="nam">ticker</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t"> <span class="nam">name</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="nam">category</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"> <span class="nam">notes</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t"> <span class="nam">target</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">float</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"> <span class="nam">stop</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">float</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="nam">alerts</span><span class="op">:</span> <span class="nam">Optional</span><span class="op">[</span><span class="nam">list</span><span class="op">]</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"><span class="op">)</span> <span class="op">-></span> <span class="nam">bool</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t"> <span class="str">"""Add a stock to holdings. Target/stop for 'buy more' alerts."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"> <span class="com"># Check if already in holdings</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="key">if</span> <span class="nam">h</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="op">==</span> <span class="nam">ticker</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t"> <span class="com"># Update existing</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="key">if</span> <span class="nam">name</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"name"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">name</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"> <span class="key">if</span> <span class="nam">category</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"category"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">category</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"> <span class="key">if</span> <span class="nam">notes</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"notes"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">notes</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t"> <span class="key">if</span> <span class="nam">target</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"target"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">target</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="key">if</span> <span class="nam">stop</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"stop"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">stop</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="key">if</span> <span class="nam">alerts</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"alerts"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">alerts</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"> <span class="com"># Add new</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">name</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t"> <span class="str">"category"</span><span class="op">:</span> <span class="nam">category</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"> <span class="str">"notes"</span><span class="op">:</span> <span class="nam">notes</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t"> <span class="str">"target"</span><span class="op">:</span> <span class="nam">target</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t159" href="#t159">159</a></span><span class="t"> <span class="str">"stop"</span><span class="op">:</span> <span class="nam">stop</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t160" href="#t160">160</a></span><span class="t"> <span class="str">"alerts"</span><span class="op">:</span> <span class="nam">alerts</span> <span class="key">or</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t161" href="#t161">161</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t162" href="#t162">162</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">.</span><span class="nam">sort</span><span class="op">(</span><span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t163" href="#t163">163</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t164" href="#t164">164</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t165" href="#t165">165</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t166" href="#t166">166</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t167" href="#t167">167</a></span><span class="t"><span class="key">def</span> <span class="nam">move_to_holdings</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t168" href="#t168">168</a></span><span class="t"> <span class="nam">ticker</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t169" href="#t169">169</a></span><span class="t"> <span class="nam">name</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t170" href="#t170">170</a></span><span class="t"> <span class="nam">category</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t171" href="#t171">171</a></span><span class="t"> <span class="nam">notes</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">""</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t172" href="#t172">172</a></span><span class="t"><span class="op">)</span> <span class="op">-></span> <span class="nam">bool</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t173" href="#t173">173</a></span><span class="t"> <span class="str">"""Move a stock from watchlist to holdings (you bought it)."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t174" href="#t174">174</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t175" href="#t175">175</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t176" href="#t176">176</a></span><span class="t"> <span class="com"># Find in watchlist</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t177" href="#t177">177</a></span><span class="t"> <span class="nam">watchlist_item</span> <span class="op">=</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t178" href="#t178">178</a></span><span class="t"> <span class="key">for</span> <span class="nam">i</span><span class="op">,</span> <span class="nam">w</span> <span class="key">in</span> <span class="nam">enumerate</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t179" href="#t179">179</a></span><span class="t"> <span class="key">if</span> <span class="nam">w</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="op">==</span> <span class="nam">ticker</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t180" href="#t180">180</a></span><span class="t"> <span class="nam">watchlist_item</span> <span class="op">=</span> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">.</span><span class="nam">pop</span><span class="op">(</span><span class="nam">i</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t181" href="#t181">181</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t182" href="#t182">182</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t183" href="#t183">183</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">watchlist_item</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t184" href="#t184">184</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; {ticker} not found in watchlist"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t185" href="#t185">185</a></span><span class="t"> <span class="key">return</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t186" href="#t186">186</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t187" href="#t187">187</a></span><span class="t"> <span class="com"># Add to holdings</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t188" href="#t188">188</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="op">{</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t189" href="#t189">189</a></span><span class="t"> <span class="str">"ticker"</span><span class="op">:</span> <span class="nam">ticker</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t190" href="#t190">190</a></span><span class="t"> <span class="str">"name"</span><span class="op">:</span> <span class="nam">name</span> <span class="key">or</span> <span class="nam">watchlist_item</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"notes"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t191" href="#t191">191</a></span><span class="t"> <span class="str">"category"</span><span class="op">:</span> <span class="nam">category</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t192" href="#t192">192</a></span><span class="t"> <span class="str">"notes"</span><span class="op">:</span> <span class="nam">notes</span> <span class="key">or</span> <span class="str">f"Bought (was on watchlist with target ${watchlist_item.get('target', 'N/A')})"</span>&nbsp;</span><span class="r"></span></p>
<p class="run run2"><span class="n"><a id="t193" href="#t193">193</a></span><span class="t"> <span class="op">}</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t194" href="#t194">194</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">.</span><span class="nam">sort</span><span class="op">(</span><span class="nam">key</span><span class="op">=</span><span class="key">lambda</span> <span class="nam">x</span><span class="op">:</span> <span class="nam">x</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="str">""</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t195" href="#t195">195</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t196" href="#t196">196</a></span><span class="t"> <span class="key">return</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t197" href="#t197">197</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t198" href="#t198">198</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t199" href="#t199">199</a></span><span class="t"><span class="key">def</span> <span class="nam">remove_stock</span><span class="op">(</span><span class="nam">ticker</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">from_list</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">"both"</span><span class="op">)</span> <span class="op">-></span> <span class="nam">bool</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t200" href="#t200">200</a></span><span class="t"> <span class="str">"""Remove a stock from holdings, watchlist, or both."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t201" href="#t201">201</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t202" href="#t202">202</a></span><span class="t"> <span class="nam">removed</span> <span class="op">=</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t203" href="#t203">203</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t204" href="#t204">204</a></span><span class="t"> <span class="key">if</span> <span class="nam">from_list</span> <span class="key">in</span> <span class="op">(</span><span class="str">"holdings"</span><span class="op">,</span> <span class="str">"both"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t205" href="#t205">205</a></span><span class="t"> <span class="nam">original_len</span> <span class="op">=</span> <span class="nam">len</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t206" href="#t206">206</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span> <span class="op">=</span> <span class="op">[</span><span class="nam">h</span> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span> <span class="key">if</span> <span class="nam">h</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="op">!=</span> <span class="nam">ticker</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t207" href="#t207">207</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">)</span> <span class="op">&lt;</span> <span class="nam">original_len</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t208" href="#t208">208</a></span><span class="t"> <span class="nam">removed</span> <span class="op">=</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t209" href="#t209">209</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t210" href="#t210">210</a></span><span class="t"> <span class="key">if</span> <span class="nam">from_list</span> <span class="key">in</span> <span class="op">(</span><span class="str">"watchlist"</span><span class="op">,</span> <span class="str">"both"</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t211" href="#t211">211</a></span><span class="t"> <span class="nam">original_len</span> <span class="op">=</span> <span class="nam">len</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t212" href="#t212">212</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span> <span class="op">=</span> <span class="op">[</span><span class="nam">w</span> <span class="key">for</span> <span class="nam">w</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span> <span class="key">if</span> <span class="nam">w</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="op">!=</span> <span class="nam">ticker</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t213" href="#t213">213</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">)</span> <span class="op">&lt;</span> <span class="nam">original_len</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t214" href="#t214">214</a></span><span class="t"> <span class="nam">removed</span> <span class="op">=</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t215" href="#t215">215</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t216" href="#t216">216</a></span><span class="t"> <span class="key">if</span> <span class="nam">removed</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t217" href="#t217">217</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t218" href="#t218">218</a></span><span class="t"> <span class="key">return</span> <span class="nam">removed</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t219" href="#t219">219</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t220" href="#t220">220</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t221" href="#t221">221</a></span><span class="t"><span class="key">def</span> <span class="nam">list_stocks</span><span class="op">(</span><span class="nam">show_holdings</span><span class="op">:</span> <span class="nam">bool</span> <span class="op">=</span> <span class="key">True</span><span class="op">,</span> <span class="nam">show_watchlist</span><span class="op">:</span> <span class="nam">bool</span> <span class="op">=</span> <span class="key">True</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t222" href="#t222">222</a></span><span class="t"> <span class="str">"""Print stocks list."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t223" href="#t223">223</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t224" href="#t224">224</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t225" href="#t225">225</a></span><span class="t"> <span class="key">if</span> <span class="nam">show_holdings</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t226" href="#t226">226</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"\n&#128202; HOLDINGS ({len(data['holdings'])})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t227" href="#t227">227</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"-"</span> <span class="op">*</span> <span class="num">50</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t228" href="#t228">228</a></span><span class="t"> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">[</span><span class="op">:</span><span class="num">20</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t229" href="#t229">229</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" {h['ticker']:10} {h.get('name', '')[:30]}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t230" href="#t230">230</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">)</span> <span class="op">></span> <span class="num">20</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t231" href="#t231">231</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" ... and {len(data['holdings']) - 20} more"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t232" href="#t232">232</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t233" href="#t233">233</a></span><span class="t"> <span class="key">if</span> <span class="nam">show_watchlist</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t234" href="#t234">234</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"\n&#128064; WATCHLIST ({len(data['watchlist'])})"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t235" href="#t235">235</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"-"</span> <span class="op">*</span> <span class="num">50</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t236" href="#t236">236</a></span><span class="t"> <span class="key">for</span> <span class="nam">w</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">[</span><span class="op">:</span><span class="num">20</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t237" href="#t237">237</a></span><span class="t"> <span class="nam">target</span> <span class="op">=</span> <span class="str">f"${w['target']}"</span> <span class="key">if</span> <span class="nam">w</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'target'</span><span class="op">)</span> <span class="key">else</span> <span class="str">"no target"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t238" href="#t238">238</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" {w['ticker']:10} {target:>10} {w.get('notes', '')[:25]}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t239" href="#t239">239</a></span><span class="t"> <span class="key">if</span> <span class="nam">len</span><span class="op">(</span><span class="nam">data</span><span class="op">[</span><span class="str">"watchlist"</span><span class="op">]</span><span class="op">)</span> <span class="op">></span> <span class="num">20</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t240" href="#t240">240</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f" ... and {len(data['watchlist']) - 20} more"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t241" href="#t241">241</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t242" href="#t242">242</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t243" href="#t243">243</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t244" href="#t244">244</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">"Unified stock management"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t245" href="#t245">245</a></span><span class="t"> <span class="nam">subparsers</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_subparsers</span><span class="op">(</span><span class="nam">dest</span><span class="op">=</span><span class="str">"command"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Commands"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t246" href="#t246">246</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t247" href="#t247">247</a></span><span class="t"> <span class="com"># list</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t248" href="#t248">248</a></span><span class="t"> <span class="nam">list_parser</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"list"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"List stocks"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t249" href="#t249">249</a></span><span class="t"> <span class="nam">list_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--holdings"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Show only holdings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t250" href="#t250">250</a></span><span class="t"> <span class="nam">list_parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--watchlist"</span><span class="op">,</span> <span class="nam">action</span><span class="op">=</span><span class="str">"store_true"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Show only watchlist"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t251" href="#t251">251</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t252" href="#t252">252</a></span><span class="t"> <span class="com"># add-watchlist</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t253" href="#t253">253</a></span><span class="t"> <span class="nam">add_watch</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"add-watchlist"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Add to watchlist"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t254" href="#t254">254</a></span><span class="t"> <span class="nam">add_watch</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t255" href="#t255">255</a></span><span class="t"> <span class="nam">add_watch</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--target"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Target price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t256" href="#t256">256</a></span><span class="t"> <span class="nam">add_watch</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--stop"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stop loss"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t257" href="#t257">257</a></span><span class="t"> <span class="nam">add_watch</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--notes"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">""</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Notes"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t258" href="#t258">258</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t259" href="#t259">259</a></span><span class="t"> <span class="com"># add-holding</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t260" href="#t260">260</a></span><span class="t"> <span class="nam">add_hold</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"add-holding"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Add to holdings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t261" href="#t261">261</a></span><span class="t"> <span class="nam">add_hold</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t262" href="#t262">262</a></span><span class="t"> <span class="nam">add_hold</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--name"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">""</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Company name"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t263" href="#t263">263</a></span><span class="t"> <span class="nam">add_hold</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--category"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">""</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Category"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t264" href="#t264">264</a></span><span class="t"> <span class="nam">add_hold</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--notes"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">""</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Notes"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t265" href="#t265">265</a></span><span class="t"> <span class="nam">add_hold</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--target"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Buy-more target price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t266" href="#t266">266</a></span><span class="t"> <span class="nam">add_hold</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--stop"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stop loss price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t267" href="#t267">267</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t268" href="#t268">268</a></span><span class="t"> <span class="com"># move (watchlist &#8594; holdings)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t269" href="#t269">269</a></span><span class="t"> <span class="nam">move</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"move"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Move from watchlist to holdings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t270" href="#t270">270</a></span><span class="t"> <span class="nam">move</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t271" href="#t271">271</a></span><span class="t"> <span class="nam">move</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--name"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">""</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Company name"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t272" href="#t272">272</a></span><span class="t"> <span class="nam">move</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--category"</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">""</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Category"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t273" href="#t273">273</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t274" href="#t274">274</a></span><span class="t"> <span class="com"># remove</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t275" href="#t275">275</a></span><span class="t"> <span class="nam">remove</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"remove"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Remove stock"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t276" href="#t276">276</a></span><span class="t"> <span class="nam">remove</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t277" href="#t277">277</a></span><span class="t"> <span class="nam">remove</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--from"</span><span class="op">,</span> <span class="nam">dest</span><span class="op">=</span><span class="str">"from_list"</span><span class="op">,</span> <span class="nam">choices</span><span class="op">=</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">,</span> <span class="str">"watchlist"</span><span class="op">,</span> <span class="str">"both"</span><span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t278" href="#t278">278</a></span><span class="t"> <span class="nam">default</span><span class="op">=</span><span class="str">"both"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Remove from which list"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t279" href="#t279">279</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t280" href="#t280">280</a></span><span class="t"> <span class="com"># set-alert (for existing holdings)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t281" href="#t281">281</a></span><span class="t"> <span class="nam">set_alert</span> <span class="op">=</span> <span class="nam">subparsers</span><span class="op">.</span><span class="nam">add_parser</span><span class="op">(</span><span class="str">"set-alert"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Set buy-more/stop alert on holding"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t282" href="#t282">282</a></span><span class="t"> <span class="nam">set_alert</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stock ticker"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t283" href="#t283">283</a></span><span class="t"> <span class="nam">set_alert</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--target"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Buy-more target price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t284" href="#t284">284</a></span><span class="t"> <span class="nam">set_alert</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">"--stop"</span><span class="op">,</span> <span class="nam">type</span><span class="op">=</span><span class="nam">float</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">"Stop loss price"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t285" href="#t285">285</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t286" href="#t286">286</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t287" href="#t287">287</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t288" href="#t288">288</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"list"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t289" href="#t289">289</a></span><span class="t"> <span class="nam">show_h</span> <span class="op">=</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">watchlist</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">holdings</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t290" href="#t290">290</a></span><span class="t"> <span class="nam">show_w</span> <span class="op">=</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">holdings</span> <span class="key">or</span> <span class="nam">args</span><span class="op">.</span><span class="nam">watchlist</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t291" href="#t291">291</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">holdings</span> <span class="key">and</span> <span class="key">not</span> <span class="nam">args</span><span class="op">.</span><span class="nam">watchlist</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t292" href="#t292">292</a></span><span class="t"> <span class="nam">show_h</span> <span class="op">=</span> <span class="nam">show_w</span> <span class="op">=</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t293" href="#t293">293</a></span><span class="t"> <span class="nam">list_stocks</span><span class="op">(</span><span class="nam">show_holdings</span><span class="op">=</span><span class="nam">show_h</span><span class="op">,</span> <span class="nam">show_watchlist</span><span class="op">=</span><span class="nam">show_w</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t294" href="#t294">294</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t295" href="#t295">295</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"add-watchlist"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t296" href="#t296">296</a></span><span class="t"> <span class="nam">add_to_watchlist</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">stop</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">notes</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t297" href="#t297">297</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Added {args.ticker.upper()} to watchlist"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t298" href="#t298">298</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t299" href="#t299">299</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"add-holding"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t300" href="#t300">300</a></span><span class="t"> <span class="nam">add_to_holdings</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">name</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">category</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">notes</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t301" href="#t301">301</a></span><span class="t"> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">stop</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t302" href="#t302">302</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Added {args.ticker.upper()} to holdings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t303" href="#t303">303</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t304" href="#t304">304</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"move"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t305" href="#t305">305</a></span><span class="t"> <span class="key">if</span> <span class="nam">move_to_holdings</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">name</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">category</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t306" href="#t306">306</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Moved {args.ticker.upper()} from watchlist to holdings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t307" href="#t307">307</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t308" href="#t308">308</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"remove"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t309" href="#t309">309</a></span><span class="t"> <span class="key">if</span> <span class="nam">remove_stock</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">from_list</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t310" href="#t310">310</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Removed {args.ticker.upper()}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t311" href="#t311">311</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t312" href="#t312">312</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; {args.ticker.upper()} not found"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t313" href="#t313">313</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t314" href="#t314">314</a></span><span class="t"> <span class="key">elif</span> <span class="nam">args</span><span class="op">.</span><span class="nam">command</span> <span class="op">==</span> <span class="str">"set-alert"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t315" href="#t315">315</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">load_stocks</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t316" href="#t316">316</a></span><span class="t"> <span class="nam">found</span> <span class="op">=</span> <span class="key">False</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t317" href="#t317">317</a></span><span class="t"> <span class="key">for</span> <span class="nam">h</span> <span class="key">in</span> <span class="nam">data</span><span class="op">[</span><span class="str">"holdings"</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t318" href="#t318">318</a></span><span class="t"> <span class="key">if</span> <span class="nam">h</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"ticker"</span><span class="op">)</span> <span class="op">==</span> <span class="nam">args</span><span class="op">.</span><span class="nam">ticker</span><span class="op">.</span><span class="nam">upper</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t319" href="#t319">319</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t320" href="#t320">320</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"target"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">target</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t321" href="#t321">321</a></span><span class="t"> <span class="key">if</span> <span class="nam">args</span><span class="op">.</span><span class="nam">stop</span> <span class="key">is</span> <span class="key">not</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t322" href="#t322">322</a></span><span class="t"> <span class="nam">h</span><span class="op">[</span><span class="str">"stop"</span><span class="op">]</span> <span class="op">=</span> <span class="nam">args</span><span class="op">.</span><span class="nam">stop</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t323" href="#t323">323</a></span><span class="t"> <span class="nam">save_stocks</span><span class="op">(</span><span class="nam">data</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t324" href="#t324">324</a></span><span class="t"> <span class="nam">found</span> <span class="op">=</span> <span class="key">True</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t325" href="#t325">325</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Set alert on {args.ticker.upper()}: target=${args.target}, stop=${args.stop}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t326" href="#t326">326</a></span><span class="t"> <span class="key">break</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t327" href="#t327">327</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">found</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t328" href="#t328">328</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; {args.ticker.upper()} not found in holdings"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t329" href="#t329">329</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t330" href="#t330">330</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t331" href="#t331">331</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">print_help</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t332" href="#t332">332</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t333" href="#t333">333</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t334" href="#t334">334</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">"__main__"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t335" href="#t335">335</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_setup_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_summarize_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,255 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/translate_portfolio.py: 0%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;translate_portfolio.py</b>:
<span class="pc_cov">0%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">88 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">0<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">88<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">2<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_summarize_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="z_de1a740d5dc98ffd_utils_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="com">#!/usr/bin/env python3</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t"><span class="str">"""Translate portfolio headlines in briefing JSON using openclaw.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t"><span class="str">Usage: python3 translate_portfolio.py /path/to/briefing.json [--lang de]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="str">Reads briefing JSON, translates portfolio article headlines via openclaw,</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t"><span class="str">writes back the modified JSON.</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"><span class="key">import</span> <span class="nam">argparse</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"><span class="key">import</span> <span class="nam">json</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"><span class="key">import</span> <span class="nam">re</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"><span class="key">import</span> <span class="nam">subprocess</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"><span class="key">def</span> <span class="nam">extract_headlines</span><span class="op">(</span><span class="nam">portfolio_message</span><span class="op">:</span> <span class="nam">str</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"> <span class="str">"""Extract article headlines (lines starting with &#8226;) from portfolio message."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"> <span class="nam">headlines</span> <span class="op">=</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"> <span class="key">for</span> <span class="nam">line</span> <span class="key">in</span> <span class="nam">portfolio_message</span><span class="op">.</span><span class="nam">split</span><span class="op">(</span><span class="str">'\n'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"> <span class="nam">line</span> <span class="op">=</span> <span class="nam">line</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t"> <span class="key">if</span> <span class="nam">line</span><span class="op">.</span><span class="nam">startswith</span><span class="op">(</span><span class="str">'&#8226;'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t"> <span class="com"># Remove bullet, reference number, and clean up</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"> <span class="com"># Format: "&#8226; Headline text [1]"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"> <span class="nam">match</span> <span class="op">=</span> <span class="nam">re</span><span class="op">.</span><span class="nam">match</span><span class="op">(</span><span class="str">r'&#8226;\s*(.+?)\s*\[\d+\]$'</span><span class="op">,</span> <span class="nam">line</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"> <span class="key">if</span> <span class="nam">match</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t"> <span class="nam">headlines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">match</span><span class="op">.</span><span class="nam">group</span><span class="op">(</span><span class="num">1</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"> <span class="com"># No reference number</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t"> <span class="nam">headlines</span><span class="op">.</span><span class="nam">append</span><span class="op">(</span><span class="nam">line</span><span class="op">[</span><span class="num">1</span><span class="op">:</span><span class="op">]</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t"> <span class="key">return</span> <span class="nam">headlines</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"><span class="key">def</span> <span class="nam">translate_headlines</span><span class="op">(</span><span class="nam">headlines</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">,</span> <span class="nam">lang</span><span class="op">:</span> <span class="nam">str</span> <span class="op">=</span> <span class="str">"de"</span><span class="op">)</span> <span class="op">-></span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="str">"""Translate headlines using openclaw agent."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">headlines</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t"> <span class="key">return</span> <span class="op">[</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"> <span class="nam">prompt</span> <span class="op">=</span> <span class="str">f"""Translate these English headlines to German.</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"><span class="str">Return ONLY a JSON array of strings in the same order.</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"><span class="str">Example: ["&#220;bersetzung 1", "&#220;bersetzung 2"]</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"><span class="str">Do not add commentary.</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"><span class="str">Headlines:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"><span class="str">"""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t46" href="#t46">46</a></span><span class="t"> <span class="key">for</span> <span class="nam">idx</span><span class="op">,</span> <span class="nam">title</span> <span class="key">in</span> <span class="nam">enumerate</span><span class="op">(</span><span class="nam">headlines</span><span class="op">,</span> <span class="nam">start</span><span class="op">=</span><span class="num">1</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t47" href="#t47">47</a></span><span class="t"> <span class="nam">prompt</span> <span class="op">+=</span> <span class="str">f"{idx}. {title}\n"</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t48" href="#t48">48</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t49" href="#t49">49</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t50" href="#t50">50</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">subprocess</span><span class="op">.</span><span class="nam">run</span><span class="op">(</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t51" href="#t51">51</a></span><span class="t"> <span class="op">[</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t52" href="#t52">52</a></span><span class="t"> <span class="str">'openclaw'</span><span class="op">,</span> <span class="str">'agent'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t53" href="#t53">53</a></span><span class="t"> <span class="str">'--session-id'</span><span class="op">,</span> <span class="str">'finance-news-translate-portfolio'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t54" href="#t54">54</a></span><span class="t"> <span class="str">'--message'</span><span class="op">,</span> <span class="nam">prompt</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t55" href="#t55">55</a></span><span class="t"> <span class="str">'--json'</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t56" href="#t56">56</a></span><span class="t"> <span class="str">'--timeout'</span><span class="op">,</span> <span class="str">'60'</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t57" href="#t57">57</a></span><span class="t"> <span class="op">]</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t58" href="#t58">58</a></span><span class="t"> <span class="nam">capture_output</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t59" href="#t59">59</a></span><span class="t"> <span class="nam">text</span><span class="op">=</span><span class="key">True</span><span class="op">,</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t60" href="#t60">60</a></span><span class="t"> <span class="nam">timeout</span><span class="op">=</span><span class="num">90</span>&nbsp;</span><span class="r"></span></p>
<p class="mis mis2 show_mis"><span class="n"><a id="t61" href="#t61">61</a></span><span class="t"> <span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t62" href="#t62">62</a></span><span class="t"> <span class="key">except</span> <span class="op">(</span><span class="nam">subprocess</span><span class="op">.</span><span class="nam">TimeoutExpired</span><span class="op">,</span> <span class="nam">FileNotFoundError</span><span class="op">,</span> <span class="nam">OSError</span><span class="op">)</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t63" href="#t63">63</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Translation failed: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t64" href="#t64">64</a></span><span class="t"> <span class="key">return</span> <span class="nam">headlines</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t65" href="#t65">65</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t66" href="#t66">66</a></span><span class="t"> <span class="key">if</span> <span class="nam">result</span><span class="op">.</span><span class="nam">returncode</span> <span class="op">!=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t67" href="#t67">67</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; openclaw error: {result.stderr}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t68" href="#t68">68</a></span><span class="t"> <span class="key">return</span> <span class="nam">headlines</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t69" href="#t69">69</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t70" href="#t70">70</a></span><span class="t"> <span class="com"># Extract reply from openclaw JSON output</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t71" href="#t71">71</a></span><span class="t"> <span class="com"># Format: {"result": {"payloads": [{"text": "..."}]}}</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t72" href="#t72">72</a></span><span class="t"> <span class="com"># Note: openclaw may print plugin loading messages before JSON, so find the JSON start</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t73" href="#t73">73</a></span><span class="t"> <span class="nam">stdout</span> <span class="op">=</span> <span class="nam">result</span><span class="op">.</span><span class="nam">stdout</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t74" href="#t74">74</a></span><span class="t"> <span class="nam">json_start</span> <span class="op">=</span> <span class="nam">stdout</span><span class="op">.</span><span class="nam">find</span><span class="op">(</span><span class="str">'{'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t75" href="#t75">75</a></span><span class="t"> <span class="key">if</span> <span class="nam">json_start</span> <span class="op">></span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t76" href="#t76">76</a></span><span class="t"> <span class="nam">stdout</span> <span class="op">=</span> <span class="nam">stdout</span><span class="op">[</span><span class="nam">json_start</span><span class="op">:</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t77" href="#t77">77</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t78" href="#t78">78</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t79" href="#t79">79</a></span><span class="t"> <span class="nam">output</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">stdout</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t80" href="#t80">80</a></span><span class="t"> <span class="nam">payloads</span> <span class="op">=</span> <span class="nam">output</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'result'</span><span class="op">,</span> <span class="op">{</span><span class="op">}</span><span class="op">)</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'payloads'</span><span class="op">,</span> <span class="op">[</span><span class="op">]</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t81" href="#t81">81</a></span><span class="t"> <span class="key">if</span> <span class="nam">payloads</span> <span class="key">and</span> <span class="nam">payloads</span><span class="op">[</span><span class="num">0</span><span class="op">]</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'text'</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t82" href="#t82">82</a></span><span class="t"> <span class="nam">reply</span> <span class="op">=</span> <span class="nam">payloads</span><span class="op">[</span><span class="num">0</span><span class="op">]</span><span class="op">[</span><span class="str">'text'</span><span class="op">]</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t83" href="#t83">83</a></span><span class="t"> <span class="key">else</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t84" href="#t84">84</a></span><span class="t"> <span class="nam">reply</span> <span class="op">=</span> <span class="nam">output</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'reply'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span> <span class="key">or</span> <span class="nam">output</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'message'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span> <span class="key">or</span> <span class="nam">stdout</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t85" href="#t85">85</a></span><span class="t"> <span class="key">except</span> <span class="nam">json</span><span class="op">.</span><span class="nam">JSONDecodeError</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t86" href="#t86">86</a></span><span class="t"> <span class="nam">reply</span> <span class="op">=</span> <span class="nam">stdout</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t87" href="#t87">87</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t88" href="#t88">88</a></span><span class="t"> <span class="com"># Parse JSON array from reply</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t89" href="#t89">89</a></span><span class="t"> <span class="nam">json_text</span> <span class="op">=</span> <span class="nam">reply</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t90" href="#t90">90</a></span><span class="t"> <span class="key">if</span> <span class="str">"```"</span> <span class="key">in</span> <span class="nam">json_text</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t91" href="#t91">91</a></span><span class="t"> <span class="nam">match</span> <span class="op">=</span> <span class="nam">re</span><span class="op">.</span><span class="nam">search</span><span class="op">(</span><span class="str">r'```(?:json)?\s*(.*?)```'</span><span class="op">,</span> <span class="nam">json_text</span><span class="op">,</span> <span class="nam">re</span><span class="op">.</span><span class="nam">DOTALL</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t92" href="#t92">92</a></span><span class="t"> <span class="key">if</span> <span class="nam">match</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t93" href="#t93">93</a></span><span class="t"> <span class="nam">json_text</span> <span class="op">=</span> <span class="nam">match</span><span class="op">.</span><span class="nam">group</span><span class="op">(</span><span class="num">1</span><span class="op">)</span><span class="op">.</span><span class="nam">strip</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t94" href="#t94">94</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t95" href="#t95">95</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t96" href="#t96">96</a></span><span class="t"> <span class="nam">translated</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">loads</span><span class="op">(</span><span class="nam">json_text</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t97" href="#t97">97</a></span><span class="t"> <span class="key">if</span> <span class="nam">isinstance</span><span class="op">(</span><span class="nam">translated</span><span class="op">,</span> <span class="nam">list</span><span class="op">)</span> <span class="key">and</span> <span class="nam">len</span><span class="op">(</span><span class="nam">translated</span><span class="op">)</span> <span class="op">==</span> <span class="nam">len</span><span class="op">(</span><span class="nam">headlines</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t98" href="#t98">98</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Translated {len(headlines)} portfolio headlines"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t99" href="#t99">99</a></span><span class="t"> <span class="key">return</span> <span class="nam">translated</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t100" href="#t100">100</a></span><span class="t"> <span class="key">except</span> <span class="nam">json</span><span class="op">.</span><span class="nam">JSONDecodeError</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t101" href="#t101">101</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; JSON parse error: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t102" href="#t102">102</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t103" href="#t103">103</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9888;&#65039; Translation failed, using original headlines"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t104" href="#t104">104</a></span><span class="t"> <span class="key">return</span> <span class="nam">headlines</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t105" href="#t105">105</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t106" href="#t106">106</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t107" href="#t107">107</a></span><span class="t"><span class="key">def</span> <span class="nam">replace_headlines</span><span class="op">(</span><span class="nam">portfolio_message</span><span class="op">:</span> <span class="nam">str</span><span class="op">,</span> <span class="nam">original</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">,</span> <span class="nam">translated</span><span class="op">:</span> <span class="nam">list</span><span class="op">[</span><span class="nam">str</span><span class="op">]</span><span class="op">)</span> <span class="op">-></span> <span class="nam">str</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t108" href="#t108">108</a></span><span class="t"> <span class="str">"""Replace original headlines with translated ones in portfolio message."""</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t109" href="#t109">109</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">portfolio_message</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t110" href="#t110">110</a></span><span class="t"> <span class="key">for</span> <span class="nam">orig</span><span class="op">,</span> <span class="nam">trans</span> <span class="key">in</span> <span class="nam">zip</span><span class="op">(</span><span class="nam">original</span><span class="op">,</span> <span class="nam">translated</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t111" href="#t111">111</a></span><span class="t"> <span class="key">if</span> <span class="nam">orig</span> <span class="op">!=</span> <span class="nam">trans</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t112" href="#t112">112</a></span><span class="t"> <span class="com"># Replace the headline text, preserving bullet and reference</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t113" href="#t113">113</a></span><span class="t"> <span class="nam">result</span> <span class="op">=</span> <span class="nam">result</span><span class="op">.</span><span class="nam">replace</span><span class="op">(</span><span class="str">f"&#8226; {orig}"</span><span class="op">,</span> <span class="str">f"&#8226; {trans}"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t114" href="#t114">114</a></span><span class="t"> <span class="key">return</span> <span class="nam">result</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t115" href="#t115">115</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t116" href="#t116">116</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t117" href="#t117">117</a></span><span class="t"><span class="key">def</span> <span class="nam">main</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t118" href="#t118">118</a></span><span class="t"> <span class="nam">parser</span> <span class="op">=</span> <span class="nam">argparse</span><span class="op">.</span><span class="nam">ArgumentParser</span><span class="op">(</span><span class="nam">description</span><span class="op">=</span><span class="str">'Translate portfolio headlines'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t119" href="#t119">119</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'json_file'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Path to briefing JSON file'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t120" href="#t120">120</a></span><span class="t"> <span class="nam">parser</span><span class="op">.</span><span class="nam">add_argument</span><span class="op">(</span><span class="str">'--lang'</span><span class="op">,</span> <span class="nam">default</span><span class="op">=</span><span class="str">'de'</span><span class="op">,</span> <span class="nam">help</span><span class="op">=</span><span class="str">'Target language (default: de)'</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t121" href="#t121">121</a></span><span class="t"> <span class="nam">args</span> <span class="op">=</span> <span class="nam">parser</span><span class="op">.</span><span class="nam">parse_args</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t122" href="#t122">122</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t123" href="#t123">123</a></span><span class="t"> <span class="com"># Read JSON</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t124" href="#t124">124</a></span><span class="t"> <span class="key">try</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t125" href="#t125">125</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">json_file</span><span class="op">,</span> <span class="str">'r'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t126" href="#t126">126</a></span><span class="t"> <span class="nam">data</span> <span class="op">=</span> <span class="nam">json</span><span class="op">.</span><span class="nam">load</span><span class="op">(</span><span class="nam">f</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t127" href="#t127">127</a></span><span class="t"> <span class="key">except</span> <span class="op">(</span><span class="nam">FileNotFoundError</span><span class="op">,</span> <span class="nam">json</span><span class="op">.</span><span class="nam">JSONDecodeError</span><span class="op">)</span> <span class="key">as</span> <span class="nam">e</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t128" href="#t128">128</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#10060; Error reading {args.json_file}: {e}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t129" href="#t129">129</a></span><span class="t"> <span class="nam">sys</span><span class="op">.</span><span class="nam">exit</span><span class="op">(</span><span class="num">1</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t130" href="#t130">130</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t131" href="#t131">131</a></span><span class="t"> <span class="nam">portfolio_message</span> <span class="op">=</span> <span class="nam">data</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">'portfolio_message'</span><span class="op">,</span> <span class="str">''</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t132" href="#t132">132</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">portfolio_message</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t133" href="#t133">133</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"No portfolio_message to translate"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t134" href="#t134">134</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">ensure_ascii</span><span class="op">=</span><span class="key">False</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t135" href="#t135">135</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t136" href="#t136">136</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t137" href="#t137">137</a></span><span class="t"> <span class="com"># Extract, translate, replace</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t138" href="#t138">138</a></span><span class="t"> <span class="nam">headlines</span> <span class="op">=</span> <span class="nam">extract_headlines</span><span class="op">(</span><span class="nam">portfolio_message</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t139" href="#t139">139</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">headlines</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t140" href="#t140">140</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"No headlines found in portfolio_message"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t141" href="#t141">141</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="nam">json</span><span class="op">.</span><span class="nam">dumps</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">ensure_ascii</span><span class="op">=</span><span class="key">False</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t142" href="#t142">142</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t143" href="#t143">143</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t144" href="#t144">144</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#128221; Found {len(headlines)} headlines to translate"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t145" href="#t145">145</a></span><span class="t"> <span class="nam">translated</span> <span class="op">=</span> <span class="nam">translate_headlines</span><span class="op">(</span><span class="nam">headlines</span><span class="op">,</span> <span class="nam">args</span><span class="op">.</span><span class="nam">lang</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t146" href="#t146">146</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t147" href="#t147">147</a></span><span class="t"> <span class="com"># Update portfolio message</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t148" href="#t148">148</a></span><span class="t"> <span class="nam">data</span><span class="op">[</span><span class="str">'portfolio_message'</span><span class="op">]</span> <span class="op">=</span> <span class="nam">replace_headlines</span><span class="op">(</span><span class="nam">portfolio_message</span><span class="op">,</span> <span class="nam">headlines</span><span class="op">,</span> <span class="nam">translated</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t149" href="#t149">149</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t150" href="#t150">150</a></span><span class="t"> <span class="com"># Write back</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t151" href="#t151">151</a></span><span class="t"> <span class="key">with</span> <span class="nam">open</span><span class="op">(</span><span class="nam">args</span><span class="op">.</span><span class="nam">json_file</span><span class="op">,</span> <span class="str">'w'</span><span class="op">)</span> <span class="key">as</span> <span class="nam">f</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t152" href="#t152">152</a></span><span class="t"> <span class="nam">json</span><span class="op">.</span><span class="nam">dump</span><span class="op">(</span><span class="nam">data</span><span class="op">,</span> <span class="nam">f</span><span class="op">,</span> <span class="nam">ensure_ascii</span><span class="op">=</span><span class="key">False</span><span class="op">,</span> <span class="nam">indent</span><span class="op">=</span><span class="num">2</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t153" href="#t153">153</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t154" href="#t154">154</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">f"&#9989; Updated {args.json_file}"</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t155" href="#t155">155</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t156" href="#t156">156</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t157" href="#t157">157</a></span><span class="t"><span class="key">if</span> <span class="nam">__name__</span> <span class="op">==</span> <span class="str">'__main__'</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="exc show_exc"><span class="n"><a id="t158" href="#t158">158</a></span><span class="t"> <span class="nam">main</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_summarize_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="z_de1a740d5dc98ffd_utils_py.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

142
htmlcov/z_de1a740d5dc98ffd_utils_py.html generated Normal file
View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Coverage for scripts/utils.py: 71%</title>
<link rel="icon" sizes="32x32" href="favicon_32_cb_c827f16f.png">
<link rel="stylesheet" href="style_cb_9ff733b0.css" type="text/css">
<script src="coverage_html_cb_dd2e7eb5.js" defer></script>
</head>
<body class="pyfile">
<header>
<div class="content">
<h1>
<span class="text">Coverage for </span><b>scripts&#8201;/&#8201;utils.py</b>:
<span class="pc_cov">71%</span>
</h1>
<aside id="help_panel_wrapper">
<input id="help_panel_state" type="checkbox">
<label for="help_panel_state">
<img id="keyboard_icon" src="keybd_closed_cb_900cfef5.png" alt="Show/hide keyboard shortcuts">
</label>
<div id="help_panel">
<p class="legend">Shortcuts on this page</p>
<div class="keyhelp">
<p>
<kbd>r</kbd>
<kbd>m</kbd>
<kbd>x</kbd>
&nbsp; toggle line displays
</p>
<p>
<kbd>j</kbd>
<kbd>k</kbd>
&nbsp; next/prev highlighted chunk
</p>
<p>
<kbd>0</kbd> &nbsp; (zero) top of page
</p>
<p>
<kbd>1</kbd> &nbsp; (one) first highlighted chunk
</p>
<p>
<kbd>[</kbd>
<kbd>]</kbd>
&nbsp; prev/next file
</p>
<p>
<kbd>u</kbd> &nbsp; up to the index
</p>
<p>
<kbd>?</kbd> &nbsp; show/hide this help
</p>
</div>
</div>
</aside>
<h2>
<span class="text">34 statements &nbsp;</span>
<button type="button" class="run button_toggle_run" value="run" data-shortcut="r" title="Toggle lines run">24<span class="text"> run</span></button>
<button type="button" class="mis show_mis button_toggle_mis" value="mis" data-shortcut="m" title="Toggle lines missing">10<span class="text"> missing</span></button>
<button type="button" class="exc show_exc button_toggle_exc" value="exc" data-shortcut="x" title="Toggle lines excluded">0<span class="text"> excluded</span></button>
</h2>
<p class="text">
<a id="prevFileLink" class="nav" href="z_de1a740d5dc98ffd_translate_portfolio_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a id="indexLink" class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a id="nextFileLink" class="nav" href="index.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
<aside class="hidden">
<button type="button" class="button_next_chunk" data-shortcut="j"></button>
<button type="button" class="button_prev_chunk" data-shortcut="k"></button>
<button type="button" class="button_top_of_page" data-shortcut="0"></button>
<button type="button" class="button_first_chunk" data-shortcut="1"></button>
<button type="button" class="button_prev_file" data-shortcut="["></button>
<button type="button" class="button_next_file" data-shortcut="]"></button>
<button type="button" class="button_to_index" data-shortcut="u"></button>
<button type="button" class="button_show_hide_help" data-shortcut="?"></button>
</aside>
</div>
</header>
<main id="source">
<p class="pln"><span class="n"><a id="t1" href="#t1">1</a></span><span class="t"><span class="str">"""Shared helpers."""</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t2" href="#t2">2</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t3" href="#t3">3</a></span><span class="t"><span class="key">import</span> <span class="nam">os</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t4" href="#t4">4</a></span><span class="t"><span class="key">import</span> <span class="nam">sys</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t5" href="#t5">5</a></span><span class="t"><span class="key">import</span> <span class="nam">time</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t6" href="#t6">6</a></span><span class="t"><span class="key">from</span> <span class="nam">pathlib</span> <span class="key">import</span> <span class="nam">Path</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t7" href="#t7">7</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t8" href="#t8">8</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t9" href="#t9">9</a></span><span class="t"><span class="key">def</span> <span class="nam">ensure_venv</span><span class="op">(</span><span class="op">)</span> <span class="op">-></span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t10" href="#t10">10</a></span><span class="t"> <span class="str">"""Re-exec inside local venv if available and not already active."""</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t11" href="#t11">11</a></span><span class="t"> <span class="key">if</span> <span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">get</span><span class="op">(</span><span class="str">"FINANCE_NEWS_VENV_BOOTSTRAPPED"</span><span class="op">)</span> <span class="op">==</span> <span class="str">"1"</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t12" href="#t12">12</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t13" href="#t13">13</a></span><span class="t"> <span class="key">if</span> <span class="nam">sys</span><span class="op">.</span><span class="nam">prefix</span> <span class="op">!=</span> <span class="nam">sys</span><span class="op">.</span><span class="nam">base_prefix</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t14" href="#t14">14</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t15" href="#t15">15</a></span><span class="t"> <span class="nam">venv_python</span> <span class="op">=</span> <span class="nam">Path</span><span class="op">(</span><span class="nam">__file__</span><span class="op">)</span><span class="op">.</span><span class="nam">resolve</span><span class="op">(</span><span class="op">)</span><span class="op">.</span><span class="nam">parent</span><span class="op">.</span><span class="nam">parent</span> <span class="op">/</span> <span class="str">"venv"</span> <span class="op">/</span> <span class="str">"bin"</span> <span class="op">/</span> <span class="str">"python3"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t16" href="#t16">16</a></span><span class="t"> <span class="key">if</span> <span class="key">not</span> <span class="nam">venv_python</span><span class="op">.</span><span class="nam">exists</span><span class="op">(</span><span class="op">)</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t17" href="#t17">17</a></span><span class="t"> <span class="nam">print</span><span class="op">(</span><span class="str">"&#9888;&#65039; finance-news venv missing; run scripts from the repo venv to avoid dependency errors."</span><span class="op">,</span> <span class="nam">file</span><span class="op">=</span><span class="nam">sys</span><span class="op">.</span><span class="nam">stderr</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t18" href="#t18">18</a></span><span class="t"> <span class="key">return</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t19" href="#t19">19</a></span><span class="t"> <span class="nam">env</span> <span class="op">=</span> <span class="nam">os</span><span class="op">.</span><span class="nam">environ</span><span class="op">.</span><span class="nam">copy</span><span class="op">(</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t20" href="#t20">20</a></span><span class="t"> <span class="nam">env</span><span class="op">[</span><span class="str">"FINANCE_NEWS_VENV_BOOTSTRAPPED"</span><span class="op">]</span> <span class="op">=</span> <span class="str">"1"</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t21" href="#t21">21</a></span><span class="t"> <span class="nam">os</span><span class="op">.</span><span class="nam">execvpe</span><span class="op">(</span><span class="nam">str</span><span class="op">(</span><span class="nam">venv_python</span><span class="op">)</span><span class="op">,</span> <span class="op">[</span><span class="nam">str</span><span class="op">(</span><span class="nam">venv_python</span><span class="op">)</span><span class="op">]</span> <span class="op">+</span> <span class="nam">sys</span><span class="op">.</span><span class="nam">argv</span><span class="op">,</span> <span class="nam">env</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t22" href="#t22">22</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t23" href="#t23">23</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t24" href="#t24">24</a></span><span class="t"><span class="key">def</span> <span class="nam">compute_deadline</span><span class="op">(</span><span class="nam">deadline_sec</span><span class="op">:</span> <span class="nam">int</span> <span class="op">|</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">float</span> <span class="op">|</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t25" href="#t25">25</a></span><span class="t"> <span class="key">if</span> <span class="nam">deadline_sec</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t26" href="#t26">26</a></span><span class="t"> <span class="key">return</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t27" href="#t27">27</a></span><span class="t"> <span class="key">if</span> <span class="nam">deadline_sec</span> <span class="op">&lt;=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="mis show_mis"><span class="n"><a id="t28" href="#t28">28</a></span><span class="t"> <span class="key">return</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t29" href="#t29">29</a></span><span class="t"> <span class="key">return</span> <span class="nam">time</span><span class="op">.</span><span class="nam">monotonic</span><span class="op">(</span><span class="op">)</span> <span class="op">+</span> <span class="nam">deadline_sec</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t30" href="#t30">30</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t31" href="#t31">31</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t32" href="#t32">32</a></span><span class="t"><span class="key">def</span> <span class="nam">time_left</span><span class="op">(</span><span class="nam">deadline</span><span class="op">:</span> <span class="nam">float</span> <span class="op">|</span> <span class="key">None</span><span class="op">)</span> <span class="op">-></span> <span class="nam">int</span> <span class="op">|</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t33" href="#t33">33</a></span><span class="t"> <span class="key">if</span> <span class="nam">deadline</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t34" href="#t34">34</a></span><span class="t"> <span class="key">return</span> <span class="key">None</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t35" href="#t35">35</a></span><span class="t"> <span class="nam">remaining</span> <span class="op">=</span> <span class="nam">int</span><span class="op">(</span><span class="nam">deadline</span> <span class="op">-</span> <span class="nam">time</span><span class="op">.</span><span class="nam">monotonic</span><span class="op">(</span><span class="op">)</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t36" href="#t36">36</a></span><span class="t"> <span class="key">return</span> <span class="nam">remaining</span>&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t37" href="#t37">37</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="pln"><span class="n"><a id="t38" href="#t38">38</a></span><span class="t">&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t39" href="#t39">39</a></span><span class="t"><span class="key">def</span> <span class="nam">clamp_timeout</span><span class="op">(</span><span class="nam">default_timeout</span><span class="op">:</span> <span class="nam">int</span><span class="op">,</span> <span class="nam">deadline</span><span class="op">:</span> <span class="nam">float</span> <span class="op">|</span> <span class="key">None</span><span class="op">,</span> <span class="nam">minimum</span><span class="op">:</span> <span class="nam">int</span> <span class="op">=</span> <span class="num">1</span><span class="op">)</span> <span class="op">-></span> <span class="nam">int</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t40" href="#t40">40</a></span><span class="t"> <span class="nam">remaining</span> <span class="op">=</span> <span class="nam">time_left</span><span class="op">(</span><span class="nam">deadline</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t41" href="#t41">41</a></span><span class="t"> <span class="key">if</span> <span class="nam">remaining</span> <span class="key">is</span> <span class="key">None</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t42" href="#t42">42</a></span><span class="t"> <span class="key">return</span> <span class="nam">default_timeout</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t43" href="#t43">43</a></span><span class="t"> <span class="key">if</span> <span class="nam">remaining</span> <span class="op">&lt;=</span> <span class="num">0</span><span class="op">:</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t44" href="#t44">44</a></span><span class="t"> <span class="key">raise</span> <span class="nam">TimeoutError</span><span class="op">(</span><span class="str">"Deadline exceeded"</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
<p class="run"><span class="n"><a id="t45" href="#t45">45</a></span><span class="t"> <span class="key">return</span> <span class="nam">max</span><span class="op">(</span><span class="nam">min</span><span class="op">(</span><span class="nam">default_timeout</span><span class="op">,</span> <span class="nam">remaining</span><span class="op">)</span><span class="op">,</span> <span class="nam">minimum</span><span class="op">)</span>&nbsp;</span><span class="r"></span></p>
</main>
<footer>
<div class="content">
<p>
<a class="nav" href="z_de1a740d5dc98ffd_translate_portfolio_py.html">&#xab; prev</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&Hat; index</a> &nbsp; &nbsp;
<a class="nav" href="index.html">&#xbb; next</a>
&nbsp; &nbsp; &nbsp;
<a class="nav" href="https://coverage.readthedocs.io/en/7.13.2">coverage.py v7.13.2</a>,
created at 2026-02-01 16:34 -0800
</p>
</div>
</footer>
</body>
</html>

53
pyproject.toml Normal file
View File

@@ -0,0 +1,53 @@
[project]
name = "finance-news"
version = "1.0.0"
description = "Finance news aggregation and market briefing skill for OpenClaw"
readme = "README.md"
requires-python = ">=3.10"
license = { text = "MIT" }
authors = [{ name = "Martin Kessler", email = "martin@kessler.io" }]
dependencies = [
"feedparser>=6.0.11",
"yfinance>=0.2.40",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=5.0",
"ruff>=0.4",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["scripts"]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
[tool.coverage.run]
source = ["scripts"]
omit = ["scripts/__pycache__/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"raise NotImplementedError",
]
fail_under = 30 # Current coverage is ~39%
[tool.ruff]
line-length = 120
target-version = "py310"
[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP"]
ignore = ["E501"]

12
pytest.ini Normal file
View File

@@ -0,0 +1,12 @@
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--strict-markers
--tb=short
--cov=scripts
--cov-report=term-missing
--cov-report=html

4
requirements-test.txt Normal file
View File

@@ -0,0 +1,4 @@
# Test dependencies
pytest>=7.4.0
pytest-cov>=4.1.0
pytest-mock>=3.12.0

2
requirements.txt Normal file
View File

@@ -0,0 +1,2 @@
feedparser>=6.0.11
yfinance

500
scripts/alerts.py Normal file
View File

@@ -0,0 +1,500 @@
#!/usr/bin/env python3
"""
Price Target Alerts - Track buy zone alerts for stocks.
Features:
- Set price target alerts (buy zone triggers)
- Check alerts against current prices
- Snooze, update, delete alerts
- Multi-currency support (USD, EUR, JPY, SGD, MXN)
Usage:
alerts.py list # Show all alerts
alerts.py set CRWD 400 --note 'Kaufzone' # Set alert
alerts.py check # Check triggered alerts
alerts.py delete CRWD # Delete alert
alerts.py snooze CRWD --days 7 # Snooze for 7 days
alerts.py update CRWD 380 # Update target price
"""
import argparse
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from utils import ensure_venv
ensure_venv()
# Lazy import to avoid numpy issues at module load
fetch_market_data = None
def get_fetch_market_data():
global fetch_market_data
if fetch_market_data is None:
from fetch_news import fetch_market_data as fmd
fetch_market_data = fmd
return fetch_market_data
SCRIPT_DIR = Path(__file__).parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
ALERTS_FILE = CONFIG_DIR / "alerts.json"
SUPPORTED_CURRENCIES = ["USD", "EUR", "JPY", "SGD", "MXN"]
def load_alerts() -> dict:
"""Load alerts from JSON file."""
if not ALERTS_FILE.exists():
return {"_meta": {"version": 1, "supported_currencies": SUPPORTED_CURRENCIES}, "alerts": []}
return json.loads(ALERTS_FILE.read_text())
def save_alerts(data: dict) -> None:
"""Save alerts to JSON file."""
data["_meta"]["updated_at"] = datetime.now().isoformat()
ALERTS_FILE.write_text(json.dumps(data, indent=2))
def get_alert_by_ticker(alerts: list, ticker: str) -> dict | None:
"""Find alert by ticker."""
ticker = ticker.upper()
for alert in alerts:
if alert["ticker"] == ticker:
return alert
return None
def format_price(price: float, currency: str) -> str:
"""Format price with currency symbol."""
symbols = {"USD": "$", "EUR": "", "JPY": "¥", "SGD": "S$", "MXN": "MX$"}
symbol = symbols.get(currency, currency + " ")
if currency == "JPY":
return f"{symbol}{price:,.0f}"
return f"{symbol}{price:,.2f}"
def cmd_list(args) -> None:
"""List all alerts."""
data = load_alerts()
alerts = data.get("alerts", [])
if not alerts:
print("📭 No price alerts set")
return
print(f"📊 Price Alerts ({len(alerts)} total)\n")
now = datetime.now()
active = []
snoozed = []
for alert in alerts:
snooze_until = alert.get("snooze_until")
if snooze_until and datetime.fromisoformat(snooze_until) > now:
snoozed.append(alert)
else:
active.append(alert)
if active:
print("### Active Alerts")
for a in active:
target = format_price(a["target_price"], a.get("currency", "USD"))
note = f'"{a["note"]}"' if a.get("note") else ""
user = f" (by {a['set_by']})" if a.get("set_by") else ""
print(f"{a['ticker']}: {target}{note}{user}")
print()
if snoozed:
print("### Snoozed")
for a in snoozed:
target = format_price(a["target_price"], a.get("currency", "USD"))
until = datetime.fromisoformat(a["snooze_until"]).strftime("%Y-%m-%d")
print(f"{a['ticker']}: {target} (until {until})")
print()
def cmd_set(args) -> None:
"""Set a new alert."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
# Check if alert exists
existing = get_alert_by_ticker(alerts, ticker)
if existing:
print(f"⚠️ Alert for {ticker} already exists. Use 'update' to change target.")
return
# Validate target price
if args.target <= 0:
print(f"❌ Target price must be greater than 0")
return
currency = args.currency.upper() if args.currency else "USD"
if currency not in SUPPORTED_CURRENCIES:
print(f"❌ Currency {currency} not supported. Use: {', '.join(SUPPORTED_CURRENCIES)}")
return
# Warn about currency mismatch based on ticker suffix
ticker_currency_map = {
".T": "JPY", # Tokyo
".SI": "SGD", # Singapore
".MX": "MXN", # Mexico
".DE": "EUR", ".F": "EUR", ".PA": "EUR", # Europe
}
expected_currency = "USD" # Default for US stocks
for suffix, curr in ticker_currency_map.items():
if ticker.endswith(suffix):
expected_currency = curr
break
if currency != expected_currency:
print(f"⚠️ Warning: {ticker} trades in {expected_currency}, but alert set in {currency}")
# Fetch current price (optional - may fail if numpy broken)
current_price = None
try:
quotes = get_fetch_market_data()([ticker], timeout=10)
if ticker in quotes and quotes[ticker].get("price"):
current_price = quotes[ticker]["price"]
except Exception as e:
print(f"⚠️ Could not fetch current price: {e}", file=sys.stderr)
alert = {
"ticker": ticker,
"target_price": args.target,
"currency": currency,
"note": args.note or "",
"set_by": args.user or "",
"set_date": datetime.now().strftime("%Y-%m-%d"),
"status": "active",
"snooze_until": None,
"triggered_count": 0,
"last_triggered": None,
}
alerts.append(alert)
data["alerts"] = alerts
save_alerts(data)
target_str = format_price(args.target, currency)
print(f"✅ Alert set: {ticker} under {target_str}")
if current_price:
pct_diff = ((current_price - args.target) / current_price) * 100
current_str = format_price(current_price, currency)
print(f" Current: {current_str} ({pct_diff:+.1f}% to target)")
def cmd_delete(args) -> None:
"""Delete an alert."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
new_alerts = [a for a in alerts if a["ticker"] != ticker]
if len(new_alerts) == len(alerts):
print(f"❌ No alert found for {ticker}")
return
data["alerts"] = new_alerts
save_alerts(data)
print(f"🗑️ Alert deleted: {ticker}")
def cmd_snooze(args) -> None:
"""Snooze an alert."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
alert = get_alert_by_ticker(alerts, ticker)
if not alert:
print(f"❌ No alert found for {ticker}")
return
days = args.days or 7
snooze_until = datetime.now() + timedelta(days=days)
alert["snooze_until"] = snooze_until.isoformat()
save_alerts(data)
print(f"😴 Alert snoozed: {ticker} until {snooze_until.strftime('%Y-%m-%d')}")
def cmd_update(args) -> None:
"""Update alert target price."""
data = load_alerts()
alerts = data.get("alerts", [])
ticker = args.ticker.upper()
alert = get_alert_by_ticker(alerts, ticker)
if not alert:
print(f"❌ No alert found for {ticker}")
return
# Validate target price
if args.target <= 0:
print(f"❌ Target price must be greater than 0")
return
old_target = alert["target_price"]
alert["target_price"] = args.target
if args.note:
alert["note"] = args.note
save_alerts(data)
currency = alert.get("currency", "USD")
old_str = format_price(old_target, currency)
new_str = format_price(args.target, currency)
print(f"✏️ Alert updated: {ticker} {old_str}{new_str}")
def cmd_check(args) -> None:
"""Check alerts against current prices."""
data = load_alerts()
alerts = data.get("alerts", [])
if not alerts:
if args.json:
print(json.dumps({"triggered": [], "watching": []}))
else:
print("📭 No alerts to check")
return
now = datetime.now()
active_alerts = []
for alert in alerts:
snooze_until = alert.get("snooze_until")
if snooze_until and datetime.fromisoformat(snooze_until) > now:
continue
active_alerts.append(alert)
if not active_alerts:
if args.json:
print(json.dumps({"triggered": [], "watching": []}))
else:
print("📭 All alerts snoozed")
return
# Fetch prices for all active alerts
tickers = [a["ticker"] for a in active_alerts]
quotes = get_fetch_market_data()(tickers, timeout=30)
triggered = []
watching = []
for alert in active_alerts:
ticker = alert["ticker"]
target = alert["target_price"]
currency = alert.get("currency", "USD")
quote = quotes.get(ticker, {})
price = quote.get("price")
if price is None:
continue
# Divide-by-zero protection
if target == 0:
pct_diff = 0
else:
pct_diff = ((price - target) / target) * 100
result = {
"ticker": ticker,
"target_price": target,
"current_price": price,
"currency": currency,
"pct_from_target": round(pct_diff, 2),
"note": alert.get("note", ""),
"set_by": alert.get("set_by", ""),
}
if price <= target:
triggered.append(result)
# Update triggered count (only once per day to avoid inflation)
last_triggered = alert.get("last_triggered")
today = now.strftime("%Y-%m-%d")
if not last_triggered or not last_triggered.startswith(today):
alert["triggered_count"] = alert.get("triggered_count", 0) + 1
alert["last_triggered"] = now.isoformat()
else:
watching.append(result)
save_alerts(data)
if args.json:
print(json.dumps({"triggered": triggered, "watching": watching}, indent=2))
return
# Translations
lang = getattr(args, 'lang', 'en')
if lang == "de":
labels = {
"title": "PREISWARNUNGEN",
"in_zone": "IN KAUFZONE",
"buy": "KAUFEN!",
"target": "Ziel",
"watching": "BEOBACHTUNG",
"to_target": "noch",
"no_data": "Keine Preisdaten für Alerts verfügbar",
}
else:
labels = {
"title": "PRICE ALERTS",
"in_zone": "IN BUY ZONE",
"buy": "BUY SIGNAL",
"target": "target",
"watching": "WATCHING",
"to_target": "to target",
"no_data": "No price data available for alerts",
}
# Date header
date_str = datetime.now().strftime("%b %d, %Y") if lang == "en" else datetime.now().strftime("%d. %b %Y")
print(f"📊 {labels['title']}{date_str}\n")
# Human-readable output
if triggered:
print(f"🟢 {labels['in_zone']}:\n")
for t in triggered:
target_str = format_price(t["target_price"], t["currency"])
current_str = format_price(t["current_price"], t["currency"])
note = f'\n "{t["note"]}"' if t.get("note") else ""
user = f"{t['set_by']}" if t.get("set_by") else ""
print(f"{t['ticker']}: {current_str} ({labels['target']}: {target_str}) ← {labels['buy']}{note}{user}")
print()
if watching:
print(f"{labels['watching']}:\n")
for w in sorted(watching, key=lambda x: x["pct_from_target"]):
target_str = format_price(w["target_price"], w["currency"])
current_str = format_price(w["current_price"], w["currency"])
print(f"{w['ticker']}: {current_str} ({labels['target']}: {target_str}) — {labels['to_target']} {abs(w['pct_from_target']):.1f}%")
print()
if not triggered and not watching:
print(f"📭 {labels['no_data']}")
def check_alerts() -> dict:
"""
Check alerts and return results for briefing integration.
Returns: {"triggered": [...], "watching": [...]}
"""
data = load_alerts()
alerts = data.get("alerts", [])
if not alerts:
return {"triggered": [], "watching": []}
now = datetime.now()
active_alerts = [
a for a in alerts
if not a.get("snooze_until") or datetime.fromisoformat(a["snooze_until"]) <= now
]
if not active_alerts:
return {"triggered": [], "watching": []}
tickers = [a["ticker"] for a in active_alerts]
quotes = get_fetch_market_data()(tickers, timeout=30)
triggered = []
watching = []
for alert in active_alerts:
ticker = alert["ticker"]
target = alert["target_price"]
currency = alert.get("currency", "USD")
quote = quotes.get(ticker, {})
price = quote.get("price")
if price is None:
continue
# Divide-by-zero protection
if target == 0:
pct_diff = 0
else:
pct_diff = ((price - target) / target) * 100
result = {
"ticker": ticker,
"target_price": target,
"current_price": price,
"currency": currency,
"pct_from_target": round(pct_diff, 2),
"note": alert.get("note", ""),
"set_by": alert.get("set_by", ""),
}
if price <= target:
triggered.append(result)
# Update triggered count (only once per day to avoid inflation)
last_triggered = alert.get("last_triggered")
today = now.strftime("%Y-%m-%d")
if not last_triggered or not last_triggered.startswith(today):
alert["triggered_count"] = alert.get("triggered_count", 0) + 1
alert["last_triggered"] = now.isoformat()
else:
watching.append(result)
save_alerts(data)
return {"triggered": triggered, "watching": watching}
def main():
parser = argparse.ArgumentParser(description="Price target alerts")
subparsers = parser.add_subparsers(dest="command", required=True)
# list
subparsers.add_parser("list", help="List all alerts")
# set
set_parser = subparsers.add_parser("set", help="Set new alert")
set_parser.add_argument("ticker", help="Stock ticker")
set_parser.add_argument("target", type=float, help="Target price")
set_parser.add_argument("--note", help="Note/reason")
set_parser.add_argument("--user", help="Who set the alert")
set_parser.add_argument("--currency", default="USD", help="Currency (USD, EUR, JPY, SGD, MXN)")
# delete
del_parser = subparsers.add_parser("delete", help="Delete alert")
del_parser.add_argument("ticker", help="Stock ticker")
# snooze
snooze_parser = subparsers.add_parser("snooze", help="Snooze alert")
snooze_parser.add_argument("ticker", help="Stock ticker")
snooze_parser.add_argument("--days", type=int, default=7, help="Days to snooze")
# update
update_parser = subparsers.add_parser("update", help="Update alert target")
update_parser.add_argument("ticker", help="Stock ticker")
update_parser.add_argument("target", type=float, help="New target price")
update_parser.add_argument("--note", help="Update note")
# check
check_parser = subparsers.add_parser("check", help="Check alerts against prices")
check_parser.add_argument("--json", action="store_true", help="JSON output")
check_parser.add_argument("--lang", default="en", help="Output language (en, de)")
args = parser.parse_args()
if args.command == "list":
cmd_list(args)
elif args.command == "set":
cmd_set(args)
elif args.command == "delete":
cmd_delete(args)
elif args.command == "snooze":
cmd_snooze(args)
elif args.command == "update":
cmd_update(args)
elif args.command == "check":
cmd_check(args)
if __name__ == "__main__":
main()

170
scripts/briefing.py Normal file
View File

@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""
Briefing Generator - Main entry point for market briefings.
Generates and optionally sends to WhatsApp group.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from utils import ensure_venv
SCRIPT_DIR = Path(__file__).parent
ensure_venv()
def send_to_whatsapp(message: str, group_name: str | None = None):
"""Send message to WhatsApp group via openclaw message tool."""
if not group_name:
group_name = os.environ.get('FINANCE_NEWS_TARGET', '')
if not group_name:
print("❌ No target specified. Set FINANCE_NEWS_TARGET env var or use --group", file=sys.stderr)
return False
# Use openclaw message tool
try:
result = subprocess.run(
[
'openclaw', 'message', 'send',
'--channel', 'whatsapp',
'--target', group_name,
'--message', message
],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
print(f"✅ Sent to WhatsApp group: {group_name}", file=sys.stderr)
return True
else:
print(f"⚠️ WhatsApp send failed: {result.stderr}", file=sys.stderr)
return False
except Exception as e:
print(f"❌ WhatsApp error: {e}", file=sys.stderr)
return False
def generate_and_send(args):
"""Generate briefing and optionally send to WhatsApp."""
# Determine briefing type based on current time or args
if args.time:
briefing_time = args.time
else:
hour = datetime.now().hour
briefing_time = 'morning' if hour < 12 else 'evening'
# Generate the briefing
cmd = [
sys.executable, SCRIPT_DIR / 'summarize.py',
'--time', briefing_time,
'--style', args.style,
'--lang', args.lang
]
if args.deadline is not None:
cmd.extend(['--deadline', str(args.deadline)])
if args.fast:
cmd.append('--fast')
if args.llm:
cmd.append('--llm')
cmd.extend(['--model', args.model])
if args.debug:
cmd.append('--debug')
# Always use JSON for internal processing to handle splits
cmd.append('--json')
print(f"📊 Generating {briefing_time} briefing...", file=sys.stderr)
timeout = args.deadline if args.deadline is not None else 300
timeout = max(1, int(timeout))
if args.deadline is not None:
timeout = timeout + 5
result = subprocess.run(
cmd,
capture_output=True,
text=True,
stdin=subprocess.DEVNULL,
timeout=timeout
)
if result.returncode != 0:
print(f"❌ Briefing generation failed: {result.stderr}", file=sys.stderr)
sys.exit(1)
try:
data = json.loads(result.stdout.strip())
except json.JSONDecodeError:
# Fallback if not JSON (shouldn't happen with --json)
print(f"⚠️ Failed to parse briefing JSON", file=sys.stderr)
print(result.stdout)
return result.stdout
# Output handling
if args.json:
print(json.dumps(data, indent=2))
else:
# Print for humans
if data.get('macro_message'):
print(data['macro_message'])
if data.get('portfolio_message'):
print("\n" + "="*20 + "\n")
print(data['portfolio_message'])
# Send to WhatsApp if requested
if args.send and args.group:
# Message 1: Macro
macro_msg = data.get('macro_message') or data.get('summary', '')
if macro_msg:
send_to_whatsapp(macro_msg, args.group)
# Message 2: Portfolio (if exists)
portfolio_msg = data.get('portfolio_message')
if portfolio_msg:
send_to_whatsapp(portfolio_msg, args.group)
return data.get('macro_message', '')
def main():
parser = argparse.ArgumentParser(description='Briefing Generator')
parser.add_argument('--time', choices=['morning', 'evening'],
help='Briefing type (auto-detected if not specified)')
parser.add_argument('--style', choices=['briefing', 'analysis', 'headlines'],
default='briefing', help='Summary style')
parser.add_argument('--lang', choices=['en', 'de'], default='en',
help='Output language')
parser.add_argument('--send', action='store_true',
help='Send to WhatsApp group')
parser.add_argument('--group', default=os.environ.get('FINANCE_NEWS_TARGET', ''),
help='WhatsApp group name or JID (default: FINANCE_NEWS_TARGET env var)')
parser.add_argument('--json', action='store_true',
help='Output as JSON')
parser.add_argument('--deadline', type=int, default=None,
help='Overall deadline in seconds')
parser.add_argument('--llm', action='store_true', help='Use LLM summary')
parser.add_argument('--model', choices=['claude', 'minimax', 'gemini'],
default='claude', help='LLM model (only with --llm)')
parser.add_argument('--fast', action='store_true',
help='Use fast mode (shorter timeouts, fewer items)')
parser.add_argument('--debug', action='store_true',
help='Write debug log with sources')
args = parser.parse_args()
generate_and_send(args)
if __name__ == '__main__':
main()

614
scripts/earnings.py Normal file
View File

@@ -0,0 +1,614 @@
#!/usr/bin/env python3
"""
Earnings Calendar - Track earnings dates for portfolio stocks.
Features:
- Fetch earnings dates from FMP API
- Show upcoming earnings in daily briefing
- Alert 24h before earnings release
- Cache results to avoid API spam
Usage:
earnings.py list # Show all upcoming earnings
earnings.py check # Check what's reporting today/this week
earnings.py refresh # Force refresh earnings data
"""
import argparse
import csv
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path
from urllib.request import urlopen, Request
from urllib.error import URLError, HTTPError
# Paths
SCRIPT_DIR = Path(__file__).parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
CACHE_DIR = SCRIPT_DIR.parent / "cache"
PORTFOLIO_FILE = CONFIG_DIR / "portfolio.csv"
EARNINGS_CACHE = CACHE_DIR / "earnings_calendar.json"
MANUAL_EARNINGS = CONFIG_DIR / "manual_earnings.json" # For JP/other stocks not in Finnhub
# OpenBB binary path
OPENBB_BINARY = None
try:
env_path = os.environ.get('OPENBB_QUOTE_BIN')
if env_path and os.path.isfile(env_path) and os.access(env_path, os.X_OK):
OPENBB_BINARY = env_path
else:
OPENBB_BINARY = shutil.which('openbb-quote')
except Exception:
pass
# API Keys
def get_fmp_key() -> str:
"""Get FMP API key from environment or .env file."""
key = os.environ.get("FMP_API_KEY", "")
if not key:
env_file = Path.home() / ".openclaw" / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("FMP_API_KEY="):
key = line.split("=", 1)[1].strip()
break
return key
def load_portfolio() -> list[dict]:
"""Load portfolio from CSV."""
if not PORTFOLIO_FILE.exists():
return []
with open(PORTFOLIO_FILE, 'r') as f:
reader = csv.DictReader(f)
return list(reader)
def load_earnings_cache() -> dict:
"""Load cached earnings data."""
if EARNINGS_CACHE.exists():
try:
return json.loads(EARNINGS_CACHE.read_text())
except Exception:
pass
return {"last_updated": None, "earnings": {}}
def load_manual_earnings() -> dict:
"""
Load manually-entered earnings dates (for JP stocks not in Finnhub).
Format: {"6857.T": {"date": "2026-01-30", "time": "amc", "note": "Q3 FY2025"}, ...}
"""
if MANUAL_EARNINGS.exists():
try:
data = json.loads(MANUAL_EARNINGS.read_text())
# Filter out metadata keys (starting with _)
return {k: v for k, v in data.items() if not k.startswith("_") and isinstance(v, dict)}
except Exception:
pass
return {}
def save_earnings_cache(data: dict):
"""Save earnings data to cache."""
CACHE_DIR.mkdir(exist_ok=True)
EARNINGS_CACHE.write_text(json.dumps(data, indent=2, default=str))
def get_finnhub_key() -> str:
"""Get Finnhub API key from environment or .env file."""
key = os.environ.get("FINNHUB_API_KEY", "")
if not key:
env_file = Path.home() / ".openclaw" / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("FINNHUB_API_KEY="):
key = line.split("=", 1)[1].strip()
break
return key
def fetch_all_earnings_finnhub(days_ahead: int = 60) -> dict:
"""
Fetch all earnings for the next N days from Finnhub.
Returns dict keyed by symbol: {"AAPL": {...}, ...}
"""
finnhub_key = get_finnhub_key()
if not finnhub_key:
return {}
from_date = datetime.now().strftime("%Y-%m-%d")
to_date = (datetime.now() + timedelta(days=days_ahead)).strftime("%Y-%m-%d")
url = f"https://finnhub.io/api/v1/calendar/earnings?from={from_date}&to={to_date}&token={finnhub_key}"
try:
req = Request(url, headers={"User-Agent": "finance-news/1.0"})
with urlopen(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
earnings_by_symbol = {}
for entry in data.get("earningsCalendar", []):
symbol = entry.get("symbol")
if symbol:
earnings_by_symbol[symbol] = {
"date": entry.get("date"),
"time": entry.get("hour", ""), # bmo/amc
"eps_estimate": entry.get("epsEstimate"),
"revenue_estimate": entry.get("revenueEstimate"),
"quarter": entry.get("quarter"),
"year": entry.get("year"),
}
return earnings_by_symbol
except Exception as e:
print(f"❌ Finnhub error: {e}", file=sys.stderr)
return {}
def normalize_ticker_for_lookup(ticker: str) -> list[str]:
"""
Convert portfolio ticker to possible Finnhub symbols.
Returns list of possible formats to try.
"""
variants = [ticker]
# Japanese stocks: 6857.T -> try 6857
if ticker.endswith('.T'):
base = ticker.replace('.T', '')
variants.extend([base, f"{base}.T"])
# Singapore stocks: D05.SI -> try D05
elif ticker.endswith('.SI'):
base = ticker.replace('.SI', '')
variants.extend([base, f"{base}.SI"])
return variants
def fetch_earnings_for_portfolio(portfolio: list[dict]) -> dict:
"""
Fetch earnings dates for portfolio stocks using Finnhub bulk API.
More efficient than per-ticker calls.
"""
# Get all earnings for next 60 days
all_earnings = fetch_all_earnings_finnhub(days_ahead=60)
if not all_earnings:
return {}
# Match portfolio tickers to earnings data
results = {}
for stock in portfolio:
ticker = stock["symbol"]
variants = normalize_ticker_for_lookup(ticker)
for variant in variants:
if variant in all_earnings:
results[ticker] = all_earnings[variant]
break
return results
def refresh_earnings(portfolio: list[dict], force: bool = False) -> dict:
"""Refresh earnings data for all portfolio stocks."""
finnhub_key = get_finnhub_key()
if not finnhub_key:
print("❌ FINNHUB_API_KEY not found", file=sys.stderr)
return {}
cache = load_earnings_cache()
# Check if cache is fresh (< 6 hours old)
if not force and cache.get("last_updated"):
try:
last = datetime.fromisoformat(cache["last_updated"])
if datetime.now() - last < timedelta(hours=6):
print(f"📦 Using cached data (updated {last.strftime('%H:%M')})")
return cache
except Exception:
pass
print(f"🔄 Fetching earnings calendar from Finnhub...")
# Use bulk fetch - much more efficient
earnings = fetch_earnings_for_portfolio(portfolio)
# Merge manual earnings (for JP stocks not in Finnhub)
manual = load_manual_earnings()
if manual:
print(f"📝 Merging {len(manual)} manual entries...")
for ticker, data in manual.items():
if ticker not in earnings: # Manual data fills gaps
earnings[ticker] = data
found = len(earnings)
total = len(portfolio)
print(f"✅ Found earnings data for {found}/{total} stocks")
if earnings:
for ticker, data in sorted(earnings.items(), key=lambda x: x[1].get("date", "")):
print(f"{ticker}: {data.get('date', '?')}")
cache = {
"last_updated": datetime.now().isoformat(),
"earnings": earnings
}
save_earnings_cache(cache)
return cache
def list_earnings(args):
"""List all upcoming earnings for portfolio."""
portfolio = load_portfolio()
if not portfolio:
print("📂 Portfolio empty")
return
cache = refresh_earnings(portfolio, force=args.refresh)
earnings = cache.get("earnings", {})
if not earnings:
print("\n❌ No earnings dates found")
return
# Sort by date
sorted_earnings = sorted(
[(ticker, data) for ticker, data in earnings.items() if data.get("date")],
key=lambda x: x[1]["date"]
)
print(f"\n📅 Upcoming Earnings ({len(sorted_earnings)} stocks)\n")
today = datetime.now().date()
for ticker, data in sorted_earnings:
date_str = data["date"]
try:
ed = datetime.strptime(date_str, "%Y-%m-%d").date()
days_until = (ed - today).days
# Emoji based on timing
if days_until < 0:
emoji = "" # Past
timing = f"{-days_until}d ago"
elif days_until == 0:
emoji = "🔴" # Today!
timing = "TODAY"
elif days_until == 1:
emoji = "🟡" # Tomorrow
timing = "TOMORROW"
elif days_until <= 7:
emoji = "🟠" # This week
timing = f"in {days_until}d"
else:
emoji = "" # Later
timing = f"in {days_until}d"
# Time of day
time_str = ""
if data.get("time") == "bmo":
time_str = " (pre-market)"
elif data.get("time") == "amc":
time_str = " (after-close)"
# EPS estimate
eps_str = ""
if data.get("eps_estimate"):
eps_str = f" | Est: ${data['eps_estimate']:.2f}"
# Stock name from portfolio
stock_name = next((s["name"] for s in portfolio if s["symbol"] == ticker), ticker)
print(f"{emoji} {date_str} ({timing}): **{ticker}** — {stock_name}{time_str}{eps_str}")
except ValueError:
print(f"{date_str}: {ticker}")
print()
def check_earnings(args):
"""Check earnings for today and this week (briefing format)."""
portfolio = load_portfolio()
if not portfolio:
return
cache = load_earnings_cache()
# Auto-refresh if cache is stale
if not cache.get("last_updated"):
cache = refresh_earnings(portfolio, force=False)
else:
try:
last = datetime.fromisoformat(cache["last_updated"])
if datetime.now() - last > timedelta(hours=12):
cache = refresh_earnings(portfolio, force=False)
except Exception:
cache = refresh_earnings(portfolio, force=False)
earnings = cache.get("earnings", {})
if not earnings:
return
today = datetime.now().date()
week_only = getattr(args, 'week', False)
# For weekly mode (Sunday cron), show Mon-Fri of upcoming week
# Calculation: weekday() returns 0=Mon, 6=Sun. (7 - weekday) % 7 gives days until next Monday.
# Special case: if today is Monday (result=0), we want next Monday (7 days), not today.
if week_only:
days_until_monday = (7 - today.weekday()) % 7
if days_until_monday == 0 and today.weekday() != 0:
days_until_monday = 7
week_start = today + timedelta(days=days_until_monday)
week_end = week_start + timedelta(days=4) # Mon-Fri
else:
week_end = today + timedelta(days=7)
today_list = []
week_list = []
for ticker, data in earnings.items():
if not data.get("date"):
continue
try:
ed = datetime.strptime(data["date"], "%Y-%m-%d").date()
stock = next((s for s in portfolio if s["symbol"] == ticker), None)
name = stock["name"] if stock else ticker
category = stock.get("category", "") if stock else ""
entry = {
"ticker": ticker,
"name": name,
"date": ed,
"time": data.get("time", ""),
"eps_estimate": data.get("eps_estimate"),
"category": category,
}
if week_only:
# Weekly mode: only show week range
if week_start <= ed <= week_end:
week_list.append(entry)
else:
# Daily mode: today + this week
if ed == today:
today_list.append(entry)
elif today < ed <= week_end:
week_list.append(entry)
except ValueError:
continue
# Handle JSON output
if getattr(args, 'json', False):
if week_only:
result = {
"week_start": week_start.isoformat(),
"week_end": week_end.isoformat(),
"earnings": [
{
"ticker": e["ticker"],
"name": e["name"],
"date": e["date"].isoformat(),
"time": e["time"],
"eps_estimate": e.get("eps_estimate"),
"category": e.get("category", ""),
}
for e in sorted(week_list, key=lambda x: x["date"])
],
}
else:
result = {
"today": [
{
"ticker": e["ticker"],
"name": e["name"],
"date": e["date"].isoformat(),
"time": e["time"],
"eps_estimate": e.get("eps_estimate"),
"category": e.get("category", ""),
}
for e in sorted(today_list, key=lambda x: x.get("time", "zzz"))
],
"this_week": [
{
"ticker": e["ticker"],
"name": e["name"],
"date": e["date"].isoformat(),
"time": e["time"],
"eps_estimate": e.get("eps_estimate"),
"category": e.get("category", ""),
}
for e in sorted(week_list, key=lambda x: x["date"])
],
}
print(json.dumps(result, indent=2))
return
# Translations
lang = getattr(args, 'lang', 'en')
if lang == "de":
labels = {
"today": "EARNINGS HEUTE",
"week": "EARNINGS DIESE WOCHE",
"week_preview": "EARNINGS NÄCHSTE WOCHE",
"pre": "vor Börseneröffnung",
"post": "nach Börsenschluss",
"pre_short": "vor",
"post_short": "nach",
"est": "Erw",
"none": "Keine Earnings diese Woche",
"none_week": "Keine Earnings nächste Woche",
}
else:
labels = {
"today": "EARNINGS TODAY",
"week": "EARNINGS THIS WEEK",
"week_preview": "EARNINGS NEXT WEEK",
"pre": "pre-market",
"post": "after-close",
"pre_short": "pre",
"post_short": "post",
"est": "Est",
"none": "No earnings this week",
"none_week": "No earnings next week",
}
# Date header
date_str = datetime.now().strftime("%b %d, %Y") if lang == "en" else datetime.now().strftime("%d. %b %Y")
# Output for briefing
output = []
# Daily mode: show today's earnings
if not week_only and today_list:
output.append(f"📅 {labels['today']}{date_str}\n")
for e in sorted(today_list, key=lambda x: x.get("time", "zzz")):
time_str = f" ({labels['pre']})" if e["time"] == "bmo" else f" ({labels['post']})" if e["time"] == "amc" else ""
eps_str = f"{labels['est']}: ${e['eps_estimate']:.2f}" if e.get("eps_estimate") else ""
output.append(f"{e['ticker']}{e['name']}{time_str}{eps_str}")
output.append("")
if week_list:
# Use different header for weekly preview mode
week_label = labels['week_preview'] if week_only else labels['week']
if week_only:
# Show date range for weekly preview
week_range = f"{week_start.strftime('%b %d')} - {week_end.strftime('%b %d')}"
output.append(f"📅 {week_label} ({week_range})\n")
else:
output.append(f"📅 {week_label}\n")
for e in sorted(week_list, key=lambda x: x["date"]):
day_name = e["date"].strftime("%a %d.%m")
time_str = f" ({labels['pre_short']})" if e["time"] == "bmo" else f" ({labels['post_short']})" if e["time"] == "amc" else ""
output.append(f"{day_name}: {e['ticker']}{e['name']}{time_str}")
output.append("")
if output:
print("\n".join(output))
else:
if args.verbose:
no_earnings_label = labels['none_week'] if week_only else labels['none']
print(f"📅 {no_earnings_label}")
def get_briefing_section() -> str:
"""Get earnings section for daily briefing (called by briefing.py)."""
from io import StringIO
import contextlib
# Capture check output
class Args:
verbose = False
f = StringIO()
with contextlib.redirect_stdout(f):
check_earnings(Args())
return f.getvalue()
def get_earnings_context(symbols: list[str]) -> list[dict]:
"""
Get recent earnings data (beats/misses) for symbols using OpenBB.
Returns list of dicts with: symbol, eps_actual, eps_estimate, surprise, revenue_actual, revenue_estimate
"""
if not OPENBB_BINARY:
return []
results = []
for symbol in symbols[:10]: # Limit to 10 symbols
try:
result = subprocess.run(
[OPENBB_BINARY, symbol, '--earnings'],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
try:
data = json.loads(result.stdout)
if isinstance(data, list) and data:
results.append({
'symbol': symbol,
'earnings': data[0] if isinstance(data[0], dict) else {}
})
except json.JSONDecodeError:
pass
except Exception:
pass
return results
def get_analyst_ratings(symbols: list[str]) -> list[dict]:
"""
Get analyst upgrades/downgrades for symbols using OpenBB.
Returns list of dicts with: symbol, rating, target_price, firm, direction
"""
if not OPENBB_BINARY:
return []
results = []
for symbol in symbols[:10]: # Limit to 10 symbols
try:
result = subprocess.run(
[OPENBB_BINARY, symbol, '--rating'],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
try:
data = json.loads(result.stdout)
if isinstance(data, list) and data:
results.append({
'symbol': symbol,
'rating': data[0] if isinstance(data[0], dict) else {}
})
except json.JSONDecodeError:
pass
except Exception:
pass
return results
def main():
parser = argparse.ArgumentParser(description="Earnings Calendar Tracker")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# list command
list_parser = subparsers.add_parser("list", help="List all upcoming earnings")
list_parser.add_argument("--refresh", "-r", action="store_true", help="Force refresh")
list_parser.set_defaults(func=list_earnings)
# check command
check_parser = subparsers.add_parser("check", help="Check today/this week")
check_parser.add_argument("--verbose", "-v", action="store_true")
check_parser.add_argument("--json", action="store_true", help="JSON output")
check_parser.add_argument("--lang", default="en", help="Output language (en, de)")
check_parser.add_argument("--week", action="store_true", help="Show full week preview (for weekly cron)")
check_parser.set_defaults(func=check_earnings)
# refresh command
refresh_parser = subparsers.add_parser("refresh", help="Force refresh all data")
refresh_parser.set_defaults(func=lambda a: refresh_earnings(load_portfolio(), force=True))
args = parser.parse_args()
if not args.command:
parser.print_help()
return
args.func(args)
if __name__ == "__main__":
main()

1126
scripts/fetch_news.py Normal file

File diff suppressed because it is too large Load Diff

317
scripts/portfolio.py Normal file
View File

@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""
Portfolio Manager - CRUD operations for stock watchlist.
"""
import argparse
import csv
import sys
from pathlib import Path
PORTFOLIO_FILE = Path(__file__).parent.parent / "config" / "portfolio.csv"
REQUIRED_COLUMNS = ['symbol', 'name']
DEFAULT_COLUMNS = ['symbol', 'name', 'category', 'notes', 'type']
def validate_portfolio_csv(path: Path) -> tuple[bool, list[str]]:
"""
Validate portfolio CSV file for common issues.
Returns:
Tuple of (is_valid, list of warnings)
"""
warnings = []
if not path.exists():
return True, warnings
try:
with open(path, 'r', encoding='utf-8') as f:
# Check for encoding issues
content = f.read()
with open(path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
# Check required columns
if reader.fieldnames is None:
warnings.append("CSV appears to be empty")
return False, warnings
missing_cols = set(REQUIRED_COLUMNS) - set(reader.fieldnames or [])
if missing_cols:
warnings.append(f"Missing required columns: {', '.join(missing_cols)}")
# Check for duplicate symbols
symbols = []
for row in reader:
symbol = row.get('symbol', '').strip().upper()
if symbol:
symbols.append(symbol)
duplicates = [s for s in set(symbols) if symbols.count(s) > 1]
if duplicates:
warnings.append(f"Duplicate symbols found: {', '.join(duplicates)}")
except UnicodeDecodeError:
warnings.append("File encoding issue - try saving as UTF-8")
except Exception as e:
warnings.append(f"Error reading portfolio: {e}")
return False, warnings
return True, warnings
def load_portfolio() -> list[dict]:
"""Load portfolio from CSV with validation."""
if not PORTFOLIO_FILE.exists():
return []
# Validate first
is_valid, warnings = validate_portfolio_csv(PORTFOLIO_FILE)
for warning in warnings:
print(f"⚠️ Portfolio warning: {warning}", file=sys.stderr)
if not is_valid:
print("⚠️ Portfolio has errors - returning empty", file=sys.stderr)
return []
try:
with open(PORTFOLIO_FILE, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
# Normalize data
portfolio = []
seen_symbols = set()
for row in reader:
symbol = row.get('symbol', '').strip().upper()
if not symbol:
continue
# Skip duplicates (keep first occurrence)
if symbol in seen_symbols:
continue
seen_symbols.add(symbol)
portfolio.append({
'symbol': symbol,
'name': row.get('name', symbol) or symbol,
'category': row.get('category', '') or '',
'notes': row.get('notes', '') or '',
'type': row.get('type', 'Watchlist') or 'Watchlist'
})
return portfolio
except Exception as e:
print(f"⚠️ Error loading portfolio: {e}", file=sys.stderr)
return []
def save_portfolio(portfolio: list[dict]):
"""Save portfolio to CSV."""
if not portfolio:
PORTFOLIO_FILE.write_text("symbol,name,category,notes,type\n")
return
with open(PORTFOLIO_FILE, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['symbol', 'name', 'category', 'notes', 'type'])
writer.writeheader()
writer.writerows(portfolio)
def list_portfolio(args):
"""List all stocks in portfolio."""
portfolio = load_portfolio()
if not portfolio:
print("📂 Portfolio is empty. Use 'portfolio add <SYMBOL>' to add stocks.")
return
print(f"\n📊 Portfolio ({len(portfolio)} stocks)\n")
# Group by Type then Category
by_type = {}
for stock in portfolio:
t = stock.get('type', 'Watchlist') or 'Watchlist'
if t not in by_type:
by_type[t] = []
by_type[t].append(stock)
for t, type_stocks in by_type.items():
print(f"# {t}")
categories = {}
for stock in type_stocks:
cat = stock.get('category', 'Other') or 'Other'
if cat not in categories:
categories[cat] = []
categories[cat].append(stock)
for cat, stocks in categories.items():
print(f"### {cat}")
for s in stocks:
notes = f"{s['notes']}" if s.get('notes') else ""
print(f"{s['symbol']}: {s['name']}{notes}")
print()
def add_stock(args):
"""Add a stock to portfolio."""
portfolio = load_portfolio()
# Check if already exists
if any(s['symbol'].upper() == args.symbol.upper() for s in portfolio):
print(f"⚠️ {args.symbol.upper()} already in portfolio")
return
new_stock = {
'symbol': args.symbol.upper(),
'name': args.name or args.symbol.upper(),
'category': args.category or '',
'notes': args.notes or '',
'type': args.type
}
portfolio.append(new_stock)
save_portfolio(portfolio)
print(f"✅ Added {args.symbol.upper()} to portfolio ({args.type})")
def remove_stock(args):
"""Remove a stock from portfolio."""
portfolio = load_portfolio()
original_len = len(portfolio)
portfolio = [s for s in portfolio if s['symbol'].upper() != args.symbol.upper()]
if len(portfolio) == original_len:
print(f"⚠️ {args.symbol.upper()} not found in portfolio")
return
save_portfolio(portfolio)
print(f"✅ Removed {args.symbol.upper()} from portfolio")
def import_csv(args):
"""Import portfolio from external CSV."""
import_path = Path(args.file)
if not import_path.exists():
print(f"❌ File not found: {args.file}")
sys.exit(1)
with open(import_path, 'r') as f:
reader = csv.DictReader(f)
imported = list(reader)
# Normalize fields
normalized = []
for row in imported:
normalized.append({
'symbol': row.get('symbol', row.get('Symbol', row.get('ticker', ''))).upper(),
'name': row.get('name', row.get('Name', row.get('company', ''))),
'category': row.get('category', row.get('Category', row.get('sector', ''))),
'notes': row.get('notes', row.get('Notes', '')),
'type': row.get('type', 'Watchlist')
})
save_portfolio(normalized)
print(f"✅ Imported {len(normalized)} stocks from {args.file}")
def create_interactive(args):
"""Interactive portfolio creation."""
print("\n📊 Portfolio Creator\n")
print("Enter stocks one per line (format: SYMBOL or SYMBOL,Name,Category)")
print("Type 'done' when finished.\n")
portfolio = []
while True:
try:
line = input("> ").strip()
except (EOFError, KeyboardInterrupt):
break
if line.lower() == 'done':
break
if not line:
continue
parts = line.split(',')
symbol = parts[0].strip().upper()
name = parts[1].strip() if len(parts) > 1 else symbol
category = parts[2].strip() if len(parts) > 2 else ''
portfolio.append({
'symbol': symbol,
'name': name,
'category': category,
'notes': '',
'type': 'Watchlist'
})
print(f" Added: {symbol}")
if portfolio:
save_portfolio(portfolio)
print(f"\n✅ Created portfolio with {len(portfolio)} stocks")
else:
print("\n⚠️ No stocks added")
def get_symbols(args=None):
"""Get list of symbols (for other scripts to use)."""
portfolio = load_portfolio()
symbols = [s['symbol'] for s in portfolio]
if args and args.json:
import json
print(json.dumps(symbols))
else:
print(','.join(symbols))
def main():
parser = argparse.ArgumentParser(description='Portfolio Manager')
subparsers = parser.add_subparsers(dest='command', required=True)
# List command
list_parser = subparsers.add_parser('list', help='List portfolio')
list_parser.set_defaults(func=list_portfolio)
# Add command
add_parser = subparsers.add_parser('add', help='Add stock')
add_parser.add_argument('symbol', help='Stock symbol')
add_parser.add_argument('--name', help='Company name')
add_parser.add_argument('--category', help='Category (e.g., Tech, Finance)')
add_parser.add_argument('--notes', help='Notes')
add_parser.add_argument('--type', choices=['Holding', 'Watchlist'], default='Watchlist', help='Portfolio type')
add_parser.set_defaults(func=add_stock)
# Remove command
remove_parser = subparsers.add_parser('remove', help='Remove stock')
remove_parser.add_argument('symbol', help='Stock symbol')
remove_parser.set_defaults(func=remove_stock)
# Import command
import_parser = subparsers.add_parser('import', help='Import from CSV')
import_parser.add_argument('file', help='CSV file path')
import_parser.set_defaults(func=import_csv)
# Create command
create_parser = subparsers.add_parser('create', help='Interactive creation')
create_parser.set_defaults(func=create_interactive)
# Symbols command (for other scripts)
symbols_parser = subparsers.add_parser('symbols', help='Get symbols list')
symbols_parser.add_argument('--json', action='store_true', help='Output as JSON')
symbols_parser.set_defaults(func=get_symbols)
args = parser.parse_args()
args.func(args)
if __name__ == '__main__':
main()

325
scripts/ranking.py Normal file
View File

@@ -0,0 +1,325 @@
#!/usr/bin/env python3
"""
Deterministic Headline Ranking - Impact-based ranking policy.
Implements #53: Deterministic impact-based ranking for headline selection.
Scoring Rubric (weights):
- Market Impact (40%): CB decisions, earnings, sanctions, oil spikes
- Novelty (20%): New vs recycled news
- Breadth (20%): Sector-wide vs single-stock
- Credibility (10%): Source reliability
- Diversity Bonus (10%): Underrepresented categories
Output:
- MUST_READ: Top 5 stories
- SCAN: 3-5 additional stories (if quality threshold met)
"""
import re
from datetime import datetime
from difflib import SequenceMatcher
# Category keywords for classification
CATEGORY_KEYWORDS = {
"macro": ["fed", "ecb", "boj", "central bank", "rate", "inflation", "gdp", "unemployment", "treasury", "yield", "bond"],
"equities": ["earnings", "revenue", "profit", "eps", "guidance", "beat", "miss", "upgrade", "downgrade", "target"],
"geopolitics": ["sanction", "tariff", "war", "conflict", "embargo", "trump", "china", "russia", "ukraine", "iran", "trade war"],
"energy": ["oil", "opec", "crude", "gas", "energy", "brent", "wti"],
"tech": ["ai", "chip", "semiconductor", "nvidia", "apple", "google", "microsoft", "meta", "amazon"],
}
# Source credibility scores (0-1)
SOURCE_CREDIBILITY = {
"Wall Street Journal": 0.95,
"WSJ": 0.95,
"Bloomberg": 0.95,
"Reuters": 0.90,
"Financial Times": 0.90,
"CNBC": 0.80,
"Yahoo Finance": 0.70,
"MarketWatch": 0.75,
"Barron's": 0.85,
"Seeking Alpha": 0.60,
"Tagesschau": 0.85,
"Handelsblatt": 0.80,
}
# Default config
DEFAULT_CONFIG = {
"dedupe_threshold": 0.7,
"must_read_count": 5,
"scan_count": 5,
"must_read_min_score": 0.4,
"scan_min_score": 0.25,
"source_cap": 2,
"weights": {
"market_impact": 0.40,
"novelty": 0.20,
"breadth": 0.20,
"credibility": 0.10,
"diversity": 0.10,
},
}
def normalize_title(title: str) -> str:
"""Normalize title for comparison."""
if not title:
return ""
cleaned = re.sub(r"[^a-z0-9\s]", " ", title.lower())
tokens = cleaned.split()
return " ".join(tokens)
def title_similarity(a: str, b: str) -> float:
"""Calculate title similarity using SequenceMatcher."""
if not a or not b:
return 0.0
return SequenceMatcher(None, normalize_title(a), normalize_title(b)).ratio()
def deduplicate_headlines(headlines: list[dict], threshold: float = 0.7) -> list[dict]:
"""Remove duplicate headlines by title similarity."""
if not headlines:
return []
unique = []
for article in headlines:
title = article.get("title", "")
is_dupe = False
for existing in unique:
if title_similarity(title, existing.get("title", "")) > threshold:
is_dupe = True
break
if not is_dupe:
unique.append(article)
return unique
def classify_category(title: str, description: str = "") -> list[str]:
"""Classify headline into categories based on keywords."""
text = f"{title} {description}".lower()
categories = []
for category, keywords in CATEGORY_KEYWORDS.items():
for keyword in keywords:
if keyword in text:
categories.append(category)
break
return categories if categories else ["general"]
def score_market_impact(title: str, description: str = "") -> float:
"""Score market impact (0-1)."""
text = f"{title} {description}".lower()
score = 0.3 # Base score
# High impact indicators
high_impact = ["fed", "rate cut", "rate hike", "earnings", "guidance", "sanctions", "war", "oil", "recession"]
for term in high_impact:
if term in text:
score += 0.15
# Medium impact
medium_impact = ["profit", "revenue", "gdp", "inflation", "tariff", "merger", "acquisition"]
for term in medium_impact:
if term in text:
score += 0.1
return min(score, 1.0)
def score_novelty(article: dict) -> float:
"""Score novelty based on recency (0-1)."""
published_at = article.get("published_at")
if not published_at:
return 0.5 # Unknown = medium
try:
if isinstance(published_at, str):
pub_time = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
else:
pub_time = published_at
hours_old = (datetime.now(pub_time.tzinfo) - pub_time).total_seconds() / 3600
if hours_old < 2:
return 1.0
elif hours_old < 6:
return 0.8
elif hours_old < 12:
return 0.6
elif hours_old < 24:
return 0.4
else:
return 0.2
except Exception:
return 0.5
def score_breadth(categories: list[str]) -> float:
"""Score breadth - sector-wide vs single-stock (0-1)."""
# More categories = broader impact
if "macro" in categories or "geopolitics" in categories:
return 0.9
if "energy" in categories:
return 0.7
if len(categories) > 1:
return 0.6
return 0.4
def score_credibility(source: str) -> float:
"""Score source credibility (0-1)."""
return SOURCE_CREDIBILITY.get(source, 0.5)
def calculate_score(article: dict, weights: dict, category_counts: dict) -> float:
"""Calculate overall score for a headline."""
title = article.get("title", "")
description = article.get("description", "")
source = article.get("source", "")
categories = classify_category(title, description)
article["_categories"] = categories # Store for later use
# Component scores
impact = score_market_impact(title, description)
novelty = score_novelty(article)
breadth = score_breadth(categories)
credibility = score_credibility(source)
# Diversity bonus - boost underrepresented categories
diversity = 0.0
for cat in categories:
if category_counts.get(cat, 0) < 1:
diversity = 0.5
break
elif category_counts.get(cat, 0) < 2:
diversity = 0.3
# Weighted sum
score = (
impact * weights.get("market_impact", 0.4) +
novelty * weights.get("novelty", 0.2) +
breadth * weights.get("breadth", 0.2) +
credibility * weights.get("credibility", 0.1) +
diversity * weights.get("diversity", 0.1)
)
article["_score"] = round(score, 3)
article["_impact"] = round(impact, 3)
article["_novelty"] = round(novelty, 3)
return score
def apply_source_cap(ranked: list[dict], cap: int = 2) -> list[dict]:
"""Apply source cap - max N items per outlet."""
source_counts = {}
result = []
for article in ranked:
source = article.get("source", "Unknown")
if source_counts.get(source, 0) < cap:
result.append(article)
source_counts[source] = source_counts.get(source, 0) + 1
return result
def ensure_diversity(selected: list[dict], candidates: list[dict], required: list[str]) -> list[dict]:
"""Ensure at least one headline from required categories if available."""
result = list(selected)
covered = set()
for article in result:
for cat in article.get("_categories", []):
covered.add(cat)
for req_cat in required:
if req_cat not in covered:
# Find candidate from this category
for candidate in candidates:
if candidate not in result and req_cat in candidate.get("_categories", []):
result.append(candidate)
covered.add(req_cat)
break
return result
def rank_headlines(headlines: list[dict], config: dict | None = None) -> dict:
"""
Rank headlines deterministically.
Args:
headlines: List of headline dicts with title, source, description, etc.
config: Optional config overrides
Returns:
{"must_read": [...], "scan": [...]}
"""
cfg = {**DEFAULT_CONFIG, **(config or {})}
weights = cfg.get("weights", DEFAULT_CONFIG["weights"])
if not headlines:
return {"must_read": [], "scan": []}
# Step 1: Deduplicate
unique = deduplicate_headlines(headlines, cfg["dedupe_threshold"])
# Step 2: Score all headlines
category_counts = {}
for article in unique:
calculate_score(article, weights, category_counts)
for cat in article.get("_categories", []):
category_counts[cat] = category_counts.get(cat, 0) + 1
# Step 3: Sort by score
ranked = sorted(unique, key=lambda x: x.get("_score", 0), reverse=True)
# Step 4: Apply source cap
capped = apply_source_cap(ranked, cfg["source_cap"])
# Step 5: Select must_read with diversity quota
# Leave room for diversity additions by taking count-1 initially
must_read_candidates = [a for a in capped if a.get("_score", 0) >= cfg["must_read_min_score"]]
must_read_count = cfg["must_read_count"]
must_read = must_read_candidates[:max(1, must_read_count - 2)] # Reserve 2 slots for diversity
must_read = ensure_diversity(must_read, capped, ["macro", "equities", "geopolitics"])
must_read = must_read[:must_read_count] # Final trim to exact count
# Step 6: Select scan (additional items)
scan_candidates = [a for a in capped if a not in must_read and a.get("_score", 0) >= cfg["scan_min_score"]]
scan = scan_candidates[:cfg["scan_count"]]
return {
"must_read": must_read,
"scan": scan,
"total_processed": len(headlines),
"after_dedupe": len(unique),
}
if __name__ == "__main__":
# Test with sample data
test_headlines = [
{"title": "Fed signals rate cut in March", "source": "WSJ", "description": "Federal Reserve hints at policy shift"},
{"title": "Apple earnings beat expectations", "source": "CNBC", "description": "Revenue up 15%"},
{"title": "Oil prices surge on OPEC cuts", "source": "Reuters", "description": "Brent crude hits $90"},
{"title": "China-US trade tensions escalate", "source": "Bloomberg", "description": "New tariffs announced"},
{"title": "Tech stocks rally on AI optimism", "source": "Yahoo Finance", "description": "Nvidia leads gains"},
{"title": "Fed hints at rate reduction", "source": "MarketWatch", "description": "Same story as WSJ"}, # Dupe
]
result = rank_headlines(test_headlines)
print("MUST_READ:")
for h in result["must_read"]:
print(f" [{h['_score']:.2f}] {h['title']} ({h['source']})")
print("\nSCAN:")
for h in result["scan"]:
print(f" [{h['_score']:.2f}] {h['title']} ({h['source']})")

283
scripts/research.py Normal file
View File

@@ -0,0 +1,283 @@
#!/usr/bin/env python3
"""
Research Module - Deep research using Gemini CLI.
Crawls articles, finds correlations, researches companies.
Outputs research_report.md for later analysis.
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from utils import ensure_venv
from fetch_news import PortfolioError, get_market_news, get_portfolio_news
SCRIPT_DIR = Path(__file__).parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
OUTPUT_DIR = SCRIPT_DIR.parent / "research"
ensure_venv()
def format_market_data(market_data: dict) -> str:
"""Format market data for research prompt."""
lines = ["## Market Data\n"]
for region, data in market_data.get('markets', {}).items():
lines.append(f"### {data['name']}")
for symbol, idx in data.get('indices', {}).items():
if 'data' in idx and idx['data']:
price = idx['data'].get('price', 'N/A')
change_pct = idx['data'].get('change_percent', 0)
emoji = '📈' if change_pct >= 0 else '📉'
lines.append(f"- {idx['name']}: {price} ({change_pct:+.2f}%) {emoji}")
lines.append("")
return '\n'.join(lines)
def format_headlines(headlines: list) -> str:
"""Format headlines for research prompt."""
lines = ["## Current Headlines\n"]
for article in headlines[:20]:
source = article.get('source', 'Unknown')
title = article.get('title', '')
link = article.get('link', '')
lines.append(f"- [{source}] {title}")
if link:
lines.append(f" URL: {link}")
return '\n'.join(lines)
def format_portfolio_news(portfolio_data: dict) -> str:
"""Format portfolio news for research prompt."""
lines = ["## Portfolio Analysis\n"]
for symbol, data in portfolio_data.get('stocks', {}).items():
quote = data.get('quote', {})
price = quote.get('price', 'N/A')
change_pct = quote.get('change_percent', 0)
lines.append(f"### {symbol} (${price}, {change_pct:+.2f}%)")
for article in data.get('articles', [])[:5]:
title = article.get('title', '')
link = article.get('link', '')
lines.append(f"- {title}")
if link:
lines.append(f" URL: {link}")
lines.append("")
return '\n'.join(lines)
def gemini_available() -> bool:
return shutil.which('gemini') is not None
def research_with_gemini(content: str, focus_areas: list = None) -> str:
"""Perform deep research using Gemini CLI.
Args:
content: Combined market/headlines/portfolio content
focus_areas: Optional list of focus areas (e.g., ['earnings', 'macro', 'sectors'])
Returns:
Research report text
"""
focus_prompt = ""
if focus_areas:
focus_prompt = f"""
Focus areas for the research:
{', '.join(f'- {area}' for area in focus_areas)}
Go deep on each area.
"""
prompt = f"""You are an experienced investment research analyst.
Your task is to deliver deep research on current market developments.
{focus_prompt}
Please analyze the following market data:
{content}
## Analysis Requirements:
1. **Macro Trends**: What is driving the market today? Which economic data/decisions matter?
2. **Sector Analysis**: Which sectors are performing best/worst? Why?
3. **Company News**: Relevant earnings, M&A, product launches?
4. **Risks**: What downside risks should be noted?
5. **Opportunities**: Which positive developments offer opportunities?
6. **Correlations**: Are there links between different news items/asset classes?
7. **Trade Ideas**: Concrete setups based on the analysis (not financial advice!)
8. **Sources**: Original links for further research
Be analytical, objective, and opinionated where appropriate.
Deliver a substantial report (500-800 words).
"""
try:
result = subprocess.run(
['gemini', prompt],
capture_output=True,
text=True,
timeout=120
)
if result.returncode == 0:
return result.stdout.strip()
else:
return f"⚠️ Gemini research error: {result.stderr}"
except subprocess.TimeoutExpired:
return "⚠️ Gemini research timeout"
except FileNotFoundError:
return "⚠️ Gemini CLI not found. Install: brew install gemini-cli"
def format_raw_data_report(market_data: dict, portfolio_data: dict) -> str:
parts = []
if market_data:
parts.append(format_market_data(market_data))
if market_data.get('headlines'):
parts.append(format_headlines(market_data['headlines']))
if portfolio_data and 'error' not in portfolio_data:
parts.append(format_portfolio_news(portfolio_data))
return '\n\n'.join(parts)
def generate_research_content(market_data: dict, portfolio_data: dict, focus_areas: list = None) -> dict:
raw_report = format_raw_data_report(market_data, portfolio_data)
if not raw_report.strip():
return {
'report': '',
'source': 'none'
}
if gemini_available():
return {
'report': research_with_gemini(raw_report, focus_areas),
'source': 'gemini'
}
return {
'report': raw_report,
'source': 'raw'
}
def generate_research_report(args):
"""Generate full research report."""
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
config_path = CONFIG_DIR / "config.json"
if not config_path.exists():
print("⚠️ No config found. Run 'finance-news wizard' first.", file=sys.stderr)
sys.exit(1)
# Fetch fresh data
print("📡 Fetching market data...", file=sys.stderr)
# Get market overview
market_data = get_market_news(
args.limit if hasattr(args, 'limit') else 5,
regions=args.regions.split(',') if hasattr(args, 'regions') else ["us", "europe"],
max_indices_per_region=2
)
# Get portfolio news
try:
portfolio_data = get_portfolio_news(
args.limit if hasattr(args, 'limit') else 5,
args.max_stocks if hasattr(args, 'max_stocks') else 10
)
except PortfolioError as exc:
print(f"⚠️ Skipping portfolio: {exc}", file=sys.stderr)
portfolio_data = None
# Build report
focus_areas = None
if hasattr(args, 'focus') and args.focus:
focus_areas = args.focus.split(',')
research_result = generate_research_content(market_data, portfolio_data, focus_areas)
research_report = research_result['report']
source = research_result['source']
if not research_report.strip():
print("⚠️ No data available for research", file=sys.stderr)
return
if source == 'gemini':
print("🔬 Running deep research with Gemini...", file=sys.stderr)
else:
print("🧾 Gemini not available; using raw data report", file=sys.stderr)
# Add metadata header
timestamp = datetime.now().isoformat()
date_str = datetime.now().strftime("%Y-%m-%d %H:%M")
full_report = f"""# Market Research Report
**Generiert:** {date_str}
**Quelle:** Finance News Skill
---
{research_report}
---
*This report was generated automatically. Not financial advice.*
"""
# Save to file
output_file = OUTPUT_DIR / f"research_{datetime.now().strftime('%Y-%m-%d')}.md"
with open(output_file, 'w') as f:
f.write(full_report)
print(f"✅ Research report saved to: {output_file}", file=sys.stderr)
# Also output to stdout
if args.json:
print(json.dumps({
'report': research_report,
'saved_to': str(output_file),
'timestamp': timestamp
}))
else:
print("\n" + "="*60)
print("RESEARCH REPORT")
print("="*60)
print(research_report)
def main():
parser = argparse.ArgumentParser(description='Deep Market Research')
parser.add_argument('--limit', type=int, default=5, help='Max headlines per source')
parser.add_argument('--regions', default='us,europe', help='Comma-separated regions')
parser.add_argument('--max-stocks', type=int, default=10, help='Max portfolio stocks')
parser.add_argument('--focus', help='Focus areas (comma-separated)')
parser.add_argument('--json', action='store_true', help='Output as JSON')
args = parser.parse_args()
generate_research_report(args)
if __name__ == '__main__':
main()

290
scripts/setup.py Normal file
View File

@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""
Finance News Skill - Interactive Setup
Configures RSS feeds, WhatsApp channels, and cron jobs.
"""
import argparse
import json
import subprocess
import sys
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
CONFIG_DIR = SCRIPT_DIR.parent / "config"
SOURCES_FILE = CONFIG_DIR / "config.json"
def load_sources():
"""Load current sources configuration."""
if SOURCES_FILE.exists():
with open(SOURCES_FILE, 'r') as f:
return json.load(f)
return get_default_sources()
def save_sources(sources: dict):
"""Save sources configuration."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
with open(SOURCES_FILE, 'w') as f:
json.dump(sources, f, indent=2)
print(f"✅ Configuration saved to {SOURCES_FILE}")
def get_default_sources():
"""Return default source configuration."""
config_path = CONFIG_DIR / "config.json"
if config_path.exists():
with open(config_path, 'r') as f:
return json.load(f)
return {}
def prompt(message: str, default: str = "") -> str:
"""Prompt for input with optional default."""
if default:
result = input(f"{message} [{default}]: ").strip()
return result if result else default
return input(f"{message}: ").strip()
def prompt_bool(message: str, default: bool = True) -> bool:
"""Prompt for yes/no input."""
default_str = "Y/n" if default else "y/N"
result = input(f"{message} [{default_str}]: ").strip().lower()
if not result:
return default
return result in ('y', 'yes', '1', 'true')
def setup_rss_feeds(sources: dict):
"""Interactive RSS feed configuration."""
print("\n📰 RSS Feed Configuration\n")
print("Enable/disable news sources:\n")
for feed_id, feed_config in sources['rss_feeds'].items():
name = feed_config.get('name', feed_id)
current = feed_config.get('enabled', True)
enabled = prompt_bool(f" {name}", current)
sources['rss_feeds'][feed_id]['enabled'] = enabled
print("\n Add custom RSS feed? (leave blank to skip)")
custom_name = prompt(" Feed name", "")
if custom_name:
custom_url = prompt(" Feed URL")
sources['rss_feeds'][custom_name.lower().replace(' ', '_')] = {
"name": custom_name,
"enabled": True,
"main": custom_url
}
print(f" ✅ Added {custom_name}")
def setup_markets(sources: dict):
"""Interactive market configuration."""
print("\n📊 Market Coverage\n")
print("Enable/disable market regions:\n")
for market_id, market_config in sources['markets'].items():
name = market_config.get('name', market_id)
current = market_config.get('enabled', True)
enabled = prompt_bool(f" {name}", current)
sources['markets'][market_id]['enabled'] = enabled
def setup_delivery(sources: dict):
"""Interactive delivery channel configuration."""
print("\n📤 Delivery Channels\n")
# Ensure delivery dict exists
if 'delivery' not in sources:
sources['delivery'] = {
'whatsapp': {'enabled': True, 'group': ''},
'telegram': {'enabled': False, 'group': ''}
}
# WhatsApp
wa_enabled = prompt_bool("Enable WhatsApp delivery",
sources.get('delivery', {}).get('whatsapp', {}).get('enabled', True))
sources['delivery']['whatsapp']['enabled'] = wa_enabled
if wa_enabled:
wa_group = prompt(" WhatsApp group name or JID",
sources['delivery']['whatsapp'].get('group', ''))
sources['delivery']['whatsapp']['group'] = wa_group
# Telegram
tg_enabled = prompt_bool("Enable Telegram delivery",
sources['delivery']['telegram'].get('enabled', False))
sources['delivery']['telegram']['enabled'] = tg_enabled
if tg_enabled:
tg_group = prompt(" Telegram group name or ID",
sources['delivery']['telegram'].get('group', ''))
sources['delivery']['telegram']['group'] = tg_group
def setup_language(sources: dict):
"""Interactive language configuration."""
print("\n🌐 Language Settings\n")
current_lang = sources['language'].get('default', 'de')
lang = prompt("Default language (de/en)", current_lang)
if lang in sources['language']['supported']:
sources['language']['default'] = lang
else:
print(f" ⚠️ Unsupported language '{lang}', keeping '{current_lang}'")
def setup_schedule(sources: dict):
"""Interactive schedule configuration."""
print("\n⏰ Briefing Schedule\n")
# Morning
morning = sources['schedule']['morning']
morning_enabled = prompt_bool(f"Enable morning briefing ({morning['description']})",
morning.get('enabled', True))
sources['schedule']['morning']['enabled'] = morning_enabled
if morning_enabled:
morning_cron = prompt(" Morning cron expression", morning.get('cron', '30 6 * * 1-5'))
sources['schedule']['morning']['cron'] = morning_cron
# Evening
evening = sources['schedule']['evening']
evening_enabled = prompt_bool(f"Enable evening briefing ({evening['description']})",
evening.get('enabled', True))
sources['schedule']['evening']['enabled'] = evening_enabled
if evening_enabled:
evening_cron = prompt(" Evening cron expression", evening.get('cron', '0 13 * * 1-5'))
sources['schedule']['evening']['cron'] = evening_cron
# Timezone
tz = prompt("Timezone", sources['schedule']['morning'].get('timezone', 'America/Los_Angeles'))
sources['schedule']['morning']['timezone'] = tz
sources['schedule']['evening']['timezone'] = tz
def setup_cron_jobs(sources: dict):
"""Set up OpenClaw cron jobs based on configuration."""
print("\n📅 Setting up cron jobs...\n")
schedule = sources.get('schedule', {})
delivery = sources.get('delivery', {})
language = sources.get('language', {}).get('default', 'de')
# Determine delivery target
if delivery.get('whatsapp', {}).get('enabled'):
group = delivery['whatsapp'].get('group', '')
send_cmd = f"--send --group '{group}'" if group else ""
elif delivery.get('telegram', {}).get('enabled'):
group = delivery['telegram'].get('group', '')
send_cmd = f"--send --group '{group}'" # Would need telegram support
else:
send_cmd = ""
# Morning job
if schedule.get('morning', {}).get('enabled'):
morning_cron = schedule['morning'].get('cron', '30 6 * * 1-5')
tz = schedule['morning'].get('timezone', 'America/Los_Angeles')
print(f" Creating morning briefing job: {morning_cron} ({tz})")
# Note: Actual cron creation would happen via openclaw cron add
print(f" ✅ Morning briefing configured")
# Evening job
if schedule.get('evening', {}).get('enabled'):
evening_cron = schedule['evening'].get('cron', '0 13 * * 1-5')
tz = schedule['evening'].get('timezone', 'America/Los_Angeles')
print(f" Creating evening briefing job: {evening_cron} ({tz})")
print(f" ✅ Evening briefing configured")
def run_setup(args):
"""Run interactive setup wizard."""
print("\n" + "="*60)
print("📈 Finance News Skill - Setup Wizard")
print("="*60)
# Load existing or default config
if args.reset:
sources = get_default_sources()
print("\n⚠️ Starting with fresh configuration")
else:
sources = load_sources()
if SOURCES_FILE.exists():
print(f"\n📂 Loaded existing configuration from {SOURCES_FILE}")
else:
print("\n📂 No existing configuration found, using defaults")
# Run through each section
if not args.section or args.section == 'feeds':
setup_rss_feeds(sources)
if not args.section or args.section == 'markets':
setup_markets(sources)
if not args.section or args.section == 'delivery':
setup_delivery(sources)
if not args.section or args.section == 'language':
setup_language(sources)
if not args.section or args.section == 'schedule':
setup_schedule(sources)
# Save configuration
print("\n" + "-"*60)
if prompt_bool("Save configuration?", True):
save_sources(sources)
# Set up cron jobs
if prompt_bool("Set up cron jobs now?", True):
setup_cron_jobs(sources)
else:
print("❌ Configuration not saved")
print("\n✅ Setup complete!")
print("\nNext steps:")
print(" • Run 'finance-news portfolio-list' to check your watchlist")
print(" • Run 'finance-news briefing --morning' to test a briefing")
print(" • Run 'finance-news market' to see market overview")
print()
def show_config(args):
"""Show current configuration."""
sources = load_sources()
print(json.dumps(sources, indent=2))
def main():
parser = argparse.ArgumentParser(description='Finance News Setup')
subparsers = parser.add_subparsers(dest='command')
# Setup command (default)
setup_parser = subparsers.add_parser('wizard', help='Run setup wizard')
setup_parser.add_argument('--reset', action='store_true', help='Reset to defaults')
setup_parser.add_argument('--section', choices=['feeds', 'markets', 'delivery', 'language', 'schedule'],
help='Configure specific section only')
setup_parser.set_defaults(func=run_setup)
# Show config
show_parser = subparsers.add_parser('show', help='Show current configuration')
show_parser.set_defaults(func=show_config)
args = parser.parse_args()
if args.command:
args.func(args)
else:
# Default to wizard
args.reset = False
args.section = None
run_setup(args)
if __name__ == '__main__':
main()

335
scripts/stocks.py Normal file
View File

@@ -0,0 +1,335 @@
#!/usr/bin/env python3
"""
stocks.py - Unified stock management for holdings and watchlist.
Single source of truth for:
- Holdings (stocks you own)
- Watchlist (stocks you're watching to buy)
Usage:
from stocks import load_stocks, save_stocks, get_holdings, get_watchlist
from stocks import add_to_watchlist, add_to_holdings, move_to_holdings
CLI:
stocks.py list [--holdings|--watchlist]
stocks.py add-watchlist TICKER [--target 380] [--notes "Buy zone"]
stocks.py add-holding TICKER --name "Company" [--category "Tech"]
stocks.py move TICKER # watchlist → holdings (you bought it)
stocks.py remove TICKER [--from holdings|watchlist]
"""
import argparse
import json
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
# Default path - can be overridden
STOCKS_FILE = Path(__file__).parent.parent / "config" / "stocks.json"
def load_stocks(path: Optional[Path] = None) -> dict:
"""Load the unified stocks file."""
path = path or STOCKS_FILE
if not path.exists():
return {
"version": "1.0",
"updated": datetime.now().strftime("%Y-%m-%d"),
"holdings": [],
"watchlist": [],
"alert_definitions": {}
}
with open(path, 'r') as f:
return json.load(f)
def save_stocks(data: dict, path: Optional[Path] = None):
"""Save the unified stocks file."""
path = path or STOCKS_FILE
data["updated"] = datetime.now().strftime("%Y-%m-%d")
with open(path, 'w') as f:
json.dump(data, f, indent=2)
def get_holdings(data: Optional[dict] = None) -> list:
"""Get list of holdings."""
if data is None:
data = load_stocks()
return data.get("holdings", [])
def get_watchlist(data: Optional[dict] = None) -> list:
"""Get list of watchlist items."""
if data is None:
data = load_stocks()
return data.get("watchlist", [])
def get_holding_tickers(data: Optional[dict] = None) -> set:
"""Get set of holding tickers for quick lookup."""
holdings = get_holdings(data)
return {h.get("ticker") for h in holdings}
def get_watchlist_tickers(data: Optional[dict] = None) -> set:
"""Get set of watchlist tickers for quick lookup."""
watchlist = get_watchlist(data)
return {w.get("ticker") for w in watchlist}
def add_to_watchlist(
ticker: str,
target: Optional[float] = None,
stop: Optional[float] = None,
notes: str = "",
alerts: Optional[list] = None
) -> bool:
"""Add a stock to the watchlist."""
data = load_stocks()
# Check if already in watchlist
for w in data["watchlist"]:
if w.get("ticker") == ticker:
# Update existing
if target is not None:
w["target"] = target
if stop is not None:
w["stop"] = stop
if notes:
w["notes"] = notes
if alerts is not None:
w["alerts"] = alerts
save_stocks(data)
return True
# Add new
data["watchlist"].append({
"ticker": ticker,
"target": target,
"stop": stop,
"alerts": alerts or [],
"notes": notes
})
data["watchlist"].sort(key=lambda x: x.get("ticker", ""))
save_stocks(data)
return True
def add_to_holdings(
ticker: str,
name: str = "",
category: str = "",
notes: str = "",
target: Optional[float] = None,
stop: Optional[float] = None,
alerts: Optional[list] = None
) -> bool:
"""Add a stock to holdings. Target/stop for 'buy more' alerts."""
data = load_stocks()
# Check if already in holdings
for h in data["holdings"]:
if h.get("ticker") == ticker:
# Update existing
if name:
h["name"] = name
if category:
h["category"] = category
if notes:
h["notes"] = notes
if target is not None:
h["target"] = target
if stop is not None:
h["stop"] = stop
if alerts is not None:
h["alerts"] = alerts
save_stocks(data)
return True
# Add new
data["holdings"].append({
"ticker": ticker,
"name": name,
"category": category,
"notes": notes,
"target": target,
"stop": stop,
"alerts": alerts or []
})
data["holdings"].sort(key=lambda x: x.get("ticker", ""))
save_stocks(data)
return True
def move_to_holdings(
ticker: str,
name: str = "",
category: str = "",
notes: str = ""
) -> bool:
"""Move a stock from watchlist to holdings (you bought it)."""
data = load_stocks()
# Find in watchlist
watchlist_item = None
for i, w in enumerate(data["watchlist"]):
if w.get("ticker") == ticker:
watchlist_item = data["watchlist"].pop(i)
break
if not watchlist_item:
print(f"⚠️ {ticker} not found in watchlist", file=sys.stderr)
return False
# Add to holdings
data["holdings"].append({
"ticker": ticker,
"name": name or watchlist_item.get("notes", ""),
"category": category,
"notes": notes or f"Bought (was on watchlist with target ${watchlist_item.get('target', 'N/A')})"
})
data["holdings"].sort(key=lambda x: x.get("ticker", ""))
save_stocks(data)
return True
def remove_stock(ticker: str, from_list: str = "both") -> bool:
"""Remove a stock from holdings, watchlist, or both."""
data = load_stocks()
removed = False
if from_list in ("holdings", "both"):
original_len = len(data["holdings"])
data["holdings"] = [h for h in data["holdings"] if h.get("ticker") != ticker]
if len(data["holdings"]) < original_len:
removed = True
if from_list in ("watchlist", "both"):
original_len = len(data["watchlist"])
data["watchlist"] = [w for w in data["watchlist"] if w.get("ticker") != ticker]
if len(data["watchlist"]) < original_len:
removed = True
if removed:
save_stocks(data)
return removed
def list_stocks(show_holdings: bool = True, show_watchlist: bool = True):
"""Print stocks list."""
data = load_stocks()
if show_holdings:
print(f"\n📊 HOLDINGS ({len(data['holdings'])})")
print("-" * 50)
for h in data["holdings"][:20]:
print(f" {h['ticker']:10} {h.get('name', '')[:30]}")
if len(data["holdings"]) > 20:
print(f" ... and {len(data['holdings']) - 20} more")
if show_watchlist:
print(f"\n👀 WATCHLIST ({len(data['watchlist'])})")
print("-" * 50)
for w in data["watchlist"][:20]:
target = f"${w['target']}" if w.get('target') else "no target"
print(f" {w['ticker']:10} {target:>10} {w.get('notes', '')[:25]}")
if len(data["watchlist"]) > 20:
print(f" ... and {len(data['watchlist']) - 20} more")
def main():
parser = argparse.ArgumentParser(description="Unified stock management")
subparsers = parser.add_subparsers(dest="command", help="Commands")
# list
list_parser = subparsers.add_parser("list", help="List stocks")
list_parser.add_argument("--holdings", action="store_true", help="Show only holdings")
list_parser.add_argument("--watchlist", action="store_true", help="Show only watchlist")
# add-watchlist
add_watch = subparsers.add_parser("add-watchlist", help="Add to watchlist")
add_watch.add_argument("ticker", help="Stock ticker")
add_watch.add_argument("--target", type=float, help="Target price")
add_watch.add_argument("--stop", type=float, help="Stop loss")
add_watch.add_argument("--notes", default="", help="Notes")
# add-holding
add_hold = subparsers.add_parser("add-holding", help="Add to holdings")
add_hold.add_argument("ticker", help="Stock ticker")
add_hold.add_argument("--name", default="", help="Company name")
add_hold.add_argument("--category", default="", help="Category")
add_hold.add_argument("--notes", default="", help="Notes")
add_hold.add_argument("--target", type=float, help="Buy-more target price")
add_hold.add_argument("--stop", type=float, help="Stop loss price")
# move (watchlist → holdings)
move = subparsers.add_parser("move", help="Move from watchlist to holdings")
move.add_argument("ticker", help="Stock ticker")
move.add_argument("--name", default="", help="Company name")
move.add_argument("--category", default="", help="Category")
# remove
remove = subparsers.add_parser("remove", help="Remove stock")
remove.add_argument("ticker", help="Stock ticker")
remove.add_argument("--from", dest="from_list", choices=["holdings", "watchlist", "both"],
default="both", help="Remove from which list")
# set-alert (for existing holdings)
set_alert = subparsers.add_parser("set-alert", help="Set buy-more/stop alert on holding")
set_alert.add_argument("ticker", help="Stock ticker")
set_alert.add_argument("--target", type=float, help="Buy-more target price")
set_alert.add_argument("--stop", type=float, help="Stop loss price")
args = parser.parse_args()
if args.command == "list":
show_h = not args.watchlist or args.holdings
show_w = not args.holdings or args.watchlist
if not args.holdings and not args.watchlist:
show_h = show_w = True
list_stocks(show_holdings=show_h, show_watchlist=show_w)
elif args.command == "add-watchlist":
add_to_watchlist(args.ticker.upper(), args.target, args.stop, args.notes)
print(f"✅ Added {args.ticker.upper()} to watchlist")
elif args.command == "add-holding":
add_to_holdings(args.ticker.upper(), args.name, args.category, args.notes,
args.target, args.stop)
print(f"✅ Added {args.ticker.upper()} to holdings")
elif args.command == "move":
if move_to_holdings(args.ticker.upper(), args.name, args.category):
print(f"✅ Moved {args.ticker.upper()} from watchlist to holdings")
elif args.command == "remove":
if remove_stock(args.ticker.upper(), args.from_list):
print(f"✅ Removed {args.ticker.upper()}")
else:
print(f"⚠️ {args.ticker.upper()} not found")
elif args.command == "set-alert":
data = load_stocks()
found = False
for h in data["holdings"]:
if h.get("ticker") == args.ticker.upper():
if args.target is not None:
h["target"] = args.target
if args.stop is not None:
h["stop"] = args.stop
save_stocks(data)
found = True
print(f"✅ Set alert on {args.ticker.upper()}: target=${args.target}, stop=${args.stop}")
break
if not found:
print(f"⚠️ {args.ticker.upper()} not found in holdings")
else:
parser.print_help()
if __name__ == "__main__":
main()

1728
scripts/summarize.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,158 @@
#!/usr/bin/env python3
"""Translate portfolio headlines in briefing JSON using openclaw.
Usage: python3 translate_portfolio.py /path/to/briefing.json [--lang de]
Reads briefing JSON, translates portfolio article headlines via openclaw,
writes back the modified JSON.
"""
import argparse
import json
import re
import subprocess
import sys
def extract_headlines(portfolio_message: str) -> list[str]:
"""Extract article headlines (lines starting with •) from portfolio message."""
headlines = []
for line in portfolio_message.split('\n'):
line = line.strip()
if line.startswith(''):
# Remove bullet, reference number, and clean up
# Format: "• Headline text [1]"
match = re.match(r'\s*(.+?)\s*\[\d+\]$', line)
if match:
headlines.append(match.group(1))
else:
# No reference number
headlines.append(line[1:].strip())
return headlines
def translate_headlines(headlines: list[str], lang: str = "de") -> list[str]:
"""Translate headlines using openclaw agent."""
if not headlines:
return []
prompt = f"""Translate these English headlines to German.
Return ONLY a JSON array of strings in the same order.
Example: ["Übersetzung 1", "Übersetzung 2"]
Do not add commentary.
Headlines:
"""
for idx, title in enumerate(headlines, start=1):
prompt += f"{idx}. {title}\n"
try:
result = subprocess.run(
[
'openclaw', 'agent',
'--session-id', 'finance-news-translate-portfolio',
'--message', prompt,
'--json',
'--timeout', '60'
],
capture_output=True,
text=True,
timeout=90
)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
print(f"⚠️ Translation failed: {e}", file=sys.stderr)
return headlines
if result.returncode != 0:
print(f"⚠️ openclaw error: {result.stderr}", file=sys.stderr)
return headlines
# Extract reply from openclaw JSON output
# Format: {"result": {"payloads": [{"text": "..."}]}}
# Note: openclaw may print plugin loading messages before JSON, so find the JSON start
stdout = result.stdout
json_start = stdout.find('{')
if json_start > 0:
stdout = stdout[json_start:]
try:
output = json.loads(stdout)
payloads = output.get('result', {}).get('payloads', [])
if payloads and payloads[0].get('text'):
reply = payloads[0]['text']
else:
reply = output.get('reply', '') or output.get('message', '') or stdout
except json.JSONDecodeError:
reply = stdout
# Parse JSON array from reply
json_text = reply.strip()
if "```" in json_text:
match = re.search(r'```(?:json)?\s*(.*?)```', json_text, re.DOTALL)
if match:
json_text = match.group(1).strip()
try:
translated = json.loads(json_text)
if isinstance(translated, list) and len(translated) == len(headlines):
print(f"✅ Translated {len(headlines)} portfolio headlines", file=sys.stderr)
return translated
except json.JSONDecodeError as e:
print(f"⚠️ JSON parse error: {e}", file=sys.stderr)
print(f"⚠️ Translation failed, using original headlines", file=sys.stderr)
return headlines
def replace_headlines(portfolio_message: str, original: list[str], translated: list[str]) -> str:
"""Replace original headlines with translated ones in portfolio message."""
result = portfolio_message
for orig, trans in zip(original, translated):
if orig != trans:
# Replace the headline text, preserving bullet and reference
result = result.replace(f"{orig}", f"{trans}")
return result
def main():
parser = argparse.ArgumentParser(description='Translate portfolio headlines')
parser.add_argument('json_file', help='Path to briefing JSON file')
parser.add_argument('--lang', default='de', help='Target language (default: de)')
args = parser.parse_args()
# Read JSON
try:
with open(args.json_file, 'r') as f:
data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"❌ Error reading {args.json_file}: {e}", file=sys.stderr)
sys.exit(1)
portfolio_message = data.get('portfolio_message', '')
if not portfolio_message:
print("No portfolio_message to translate", file=sys.stderr)
print(json.dumps(data, ensure_ascii=False, indent=2))
return
# Extract, translate, replace
headlines = extract_headlines(portfolio_message)
if not headlines:
print("No headlines found in portfolio_message", file=sys.stderr)
print(json.dumps(data, ensure_ascii=False, indent=2))
return
print(f"📝 Found {len(headlines)} headlines to translate", file=sys.stderr)
translated = translate_headlines(headlines, args.lang)
# Update portfolio message
data['portfolio_message'] = replace_headlines(portfolio_message, headlines, translated)
# Write back
with open(args.json_file, 'w') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f"✅ Updated {args.json_file}", file=sys.stderr)
if __name__ == '__main__':
main()

45
scripts/utils.py Normal file
View File

@@ -0,0 +1,45 @@
"""Shared helpers."""
import os
import sys
import time
from pathlib import Path
def ensure_venv() -> None:
"""Re-exec inside local venv if available and not already active."""
if os.environ.get("FINANCE_NEWS_VENV_BOOTSTRAPPED") == "1":
return
if sys.prefix != sys.base_prefix:
return
venv_python = Path(__file__).resolve().parent.parent / "venv" / "bin" / "python3"
if not venv_python.exists():
print("⚠️ finance-news venv missing; run scripts from the repo venv to avoid dependency errors.", file=sys.stderr)
return
env = os.environ.copy()
env["FINANCE_NEWS_VENV_BOOTSTRAPPED"] = "1"
os.execvpe(str(venv_python), [str(venv_python)] + sys.argv, env)
def compute_deadline(deadline_sec: int | None) -> float | None:
if deadline_sec is None:
return None
if deadline_sec <= 0:
return None
return time.monotonic() + deadline_sec
def time_left(deadline: float | None) -> int | None:
if deadline is None:
return None
remaining = int(deadline - time.monotonic())
return remaining
def clamp_timeout(default_timeout: int, deadline: float | None, minimum: int = 1) -> int:
remaining = time_left(deadline)
if remaining is None:
return default_timeout
if remaining <= 0:
raise TimeoutError("Deadline exceeded")
return max(min(default_timeout, remaining), minimum)

109
scripts/venv-setup.sh Normal file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Finance News - venv Setup Script
# Creates or rebuilds the Python virtual environment
# Handles NixOS libstdc++ issues automatically
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
VENV_DIR="${BASE_DIR}/venv"
echo "📦 Finance News - venv Setup"
echo "============================"
echo ""
# Check Python version
PYTHON_BIN="${PYTHON_BIN:-python3}"
PYTHON_VERSION=$("$PYTHON_BIN" --version 2>&1)
echo "Using: $PYTHON_VERSION"
echo "Path: $(command -v "$PYTHON_BIN" 2>/dev/null || echo "$PYTHON_BIN")"
echo ""
# Remove existing venv if --force flag
if [[ "$1" == "--force" || "$1" == "-f" ]]; then
if [[ -d "$VENV_DIR" ]]; then
echo "🗑️ Removing existing venv..."
rm -rf "$VENV_DIR"
fi
fi
# Check if venv exists
if [[ -d "$VENV_DIR" ]]; then
echo "⚠️ venv already exists at $VENV_DIR"
echo " Use --force to rebuild"
exit 0
fi
# Create venv
echo "📁 Creating virtual environment..."
"$PYTHON_BIN" -m venv "$VENV_DIR"
# Activate venv
source "$VENV_DIR/bin/activate"
# Upgrade pip
echo "⬆️ Upgrading pip..."
pip install --upgrade pip --quiet
# Install requirements
echo "📥 Installing dependencies..."
pip install -r "$BASE_DIR/requirements.txt" --quiet
# NixOS-specific: Add LD_LIBRARY_PATH to activate script
if [[ -d "/nix/store" ]]; then
echo "🐧 NixOS detected - configuring libstdc++ path..."
ACTIVATE_SCRIPT="$VENV_DIR/bin/activate"
# Find libstdc++ path
LIBSTDCXX_PATH=""
if [[ -d "/home/linuxbrew/.linuxbrew/lib" ]]; then
LIBSTDCXX_PATH="/home/linuxbrew/.linuxbrew/lib"
elif [[ -d "$HOME/.linuxbrew/lib" ]]; then
LIBSTDCXX_PATH="$HOME/.linuxbrew/lib"
else
# Try nix store - only set if find returns a result
GCC_LIB_DIR=$(find /nix/store -maxdepth 2 -name "*-gcc-*-lib" -print -quit 2>/dev/null)
if [[ -n "$GCC_LIB_DIR" && -d "$GCC_LIB_DIR/lib" ]]; then
LIBSTDCXX_PATH="$GCC_LIB_DIR/lib"
fi
fi
if [[ -n "$LIBSTDCXX_PATH" && -d "$LIBSTDCXX_PATH" ]]; then
# Add to activate script if not already there
if ! grep -q "FINANCE_NEWS_LD_LIBRARY_PATH" "$ACTIVATE_SCRIPT"; then
cat >> "$ACTIVATE_SCRIPT" << EOF
# NixOS libstdc++ fix for numpy/yfinance (added by venv-setup.sh)
if [[ -z "\${FINANCE_NEWS_LD_LIBRARY_PATH:-}" ]]; then
export FINANCE_NEWS_LD_LIBRARY_PATH=1
if [[ -z "\${LD_LIBRARY_PATH:-}" ]]; then
export LD_LIBRARY_PATH="$LIBSTDCXX_PATH"
else
export LD_LIBRARY_PATH="$LIBSTDCXX_PATH:\$LD_LIBRARY_PATH"
fi
fi
EOF
echo " Added LD_LIBRARY_PATH=$LIBSTDCXX_PATH to activate script"
fi
else
echo " ⚠️ Could not find libstdc++.so.6 path"
echo " Install Linuxbrew: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
fi
fi
# Verify installation
echo ""
echo "✅ venv created successfully!"
echo ""
echo "Verifying installation..."
"$VENV_DIR/bin/python3" -c "import feedparser; print(' ✓ feedparser')"
"$VENV_DIR/bin/python3" -c "import yfinance; print(' ✓ yfinance')" 2>/dev/null || echo " ⚠️ yfinance import failed (may need LD_LIBRARY_PATH)"
echo ""
echo "To activate manually:"
echo " source $VENV_DIR/bin/activate"
echo ""
echo "Or just use the CLI:"
echo " ./scripts/finance-news briefing --morning"

34
tests/README.md Normal file
View File

@@ -0,0 +1,34 @@
# Unit Tests
## Setup
```bash
# Install test dependencies
pip install -r requirements-test.txt
# Run tests
pytest
# Run with coverage
pytest --cov=scripts --cov-report=html
# Run specific test file
pytest tests/test_portfolio.py
```
## Test Structure
- `test_portfolio.py` - Portfolio CRUD operations
- `test_fetch_news.py` - RSS feed parsing with mocked responses
- `test_setup.py` - Setup wizard validation
- `fixtures/` - Sample RSS and portfolio data
## Coverage Target
60%+ coverage for core functions (portfolio, fetch_news, setup).
## Notes
- Tests use `tmp_path` for file isolation
- Network calls are mocked with `unittest.mock`
- `pytest-mock` provides `mocker` fixture for advanced mocking

4
tests/fixtures/sample_portfolio.csv vendored Normal file
View File

@@ -0,0 +1,4 @@
symbol,name,category,notes
AAPL,Apple Inc,Tech,Core holding
TSLA,Tesla Inc,Auto,Growth play
MSFT,Microsoft,Tech,Dividend stock
1 symbol name category notes
2 AAPL Apple Inc Tech Core holding
3 TSLA Tesla Inc Auto Growth play
4 MSFT Microsoft Tech Dividend stock

20
tests/fixtures/sample_rss.xml vendored Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>Test Market News</title>
<link>https://example.com</link>
<description>Sample RSS feed for testing</description>
<item>
<title>Apple Stock Rises 5%</title>
<link>https://example.com/apple-rises</link>
<description>Apple Inc. shares rose 5% today on strong earnings.</description>
<pubDate>Mon, 20 Jan 2025 10:00:00 GMT</pubDate>
</item>
<item>
<title>Tesla Announces New Model</title>
<link>https://example.com/tesla-model</link>
<description>Tesla unveils new electric vehicle model.</description>
<pubDate>Mon, 20 Jan 2025 11:30:00 GMT</pubDate>
</item>
</channel>
</rss>

110
tests/test_alerts.py Normal file
View File

@@ -0,0 +1,110 @@
import sys
from pathlib import Path
import json
import pytest
from unittest.mock import Mock, patch
from datetime import datetime, timedelta
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from alerts import check_alerts, load_alerts, save_alerts
@pytest.fixture
def mock_alerts_data():
return {
"_meta": {"version": 1, "supported_currencies": ["USD", "EUR"]},
"alerts": [
{
"ticker": "AAPL",
"target_price": 150.0,
"currency": "USD",
"note": "Buy Apple",
"triggered_count": 0,
"last_triggered": None
},
{
"ticker": "TSLA",
"target_price": 200.0,
"currency": "USD",
"note": "Buy Tesla",
"triggered_count": 5,
"last_triggered": "2026-01-26T10:00:00"
}
]
}
def test_check_alerts_trigger(mock_alerts_data, monkeypatch, tmp_path):
# Setup mock alerts file
alerts_file = tmp_path / "alerts.json"
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
alerts_file.write_text(json.dumps(mock_alerts_data))
# Mock market data: AAPL is under target, TSLA is over
mock_quotes = {
"AAPL": {"price": 145.0},
"TSLA": {"price": 210.0}
}
with patch("alerts.get_fetch_market_data") as mock_fmd_getter:
mock_fmd = Mock(return_value=mock_quotes)
mock_fmd_getter.return_value = mock_fmd
results = check_alerts()
assert len(results["triggered"]) == 1
assert results["triggered"][0]["ticker"] == "AAPL"
assert results["triggered"][0]["current_price"] == 145.0
assert len(results["watching"]) == 1
assert results["watching"][0]["ticker"] == "TSLA"
# Verify triggered count incremented for AAPL
updated_data = json.loads(alerts_file.read_text())
aapl_alert = next(a for a in updated_data["alerts"] if a["ticker"] == "AAPL")
assert aapl_alert["triggered_count"] == 1
assert aapl_alert["last_triggered"] is not None
def test_check_alerts_deduplication(mock_alerts_data, monkeypatch, tmp_path):
# If already triggered today, triggered_count should NOT increment
now = datetime.now()
mock_alerts_data["alerts"][0]["last_triggered"] = now.isoformat()
mock_alerts_data["alerts"][0]["triggered_count"] = 1
alerts_file = tmp_path / "alerts.json"
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
alerts_file.write_text(json.dumps(mock_alerts_data))
mock_quotes = {"AAPL": {"price": 140.0}, "TSLA": {"price": 250.0}}
with patch("alerts.get_fetch_market_data") as mock_fmd_getter:
mock_fmd = Mock(return_value=mock_quotes)
mock_fmd_getter.return_value = mock_fmd
check_alerts()
updated_data = json.loads(alerts_file.read_text())
aapl_alert = next(a for a in updated_data["alerts"] if a["ticker"] == "AAPL")
assert aapl_alert["triggered_count"] == 1 # Still 1, didn't increment because same day
def test_check_alerts_snooze(mock_alerts_data, monkeypatch, tmp_path):
# Snoozed alert should be ignored
future_date = datetime.now() + timedelta(days=1)
mock_alerts_data["alerts"][0]["snooze_until"] = future_date.isoformat()
alerts_file = tmp_path / "alerts.json"
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
alerts_file.write_text(json.dumps(mock_alerts_data))
mock_quotes = {"AAPL": {"price": 140.0}, "TSLA": {"price": 190.0}}
with patch("alerts.get_fetch_market_data") as mock_fmd_getter:
mock_fmd = Mock(return_value=mock_quotes)
mock_fmd_getter.return_value = mock_fmd
results = check_alerts()
# AAPL is snoozed, so only TSLA should be in triggered
assert len(results["triggered"]) == 1
assert results["triggered"][0]["ticker"] == "TSLA"
assert all(t["ticker"] != "AAPL" for t in results["triggered"])

View File

@@ -0,0 +1,390 @@
"""Extended tests for alerts.py - price target alerts."""
import json
import sys
from argparse import Namespace
from datetime import datetime, timedelta
from pathlib import Path
from unittest.mock import Mock, patch
from io import StringIO
import pytest
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from alerts import (
load_alerts,
save_alerts,
get_alert_by_ticker,
format_price,
cmd_list,
cmd_set,
cmd_delete,
cmd_snooze,
cmd_update,
SUPPORTED_CURRENCIES,
)
@pytest.fixture
def sample_alerts_data():
"""Sample alerts data for testing."""
return {
"_meta": {"version": 1, "supported_currencies": SUPPORTED_CURRENCIES},
"alerts": [
{
"ticker": "AAPL",
"target_price": 150.0,
"currency": "USD",
"note": "Buy Apple",
"set_by": "art",
"set_date": "2026-01-15",
"status": "active",
"snooze_until": None,
"triggered_count": 0,
"last_triggered": None,
},
{
"ticker": "TSLA",
"target_price": 200.0,
"currency": "USD",
"note": "Buy Tesla",
"set_by": "",
"set_date": "2026-01-20",
"status": "active",
"snooze_until": None,
"triggered_count": 5,
"last_triggered": "2026-01-26T10:00:00",
},
],
}
@pytest.fixture
def alerts_file(tmp_path, sample_alerts_data):
"""Create a temporary alerts file."""
alerts_path = tmp_path / "alerts.json"
alerts_path.write_text(json.dumps(sample_alerts_data))
return alerts_path
class TestLoadAlerts:
"""Tests for load_alerts()."""
def test_load_existing_file(self, alerts_file, monkeypatch):
"""Load alerts from existing file."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
data = load_alerts()
assert "_meta" in data
assert len(data["alerts"]) == 2
assert data["alerts"][0]["ticker"] == "AAPL"
def test_load_missing_file(self, tmp_path, monkeypatch):
"""Return default structure when file doesn't exist."""
missing_path = tmp_path / "missing.json"
monkeypatch.setattr("alerts.ALERTS_FILE", missing_path)
data = load_alerts()
assert data["_meta"]["version"] == 1
assert data["alerts"] == []
assert "supported_currencies" in data["_meta"]
class TestSaveAlerts:
"""Tests for save_alerts()."""
def test_save_updates_timestamp(self, tmp_path, sample_alerts_data, monkeypatch):
"""Save should update the updated_at field."""
alerts_path = tmp_path / "alerts.json"
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
save_alerts(sample_alerts_data)
saved = json.loads(alerts_path.read_text())
assert "updated_at" in saved["_meta"]
def test_save_preserves_data(self, tmp_path, sample_alerts_data, monkeypatch):
"""Save should preserve all alert data."""
alerts_path = tmp_path / "alerts.json"
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
save_alerts(sample_alerts_data)
saved = json.loads(alerts_path.read_text())
assert len(saved["alerts"]) == 2
assert saved["alerts"][0]["ticker"] == "AAPL"
class TestGetAlertByTicker:
"""Tests for get_alert_by_ticker()."""
def test_find_existing_alert(self, sample_alerts_data):
"""Find alert by ticker."""
alerts = sample_alerts_data["alerts"]
result = get_alert_by_ticker(alerts, "AAPL")
assert result is not None
assert result["ticker"] == "AAPL"
assert result["target_price"] == 150.0
def test_find_case_insensitive(self, sample_alerts_data):
"""Find alert regardless of case."""
alerts = sample_alerts_data["alerts"]
result = get_alert_by_ticker(alerts, "aapl")
assert result is not None
assert result["ticker"] == "AAPL"
def test_not_found_returns_none(self, sample_alerts_data):
"""Return None for non-existent ticker."""
alerts = sample_alerts_data["alerts"]
result = get_alert_by_ticker(alerts, "GOOG")
assert result is None
class TestFormatPrice:
"""Tests for format_price()."""
def test_format_usd(self):
"""Format USD price."""
assert format_price(150.50, "USD") == "$150.50"
assert format_price(1234.56, "USD") == "$1,234.56"
def test_format_eur(self):
"""Format EUR price."""
assert format_price(100.00, "EUR") == "€100.00"
def test_format_jpy(self):
"""Format JPY without decimals."""
assert format_price(15000, "JPY") == "¥15,000"
def test_format_sgd(self):
"""Format SGD price."""
assert format_price(50.00, "SGD") == "S$50.00"
def test_format_mxn(self):
"""Format MXN price."""
assert format_price(100.00, "MXN") == "MX$100.00"
def test_format_unknown_currency(self):
"""Format unknown currency with code prefix."""
result = format_price(100.00, "GBP")
assert "GBP" in result
assert "100.00" in result
class TestCmdList:
"""Tests for cmd_list()."""
def test_list_empty_alerts(self, tmp_path, monkeypatch, capsys):
"""List with no alerts."""
alerts_path = tmp_path / "alerts.json"
alerts_path.write_text(json.dumps({"_meta": {}, "alerts": []}))
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
cmd_list(Namespace())
captured = capsys.readouterr()
assert "No price alerts set" in captured.out
def test_list_active_alerts(self, alerts_file, monkeypatch, capsys):
"""List active alerts."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
cmd_list(Namespace())
captured = capsys.readouterr()
assert "Price Alerts" in captured.out
assert "AAPL" in captured.out
assert "$150.00" in captured.out
def test_list_snoozed_alerts(self, tmp_path, monkeypatch, capsys):
"""List snoozed alerts separately."""
future = (datetime.now() + timedelta(days=7)).isoformat()
data = {
"_meta": {},
"alerts": [
{"ticker": "AAPL", "target_price": 150, "currency": "USD", "snooze_until": future}
],
}
alerts_path = tmp_path / "alerts.json"
alerts_path.write_text(json.dumps(data))
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_path)
cmd_list(Namespace())
captured = capsys.readouterr()
assert "Snoozed" in captured.out
assert "AAPL" in captured.out
class TestCmdSet:
"""Tests for cmd_set()."""
def test_set_new_alert(self, alerts_file, monkeypatch, capsys):
"""Set a new alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
with patch("alerts.get_fetch_market_data") as mock_fmd:
mock_fmd.return_value = Mock(return_value={"GOOG": {"price": 175.0}})
args = Namespace(ticker="GOOG", target=150.0, currency="USD", note="Buy Google", user="art")
cmd_set(args)
captured = capsys.readouterr()
assert "Alert set: GOOG" in captured.out
data = json.loads(alerts_file.read_text())
goog = next((a for a in data["alerts"] if a["ticker"] == "GOOG"), None)
assert goog is not None
assert goog["target_price"] == 150.0
def test_set_duplicate_alert(self, alerts_file, monkeypatch, capsys):
"""Cannot set duplicate alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL", target=140.0, currency="USD", note="", user="")
cmd_set(args)
captured = capsys.readouterr()
assert "already exists" in captured.out
def test_set_invalid_target(self, alerts_file, monkeypatch, capsys):
"""Reject invalid target price."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="GOOG", target=-10.0, currency="USD", note="", user="")
cmd_set(args)
captured = capsys.readouterr()
assert "must be greater than 0" in captured.out
def test_set_invalid_currency(self, alerts_file, monkeypatch, capsys):
"""Reject invalid currency."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="GOOG", target=150.0, currency="XYZ", note="", user="")
cmd_set(args)
captured = capsys.readouterr()
assert "not supported" in captured.out
class TestCmdDelete:
"""Tests for cmd_delete()."""
def test_delete_existing_alert(self, alerts_file, monkeypatch, capsys):
"""Delete an existing alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL")
cmd_delete(args)
captured = capsys.readouterr()
assert "Alert deleted: AAPL" in captured.out
data = json.loads(alerts_file.read_text())
assert not any(a["ticker"] == "AAPL" for a in data["alerts"])
def test_delete_nonexistent_alert(self, alerts_file, monkeypatch, capsys):
"""Cannot delete non-existent alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="GOOG")
cmd_delete(args)
captured = capsys.readouterr()
assert "No alert found" in captured.out
class TestCmdSnooze:
"""Tests for cmd_snooze()."""
def test_snooze_alert(self, alerts_file, monkeypatch, capsys):
"""Snooze an alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL", days=7)
cmd_snooze(args)
captured = capsys.readouterr()
assert "Alert snoozed: AAPL" in captured.out
data = json.loads(alerts_file.read_text())
aapl = next(a for a in data["alerts"] if a["ticker"] == "AAPL")
assert aapl["snooze_until"] is not None
def test_snooze_nonexistent_alert(self, alerts_file, monkeypatch, capsys):
"""Cannot snooze non-existent alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="GOOG", days=7)
cmd_snooze(args)
captured = capsys.readouterr()
assert "No alert found" in captured.out
def test_snooze_default_days(self, alerts_file, monkeypatch, capsys):
"""Default snooze is 7 days."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL", days=None)
cmd_snooze(args)
captured = capsys.readouterr()
assert "Alert snoozed" in captured.out
class TestCmdUpdate:
"""Tests for cmd_update()."""
def test_update_target_price(self, alerts_file, monkeypatch, capsys):
"""Update alert target price."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL", target=140.0, note=None)
cmd_update(args)
captured = capsys.readouterr()
assert "Alert updated: AAPL" in captured.out
assert "$150.00" in captured.out # Old price
assert "$140.00" in captured.out # New price
data = json.loads(alerts_file.read_text())
aapl = next(a for a in data["alerts"] if a["ticker"] == "AAPL")
assert aapl["target_price"] == 140.0
def test_update_with_note(self, alerts_file, monkeypatch, capsys):
"""Update alert with new note."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL", target=145.0, note="New buy zone")
cmd_update(args)
data = json.loads(alerts_file.read_text())
aapl = next(a for a in data["alerts"] if a["ticker"] == "AAPL")
assert aapl["note"] == "New buy zone"
def test_update_nonexistent_alert(self, alerts_file, monkeypatch, capsys):
"""Cannot update non-existent alert."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="GOOG", target=150.0, note=None)
cmd_update(args)
captured = capsys.readouterr()
assert "No alert found" in captured.out
def test_update_invalid_target(self, alerts_file, monkeypatch, capsys):
"""Reject invalid target price on update."""
monkeypatch.setattr("alerts.ALERTS_FILE", alerts_file)
args = Namespace(ticker="AAPL", target=-10.0, note=None)
cmd_update(args)
captured = capsys.readouterr()
assert "must be greater than 0" in captured.out

101
tests/test_briefing.py Normal file
View File

@@ -0,0 +1,101 @@
import sys
from pathlib import Path
import json
import pytest
from unittest.mock import Mock, patch
import subprocess
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from briefing import generate_and_send
def test_generate_and_send_success():
# Mock subprocess.run for summarize.py
mock_briefing_data = {
"macro_message": "Macro Summary",
"portfolio_message": "Portfolio Summary",
"summary": "Full Summary"
}
with patch("briefing.subprocess.run") as mock_run:
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = json.dumps(mock_briefing_data)
mock_run.return_value = mock_result
args = Mock()
args.time = "morning"
args.style = "briefing"
args.lang = "en"
args.deadline = 300
args.fast = False
args.llm = False
args.debug = False
args.json = True
args.send = False
result = generate_and_send(args)
assert result == "Macro Summary"
assert mock_run.called
# Check if summarize.py was called with correct args
call_args = mock_run.call_args[0][0]
assert "summarize.py" in str(call_args[1])
assert "--time" in call_args
assert "morning" in call_args
def test_generate_and_send_with_whatsapp():
mock_briefing_data = {
"macro_message": "Macro Summary",
"portfolio_message": "Portfolio Summary"
}
with patch("briefing.subprocess.run") as mock_run, \
patch("briefing.send_to_whatsapp") as mock_send:
# First call is summarize.py
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = json.dumps(mock_briefing_data)
mock_run.return_value = mock_result
args = Mock()
args.time = "evening"
args.style = "briefing"
args.lang = "en"
args.deadline = None
args.fast = True
args.llm = False
args.json = False
args.send = True
args.group = "Test Group"
args.debug = False
generate_and_send(args)
# Check if send_to_whatsapp was called for both messages
assert mock_send.call_count == 2
mock_send.assert_any_call("Macro Summary", "Test Group")
mock_send.assert_any_call("Portfolio Summary", "Test Group")
def test_generate_and_send_failure():
with patch("briefing.subprocess.run") as mock_run:
mock_result = Mock()
mock_result.returncode = 1
mock_result.stderr = "Error occurred"
mock_run.return_value = mock_result
args = Mock()
args.time = "morning"
args.style = "briefing"
args.lang = "en"
args.deadline = None
args.fast = False
args.llm = False
args.json = False
args.send = False
args.debug = False
with pytest.raises(SystemExit):
generate_and_send(args)

111
tests/test_earnings.py Normal file
View File

@@ -0,0 +1,111 @@
import sys
from pathlib import Path
import json
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from earnings import (
fetch_all_earnings_finnhub,
get_briefing_section,
load_earnings_cache,
save_earnings_cache,
refresh_earnings
)
@pytest.fixture
def mock_finnhub_response():
return {
"earningsCalendar": [
{
"symbol": "AAPL",
"date": "2026-02-01",
"hour": "amc",
"epsEstimate": 1.5,
"revenueEstimate": 100000000,
"quarter": 1,
"year": 2026
},
{
"symbol": "TSLA",
"date": "2026-01-27",
"hour": "bmo",
"epsEstimate": 0.8,
"revenueEstimate": 25000000,
"quarter": 4,
"year": 2025
}
]
}
def test_fetch_earnings_finnhub_success(mock_finnhub_response):
with patch("earnings.urlopen") as mock_urlopen:
mock_resp = MagicMock()
mock_resp.read.return_value = json.dumps(mock_finnhub_response).encode("utf-8")
mock_resp.__enter__.return_value = mock_resp
mock_urlopen.return_value = mock_resp
with patch("earnings.get_finnhub_key", return_value="fake_key"):
result = fetch_all_earnings_finnhub(days_ahead=30)
assert "AAPL" in result
assert result["AAPL"]["date"] == "2026-02-01"
assert result["AAPL"]["time"] == "amc"
assert "TSLA" in result
assert result["TSLA"]["date"] == "2026-01-27"
def test_cache_logic(tmp_path, monkeypatch):
cache_file = tmp_path / "earnings_calendar.json"
monkeypatch.setattr("earnings.EARNINGS_CACHE", cache_file)
monkeypatch.setattr("earnings.CACHE_DIR", tmp_path)
test_data = {
"last_updated": "2026-01-27T08:00:00",
"earnings": {"AAPL": {"date": "2026-02-01"}}
}
save_earnings_cache(test_data)
assert cache_file.exists()
loaded_data = load_earnings_cache()
assert loaded_data["earnings"]["AAPL"]["date"] == "2026-02-01"
def test_get_briefing_section_output():
# Mock portfolio and cache to return specific earnings
mock_portfolio = [{"symbol": "AAPL", "name": "Apple", "category": "Tech"}]
mock_cache = {
"last_updated": datetime.now().isoformat(),
"earnings": {
"AAPL": {
"date": datetime.now().strftime("%Y-%m-%d"),
"time": "amc",
"eps_estimate": 1.5
}
}
}
with patch("earnings.load_portfolio", return_value=mock_portfolio), \
patch("earnings.load_earnings_cache", return_value=mock_cache), \
patch("earnings.refresh_earnings", return_value=mock_cache):
section = get_briefing_section()
assert "EARNINGS TODAY" in section
assert "AAPL" in section
assert "Apple" in section
assert "after-close" in section
assert "Est: $1.50" in section
def test_refresh_earnings_force(mock_finnhub_response):
mock_portfolio = [{"symbol": "AAPL", "name": "Apple"}]
with patch("earnings.get_finnhub_key", return_value="fake_key"), \
patch("earnings.fetch_all_earnings_finnhub", return_value={"AAPL": mock_finnhub_response["earningsCalendar"][0]}), \
patch("earnings.save_earnings_cache") as mock_save:
refresh_earnings(mock_portfolio, force=True)
assert mock_save.called
args, _ = mock_save.call_args
assert "AAPL" in args[0]["earnings"]

136
tests/test_fetch_news.py Normal file
View File

@@ -0,0 +1,136 @@
"""Tests for RSS feed fetching and parsing."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import json
import pytest
from unittest.mock import Mock, patch, MagicMock
from fetch_news import fetch_market_data, fetch_rss, _get_best_feed_url
from utils import clamp_timeout, compute_deadline
@pytest.fixture
def sample_rss_content():
"""Load sample RSS fixture."""
fixture_path = Path(__file__).parent / "fixtures" / "sample_rss.xml"
return fixture_path.read_bytes()
def test_fetch_rss_success(sample_rss_content):
"""Test successful RSS fetch and parse."""
with patch("urllib.request.urlopen") as mock_urlopen:
mock_response = MagicMock()
mock_response.read.return_value = sample_rss_content
mock_response.__enter__.return_value = mock_response
mock_urlopen.return_value = mock_response
articles = fetch_rss("https://example.com/feed.xml", timeout=7)
assert len(articles) == 2
assert articles[0]["title"] == "Apple Stock Rises 5%"
assert articles[1]["title"] == "Tesla Announces New Model"
assert "apple-rises" in articles[0]["link"]
assert mock_urlopen.call_args.kwargs["timeout"] == 7
def test_fetch_rss_network_error():
"""Test RSS fetch handles network errors."""
with patch("urllib.request.urlopen", side_effect=Exception("Network error")):
articles = fetch_rss("https://example.com/feed.xml")
assert articles == []
def test_get_best_feed_url_priority():
"""Test feed URL selection prioritizes 'top' key."""
source = {
"name": "Test Source",
"homepage": "https://example.com",
"top": "https://example.com/top.xml",
"markets": "https://example.com/markets.xml"
}
url = _get_best_feed_url(source)
assert url == "https://example.com/top.xml"
def test_get_best_feed_url_fallback():
"""Test feed URL falls back to other http URLs when priority keys missing."""
source = {
"name": "Test Source",
"feed": "https://example.com/feed.xml"
}
url = _get_best_feed_url(source)
assert url == "https://example.com/feed.xml"
def test_get_best_feed_url_none_if_no_urls():
"""Test returns None when no valid URLs found."""
source = {
"name": "Test Source",
"enabled": True,
"note": "No URLs here"
}
url = _get_best_feed_url(source)
assert url is None
def test_get_best_feed_url_skips_non_urls():
"""Test skips non-URL values."""
source = {
"name": "Test Source",
"enabled": True,
"count": 5,
"rss": "https://example.com/rss.xml"
}
url = _get_best_feed_url(source)
assert url == "https://example.com/rss.xml"
def test_clamp_timeout_respects_deadline(monkeypatch):
start = 100.0
monkeypatch.setattr("utils.time.monotonic", lambda: start)
deadline = compute_deadline(5)
monkeypatch.setattr("utils.time.monotonic", lambda: 103.0)
assert clamp_timeout(30, deadline) == 2
def test_clamp_timeout_deadline_exceeded(monkeypatch):
start = 200.0
monkeypatch.setattr("utils.time.monotonic", lambda: start)
deadline = compute_deadline(1)
monkeypatch.setattr("utils.time.monotonic", lambda: 205.0)
with pytest.raises(TimeoutError):
clamp_timeout(30, deadline)
def test_fetch_market_data_price_fallback(monkeypatch):
sample = {
"price": None,
"open": 100,
"prev_close": 105,
"change_percent": None,
}
def fake_run(*_args, **_kwargs):
class Result:
returncode = 0
stdout = json.dumps(sample)
stderr = ""
return Result()
monkeypatch.setattr("fetch_news.OPENBB_BINARY", "/bin/openbb-quote")
monkeypatch.setattr("fetch_news.subprocess.run", fake_run)
no_fallback = fetch_market_data(["^GSPC"], allow_price_fallback=False)
assert no_fallback["^GSPC"]["price"] is None
with_fallback = fetch_market_data(["^GSPC"], allow_price_fallback=True)
assert with_fallback["^GSPC"]["price"] == 100

76
tests/test_portfolio.py Normal file
View File

@@ -0,0 +1,76 @@
"""Tests for portfolio operations."""
import sys
from pathlib import Path
# Add scripts to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import pytest
from portfolio import load_portfolio, save_portfolio
def test_load_portfolio_success(tmp_path, monkeypatch):
"""Test loading valid portfolio CSV."""
portfolio_file = tmp_path / "portfolio.csv"
portfolio_file.write_text("symbol,name,category,notes,type\nAAPL,Apple,Tech,,\nTSLA,Tesla,Auto,,\n")
monkeypatch.setattr("portfolio.PORTFOLIO_FILE", portfolio_file)
positions = load_portfolio()
assert len(positions) == 2
assert positions[0]["symbol"] == "AAPL"
assert positions[0]["name"] == "Apple"
assert positions[1]["symbol"] == "TSLA"
def test_load_portfolio_missing_file(tmp_path, monkeypatch):
"""Test loading non-existent portfolio returns empty list."""
portfolio_file = tmp_path / "nonexistent.csv"
monkeypatch.setattr("portfolio.PORTFOLIO_FILE", portfolio_file)
positions = load_portfolio()
assert positions == []
def test_save_portfolio(tmp_path, monkeypatch):
"""Test saving portfolio to CSV."""
portfolio_file = tmp_path / "portfolio.csv"
monkeypatch.setattr("portfolio.PORTFOLIO_FILE", portfolio_file)
positions = [
{"symbol": "AAPL", "name": "Apple", "category": "Tech", "notes": "", "type": "stock"},
{"symbol": "MSFT", "name": "Microsoft", "category": "Tech", "notes": "", "type": "stock"}
]
save_portfolio(positions)
content = portfolio_file.read_text()
assert "symbol,name,category,notes,type" in content
assert "AAPL" in content
assert "MSFT" in content
def test_save_empty_portfolio(tmp_path, monkeypatch):
"""Test saving empty portfolio creates header."""
portfolio_file = tmp_path / "portfolio.csv"
monkeypatch.setattr("portfolio.PORTFOLIO_FILE", portfolio_file)
save_portfolio([])
content = portfolio_file.read_text()
assert content == "symbol,name,category,notes,type\n"
def test_load_portfolio_preserves_fields(tmp_path, monkeypatch):
"""Test loading portfolio preserves all fields."""
portfolio_file = tmp_path / "portfolio.csv"
portfolio_file.write_text("symbol,name,category,notes,type\nAAPL,Apple Inc,Tech,Core holding,stock\n")
monkeypatch.setattr("portfolio.PORTFOLIO_FILE", portfolio_file)
positions = load_portfolio()
assert len(positions) == 1
assert positions[0]["symbol"] == "AAPL"
assert positions[0]["name"] == "Apple Inc"
assert positions[0]["category"] == "Tech"
assert positions[0]["notes"] == "Core holding"
assert positions[0]["type"] == "stock"

70
tests/test_ranking.py Normal file
View File

@@ -0,0 +1,70 @@
import sys
from pathlib import Path
import pytest
from datetime import datetime, timedelta
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from ranking import calculate_score, rank_headlines, classify_category
def test_classify_category():
assert "macro" in classify_category("Fed signals rate cut")
assert "equities" in classify_category("Apple earnings beat")
assert "energy" in classify_category("Oil prices surge")
assert "tech" in classify_category("AI chip demand remains high")
assert "geopolitics" in classify_category("US imposes new sanctions on Russia")
assert classify_category("Weather is nice") == ["general"]
def test_calculate_score_impact():
weights = {"market_impact": 0.4, "novelty": 0.2, "breadth": 0.2, "credibility": 0.1, "diversity": 0.1}
category_counts = {}
high_impact = {"title": "Fed announces emergency rate cut", "source": "Reuters", "published_at": datetime.now().isoformat()}
low_impact = {"title": "Local coffee shop opens", "source": "Blog", "published_at": datetime.now().isoformat()}
score_high = calculate_score(high_impact, weights, category_counts)
score_low = calculate_score(low_impact, weights, category_counts)
assert score_high > score_low
def test_rank_headlines_deduplication():
headlines = [
{"title": "Fed signals rate cut in March", "source": "WSJ"},
{"title": "FED SIGNALS RATE CUT IN MARCH!!!", "source": "Reuters"}, # Dupe
{"title": "Apple earnings are out", "source": "CNBC"}
]
result = rank_headlines(headlines)
# After dedupe, we should have 2 unique headlines
assert result["after_dedupe"] == 2
# must_read should contain the best ones
assert len(result["must_read"]) <= 2
def test_rank_headlines_sorting():
headlines = [
{"title": "Local news", "source": "SmallBlog", "description": "Nothing much"},
{"title": "FED EMERGENCY RATE CUT", "source": "Bloomberg", "description": "Huge market impact"},
{"title": "Nvidia Earnings Surprise", "source": "Reuters", "description": "AI demand surges"}
]
result = rank_headlines(headlines)
# FED should be first due to macro impact + credibility
assert "FED" in result["must_read"][0]["title"]
assert "Nvidia" in result["must_read"][1]["title"]
def test_source_cap():
# Test that we don't have too many items from the same source
headlines = [
{"title": f"Story {i}", "source": "Reuters"} for i in range(10)
]
# Default source cap is 2
result = rank_headlines(headlines)
reuters_in_must_read = [h for h in result["must_read"] if h["source"] == "Reuters"]
reuters_in_scan = [h for h in result["scan"] if h["source"] == "Reuters"]
assert len(reuters_in_must_read) + len(reuters_in_scan) <= 2

356
tests/test_research.py Normal file
View File

@@ -0,0 +1,356 @@
"""Tests for research.py - deep research module."""
import json
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import subprocess
import pytest
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from research import (
format_market_data,
format_headlines,
format_portfolio_news,
gemini_available,
research_with_gemini,
format_raw_data_report,
generate_research_content,
)
@pytest.fixture
def sample_market_data():
"""Sample market data for testing."""
return {
"markets": {
"us": {
"name": "US Markets",
"indices": {
"SPY": {
"name": "S&P 500",
"data": {"price": 5200.50, "change_percent": 1.25}
},
"QQQ": {
"name": "Nasdaq 100",
"data": {"price": 18500.00, "change_percent": -0.50}
}
}
},
"europe": {
"name": "European Markets",
"indices": {
"DAX": {
"name": "DAX",
"data": {"price": 18200.00, "change_percent": 0.75}
}
}
}
},
"headlines": [
{"source": "Reuters", "title": "Fed holds rates steady", "link": "https://example.com/1"},
{"source": "Bloomberg", "title": "Tech stocks rally", "link": "https://example.com/2"},
]
}
@pytest.fixture
def sample_portfolio_data():
"""Sample portfolio data for testing."""
return {
"stocks": {
"AAPL": {
"quote": {"price": 185.50, "change_percent": 2.3},
"articles": [
{"title": "Apple reports strong earnings", "link": "https://example.com/aapl1"},
{"title": "iPhone sales beat expectations", "link": "https://example.com/aapl2"},
]
},
"MSFT": {
"quote": {"price": 420.00, "change_percent": -1.1},
"articles": [
{"title": "Microsoft cloud growth slows", "link": "https://example.com/msft1"},
]
}
}
}
class TestFormatMarketData:
"""Tests for format_market_data()."""
def test_formats_market_indices(self, sample_market_data):
"""Format market indices with prices and changes."""
result = format_market_data(sample_market_data)
assert "## Market Data" in result
assert "### US Markets" in result
assert "S&P 500" in result
assert "5200.5" in result # Price (may not have trailing zero)
assert "+1.25%" in result
assert "📈" in result # Positive change
def test_shows_negative_change_emoji(self, sample_market_data):
"""Negative changes show down emoji."""
result = format_market_data(sample_market_data)
assert "Nasdaq 100" in result
assert "-0.50%" in result
assert "📉" in result # Negative change
def test_handles_empty_data(self):
"""Handle empty market data."""
result = format_market_data({})
assert "## Market Data" in result
assert "### " not in result # No region headers
def test_handles_missing_index_data(self):
"""Handle indices without data."""
data = {
"markets": {
"us": {
"name": "US Markets",
"indices": {
"SPY": {"name": "S&P 500"} # No 'data' key
}
}
}
}
result = format_market_data(data)
assert "## Market Data" in result
# Should not crash, just skip the index
class TestFormatHeadlines:
"""Tests for format_headlines()."""
def test_formats_headlines_with_links(self):
"""Format headlines with sources and links."""
headlines = [
{"source": "Reuters", "title": "Breaking news", "link": "https://example.com/1"},
{"source": "Bloomberg", "title": "Market update", "link": "https://example.com/2"},
]
result = format_headlines(headlines)
assert "## Current Headlines" in result
assert "[Reuters] Breaking news" in result
assert "URL: https://example.com/1" in result
assert "[Bloomberg] Market update" in result
def test_handles_missing_source(self):
"""Handle headlines with missing source."""
headlines = [{"title": "No source headline", "link": "https://example.com"}]
result = format_headlines(headlines)
assert "[Unknown] No source headline" in result
def test_handles_missing_link(self):
"""Handle headlines without links."""
headlines = [{"source": "Reuters", "title": "No link"}]
result = format_headlines(headlines)
assert "[Reuters] No link" in result
assert "URL:" not in result
def test_limits_to_20_headlines(self):
"""Limit output to 20 headlines max."""
headlines = [{"source": f"Source{i}", "title": f"Title {i}"} for i in range(30)]
result = format_headlines(headlines)
assert "[Source19]" in result
assert "[Source20]" not in result
def test_handles_empty_list(self):
"""Handle empty headlines list."""
result = format_headlines([])
assert "## Current Headlines" in result
class TestFormatPortfolioNews:
"""Tests for format_portfolio_news()."""
def test_formats_portfolio_stocks(self, sample_portfolio_data):
"""Format portfolio stocks with quotes and news."""
result = format_portfolio_news(sample_portfolio_data)
assert "## Portfolio Analysis" in result
assert "### AAPL" in result
assert "$185.5" in result # Price (may not have trailing zero)
assert "+2.30%" in result
assert "Apple reports strong earnings" in result
def test_shows_negative_changes(self, sample_portfolio_data):
"""Show negative change percentages."""
result = format_portfolio_news(sample_portfolio_data)
assert "### MSFT" in result
assert "-1.10%" in result
def test_limits_articles_to_5(self):
"""Limit articles per stock to 5."""
data = {
"stocks": {
"AAPL": {
"quote": {"price": 185.0, "change_percent": 1.0},
"articles": [{"title": f"Article {i}"} for i in range(10)]
}
}
}
result = format_portfolio_news(data)
assert "Article 4" in result
assert "Article 5" not in result
def test_handles_empty_stocks(self):
"""Handle empty stocks dict."""
result = format_portfolio_news({"stocks": {}})
assert "## Portfolio Analysis" in result
class TestGeminiAvailable:
"""Tests for gemini_available()."""
def test_returns_true_when_gemini_found(self):
"""Return True when gemini CLI is found."""
with patch("shutil.which", return_value="/usr/local/bin/gemini"):
assert gemini_available() is True
def test_returns_false_when_gemini_not_found(self):
"""Return False when gemini CLI is not found."""
with patch("shutil.which", return_value=None):
assert gemini_available() is False
class TestResearchWithGemini:
"""Tests for research_with_gemini()."""
def test_successful_research(self):
"""Execute gemini research successfully."""
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = "# Research Report\n\nMarket analysis..."
with patch("subprocess.run", return_value=mock_result) as mock_run:
result = research_with_gemini("Market data content")
assert result == "# Research Report\n\nMarket analysis..."
mock_run.assert_called_once()
def test_research_with_focus_areas(self):
"""Include focus areas in prompt."""
mock_result = Mock()
mock_result.returncode = 0
mock_result.stdout = "Focused analysis"
with patch("subprocess.run", return_value=mock_result) as mock_run:
result = research_with_gemini("content", focus_areas=["earnings", "macro"])
assert result == "Focused analysis"
# Verify focus areas were in the prompt
call_args = mock_run.call_args[0][0]
prompt = call_args[1]
assert "earnings" in prompt
assert "macro" in prompt
def test_handles_gemini_error(self):
"""Handle gemini error gracefully."""
mock_result = Mock()
mock_result.returncode = 1
mock_result.stderr = "API error"
with patch("subprocess.run", return_value=mock_result):
result = research_with_gemini("content")
assert "⚠️ Gemini research error" in result
assert "API error" in result
def test_handles_timeout(self):
"""Handle subprocess timeout."""
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="gemini", timeout=120)):
result = research_with_gemini("content")
assert "⚠️ Gemini research timeout" in result
def test_handles_missing_gemini(self):
"""Handle missing gemini CLI."""
with patch("subprocess.run", side_effect=FileNotFoundError()):
result = research_with_gemini("content")
assert "⚠️ Gemini CLI not found" in result
class TestFormatRawDataReport:
"""Tests for format_raw_data_report()."""
def test_combines_market_and_portfolio(self, sample_market_data, sample_portfolio_data):
"""Combine market data, headlines, and portfolio."""
result = format_raw_data_report(sample_market_data, sample_portfolio_data)
assert "## Market Data" in result
assert "## Current Headlines" in result
assert "## Portfolio Analysis" in result
def test_handles_no_headlines(self, sample_portfolio_data):
"""Handle market data without headlines."""
market_data = {"markets": {"us": {"name": "US", "indices": {}}}}
result = format_raw_data_report(market_data, sample_portfolio_data)
assert "## Market Data" in result
assert "## Current Headlines" not in result
def test_handles_portfolio_error(self, sample_market_data):
"""Skip portfolio with error."""
portfolio_data = {"error": "No portfolio configured"}
result = format_raw_data_report(sample_market_data, portfolio_data)
assert "## Portfolio Analysis" not in result
def test_handles_empty_data(self):
"""Handle empty market and portfolio data."""
result = format_raw_data_report({}, {})
assert result == ""
class TestGenerateResearchContent:
"""Tests for generate_research_content()."""
def test_uses_gemini_when_available(self, sample_market_data, sample_portfolio_data):
"""Use Gemini research when available."""
with patch("research.gemini_available", return_value=True):
with patch("research.research_with_gemini", return_value="Gemini report") as mock_gemini:
result = generate_research_content(sample_market_data, sample_portfolio_data)
assert result["report"] == "Gemini report"
assert result["source"] == "gemini"
mock_gemini.assert_called_once()
def test_falls_back_to_raw_report(self, sample_market_data, sample_portfolio_data):
"""Fall back to raw report when Gemini unavailable."""
with patch("research.gemini_available", return_value=False):
result = generate_research_content(sample_market_data, sample_portfolio_data)
assert "## Market Data" in result["report"]
assert result["source"] == "raw"
def test_handles_empty_report(self):
"""Return empty when no data available."""
result = generate_research_content({}, {})
assert result["report"] == ""
assert result["source"] == "none"
def test_passes_focus_areas_to_gemini(self, sample_market_data, sample_portfolio_data):
"""Pass focus areas to Gemini research."""
focus = ["earnings", "tech"]
with patch("research.gemini_available", return_value=True):
with patch("research.research_with_gemini", return_value="Report") as mock_gemini:
generate_research_content(sample_market_data, sample_portfolio_data, focus_areas=focus)
mock_gemini.assert_called_once()
# Check that focus_areas was passed (positional or keyword)
call_args = mock_gemini.call_args
# Focus areas passed as second positional arg
assert call_args[0][1] == focus or call_args.kwargs.get("focus_areas") == focus

84
tests/test_setup.py Normal file
View File

@@ -0,0 +1,84 @@
"""Tests for setup wizard functionality."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
import pytest
import json
from unittest.mock import patch
from setup import load_sources, save_sources, get_default_sources, setup_language, setup_markets
def test_load_sources_missing_file(tmp_path, monkeypatch):
"""Test loading non-existent sources returns defaults."""
sources_file = tmp_path / "sources.json"
# Patch both path constants to use temp file
monkeypatch.setattr("setup.SOURCES_FILE", sources_file)
# File doesn't exist, so load_sources should call get_default_sources
sources = load_sources()
assert isinstance(sources, dict)
assert "rss_feeds" in sources # Default structure has rss_feeds
def test_save_sources(tmp_path, monkeypatch):
"""Test saving sources to JSON."""
sources_file = tmp_path / "sources.json"
monkeypatch.setattr("setup.SOURCES_FILE", sources_file)
sources = {
"rss_feeds": {
"test_source": {
"name": "Test",
"enabled": True,
"top": "https://example.com/rss"
}
}
}
save_sources(sources)
assert sources_file.exists()
with open(sources_file) as f:
saved = json.load(f)
assert saved["rss_feeds"]["test_source"]["enabled"] is True
def test_get_default_sources():
"""Test default sources structure."""
sources = get_default_sources()
assert isinstance(sources, dict)
assert "rss_feeds" in sources
# Should have common sources like wsj, barrons, cnbc
feeds = sources["rss_feeds"]
assert any("wsj" in k.lower() or "barrons" in k.lower() or "cnbc" in k.lower()
for k in feeds.keys())
@patch("setup.prompt", side_effect=["en"])
@patch("setup.save_sources")
def test_setup_language(mock_save, mock_prompt):
"""Test language setup function."""
sources = {"language": {"supported": ["en", "de"], "default": "de"}}
setup_language(sources)
# Should have called prompt
mock_prompt.assert_called()
# Language should be updated
assert sources["language"]["default"] == "en"
@patch("setup.prompt_bool", side_effect=[True, False])
@patch("setup.save_sources")
def test_setup_markets(mock_save, mock_prompt):
"""Test markets setup function."""
sources = {"markets": {"us": {"enabled": False}, "eu": {"enabled": False}}}
setup_markets(sources)
# Should have prompted (at least once for US)
assert mock_prompt.called

286
tests/test_stocks.py Normal file
View File

@@ -0,0 +1,286 @@
"""Tests for stocks.py - unified stock management."""
import json
import sys
from datetime import datetime
from pathlib import Path
from unittest.mock import patch
import pytest
# Add scripts to path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from stocks import (
load_stocks,
save_stocks,
get_holdings,
get_watchlist,
get_holding_tickers,
get_watchlist_tickers,
add_to_watchlist,
add_to_holdings,
move_to_holdings,
remove_stock,
)
@pytest.fixture
def sample_stocks_data():
"""Sample stocks data for testing."""
return {
"version": "1.0",
"updated": "2026-01-30",
"holdings": [
{"ticker": "AAPL", "name": "Apple Inc.", "category": "Tech"},
{"ticker": "MSFT", "name": "Microsoft", "category": "Tech"},
],
"watchlist": [
{"ticker": "NVDA", "target": 800.0, "notes": "Buy on dip"},
{"ticker": "TSLA", "target": 200.0, "notes": "Watch earnings"},
],
"alert_definitions": {},
}
@pytest.fixture
def stocks_file(tmp_path, sample_stocks_data):
"""Create a temporary stocks file."""
stocks_path = tmp_path / "stocks.json"
stocks_path.write_text(json.dumps(sample_stocks_data))
return stocks_path
class TestLoadStocks:
"""Tests for load_stocks()."""
def test_load_existing_file(self, stocks_file, sample_stocks_data):
"""Load stocks from existing file."""
data = load_stocks(stocks_file)
assert data["version"] == "1.0"
assert len(data["holdings"]) == 2
assert len(data["watchlist"]) == 2
def test_load_missing_file(self, tmp_path):
"""Return default structure when file doesn't exist."""
missing_path = tmp_path / "missing.json"
data = load_stocks(missing_path)
assert data["version"] == "1.0"
assert data["holdings"] == []
assert data["watchlist"] == []
assert "alert_definitions" in data
class TestSaveStocks:
"""Tests for save_stocks()."""
def test_save_updates_timestamp(self, tmp_path, sample_stocks_data):
"""Save should update the 'updated' field."""
stocks_path = tmp_path / "stocks.json"
save_stocks(sample_stocks_data, stocks_path)
saved = json.loads(stocks_path.read_text())
assert saved["updated"] == datetime.now().strftime("%Y-%m-%d")
def test_save_preserves_data(self, tmp_path, sample_stocks_data):
"""Save should preserve all data."""
stocks_path = tmp_path / "stocks.json"
save_stocks(sample_stocks_data, stocks_path)
saved = json.loads(stocks_path.read_text())
assert len(saved["holdings"]) == 2
assert saved["holdings"][0]["ticker"] == "AAPL"
class TestGetHoldings:
"""Tests for get_holdings()."""
def test_get_holdings_from_data(self, sample_stocks_data):
"""Get holdings from provided data."""
holdings = get_holdings(sample_stocks_data)
assert len(holdings) == 2
assert holdings[0]["ticker"] == "AAPL"
def test_get_holdings_empty(self):
"""Return empty list for empty data."""
data = {"holdings": [], "watchlist": []}
holdings = get_holdings(data)
assert holdings == []
class TestGetWatchlist:
"""Tests for get_watchlist()."""
def test_get_watchlist_from_data(self, sample_stocks_data):
"""Get watchlist from provided data."""
watchlist = get_watchlist(sample_stocks_data)
assert len(watchlist) == 2
assert watchlist[0]["ticker"] == "NVDA"
def test_get_watchlist_empty(self):
"""Return empty list for empty data."""
data = {"holdings": [], "watchlist": []}
watchlist = get_watchlist(data)
assert watchlist == []
class TestGetHoldingTickers:
"""Tests for get_holding_tickers()."""
def test_get_holding_tickers(self, sample_stocks_data):
"""Get set of holding tickers."""
tickers = get_holding_tickers(sample_stocks_data)
assert tickers == {"AAPL", "MSFT"}
def test_get_holding_tickers_empty(self):
"""Return empty set for no holdings."""
data = {"holdings": [], "watchlist": []}
tickers = get_holding_tickers(data)
assert tickers == set()
class TestGetWatchlistTickers:
"""Tests for get_watchlist_tickers()."""
def test_get_watchlist_tickers(self, sample_stocks_data):
"""Get set of watchlist tickers."""
tickers = get_watchlist_tickers(sample_stocks_data)
assert tickers == {"NVDA", "TSLA"}
def test_get_watchlist_tickers_empty(self):
"""Return empty set for empty watchlist."""
data = {"holdings": [], "watchlist": []}
tickers = get_watchlist_tickers(data)
assert tickers == set()
class TestAddToWatchlist:
"""Tests for add_to_watchlist()."""
def test_add_new_to_watchlist(self, stocks_file, monkeypatch):
"""Add new stock to watchlist."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = add_to_watchlist("AMD", target=150.0, notes="Watch for dip")
assert result is True
data = load_stocks(stocks_file)
tickers = get_watchlist_tickers(data)
assert "AMD" in tickers
def test_update_existing_watchlist(self, stocks_file, monkeypatch):
"""Update existing watchlist entry."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
# NVDA already in watchlist with target 800
result = add_to_watchlist("NVDA", target=750.0, notes="Updated target")
assert result is True
data = load_stocks(stocks_file)
nvda = next(w for w in data["watchlist"] if w["ticker"] == "NVDA")
assert nvda["target"] == 750.0
assert nvda["notes"] == "Updated target"
def test_add_with_alerts(self, stocks_file, monkeypatch):
"""Add stock with alert definitions."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
alerts = ["below_target", "above_stop"]
result = add_to_watchlist("GOOG", target=180.0, alerts=alerts)
assert result is True
data = load_stocks(stocks_file)
goog = next(w for w in data["watchlist"] if w["ticker"] == "GOOG")
assert goog["alerts"] == alerts
class TestAddToHoldings:
"""Tests for add_to_holdings()."""
def test_add_new_holding(self, stocks_file, monkeypatch):
"""Add new stock to holdings."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = add_to_holdings("GOOG", name="Alphabet", category="Tech")
assert result is True
data = load_stocks(stocks_file)
tickers = get_holding_tickers(data)
assert "GOOG" in tickers
def test_update_existing_holding(self, stocks_file, monkeypatch):
"""Update existing holding."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = add_to_holdings("AAPL", name="Apple Inc.", category="Consumer", notes="Core holding")
assert result is True
data = load_stocks(stocks_file)
aapl = next(h for h in data["holdings"] if h["ticker"] == "AAPL")
assert aapl["category"] == "Consumer"
assert aapl["notes"] == "Core holding"
class TestMoveToHoldings:
"""Tests for move_to_holdings()."""
def test_move_from_watchlist(self, stocks_file, monkeypatch):
"""Move stock from watchlist to holdings."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
# NVDA is in watchlist, not holdings
result = move_to_holdings("NVDA", name="NVIDIA Corp", category="Semis")
assert result is True
data = load_stocks(stocks_file)
assert "NVDA" in get_holding_tickers(data)
assert "NVDA" not in get_watchlist_tickers(data)
def test_move_nonexistent_returns_false(self, stocks_file, monkeypatch):
"""Moving non-existent ticker returns False."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = move_to_holdings("NONEXISTENT")
assert result is False
class TestRemoveStock:
"""Tests for remove_stock()."""
def test_remove_from_holdings(self, stocks_file, monkeypatch):
"""Remove stock from holdings."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = remove_stock("AAPL", from_list="holdings")
assert result is True
data = load_stocks(stocks_file)
assert "AAPL" not in get_holding_tickers(data)
def test_remove_from_watchlist(self, stocks_file, monkeypatch):
"""Remove stock from watchlist."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = remove_stock("NVDA", from_list="watchlist")
assert result is True
data = load_stocks(stocks_file)
assert "NVDA" not in get_watchlist_tickers(data)
def test_remove_nonexistent_returns_false(self, stocks_file, monkeypatch):
"""Removing non-existent ticker returns False."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
result = remove_stock("NONEXISTENT", from_list="holdings")
assert result is False
def test_remove_auto_detects_list(self, stocks_file, monkeypatch):
"""Remove without specifying list auto-detects."""
monkeypatch.setattr("stocks.STOCKS_FILE", stocks_file)
# AAPL is in holdings
result = remove_stock("AAPL")
assert result is True
data = load_stocks(stocks_file)
assert "AAPL" not in get_holding_tickers(data)

345
tests/test_summarize.py Normal file
View File

@@ -0,0 +1,345 @@
"""Tests for summarize helpers."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from datetime import datetime
import summarize
from summarize import (
MoverContext,
SectorCluster,
WatchpointsData,
build_watchpoints_data,
classify_move_type,
detect_sector_clusters,
format_watchpoints,
get_index_change,
match_headline_to_symbol,
)
class FixedDateTime(datetime):
@classmethod
def now(cls, tz=None):
return cls(2026, 1, 1, 15, 0)
def test_generate_briefing_auto_time_evening(capsys, monkeypatch):
def fake_market_news(*_args, **_kwargs):
return {
"headlines": [
{"source": "CNBC", "title": "Headline one", "link": "https://example.com/1"},
{"source": "Yahoo", "title": "Headline two", "link": "https://example.com/2"},
{"source": "CNBC", "title": "Headline three", "link": "https://example.com/3"},
],
"markets": {
"us": {
"name": "US Markets",
"indices": {
"^GSPC": {"name": "S&P 500", "data": {"price": 100, "change_percent": 1.0}},
},
}
},
}
def fake_summary(*_args, **_kwargs):
return "OK"
monkeypatch.setattr(summarize, "get_market_news", fake_market_news)
monkeypatch.setattr(summarize, "get_portfolio_news", lambda *_a, **_k: None)
monkeypatch.setattr(summarize, "summarize_with_claude", fake_summary)
monkeypatch.setattr(summarize, "datetime", FixedDateTime)
args = type(
"Args",
(),
{
"lang": "de",
"style": "briefing",
"time": None,
"model": "claude",
"json": False,
"research": False,
"deadline": None,
"fast": False,
"llm": False,
"debug": False,
},
)()
summarize.generate_briefing(args)
stdout = capsys.readouterr().out
assert "Börsen Abend-Briefing" in stdout
# --- Tests for watchpoints feature (Issue #92) ---
class TestGetIndexChange:
def test_extracts_sp500_change(self):
market_data = {
"markets": {
"us": {
"indices": {
"^GSPC": {"data": {"change_percent": -1.5}}
}
}
}
}
assert get_index_change(market_data) == -1.5
def test_returns_zero_on_missing_data(self):
assert get_index_change({}) == 0.0
assert get_index_change({"markets": {}}) == 0.0
assert get_index_change({"markets": {"us": {}}}) == 0.0
class TestMatchHeadlineToSymbol:
def test_exact_symbol_match_dollar(self):
headlines = [{"title": "Breaking: $NVDA surges on AI demand"}]
result = match_headline_to_symbol("NVDA", "NVIDIA Corporation", headlines)
assert result is not None
assert "NVDA" in result["title"]
def test_exact_symbol_match_parens(self):
headlines = [{"title": "Tesla (TSLA) reports record deliveries"}]
result = match_headline_to_symbol("TSLA", "Tesla Inc", headlines)
assert result is not None
def test_exact_symbol_match_word_boundary(self):
headlines = [{"title": "AAPL announces new product line"}]
result = match_headline_to_symbol("AAPL", "Apple Inc", headlines)
assert result is not None
def test_company_name_match(self):
headlines = [{"title": "Apple announces record iPhone sales"}]
result = match_headline_to_symbol("AAPL", "Apple Inc", headlines)
assert result is not None
def test_no_match_returns_none(self):
headlines = [{"title": "Fed raises interest rates"}]
result = match_headline_to_symbol("NVDA", "NVIDIA Corporation", headlines)
assert result is None
def test_avoids_partial_symbol_match(self):
# "APP" should not match "application"
headlines = [{"title": "New application launches today"}]
result = match_headline_to_symbol("APP", "AppLovin Corp", headlines)
assert result is None
def test_empty_headlines(self):
result = match_headline_to_symbol("NVDA", "NVIDIA", [])
assert result is None
class TestDetectSectorClusters:
def test_detects_cluster_three_stocks_same_direction(self):
movers = [
{"symbol": "NVDA", "change_pct": -5.0},
{"symbol": "AMD", "change_pct": -4.0},
{"symbol": "INTC", "change_pct": -3.0},
]
portfolio_meta = {
"NVDA": {"category": "Tech"},
"AMD": {"category": "Tech"},
"INTC": {"category": "Tech"},
}
clusters = detect_sector_clusters(movers, portfolio_meta)
assert len(clusters) == 1
assert clusters[0].category == "Tech"
assert clusters[0].direction == "down"
assert len(clusters[0].stocks) == 3
def test_no_cluster_if_less_than_three(self):
movers = [
{"symbol": "NVDA", "change_pct": -5.0},
{"symbol": "AMD", "change_pct": -4.0},
]
portfolio_meta = {
"NVDA": {"category": "Tech"},
"AMD": {"category": "Tech"},
}
clusters = detect_sector_clusters(movers, portfolio_meta)
assert len(clusters) == 0
def test_no_cluster_if_mixed_direction(self):
movers = [
{"symbol": "NVDA", "change_pct": 5.0},
{"symbol": "AMD", "change_pct": -4.0},
{"symbol": "INTC", "change_pct": 3.0},
]
portfolio_meta = {
"NVDA": {"category": "Tech"},
"AMD": {"category": "Tech"},
"INTC": {"category": "Tech"},
}
clusters = detect_sector_clusters(movers, portfolio_meta)
assert len(clusters) == 0
class TestClassifyMoveType:
def test_earnings_with_keyword(self):
headline = {"title": "Company beats Q3 earnings expectations"}
result = classify_move_type(headline, False, 5.0, 0.1)
assert result == "earnings"
def test_sector_cluster(self):
result = classify_move_type(None, True, -3.0, -0.5)
assert result == "sector"
def test_market_wide(self):
result = classify_move_type(None, False, -2.0, -2.0)
assert result == "market_wide"
def test_company_specific_with_headline(self):
headline = {"title": "Company announces acquisition"}
result = classify_move_type(headline, False, 3.0, 0.1)
assert result == "company_specific"
def test_company_specific_large_move_no_headline(self):
result = classify_move_type(None, False, 8.0, 0.1)
assert result == "company_specific"
def test_unknown_small_move_no_context(self):
result = classify_move_type(None, False, 1.5, 0.2)
assert result == "unknown"
class TestFormatWatchpoints:
def test_formats_sector_cluster(self):
cluster = SectorCluster(
category="Tech",
stocks=[
MoverContext("NVDA", -5.0, 100.0, "Tech", None, "sector", None),
MoverContext("AMD", -4.0, 80.0, "Tech", None, "sector", None),
MoverContext("INTC", -3.0, 30.0, "Tech", None, "sector", None),
],
avg_change=-4.0,
direction="down",
vs_index=-3.5,
)
data = WatchpointsData(
movers=[],
sector_clusters=[cluster],
index_change=-0.5,
market_wide=False,
)
result = format_watchpoints(data, "en", {})
assert "Tech" in result
assert "-4.0%" in result
assert "vs Index" in result
def test_formats_individual_mover_with_headline(self):
mover = MoverContext(
symbol="NVDA",
change_pct=5.0,
price=100.0,
category="Tech",
matched_headline={"title": "NVIDIA reports record revenue"},
move_type="company_specific",
vs_index=4.5,
)
data = WatchpointsData(
movers=[mover],
sector_clusters=[],
index_change=0.5,
market_wide=False,
)
result = format_watchpoints(data, "en", {})
assert "NVDA" in result
assert "+5.0%" in result
assert "record revenue" in result
def test_formats_market_wide_move_english(self):
data = WatchpointsData(
movers=[],
sector_clusters=[],
index_change=-2.0,
market_wide=True,
)
result = format_watchpoints(data, "en", {})
assert "Market-wide move" in result
assert "S&P 500 fell 2.0%" in result
def test_formats_market_wide_move_german(self):
data = WatchpointsData(
movers=[],
sector_clusters=[],
index_change=2.5,
market_wide=True,
)
result = format_watchpoints(data, "de", {})
assert "Breite Marktbewegung" in result
assert "stieg 2.5%" in result
def test_uses_label_fallbacks(self):
mover = MoverContext(
symbol="XYZ",
change_pct=1.5,
price=50.0,
category="Other",
matched_headline=None,
move_type="unknown",
vs_index=1.0,
)
data = WatchpointsData(
movers=[mover],
sector_clusters=[],
index_change=0.5,
market_wide=False,
)
labels = {"no_catalyst": " -- no news"}
result = format_watchpoints(data, "en", labels)
assert "XYZ" in result
assert "no news" in result
class TestBuildWatchpointsData:
def test_builds_complete_data_structure(self):
movers = [
{"symbol": "NVDA", "change_pct": -5.0, "price": 100.0},
{"symbol": "AMD", "change_pct": -4.0, "price": 80.0},
{"symbol": "INTC", "change_pct": -3.0, "price": 30.0},
{"symbol": "AAPL", "change_pct": 2.0, "price": 150.0},
]
headlines = [
{"title": "NVIDIA reports weak guidance"},
{"title": "Apple announces new product"},
]
portfolio_meta = {
"NVDA": {"category": "Tech", "name": "NVIDIA Corporation"},
"AMD": {"category": "Tech", "name": "Advanced Micro Devices"},
"INTC": {"category": "Tech", "name": "Intel Corporation"},
"AAPL": {"category": "Tech", "name": "Apple Inc"},
}
index_change = -0.5
result = build_watchpoints_data(movers, headlines, portfolio_meta, index_change)
# Should detect Tech sector cluster (3 losers)
assert len(result.sector_clusters) == 1
assert result.sector_clusters[0].category == "Tech"
assert result.sector_clusters[0].direction == "down"
# All movers should be present
assert len(result.movers) == 4
# NVDA should have matched headline
nvda_mover = next(m for m in result.movers if m.symbol == "NVDA")
assert nvda_mover.matched_headline is not None
assert "guidance" in nvda_mover.matched_headline["title"]
# vs_index should be calculated
assert nvda_mover.vs_index == -5.0 - (-0.5) # -4.5
def test_handles_empty_movers(self):
result = build_watchpoints_data([], [], {}, 0.0)
assert result.movers == []
assert result.sector_clusters == []
assert result.market_wide is False
def test_detects_market_wide_move(self):
result = build_watchpoints_data([], [], {}, -2.0)
assert result.market_wide is True

97
workflows/README.md Normal file
View File

@@ -0,0 +1,97 @@
# Lobster Workflows
This directory contains [Lobster](https://github.com/openclaw/lobster) workflow definitions for the finance-news skill.
## Available Workflows
### `briefing.yaml` - Market Briefing with Approval
Generates a market briefing and sends to WhatsApp with an approval gate.
**Usage:**
```bash
# Run via Lobster CLI
lobster "workflows.run --file ~/projects/finance-news-openclaw-skill/workflows/briefing.yaml"
# With custom args
lobster "workflows.run --file workflows/briefing.yaml --args-json '{\"time\":\"evening\",\"lang\":\"en\"}'"
```
**Arguments:**
| Arg | Default | Description |
|-----|---------|-------------|
| `time` | `morning` | Briefing type: `morning` or `evening` |
| `lang` | `de` | Language: `en` or `de` |
| `channel` | `whatsapp` | Delivery channel: `whatsapp` or `telegram` |
| `target` | env var | Group name, JID, phone number, or Telegram chat ID |
| `fast` | `false` | Use fast mode (shorter timeouts) |
**Environment Variables:**
| Variable | Description |
|----------|-------------|
| `FINANCE_NEWS_CHANNEL` | Default channel: `whatsapp` or `telegram` |
| `FINANCE_NEWS_TARGET` | Default target (group name, phone, chat ID) |
**Examples:**
```bash
# WhatsApp group (default)
lobster "workflows.run --file workflows/briefing.yaml"
# Telegram group
lobster "workflows.run --file workflows/briefing.yaml --args-json '{\"channel\":\"telegram\",\"target\":\"-1001234567890\"}'"
# WhatsApp DM to phone number
lobster "workflows.run --file workflows/briefing.yaml --args-json '{\"target\":\"+15551234567\"}'"
# Telegram DM to user
lobster "workflows.run --file workflows/briefing.yaml --args-json '{\"channel\":\"telegram\",\"target\":\"@username\"}'"
```
**Flow:**
1. **Generate** - Runs Docker container to produce briefing JSON
2. **Approve** - Halts for human review (shows briefing preview)
3. **Send** - Delivers to channel (WhatsApp/Telegram) after approval
**Requirements:**
- Docker with `finance-news-briefing` image built
- `jq` for JSON parsing
- `openclaw` CLI for message delivery
## Adding to Lobster Registry
To make these workflows available as named workflows in Lobster:
```typescript
// In lobster/src/workflows/registry.ts
export const workflowRegistry = {
// ... existing workflows
'finance.briefing': {
name: 'finance.briefing',
description: 'Generate market briefing with approval gate for WhatsApp/Telegram',
argsSchema: {
type: 'object',
properties: {
time: { type: 'string', enum: ['morning', 'evening'], default: 'morning' },
lang: { type: 'string', enum: ['en', 'de'], default: 'de' },
channel: { type: 'string', enum: ['whatsapp', 'telegram'], default: 'whatsapp' },
target: { type: 'string', description: 'Group name, JID, phone, or chat ID' },
fast: { type: 'boolean', default: false },
},
},
examples: [
{ args: { time: 'morning', lang: 'de' }, description: 'German morning briefing to WhatsApp' },
{ args: { channel: 'telegram', target: '-1001234567890' }, description: 'Send to Telegram group' },
],
sideEffects: ['message.send'],
},
};
```
## Why Lobster?
Using Lobster instead of direct cron execution provides:
- **Approval gates** - Review briefing before it's sent
- **Resumability** - If interrupted, continue from last step
- **Token efficiency** - One workflow call vs. multiple LLM tool calls
- **Determinism** - Same inputs = same outputs

View File

@@ -0,0 +1,45 @@
# Price Alerts Workflow for Cron (No Approval Gate)
# Usage: lobster run --file workflows/alerts-cron.yaml --args-json '{"lang":"en"}'
#
# Schedule: 2:00 PM PT / 5:00 PM ET (1 hour after market close)
# Checks price alerts against current prices (including after-hours)
name: finance.alerts.cron
description: Check price alerts and send triggered alerts to WhatsApp/Telegram
args:
lang:
default: en
description: "Language: en or de"
channel:
default: "${FINANCE_NEWS_CHANNEL:-whatsapp}"
description: "Delivery channel: whatsapp or telegram"
target:
default: "${FINANCE_NEWS_TARGET}"
description: "Target: group name, JID, or chat ID"
steps:
# Check alerts against current prices
- id: check_alerts
command: |
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
python3 "$SKILL_DIR/scripts/alerts.py" check --lang "${lang}"
description: Check price alerts against current prices
# Send alert message if there's content
- id: send_alerts
command: |
MSG=$(cat)
MSG=$(echo "$MSG" | tr -d '\r')
# Only send if message has actual content (not just "No price data" message)
if echo "$MSG" | grep -q "IN BUY ZONE\|IN KAUFZONE\|WATCHING\|BEOBACHTUNG"; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
echo "Sent price alerts to ${channel}"
else
echo "No triggered alerts or watchlist items to send"
fi
stdin: $check_alerts.stdout
description: Send price alerts to channel

View File

@@ -0,0 +1,101 @@
# Finance Briefing Workflow for Cron (No Approval Gate)
# Usage: lobster run --file workflows/briefing-cron.yaml --args-json '{"time":"morning","lang":"de"}'
#
# This workflow:
# 1. Generates a market briefing via Docker
# 2. Translates portfolio headlines (German)
# 3. Sends directly to messaging channel (no approval)
name: finance.briefing.cron
description: Generate market briefing and send to WhatsApp/Telegram (auto-approve for cron)
args:
time:
default: morning
description: "Briefing type: morning or evening"
lang:
default: de
description: "Language: en or de"
channel:
default: "${FINANCE_NEWS_CHANNEL:-whatsapp}"
description: "Delivery channel: whatsapp or telegram"
target:
default: "${FINANCE_NEWS_TARGET}"
description: "Target: group name, JID, phone number, or Telegram chat ID (requires FINANCE_NEWS_TARGET env var if not specified)"
fast:
default: "false"
description: "Use fast mode: true or false"
steps:
# Generate briefing and save to temp file
- id: generate
command: |
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
FAST_FLAG=""
if [ "${fast}" = "true" ]; then FAST_FLAG="--fast"; fi
OUTFILE="/tmp/lobster-briefing-$$.json"
# Resolve openbb-quote symlink for Docker mount
OPENBB_BIN=$(realpath "$HOME/.local/bin/openbb-quote" 2>/dev/null || echo "")
OPENBB_MOUNT=""
if [ -f "$OPENBB_BIN" ]; then
OPENBB_MOUNT="-v $OPENBB_BIN:/usr/local/bin/openbb-quote:ro"
fi
docker run --rm \
-v "$SKILL_DIR/config:/app/config:ro" \
-v "$SKILL_DIR/scripts:/app/scripts:ro" \
$OPENBB_MOUNT \
finance-news-briefing python3 scripts/briefing.py \
--time "${time}" \
--lang "${lang}" \
--json \
$FAST_FLAG > "$OUTFILE"
# Output the file path for subsequent steps
echo "$OUTFILE"
description: Generate briefing via Docker
# Translate portfolio headlines (if German)
- id: translate
command: |
OUTFILE=$(cat)
OUTFILE=$(echo "$OUTFILE" | tr -d '\n')
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
if [ "${lang}" = "de" ]; then
python3 "$SKILL_DIR/scripts/translate_portfolio.py" "$OUTFILE" --lang de || true
fi
echo "$OUTFILE"
stdin: $generate.stdout
description: Translate portfolio headlines via openclaw
# Send macro briefing (market overview) - NO APPROVAL GATE
- id: send_macro
command: |
OUTFILE=$(cat)
OUTFILE=$(echo "$OUTFILE" | tr -d '\n')
MSG=$(jq -r '.macro_message // empty' "$OUTFILE")
if [ -n "$MSG" ]; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
else
echo "No macro message to send"
fi
stdin: $translate.stdout
description: Send macro briefing
# Send portfolio briefing (stock movers)
- id: send_portfolio
command: |
OUTFILE=$(cat)
OUTFILE=$(echo "$OUTFILE" | tr -d '\n')
MSG=$(jq -r '.portfolio_message // empty' "$OUTFILE")
if [ -n "$MSG" ]; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
else
echo "No portfolio message to send"
fi
stdin: $translate.stdout
description: Send portfolio briefing

115
workflows/briefing.yaml Normal file
View File

@@ -0,0 +1,115 @@
# Finance Briefing Workflow for Lobster
# Usage: lobster "workflows.run --file workflows/briefing.yaml --args-json '{\"time\":\"morning\",\"lang\":\"de\"}'"
#
# This workflow:
# 1. Generates a market briefing via Docker
# 2. Halts for approval before sending
# 3. Sends to messaging channel after approval
name: finance.briefing
description: Generate market briefing and send to WhatsApp/Telegram with approval gate
args:
time:
default: morning
description: "Briefing type: morning or evening"
lang:
default: de
description: "Language: en or de"
channel:
default: "${FINANCE_NEWS_CHANNEL:-whatsapp}"
description: "Delivery channel: whatsapp or telegram"
target:
default: "${FINANCE_NEWS_TARGET}"
description: "Target: group name, JID, phone number, or Telegram chat ID (requires FINANCE_NEWS_TARGET env var if not specified)"
fast:
default: "false"
description: "Use fast mode: true or false"
steps:
# Generate briefing and save to temp file
- id: generate
command: |
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
FAST_FLAG=""
if [ "${fast}" = "true" ]; then FAST_FLAG="--fast"; fi
OUTFILE="/tmp/lobster-briefing-$$.json"
# Resolve openbb-quote symlink for Docker mount
OPENBB_BIN=$(realpath "$HOME/.local/bin/openbb-quote" 2>/dev/null || echo "")
OPENBB_MOUNT=""
if [ -f "$OPENBB_BIN" ]; then
OPENBB_MOUNT="-v $OPENBB_BIN:/usr/local/bin/openbb-quote:ro"
fi
docker run --rm \
-v "$SKILL_DIR/config:/app/config:ro" \
-v "$SKILL_DIR/scripts:/app/scripts:ro" \
$OPENBB_MOUNT \
finance-news-briefing python3 scripts/briefing.py \
--time "${time}" \
--lang "${lang}" \
--json \
$FAST_FLAG > "$OUTFILE"
# Output the file path for subsequent steps
echo "$OUTFILE"
description: Generate briefing via Docker
# Translate portfolio headlines (if German)
- id: translate
command: |
OUTFILE=$(cat)
OUTFILE=$(echo "$OUTFILE" | tr -d '\n')
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
if [ "${lang}" = "de" ]; then
python3 "$SKILL_DIR/scripts/translate_portfolio.py" "$OUTFILE" --lang de || true
fi
echo "$OUTFILE"
stdin: $generate.stdout
description: Translate portfolio headlines via openclaw
# Approval gate - workflow halts here until user approves
- id: approve
approval: required
command: |
OUTFILE=$(cat)
echo "Briefing saved to: $OUTFILE"
echo "Target: ${target}"
echo "Channel: ${channel}"
cat "$OUTFILE" | jq -r '.macro_message' | head -20
echo "..."
echo "Review above. Approve to send."
stdin: $translate.stdout
description: Approval gate before message delivery
# Send macro briefing (market overview)
- id: send_macro
command: |
OUTFILE=$(cat)
OUTFILE=$(echo "$OUTFILE" | tr -d '\n')
MSG=$(jq -r '.macro_message // empty' "$OUTFILE")
if [ -n "$MSG" ]; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
else
echo "No macro message to send"
fi
stdin: $translate.stdout
description: Send macro briefing
# Send portfolio briefing (stock movers)
- id: send_portfolio
command: |
OUTFILE=$(cat)
OUTFILE=$(echo "$OUTFILE" | tr -d '\n')
MSG=$(jq -r '.portfolio_message // empty' "$OUTFILE")
if [ -n "$MSG" ]; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
else
echo "No portfolio message to send"
fi
stdin: $translate.stdout
description: Send portfolio briefing

View File

@@ -0,0 +1,45 @@
# Earnings Alert Workflow for Cron (No Approval Gate)
# Usage: lobster run --file workflows/earnings-cron.yaml --args-json '{"lang":"en"}'
#
# Schedule: 6:00 AM PT / 9:00 AM ET (30 min before market open)
# Sends today's earnings calendar to WhatsApp/Telegram
name: finance.earnings.cron
description: Send earnings alerts for today's reports
args:
lang:
default: en
description: "Language: en or de"
channel:
default: "${FINANCE_NEWS_CHANNEL:-whatsapp}"
description: "Delivery channel: whatsapp or telegram"
target:
default: "${FINANCE_NEWS_TARGET}"
description: "Target: group name, JID, or chat ID"
steps:
# Check earnings calendar for today and this week
- id: check_earnings
command: |
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
python3 "$SKILL_DIR/scripts/earnings.py" check --lang "${lang}"
description: Get today's earnings calendar
# Send earnings alert if there's content
- id: send_earnings
command: |
MSG=$(cat)
MSG=$(echo "$MSG" | tr -d '\r')
# Only send if there are actual earnings today
if echo "$MSG" | grep -q "EARNINGS TODAY\|EARNINGS HEUTE"; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
echo "Sent earnings alert to ${channel}"
else
echo "No earnings today - skipping message"
fi
stdin: $check_earnings.stdout
description: Send earnings alert to channel

View File

@@ -0,0 +1,45 @@
# Weekly Earnings Alert Workflow for Cron (No Approval Gate)
# Usage: lobster run --file workflows/earnings-weekly-cron.yaml --args-json '{"lang":"en"}'
#
# Schedule: Sunday 7:00 AM PT (before market week starts)
# Sends upcoming week's earnings calendar to WhatsApp/Telegram
name: finance.earnings.weekly.cron
description: Send weekly earnings preview for portfolio stocks
args:
lang:
default: en
description: "Language: en or de"
channel:
default: "${FINANCE_NEWS_CHANNEL:-whatsapp}"
description: "Delivery channel: whatsapp or telegram"
target:
default: "${FINANCE_NEWS_TARGET}"
description: "Target: group name, JID, or chat ID"
steps:
# Check earnings calendar for upcoming week
- id: check_earnings
command: |
SKILL_DIR="${SKILL_DIR:-$HOME/projects/skills/personal/finance-news}"
python3 "$SKILL_DIR/scripts/earnings.py" check --week --lang "${lang}"
description: Get upcoming week's earnings calendar
# Send earnings alert if there's content
- id: send_earnings
command: |
MSG=$(cat)
MSG=$(echo "$MSG" | tr -d '\r')
# Only send if there are actual earnings next week
if echo "$MSG" | grep -qE "EARNINGS (NEXT WEEK|NÄCHSTE WOCHE)"; then
openclaw message send \
--channel "${channel}" \
--target "${target}" \
--message "$MSG"
echo "Sent weekly earnings preview to ${channel}"
else
echo "No earnings next week - skipping message"
fi
stdin: $check_earnings.stdout
description: Send weekly earnings alert to channel