unit Myc.Fmx.Chart.Series;

interface

uses
    System.SysUtils,
    System.UITypes,
    FMX.Graphics,
    Myc.Signals,
    Myc.Mutable,
    Myc.Trade.Types,
    Myc.Data.Series,
    Myc.Data.Pipeline,
    Myc.Fmx.Chart;

type
    { TChartCustomLayer }
    TChartCustomLayer<T> = class abstract(TMycChart.TDataLayer)
    private
        FProducer: TProducer<T>.TSubscription;
        FSeries: TLazy<TSeries<T>>;
        FData: TSeries<T>;
    protected
        function GetCount: Int64; override;
        function GetTotalCount: Int64; override;
        function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; abstract;
        function Update: Boolean; override;
        procedure Paint(
            const Canvas: TCanvas;
            First, Last: Int64;
            const XForm: TFunc<Int64, Single>;
            const YForm: TFunc<Double, Single>
        ); override; abstract;
    public
        constructor Create(AParent: TMycChart.TPanel; const AProducer: TProducer<T>);
        destructor Destroy; override;
        property Data: TSeries<T> read FData;
    end;

    { TChartOhlcLayer }
    TChartOhlcLayer = class(TChartCustomLayer<TOhlcItem>)
    private
        FUpColor: TMutable<TAlphaColor>;
        FDownColor: TMutable<TAlphaColor>;
    protected
        function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override;
        procedure Paint(
            const Canvas: TCanvas;
            First, Last: Int64;
            const XForm: TFunc<Int64, Single>;
            const YForm: TFunc<Double, Single>
        ); override;
    public
        constructor Create(
            AParent: TMycChart.TPanel;
            const AProducer: TProducer<TOhlcItem>;
            const AUpColor, ADownColor: TMutable<TAlphaColor>
        );
    end;

    { TChartLineLayer }
    TChartLineLayer = class(TChartCustomLayer<Double>)
    private
        FLineColor: TAlphaColor;
        FLineWidth: Single;
    protected
        function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override;
        procedure Paint(
            const Canvas: TCanvas;
            First, Last: Int64;
            const XForm: TFunc<Int64, Single>;
            const YForm: TFunc<Double, Single>
        ); override;
    public
        constructor Create(
            AParent: TMycChart.TPanel;
            const AProducer: TProducer<Double>;
            const ALineColor: TAlphaColor;
            ALineWidth: Single
        );
    end;

    { TChartXAxisLayer }
    TChartXAxisLayer<T> = class(TMycChart.TXAxisLayer)
    private
        FProducer: TProducer<T>.TSubscription;
        FReceiver: TLazy<TSeries<T>>;
        FData: TSeries<T>;
    protected
        function Update: Boolean; override;
        function GetCaption(Idx: Int64): String; override;
        function GetCount: Int64; override;
        function GetTotalCount: Int64; override;
    public
        constructor Create(AOwner: TMycChart; const AProducer: TProducer<T>);
        destructor Destroy; override;
        property Data: TSeries<T> read FData;
    end;

    { TChartXAxisTimestampLayer }
    TChartXAxisTimestampLayer = class(TChartXAxisLayer<TDateTime>)
    private
        FTimeframe: TTimeframe;
    protected
        // Provide a formatted timestamp string for a given data index.
        function GetCaption(Idx: Int64): String; override; final;
    public
        constructor Create(AOwner: TMycChart; ATimeframe: TTimeframe; const AProducer: TProducer<TDateTime>);
    end;

implementation

uses
    System.Types,
    System.Math,
    FMX.Types;

{ TChartLineLayer }

constructor TChartLineLayer.Create(
    AParent: TMycChart.TPanel;
    const AProducer: TProducer<Double>;
    const ALineColor: TAlphaColor;
    ALineWidth: Single
);
begin
    inherited Create(AParent, AProducer);
    FLineColor := ALineColor;
    FLineWidth := ALineWidth;
end;

function TChartLineLayer.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean;
var
    i: Int64;
begin
    MinValue := MaxDouble;
    MaxValue := -MaxDouble;
    for i := First to Last do
    begin
        var v := Data.Items[i];
        if not IsNaN(v) then
        begin
            if v < MinValue then
                MinValue := v;
            if v > MaxValue then
                MaxValue := v;
        end;
    end;
    Result := (MinValue <= MaxValue);
end;

procedure TChartLineLayer.Paint(
    const Canvas: TCanvas;
    First, Last: Int64;
    const XForm: TFunc<Int64, Single>;
    const YForm: TFunc<Double, Single>
);
var
    points: TPathData;
    n: Int64;
begin
    points := TPathData.Create;
    try
        n := First;
        while n < Last do
        begin
            // Skip warmup/gap data
            while (n <= Last) and (IsNaN(Data[n])) do
                inc(n);

            if n < Last then
            begin
                points.MoveTo(TPointF.Create(XForm(n), YForm(Data[n])));
                inc(n);
                while n <= Last do
                begin
                    if IsNaN(Data[n]) then
                        break;
                    points.LineTo(TPointF.Create(XForm(n), YForm(Data[n])));
                    inc(n);
                end;
            end;
        end;

        Canvas.Stroke.Color := FLineColor;
        Canvas.Stroke.Thickness := FLineWidth;
        Canvas.Stroke.Join := TStrokeJoin.Bevel;
        Canvas.DrawPath(points, 1);
    finally
        points.Free;
    end;
end;

{ TChartOhlcLayer }

constructor TChartOhlcLayer.Create(
    AParent: TMycChart.TPanel;
    const AProducer: TProducer<TOhlcItem>;
    const AUpColor, ADownColor: TMutable<TAlphaColor>
);
begin
    inherited Create(AParent, AProducer);
    FUpColor := AUpColor;
    FDownColor := ADownColor;

    FUpColor.Changed.Subscribe(Owner.NeedRepaint);
    FDownColor.Changed.Subscribe(Owner.NeedRepaint);
end;

function TChartOhlcLayer.GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean;
var
    i: Int64;
begin
    MinValue := MaxDouble;
    MaxValue := -MaxDouble;

    for i := First to Last do
    begin
        var item := Data.Items[i];
        if item.Low < MinValue then
            MinValue := item.Low;
        if item.High > MaxValue then
            MaxValue := item.High;
    end;
    Result := MinValue <= MaxDouble;
end;

procedure TChartOhlcLayer.Paint(
    const Canvas: TCanvas;
    First, Last: Int64;
    const XForm: TFunc<Int64, Single>;
    const YForm: TFunc<Double, Single>
);
var
    i: Int64;
    x, candleWidth: Single;
    yOpen, yHigh, yLow, yClose: Single;
    item: TOhlcItem;
    isUp: boolean;
begin
    candleWidth := Max(2, 0.8 * Abs((XForm(First + 1) - XForm(First))));

    for i := First to Last do
    begin
        item := Data.Items[i];
        x := XForm(i);
        yOpen := YForm(item.Open);
        yClose := YForm(item.Close);
        yHigh := YForm(item.High);
        yLow := YForm(item.Low);
        isUp := item.Close >= item.Open;

        if isUp then
            Canvas.Stroke.Color := FUpColor.Value
        else
            Canvas.Stroke.Color := FDownColor.Value;
        Canvas.Stroke.Thickness := 1.0;

        Canvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1);

        Canvas.Fill.Color := Canvas.Stroke.Color;
        if isUp then
            Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1)
        else
            Canvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1);
    end;
end;

{ TChartCustomLayer }

constructor TChartCustomLayer<T>.Create(AParent: TMycChart.TPanel; const AProducer: TProducer<T>);
begin
    inherited Create(AParent);
    FProducer := AProducer.CreateEndpoint(Owner.Lookback.Value, FSeries);
end;

destructor TChartCustomLayer<T>.Destroy;
begin
    FProducer.Unlink;
    inherited;
end;

function TChartCustomLayer<T>.GetCount: Int64;
begin
    Result := FData.Count;
end;

function TChartCustomLayer<T>.GetTotalCount: Int64;
begin
    Result := FData.TotalCount;
end;

function TChartCustomLayer<T>.Update: Boolean;
begin
    Result := FSeries.Update(FData);
end;

{ TChartXAxisLayer }

constructor TChartXAxisLayer<T>.Create(AOwner: TMycChart; const AProducer: TProducer<T>);
begin
    inherited Create(AOwner);
    FProducer := AProducer.CreateEndpoint(Owner.Lookback.Value, FReceiver);
end;

destructor TChartXAxisLayer<T>.Destroy;
begin
    FProducer.Unlink;
    inherited;
end;

function TChartXAxisLayer<T>.GetCaption(Idx: Int64): String;
begin
    Result := IntToStr(Owner.ViewStartIndex + Idx);
end;

function TChartXAxisLayer<T>.GetCount: Int64;
begin
    Result := FData.Count;
end;

function TChartXAxisLayer<T>.GetTotalCount: Int64;
begin
    Result := FData.TotalCount;
end;

function TChartXAxisLayer<T>.Update: Boolean;
begin
    Result := FReceiver.Update(FData);
end;

{ TChartXAxisTimestampLayer }

constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const AProducer: TProducer<TDateTime>);
begin
    inherited Create(AOwner, AProducer);
    FTimeframe := ATimeframe;
end;

function TChartXAxisTimestampLayer.GetCaption(Idx: Int64): String;
var
    dt: TDateTime;
    formatStr: string;
begin
    Result := '';
    if (Idx >= 0) and (Idx < Data.Count) then
    begin
        dt := Data[Idx];

        // Choose format based on the series timeframe.
        if FTimeframe >= D then // Daily, Weekly, Monthly, Yearly
            formatStr := 'dd.mm.yyyy'
        else if FTimeframe >= M then // Hourly + Minutes
            formatStr := 'dd.mm.yyyy hh:nn'
        else // Seconds
            formatStr := 'dd.mm.yyyy hh:nn:ss';

        Result := FormatDateTime(formatStr, dt);
    end;
end;

end.
