CLI stock screener for TSX/TSXV — Greenblatt-style rank-based screening
Find a file
2026-07-10 18:47:19 -04:00
screener initial commit: CLI stock screener for TSX/TSXV 2026-07-10 18:47:19 -04:00
.gitignore initial commit: CLI stock screener for TSX/TSXV 2026-07-10 18:47:19 -04:00
README.md initial commit: CLI stock screener for TSX/TSXV 2026-07-10 18:47:19 -04:00
requirements.txt initial commit: CLI stock screener for TSX/TSXV 2026-07-10 18:47:19 -04:00
setup.py initial commit: CLI stock screener for TSX/TSXV 2026-07-10 18:47:19 -04:00

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

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. Reads CACHE_DB_PATH, FMP_API_KEY, OPENAI_API_KEY, BRAVE_API_KEY from environment or .env.
  • preset.PresetStore — CRUD over presets table in the same SQLite DB as the cache. Stores factor pair identifier and per-factor weights.
  • screener.ScreenerService — the heart of the system:
    1. Fetch TSX/TSXV ticker list (cached 1h, pre-filtered to drop ETFs/REITs)
    2. Filter to market-cap range via yfinance metadata
    3. Fetch fundamentals concurrently (MAX_CONCURRENT_FETCHES = 20, 0.5s delay between calls to stay under Yahoo rate limits)
    4. Apply preset-specific filters (e.g. roic_fcf requires positive FCF)
    5. Fetch 6-mo momentum only if the preset uses it (and only for survivors)
    6. Rank within each factor (ROIC/EV-FCF/EY/EBIT-Assets/Mom/Growth)
    7. 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__.py Flask factory is gone. config.py no longer references SECRET_KEY for signing (kept as None for 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 importlib to bypass Flask-triggering __init__.py files.
  • No USD→CAD conversion. The original Flask routes/screener.py used get_usd_to_cad() for template display. The CLI uses raw market_cap values directly. A stub get_usd_to_cad() returning 1.35 is kept in config.py for compatibility.
  • Self-contained cache DB. No shared SQLite file with the parent app.

License

MIT. See setup.py.