feat(trading-strategies): add AtrTrailStrategy (ATR-sized trailing stop)#1131
Open
bennycode wants to merge 6 commits into
Open
feat(trading-strategies): add AtrTrailStrategy (ATR-sized trailing stop)#1131bennycode wants to merge 6 commits into
bennycode wants to merge 6 commits into
Conversation
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.
bennycode
commented
Jun 19, 2026
bennycode
commented
Jun 19, 2026
- 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.
Contributor
There was a problem hiding this comment.
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
AtrTrailStrategywithinit()-time ATR%-based sizing and trailing-stop exit behavior (plus optional rolling ATR re-sizing). - Register and export the new strategy/schema/types via
StrategyRegistryand packageindex.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); | ||
| /* |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
init(market, pair)it pulls recent history viagetRecentCandles, measures ATR% (AtrPercent), and freezes the trail width atatrMultiple × ATR%.closedrops topeak × (1 − trailPct/100).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
Notes
AtrPercent).DynamicTrailStrategy: it won't adapt to a mid-trade volatility regime shift — acceptable for "size sensibly from recent history, then just trail."7 unit tests cover sizing, attach, ratchet, breach/exit, the no-history and no-init cases.