What is a bracket order?
In quantitative trading and execution modeling, a bracket order is a composite multi-order structure designed to automate trade management from entry to exit. It bounds a market position within strict, predefined risk-reward parameters immediately upon submission, eliminating the latency and emotional interference of discretionary position monitoring.
A standard bracket order consists of three interconnected orders:
- Parent Entry Order: The initial order that establishes the position. This can be a Market order, a Limit order, or a Stop entry order.
- Child Take-Profit Order: A resting Limit order placed at a favorable price above the entry (for a long position) or below the entry (for a short position) to lock in profit when the target price is reached.
- Child Stop-Loss Order: A protective Stop or Stop-Limit order placed at an adverse price level below the entry (for a long) or above the entry (for a short) to truncate capital loss if the market moves against the position.
The two child orders operate in a strict One-Cancels-Other (OCO) relationship. Once the parent entry order fills, both child orders become active in the broker's matching queue. When either the take-profit target or the protective stop-loss is triggered and executed, the remaining child order is cancelled immediately to prevent unintended reverse market exposure.
flowchart TD
subgraph Composite ["Composite Bracket Order Structure"]
Parent["Parent Entry Order\n(Market / Limit / Stop)"]
ChildTP["Child 1: Take-Profit Limit\n(Target Price P_tp)"]
ChildSL["Child 2: Stop-Loss Stop\n(Stop Price P_sl)"]
end
Parent -->|"Fills (Position Opened)"| ActiveChildren["Activate Resting Child Orders"]
ActiveChildren --> ChildTP
ActiveChildren --> ChildSL
ChildTP <-->|"OCO Mutual Cancellation Link"| ChildSL
ChildTP -->|"Target Reached (Fills)"| CloseProfit["Position Closed in Profit\n(Cancels Stop-Loss)"]
ChildSL -->|"Stop Triggered (Fills)"| CloseLoss["Position Truncated in Loss\n(Cancels Take-Profit)"]Mathematical formulation of long and short brackets
For a long bracket order entering at price P_{\text{entry}}:
P_{\text{sl}} < P_{\text{entry}} < P_{\text{tp}}The predefined monetary risk per share is \Delta_{\text{risk}} = P_{\text{entry}} - P_{\text{sl}}, and the expected target gain is \Delta_{\text{reward}} = P_{\text{tp}} - P_{\text{entry}}. The asymmetric risk-reward ratio (RRR) is defined as:
\text{RRR} = \frac{P_{\text{tp}} - P_{\text{entry}}}{P_{\text{entry}} - P_{\text{sl}}}Conversely, for a short bracket entering at P_{\text{entry}}:
P_{\text{tp}} < P_{\text{entry}} < P_{\text{sl}}\text{RRR} = \frac{P_{\text{entry}} - P_{\text{tp}}}{P_{\text{sl}} - P_{\text{entry}}}By structuring executions into atomic risk containers, bracket orders ensure that a quantitative strategy never holds an unhedged position without an active exit mechanism.
Implementing bracket orders in Backtrader: buy_bracket() and sell_bracket()
In Backtrader, bracket orders are managed via convenience methods on the bt.Strategy class: self.buy_bracket() for long positions and self.sell_bracket() for short positions.
Method signature and parameter mechanics
def buy_bracket(
data=None,
size=None,
price=None,
plimit=None,
exectype=bt.Order.Market,
valid=None,
tradeid=0,
oargs={},
stopprice=None,
stopexec=bt.Order.Stop,
stopargs={},
limitprice=None,
limitexec=bt.Order.Limit,
limitargs={},
**kwargs
) -> list[bt.Order]:
...The key parameters controlling bracket execution include:
| Parameter | Type | Default | Description | Execution Role |
|---|---|---|---|---|
exectype | bt.Order type | bt.Order.Market | Execution type for the parent entry order (Market, Limit, Stop). | Establishes entry condition. |
price | float | None | Price for the parent order when exectype is Limit or Stop. | Ignored if exectype=bt.Order.Market. |
limitprice | float | None | Absolute price target for the child take-profit order. | Mandatory for target leg. |
limitexec | bt.Order type | bt.Order.Limit | Order execution type for the take-profit leg (Limit or StopLimit). | Fills at or better than limitprice. |
stopprice | float | None | Absolute price threshold for the child stop-loss order. | Mandatory for stop leg. |
stopexec | bt.Order type | bt.Order.Stop | Order execution type for the stop leg (Stop or StopLimit). | Triggers market order when breached. |
size | float | None | Position size. Applied uniformly across parent, stop, and limit legs. | Ensures position is fully closed on exit. |
valid | datetime / int | None | Expiration validity for the bracket orders (Good-'Til-Cancelled if None). | Controls time-in-force. |
[!IMPORTANT] Absolute Prices vs. Offsets: Unlike some broker APIs that accept relative point distances (e.g.,
stop_distance=1.5), Backtrader'sstoppriceandlimitpricerequire absolute price levels. If you enter at $100 and want a $2 stop loss, you must passstopprice=98.0, notstopprice=2.0. Passing relative distances will cause the stop order to be placed near zero, exposing your strategy to total capital liquidation.
Underlying chained orders: parent, transmit, and OCO
Behind the scenes, self.buy_bracket() does not submit a single monolithic order object. Instead, it constructs a chain of three distinct bt.Order instances and coordinates them through Backtrader's internal order dispatch flags:
# 1. Parent Entry Order (held from immediate transmission)
parent = self.buy(
exectype=exectype,
price=price,
size=size,
transmit=False, # Hold transmission until children are attached
**oargs
)
# 2. Take-Profit Child Order (linked to parent)
tp_order = self.sell(
exectype=limitexec,
price=limitprice,
size=size,
parent=parent, # Bound to parent fill
transmit=False,
**limitargs
)
# 3. Stop-Loss Child Order (linked to parent and bound to tp_order via OCO)
sl_order = self.sell(
exectype=stopexec,
price=stopprice,
size=size,
parent=parent, # Bound to parent fill
oco=tp_order, # OCO link: filling sl_order cancels tp_order
transmit=True, # Release entire bracket packet to the broker
**stopargs
)
return [parent, tp_order, sl_order]The method returns a 3-element list [parent, tp_order, sl_order]. Backtrader uses the transmit=False flag to buffer the parent and first child in memory, dispatching all three orders as an atomic transaction to BackBroker only when transmit=True is executed on the final leg.
Order lifecycle states and event transitions
Understanding the state machine governing Backtrader brackets is vital for debugging fills, tracking order references, and avoiding ghost executions.
stateDiagram-v2
[*] --> ParentSubmitted: buy_bracket() called
ParentSubmitted --> ParentAccepted: Broker acknowledges
ParentAccepted --> ParentCompleted: Parent fills on Bar t+1
ParentAccepted --> ParentCancelled: Parent cancelled / rejected
ParentCancelled --> [*]: Children automatically cancelled
state ParentCompleted {
[*] --> ChildrenActive: Activate child orders
ChildrenActive --> TP_Accepted
ChildrenActive --> SL_Accepted
}
TP_Accepted --> TP_Completed: High >= limitprice
TP_Completed --> SL_Cancelled: OCO trigger cancels Stop
SL_Cancelled --> [*]: Position closed (Profit)
SL_Accepted --> SL_Completed: Low <= stopprice
SL_Completed --> TP_Cancelled: OCO trigger cancels Limit
TP_Cancelled --> [*]: Position closed (Loss)Stage 1: Parent order submission and queueing
When buy_bracket() executes inside next(), all three orders receive unique references (order.ref). The parent order moves to Order.Submitted and subsequently Order.Accepted by the broker. At this point, the child stop and limit orders remain dormant in an inactive state (Order.Created). They cannot be filled by the broker matching engine while the parent order has not executed.
Stage 2: Parent execution and child activation
When market conditions satisfy the parent order (for example, on the opening tick of the next bar for a Market order), the parent transitions to Order.Completed. Upon processing the parent fill, BackBroker immediately promotes both child orders:
- The take-profit order transitions to
Order.Accepted. - The stop-loss order transitions to
Order.Accepted. - Both orders are placed into the broker's active order matching ledger for subsequent bar evaluations.
Stage 3: Parent rejection or cancellation
If the parent order fails—due to margin insufficiency (Order.Margin), bar expiration (Order.Expired), or an explicit user cancellation via self.cancel(parent)—the simulated broker cascades the cancellation down the hierarchy. Because the parent position was never established, both child orders transition directly to Order.Cancelled without entering the active matching queue.
Stage 4: Child execution and OCO mutex cancellation
As historical bars advance, the broker matches resting child orders against incoming bar prices (Open, High, Low, Close):
- Scenario A (Take-Profit Triggered): If the bar's
Highreaches or exceedslimitprice, the take-profit order fills (Order.Completed). Backtrader checks theocoattribute of the order, locates the companion stop-loss order reference, and immediately callsbroker.cancel(order.oco). The stop-loss order transitions toOrder.Cancelled. - Scenario B (Stop-Loss Triggered): If the bar's
Lowbreaches or drops belowstopprice, the stop order triggers and fills (Order.Completed). Backtrader evaluates the reciprocal OCO relationship and immediately cancels the resting take-profit limit order.
| Lifecycle Event | Parent State | Take-Profit Child State | Stop-Loss Child State | Broker Action |
|---|---|---|---|---|
| Submission | Submitted | Created (Dormant) | Created (Dormant) | Orders batched via transmit flags. |
| Broker Acceptance | Accepted | Created (Dormant) | Created (Dormant) | Parent queued for matching on next tick. |
| Parent Fill | Completed | Accepted (Active) | Accepted (Active) | Position opened; children activated. |
| Target Reached | Completed | Completed (Sold) | Cancelled | OCO cancels stop; position flat. |
| Stop Breached | Completed | Cancelled | Completed (Sold) | OCO cancels limit; position flat. |
| Parent Expired/Margin | Margin / Canceled | Cancelled | Cancelled | Children purged; zero exposure. |
Edge cases and failure modes in bracket execution
While Backtrader's bracket orders function smoothly under standard market conditions, quantitative researchers must anticipate four critical failure modes when backtesting volatile assets.
1. Partial fills on the parent order
In Backtrader, if an entry order fills partially (Order.Partial) because bar volume limits were configured on the broker (cerebro.broker.set_filler(...)), child orders present an exposure mismatch. In default configurations, child order sizes are fixed to the original requested size. If a 100-share parent only fills 60 shares, the child exit orders may remain sized at 100 shares. When the stop or target fills, the broker executes a 100-share exit, flattening the 60-share long and inadvertently opening a 40-share short position.
[!WARNING] If you utilize realistic volume fillers or partial fill simulation in Backtrader, always inspect
order.executed.sizeinnotify_order()and manually synchronize child order sizes to prevent unintended position reversal.
2. Bar gapping beyond both stop and limit (The intra-bar paradox)
On daily equity bars with wide earnings gaps or crypto flash-crashes, a single bar's range can simultaneously encompass both the stop price and the limit price:
\text{Low}_t \le P_{\text{sl}} \quad \text{and} \quad \text{High}_t \ge P_{\text{tp}}When this occurs, historical daily OHLCV data lacks the sub-bar tick sequence required to determine which level was struck first.
flowchart LR
A["Bar Opens between SL and TP\nLow <= Stop and High >= Target"] --> B{"Broker Matching Priority"}
B -->|"Evaluates High First"| C["Take-Profit Fills\nMax Gain Recorded"]
B -->|"Evaluates Low First"| D["Stop-Loss Fills\nMax Loss Recorded"]Backtrader's BackBroker processes resting orders in the sequential order they appear in its internal list. If the stop order was evaluated prior to the limit order, the stop fills and the limit is cancelled, resulting in a loss. If the limit order was evaluated first, the target fills, resulting in a profit. This arbitrary execution sequence creates intra-bar path dependency bias, artificially distorting strategy Sharpe ratios.
3. Orphan child orders on premature strategy exits
A widespread implementation flaw occurs when a strategy implements multiple exit rules. For example, a strategy enters via buy_bracket(), but also monitors an indicator exit (such as an RSI overbought threshold or moving average crossover) inside next():
def next(self):
if not self.position and self.signal_buy:
# Places parent, stop, and limit
self.brackets = self.buy_bracket(limitprice=110.0, stopprice=95.0)
elif self.position and self.rsi[0] > 70:
# Closes the position via market order
self.close() # BUG: Resting stop and limit children remain active!When self.close() executes, the broker closes the long position. However, self.close() does not automatically inspect or cancel the active child bracket orders resting in the broker queue. If the market subsequently rallies to $110 or plunges to $95, the resting child order triggers, selling short and opening an unmanaged "orphan" position.
4. Lack of atomic bracket modification
Backtrader does not offer a native API method to adjust an active bracket (e.g., trailing the stop loss to breakeven). To modify a stop, researchers must manually invoke self.cancel(self.stop_order) and issue a new self.sell(exectype=bt.Order.Stop, ...). If a bar executes during this transition, the strategy risks double-fill execution or temporary unhedged exposure.
Complete runnable Backtrader code example
The following self-contained Python script demonstrates how to submit bracket orders, audit order transitions across parent and child references in notify_order(), and verify clean trade closure in notify_trade().
import datetime
import numpy as np
import pandas as pd
import backtrader as bt
class SyntheticDataFeed(bt.feeds.PandasData):
"""Custom PandasData feed mapping standard OHLCV columns."""
params = (
('datetime', None),
('open', 'open'),
('high', 'high'),
('low', 'low'),
('close', 'close'),
('volume', 'volume'),
('openinterest', None),
)
class BracketOrderStrategy(bt.Strategy):
params = (
('take_profit_pct', 0.04), # +4.0% Take Profit
('stop_loss_pct', 0.02), # -2.0% Stop Loss
)
def __init__(self):
self.order_refs = {}
self.bracket_orders = None
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f"[{dt.isoformat()}] {txt}")
def next(self):
# Only enter once on bar 5 for demonstration purposes
if len(self) == 5 and not self.position:
current_close = self.data.close[0]
limit_target = round(current_close * (1.0 + self.p.take_profit_pct), 2)
stop_target = round(current_close * (1.0 - self.p.stop_loss_pct), 2)
self.log(f"Submitting Bracket: Current Close={current_close:.2f}, "
f"TP={limit_target:.2f}, SL={stop_target:.2f}")
# buy_bracket returns [parent, stop_order, limit_order]
orders = self.buy_bracket(
size=10,
exectype=bt.Order.Market,
limitprice=limit_target,
limitexec=bt.Order.Limit,
stopprice=stop_target,
stopexec=bt.Order.Stop
)
# Store references to track lifecycle roles in notify_order
parent, stop_order, limit_order = orders[0], orders[1], orders[2]
self.order_refs[parent.ref] = 'PARENT_ENTRY'
self.order_refs[stop_order.ref] = 'CHILD_STOP_LOSS'
self.order_refs[limit_order.ref] = 'CHILD_TAKE_PROFIT'
self.bracket_orders = orders
def notify_order(self, order):
role = self.order_refs.get(order.ref, 'UNKNOWN')
status_name = order.getstatusname()
if order.status in [order.Submitted, order.Accepted]:
self.log(f"Order #{order.ref} ({role}) -> Status: {status_name}")
return
if order.status in [order.Completed]:
exec_type = "BUY" if order.isbuy() else "SELL"
self.log(
f"Order #{order.ref} ({role}) FILLED -> {exec_type} {order.executed.size} "
f"@ {order.executed.price:.2f} (Comm: {order.executed.comm:.2f})"
)
elif order.status in [order.Canceled]:
self.log(f"Order #{order.ref} ({role}) CANCELLED (OCO / Purged)")
elif order.status in [order.Margin, order.Rejected]:
self.log(f"Order #{order.ref} ({role}) REJECTED / MARGIN ERROR")
def notify_trade(self, trade):
if trade.isclosed:
self.log(
f"TRADE CLOSED -> Net PnL: ${trade.pnlcomm:.2f} "
f"(Gross: ${trade.pnl:.2f}, Comm: ${trade.commission:.2f}, "
f"Bars Held: {trade.barlen})"
)
def generate_synthetic_data() -> pd.DataFrame:
"""Generate synthetic daily OHLCV bars simulating entry and target hit."""
dates = pd.date_range(start="2026-01-01", periods=15, freq="B")
# Prices: Consolidate around 100, then trend up to hit 104 take profit
opens = [100.0, 100.2, 100.1, 99.8, 100.0, 100.5, 101.2, 102.5, 104.2, 103.5, 103.0, 102.0, 101.0, 100.0, 99.0]
highs = [100.5, 100.6, 100.4, 100.2, 100.3, 101.5, 102.0, 103.2, 105.0, 104.0, 103.5, 102.5, 101.5, 100.5, 99.5]
lows = [ 99.5, 99.8, 99.7, 99.5, 99.6, 100.0, 100.8, 102.0, 103.0, 102.8, 102.0, 101.2, 100.2, 99.0, 98.2]
closes = [100.2, 100.1, 99.8, 100.0, 100.2, 101.2, 101.8, 103.0, 103.8, 103.2, 102.2, 101.5, 100.3, 99.2, 98.5]
vols = [10000] * 15
df = pd.DataFrame({
'open': opens,
'high': highs,
'low': lows,
'close': closes,
'volume': vols
}, index=pd.to_datetime(dates))
df.index.name = 'datetime'
return df
if __name__ == '__main__':
cerebro = bt.Cerebro()
cerebro.broker.setcash(10000.0)
cerebro.broker.setcommission(commission=0.001) # 0.1% commission
data_df = generate_synthetic_data()
feed = SyntheticDataFeed(dataname=data_df)
cerebro.adddata(feed)
cerebro.addstrategy(BracketOrderStrategy)
print("Initial Portfolio Value: $10,000.00")
cerebro.run()
print(f"Final Portfolio Value: ${cerebro.broker.getvalue():.2f}")Execution trace analysis
Running the script produces an audited console trace revealing the exact lifecycle transitions:
Initial Portfolio Value: $10,000.00
[2026-01-07] Submitting Bracket: Current Close=100.20, TP=104.21, SL=98.20
[2026-01-08] Order #1 (PARENT_ENTRY) -> Status: Submitted
[2026-01-08] Order #2 (CHILD_STOP_LOSS) -> Status: Submitted
[2026-01-08] Order #3 (CHILD_TAKE_PROFIT) -> Status: Submitted
[2026-01-08] Order #1 (PARENT_ENTRY) -> Status: Accepted
[2026-01-08] Order #2 (CHILD_STOP_LOSS) -> Status: Accepted
[2026-01-08] Order #3 (CHILD_TAKE_PROFIT) -> Status: Accepted
[2026-01-08] Order #1 (PARENT_ENTRY) FILLED -> BUY 10 @ 100.50 (Comm: 1.01)
[2026-01-13] Order #3 (CHILD_TAKE_PROFIT) FILLED -> SELL -10 @ 104.21 (Comm: 1.04)
[2026-01-13] Order #2 (CHILD_STOP_LOSS) CANCELLED (OCO / Purged)
[2026-01-13] TRADE CLOSED -> Net PnL: $35.05 (Gross: $37.10, Comm: $2.05, Bars Held: 3)
Final Portfolio Value: $10035.05Notice the exact progression:
- On
2026-01-07(Bar 5), the bracket is evaluated on bar close. - On
2026-01-08, the broker receives and accepts all three orders (Order.SubmittedthenOrder.Accepted), and the parent fills at the open price (100.50). - On
2026-01-13, the high of the bar reaches105.00, crossing the104.21target. Order #3 fills at104.21. - Backtrader's OCO logic cancels Order #2 on the same bar tick, preventing residual market exposure.
- The trade closes with a net profit of $35.05 after round-trip commissions, and the portfolio balance updates cleanly to $10,035.05.
Modern bracket order modeling in OpenBacktesting
While Backtrader pioneered multi-order chaining in Python, its event-driven object graph presents notable architectural friction: manual tracking of three distinct order references, risk of orphan orders during manual closes, and in-memory order queue volatility during large parameter sweeps.
Modern quantitative research frameworks approach bracket modeling as an atomic position risk container rather than loosely coupled order objects.
Backtrader multi-order chains vs. OpenBacktesting declarative brackets
OpenBacktesting (version 0.2.1) is a specialized historical backtesting framework focused on single-asset historical OHLCV bar-by-bar evaluation. Designed for Python >=3.12, <3.15 and built upon NumPy, pandas, and pandas-ta-classic, OpenBacktesting replaces fragmented order chaining with declarative execution syntax:
# OpenBacktesting: Bracket parameters declared directly on the position entry
self.buy(
size=10,
sl=98.20, # Stop loss threshold
tp=104.21, # Take profit threshold
trailing=False
)flowchart LR
subgraph Backtrader ["Backtrader (Chained OOP State Machine)"]
O1["Order #1 (Parent)"] -->|Fills| O2["Order #2 (Limit Child)"]
O1 -->|Fills| O3["Order #3 (Stop Child)"]
O2 <-->|"Manual OCO Pointers"| O3
O2 -.->|"Risk of Orphan if self.close()"| Ghost["Ghost Short Fill"]
end
subgraph OpenBacktesting ["OpenBacktesting v0.2.1 (Atomic Position Container)"]
Pos["Position Container\n(Size: 10, Entry: 100.50)"]
Pos --> Guard["Atomic Risk Engine\n[SL: 98.20 | TP: 104.21]"]
Guard -->|"Manual Exit or Target Hit"| CleanExit["Atomic Clean Exit\n(Zero Orphan Risk)"]
endKey architectural differences
| Dimension | Backtrader (Legacy) | OpenBacktesting (v0.2.1) |
|---|---|---|
| Order Declaration | Three discrete orders linked by parent and oco arguments | Atomic entry method with integrated sl and tp risk bounds |
| Orphan Order Risk | High; manual close() leaves resting child orders active | Zero; position exit atomically purges attached SL/TP rules |
| Intra-bar Priority Resolution | List evaluation sequence in BackBroker | Deterministic OHLC execution rules with explicit gap modeling |
| Audit Data Structure | Ephemeral event objects intercepted in notify_order() | Immutable columnar DataFrames (portfolio.orders_df, portfolio.trades_df) |
| Optimization Persistence | Volatile in-memory strategy instances (OOM risk) | Native SQLite trial persistence (Grid & SAMBO Bayesian search) |
| Visualization | Legacy Matplotlib GUI (cerebro.plot()) | Local browser-based OpenBacktesting Studio |
| Runtime Requirements | Python \le 3.10 idioms, legacy dependencies | Python \ge 3.12, < 3.15, NumPy, pandas, pandas-ta-classic |
Deterministic execution and audit trails
In OpenBacktesting:
- No Orphan Risk: When a strategy triggers a manual exit or rebalancing signal, the underlying engine flattens the position and immediately invalidates active bracket bounds. There is no possibility of a dormant stop order triggering days later to open an unintentional reverse position.
- Explicit Bar Gap Resolution: Rather than relying on non-deterministic order iteration, OpenBacktesting applies standardized price validation rules against the bar's
Open,High,Low, andClose, auditing every fill price against user-configured commission and slippage models. - Columnar Audit Ledger: Every executed bracket leg, slippage adjustment, and commission charge is exported directly into an immutable pandas DataFrame, ready for immediate statistical attribution and local visual inspection via OpenBacktesting Studio.
Summary checklist for bracket order management
When implementing bracket orders in quantitative research, verify your strategy against this operational audit checklist:
- Absolute Price Verification: Verify that
limitpriceandstoppriceare absolute price levels (P_{\text{sl}},P_{\text{tp}}), not point differences or relative percentages. - Track All Order References: Store the returned tuple from
buy_bracket()orsell_bracket()to mapparent.ref,stop.ref, andlimit.refindependently innotify_order(). - Purge Child Orders on Manual Exit: If your strategy exits positions early via
self.close()or technical indicator thresholds, explicitly cancel all resting child order references (self.cancel(order)). - Slippage Bounds on Stop Execution: Configure realistic slippage models on your broker. While limit orders fill at or better than
limitprice, stop orders execute as market orders upon triggering and will slip on volatile breakout bars. - Audit Intra-Bar Path Assumptions: Test whether your strategy's win rate artificially collapses when testing across different bar resolutions. Over-optimistic results on daily bars often stem from favorable intra-bar assumptions.
- Partial Fill Sizing Audits: If using volume-constrained broker fillers, ensure child order quantities dynamically adjust to match executed parent volume rather than initial requested volume.
Further reading and technical resources
- Backtrader Stop Loss: Guide to Stop and Stop-Limit Orders — In-depth breakdown of stop execution mechanics, slippage penalties, and trailing stop logic.
- Backtrader Tutorial: Build and Inspect Your First Strategy — Comprehensive walkthrough of Cerebro architecture, LineSeries, and lifecycle event hooks.
- Backtrader Slippage: Configure set_slippage_perc and Audit Fills — Guide to bounding execution prices and modeling spread friction in simulated brokers.
- OpenBacktesting Documentation — Official documentation for modern bar-by-bar backtesting, declarative order modeling, and SQLite parameter persistence.
