Scope & intended use
Reamer targets mid-frequency OHLCV strategies — intraday to multi-day holding periods — across forex/CFD, crypto, futures, and equities.
- 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.
| Platform | Distribution | How to run |
|---|---|---|
| Windows | .exe installer | Run the installer, then launch Reamer from the Start menu |
| macOS | .dmg | Open the DMG, drag Reamer to Applications, double-click to open |
| Linux | .AppImage | chmod +x Reamer.AppImage && ./Reamer.AppImage |
reamer_py (headless / scripts / notebooks)
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)
- Install:
pip install --find-links https://reamerlabs.github.io/Reamer/index.html reamer-py— pip resolves the correct wheel for your platform automatically - 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.
- Open Reamer. Click the Data tab → Open… and select a
.reamerfile, or pass the path on the command line:reamer_gui my_backtest.reamer. - Results appear in Backtest Results. Step through bar-by-bar in Backtest Replay.
print()output appears in the Console sub-tab.
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"
| Field | Type | Description |
|---|---|---|
tv = data['TICKER'] | _TickerView | Zero-copy view for that ticker. 'TICKER' must be an uppercase string matching the key passed to run_backtest. |
tv.valid | bool | False when the ticker has no bar at the current step (union alignment). Always check before acting. |
tv.close[-1] | float64 | Current bar close. Arrays are shape (lookback,). Also: tv.open, tv.high, tv.low, tv.volume. |
tv.close[-2] | float64 | One bar ago. tv.close[0] is the oldest bar in the window. |
data.timestamp | str | Current step timestamp ("YYYY-MM-DD HH:MM:SS"). |
tv.position.qty | float | Open position size. 0.0 when flat. |
tv.position.side | int | 1=long, -1=short, 0=flat. |
tv.position.entry_price | float | Weighted-average entry fill price. 0.0 when flat. |
tv.position.unrealized | float | Unrealized 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
| Return | Effect |
|---|---|
None | No action this bar |
A dict | One order |
A list or tuple of dicts | Multiple 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.reamerfile. - 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
| Order | Fill price |
|---|---|
| Buy market | tick_ask + slippage at the fill tick within bar N |
| Sell market | max(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.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)
| Order | Trigger | Fill price |
|---|---|---|
| Buy limit | tick_ask ≤ limit_price | tick_ask + slippage (≤ limit_price + slippage) |
| Sell limit | tick_bid ≥ limit_price | max(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_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)
| Order | Trigger | Fill price |
|---|---|---|
| Buy stop | tick_ask ≥ stop_price (price rises to stop) | tick_ask + slippage |
| Sell stop | tick_bid ≤ stop_price (price falls to stop) | max(0, tick_bid − slippage) |
Limit vs. stop at a glance
| Limit | Stop | |
|---|---|---|
| Buy triggers when | price falls to or below your price | price rises to or above your price |
| Sell triggers when | price rises to or above your price | price falls to or below your price |
| Use for | entering on pullbacks | entering on breakouts |
| Fill price | tick_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 side | Take profit must be | Stop loss must be |
|---|---|---|
| Buy (long) | above entry price | below entry price |
| Sell (short) | below entry price | above 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:
| Exit | Trigger | Fill price (long) | Fill price (short) |
|---|---|---|---|
| Take profit | TP level touched | max(0, tick_bid − slippage) | tick_ask + slippage |
| Stop loss | SL level touched | max(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
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)
| TIF | Behavior |
|---|---|
"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)
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.qtyincreases;tv.position.entry_priceupdates to the weighted average. - Reversal via netting: close-side order with
qty > tv.position.qtycloses 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
| Tab | Purpose |
|---|---|
| Data | Open and switch between .reamer result files. Recent files are available for quick access within the session. |
| Backtest Replay | Step through the series bar-by-bar with full execution state: positions, orders, trade history, and Console output from the strategy's print() calls. |
| Backtest Results | Headline metrics and equity curve. |
| Monte Carlo | Distribution of 1000 bootstrapped equity curves: risk of ruin, drawdown percentiles, probability of profit. |
| Trade History | All completed round-trips with entry/exit prices, fees, slippage, PnL, and return on margin. |
Data tab
Open and switch between .reamer result files:
| Control | Action |
|---|---|
| Open… | Open a file picker to select a .reamer result file. The file and its .bin sidecars are loaded automatically. |
| Recent files | Quick-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
| Tab | Contents |
|---|---|
| Backtest Results | Total 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 Carlo | 1000 bootstrapped equity curves: mean/median/best/worst terminal equity, probability of loss, risk of ruin, drawdown percentiles (p5/p50/p95). |
| Trade History | All completed round-trips with entry/exit prices, fees, slippage, PnL, return on margin. Sortable columns. |
Export
| Format | Contents |
|---|---|
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)
| Field | Accepted header names |
|---|---|
| Date/time (combined) | date, datetime, ts, gmt_time, utc_datetime, open_time, ts_event, candle_begin_time, local_time |
| Separate time column | time, timestamp |
| Open | open, o |
| High | high, h, ask_high, bid_high |
| Low | low, l, ask_low, bid_low |
| Close | close, 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
| Format | Example | Source |
|---|---|---|
| Unix integers (s / ms / µs / ns) | 1704067200 | Databento, any Unix-epoch feed |
YYYY-MM-DD HH:MM:SS | 2024-01-15 09:30:00 | Alpaca, most REST APIs |
YYYY.MM.DD HH:MM:SS | 2024.01.15 09:30:00 | MT4 / MT5 |
YYYY/MM/DD HH:MM:SS | 2024/01/15 09:30:00 | Generic |
MM/DD/YYYY | 01/15/2024 | Yahoo Finance |
DD/MM/YYYY | 15/01/2024 | EU slash format |
DD.MM.YYYY HH:MM:SS | 15.01.2024 09:30:00 | Dukascopy |
YYYYMMDD HH:MM:SS | 20240115 09:30:00 | IBKR compact |
YYYYMMDDHHMMSS | 20240115093000 | Packed compact |
HH:MM | 09:30 | cTrader (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)
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:
| Metric | What it means |
|---|---|
risk_of_ruin | Fraction of paths where equity hit zero. Healthy: < 0.05. |
profitable_runs_pct | % of 1000 simulations that ended above initial capital. Healthy: > 80%. |
ci95_floor | 95th-percentile lower bound: equity stayed above this in 95% of resampled histories. |
max_dd_p95 | Worst-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 type | Trigger condition | Fill price |
|---|---|---|
| Market buy | always | tick_ask + slippage |
| Market sell | always | max(0, tick_bid − slippage) |
| Buy limit | tick_ask ≤ limit_price | tick_ask + slippage |
| Sell limit | tick_bid ≥ limit_price | max(0, tick_bid − slippage) |
| Buy stop | tick_ask ≥ stop_price | tick_ask + slippage |
| Sell stop | tick_bid ≤ stop_price | max(0, tick_bid − slippage) |
| Long TP exit | tick_bid ≥ take_profit | max(0, tick_bid − slippage) |
| Long SL exit | tick_bid ≤ stop_loss | max(0, tick_bid − slippage) |
| Short TP exit | tick_ask ≤ take_profit | tick_ask + slippage |
| Short SL exit | tick_ask ≥ stop_loss | tick_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 type | Meaning | Buy fill | Sell fill |
|---|---|---|---|
"bid" | Data is bid prices | tick + spread (pays spread) | tick |
"ask" | Data is ask prices | tick | tick − spread (pays spread) |
"midpoint" | Data is mid prices | tick + spread/2 | tick − spread/2 |
Commission
| Mode | Open leg | Close leg |
|---|---|---|
"round_trip" | qty × rate | qty × rate |
"open_only" | qty × rate | 0 |
"close_only" | 0 | qty × 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 class | 1 unit means | Example |
|---|---|---|
| Equities | 1 share | SPX: qty=100 = 100 shares |
| Crypto | 1 coin / token | BTC: qty=0.5 = 0.5 BTC |
| Forex | 1 unit of base currency | EURUSD: 1 unit ≈ $0.00013 P&L per pip |
| Commodities | 1 unit of underlying | Gold: check lot definition for your feed |
Forex lot equivalents
| Lot type | Units | qty value |
|---|---|---|
| Standard lot | 100,000 | qty=100000 |
| Mini lot | 10,000 | qty=10000 |
| Micro lot | 1,000 | qty=1000 |
| Nano lot | 100 | qty=100 |
Scale commission_per_unit to match. A broker charging $7 per standard lot → commission_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.DefaultExecutionModelConfig() before passing to run_backtest.GUI: execution config is embedded in the
.reamer file and displayed in the Backtest Results tab.
| Parameter | Units | Description |
|---|---|---|
initial_capital | $ | Starting equity. |
leverage | × | Max notional: new_notional + existing_notional ≤ equity × leverage. |
commission_per_unit | $ / unit | Fixed fee per unit traded. |
commission_mode | enum | "round_trip", "open_only", "close_only". |
slippage | price units | Mean slippage per fill. Absolute, not fractional. |
spread | price units | Bid-ask spread per fill. Direction depends on ohlcv_type. |
ohlcv_type | enum | "bid", "ask", "midpoint". |
rng_seed | int | Seed 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 / night | Overnight 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
| Parameter | Equities | Crypto | Forex |
|---|---|---|---|
commission_per_unit | 0.01 | 0.0005 | 0.0 |
slippage | 0.05 | 5.0 | 0.0001 |
spread | 0.10 | 10.0 | 0.0002 |
leverage | 1.0 | 1.0 | 100.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,
)
| Mode | When on_bar fires | tv.valid |
|---|---|---|
"union" | Every timestamp from any ticker | False when ticker has no bar at that step |
"intersection" | Only timestamps present in all tickers | Always True |
"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 type | Pattern to use | Why |
|---|---|---|
| Each ticker's decision depends only on its own data | Per-ticker loop | Simple, readable, no extra allocation |
| Decisions compare tickers against each other (ranking, correlation, weights) | Build 2D arrays first | Vectorised 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
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,)
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
| Tab | Contents |
|---|---|
| Positions | Open positions with size, entry price, unrealized PnL, TP/SL levels. |
| Open Orders | Pending limit and stop orders. Click ✕ to cancel. |
| Trade History | Completed round-trips with entry, exit, PnL, return. |
| Console | print() 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
reamer_py, inspect result.order_log / result.closed_trades in Python, then open the .reamer file in the Reamer GUI for visual replay.- Define your signal. Start with
on_barreturningNonealways. Add indicator computation andprint()the values. Run viareamer_pyto confirm the signal fires when expected. - Add market orders. Use
buy_marketandclose_positionfirst. Verify fills in the Replay view. - Improve entry with limits or stops. Replace
buy_marketwithbuy_limit(enter on pullback) orbuy_stop(enter on breakout). Checkresult.order_logfor how many expired vs. filled. - Add TP/SL brackets. Replace manual close logic. Check
result.closed_trades— bracket exits appear there, not inorder_log. - Add GTD expiry to limit/stop orders to prevent stale fills in changed conditions.
- Tune execution config. Add realistic slippage, spread, and commission. A strategy that breaks when costs are added has thin or no edge.
- Review Monte Carlo. Check
risk_of_ruin < 0.05andprofitable_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_tradesnotorder_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,
)
| Helper | Signature |
|---|---|
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_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
| Field | Type | Description |
|---|---|---|
gross_pnl | float | PnL before fees and slippage |
net_pnl | float | PnL after all costs |
total_fees | float | All commission charges including bracket exits |
total_slippage_cost | float | All slippage costs including bracket exits |
total_swap_cost | float | Cumulative overnight swap (positive = cost to longs) |
trades | int | Completed round-trip count |
closed_trades | list[ClosedTrade] | All completed trades including bracket exits |
order_log | list[LiveOrder] | Strategy-submitted orders with final status; bracket exits not included |
open_orders_end | list[LiveOrder] | Orders still pending at end of run |
returns | list[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 string | Cause | Fix |
|---|---|---|
"insufficient margin" | new_notional + existing_notional > equity × leverage | Reduce qty or increase leverage |
"invalid TP/SL for side" | TP on wrong side of entry price, or SL on wrong side | Buy: TP > entry, SL < entry. Sell: TP < entry, SL > entry. |
"qty must be specified for entry orders (qty > 0)" | Entry submitted with qty ≤ 0 | Always pass positive qty for entries |
"no market data" | Order submitted for a ticker with no bar at the current step | Check tv.valid before submitting in union mode |
"missing ticker" | Order has no ticker field and it couldn't be inferred | Always 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.