5.2 KiB
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.pyruns 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/.envon 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
# 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.pyuses PySide6 (not PyQt5). Offscreen:QT_QPA_PLATFORM=offscreenscreener_gui.pyimports fromstock_screener(same dir) — tests mustcd 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_cachein Phase 5 of cache_builder. If Phase 1 partially fails, sector medians are biased. _sum_framesrequires >= 4 quarters for valid TTM — returns None otherwise.- Forward PE not in sector stats —
_z_scorereturns neutral 50 for forward PE z-scoring.