feat(trading-strategies): assessAtrMultiples — sweep to choose the ATR multiple - #1132
feat(trading-strategies): assessAtrMultiples — sweep to choose the ATR multiple#1132bennycode wants to merge 1 commit into
Conversation
236bd85 to
970954a
Compare
Sweeps a range of ATR multiples over an instrument's candles and reports how each would have behaved (held / protective / whipsawed) plus the static whippy/balanced/ loose band, the resulting trail %, exit, final value, and edge vs buy & hold — so the trail width is chosen from evidence instead of guessed. On the STX window the knee sits between 2.5x (whipsawed) and 3x (held), and the empirical outcome corrects the static band (2-2.5x classify as 'balanced' but whipsaw here).
970954a to
911b346
Compare
There was a problem hiding this comment.
Pull request overview
Adds an evidence-based helper to choose an ATR trailing-stop multiple by backtesting a sweep of multiples against a window of candles, reporting both the static “band” (whippy/balanced/loose) and the empirical outcome (held/protective/whipsawed) plus edge vs buy & hold.
Changes:
- Introduces
assessAtrMultiples()to sweep ATR multiples, warmAtrTrailStrategyfrom history, backtest across candles, and report outcome/edge metrics. - Adds Vitest coverage exercising sweep shape, a real STX regression outcome (2× whipsawed vs 3× held), value comparison, and empty-input guard.
- Exports the new API and types from the package entrypoint.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/trading-strategies/src/strategy-atr-trail/assessAtrMultiples.ts | New sweep/backtest utility producing per-multiple outcome + performance metrics. |
| packages/trading-strategies/src/strategy-atr-trail/assessAtrMultiples.test.ts | Unit tests validating the sweep report and STX regression outcomes. |
| packages/trading-strategies/src/index.ts | Publicly exports assessAtrMultiples and its related types. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let outcome: AtrMultipleOutcome = 'held'; | ||
| if (exitPrice !== null) { | ||
| outcome = lastClose > exitPrice ? 'whipsawed' : 'protective'; | ||
| } |
| const lastClose = parseFloat(candles[candles.length - 1].close); | ||
| const buyHoldValue = parseFloat(baseBalance) * lastClose; | ||
|
|
There was a problem hiding this comment.
🟡 Changes recommended
The new assessment logic has a couple of correctness/robustness gaps (notably outcome classification and missing input/test guards) that should be addressed before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
packages/trading-strategies/src/strategy-atr-trail/assessAtrMultiples.ts:96
outcomeis derived fromlastClose > exitPrice, but the docstring defines whipsawed as "price recovered above the exit" (at any point after the exit), not necessarily by the end of the window. This can misclassify a stop-out that recovers above the exit and then sells off again asprotective. Consider scanning candles after the exit for a recovery above the exit price (e.g., max high/close after exit).
let outcome: AtrMultipleOutcome = 'held';
if (exitPrice !== null) {
outcome = lastClose > exitPrice ? 'whipsawed' : 'protective';
}
packages/trading-strategies/src/strategy-atr-trail/assessAtrMultiples.ts:72
assessAtrMultiplesderivespairfromcandles[0]butBrokerMock.getRecentCandles()ignores the pair and will size ATR from whatever is inhistory. If a caller accidentally passes mixed pairs (or mismatchedhistoryvscandles), the assessment silently becomes nonsensical. Also,buyHoldValuecan become 0/NaN (e.g.baseBalance: '0'or non-numeric strings), which will produceInfinity/NaNedges. Add basic validation for a single consistent pair and a positive numericbaseBalance.
const pair = new TradingPair(candles[0].base, candles[0].counter);
const lastClose = parseFloat(candles[candles.length - 1].close);
const buyHoldValue = parseFloat(baseBalance) * lastClose;
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Lite
| const entryIndex = uptrendStxCandles.findIndex(candle => candle.openTimeInISO.startsWith('2026-05-12')); | ||
| const history = uptrendStxCandles.slice(0, entryIndex); | ||
| const candles = uptrendStxCandles.slice(entryIndex); |
Stacked on #1131 (it imports
AtrTrailStrategy). Retargets tomainonce #1131 merges.Problem
We had helpers to classify a multiple (
classifyAtrMultiple→ whippy/balanced/loose) and convert units (atrMultipleToPercent), but nothing to choose the multiple from an instrument's actual behavior.What this adds
assessAtrMultiples({history, candles, multiples?})sweeps a range of ATR multiples and, for each, warmsAtrTrailStrategyfromhistory, trails it overcandles, and reports:band— static whippy/balanced/looseoutcome— empirical:held/protective(exited then kept falling) /whipsawed(exited then recovered)trailDownPct,exitPrice/exitDate,finalValue,buyHoldValue,edgeVsHoldPctSo you pick the width from evidence instead of a guess.
On the real STX window (5 shares, buy & hold ≈ 4628)
The knee is between 2.5× and 3×. Note the empirical outcome corrects the static band: 2× and 2.5× classify as "balanced" but actually whipsawed on this instrument — which is the whole point of measuring instead of trusting a generic threshold.
Scope (MVP)
Outcome + edge vs buy & hold. Deliberately does not pull the richer suitability metrics (Kaufman efficiency ratio, max drawdown, full verdict) from the old
assessTrailSuitability— easy to add later if wanted.4 tests covering the sweep shape, the 2×-whipsaw / 3×-held outcomes, the edge comparison, and the empty-input guard.