Skip to content

Strategy: define trading logic

View Markdown

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

Attribute Meaning
close Close-price series for the current run, usable directly with self.ta in init.
closed_trades Completed trades for individual-trade analysis.
data Currently visible original market-data entry point; full history in init and only the current prefix in next.
df Currently visible original market-data table; no guarantee of deep copying or content isolation.
equity Current account equity, including cash and position value.
high High-price series for the current run.
hl2 (High + Low) / 2, computed lazily per run.
hlc3 (High + Low + Close) / 3。
hlcc4 (High + Low + Close + Close) / 4, excluding Open.
low Low-price series for the current run.
open Open-price series for the current run; [-1] is the current value.
orders Orders awaiting execution; cancel may be called on these orders.
pip Retains full-sample decimal-precision estimation; does not represent an exchange minimum step.
position Current net-position aggregate, supporting size/profit reads and closure requests.
ta Indicator entry point for the current run, such as ta.ema(length=20) or ta.macd().histogram.
trades Currently held trades, distinct from settled closed_trades.
volume Volume series; missing Volume remains NaN.
instrument Asset rules for this run.
account Frozen snapshot of current balance, equity, actual fees, and margin.
plotting Pure plotting-data entry point for the current run; declare in init and write during runtime callbacks.
timeframe This chart's timeframe; None without low-level context, with no inference from market data.
data_index Currently visible index; saved pandas objects do not update with the cursor.
bar_index Current zero-based data position; unavailable in init, on_finish, and after the run ends.
bar_state Frozen first/last snapshot for the current data position; early termination does not relabel the current position as the last bar.
log Structured logs for the current run; use info, warning, and error inside author callbacks.
closed_trade_stats Read frozen statistics for closed portions, including allocated fees and excluding unrealized profit or loss.

Strategy.ind

Strategy.ind(func, *args, name=None, **kwargs)

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.init()

Strategy initialization entry point for precomputation and indicator registration; complete historical data is accessible.

Strategy.next

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

Strategy.on_finish(summary)

Receive a frozen finish summary, including exceptional termination; trading is prohibited.

Strategy.close_position

Strategy.close_position(portion=None)

Exit each lot; return an empty tuple when flat, while self.close remains market data.

Strategy.on_start

Strategy.on_start()

One notification after initialization completes; order submission is prohibited here.

Strategy.on_fill

Strategy.on_fill(fill)

Receive actual fill facts; callbacks do not recursively trigger matching.

Strategy.submit_oco

Strategy.submit_oco(*, first, second)

Atomically submit two entry requests; validate both first, prioritize first, and return a queryable group.

Strategy.on_order

Strategy.on_order(event)

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

StrategyLog.info(message)

Record informational messages, preserving empty strings, Chinese text, and newlines.

StrategyLog.warning

StrategyLog.warning(message)

Record warning messages without changing strategy execution.

StrategyLog.error

StrategyLog.error(message)

Record error-level messages; authors must explicitly raise to stop the strategy.

Backtest · Strategy parameters · User manual