unit StrategyTest; interface uses Myc.Signals, Myc.Data.Pipeline, Myc.Trade.Types, Myc.Trade.Pipeline, Myc.Trade.Indicators.Common; function CreateStrategy1(Timeframe: TTimeframe): TConverter, Double>; overload; implementation uses System.SysUtils, System.Math; function CreateStrategy1(Timeframe: TTimeframe): TConverter, Double>; type // 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>; var OhlcPoint := ticker.Producer.Chain>(TTradeConverter.CreateOhlcAggregation(Timeframe)); var Ohlc := OhlcPoint.Field('Data'); var Closes := Ohlc.Field('Close'); var Hull := Closes.Chain(THMA.CreateHMA(250)).MakeParallel; var Sma := Closes.Chain(TSMA.CreateSMA(200)).MakeParallel; var ATR := Ohlc.Chain(TATR.CreateATR(50)); var conv := TConverter.Join(jmAll, [Ohlc.Field('Low'), Ohlc.Field('High'), Closes, ATR, Hull, Sma]); // 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 := conv.Chain( TConverter, TSignalEvent>.CreateConverter( function(const Values: TArray): TSignalEvent begin 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 Result.Low < Lowest then Lowest := Result.Low; if Result.High > Highest then Highest := Result.High; Result.Signal := 0; Result.InitialSL := Double.NaN; if (hull < sma) and (lastHull >= lastSma) then begin 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 Result.Signal := 1; Result.InitialSL := Lowest; Highest := Double.MinValue; // Reset for next trend Lowest := Double.MaxValue; end; lastHull := hull; lastSma := sma; end ) ); // 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 positionManager := signalGenerator.Chain( TConverter.CreateAggregation( function(const Value: TSignalEvent; const Broadcast: TBroadcastFunc): 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 ) ); // 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 := positionManager.Chain( TConverter.CreateAggregation( function(const Value: Double; const Broadcast: TBroadcastFunc): TState begin if not FInit then begin FInit := true; Broadcast(FEquity); end; if not IsNan(Value) then begin FEquity := FEquity + Value; Result := Broadcast(FEquity); end; end ) ); Result := TConverter, Double>.Construct(ticker.Consumer, equity); end; end.