Skip to content

Learn

Stock Backtest Research

Batch-test stock strategy parameters with valid combinations

A parameter search begins with a queue of distinct, valid experiments. Before comparing any stock strategy returns, establish which combinations are permitted and prove that the execution function receives those values. Eighteen configuration files do not establish eighteen different experiments when a wrapper silently calls the same defaults every time.

This guide produces a combination ledger, execution references and effective-parameter receipts. Its small standard-library example uses invented prices and computes signals, so it tests batch construction and parameter delivery rather than historical performance. Run each market and currency separately. CoTrading capabilities remain planned or unverified; the procedure does not assume a product optimizer, data connection or brokerage integration.

Freeze the strategy contract before expanding the grid

Start with a strategy that already passes a single-run check. Record its code revision, input schema, trading calendar, adjustment convention, observation cutoff, order timing, starting cash and cost assumptions. Decide which fields are tunable and which are fixed. Otherwise a search can accidentally compare a moving-average strategy on one dataset with another strategy on a revised dataset.

For this example, the only tunable fields are integer fast lookback, integer slow lookback and a fractional signal threshold. Both lookbacks include the current observation. A signal is one when the fast mean exceeds the slow mean multiplied by one plus the threshold. It is zero otherwise. The example does not implement position sizing, matching or profit accounting; those would need their own validated execution contract before a production backtest.

Specify constraints independently of observed results. Fast must be positive and strictly less than slow; threshold must be nonnegative. The historical input must contain enough observations for the largest slow lookback. Require exact field names and reject unknown keys instead of allowing a misspelled threshold to disappear into a permissive options dictionary. These are validity decisions, not filters for unattractive results.

Count the unfiltered domain and each exclusion

Use fast values 2, 3 and 4; slow values 3, 4 and 5; thresholds 0 and 0.01. The Cartesian product contains 3 × 3 × 2 = 18 proposals. The Python product documentation defines the standard-library iterator used to enumerate that product. Filtering remains an explicit rule that we add ourselves.

FastPermitted slow valuesLegal runs across two thresholdsExcluded runs
23, 4, 560
34, 542
4524
TotalSix legal lookback pairs126

Store rejected configurations with the reason fast>=slow. A rejection ledger explains why a proposed point never entered the engine. A runtime failure is different: its configuration was legal, but its execution did not finish. Keeping those categories separate prevents a timeout from being retrospectively described as an invalid strategy.

In a larger grid, document how overlapping rejection rules are counted. Either assign one first rejection reason in a fixed order or keep multiple reason flags while counting each proposal once. Do not add reason counts and infer a proposal count when one configuration may fail several rules. Deduplicate repeated values before expansion, preserving the original request as a separate record if necessary.

Serialize the validated configuration with sorted keys and hash it. The hashlib documentation supplies SHA-256; a digest identifies supplied bytes, not the economic validity of a strategy. Keep the full serialized configuration alongside the digest so another person can inspect the actual parameters.

The example stores short identifiers for convenience, but a research archive should retain full digests and check uniqueness. Attach the frozen code reference, data digest, execution-policy reference and batch identifier. Configuration identity alone is insufficient when code or input data changes. A repeated configuration under a different data snapshot is a new experiment context, even if its parameter digest remains the same.

A queue row should have a requested configuration, status, attempt number, effective configuration and output reference. Pending, running, completed and failed should have defined meanings. Retrying a failed run adds an attempt under the same frozen context; changing a parameter creates a new candidate. Preserve the unsuccessful attempt and its error instead of replacing it with the eventual success.

Execute through one explicit parameter path

The core of the repository example is:

from itertools import product

for fast, slow, threshold in product([2, 3, 4], [3, 4, 5], [0, 0.01]):
    config = dict(fast=fast, slow=slow, threshold=threshold)
    if fast >= slow:
        excluded.append({"config": config, "reason": "fast>=slow"})
        continue
    result = strategy(PRICES, **config)
    assert result["effective"] == config

Here strategy constructs its receipt from the arguments it actually uses for the moving-average calculation. A receipt copied from the requested queue before entering the strategy would prove little: the wrapper could still discard the request afterward. In an existing engine, inspect the point where validated configuration becomes the strategy constructor or function arguments. Record the resolved values there, including defaults and unit conversions.

Use the same observation interval for all configurations. Our invented closes are [10, 11, 9, 12, 13, 10, 14, 15, 11, 16]; signals are evaluated at zero-based indices 5 through 8. Every candidate therefore has four scored signal observations after sufficient history. The current close enters the signal, so any later trading implementation must wait for an executable subsequent price rather than fill retrospectively at an unavailable close.

Read the actual output as a delivery check

Run python docs/growth/research/round-5/examples/R605.py from the repository root. The standalone file includes the strategy, digests, queue and assertions. Its actual output reconciles proposed=18 excluded=6 completed=12. The first requested configuration, fast 2, slow 3, threshold 0, has effective values that match and signals [0, 0, 1, 0]. The final configuration, fast 4, slow 5, threshold 0.01, also matches and produces [0, 1, 1, 0].

Those two traces establish a useful inspection path from proposed fields to actual signal computation. They do not establish that every different configuration must produce a different signal array. Nearby thresholds can legitimately produce identical decisions on a small input. Verify the parameter receipt first; interpret identical outputs only after that check passes.

Completion here means that this synthetic signal function returned successfully. In a full backtest, completion must additionally require valid signal alignment, order processing and the agreed result artifacts. Do not label an empty output file or an interrupted worker as completed because the process was launched.

Catch the different failure mechanism: ignored configuration

The counterexample bypasses the requested configuration and always calls fast 2, slow 5, threshold 0. All twelve legal queue rows still appear to run. One happens to equal those defaults; eleven do not. Comparing receipts gives ignored_config_mismatches=11, and the assertion detects the broken delivery path without relying on a return difference.

This failure is different from fast exceeding slow. The requests are legal; the adapter is wrong. Repair the argument mapping, rerun affected rows under a new execution reference and retain the invalidated batch. Never copy a requested value into the receipt merely to silence the assertion. If the engine transforms units, compare against an explicit, tested transformation rather than requiring accidental textual equality.

Accept the batch and define when to rebuild it

Before ranking results, reconcile proposed = excluded + legal, then legal = pending + running + completed + failed with each row counted once. Confirm unique identities, readable artifacts and matching effective values. Inspect the two traces above and a rejected row. For retries, reconcile terminal states by candidate and attempt so repeated work does not inflate the number of distinct tested configurations.

Rebuild the experiment context when data, engine, code, fee policy or scoring interval changes. Extend the queue transparently when adding parameter values, preserving all previously attempted points. This article solves executable batch construction; it does not correct statistical selection bias or authorize treating the best observed candidate as predictive. Record the entire search before moving to platform selection or a genuinely unseen holdout test.

Hand off a queue that another runner can inspect

Export one row per candidate with separate columns for requested parameters, resolved parameters, context digest, attempt and terminal status. Keep an exclusion export with the same parameter columns. A second runner should be able to reconstruct the twelve legal requests without reading a chart or copying prose. Validate a deliberately misspelled field before dispatch, confirm it stops the request, and verify that no completed result is created for it. If a worker writes output before crashing, preserve that artifact under its failed attempt; promote it only after the completion checks pass. This makes the accounting usable when execution becomes asynchronous.

Download the complete example. Use Python 3.12 or later and run python R605.py in the download directory. It uses only the standard library and synthetic inputs.

FAQ

Can identical signals prove that parameters were ignored?

No. Different valid settings can generate the same signals. Compare the values resolved inside the strategy before investigating signal sensitivity.

Is this an automatic stock strategy optimizer?

No. It is a standalone batch-construction and delivery example. It neither searches live data nor verifies any CoTrading optimizer capability.