Skip to content

Learn

Stock Backtest Research

Diagnose zero trades in a stock backtest from data to execution

A stock backtest can produce an equity curve without producing a single trade. Diagnose it by following the first broken stage: usable data, strategy input keys, warmup, nonzero signals, target quantities and execution permission. A flat curve alone cannot identify which stage failed, and lowering a threshold can conceal the real problem.

This guide delivers a staged diagnosis ledger with six distinct failure cases and one repaired path. The standard-library fixture uses an invented US stock and synthetic prices. It is not a trace from a real engine or proof that CoTrading diagnostic features are available; those capabilities remain planned or unverified.

Preserve one run before changing anything

Keep the exact configuration, source revision, input sample, requested symbols, signal artifact, target artifact and available rejection log. Record the run identifier and date range. If a stage has no output, mark it unobserved rather than assume it worked. An absent rejection log does not prove that no order was rejected; the engine may simply have failed to expose that evidence.

Choose one security and one expected opportunity for the first investigation. A small trace is easier to reconcile than aggregate counts across thousands of symbols. For each stage record its input count, output count, first relevant timestamp and reason code. Stop causal interpretation at the earliest evidenced failure, while retaining downstream unknowns for the next iteration.

Do not use an equity-file timestamp as evidence of execution. Some systems create an initial cash snapshot even when no instrument is loaded. Others value an unchanged holding with stale marks. Both can produce a line on a chart. The question is which actual events moved the account, not whether a plotting function succeeded.

Check data and security identity first

Verify that the requested stock resolves to the intended market and that usable rows exist in the strategy interval. Check required fields, finite positive prices, unique ordered timestamps and the distinction between warmup and performance dates. A nonempty download can still contain no usable opening prices for the scheduled execution model.

The fixture returns data:no_rows for an empty list. It then checks the exact key US:ALFA. Passing the bare key ALFA produces signal:symbol_key even though the numeric rows exist. This separates missing data from an interface mapping mismatch. The fix is a verified identifier translation, not a looser trading condition.

Keep the original and normalized identifiers in the ledger. Renaming every key indiscriminately may merge share classes or hide a wrong market route. After correcting the mapping, rerun the same input and confirm that the strategy receives it. Do not jump directly to a larger date range, which would change two possible causes at once.

Distinguish insufficient history from a legitimate zero

The base fixture contains closes ten, eleven and twelve, a required window of three and a threshold of eleven. If the required window is changed to 120, the first failure is signal:warmup. A strategy that has not accumulated its window is different from a fully evaluated rule that decides not to hold.

Provide enough earlier data or deliberately move the eligible start, according to the approved specification. Do not shorten a 120-day window merely to make trades appear. Report the count of warmup rows separately from eligible decision rows. If every row is still warmup, there has been no tested trading opportunity under that configuration.

With the full three-row window but threshold thirteen, the final close twelve does not qualify. The fixture returns signal:zero. That result can be correct. Compare the intended arithmetic with a hand-checkable row before labeling it a bug. A no-trade outcome may be the proper result of a strict rule on a particular dataset, and changing the threshold creates a new strategy version.

Follow a nonzero signal into quantity

With threshold eleven, the final close qualifies. Reserve a fictional commission of one and calculate whole shares from available cash divided by the supplied execution price ten. With 101 cash, one hundred remains for notional and ten shares are possible. With only five cash, the result rounds down to zero and the report is target:zero_quantity.

This is a sizing failure for this account and quantity rule, not a signal failure. The evidence should include available cash, reserve, price, quantity step and target allocation. In another market the relevant step might be a board lot; verify that rule separately rather than assuming the fixture’s one-share increment applies everywhere.

Check whether the account model actually permits fractional shares, leverage or a minimum notional before proposing a remedy. Those are changes to execution assumptions, not generic debugging switches. For the present fixture, using the approved 101 cash input restores ten shares without modifying the threshold or inventing a fractional trade.

Inspect execution permission last

If the target quantity is positive but the explicit permission flag is false, the result is execution:blocked. The fixture does not assign a real-market reason to that flag. In a production trace, obtain the actual date, security, requested side, rule input and rejection reason. Missing evidence should remain unknown until a minimal reproduction confirms it.

Investor.gov’s order documentation distinguishes market, limit and stop instructions. A target is not itself an executed order, and an order’s price condition matters. For a US example, a limit order can remain unfilled; for other markets, investigate their own relevant rules and historical versions. Do not infer a specific restriction merely from zero fills.

Also check the execution timeline. A nonzero final-close target with no later open in the dataset may have no executable opportunity at all. A duplicated one-row delay can move the target outside the sample. Preserve signal date and intended fill date before concluding that the execution engine ignored a valid order.

Run the fault-injection table

The artifact docs/growth/research/round-5/examples/R595.py uses one deterministic function and changes one input for each independent case. The core order of checks is:

if not rows:
    return 'data:no_rows'
if key != 'US:ALFA':
    return 'signal:symbol_key'
if len(rows) < window:
    return 'signal:warmup'

Subsequent checks test the final signal, affordable quantity and permission. Run python docs/growth/research/round-5/examples/R595.py. The output contains data:no_rows, signal:symbol_key, signal:warmup, signal:zero, target:zero_quantity, execution:blocked and the repaired fill:10. Assertions verify every result and confirm that empty data take precedence even when permission is also false.

The permission case is the different-mechanism counterexample to an ordinary zero signal: both produce no trades, but one has a valid positive target and the other never does. The fixture proves diagnostic classification on these explicit inputs. It does not replace an engine’s actual transaction ledger or prove which reason applies to an unobserved real run.

Verify the repair without optimizing the strategy

Change only the evidenced defect, rerun the minimal trace and inspect the next stage. A corrected key may reveal a genuine warmup issue that was previously hidden. Keep this sequence in the issue record rather than rewriting the original cause after later findings. Once the minimal path reaches execution, rerun the original configuration and reconcile actual fills and cash.

Close the diagnosis when the first failing stage is evidenced, the correction is reproducible and expected downstream behavior is observed. A correct no-trade outcome can also close the issue when the rule was evaluated and genuinely never qualified. Reopen the checks after changing source, identifiers, window, quantity model or market rules. The aim is an explained run, not a forced trade count.

Download the complete example. Save it as R595.py and run python R595.py with Python 3.12 or later. It uses only the standard library and synthetic data.

Keep the diagnosis ledger after a successful repair

Retain a before-and-after row for each tested defect, naming the changed input and the first downstream observation that became available. For a symbol correction, that observation may be a nonempty generator input, not yet a fill. For a quantity correction, it should be a positive order plan before execution permission is evaluated. This prevents the final successful result from erasing the evidence that established the cause.

When several defects coexist, report them in discovery order and rerun earlier checks after every change. A new data source can repair coverage while introducing a different symbol convention. The diagnosis is complete only for the final saved configuration. Keep unexplained stages open instead of using the repaired fixture’s fill:10 as a claim about an unrelated run.

FAQ

Should I keep lowering the threshold until trades appear?

No. First identify the failing stage. A threshold change alters strategy meaning and cannot repair missing data or a wrong key.

Can no trades be a valid outcome?

Yes, if eligible inputs were evaluated and the approved rule never requested an executable position.