Skip to content

feat(trading-strategies): add AtrTrailStrategy (ATR-sized trailing stop)#1131

Open
bennycode wants to merge 6 commits into
mainfrom
feat/atr-trail-strategy
Open

feat(trading-strategies): add AtrTrailStrategy (ATR-sized trailing stop)#1131
bennycode wants to merge 6 commits into
mainfrom
feat/atr-trail-strategy

Conversation

@bennycode

Copy link
Copy Markdown
Owner

A simpler alternative to DynamicTrailStrategy: a trailing stop whose width is sized once from the stock's own volatility, then left alone.

What it does

  • On init(market, pair) it pulls recent history via getRecentCandles, measures ATR% (AtrPercent), and freezes the trail width at atrMultiple × ATR%.
  • From then on it's an ordinary percentage trailing stop: ratchet the peak up, exit (market sell) when close drops to peak × (1 − trailPct/100).
  • No live ATR feed and no per-candle re-sizing — that's the whole point of being simpler than DynamicTrailStrategy.

Why

Sizes a volatility-aware stop distance for the specific instrument (wide for a volatile name like STX, tight for a calm one) without hand-tuning a percentage — which is exactly what avoids the too-tight fixed-% whipsaw.

Usage

new AtrTrailStrategy({atrInterval: 14, atrMultiple: '2', atrIntervalMillis: 86_400_000});

Notes

7 unit tests cover sizing, attach, ratchet, breach/exit, the no-history and no-init cases.

A simpler alternative to DynamicTrailStrategy: on init it pulls recent history,
measures ATR%, and freezes the trail width at atrMultiple x ATR%. From then on
it is a plain percentage trail that ratchets the peak and exits on a breach — no
live ATR feed, no re-sizing. Sizes a sensible volatility-aware stop for the
specific stock without hand-tuning a percent.

Reuses the merged building blocks: the init(market, pair) warm-up hook,
getRecentCandles, AtrPercent. Exit-only, market exit, daily ATR by default.
Registered and exported.
Place the exit sell as a LIMIT at the trail target instead of a market order, so
a gap straight through the stop can't fill far below it. The limit price is the
stop price, re-emitted each candle as the stop ratchets, until it fills.
Runs the strategy on the real STX candles: ATR sized from the history before the
2026-05-12 position, then trailed through the May shakeout to the ~925 recovery.
A 2x ATR trail is whipsawed out near 761; a 3x trail rides through and ends with
a higher portfolio value. Demonstrates volatility-sizing the stop end-to-end.
New `rolling` flag (default false): instead of freezing the trail width at init,
keep re-sizing it from a rolling ATR as live candles arrive. The ATR advances
only on completed atrIntervalMillis bars, so a daily ATR steps once per day —
intraday minute candles accumulate into the current bar and don't move the width
until the day closes. The stop only ratchets up, so a volatility spike widens the
trail for future peaks but never loosens a locked stop.

Two unit tests cover the intraday invariance and the re-size on day close.
Comment thread packages/trading-strategies/src/strategy-atr-trail/AtrTrailStrategy.ts Outdated
Comment thread packages/trading-strategies/src/strategy-atr-trail/AtrTrailStrategy.ts Outdated
- use ms('1d') instead of a hand-rolled ONE_DAY_IN_MS constant
- replace the verbose hand-rolled state type-guard with a small zod schema
  (safeParse on restore); add tests for valid restore and malformed fallback
Document that ATR-multiple trailing fits high-volatility single names, not calm
trending indices — on a low-volatility index the ATR-sized trail is too tight and
whipsaws out of a steady uptrend (URTH 2024/2025 backtest), whereas on a volatile
name the same multiple rides a shakeout through.

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 a new exit-only trailing-stop strategy (AtrTrailStrategy) that sizes its trail width from historical ATR% (optionally rolling) and wires it into the trading-strategies package exports/registry, with unit and regression test coverage.

Changes:

  • Implement AtrTrailStrategy with init()-time ATR%-based sizing and trailing-stop exit behavior (plus optional rolling ATR re-sizing).
  • Register and export the new strategy/schema/types via StrategyRegistry and package index.ts.
  • Add focused unit tests and an STX historical regression-style backtest test.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/trading-strategies/src/strategy/StrategyRegistry.ts Registers AtrTrailStrategy so it can be created by name with schema validation.
packages/trading-strategies/src/strategy-atr-trail/AtrTrailStrategy.ts New ATR-sized trailing stop strategy implementation (state, init sizing, candle processing, fill handling, restore).
packages/trading-strategies/src/strategy-atr-trail/AtrTrailStrategy.test.ts Unit tests for sizing, attach/ratchet, breach/exit advice, restore-state validation, and rolling ATR behavior.
packages/trading-strategies/src/strategy-atr-trail/AtrTrailStxRegression.test.ts Regression backtest demonstrating behavior differences between 2x and 3x ATR trails on STX dataset.
packages/trading-strategies/src/index.ts Exports AtrTrailStrategy/schema/types from the package entrypoint.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +120 to +123
async init(market: Pick<MarketDataSource, 'getRecentCandles'>, pair: TradingPair): Promise<void> {
// ATR(n) needs 2n-1 inputs to stabilize; pull a small buffer beyond that.
const count = this.#atrInterval * 2 + 1;
const candles = await market.getRecentCandles(pair, count, this.#atrIntervalMillis);
Comment on lines +214 to +216
const reason = `ATR trailing stop: close ${candle.close.toFixed()} <= ${stop.toFixed()} (peak ${peak.toFixed()} -${trailDownPct.toFixed(2)}%).`;
this.onMessage?.(reason);
/*
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