Skip to content

feat(trading-strategies): assessAtrMultiples — sweep to choose the ATR multiple - #1132

Open
bennycode wants to merge 1 commit into
feat/atr-trail-strategyfrom
feat/atr-multiple-sweep
Open

feat(trading-strategies): assessAtrMultiples — sweep to choose the ATR multiple#1132
bennycode wants to merge 1 commit into
feat/atr-trail-strategyfrom
feat/atr-multiple-sweep

Conversation

@bennycode

Copy link
Copy Markdown
Owner

Stacked on #1131 (it imports AtrTrailStrategy). Retargets to main once #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, warms AtrTrailStrategy from history, trails it over candles, and reports:

  • band — static whippy/balanced/loose
  • outcomeempirical: held / protective (exited then kept falling) / whipsawed (exited then recovered)
  • trailDownPct, exitPrice/exitDate, finalValue, buyHoldValue, edgeVsHoldPct

So you pick the width from evidence instead of a guess.

On the real STX window (5 shares, buy & hold ≈ 4628)

1.5x  whippy    trail 6.9%   whipsawed  exit@780 05-21  edge -15.8%
2x    balanced  trail 9.2%   whipsawed  exit@761 05-20  edge -17.9%
2.5x  balanced  trail 11.4%  whipsawed  exit@742 05-19  edge -20.0%
3x    balanced  trail 13.7%  held                       edge  0.0%
3.5x  loose     trail 16.0%  held                       edge  0.0%
4x    loose     trail 18.3%  held                       edge  0.0%

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.

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).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, warm AtrTrailStrategy from 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.

Comment on lines +93 to +96
let outcome: AtrMultipleOutcome = 'held';
if (exitPrice !== null) {
outcome = lastClose > exitPrice ? 'whipsawed' : 'protective';
}
Comment on lines +70 to +72
const lastClose = parseFloat(candles[candles.length - 1].close);
const buyHoldValue = parseFloat(baseBalance) * lastClose;

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

  • outcome is derived from lastClose > 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 as protective. 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

  • assessAtrMultiples derives pair from candles[0] but BrokerMock.getRecentCandles() ignores the pair and will size ATR from whatever is in history. If a caller accidentally passes mixed pairs (or mismatched history vs candles), the assessment silently becomes nonsensical. Also, buyHoldValue can become 0/NaN (e.g. baseBalance: '0' or non-numeric strings), which will produce Infinity/NaN edges. Add basic validation for a single consistent pair and a positive numeric baseBalance.
  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

Comment on lines +5 to +7
const entryIndex = uptrendStxCandles.findIndex(candle => candle.openTimeInISO.startsWith('2026-05-12'));
const history = uptrendStxCandles.slice(0, entryIndex);
const candles = uptrendStxCandles.slice(entryIndex);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants