TestMaxBlog
backtesting · September 7, 2026 · by Joel

Python Backtesting: A Small Example You Can Check by Hand

Run a small Python backtest on supplied example bars. Check next-bar entries, costs, trade timing and the limits of the model by hand.

A Python backtest applies trading rules to historical observations and records hypothetical executions. The first useful version should be small enough to inspect trade by trade. If you cannot explain when a signal becomes known and which price the next order uses, a larger dataset will make the mistake harder to find.

Below is a complete standalone exercise using invented OHLC bars. It runs locally with Python's standard library and does not connect to a broker, download data or place orders. The result demonstrates the code's behavior, not a profitable strategy.

Define the experiment before writing the loop

Use a simple long-only rule: a completed bar's close must exceed the highs of the preceding three bars. Enter at the following bar's open, then exit at the open two bars after entry. Allow one position at a time and ignore signals formed while that position is held.

There is no stop or profit target in this deliberately limited example. That keeps intrabar fill ordering out of the first lesson. A real research model needs an explicit treatment of those orders if the strategy uses them.

We will subtract a hypothetical round-trip cost of 0.04 price units per trade. All quantities are one unit. These are abstract price units, not an estimate of futures commission or a particular forex spread.

The general backtesting guide explains the broader process. This page focuses on translating a few rules into inspectable Python.

Run the complete example

Save this as example.py and run it with python3 example.py. Each tuple contains open, high, low and close, in that order. List positions are bar numbers starting at zero.

bars = [
    (100, 101, 99, 100),
    (100, 102, 99, 101),
    (101, 103, 100, 102),
    (102, 104, 101, 103.5),
    (104, 106, 103, 105),
    (105, 106, 102, 103),
    (103, 108, 102, 107.5),
    (108, 109, 107, 108.5),
    (109, 111, 108, 110),
    (110, 112, 109, 111),
]


def test(bars, n=3, hold=2):
    if n < 1 or hold < 1:
        raise ValueError("Window")
    trades = []
    free_from = 0
    unfinished = 0
    for i in range(n, len(bars)):
        if i < free_from:
            continue
        prev = bars[i - n:i]
        highs = [b[1] for b in prev]
        top = max(highs)
        if bars[i][3] <= top:
            continue
        entry = i + 1
        exit_at = entry + hold
        if exit_at >= len(bars):
            unfinished += 1
            break
        buy = bars[entry][0]
        sell = bars[exit_at][0]
        gross = sell - buy
        net = round(gross - .04, 2)
        trades.append((i, entry,
                       exit_at, net))
        free_from = exit_at
    return trades, unfinished


trades, unfinished = test(bars)
for trade in trades:
    print(trade)
total = sum(t[3] for t in trades)
print(round(total, 2))
print("Unfinished:", unfinished)

Expected output:

(3, 4, 6, -1.04)
(6, 7, 9, 1.96)
0.92
Unfinished: 0

Each trade row is signal bar, entry bar, exit bar and net result. The final result is 0.92 price units after the two illustrative costs. There are only two trades on fabricated data, so no performance conclusion follows from it.

Check the first trade without trusting the program

At bar 3, the close is 103.5. The preceding three highs are 101, 102 and 103, so the signal qualifies. Its entry is bar 4's open, 104, and its exit is bar 6's open, 103. Gross result is -1; subtracting 0.04 gives -1.04.

The loop ignores signal bars 4 and 5 while the position is held. It can evaluate bar 6's closing signal because the earlier position exited at bar 6's open. That leads to the second entry at 108 and exit at 110, producing 1.96 after the same cost.

The exit price is read to evaluate a completed trade. It is never used to decide whether its earlier signal qualifies. Future information must not enter the signal rule.

Make the limitations visible

This compact example deliberately omits timestamps, missing-data validation, account balance, leverage, position sizing, financing, partial fills and order queues. It assumes next-open execution and reports candidates that cannot complete before the sample ends.

Do not silently delete unfinished candidates from a real report. Choose a documented end-of-test policy, such as marking the open position separately or closing it at an available final price. Apply the policy consistently.

The same applies to costs. A fixed 0.04 is only a teaching assumption. Use the selected instrument's multiplier and actual cost conventions before interpreting a monetary result. The futures calculator helps with contract arithmetic, and the journal template provides a place for execution assumptions.

Tests worth running before adding more data

Try a flat-price sequence: the strict breakout rule should produce no trades. Change prices after a signal without changing its earlier bars: the signal's decision must remain unchanged. Shorten the data near the end and confirm that unfinished candidates are reported.

Also inspect the entry and exit indices, rather than checking only the final total. Two timing errors can occasionally cancel in aggregate while leaving every simulated trade wrong.

When you replace the toy bars, validate chronological order and OHLC consistency. Keep development dates separate from later untouched dates. Changing the parameters after viewing the latter converts them into development data too.

Move from a local example to a replay workflow

This script is a local teaching example, not a ready-to-import TestMax strategy. TestMax's Pro Algo Playground has its own documented strategy interface and helper functions. Use the Playground getting-started guide when adapting a strategy to that environment.

Explore the Algo Playground and current Pro access if you want to investigate that workflow. Or create a free account and practice the same decision timing manually on an eligible instrument within the Free history window. Supported exchange futures require Pro.

The next useful milestone is agreement between your written rule, the Python trade log and a few manually inspected chart examples. Expand the dataset after those agree.

Tags: Python backtesting, backtest python, OHLC