Scope & intended use
reamer_research targets mid-frequency OHLCV strategies — intraday to multi-day holding periods — across forex/CFD, 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.
- Equity OHLCV input must already be split/dividend-adjusted — required, not optional. reamer_research does not implement corporate-actions ingestion. Unadjusted input shows a fake price gap/loss at split dates and understates total return from missing dividends, with no engine-level detection. Use an already-adjusted (ideally total-return-adjusted) OHLCV source before loading.
Forex/CFD is a direct, accurate match — spread, slippage, commission, and swap map onto real broker cost structure as-is. Futures are well-supported across contract rolls — set roll_cycle_months and roll_days_before_monthend on the ticker's execution config to roll the continuous contract automatically (ratio-rebase adjustment) at a configurable point before month-end. Equities remain well-supported as long as input stays within a single corporate-action-free series — see Execution config reference for asset-class defaults.
Installation
Download the release tarball for your platform (Linux x86_64 or macOS arm64) from releases.reamerlabs.com. Extract and link against reamer_research_abi.h:
tar xzf reamer-research-3.2.0-linux_x86_64.tar.gz
cd reamer-research-3.2.0-linux_x86_64
gcc -c my_backtest.c -I./include
gcc my_backtest.o -L./lib -lreamer_research -o my_backtest
./my_backtest
Full integration guide: see DEPLOYMENT.md Step 3 in the release archive. Build a ReamerResearchStrategyVtable (the strategy callback interface), populate your backtest config and input data, and call reamer_research_run_backtest().
A complete backtest in C
#include "reamer_research_abi.h"
#include <stdio.h>
// Implement your strategy as a callback
size_t on_bar(void* user_data, const OhlcvBar* window, size_t window_len,
size_t ticker_count, ReamerOrderRequest* out_orders, size_t max_orders) {
// window[window_len - 1] is the current bar
// Return the number of orders written to out_orders (0 to max_orders)
return 0; // no action this bar
}
int main() {
// Set up bars, config, and strategy
ReamerResearchStrategyVtable strategy = {
.user_data = NULL,
.on_bar = on_bar
};
ReamerBacktestConfig config = {
.initial_capital = 10000.0,
.leverage = 1.0,
// ... set commission, slippage, spread, etc.
};
ReamerBacktestHandle result = NULL;
ReamerResearchStatus status = reamer_research_run_backtest(
&bars_by_ticker, bar_counts, ticker_ids, ticker_count,
&strategy, &config,
10000.0, 1.0);
if (status == REAMER_RESEARCH_SUCCESS) {
ReamerBacktestSummary summary;
reamer_research_get_summary(result, &summary);
printf("Net PnL: %.2f\n", summary.net_pnl);
reamer_research_free_result(result);
}
return 0;
}
See DEPLOYMENT.md for the complete Step 3 walkthrough: building the bars array, configuring execution costs, and reading results via the summary and closed-trades accessors.
Python quick start (reference, unsupported)
The reference-python/ ctypes binding in the Reamer Research release archive provides an alternative quick start for single-ticker market-order strategies with optional take-profit/stop-loss brackets. It is not a supported product interface — it is demonstration code showing how to wrap the C ABI in Python ergonomics.
from reamer_research_binding import run_backtest
from engine.orders import buy_market, close_position
class MyStrategy:
lookback = 1
def on_bar(self, data):
tv = data['SPX']
if tv.close[-1] > tv.close[-2]:
return buy_market(10.0, ticker='SPX',
take_profit=tv.close[-1] + 2.0,
stop_loss=tv.close[-1] - 1.0)
return None
result = run_backtest(MyStrategy, config, {'SPX': bars_array})
print(f"Net PnL: {result.net_pnl:.2f}")
This binding supports single-ticker strategies, market entries with brackets, and basic CSV loading. See reference-python/README.md for the complete feature list, templates, and examples.
tick_ask + slippage on bar N — not at bar N+1's open.
The strategy callback
C ABI signature
Implement a callback matching this signature:
size_t on_bar(
void* user_data,
const OhlcvBar* window, // borrowed: rolling-window array
size_t window_len, // bars in window[] (including current bar)
size_t ticker_count, // total tickers in this run
ReamerOrderRequest* out_orders, // caller-allocated output buffer
size_t max_orders) // max orders to write
| Parameter | Details |
|---|---|
user_data | Opaque pointer you provide during setup. Ignored by engine. Use to pass strategy state. |
window | Borrowed pointer to OhlcvBar` array — never allocate or free. Valid only for this call. |
window_len | Number of bars in window. Your requested lookback depth, or smaller on the first bars of a run. |
ticker_count | Total number of tickers in this backtest. The same callbacks fires for each ticker independently each step. |
out_orders | Caller-allocated buffer you populate with ReamerOrderRequest structs. Write-only, you do not own this memory. |
max_orders | Maximum number of orders this callback can write (typically 64). Return fewer if you wish. |
Return value and order marshaling
Return the count of orders actually written to out_orders (0 to max_orders). Each order is a ReamerOrderRequest struct with fields: ticker_id (0-based index into your ticker list), order_type, kind, side, tif, qty, limit_price, stop_price, take_profit, stop_loss, is_cancel, is_close. See reamer_research_abi.h for the complete struct definition and enum values.
No dynamic allocation crosses the ABI boundary
The engine passes borrowed pointers only — you never allocate or free memory for bars, orders, or results. This is the key to the ABI's stability: every resource is owned and managed entirely by one side (either your code or the engine), never shared.
Windowing and lookback
window_len reflects your requested lookback depth, or fewer on the first bars of the run when history doesn't yet exist. The window always has window[window_len - 1] as the current bar — no need for a separate "current bar" index.
Order types and fills
All order fills are priced against a deterministic synthetic tick sequence generated within each bar. The ABI supports market orders, limit orders, stop orders, and brackets (take-profit / stop-loss). See reamer_research_abi.h for the complete enum definitions (ReamerOrderKind, ReamerOrderType, ReamerOrderSide, ReamerOrderTimeInForce).
| Order type | Trigger / Fill price |
|---|---|
| Market buy | Always fills at tick_ask + slippage within bar N |
| Market sell | Always fills at max(0, tick_bid - slippage) within bar N |
| Buy limit | Fills when tick_ask ≤ limit_price at tick_ask + slippage |
| Sell limit | Fills when tick_bid ≥ limit_price at max(0, tick_bid - slippage) |
| Buy stop | Fills when tick_ask ≥ stop_price at tick_ask + slippage |
| Sell stop | Fills when tick_bid ≤ stop_price at max(0, tick_bid - slippage) |
Take-profit and stop-loss brackets
Attach automatic exits to any entry order via ReamerOrderRequest.take_profit and .stop_loss fields (both doubles, can be 0.0 to omit). The brackets arm after the entry fills and trigger independently based on price action in subsequent ticks.
| Exit type | 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 |
Time-in-force (TIF)
Set via ReamerOrderRequest.tif enum field:
| TIF | Behavior |
|---|---|
GTC | Good Till Cancelled — persists across bars until filled or end of run |
IOC | Immediate Or Cancel — fills on bar N only, cancelled at bar N's seal if unfilled |
GTD | Good Till Date — expires at a specific timestamp. Set via order's expiry_timestamp field (if present in struct) |
Timing: same-bar fill semantics
Orders submitted via callback at bar N can fill anywhere within bar N's tick sequence. Market orders fill at tick 1 (the first interpolated step away from bar.open, closely approximating but not exactly equaling the open). Limit and stop orders fill at whatever tick their condition is first satisfied.
rng_seed (default 42).
Positions and sizing
Position state at callback time
The engine provides your current position via the ReamerBacktestConfig.positions_callback or by storing position state in your user_data pointer. The reference-python binding exposes it as a simple float: data.position(ticker) returns the signed quantity (positive = long, negative = short, 0.0 = flat).
Margin check
An entry order is rejected if total notional (new + existing open positions) would exceed equity × leverage:
Position sizing helpers
A common pattern: size a position so that hitting a stop-loss costs exactly your risk budget.
Accessing results
After reamer_research_run_backtest() returns success, retrieve results via the ABI's accessor functions:
ReamerBacktestSummary summary;
reamer_research_get_summary(result, &summary);
printf("Net PnL: %.2f, Trades: %u\n", summary.net_pnl, summary.trade_count);
ReamerBacktestSummary contains scalar metrics: net_pnl, gross_pnl, total_fees, trade_count, win_rate, max_drawdown, sharpe_ratio, and others. See reamer_research_abi.h for the complete struct definition.
Closed trades
Iterate over all completed trades via reamer_research_get_closed_trades() — a poll-style accessor: you provide a buffer, the library fills it, and you call repeatedly for more:
ReamerClosedTrade trades[100];
size_t total_returned = 0;
size_t offset = 0;
while (1) {
size_t count = 0;
reamer_research_get_closed_trades(result, trades, 100, offset, &count);
if (count == 0) break;
for (size_t i = 0; i < count; i++) {
printf("Trade %u: entry %.2f, exit %.2f, pnl %.2f\n",
offset + i, trades[i].entry_price, trades[i].exit_price, trades[i].net_pnl);
}
offset += count;
}
Memory management
After you're done with results, always call reamer_research_free_result(result) to release the handle's backing memory (owned entirely by the library).
Fill prices
All fills use the prevailing synthetic tick bid or ask, adjusted for slippage. The tick sequence within each bar is deterministic given rng_seed. This page summarizes the behavior — the execution specification is the authoritative, tested definition, and the conformance test suite verifies every rule in it, included as part of an evaluation.
| Order 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
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 |
| 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_research.DefaultExecutionModelConfig() before passing to run_backtest. The config used is embedded in any .reamer file written by save_result, so it's always recoverable from the result record itself.
| 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_research. |
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). |
roll_cycle_months | list[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_monthend | business days | Business days before each cycle month's last calendar day that the roll fires. Default 5. |
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
| Parameter | Equities | Forex |
|---|---|---|
commission_per_unit | 0.01 | 0.0 |
slippage | 0.05 | 0.0001 |
spread | 0.10 | 0.0002 |
leverage | 1.0 | 100.0 |
ohlcv_type | "bid" | "bid" |
License
Reamer Research requires an active license key to run. Activation is offline and per-machine via the ./reamer-license activate CLI included in the release archive. See DEPLOYMENT.md Step 2 for the full activation flow. Contact [email protected] for licensing inquiries or a free, time-limited test license to evaluate before committing to a seat.
Per-ticker overrides
By default every ticker in a portfolio backtest shares one exec_config. Pass ticker_overrides to run_backtest() (reamer_research only) when specific tickers need genuinely different execution costs — the two common cases are per-ticker swap (each FX pair has its own overnight financing rate) and per-ticker commission (a broker charging a different schedule per instrument). Per-ticker spread/slippage/price_volatility/ohlcv_type are also supported the same way, for whenever tighter per-instrument realism is worth it.
eurusd_cfg = reamer_research.DefaultExecutionModelConfig()
eurusd_cfg.spread = 0.0002
eurusd_cfg.set_swap_by_name("wednesday", -0.15) # this broker rolls triple-swap on Wednesday
audjpy_cfg = reamer_research.DefaultExecutionModelConfig()
audjpy_cfg.spread = 0.0006
audjpy_cfg.set_swap_by_name("wednesday", 0.30)
result = reamer_research.run_backtest(
data=["EURUSD", "AUDJPY", "GBPUSD"],
strategy=MyStrategy(),
exec_config=global_cfg, # GBPUSD has no override, so it uses this unchanged
ticker_overrides={
"EURUSD": eurusd_cfg,
"AUDJPY": audjpy_cfg,
},
)
ticker_overrides is a dict[str, DefaultExecutionModelConfig], keyed by the same uppercase ticker strings used in data. A ticker with no entry in the dict always falls back to exec_config unchanged — never a silent zero-cost default.
ticker_overrides is non-empty, a stderr warning is printed for any traded ticker not covered by it, and for any override key that doesn't match a ticker in data (catches typos). rng_seed set inside a per-ticker override is ignored — the synthetic tick generator's seed always comes from exec_config.rng_seed. Pass the same ticker_overrides dict to save_result() that you passed to run_backtest(), so a later debug_trade() re-run against the saved .reamer file sees the same per-ticker config.
Exogenous data
Attach an arbitrary, freeform timeseries to any ticker — earnings surprises, macro prints, sentiment scores, analyst ratings, anything JSON-serializable — and read the latest-known-as-of-this-bar value from on_bar via tv.exogenous. Every timestamp in your simulation can carry structured context alongside the market data itself. The engine never parses, validates, or imposes a schema on the value — it only tracks which entry is current as of each bar. Parsing (json.loads or whatever the value actually is) is the strategy's own responsibility.
load_exogenous_csv(path, out) parses a two-column CSV into a .exo.bin sidecar file:
2024-01-03 00:00:00,{"eps_surprise": 0.03}
2024-01-15 00:00:00,{"eps_surprise": -0.01, "note": "guidance cut"}
Column 1 is a timestamp ("YYYY-MM-DD HH:MM[:SS]", "YYYYMMDD HH:MM[:SS]", or "YYYYMMDD"); column 2 is everything after the first delimiter to end of line, taken verbatim — a JSON value containing commas doesn't need quoting. Pass the resulting sidecar paths as run_backtest()'s exogenous arg — a sparse dict[str, str] keyed the same way as data/ticker_overrides. A ticker missing from exogenous simply never has tv.exogenous_valid == True.
reamer_research.load_exogenous_csv("aapl_earnings.csv", "aapl_earnings.exo.bin")
result = reamer_research.run_backtest(
data=["AAPL"],
strategy=MyStrategy(),
exogenous={"AAPL": "aapl_earnings.exo.bin"},
)
class MyStrategy:
def on_bar(self, data):
tv = data["AAPL"]
if not tv.exogenous_valid:
return None # no exogenous entry yet for this ticker at this step
info = json.loads(tv.exogenous)
if info.get("eps_surprise", 0) > 0.02:
return buy_market(10, ticker="AAPL")
return None
tv.exogenous is '' and tv.exogenous_valid is False both before the series' first entry and for any ticker with no series attached at all — always check .exogenous_valid first, exactly like .valid. save_result() needs no changes for exogenous data — pass the same exogenous dict to debug_trade() that you passed to the original run_backtest() call for the re-run to see the same values and stay bit-identical.
Replay & debugging
reamer_research.debug_trade() re-runs a backtest and calls breakpoint() before on_bar for every step in [trade.open_step - buffer, trade.close_step + buffer] — dropping you into whatever debugger PYTHONBREAKPOINT points at (pdb by default, or ipdb/VS Code/PyCharm) exactly where a specific trade's decisions were made. The direct replacement for stepping through a bar-by-bar replay UI: real line-by-line debugging in your own tools, driven live, instead of a bespoke widget.
reamer_research.debug_trade(
data, # same data= list passed to run_backtest
strategy, # a fresh instance of the same strategy class
trade=result.closed_trades[0],
exec_config=cfg, # same exec_config/ticker_overrides/etc. as the original run
buffer=5, # extra bars before/after the trade to step through
)
Console output
print() inside on_bar goes straight to your terminal or notebook, and is also captured into result.prints_by_step and stored in the .reamer file (via save_result) for later inspection — no separate console view needed:
[2024-01-02 09:30:00] bar close=130.14 in_pos=False
[2024-01-03 09:30:00] ENTRY signal at 129.99: sma_short=129.50 sma_long=128.80
[2024-01-04 09:30:00] bar close=128.51 in_pos=True
Order status strings
Status values are Title case: Filled, Pending, Cancelled, Expired, Rejected. String comparisons in Python are case-sensitive: o.status == "Filled" works; "filled" does not.
Determinism
The tick sequence within each bar is seeded by rng_seed (default: 42). Re-running the same backtest always produces identical fills — including a debug_trade() re-run, which reproduces the exact same fills as the original run. Change rng_seed to test sensitivity to tick ordering.
Strategy development workflow
reamer_research, inspect result.order_log / result.closed_trades in Python, then use debug_trade() to step into your own debugger for any trade that needs a closer look, or export_html_report() for a shareable summary.- Define your signal. Start with
on_barreturningNonealways. Add indicator computation andprint()the values. Run viareamer_researchto confirm the signal fires when expected. - Add market orders. Use
buy_marketandclose_positionfirst. Verify fills by inspectingresult.closed_tradesor viadebug_trade(). - 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.05andprobability_of_loss < 0.20for 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 inspectingresult.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_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) |
roll_log | list[RollEvent] | Applied futures roll events (ticker, timestamp, ratio). Present only for tickers with a configured roll_cycle_months that actually rolled during the run. |
LiveOrder
o.id # int — engine-assigned sequential ID (starts at 1)
o.created_timestamp # str — "YYYY-MM-DD HH:MM:SS"
o.closed_timestamp # str
o.ticker # str — uppercase
o.order_type_str # "buy", "sell", "buy_limit", "sell_limit", "buy_stop", "sell_stop"
o.tif_str # "GTC", "IOC", "GTD"
o.status # OrderStatus enum — compare with: o.status == "Filled"
o.reject_reason # str — non-empty when status is Rejected
o.qty # float
o.limit_price # float (0.0 for market orders)
o.stop_price # float (0.0 for non-stop orders)
o.take_profit # float (0.0 if not set)
o.stop_loss # float (0.0 if not set)
o.fill_price # float (0.0 if not filled)
o.fees # float
o.slippage_cost # float
ClosedTrade
ct.ticker # str — symbol
ct.side # Side enum; ct.side == "buy" works
ct.open_timestamp # str
ct.close_timestamp # str
ct.entry_price # float — includes spread and slippage
ct.exit_price # float — includes spread and slippage
ct.qty # float
ct.leverage # float
ct.margin_used # float
ct.fees # float — combined open + close commission (including bracket exits)
ct.slippage # float — combined open + close slippage cost
ct.gross_pnl # float — (exit - entry) × qty × ±1
ct.net_pnl # float — gross_pnl - fees
ct.return_pct # float — net_pnl / margin_used × 100
ct.open_step # int — bar index when position was opened (read-only)
ct.close_step # int — bar index when position was closed (read-only)
ct.open_tick_i # int — synthetic tick index within bar when opened (read-only)
ct.close_tick_i # int — synthetic tick index within bar when closed (read-only)
OhlcvBar
No-argument constructor — set fields individually:
b = reamer_research.OhlcvBar()
b.timestamp = "2024-01-15 09:30:00" # "YYYY-MM-DD HH:MM:SS"
b.open = 130.0
b.high = 131.5
b.low = 129.5
b.close = 131.0
b.volume = 1_500_000.0
All timestamps must be in "YYYY-MM-DD HH:MM:SS" format and in ascending order within each ticker's bar list.
DefaultExecutionModelConfig
cfg = reamer_research.DefaultExecutionModelConfig()
cfg.commission_per_unit = 0.01
cfg.commission_mode = "round_trip" # or CommissionMode.RoundTrip
cfg.slippage = 0.05
cfg.spread = 0.10
cfg.ohlcv_type = "bid" # or OhlcvType.Bid
cfg.price_volatility = 0.0 # 0 = deterministic fills
cfg.rng_seed = 42
cfg.roll_cycle_months = [3, 6, 9, 12] # futures only; empty = disabled (default)
cfg.roll_days_before_monthend = 5 # default
cfg.set_swap(1, 1.5) # Monday swap: $1.50/unit/night
cfg.get_swap(1) # returns 1.5
run_backtest signature
reamer_research.run_backtest(
data, # list[str] — ticker names, already ingested via
# load_csv/load_csv_portfolio/write_bin
strategy, # object with on_bar(self, data) method
alignment_mode="union", # the only supported mode
exec_config=DefaultExecutionModelConfig(),
initial_capital=10000.0,
leverage=1.0,
show_progress=True, # live ETA to stderr; auto-suppressed when stderr isn't a terminal
ticker_overrides={}, # dict[str, DefaultExecutionModelConfig] — see Per-ticker overrides
exogenous={}, # dict[str, str] — ticker → .exo.bin path, see Exogenous data
) -> BacktestResult
save_result signature
reamer_research.save_result(
result, # BacktestResult from run_backtest
path, # output .reamer file path
exec_config, # DefaultExecutionModelConfig used in the run
data_paths, # sparse dict[str, str] — ticker → placeholder recordkeeping
# value (data is addressed by ticker,
# not by file path) — pass {t: t for t in data}
initial_capital, # float
leverage=1.0,
alignment_mode="union",
asset_class="equity", # "equity" | "fx" | "futures" — report/trade-display hint only
show_progress=True, # live ETA to stderr; auto-suppressed when stderr isn't a terminal
ticker_overrides={}, # pass the same dict given to run_backtest, if any
strategy=None, # same class/instance passed to run_backtest — embeds its full
# source in the .reamer file when given (see below)
)
When strategy is passed, the full source of the module the strategy class is defined in — imports, helpers, constants included, not just the class body — is embedded in the .reamer file as strategy_code, alongside strategy_class_name. A .reamer file is plain JSON, so opening it directly in any text editor shows the exact code that produced the results next to the results themselves — a self-contained record for long-term reference, not just something debug_trade() can re-run. Best-effort: omitted if the source can't be retrieved (e.g. a class defined interactively in a REPL), which never fails the save.
run_monte_carlo signature
reamer_research.run_monte_carlo(
returns, # list[float] — typically result.returns
num_simulations=10000,
initial_capital=10000.0,
) -> MonteCarloStats
Bootstrap-resamples the returns series and returns the same statistics export_html_report()'s Monte Carlo section shows. Not seeded — each call resamples independently, so results are not bit-reproducible across runs. See Monte Carlo.
Rejection reasons
When an order is rejected, o.reject_reason contains one of these strings:
| Reason 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}")