Rank stock strategy parameters after trading costs
The highest gross backtest result can lose its lead once comparable trading costs are charged. Diagnose that reversal with a candidate-by-cost table linked to actual fills. Counting signals or subtracting a single arbitrary amount from every strategy does not measure the different notional amounts traded by each candidate.
This guide recomputes two frozen, synthetic round trips under a common basis-point charge and solves their ranking crossover. It is an external ledger sensitivity calculation, not an engine rerun or actual stock return study. Assume the same research period, initial capital and currency for both candidates; CoTrading functionality remains planned or unverified.
Obtain fill-level evidence before making a cost matrix
Export candidate ID, frozen parameters, run reference, security, side, quantity, execution price, currency and original charged fees. Include opening and closing holdings and the account valuation basis. Reconcile the gross result independently so a missing sale or an unmarked position cannot masquerade as a fee effect.
Choose whether the scenario replaces existing fees or adds a separate incremental component. If the reported result is already net, subtracting the complete fee schedule again double-counts it. Recover a documented gross basis or explicitly model only the incremental charge. Preserve original and scenario fees in separate columns rather than overwriting the run’s historical record.
The SEC investor bulletin on fees distinguishes transaction charges from ongoing expenses and points to disclosures and trade confirmations for actual costs. Our invented basis-point scenarios are neither a current brokerage schedule nor a complete tax model for any stock market.
Define the cost unit and the permitted simplification
One basis point is one ten-thousandth of traded value. In this example a rate c charges absolute quantity × execution price × c / 10000 on each buy and each sell. Sum both legs. The same two trade counts can have very different total costs when quantities differ.
Use positive absolute quantities for notional and a separate side sign for cash movement. Reject inconsistent currencies and nonpositive prices instead of summing incomparable amounts. When using Python Decimal, construct rates and prices from decimal strings and declare the rounding policy. Our calculation keeps full intermediate precision and rounds only displayed numbers; a real per-fill fee rounded to cents needs that policy implemented explicitly.
An ad valorem rate omits fixed commissions, minimum charges, asymmetric taxes, spread changes and market impact. List those omissions. Do not call a single uniform number an all-in fee unless the actual components support that claim. The matrix can still be useful as an explicitly limited sensitivity analysis.
Build the two-candidate worked example
Candidate A buys 600 shares at 100 and sells them at 100.2. Its purchase amount is 60000, sale amount 60120, gross profit 120 and two-sided traded notional 120120. Candidate B buys 100 shares at 100 and sells at 101, giving purchase 10000, sale 10100, gross profit 100 and traded notional 20100.
Treat both as experiments using the same 100000 opening research capital over the same synthetic interval. The larger trade count is not the explanation: both have one buy and one sell. A’s greater traded amount is what makes its result more cost-sensitive. The example isolates that mechanism without selecting actual securities.
| Cost per traded side | A net profit | B net profit | Higher result |
|---|---|---|---|
| 0 bp | 120 | 100 | A |
| 1 bp | 107.988 | 97.990 | A |
| 4 bp | 71.952 | 91.960 | B |
These are profit amounts, not annualized percentages. Since opening capital and interval are shared, amounts can be ranked directly. For a report in returns, divide both by the same agreed capital basis and keep annualization separate. Different starting capital would require a comparable normalization first.
Compute the matrix and ranking crossover
For each scenario, calculate each fill’s cost, aggregate by candidate and subtract from the reconciled gross amount. The core formula is deliberately visible:
def net(candidate, bps):
return gross[candidate] - traded_notional[candidate] * bps / 10000
Set the two net amounts equal. Here 120 - 120120*c/10000 = 100 - 20100*c/10000, so c = 20*10000/100020, approximately 1.999600 basis points. It is slightly below two, because the actual sale proceeds also enter traded notional. Rounding notionals to round-trip purchase amounts would shift the crossover.
The complete script derives gross and notional from the fill rows and checks that the two net values agree at the calculated threshold. A finite scenario grid alone might show only that a reversal occurs somewhere between one and four. The algebra supplies a precise crossover for this linear model, not for a nonlinear real-world fee schedule.
Keep ties, failures and rule changes visible
Use a declared tie tolerance or exact decimal comparison. Do not assign a winner from display rounding when two net results round to the same amount. Record a tie at the modeled crossover; if uncertainty in the fee assumption spans it, report that the ranking depends on the assumption rather than presenting one definitive best parameter.
Missing fills are not zero cost. A failed run should have an unavailable scenario result, with its error retained. A candidate with no trades may genuinely have zero transaction cost, but only after the complete run and empty fill ledger are confirmed. Ongoing expenses can still apply even in a no-trade period.
If a fee schedule changes only on the sell side, model that side explicitly. A generic doubled buy-side rate can conceal asymmetric charges. Also reconcile currency and amount units in exports: a column containing cents or percentage points cannot be fed into a basis-point formula without conversion and a tested unit label.
Recognize when frozen-fill subtraction stops being valid
The different-mechanism counterexample is a cash-constrained order. With cash 1000, buying ten shares at 100 uses the entire balance before fees. An additional fee of one makes the required cash 1001. The original fill is no longer feasible under the new assumption. Subtracting one afterward would retain a trade the changed engine should reject or resize.
Mark that scenario rerun_required and execute the complete cash-and-order process under the new fee policy. The same issue arises when fees alter subsequent sizing, leverage eligibility or the ability to place another order. An external recalculation is valid only under the stated fixed-fill assumption, not merely because its arithmetic is correct.
The backtesting.py API describes its own commission treatment, including entry and exit application and execution implications. Read the actual engine’s behavior before reusing its cost field. A reference library’s documentation does not verify a CoTrading adapter or justify assuming every engine implements an identical convention.
Execute, accept and update the candidate decision
Download the complete example. Use Python 3.12 or later and run python R609.py. Its standard-library Decimal calculation produces gross amounts 120 and 100, notionals 120120 and 20100, crossover 1.999600 bp, and four-basis-point net amounts 71.952 and 91.960. Assertions reject a mismatched unit and flag the cash-feasibility boundary.
Archive the fill export, original gross/net definition, cost formulas, scenario units, calculation version and parameter-to-run links. State whether each cell is an independent engine rerun or a fixed-fill external computation. The latter is our example’s method. Preserve A as a rejected high-notional candidate at four basis points rather than deleting it from the table.
For each published cell, retain the two individual fee amounts as well as their sum. That small reconciliation lets a reviewer detect a missing sale charge, an accidental extra round-trip multiplier or a net result charged twice.
Recompute when fills, prices, cost components, rounding or account constraints change. If the new scenario changes execution, replace only the relevant scenario cells with clearly referenced reruns; do not mix methods without labels. A cost-aware ranking is a conditional candidate decision. It cannot establish future profitability or cover charges that were omitted from the ledger.
FAQ
Can I charge only the buy side and double it?
Only when the modeled policy is truly symmetric and both actual notionals are handled correctly. Real buy and sell charges can differ.
Does external subtraction equal a new backtest?
No. It holds fills fixed. Changed cash feasibility or sizing requires execution to be rerun.