A Python backtest needs a chain from data to information available at the time, orders, fills and a reconciled ledger. Preserve record identifiers as well as timestamps. A fast equity plot cannot tell you whether a signal used a later release, an incomplete bar or a price your execution model never allowed.

Choose the environment for the research question

MT5’s Strategy Tester supports multicurrency Expert Advisors, testing and optimization. Python is another way to organize data, custom calculations and research, rather than a necessary escape from a one-pair limit. Compare the actual data, contract and execution requirements. Keep the same frozen dataset and rules when investigating differences between implementations.

Save the data and its meaning

Record the instrument and product, provider, time zone, quote side, sampling interval and download range. Check duplicate identifiers, record order and missing periods. A market closure is not automatically a corrupt file; filling a gap can invent observations. Preserve the original file alongside the transformation steps.

MetaTrader5’s copy_rates_range returns time, OHLC, tick volume, spread and real volume fields. It does not provide a complete historical commission or swap tariff. Available bars depend on terminal history and settings, so requesting a date range does not prove it was returned completely. Its time arguments and returned data use UTC. Use explicit time zones before converting any session schedule.

pandas and NumPy support data calculations. Backtrader provides an event-driven workflow. vectorbt’s portfolio simulation also updates cash and asset state and offers orders, signals and custom order-function modes. Neither label guarantees realistic execution or a fixed runtime. Choose the smallest useful stack, record versions and check its license, data rights and supported environment. Data and deployment may have separate costs.

Removing the index can attach a signal to another bar

Our test has three completed fictional price records: A = 100, B = 102, C = 104. A separate flag Series contains C: 1, A: 0, B: 0 in that order. Flag 1 identifies only a candidate for further modeling, not an order or execution price.

Labels select a different record from array positionPrices A 100, B 102, C 104 are ordered A, B, C. Flags ordered C, A, B contain 1, 0, 0. Label alignment selects C 104. Array multiplication without IDs selects the first record A 100.Prices by IDDifferent flag orderA 100C 1B 102A 0C 104B 0Series: ID C → 104No ID: first row → 100The same data select different records.
The green connection preserves ID C. The first unindexed array position does not have the same meaning.

pandas aligns Series by label. The result of prices * flags is A: 0, B: 0, C: 104. Converting both Series to arrays and multiplying by position gives 100, 0, 0. Assigning that result to A, B, C makes A the candidate. Check IDs before to_numpy() instead of checking only array lengths. Correct alignment still requires checking when the signal became available.

Assumptions: Fictional records, unique IDs and arbitrary prices. No costs or financial result. Multiplication by a flag demonstrates record assignment, not a complete backtest. Official material: pandas — Series and label alignment.

Define when a record becomes usable

A bar labelled by its opening time is not fully observed at that instant. An H1 bar opening at 10:00 has its completed OHLC only after the period ends. In pandas resampling, closed controls interval membership and label its displayed edge. Changing a label does not change when an aggregate was available.

Join macroeconomic observations by the publication time of the available vintage, not just the month they describe. merge_asof requires sorted merge keys; backward selects an earlier or equal key while nearest can select a later one. Neither option knows whether a value was subsequently revised. Define tolerances and the order of events sharing a timestamp.

Own record-matching test. A decision is stamped 10:04 UTC. The only saved releases are 10:00 with value 50 and 10:05 with value 60. With no tolerance restriction, backward matching selects 50, four minutes earlier. Nearest selects 60, only one minute away but still one minute in the future. An inclusive two-minute backward tolerance gives no match. Record that missing value; substituting the nearer future record would change the information set.

Our separate closed-bar recipe assumes high values 100 through 119 for the first twenty fictional bars, with each Open = High − 0.5, Low = High − 2 and Close = High − 1. Bar 21 has O120/H121/L118/C120.5; bar 22 has O120.6/H122/L119/C121. These arbitrary prices are not market data or an FX trade.

previous_high = bars["high"].shift(1).rolling(20).max()
signal = bars["close"] > previous_high
next_bar_signal = signal.shift(1, fill_value=False)

At bar 21 the previous twenty-bar maximum is 119, so 120.5 > 119 is true. Including the current High makes the threshold 121 and the condition false. The last line places the observed signal in bar 22; it does not supply a fill price, prove an order was accepted or choose an exit. Keep only completed input bars and specify position size, order type and event order separately.

Separate the signal, fill and costs

In Backtrader’s documented standard model, a Market order placed after the current bar is processed uses the next available bar’s opening price. Other settings or execution models can change timing. Inspect what happens when both a stop and a target fall within one bar: OHLC alone does not identify their order of observation.

Own one-trade accounting model. Buy 10,000 EUR at an assigned EUR/USD Ask of 1.1002; the simultaneous Bid is 1.1000. Later sell all units at an assigned Bid of 1.1012, with Ask 1.1014. A pip is 0.0001 USD per EUR. The fill-to-fill difference is ten pips, or 10 USD. With commission of 1 USD on each side, net profit is 8 USD. Funding and additional slippage are assumed zero in this exercise. Deducting another two-pip entry spread would instead report 6 USD and count an already included price difference twice.

Specify financing, conversions, contract size, margin rules and rejected orders for the intended product. For several instruments, model shared cash, available margin and the processing order of simultaneous requests. Adding independent equity curves can miss orders that a shared account could not fund. A random slippage model also needs a stated distribution, seed and library version.

A ledger makes the performance claim checkable

For a separate equal-R illustration, exactly forty completed wins at +2R and sixty losses at −1R give gross gains 80R, gross losses 60R and net 20R. Profit factor is 80/60 ≈ 1.33 and mean outcome 0.2R per trade. A cost of 0.1R on every trade leaves forty outcomes of +1.9R and sixty of −1.1R: gains 76R, losses 66R, net 10R, profit factor about 1.15 and mean 0.1R. R has the same monetary size throughout; this arithmetic is not a real backtest.

Own re-entry counterexample. Conditions on five bars are true, true, true, false, true. Both policies enter on bar 1; a stop closes the position on bar 2 before entry is evaluated. There are no later exits. A policy that enters whenever flat and true re-enters on bar 2 and remains in that position. A policy requiring a new false before re-arming waits through bars 2 and 3, re-arms on bar 4 and enters on bar 5. Both count two entries, at different times. This is an explicitly chosen state machine, not a default claim about either library.

Preserve both open-position equity and closed-trade results. Include starting capital when measuring drawdown and separate deposits and withdrawals from strategy returns. Do not add a return, Sharpe ratio or drawdown statistic that cannot be reconstructed from the relevant dated ledger and equity series.

Keep selection and withheld-data results separate

Fix the chronological training and test windows before selecting the preferred rules. Record every instrument, parameter set, filter and split tried, including rejected versions. A small parameter count does not mean only a small number of hypotheses were tested. Bailey and co-authors’ paper examines repeated strategy selection and backtest overfitting; this article does not estimate a PBO from its fictional examples.

A high win rate alone is not a diagnostic. Another invented hundred-trade set with seventy-five gains at +0.2R and twenty-five losses at −1R has a 75% hit rate yet loses 10R before costs: gains 15R, losses 25R, profit factor 0.6. Verify observations, sample dependence, execution, costs and parameter sensitivity rather than diagnosing bias from universal hit-rate or profit-factor thresholds.

What to do tomorrow

  1. Create an isolated project and save the untouched data with identifiers, source, UTC timestamps, quote side and version information. This offline learning exercise requires neither live credentials nor an order.
  2. Work through a few specified records. Log when each signal is available, which next event can execute it, and how a missing or revised record is handled.
  3. Choose the position-state rules: whether repeated true conditions add, reverse, re-enter after a stop or wait for re-arming. Check expected entry identifiers.
  4. Reconcile costs and each fill, then derive outcomes from the ledger. Run the same frozen rules with stated alternative execution assumptions.
  5. Preserve the trial log, withheld windows, versions and limitations. Compare like periods and uncertainty; a fixed performance drop does not identify its cause.