Testing
The whole suite runs offline — no API keys, no network, no alpaca import.
That's a direct benefit of the broker abstraction: tests
inject in-memory fakes where production injects Alpaca.
Running the tests
make test # the standard way (uv run --extra dev pytest)
make test installs the dev extra (pytest, ruff, scikit-learn, OR-Tools) and
runs the whole suite. To run pytest directly or narrow the run:
uv run --extra dev pytest # full suite, default verbosity
uv run --extra dev pytest -q # quiet
uv run --extra dev pytest tests/test_backtest.py # one file
uv run --extra dev pytest -k beta # tests matching "beta"
uv run --extra dev pytest tests/test_live_trader.py::test_hold_is_noop # one test
Notes:
- No setup needed — no keys or network. Tests use the fakes below.
- The OR-Tools portfolio tests
skipautomatically if that optional dependency isn't installed; everything else runs with just thedevextra. - CI runs the same command on every push/PR, alongside
ruff checkandruff format --check(see Coding standards).
Secret scanning
The repository is public, so CI also runs
gitleaks over everything reachable from the
pushed ref — not just the diff, because a secret deleted in a later commit is still
readable in the commit that added it. This is the one job that runs on every branch
push, not only on main and PRs, since a secret on a topic branch is public the
moment it is pushed. Reproduce it locally with:
make secret-scan # needs gitleaks installed (brew install gitleaks)
Credentials belong in .env (gitignored); .env.example carries placeholders only.
Lint & format
uv run ruff check . # lint
uv run ruff format . # auto-format (CI uses --check)
Test doubles (tests/fakes.py)
| Fake | Implements | Used for |
|---|---|---|
FakeMarketData | MarketDataProvider | deterministic synthetic OHLCV (random walk + volume spikes) |
DictMarketData | MarketDataProvider | serves exact fixture frames for precise engine assertions |
FakeBroker | Broker | records submitted orders / closes for execution tests |
What's covered
| File | Focus |
|---|---|
test_units.py | timeframe parsing, numeric helpers, metric primitives, parameter space |
test_strategy.py | sizing caps, exit conditions, signal validation, indicator columns |
test_scanner.py | volume-scanner signal logic, config validation, forward scoring |
test_backtest.py | engine fills — take-profit, stop-loss, signal exit, end-of-period P&L (exact) |
test_live_trader.py | entry → bracket order, dup-position skip, exit close, insufficient funds |
test_optimizer.py | grid/random ranking, max-evals cap, Bayesian path |
test_portfolio.py | cardinality, weight caps, score preference (skipped if OR-Tools absent) |
test_offline.py | full end-to-end backtest / scan / optimize on synthetic data |
A real bug the tests caught
grid_search originally materialized the entire parameter grid before capping —
billions of combinations for the bundled strategy, which OOM-killed the process
(exit 137). The optimizer test surfaced it immediately; the fix computes
grid_size() and samples instead. See Optimization.
Engine fill logic is asserted with a ScriptedStrategy that emits a fixed signal
per bar, so stop/take/exit/end-of-period behavior and P&L are checked exactly,
independent of any indicator quirk.