Trading Strategy & Backtest Review
A methodology for reviewing a trading strategy, a backtest, or strategy code (Python,
Pine, or any language) for the failure modes that make a backtest look great in a chart
and lose money in production. The output is a severity-rated findings report with a
concrete fix for each problem.
This skill is grounded in the established backtest-overfitting literature, primarily the
work of David H. Bailey and Marcos López de Prado — "The Probability of Backtest
Overfitting" (PBO), "The Deflated Sharpe Ratio", "Pseudo-Mathematics and Financial
Charlatanism", and "The Seven Sins of Quantitative Investing." The central insight of
that literature: if you try enough strategy configurations, a spectacular in-sample
backtest is the expected outcome even when the strategy has zero real edge. Most of the
checklist below exists to detect exactly that.
Not financial advice. This skill performs an educational, methodological review of
a strategy or backtest. It is not investment advice, not a recommendation to trade, and
not a guarantee of any outcome. Past backtest performance — even after every fix here —
does not guarantee live results. The user is solely responsible for their own
trading and risk decisions.
When to Activate
Activate this skill when the user:
- Shares a trading strategy idea, ruleset, or signal and asks whether it is sound.
- Shares backtest code (Python/pandas/
backtrader/vectorbt/zipline, Pine Script,
or any language) and asks for a review.
- Shares backtest results — an equity curve, a metrics table (Sharpe, CAGR, max
drawdown, win rate), or a screenshot from TradingView / a backtesting platform.
- Asks for a "pre-deployment check," "is this overfit?", "why might this fail live?", or a
risk review before putting real capital behind a strategy.
If the user only shares results with no code, you can still run Steps 1, 2, 4 and 5 and
the metrics sanity check; flag that code-level confirmation is needed for several findings.
Step 1: Scope & Context
Before reviewing, establish context. Ask for anything missing — these answers change the
severity of nearly every finding. If the user cannot answer, note the gap as an Info
finding and assume the worst reasonable case.
| Question |
Why it matters |
| Asset class & instrument (equities, futures, FX, crypto, options) |
Drives cost model, liquidity, borrow/funding, session boundaries. |
| Timeframe / bar size (tick, 1m, 1h, daily) |
Sets the realistic Sharpe ceiling and how dangerous look-ahead is. |
| Backtest window & in-sample period |
Short windows + one regime = fragile. |
| Number of parameters in the strategy |
More knobs → more overfitting capacity. |
| Number of strategies / configs tried (the "trials") |
The single biggest driver of false discovery; needed for Deflated Sharpe / PBO. |
| Out-of-sample / walk-forward done? |
If everything is in-sample, results are unverified. |
| Live or paper track record? |
Live > paper > backtest in evidentiary weight. |
| Capital, position size, leverage |
Determines risk-of-ruin and market-impact realism. |
| Costs modeled? (commission, slippage, spread, borrow, funding) |
Frictionless backtests are the most common silent killer. |
Step 2: Severity Model
Rate every finding with this scale. The driving question is always: "Does this change the
decision to trade, or just the precision of the estimate?"
| Severity |
Definition |
Examples |
| Critical |
Invalidates the results entirely, or exposes the account to ruin. The backtest cannot be trusted and/or the strategy can blow up. |
Look-ahead bias in the signal; zero transaction costs on a high-turnover strategy; no risk limit / unbounded position sizing; survivorship-biased universe; results are 100% in-sample with hundreds of trials. |
| High |
Materially inflates performance or materially understates risk; likely flips the strategy from "edge" to "no edge" once corrected. |
Optimistic fills (mid/close, no slippage); over-parameterization without OOS; ignoring borrow/funding; regime dependence on a single bull market. |
| Medium |
Biases results in a meaningful but recoverable way; needs correction before sizing real money. |
Curve-fit stop/target levels; survivorship in a secondary filter; small trade count weakening significance; timezone/session edge cases. |
| Low |
Minor realism or robustness gap; unlikely to flip the conclusion but should be fixed. |
Rounding/contract-size handling; minor parameter sensitivity; cost estimate slightly optimistic. |
| Info |
Context, missing information, or good practice to confirm — not a defect per se. |
"Number of trials not disclosed"; "confirm data is point-in-time"; "consider regime-tagging the equity curve." |
A single Critical finding means do not deploy until fixed, regardless of how good the
metrics look.
Step 3: Failure-Mode Checklist
Walk every item. For each, you have: what it is → how to detect it → why it inflates
performance → the fix. Cover at minimum the 16 modes below.
3.1 Look-ahead bias (usually Critical)
What: The strategy uses information at bar t that was not actually available until
t+1 or later — future prices, end-of-bar values used intrabar, same-bar close used to
decide a same-bar entry, full-series statistics (mean, z-score, scaler) computed over
data that includes the future.
Detect: Signals that index the future (close[t+1], df['close'].shift(-1),
.shift(-n) feeding a feature); indicators computed on the entire series then used at
earlier bars; fillna(method='bfill'); a StandardScaler/MinMaxScaler fit on the
whole dataset before the train/test split; using a bar's high/low to decide entries that
also assume the favorable extreme was filled; resampling that leaks the closing bar.
Why it inflates: Trading with tomorrow's information is the most powerful "edge" there
is — and it is entirely fake. It typically produces unrealistically smooth equity curves
and Sharpe ratios that are physically impossible.
Fix: Use point-in-time data and a frozen, as-of view of every input. Lag all
signals by one bar relative to execution. Fit any normalizer/scaler inside the
walk-forward fold on training data only. Decide on bar t's closed values, execute at
bar t+1's open (or next available price).
# BAD — decides and fills on the same bar's close (look-ahead)
signal = (df['close'] > df['sma']).astype(int)
df['ret'] = signal * df['close'].pct_change() # uses same-bar close
# GOOD — decide on closed bar t, execute next bar
signal = (df['close'] > df['sma']).astype(int)
df['position'] = signal.shift(1) # act on prior bar's signal
df['ret'] = df['position'] * df['close'].pct_change()
3.2 Survivorship bias (High–Critical)
- What: The universe only contains instruments that survived to today — delisted,
bankrupt, merged, or relegated names are missing.
- Detect: Universe pulled from a current index membership / current ticker list;
equities backtest with no delisting returns; crypto backtest excluding dead coins; using
"today's S&P 500" over a 20-year history.
- Why it inflates: You systematically exclude the losers. Buy-and-hold and especially
mean-reversion strategies look far better than reality because the names that went to zero
were never in the test.
- Fix: Use a point-in-time, survivorship-free universe with historical constituents
and delisting/bankruptcy returns. Reconstruct index membership as of each date.
3.3 Data-snooping / overfitting (too many params, in-sample tuning) (Critical)
- What: Parameters were chosen by maximizing performance on the same data used to report
it; or the strategy has so many free parameters it can fit noise.
- Detect: Grid/optimizer over many parameters reporting only the best result; no
held-out data; parameter count high relative to trade count; "we tried a few and kept the
best."
- Why it inflates: With enough knobs and enough trials, you can fit historical noise
perfectly. Per Bailey–López de Prado, the expected maximum in-sample Sharpe across N
independent trials grows roughly like √(2·ln N) even when true Sharpe is zero — so a great
backtest is the default outcome of a search, not evidence of edge.
- Fix: Out-of-sample holdout + walk-forward analysis. Minimize parameters
(parsimony). Report the Deflated Sharpe Ratio and PBO (below). Pre-register the
rule before testing where possible.
3.4 Multiple testing / p-hacking (Critical)
- What: Many configurations, indicators, or universes were tried; only the winner is
reported. The reported Sharpe is a maximum over trials, not a single draw.
- Detect: "Selected the best of K"; many indicators/lookbacks scanned; the count of
trials is undisclosed or large.
- Why it inflates: The more you try, the higher the best in-sample result purely by
chance. A nominal Sharpe of 2.0 over 1,000 trials may be statistically indistinguishable
from luck.
- Fix: Deflated Sharpe Ratio (DSR) — adjust the observed Sharpe for the number of
trials, the variance of trial Sharpes, and the non-normality (skew/kurtosis) of returns;
DSR is the probability the true Sharpe > 0 after that haircut. Probability of Backtest
Overfitting (PBO) via Combinatorially Symmetric Cross-Validation (CSCV): the fraction of
splits where the in-sample-best config underperforms the median out-of-sample. PBO > 0.5
means the selection process is overfit. Always report how many trials were run.
3.5 Unrealistic fills (High–Critical)
- What: Orders assumed to fill at prices you could not actually get — mid, close,
best-case high/low of the bar, or instantly with no queue.
- Detect: Backtest fills at the signal bar's
close; limit orders assumed filled
whenever price merely touched the level; market-on-open with no gap handling; no spread.
- Why it inflates: Best-case fills quietly add returns every single trade; the effect is
largest for high-frequency / high-turnover strategies.
- Fix: Model the bid/ask spread; fill market orders at the next bar's open (or
worse); for limit orders, require the price to trade through the level, not just touch
it, and account for queue position. Add conservative slippage (below).
3.6 Slippage & commission omitted/under-modeled (High–Critical)
What: No (or token) per-trade costs.
Detect: Cost = 0; commission set but slippage = 0; costs not scaled by turnover.
Why it inflates: A strategy with a 5 bps gross edge and 200% monthly turnover is
guaranteed to lose after realistic costs, yet looks profitable frictionless.
Fix: Apply realistic commission + spread + slippage per fill, scaled by turnover.
A useful sanity check: compute the break-even cost (the per-trade cost at which the
strategy's edge disappears) and ask whether real-world costs exceed it.
COST_BPS = 5 # round-trip commission + half-spread + slippage, in basis points
df['turnover'] = df['position'].diff().abs()
df['net_ret'] = df['gross_ret'] - df['turnover'] * (COST_BPS / 1e4)
3.7 Market impact & liquidity ignored (High)
- What: Assumes you can trade any size at the quoted price with no impact.
- Detect: Position sizes large vs. average daily volume (ADV); thin instruments; no cap
on participation rate.
- Why it inflates: Real orders move the price against you; the backtest captures alpha
that evaporates at size.
- Fix: Cap order size to a fraction of ADV (e.g. ≤1–5% participation), add a
size-dependent impact cost model (e.g. square-root impact), and stress-test at the
capital you actually intend to deploy.
3.8 Regime dependence (High)
- What: The strategy only works in one market regime (one long bull market, one
low-vol period, one rate environment) present in the backtest.
- Detect: Backtest spans a single regime; equity curve is one smooth uptrend; no
drawdowns through 2008/2020/2022-type stress.
- Why it inflates: It is a bet on the regime continuing, not a durable edge.
- Fix: Test across multiple regimes (bull/bear/high-vol/low-vol/rate cycles), tag
performance by regime, and run out-of-sample on a different regime than the one tuned
on.
3.9 No out-of-sample / walk-forward validation (Critical)
- What: All reported performance is in-sample.
- Detect: No train/test split; no holdout; no walk-forward.
- Why it inflates: In-sample performance is an upper bound, not an estimate of the
future.
- Fix: Walk-forward analysis — repeatedly tune on a rolling/anchored window and
evaluate on the immediately following untouched window; concatenate the out-of-sample
segments to get an honest equity curve. Keep a final lockbox holdout never used for
tuning.
3.10 Position sizing & risk-of-ruin (Critical)
- What: Sizing is ad hoc, fixed-fractional too large, or unbounded; no account-level
risk limit.
- Detect: "All-in" sizing; no max position; bet size that risks a large fraction of
capital per trade; ignoring the probability of a terminal drawdown.
- Why it inflates / endangers: Even a positive-edge strategy bankrupts with too-large
bets due to the variance drag and path dependence; risk-of-ruin can be high even when
expectancy is positive.
- Fix: Volatility targeting (size to a target portfolio vol) and/or a fractional
Kelly stake (e.g. ¼–½ Kelly, never full Kelly), with hard per-trade and per-account risk
caps. Estimate risk-of-ruin explicitly given edge, variance, and bet size.
3.11 Leverage & path dependence (High–Critical)
- What: Leverage applied to the return series without modeling margin, intraday path,
or forced liquidation.
- Detect: Returns simply multiplied by a leverage factor; no margin call / liquidation
logic; ignoring volatility drag of leveraged compounding.
- Why it inflates: A 3× return series ignores that an intraday spike can liquidate you
before the favorable close; leveraged compounding suffers volatility decay.
- Fix: Model margin requirements, intraday path, and liquidation; account for
volatility drag; cap leverage; backtest on the actual path, not just close-to-close.
3.12 Funding & borrow costs ignored (High for shorts/crypto/leverage)
- What: Short rebate/borrow fees, hard-to-borrow constraints, perpetual-swap funding
rates, and carry are omitted.
- Detect: Short strategy with no borrow cost; crypto perp strategy with no funding;
long-short with no financing.
- Why it inflates: Borrow/funding can exceed the gross edge, especially on shorts and
crypto perps where funding flips sign and is paid continuously.
- Fix: Apply borrow fees (and hard-to-borrow availability) on shorts and funding
rates on perps; include financing on leveraged/long-short books.
3.13 Timezone / session-boundary bugs (Medium–High)
- What: Bars aligned to the wrong timezone, daylight-saving shifts, or session
open/close handled incorrectly; mixing exchange time and UTC.
- Detect: Naive timestamps; DST not handled; daily bars rolled at the wrong hour;
overnight gaps treated as intraday moves; 24/7 crypto vs. session-based equities mismatch.
- Why it inflates: Misaligned bars can accidentally introduce look-ahead or attribute
the overnight gap to an intraday signal.
- Fix: Use timezone-aware timestamps, define sessions explicitly per instrument,
handle DST, and verify bar boundaries against the exchange calendar.
3.14 Repainting indicators (Pine / charting) (Critical on the affected signal)
- What: An indicator changes its historical values after the fact —
security()
pulling higher-timeframe data without offset/lookahead guards, signals confirmed only on
the closing bar but evaluated intrabar in the backtest, request.security(..., lookahead=barmerge.lookahead_on).
- Detect: Pine
request.security with lookahead_on; signals that reference the
current (unclosed) bar; alerts that fire then disappear; backtest results that change on
reload.
- Why it inflates: Repainting bakes future information into historical signals — the
on-chart backtest is fiction.
- Fix: Use
lookahead=barmerge.lookahead_off and reference confirmed/closed values
([1] offset for HTF data); only act on bar-close confirmed signals
(barstate.isconfirmed); compare strategy-tester results to bar-close-only alerts.
3.15 Curve-fit stop-loss / take-profit levels (Medium–High)
- What: Stop and target levels (or trailing parameters) tuned to maximize the same
backtest.
- Detect: Oddly specific levels (e.g. "stop at 2.37×ATR, target 4.81×ATR"); stops/targets
swept in the same optimization as entries.
- Why it inflates: Exits become additional fitted parameters that snugly avoid the
specific historical losers.
- Fix: Derive exits from risk logic (volatility/ATR-based, fixed-R multiples) chosen
a priori; validate stop/target robustness out-of-sample; prefer few, round,
rule-based levels over swept ones.
3.16 Too few trades / weak statistical significance (Medium–High)
- What: The track record rests on too few trades to distinguish skill from luck.
- Detect: < ~30 trades (often < 100 for a real claim); a handful of trades dominate P&L;
concentrated in one period.
- Why it inflates: Small samples have huge sampling error; one or two lucky trades can
manufacture a great Sharpe.
- Fix: Require a meaningful trade count, test across periods, and report confidence
intervals on the metrics; treat the Sharpe as an estimate with error bars, not a point.
Step 4: Red-Flags Quick Scan
Instant danger signs. Any hit warrants a finding and a closer look at the related checklist
item.
- Sharpe > 3 on daily data (or > ~2 for a simple retail strategy) — almost always
look-ahead, cost omission, or overfitting.
- Equity curve is "too smooth" / near-straight diagonal with tiny drawdowns — classic
look-ahead or in-sample fitting signature.
- No transaction costs / slippage anywhere in the code.
- Indicator or signal references the future —
close[future], shift(-n),
lookahead_on, full-series fit() before split.
- Parameters tuned on the whole series with no holdout.
- < 30 trades, or P&L dominated by 1–2 trades.
- Win rate ~100% or suspiciously high with no losing streak.
- Many configs tried, only the best reported (undisclosed trial count).
- Universe is "today's" index / current tickers over a long history (survivorship).
- Leverage applied by multiplying returns, no margin/liquidation logic.
- Shorts/perps with no borrow/funding cost.
- Backtest results change on reload (repainting).
- Reported max drawdown is implausibly small relative to the strategy's volatility.
Step 5: Output Format
Produce the review in this structure.
5.1 Findings summary table
| ID |
Issue |
Severity |
Where |
| F1 |
Look-ahead: signal uses same-bar close |
Critical |
strategy.py:42 |
| F2 |
No transaction costs modeled |
Critical |
backtest.py (no cost term) |
| F3 |
Params tuned on full series, no OOS |
High |
optimizer block |
| … |
… |
… |
… |
5.2 Per-finding detail
For each finding:
- ID & title (e.g. F1 — Look-ahead bias in entry signal).
- Severity and one-line rationale.
- Impact: what it does to the reported numbers (e.g. "inflates Sharpe; equity curve is
not achievable").
- How to verify: the exact check the user can run to confirm (e.g. "lag the signal by one
bar and re-run — if returns collapse, this was the source").
- Fix: concrete remedy, with a short before/after code snippet where useful.
5.3 Metrics sanity check
Recompute or critically assess the headline metrics; define each so the user can audit them:
- Sharpe ratio = (annualized mean excess return) ÷ (annualized return volatility).
Daily-to-annual scaling uses √252 (√365 or √(24·365) for crypto). Flag any Sharpe that is
implausible for the asset/timeframe.
- Sortino ratio = like Sharpe but the denominator is downside deviation only
(volatility of negative returns), rewarding strategies that are only "volatile" to the
upside.
- Max drawdown = the largest peak-to-trough decline in the equity curve; check it is
measured on the net (post-cost) curve and is plausible vs. the strategy's volatility.
- Deflated Sharpe Ratio (DSR) = the probability the true Sharpe > 0 after correcting the
observed Sharpe for the number of trials, the dispersion of trial Sharpes, and
skew/kurtosis of returns. Ask for the trial count so DSR can be assessed.
- Probability of Backtest Overfitting (PBO) = via CSCV, the fraction of splits where the
in-sample-best configuration ranks below median out-of-sample; > 0.5 ⇒ overfit.
- Trade count & exposure: number of trades (significance), average holding period,
time in market (exposure) — low exposure with a high Sharpe can still mean tiny,
fragile edges.
- Turnover & break-even cost: confirm net-of-cost results and that real costs are below
the break-even cost.
Where you cannot recompute (no return series provided), say so explicitly and state what the
user must supply.
5.4 Verdict
A short, honest bottom line: which findings (if any) are Critical and therefore block
deployment, what must be re-run (e.g. "add costs + lag signals + walk-forward, then
re-evaluate"), and whether the remaining edge is plausible.
5.5 Disclaimer (always include)
Educational review — not financial advice. This is a methodological critique of a
strategy/backtest, not investment advice and not a recommendation to trade. Even after
every fix, past backtest performance does not guarantee live results. Markets change,
costs and liquidity vary, and all trading carries risk of loss. You are solely responsible
for your own trading and risk decisions.
Tools that operationalize these fixes
These Viprasol open-source tools implement parts of this methodology and pair naturally with
this review:
bar-by-bar — an agentic backtester
with a built-in look-ahead guard that structurally prevents the Step 3.1 / 3.9 class of
bugs (point-in-time iteration, no future leakage).
edgehunt — a prediction-market
edge/arbitrage engine for finding and pricing real edges, useful when validating whether a
measured edge survives realistic costs (Step 3.6 / 3.7).
Use this skill to find the failure modes; use those tools to enforce the fixes.
1---2name: trading-strategy-review3description: Review trading strategies and backtests for the failure modes that blow up live — look-ahead bias, overfitting, survivorship bias, unrealistic fills, and risk-of-ruin. Grounded in backtest-overfitting research (PBO, Deflated Sharpe). Outputs severity-rated findings with fixes. Use to vet a strategy, audit a backtest, or pre-deployment risk-check.4---56# Trading Strategy & Backtest Review78A methodology for reviewing a trading strategy, a backtest, or strategy code (Python,9Pine, or any language) for the failure modes that make a backtest look great in a chart10and lose money in production. The output is a **severity-rated findings report** with a11concrete fix for each problem.1213This skill is grounded in the established backtest-overfitting literature, primarily the14work of **David H. Bailey and Marcos López de Prado** — *"The Probability of Backtest15Overfitting"* (PBO), *"The Deflated Sharpe Ratio"*, *"Pseudo-Mathematics and Financial16Charlatanism"*, and *"The Seven Sins of Quantitative Investing."* The central insight of17that literature: if you try enough strategy configurations, **a spectacular in-sample18backtest is the expected outcome even when the strategy has zero real edge.** Most of the19checklist below exists to detect exactly that.2021> **Not financial advice.** This skill performs an *educational, methodological review of22> a strategy or backtest*. It is not investment advice, not a recommendation to trade, and23> not a guarantee of any outcome. Past backtest performance — even after every fix here —24> does **not** guarantee live results. The user is solely responsible for their own25> trading and risk decisions.2627---2829## When to Activate3031Activate this skill when the user:3233- Shares a trading strategy idea, ruleset, or signal and asks whether it is sound.34- Shares **backtest code** (Python/pandas/`backtrader`/`vectorbt`/`zipline`, Pine Script,35 or any language) and asks for a review.36- Shares **backtest results** — an equity curve, a metrics table (Sharpe, CAGR, max37 drawdown, win rate), or a screenshot from TradingView / a backtesting platform.38- Asks for a "pre-deployment check," "is this overfit?", "why might this fail live?", or a39 risk review before putting real capital behind a strategy.4041If the user only shares *results* with no code, you can still run Steps 1, 2, 4 and 5 and42the metrics sanity check; flag that code-level confirmation is needed for several findings.4344---4546## Step 1: Scope & Context4748Before reviewing, establish context. Ask for anything missing — these answers change the49severity of nearly every finding. If the user cannot answer, note the gap as an `Info`50finding and assume the worst reasonable case.5152| Question | Why it matters |53|---|---|54| **Asset class & instrument** (equities, futures, FX, crypto, options) | Drives cost model, liquidity, borrow/funding, session boundaries. |55| **Timeframe / bar size** (tick, 1m, 1h, daily) | Sets the realistic Sharpe ceiling and how dangerous look-ahead is. |56| **Backtest window & in-sample period** | Short windows + one regime = fragile. |57| **Number of parameters** in the strategy | More knobs → more overfitting capacity. |58| **Number of strategies / configs tried** (the "trials") | The single biggest driver of false discovery; needed for Deflated Sharpe / PBO. |59| **Out-of-sample / walk-forward done?** | If everything is in-sample, results are unverified. |60| **Live or paper track record?** | Live > paper > backtest in evidentiary weight. |61| **Capital, position size, leverage** | Determines risk-of-ruin and market-impact realism. |62| **Costs modeled?** (commission, slippage, spread, borrow, funding) | Frictionless backtests are the most common silent killer. |6364---6566## Step 2: Severity Model6768Rate every finding with this scale. The driving question is always: *"Does this change the69decision to trade, or just the precision of the estimate?"*7071| Severity | Definition | Examples |72|---|---|---|73| **Critical** | Invalidates the results entirely, or exposes the account to ruin. The backtest cannot be trusted and/or the strategy can blow up. | Look-ahead bias in the signal; zero transaction costs on a high-turnover strategy; no risk limit / unbounded position sizing; survivorship-biased universe; results are 100% in-sample with hundreds of trials. |74| **High** | Materially inflates performance or materially understates risk; likely flips the strategy from "edge" to "no edge" once corrected. | Optimistic fills (mid/close, no slippage); over-parameterization without OOS; ignoring borrow/funding; regime dependence on a single bull market. |75| **Medium** | Biases results in a meaningful but recoverable way; needs correction before sizing real money. | Curve-fit stop/target levels; survivorship in a secondary filter; small trade count weakening significance; timezone/session edge cases. |76| **Low** | Minor realism or robustness gap; unlikely to flip the conclusion but should be fixed. | Rounding/contract-size handling; minor parameter sensitivity; cost estimate slightly optimistic. |77| **Info** | Context, missing information, or good practice to confirm — not a defect per se. | "Number of trials not disclosed"; "confirm data is point-in-time"; "consider regime-tagging the equity curve." |7879A single **Critical** finding means *do not deploy* until fixed, regardless of how good the80metrics look.8182---8384## Step 3: Failure-Mode Checklist8586Walk every item. For each, you have: **what it is → how to detect it → why it inflates87performance → the fix.** Cover at minimum the 16 modes below.8889### 3.1 Look-ahead bias *(usually Critical)*90- **What:** The strategy uses information at bar *t* that was not actually available until91 *t+1* or later — future prices, end-of-bar values used intrabar, same-bar close used to92 decide a same-bar entry, full-series statistics (mean, z-score, scaler) computed over93 data that includes the future.94- **Detect:** Signals that index the future (`close[t+1]`, `df['close'].shift(-1)`,95 `.shift(-n)` feeding a feature); indicators computed on the *entire* series then used at96 earlier bars; `fillna(method='bfill')`; a `StandardScaler`/`MinMaxScaler` fit on the97 whole dataset before the train/test split; using a bar's high/low to decide entries that98 also assume the favorable extreme was filled; resampling that leaks the closing bar.99- **Why it inflates:** Trading with tomorrow's information is the most powerful "edge" there100 is — and it is entirely fake. It typically produces unrealistically smooth equity curves101 and Sharpe ratios that are physically impossible.102- **Fix:** Use **point-in-time data** and a **frozen, as-of view** of every input. Lag all103 signals by one bar relative to execution. Fit any normalizer/scaler **inside** the104 walk-forward fold on training data only. Decide on bar *t*'s **closed** values, execute at105 bar *t+1*'s open (or next available price).106107 ```python108 # BAD — decides and fills on the same bar's close (look-ahead)109 signal = (df['close'] > df['sma']).astype(int)110 df['ret'] = signal * df['close'].pct_change() # uses same-bar close111112 # GOOD — decide on closed bar t, execute next bar113 signal = (df['close'] > df['sma']).astype(int)114 df['position'] = signal.shift(1) # act on prior bar's signal115 df['ret'] = df['position'] * df['close'].pct_change()116 ```117118### 3.2 Survivorship bias *(High–Critical)*119- **What:** The universe only contains instruments that survived to today — delisted,120 bankrupt, merged, or relegated names are missing.121- **Detect:** Universe pulled from a *current* index membership / current ticker list;122 equities backtest with no delisting returns; crypto backtest excluding dead coins; using123 "today's S&P 500" over a 20-year history.124- **Why it inflates:** You systematically exclude the losers. Buy-and-hold and especially125 mean-reversion strategies look far better than reality because the names that went to zero126 were never in the test.127- **Fix:** Use a **point-in-time, survivorship-free universe** with historical constituents128 and delisting/bankruptcy returns. Reconstruct index membership as of each date.129130### 3.3 Data-snooping / overfitting (too many params, in-sample tuning) *(Critical)*131- **What:** Parameters were chosen by maximizing performance on the same data used to report132 it; or the strategy has so many free parameters it can fit noise.133- **Detect:** Grid/optimizer over many parameters reporting only the *best* result; no134 held-out data; parameter count high relative to trade count; "we tried a few and kept the135 best."136- **Why it inflates:** With enough knobs and enough trials, you can fit historical noise137 perfectly. Per Bailey–López de Prado, the **expected maximum** in-sample Sharpe across N138 independent trials grows roughly like √(2·ln N) even when true Sharpe is zero — so a great139 backtest is the *default* outcome of a search, not evidence of edge.140- **Fix:** **Out-of-sample holdout + walk-forward** analysis. Minimize parameters141 (parsimony). Report the **Deflated Sharpe Ratio** and **PBO** (below). Pre-register the142 rule before testing where possible.143144### 3.4 Multiple testing / p-hacking *(Critical)*145- **What:** Many configurations, indicators, or universes were tried; only the winner is146 reported. The reported Sharpe is a **maximum over trials**, not a single draw.147- **Detect:** "Selected the best of K"; many indicators/lookbacks scanned; the count of148 trials is undisclosed or large.149- **Why it inflates:** The more you try, the higher the best in-sample result purely by150 chance. A nominal Sharpe of 2.0 over 1,000 trials may be statistically indistinguishable151 from luck.152- **Fix:** **Deflated Sharpe Ratio (DSR)** — adjust the observed Sharpe for the number of153 trials, the variance of trial Sharpes, and the non-normality (skew/kurtosis) of returns;154 DSR is the probability the true Sharpe > 0 after that haircut. **Probability of Backtest155 Overfitting (PBO)** via Combinatorially Symmetric Cross-Validation (CSCV): the fraction of156 splits where the in-sample-best config underperforms the median out-of-sample. **PBO > 0.5157 means the selection process is overfit.** Always report **how many trials** were run.158159### 3.5 Unrealistic fills *(High–Critical)*160- **What:** Orders assumed to fill at prices you could not actually get — mid, close,161 best-case high/low of the bar, or instantly with no queue.162- **Detect:** Backtest fills at the signal bar's `close`; limit orders assumed filled163 whenever price merely *touched* the level; market-on-open with no gap handling; no spread.164- **Why it inflates:** Best-case fills quietly add returns every single trade; the effect is165 largest for high-frequency / high-turnover strategies.166- **Fix:** Model the **bid/ask spread**; fill market orders at the **next bar's open** (or167 worse); for limit orders, require the price to **trade through** the level, not just touch168 it, and account for queue position. Add conservative slippage (below).169170### 3.6 Slippage & commission omitted/under-modeled *(High–Critical)*171- **What:** No (or token) per-trade costs.172- **Detect:** Cost = 0; commission set but slippage = 0; costs not scaled by turnover.173- **Why it inflates:** A strategy with a 5 bps gross edge and 200% monthly turnover is174 *guaranteed* to lose after realistic costs, yet looks profitable frictionless.175- **Fix:** Apply realistic **commission + spread + slippage** per fill, scaled by turnover.176 A useful sanity check: compute the **break-even cost** (the per-trade cost at which the177 strategy's edge disappears) and ask whether real-world costs exceed it.178179 ```python180 COST_BPS = 5 # round-trip commission + half-spread + slippage, in basis points181 df['turnover'] = df['position'].diff().abs()182 df['net_ret'] = df['gross_ret'] - df['turnover'] * (COST_BPS / 1e4)183 ```184185### 3.7 Market impact & liquidity ignored *(High)*186- **What:** Assumes you can trade any size at the quoted price with no impact.187- **Detect:** Position sizes large vs. average daily volume (ADV); thin instruments; no cap188 on participation rate.189- **Why it inflates:** Real orders move the price against you; the backtest captures alpha190 that evaporates at size.191- **Fix:** Cap order size to a fraction of ADV (e.g. ≤1–5% participation), add a192 size-dependent **impact cost** model (e.g. square-root impact), and stress-test at the193 capital you actually intend to deploy.194195### 3.8 Regime dependence *(High)*196- **What:** The strategy only works in one market regime (one long bull market, one197 low-vol period, one rate environment) present in the backtest.198- **Detect:** Backtest spans a single regime; equity curve is one smooth uptrend; no199 drawdowns through 2008/2020/2022-type stress.200- **Why it inflates:** It is a bet on the regime continuing, not a durable edge.201- **Fix:** Test across **multiple regimes** (bull/bear/high-vol/low-vol/rate cycles), tag202 performance by regime, and run **out-of-sample on a different regime** than the one tuned203 on.204205### 3.9 No out-of-sample / walk-forward validation *(Critical)*206- **What:** All reported performance is in-sample.207- **Detect:** No train/test split; no holdout; no walk-forward.208- **Why it inflates:** In-sample performance is an upper bound, not an estimate of the209 future.210- **Fix:** **Walk-forward analysis** — repeatedly tune on a rolling/anchored window and211 evaluate on the immediately following untouched window; concatenate the out-of-sample212 segments to get an honest equity curve. Keep a **final lockbox** holdout never used for213 tuning.214215### 3.10 Position sizing & risk-of-ruin *(Critical)*216- **What:** Sizing is ad hoc, fixed-fractional too large, or unbounded; no account-level217 risk limit.218- **Detect:** "All-in" sizing; no max position; bet size that risks a large fraction of219 capital per trade; ignoring the probability of a terminal drawdown.220- **Why it inflates / endangers:** Even a positive-edge strategy bankrupts with too-large221 bets due to the variance drag and path dependence; risk-of-ruin can be high even when222 expectancy is positive.223- **Fix:** **Volatility targeting** (size to a target portfolio vol) and/or a **fractional224 Kelly** stake (e.g. ¼–½ Kelly, never full Kelly), with hard per-trade and per-account risk225 caps. Estimate **risk-of-ruin** explicitly given edge, variance, and bet size.226227### 3.11 Leverage & path dependence *(High–Critical)*228- **What:** Leverage applied to the *return series* without modeling margin, intraday path,229 or forced liquidation.230- **Detect:** Returns simply multiplied by a leverage factor; no margin call / liquidation231 logic; ignoring volatility drag of leveraged compounding.232- **Why it inflates:** A 3× return series ignores that an intraday spike can liquidate you233 before the favorable close; leveraged compounding suffers volatility decay.234- **Fix:** Model **margin requirements, intraday path, and liquidation**; account for235 **volatility drag**; cap leverage; backtest on the **actual path**, not just close-to-close.236237### 3.12 Funding & borrow costs ignored *(High for shorts/crypto/leverage)*238- **What:** Short rebate/borrow fees, hard-to-borrow constraints, perpetual-swap **funding239 rates**, and carry are omitted.240- **Detect:** Short strategy with no borrow cost; crypto perp strategy with no funding;241 long-short with no financing.242- **Why it inflates:** Borrow/funding can exceed the gross edge, especially on shorts and243 crypto perps where funding flips sign and is paid continuously.244- **Fix:** Apply **borrow fees** (and hard-to-borrow availability) on shorts and **funding245 rates** on perps; include financing on leveraged/long-short books.246247### 3.13 Timezone / session-boundary bugs *(Medium–High)*248- **What:** Bars aligned to the wrong timezone, daylight-saving shifts, or session249 open/close handled incorrectly; mixing exchange time and UTC.250- **Detect:** Naive timestamps; DST not handled; daily bars rolled at the wrong hour;251 overnight gaps treated as intraday moves; 24/7 crypto vs. session-based equities mismatch.252- **Why it inflates:** Misaligned bars can accidentally introduce look-ahead or attribute253 the overnight gap to an intraday signal.254- **Fix:** Use **timezone-aware** timestamps, define sessions explicitly per instrument,255 handle DST, and verify bar boundaries against the exchange calendar.256257### 3.14 Repainting indicators (Pine / charting) *(Critical on the affected signal)*258- **What:** An indicator changes its historical values after the fact — `security()`259 pulling higher-timeframe data without offset/lookahead guards, signals confirmed only on260 the **closing** bar but evaluated intrabar in the backtest, `request.security(...,261 lookahead=barmerge.lookahead_on)`.262- **Detect:** Pine `request.security` with `lookahead_on`; signals that reference the263 current (unclosed) bar; alerts that fire then disappear; backtest results that change on264 reload.265- **Why it inflates:** Repainting bakes future information into historical signals — the266 on-chart backtest is fiction.267- **Fix:** Use `lookahead=barmerge.lookahead_off` and reference **confirmed/closed** values268 (`[1]` offset for HTF data); only act on **bar-close confirmed** signals269 (`barstate.isconfirmed`); compare strategy-tester results to bar-close-only alerts.270271### 3.15 Curve-fit stop-loss / take-profit levels *(Medium–High)*272- **What:** Stop and target levels (or trailing parameters) tuned to maximize the same273 backtest.274- **Detect:** Oddly specific levels (e.g. "stop at 2.37×ATR, target 4.81×ATR"); stops/targets275 swept in the same optimization as entries.276- **Why it inflates:** Exits become additional fitted parameters that snugly avoid the277 specific historical losers.278- **Fix:** Derive exits from **risk logic** (volatility/ATR-based, fixed-R multiples) chosen279 *a priori*; validate stop/target robustness **out-of-sample**; prefer few, round,280 rule-based levels over swept ones.281282### 3.16 Too few trades / weak statistical significance *(Medium–High)*283- **What:** The track record rests on too few trades to distinguish skill from luck.284- **Detect:** < ~30 trades (often < 100 for a real claim); a handful of trades dominate P&L;285 concentrated in one period.286- **Why it inflates:** Small samples have huge sampling error; one or two lucky trades can287 manufacture a great Sharpe.288- **Fix:** Require a **meaningful trade count**, test across periods, and report **confidence289 intervals** on the metrics; treat the Sharpe as an estimate with error bars, not a point.290291---292293## Step 4: Red-Flags Quick Scan294295Instant danger signs. Any hit warrants a finding and a closer look at the related checklist296item.297298- **Sharpe > 3 on daily data** (or > ~2 for a simple retail strategy) — almost always299 look-ahead, cost omission, or overfitting.300- **Equity curve is "too smooth"** / near-straight diagonal with tiny drawdowns — classic301 look-ahead or in-sample fitting signature.302- **No transaction costs / slippage** anywhere in the code.303- **Indicator or signal references the future** — `close[future]`, `shift(-n)`,304 `lookahead_on`, full-series `fit()` before split.305- **Parameters tuned on the whole series** with no holdout.306- **< 30 trades**, or P&L dominated by 1–2 trades.307- **Win rate ~100%** or suspiciously high with no losing streak.308- **Many configs tried, only the best reported** (undisclosed trial count).309- **Universe is "today's" index / current tickers** over a long history (survivorship).310- **Leverage applied by multiplying returns**, no margin/liquidation logic.311- **Shorts/perps with no borrow/funding cost.**312- **Backtest results change on reload** (repainting).313- **Reported max drawdown is implausibly small** relative to the strategy's volatility.314315---316317## Step 5: Output Format318319Produce the review in this structure.320321### 5.1 Findings summary table322323| ID | Issue | Severity | Where |324|----|-------|----------|-------|325| F1 | Look-ahead: signal uses same-bar close | Critical | `strategy.py:42` |326| F2 | No transaction costs modeled | Critical | `backtest.py` (no cost term) |327| F3 | Params tuned on full series, no OOS | High | optimizer block |328| … | … | … | … |329330### 5.2 Per-finding detail331332For each finding:333334- **ID & title** (e.g. *F1 — Look-ahead bias in entry signal*).335- **Severity** and one-line rationale.336- **Impact:** what it does to the reported numbers (e.g. "inflates Sharpe; equity curve is337 not achievable").338- **How to verify:** the exact check the user can run to confirm (e.g. "lag the signal by one339 bar and re-run — if returns collapse, this was the source").340- **Fix:** concrete remedy, with a short **before/after** code snippet where useful.341342### 5.3 Metrics sanity check343344Recompute or critically assess the headline metrics; define each so the user can audit them:345346- **Sharpe ratio** = (annualized mean excess return) ÷ (annualized return volatility).347 Daily-to-annual scaling uses √252 (√365 or √(24·365) for crypto). Flag any Sharpe that is348 implausible for the asset/timeframe.349- **Sortino ratio** = like Sharpe but the denominator is **downside** deviation only350 (volatility of negative returns), rewarding strategies that are only "volatile" to the351 upside.352- **Max drawdown** = the largest peak-to-trough decline in the equity curve; check it is353 measured on the **net** (post-cost) curve and is plausible vs. the strategy's volatility.354- **Deflated Sharpe Ratio (DSR)** = the probability the true Sharpe > 0 after correcting the355 observed Sharpe for the **number of trials**, the **dispersion** of trial Sharpes, and356 **skew/kurtosis** of returns. Ask for the trial count so DSR can be assessed.357- **Probability of Backtest Overfitting (PBO)** = via CSCV, the fraction of splits where the358 in-sample-best configuration ranks below median out-of-sample; **> 0.5 ⇒ overfit.**359- **Trade count & exposure:** number of trades (significance), average holding period,360 **time in market** (exposure) — low exposure with a high Sharpe can still mean tiny,361 fragile edges.362- **Turnover & break-even cost:** confirm net-of-cost results and that real costs are below363 the break-even cost.364365Where you cannot recompute (no return series provided), say so explicitly and state what the366user must supply.367368### 5.4 Verdict369370A short, honest bottom line: *which* findings (if any) are **Critical** and therefore block371deployment, what must be re-run (e.g. "add costs + lag signals + walk-forward, then372re-evaluate"), and whether the remaining edge is plausible.373374### 5.5 Disclaimer (always include)375376> **Educational review — not financial advice.** This is a methodological critique of a377> strategy/backtest, not investment advice and not a recommendation to trade. Even after378> every fix, **past backtest performance does not guarantee live results.** Markets change,379> costs and liquidity vary, and all trading carries risk of loss. You are solely responsible380> for your own trading and risk decisions.381382---383384## Tools that operationalize these fixes385386These Viprasol open-source tools implement parts of this methodology and pair naturally with387this review:388389- **[`bar-by-bar`](https://github.com/Viprasol-Tech/bar-by-bar)** — an agentic backtester390 with a built-in **look-ahead guard** that structurally prevents the Step 3.1 / 3.9 class of391 bugs (point-in-time iteration, no future leakage).392- **[`edgehunt`](https://github.com/Viprasol-Tech/edgehunt)** — a prediction-market393 edge/arbitrage engine for finding and pricing real edges, useful when validating whether a394 measured edge survives realistic costs (Step 3.6 / 3.7).395396Use this skill to *find* the failure modes; use those tools to *enforce* the fixes.