- OpenBacktesting
- Strategy: define trading logic
Strategy: define trading logic¶
Subclass openbacktesting.Strategy and implement init() and next(). Pass the class to Backtest; the framework creates instances. A strategy normally computes indicators once and makes decisions on each bar.
Strategy · BarState · StrategyLog
A complete strategy¶
from openbacktesting import Backtest, Strategy, inputs
from openbacktesting.test import GOOG
class EmaCross(Strategy):
fast_length = inputs.int(10, title="Fast length", minval=1)
slow_length = inputs.int(30, title="Slow length", minval=2)
def init(self):
self.fast = self.ta.ema(self.close, length=self.fast_length)
self.slow = self.ta.ema(self.close, length=self.slow_length)
def next(self):
if len(self.close) < 2:
return
if not (self.fast.valid[-2:].all() and self.slow.valid[-2:].all()):
return
if self.ta.is_crossover(self.fast, self.slow) and not self.position:
self.buy(qty=1, tag="cross-up")
elif self.ta.is_crossunder(self.fast, self.slow) and self.position:
self.position.close()
result = Backtest(GOOG, EmaCross, end_policy="close_at_last_close").run()
print(result["# Trades"])
Lifecycle and visible data¶
| Method | When it runs | What to do there |
|---|---|---|
init() |
Once, with the full data history available | Register self.ta/self.ind indicators and static plots; initialize your own state. Do not place orders or calculate signals with future data. |
on_start() |
Once before the bar loop | Optional setup or logging; no trading. |
next() |
Every reached bar, including the first | Read the current prefix, check indicator validity, place/cancel orders, inspect positions. |
on_order(event) |
Batched order-status notifications | Inspect event.snapshot.order_id, event.snapshot.status, and event.snapshot.reason. New orders cannot execute recursively in that notification. |
on_fill(fill) |
After an actual execution | Inspect fill.price, fill.qty, and fees; a submitted order alone does not imply a fill. |
on_finish(summary) |
At termination, including errors | Inspect completion/reason/error or log a summary; no trading. |
next() is not delayed until indicators are ready. Check the channel's valid[-1] before consuming it; check length before [-2]. init() can see the future, so use trailing calculations there and retain handles, not precomputed future decisions.
See account snapshots and events for a complete callback example and all returned fields.
Data and state you use most often¶
| Attribute | Usage |
|---|---|
self.close[-1], self.high[-1], etc. |
Current bar values; [-2] is the previous bar. self.close is price data, not a close-position method. |
self.data_index[-1] |
Current timestamp or index value. |
self.bar_index |
Zero-based bar position; use self.bar_state.is_first/is_last for boundaries. |
self.data, self.df |
Additional columns and the currently visible DataFrame; see data. |
self.equity, self.account |
Current account value and a frozen balance/margin snapshot. |
self.position |
Aggregate exposure; close with self.position.close() or self.close_position(). |
self.orders |
Pending orders; call an order's cancel() to withdraw it. |
self.trades, self.closed_trades |
Open lots and closed fragments. Use trade.sl/trade.tp to change protection. |
self.closed_trade_stats |
Running closed-fragment counts and net profit, without rescanning all closed trades. |
self.instrument, self.timeframe |
Instrument rules and optional declared timeframe (None without context). |
self.log, self.plotting |
Logs and visual declarations. |
Place and manage orders¶
self.buy(qty=1) and self.sell(qty=1) return an Order. You may instead specify amount=1000 or percent=20 (20% of equity); these are mutually exclusive incremental sizes. All prices (limit, stop, sl, tp) are absolute prices. See orders for every parameter, exits, target positions, OCO, and execution timing.
Register custom calculations¶
def init(self):
self.mean = self.ind(
lambda series, length: series.rolling(length).mean(),
self.close.s, 20, name="Custom mean",
)
def next(self):
if self.mean.valid[-1]:
self.log.info(f"Mean: {self.mean[-1]:.2f}")
These methods belong inside your subclass. ind(func, *args, name=None, **kwargs) calls func during init() and wraps its result in an IndicatorHandle. Return a Series/DataFrame with exactly the market-data index, or a positional array of matching length. A DataFrame supplies multiple channels. Keep calculations causal; negative shifts introduce future information. Use self.ta for built-in formulas and see indicator handles for indexing and multi-output results.
Strategy¶
Strategy base class: Register indicators in init and place orders from currently visible history in next.
Attributes¶
Strategy.ind¶
Compute custom indicators in bulk during initialization and return read-only handles read through the backtest cursor.
Strategy.buy¶
Strategy.buy(
*,
qty=...,
amount=...,
percent=...,
limit=None,
stop=None,
sl=None,
tp=None,
tag=None,
reduce_only=False,
expires_at=None,
valid_bars=None,
)
Submit an incremental order; choose one of qty/amount/percent, where percent=20 means 20% of equity.
Strategy.init¶
Strategy initialization entry point for precomputation and indicator registration; complete historical data is accessible.
Strategy.next¶
Per-bar strategy callback, starting at input bar 0 and called once for each normally reached bar.
Strategy.sell¶
Strategy.sell(
*,
qty=...,
amount=...,
percent=...,
limit=None,
stop=None,
sl=None,
tp=None,
tag=None,
reduce_only=False,
expires_at=None,
valid_bars=None,
)
Submit an incremental order; choose one of qty/amount/percent, where percent=20 means 20% of equity.
Strategy.order_target_value¶
Strategy.order_target_value(
*,
target,
limit=None,
stop=None,
sl=None,
tp=None,
tag=None,
reduce_only=False,
expires_at=None,
valid_bars=None,
)
Convert target net notional to quantity using Close at request time; explicit conditions match quantity targets.
Strategy.order_target_quantity¶
Strategy.order_target_quantity(
*,
target,
limit=None,
stop=None,
sl=None,
tp=None,
tag=None,
reduce_only=False,
expires_at=None,
valid_bars=None,
)
Generate a difference order from the actual net position; always validate conditions and reuse identical active targets.
Strategy.order_target_percent¶
Strategy.order_target_percent(
*,
target,
limit=None,
stop=None,
sl=None,
tp=None,
tag=None,
reduce_only=False,
expires_at=None,
valid_bars=None,
)
Determine target net notional from equity percentage at request time; no continuous rebalancing or guaranteed fill weight.
Strategy.on_finish¶
Receive a frozen finish summary, including exceptional termination; trading is prohibited.
Strategy.close_position¶
Exit each lot; return an empty tuple when flat, while self.close remains market data.
Strategy.on_start¶
One notification after initialization completes; order submission is prohibited here.
Strategy.on_fill¶
Receive actual fill facts; callbacks do not recursively trigger matching.
Strategy.submit_oco¶
Atomically submit two entry requests; validate both first, prioritize first, and return a queryable group.
Strategy.on_order¶
Receive frozen order events; new requests execute no earlier than the next bar.
BarState¶
First/last snapshot at the current execution position; saved values do not change with the run cursor.
| Attribute | Type | Meaning |
|---|---|---|
is_first |
bool |
True on the first input bar. |
is_last |
bool |
True on the final input bar. |
Write logs¶
Call self.log.info("entry submitted"), .warning(...), or .error(...) with one string. Read them with bt.get_logs() after running. Records include bar/time and source location. Logging does not itself change order state or raise a strategy error.
StrategyLog¶
Read-only strategy log handle, accessed through self.log without author construction.
StrategyLog.info¶
Record informational messages, preserving empty strings, Chinese text, and newlines.
StrategyLog.warning¶
Record warning messages without changing strategy execution.
StrategyLog.error¶
Record error-level messages; authors must explicitly raise to stop the strategy.