Locate slow stages in a stock backtest
A slow stock backtest needs measured stage boundaries before it needs a faster machine. Separate input loading, indicators, signals, portfolio targets, execution/accounting and output metrics. Then optimize one measured stage while requiring the complete result to remain equivalent. Running fewer stocks is a smaller experiment, not a speed improvement of the original one.
This guide instruments a fixed three-stock synthetic pipeline at two observation counts and compares naive rolling sums with prefix sums. It is an external profiling method, not a CoTrading benchmark. No live data source, cache service or broker is used, and the synthetic profits have no historical investment meaning.
Freeze the measurement contract
Record machine, interpreter, input identity, stock count, observations per stock, lookbacks, execution policy and output requirements. Keep the workload constant between implementations. Include failed or interrupted measurements rather than quietly choosing only the fastest successful trial.
Define which work is timed. Our load stage reparses an in-memory JSON string; generating that string is outside the measured stage. It does not include network transfer, disk access or authentication. A real investigation should time those separately if they matter, rather than interpreting our parser timing as a data-provider latency estimate.
State cache conditions and execution order. The sample repeats three runs per implementation in one process, first naive then optimized, with JSON reparsed each time and no external cache. Allocator state and CPU effects can still change across repetitions. This is an inspectable local experiment, not a guarantee of identical cold-start conditions.
Put timers around meaningful boundaries
The Python performance-counter documentation describes an elapsed-time counter suitable for short durations. Use differences between readings; its absolute reference has no business meaning. The script uses the nanosecond variant and converts differences to milliseconds for display.
started = perf_counter_ns()
indicators = calculate_indicators(prices)
timings['indicators'] = perf_counter_ns() - started
Keep timer overhead outside tight inner loops where possible. Wrapping every arithmetic operation would change the workload and obscure the stage being investigated. Our boundaries are coarse enough to identify the major synthetic work while preserving a straightforward control flow.
Use named stages that map to actual responsibilities. Portfolio target construction in this fixture is a simple copy of zero/one signals; execution then computes share changes, cash and equity. It is not a hidden optimizer. A more complex engine may need separate allocation, risk, matching and persistence stages.
Run a small and a target-sized workload
The fixed basket is A, B and C. Each has a deterministic synthetic price series, generated from the same formula with a different offset. Small runs use 2000 observations per stock and larger runs 8000. Both implementations calculate three-observation and twenty-observation means with the same warm-up.
The signal is one when the fast mean exceeds the slow mean after both are valid. A signal fills at the next observation, targeting zero or one share. Each synthetic stock has its own opening cash 1000, for total opening capital 3000. Costs are zero; the fixture is designed for workload equivalence, not realistic portfolio optimization.
For each size and implementation, retain three stage-time records and report their medians. The output includes six named stages: load, indicators, signals, portfolio targets, account/fills and metrics. Do not add those medians and pretend the sum is necessarily one observed end-to-end runtime; each median can originate from a different repetition.
Locate the measured bottleneck before editing
Inspect the actual output on the current machine. In the recorded local runs, repeated indicator summation was a substantial component. That observation motivates testing a different sum algorithm. It does not establish a universal bottleneck for real engines, where network loading, serialization or matching may dominate.
Compare scaling between the two sizes rather than extrapolating from one tiny run. Keep stock count and strategy fixed while changing observation count. To study universe scaling, perform another explicitly sized experiment instead of mixing a larger date range and fewer stocks into the same claimed comparison.
The Python 3.12 profiler documentation distinguishes execution profiling from benchmarking and notes instrumentation overhead. A profiler can help locate expensive calls inside a slow stage, but those instrumented measurements should not be presented as interchangeable with ordinary elapsed-time runs. This script uses explicit stage timers rather than a profiler.
Optimize the rolling-sum implementation
The baseline repeatedly slices and sums each lookback window. The alternative builds a prefix sum once and obtains a window sum from the difference of two prefix entries. Both use the same integer prices and divide by the same window length, preserving the fixture’s exact mean outputs.
For a window ending at t with length n, the sum is prefix[t+1] - prefix[t-n+1]. Check the first valid index and the last window carefully. An off-by-one change can be faster while subtly changing every signal. Do not discard warm-up observations to make the new array line up visually.
Prefix arithmetic can behave differently with arbitrary floating inputs because of accumulated cancellation and rounding. A production optimization must use appropriate field-level tolerances or a numerically suitable alternative. Exact equality in this integer-price fixture is evidence for this fixture, not a universal floating-point guarantee.
Verify full output equivalence before accepting speed
The script compares indicator arrays, signal arrays, fill ledgers, every equity observation and final metrics against the baseline. It also asserts fixed stock and observation counts and known deterministic metric anchors. A matching final balance alone would miss offsetting trade errors or changed risk along the path.
At 2000 observations, both versions end at 4732 with 2794 fill events across three stocks. At 8000, both end at 10047 with 11266 events. Those are synthetic regression anchors, not returns achieved on market data. They remain unchanged across timed repetitions; measured milliseconds are intentionally allowed to vary.
The different-mechanism failure is reducing work to appear faster. A missing stock, shorter series or skipped output requirement violates coverage checks even if a summary metric looks similar. A second failure is an incorrect rolling boundary; the full indicator comparison catches it before performance interpretation.
Keep cache and correctness explanations together
If a real optimization introduces caching, record cache keys, input versions, hit/miss state and invalidation policy. A warm-cache run and a cold-source run answer different operational questions. Do not delete shared caches or modify a running service merely to make a local timing experiment look clean.
If an optimization changes parallel scheduling or random-stream assignment, check reproducibility as well as equality. Record concurrency and merge order. Faster aggregation with a different event order may change cash feasibility or numerical results, so it needs a separate execution review rather than a timing-only approval.
Archive raw repetitions, medians, environment and output-comparison evidence. Avoid promising a fixed speed multiple from a handful of short measurements. This sample makes no speed-threshold assertion; a slower optimized trial can still pass correctness, while an incorrect fast trial must fail.
Execute and accept the measurement package
Download the complete example. Use Python 3.12 or later and run python R624.py. It performs standard-library synthetic calculations, prints current stage medians for both sizes and checks complete output equivalence with the deterministic anchors above. Its timing values are local observations, not a portable benchmark.
Accept an optimization only after coverage and required outputs match the declared policy, and the observed timing evidence supports the intended operational question. Reprofile when data shape, universe, engine, machine, cache or output requirements change. Preserve the earlier measurement context rather than comparing unrelated numbers under one speed claim.
FAQ
Is a lower runtime enough to accept an optimization?
No. First verify identical workload and the required signal, fill, equity and metric outputs.
Does this measure live data loading speed?
No. The load stage reparses synthetic in-memory JSON and excludes network and disk acquisition.