Coding standards
These are the conventions the codebase follows. CI enforces the mechanical ones (lint, format, tests); the rest are reviewed in PRs.
Tooling
- uv for dependencies and running everything.
- ruff for both linting and formatting.
- Lint rules:
E,W(pycodestyle),F(pyflakes),I(import sorting). - Line length 110; the formatter owns wrapping (so
E501is not linted). - Run locally:
uv run ruff check .anduv run ruff format ..
- Lint rules:
- pytest for tests:
make test(offline).
CI (.github/workflows/ci.yml) runs lint, format-check, and tests on every push
and PR.
Architecture rules
These keep the layering honest — they matter more than any style nit:
- Dependencies point downward. A layer may import from layers below it, never above. See Architecture.
- No vendor SDK above the broker layer. Only
src/brokers/<vendor>/importsalpaca(or any other broker SDK). Everything else uses theBroker/MarketDataProviderinterfaces. See Broker abstraction. - One concern per module. Signals, sizing, fills, execution, metrics, and data access live in different layers. See Separation of concerns.
- Heavy/optional dependencies are lazy-imported behind extras (scikit-learn for Bayesian optimization, OR-Tools for portfolio allocation), so the base install stays small.
Conventions
- Type hints on public functions, methods, and dataclasses.
- Docstrings on modules and public classes/methods — explain why, not just what. Note explicitly what a class is not responsible for when it clarifies a boundary.
- Domain types are vendor-neutral dataclasses/enums (
Position,OrderSide,BarEvent, ...); adapters map SDK objects to them. - Shared vocabulary lives in one place — e.g. trade signals in
strategies/signals.py, scan signals inscanners/base.py. Never hard-code a signal string in two layers. - Indicators are pure pandas/numpy (
src/indicators). No TA-Lib or other compiled dependency in the base install. - Reuse the shared helpers (
utils/numeric,utils/timeutils,utils/streaming,analytics/metrics) rather than re-deriving the same logic.
Error handling
- Real-time handlers never raise —
process_barand stream callbacks log and continue so a single bad bar can't kill a live session. - Streams reconnect, they don't crash — via
utils/streaming.run_with_reconnect. - Fail fast on configuration — invalid parameters raise at construction
(
Strategy._validate_parameters), not mid-run. - Catch-all
except Exceptionis acceptable only at stream/loop boundaries, and is annotated# noqa: BLE001with a reason.
Tests
- Offline and deterministic. Use the in-memory fakes (
tests/fakes.py); seed any randomness. No network or API keys. - One behaviour per test, named for what it asserts.
- Cover the boundary you changed: a new sizer, scanner, or broker gets a test through its interface.