stock-screener
CLAUDE.md417 starsChanged 6 days agoApache-2.0View on GitHub
Stock screening platform implementing CANSLIM (William O'Neil) and Minervini methodologies, with theme discovery, AI chatbot, and market analysis. Full-stack application with FastAPI backend, React frontend, PostgreSQL, Redis caching, and Celery for background tasks. Layered Docker Compose architecture with three scenarios: Docker files: - docker-compose.yml - Base config (local dev) - docker-compose.prod.yml - Production overlay (re limits, health checks, logging) - docker-compose.https.yml - HTTPS overlay (Caddy with Let's Encrypt) - .env.docker.example - Docker environment template - Caddyfile - Caddy TLS configuration
- Reads credentials
- Deletes or force-pushes
In this repository2 other files it gives its agents.
- AGENTS.mdAGENTS.md
- contracts/llms.txtllms.txt
The file
We split this CLAUDE.md into the rules it gives an agent
12 rules. About 0 are common, 4 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 12
Run the FULL local test suite, including workflow/CI tests (e.g.
- Very specific: line 13
Do not invent action version tags (e.g.
- Very specific: line 14
Before pushing, self-review the diff for the bug classes reviewers keep finding here: swallowed exceptions or overly broad except, race …
- Distinctive: line 19
After fixing a round, look for sibling cases of the same bug elsewhere in the diff before pushing, so the next review round doesn't flag …
- Very specific: line 20
After pushing, request re-review (@codex review) and use ScheduleWakeup to poll CI and reviews.
- Distinctive: line 26
Do one feature per git worktree.
- Distinctive: line 28
Before stating facts in READMEs, issues or PR comments (versions, schedules, behavior), verify them against the code or config and cite the …
- Very specific: line 190
All screeners extend BaseStockScreener abstract class
- Very specific: line 197
All external API tasks route to data_fetch to prevent rate limit violations
- Very specific: line 233
When adding new API endpoints, use paths starting with /v1/:
- Distinctive: line 268
When designing a feature that reads or writes prices, state what it does mid-session: whether it can see a partial bar, whether it can …
- Very specific: line 299
If you see an empty database or missing data, verify DATABASE_URL points to the intended PostgreSQL instance
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Stock screening platform implementing CANSLIM (William O'Neil) and Minervini methodologies, with theme discovery, AI chatbot, and market analysis. Full-stack application with FastAPI backend, React frontend, PostgreSQL, Redis caching, and Celery for background tasks.
PR Workflow
Before pushing / opening a PR
- Run the FULL local test suite, including workflow/CI tests (e.g. tests that check .github/workflows). Never push workflow YAML changes without running those tests locally.
- Do not invent action version tags (e.g. setup-uv@v10). Check that the tag exists with
gh api repos/<owner>/<repo>/git/refs/tags/<tag>first. - Before pushing, self-review the diff for the bug classes reviewers keep finding here: swallowed exceptions or overly broad
except, race conditions on first claim or lease, circuit breakers or state that can never reset, platform-specific exceptions (Windows ValueError), and missing imports.
PR Review Workflow
Addressing PR review comments
- For each bot or human thread: verify the finding, write a test that fails on the current code when applicable, fix it, then reply on the thread with the REAL commit SHA (never a placeholder).
- After fixing a round, look for sibling cases of the same bug elsewhere in the diff before pushing, so the next review round doesn't flag the gap.
- After pushing, request re-review (
@codex review) and use ScheduleWakeup to poll CI and reviews. If gh returns a transient 401 or connection error, retry once before reporting failure.
Environment & Conventions
Environment & facts
- Shell is zsh: quote variables and use arrays, since unquoted
$VARdoes not word-split. Avoidsource/nvmandrm -rf(blocked by safety hooks). - Do one feature per git worktree. Never
git checkouta commit hash (it detaches HEAD); usegit switchwith a branch. - Schedules: the US cron runs in America/New_York, not UTC. CN/HK/JP market hours matter: don't trigger data runs while the market is open.
- Before stating facts in READMEs, issues or PR comments (versions, schedules, behavior), verify them against the code or config and cite the file.
Development Commands
Backend
cd backend
. venv/bin/activate
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000Frontend
cd frontend
npm run dev # Vite dev server on :5173
npm run build # Production build
npm run lint # ESLintCelery Workers (required for scans)
cd backend
./start_celery.sh # Starts both queues
# Or manually:
./venv/bin/celery -A app.celery_app worker --pool=solo -Q celery -n general@%h
./venv/bin/celery -A app.celery_app worker --pool=solo -Q data_fetch -n datafetch@%h
./venv/bin/celery -A app.celery_app beat --loglevel=info # SchedulerDocker Deployment
Layered Docker Compose architecture with three scenarios:
# Local development
cp .env.docker.example .env # Add API keys for chatbot
docker-compose up
# Homelab (behind reverse proxy like Traefik/nginx proxy manager)
cp .env.docker.example .env.docker
# Edit: CORS_ORIGINS=https://stocks.home.lan
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# VPS with auto-HTTPS (Hostinger, DigitalOcean, etc.)
cp .env.docker.example .env.docker
# Edit: DOMAIN=stocks.yourdomain.com, CORS_ORIGINS=https://stocks.yourdomain.com
docker-compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.https.yml up -dDocker files:
docker-compose.yml- Base config (local dev)docker-compose.prod.yml- Production overlay (re limits, health checks, logging)docker-compose.https.yml- HTTPS overlay (Caddy with Let's Encrypt).env.docker.example- Docker environment templateCaddyfile- Caddy TLS configuration
Dependencies: Images install the hash-locked backend/requirements-runtime.lock. After changing backend/requirements-runtime.txt, run make lock-runtime, which ignores releases from the last 7 days. Add LOCK_ARGS=--upgrade to update existing pins.
Note: Backend runs as non-root user (uid 1000). After upgrade: sudo chown -R 1000:1000 ./data
Running Tests
Backend (pytest)
cd backend
. venv/bin/activate
# Run all tests
pytest
# Run unit tests only
pytest tests/unit/
# Run integration tests (requires running server at localhost:8000)
pytest tests/integration/ -m integration
# Run with verbose output
pytest -v
# Run specific test file
pytest tests/unit/test_canslim_scanner.pyFrontend (Vitest + React Testing Library)
cd frontend
# Run all tests once (CI mode)
npm run test:run
# Run tests in watch mode (development)
npm run test
# Run a specific test file
npx vitest run src/components/Scan/ResultsTable.test.jsx
# Lint test files
npm run lintNote: Vitest 4.x requires Node 18+ (the Smithery CLI used for stockscreen-mcp needs 20+). On this machine the default node is already v24: nvm's default alias and /usr/local/bin/node are both Node 24, and nvm also has v22. No activation step is needed.
Test file locations:
frontend/src/components/Scan/— component tests (ResultsTable, FilterPanel, SetupEngineDrawer)frontend/src/components/Scan/filters/— filter sub-component tests (CompactRangeInput, CompactCheckbox)frontend/src/utils/— utility tests (formatUtils)frontend/src/test/fixtures/— shared test fixturesfrontend/src/test/renderWithProviders.jsx— MUI ThemeProvider test wrapper
Quality Gates (Setup Engine)
make help # List all available targets
make gates # Run all 5 SE quality gates
make gate-1 # Detector correctness
make gate-2 # Temporal integrity
make gate-3 # Integration coverage
make gate-4 # Performance baselines (advisory)
make gate-5 # Golden regression
make gate-check # Verify all SE test files are in a gate
make all # Full CI (backend gates + frontend)
make golden-update # Regenerate golden snapshotsDiagnostic Scripts
Utility scripts are in backend/scripts/:
cd backend
. venv/bin/activate
python scripts/inspect_redis.py # Inspect Redis cache keys
python scripts/cache_diagnostic.py # Trace cache flow (DB → Redis)
python scripts/check_cache_status.py # Check price cache status
python scripts/clear_redis_price_cache.py # Clear Redis cache after config change
python scripts/force_full_cache_refresh.py # Force full cache refresh
python scripts/cleanup_orphaned_scans.py # Synchronously delete orphaned scans
python scripts/profile_scan_phases.py # Time a bulk scan's prefetch/compute/persist phases (run in a worker container)Manual orphaned scan cleanup runs directly:
python scripts/cleanup_orphaned_scans.pyThe Celery task app.tasks.cache_tasks.cleanup_orphaned_scans remains the scheduled background path and requires a live worker to execute.
Architecture
Backend Structure
- FastAPI application in
backend/app/main.py - API routes in
backend/app/api/v1/- RESTful endpoints for stocks, scans, themes, chatbot, signals - Services in
backend/app/services/- Business logic layer (70+ service files) - Scanners in
backend/app/scanners/- Stock screening implementations - Tasks in
backend/app/tasks/- Celery background tasks - Models in
backend/app/models/- SQLAlchemy ORM models - Schemas in
backend/app/schemas/- Pydantic request/response schemas
Key Architectural Patterns
Multi-Screener Orchestrator (scanners/scan_orchestrator.py):
- Coordinates Minervini, CANSLIM, IPO, Volume Breakthrough, and Custom screeners
- All screeners extend
BaseStockScreenerabstract class - Data fetched once and shared across screeners
- Composite scoring via configurable aggregation (weighted_average, maximum, minimum)
Two-Queue Celery Architecture:
celeryqueue: General compute tasks (4 workers local; Docker concurrency follows the deployed worker topology)data_fetchqueue: API calls (1 worker, serialized to respect rate limits)- All external API tasks route to
data_fetchto prevent rate limit violations - Exception — company exposure research: its tasks route only to the dedicated
exposure_researchqueue and opt-incelery-exposure-researchworker (docker-compose.exposure.yml, orEXPOSURE_WORKER_ENABLED=trueinstart_celery.sh), so long LLM/document stages never occupy price-fetch workers. Every research HTTP attempt still acquires the sharedRateBudgetPolicyprovider keys (e.g.sec_edgar); queue isolation grants no extra upstream rate. Research is disabled by default (EXPOSURE_RESEARCH_MODE=disabled). Beat entriescompany-exposure-*feed that queue with messageexpires, so they drop harmlessly when the worker is not deployed.
Per-Market Scan Queues:
- Manual user scans (
run_bulk_scan) route to market-specific queues:user_scans_us,user_scans_hk,user_scans_jp,user_scans_tw(withuser_scans_sharedas fallback when no market is set) start_celery.shspawns one worker per market inENABLED_MARKETS(env, elsebackend/.env, elseUS), so withUS,HKthe US and HK scans run in parallel on separate workers@serialized_market_workload(seetasks/workload_coordination.py) holds a Redis lock keyed by market, so scans targeting the same market serialize while different markets remain independent- Manual scans run in cache-only mode — they do not fall back to yfinance/Finviz. The API boundary rejects scans with
409 market_data_stalewhen cached prices haven't caught up to the last completed trading day for the market (seeservices/market_data_freshness.py::check_symbol_freshness). Inside the scan,RunBulkScanCommand(cache_only=True)reachesPriceCacheService.get_many(cache_only=True), which serves per-stock prices from whatever the database holds (even under 50 bars, or stale) and never calls a provider. The market benchmark lookup is not cache-only (it fetches from Yahoo if no cached benchmark reaches the last completed session), so the gate also refuses a market whose cached benchmark candidates are all behind that session (reason: benchmark_stale, #455)
Redis Caching Strategy (three-tier: Redis → PostgreSQL → API):
- DB 0: Celery broker
- DB 1: Celery results (24h TTL, auto-cleanup)
- DB 2: Application cache via shared connection pool (
services/redis_pool.py)- Price cache: 7d TTL, stores 5 years of OHLCV (required for Volume Breakthrough Scanner)
- Fundamentals cache: 7d TTL with DB fallback on Redis miss
- Benchmark (SPY): 24h TTL with distributed locking to prevent thundering herd
LLM Integration (services/llm/):
- Supported provider path: Groq for chatbot/research, Minimax for primary theme extraction/merge, Z.AI fallback for extraction/merge
- The assistant (
services/assistant_gateway_service.py) proxies chat to the Hermes sidecar. Hermes readsTAVILY_API_KEYandSERPER_API_KEY; the backend reads neither
Frontend Structure
- React 18 with Vite
- Material-UI for components
- React Query (TanStack) for data fetching
- MUI Table with TanStack Virtual row virtualization for results display
- Pages in
frontend/src/pages/: ScanPage, ChatbotPage, ThemesPage, GroupRankingsPage, BreadthPage, SignalsPage
Frontend API Client Convention
CRITICAL: API paths must NOT include /api prefix
The axios client in frontend/src/api/client.js handles the /api prefix via baseURL:
- Local dev:
baseURL = 'http://localhost:8000/api' - Docker:
baseURL = '/api'(set viaVITE_API_URLbuild arg)
When adding new API endpoints, use paths starting with /v1/:
// ✅ CORRECT - path without /api prefix
const response = await apiClient.get('/v1/themes/rankings');
// ❌ WRONG - will cause double prefix in Docker (/api/api/v1/...)
const response = await apiClient.get('/api/v1/themes/rankings');For API modules with a BASE_PATH constant:
// ✅ CORRECT
const BASE_PATH = '/v1/user-themes';
// ❌ WRONG
const BASE_PATH = '/api/v1/user-themes';Why this matters: In Docker, nginx proxies /api/* to the backend. If paths include /api, you get /api/api/v1/... which returns 404.
Data Sources & Rate Limits
- yfinance: 1 req/sec (self-imposed)
- Finviz: Rate-limited via wrapper
- SEC EDGAR: 10 req/sec (150ms between requests)
Market Hours Awareness
Price data means different things during and after a trading session. Check the market's state before running anything that reads or fetches prices against a real database, and design for both states.
- Freshness is judged against the last completed session.
MarketCalendarService.last_completed_trading_day(market)is that day;is_market_open(market)says whether a session is in progress. US regular hours are 09:30–16:00 ET (13:30–20:00 UTC in daylight time, 14:30–21:00 UTC otherwise). Every market has its own calendar. - A fetch during a session stores a partial bar for today. The price cache marks it in Redis fetch metadata (
needs_refresh_after_close) and refreshes it after the close. The marker lives in the cache Redis database, so a process pointed at a differentCACHE_REDIS_DBwrites the partial bar to PostgreSQL with no marker the live stack can see. - Only the scan's
cache_onlypath keeps per-stock prices off providers.batch_only_prices/batch_only_fundamentalsstop the per-symbol fallback only, and a plainPriceCacheService.get_manystill batch-fetches symbols with no usable database frame (stale rows, or fewer than 50 bars). The benchmark lookup can fetch in any mode; for manual scans the gate keeps it from needing to (#455). Code that must not reach a provider at all has to block providers itself, asbackend/scripts/profile_scan_phases.pydoes for the benchmark and single-symbol fetches. - The freshness cut-off moves at the close. A job that straddles it sees symbols turn stale part-way through. Start and finish long runs, benchmarks and profiles inside one session state, and record which state they ran in.
- Scheduled refresh jobs run after the close. They compete for CPU and rewrite the cache, so timings taken then are not comparable with timings taken mid-session.
When designing a feature that reads or writes prices, state what it does mid-session: whether it can see a partial bar, whether it can trigger a fetch, and what happens if the session closes while it runs.
Environment Variables
Local development: backend/.env (see backend/.env.example)
Docker deployment: .env.docker in project root (see .env.docker.example)
Required for chatbot (at least one supported LLM provider):
GROQ_API_KEY,GROQ_API_KEYS- Groq (fast inference, free tier)MINIMAX_API_KEY- Minimax (primary theme extraction)ZAI_API_KEY,ZAI_API_KEYS- Z.AI (theme extraction/merge fallback)
Web search (read by the Hermes assistant sidecar, not the backend):
TAVILY_API_KEY,SERPER_API_KEY
Infrastructure:
DATABASE_URL- PostgreSQL connection string (e.g.,postgresql://user:pass@localhost/stockscanner)REDIS_HOST,CELERY_BROKER_URL- Redis/Celery configurationCORS_ORIGINS- Comma-separated allowed origins (for production)
LLM routing (optional):
LLM_DEFAULT_PROVIDER- Optional override for sanctioned primary paths (Groq chatbot/research, Minimax extraction/merge)LLM_CHATBOT_MODEL,LLM_RESEARCH_MODEL- Model overrides (LiteLLM format)LLM_FALLBACK_ENABLED- Enable automatic provider fallback
Database
CRITICAL: Database Location
- Supported database: PostgreSQL referenced by
DATABASE_URL docker-data/postgres/holds the Docker Postgres data directorydata/remains for non-database state such as caches, backups, and Celery beat state- If you see an empty database or missing data, verify
DATABASE_URLpoints to the intended PostgreSQL instance
Persistent data paths:
docker-data/postgres/- PostgreSQL data directory (Docker)backend/celerybeat-schedule.db- Celery Beat scheduler state (legacy local path)
Key tables:
stock_prices,stock_fundamentals,stock_universe- Core stock datascans,scan_results- Scan metadata and results with multi-screener scoresibd_groups,ibd_group_ranks- Industry group rankingstheme_clusters,theme_constituents- Theme discoverysignals- Technical signal detectionschat_sessions,chat_messages- Chatbot conversation history
Migrations are versioned under backend/alembic/. PostgreSQL is the supported database for development and Docker deployment.
Screening Methodologies
Minervini Template: RS Rating > 70-80, Stage 2 uptrend, MA alignment (50 > 150 > 200), price 30%+ above 52-week low
CANSLIM: Current quarterly EPS > 25%, Annual EPS growth > 25% 3yr, new highs, volume patterns, RS > 70, institutional ownership 40-70%
macOS Development Note
For Celery on macOS, use --pool=solo (set via start_celery.sh) to avoid fork() crashes with curl_cffi. Also set:
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES
export TOKENIZERS_PARALLELISM=falseGit Conventions
This project uses Conventional Commits for all commit messages. Format:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]Types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changes (formatting, no logic change)refactor: Code refactoring (no feature or fix)perf: Performance improvementstest: Adding or updating testsbuild: Build system or dependency changesci: CI/CD configuration changeschore: Maintenance tasks (deps, configs)
Examples:
feat(scanner): add volume breakthrough screener
fix(chatbot): handle empty response from LLM provider
docs: update API endpoint documentation
refactor(api): consolidate stock data fetching logic
test(canslim): add unit tests for EPS calculationScopes (optional): api, scanner, chatbot, frontend, celery, db, cache, themes, signals
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

