Documentation

Strategy SDK & User Guide

Reamer Research is a closed-source C ABI for backtesting mid-frequency trading strategies. Link the library, implement a strategy callback, and backtest against deterministic synthetic ticks verified by a 267-check conformance suite.

Covers C ABI integration · execution model · order types · market data · results access · Python quick start (reference implementation)

Paste into Claude, GPT, or any AI assistant to give it full Reamer Research context for writing strategies.

Scope & intended use

reamer_research is research-only. No live orders, no broker connection — that's what Reamer Server is for, once a strategy is validated here.

reamer_research targets mid-frequency OHLCV strategies — intraday to multi-day holding periods — across forex/CFD, futures, and equities.

Not a fit for:
  • High-frequency / order-book strategies. Execution is OHLCV bars plus deterministic synthetic ticks within each bar — no L2/L3 depth data, no real tick data.
  • Options. No implied vol, Greeks, or exercise/assignment modeling.
  • Equity OHLCV input must already be split/dividend-adjusted — required, not optional. reamer_research does not implement corporate-actions ingestion. Unadjusted input shows a fake price gap/loss at split dates and understates total return from missing dividends, with no engine-level detection. Use an already-adjusted (ideally total-return-adjusted) OHLCV source before loading.

Forex/CFD is a direct, accurate match — spread, slippage, commission, and swap map onto real broker cost structure as-is. Futures are well-supported across contract rolls — set roll_cycle_months and roll_days_before_monthend on the ticker's execution config to roll the continuous contract automatically (ratio-rebase adjustment) at a configurable point before month-end. Equities remain well-supported as long as input stays within a single corporate-action-free series — see Execution config reference for asset-class defaults.

Installation

License required: Reamer Research requires an active license to run. Contact [email protected] for a free, time-limited test license to evaluate before committing to a seat.

Download the release tarball for your platform (Linux x86_64 or macOS arm64) from releases.reamerlabs.com. Extract and link against reamer_research_abi.h:

tar xzf reamer-research-3.2.0-linux_x86_64.tar.gz
cd reamer-research-3.2.0-linux_x86_64
gcc -c my_backtest.c -I./include
gcc my_backtest.o -L./lib -lreamer_research -o my_backtest
./my_backtest

Full integration guide: see DEPLOYMENT.md Step 3 in the release archive. Build a ReamerResearchStrategyVtable (the strategy callback interface), populate your backtest config and input data, and call reamer_research_run_backtest().

A complete backtest in C

#include "reamer_research_abi.h"
#include <stdio.h>

// Implement your strategy as a callback
size_t on_bar(void* user_data, const OhlcvBar* window, size_t window_len,
              size_t ticker_count, ReamerOrderRequest* out_orders, size_t max_orders) {
    // window[window_len - 1] is the current bar
    // Return the number of orders written to out_orders (0 to max_orders)
    return 0;  // no action this bar
}

int main() {
    // Set up bars, config, and strategy
    ReamerResearchStrategyVtable strategy = {
        .user_data = NULL,
        .on_bar = on_bar
    };

    ReamerBacktestConfig config = {
        .initial_capital = 10000.0,
        .leverage = 1.0,
        // ... set commission, slippage, spread, etc.
    };

    ReamerBacktestHandle result = NULL;
    ReamerResearchStatus status = reamer_research_run_backtest(
        &bars_by_ticker, bar_counts, ticker_ids, ticker_count,
        &strategy, &config,
        10000.0, 1.0);

    if (status == REAMER_RESEARCH_SUCCESS) {
        ReamerBacktestSummary summary;
        reamer_research_get_summary(result, &summary);
        printf("Net PnL: %.2f\n", summary.net_pnl);
        reamer_research_free_result(result);
    }
    return 0;
}

See DEPLOYMENT.md for the complete Step 3 walkthrough: building the bars array, configuring execution costs, and reading results via the summary and closed-trades accessors.

Python quick start (reference, unsupported)

The reference-python/ ctypes binding in the Reamer Research release archive provides an alternative quick start for single-ticker market-order strategies with optional take-profit/stop-loss brackets. It is not a supported product interface — it is demonstration code showing how to wrap the C ABI in Python ergonomics.

from reamer_research_binding import run_backtest
from engine.orders import buy_market, close_position

class MyStrategy:
    lookback = 1
    def on_bar(self, data):
        tv = data['SPX']
        if tv.close[-1] > tv.close[-2]:
            return buy_market(10.0, ticker='SPX',
                            take_profit=tv.close[-1] + 2.0,
                            stop_loss=tv.close[-1] - 1.0)
        return None

result = run_backtest(MyStrategy, config, {'SPX': bars_array})
print(f"Net PnL: {result.net_pnl:.2f}")

This binding supports single-ticker strategies, market entries with brackets, and basic CSV loading. See reference-python/README.md for the complete feature list, templates, and examples.

Key rule: Orders submitted on bar N fill within bar N's own tick sequence (same-bar fill). A market order fills at tick_ask + slippage on bar N — not at bar N+1's open.

The strategy callback

C ABI signature

Implement a callback matching this signature:

size_t on_bar(
    void* user_data,
    const OhlcvBar* window,         // borrowed: rolling-window array
    size_t window_len,               // bars in window[] (including current bar)
    size_t ticker_count,             // total tickers in this run
    ReamerOrderRequest* out_orders,  // caller-allocated output buffer
    size_t max_orders)               // max orders to write
ParameterDetails
user_dataOpaque pointer you provide during setup. Ignored by engine. Use to pass strategy state.
windowBorrowed pointer to OhlcvBar` array — never allocate or free. Valid only for this call. window[window_len - 1] is the current bar; earlier indices are prior bars.
window_lenNumber of bars in window. Your requested lookback depth, or smaller on the first bars of a run.
ticker_countTotal number of tickers in this backtest. The same callbacks fires for each ticker independently each step.
out_ordersCaller-allocated buffer you populate with ReamerOrderRequest structs. Write-only, you do not own this memory.
max_ordersMaximum number of orders this callback can write (typically 64). Return fewer if you wish.

Return value and order marshaling

Return the count of orders actually written to out_orders (0 to max_orders). Each order is a ReamerOrderRequest struct with fields: ticker_id (0-based index into your ticker list), order_type, kind, side, tif, qty, limit_price, stop_price, take_profit, stop_loss, is_cancel, is_close. See reamer_research_abi.h for the complete struct definition and enum values.

No dynamic allocation crosses the ABI boundary

The engine passes borrowed pointers only — you never allocate or free memory for bars, orders, or results. This is the key to the ABI's stability: every resource is owned and managed entirely by one side (either your code or the engine), never shared.

Windowing and lookback

window_len reflects your requested lookback depth, or fewer on the first bars of the run when history doesn't yet exist. The window always has window[window_len - 1] as the current bar — no need for a separate "current bar" index.

Order types and fills

All order fills are priced against a deterministic synthetic tick sequence generated within each bar. The ABI supports market orders, limit orders, stop orders, and brackets (take-profit / stop-loss). See reamer_research_abi.h for the complete enum definitions (ReamerOrderKind, ReamerOrderType, ReamerOrderSide, ReamerOrderTimeInForce).

Order typeTrigger / Fill price
Market buyAlways fills at tick_ask + slippage within bar N
Market sellAlways fills at max(0, tick_bid - slippage) within bar N
Buy limitFills when tick_ask ≤ limit_price at tick_ask + slippage
Sell limitFills when tick_bid ≥ limit_price at max(0, tick_bid - slippage)
Buy stopFills when tick_ask ≥ stop_price at tick_ask + slippage
Sell stopFills when tick_bid ≤ stop_price at max(0, tick_bid - slippage)

Take-profit and stop-loss brackets

Attach automatic exits to any entry order via ReamerOrderRequest.take_profit and .stop_loss fields (both doubles, can be 0.0 to omit). The brackets arm after the entry fills and trigger independently based on price action in subsequent ticks.

Exit typeTriggerFill price (long)Fill price (short)
Take profitTP level touchedmax(0, tick_bid - slippage)tick_ask + slippage
Stop lossSL level touchedmax(0, tick_bid - slippage)tick_ask + slippage

Time-in-force (TIF)

Set via ReamerOrderRequest.tif enum field:

TIFBehavior
GTCGood Till Cancelled — persists across bars until filled or end of run
IOCImmediate Or Cancel — fills on bar N only, cancelled at bar N's seal if unfilled
GTDGood Till Date — expires at a specific timestamp. Set via order's expiry_timestamp field (if present in struct)

Timing: same-bar fill semantics

Orders submitted via callback at bar N can fill anywhere within bar N's tick sequence. Market orders fill at tick 1 (the first interpolated step away from bar.open, closely approximating but not exactly equaling the open). Limit and stop orders fill at whatever tick their condition is first satisfied.

Common mistake: expecting a fill at bar N+1's open. Fills happen within bar N. The exact fill tick within bar N is deterministic given rng_seed (default 42).

Positions and sizing

Position state at callback time

The engine provides your current position via the ReamerBacktestConfig.positions_callback or by storing position state in your user_data pointer. The reference-python binding exposes it as a simple float: data.position(ticker) returns the signed quantity (positive = long, negative = short, 0.0 = flat).

Margin check

An entry order is rejected if total notional (new + existing open positions) would exceed equity × leverage:

rejected if: new_notional + existing_notional > equity × leverage where new_notional = qty × fill_price existing_notional = sum of all other open position notionals

Position sizing helpers

A common pattern: size a position so that hitting a stop-loss costs exactly your risk budget.

risk_sized_qty = risk_dollars / abs(entry_price - stop_price)

Accessing results

After reamer_research_run_backtest() returns success, retrieve results via the ABI's accessor functions:

ReamerBacktestSummary summary;
reamer_research_get_summary(result, &summary);
printf("Net PnL: %.2f, Trades: %u\n", summary.net_pnl, summary.trade_count);

ReamerBacktestSummary contains scalar metrics: net_pnl, gross_pnl, total_fees, trade_count, win_rate, max_drawdown, sharpe_ratio, and others. See reamer_research_abi.h for the complete struct definition.

Closed trades

Iterate over all completed trades via reamer_research_get_closed_trades() — a poll-style accessor: you provide a buffer, the library fills it, and you call repeatedly for more:

ReamerClosedTrade trades[100];
size_t total_returned = 0;
size_t offset = 0;
while (1) {
    size_t count = 0;
    reamer_research_get_closed_trades(result, trades, 100, offset, &count);
    if (count == 0) break;
    for (size_t i = 0; i < count; i++) {
        printf("Trade %u: entry %.2f, exit %.2f, pnl %.2f\n",
               offset + i, trades[i].entry_price, trades[i].exit_price, trades[i].net_pnl);
    }
    offset += count;
}

Memory management

After you're done with results, always call reamer_research_free_result(result) to release the handle's backing memory (owned entirely by the library).

Fill prices

All fills use the prevailing synthetic tick bid or ask, adjusted for slippage. The tick sequence within each bar is deterministic given rng_seed. This page summarizes the behavior — the execution specification is the authoritative, tested definition, and the conformance test suite verifies every rule in it, included as part of an evaluation.

Order typeTrigger conditionFill price
Market buyalwaystick_ask + slippage
Market sellalwaysmax(0, tick_bid − slippage)
Buy limittick_ask ≤ limit_pricetick_ask + slippage
Sell limittick_bid ≥ limit_pricemax(0, tick_bid − slippage)
Buy stoptick_ask ≥ stop_pricetick_ask + slippage
Sell stoptick_bid ≤ stop_pricemax(0, tick_bid − slippage)
Long TP exittick_bid ≥ take_profitmax(0, tick_bid − slippage)
Long SL exittick_bid ≤ stop_lossmax(0, tick_bid − slippage)
Short TP exittick_ask ≤ take_profittick_ask + slippage
Short SL exittick_ask ≥ stop_losstick_ask + slippage

Fill prices can exceed the raw bar OHLCV range by up to spread + slippage in each direction. This is correct — spread and slippage are applied on top of tick price, which is clamped to [bar.low, bar.high].

Tick sequence

Each bar generates a synthetic tick sequence whose length equals the time delta to the next bar in seconds — a 1-minute bar has 60 ticks, a daily bar has 86,400. Tick 0 is exactly bar.open and tick N−1 is exactly bar.close; intermediate ticks are interpolated through the bar's high/low with small deterministic noise.

Fills never occur on tick 0. The tick loop starts at tick 1, so no order can fill before the bar's first interpolated price.

Same-bar fill semantics

An order submitted via on_bar at bar N has created_ts = bar_N.epoch and is eligible for all ticks 1 … N−1 of that same bar.

  • Market orders fill at tick 1 unconditionally — no price condition. Tick 1 is the first interpolated step away from bar.open, so the fill price closely approximates the open but is not exactly equal to it.
  • Limit and stop orders fill at whatever tick their price condition is first satisfied — potentially anywhere from tick 1 to tick N−1. This is the only way to achieve a condition-driven mid-bar fill.

Slippage cost accounting

slippage_cost = |fill_price − reference_price| × qty
# reference_price = tick_ask (buy) or tick_bid (sell)

This includes the spread contribution when OHLCV data is bid-only or ask-only.

Slippage, spread & commission

Slippage — absolute price units

slippage is a mean slippage in absolute price units (not a fraction of price or bar range).

  • Equities: 0.05 = 5 cents per fill
  • Forex (1 unit = 1 base currency): 0.0001 = 1 pip

Spread — absolute price units

Direction depends on ohlcv_type:

OHLCV typeMeaningBuy fillSell fill
"bid"Data is bid pricestick + spread (pays spread)tick
"ask"Data is ask pricesticktick − spread (pays spread)
"midpoint"Data is mid pricestick + spread/2tick − spread/2

Commission

ModeOpen legClose leg
"round_trip"qty × rateqty × rate
"open_only"qty × rate0
"close_only"0qty × rate

Overnight swap

Charged at each calendar date boundary in the bar sequence. Long positions pay; short positions receive.

cfg.set_swap(0, 0.0)  # Sunday
cfg.set_swap(1, 1.5)  # Monday — $1.50 per unit held overnight
# 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat

Units and qty sizing

qty is always in individual base-asset units — not lots:

Asset class1 unit meansExample
Equities1 shareSPX: qty=100 = 100 shares
Forex1 unit of base currencyEURUSD: 1 unit ≈ $0.00013 P&L per pip
Commodities1 unit of underlyingGold: check lot definition for your feed

Forex lot equivalents

Lot typeUnitsqty value
Standard lot100,000qty=100000
Mini lot10,000qty=10000
Micro lot1,000qty=1000
Nano lot100qty=100

Scale commission_per_unit to match. A broker charging $7 per standard lotcommission_per_unit = 7 / 100_000 = 0.00007. Swap rates follow the same rule — if your broker quotes swap as $X per lot per night, divide by 100,000 for swap_* fields.

Execution config reference

Set fields on reamer_research.DefaultExecutionModelConfig() before passing to run_backtest. The config used is embedded in any .reamer file written by save_result, so it's always recoverable from the result record itself.
ParameterUnitsDescription
initial_capital$Starting equity.
leverage×Max notional: new_notional + existing_notional ≤ equity × leverage.
commission_per_unit$ / unitFixed fee per unit traded.
commission_modeenum"round_trip", "open_only", "close_only".
slippageprice unitsMean slippage per fill. Absolute, not fractional.
spreadprice unitsBid-ask spread per fill. Direction depends on ohlcv_type.
ohlcv_typeenum"bid", "ask", "midpoint".
rng_seedintSeed for deterministic tick generation (default 42).
price_volatilityσEnables stochastic noise on both spread and slippage, scaled by the bar's high-low range. 0 = deterministic (exact spread and slippage every fill). When > 0, spread noise is capped and floored at zero; slippage noise is uncapped and can go negative — occasional favorable slippage is intentional, not a bug. Only via reamer_research.
swap_per_unit$ / unit / nightOvernight swap by weekday. In the .reamer JSON file this is one 7-element array, index 0=Sun…6=Sat. In Python, set per-day via cfg.set_swap(day, value).
roll_cycle_monthslist[int]Futures only. Months (1=Jan…12=Dec) the continuous-contract roll recurs in. Empty (default) = disabled, identical to today's behavior. Set via ticker_overrides — see Per-ticker overrides.
roll_days_before_monthendbusiness daysBusiness days before each cycle month's last calendar day that the roll fires. Default 5.
Roll-cycle adjustment rebases execution-model bookkeeping only — open position (entry_fill_price, take-profit, stop-loss) and resting order price levels are ratio-adjusted forward from the roll date. It does not rewrite historical bars: a strategy's own tv.close/open/high/low still shows the raw splice jump across the roll boundary. Every applied roll is logged to result.roll_log — see BacktestResult.

Typical defaults by asset class

ParameterEquitiesForex
commission_per_unit0.010.0
slippage0.050.0001
spread0.100.0002
leverage1.0100.0
ohlcv_type"bid""bid"

License

Reamer Research requires an active license key to run. Activation is offline and per-machine via the ./reamer-license activate CLI included in the release archive. See DEPLOYMENT.md Step 2 for the full activation flow. Contact [email protected] for licensing inquiries or a free, time-limited test license to evaluate before committing to a seat.

Per-ticker overrides

By default every ticker in a portfolio backtest shares one exec_config. Pass ticker_overrides to run_backtest() (reamer_research only) when specific tickers need genuinely different execution costs — the two common cases are per-ticker swap (each FX pair has its own overnight financing rate) and per-ticker commission (a broker charging a different schedule per instrument). Per-ticker spread/slippage/price_volatility/ohlcv_type are also supported the same way, for whenever tighter per-instrument realism is worth it.

eurusd_cfg = reamer_research.DefaultExecutionModelConfig()
eurusd_cfg.spread = 0.0002
eurusd_cfg.set_swap_by_name("wednesday", -0.15)  # this broker rolls triple-swap on Wednesday

audjpy_cfg = reamer_research.DefaultExecutionModelConfig()
audjpy_cfg.spread = 0.0006
audjpy_cfg.set_swap_by_name("wednesday", 0.30)

result = reamer_research.run_backtest(
    data=["EURUSD", "AUDJPY", "GBPUSD"],
    strategy=MyStrategy(),
    exec_config=global_cfg,          # GBPUSD has no override, so it uses this unchanged
    ticker_overrides={
        "EURUSD": eurusd_cfg,
        "AUDJPY": audjpy_cfg,
    },
)

ticker_overrides is a dict[str, DefaultExecutionModelConfig], keyed by the same uppercase ticker strings used in data. A ticker with no entry in the dict always falls back to exec_config unchanged — never a silent zero-cost default.

If ticker_overrides is non-empty, a stderr warning is printed for any traded ticker not covered by it, and for any override key that doesn't match a ticker in data (catches typos). rng_seed set inside a per-ticker override is ignored — the synthetic tick generator's seed always comes from exec_config.rng_seed. Pass the same ticker_overrides dict to save_result() that you passed to run_backtest(), so a later debug_trade() re-run against the saved .reamer file sees the same per-ticker config.

Exogenous data

Attach an arbitrary, freeform timeseries to any ticker — earnings surprises, macro prints, sentiment scores, analyst ratings, anything JSON-serializable — and read the latest-known-as-of-this-bar value from on_bar via tv.exogenous. Every timestamp in your simulation can carry structured context alongside the market data itself. The engine never parses, validates, or imposes a schema on the value — it only tracks which entry is current as of each bar. Parsing (json.loads or whatever the value actually is) is the strategy's own responsibility.

load_exogenous_csv(path, out) parses a two-column CSV into a .exo.bin sidecar file:

2024-01-03 00:00:00,{"eps_surprise": 0.03}
2024-01-15 00:00:00,{"eps_surprise": -0.01, "note": "guidance cut"}

Column 1 is a timestamp ("YYYY-MM-DD HH:MM[:SS]", "YYYYMMDD HH:MM[:SS]", or "YYYYMMDD"); column 2 is everything after the first delimiter to end of line, taken verbatim — a JSON value containing commas doesn't need quoting. Pass the resulting sidecar paths as run_backtest()'s exogenous arg — a sparse dict[str, str] keyed the same way as data/ticker_overrides. A ticker missing from exogenous simply never has tv.exogenous_valid == True.

reamer_research.load_exogenous_csv("aapl_earnings.csv", "aapl_earnings.exo.bin")

result = reamer_research.run_backtest(
    data=["AAPL"],
    strategy=MyStrategy(),
    exogenous={"AAPL": "aapl_earnings.exo.bin"},
)
class MyStrategy:
    def on_bar(self, data):
        tv = data["AAPL"]
        if not tv.exogenous_valid:
            return None  # no exogenous entry yet for this ticker at this step
        info = json.loads(tv.exogenous)
        if info.get("eps_surprise", 0) > 0.02:
            return buy_market(10, ticker="AAPL")
        return None
tv.exogenous is '' and tv.exogenous_valid is False both before the series' first entry and for any ticker with no series attached at all — always check .exogenous_valid first, exactly like .valid. save_result() needs no changes for exogenous data — pass the same exogenous dict to debug_trade() that you passed to the original run_backtest() call for the re-run to see the same values and stay bit-identical.

Replay & debugging

reamer_research.debug_trade() re-runs a backtest and calls breakpoint() before on_bar for every step in [trade.open_step - buffer, trade.close_step + buffer] — dropping you into whatever debugger PYTHONBREAKPOINT points at (pdb by default, or ipdb/VS Code/PyCharm) exactly where a specific trade's decisions were made. The direct replacement for stepping through a bar-by-bar replay UI: real line-by-line debugging in your own tools, driven live, instead of a bespoke widget.

reamer_research.debug_trade(
    data,               # same data= list passed to run_backtest
    strategy,           # a fresh instance of the same strategy class
    trade=result.closed_trades[0],
    exec_config=cfg,    # same exec_config/ticker_overrides/etc. as the original run
    buffer=5,           # extra bars before/after the trade to step through
)

Console output

print() inside on_bar goes straight to your terminal or notebook, and is also captured into result.prints_by_step and stored in the .reamer file (via save_result) for later inspection — no separate console view needed:

[2024-01-02 09:30:00] bar close=130.14  in_pos=False
[2024-01-03 09:30:00] ENTRY signal at 129.99: sma_short=129.50 sma_long=128.80
[2024-01-04 09:30:00] bar close=128.51  in_pos=True

Order status strings

Status values are Title case: Filled, Pending, Cancelled, Expired, Rejected. String comparisons in Python are case-sensitive: o.status == "Filled" works; "filled" does not.

Determinism

The tick sequence within each bar is seeded by rng_seed (default: 42). Re-running the same backtest always produces identical fills — including a debug_trade() re-run, which reproduces the exact same fills as the original run. Change rng_seed to test sensitivity to tick ordering.

Strategy development workflow

Run backtests via reamer_research, inspect result.order_log / result.closed_trades in Python, then use debug_trade() to step into your own debugger for any trade that needs a closer look, or export_html_report() for a shareable summary.
  1. Define your signal. Start with on_bar returning None always. Add indicator computation and print() the values. Run via reamer_research to confirm the signal fires when expected.
  2. Add market orders. Use buy_market and close_position first. Verify fills by inspecting result.closed_trades or via debug_trade().
  3. Improve entry with limits or stops. Replace buy_market with buy_limit (enter on pullback) or buy_stop (enter on breakout). Check result.order_log for how many expired vs. filled.
  4. Add TP/SL brackets. Replace manual close logic. Check result.closed_trades — bracket exits appear there, not in order_log.
  5. Add GTD expiry to limit/stop orders to prevent stale fills in changed conditions.
  6. Tune execution config. Add realistic slippage, spread, and commission. A strategy that breaks when costs are added has thin or no edge.
  7. Review Monte Carlo. Check risk_of_ruin < 0.05 and probability_of_loss < 0.20 for robustness.

Progression checklist

  • Signal fires on expected bars (verified via print output)
  • Market order fills are at the expected tick (verified via debug_trade() or by inspecting result.closed_trades)
  • Position state is consistent — no double-entry rejections
  • Limit/stop orders fill at correct prices (verified via order_log)
  • Bracket exits appear in closed_trades not order_log
  • Performance holds up with realistic execution costs
  • Monte Carlo risk_of_ruin < 0.05

Order helpers (engine.orders)

from engine.orders import (
    buy_market, sell_market, close_position,
    buy_limit, sell_limit,
    buy_stop, sell_stop,
    cancel_order, orders,
)
HelperSignature
buy_market(qty, *, ticker, tif=None, take_profit=None, stop_loss=None)
sell_market(qty, *, ticker, tif=None, take_profit=None, stop_loss=None)
close_position(qty=0, *, ticker) — closes an open position (long or short); engine resolves direction. qty=0 closes full position; qty=N closes N units (partial).
buy_limit(price, qty, tif="GTC", *, ticker, take_profit=None, stop_loss=None, expiry_timestamp=None)
sell_limit(price, qty, tif="GTC", *, ticker, take_profit=None, stop_loss=None, expiry_timestamp=None)
buy_stop(price, qty, tif="GTC", *, ticker, take_profit=None, stop_loss=None, expiry_timestamp=None)
sell_stop(price, qty, tif="GTC", *, ticker, take_profit=None, stop_loss=None, expiry_timestamp=None)
cancel_order(order_id, *, ticker=None)
orders(*items) — returns single item or list
Order dicts returned by these helpers do not contain an order_id key — IDs are assigned by the engine. Mirror the engine's 1-based sequential counter in your strategy if you need to predict IDs for cancellation.

BacktestResult

FieldTypeDescription
gross_pnlfloatPnL before fees and slippage
net_pnlfloatPnL after all costs
total_feesfloatAll commission charges including bracket exits
total_slippage_costfloatAll slippage costs including bracket exits
total_swap_costfloatCumulative overnight swap (positive = cost to longs)
tradesintCompleted round-trip count
closed_tradeslist[ClosedTrade]All completed trades including bracket exits
order_loglist[LiveOrder]Strategy-submitted orders with final status; bracket exits not included
open_orders_endlist[LiveOrder]Orders still pending at end of run
returnslist[float]Per-trade equity returns (Monte Carlo input)
roll_loglist[RollEvent]Applied futures roll events (ticker, timestamp, ratio). Present only for tickers with a configured roll_cycle_months that actually rolled during the run.

LiveOrder

o.id                  # int — engine-assigned sequential ID (starts at 1)
o.created_timestamp   # str — "YYYY-MM-DD HH:MM:SS"
o.closed_timestamp    # str
o.ticker              # str — uppercase
o.order_type_str      # "buy", "sell", "buy_limit", "sell_limit", "buy_stop", "sell_stop"
o.tif_str             # "GTC", "IOC", "GTD"
o.status              # OrderStatus enum — compare with: o.status == "Filled"
o.reject_reason       # str — non-empty when status is Rejected
o.qty                 # float
o.limit_price         # float (0.0 for market orders)
o.stop_price          # float (0.0 for non-stop orders)
o.take_profit         # float (0.0 if not set)
o.stop_loss           # float (0.0 if not set)
o.fill_price          # float (0.0 if not filled)
o.fees                # float
o.slippage_cost       # float

ClosedTrade

ct.ticker              # str — symbol
ct.side                # Side enum; ct.side == "buy" works
ct.open_timestamp      # str
ct.close_timestamp     # str
ct.entry_price         # float — includes spread and slippage
ct.exit_price          # float — includes spread and slippage
ct.qty                 # float
ct.leverage            # float
ct.margin_used         # float
ct.fees                # float — combined open + close commission (including bracket exits)
ct.slippage            # float — combined open + close slippage cost
ct.gross_pnl           # float — (exit - entry) × qty × ±1
ct.net_pnl             # float — gross_pnl - fees
ct.return_pct          # float — net_pnl / margin_used × 100
ct.open_step           # int — bar index when position was opened (read-only)
ct.close_step          # int — bar index when position was closed (read-only)
ct.open_tick_i         # int — synthetic tick index within bar when opened (read-only)
ct.close_tick_i        # int — synthetic tick index within bar when closed (read-only)

OhlcvBar

No-argument constructor — set fields individually:

b = reamer_research.OhlcvBar()
b.timestamp = "2024-01-15 09:30:00"   # "YYYY-MM-DD HH:MM:SS"
b.open   = 130.0
b.high   = 131.5
b.low    = 129.5
b.close  = 131.0
b.volume = 1_500_000.0

All timestamps must be in "YYYY-MM-DD HH:MM:SS" format and in ascending order within each ticker's bar list.

DefaultExecutionModelConfig

cfg = reamer_research.DefaultExecutionModelConfig()
cfg.commission_per_unit       = 0.01
cfg.commission_mode           = "round_trip"    # or CommissionMode.RoundTrip
cfg.slippage                  = 0.05
cfg.spread                    = 0.10
cfg.ohlcv_type                = "bid"           # or OhlcvType.Bid
cfg.price_volatility          = 0.0             # 0 = deterministic fills
cfg.rng_seed                  = 42
cfg.roll_cycle_months         = [3, 6, 9, 12]   # futures only; empty = disabled (default)
cfg.roll_days_before_monthend = 5               # default
cfg.set_swap(1, 1.5)           # Monday swap: $1.50/unit/night
cfg.get_swap(1)                # returns 1.5

run_backtest signature

reamer_research.run_backtest(
    data,                      # list[str] — ticker names, already ingested via
                               # load_csv/load_csv_portfolio/write_bin
    strategy,                  # object with on_bar(self, data) method
    alignment_mode="union",    # the only supported mode
    exec_config=DefaultExecutionModelConfig(),
    initial_capital=10000.0,
    leverage=1.0,
    show_progress=True,        # live ETA to stderr; auto-suppressed when stderr isn't a terminal
    ticker_overrides={},       # dict[str, DefaultExecutionModelConfig] — see Per-ticker overrides
    exogenous={},              # dict[str, str] — ticker → .exo.bin path, see Exogenous data
) -> BacktestResult

save_result signature

reamer_research.save_result(
    result,                    # BacktestResult from run_backtest
    path,                      # output .reamer file path
    exec_config,               # DefaultExecutionModelConfig used in the run
    data_paths,                # sparse dict[str, str] — ticker → placeholder recordkeeping
                                # value (data is addressed by ticker,
                                # not by file path) — pass {t: t for t in data}
    initial_capital,           # float
    leverage=1.0,
    alignment_mode="union",
    asset_class="equity",      # "equity" | "fx" | "futures" — report/trade-display hint only
    show_progress=True,        # live ETA to stderr; auto-suppressed when stderr isn't a terminal
    ticker_overrides={},       # pass the same dict given to run_backtest, if any
    strategy=None,             # same class/instance passed to run_backtest — embeds its full
                                # source in the .reamer file when given (see below)
)

When strategy is passed, the full source of the module the strategy class is defined in — imports, helpers, constants included, not just the class body — is embedded in the .reamer file as strategy_code, alongside strategy_class_name. A .reamer file is plain JSON, so opening it directly in any text editor shows the exact code that produced the results next to the results themselves — a self-contained record for long-term reference, not just something debug_trade() can re-run. Best-effort: omitted if the source can't be retrieved (e.g. a class defined interactively in a REPL), which never fails the save.

run_monte_carlo signature

reamer_research.run_monte_carlo(
    returns,                   # list[float] — typically result.returns
    num_simulations=10000,
    initial_capital=10000.0,
) -> MonteCarloStats

Bootstrap-resamples the returns series and returns the same statistics export_html_report()'s Monte Carlo section shows. Not seeded — each call resamples independently, so results are not bit-reproducible across runs. See Monte Carlo.

Rejection reasons

When an order is rejected, o.reject_reason contains one of these strings:

Reason stringCauseFix
"insufficient margin"new_notional + existing_notional > equity × leverageReduce qty or increase leverage
"invalid TP/SL for side"TP on wrong side of entry price, or SL on wrong sideBuy: TP > entry, SL < entry. Sell: TP < entry, SL > entry.
"qty must be specified for entry orders (qty > 0)"Entry submitted with qty ≤ 0Always pass positive qty for entries
"no market data"Order submitted for a ticker with no bar at the current stepCheck tv.valid before submitting in union mode
"missing ticker"Order has no ticker field and it couldn't be inferredAlways pass ticker= explicitly

Checking rejections after a run

rejected = [o for o in result.order_log if o.status == "Rejected"]
for o in rejected:
    print(f"bar {o.created_timestamp}  {o.order_type_str}  reason={o.reject_reason}")