Skip to main content
Three objects model the state of your strategy’s market activity: Order, Trade, and Position. Understanding them lets you write precise entry, exit, and risk management logic.

Lifecycle overview

1

Place an order

Call self.buy() or self.sell() in next(). This creates an Order object and queues it for execution.
order = self.buy(size=100, sl=95.0, tp=115.0)
2

Order fills

On a subsequent bar, the broker evaluates market conditions. If the order’s price conditions are met, it fills and becomes a Trade. Contingent SL/TP orders are created automatically.
3

Manage the trade

The Trade object is available in self.trades. Adjust stop-loss and take-profit in real time, or close the trade manually.
for trade in self.trades:
    trade.sl = self.data.Close[-1] * 0.98
4

Trade closes

When a trade is closed (by SL/TP, Trade.close(), or Position.close()), it moves to self.closed_trades and its P&L is settled.

Order

An Order is created by self.buy() or self.sell() and waits in the broker’s queue until its price conditions are met or it is canceled. All orders are Good Till Canceled — they remain active until they fill or you explicitly cancel them.

Properties

size
float
Order size (negative for short orders). If between 0 and 1, it is a fraction of available equity. If 1 or greater, it is an absolute number of units.
limit
float | None
Limit price. None for market orders. A long limit order fills when the price falls to or below this value; a short limit order fills when the price rises to or above it.
stop
float | None
Stop price that activates the order. When hit, the order becomes a market order (stop-market) or a limit order (stop-limit, if limit is also set). None once the stop has been triggered.
sl
float | None
Stop-loss price. After this order fills, a contingent stop-market order is automatically placed at this price to close the resulting trade.
tp
float | None
Take-profit price. After this order fills, a contingent limit order is automatically placed at this price to close the resulting trade.
tag
any
Arbitrary tracking value inherited by the resulting Trade.
is_long
bool
True if the order size is positive (a buy order).
is_short
bool
True if the order size is negative (a sell order).
is_contingent
bool
True for OCO bracket orders (SL/TP) placed automatically upon a trade. These orders are canceled when their parent trade closes.

Methods

order.cancel() — Remove the order from the queue. Also cancels contingent SL/TP bracket if applicable.
def next(self):
    # Cancel all pending long orders
    for order in self.orders:
        if order.is_long:
            order.cancel()

Order types

No limit or stop set. Fills on the next bar’s open (or current bar’s close if trade_on_close=True).
self.buy()                   # market buy at next open
self.sell(size=100)          # market sell, 100 units
For long orders, price ordering must hold: SL < limit/stop/market_price < TP. For short orders: TP < limit/stop/market_price < SL. Violations raise a ValueError.

Trade

A Trade is created when an Order fills. Active trades are available in self.trades; settled trades are in self.closed_trades.

Properties

size
int
Trade volume in units. Negative for short trades.
entry_price
float
Price at which the trade was opened.
exit_price
float | None
Price at which the trade was closed. None if the trade is still active.
entry_bar
int
Integer bar index when the trade was entered.
exit_bar
int | None
Integer bar index when the trade was exited. None if still active.
entry_time
pd.Timestamp | int
Timestamp (or bar integer) of trade entry.
exit_time
pd.Timestamp | int | None
Timestamp (or bar integer) of trade exit. None if still active.
pl
float
Profit (positive) or loss (negative) in cash units. Commissions are included only after the trade is closed.
pl_pct
float
Profit or loss as a percentage of the entry price, net of commissions.
value
float
Total trade value in cash: abs(size) × current_price.
is_long
bool
True if the trade is long.
is_short
bool
True if the trade is short.
sl
float | None
Current stop-loss price. Writable — assign a new value to move the stop, or assign None to cancel it.
tp
float | None
Current take-profit price. Writable — assign a new value to move the target, or assign None to cancel it.
tag
any
Arbitrary value inherited from the order that opened this trade.

Methods

trade.close(portion=1.) — Place a market order to close portion of the trade at the next available price. portion must be between 0 and 1.
def next(self):
    for trade in self.trades:
        if trade.pl_pct > 0.05:
            trade.close()       # close fully
        elif trade.pl_pct < -0.02:
            trade.close(0.5)    # close half

Managing SL and TP

SL and TP are stored as contingent orders on the trade. You can read and modify them at any time in next():
def next(self):
    for trade in self.trades:
        # Trail the stop-loss as price moves up
        new_sl = self.data.Close[-1] * 0.98
        if trade.sl is None or new_sl > trade.sl:
            trade.sl = new_sl

        # Move take-profit to lock in more profit
        trade.tp = self.data.Close[-1] * 1.05

        # Cancel the take-profit
        trade.tp = None
Assigning trade.sl or trade.tp creates or replaces the existing contingent bracket order. Assigning None cancels it. The underlying Order is managed automatically.

Tagging trades

Tags let you track orders and trades through their lifecycle:
def next(self):
    if signal_a:
        self.buy(tag='signal_a')
    if signal_b:
        self.buy(tag='signal_b')

    # Filter active trades by tag
    signal_a_trades = [t for t in self.trades if t.tag == 'signal_a']

Position

self.position is the aggregate of all currently active trades. It can be used in boolean context: if self.position: is True when any trade is open.

Properties

size
float
Net position size in units, summed across all active trades. Positive for net long, negative for net short, zero for flat.
pl
float
Aggregate unrealized profit or loss in cash across all active trades.
pl_pct
float
Aggregate unrealized P&L as a percentage of total invested capital.
is_long
bool
True if net position size is positive.
is_short
bool
True if net position size is negative.

Methods

position.close(portion=1.) — Close portion of every active trade. Equivalent to calling trade.close(portion) on each trade in self.trades.
def next(self):
    # Close everything
    self.position.close()

    # Close 50% of all active trades
    self.position.close(0.5)

FIFO position closing

By default (hedging=False), placing a trade in the opposite direction automatically closes existing trades in FIFO (First In, First Out) order before opening the new trade.
# hedging=False (default): buy() after being short closes the short first
bt = Backtest(data, MyStrategy)             # hedging=False

# hedging=True: simultaneous longs and shorts are allowed
bt = Backtest(data, MyStrategy, hedging=True)
self.sell(size=0.1) does not close an existing self.buy(size=0.1) trade unless exclusive_orders=True, or both trades were opened at the same price with zero spread and commission. Use Trade.close() or Position.close() to explicitly exit.

Exclusive orders mode

With exclusive_orders=True, each new call to self.buy() or self.sell() automatically:
  1. Cancels all pending non-contingent orders.
  2. Closes all open trades.
  3. Opens the new trade.
This enforces a single-position-at-a-time regime and simplifies signal-following strategies:
bt = Backtest(data, MyStrategy, exclusive_orders=True)
def next(self):
    if crossover(self.ma1, self.ma2):
        self.buy()   # automatically closes any short
    elif crossover(self.ma2, self.ma1):
        self.sell()  # automatically closes any long

Good-Till-Canceled semantics

All orders placed via self.buy() and self.sell() are Good Till Canceled. They remain in the queue across multiple bars until one of the following occurs:
  • The price condition is met and the order fills.
  • You call order.cancel() explicitly.
  • A new order is placed with exclusive_orders=True, which cancels all pending non-contingent orders.
  • The broker cancels the order due to insufficient margin (a warning is emitted).
def next(self):
    # Place a limit order — it stays active until filled or canceled
    if not self.orders:
        order = self.buy(limit=self.data.Close[-1] * 0.99)

    # Cancel if price has moved far away
    for order in self.orders:
        if self.data.Close[-1] > order.limit * 1.02:
            order.cancel()

Build docs developers (and LLMs) love