TradingView backtesting research

Genetic search for trading parameters: fitness and selection bias

A genetic algorithm trading strategy search allocates tests toward promising parameter sets. Grid search covers a specified grid. Neither method establishes an edge: compare equal search budgets, record the random seeds, and test frozen choices on later data.

Inspect the 32-evaluation experiment

Synthetic research example. No live returns or investment recommendation. Sources and product implementation reviewed October 18, 2026.

Choose the search by coverage and budget

A grid with 8 fast lengths and 8 slow lengths contains 64 combinations. Testing all 64 proves only which measured score is highest on that grid. A genetic algorithm uses a population of candidates and a fitness score to guide subsequent tests. A generation can stagnate or lose diversity; improvement is not guaranteed.

Use a complete grid when it fits the evaluation budget and coverage matters. Consider genetic search when the full grid is too costly and you can tolerate missed regions, seed sensitivity, and extra search settings. There is no universal combination-count threshold or speed multiplier. Measure the cost per backtest and count all searches.

Algorithm terminology follows DEAP's algorithm documentation; the implementation below is a small standalone example with explicit rules.

A reproducible population, fitness, and grid baseline

The experiment generates 274 synthetic prices with a sinusoidal return pattern whose period changes. It tests a long-or-flat moving-average rule with full-equity exposure and no leverage. The signal uses prices through t−2, fills at t−1, and earns the return to t. Every entry and exit costs 0.1% of current equity. Each split starts flat and closes any remaining position at its end.

Genes and bounds
Fast: 2 through 9. Slow: 12, 15, 18, 21, 24, 27, 30, 33. Every pair has fast < slow.
Fitness on training data
F = net return (%) − 0.5 × maximum drawdown (%). F = −∞ with fewer than 3 completed trades. Drawdown uses end-of-bar equity.
Population and selection
8 initial distinct candidates; 3 replacement generations. Keep 1 elite. Each parent wins a two-candidate tournament; ties favor the smaller gene tuple.
Crossover and mutation
Take the fast gene from parent A and slow gene from B. Independently reset each gene to a random allowed value with probability 0.25; a reset can retain the same value.
Budget and seeds
8 + 3 × 8 = 32 training evaluations per run, including duplicates and elites. Seeds: 7, 11, 19. The coarse grid uses fast values 2, 4, 6, 9 and all 8 slow values: 32 tests.
Chronological split
Training bars 34 to 177, validation 178 to 225, final test 226 to 273, inclusive. Earlier bars supply indicator warm-up only. Training selects each winner; later splits never enter selection.

For seed 7 the initial population is (7,15), (9,24), (3,15), (5,15), (9,33), (2,21), (2,24), (8,24). Its first elite is (3,15). The script prints all four populations so duplicate candidates and convergence can be checked.

Synthetic results; returns include modeled fees. Fitness is in percentage points.
MethodTestsUniqueWinnerTrain FTrain returnValidationFinal test
Coarse grid3232(2, 12)8.273510.2356%-12.6906%-6.7743%
GA seed 73219(3, 15)-6.1033-3.2691%-12.7231%-10.6418%
GA seed 113214(2, 15)0.68263.1720%-9.0297%-6.7743%
GA seed 193217(2, 12)8.273510.2356%-12.6906%-6.7743%
Full grid reference6464(2, 12)8.273510.2356%-12.6906%-6.7743%

The coarse-grid winner has 10.2356% training return, 3.9242% drawdown, and 6 completed trades: F = 10.2356 − 0.5 × 3.9242 = 8.2735. Seed 7 misses this region. Seed 19 finds it, but its later returns remain negative. The full grid is a separate 64-evaluation reference, not part of the equal-budget comparison.

All three GA runs together cost 96 training evaluations. Selecting only the best seed and calling it a 32-test method would understate the budget. A production implementation can cache duplicates; then compare the same number of unique evaluations or actual compute cost and disclose the convention.

Run the experiment in Python

Copy into a Python 3 file and run it with the standard library. It generates its own prices, prints the candidate trace, and recalculates the table. No market feed, external package, or Pine Script is required. Diagnostic metric replays and the exhaustive reference are outside the 32-test search budget.

import math
import random
from itertools import product

FAST = tuple(range(2, 10))
SLOW = tuple(range(12, 34, 3))
SPACE = list(product(FAST, SLOW))
SPLITS = ((34, 178), (178, 226), (226, 274))
FEE = 0.001  # 10 basis points per entry or exit
prices = [100.0]
for t in range(1, 274):
    period = 25 if t < 178 else (11 if t < 226 else 17)
    r = 0.001 + 0.012 * math.sin(2 * math.pi * t / period)
    prices.append(prices[-1] * (1 + r))


def metrics(gene, start, end):
    fast, slow = gene
    equity = peak = 1.0
    drawdown = 0.0
    position = trades = 0
    for t in range(start, end):
        # Signals use data through t-2. Fill at t-1, earn return to t.
        signal = int(sum(prices[t-1-fast:t-1]) / fast >
                     sum(prices[t-1-slow:t-1]) / slow)
        if signal != position:
            equity *= 1 - FEE
            trades += int(position == 1)
        position = signal
        equity *= 1 + position * (prices[t] / prices[t-1] - 1)
        if t == end - 1 and position:
            equity *= 1 - FEE
            trades += 1
        peak = max(peak, equity)
        drawdown = max(drawdown, 1 - equity / peak)
    return (100 * (equity - 1), 100 * drawdown, trades)


def fitness(gene):
    net, dd, trades = metrics(gene, *SPLITS[0])
    return net - 0.5 * dd if trades >= 3 else -math.inf


def rank(gene):
    return (-fitness(gene), gene)  # Smaller genes break fitness ties.


def ga(seed):
    rng = random.Random(seed)
    population = rng.sample(SPACE, 8)
    evaluated = list(population)
    scores = {g: fitness(g) for g in population}
    def ranked(g):
        return (-scores[g], g)
    trace = [population[:]]
    for _ in range(3):
        def parent():
            return min(rng.sample(population, 2), key=ranked)
        children = [min(population, key=ranked)]  # One elite
        while len(children) < 8:
            a, b = parent(), parent()
            child = [a[0], b[1]]  # One-point crossover
            for j, domain in enumerate((FAST, SLOW)):
                if rng.random() < 0.25:  # Per-gene reset mutation
                    child[j] = rng.choice(domain)
            children.append(tuple(child))
        population = children
        for gene in population:
            scores[gene] = fitness(gene)  # Charge duplicate evaluations too.
        evaluated.extend(population)
        trace.append(population[:])
    assert len(evaluated) == 32
    return min(evaluated, key=ranked), len(set(evaluated)), trace


def report(label, gene, unique):
    values = [metrics(gene, *s) for s in SPLITS]
    print(label, gene, 'unique', unique, 'fitness', round(fitness(gene), 4),
          'return/dd/trades', [tuple(round(x, 4) for x in row) for row in values])


grid = list(product((2, 4, 6, 9), SLOW))
assert len(grid) == 32 and len(SPACE) == 64
report('Grid: 32 evaluations', min(grid, key=rank), len(grid))
for seed in (7, 11, 19):
    winner, unique, trace = ga(seed)
    report('GA seed ' + str(seed) + ': 32 evaluations', winner, unique)
    if seed == 7:
        print('Seed 7 populations:', trace)
# The following uses 64 evaluations; it is only an exhaustive reference.
report('Full grid: 64 evaluations', min(SPACE, key=rank), len(SPACE))

This deliberately short example illustrates search mechanics. Its tiny samples, deterministic price cycles, fixed cost model, and bar-level fills do not estimate real trading performance. The arbitrary fitness weight and minimum trade count are experiment settings, not recommended thresholds.

Selection bias survives a better fitness function

Searching more candidates creates more opportunities to select favorable historical noise. A drawdown penalty changes the target; it does not turn a selected winner into an unbiased estimate. Bailey and coauthors' backtest-overfitting paper studies this distinction between selected in-sample performance and later performance.

A numerical counterexample: suppose 20 independent useless strategies each have a 5% chance of passing a fixed significance threshold by luck. The probability that at least one passes is 1 − 0.95²⁰ = 64.15%. At 100 strategies it is 1 − 0.95¹⁰⁰ = 99.41%. Independence is a teaching assumption; correlated parameter trials do not obey this exact calculation. These probabilities are not the PBO statistic and are not estimates for the synthetic experiment.

Preserve a trial ledger covering parameter sets, seeds, objective weights, strategy rewrites, and failed runs. Predeclare what validation can decide and keep a later final test untouched. If validation suggests another mutation rate or trading rule, it has become development data; reserve new evidence before making a performance claim.

In the example, the full-grid training winner fails validation. That supports rejecting this research result. It does not justify tuning against the final-test column. The final test is shown for teaching; once inspected, it cannot serve as untouched evidence for a follow-up experiment.

Carry the experiment into a TradingView workflow

Freeze symbol, session, timeframe, adjusted-price settings, date boundaries, capital, sizing, commission, slippage, and order timing before comparing candidates. TradingView's broker emulator has its own fill assumptions; reconcile trades and equity with any external Python engine before comparing scores. See TradingView's strategy documentation.

Keep training, validation, and test exports separate. Indicator warm-up may use earlier observations, but never later observations. Define how boundary positions close, whether gaps are needed for overlapping labels or trades, and whether draws are measured intratrade or on closed-trade equity. These choices can change the ranking.

The current Pineify Strategy Optimizer enumerates the Cartesian product of selected input values. Backtest Deep Report parses exported trade rows, including entry and exit times, prices, quantity, net P&L, and excursion fields, and provides trade and risk analysis. A trade export alone cannot reconstruct every bar's equity or rerun untested parameter sets.

Use the Pine Script AI Coding Agent to help express and review a strategy's rules, then test the resulting script in TradingView. Use Backtest Deep Report to inspect each exported run. Keep this page's genetic search, custom fitness, seed ledger, and split-selection procedure in your own research code; they are not claimed as built-in report features or automatic walk-forward optimization.

Frequently asked questions

What does a genetic algorithm optimize in a trading strategy?

It searches a chosen representation, such as moving-average lengths, using a training fitness function. Selection favors higher-scoring candidates; crossover and mutation propose more candidates. It optimizes that historical score, which may reward noise or an unrealistic execution assumption.

Does grid search guarantee the best trading strategy?

A completed exhaustive grid identifies the highest measured score on its finite grid for the specified data and execution model. It says nothing about values between grid points, omitted rules, or future returns. A coarse-to-fine search can miss a better region outside its refined range.

How should I compare genetic search with grid search?

Freeze the data, fitness, costs, parameter bounds, and evaluation budget. Predeclare several random seeds and report every run, including failures and duplicate evaluations. Compare validation results after training has stopped. Choosing the best seed adds another selection step and consumes the combined budget.

Does a fixed random seed prevent overfitting?

A seed makes a stochastic run reproducible under the same code and environment. It does not remove selection bias. Reusing a validation period to tune seeds, mutation rates, or fitness weights turns that period into development data.

Does Pineify currently run this genetic algorithm?

The current Strategy Optimizer generates a Cartesian product of the selected parameter values. This page provides a separate Python genetic-search experiment. Backtest Deep Report analyzes exported trades; it does not implement this search, its custom fitness, or automatic walk-forward selection.

Sources and related research

Primary sources reviewed October 18, 2026. The Python example and all table calculations are independent educational work.