Documentation

Strategy SDK & User Guide

reamer_py is the core product — run backtests headlessly in Python scripts and notebooks, then open the resulting .reamer files in the Reamer GUI for visual replay and inspection.

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 context for writing strategies.

Scope & intended use

Reamer targets mid-frequency OHLCV strategies — intraday to multi-day holding periods — across forex/CFD, crypto, 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.
  • Long-horizon futures backtests spanning a contract roll. Continuous-contract roll adjustment is not handled — the backtest will show a false P&L jump/drop at the roll date.
  • Long-horizon equity backtests spanning a stock split or dividend. Corporate-action adjustment is not handled — same false-jump issue at the event date.

Forex/CFD is a direct, accurate match — spread, slippage, commission, and swap map onto real broker cost structure as-is. Crypto spot is fully supported (set swap to zero). Crypto perpetuals: swap approximates funding cost but isn't mechanistically identical (real funding is variable and basis-driven, recalculated every few hours; Reamer's swap_* is a fixed per-weekday rate). Futures and equities are well-supported for backtests that stay within a single contract or don't cross a corporate-action date — see Execution config reference for asset-class defaults.

Installation

GUI Desktop

Reamer is distributed as a self-contained desktop application. No Python installation, no terminal, no configuration required.

PlatformDistributionHow to run
Windows.exe installerRun the installer, then launch Reamer from the Start menu
macOS.dmgOpen the DMG, drag Reamer to Applications, double-click to open
Linux.AppImagechmod +x Reamer.AppImage && ./Reamer.AppImage

reamer_py (headless / scripts / notebooks)

Installs are temporarily broken. We're finishing deployment of the latest build — the command below will work again shortly. If you hit an install error right now, that's why; check back soon or contact support@reamerlabs.com.

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.github.io/Reamer/index.html 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

Path A — reamer_py (headless)

Installs are temporarily broken while we finish deploying the latest build — see the Installation section above.
  1. Install: pip install --find-links https://reamerlabs.github.io/Reamer/index.html 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

# Convert CSV to .bin (auto-detects format — Yahoo, Alpaca, MT4/MT5, etc.)
reamer_py.load_csv("data.csv", "data.bin")

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

result = reamer_py.run_backtest(
    data={"SPX": "data.bin"},
    strategy=MyFirstStrategy(),
    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}")

# Save for GUI replay
reamer_py.save_result(result, "my_backtest.reamer",
                      exec_config=cfg, data_paths={"SPX": "data.bin"},
                      initial_capital=10_000.0)

Path B — GUI Desktop

The Reamer GUI is a viewer for .reamer files produced by reamer_py. No Python required.

  1. Open Reamer. Click the Data tab → Open… and select a .reamer file, or pass the path on the command line: reamer_gui my_backtest.reamer.
  2. Results appear in Backtest Results. Step through bar-by-bar in Backtest Replay. print() output appears in the Console sub-tab.
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: if tv.close.shape[0] < period: return None.

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:

  • reamer_py: print() output goes to your terminal or notebook output, and is stored in the .reamer file.
  • GUI: captured per step and shown in the Console tab in Replay as you step through bars.
# 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.

GUI (Desktop)

The Reamer GUI opens .reamer result files produced by reamer_py. No Python or strategy editing required in the GUI.

Main workspace tabs

TabPurpose
DataOpen and switch between .reamer result files. Recent files are available for quick access within the session.
Backtest ReplayStep through the series bar-by-bar with full execution state: positions, orders, trade history, and Console output from the strategy's print() calls.
Backtest ResultsHeadline metrics and equity curve.
Monte CarloDistribution of 1000 bootstrapped equity curves: risk of ruin, drawdown percentiles, probability of profit.
Trade HistoryAll completed round-trips with entry/exit prices, fees, slippage, PnL, and return on margin.

Data tab

Open and switch between .reamer result files:

ControlAction
Open…Open a file picker to select a .reamer result file. The file and its .bin sidecars are loaded automatically.
Recent filesQuick-switch between previously opened .reamer files within the same session.

To produce a .reamer file, run a backtest with reamer_py and call reamer_py.save_result(). See reamer_py for the full workflow.

Results tabs detail

TabContents
Backtest ResultsTotal 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).
Monte Carlo1000 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. Sortable columns.

Export

FormatContents
HTML Report (.html)Self-contained report: equity curve chart, Monte Carlo chart, performance statistics table, trade history. Open in a browser and use File → Print → Save as PDF for shareable deliverables.
Order History CSV (.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.

Market data formats

Files processed by reamer_py.load_csv or the GUI Data tab 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)
reamer_py.load_csv("aapl.csv", "aapl.bin")
reamer_py.load_csv_portfolio({"AAPL": "aapl.csv", "SPY": "spy.csv"}, out_dir="data/")

# 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.bin")

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

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

# Convert CSV to .bin (auto-detects format)
reamer_py.load_csv("spx.csv", "spx.bin")

# 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"

result = reamer_py.run_backtest(
    data={"SPX": "spx.bin"},
    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))

# Save for GUI visualization
reamer_py.save_result(result, "my_backtest.reamer",
                      exec_config=cfg, data_paths={"SPX": "spx.bin"},
                      initial_capital=10_000.0)

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

After each backtest the engine bootstraps the per-trade return series 1000 times to produce synthetic equity curve distributions. Key statistics:

MetricWhat it means
risk_of_ruinFraction of paths where equity hit zero. Healthy: < 0.05.
profitable_runs_pct% of 1000 simulations that ended above initial capital. Healthy: > 80%.
ci95_floor95th-percentile lower bound: equity stayed above this in 95% of resampled histories.
max_dd_p95Worst-case drawdown at 95th percentile — use for risk limit planning.

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.

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
  • Crypto: 5.0 = $5 per BTC fill

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
Crypto1 coin / tokenBTC: qty=0.5 = 0.5 BTC
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

reamer_py: set fields on reamer_py.DefaultExecutionModelConfig() before passing to run_backtest.
GUI: execution config is embedded in the .reamer file and displayed in the Backtest Results tab.
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).

Typical defaults by asset class

ParameterEquitiesCryptoForex
commission_per_unit0.010.00050.0
slippage0.055.00.0001
spread0.1010.00.0002
leverage1.01.0100.0
ohlcv_type"bid""midpoint""bid"

Multi-asset strategies

result = reamer_py.run_backtest(
    data={"SPX": "spx.bin", "ETH": "eth.bin"},
    strategy=MyStrategy(),
    alignment_mode="union",     # or "intersection"
    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"Only timestamps present in all tickersAlways True
Use "union" for mixed-schedule instruments (equities + crypto). In union mode, always check tv.valid before acting — it is False when a ticker has no bar at the current step.

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

Replay & debugging

The Replay tab (GUI) advances the backtest bar-by-bar through the same execution model. The strategy's pre-recorded orders are loaded from the .reamer file — stepping through bars replays them so you can inspect every fill, position, and bracket exit in sequence.

Bottom panel tabs

TabContents
PositionsOpen positions with size, entry price, unrealized PnL, TP/SL levels.
Open OrdersPending limit and stop orders. Click ✕ to cancel.
Trade HistoryCompleted round-trips with entry, exit, PnL, return.
Consoleprint() output from the strategy, one entry per bar, prefixed with the bar timestamp.

Transport controls

↺ Rewind · |◀ Step Back · ▶ Play/Pause · ▶| Step Forward · +5 / +10 / +50 bars. Speed slider has 12 positions (800 ms/bar down to 50 bars/tick at 1 ms).

Allow Algo checkbox

When checked, the strategy's pre-computed orders are injected automatically at the correct bar. When unchecked, only manual orders from the order-entry panel are processed.

Console output format

[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 in order table

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. 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 open the .reamer file in the Reamer GUI for visual replay.
  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 in the Replay view.
  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 profitable_runs_pct > 80% for robustness.

Progression checklist

  • Signal fires on expected bars (verified via print + Console tab)
  • Market order fills are at the expected tick (verified via Replay)
  • 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)

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.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,                      # dict[str, str] — ticker → .bin file path
    strategy,                  # object with on_bar(self, data) method
    alignment_mode="union",    # "union" | "intersection"
    exec_config=DefaultExecutionModelConfig(),
    initial_capital=10000.0,
    leverage=1.0,
) -> 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,                # dict[str, str] — ticker → .bin path (stored relative to .reamer)
    initial_capital,           # float
    leverage=1.0,
    alignment_mode="union",
)

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

The Reamer GUI desktop application is free to download and use, with no license key required. reamer_py, the Python package that runs backtests and produces .reamer result files, requires a paid license. Use of either product constitutes acceptance of the license terms. Unauthorized redistribution, reverse engineering, or resale is prohibited.

For licensing enquiries contact hello@reamerlabs.com.