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
- Load data: Historical bars (OHLCV) from BAR6 files
- Reset state: Strategy parameters, indicators, and positions reset to start
- Iterate bars: For each bar in the period, feed it to the strategy
- Check signals: Does the strategy generate entry or exit signals?
- Execute trades: Entry opens a position; exit closes it. Record entry/exit price, P&L
- Calculate metrics: Win rate, drawdown, profit factor, etc.
- 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.
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.
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
- Total Return: Ending equity − starting capital, as a percentage of the starting capital
- Win Rate: Winning trades ÷ total trades. A high win rate with a low profit factor means the gains are small and the losses are large.
- Profit Factor: Gross profit ÷ gross loss. At 1.0 the winning trades earned exactly what the losing trades lost; above 1.0 they earned more. The ratio does not show how many trades produced it: a handful of trades can give a very high value.
- Max Drawdown: Largest fall of the equity curve from a peak to the trough after it. It is the deepest fall on this one path through history; the same rules on another period can fall further.
- Sharpe Ratio: Average monthly excess return ÷ its standard deviation, annualised. Winning and losing months both count as variation. Excess means above the risk-free rate: the policy rate of the benchmark chosen in the risk-free settings, taken from the central banks' own publications (USD: the effective federal funds rate of the Federal Reserve Bank of New York; EUR: the ECB deposit facility rate from the ECB Data Portal; GBP: Bank Rate from the Bank of England's statistical database; CNY: the 1-year Loan Prime Rate from the National Interbank Funding Center; JPY: the Bank of Japan's policy rate, entered by hand from its statements). Every calendar month of the run counts, also a month in which no trade closed (its return is 0, its excess return is minus that month's risk-free rate). A first or last month that lies only partly in the run counts with the trades that closed in that part and is charged that part of the month's risk-free rate: days in the run ÷ days in the month. The Sortino ratio uses the same months, with only the months below the risk-free rate in its denominator. With fewer than two months, or monthly results that do not vary, the ratios are not defined and show a dash, with the reason on hover. Results computed before this definition keep their numbers and are marked "earlier method".
- Trades per year: How often the rules trade. With few trades, one trade more or less can move every other figure a long way.
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:
- Built from trades (only the trades the strategy closed): total trades, win rate, profit factor, average win and loss and their ratio, gross profit and gross loss, the Kelly fraction, average hold time, the context tables, the Monte Carlo sample, and in Diagnostic Mode the median profit factor, win rate and average R.
- Built from the equity curve (the open position included at the price it was marked at): net profit and total return, max drawdown and its duration, Sharpe, Sortino, Calmar, gain to pain, the monthly results and the equity chart, and in Diagnostic Mode the maximum drawdown in R.
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:
- MFE (maximum favourable excursion): the largest move in the trade's favour. 0 or positive.
- MAE (maximum adverse excursion): the largest move against the trade. 0 or negative.
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.
- Session and weekday: the Europe/Berlin clock time of the entry.
- Volatility: ATR(14), the average true range, Wilder-smoothed; true range = max(high − low, |high − previous close|, |low − previous close|).
- Trend strength: ADX(14), Wilder's: +DM and −DM from the change of the high and the low, each smoothed over 14 bars and divided by the smoothed true range (+DI, −DI); DX = 100 × |+DI − −DI| ÷ (+DI + −DI); ADX = DX smoothed over 14 bars. 0 without directional movement, 100 on a straight line.
- Distance from the mean: (close − EMA(20)) ÷ ATR(14), in ATR units. For a long as it is, for a short with the sign turned: above 0 the trade was entered in the direction the price had moved away from its mean, below 0 against it, after a pullback.
- Position in the recent range: (close − lowest low) ÷ (highest high − lowest low) over the last 20 bars, 0 to 100 %. For a short 100 % minus that, so 100 % is the extreme in the trade's direction.
- Streak before the trade: the run of wins or losses among the trades that had closed before the entry, ordered by their exit: after 2 or more losses, after 1 loss, no previous trade, after a break-even trade, after 1 win, after 2 or more wins. A trade still open at the entry does not count, and a trade that closed at exactly 0 ends a run.
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).
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
- Define searchable parameters: Which block parameters can be tuned? (e.g., EMA period 10–100, RSI level 20–50)
- Initialize swarm: Create N particles, each with random parameter values (e.g., 30 particles)
- Evaluate each particle: Run a backtest with those parameters. Record profit factor (or your chosen metric)
- Track best: Each particle remembers its best result ever. The swarm remembers the global best.
- Update velocities: Each particle adjusts its parameters toward its personal best and the global best (with randomness)
- Repeat: Run multiple generations (e.g., 60 iterations). The swarm gathers around the sets that measured highest on the chosen metric.
- Return champion: The parameter set that measured highest across all generations
What PSO does and does not do
- Number of backtests: A grid search backtests every combination; PSO backtests up to particles × generations parameter sets.
- Parameter types: It handles any mix of discrete and continuous parameters.
- A search, not a full test: It tries many sets, not every set, so it can end without having tried the set that would measure highest.
- Where its figures come from: The champion's figures were measured on the range it was chosen on, so they include whatever the search fitted to that range. A backtest on a later range measures the same set on data the search did not read.
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
- In-sample window: The first out-of-sample block starts IS Window (months) after Full Range Start.
- Out-of-sample blocks: The range after it is cut into consecutive blocks of OOS Block Size (months) each.
- 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.
- 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.
- 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.
- WFO tab: The parameters are re-tuned on each block's in-sample window before that block is measured (see below).
Reading the results
- Profitable blocks: The number of blocks whose out-of-sample net profit is above zero, out of the blocks that completed and had at least one out-of-sample trade. A block with a net profit of exactly zero is not in the count. A block without an out-of-sample trade has no result to count either way; the summary says how many there were.
- Avg OOS PF: The mean of the blocks' out-of-sample profit factors. A block with no losing trade has no profit factor to average and is left out, and so is a block without an out-of-sample trade: it has no measured result. When no block has an out-of-sample trade, the averages show a dash. One block with a very high value can carry the mean; the block table shows each block's own figure, and a dash for a block without an out-of-sample trade, which the out-of-sample profit factor chart shows without a bar.
- Min OOS PF: The lowest out-of-sample profit factor of any block with an out-of-sample trade, which is the period in which the result was lowest.
- Avg Sharpe: The mean of the out-of-sample Sharpe ratios of the blocks with an out-of-sample trade whose ratio is defined and that do not carry the mark "few observations"; the card says over how many blocks (for example over 5 of 7) and names the blocks left out on hover, and shows a dash when no block's ratio is defined or every block with a defined ratio carries the mark. A block's Sharpe and Sortino rest on its monthly results: every calendar month of its out-of-sample window, a partial first or last month charged its share of the risk-free rate. A block of one calendar month has a single observation and no defined ratio; a block of one month and a few days of the next has two observations, one of them nearly zero, and its Sharpe then sits close to ±2.45 whatever the strategy did. With fewer than 6 months, or with monthly results above the risk-free rate that vary by less than 0.1 % of the account, they can reach extreme values (two almost equal months can give a Sharpe beyond -100). The block table still shows such values, with the mark "few observations" and the reason on hover, and says that the block is not counted in the average. A result stored before this rule keeps its average over every defined block and carries the mark "earlier method".
- OOS trades: The number of trades across all out-of-sample blocks. Figures from few trades change a lot with one trade more or less.
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
- Split data: In-sample window and out-of-sample blocks, as for walk-forward above.
- Per block: PSO on the block's in-sample window → backtest of the tuned parameters on the block
- 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
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
- Build: A strategy is a graph of blocks in the graph editor.
- Backtest: Runs the graph on a date range with the parameter values it has.
- Optimize: PSO searches the tunable parameters on an in-sample range.
- Out-of-sample backtest: Measures the chosen parameters on a later range the search did not read.
- Walk-Forward: Measures the graph, unchanged, on several consecutive out-of-sample blocks.
- WFO: Re-tunes on each block's in-sample window, then measures the block after it.
- 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:
- Close sequences: the last one to four closes rose or fell in a given order, for example three falling closes in a row (30 conditions).
- Range breaks: the close is above the highest high, or below the lowest low, of the previous 5, 10 or 20 bars (6 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
- Lookahead: On bar data the engine runs on closed bars only, so no block reads a bar before it has closed. Tick data runs tick by tick.
- Costs: Every fill pays the spread stored in your data (the configured spread on bars that carry no quotes) and slippage of 5% of the average bar range (ATR); slippage applies to entries, exits at the bar’s close and stops, while a target, being a resting limit order, pays the spread only. Commission is not charged, and overnight financing is off unless you switch it on. Fills in a live market can differ from both.
- Financing: The two financing rates are your broker’s, and you enter them: each charge is the rate times the position’s entry price, once for every night the position is held (with the per-interval model, once per funding interval). The model has no currency, no benchmark rate and no broker markup; it charges only the two rates you enter. The central bank rates Partiqon keeps serve only as the risk-free rate of the Sharpe and Sortino ratios.
- Currency: Partiqon does not convert between currencies. Every money figure is the move in points times the value per point you enter, one rate for the whole backtest. For a market quoted in another currency than your account, that rate applies to every trade, whatever the exchange rate was on the trade’s day; for a stake per point in your own account currency the figure is exact.
- Gaps: A stop or target the price jumps past between two bars fills at the open of the bar after the jump, the first price after the gap, not at its own level. On tick data it fills at the first tick beyond it.
- A bar that reaches both the stop and the target: When a bar opens between a position’s stop and target and reaches both, the stop is booked. A bar gives one high and one low, not the order they came in, so the less favourable of the two is taken. The Diagnostic books such a bar the same way. Backtests run before this rule decided such a bar by where it closed in its range, so a result saved earlier can show a target where a new run shows the stop.
- Number of tunable parameters: Each tunable parameter adds a dimension the search can fit to the in-sample period. That fit shows in the in-sample figures and not in the out-of-sample ones.
- The optimization metric: The metric picked for PSO is the whole fitness of a parameter set; the metrics are not combined. Profit factor, for example, does not see drawdown.
- In-sample figures: A result measured on the range its parameters were chosen on includes whatever the search fitted to that range. A range the search did not read measures the same parameters without it.
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.