Skip to content

Strategy Guide

This guide explains how to write a trading strategy in Quantex from the ground up.

If you are new to the library, the most important idea is this: a strategy is just a Python class that decides what to do on each bar of historical market data.

In Quantex, strategy classes inherit from Strategy.

What a strategy is responsible for

Your strategy does three things:

  1. load or attach data in Strategy.init()
  2. optionally create indicator arrays with Strategy.Indicator()
  3. place orders in Strategy.next()

The backtester does the rest. It advances time, updates data visibility, asks brokers to process orders, and records the equity curve through SimpleBacktester.run().

The two required methods

Every concrete strategy must implement:

Strategy.init()

This method runs once before the backtest loop starts.

Use it to:

Strategy.next()

This method runs once per backtest step.

Use it to:

First complete example

from quantex import Strategy, CSVDataSource, SimpleBacktester
import pandas as pd


class MovingAverageCross(Strategy):
    def __init__(self, fast_period=5, slow_period=20):
        super().__init__()
        self.fast_period = fast_period
        self.slow_period = slow_period

    def init(self):
        self.add_data(CSVDataSource("data.csv"), "TEST")
        close = self.data["TEST"].Close
        self.fast_ma = self.Indicator(
            pd.Series(close).rolling(window=self.fast_period).mean().to_numpy()
        )
        self.slow_ma = self.Indicator(
            pd.Series(close).rolling(window=self.slow_period).mean().to_numpy()
        )

    def next(self):
        if len(self.fast_ma) < 2 or len(self.slow_ma) < 2:
            return

        broker = self.positions["TEST"]

        crossed_up = self.fast_ma[-2] <= self.slow_ma[-2] and self.fast_ma[-1] > self.slow_ma[-1]
        crossed_down = self.fast_ma[-2] >= self.slow_ma[-2] and self.fast_ma[-1] < self.slow_ma[-1]

        if crossed_up and broker.is_closed():
            broker.buy(quantity=1.0)
        elif crossed_down and broker.is_long():
            broker.close()


# Run with optional leverage for amplified position sizing
report = SimpleBacktester(MovingAverageCross(), cash=10_000, leverage=1.0).run()
print(report)

Understanding the objects available inside a strategy

self.data

self.data is a dictionary whose keys are the symbol names you used with Strategy.add_data().

Data sources are added inside the init() method:

class MyStrategy(Strategy):
    def init(self):
        self.add_data(CSVDataSource("eurusd.csv"), "EURUSD")
        self.add_data(CSVDataSource("gbpusd.csv"), "GBPUSD")

    def next(self):
        eurusd_close = self.data["EURUSD"].CClose
        gbpusd_close = self.data["GBPUSD"].CClose

Each value is a DataSource object.

self.positions

self.positions is another dictionary keyed by the same symbol names.

Each value is a Broker created automatically when you call Strategy.add_data().

Example:

def next(self):
    broker = self.positions["EURUSD"]

    if broker.is_closed():
        broker.buy(quantity=0.5)

self.indicators

self.indicators stores indicator arrays created through Strategy.Indicator().

In most strategies you will also keep direct references such as self.fast_ma or self.rsi, because those are easier to read than indexing the list directly.

Reading market data

The most common values you will read are:

These all mean “the current visible bar”.

You can also access visible history with:

Example:

def next(self):
    close_now = self.data["TEST"].CClose
    previous_close = self.data["TEST"].Close[-2]
    recent_closes = self.data["TEST"].Close[-10:]

Creating indicators

Quantex provides a built-in indicator catalog via self.ta or quantex.indicators. You can also calculate arrays yourself and register them as time-aware indicators.

Using built-in indicators

def init(self):
    close = self.data["TEST"].Close

    # Use built-in indicators from self.ta
    self.sma_20 = self.Indicator(self.ta.sma(close, 20))
    self.sma_50 = self.Indicator(self.ta.sma(close, 50))
    self.rsi_14 = self.Indicator(self.ta.rsi(close, 14))

Using custom indicators

import pandas as pd


def init(self):
    close = self.data["TEST"].Close

    sma_20 = pd.Series(close).rolling(window=20).mean().to_numpy()
    sma_50 = pd.Series(close).rolling(window=50).mean().to_numpy()

    self.sma_20 = self.Indicator(sma_20)
    self.sma_50 = self.Indicator(sma_50)

The returned object is a TimeNDArray, which exposes only data up to the current backtest step.

That lets you write logic such as:

def next(self):
    if len(self.sma_20) < 2 or len(self.sma_50) < 2:
        return

    if self.sma_20[-2] <= self.sma_50[-2] and self.sma_20[-1] > self.sma_50[-1]:
        self.positions["TEST"].buy(quantity=0.5)

Placing orders

The current public order methods are:

Buy orders

def next(self):
    broker = self.positions["TEST"]

    broker.buy(quantity=0.25)
    broker.buy(quantity=0.25, limit=99.5)
    broker.buy(quantity=0.25, stop_loss=95.0, take_profit=110.0)

In the current implementation of Broker.buy(), quantity is interpreted as a fraction of broker cash unless amount is supplied.

Sell orders

def next(self):
    broker = self.positions["TEST"]

    broker.sell(quantity=0.25)
    broker.sell(quantity=0.25, limit=105.0)

Broker.sell() can reduce a long position or open/increase a short position, depending on the current broker state.

Closing a position

def next(self):
    broker = self.positions["TEST"]

    if broker.is_long():
        broker.close()

Broker.close() submits a market order that offsets the current position.

Position state

Useful broker attributes and helpers include:

Example:

def next(self):
    broker = self.positions["TEST"]

    print("position", broker.position)
    print("avg price", broker.position_avg_price)
    print("cash", broker.cash)
    print("pending orders", len(broker.orders))
    print("completed orders", len(broker.complete_orders))

Multi-symbol strategies

Quantex can attach multiple symbols to one strategy.

from quantex import Strategy, CSVDataSource


class MultiSymbolStrategy(Strategy):
    def init(self):
        self.add_data(CSVDataSource("eurusd.csv"), "EURUSD")
        self.add_data(CSVDataSource("gbpusd.csv"), "GBPUSD")

    def next(self):
        if self.data["EURUSD"].CClose > self.data["GBPUSD"].CClose:
            self.positions["EURUSD"].buy(quantity=0.2)

Important detail: SimpleBacktester.run() splits starting cash evenly across all attached brokers.

That means the cash available to each symbol-specific broker is a fraction of the total starting cash.

Common mistakes to avoid

1. Forgetting to attach data

If you never call Strategy.add_data(), the strategy has no symbol data and no broker to trade through.

3. Using indicator values too early

Rolling calculations often contain NaN values at the start. Guard with length checks before using recent values.

4. Assuming orders fill at close

The current broker logic in Broker._iterate() executes market orders at the current open, not the current close.

Minimal debugging approach

One easy way to understand a strategy is to print values inside Strategy.next():

def next(self):
    print("time", self.data["TEST"].Index[self.data["TEST"].current_index])
    print("open", self.data["TEST"].COpen)
    print("close", self.data["TEST"].CClose)
    print("position", self.positions["TEST"].position)

Running the strategy

Once the class is written, execution follows this pattern:

# 1. Create backtester with optional leverage
# Data sources are added inside the strategy's init() method
backtester = SimpleBacktester(MovingAverageCross(fast_period=5, slow_period=20), cash=10_000, leverage=1.0)

# 2. Run the backtest
report = backtester.run()

print(report)

For a deeper explanation of reports and performance metrics, see Backtesting guide.