Documentation

Strategy SDK & User Guide

reamer_py is the research SDK — run headlessly in a script or notebook, inspect results via a self-contained HTML report, or step into your own debugger at any trade's decision bars with debug_trade().

Covers installation · Python strategy interface · all order types · execution model · backtesting · replay · full API reference

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

Scope & intended use

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

reamer_py 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_py 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

reamer_py (scripts / notebooks)

License required: reamer_py requires an active license key to run (see License) — contact us for a free, time-limited test license to evaluate it before committing to a seat.

Requires Python 3.10 or later. reamer_py is distributed as a pre-built wheel via a --find-links index — pip resolves the correct wheel for your OS and Python version automatically:

pip install --find-links https://reamerlabs.com/pip reamer-py

Verify the install:

python -c "import reamer_py; print('ok')"

reamer_py works in Python scripts, Jupyter notebooks, and any Python 3.10+ environment. The order helpers are imported from the bundled engine.orders sub-package:

from engine.orders import buy_market, sell_market, close_position, buy_limit, sell_limit

Getting started

Install, write the strategy below, run it against real fills.

  1. Install: pip install --find-links https://reamerlabs.com/pip reamer-py — pip resolves the correct wheel for your platform automatically
  2. Load data, configure execution, and run:
import reamer_py
from engine.orders import buy_market, close_position

class MyFirstStrategy:
    lookback = 1
    ticker = "SPX"

    def __init__(self, config_path=None):
        self.bar = 0

    def on_bar(self, data):
        self.bar += 1
        ticker = self.ticker
        tv = data[ticker]
        if not tv.valid:
            return None
        if self.bar == 5 and tv.position.qty == 0:
            return buy_market(10.0, ticker=ticker)
        if self.bar == 15 and tv.position.qty > 0:
            return close_position(ticker=ticker)
        return None

# Ingest the CSV into Reamer Stream under a ticker name (auto-detects format — Yahoo, Alpaca, MT4/MT5, etc.)
reamer_py.load_csv("data.csv", "SPX")

cfg = reamer_py.DefaultExecutionModelConfig()
cfg.commission_per_unit = 0.01
cfg.slippage            = 0.05
cfg.spread              = 0.10

strategy = MyFirstStrategy()
result = reamer_py.run_backtest(
    data=["SPX"],
    strategy=strategy,
    alignment_mode="union",
    exec_config=cfg,
    initial_capital=10_000.0,
    leverage=1.0,
)

print(f"net_pnl={result.net_pnl:.2f}  trades={result.trades}")

# strategy= embeds this file's own source alongside the results,
# so the .reamer file is a self-contained record
reamer_py.save_result(result, "my_backtest.reamer",
                      exec_config=cfg, data_paths={"SPX": "SPX"},
                      initial_capital=10_000.0, strategy=strategy)

# Inspect results directly, or export a self-contained HTML report
reamer_py.export_html_report(result, "report.html", initial_capital=10_000.0)
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.

Writing a strategy

The on_bar callback

Implement a class with __init__(self, config_path=None) and on_bar(self, data). The engine calls on_bar once per aligned step. Declare lookback on the class to set the rolling history window depth. Return an order dict, a list of dicts, or None.

lookback defaults to 1 if omitted (with a UserWarning). A strategy that uses a 20-bar SMA must declare lookback = 20 — otherwise tv.close is always length 1 and indicator calculations silently produce wrong results. Also guard against the warmup period on the first bars — tv.close.shape[0] is always (lookback,), zero-padded on the left until real history exists, so it can never be used to detect warmup. Track a bar counter instead: self.bar_count = 0 in __init__, increment it on every on_bar call, and return None until it reaches lookback.

Accessing bar data

class Strategy:
    lookback = 20   # request 20 bars of history
    ticker = "SPX"  # set to your ticker name

    def on_bar(self, data):
        # data['TICKER'] returns a _TickerView with (lookback,) arrays
        # index -1 is the current bar; index -2 is one bar ago
        tv = data[self.ticker]
        if not tv.valid:             # always check in union-mode multi-asset
            return None

        close      = float(tv.close[-1])    # current bar close
        high       = float(tv.high[-1])
        prev_close = float(tv.close[-2])    # one bar ago (lookback >= 2)
        ts         = data.timestamp         # "YYYY-MM-DD HH:MM:SS"
FieldTypeDescription
tv = data['TICKER']_TickerViewZero-copy view for that ticker. 'TICKER' must be an uppercase string matching the key passed to run_backtest.
tv.validboolFalse when the ticker has no bar at the current step (union alignment). Always check before acting.
tv.close[-1]float64Current bar close. Arrays are shape (lookback,). Also: tv.open, tv.high, tv.low, tv.volume.
tv.close[-2]float64One bar ago. tv.close[0] is the oldest bar in the window.
data.timestampstrCurrent step timestamp ("YYYY-MM-DD HH:MM:SS").
tv.position.qtyfloatOpen position size. 0.0 when flat.
tv.position.sideint1=long, -1=short, 0=flat.
tv.position.entry_pricefloatWeighted-average entry fill price. 0.0 when flat.
tv.position.unrealizedfloatUnrealized PnL at current bar's close. 0.0 when flat.

Loading config from JSON

import json

class MyStrategy:
    def __init__(self, config_path=None):
        cfg = {}
        if config_path:
            with open(config_path) as f:
                cfg = json.load(f)
        self.qty = float(cfg.get("qty", 10.0))
        self.window = int(cfg.get("window", 20))

Return values

ReturnEffect
NoneNo action this bar
A dictOne order
A list or tuple of dictsMultiple orders submitted this bar
Integer (1 / -1 / 0)Ignored — always return dicts

Printing & debugging

Use print() freely inside on_bar — output goes straight to your terminal or notebook, and is stored in the .reamer file. For a closer look at a specific trade, debug_trade() re-runs the backtest and drops you into your own debugger (pdb/ipdb/your IDE) exactly at the bars where that trade's decisions were made.

# Inside on_bar — tv is assigned from data[self.ticker]
tv = data[self.ticker]
print(f"bar={data.timestamp[:10]}  close={float(tv.close[-1]):.2f}")

Market orders

Market orders execute within the same bar they are submitted, filling at the tick ask (buy) or tick bid (sell) plus slippage.

from engine.orders import buy_market, sell_market, close_position

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)   # side-agnostic close — works for long and short
OrderFill price
Buy markettick_ask + slippage at the fill tick within bar N
Sell marketmax(0, tick_bid − slippage) at the fill tick within bar N

Closing positions

close_position(ticker=ticker) is side-agnostic — it closes either a long or a short position. The engine resolves the direction automatically. Use qty=0 (default) to close the full position, or qty=N for a partial close.

# Close a long position
return close_position(ticker=self.ticker)

# Close a short position — same call, engine resolves direction
return close_position(ticker=self.ticker)

# Partial close — close 5 units of a 10-unit position (long or short)
return close_position(qty=5.0, ticker=self.ticker)

Timing

Bar N: on_bar sees bar N data → returns buy_market(10.0) Bar N: order fills within bar N's tick sequence at tick_ask + slippage ← you are now long from within bar N
Common mistake — expecting a fill at bar N+1's open: fills happen within bar N. A market order fills at tick 1 of bar N — the first interpolated tick, which closely approximates bar.open but is not exactly equal to it.

Limit orders

A limit order waits until the price crosses your level, then fills at the tick price ± slippage (which is at most limit_price + slippage for buys).

from engine.orders import buy_limit, sell_limit

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)
OrderTriggerFill price
Buy limittick_ask ≤ limit_pricetick_ask + slippage (≤ limit_price + slippage)
Sell limittick_bid ≥ limit_pricemax(0, tick_bid − slippage) (≥ limit_price − slippage)

With zero slippage the fill equals or beats the stated limit price. Limit orders are GTC by default and persist across bars until filled or cancelled.

Example: enter on pullback

from engine.orders import buy_limit, close_position

class PullbackStrategy:
    ticker = "SPX"

    def __init__(self, config_path=None):
        self.has_pending = False
        self.entry_price = None
        self._order_count = 0  # mirrors engine's sequential order ID (1-based)

    def on_bar(self, data):
        ticker = self.ticker
        tv = data[ticker]
        if not tv.valid:
            return None
        close = float(tv.close[-1])
        qty = tv.position.qty

        if qty == 0 and not self.has_pending:
            entry_price = round(close * 0.98, 2)
            self._order_count += 1
            self.has_pending = True
            self.entry_price = entry_price
            return buy_limit(entry_price, 5.0, ticker=ticker)

        if qty > 0 and self.entry_price and close > self.entry_price * 1.03:
            self.has_pending = False
            return close_position(ticker=ticker)
        return None
Order dicts don't carry an order_id key. IDs are assigned by the engine. To cancel a pending limit later, mirror the engine's sequential counter in your strategy (as above) or look up the order in result.order_log after the run.

Limit price on wrong side

close = 130.0
buy_limit(131.0, ...)  # wrong — 131 is above close; tick_ask ≤ 131 is immediately true
buy_limit(129.0, ...)  # correct — waits for price to fall to 129

Stop orders

A stop order triggers when price crosses your level in the breakout direction, then fills at the tick price ± slippage.

from engine.orders import buy_stop, sell_stop

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)
OrderTriggerFill price
Buy stoptick_ask ≥ stop_price (price rises to stop)tick_ask + slippage
Sell stoptick_bid ≤ stop_price (price falls to stop)max(0, tick_bid − slippage)

Limit vs. stop at a glance

LimitStop
Buy triggers whenprice falls to or below your priceprice rises to or above your price
Sell triggers whenprice rises to or above your priceprice falls to or below your price
Use forentering on pullbacksentering on breakouts
Fill pricetick_ask + slippage (buy) or tick_bid − slippage (sell)same
close = 130.0

buy_limit(128.0, ...)   # fills if price drops to 128 — enter the dip
buy_stop(132.0, ...)    # fills if price rises to 132 — enter the breakout

# WRONG usage:
buy_stop(128.0, ...)    # stop_price=128 is below current price (130)
                        # tick_ask ≥ 128 is immediately true → fills as a market order

Take profit & stop loss

Attach automatic exits to any entry by passing take_profit= and/or stop_loss=. Both are optional and can be used independently.

tv = data[self.ticker]
close = float(tv.close[-1])

buy_market(5.0, ticker=self.ticker,
    take_profit=close + 3.00,
    stop_loss=close - 1.50)

Bracket validation rules

Entry sideTake profit must beStop loss must be
Buy (long)above entry pricebelow entry price
Sell (short)below entry priceabove entry price

Invalid brackets are rejected at order submission with reason "invalid TP/SL for side".

How bracket exits fill

Both TP and SL use the same fill formula — there is no "limit-type exact-price fill" for TP:

ExitTriggerFill 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

With zero slippage, the TP fill equals or exceeds the stated TP price for longs (since the trigger requires tick_bid ≥ take_profit). With nonzero slippage, both exits are reduced by slippage from the trigger tick price.

TP/SL collision

If both TP and SL are crossed within the same bar's tick sequence, whichever level is hit first in tick order wins. The tick sequence is deterministic given rng_seed — there is no fixed "TP wins" rule.

Bracket lifecycle

Bar N: buy_market(5.0, take_profit=133.0, stop_loss=127.0) Bar N: fills within bar N's ticks at tick_ask + slippage ≈ 130.00 ← bracket arms: TP=133, SL=127 Bar N+1: (price between 127–133 — bracket resting) Bar N+2: high reaches 133.10 → TP triggered fills at tick_bid − slippage ≈ 133.00 (or: low reaches 126.80 → SL fills at tick_bid − slippage ≈ 126.8x)
Bracket exits do not appear in result.order_log. Only entry orders are logged there. Use result.closed_trades to count and inspect all completed trades including bracket exits.
# Wrong — misses bracket exits
print(len(result.order_log))    # only entry orders

# Correct — all completed trades
for trade in result.closed_trades:
    print(trade.entry_price, trade.exit_price, trade.net_pnl)

Order expiry (TIF)

TIFBehavior
"GTC"Good Till Cancelled — stays pending until filled, cancelled, or end of data. Default.
"IOC"Immediate Or Cancel — attempts to fill on bar N (the same bar it is submitted). Cancelled at bar N's seal if not filled. Never persists to bar N+1.
"GTD"Good Till Date — requires expiry_timestamp="YYYY-MM-DD HH:MM:SS". Expired when bar_epoch ≥ expiry_epoch; status becomes "Expired".

GTD — expire at a timestamp

from engine.orders import buy_limit
from datetime import datetime, timedelta

def on_bar(self, data):
    tv = data[self.ticker]
    if not tv.valid:
        return None
    ts = data.timestamp
    dt = datetime.strptime(ts, "%Y-%m-%d %H:%M:%S")
    expiry = (dt + timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S")

    return buy_limit(
        float(tv.close[-1]) * 0.98,
        10.0,
        "GTD",
        ticker=self.ticker,
        expiry_timestamp=expiry,
    )

IOC — fill this bar or cancel

# Market IOC: fills within bar N's tick sequence (essentially guaranteed)
return buy_market(10.0, ticker=self.ticker, tif="IOC")

# Limit IOC: fills on bar N only if bar N's ticks reach your price
return buy_limit(128.0, 10.0, "IOC", ticker=self.ticker)
IOC timing: IOC fills on bar N (same bar submitted), not on bar N+1. If unfilled at bar N's seal it is cancelled. It never reaches bar N+1.

OrderStatus strings

o.status is an OrderStatus enum. String comparison is case-sensitive:

if o.status == "Filled":   ...   # correct — capital F
if o.status == "filled":   ...   # WRONG — won't match

Valid values: "Pending", "Filled", "Cancelled", "Expired", "Rejected".

Cancelling orders

Submit a cancel request by order ID. The engine assigns IDs sequentially starting from 1. Mirror the counter in your strategy to predict the next ID:

from engine.orders import buy_limit, cancel_order

class CancelExample:
    ticker = "SPX"

    def __init__(self, config_path=None):
        self.pending_id = None
        self.bar = 0
        self._order_count = 0  # mirrors engine's 1-based sequential ID

    def on_bar(self, data):
        self.bar += 1
        ticker = self.ticker
        tv = data[ticker]
        if not tv.valid:
            return None

        if self.bar == 5:
            self._order_count += 1
            self.pending_id = self._order_count
            return buy_limit(float(tv.close[-1]) * 0.97, 10.0, ticker=ticker)

        if self.bar == 8 and self.pending_id is not None:
            oid = self.pending_id
            self.pending_id = None
            return cancel_order(oid, ticker=ticker)

        return None

Cancelling a nonexistent or already-closed order ID is silently ignored.

Position management

Reading position state

tv = data['TICKER'] exposes position state at the start of the current bar:

tv.position.qty            # float — 0.0 when flat
tv.position.side           # int   — 1=long, -1=short, 0=flat
tv.position.entry_price    # float — weighted-average fill price
tv.position.unrealized     # float — unrealized PnL at current bar close

One position per ticker; scale-in and reversals

  • Scale-in: same-side order while in a position adds to it. tv.position.qty increases; tv.position.entry_price updates to the weighted average.
  • Reversal via netting: close-side order with qty > tv.position.qty closes the existing position and opens the opposite direction for the excess.
def on_bar(self, data):
    tv = data[self.ticker]
    if not tv.valid:
        return None
    side  = tv.position.side
    entry = tv.position.entry_price
    close = float(tv.close[-1])
    if side == 0:                          # flat — enter
        return buy_market(10.0, ticker=self.ticker)
    if side == 1 and close > entry * 1.05: # long — close
        return close_position(ticker=self.ticker)

Partial closes

close_position(qty=N, ticker=ticker) closes exactly N units, leaving the remainder open. qty=0 (default) closes the full position.

return close_position(qty=5.0, ticker=self.ticker)  # close half of a 10-unit long
return close_position(ticker=self.ticker)           # close everything

Position sizing

def risk_sized_qty(entry, stop, risk_dollars):
    """Size so that hitting the stop costs exactly risk_dollars."""
    risk_per_unit = abs(entry - stop)
    return risk_dollars / risk_per_unit if risk_per_unit > 0 else 0.0

Margin check

An entry is rejected if the total notional would exceed your 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

Multiple orders per bar

Return a list to submit multiple orders on the same bar:

from engine.orders import buy_limit, sell_limit, orders

def on_bar(self, data):
    tv = data[self.ticker]
    if not tv.valid:
        return None
    close = float(tv.close[-1])

    return orders(
        buy_limit(close - 1.0,  5.0, ticker=self.ticker),
        sell_limit(close + 1.0, 5.0, ticker=self.ticker),
    )

orders(*items) returns the single item if only one is passed, otherwise a list.

Inspecting results

Every result is inspected through plain function calls on the BacktestResult Python object run_backtest() returns — no separate application, nothing to install beyond reamer_py itself.

export_html_report()

A self-contained HTML report — open it in any browser, or File → Print → Save as PDF for a shareable deliverable:

ContentsDetails
Performance statisticsTotal return %, win rate, profit factor, Sharpe, Sortino, net EV/trade, avg/min/max hold time, recovery factor, drawdown (maximal & relative), order dispositions (filled / expired / cancelled / rejected)
Equity curveEmbedded SVG chart
Monte Carlo10,000 bootstrapped equity curves: mean/median/best/worst terminal equity, probability of loss, risk of ruin, drawdown percentiles (p5/p50/p95)
Trade historyAll completed round-trips with entry/exit prices, fees, slippage, PnL, return on margin
reamer_py.export_html_report(result, "report.html", initial_capital=10_000.0)

export_orders_csv()

Flat log of every order event — submissions, fills, expirations, cancellations, rejections — with timestamps, ticker, order type, price, qty, and disposition. For audit trails, compliance exports, or downstream analysis.

reamer_py.export_orders_csv(result.order_log, "orders.csv")

debug_trade()

Re-runs the backtest and drops into your own debugger (pdb/ipdb/VS Code/PyCharm — whatever PYTHONBREAKPOINT points at) exactly at the bars where a specific trade's decisions were made — the direct replacement for stepping through a bar-by-bar replay UI.

reamer_py.debug_trade(data, strategy, trade=result.closed_trades[0])

Saving a reproducible record

To keep a result for later — a self-contained, inspectable record you can reopen and re-run debug_trade() against without needing the original script's data still on disk — call reamer_py.save_result(). See reamer_py for the full workflow.

Market data formats

Files processed by reamer_py.load_csv use a smart format-detection parser. No conversion or cleanup is needed for data from major providers.

Supported delimiters

Comma, tab, semicolon, pipe — auto-detected (highest-frequency character wins).

Recognized column headers (case-insensitive, any order)

FieldAccepted header names
Date/time (combined)date, datetime, ts, gmt_time, utc_datetime, open_time, ts_event, candle_begin_time, local_time
Separate time columntime, timestamp
Openopen, o
Highhigh, h, ask_high, bid_high
Lowlow, l, ask_low, bid_low
Closeclose, c, last, settle, adj close
Volume (optional)volume, vol, v, tickvol, tick_volume, ticks, qty, quantity

Extra columns beyond OHLCV are silently ignored. Files without recognized headers fall back to positional layout: date [time] open high low close [volume].

Timestamp formats auto-detected

FormatExampleSource
Unix integers (s / ms / µs / ns)1704067200Databento, any Unix-epoch feed
YYYY-MM-DD HH:MM:SS2024-01-15 09:30:00Alpaca, most REST APIs
YYYY.MM.DD HH:MM:SS2024.01.15 09:30:00MT4 / MT5
YYYY/MM/DD HH:MM:SS2024/01/15 09:30:00Generic
MM/DD/YYYY01/15/2024Yahoo Finance
DD/MM/YYYY15/01/2024EU slash format
DD.MM.YYYY HH:MM:SS15.01.2024 09:30:00Dukascopy
YYYYMMDD  HH:MM:SS20240115 09:30:00IBKR compact
YYYYMMDDHHMMSS20240115093000Packed compact
HH:MM09:30cTrader (no seconds)

Special handling

  • Databento: Unix nanoseconds + fixed-point prices (scaled by 10⁹) auto-detected and corrected.
  • Duplicate timestamps: deduplicated — first row wins; later duplicates of the same timestamp are discarded.
  • UTF-8 BOM: stripped automatically.
  • Headerless files: positional fallback — date [time] open high low close [volume].

reamer_py — loading CSV or constructing bars

Use load_csv for any OHLCV CSV, or construct OhlcvBar objects programmatically:

# Load from CSV (auto-detects format) — ingests into Reamer Stream under a ticker name
reamer_py.load_csv("aapl.csv", "AAPL")
reamer_py.load_csv_portfolio({"AAPL": "aapl.csv", "SPY": "spy.csv"})

# Or construct bars directly
b = reamer_py.OhlcvBar()
b.timestamp = "2024-01-15 09:30:00"   # "YYYY-MM-DD HH:MM:SS" required
b.open, b.high, b.low, b.close, b.volume = 100.0, 105.0, 98.0, 103.0, 1_500_000.0
reamer_py.write_bin([b], "AAPL")

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

Timezone requirement: CSV-loaded data must already be in a single, consistent timezone before loading — reamer_py never converts, aligns, or otherwise reasons about timezones for you; every parsed timestamp is treated as literal UTC wall-clock, unconditionally. Column-name aliases like gmt_time / utc_datetime are a hint about what a given export usually contains, not a guarantee or a conversion. Mixing tickers whose CSVs were exported in different timezones will misalign bars against each other silently.

DST is a separate, sharper risk even for a single, internally-consistent ticker: if a CSV's timestamps are in a DST-observing local exchange time (e.g. US/Eastern) rather than a fixed offset, the same local clock reading maps to a different UTC instant before and after each transition — so bars silently shift by an hour twice a year. The transition dates themselves can also produce a duplicate local hour (collapsed by the last-row-wins deduplication above, quietly losing data) or a skipped local hour (an artificial gap that isn't a real market closure). reamer_py has no DST tables and does no timezone math at all — this can't be detected or corrected after the fact. See Databento loading below for a path that sidesteps all of this: Databento serves fixed UTC epoch nanoseconds, unaffected by DST.

Loading data from Databento

If you have a Databento API key, reamer_py can fetch historical bars directly into .bin format — no CSV export/import step, and no timezone handling, since Databento data is UTC by default.

Setup

pip install reamer-py[databento]
export DATABENTO_API_KEY=db-...

The databento package is an optional extra — it is not part of the base reamer-py install, so installing reamer-py normally never pulls it in.

Loading

from engine.databento_loader import load_databento, load_databento_portfolio

# Single ticker — fetched straight into Reamer Stream under `ticker`, no .bin file involved
load_databento(
    dataset="XNAS.ITCH",
    symbols=["AAPL"],
    schema="ohlcv-1m",
    start="2024-01-01",
    end="2024-02-01",
    ticker="AAPL",
)

# Portfolio — ticker name -> Databento symbol (parent symbology: every
# contract under the ES/NQ root, not one specific expiry)
load_databento_portfolio(
    dataset="GLBX.MDP3",
    symbols={"ES": "ES.FUT", "NQ": "NQ.FUT"},
    schema="ohlcv-1d",
    start="2024-01-01",
    end="2024-06-01",
    stype_in="parent",
)

api_key can also be passed explicitly as a keyword argument; it otherwise falls back to the DATABENTO_API_KEY environment variable. Query the ingested tickers back via run_backtest(data=[ticker, ...]) — see reamer_py.

Symbology (stype_in)

Defaults to "raw_symbol" — a plain ticker like "AAPL". Two other Databento symbologies are also supported by passing stype_in explicitly: "continuous" for rolling front-month notation like "ES.c.0", and "parent" for a whole contract root like "ES.FUT" (futures) or "AAPL.OPT" (the full options chain). Passing continuous or parent-style symbols with the default raw_symbol fails — this isn't optional, it has to match what the symbol actually is.

Supported schemas

reamer_py only consumes OHLCV bars, so schema is restricted to ohlcv-1s, ohlcv-1m, ohlcv-1h, and ohlcv-1d — any other Databento schema (e.g. trades, mbo) raises immediately, before any request is sent or any cost incurred.

Cost safety

Before fetching, the estimated cost of the request (via Databento's own cost-estimation API) is checked; if it exceeds $1 a warning is printed to stderr. This is a safety net against an accidentally wide date range, not a hard limit — the request still proceeds.

UTC-native: unlike CSV loading, data fetched via load_databento / load_databento_portfolio requires no manual timezone alignment — Databento serves UTC by default, and Reamer Stream's internal bar storage is always UTC epoch seconds.

reamer_py

import reamer_py
from engine.orders import buy_market, close_position

class Strategy:
    lookback = 1
    ticker = "SPX"

    def __init__(self, config_path=None):
        pass

    def on_bar(self, data):
        tv = data[self.ticker]
        if not tv.valid:
            return None
        if tv.position.qty == 0:
            return buy_market(1.0, ticker=self.ticker)
        return None

# Ingest the CSV into Reamer Stream under a ticker name (auto-detects format)
reamer_py.load_csv("spx.csv", "SPX")

# Configure execution
cfg = reamer_py.DefaultExecutionModelConfig()
cfg.commission_per_unit = 0.01
cfg.commission_mode     = "round_trip"
cfg.slippage            = 0.05
cfg.spread              = 0.10
cfg.ohlcv_type          = "bid"

strategy = Strategy()
result = reamer_py.run_backtest(
    data=["SPX"],
    strategy=strategy,
    alignment_mode="union",
    exec_config=cfg,
    initial_capital=10_000.0,
    leverage=1.0,
)

print(result.net_pnl)
print(len(result.closed_trades))

# strategy= embeds this file's own source alongside the results,
# so the .reamer file is a self-contained record
reamer_py.save_result(result, "my_backtest.reamer",
                      exec_config=cfg, data_paths={"SPX": "SPX"},
                      initial_capital=10_000.0, strategy=strategy)

Reading results

result.gross_pnl            # PnL before costs
result.net_pnl              # PnL after fees and slippage
result.total_fees           # all commission charges (including bracket exits)
result.total_slippage_cost  # all slippage costs (including bracket exits)
result.total_swap_cost      # cumulative overnight swap paid (positive = cost to longs)
result.trades               # int — completed round-trip count
result.closed_trades        # list[ClosedTrade]
result.order_log            # list[LiveOrder] — entry orders only, not bracket exits
result.open_orders_end      # list[LiveOrder] — orders still pending at end of run
result.returns              # list[float] — per-trade equity returns (Monte Carlo input)
Metrics like win_rate_pct, sharpe, and equity_dd_max are printed to the console but are not Python attributes on BacktestResult. Compute them from result.closed_trades if needed in code.

Iterating trades

wins = sum(1 for t in result.closed_trades if t.net_pnl > 0)
total = len(result.closed_trades)
win_rate = wins / total if total > 0 else 0.0

for trade in result.closed_trades:
    print(f"{trade.ticker}  entry={trade.entry_price:.2f}  "
          f"exit={trade.exit_price:.2f}  pnl={trade.net_pnl:.2f}")

Inspecting orders

for o in result.order_log:
    print(o.id, o.status, o.fill_price, o.reject_reason)

filled = [o for o in result.order_log if o.status == "Filled"]
rejected = [o for o in result.order_log if o.status == "Rejected"]

Monte Carlo

Bootstrap-resamples the per-trade return series (10,000 simulations by default) to produce synthetic equity curve distributions — shown in export_html_report()'s Monte Carlo section, and available directly from reamer_py:

mc = reamer_py.run_monte_carlo(result.returns, num_simulations=10000, initial_capital=100_000.0)

mc.risk_of_ruin              # fraction of runs that ever hit 0 equity
mc.probability_of_loss       # fraction of runs ending below initial_capital
mc.mean_final_equity, mc.median_final_equity
mc.worst_final_equity, mc.best_final_equity
mc.confidence_interval_95    # half-width of 95% CI around mean final equity
mc.max_drawdown_p5, mc.max_drawdown_p50, mc.max_drawdown_p95
mc.max_drawdown_per_run      # list[float], one entry per simulation
mc.per_trade_return_variance
MetricWhat it means
risk_of_ruinFraction of paths where equity hit zero. Healthy: < 0.05.
probability_of_lossFraction of simulations that ended below initial capital. Healthy: < 0.20.
confidence_interval_95Half-width of the 95% confidence interval around mean final equity.
max_drawdown_p95Worst-case drawdown at the 95th percentile — use for risk limit planning.

run_monte_carlo is not seeded — each call resamples independently, so statistics converge as num_simulations grows but aren't bit-reproducible across runs. Monte Carlo assumes i.i.d. returns — it does not model autocorrelation or regime change. Read alongside the primary backtest, not instead of it.

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 is downloadable to verify it directly.

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_py.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_py.
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"

Multi-asset strategies

result = reamer_py.run_backtest(
    data=["SPX", "EURUSD"],
    strategy=MyStrategy(),
    alignment_mode="union",     # the only supported mode
    exec_config=cfg,
    initial_capital=100_000.0,
)
ModeWhen on_bar firestv.valid
"union"Every timestamp from any tickerFalse when ticker has no bar at that step

"intersection" mode (only timestamps shared by every ticker) was removed — exact timestamp coincidence across every ticker is something real multi-instrument calendars essentially never give you. alignment_mode other than "union" now raises ValueError.

In union mode, always check tv.valid before acting — it is False when a ticker has no bar at the current step, which is common for mixed-schedule instruments (equities + forex).

Choosing a pattern: loop vs. arrays

Strategy typePattern to useWhy
Each ticker's decision depends only on its own dataPer-ticker loopSimple, readable, no extra allocation
Decisions compare tickers against each other (ranking, correlation, weights)Build 2D arrays firstVectorised numpy math across all tickers at once

Pattern A — per-ticker loop (independent decisions)

Use when each ticker's entry/exit logic depends only on that ticker's own data. Declare tickers as a class attribute.

class MyStrategy:
    tickers = ["AAPL", "SPY", "QQQ"]

    def on_bar(self, data):
        out = []
        for t in self.tickers:
            tv = data[t]
            if not tv.valid:
                continue
            if tv.close[-1] > tv.open[-1] and tv.position.qty == 0:
                out.append(buy_market(1.0, ticker=t))
        return out if out else None

Pattern B — 2D arrays (cross-sectional decisions) ✦ best practice

For any strategy that ranks, weights, or compares tickers against each other, build numpy arrays from the ticker views first, then apply vectorised logic. Do not use a loop with per-ticker conditionals for cross-sectional math — it is error-prone and defeats the purpose of numpy.

import numpy as np
from engine.orders import buy_market, close_position, orders

class MomentumRank:
    lookback = 20
    tickers = ["AAPL", "SPY", "QQQ", "GLD"]  # declare your tickers

    def __init__(self, config_path=None):
        self.n_long = 2   # hold top-N tickers by momentum

    def on_bar(self, data):
        tickers = self.tickers
        # Build arrays — one row per ticker, shape (N,) or (N, lookback)
        valid   = np.array([data[t].valid          for t in tickers])
        closes  = np.array([data[t].close[-1]      for t in tickers])
        oldest  = np.array([data[t].close[0]       for t in tickers])
        in_pos  = np.array([data[t].position.qty   for t in tickers]) > 0

        # Guard: skip if any ticker has no bar or insufficient history
        if not valid.all():
            return None

        # Cross-sectional momentum — vectorised, no per-ticker if/else
        momentum = closes / oldest - 1          # (N,) lookback-period return
        ranked   = np.argsort(momentum)[::-1]   # descending rank

        out = []
        for rank, i in enumerate(ranked):
            t = tickers[i]
            if rank < self.n_long and not in_pos[i]:
                out.append(buy_market(1.0, ticker=t))
            elif rank >= self.n_long and in_pos[i]:
                out.append(close_position(ticker=t))
        return out if out else None
Key arrays to build at the top of on_bar: (where tickers = self.tickers)
  • closes = np.array([data[t].close[-1] for t in tickers]) — current close, shape (N,)
  • matrix = np.stack([data[t].close for t in tickers]) — full history, shape (N, lookback)
  • valid = np.array([data[t].valid for t in tickers]) — bool mask, shape (N,)
  • in_pos = np.array([data[t].position.qty for t in tickers]) > 0 — position mask, shape (N,)
Then use np.argsort, np.corrcoef, matrix.mean(axis=1), etc. on the full arrays — never reconstruct them inside a loop.

Ticker keys are uppercase

Ticker keys passed to run_backtest are uppercase. Pass them directly to order helpers — no .upper() needed:

return buy_market(10.0, ticker="SPX")  # correct — no conversion

Per-ticker overrides

By default every ticker in a portfolio backtest shares one exec_config. Pass ticker_overrides to run_backtest() (reamer_py 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_py.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_py.DefaultExecutionModelConfig()
audjpy_cfg.spread = 0.0006
audjpy_cfg.set_swap_by_name("wednesday", 0.30)

result = reamer_py.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_py.load_exogenous_csv("aapl_earnings.csv", "aapl_earnings.exo.bin")

result = reamer_py.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_py.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_py.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_py, 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_py 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_py.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_py.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_py.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_py.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 lives in Reamer Stream, 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_py.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}")

License

reamer_py requires an active license to run backtests at all — there is no free or unlicensed tier. A license is a one-time purchase, tied to your machine and the major version it covers, not a subscription; it doesn't expire or renew on its own. Contact [email protected] for a free, time-limited test license to evaluate it first. Use constitutes acceptance of the license terms. Unauthorized redistribution, reverse engineering, or resale is prohibited.

For licensing enquiries contact [email protected].