- Python 100%
| screener | ||
| .gitignore | ||
| README.md | ||
| requirements.txt | ||
| setup.py | ||
screener
A CLI stock screener for TSX / TSXV (Toronto Stock Exchange and TSX Venture), implementing Greenblatt-style rank-based factor screening.
Built as a standalone, Flask-free extraction from
investment-system.
Same ScreenerService, same TTLCache, same factor math — just no web app.
What it does
Ranks Canadian equities by a configurable combination of quality + value (+ optional momentum and revenue growth) factors:
| Factor pair | Quality | Value | Momentum | Growth |
|---|---|---|---|---|
roic_fcf (default) |
ROIC | EV/FCF | — | — |
ey_ebit_assets |
EBIT / Total Assets | Earnings Yield | — | — |
roic_fcf_momentum |
ROIC | EV/FCF | 6-mo price return | — |
roic_fcf_growth_momentum |
ROIC | EV/FCF | 6-mo price return | Revenue YoY |
A lower combined score = better. Stocks with combined_score = null are excluded
from ranking and pushed to the bottom.
Installation
cd ~/projects/screener
pip install -e .
Or run without installing:
cd ~/projects/screener
python3 -m screener.cli --help
Requirements
- Python ≥ 3.10
- See
requirements.txt:yfinance,python-dotenv,numpy,pandas
No API keys are required. The screener pulls all data from yfinance and the public
TSX/TSXV company-directory endpoints. (FMP / OpenAI / Brave keys are reserved in
config.py for future research features but aren't used by the current CLI.)
Usage
# Default: magic_formula preset, market cap < $2B, top 50 results, JSON output
python3 -m screener.cli
# Use a different preset
python3 -m screener.cli --preset multi_bagger
python3 -m screener.cli --preset quality_value_momentum
# Filter to small caps only
python3 -m screener.cli --preset multi_bagger --market-cap 500M
# Top 20 results
python3 -m screener.cli --preset magic_formula --limit 20
# Only stocks with positive ROIC
python3 -m screener.cli --positive-roic
# Exclude certain industries (comma-separated)
python3 -m screener.cli --exclude-industries "REIT,Bank—Regional"
# Pretty-printed table instead of JSON
python3 -m screener.cli --pretty
# Write JSON to a file
python3 -m screener.cli --output results.json
# List all available presets
python3 -m screener.cli --presets
Market cap format
--market-cap accepts short forms:
| Input | Value |
|---|---|
2B |
2,000,000,000 |
500M |
500,000,000 |
100K |
100,000 |
1500000 |
1,500,000 |
Output format (JSON)
{
"meta": {
"preset": "magic_formula",
"market_cap_max": 2000000000,
"limit": 50,
"offset": 0,
"total": 42
},
"results": [
{
"ticker": "ENB.TO",
"name": "Enbridge Inc.",
"sector": "Energy",
"industry": "Oil & Gas Midstream",
"market_cap": 98000000000,
"price": 48.20,
"combined_score": 0.0312,
"roic": 0.0854,
"ev_fcf": 11.2,
"roic_rank": 5,
"roic_n": 312,
"fcf_rank": 8,
"fcf_n": 287,
"trailing_pe": 18.4
}
]
}
Cache
Fundamentals and stock lists are cached in a local SQLite DB at cache.db
(configurable via CACHE_DB_PATH env var or .env). TTLs:
| Data | TTL |
|---|---|
| Stock universe | 1 hour |
| Fundamentals | 24 hours |
| Prices | 15 min |
| Research | 24 hours |
Delete cache.db to force a fresh full scan.
Presets
Four default presets are seeded automatically on first run:
| Name | Factor pair | Weights |
|---|---|---|
magic_formula |
roic_fcf |
ROIC 50%, FCF 50% |
microcap |
ey_ebit_assets |
ROIC 50%, FCF 50% |
quality_value_momentum |
roic_fcf_momentum |
ROIC 42%, FCF 42%, Mom 16% |
multi_bagger |
roic_fcf_growth_momentum |
ROIC 30%, FCF 25%, Mom 20%, Gr 25% |
Add or edit presets via SQL on the presets table in cache.db, or via the
PresetStore API in screener.preset.
Architecture
screener/
├── screener/
│ ├── __init__.py # package marker
│ ├── cache.py # TTLCache — SQLite WAL-mode cache with fetch stampede protection
│ ├── config.py # Config + env var loader (Flask-free)
│ ├── preset.py # PresetStore — named factor weight configurations
│ ├── screener.py # ScreenerService — fetch + rank pipeline
│ └── cli.py # argparse CLI entry point
├── requirements.txt
├── setup.py
└── README.md
Module roles
cache.TTLCache— SQLite cache with per-key locking to prevent fetch stampedes. Numpy-aware JSON serializer. WAL mode for safe concurrent access.config.Config— env-var-backed configuration. ReadsCACHE_DB_PATH,FMP_API_KEY,OPENAI_API_KEY,BRAVE_API_KEYfrom environment or.env.preset.PresetStore— CRUD overpresetstable in the same SQLite DB as the cache. Stores factor pair identifier and per-factor weights.screener.ScreenerService— the heart of the system:- Fetch TSX/TSXV ticker list (cached 1h, pre-filtered to drop ETFs/REITs)
- Filter to market-cap range via yfinance metadata
- Fetch fundamentals concurrently (
MAX_CONCURRENT_FETCHES = 20, 0.5s delay between calls to stay under Yahoo rate limits) - Apply preset-specific filters (e.g.
roic_fcfrequires positive FCF) - Fetch 6-mo momentum only if the preset uses it (and only for survivors)
- Rank within each factor (ROIC/EV-FCF/EY/EBIT-Assets/Mom/Growth)
- Combine via normalized weighted ranks
cli— argparse wrapper. Loads.env, builds the service, dispatches to JSON or pretty-print output.
Differences from the original investment-system
This repo is the screener extracted from
~/projects/investment-system:
- No Flask. The
app/__init__.pyFlask factory is gone.config.pyno longer referencesSECRET_KEYfor signing (kept asNonefor import compatibility with ported code). - No web routes, no templates, no static assets. Just the screening core.
- Normal imports. The CLI script no longer uses
importlibto bypass Flask-triggering__init__.pyfiles. - No USD→CAD conversion. The original Flask
routes/screener.pyusedget_usd_to_cad()for template display. The CLI uses rawmarket_capvalues directly. A stubget_usd_to_cad()returning1.35is kept inconfig.pyfor compatibility. - Self-contained cache DB. No shared SQLite file with the parent app.
License
MIT. See setup.py.