109 lines
5.2 KiB
Markdown
109 lines
5.2 KiB
Markdown
# 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 0–100. 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.
|