Skip to content

Learn

Stock Backtest Research

Ablate stock strategy filters with controlled reruns

A stock strategy’s extra filter can improve a development result while also changing which opportunities are ever observed. To inspect its role, verify that the modular implementation reproduces the original baseline, disable one declared module and rerun under the same conditions. Match the changed opportunities before explaining the final performance difference.

This guide produces a switch matrix, added/removed/retimed trade comparison and synthetic net-result table. It is an external method, not a verified CoTrading module system. Use one market and currency. The example uses manually specified pre-entry filter flags and invented subsequent returns, not a market scanner or historical profitability study.

Define modules by executable boundaries

Write the core signal and each filter’s input, information cutoff, decision output and missing-data policy. A volume gate and a trend gate should have separate explicit switches. Do not call an edit “volume off” if it also changes the core threshold, universe or position sizing.

If the existing strategy is monolithic, first create a modular equivalent and run it with every original module enabled. Compare signals, targets, fills and account state under frozen input. A refactor that already changes behavior cannot serve as an equivalent baseline for attributing the next change to one filter.

The backtesting.py strategy documentation describes sequential data revelation and strategy callbacks. It is a useful reminder that filter inputs must be available before the decision they influence. Our small function uses declared fixture flags rather than executing that library or verifying a product integration.

Freeze controls and the switch matrix

Keep code outside the target module, data, period, costs, starting state, execution and sizing fixed. Record an allowed-change set for each variant. A single-module ablation changes exactly one switch relative to baseline; turning both filters off is a different intervention and should be rejected from that comparison.

The example’s baseline enables volume and trend. Variant V disables volume only; variant T disables trend only. The core accepts all five predeclared opportunities before filtering. Each admitted opportunity represents one sequential period of unit exposure, with zero costs and capital compounded across admitted outcomes.

Those assumptions remove cash competition and overlapping holdings from the miniature example. A real multi-position strategy needs its full account rerun because an added trade can consume capital and displace another trade. Merely filtering the old completed-trade list cannot reproduce that mechanism.

Verify the equivalent baseline

Five opportunity records contain identifiers, volume/trend booleans known before entry and later realized returns. Baseline acceptance is the original conjunction of both booleans. The modular runner must produce exactly the same accepted opportunity IDs as that conjunction; the sample asserts this rather than assuming the refactor is harmless.

Keep a reference to the pre-refactor implementation and expected output. Compare failures and missing-value behavior as well as positive cases. A gate that treats missing data as false in one implementation and true in another can change the baseline even when ordinary rows agree.

Only after equivalence is established should the changed switch be executed. Save the baseline once and retain its result, rather than silently rerunning a revised baseline after each variant. If common code changes, invalidate and rebuild the entire comparison context.

Work through five synthetic opportunities

OpportunityVolume gateTrend gateSubsequent return
1passpass+2%
2failpass−3%
3passfail+1%
4passpass+4%
5failfail−1%

Baseline admits one and four, yielding 1.02 × 1.04 − 1 = 6.08%. Disabling volume adds opportunity two, giving 1.02 × 0.97 × 1.04 − 1 = 2.8976%. Disabling trend adds opportunity three, giving 1.02 × 1.01 × 1.04 − 1 = 7.1408%.

Opportunity five remains excluded in both one-module variants because the other enabled filter still rejects it. This is why an ablation effect is conditional on the rest of the strategy. The sample does not claim that each filter has a standalone universal causal contribution equal to the observed return difference.

Match opportunities before comparing totals

Use stable opportunity or signal IDs with entry and exit timestamps. Classify IDs present only in the variant as added, only in baseline as removed, and present in both with changed timing as retimed. Keep quantities and prices in the real ledger so timing changes can be separated from resizing.

added = sorted(variant.keys() - baseline.keys())
removed = sorted(baseline.keys() - variant.keys())
retimed = [key for key in baseline.keys() & variant.keys()
           if baseline[key]['entry_exit'] != variant[key]['entry_exit']]

In this independent-opportunity fixture, V adds only ID two and T adds only ID three; neither removes or retimes an existing trade. Report those empty categories explicitly. Do not imply that all real ablations can only add trades: cash constraints, cooldowns or changed exits can remove or reschedule later opportunities.

The complete sample also alters the exit time of a matched baseline opportunity and verifies that the comparison identifies it as retimed rather than added plus removed. That exercises the matching logic without pretending the main filter example naturally produced a timing change.

Reject mixed interventions

The different-mechanism counterexample turns both volume and trend off. The ablation guard rejects it because two switches differ from baseline. It is not repaired by naming the variant after only one filter. A multi-module experiment may be useful later, but it requires its own declared design and interpretation.

Similarly, a variant with a revised universe, lower fee or different starting cash is not a clean filter ablation. Check control references before attributing performance. If an input defect must be corrected, rerun baseline and variants under a new shared context and retain the original comparison.

Missing or failed variants must remain unavailable. Do not interpret an execution error as a zero-profit alternative or remove it from the switch matrix. The evidence table should show what actually ran, what failed and which trade changes could be inspected.

Interpret the development result narrowly

Under this fixture, removing volume worsens the result and removing trend improves it. That describes the chosen opportunities and costs. It does not prove that volume has an independent causal effect on future returns or that trend should be deleted from a live strategy.

The scikit-learn evaluation guide explains the risk of adjusting choices using evaluation outcomes. If these ablations guide further strategy development, preserve all attempted variants and seek new independent evidence for the final revised candidate. Reusing the same favorable development path is not confirmation.

Look beyond final profit to opportunity counts, exposure, costs, concentration and incomplete trades. A filter may reduce activity enough to make a high percentage result depend on very few observations. Those operational differences belong next to the return table, not hidden in a separate appendix.

Execute and accept the ablation package

Download the complete example. Use Python 3.12 or later and run python R623.py. Standard-library assertions establish baseline equivalence, returns 6.080000%, 2.897600% and 7.140800%, additions two and three, correct retiming classification and rejection of a two-switch intervention.

Accept when the baseline is equivalent, each variant changes only its named switch and every changed trade has an inspectable match or reason. Archive the switch matrix, input timing, common controls, trade comparison and complete results, including rejected or failed variants.

Include rejected opportunity five in the reason ledger: it remains blocked by the other enabled filter in each variant. This prevents a surviving-trade export from being mistaken for full opportunity coverage.

Rebuild the package when module definitions, data, execution, costs or account policy change. The example verifies synthetic module control and matching; it does not validate a real stock filter, a generic modularization tool or an existing product workflow.

FAQ

Can I infer the filter’s independent causal value from the return gap?

No. The gap is conditional on the remaining strategy, input path and execution assumptions.

Must disabling a filter only add trades?

Not in a stateful portfolio. Added activity can change cash, positions and later timing.