unit Myc.Fmx.Chart; interface uses System.SysUtils, System.Classes, System.Types, System.Generics.Collections, System.UITypes, System.UIConsts, System.Messaging, System.Math.Vectors, FMX.Types, FMX.Controls, FMX.Graphics, Myc.Trade.DataPoint, Myc.Signals; type TCandleStyle = (csCandleStick, csHiLoBar); TMycChart = class(TStyledControl) type TSeries = class abstract(TContainedObject) private function GetMainSeries: TSeries; function GetOwner: TMycChart; protected function GetCount: Int64; virtual; abstract; function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract; function Update: Boolean; virtual; abstract; procedure Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc); virtual; abstract; public constructor Create(AOwner: TMycChart); property Count: Int64 read GetCount; property MainSeries: TSeries read GetMainSeries; property Owner: TMycChart read GetOwner; end; private FSeriesList: TList; FLookback: Integer; FIdleSubscrId: TMessageSubscriptionId; protected procedure Paint; override; procedure DoIdle; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; // Creates an OHLC candlestick/bar series function CreateOhlcListener( const AUpColor: TAlphaColor = TAlphaColors.Green; const ADownColor: TAlphaColor = TAlphaColors.Red; const AStyle: TCandleStyle = csCandleStick ): IMycProcessor>>; // Creates a simple line series for double values function CreateDoubleListener(const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5): IMycProcessor>; // The maximum number of data points to display from the main series. property Lookback: Integer read FLookback write FLookback; end; implementation uses System.Math, System.SyncObjs, WinApi.Windows, Myc.Trade.DataArray; type TChartSeriesProcessor = class(TMycChart.TSeries, IMycProcessor>) strict private FDataSeries: TMycDataArray; FChanged: Boolean; FLock: TSpinLock; procedure ProcessData(const Values: TArray); private FData: TMycDataArray; protected function GetCount: Int64; override; function Update: Boolean; override; public constructor Create(AOwner: TMycChart); property Data: TMycDataArray read FData; end; { TChartOhlcSeries } TChartOhlcSeries = class(TChartSeriesProcessor>) private FUpColor: TAlphaColor; FDownColor: TAlphaColor; FStyle: TCandleStyle; protected function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; override; procedure Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc); override; public constructor Create(AOwner: TMycChart; const AUpColor, ADownColor: TAlphaColor; AStyle: TCandleStyle); end; { TChartLineSeries } TChartLineSeries = class(TChartSeriesProcessor) private FLineColor: TAlphaColor; FLineWidth: Single; protected function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; override; procedure Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc); override; public constructor Create(AOwner: TMycChart; const ALineColor: TAlphaColor; ALineWidth: Single); end; { TMycChart.TSeries } constructor TMycChart.TSeries.Create(AOwner: TMycChart); begin inherited Create(AOwner); end; function TMycChart.TSeries.GetMainSeries: TSeries; begin Result := nil; if Owner.FSeriesList.Count > 0 then Result := Owner.FSeriesList[0]; end; function TMycChart.TSeries.GetOwner: TMycChart; begin Result := Controller as TMycChart; end; { TMycChart } constructor TMycChart.Create(AOwner: TComponent); begin inherited Create(AOwner); FSeriesList := TObjectList.Create(true); FLookback := 100; // Default lookback FIdleSubscrId := TMessageManager .DefaultManager .SubscribeToMessage(TIdleMessage, procedure(const Sender: TObject; const M: TMessage) begin DoIdle; end); end; destructor TMycChart.Destroy; begin TMessageManager.DefaultManager.Unsubscribe(TIdleMessage, FIdleSubscrId); FSeriesList.Free; inherited; end; function TMycChart.CreateDoubleListener(const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineWidth: Single = 1.5): IMycProcessor>; var series: TChartLineSeries; begin series := TChartLineSeries.Create(Self, ALineColor, ALineWidth); FSeriesList.Add(series); Result := series; end; function TMycChart.CreateOhlcListener( const AUpColor: TAlphaColor = TAlphaColors.Green; const ADownColor: TAlphaColor = TAlphaColors.Red; const AStyle: TCandleStyle = csCandleStick ): IMycProcessor>>; var series: TChartOhlcSeries; begin series := TChartOhlcSeries.Create(Self, AUpColor, ADownColor, AStyle); FSeriesList.Add(series); Result := series; end; procedure TMycChart.DoIdle; begin var doRepaint := false; for var series in FSeriesList do if series.Update then doRepaint := true; if doRepaint then Repaint; end; procedure TMycChart.Paint; var rect: TRectF; series: TSeries; globalMin, globalMax, seriesMin, seriesMax, padding: Double; rangeInitialized: Boolean; xTransform: TFunc; yTransform: TFunc; begin inherited; rect := Self.LocalRect; if (FSeriesList.Count = 0) or (FLookback <= 1) then begin Canvas.Fill.Color := TAlphaColors.Gray; Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center); Exit; end; rangeInitialized := false; for series in FSeriesList do begin if series.GetValueRange(0, FLookback, seriesMin, seriesMax) then begin if not rangeInitialized then begin globalMin := seriesMin; globalMax := seriesMax; rangeInitialized := true; end else begin globalMin := Min(globalMin, seriesMin); globalMax := Max(globalMax, seriesMax); end; end; end; if not rangeInitialized then Exit; padding := (globalMax - globalMin) * 0.1; globalMin := globalMin - padding; globalMax := globalMax + padding; if (globalMax - globalMin) = 0 then exit; xTransform := function(index: Double): Single begin Result := rect.Right - (index / (FLookback - 1)) * rect.Width; end; yTransform := function(value: Double): Single begin Result := rect.Top + (1 - (value - globalMin) / (globalMax - globalMin)) * rect.Height; end; // var T := // TMatrix.CreateTranslation(rect.Left, rect.Top + rect.Height*globalMax / (globalMax - globalMin)) * // TMatrix.CreateScaling(rect.Width / (FLookback - 1), -rect.Height / (globalMax - globalMin)); for series in FSeriesList do begin series.Paint(Self.Canvas, xTransform, yTransform); end; end; { TChartSeriesProcessor } constructor TChartSeriesProcessor.Create(AOwner: TMycChart); begin inherited Create(AOwner); FLock := TSpinLock.Create(false); FDataSeries := TMycDataArray.CreateEmpty; FData := FDataSeries; end; function TChartSeriesProcessor.GetCount: Int64; begin Result := FData.Count; end; procedure TChartSeriesProcessor.ProcessData(const Values: TArray); begin FLock.Enter; try FDataSeries := FDataSeries.Add(Values, 0, Length(Values), Owner.Lookback); FChanged := true; finally FLock.Exit; end; end; function TChartSeriesProcessor.Update: Boolean; begin FLock.Enter; try Result := FChanged; FChanged := false; if Result then FData := FDataSeries; finally FLock.Exit; end; end; { TChartOhlcSeries } constructor TChartOhlcSeries.Create(AOwner: TMycChart; const AUpColor, ADownColor: TAlphaColor; AStyle: TCandleStyle); begin inherited Create(AOwner); FUpColor := AUpColor; FDownColor := ADownColor; FStyle := AStyle; end; function TChartOhlcSeries.GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; var i: Int64; begin Result := GetCount > 0; if not Result then Exit; Min := MaxDouble; Max := -MaxDouble; for i := StartIndex to System.Math.Min(GetCount - 1, StartIndex + Count - 1) do begin Min := System.Math.Min(Min, Data.Items[i].Data.Low); Max := System.Math.Max(Max, Data.Items[i].Data.High); end; Result := (Min <> MaxDouble); end; procedure TChartOhlcSeries.Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc); var i, displayCount: Int64; x, candleWidth: Single; yOpen, yHigh, yLow, yClose: Single; item: TDataPoint; isUp: boolean; begin displayCount := System.Math.Min(Data.Count, Owner.Lookback); if displayCount <= 0 then Exit; if displayCount > 1 then candleWidth := Max(2, 0.8 * Abs((AXForm(1) - AXForm(0)))) else candleWidth := 10; for i := 0 to displayCount - 1 do begin item := Data.Items[i]; x := AXForm(i); yOpen := AYForm(item.Data.Open); yClose := AYForm(item.Data.Close); yHigh := AYForm(item.Data.High); yLow := AYForm(item.Data.Low); isUp := item.Data.Close >= item.Data.Open; if isUp then ACanvas.Stroke.Color := FUpColor else ACanvas.Stroke.Color := FDownColor; ACanvas.Stroke.Thickness := 1.0; ACanvas.DrawLine(TPointF.Create(x, yHigh), TPointF.Create(x, yLow), 1); if FStyle = csCandleStick then begin ACanvas.Fill.Color := ACanvas.Stroke.Color; if isUp then ACanvas.FillRect(TRectF.Create(x - candleWidth / 2, yClose, x + candleWidth / 2, yOpen), 0, 0, AllCorners, 1) else ACanvas.FillRect(TRectF.Create(x - candleWidth / 2, yOpen, x + candleWidth / 2, yClose), 0, 0, AllCorners, 1); end; end; end; { TChartLineSeries } constructor TChartLineSeries.Create(AOwner: TMycChart; const ALineColor: TAlphaColor; ALineWidth: Single); begin inherited Create(AOwner); FLineColor := ALineColor; FLineWidth := ALineWidth; end; function TChartLineSeries.GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; var i: Int64; begin Result := GetCount > 0; if not Result then Exit; Min := MaxDouble; Max := -MaxDouble; for i := StartIndex to System.Math.Min(GetCount - 1, StartIndex + Count - 1) do begin Min := System.Math.Min(Min, Data.Items[i]); Max := System.Math.Max(Max, Data.Items[i]); end; Result := (Min <> MaxDouble); end; procedure TChartLineSeries.Paint(const ACanvas: TCanvas; const AXForm, AYForm: TFunc); begin if not Assigned(MainSeries) or (Data.Count = 0) or (MainSeries.Count = 0) then Exit; var points := TPathData.Create; var displayCount := System.Math.Min(Data.Count, Owner.Lookback); if displayCount < 2 then Exit; points.MoveTo( TPointF.Create(AXForm(0), AYForm(Data[0])) ); for var i := 1 to displayCount - 1 do points.LineTo( TPointF.Create(AXForm(i), AYForm(Data[i])) ); ACanvas.Stroke.Color := FLineColor; ACanvas.Stroke.Thickness := FLineWidth; ACanvas.DrawPath(points, 1); end; end.