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<TDataPoint<TOhlcItem>>;
    const Log: TStrings;
    const Chart1H, Chart24H: TMycChart
): TProducer<Double>;

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<Double, Double>;
var
    history: TQueue<Double>; // Captured state for the history of values
begin
    if N <= 0 then
        raise EArgumentException.Create('Delta period N must be positive.');

    history := TQueue<Double>.Create;

    Result :=
        TConverter<Double, Double>.CreateAggregation(
            // This function is the core of the aggregator. It is called for each value.
            function(const Value: Double; const Broadcast: TBroadcastFunc<Double>): 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<Double, TExtrema>;
var
    // Captured state for the aggregator
    history: TQueue<Double>;
    lastHigh, lastLow: Double;
    initialized: Boolean;
begin
    if N <= 0 then
        raise EArgumentException.Create('ExtremaFinder period N must be positive.');

    initialized := False;
    history := TQueue<Double>.Create;
    lastHigh := 0;
    lastLow := 0;

    Result :=
        TConverter<Double, TExtrema>.CreateAggregation(
            function(const Value: Double; const Broadcast: TBroadcastFunc<TExtrema>): TState
            var
                data: TArray<Double>;
                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<T>(const P: TProducer<T>): TProducer<TValue>; static;
    end;

// Helper to convert a producer of any type T into a producer of TValue.
class function TValueHelper.AsValue<T>(const P: TProducer<T>): TProducer<TValue>;
begin
    Result := P.Chain<TValue>(function(const V: T): TValue begin Result := TValue.From<T>(V); end);
end;

function CreateStrategy2(
    const Ticker: TProducer<TDataPoint<TOhlcItem>>;
    const Log: TStrings;
    const Chart1H, Chart24H: TMycChart
): TProducer<Double>;
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<TDataPoint<TOhlcItem>>;
    Close1H, Close24H: TProducer<Double>;
    Time1H, Time24H: TProducer<TDateTime>;

    // 24H Indicators
    Hma250_24H, Sma200_24H, Atr50_24H: TProducer<Double>;
    // 1H Indicators
    Hma250_1H, Sma200_1H, Atr50_1H, Hma20_1H: TProducer<Double>;

    // Bias producers
    Bias24H_Bullish, Bias24H_Bearish: TProducer<Boolean>;
    Bias1H_Bullish, Bias1H_Bearish: TProducer<Boolean>;
    OverallBias_Bullish, OverallBias_Bearish: TProducer<Boolean>;

    // Filter producers
    Filter24H, Filter1H, TradeAllowed: TProducer<Boolean>;

    // Entry producers
    Hma20_Extrema: TProducer<TExtrema>;
    LastLow_HMA20, LastHigh_HMA20: TProducer<Double>;
    TriggerBuy, TriggerShort: TProducer<Boolean>;
    GoLong, GoShort: TProducer<Boolean>;
{$endregion}
begin
    {$region 'Helper function implementations'}
    var All :=
        function(const Prods: TArray<TProducer<Boolean>>): TProducer<Boolean>
        begin
            // TConverter.Join combines multiple producers of the same type into a producer of an array of that type.
            Result :=
                TConverter
                    .Join<Boolean>(TConverter.TJoinMode.jmAll, Prods)
                    .Chain<Boolean>(
                        function(const V: TArray<Boolean>): 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<Double>): TProducer<Boolean>
        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<Double>(Delta(1)).Chain<Boolean>(function(const D: Double): Boolean begin Result := D > 0; end);
        end;

    var IsFalling :=
        function(const P: TProducer<Double>): TProducer<Boolean>
        begin
            // A series is falling if the difference to its previous value is negative.
            Result := P.Chain<Double>(Delta(1)).Chain<Boolean>(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<TDataPoint<TOhlcItem>>(TTradeConverter.CreateOhlcAggregation(TTimeframe.H));
    Ohlc24H := Ticker.Chain<TDataPoint<TOhlcItem>>(TTradeConverter.CreateOhlcAggregation(TTimeframe.D));

    var Ohlc1HData := Ohlc1H.Field<TOhlcItem>('Data');
    var Ohlc24HData := Ohlc24H.Field<TOhlcItem>('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<Double>('Close');
    Time1H := Ohlc1H.Field<TDateTime>('Time');
    Close24H := Ohlc24HData.Field<Double>('Close');
    Time24H := Ohlc24H.Field<TDateTime>('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<Double>(THMA.CreateHMA(HMA_BIAS_PERIOD));
    Sma200_24H := Close24H.Chain<Double>(TSMA.CreateSMA(SMA_BIAS_PERIOD));
    // ATR is calculated from OHLC data, not just the close price.
    Atr50_24H := Ohlc24H.Field<TOhlcItem>('Data').Chain<Double>(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<Double>(THMA.CreateHMA(HMA_BIAS_PERIOD));
    Sma200_1H := Close1H.Chain<Double>(TSMA.CreateSMA(SMA_BIAS_PERIOD));
    Atr50_1H := Ohlc1H.Field<TOhlcItem>('Data').Chain<Double>(TATR.CreateATR(ATR_PERIOD));
    Hma20_1H := Close1H.Chain<Double>(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<Double>): TProducer<Boolean>
        begin
            var Dist :=
                TConverter
                    .Join<Double>(jmAll, [Hma, Sma])
                    .Chain<Double>(function(const V: TArray<Double>): Double begin Result := Abs(V[0] - V[1]); end);

            var Threshold := Atr.Chain<Double>(function(const V: Double): Double begin Result := V * ATR_FILTER_MULTIPLIER; end);

            Result :=
                TConverter
                    .Join<Double>(jmAll, [Dist, Threshold])
                    .Chain<Boolean>(function(const V: TArray<Double>): 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<TExtrema>(ExtremaFinder(1));
    LastLow_HMA20 := Hma20_Extrema.Field<Double>('Low');
    LastHigh_HMA20 := Hma20_Extrema.Field<Double>('High');

    // Define entry triggers: price crossing below the last low (for longs) or above the last high (for shorts).
    TriggerBuy :=
        TConverter
            .Join<Double>(jmAll, [Close1H, LastLow_HMA20])
            .Chain<Boolean>(function(const V: TArray<Double>): Boolean begin Result := V[0] < V[1]; end);

    TriggerShort :=
        TConverter
            .Join<Double>(jmAll, [Close1H, LastHigh_HMA20])
            .Chain<Boolean>(function(const V: TArray<Double>): 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<TArray<TValue>, Double> :=
        function(const Value: TArray<TValue>; const Broadcast: TBroadcastFunc<Double>): 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<Boolean> 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<TValue>.
    var producers: TArray<TProducer<TValue>> :=
        [
            TValue.AsValue<Boolean>(GoLong),
            TValue.AsValue<Boolean>(GoShort),
            TValue.AsValue<Double>(Close1H),
            TValue.AsValue<Double>(Atr50_1H),
            TValue.AsValue<Double>(LastHigh_HMA20),
            TValue.AsValue<Double>(LastLow_HMA20)
        ];

    // 2. Join them into a single producer of an array of TValue.
    var combinedProducer := TConverter.Join<TValue>(jmAll, producers);

    // 3. Create the aggregator converter and chain it to the combined producer.
    var strategyAggregator := TConverter<TArray<TValue>, Double>.CreateAggregation(aggregatorFunc);
    Result := combinedProducer.Chain<Double>(strategyAggregator);
    {$endregion}
end;

end.
