Format: Keep a Changelog. Every entry below describes a customer-visible change to the Reamer Research product — the C ABI, the shipped reference material, or the kit contents — not internal refactors. Updated at release, alongside the tag.
Version numbers here are the release version (kReamerVersion), which is
shared with Reamer Server and moves independently of REAMER_ABI_VERSION.
ABI bumps are called out explicitly where they occur.
[4.5.0] - 2026-10-09
ABI bump: REAMER_ABI_VERSION 6 → 7. Breaking. Rebuild strategies
against the ABI 7 header.
Added
on_barreceivesposition_states(oneReamerPositionStateper ticker: signedqty,avg_entry_price,take_profit,stop_loss,unrealized_pnl,entry_ts) andopen_orders/open_order_count(every resting order as aReamerOpenOrder, sorted byorder_id). A strategy can now cancel its own resting order by theorder_idit reads here.on_barwrites order and position actions toout_actions(ReamerOrderAction, up to 64 per call):MODIFY_ORDERre-prices a resting order and replaces its brackets,MODIFY_POSITIONreplaces a position's take-profit and stop-loss,CLOSE_ALLcloses every position at market,CANCEL_ALLcancels every resting order. Modify and cancel-all take effect from the next bar; a stop may move to breakeven or into profit. Each action reportsappliedandreject_reason.EXECUTION_SPEC.md§6b has the timing and validation.- Optional vtable slot
on_progress(user_data, steps_done, steps_total). The engine calls it between bars, at most once per 256 bars and 100 ms, plus a final call withsteps_done == steps_total. Return nonzero to stop the run:reamer_run_backtest()andreamer_run_backtest_files()returnREAMER_RESEARCH_ERROR_CANCELLED(11) and no handle. NULL means no calls. A run's result is the same with or without it. - Result JSON
order_logrecords each applied modification with status"Modified": the order's snapshot under its ownid, orid0 for a position's brackets. - reference-cpp passes the new arrays to the strategy function as spans,
plus
std::span<ReamerOrderAction>and asize_t&action count. reference-python exposes them asdata.position_state(ticker),data.open_orders()anddata.action_results();engine/actions.pybuilds the four actions, and a strategy'son_progress(done, total)method, if defined, receives progress calls and stops the run by returning a truthy value. - Strategy relay to Reamer Server:
reamer_relay_open(),reamer_relay_send(),reamer_relay_poll(),reamer_relay_get_positions(),reamer_relay_is_connected(),reamer_relay_close()andReamerRelayHandle. Sends the sameReamerOrderRequesta strategy returns fromon_barto a running server over its local-mode strategy socket, with backtest order IDs and submission rules, and reads fills back asReamerPositionState.take_profitandstop_losstravel as Reamer Server ABI 4 order fields; the server tracks the exits as<order_id>:tpand<order_id>:sl, your broker connector places them at the venue, and their fills move the reported position. Brackets on a close (is_close) are rejected: they attach to an entry. New error codeREAMER_RESEARCH_ERROR_RELAY(12). Linux and macOS.RESEARCH_TO_SERVER.mdhas a one-order round trip against the reference gate. The gate and the broker connector remain yours to build.
Changed
on_bartakes three new parameters beforeout_orders:const ReamerPositionState* position_states,const ReamerOpenOrder* open_orders,size_t open_order_count, and three aftermax_orders:ReamerOrderAction* out_actions,size_t max_actions,size_t* out_action_count. The vtable becomes{user_data, on_bar, on_progress}(24 bytes).- A cancel request (
is_cancel) takes effect after the current bar replays, not before. In 4.4.0 a strategy that had seen bar N could cancel an order bar N would have filled. Runs that cancel orders can now differ from 4.4.0: an order that fills on the bar the strategy saw stays filled.EXECUTION_SPEC.md§6. - Result JSON
schema_version1 → 2:order_logidrepeats on"Modified"records.summary.total_ordersdoes not count them. - A strategy that writes no actions produces the same result JSON as 4.4.0,
byte for byte, apart from
abi_versionandschema_version. - Cost: about 1-2%.
research-benchP50 is 1.98–2.00 µs peron_barcall against 1.96 µs on 4.4.0 (same machine, same build flags, steady runs).
[4.4.0] - 2026-10-09
ABI bump: REAMER_ABI_VERSION 5 → 6. Breaking. Rebuild against the ABI 6
header.
Removed
reamer_research_run_backtest()(v1) andreamer_research_run_backtest_v2().- The 7-field
ReamerBacktestSummaryandreamer_research_get_summary(). - Vtable slots
on_bar(v1),on_bar_v2andon_bar_v3. - The
ticker_ids[]parameter ofreamer_run_backtest(). A ticker'sticker_idis its array position. REAMER_RESEARCH_ERROR_INVALID_TICKER_ID(3). Other codes keep their values.
Changed
- Every exported function is renamed from
reamer_research_*toreamer_*.reamer_research_run_backtest_v3()is nowreamer_run_backtest(), andreamer_research_get_summary_v2()is nowreamer_get_summary(). ReamerBacktestSummaryV2is renamedReamerBacktestSummary(31 metrics, 248 bytes).reamer_get_summary(handle, periods_per_year, out)takes the Sharpe and Sortino annualisation (252 daily, 52 weekly).<= 0infers it from closed-trade timestamps, as before. reference-cpp'ssummary()and reference-python'speriods_per_yearconfig key pass it through.- Docs:
calmar_ratioistotal_return_pctover relative drawdown and is not annualised. The header andRESULT_JSON_SCHEMA.mdsaid it was. ReamerResearchStrategyVtableis{user_data, on_bar}(16 bytes).on_bartakes the formeron_bar_v4signature:window,window_valid,window_len,ticker_count,positions,exogenous,exogenous_len,out_orders,max_orders.- All 11 exports sit in one version node,
REAMER_RESEARCH_2.0. The 1.0-1.3 nodes are gone. - reference-cpp:
run_backtest<>()takes a requiredticker_namesargument and noticker_ids; the callable takes(window, ticker_count, positions, window_valid, out_orders).run_backtest_v2<>()is removed. - reference-python: the ctypes mirrors follow the new vtable and summary.
on_bardocumentation: a forward-filled row keeps the last real bar'sts, so per-ticker staleness is readable from the window.
Added
reamer_run_backtest_files(): run a backtest over memory-mapped.binfiles, one per ticker, with an optional inclusivestart_ts/end_tsrange. Datasets larger than RAM run. Results are identical toreamer_run_backtest()on the same bars.reamer_write_bin(): write validated, sorted bars to a.binfile.REAMER_RESEARCH_ERROR_BAR_FILE(10): a.binfile is missing, unreadable, lacks its header, is truncated, or is unsorted.bin/reamer-csv-build: converts an OHLCV CSV into a.binfile forreamer_run_backtest_files(). Detects delimiter, columns and timestamp format; prints a parse report (skipped rows, duplicate timestamps, first and last timestamp). Needs no license.reamer_run_monte_carlo(): resamples a completed run's per-trade returns with replacement and reports final-equity, loss, ruin and max-drawdown percentile stats inReamerMonteCarloStats. Seeded, and bit-identical for any thread count. A run with fewer than two closed trades returns all zeros. License-gated.- Result JSON
equity_curveandequity_curve_ts: realised equity after each closed trade, with timestamps. It is the curveequity_drawdown_maximalis measured on, floored at 0.schema_versionstays 1 (fields added).
Fixed
- Result JSON
open_orders_end[]was always empty. An order still pending at the end of a run now appears there asPending, and inorder_log[]asCancelled. reamer_run_backtest(): a ticker passed with zero bars no longer shifts every later ticker down one asset index. Before the fix,on_bar'sticker_id,positions[]and closed-trade names for later tickers pointed at the wrong instrument.
[4.3.2] - 2026-10-03
No change to the C ABI or the Reamer Research binary behaviour. Released
alongside a Reamer Server fix to the license.expiring warning schedule.
Added
LICENSING.md"Renew on the same machine": deactivate the old key, then activate the new one.
[4.3.1] - 2026-09-17
No change to the C ABI. REAMER_ABI_VERSION stays at 5, every entry point and
struct is untouched, and a program compiled against an earlier header links
and runs against this build unmodified. The version moves with the shared
release number (kReamerVersion, one number across both products); the
substantive documentation change this release is on the server side (see the
Reamer Server changelog).
Changed
- The written ABI versioning policy is clarified. The advance-notice and deprecation sections are resolved to the actual policy: support the latest version only, no sunsetting because the surface is additive-only, and a version bump is communicated through the changelog at release. The document is also rewritten in plain technical english with no em or en dashes.
[4.3.0] - 2026-09-13
No change to the C ABI. REAMER_ABI_VERSION stays at 5, every entry point and
struct is untouched, and a program compiled against an earlier header links
and runs against this build unmodified. This release carries new benchmark
evidence and the documentation built on it; the version moves with the shared
release number (kReamerVersion, one number across both products).
Changed
BENCHMARK.mdnow includes a many-core verification section measured on a 64-core AMD EPYC 9575F. The pure-ABI native engine held a per-call latency tail essentially flat across 2000 independent backtests (P99.9 within ~3% of P50) with per-run throughput CV 0.59% (1.04× min→max spread) — the quantitative form of the determinism claim. A stress/capacity pass on the same box reported no bottlenecks, ingesting a 50-million-bar universe (10,000 tickers × 20 years) without failure.
[4.2.1] - 2026-09-12
No change to the C ABI. REAMER_ABI_VERSION stays at 5, every entry point and
struct is untouched, and a program compiled against 4.1.15 links and runs
against this build unmodified. The fix is entirely inside the shipped Python
reference binding; the library and every other reference tree are unchanged.
Fixed
Multi-ticker strategies in the Python reference binding now read each ticker's own bars instead of aliasing every ticker to the first. The ctypes binding hands
on_barone_TickerViewper instrument over the flat, row-major(ticker_count, window_len)window the ABI delivers. Every view was constructed on the same base pointer, so all tickers reported ticker 0's OHLCV; a strategy reading two instruments saw the first one twice. The binding now offsets each view byticker_id * window_leninto the window before constructing it, so each ticker reads its own row. Single ticker runs were unaffected (offset 0), which is why this survived to now.This is a read-side view bug only. The engine's execution path — tick simulation between bars, the synthetic-tick PRNG, spread and slippage randomization, and per-ticker wall-clock alignment of asynchronous bars — was never involved and is unchanged. Fills, results, and RNG draws for any run that did not read a multi-ticker window are byte-identical to 4.2.0.
No change to the C ABI. REAMER_ABI_VERSION stays at 5, every entry point and
struct is untouched, and a program compiled against 4.1.15 links and runs
against this build unmodified.
Fixed
Malformed orders are now rejected at the ABI boundary instead of reaching the engine. Order requests are validated in
ToEngineOrderbefore any field is cast: the four enums (order_type,kind,side,tif) are range-checked, andqty,limit_price,stop_price,take_profitandstop_lossmust be finite. A failing order is rejected with a reason naming the field —qty is not finite,side out of range— through the existing rejection path.This closes two paths that previously produced wrong answers rather than errors. A non-finite quantity on a position reversal bypassed the margin check entirely and filled: the run returned success with a NaN
net_pnland no indication anything had gone wrong. An out-of-range enum matched no comparison in the engine, so the order silently never became fill-eligible and sat pending for the life of the run.Inbound bars were already checked for finiteness at ingest; orders crossing the other way were not. Runs remain byte-identical for identical input: rejection consumes no RNG.
Added
MONITORING.mdships in the kit. For running backtests unattended: what the library writes to stderr and at what volume, the error codes worth alerting on, how to tell a run that failed from a run whose orders were all rejected (a distinction exit status does not make), what to capture for a support report, and what changes when running a grid.reference-cpp/examples/rejected_orders.cpp. A worked example of reading rejections out of a result.summary()andclosed_trades()report what a strategy achieved; only the order log says what the engine refused, and it reaches callers through the result JSON.BacktestResult::result_json()in the reference C++ wrapper, exposingreamer_research_get_result_json(). The full result document — order log, rejection reasons, thereturns[]series — was previously unreachable through the wrapper.
Changed
- The vtable's callback contract is written down.
reamer_research_abi.hnow states what happens when a callback throws (it is caught at the ABI entry point and reported asREAMER_RESEARCH_ERROR_PROCESS_FATAL, but the run ends — there is no per-bar recovery), that callback execution time is deliberately unbounded and uninterruptible, and thatuser_datais passed through untouched.DEPLOYMENT.mdcovers the same ground for operators.
[4.1.13] - 2026-09-05
No change to the C ABI. REAMER_ABI_VERSION stays at 5, every entry point
and struct is untouched, and a program compiled against 4.1.12 links and
runs against this build unmodified.
Added
LICENSE.txtships in the kit. The archive previously carried no licence terms at all. The file sits in the kit root and is covered bySHA256SUMS.Activation requires accepting the terms.
reamer-license activatenow displaysLICENSE.txtand requires an explicitacceptbefore it contacts the licence server, converting activation from browsewrap to clickwrap. Declining consumes no seat, and the acceptance record — key, machine fingerprint, UTC timestamp, licence version, and the SHA-256 of the accepted text — is written next to the activation record only after activation succeeds. Amended terms hash differently and prompt again.For unattended provisioning set
REAMER_ACCEPT_LICENSE=accept. Activation with no terminal and no such variable fails rather than defaulting to acceptance, so existing non-interactive provisioning that activates a licence must set it.reamer-licensereadsLICENSE.txtfrom the kit root, one level abovebin/. Movingbin/out of the extracted tree makes activation fail with a message naming the cause.
[4.1.12] - 2026-09-02
REAMER_ABI_VERSION 4 → 5. Purely additive. Four new exported
functions and one new
optional vtable slot; the library now exports eleven symbols across three
version-script nodes (REAMER_RESEARCH_1.0 / 1.1 / 1.2). Every
pre-existing entry point and struct is untouched, so a program compiled
against ABI 1-4 links and runs against this build unmodified. No
recompilation is forced by this release.
This bump exposes three capabilities that were already implemented in the engine but unreachable through the ABI, which is the product.
Added
reamer_research_run_backtest_v3()— the entry point to write new integrations against. Adds per-ticker execution config, mandatory instrument names, and exogenous data sidecars in one signature.Per-ticker execution config. A sparse
ticker_configs[]array applies distinct commission, slippage, spread, and swap terms per instrument within a single run. A NULL entry falls through to the global config. Resolution order is specified inEXECUTION_SPEC.md§0.rng_seedis deliberately excluded from override: it is forced from the global config when building each resolved entry, so differing costs between instruments can never introduce cost-correlated randomness into the synthetic tick path. Enforced in code, not only documented.Mandatory
ticker_names[]. A NULL array is rejected at this entry point. Consequently every instrument name in a v3 run's results —ReamerClosedTrade.tickerand everytickerfield in the result JSON — is the caller's own symbol, never the synthetic"T%09zu"placeholder the older entry points fall back to. This makes the research→server instrument-identity mapping checkable rather than merely required; seeRESEARCH_TO_SERVER.md§2.Exogenous data sidecars. An optional
exogenous_paths[]array attaches a non-OHLCV series per instrument. Delivered toon_bar_v4.on_bar_v4vtable slot. Extendson_bar_v3with per-ticker exogenous bytes and lengths. The vtable grows 32 → 40 bytes, which is safe only because the caller passes it by pointer, the library reads no slot it does not know about, and the header has mandated zero-initialization since ABI 2. Dispatch falls backon_bar_v4 → v3 → v2 → on_bar, so a strategy that sets onlyon_bargets exactly v1 behavior.Resolution is strictly point-in-time. At each bar the engine resolves the latest entry with
ts <= current_bar_tsand never a later one; a per-ticker cursor advances forward only, keeping this O(1) amortised rather than a per-bar search. Where no sidecar is attached or no entry qualifies yet, the strategy receives NULL and length 0. The bytes are engine-owned, valid only for the duration of the call, and are never parsed or validated by the engine.reamer_research_get_summary_v2()andReamerBacktestSummaryV2— 31 metrics whereget_summary()reported 7: Sharpe, Sortino, Calmar, profit factor, recovery factor, expectancy, win rate, consecutive win/loss streaks, holding-time statistics, absolute and relative drawdown, and full order-outcome counts.ReamerBacktestSummaryis frozen at 56 bytes andget_summary()returns byte-identical values to previous releases. The new metrics went into a separate struct reached through a separate entry point rather than growing the existing one, because growing a caller-allocated struct overruns a buffer sized by an older compiled header — silently.Sharpe, Sortino, and Calmar are annualised by
sqrt(periods_per_year), whereperiods_per_yearis inferred from the median gap between consecutive closed trades'close_timestampvalues, falling back to 252 below two closed trades or on unparseable timestamps.reamer_research_get_result_json()andreamer_research_write_result_json()— the complete run as a single JSON document: summary, closed trades, fills, every order and its outcome, scale-in fills, roll events, and the per-trade return series. Two-call sizing (buf = NULLreports the required size including the NUL); an undersized buffer is rejected rather than truncated. Output is locale-independent by construction — doubles are formatted by the serializer's own Grisu2 path, never the C locale'sprintf— and this is enforced by a test running underde_DE.UTF-8.Replay scaffolding (
orders_by_step,tick_orders_by_ticker,prints_by_step) is deliberately excluded: it carries no information not already inorder_logand would multiply the document size.bin/reamer-exo-build— converts a two-column CSV into the.exo.binsidecarrun_backtest_v3()reads. Reports entry count, duplicates collapsed, and byte size; exits non-zero with a stderr message on unreadable, malformed, or empty input. No license check — it is an offline converter that links no engine code, so gating it would add an activation failure mode ahead of evaluation.EXOGENOUS_FORMAT.md— CSV authoring format, the.exo.binbinary layout, the point-in-time visibility rule, and CLI usage.RESULT_JSON_SCHEMA.md— field-by-field schema for the result document, withschema_versionversioned independently ofREAMER_ABI_VERSION.It documents
returns[]explicitly, because the definition is not the one most consumers assume:returns[i] = net_pnl / equity_at_entry, clamped to[-1, 1]. The denominator is account equity at entry, not the trade's own margin, so it is notclosed_trades[i].return_pct / 100. This is the exact series the Sharpe, Sortino and Calmar figures are computed from — recomputing risk metrics fromclosed_trades[].net_pnlwill not reconcile withsummary.
Both new documents ship in the kit and are enforced by the archive verifier, not merely copied by the build script.
Changed
EXECUTION_SPEC.mdgains §0 (per-ticker execution config resolution, including therng_seedexclusion) and §13 (exogenous data resolution).RESEARCH_TO_SERVER.md§2 — instrument-identity mapping is still required and still yours to build, but arun_backtest_v3()run's results now carry the same string identifiers Server'sintent.Instrumentuses, so the mapping can be asserted as a build step instead of trusted.- The internal ABI versioning policy records the v5 row and the struct-growth hazard that motivated putting the new metrics in a separate struct.
[4.1.11] - 2026-09-02
Changed
- Shipped archive filename renamed for clarity: this kit is now
research-kit-<version>-<platform>.tar.gz(wasreamer-research-<version>-<platform>.tar.gz). Only the customer-facing archive filename moved; the extracted layout is unchanged.