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 = class abstract(TMycChart.TDataLayer) private FProducer: TProducer.TSubscription; FSeries: TLazy>; FData: TSeries; 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; const YForm: TFunc ); override; abstract; public constructor Create(AParent: TMycChart.TPanel; const AProducer: TProducer); destructor Destroy; override; property Data: TSeries read FData; end; { TChartOhlcLayer } TChartOhlcLayer = class(TChartCustomLayer) private FUpColor: TMutable; FDownColor: TMutable; protected function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; procedure Paint( const Canvas: TCanvas; First, Last: Int64; const XForm: TFunc; const YForm: TFunc ); override; public constructor Create( AParent: TMycChart.TPanel; const AProducer: TProducer; const AUpColor, ADownColor: TMutable ); end; { TChartLineLayer } TChartLineLayer = class(TChartCustomLayer) 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; const YForm: TFunc ); override; public constructor Create( AParent: TMycChart.TPanel; const AProducer: TProducer; const ALineColor: TAlphaColor; ALineWidth: Single ); end; { TChartXAxisLayer } TChartXAxisLayer = class(TMycChart.TXAxisLayer) private FProducer: TProducer.TSubscription; FReceiver: TLazy>; FData: TSeries; 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); destructor Destroy; override; property Data: TSeries read FData; end; { TChartXAxisTimestampLayer } TChartXAxisTimestampLayer = class(TChartXAxisLayer) 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); end; implementation uses System.Types, System.Math, FMX.Types; { TChartLineLayer } constructor TChartLineLayer.Create( AParent: TMycChart.TPanel; const AProducer: TProducer; 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; const YForm: TFunc ); 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; const AUpColor, ADownColor: TMutable ); 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; const YForm: TFunc ); 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.Create(AParent: TMycChart.TPanel; const AProducer: TProducer); begin inherited Create(AParent); FProducer := AProducer.CreateEndpoint(Owner.Lookback.Value, FSeries); end; destructor TChartCustomLayer.Destroy; begin FProducer.Unlink; inherited; end; function TChartCustomLayer.GetCount: Int64; begin Result := FData.Count; end; function TChartCustomLayer.GetTotalCount: Int64; begin Result := FData.TotalCount; end; function TChartCustomLayer.Update: Boolean; begin Result := FSeries.Update(FData); end; { TChartXAxisLayer } constructor TChartXAxisLayer.Create(AOwner: TMycChart; const AProducer: TProducer); begin inherited Create(AOwner); FProducer := AProducer.CreateEndpoint(Owner.Lookback.Value, FReceiver); end; destructor TChartXAxisLayer.Destroy; begin FProducer.Unlink; inherited; end; function TChartXAxisLayer.GetCaption(Idx: Int64): String; begin Result := IntToStr(Owner.ViewStartIndex + Idx); end; function TChartXAxisLayer.GetCount: Int64; begin Result := FData.Count; end; function TChartXAxisLayer.GetTotalCount: Int64; begin Result := FData.TotalCount; end; function TChartXAxisLayer.Update: Boolean; begin Result := FReceiver.Update(FData); end; { TChartXAxisTimestampLayer } constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const AProducer: TProducer); 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.