# 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.