How to write a Python stock backtest strategy interface
A Python stock strategy interface should be tested before its returns are interpreted. Construct the class, call generate(data_map), inspect each returned security key and signal series, and verify index and meaning. A type annotation or successful import does not prove that the generated object matches the intended data contract.
This guide supplies two separate checks: a dependency-free portable-contract example and an additional test using real pandas DataFrame and Series objects. CoTrading strategy-interface capabilities remain planned or unverified. Neither check starts Vibe or establishes a complete runner integration, execution model or profitable strategy.
Distinguish the reference interface from this example’s policy
The reviewed reference loading path requires zero-argument construction and a callable generate; its returned object is a dictionary whose values are pandas Series. Downstream reference processing uses the intersection of signal and data keys. It does not by itself require every input security to appear in the output.
This tutorial deliberately tightens that boundary for one smoke-test contract: every supplied stock must return a series, its index must match the input exactly and its values must be zero or one with a declared long-target meaning. These are author-chosen acceptance rules. They must not be described as universal native requirements of every Vibe strategy.
The semantics = 'target_long_state' attribute in the examples is likewise our policy marker, not a mandatory native Vibe attribute. Under this chosen policy, minus one is rejected. That does not establish that all negative signals are invalid for every other strategy interface. Another strategy must declare and test its own signal semantics and permitted execution behavior.
Specify the input map before writing the class
Use a dictionary keyed by stable research identifiers. The pandas fixture has US:A and US:B, each containing a DataFrame with a close column and the same three ordered synthetic dates. A production adapter also needs price units, completion status, required fields and availability conventions; a DataFrame container alone provides none of those guarantees.
Reject empty input maps, wrong frame types, missing close columns, empty frames and missing closes in the real-pandas generator. Check that each input index is unique and increasing before calculating the rule. These checks define this example’s supported data shape rather than promise that arbitrary provider tables have been normalized automatically.
The supplied dates are January 5 through January 7, 2026, but the fixture does not query an exchange calendar. It uses completed observations to test interface alignment. A separate execution study must establish the next tradable session and market restrictions; passing this interface check should not be reported as that later work having occurred.
Preserve labels rather than return a bare array
The pandas Series reference describes a one-dimensional object with axis labels. Labels matter because pandas operations can align by index rather than list position. Returning a Python list of the same three values does not satisfy the real-pandas contract and can conceal a timestamp mismatch.
Construct outputs from the input series so the original date index is retained. A new Series built from a list without an explicit index normally receives a default integer index. Its length can be correct while its labels are wrong. The validator therefore checks both actual Series type and exact index equality, rather than comparing length only.
The Index.is_unique reference provides the uniqueness check. Pandas itself can represent duplicate labels, so rejecting them is our application contract rather than an intrinsic Series limitation. Never silently drop duplicates inside validation and then compare a shortened signal to the original input.
Define and calculate one simple signal
The example class has no required constructor arguments. Its rule compares each completed close with the arithmetic mean of the current and previous two closes. The first two rows have insufficient history and output zero. Strictly above outputs one; equal or below outputs zero. It is a desired long-holding state, not a repeated instruction to buy another full position.
US:A receives closes ten, ten and twelve, producing [0, 0, 1]. US:B receives ten, ten and eight, producing [0, 0, 0]. Both outputs retain their respective three-date input index. No fill, quantity, cost or account return is calculated because those are outside this interface-only task.
The pandas implementation calculates a trailing rolling mean with three required observations and converts the Boolean comparison to integer values. It does not alter the input frames. The test saves deep copies and uses pandas’ frame comparison to verify that both original inputs remain unchanged after generation.
Run the dependency-free contract test honestly
Download the standard-library example, save it as R573.py and run python R573.py with Python 3.12 or later. The source is docs/growth/research/round-5/examples/R573.py. It uses synthetic lists and an explicitly named Signals dataclass to check portable key, index, domain and meaning rules.
That dataclass is not pandas Series. A passing dependency-free result proves only the declared portable checks and signal sequence; it must not be used as evidence of real pandas type compatibility. The separate implementation below closes that specific evidence gap without making a broader runner claim.
The portable pack rejects an outer list, wrong security keys, duplicate input labels, a list substituted for its expected signal object and a negative value under its zero/one policy. It also rejects a missing semantics declaration. Those failures are contract violations for this fixture, not statements about what every possible external engine rejects.
Run the real-pandas supplement separately
Download the pandas supplement, save it as R573-pandas.py, and run it in an environment with pandas 3.0.1. A reproducible reader setup is python -m pip install pandas==3.0.1, followed by python R573-pandas.py. The repository source is docs/growth/research/round-5/examples-extra/R573-pandas.py.
The author actually executed this supplement with Python 3.12.14 and pandas 3.0.1. It produced the two expected signal lists, passed the input-unchanged check and rejected six injected errors: wrong outer return type, wrong keys, non-Series value, duplicate input index, wrong output index and a negative signal under the chosen long-target policy. A separate check rejected undeclared semantics.
This is evidence from actual pandas objects, not an unrun illustrative snippet or a substituted class bearing a similar name. The static website merely serves the file and does not execute Python. Readers need the optional dependency locally; the ordinary portable example remains standard-library only.
Review the adapter before extending the claim
When adapting an existing strategy, decide whether partial key coverage is allowed. The reference intersection behavior permits a subset, whereas this smoke test demands exact coverage to expose an accidentally missing stock. If changing that policy, also define how absent names affect existing positions. A missing signal must not silently mean hold, zero target and forced exit at the same time.
Keep interface conformance, temporal correctness and trading validity as separate results. A perfectly aligned Series can still contain look-ahead logic. A valid negative value under another signal contract does not prove that short execution is available in the chosen market. A passed class construction does not prove that data acquisition or the complete runner succeeded.
Archive source version, Python and pandas versions, input schema, chosen strict policies and actual assertion output. Recheck after adapter, dependency or signal-semantics changes. Completion here means a reproducible, correctly labeled interface test with both portable and actual-pandas evidence, plus explicit limits on the untested stages.
Test policy changes without confusing them with type changes
A valid subset such as a signal for US:A alone is different from an unknown key such as A without its namespace. The reviewed reference intersection can use the former; our full-coverage smoke test deliberately rejects it. Decide that policy before treating a missing output as a bug. If partial output is allowed, preserve an explicit no-signal status for US:B and specify its effect on prior holdings outside this interface test.
Likewise, the Series container can hold values that this long-only example forbids. The validator’s zero/one check enforces an economic output vocabulary, not pandas’ storage capability. Separate container failures, label failures and semantics failures in a handoff report. A dependency upgrade may affect object behavior, while a strategy change may affect the permitted vocabulary; they need different fixes and evidence.
FAQ
Are full key coverage and a semantics attribute native requirements?
No. They are deliberate extra rules of this tutorial’s long-target smoke test; the reviewed reference uses intersecting keys.
Does the standard-library file verify pandas types?
No. Use the optional real-pandas supplement for that separate check.