Engine & Backtest¶
The core backtest engine. Load OHLCV data once, build a signal set, then run. Bars are merged across symbols by timestamp; market orders fill at the bar close.
Engine¶
| Parameter | Type | Default | Description |
|---|---|---|---|
initial_capital |
float |
100000.0 |
Starting capital |
fee_rate |
float |
0.0001 |
Fee rate per trade (percentage mode) |
Loading¶
Each loader registers (or overwrites) one named symbol. When symbol is omitted, load_csv infers the name from the filename and load_ohlcv / load_df use "default". Timestamps are auto-normalized to nanoseconds (seconds, milliseconds and microseconds are detected by magnitude).
load_csv(path, symbol='')¶
Load bars from a headed CSV with columns timestamp,open,high,low,close,volume in that order. The symbol name is derived from the file stem (uppercased, with a trailing _1m / _5m / _15m / _1h / _4h / _1d stripped) unless symbol is given.
engine.load_csv("./data/btcusdt_1h.csv") # symbol -> "BTCUSDT"
engine.load_csv("./data/eth.csv", symbol="ETHUSDT")
Raises FloxError(code="E_IO_001") if the file cannot be opened.
load_ohlcv(data, symbol='')¶
Load bars from a dict of numpy arrays.
engine.load_ohlcv({
"ts": timestamps, # int64 — 'timestamp' is also accepted
"open": opens,
"high": highs,
"low": lows,
"close": closes,
"volume": volumes,
}, symbol="BTCUSDT")
| Key | Type | Description |
|---|---|---|
ts or timestamp |
int64[] |
Bar timestamps (s, ms, us or ns — auto-detected) |
open |
float64[] |
Open prices |
high |
float64[] |
High prices |
low |
float64[] |
Low prices |
close |
float64[] |
Close prices |
volume |
float64[] |
Volume |
A missing key raises FloxError(code="E_KEY_001").
load_df(df, symbol='')¶
Load bars from a pandas DataFrame with open, high, low, close, volume columns. The timestamp column is the first of ts, timestamp, open_time, time that exists; if none does, the DataFrame index is used.
resample(symbol, target, interval)¶
Resample an already-loaded symbol into a new symbol named target.
| Parameter | Type | Description |
|---|---|---|
symbol |
str |
Source symbol name |
target |
str |
Name of the resampled symbol |
interval |
str |
Bucket size — number plus s, m, h or d |
An unknown unit raises FloxError(code="E_TIME_001").
Running¶
run(signals, default_symbol=0) -> Stats¶
Run a backtest over every loaded symbol. signals is a SignalBuilder. Signals built without an explicit symbol name are routed to default_symbol (a numeric symbol ID); 0 means the first loaded symbol.
Referencing an unregistered symbol name raises FloxError(code="E_SYM_001").
Accessors¶
| Member | Type | Description |
|---|---|---|
symbols |
list[str] |
Registered symbol names (property) |
bar_count(symbol='') |
int |
Number of bars loaded for a symbol |
ts(symbol='') |
float64[] |
Bar timestamps in nanoseconds |
open(symbol='') |
float64[] |
Open prices |
high(symbol='') |
float64[] |
High prices |
low(symbol='') |
float64[] |
Low prices |
close(symbol='') |
float64[] |
Close prices |
volume(symbol='') |
float64[] |
Volume |
An empty symbol selects the first symbol loaded. Calling any accessor before loading data raises FloxError(code="E_RUN_002").
SignalBuilder¶
Accumulates the orders a backtest should submit. Timestamps are normalized the same way as bar timestamps; each signal fires on the first bar whose timestamp is at or past it.
signals = flox.SignalBuilder()
signals.buy(ts_ns, 1.0)
signals.limit_sell(ts_ns, 51_000.0, 1.0, "BTCUSDT")
len(signals)
| Method | Description |
|---|---|
buy(ts, qty, symbol='') |
Market buy |
sell(ts, qty, symbol='') |
Market sell |
limit_buy(ts, price, qty, symbol='') |
Limit buy |
limit_sell(ts, price, qty, symbol='') |
Limit sell |
clear() |
Drop all accumulated signals |
__len__() |
Signal count |
symbol is a registered symbol name. Left empty, the signal is routed to run()'s default_symbol.
Stats¶
Returned by Engine.run(). Fields are readable as attributes or by key, and to_dict() returns the whole set as a plain dict.
stats = engine.run(signals)
stats.sharpe_ratio # attribute access
stats["sharpe_ratio"] # key access
stats.to_dict() # dict of every field
repr(stats) # Stats(trades=... pnl=... ret=...% sharpe_ratio=... dd=...%)
| Field | Type | Description |
|---|---|---|
total_trades |
int |
Round-trip trade count |
winning_trades |
int |
Profitable trade count |
losing_trades |
int |
Losing trade count |
initial_capital |
float |
Starting capital |
final_capital |
float |
Ending capital |
total_pnl |
float |
Gross PnL |
total_fees |
float |
Total fees paid |
net_pnl |
float |
PnL after fees |
gross_profit |
float |
Sum of winning trades |
gross_loss |
float |
Sum of losing trades |
max_drawdown |
float |
Maximum drawdown (absolute) |
max_drawdown_pct |
float |
Maximum drawdown (percentage) |
win_rate |
float |
Fraction of winning trades |
profit_factor |
float |
Gross profit / gross loss |
avg_win |
float |
Average winning trade |
avg_loss |
float |
Average losing trade |
sharpe_ratio |
float |
Annualized Sharpe ratio |
sortino_ratio |
float |
Annualized Sortino ratio |
calmar_ratio |
float |
Calmar ratio |
return_pct |
float |
Net return percentage |
BacktestResult.stats() in Backtest Components returns a dict with a different, larger key set — the ratios are named sharpe_ratio / sortino_ratio / calmar_ratio there.
Example¶
import numpy as np
import flox_py as flox
n = 500
rng = np.random.default_rng(42)
ts = 1_700_000_000_000_000_000 + np.arange(n, dtype=np.int64) * 3_600_000_000_000
close = 30_000.0 + np.cumsum(rng.normal(0.0, 25.0, n))
engine = flox.Engine(initial_capital=100_000, fee_rate=0.0001)
engine.load_ohlcv({
"ts": ts,
"open": close,
"high": close + 5.0,
"low": close - 5.0,
"close": close,
"volume": np.full(n, 1.0),
}, symbol="BTCUSDT")
closes = engine.close("BTCUSDT")
fast = flox.ema(closes, 10)
slow = flox.ema(closes, 30)
signals = flox.SignalBuilder()
for i in range(1, n):
if fast[i] > slow[i] and fast[i - 1] <= slow[i - 1]:
signals.buy(int(ts[i]), 1.0, "BTCUSDT")
elif fast[i] < slow[i] and fast[i - 1] >= slow[i - 1]:
signals.sell(int(ts[i]), 1.0, "BTCUSDT")
stats = engine.run(signals)
print(f"Signals: {len(signals)}, trades: {stats.total_trades}")
print(f"Net PnL: {stats.net_pnl:.2f}, Sharpe: {stats.sharpe_ratio:.4f}")