Documentation

Backtest & Optimize

How partiqon tests strategies on historical data, searches for parameter values, and measures them on data the search did not use. Includes backtesting, optimization, walk-forward validation, and WFO.

Backtesting

A backtest simulates a strategy on historical data. For each bar, we check if entry or exit signals fire, and record all trades with their P&L. No real money is used. It's pure simulation to measure past performance.

How It Works

  1. Load data: Historical bars (OHLCV) from BAR6 files
  2. Reset state: Strategy parameters, indicators, and positions reset to start
  3. Iterate bars: For each bar in the period, feed it to the strategy
  4. Check signals: Does the strategy generate entry or exit signals?
  5. Execute trades: Entry opens a position; exit closes it. Record entry/exit price, P&L
  6. Calculate metrics: Win rate, drawdown, profit factor, etc.
  7. Output results: Trade list, equity curve, performance statistics

Trade timestamps

Bar data goes into the engine as UTC. Trade timestamps come out in Europe/Berlin local time (CET in winter, CEST in summer): the EntryTime and ExitTime columns of detailed_trades.csv, the trade list, and the hour-of-day and day-of-week breakdowns.

Gate.Session windows use that same clock, so a trade reads in the same clock as the session rule that admitted it.

⚠ The daily blocks do NOT use that clock. Aggregator.D1, Aggregator.TickToBar.D1, the daily Indicator.VWAP reset and Indicator.PivotPoints key their day on a fixed UTC+1, 23:00→23:00 UTC, all year. That is midnight Berlin time in winter and 01:00 Berlin time in summer, so for half the year a Gate.Session window and an Aggregator.D1 bar disagree by an hour about when a day begins. A dataset you convert yourself, and the bars the chart draws, use a third boundary again: the plain UTC grid, 00:00 UTC. All three are deliberate and none of them is being changed, because moving a day boundary silently moves the results of every saved D1 strategy. The block reference sets out which surface uses which.
⚠ The offset is not constant. Berlin is UTC+1 in winter and UTC+2 in summer, so the same UTC bar reads 10:00 on 15 January and 11:00 on 15 July. If you parse these timestamps back into instants (in a spreadsheet, a notebook, or your own tooling), interpret them as Europe/Berlin with daylight saving. Reading them as UTC, or as a fixed UTC+1, puts every summer trade an hour out.

Key Metrics

Positions open at the end of the data

A position still open when the data ends is closed by the engine at the price of the last bar, with the exit reason END_OF_DATA. That close is not a decision of the strategy, and its result depends on where the data happens to end. So the figures are split in two:

The results show how many positions were open at the end and their unrealised result; the trade list and the trade export mark them in the column Open at data end. Net profit equals gross profit minus gross loss plus that unrealised result. When every trade of a result was still open at the end, the figures built from trades show a dash. Results computed before this rule counted such positions as ordinary trades in every figure and keep those numbers.

MFE and MAE

The trade list shows how far the price moved while each trade was open, in price points per unit of position size:

Both are measured from the price the trade was filled at (the entry's spread and slippage are in that price), over every bar from the bar the trade was filled on through the bar it closed on, the exit bar included, using each bar's high and low. For a long trade MFE = highest high − fill price and MAE = lowest low − fill price; for a short trade MFE = fill price − lowest low and MAE = fill price − highest high.

Because the exit bar counts, the price a stop or target filled at, or the open of a bar that gapped through it, lies inside the measured range: the price move from the fill to the exit is at most MFE and at least MAE, a take-profit has an MFE of at least its target distance, and a stop-loss an MAE at least as large as its stop distance. The whole exit bar counts, also its part after the exit, because a bar gives one high and one low and not the order of its prices; on a bar that reached both the stop and the target, one of the two values therefore goes beyond its level.

The exit's spread and slippage, commission and financing are not part of MFE and MAE. A trade's P&L includes them and is in account currency for the whole position, so it compares directly with MFE and MAE only at a value per point of 1 and a size of 1. A result saved before this definition shows MFE and MAE as they were stored, with a note: without the exit bar and in account currency per unit.

Trade context breakdown

The Context tab groups the same trades by the situation each one was entered in, in tables with trades, share, win rate, profit factor, expectancy and net P&L per group. Every value is read on the last bar that had closed before the entry, never on the bar the trade was entered on, whose high, low and close lie after the entry.

Volatility, trend strength, distance and position are split into thirds. The two cut-offs are taken over every bar of the dataset, for distance and position with each bar counted once as a long and once as a short, and each table states them. Because the cut-offs use the bars after each trade as well, the bands describe the past and cannot be used as a filter in live trading. A group with fewer than 30 trades is marked low sample; with several tables side by side, small differences between bands are usually noise. The trade export carries each trade's four values in the columns entry_adx, entry_mean_distance, entry_range_position and streak_before (+n after n wins, −n after n losses, 0 after a break-even trade, empty with no previous trade).

⚠ Important: Backtest results are historical. They show what the rules did on past data, not what they will do. A figure measured on the period the rules or their parameters were chosen on includes whatever those choices fitted to that period. The out-of-sample measurements below use data the choices did not see, and they are still measurements of the past, not a forecast.

PSO Optimization

PSO (Particle Swarm Optimization) is a search algorithm that looks for parameter values automatically. Each particle is one set of parameter values. After every generation each particle moves towards the best set it has found so far and the best set the whole swarm has found, ranked by the metric you chose.

How PSO Works

  1. Define searchable parameters: Which block parameters can be tuned? (e.g., EMA period 10–100, RSI level 20–50)
  2. Initialize swarm: Create N particles, each with random parameter values (e.g., 30 particles)
  3. Evaluate each particle: Run a backtest with those parameters. Record profit factor (or your chosen metric)
  4. Track best: Each particle remembers its best result ever. The swarm remembers the global best.
  5. Update velocities: Each particle adjusts its parameters toward its personal best and the global best (with randomness)
  6. Repeat: Run multiple generations (e.g., 60 iterations). The swarm gathers around the sets that measured highest on the chosen metric.
  7. Return champion: The parameter set that measured highest across all generations

What PSO does and does not do

Iteration 1: particles → backtest → evaluate Iteration 2: particles move toward best → backtest → evaluate Iteration 3: particles move again → backtest → evaluate ... Iteration 60: final positions → return best

Walk-Forward Validation

A single in-sample / out-of-sample split measures the strategy on one later period. Walk-forward repeats that measurement on several consecutive periods, so the figures show how the result differs from one period to the next.

Why a separate period

Parameters chosen on one period can fit the noise of that specific history as well as whatever pattern it holds. Measured on that same period, the result contains that fit. A later period the choice did not see measures the same rules without it.

Walk-Forward Process

  1. In-sample window: The first out-of-sample block starts IS Window (months) after Full Range Start.
  2. Out-of-sample blocks: The range after it is cut into consecutive blocks of OOS Block Size (months) each.
  3. Block edges: Each out-of-sample block ends the day before the next one begins, so no day belongs to two blocks. The last block ends at Full Range End and can be shorter than the block size. The dates are whole days in UTC, both ends included.
  4. Which trades a block counts: A trade belongs to the block in which it was entered. The trade list shows times in Central European time, so in winter a trade entered at 00:30 on a block’s first day (23:30 UTC the day before) belongs to the block before.
  5. Walk-Forward tab: Every block's in-sample window runs from the start date up to that block (anchored), and the graph is backtested on each block as it is, without re-tuning.
  6. WFO tab: The parameters are re-tuned on each block's in-sample window before that block is measured (see below).
Anchored Block 1: IS [2020─────2021] OOS [2021─2022] Anchored Block 2: IS [2020──────────2022] OOS [2022─2023] Anchored Block 3: IS [2020─────────────────2023] OOS [2023─2024] ...

Reading the results

Walk-Forward Optimization (WFO)

WFO re-tunes the tunable parameters with PSO on each block's in-sample window, then backtests the tuned parameters on the out-of-sample block that follows. The in-sample window is anchored at the start date by default, or rolling: a window of IS Window (months) that moves forward with the blocks.

WFO Process

  1. Split data: In-sample window and out-of-sample blocks, as for walk-forward above.
  2. Per block: PSO on the block's in-sample window → backtest of the tuned parameters on the block
  3. Summary: Profitable blocks, average and minimum out-of-sample profit factor, average Sharpe and the out-of-sample trade count, with each block's own figures in the block table
💡 What WFO measures: the procedure of re-tuning on an in-sample window and then measuring the result on the period after it, repeated block by block. Every out-of-sample figure comes from data that block's tuning did not read. A block's in-sample figure also contains what the tuning fitted to that window, so the gap between its in-sample and out-of-sample figures includes that fit. All of them remain measurements of past periods, not a forecast.

Fit on full history

A WFO leaves one parameter set per block, not one for later use. After a WFO has finished, Fit on full history in its results runs the same procedure once more, on the window a next block would be tuned on: from the start date to the last bar of the dataset when the in-sample window is anchored, or the last IS Window (months) before that bar when it is rolling (Fit on the last N months). The fit uses the WFO's own PSO settings, fitness metric and trade floor, and is saved as a new version of the graph; the version before it stays in the version history.

No out-of-sample period exists for these parameters: the WFO's out-of-sample figures describe the procedure, not this parameter set. The procedure re-tunes every OOS Block Size (months), so the graph shows the date its next fit is due, and Fit again repeats the fit on newer data. The first fit after a WFO is included with the WFO run; each later one counts as one optimization run.

How the tools fit together

  1. Build: A strategy is a graph of blocks in the graph editor.
  2. Backtest: Runs the graph on a date range with the parameter values it has.
  3. Optimize: PSO searches the tunable parameters on an in-sample range.
  4. Out-of-sample backtest: Measures the chosen parameters on a later range the search did not read.
  5. Walk-Forward: Measures the graph, unchanged, on several consecutive out-of-sample blocks.
  6. WFO: Re-tunes on each block's in-sample window, then measures the block after it.
  7. Fit on full history: Runs a finished WFO's procedure once more, on the window after its last block, and saves the parameters as a new version of the graph.

Every step reports measurements of past data. None of them says whether a strategy is fit to trade.

Structure Finder: pattern mining

A Structure Finder run (Premium) records a pattern report before its search begins. It tests the in-sample part of each walk-forward fold for 36 fixed conditions:

For each condition, the move of the next bar (close to next close, in basis points) after the bars where the condition holds is compared with the next-bar move after all other bars of the window, with a two-sided Mann-Whitney U test. Before the comparison each move is divided by the mean absolute close-to-close move of the 8 bars that end at the bar the condition is judged on, so that a condition that tends to occur in volatile stretches is not compared with calm ones on the raw scale; the figures in the view stay in basis points. A bar without 8 earlier moves, or whose 8 earlier moves are all zero, is left out of both groups, and results stored before this change keep the q-values they were calculated with. A condition is tested only when it holds on at least 30 bars and fails on at least 30. The p-values of all conditions tested in a window are adjusted with the Benjamini-Hochberg correction, because testing many conditions at once produces chance results; a condition counts as found when its adjusted value (q) is 0.05 or lower.

The Patterns view of the result shows, per fold, the window mined and the window measured, how many conditions were tested, and for each found condition: its description and the blocks that build it, how often it held, the share of rising next bars, the mean next-bar move, the rank probability and q. Each found condition is then measured on the fold's out-of-sample window without being tested again, and the reserved holdout is never read. When no condition reaches q 0.05 or lower, the view says so.

The correction runs within each window, so the stated 5% is per window: on data without structure about 5% of windows list a condition. A run mines three folds, so on data without structure about one run in ten lists a condition in at least one fold (between 8.6% and 10.3% of runs, measured on simulated series of 20,000 bars without structure). Each condition shows the number of folds that found it; a condition found in all three folds is more evidence than one found in a single fold, although the folds’ windows overlap.

The report is shown next to the candidates and does not steer the search. It describes past data: a condition that passed the test is a measured regularity of that window, not a trading signal. On tick data the ticks are first built into 1, 5, 15 and 30 minute bars from the mid price, by the rule the tick-to-bar blocks use, and the conditions of all four periods are corrected as one family.

Structure Finder: fitness and the trade minimum

A candidate’s fitness is the median of its out-of-sample values over the walk-forward folds, 3 by default. In each fold the tunable parameters are fitted on the fold’s in-sample window, and the candidate is then measured on the out-of-sample window that follows.

A candidate has a fitness only when every fold holds enough trades. The run’s minimum, 30 trades by default, is shared out over the folds and rounded up: with 3 folds each fold needs at least 10 out-of-sample trades, with 4 folds at least 8. A candidate with one fold below that is listed without a value, whatever the other folds measured, and the explanation of its row names the fold and the number it fell short of.

The reason is the cap on the profit factor. A fold’s profit factor counts at most 10, and a fold of three winning trades reaches it. With the minimum counted over all folds together, such a fold could decide the median of a candidate whose other two folds held the rest of the trades.

When the fitness is a Sharpe, Sortino or Adjusted Sortino ratio, a fold whose ratio carries the mark “few observations” (fewer than 6 monthly results, or monthly results that barely vary) is shown among the candidate’s folds and not counted in the median. A candidate with no fold left to count, or with a fold whose ratio is not defined, has no fitness and is listed without a value.

Beside the fitness and the held-back value, each candidate shows the risk that closed trades do not show. Distance to stop ÷ avg win is the mean distance from entry to the stop on the candidate’s out-of-sample trades, divided by its average winning trade. MAE ÷ avg win is the furthest any single trade went against its entry, divided by the same average win. Wide stop marks a candidate whose stop sits more than 20 times as far from its entry as its target, or as its average win when it sets no target. Nothing is excluded and the order does not change: the figures are columns of the results table and are part of the export of a candidate.

A profit factor counts only the trades that closed: when a stop never filled inside the windows, no loss from it is in the figure, however far away the stop sits. A profit factor of 10 beside a stop that never filled therefore shows how often the target was reached, and not what the stop would have cost.

What a backtest does and does not model

Next Steps

Ready to build a strategy? Start in the graph editor on the /graph page. Or read the block reference to understand what's available.