Repository navigation
Releases: BoringQuantSystems/nanobook
Release list
v0.19.2
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.mdand
rebalancer/README.mddescribe 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.mdstill said
nanobook = "0.10.0". They now saynanobook = "0.19", and the crates.io
page shows the new lines. test_recovery_time_typical_scenariostimed 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 beforereconstruct_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
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
andpython/README.mdnow 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
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,bbandsandatrcompute every name of a row-major panel in one
call; the*_by_columnvariants walk one name at a time. The Python binding
exposes thepy_*_panelfunctions and thesma_panelandema_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 shipLICENSE, and the Python
wheel recordsLicense-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
rustls0.23.42 → 0.23.45 (RUSTSEC-2026-0285, TLS 1.3 handshake
messages accepted across encryption levels), withaws-lc-rs1.18.1,
aws-lc-sys0.45.0 andrustls-webpki0.103.15.chacha20moves 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
Fixed
py_backtest_weightsraises 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
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.quantityisShares, noti64.- The Python
Position.quantitygetter returns a float. backtest_weights_with_optionstakes new optional fields on
BacktestBridgeOptions; existing calls keep compiling, and every new default
is a strict no-op.
Added
- Optional
fill_scheduleonbacktest_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/NextBarTypicalreadfill_schedule[i]instead
ofprice_schedule[i+1]; an empty entry means no fill for that period.
When omitted, behaviour is unchanged.SignalBarCloseignores it.
Changed
- BREAKING: position quantities are fractional.
Position.quantitybecomes
Shares, a newtype overi64counting micro-shares (1e-6), andPortfolio
gainsquantity_stepin the same unit —1_000_000for whole shares
(default),1_000for 0.001,1for 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
f64so results stay reproducible regardless of summation order.notional
goes throughi128before narrowing, since micro-shares of a large position
exceedi64. The order book is untouched —Quantity = u64still drives
matching, because a limit order book matches whole lots. - BREAKING (Python):
Position.quantityreturns a float. It called
Shares::whole(), so a position of 0.4 shares reported 0 and was invisible
from Python.quantity_microgives the exact integer andquantity_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 clippyran 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 the100_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 undertests/is its own crate root, so alib.rsattribute
cannot). Another 38 were machine-applied bycargo clippy --fix. The
remainder were dead-code warnings on shared test mocks:mock_binance.rs
andmock_tws.rsare 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 isscenariosand
indicatorsarriving in v0.17.0, plus a rendering change in newer
cargo-public-api. Regeneration is now a step inRELEASING.md, and
docs/public-api/README.mdrecords the tool version, since the output
format is not stable across releases of it. rebalancer killstates 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.mdmarks live execution on Windows unsupported.ibapimajor 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_filldeducted
an unaffordable buy withsaturating_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 largestquantity_stepmultiple whose notional plus
CostModel::compute_costfits 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_commissionexceeds cash plus proceeds is skipped whole — reducing it
would only make the floor worse. The buy-side trim is observable through
trimmed_fill_countandtrimmed_shortfall_centsso the portfolio cannot
quietly diverge from the strategy with nothing recording why. Additive
accessors only —new,with_quantity_step,rebalance_simple, and the
execute_fillcall 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
becausePortfoliohas no concept of dates and does not gain one. The bridge
has no dates either, so the caller suppliesperiod_day_ordinaland
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_countandlast_rebalance_notionalreport what a
rebalance actually executed, after every filter. Additive accessors rather
than a changedrebalance_simplesignature, which is published API. -
quantity_stepis settable from Python, so fractional sizing is
reachable through the bindings;Positionalso exposesquantity_micro
(exact) andquantity_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 — therand0.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 throughexp/lnwhose last ulp is
libm-dependent across platforms; any real stream change moves the numbers by
whole percent.
v0.17.2
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 fornanobook-broker,nanobook-riskand
nanobook-rebalancer: the 0.17.1 dependency sweep pulledtimeto 0.3.54
(viaibapiandtracing-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 letblocks inlevel,price_levels,stop,exchange,
backtest_bridgeand two examples are now let-chains, which 1.88 permits.
Machine-applied and semantically identical; the suite is unchanged at 1134
passing.
Fixed
nanobook-rebalancerdid not compile on Windows. The#[cfg(windows)]
arm of the kill switch opened withuse windows::Win32::System::Console::…
for symbols it never referenced, and thewindowscrate 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_submissionno longer calls the live Binance
API. It opened a real connection as setup, so it failed with
451 Unavailable For Legal Reasonson runners in regions Binance
restricts.submit_orderchecks 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-featureswithout--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
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
rustlsTLS backend now uses the OS trust store, not a bundled one.
reqwest0.13 renamed itsrustls-tlsfeature torustlsand changed what
it pulls in:rustls-platform-verifier(system trust anchors) andaws-lc-rs
replace the previously vendoredwebpki-rootsandring. The
nanobook-brokerfeature 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. Installca-certificates, or selectnative-tlsinstead. Only
affects builds with thebinancefeature enabled;nanobook-riskand
nanobook-rebalancerdo not enable it.
Changed
- MSRV raised from 1.85 to 1.86.
criterion0.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:
checkout7.0.1,
setup-python7.0.0,setup-uv9.0.0,action-gh-release3.0.2,
gh-action-pypi-publish1.14.1. Cargo:serde1.0.229,criterion0.8,
toml1.1,dialoguer0.12,nix0.31,signal-hook0.4,
tokio-tungstenite0.30,reqwest0.13, andsha20.11 withhmac0.13
(the digest 0.11 generation —new_from_slicemoved fromMacto
KeyInit). Binance request signing is unchanged; the known-signature vector
still matches. rand0.8 → 0.10 (withrand_chacha0.3 → 0.10 andrand_distr0.4 →
0.6). The only affected call site is theseed=Nonehelper 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:
nanobookandnanobook-pythonto 0.17.1,
nanobook-riskto 0.6.3,nanobook-rebalancerto 0.8.3.ibapistays 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
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_paritymarks golden-backed entries). No
runtime dependency on the TA-Lib C library; the remaining functions are classified and
explicitly deferred indocs/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 asmonte_carlo_stock_valuation_parityfor 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 insrc/scenarios.rsbehind thescenariosfeature. 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 forrandom.Randomseeds and no-numpy
environments. Seedocs/adr/0006-scenarios-rust-port-rng.md. -
Pure-Python Monte Carlo scenarios —
nanobook.scenariosexposes
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 intobacktest_weights.
Seepython/examples/scenario_backtest.py. -
IBKR resting STOP / STOP_LIMIT orders —
IbkrBroker.submit_orderaccepts a
new optionalstop_price_centsand routesorder_type="stop"/"stop_limit"
through ibapi'sstop/stop_limitorder 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
pyo3andnumpy0.27 → 0.29
(RUSTSEC-2026-0176, out-of-bounds read inPyList/PyTuplenth/nth_back;
RUSTSEC-2026-0177, missingSyncbound onPyCFunction::new_closure); bumped
quinn-prototo 0.11.16 (RUSTSEC-2026-0185, remote memory exhaustion) and
crossbeam-epochto 0.9.20 (RUSTSEC-2026-0204, invalid pointer dereference).
The pyo3 bump adds explicitfrom_py_objectopt-in to theClone-deriving
#[pyclass]types; behaviour is unchanged. Also cleared the two remaining
unsoundness advisories by bumpinganyhowto 1.0.104 (RUSTSEC-2026-0190,
Error::downcast_mut) andmemmap2to 0.9.11 (RUSTSEC-2026-0186, unchecked
pointer offset).cargo auditandcargo deny checkare both clean.
Fixed
- Python
__version__drift — the binding hardcoded0.9.1while the wheel
shipped as0.16.2. It is now taken from the crate manifest via
env!("CARGO_PKG_VERSION"), andtest_versionasserts it against the
installed distribution metadata so the two cannot diverge again. - CI toolchain and lints — pinned
dtolnay/rust-toolchainto the known-good
stable SHA, appliedcargo fmt --all, and resolvedclippy -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
Patch release. No behavior changes.
Fixed
- MSRV regression: v0.16.1 did not compile on Rust 1.85 despite
declaringrust-version = "1.85"—src/backtest_bridge.rsused a
let-chain, stabilized only in Rust 1.88. Rewritten as nestedif let. examples/itch-replay/download.shfailed on a fresh checkout (resolved
thedata/path bycd-ing into it before creating it).
Changed
- Repository moved to the BoringQuantSystems organization; all
repository/homepagemetadata and links updated. - CI: GitHub Actions pinned to commit SHAs, Dependabot enabled for
actions,ocaml/setup-ocamlbumped to v3, uv smoke jobs create a venv
before installing dependencies.
v0.16.1
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_valueoverflow (src/portfolio/position.rs:95):
self.quantity * pricepreviously panicked oni64overflow.
Replaced withsaturating_mul. Reachable via the public
Portfolio::rebalance_simpleAPI; crash artifact preserved at
fuzz/artifacts/fuzz_execute_fill/crash-330fb9097052e84d78571d06b840334680573582.Position::unrealized_pnloverflow (position.rs:104): same root
cause; both the subtraction and multiplication now saturate.Position::apply_filloverflow (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_mapoverflow (src/portfolio/mod.rs:430-435):
bare.sum()overpos.market_value(price)and bareself.cash + position_value
could panic when many large positions accumulated. Replaced withsaturating_*.
Found byfuzz_execute_fillre-run after the position.rs fix landed (crash artifact
crash-6bafbd4544cfff56e4ac3e2d68e18ebeecf6c2d7).Portfolio::rebalance_simplearithmetic (src/portfolio/mod.rs:238, 285, 346, 385):
defensivesaturating_*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::snapshottotal_realized_pnloverflow (src/portfolio/mod.rs:397)
[Inference]: bare.sum()overpos.realized_pnlfields — same pattern as the
total_equity_from_price_mapbug; replaced withsaturating_addfold. 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.