unit Strategy2; interface uses System.Classes, Myc.Signals, Myc.Data.Pipeline, Myc.Trade.Types, Myc.FMX.Chart, Myc.Trade.Pipeline; function CreateStrategy2( const Ticker: TProducer>; const Log: TStrings; const Chart1H, Chart24H: TMycChart ): TProducer; implementation uses System.SysUtils, System.Rtti, System.Math, System.Generics.Collections, System.Types, System.UITypes, FMX.Types, Myc.Trade.Indicators.Common, Myc.Data.Records; // Calculates the difference between the current value and the value N periods ago. // (Value[0] - Value[N]) function Delta(N: Integer): TConverter; var history: TQueue; // Captured state for the history of values begin if N <= 0 then raise EArgumentException.Create('Delta period N must be positive.'); history := TQueue.Create; Result := TConverter.CreateAggregation( // This function is the core of the aggregator. It is called for each value. function(const Value: Double; const Broadcast: TBroadcastFunc): TState var deltaValue, oldestValue: Double; begin // Add current value to the history history.Enqueue(Value); // If the history is larger than needed, remove the oldest element. // We need N+1 elements to have the current and the Nth previous value. if history.Count > (N + 1) then history.Dequeue; // Calculate delta if we have enough data if history.Count = (N + 1) then begin oldestValue := history.Peek; // Oldest value is at the front deltaValue := Value - oldestValue; Broadcast(deltaValue); end else begin // Not enough data yet, broadcast a neutral value. Broadcast(0.0); end; Result := TState.Null; end ); end; type // Defines the direction of the slope between two points TSlope = (sFalling, sFlat, sRising); // Represents the last found high and low point in a series. TExtrema = record High: Double; Low: Double; constructor Create(AHigh, ALow: Double); end; constructor TExtrema.Create(AHigh, ALow: Double); begin High := AHigh; Low := ALow; end; // Finds the last peak/trough. A peak is a value surrounded by N lower values on both sides. // This introduces a signal lag of N periods. function ExtremaFinder(N: Integer): TConverter; var // Captured state for the aggregator history: TQueue; lastHigh, lastLow: Double; initialized: Boolean; begin if N <= 0 then raise EArgumentException.Create('ExtremaFinder period N must be positive.'); initialized := False; history := TQueue.Create; lastHigh := 0; lastLow := 0; Result := TConverter.CreateAggregation( function(const Value: Double; const Broadcast: TBroadcastFunc): TState var data: TArray; centerValue: Double; isPeak, isTrough: Boolean; i: Integer; begin history.Enqueue(Value); // Keep the history buffer at the required size for the sliding window if history.Count > (2 * N + 1) then history.Dequeue; // Initialize extrema with the first value once the buffer is full if not initialized and (history.Count = (2 * N + 1)) then begin lastHigh := history.Peek; lastLow := history.Peek; initialized := True; end; // Only proceed if the window is full if history.Count = (2 * N + 1) then begin data := history.ToArray; centerValue := data[N]; // The value to be tested is in the middle of the window // Test for a peak (center value is highest in the window) isPeak := True; for i := 0 to High(data) do begin if i <> N then begin if centerValue <= data[i] then begin isPeak := False; break; end; end; end; if isPeak then lastHigh := centerValue; // Test for a trough (center value is lowest in the window) isTrough := True; if not isPeak then // Small optimization begin for i := 0 to High(data) do begin if i <> N then begin if centerValue >= data[i] then begin isTrough := False; break; end; end; end; end else isTrough := False; if isTrough then lastLow := centerValue; end; // Broadcast the latest known extrema. if initialized then Broadcast(TExtrema.Create(lastHigh, lastLow)); Result := TState.Null; end ); end; type TValueHelper = record helper for TValue class function AsValue(const P: TProducer): TProducer; static; end; // Helper to convert a producer of any type T into a producer of TValue. class function TValueHelper.AsValue(const P: TProducer): TProducer; begin Result := P.Chain(function(const V: T): TValue begin Result := TValue.From(V); end); end; function CreateStrategy2( const Ticker: TProducer>; const Log: TStrings; const Chart1H, Chart24H: TMycChart ): TProducer; const // Timeframe independent constants HMA_BIAS_PERIOD = 250; SMA_BIAS_PERIOD = 200; ATR_PERIOD = 50; ATR_FILTER_MULTIPLIER = 3.0; ATR_SL_MULTIPLIER = 4.0; // 1H specific constants HMA_ENTRY_PERIOD = 20; type // Represents the current state of the trading logic TTradeStatus = (tsFlat, tsLong, tsShort); // Holds all necessary data for the stateful trade management aggregator TTradeState = record Status: TTradeStatus; EntryPrice: Double; StopLoss: Double; TakeProfit: Double; end; var {$region 'Producers for Indicators and Logic Signals'} // Timeframe specific producers Ohlc1H, Ohlc24H: TProducer>; Close1H, Close24H: TProducer; Time1H, Time24H: TProducer; // 24H Indicators Hma250_24H, Sma200_24H, Atr50_24H: TProducer; // 1H Indicators Hma250_1H, Sma200_1H, Atr50_1H, Hma20_1H: TProducer; // Bias producers Bias24H_Bullish, Bias24H_Bearish: TProducer; Bias1H_Bullish, Bias1H_Bearish: TProducer; OverallBias_Bullish, OverallBias_Bearish: TProducer; // Filter producers Filter24H, Filter1H, TradeAllowed: TProducer; // Entry producers Hma20_Extrema: TProducer; LastLow_HMA20, LastHigh_HMA20: TProducer; TriggerBuy, TriggerShort: TProducer; GoLong, GoShort: TProducer; {$endregion} begin {$region 'Helper function implementations'} var All := function(const Prods: TArray>): TProducer begin // TConverter.Join combines multiple producers of the same type into a producer of an array of that type. Result := TConverter .Join(TConverter.TJoinMode.jmAll, Prods) .Chain( function(const V: TArray): Boolean var i: Integer; begin // The output is true only if all input booleans are true. Result := True; for i := 0 to High(V) do begin if not V[i] then begin Result := False; Exit; end; end; end); end; var IsRising := function(const P: TProducer): TProducer begin // A series is rising if the difference to its previous value is positive. // Assumes the existence of a Delta(1) converter. Result := P.Chain(Delta(1)).Chain(function(const D: Double): Boolean begin Result := D > 0; end); end; var IsFalling := function(const P: TProducer): TProducer begin // A series is falling if the difference to its previous value is negative. Result := P.Chain(Delta(1)).Chain(function(const D: Double): Boolean begin Result := D < 0; end); end; {$endregion} {$region '1. Timeframe Aggregation & Chart Setup'} // Aggregate Ticker data to 1H and 24H timeframes using a trade-specific converter. Ohlc1H := Ticker.Chain>(TTradeConverter.CreateOhlcAggregation(TTimeframe.H)); Ohlc24H := Ticker.Chain>(TTradeConverter.CreateOhlcAggregation(TTimeframe.D)); var Ohlc1HData := Ohlc1H.Field('Data'); var Ohlc24HData := Ohlc24H.Field('Data'); // Extract relevant data fields (Close price and Time) from the aggregated data points. // The Field helper uses RTTI to access nested record fields. Close1H := Ohlc1HData.Field('Close'); Time1H := Ohlc1H.Field('Time'); Close24H := Ohlc24HData.Field('Close'); Time24H := Ohlc24H.Field('Time'); // Setup the 1H chart Chart1H.SetXAxisSeries(TTimeframe.H, Time1H); var Panel1H_Main := Chart1H.AddPanel; Panel1H_Main.AddOhlcSeries(Ohlc1HData); var Panel1H_ATR := Chart1H.AddPanel; Panel1H_ATR.Weight := 0.25; // Setup the 24H chart Chart24H.SetXAxisSeries(TTimeframe.D, Time24H); var Panel24H_Main := Chart24H.AddPanel; Panel24H_Main.AddOhlcSeries(Ohlc24HData); var Panel24H_ATR := Chart24H.AddPanel; Panel24H_ATR.Weight := 0.25; {$endregion} {$region '2. Indicator Calculation'} // Calculate indicators for the 24H timeframe and add them to the chart. Hma250_24H := Close24H.Chain(THMA.CreateHMA(HMA_BIAS_PERIOD)); Sma200_24H := Close24H.Chain(TSMA.CreateSMA(SMA_BIAS_PERIOD)); // ATR is calculated from OHLC data, not just the close price. Atr50_24H := Ohlc24H.Field('Data').Chain(TATR.CreateATR(ATR_PERIOD)); Panel24H_Main.AddDoubleSeries(Hma250_24H, TAlphaColors.Aqua); Panel24H_Main.AddDoubleSeries(Sma200_24H, TAlphaColors.Orange); Panel24H_ATR.AddDoubleSeries(Atr50_24H, TAlphaColors.Magenta); // Calculate indicators for the 1H timeframe and add them to the chart. Hma250_1H := Close1H.Chain(THMA.CreateHMA(HMA_BIAS_PERIOD)); Sma200_1H := Close1H.Chain(TSMA.CreateSMA(SMA_BIAS_PERIOD)); Atr50_1H := Ohlc1H.Field('Data').Chain(TATR.CreateATR(ATR_PERIOD)); Hma20_1H := Close1H.Chain(THMA.CreateHMA(HMA_ENTRY_PERIOD)); Panel1H_Main.AddDoubleSeries(Hma250_1H, TAlphaColors.Aqua); Panel1H_Main.AddDoubleSeries(Sma200_1H, TAlphaColors.Orange); Panel1H_ATR.AddDoubleSeries(Atr50_1H, TAlphaColors.Magenta); Panel1H_Main.AddDoubleSeries(Hma20_1H, TAlphaColors.Yellow, 2.0); {$endregion} {$region '3. Bias and Filter Logic'} // Determine bullish/bearish bias for each timeframe based on moving average slopes. Bias24H_Bullish := All([IsRising(Hma250_24H), IsRising(Sma200_24H)]); Bias24H_Bearish := All([IsFalling(Hma250_24H), IsFalling(Sma200_24H)]); Bias1H_Bullish := All([IsRising(Hma250_1H), IsRising(Sma200_1H)]); Bias1H_Bearish := All([IsFalling(Hma250_1H), IsFalling(Sma200_1H)]); // Determine overall bias: both timeframes must agree. OverallBias_Bullish := All([Bias24H_Bullish, Bias1H_Bullish]); OverallBias_Bearish := All([Bias24H_Bearish, Bias1H_Bearish]); // Define the ATR filter logic. var GetFilter := function(const Hma, Sma, Atr: TProducer): TProducer begin var Dist := TConverter .Join(jmAll, [Hma, Sma]) .Chain(function(const V: TArray): Double begin Result := Abs(V[0] - V[1]); end); var Threshold := Atr.Chain(function(const V: Double): Double begin Result := V * ATR_FILTER_MULTIPLIER; end); Result := TConverter .Join(jmAll, [Dist, Threshold]) .Chain(function(const V: TArray): Boolean begin Result := V[0] > V[1]; end); end; // A trade is only allowed if the HMA/SMA distance is wide enough on both timeframes. Filter24H := GetFilter(Hma250_24H, Sma200_24H, Atr50_24H); Filter1H := GetFilter(Hma250_1H, Sma200_1H, Atr50_1H); TradeAllowed := All([Filter1H, Filter24H]); {$endregion} {$region '4. Entry Logic'} // Find the last high and low points of the 20-period HMA on the 1H chart. // Assumes an ExtremaFinder converter that returns a TExtrema record. Hma20_Extrema := Hma20_1H.Chain(ExtremaFinder(1)); LastLow_HMA20 := Hma20_Extrema.Field('Low'); LastHigh_HMA20 := Hma20_Extrema.Field('High'); // Define entry triggers: price crossing below the last low (for longs) or above the last high (for shorts). TriggerBuy := TConverter .Join(jmAll, [Close1H, LastLow_HMA20]) .Chain(function(const V: TArray): Boolean begin Result := V[0] < V[1]; end); TriggerShort := TConverter .Join(jmAll, [Close1H, LastHigh_HMA20]) .Chain(function(const V: TArray): Boolean begin Result := V[0] > V[1]; end); // Combine all conditions for the final entry signals. GoLong := All([OverallBias_Bullish, TradeAllowed, TriggerBuy]); GoShort := All([OverallBias_Bearish, TradeAllowed, TriggerShort]); {$endregion} {$region '5. Trade Management via Aggregation'} // The core state machine of the strategy. It's implemented as an aggregate function // that captures a state record and processes a stream of combined input data. var tradeState: TTradeState; tradeState.Status := tsFlat; tradeState.EntryPrice := 0; tradeState.StopLoss := 0; tradeState.TakeProfit := 0; // The aggregator function processes an array of TValue, where each element // corresponds to an input producer in a defined order. var aggregatorFunc: TAggregateFunc, Double> := function(const Value: TArray; const Broadcast: TBroadcastFunc): TState var goLong, goShort: Boolean; close, atr, lastHigh, lastLow: Double; begin // Not enough data, or data types are incorrect -> do nothing. if (Length(Value) <> 6) or not Value[0].IsType then begin Result := TState.Null; Exit; end; // Extract current values from the TValue array by index. // This order must match the order in the 'producers' array below. goLong := Value[0].AsBoolean; goShort := Value[1].AsBoolean; close := Value[2].AsExtended; atr := Value[3].AsExtended; lastHigh := Value[4].AsExtended; lastLow := Value[5].AsExtended; // The state machine logic for trade management remains identical. case tradeState.Status of tsFlat: begin if goLong then begin tradeState.Status := tsLong; tradeState.EntryPrice := close; tradeState.TakeProfit := lastHigh; tradeState.StopLoss := close - atr * ATR_SL_MULTIPLIER; end else if goShort then begin tradeState.Status := tsShort; tradeState.EntryPrice := close; tradeState.TakeProfit := lastLow; tradeState.StopLoss := close + atr * ATR_SL_MULTIPLIER; end; end; tsLong: begin var newSL := close - atr * ATR_SL_MULTIPLIER; if (newSL > tradeState.StopLoss) then tradeState.StopLoss := newSL; if (close >= tradeState.TakeProfit) or (close <= tradeState.StopLoss) then tradeState.Status := tsFlat; end; tsShort: begin var newSL := close + atr * ATR_SL_MULTIPLIER; if (newSL < tradeState.StopLoss) then tradeState.StopLoss := newSL; if (close <= tradeState.TakeProfit) or (close >= tradeState.StopLoss) then tradeState.Status := tsFlat; end; end; // Broadcast the current position status. Broadcast(Integer(tradeState.Status) - Integer(tsLong)); Result := TState.Null; end; // To feed the aggregator, combine all required data streams into one. // 1. Convert each producer to TProducer. var producers: TArray> := [ TValue.AsValue(GoLong), TValue.AsValue(GoShort), TValue.AsValue(Close1H), TValue.AsValue(Atr50_1H), TValue.AsValue(LastHigh_HMA20), TValue.AsValue(LastLow_HMA20) ]; // 2. Join them into a single producer of an array of TValue. var combinedProducer := TConverter.Join(jmAll, producers); // 3. Create the aggregator converter and chain it to the combined producer. var strategyAggregator := TConverter, Double>.CreateAggregation(aggregatorFunc); Result := combinedProducer.Chain(strategyAggregator); {$endregion} end; end.