diff --git a/AuraTrader/StrategyTest.pas b/AuraTrader/StrategyTest.pas index 2cf8e6f..0423d81 100644 --- a/AuraTrader/StrategyTest.pas +++ b/AuraTrader/StrategyTest.pas @@ -20,12 +20,13 @@ uses function CreateStrategy1(Timeframe: TTimeframe): TConverter, Double>; type - TSignal = record - Sig: Double; - SL: Double; - Entry: Double; - pnl: Double; + // A record to transfer a detected signal event and the required price data to the next stage. + TSignalEvent = record + Signal: Integer; // -1 for short, 1 for long, 0 for no new signal + Close, Low, High, ATR: Double; + InitialSL: Double; // The calculated SL (Highest/Lowest) at the time of the signal end; + begin var ticker := TConverter.CreateIdentity>; @@ -37,133 +38,134 @@ begin var Hull := Closes.Chain(TIndicators.CreateHMA(250)).MakeParallel; var Sma := Closes.Chain(TIndicators.CreateSMA(200)).MakeParallel; - - var Lowest: Double := Double.MaxValue; - var Highest: Double := Double.MinValue; - var ATR := Ohlc.Chain(TIndicators.CreateATR(50)).MakeParallel; - // next stage - - var ATREndPoint := TConverter.CreateEndpoint(ATR.Sender, 5); - var ATRSeries: TSeries; - - var HullEndPoint := TConverter.CreateEndpoint(Hull.Sender, 5); - var HullSeries: TSeries; - - var SmaEndPoint := TConverter.CreateEndpoint(Sma.Sender, 5); - var SmaSeries: TSeries; - - var curr: TSignal; - curr.SL := Double.NaN; - curr.Entry := Double.NaN; - - var lastHull, lastSma: Double; - var conv := TConverter.Join( [Ohlc.Field('Low').Sender, Ohlc.Field('High').Sender, Closes.Sender, ATR.Sender, Hull.Sender, Sma.Sender] ); - var Signal := - TConverter, TSignal>.CreateGeneric( - function(const Values: TArray): TSignal + // STAGE 1: Signal Generation. This converter is stateless regarding the trade itself. + // It only detects the crossover event and prepares the data for the next stage. + var Lowest: Double := Double.MaxValue; + var Highest: Double := Double.MinValue; + var lastHull, lastSma: Double; + + var signalGenerator := + TConverter, TSignalEvent>.CreateGeneric( + function(const Values: TArray): TSignalEvent begin - var low := Values[0]; - var high := Values[1]; - var close := Values[2]; - var atr := Values[3]; + Result.Low := Values[0]; + Result.High := Values[1]; + Result.Close := Values[2]; + Result.ATR := Values[3]; var hull := Values[4]; var sma := Values[5]; - if low < Lowest then - Lowest := low; - if high > Highest then - Highest := high; + if Result.Low < Lowest then + Lowest := Result.Low; + if Result.High > Highest then + Highest := Result.High; - Result := curr; - Result.Sig := 0; - var pnl: double := NaN; + Result.Signal := 0; + Result.InitialSL := Double.NaN; if (hull < sma) and (lastHull >= lastSma) then begin - if curr.Sig > 0 then - pnl := close - curr.Entry; - - curr.Sig := -1; - curr.SL := Highest; - curr.Entry := close; - Result := curr; + Result.Signal := -1; + Result.InitialSL := Highest; + Highest := Double.MinValue; // Reset for next trend + Lowest := Double.MaxValue; end else if (hull > sma) and (lastHull <= lastSma) then begin - if curr.Sig < 0 then - pnl := curr.Entry - close; - - curr.Sig := 1; - curr.SL := Lowest; - curr.Entry := close; - Result := curr; - end; - - atr := 15 * atr; - if curr.Sig > 0 then - begin - if close > curr.SL then - begin - if curr.SL < close - atr then - curr.SL := close - atr; - Result.SL := curr.SL; - end; - - if low <= curr.SL then - begin - pnl := curr.SL - curr.Entry; - curr.Sig := 0; - Result.Sig := 0; - curr.SL := NaN; - end; - end - else if curr.Sig < 0 then - begin - if close < curr.SL then - begin - if curr.SL > close + atr then - curr.SL := close + atr; - Result.SL := curr.SL; - end; - - if high >= curr.SL then - begin - pnl := curr.Entry - curr.SL; - curr.Sig := 0; - Result.Sig := 0; - curr.SL := NaN; - end; - end; - - if Result.Sig <> 0 then - begin + Result.Signal := 1; + Result.InitialSL := Lowest; + Highest := Double.MinValue; // Reset for next trend Lowest := Double.MaxValue; - Highest := Double.MinValue; - Result.SL := Double.NaN; - Result.Entry := Double.NaN; end; - Result.pnl := pnl; - lastHull := hull; lastSma := sma; end ); - conv.Link(Signal); + // STAGE 2: Position Management. This stateful converter manages the lifecycle + // of a single trade (entry, trailing stop, exit) and outputs the PnL. + // State variables for the position manager + var currSig: Integer := 0; + var currSL := Double.NaN; + var currEntry := Double.NaN; - var pnl := Signal.Field('pnl'); + var positionManager := + TConverter.CreateAggregation( + function(const Value: TSignalEvent; const Broadcast: TConverter.TBroadcastProc): TState + var + pnl: Double; + begin + Result := TState.Null; + pnl := Double.NaN; + // 1. Check for a new signal to open or reverse a position + if Value.Signal <> 0 then + begin + // If a position is already open, close it first + if currSig > 0 then + pnl := Value.Close - currEntry + else if currSig < 0 then + pnl := currEntry - Value.Close; + + // Open new position + currSig := Value.Signal; + currEntry := Value.Close; + currSL := Value.InitialSL; + end + // 2. If no new signal, manage the currently open position + else + begin + var atrValue := 15 * Value.ATR; + if currSig > 0 then // Manage long position + begin + if Value.Close > currSL then + if currSL < Value.Close - atrValue then + currSL := Value.Close - atrValue; + + if Value.Low <= currSL then + begin + pnl := currSL - currEntry; + currSig := 0; // Close position + end; + end + else if currSig < 0 then // Manage short position + begin + if Value.Close < currSL then + if currSL > Value.Close + atrValue then + currSL := Value.Close + atrValue; + + if Value.High >= currSL then + begin + pnl := currEntry - currSL; + currSig := 0; // Close position + end; + end; + end; + + // 3. If a PnL was generated (trade closed), broadcast it + if not IsNan(pnl) then + begin + currSL := Double.NaN; + Broadcast(pnl); + end; + end + ); + + // Chain the stages together + conv.Link(signalGenerator); + signalGenerator.Sender.Link(positionManager); + + // The final equity calculation remains the same, it just consumes the PnL from the position manager var FEquity: Double := 10000; var FInit: Boolean := false; - var equity := TConverter.CreateAggregation( function(const Value: Double; const Broadcast: TConverter.TBroadcastProc): TState @@ -182,7 +184,7 @@ begin end ); - pnl.Sender.Link(equity); + positionManager.Sender.Link(equity); Result := TConverter, Double>.Construct(ticker, equity.Sender); end;