Files
MycLib/AuraTrader/TestChartControl.pas
T
Michael Schimmel 58ce84e567 Chart Control V1
2025-07-02 14:49:33 +02:00

699 lines
21 KiB
ObjectPascal

unit TestChartControl;
interface
uses
System.SysUtils,
System.Types,
System.UITypes,
System.Classes,
System.Variants,
System.Generics.Collections,
System.Math,
System.UIConsts, // For AlphaColors
FMX.Types,
FMX.Controls,
FMX.Forms,
FMX.Graphics,
FMX.Dialogs,
FMX.Objects,
Myc.Trade.DataPoint;
type
// Declared for testing, will be moved to external unit
TOhlcItem = record
Open, High, Low, Close: Double;
end;
// Custom high-precision point type using Double.
TPointD = record
public
X: Double;
Y: Double;
constructor Create(AX, AY: Double);
end;
// Custom high-precision rectangle type using Double.
TRectD = record
private
function GetHeight: Double;
function GetWidth: Double;
public
Left, Top, Right, Bottom: Double;
constructor Create(const ALeft, ATop, ARight, ABottom: Double); overload;
constructor Create(const ATopLeft, ABottomRight: TPointD); overload;
property Width: Double read GetWidth;
property Height: Double read GetHeight;
end;
TChart = class; // Forward declaration
// Abstract base class for a single series in a chart.
TChartSeries = class abstract(TCollectionItem)
private
FColor: TAlphaColor;
FThickness: Single;
procedure SetColor(const Value: TAlphaColor);
procedure SetThickness(const Value: Single);
protected
function GetChart: TChart;
public
constructor Create(Collection: TCollection); override;
// Calculates the bounding box for this series.
procedure GetBounds(var MinPoint, MaxPoint: TPointD); virtual; abstract;
// Draws the series on the canvas.
procedure Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF); virtual; abstract;
published
property Color: TAlphaColor read FColor write SetColor;
property Thickness: Single read FThickness write SetThickness;
end;
// Generic, abstract adapter to connect a TDataSeries<T> to a TChart.
TChartSeriesAdapter<T> = class abstract(TChartSeries)
private
FDataSeries: TDataSeries<T>;
protected
procedure SetDataSeries(const Value: TDataSeries<T>); virtual;
// Converts a data point from the source series to a high-precision visual point for the chart.
// May not be applicable for all series types (e.g., OHLC).
function DataToPoint(const DataPoint: TDataPoint<T>): TPointD; virtual; abstract;
public
constructor Create(Collection: TCollection); override;
property DataSeries: TDataSeries<T> read FDataSeries write SetDataSeries;
end;
TDataSourceField = (dsfAsk, dsfBid);
// Concrete series implementation for displaying TDataSeries<TAskBidItem>.
TChartAskBidSeries = class(TChartSeriesAdapter<TAskBidItem>)
private
FDataSourceField: TDataSourceField;
FCachedPoints: TArray<TPointD>;
FCacheValid: Boolean;
procedure SetDataSourceField(const Value: TDataSourceField);
procedure EnsureCacheIsValid;
protected
procedure SetDataSeries(const Value: TDataSeries<TAskBidItem>); override;
function DataToPoint(const DataPoint: TDataPoint<TAskBidItem>): TPointD; override;
public
constructor Create(Collection: TCollection); override;
procedure GetBounds(var MinPoint, MaxPoint: TPointD); override;
procedure Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF); override;
published
property DataSourceField: TDataSourceField read FDataSourceField write SetDataSourceField;
end;
// Concrete series implementation for displaying TDataSeries<TOhlcItem> as candlesticks.
TChartOhlcSeries = class(TChartSeriesAdapter<TOhlcItem>)
private
FUpColor: TAlphaColor;
FDownColor: TAlphaColor;
protected
// This function is not used for OHLC drawing but must be implemented.
function DataToPoint(const DataPoint: TDataPoint<TOhlcItem>): TPointD; override;
public
constructor Create(Collection: TCollection); override;
procedure GetBounds(var MinPoint, MaxPoint: TPointD); override;
procedure Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF); override;
published
property UpColor: TAlphaColor read FUpColor write FUpColor;
property DownColor: TAlphaColor read FDownColor write FDownColor;
end;
TChartSeriesCollection = class(TCollection)
private
[weak]
FOwner: TChart;
function GetItem(Index: Integer): TChartSeries;
procedure SetItem(Index: Integer; const Value: TChartSeries);
protected
function GetOwner: TPersistent; override;
procedure Update(Item: TCollectionItem); override;
public
constructor Create(AOwner: TChart);
property Items[Index: Integer]: TChartSeries read GetItem write SetItem; default;
end;
// A control for displaying charts.
TChart = class(TControl)
private
FSeries: TChartSeriesCollection;
FAxisColor: TAlphaColor;
FGridColor: TAlphaColor;
FPadding: Single;
procedure SetSeries(const Value: TChartSeriesCollection);
procedure SetAxisColor(const Value: TAlphaColor);
procedure SetGridColor(const Value: TAlphaColor);
procedure SetPadding(const Value: Single);
protected
procedure Paint; override;
procedure RecalcDataBounds(out MinPoint, MaxPoint: TPointD);
procedure DrawAxes(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
procedure DrawGrid(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Repaint;
published
property Align;
property Anchors;
property Series: TChartSeriesCollection read FSeries write SetSeries;
property AxisColor: TAlphaColor read FAxisColor write SetAxisColor;
property GridColor: TAlphaColor read FGridColor write SetGridColor;
property Padding: Single read FPadding write SetPadding;
end;
TTestChartForm = class(TForm)
procedure FormCreate(Sender: TObject);
private
FChart: TChart;
public
end;
var
TestChartForm: TTestChartForm;
implementation
{$R *.fmx}
{ TPointD }
constructor TPointD.Create(AX, AY: Double);
begin
X := AX;
Y := AY;
end;
{ TRectD }
constructor TRectD.Create(const ALeft, ATop, ARight, ABottom: Double);
begin
Left := ALeft;
Top := ATop;
Right := ARight;
Bottom := ABottom;
end;
constructor TRectD.Create(const ATopLeft, ABottomRight: TPointD);
begin
Left := ATopLeft.X;
Top := ATopLeft.Y;
Right := ABottomRight.X;
Bottom := ABottomRight.Y;
end;
function TRectD.GetHeight: Double;
begin
Result := Bottom - Top;
end;
function TRectD.GetWidth: Double;
begin
Result := Right - Left;
end;
{ TChartSeries }
constructor TChartSeries.Create(Collection: TCollection);
begin
inherited Create(Collection);
FColor := TAlphaColors.Black; // Default color for wicks in OHLC
FThickness := 1;
end;
function TChartSeries.GetChart: TChart;
begin
Result := (Collection as TChartSeriesCollection).GetOwner as TChart;
end;
procedure TChartSeries.SetColor(const Value: TAlphaColor);
begin
if (FColor <> Value) then
begin
FColor := Value;
if Assigned(Collection) then
Changed(False);
end;
end;
procedure TChartSeries.SetThickness(const Value: Single);
begin
if (FThickness <> Value) then
begin
FThickness := Value;
if Assigned(Collection) then
Changed(False);
end;
end;
{ TChartSeriesAdapter<T> }
constructor TChartSeriesAdapter<T>.Create(Collection: TCollection);
begin
inherited Create(Collection);
FDataSeries := TDataSeries<T>.Null;
end;
procedure TChartSeriesAdapter<T>.SetDataSeries(const Value: TDataSeries<T>);
begin
FDataSeries := Value;
if Assigned(Collection) then
Changed(False);
end;
{ TChartAskBidSeries }
constructor TChartAskBidSeries.Create(Collection: TCollection);
begin
inherited Create(Collection);
Self.Color := TAlphaColors.Blue; // Override default for line charts
Self.Thickness := 2;
FDataSourceField := dsfAsk;
FCacheValid := False;
end;
procedure TChartAskBidSeries.EnsureCacheIsValid;
var
i: Int64;
pointCount: Int64;
begin
if FCacheValid then
Exit;
pointCount := DataSeries.Count;
SetLength(FCachedPoints, pointCount);
if (pointCount > 0) then
begin
for i := 0 to pointCount - 1 do
begin
FCachedPoints[i] := DataToPoint(DataSeries[pointCount - 1 - i]);
end;
end;
FCacheValid := True;
end;
procedure TChartAskBidSeries.GetBounds(var MinPoint, MaxPoint: TPointD);
var
pt: TPointD;
isFirst: Boolean;
begin
EnsureCacheIsValid;
if (Length(FCachedPoints) = 0) then
Exit;
isFirst := (MinPoint.X = MaxPoint.X) and (MinPoint.Y = MaxPoint.Y);
if isFirst then
begin
MinPoint := FCachedPoints[0];
MaxPoint := FCachedPoints[0];
end;
for pt in FCachedPoints do
begin
MinPoint.X := Min(MinPoint.X, pt.X);
MinPoint.Y := Min(MinPoint.Y, pt.Y);
MaxPoint.X := Max(MaxPoint.X, pt.X);
MaxPoint.Y := Max(MaxPoint.Y, pt.Y);
end;
end;
procedure TChartAskBidSeries.Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
var
scaleX, scaleY: Double;
pt1, pt2: TPointD;
transformedPt1, transformedPt2: TPointF;
j: Integer;
begin
EnsureCacheIsValid;
if Length(FCachedPoints) < 2 then
Exit;
if (ADataRect.Width <= 0) or (ADataRect.Height <= 0) then
Exit;
scaleX := ACanvasRect.Width / ADataRect.Width;
scaleY := ACanvasRect.Height / ADataRect.Height;
ACanvas.Stroke.Kind := TBrushKind.Solid;
ACanvas.Stroke.Color := Self.Color;
ACanvas.Stroke.Thickness := Self.Thickness;
pt1 := FCachedPoints[0];
for j := 1 to High(FCachedPoints) do
begin
pt2 := FCachedPoints[j];
transformedPt1.X := ACanvasRect.Left + (pt1.X - ADataRect.Left) * scaleX;
transformedPt1.Y := ACanvasRect.Bottom - (pt1.Y - ADataRect.Top) * scaleY;
transformedPt2.X := ACanvasRect.Left + (pt2.X - ADataRect.Left) * scaleX;
transformedPt2.Y := ACanvasRect.Bottom - (pt2.Y - ADataRect.Top) * scaleY;
ACanvas.DrawLine(transformedPt1, transformedPt2, 1);
pt1 := pt2;
end;
end;
procedure TChartAskBidSeries.SetDataSeries(const Value: TDataSeries<TAskBidItem>);
begin
inherited SetDataSeries(Value);
FCacheValid := False;
end;
function TChartAskBidSeries.DataToPoint(const DataPoint: TDataPoint<TAskBidItem>): TPointD;
begin
Result.X := DataPoint.Time;
case FDataSourceField of
dsfAsk: Result.Y := DataPoint.Data.Ask;
dsfBid: Result.Y := DataPoint.Data.Bid;
else
Result.Y := 0;
end;
end;
procedure TChartAskBidSeries.SetDataSourceField(const Value: TDataSourceField);
begin
if (FDataSourceField <> Value) then
begin
FDataSourceField := Value;
FCacheValid := False;
if Assigned(Collection) then
Changed(False);
end;
end;
{ TChartOhlcSeries }
constructor TChartOhlcSeries.Create(Collection: TCollection);
begin
inherited Create(Collection);
FUpColor := TAlphaColors.Green;
FDownColor := TAlphaColors.Red;
end;
function TChartOhlcSeries.DataToPoint(const DataPoint: TDataPoint<TOhlcItem>): TPointD;
begin
// Not used for OHLC series drawing, but must be implemented for the abstract parent.
Result.X := DataPoint.Time;
Result.Y := DataPoint.Data.Close;
end;
procedure TChartOhlcSeries.GetBounds(var MinPoint, MaxPoint: TPointD);
var
isFirst: Boolean;
i: Int64;
dataPoint: TDataPoint<TOhlcItem>;
begin
if (DataSeries.Count = 0) then
Exit;
isFirst := (MinPoint.X > MaxPoint.X); // A more robust check for an uninitialized state
for i := 0 to DataSeries.Count - 1 do
begin
dataPoint := DataSeries[i];
if isFirst then
begin
MinPoint.X := dataPoint.Time;
MaxPoint.X := dataPoint.Time;
MinPoint.Y := dataPoint.Data.Low;
MaxPoint.Y := dataPoint.Data.High;
isFirst := False;
end
else
begin
MinPoint.X := Min(MinPoint.X, dataPoint.Time);
MaxPoint.X := Max(MaxPoint.X, dataPoint.Time);
MinPoint.Y := Min(MinPoint.Y, dataPoint.Data.Low);
MaxPoint.Y := Max(MaxPoint.Y, dataPoint.Data.High);
end;
end;
end;
procedure TChartOhlcSeries.Draw(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
var
scaleX, scaleY: Double;
i: Int64;
dataPoint: TDataPoint<TOhlcItem>;
x_center, candleWidth: Single;
y_high, y_low, y_open, y_close: Single;
body: TRectF;
begin
if (DataSeries.Count = 0) or (ADataRect.Width <= 0) or (ADataRect.Height <= 0) then
Exit;
scaleX := ACanvasRect.Width / ADataRect.Width;
scaleY := ACanvasRect.Height / ADataRect.Height;
candleWidth := Max(3.0, (ACanvasRect.Width / DataSeries.Count) * 0.8);
for i := 0 to DataSeries.Count - 1 do
begin
dataPoint := DataSeries[i];
x_center := ACanvasRect.Left + (dataPoint.Time - ADataRect.Left) * scaleX;
y_high := ACanvasRect.Bottom - (dataPoint.Data.High - ADataRect.Top) * scaleY;
y_low := ACanvasRect.Bottom - (dataPoint.Data.Low - ADataRect.Top) * scaleY;
y_open := ACanvasRect.Bottom - (dataPoint.Data.Open - ADataRect.Top) * scaleY;
y_close := ACanvasRect.Bottom - (dataPoint.Data.Close - ADataRect.Top) * scaleY;
ACanvas.Stroke.Color := Self.Color;
ACanvas.Stroke.Thickness := Self.Thickness;
ACanvas.DrawLine(TPointF.Create(x_center, y_high), TPointF.Create(x_center, y_low), 1.0);
if (dataPoint.Data.Close >= dataPoint.Data.Open) then
ACanvas.Fill.Color := FUpColor
else
ACanvas.Fill.Color := FDownColor;
body := TRectF.Create(x_center - candleWidth / 2, y_open, x_center + candleWidth / 2, y_close);
NormalizeRect(body);
ACanvas.FillRect(body, 0, 0, [], 1.0);
end;
end;
{ TChartSeriesCollection }
constructor TChartSeriesCollection.Create(AOwner: TChart);
begin
inherited Create(TChartSeries);
FOwner := AOwner;
end;
function TChartSeriesCollection.GetItem(Index: Integer): TChartSeries;
begin
Result := TChartSeries(inherited GetItem(Index));
end;
function TChartSeriesCollection.GetOwner: TPersistent;
begin
Result := FOwner;
end;
procedure TChartSeriesCollection.SetItem(Index: Integer; const Value: TChartSeries);
begin
inherited SetItem(Index, Value);
end;
procedure TChartSeriesCollection.Update(Item: TCollectionItem);
begin
inherited;
if Assigned(FOwner) then
FOwner.Repaint;
end;
{ TChart }
constructor TChart.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FSeries := TChartSeriesCollection.Create(Self);
FAxisColor := TAlphaColors.Black;
FGridColor := TAlphaColors.Lightgray;
FPadding := 30;
end;
destructor TChart.Destroy;
begin
FSeries.Free;
inherited Destroy;
end;
procedure TChart.Paint;
var
minPt, maxPt: TPointD;
dataRect: TRectD;
canvasRect: TRectF;
series: TCollectionItem;
hasData: Boolean;
begin
inherited;
Canvas.Fill.Color := TAlphaColors.White;
Canvas.FillRect(LocalRect, 1);
if (FSeries.Count = 0) then
Exit;
RecalcDataBounds(minPt, maxPt);
hasData := (minPt.X < maxPt.X) and (minPt.Y < maxPt.Y);
if not hasData then
Exit;
dataRect := TRectD.Create(minPt, maxPt);
canvasRect := Self.LocalRect;
canvasRect.Inflate(-FPadding, -FPadding);
if (canvasRect.Width < 1) or (canvasRect.Height < 1) then
Exit;
DrawGrid(Canvas, dataRect, canvasRect);
DrawAxes(Canvas, dataRect, canvasRect);
for series in FSeries do
begin
(series as TChartSeries).Draw(Canvas, dataRect, canvasRect);
end;
end;
procedure TChart.RecalcDataBounds(out MinPoint, MaxPoint: TPointD);
var
series: TCollectionItem;
begin
MinPoint := TPointD.Create(Infinity, Infinity);
MaxPoint := TPointD.Create(NegInfinity, NegInfinity);
for series in FSeries do
begin
(series as TChartSeries).GetBounds(MinPoint, MaxPoint);
end;
end;
procedure TChart.DrawAxes(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
begin
ACanvas.Stroke.Kind := TBrushKind.Solid;
ACanvas.Stroke.Color := FAxisColor;
ACanvas.Stroke.Thickness := 1;
ACanvas.DrawLine(TPointF.Create(ACanvasRect.Left, ACanvasRect.Bottom), TPointF.Create(ACanvasRect.Right, ACanvasRect.Bottom), 1);
ACanvas.DrawLine(TPointF.Create(ACanvasRect.Left, ACanvasRect.Top), TPointF.Create(ACanvasRect.Left, ACanvasRect.Bottom), 1);
end;
procedure TChart.DrawGrid(const ACanvas: TCanvas; const ADataRect: TRectD; const ACanvasRect: TRectF);
var
i: Integer;
x, y: Single;
const
GridLines = 5;
begin
ACanvas.Stroke.Kind := TBrushKind.Solid;
ACanvas.Stroke.Color := FGridColor;
ACanvas.Stroke.Thickness := 1;
ACanvas.Stroke.Dash := TStrokeDash.Dot;
for i := 1 to GridLines do
begin
x := ACanvasRect.Left + i * ACanvasRect.Width / GridLines;
ACanvas.DrawLine(TPointF.Create(x, ACanvasRect.Top), TPointF.Create(x, ACanvasRect.Bottom), 1);
end;
for i := 0 to GridLines - 1 do
begin
y := ACanvasRect.Bottom - i * ACanvasRect.Height / GridLines;
ACanvas.DrawLine(TPointF.Create(ACanvasRect.Left, y), TPointF.Create(ACanvasRect.Right, y), 1);
end;
ACanvas.Stroke.Dash := TStrokeDash.Solid;
end;
procedure TChart.SetSeries(const Value: TChartSeriesCollection);
begin
FSeries.Assign(Value);
end;
procedure TChart.SetAxisColor(const Value: TAlphaColor);
begin
if (FAxisColor <> Value) then
begin
FAxisColor := Value;
Repaint;
end;
end;
procedure TChart.SetGridColor(const Value: TAlphaColor);
begin
if (FGridColor <> Value) then
begin
FGridColor := Value;
Repaint;
end;
end;
procedure TChart.SetPadding(const Value: Single);
begin
if (FPadding <> Value) then
begin
FPadding := Value;
Repaint;
end;
end;
procedure TChart.Repaint;
begin
inherited;
end;
{ TTestChartForm }
procedure TTestChartForm.FormCreate(Sender: TObject);
var
ohlcSeries: TChartOhlcSeries;
askSeries, bidSeries: TChartAskBidSeries;
i: Integer;
ohlcData: TDataSeries<TOhlcItem>;
askBidData: TDataSeries<TAskBidItem>;
ohlcDataPoints: array of TDataPoint<TOhlcItem>;
askBidDataPoints: array of TDataPoint<TAskBidItem>;
startTime: TDateTime;
lastClose, o, h, l, c: Double;
ohlcItem: TOhlcItem;
const
NUM_POINTS = 50;
begin
FChart := TChart.Create(Self);
FChart.Parent := Self;
FChart.Align := TAlignLayout.Client;
// 1. Create OHLC Data
ohlcData := TDataSeries<TOhlcItem>.CreateWriteable(NUM_POINTS);
askBidData := TDataSeries<TAskBidItem>.CreateWriteable(NUM_POINTS);
startTime := Now;
SetLength(ohlcDataPoints, NUM_POINTS);
SetLength(askBidDataPoints, NUM_POINTS);
lastClose := 100;
for i := 0 to NUM_POINTS - 1 do
begin
o := lastClose + (Random - 0.5) * 2;
h := o + Random * 3;
l := o - Random * 3;
c := l + Random * (h - l);
lastClose := c;
ohlcItem.Open := o;
ohlcItem.High := h;
ohlcItem.Low := l;
ohlcItem.Close := c;
ohlcDataPoints[i] := TDataPoint<TOhlcItem>.Create(startTime + i, ohlcItem);
// Derive Ask/Bid data from OHLC close price
askBidDataPoints[i] :=
TDataPoint<TAskBidItem>.Create(
startTime + i,
TAskBidItem.Create(c, c * 0.995) // Ask = Close, Bid = Close - 0.5% spread
);
end;
ohlcData.Add(ohlcDataPoints);
askBidData.Add(askBidDataPoints);
// 2. Create and add OHLC Series
ohlcSeries := TChartOhlcSeries.Create(FChart.Series);
ohlcSeries.DataSeries := ohlcData;
// 3. Create and add Ask/Bid Line Series
askSeries := TChartAskBidSeries.Create(FChart.Series);
askSeries.DataSeries := askBidData;
askSeries.DataSourceField := dsfAsk;
askSeries.Color := TAlphaColors.Blue;
askSeries.Thickness := 2;
bidSeries := TChartAskBidSeries.Create(FChart.Series);
bidSeries.DataSeries := askBidData;
bidSeries.DataSourceField := dsfBid;
bidSeries.Color := TAlphaColors.Orange;
bidSeries.Thickness := 2;
end;
end.