Execution Guide
This guide explains how orders work in Quantex and what the broker actually simulates.
If you are new to the library, the most important thing to understand is that you do not place orders directly on the backtester. You place them on a symbol-specific Broker, usually from inside Strategy.next().
Where brokers come from
When you call Strategy.add_data(), Quantex does two things:
- stores the data source in
Strategy.data - creates a matching
BrokerinStrategy.positions
That means this call:
self.add_data(CSVDataSource("eurusd.csv"), "EURUSD")
gives you a broker at:
self.positions["EURUSD"]
The public execution methods
The broker methods intended for normal strategy code are:
Basic market orders
Buying
def next(self):
broker = self.positions["EURUSD"]
broker.buy(quantity=0.5)
In the current implementation of Broker.buy(), quantity=0.5 means “use roughly 50% of the broker cash for this symbol”, unless you pass amount.
Selling
def next(self):
broker = self.positions["EURUSD"]
broker.sell(quantity=0.25)
Broker.sell() can reduce an existing long position or create/increase a short position.
Closing
def next(self):
broker = self.positions["EURUSD"]
if broker.is_long():
broker.close()
Broker.close() submits an offsetting market order for the current position.
Order sizing rules
This is one of the most important practical details in Quantex.
Fractional sizing through quantity
In both Broker.buy() and Broker.sell(), the quantity argument must be greater than 0 and less than or equal to 1.
That validation is enforced in both methods.
For buys, share count is calculated from available broker cash and current visible price.
Example:
broker.buy(quantity=0.25)
means “allocate about 25% of broker cash to a buy order”.
Explicit sizing through amount
If you pass amount, it overrides the automatic quantity calculation.
broker.buy(amount=100)
broker.sell(amount=50)
Important correction: in the current code, amount is treated as the number of units or shares, not a cash amount. Some older documentation described amount as currency-based sizing, which is inaccurate for the current implementation.
Order types
The order type enum is OrderType.
Current supported types:
Market orders
If you do not pass a limit, the broker creates a market order.
broker.buy(quantity=0.5)
broker.sell(quantity=0.5)
Market orders are processed in Broker._iterate() and execute using DataSource.COpen.
That means the fill logic is based on the current bar's open price, not the close.
Limit orders
If you pass limit, the broker creates a limit order.
broker.buy(quantity=0.5, limit=99.5)
broker.sell(quantity=0.5, limit=105.0)
In the current implementation:
- a buy limit executes when
DataSource.COpenis less than or equal to the limit price - a sell limit executes when
DataSource.COpenis greater than or equal to the limit price
Stop loss and take profit behavior
Both Broker.buy() and Broker.sell() accept:
stop_losstake_profit
Example:
broker.buy(
quantity=0.25,
stop_loss=95.0,
take_profit=110.0,
)
When an order with stop-loss or take-profit levels becomes active, the broker keeps monitoring it in Broker._iterate().
Important implementation detail:
- stop-loss and take-profit checks are also based on
DataSource.COpen - when the condition is met, the broker creates a new market order in the opposite direction
Order lifecycle
Order state is represented by OrderStatus.
The three states are:
What they mean in practice
PENDING: order exists but has not been fully processed yetACTIVE: order has executed and still has stop-loss or take-profit monitoring attachedCOMPLETE: order no longer has further actions to manage
You can inspect pending orders through Broker.orders and completed ones through Broker.complete_orders.
Example:
def next(self):
broker = self.positions["EURUSD"]
print("pending", len(broker.orders))
print("completed", len(broker.complete_orders))
for order in broker.orders:
print(order.side, order.type, order.status)
Position state
Useful broker fields include:
Example:
def next(self):
broker = self.positions["EURUSD"]
print("position", broker.position)
print("avg entry", broker.position_avg_price)
print("cash", broker.cash)
If you want a simple unrealized value estimate, use the current close:
def next(self):
broker = self.positions["EURUSD"]
current_price = self.data["EURUSD"].CClose
unrealized_value = broker.position * current_price
print(unrealized_value)
Important correction: the current Broker class does not expose a public unrealized_pnl property. Older docs referenced one, but it is not in the current implementation.
Commission handling
Commission logic is controlled by CommissionType and implemented in Broker._calc_commission().
Percentage commission
With CommissionType.PERCENTAGE, commission is:
quantity * price * commission
Cash commission
With CommissionType.CASH, commission is:
quantity * commission / lot_size
Margin-call behavior
The broker stores a margin threshold in Broker.margin_call. During Broker._iterate(), if equity falls below the calculated margin threshold while the position is short, the broker calls Broker.close().
This is a limited form of margin handling, not a full brokerage margin model.
Leverage for amplified position sizing
The broker supports leverage to amplify position sizing. With leverage enabled via the SimpleBacktester:
leveragemultiplier is set on the backtester and passed to each broker- When buying, shares are calculated as
base_shares * leverage - Margin (cash used) is calculated as
position_value / leverage - This allows controlling larger positions with the same cash
Example with leverage:
from quantex import SimpleBacktester
# 2x leverage means you control 2x the position while only using 1x cash as margin
backtester = SimpleBacktester(
strategy,
cash=10_000,
leverage=2.0,
)
# In a strategy, a buy with quantity=0.5:
# - Without leverage: controls 50% of cash worth of shares
# - With 2x leverage: controls 100% of cash worth of shares
# - Margin used: 100% / 2 = 50% of cash
Note: Leverage must be at least 0.1 (validated in the backtester constructor).
A realistic example
from quantex import Strategy, CSVDataSource
class BreakoutStrategy(Strategy):
def __init__(self):
super().__init__()
self.entered = False
def init(self):
self.add_data(CSVDataSource("data.csv"), "TEST")
def next(self):
broker = self.positions["TEST"]
current_open = self.data["TEST"].COpen
if not self.entered:
broker.buy(
quantity=0.25,
stop_loss=current_open * 0.98,
take_profit=current_open * 1.04,
)
self.entered = True
if broker.is_long() and self.data["TEST"].CClose < current_open:
broker.close()
Things the current execution engine does not do
The current codebase does not include:
- slippage modeling
- partial fills
- public order cancellation
- separate bid/ask prices
- advanced order types such as stop-limit orders
Some earlier documentation discussed these ideas conceptually, but they are not current built-in features.
Debugging order behavior
One good way to debug is to print current market state, broker state, and queued orders from inside Strategy.next().
def next(self):
broker = self.positions["EURUSD"]
source = self.data["EURUSD"]
print(source.Index[source.current_index])
print("open", source.COpen, "close", source.CClose)
print("position", broker.position, "cash", broker.cash)
for order in broker.orders:
print(order.side, order.type, order.status, order.price)
See tests/test_broker.py for the behavior currently asserted by the test suite.
Summary
Use the broker in Strategy.positions to place orders.
Remember these three rules:
- market orders execute from current open logic, not close logic
quantityis fractional sizing, whileamountis unit count- the broker API is intentionally small and does not model every market microstructure detail
For portfolio results and reports, see Backtesting guide.