Stock-Tool/CLAUDE.md

109 lines
5.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Stock Tool — Claude Context
## Project Overview
A multi-factor stock screener that evaluates ~6,500 US-listed equities using a composite
score across 8 dimensions: Value, Growth, Momentum, Quality, Profitability, Sentiment,
Analyst, and Risk. Sold as a licensed desktop app (PySide6 GUI).
## File Map
| File | Purpose |
|------|---------|
| `dist/stock_screener.py` | All scoring logic, data fetching, index/ticker collection. Pure Python + pandas. |
| `dist/screener_gui.py` | PySide6 GUI. Imports from `stock_screener`. Main class: `ScreenerApp(QMainWindow)`. |
| `cache_builder.py` | Nightly job that populates PostgreSQL cache from EDGAR + yfinance. Runs on VPS via cron. |
| `api/main.py` | FastAPI server on VPS. Handles license auth, cache serving, version management. |
| `updater.py` | Client-side auto-updater. Checks `/update` endpoint, downloads new build. |
| `license_check.py` | HMAC + JWT license validation. Baked into app via PyArmor. |
| `push_update.py` | Dev tool: SCP new build to VPS + register version via `/admin/register-version`. |
| `launcher.py` | Thin launcher that checks license before starting GUI. |
| `health_monitor.py` | VPS health monitoring. |
| `discord_bot/bot.py` | Discord bot integration (separate process). |
## Architecture (3-tier)
```
[cache_builder.py] → [PostgreSQL on VPS] → [API] → [Client app]
nightly 6 tables FastAPI screener_gui + stock_screener
```
- `cache_builder.py` runs nightly (2am stable / 3am beta) via cron on the VPS
- Data flows: EDGAR XBRL Frames API + yfinance → PostgreSQL tables → served by FastAPI
- Client fetches cached data at startup via `/cache/analyst`, `/cache/fundamentals`, `/cache/sector`
- Scoring runs entirely client-side in `stock_screener.py` — no server-side scoring
## PostgreSQL Tables (database: `verimund`, user: `verimund_user`)
| Table | Contents |
|-------|----------|
| `analyst_cache` | Price history, returns, sentiment, analyst ratings, short interest (~6,975 rows) |
| `fundamentals_cache` | EDGAR-sourced financials: revenue, margins, D/E, ROA/ROE, Piotroski, accruals |
| `sector_stats` | Per-sector median/MAD for each metric. Used by `_z_score()` for normalization. |
| `app_versions` | Version registry per channel (stable/beta). Read by `/admin/versions`. |
| `licenses` | License keys + HWIDs + expiry. Validated at auth time. |
| `auth_log` | Auth attempt log. |
## Scoring Formula
```
Composite = 0.18*Value + 0.18*Growth + 0.14*Momentum + 0.11*Quality
+ 0.08*Profitability + 0.13*Sentiment + 0.10*Analyst + 0.08*Risk
```
Each score is 0100. Z-scores use sector median/MAD from `sector_stats` table.
If sector stats are missing for a metric, `_z_score()` returns neutral 50.
## Profile System (important)
`_classify_profile(row)` classifies each stock into one of:
`mature`, `high_growth`, `financial`, `capital_intensive`, `early_stage`, `turnaround`
Profiles define sub-weights per scoring dimension in `_PROFILE_WEIGHTS`.
Weights are normalized in `_profile_score_row()` — they don't need to sum to 1.0 in the dict.
**D/E handling**: yfinance returns `debtToEquity` as percentage (e.g. 150 = 1.5x).
Code divides by 100 at ingest (`stock_screener.py` line ~1356).
Negative D/E means negative book equity — replaced with 999.0 before z-scoring.
## Key Patterns
- `_z_series(series, metric, sectors, sector_stats)` — vectorized z-score using sector stats
- `_score_series(series, breakpoints)` — absolute breakpoint scoring (no sector normalization)
- `_profile_score_row(row, dimension, metric_scores)` — blend metric scores using profile weights
- All scoring functions signature: `compute_X_score(df, sector_stats=None) → pd.Series`
## VPS Infrastructure
- **Provider**: Hetzner CPX21, Ubuntu 22.04
- **Domains**: api.verimundsolutions.com (API), git.verimundsolutions.com (Gitea)
- **API**: FastAPI via uvicorn, managed by PM2
- **Secrets**: `/srv/api/.env` on VPS (DB_PASS, JWT_SECRET, HMAC_SECRET, ADMIN_KEY)
- **Backups**: nightly rclone → Cloudflare R2 bucket `verimund-backups`
- **Git**: Gitea at git.verimundsolutions.com, repo: ssz223/Stock-Tool, branches: main (stable) / beta
- **Website**: Taken down intentionally (2026-04-03), will be re-done under different brand
## Running Tests
```bash
# Fast tests (run automatically via hook on every edit):
python -m pytest tests/test_scoring.py tests/test_gui.py -q
# Full suite including network (run before pushing):
python -m pytest tests/ -v
# Network tests only:
python -m pytest tests/test_network.py -v
# Skip network tests:
python -m pytest tests/ -m "not network" -v
```
## Known Pitfalls
- `screener_gui.py` uses PySide6 (not PyQt5). Offscreen: `QT_QPA_PLATFORM=offscreen`
- `screener_gui.py` imports from `stock_screener` (same dir) — tests must `cd dist/` or add dist to sys.path
- EDGAR XBRL Frames API: one call per concept returns all US filers. Foreign/IFRS filers can have data 18 months old with no user-facing flag.
- Sector stats built from `analyst_cache` in Phase 5 of cache_builder. If Phase 1 partially fails, sector medians are biased.
- `_sum_frames` requires >= 4 quarters for valid TTM — returns None otherwise.
- Forward PE not in sector stats — `_z_score` returns neutral 50 for forward PE z-scoring.