@@ -11,6 +11,30 @@ export type Candle = {
1111
1212export type ExitReason = "initial_stop" | "trailing_stop" | "trend_exit" ;
1313
14+ export type StrategyParameters = {
15+ emaFast : number ;
16+ emaSlow : number ;
17+ donchianLookback : number ;
18+ atrLength : number ;
19+ atrFloor : number ;
20+ initialStopAtr : number ;
21+ trailingStopAtr : number ;
22+ commission : number ;
23+ slippage : number ;
24+ } ;
25+
26+ export const REGIME_TREND_V1_DEFAULTS : Readonly < StrategyParameters > = Object . freeze ( {
27+ emaFast : 50 ,
28+ emaSlow : 200 ,
29+ donchianLookback : 20 ,
30+ atrLength : 14 ,
31+ atrFloor : 0.005 ,
32+ initialStopAtr : 2.5 ,
33+ trailingStopAtr : 3 ,
34+ commission : 0.001 ,
35+ slippage : 0.0005
36+ } ) ;
37+
1438export type SignalRecord = {
1539 signalIndex : number ;
1640 signalTimestamp : Timestamp ;
@@ -28,7 +52,7 @@ export type StopUpdate = {
2852
2953export type TradeLedger = {
3054 strategy_id : "regime-trend-v1" ;
31- implementation_version : "typescript-reference-v1.0.1" ;
55+ implementation_version : string ;
3256 dataset_hash : string ;
3357 symbol : string ;
3458 timeframe : "4h" ;
@@ -75,16 +99,20 @@ export type BacktestResult = {
7599 openPosition : OpenPosition | null ;
76100} ;
77101
78- const EMA_FAST = 50 ;
79- const EMA_SLOW = 200 ;
80- const DONCHIAN = 20 ;
81- const ATR_LENGTH = 14 ;
82- const ATR_FLOOR = 0.005 ;
83- const INITIAL_STOP_ATR = 2.5 ;
84- const TRAILING_STOP_ATR = 3 ;
85- const COMMISSION = 0.001 ;
86- const SLIPPAGE = 0.0005 ;
87- const WARMUP_INDEX = Math . max ( EMA_SLOW , DONCHIAN , ATR_LENGTH ) - 1 ;
102+ function validateParameters ( parameters : StrategyParameters ) : void {
103+ const positiveIntegers = [ parameters . emaFast , parameters . emaSlow , parameters . donchianLookback , parameters . atrLength ] ;
104+ if ( positiveIntegers . some ( ( value ) => ! Number . isInteger ( value ) || value <= 0 ) ) {
105+ throw new Error ( "Indicator lengths must be positive integers" ) ;
106+ }
107+ if ( parameters . emaFast >= parameters . emaSlow ) throw new Error ( "emaFast must be less than emaSlow" ) ;
108+ const positive = [ parameters . atrFloor , parameters . initialStopAtr , parameters . trailingStopAtr ] ;
109+ if ( positive . some ( ( value ) => ! Number . isFinite ( value ) || value <= 0 ) ) {
110+ throw new Error ( "ATR parameters must be positive" ) ;
111+ }
112+ if ( parameters . commission < 0 || parameters . slippage < 0 ) {
113+ throw new Error ( "Costs cannot be negative" ) ;
114+ }
115+ }
88116
89117function assertCandle ( candle : Candle , index : number ) : void {
90118 const values = [ candle . open , candle . high , candle . low , candle . close , candle . volume ] ;
@@ -121,7 +149,7 @@ export function ema(values: number[], length: number): Array<number | undefined>
121149 return output ;
122150}
123151
124- export function wilderAtr ( candles : Candle [ ] , length = ATR_LENGTH ) : Array < number | undefined > {
152+ export function wilderAtr ( candles : Candle [ ] , length = 14 ) : Array < number | undefined > {
125153 if ( ! Number . isInteger ( length ) || length <= 0 ) throw new Error ( "ATR length must be positive" ) ;
126154 const output : Array < number | undefined > = new Array ( candles . length ) ;
127155 if ( candles . length === 0 ) return output ;
@@ -147,10 +175,10 @@ export function wilderAtr(candles: Candle[], length = ATR_LENGTH): Array<number
147175 return output ;
148176}
149177
150- function highestPreviousHigh ( candles : Candle [ ] , index : number ) : number | undefined {
151- if ( index < DONCHIAN ) return undefined ;
178+ function highestPreviousHigh ( candles : Candle [ ] , index : number , lookback : number ) : number | undefined {
179+ if ( index < lookback ) return undefined ;
152180 let highest = Number . NEGATIVE_INFINITY ;
153- for ( let cursor = index - DONCHIAN ; cursor < index ; cursor += 1 ) {
181+ for ( let cursor = index - lookback ; cursor < index ; cursor += 1 ) {
154182 highest = Math . max ( highest , candles [ cursor ] . high ) ;
155183 }
156184 return highest ;
@@ -169,16 +197,29 @@ function closeTrade(args: {
169197 exitReason : ExitReason ;
170198 datasetHash : string ;
171199 symbol : string ;
200+ commission : number ;
201+ implementationVersion : string ;
172202} ) : TradeLedger {
173- const { position, exitIndex, exitTimestamp, rawExitReference, exitFill, exitReason, datasetHash, symbol } = args ;
174- const exitFee = exitFill * position . quantity * COMMISSION ;
203+ const {
204+ position,
205+ exitIndex,
206+ exitTimestamp,
207+ rawExitReference,
208+ exitFill,
209+ exitReason,
210+ datasetHash,
211+ symbol,
212+ commission,
213+ implementationVersion
214+ } = args ;
215+ const exitFee = exitFill * position . quantity * commission ;
175216 const grossPnl = ( exitFill - position . entryFill ) * position . quantity ;
176217 const netPnl = grossPnl - position . entryFee - exitFee ;
177218 const entryNotional = position . entryFill * position . quantity ;
178219
179220 return {
180221 strategy_id : "regime-trend-v1" ,
181- implementation_version : "typescript-reference-v1.0.1" ,
222+ implementation_version : implementationVersion ,
182223 dataset_hash : datasetHash ,
183224 symbol,
184225 timeframe : "4h" ,
@@ -205,16 +246,33 @@ function closeTrade(args: {
205246
206247export function runRegimeTrendV1 (
207248 candles : Candle [ ] ,
208- options : { datasetHash ?: string ; symbol ?: string } = { }
249+ options : {
250+ datasetHash ?: string ;
251+ symbol ?: string ;
252+ parameters ?: Partial < StrategyParameters > ;
253+ implementationVersion ?: string ;
254+ } = { }
209255) : BacktestResult {
210256 validateCandles ( candles ) ;
211257
258+ const parameters : StrategyParameters = {
259+ ...REGIME_TREND_V1_DEFAULTS ,
260+ ...( options . parameters ?? { } )
261+ } ;
262+ validateParameters ( parameters ) ;
263+
212264 const datasetHash = options . datasetHash ?? "synthetic" ;
213265 const symbol = options . symbol ?? "TESTUSDT" ;
266+ const implementationVersion = options . implementationVersion ?? "typescript-reference-v1.1.0" ;
267+ const warmupIndex = Math . max (
268+ parameters . emaSlow ,
269+ parameters . donchianLookback ,
270+ parameters . atrLength
271+ ) - 1 ;
214272 const closes = candles . map ( ( candle ) => candle . close ) ;
215- const ema50 = ema ( closes , EMA_FAST ) ;
216- const ema200 = ema ( closes , EMA_SLOW ) ;
217- const atr14 = wilderAtr ( candles , ATR_LENGTH ) ;
273+ const fastEma = ema ( closes , parameters . emaFast ) ;
274+ const slowEma = ema ( closes , parameters . emaSlow ) ;
275+ const atr = wilderAtr ( candles , parameters . atrLength ) ;
218276
219277 const trades : TradeLedger [ ] = [ ] ;
220278 const signals : SignalRecord [ ] = [ ] ;
@@ -239,7 +297,9 @@ export function runRegimeTrendV1(
239297 exitFill,
240298 exitReason,
241299 datasetHash,
242- symbol
300+ symbol,
301+ commission : parameters . commission ,
302+ implementationVersion
243303 } )
244304 ) ;
245305 position = null ;
@@ -251,10 +311,10 @@ export function runRegimeTrendV1(
251311 let exitedThisBar = false ;
252312
253313 if ( pendingEntry ) {
254- const entryFill = candle . open * ( 1 + SLIPPAGE ) ;
314+ const entryFill = candle . open * ( 1 + parameters . slippage ) ;
255315 const quantity = 1 / entryFill ;
256- const entryFee = entryFill * quantity * COMMISSION ;
257- const initialStop = entryFill - INITIAL_STOP_ATR * pendingEntry . atr ;
316+ const entryFee = entryFill * quantity * parameters . commission ;
317+ const initialStop = entryFill - parameters . initialStopAtr * pendingEntry . atr ;
258318 position = {
259319 signalIndex : pendingEntry . signalIndex ,
260320 signalTimestamp : pendingEntry . signalTimestamp ,
@@ -275,56 +335,56 @@ export function runRegimeTrendV1(
275335
276336 if ( position && pendingTrendExit ) {
277337 if ( candle . open <= position . activeStop ) {
278- recordExit ( index , candle . open , candle . open * ( 1 - SLIPPAGE ) , stopReason ( position ) ) ;
338+ recordExit ( index , candle . open , candle . open * ( 1 - parameters . slippage ) , stopReason ( position ) ) ;
279339 } else {
280- recordExit ( index , candle . open , candle . open * ( 1 - SLIPPAGE ) , "trend_exit" ) ;
340+ recordExit ( index , candle . open , candle . open * ( 1 - parameters . slippage ) , "trend_exit" ) ;
281341 }
282342 exitedThisBar = true ;
283343 }
284344
285345 if ( position && ! exitedThisBar ) {
286346 const stop = position . activeStop ;
287347 if ( candle . open <= stop ) {
288- recordExit ( index , candle . open , candle . open * ( 1 - SLIPPAGE ) , stopReason ( position ) ) ;
348+ recordExit ( index , candle . open , candle . open * ( 1 - parameters . slippage ) , stopReason ( position ) ) ;
289349 exitedThisBar = true ;
290350 } else if ( candle . low <= stop ) {
291- recordExit ( index , stop , stop * ( 1 - SLIPPAGE ) , stopReason ( position ) ) ;
351+ recordExit ( index , stop , stop * ( 1 - parameters . slippage ) , stopReason ( position ) ) ;
292352 exitedThisBar = true ;
293353 }
294354 }
295355
296356 if ( position && ! exitedThisBar ) {
297357 position . highestCloseSinceEntry = Math . max ( position . highestCloseSinceEntry , candle . close ) ;
298- const currentAtr = atr14 [ index ] ;
358+ const currentAtr = atr [ index ] ;
299359 if ( currentAtr !== undefined ) {
300360 const previousStop = position . activeStop ;
301- const candidateStop = position . highestCloseSinceEntry - TRAILING_STOP_ATR * currentAtr ;
361+ const candidateStop = position . highestCloseSinceEntry - parameters . trailingStopAtr * currentAtr ;
302362 const activeStop = Math . max ( previousStop , candidateStop ) ;
303363 if ( activeStop > previousStop ) position . trailingActivated = true ;
304364 position . activeStop = activeStop ;
305365 stopUpdates . push ( { index, timestamp : candle . timestamp , previousStop, candidateStop, activeStop } ) ;
306366 }
307367
308- const fast = ema50 [ index ] ;
368+ const fast = fastEma [ index ] ;
309369 if ( fast !== undefined && candle . close < fast && index + 1 < candles . length ) {
310370 pendingTrendExit = true ;
311371 }
312372 }
313373
314- if ( ! position && ! pendingEntry && ! exitedThisBar && index >= WARMUP_INDEX && index + 1 < candles . length ) {
315- const fast = ema50 [ index ] ;
316- const slow = ema200 [ index ] ;
317- const atr = atr14 [ index ] ;
318- const breakoutLevel = highestPreviousHigh ( candles , index ) ;
319- if ( fast !== undefined && slow !== undefined && atr !== undefined && breakoutLevel !== undefined ) {
374+ if ( ! position && ! pendingEntry && ! exitedThisBar && index >= warmupIndex && index + 1 < candles . length ) {
375+ const fast = fastEma [ index ] ;
376+ const slow = slowEma [ index ] ;
377+ const currentAtr = atr [ index ] ;
378+ const breakoutLevel = highestPreviousHigh ( candles , index , parameters . donchianLookback ) ;
379+ if ( fast !== undefined && slow !== undefined && currentAtr !== undefined && breakoutLevel !== undefined ) {
320380 const bullishRegime = fast > slow && candle . close > slow ;
321381 const breakout = candle . close > breakoutLevel ;
322- const volatilityAllowed = atr / candle . close >= ATR_FLOOR ;
382+ const volatilityAllowed = currentAtr / candle . close >= parameters . atrFloor ;
323383 if ( bullishRegime && breakout && volatilityAllowed ) {
324384 pendingEntry = {
325385 signalIndex : index ,
326386 signalTimestamp : candle . timestamp ,
327- atr,
387+ atr : currentAtr ,
328388 breakoutLevel
329389 } ;
330390 signals . push ( pendingEntry ) ;
0 commit comments