Skip to content

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:

  1. stores the data source in Strategy.data
  2. creates a matching Broker in Strategy.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.COpen is less than or equal to the limit price
  • a sell limit executes when DataSource.COpen is greater than or equal to the limit price

Stop loss and take profit behavior

Both Broker.buy() and Broker.sell() accept:

  • stop_loss
  • take_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 yet
  • ACTIVE: order has executed and still has stop-loss or take-profit monitoring attached
  • COMPLETE: 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:

  • leverage multiplier 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:

  1. market orders execute from current open logic, not close logic
  2. quantity is fractional sizing, while amount is unit count
  3. the broker API is intentionally small and does not model every market microstructure detail

For portfolio results and reports, see Backtesting guide.