Skip to content

Learn

Stock Backtest Research

Migrate stock backtest settings after an engine upgrade

An engine upgrade can accept an old-looking configuration while changing the experiment. Renamed fields, new defaults, fee units or execution semantics may alter signals and fills even when loading succeeds. Migrate the meaning of each setting explicitly, replay a frozen fixture and retain an executable rollback package before claiming compatibility.

This guide implements synthetic old and new configuration adapters around a shared account kernel. It validates semantic mapping and replay mechanics, not a real engine release. The migration tool is an external extension; no native Vibe or CoTrading migrator is assumed. Keep stock market, currency, strategy and input data fixed.

Preserve the pre-upgrade reference

Before upgrading, save the old engine artifact or reproducible environment, strategy code, raw and resolved configuration, fixture data, signals, fills and account outputs. Record dependency versions and execution policy. Without the old version or trustworthy old outputs, successful loading under a new version cannot establish that behavior was preserved.

Choose the compatibility scope. Some changes intentionally alter fees or matching behavior; they should be declared migrations with changed results, not forced into an equivalence test. Define which paths must remain equal and which unsupported cases require a human decision before proceeding.

The backtesting.py API’s version note documents a change in commission application in that library. It illustrates why financial semantics need inspection. Our field names and version labels below are invented fixtures, not a claim that the library used this exact schema or that its real releases have been tested here.

Write the field and default mapping

The synthetic old schema uses period, percentage-valued commission_pct and trade_on_close. Its defaults are three, 0.1 and false. The new schema uses lookback, fee_bps and an explicit execution string. Its omitted fee default is zero, which would change the old experiment.

MeaningOld effective valueExplicit migrated value
Lookbackperiod 3lookback 3
Fee0.1 percent10.0 basis points
Executiontrade_on_close falsenext_open

The effective fee rate in both adapters is 0.001. Resolve the old defaults before mapping, then emit explicit new values instead of trusting new defaults. Retain original, converted and effective representations so a reviewer can verify the unit conversion rather than only inspect renamed keys.

Reject unknown and ambiguous settings

Both adapters reject unrecognized keys. The old misspelling commision_pct cannot quietly disappear and leave the default in effect. A new fee_bp typo is also rejected. Unknown fields may represent unsupported behavior, so deleting them automatically is not a safe compatibility strategy.

This new fixture supports only next-opening execution. An old trade_on_close: true therefore stops migration as unsupported rather than being approximated by next-open. A real upgrade might support an equivalent alternative, but that requires an explicit tested mapping; the script does not invent one.

Validate values as well as names: positive integer lookback, finite nonnegative fee and supported execution. Do not treat a configuration that parses as evidence that these semantics reached the strategy. Preserve a resolved receipt at the point consumed by execution.

Convert configuration before running the new adapter

The essential conversion is:

old = resolve_old(raw)
converted = {
    'lookback': old['period'],
    'fee_bps': str(Decimal(str(old['commission_pct'])) * 100),
    'execution': 'next_open',
}

Multiplying percent units by 100 gives basis points; dividing either representation correctly yields the same decimal rate. The example uses standard-library Decimal arithmetic. A production mapping must additionally inspect minimum charges, asymmetric fees, rounding and any execution consequences of costs rather than reducing everything to one number.

Store a migration record with source and target schema versions, converted fields, explicit defaults, rejected fields and approval requirements for unsupported rules. Do not overwrite the only copy of the old configuration. A reversible deployment needs the original artifact, not an attempted inverse transformation guessed later.

Retain the decision owner and unresolved semantic questions alongside that record.

Replay frozen signals and fills

The fixed prices are [100,101,102,100,104,105]. The unchanged rule compares the current price with its three-observation mean, targets zero or one share and fills at the next observation. It starts with cash 1000, charges both traded sides at rate 0.001 and marks any final share.

Signal targets at indices two, three and four are one, zero and one. Fills buy at 100, sell at 104 and buy at 105, with charges 0.100, 0.104 and 0.105. The exact ending value is 1003.691. Both adapters return matching effective semantics, signals, fills and ending value after migration.

The shared kernel deliberately isolates adapter migration. It does not test two independently implemented real engines. In a production upgrade, execute both actual engine versions and compare their recorded paths under predefined numeric tolerances. A shared helper cannot prove that an untested new matching engine preserves old behavior.

Include boundary fixtures relevant to the actual upgrade, such as an exact-threshold signal, a final open position, a rejected order or a minimum-fee calculation. Choose them from documented semantic risks rather than adding arbitrary market histories until one pair agrees. This example covers only the declared moving-average and proportional-fee path. Extending its coverage requires new executable cases and retained outputs. When a first difference appears, inspect the resolved setting and earliest affected event before changing tolerances. Equivalent final equity can conceal different intermediate cash or fills, so matching a single ending number is weaker than the path contract used here.

Catch the silent-new-default counterexample

Passing only lookback: 3 to the new adapter loads successfully but leaves fee at zero. The same fixture then ends at 1004 instead of 1003.691. This is incompatible with the required old-cost experiment even though the signals and quantities can look similar.

The example asserts that exact counterexample value and the mapped result. It also rejects unknown old/new keys and unsupported close execution. These checks distinguish parsing, semantic preservation and coverage boundaries; a single successful configuration load would exercise none of those distinctions.

If a real upgrade intentionally changes fee semantics, publish the difference and treat it as a changed-policy comparison. Do not adjust expected outputs until they match and then call the release compatible. Keep intentional changes separate from accidental migration defects.

Build and execute the rollback artifact

The script creates a ZIP containing the complete example runner, old configuration, fixed data and expected old result, plus a digest manifest. The Python zipfile documentation supplies the archive interface. Four payload files are hashed; the manifest is an additional archive member.

It restores those verified bytes into another directory and starts a new isolated Python process in old-adapter replay mode. The child reads the restored configuration and data, executes the restored runner and must reproduce the old result. This tests an executable rollback artifact rather than a note saying “revert if necessary.”

The temporary demonstration archive is cleaned up after validation. A production rollback package must be retained with the actual old engine and dependencies; this synthetic standard-library runner is not an installer for an external engine. Do not distribute restricted market data or credentials merely to make a package self-contained.

Accept compatibility with a scoped record

Validate the old commission value before applying the percentage-to-basis-point mapping. Both schema resolvers require a finite, nonnegative Decimal, and the migration path must preserve that restriction instead of relying on the new resolver to discover a bad source later. The fixture sends numeric and textual NaN and both infinities through the old resolver, new resolver and migration entry. Each applicable path raises ValueError; none may turn an invalid legacy fee into an apparently compatible run. Checking only whether a fee is negative would leave NaN untreated.

Download the complete example. Use Python 3.12 or later and run python R625.py. Assertions verify explicit mapping, rate 0.001, exact ending 1003.691 in both adapters, unknown-key rejection, unsupported-rule rejection, zero-fee incompatibility and independent old replay from four hashed payloads.

Accept only the tested equivalent paths, record unsupported rules and retain old artifacts until rollback is no longer required under the deployment policy. If code, data, costs or either engine changes again, rebuild the compatibility evidence. The result verifies synthetic migration mechanics, not real historical performance or an already available product migration service.

FAQ

Does successful configuration loading prove compatibility?

No. Compare actual effective values, signals, fills and account outputs under the declared equivalent semantics.

Should unsupported settings be silently dropped?

No. Reject them or obtain an explicit tested decision before continuing.