docs/developer_guide/coding_standards.md
The current codebase can be used as a guide for formatting conventions. Additional guidelines are provided below.
The following applies to all source files (Rust, Python, Cython, shell, etc.):
color, serialize, behavior).Shell scripts in this repository use bash (not POSIX sh) and must be portable across Linux and macOS. User-facing scripts (e.g., scripts/cli/install.sh) must also work on Windows via Git Bash or WSL.
Shebang: Always use #!/usr/bin/env bash for portability.
Common pitfalls: GNU and BSD utilities differ between Linux and macOS:
| Command | Linux (GNU) | macOS (BSD) | Portable solution |
|---|---|---|---|
sed -i | sed -i 's/…' | sed -i '' 's/…' | Use backup extension: sed -i.bak 's/…' |
stat (file size) | stat -c%s file | stat -f%z file | Detect with stat --version |
sha256sum | sha256sum file | N/A | Use shasum -a 256 or detect |
readlink -f | Works | N/A | Avoid, or use realpath |
grep -P (PCRE) | Works | N/A | Use -E (extended regex) instead |
date (nanoseconds) | date +%N | N/A | Use $RANDOM for cache‑busting |
Bash version: macOS ships with bash 3.2; avoid bash 4+ features in user-facing scripts:
| Feature | Bash version | Alternative |
|---|---|---|
Associative arrays (declare -A) | 4.0+ | Use files or simple arrays |
readarray / mapfile | 4.0+ | Use while read loops |
${var,,} / ${var^^} (case) | 4.0+ | Use tr '[:upper:]' '[:lower:]' |
CI scripts (scripts/ci/*) run on Linux runners, so bash 4+ and GNU tools are acceptable there.
Rust doc comments should be written in the indicative mood - e.g. "Returns a cached client."
This convention aligns with the prevailing style of the Rust ecosystem and makes generated documentation feel natural to end-users.
Error messages: Avoid using ", got" in error messages. Use more descriptive alternatives like ", was", ", received", or ", found" depending on context.
"Expected string, got {type(value)}""Expected string, was {type(value)}"Spelling: Use "hardcoded" (single word) rather than "hard-coded" or "hard coded" - this is the more modern and accepted spelling.
Error variable naming: Use single-letter e for caught errors/exceptions:
Err(e) not Err(err) or Err(error), and |e| not |err| in closuresexcept SomeError as e: not as err: or as error:Internal fields: Abbreviations are acceptable for private/internal fields (e.g., _price_prec, _size_prec) to keep hot-path code concise.
User-facing API: Use full, descriptive names for public properties, function parameters, return types, and metric names/labels (e.g., price_precision, size_precision). This prevents abbreviated terminology from leaking into dashboards or alerts.
Error messages and logs: Use full words for clarity (e.g., "price precision" not "price prec"). The user should never see abbreviated terminology.
Use free functions for stateless data ingestion. Use a class only when instances retain reusable configuration, caches, workers, iteration state, or an open resource across calls. Do not use a zero-state class solely to group static or class methods.
Follow the semantic distinction in the
Polars I/O API, adapted to the
established Nautilus load_* vocabulary:
load_<source>_<data> eagerly reads, normalizes, and materializes a complete result.scan_<source>_<data> creates a lazy query or deferred execution plan.stream_<source>_<data> incrementally yields records or batches.write_<format> eagerly writes an in-memory result.sink_<format> writes through a lazy or streaming execution path.For ingestion, order names from general to specific: verb, source, logical data, then an optional
representation. Include the representation only when real sibling formats exist. For example,
load_binance_order_book_deltas is preferable to a stateless
BinanceOrderBookDeltaDataLoader.load class or a generic load_binance_data function.
Each package under nautilus_trader/adapters/ is a thin facade over the private
_libnautilus extension. Every adapter __init__.py declares a deterministic __all__ that is
the single source of truth for its public API; python/generate_stubs.py copies that list into the
matching .pyi so runtime and stub exports agree exactly.
A venue adapter exposes its canonical identity constants plus the supported public surface:
<VENUE>, <VENUE>_CLIENT_ID, <VENUE>_VENUE, registered from Rust via m.add in the adapter's
python/mod.rs*Config, *Factory, user-facing enums (such as *Environment and *ProductType)load_*, stream_*, convert_*) and intentional utilities
(decode_*, get_*_arrow_schema_map)Keep the facade thin. Never add raw HTTP or WebSocket clients, wire models, endpoint helpers
(get_*_url, *_HTTP_URL), caches, or other internals to __all__ merely for structural parity.
Data providers (such as databento and tardis), the blockchain data client, the sandbox
execution client, and the multi-venue interactive_brokers broker omit venue constants because the
constants would be meaningless for them.
Order __all__ entries so the RUF022 pre-commit gate owns sort order; do not hand-order the list.
For longer lines of code, and when passing more than a couple of arguments, you should take a new line which aligns at the next logical indent (rather than attempting a hanging 'vanity' alignment off an opening parenthesis). This practice conserves space to the right, keeps important code more central in view, and survives function/method name changes.
The closing parenthesis should be located on a new line, aligned at the logical indent.
Multiple hanging parameters or arguments should end with a trailing comma:
long_method_with_many_params(
some_arg1,
some_arg2,
some_arg3, # <-- trailing comma
)
Commit messages use a capitalized, imperative subject naming the affected surface, optionally followed by a body explaining the change. Gitlint encodes the length limits and some formatting rules. It is opt-in today but may be enforced in CI later, so write messages that pass it now.
Add, Fix, Improve, Refine, Update, Remove, Refactor, and Standardize cover most of the history.title-min-length and title-max-length).Add Decimal constructors to Instrument trait
Fix non-atomic order event application
Refine cross-platform wheel validation
Remove stale security audit exceptions
Avoid these shapes:
feat(bybit): add due_post_only flag # Conventional Commits type and scope
fix: bug # lowercase, unspecific, too short
Fixed the Bybit post-only rejection flag. # past tense, trailing period
Update stuff # says nothing about the surface
Do NOT use Conventional Commits syntax for commit messages or pull request titles. Many editors and AI assistants emit that format by default, but no commit in this repository's history uses it, and the type and scope ceremony duplicates what the subject already carries. Pull request titles matter here too, because a squash merge turns the PR title into the commit subject.
The body is optional and gitlint does not require one, but anything beyond a trivial change should say why the change was made rather than restate the diff.
body-max-line-length).Resolves #4534 when the commit closes the
issue, or Related to #4547 when it is partial work.Fix TWAP child-order sizing and interval validation (#4544). Do not add that suffix by hand.(#1234) (ignore-by-title), which is why squash-merged
subjects in the log can exceed 60 characters.Gitlint is available to help enforce commit message standards automatically. It checks length limits and formatting such as trailing punctuation, not the choice of verb, capitalization, or surface. This is opt-in and not enforced in CI.
Benefits: Encourages concise yet expressive commit messages, helps develop clear explanations of changes.
Installation: First install gitlint to run it locally:
uv pip install gitlint
To enable gitlint as an automatic commit-msg hook:
prek install --hook-type commit-msg
Manual usage: Check your last commit message:
gitlint
Configuration is in .gitlint at the repository root:
body-is-missing and body-min-length are ignored, so short commits need no body.:::note Gitlint may be enforced in CI in the future, so adopting these practices early eases the transition. :::