unit Myc.Fmx.Chart.Series; interface uses System.SysUtils, System.SyncObjs, System.UITypes, FMX.Graphics, Myc.Signals, Myc.Mutable, Myc.Trade.Types, Myc.Trade.DataArray, Myc.Trade.DataPoint, Myc.Fmx.Chart; type TChartSeriesCounter = class(TMycConverter) private FCount: Int64; protected function ProcessData(const Value: T): TState; override; public constructor Create; end; TChartSeriesReceiver = class(TMycProcessor) strict private FCurrData: TMycDataArray; FLookback: TMutable; private FData: TWriteable>; protected function ProcessData(const Value: T): TState; override; public constructor Create(const ALookback: TMutable); property Data: TWriteable> read FData; end; TChartSeriesProcessor = class(TMycChart.TSeries) strict private FDataSeries: TMycDataArray; FLock: TSpinLock; private FData: TMycDataArray; FDataProvider: IMycDataProvider; FReceiver: TChartSeriesReceiver; FReceiverTag: TTag; protected function GetCount: Int64; override; function GetTotalCount: Int64; override; procedure Update; public constructor Create(const ADataProvider: IMycDataProvider; const ALookback: TMutable); destructor Destroy; override; property Data: TMycDataArray read FData; end; { TChartCustomLayer } TChartCustomLayer = class abstract(TMycChart.TDataLayer) private FSeries: TChartSeriesProcessor; protected function GetSeries: TMycChart.TSeries; override; function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; abstract; procedure Update; override; procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); override; abstract; public constructor Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider); destructor Destroy; override; 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, YForm: TFunc); override; public constructor Create( AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; 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, YForm: TFunc); override; public constructor Create( AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const ALineColor: TAlphaColor; ALineWidth: Single ); end; { TChartXAxisLayer } TChartXAxisLayer = class(TMycChart.TXAxisLayer) private FSeries: TChartSeriesProcessor; protected function GetSeries: TMycChart.TSeries; override; procedure Update; override; function GetCaption(Idx: Int64): String; override; abstract; public constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); destructor Destroy; override; 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 ADataProvider: IMycDataProvider); end; implementation uses System.Types, System.Math, FMX.Types; constructor TChartSeriesCounter.Create; begin inherited Create; FCount := 0; end; function TChartSeriesCounter.ProcessData(const Value: T): TState; begin Result := Broadcast(FCount); inc(FCount); end; { TChartSeriesReceiver } constructor TChartSeriesReceiver.Create(const ALookback: TMutable); begin inherited Create; FLookback := ALookback; FCurrData := TMycDataArray.CreateEmpty; FData := TWriteable>.CreateWriteable(FCurrData).Protect; end; function TChartSeriesReceiver.ProcessData(const Value: T): TState; begin Result := TState.Null; FCurrData := FCurrData.Add(Value, FLookback.Value); FData.Value := FCurrData; end; { TChartSeriesProcessor } constructor TChartSeriesProcessor.Create(const ADataProvider: IMycDataProvider; const ALookback: TMutable); begin inherited Create; FDataProvider := ADataProvider; FLock := TSpinLock.Create(false); FDataSeries := TMycDataArray.CreateEmpty; FData := FDataSeries; FReceiver := TChartSeriesReceiver.Create(ALookback); FReceiverTag := FDataProvider.Link(FReceiver); end; destructor TChartSeriesProcessor.Destroy; begin FDataProvider.Unlink(FReceiverTag); inherited; end; function TChartSeriesProcessor.GetCount: Int64; begin Result := FData.Count; end; function TChartSeriesProcessor.GetTotalCount: Int64; begin Result := FData.TotalCount; end; procedure TChartSeriesProcessor.Update; begin FData := FReceiver.Data.Value; end; { TChartLineLayer } constructor TChartLineLayer.Create( AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const ALineColor: TAlphaColor; ALineWidth: Single ); begin inherited Create(AParent, ADataProvider); 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 := FSeries.Data.Items[i]; if not IsNaN(v) then begin MinValue := Min(MinValue, v); MaxValue := Max(MaxValue, v); end; end; Result := (MinValue <> MaxDouble); end; procedure TChartLineLayer.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc); var points: TPathData; i, n: Int64; begin points := TPathData.Create; try // Skip warmup data n := First; while (n <= Last) and (IsNaN(FSeries.Data[n])) do inc(n); if (n > Last) then exit; points.MoveTo(TPointF.Create(XForm(n), YForm(FSeries.Data[n]))); for i := n + 1 to Last do if not IsNaN(FSeries.Data[i]) then points.LineTo(TPointF.Create(XForm(i), YForm(FSeries.Data[i]))); Canvas.Stroke.Color := FLineColor; Canvas.Stroke.Thickness := FLineWidth; Canvas.DrawPath(points, 1); finally points.Free; end; end; { TChartOhlcLayer } constructor TChartOhlcLayer.Create( AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider; const AUpColor, ADownColor: TMutable ); begin inherited Create(AParent, ADataProvider); 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 := FSeries.Data.Items[i]; MinValue := Min(MinValue, item.Low); MaxValue := Max(MaxValue, item.High); end; Result := (MinValue <> MaxDouble); end; procedure TChartOhlcLayer.Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, 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 := FSeries.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 ADataProvider: IMycDataProvider); begin inherited Create(AParent); FSeries := TChartSeriesProcessor.Create(ADataProvider, AParent.Owner.Lookback.AsMutable); end; destructor TChartCustomLayer.Destroy; begin FSeries.Free; inherited; end; function TChartCustomLayer.GetSeries: TMycChart.TSeries; begin Result := FSeries; end; procedure TChartCustomLayer.Update; begin FSeries.Update; end; { TChartXAxisLayer } constructor TChartXAxisLayer.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider); begin inherited Create(AOwner); FSeries := TChartSeriesProcessor.Create(ADataProvider, AOwner.Lookback.AsMutable); end; destructor TChartXAxisLayer.Destroy; begin FSeries.Free; inherited; end; function TChartXAxisLayer.GetSeries: TMycChart.TSeries; begin Result := FSeries; end; procedure TChartXAxisLayer.Update; begin FSeries.Update; end; constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: IMycDataProvider); begin inherited Create(AOwner, ADataProvider); FTimeframe := ATimeframe; end; function TChartXAxisTimestampLayer.GetCaption(Idx: Int64): String; var dt: TDateTime; formatStr: string; begin Result := ''; if (Idx >= 0) and (Idx < FSeries.Count) then begin dt := FSeries.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.