Skip to content

Releases: BoringQuantSystems/nanobook

v0.19.2

Choose a tag to compare

@github-actions github-actions released this 12 Oct 13:46
v0.19.2
6716796

nanobook and the Python package move from 0.19.1 to 0.19.2, nanobook-risk from
0.8.0 to 0.8.1, and nanobook-rebalancer from 0.10.0 to 0.10.1. nanobook-broker
does not change and is not republished. The library code and the public API do
not change.

Added

  • READMEs for nanobook-risk and nanobook-rebalancer. risk/README.md and
    rebalancer/README.md describe each crate and carry the disclaimer of the
    main README. The crates.io pages of both crates showed only the one-line
    package description and no disclaimer; they show the READMEs from this
    release on.

Fixed

  • Install snippets in the README. Three snippets in README.md still said
    nanobook = "0.10.0". They now say nanobook = "0.19", and the crates.io
    page shows the new lines.
  • test_recovery_time_typical_scenarios timed the file setup. The timer
    started before the test wrote its temporary audit log, so the 1 s limit also
    covered the file writes. A Windows CI runner took 1.55 s. The timer now starts
    just before reconstruct_state, and the 1 s limit is unchanged.
  • Changelog gaps. The changelog had no section for 0.15.2 and no compare
    links for 0.18.1 and 0.19.0. It now has both.

v0.19.1

Choose a tag to compare

@github-actions github-actions released this 10 Oct 17:40
v0.19.1
2f39fc1

nanobook-broker moves from 0.10.0 to 0.10.1 and the Python package from 0.19.0
to 0.19.1. nanobook-risk and nanobook-rebalancer do not change and are not
republished.

Added

  • Disclaimer on the broker and Python package pages. broker/README.md
    and python/README.md now carry the disclaimer that the main README gained
    in 0.19.0: no warranty, not financial advice, not a broker, and the broker
    adapters and the rebalancer send real orders at the user's own risk. The
    crates.io page of nanobook-broker and the PyPI page of nanobook show it from
    this release on. The code and the public API do not change.

v0.19.0

Choose a tag to compare

@github-actions github-actions released this 09 Oct 15:07
v0.19.0
574969a

The member crates move with the core because they depend on nanobook 0.19:
nanobook-broker 0.9.0 to 0.10.0, nanobook-risk 0.7.0 to 0.8.0,
nanobook-rebalancer 0.9.0 to 0.10.0.

Added

  • Panel indicators over a time-by-name close array. sma, ema, rsi,
    macd, bbands and atr compute every name of a row-major panel in one
    call; the *_by_column variants walk one name at a time. The Python binding
    exposes the py_*_panel functions and the sma_panel and ema_panel
    wrappers. Tests check each panel column against the one-dimensional
    indicator.
  • License text in every published package. nanobook-broker,
    nanobook-risk and nanobook-rebalancer now ship LICENSE, and the Python
    wheel records License-File: LICENSE. Earlier releases of those packages
    carried only the SPDX identifier.
  • README: a Disclaimer section. nanobook is not financial advice and not a
    broker; live orders are at the user's own risk.

Security

  • rustls 0.23.42 → 0.23.45 (RUSTSEC-2026-0285, TLS 1.3 handshake
    messages accepted across encryption levels), with aws-lc-rs 1.18.1,
    aws-lc-sys 0.45.0 and rustls-webpki 0.103.15. chacha20 moves from the
    yanked 0.10.1 to 0.10.2. This changes the lock file, so it reaches the Python
    wheels and release binaries; crates.io users resolve their own versions.

v0.18.1

Choose a tag to compare

@github-actions github-actions released this 17 Sep 13:32
v0.18.1
ff16be7

Fixed

  • py_backtest_weights raises when a still-open lot has no positive fill or
    close price.
    It no longer records a −100% week and then drops later
    returns. A true zero-equity bar still emits one return per period.

v0.18.0

Choose a tag to compare

@github-actions github-actions released this 05 Sep 08:21
v0.18.0
6521b03

The member crates move with the core because they now depend on nanobook 0.18
and its fractional Position.quantity: nanobook-broker 0.8.1 to 0.9.0,
nanobook-risk 0.6.4 to 0.7.0, nanobook-rebalancer 0.8.4 to 0.9.0. Their
published predecessors still pin nanobook 0.17.

Breaking (nanobook 0.17.2 → 0.18.0):

  • Position.quantity is Shares, not i64.
  • The Python Position.quantity getter returns a float.
  • backtest_weights_with_options takes new optional fields on
    BacktestBridgeOptions; existing calls keep compiling, and every new default
    is a strict no-op.

Added

  • Optional fill_schedule on backtest_weights / py_backtest_weights.
    Callers that decide on a coarse calendar (monthly rebalances) but want to
    fill on the real next trading bar can pass a parallel bar schedule. When
    supplied, NextBarOpen / NextBarTypical read fill_schedule[i] instead
    of price_schedule[i+1]; an empty entry means no fill for that period.
    When omitted, behaviour is unchanged. SignalBarClose ignores it.

Changed

  • BREAKING: position quantities are fractional. Position.quantity becomes
    Shares, a newtype over i64 counting micro-shares (1e-6), and Portfolio
    gains quantity_step in the same unit — 1_000_000 for whole shares
    (default), 1_000 for 0.001, 1 for effectively continuous. Whole shares
    were not a rounding detail at small size: $1,000 spread over 20 names priced
    near $219 rounds every target to zero and the portfolio holds nothing. A
    newtype rather than an alias so the compiler flags every call site instead of
    letting them silently mean something 1e6 different; fixed point rather than
    f64 so results stay reproducible regardless of summation order. notional
    goes through i128 before narrowing, since micro-shares of a large position
    exceed i64. The order book is untouched — Quantity = u64 still drives
    matching, because a limit order book matches whole lots.
  • BREAKING (Python): Position.quantity returns a float. It called
    Shares::whole(), so a position of 0.4 shares reported 0 and was invisible
    from Python. quantity_micro gives the exact integer and quantity_whole
    the old truncated value.
  • The monthly value budget is an exact cap, not an approximation. Each
    order was tightened against the budget remaining at the start of a period,
    so every order looked compliant alone while the period overspent — three
    positions each fitting under a $20,000 budget spent $30,000 of it. It is now
    a running sum over the priority-sorted orders. This matters because
    transaction volume relative to portfolio size is a criterion that can
    reclassify a private investor as a professional trader for tax, and a cap
    that can be exceeded by half again is not a cap.
  • CI now lints the whole workspace. cargo clippy ran without
    --workspace, so only the root crate was checked and broker, risk and
    rebalancer accumulated 141 unchecked findings. Clearing them was mostly
    mechanical: 46 were the 100_00 = $100.00 digit-grouping convention, which
    the root crate had declared crate-wide but the other two had not — now set
    once per crate in [lints.clippy], which reaches integration tests as well
    (each file under tests/ is its own crate root, so a lib.rs attribute
    cannot). Another 38 were machine-applied by cargo clippy --fix. The
    remainder were dead-code warnings on shared test mocks: mock_binance.rs
    and mock_tws.rs are each pulled into four test binaries, so a method one
    binary drives looks dead to the other three.
  • Public API baselines regenerated (docs/public-api/) — they were eight
    releases out of date. Nothing was removed; the growth is scenarios and
    indicators arriving in v0.17.0, plus a rendering change in newer
    cargo-public-api. Regeneration is now a step in RELEASING.md, and
    docs/public-api/README.md records the tool version, since the output
    format is not stable across releases of it.
  • rebalancer kill states plainly that it does nothing on Windows. It
    already refused there, but with a bare "not fully supported"; it now names
    the manual steps (taskkill, then cancel open orders at the broker) so an
    operator is not left assuming an emergency stop happened.
    docs/operations/kill-switch.md marks live execution on Windows unsupported.
  • ibapi major bumps are now ignored by dependabot, with the reasoning in
    .github/dependabot.yml. 3.x removes the arm that detects IBKR disconnects
    mid-execution; it needs its own change driven by the mock-TWS drills rather
    than arriving as a weekly PR. Minor and patch bumps still come through.

Fixed

  • Buys never spend cash the account does not have. execute_fill deducted
    an unaffordable buy with saturating_sub, so cash went negative and the
    portfolio held more than its equity — leverage financed by an overdraft no
    broker extends. On a $1,000 account (10 equal-weight names, US-equities-tiered
    costs) that stabilised around cash −$5.30 / invested 100.55%. Buys are now
    truncated to the largest quantity_step multiple whose notional plus
    CostModel::compute_cost fits in cash; a quantity that snaps to zero is
    skipped entirely (no zero-notional fill that would still charge
    min_commission). Sell quantities are never reduced, but a sell whose
    min_commission exceeds cash plus proceeds is skipped whole — reducing it
    would only make the floor worse. The buy-side trim is observable through
    trimmed_fill_count and trimmed_shortfall_cents so the portfolio cannot
    quietly diverge from the strategy with nothing recording why. Additive
    accessors only — new, with_quantity_step, rebalance_simple, and the
    execute_fill call sites keep their signatures.

Added

  • Execution constraints on Portfolio, as parameters rather than policy:
    no_trade_band_bps (leave a position alone until it is off target by more
    than this), min_order_value (never place an order below this notional),
    max_trades_per_rebalance, max_order_value (truncates the order rather
    than dropping it, the way a desk works a large order down) and
    max_rebalance_notional. Which values are right depends on the account and
    the venue, so they are something to sweep or solve for, not to hardcode.

    These exist because of what fractional shares uncovered. A portfolio with
    constant target weights and flat prices placed 360 orders across 36
    rebalances of 10 positions; the correct answer is 10, the initial buy. The
    loop is driven by commission itself — a rebalance pays commission, equity
    falls, every target value falls with it, so next period every position reads
    as marginally over target and gets corrected, which costs more commission.
    Whole-share rounding used to mask this as an accidental no-trade band. A band
    of one basis point brings it back to 10 orders, which is the measure of how
    little those other 350 were correcting.

    Defaults (0, 0.0, None) are a strict no-op and reproduce the previous
    behaviour exactly. When a cap binds, the surviving orders are those furthest
    from target with ties broken by symbol, so the result never depends on hash
    iteration order.

  • Windowed trade budgets on the backtest bridge: max_trades_per_day,
    max_trades_per_month, max_traded_value_per_month. They live in the bridge
    because Portfolio has no concept of dates and does not gain one. The bridge
    has no dates either, so the caller supplies period_day_ordinal and
    period_month_ordinal — plain monotonic integers. That keeps a calendar
    library out of this crate, since the only questions asked are "same day?" and
    "same month?", and it makes 24/7 venues fall out for free: several periods
    sharing a day ordinal simply share the daily budget. A budget is inert
    unless both the budget and its ordinal array are supplied — a cap can never
    be enforced against a calendar the caller did not provide, so absent ordinals
    fail open to no constraint rather than to a guessed one. Mismatched ordinal
    length is an error, not a silent truncation.

  • last_rebalance_order_count and last_rebalance_notional report what a
    rebalance actually executed, after every filter. Additive accessors rather
    than a changed rebalance_simple signature, which is published API.

  • quantity_step is settable from Python, so fractional sizing is
    reachable through the bindings; Position also exposes quantity_micro
    (exact) and quantity_whole (truncated).

  • Frozen reference test for the native ChaCha20 Monte Carlo path
    (native_mc_matches_frozen_reference). The existing reproducibility test
    compares two runs inside one build, so it passes whether or not an RNG
    upgrade moves the stream — the rand 0.8 → 0.10 bump in 0.17.1 had to be
    checked by hand. The comparison is relative to 1e-12 rather than bitwise,
    because the terminal math goes through exp/ln whose last ulp is
    libm-dependent across platforms; any real stream change moves the numbers by
    whole percent.

v0.17.2

Choose a tag to compare

@github-actions github-actions released this 27 Jul 16:40
v0.17.2
6b19a54

0.17.1 reached PyPI and the GitHub release but never reached crates.io: the
publish gate ran the workspace test suite and one test failed on the runner.
This release carries the same contents as 0.17.1 plus the fix, and is the
version to use. Crate versions: nanobook 0.17.2, nanobook-broker 0.8.1,
nanobook-risk 0.6.4, nanobook-rebalancer 0.8.4.

Changed

  • MSRV raised from 1.86 to 1.88 across all four crates. The 1.86 claim was
    already false for nanobook-broker, nanobook-risk and
    nanobook-rebalancer: the 0.17.1 dependency sweep pulled time to 0.3.54
    (via ibapi and tracing-appender), which requires 1.88. Declaring 1.88
    everywhere keeps one number across the workspace. Cargo's resolver is
    MSRV-aware, so a consumer on an older toolchain resolves to an earlier
    version rather than failing to build.
  • Nested if let blocks in level, price_levels, stop, exchange,
    backtest_bridge and two examples are now let-chains, which 1.88 permits.
    Machine-applied and semantically identical; the suite is unchanged at 1134
    passing.

Fixed

  • nanobook-rebalancer did not compile on Windows. The #[cfg(windows)]
    arm of the kill switch opened with use windows::Win32::System::Console::…
    for symbols it never referenced, and the windows crate was never declared
    as a dependency. The import is removed; the arm already returned "Kill
    switch not fully supported on Windows yet" unconditionally, so behaviour is
    unchanged. Every published version of this crate has been broken on Windows.
  • test_reconciliation_blocks_submission no longer calls the live Binance
    API.
    It opened a real connection as setup, so it failed with
    451 Unavailable For Legal Reasons on runners in regions Binance
    restricts. submit_order checks the reconciliation flag before it requires
    a client, so the connection was never needed for the assertion; the test
    still fails if that check order is ever reversed.
  • CI ran cargo test --all-features without --workspace, so it only
    ever tested the root crate — the broker, risk and rebalancer suites were
    invisible to it apart from three explicitly-named failure-injection drills.
    That is why a test making live network calls survived unnoticed. The test
    job now runs the whole workspace.

v0.17.1

Choose a tag to compare

@github-actions github-actions released this 26 Jul 18:51
v0.17.1
6aba0f1

A maintenance release. No public API changed in any crate, and no numerical
output moved. The one behavioural change is the TLS trust store used by the
optional Binance HTTPS client, which is why nanobook-broker takes a minor
bump while everything else is a patch.

Breaking (nanobook-broker 0.7.2 → 0.8.0):

  • The rustls TLS backend now uses the OS trust store, not a bundled one.
    reqwest 0.13 renamed its rustls-tls feature to rustls and changed what
    it pulls in: rustls-platform-verifier (system trust anchors) and aws-lc-rs
    replace the previously vendored webpki-roots and ring. The
    nanobook-broker feature names (rustls, native-tls) are unchanged, so no
    manifest edit is needed — but a minimal container image that ships no CA
    bundle will now fail the TLS handshake to Binance where it previously
    succeeded. Install ca-certificates, or select native-tls instead. Only
    affects builds with the binance feature enabled; nanobook-risk and
    nanobook-rebalancer do not enable it.

Changed

  • MSRV raised from 1.85 to 1.86. criterion 0.8 requires rustc 1.86, and
    holding the benchmark harness three majors back to preserve 1.85 was the worse
    trade. The library itself uses no 1.86-only features, so this is a floor
    change rather than a code change; let-chains still need 1.88 and remain
    written out longhand. Cargo's resolver has been MSRV-aware since 1.84, so a
    toolchain still on 1.85 resolves to 0.17.0 rather than failing to build.
  • Dependencies brought current — GitHub Actions: checkout 7.0.1,
    setup-python 7.0.0, setup-uv 9.0.0, action-gh-release 3.0.2,
    gh-action-pypi-publish 1.14.1. Cargo: serde 1.0.229, criterion 0.8,
    toml 1.1, dialoguer 0.12, nix 0.31, signal-hook 0.4,
    tokio-tungstenite 0.30, reqwest 0.13, and sha2 0.11 with hmac 0.13
    (the digest 0.11 generation — new_from_slice moved from Mac to
    KeyInit). Binance request signing is unchanged; the known-signature vector
    still matches.
  • rand 0.8 → 0.10 (with rand_chacha 0.3 → 0.10 and rand_distr 0.4 →
    0.6). The only affected call site is the seed=None helper in
    scenarios; seeded Monte Carlo output was compared before and after the
    upgrade for a fixed seed and is bit-identical to 17 significant digits.
  • Patch versions: nanobook and nanobook-python to 0.17.1,
    nanobook-risk to 0.6.3, nanobook-rebalancer to 0.8.3. ibapi stays on
    the 2.x line deliberately; the 3.x migration changes disconnect-detection
    semantics in live order placement and is scoped as its own change.

v0.17.0

Choose a tag to compare

@github-actions github-actions released this 25 Jul 06:23
v0.17.0
71fb372

Added

  • Technical-analysis indicators (TA-Lib parity) — 25 curated indicators implemented in
    pure Rust with golden parity against pinned TA-Lib outputs: trend and momentum (rsi,
    macd, stoch, stochf, stochrsi, willr, cci, roc, rocp, rocr, mom,
    ultosc), the directional family (adx, dx, plus_di, minus_di), volatility (atr,
    natr, trange, bbands), and volume (obv, ad, adosc). Wilder smoothing for the
    RSI/ATR/ADX family and population standard deviation for Bollinger bands. A JSON indicator
    registry drives golden generation alongside the Rust and Python parity tests, and
    list_supported() exposes the contract (has_parity marks golden-backed entries). No
    runtime dependency on the TA-Lib C library; the remaining functions are classified and
    explicitly deferred in docs/ta-lib-full-coverage-matrix.md. See
    docs/adr/0005-ta-indicators-scope.md.

  • Native ChaCha20 Monte Carlo hot path — the default Python monte_carlo_stock_valuation
    now draws with native Rust ChaCha20 and no NumPy RNG round-trip; the NumPy-bridge path is
    retained as monte_carlo_stock_valuation_parity for frozen audit and CI. Terminal prices
    return zero-copy as a NumPy array, and runs of 50k paths or more parallelise. See
    docs/adr/0007-scenarios-chacha20-hot-path.md.

  • Rust Monte Carlo scenarios — Core terminal valuation (simple GBM and advanced
    multi-driver) lives in src/scenarios.rs behind the scenarios feature. The
    Python extension draws with NumPy PCG64 and runs math/stats in Rust for frozen
    parity (rel_tol=1e-9). A native ChaCha20 Rust API is available for Rust-only
    callers. Pure-Python fallback remains for random.Random seeds and no-numpy
    environments. See docs/adr/0006-scenarios-rust-port-rng.md.

  • Pure-Python Monte Carlo scenarios — nanobook.scenarios exposes
    monte_carlo_stock_valuation, MonteCarloResult, and helpers with stdlib-only
    runtime (optional numpy acceleration when installed). Includes frozen numpy parity
    fixtures, hypothesis property tests, and an e2e path into backtest_weights.
    See python/examples/scenario_backtest.py.

  • IBKR resting STOP / STOP_LIMIT orders — IbkrBroker.submit_order accepts a
    new optional stop_price_cents and routes order_type="stop" / "stop_limit"
    through ibapi's stop / stop_limit order builders (aux_price = stop trigger,
    limit_price = limit for stop-limit). Previously only market/limit were encodable.
    Additive and backward-compatible: existing market/limit calls are unchanged and the
    new parameter is last and optional. Enables broker-resting protective stops.

Security

  • Dependency advisories cleared — upgraded pyo3 and numpy 0.27 → 0.29
    (RUSTSEC-2026-0176, out-of-bounds read in PyList/PyTuple nth/nth_back;
    RUSTSEC-2026-0177, missing Sync bound on PyCFunction::new_closure); bumped
    quinn-proto to 0.11.16 (RUSTSEC-2026-0185, remote memory exhaustion) and
    crossbeam-epoch to 0.9.20 (RUSTSEC-2026-0204, invalid pointer dereference).
    The pyo3 bump adds explicit from_py_object opt-in to the Clone-deriving
    #[pyclass] types; behaviour is unchanged. Also cleared the two remaining
    unsoundness advisories by bumping anyhow to 1.0.104 (RUSTSEC-2026-0190,
    Error::downcast_mut) and memmap2 to 0.9.11 (RUSTSEC-2026-0186, unchecked
    pointer offset). cargo audit and cargo deny check are both clean.

Fixed

  • Python __version__ drift — the binding hardcoded 0.9.1 while the wheel
    shipped as 0.16.2. It is now taken from the crate manifest via
    env!("CARGO_PKG_VERSION"), and test_version asserts it against the
    installed distribution metadata so the two cannot diverge again.
  • CI toolchain and lints — pinned dtolnay/rust-toolchain to the known-good
    stable SHA, applied cargo fmt --all, and resolved clippy -D warnings
    (manual memcpy, range/counter loops) in the indicators module. Bumped the
    pinned GitHub Actions (setup-python, upload-artifact, setup-uv, codecov).

v0.16.2

Choose a tag to compare

@github-actions github-actions released this 11 Jun 16:59
v0.16.2
def3222

Patch release. No behavior changes.

Fixed

  • MSRV regression: v0.16.1 did not compile on Rust 1.85 despite
    declaring rust-version = "1.85" — src/backtest_bridge.rs used a
    let-chain, stabilized only in Rust 1.88. Rewritten as nested if let.
  • examples/itch-replay/download.sh failed on a fresh checkout (resolved
    the data/ path by cd-ing into it before creating it).

Changed

  • Repository moved to the BoringQuantSystems organization; all
    repository/homepage metadata and links updated.
  • CI: GitHub Actions pinned to commit SHAs, Dependabot enabled for
    actions, ocaml/setup-ocaml bumped to v3, uv smoke jobs create a venv
    before installing dependencies.

v0.16.1

Choose a tag to compare

@github-actions github-actions released this 19 May 22:29
v0.16.1
f404e8b

Patch release. Replaces bare integer arithmetic in Position with
saturating variants, matching the pattern already used in
Portfolio::execute_fill. Found by fuzz_execute_fill (added in v0.16.0
as part of the property-based testing pass after the v0.16.0 ship).

Fixed

  • Position::market_value overflow (src/portfolio/position.rs:95):
    self.quantity * price previously panicked on i64 overflow.
    Replaced with saturating_mul. Reachable via the public
    Portfolio::rebalance_simple API; crash artifact preserved at
    fuzz/artifacts/fuzz_execute_fill/crash-330fb9097052e84d78571d06b840334680573582.
  • Position::unrealized_pnl overflow (position.rs:104): same root
    cause; both the subtraction and multiplication now saturate.
  • Position::apply_fill overflow (lines 55, 58, 69, 80, 87): all
    intermediate products/sums switched to saturating variants. Real-world
    trigger requires extreme quantity × price products; bug was latent
    pre-v0.16.0 but only surfaced after the fuzz harness was added.
  • Portfolio::total_equity_from_price_map overflow (src/portfolio/mod.rs:430-435):
    bare .sum() over pos.market_value(price) and bare self.cash + position_value
    could panic when many large positions accumulated. Replaced with saturating_*.
    Found by fuzz_execute_fill re-run after the position.rs fix landed (crash artifact
    crash-6bafbd4544cfff56e4ac3e2d68e18ebeecf6c2d7).
  • Portfolio::rebalance_simple arithmetic (src/portfolio/mod.rs:238, 285, 346, 385):
    defensive saturating_* replacements at five other i64 arithmetic sites in
    the rebalance / equity / price-mid paths. Each was a latent overflow risk
    reachable through the public API under extreme inputs.
  • Portfolio::snapshot total_realized_pnl overflow (src/portfolio/mod.rs:397)
    [Inference]: bare .sum() over pos.realized_pnl fields — same pattern as the
    total_equity_from_price_map bug; replaced with saturating_add fold. Not in the
    original fuzz crash path but reachable from the same extreme-input scenarios.

Added

  • tests/regression_position_overflow.rs: integration tests that
    assert each previously-crashing input sequence now returns a saturated
    i64 instead of panicking. Replays the libfuzzer crash artifact by hand.