unit StrategyTest;

interface

uses
    Myc.Signals,
    Myc.Data.Pipeline,
    Myc.Trade.Types,
    Myc.Trade.Pipeline,
    Myc.Trade.Indicators.Common;

function CreateStrategy1(Timeframe: TTimeframe): TConverter<TDataPoint<TOhlcItem>, Double>; overload;

implementation

uses
    System.SysUtils,
    System.Math;

function CreateStrategy1(Timeframe: TTimeframe): TConverter<TDataPoint<TOhlcItem>, 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<TDataPoint<TOhlcItem>>;

    var OhlcPoint := ticker.Producer.Chain<TDataPoint<TOhlcItem>>(TTradeConverter.CreateOhlcAggregation(Timeframe));

    var Ohlc := OhlcPoint.Field<TOhlcItem>('Data');

    var Closes := Ohlc.Field<Double>('Close');

    var Hull := Closes.Chain<Double>(THMA.CreateHMA(250)).MakeParallel;
    var Sma := Closes.Chain<Double>(TSMA.CreateSMA(200)).MakeParallel;

    var ATR := Ohlc.Chain<Double>(TATR.CreateATR(50));

    var conv := TConverter.Join<Double>(jmAll, [Ohlc.Field<Double>('Low'), Ohlc.Field<Double>('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<TSignalEvent>(
            TConverter<TArray<Double>, TSignalEvent>.CreateConverter(
                function(const Values: TArray<Double>): 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<Double>(
            TConverter<TSignalEvent, Double>.CreateAggregation(
                function(const Value: TSignalEvent; const Broadcast: TBroadcastFunc<Double>): 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<Double>(
            TConverter<Double, Double>.CreateAggregation(
                function(const Value: Double; const Broadcast: TBroadcastFunc<Double>): 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<TDataPoint<TOhlcItem>, Double>.Construct(ticker.Consumer, equity);
end;

end.
