Chart panels

This commit is contained in:
Michael Schimmel
2025-07-13 16:57:58 +02:00
parent 6e5c0de876
commit 840904e42d
9 changed files with 358 additions and 250 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ uses
FirstStrategy in 'FirstStrategy.pas',
Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas',
Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas',
Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas';
Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas',
Myc.Trade.Types in '..\Src\Myc.Trade.Types.pas';
{$R *.res}
+8 -1
View File
@@ -4,7 +4,7 @@
<ProjectVersion>20.3</ProjectVersion>
<FrameworkType>FMX</FrameworkType>
<Base>True</Base>
<Config Condition="'$(Config)'==''">Release</Config>
<Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win64</Platform>
<ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName>
<TargetedPlatforms>3</TargetedPlatforms>
@@ -142,6 +142,7 @@
<DCCReference Include="..\Src\Myc.Trade.DataArray.pas"/>
<DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Indicators.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Types.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
@@ -185,6 +186,12 @@
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win64\Debug\AuraTrader.rsm" Configuration="Debug" Class="DebugSymbols">
<Platform Name="Win64">
<RemoteName>AuraTrader.rsm</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win64\Release\AuraTrader.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win64">
<RemoteName>AuraTrader.exe</RemoteName>
+1
View File
@@ -8,6 +8,7 @@ uses
Myc.Signals,
Myc.Lazy,
Myc.TaskManager,
Myc.Trade.Types,
Myc.Trade.DataPoint,
Myc.Trade.DataArray;
+10 -13
View File
@@ -27,6 +27,7 @@ uses
FMX.Memo,
FMX.Objects,
Myc.Futures,
Myc.Trade.Types,
Myc.Trade.DataStream,
Myc.Trade.DataPoint,
Myc.Trade.DataProvider,
@@ -183,17 +184,17 @@ begin
var Hull: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateHMA(150));
Closes.Sender.Link(Hull);
chart.AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue);
chart.Panels[0].AddDoubleSeries(Hull.Sender, TAlphaColors.Aliceblue);
// Add SMA (Simple Moving Average)
var Sma: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateSMA(50));
Closes.Sender.Link(Sma);
chart.AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow);
chart.Panels[0].AddDoubleSeries(Sma.Sender, TAlphaColors.Yellow);
// Add EMA (Exponential Moving Average)
var Ema: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateEMA(21));
Closes.Sender.Link(Ema);
chart.AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua);
chart.Panels[0].AddDoubleSeries(Ema.Sender, TAlphaColors.Aqua);
// Add Bollinger Bands (20, 2.0)
var Boli: IMycConverter<Double, TBollingerBandsResult> :=
@@ -204,30 +205,26 @@ begin
TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.UpperBand; end);
Boli.Sender.Link(BoliUpper);
chart.AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray);
chart.Panels[0].AddDoubleSeries(BoliUpper.Sender, TAlphaColors.Gray);
var BoliMiddle: IMycConverter<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.MiddleBand; end);
Boli.Sender.Link(BoliMiddle);
chart.AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0);
chart.Panels[0].AddDoubleSeries(BoliMiddle.Sender, TAlphaColors.Darkgray, 1.0);
var BoliLower: IMycConverter<TBollingerBandsResult, Double> :=
TMycGenericConverter<TBollingerBandsResult, Double>
.Create(function(const Item: TBollingerBandsResult): Double begin Result := Item.LowerBand; end);
Boli.Sender.Link(BoliLower);
chart.AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray);
chart.Panels[0].AddDoubleSeries(BoliLower.Sender, TAlphaColors.Gray);
var rsiChart := TMycChart.Create(Self);
AlignControl(rsiChart);
rsiChart.Height := Layout.ChildrenRect.Width * 9 / 32;
rsiChart.Lookback.Value := 50000;
rsiChart.SetXAxisSeries<TDateTime>(Timestamps.Sender);
chart.AddPanel;
// Add RSI (Relative Strength Index)
var Rsi: IMycConverter<Double, Double> := TGenericIndicator<Double, Double>.Create(TIndicators.CreateRSI(14));
Closes.Sender.Link(Rsi);
rsiChart.AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia);
chart.Panels[1].AddDoubleSeries(Rsi.Sender, TAlphaColors.Fuchsia);
{
// Add MACD (12, 26, 9)
var Macd: IMycConverter<Double, TMacdResult> := TGenericIndicator<Double, TMacdResult>.Create(TIndicators.CreateMACD(12, 26, 9));
@@ -260,7 +257,7 @@ begin
OhlcPoint.Sender.Link(TimeStamps);
chart.SetXAxisSeries<TDateTime>(Timestamps.Sender);
chart.AddOhlcSeries(Ohlc.Sender);
chart.Panels[0].AddOhlcSeries(Ohlc.Sender);
var done := ExecuteStrategy(Symbol, OhlcPoint);
+11 -10
View File
@@ -9,6 +9,7 @@ uses
FMX.Graphics,
Myc.Signals,
Myc.Lazy,
Myc.Trade.Types,
Myc.Trade.DataArray,
Myc.Trade.DataPoint,
Myc.Fmx.Chart;
@@ -66,7 +67,7 @@ type
procedure Update; override;
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); override;
public
constructor Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider<T>);
constructor Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider<T>);
destructor Destroy; override;
end;
@@ -80,7 +81,7 @@ type
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); override;
public
constructor Create(
AOwner: TMycChart;
AParent: TMycChart.TPanel;
const ADataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor, ADownColor: TMutable<TAlphaColor>
);
@@ -96,7 +97,7 @@ type
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); override;
public
constructor Create(
AOwner: TMycChart;
AParent: TMycChart.TPanel;
const ADataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor;
ALineWidth: Single
@@ -176,13 +177,13 @@ end;
{ TChartLineLayer }
constructor TChartLineLayer.Create(
AOwner: TMycChart;
AParent: TMycChart.TPanel;
const ADataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor;
ALineWidth: Single
);
begin
inherited Create(AOwner, ADataProvider);
inherited Create(AParent, ADataProvider);
FLineColor := ALineColor;
FLineWidth := ALineWidth;
end;
@@ -235,12 +236,12 @@ end;
{ TChartOhlcLayer }
constructor TChartOhlcLayer.Create(
AOwner: TMycChart;
AParent: TMycChart.TPanel;
const ADataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor, ADownColor: TMutable<TAlphaColor>
);
begin
inherited Create(AOwner, ADataProvider);
inherited Create(AParent, ADataProvider);
FUpColor := AUpColor;
FDownColor := ADownColor;
@@ -302,10 +303,10 @@ end;
{ TChartCustomLayer }
constructor TChartCustomLayer<T>.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider<T>);
constructor TChartCustomLayer<T>.Create(AParent: TMycChart.TPanel; const ADataProvider: IMycDataProvider<T>);
begin
inherited Create(AOwner);
FSeries := TChartSeriesProcessor<T>.Create(ADataProvider, AOwner.Lookback.AsMutable);
inherited Create(AParent);
FSeries := TChartSeriesProcessor<T>.Create(ADataProvider, AParent.Owner.Lookback.AsMutable);
end;
destructor TChartCustomLayer<T>.Destroy;
+279 -187
View File
@@ -15,6 +15,7 @@ uses
FMX.Controls,
FMX.Graphics,
FMX.Forms,
Myc.Trade.Types,
Myc.Trade.DataPoint,
Myc.Signals,
Myc.Lazy;
@@ -23,6 +24,9 @@ type
TMycChart = class(TStyledControl)
public
type
TPanel = class;
// Abstract base for a data series within the chart.
TSeries = class abstract(TObject)
protected
function GetCount: Int64; virtual; abstract;
@@ -32,25 +36,54 @@ type
property TotalCount: Int64 read GetTotalCount;
end;
// Abstract base for a drawable layer in a panel.
TLayer = class abstract(TObject)
private
FOwner: TMycChart;
FParent: TPanel;
function GetOwner: TMycChart;
protected
function GetSeries: TSeries; virtual; abstract;
procedure Update; virtual; abstract;
function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract;
// The signature is changed to support viewport painting with an index offset
function GetValueRange(First, Last: Int64; out Min, Max: Double): Boolean; virtual; abstract;
procedure Paint(const Canvas: TCanvas; First, Last: Int64; const XForm, YForm: TFunc<Double, Single>); virtual; abstract;
public
constructor Create(AOwner: TMycChart);
constructor Create(AOwner: TPanel);
procedure Repaint;
property Owner: TMycChart read FOwner;
property Owner: TMycChart read GetOwner;
property Series: TSeries read GetSeries;
end;
// A panel is a rectangular area in the chart with its own Y-axis, which can contain multiple layers.
TPanel = class(TObject)
private
FOwner: TMycChart;
FSeriesList: TObjectList<TMycChart.TLayer>;
protected
procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount: Int64; ViewStartIndex, ViewCount: Int64);
procedure UpdateAndCheckChanges(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean);
public
constructor Create(AOwner: TMycChart);
destructor Destroy; override;
function AddOhlcSeries(
const DataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red
): TMycChart.TLayer;
function AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single = 1.5
): TMycChart.TLayer;
property Owner: TMycChart read FOwner;
end;
private
FSeriesList: TList<TLayer>;
FXAxisSeries: TLayer;
FPanelList: TObjectList<TPanel>;
FXAxisSeries: TMycChart.TLayer;
FLookback: TWriteable<Int64>;
FIdleSubscrId: TMessageSubscriptionId;
FViewStartIndex: Int64;
@@ -61,6 +94,8 @@ type
FJumpButtonHot: Boolean;
FJumpButtonPressed: Boolean;
FNeedRepaint: TFlag;
function GetPanel(Index: Integer): TPanel;
function GetPanelCount: Integer;
protected
procedure Paint; override;
procedure DoIdle;
@@ -72,27 +107,16 @@ type
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
// Adds a new, empty panel to the bottom of the chart.
function AddPanel: TPanel;
// Sets the master series that defines the time scale (X-axis). This series is not drawn.
// The chart will not render any data until the X-axis series is set.
function SetXAxisSeries<T>(const DataProvider: IMycDataProvider<T>): TLayer;
// Creates an OHLC candlestick/bar series and returns it.
// The data provider directly supplies OHLC items, as timestamps are managed by the data series internally.
function AddOhlcSeries(
const DataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red
): TLayer;
// Creates a simple line series for double values and returns it
function AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single = 1.5
): TLayer;
function SetXAxisSeries<T>(const DataProvider: IMycDataProvider<T>): TMycChart.TLayer;
property Lookback: TWriteable<Int64> read FLookback write FLookback;
property NeedRepaint: TFlag read FNeedRepaint;
property PanelCount: Integer read GetPanelCount;
property Panels[Index: Integer]: TPanel read GetPanel; default;
end;
implementation
@@ -106,18 +130,21 @@ uses
constructor TMycChart.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FSeriesList := TObjectList<TLayer>.Create(true);
FPanelList := TObjectList<TPanel>.Create(true);
FXAxisSeries := nil;
FLookback := TWriteable<Int64>.CreateWriteable(1000); // Load more data for panning
FLookback := TWriteable<Int64>.CreateWriteable(1000);
FViewStartIndex := 0;
FViewCount := 100; // Show 100 items by default
FViewCount := 100;
FIsDragging := false;
FIdleSubscrId :=
TMessageManager
.DefaultManager
.SubscribeToMessage(TIdleMessage, procedure(const Sender: TObject; const M: TMessage) begin DoIdle; end);
// Create the default panel.
AddPanel;
end;
destructor TMycChart.Destroy;
@@ -125,97 +152,159 @@ begin
TMessageManager.DefaultManager.Unsubscribe(TIdleMessage, FIdleSubscrId);
FXAxisSeries.Free;
FSeriesList.Free;
FPanelList.Free;
inherited;
end;
function TMycChart.SetXAxisSeries<T>(const DataProvider: IMycDataProvider<T>): TLayer;
function TMycChart.AddPanel: TPanel;
begin
Result := TPanel.Create(Self);
FPanelList.Add(Result);
Repaint;
end;
function TMycChart.GetPanel(Index: Integer): TPanel;
begin
Result := FPanelList[Index];
end;
function TMycChart.GetPanelCount: Integer;
begin
Result := FPanelList.Count;
end;
function TMycChart.SetXAxisSeries<T>(const DataProvider: IMycDataProvider<T>): TMycChart.TLayer;
begin
// Free the old one if it exists, and create the new master series
FXAxisSeries.Free;
// Note: TChartCustomLayer needs its owner to be a TPanel. We use the first panel.
var counter := TChartSeriesCounter<T>.Create;
DataProvider.Link(counter);
FXAxisSeries := TChartCustomLayer<Int64>.Create(Self, counter.Sender);
FXAxisSeries := TChartCustomLayer<Int64>.Create(Self.Panels[0], counter.Sender);
Result := FXAxisSeries;
end;
function TMycChart.AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single = 1.5
): TLayer;
begin
Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth);
FSeriesList.Add(Result);
end;
function TMycChart.AddOhlcSeries(
const DataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red
): TLayer;
begin
Result :=
TChartOhlcLayer.Create(Self, DataProvider, TMutable<TAlphaColor>.Constant(AUpColor), TMutable<TAlphaColor>.Constant(ADownColor));
FSeriesList.Add(Result);
end;
procedure TMycChart.DoIdle;
var
prevTotalCount, newTotalCount: Int64;
seriesChanged, seriesRepaint, repaintSignal: Boolean;
countDelta: Int64;
begin
// An X-axis series is required for any operation
if not Assigned(FXAxisSeries) then
exit;
// First update the master series
var prevTotalCount := FXAxisSeries.Series.TotalCount;
prevTotalCount := FXAxisSeries.Series.TotalCount;
FXAxisSeries.Update;
newTotalCount := FXAxisSeries.Series.TotalCount;
// Check if the master series itself has changed
var newTotalCount := FXAxisSeries.Series.TotalCount;
var seriesChanged := prevTotalCount <> newTotalCount;
seriesChanged := prevTotalCount <> newTotalCount;
seriesRepaint := (FViewStartIndex = 0);
// Repaint if the last bar is visible, so we display live data
var seriesRepaint := FViewStartIndex = 0;
for var series in FSeriesList do
for var panel in FPanelList do
begin
var seriesCount := series.Series.TotalCount;
// Check if this series may need a repaint (if it lags behind the master series and these lags are currently visible)
if FViewStartIndex <= prevTotalCount - seriesCount then
seriesRepaint := true;
// Update the series
series.Update;
if seriesCount <> series.Series.TotalCount then
seriesChanged := true;
var panelChanged, panelRepaint: Boolean;
panel.UpdateAndCheckChanges(prevTotalCount, FViewStartIndex, panelChanged, panelRepaint);
seriesChanged := seriesChanged or panelChanged;
seriesRepaint := seriesRepaint or panelRepaint;
end;
// Don't move chart, if the last bar isn't visible
if FViewStartIndex > 0 then
if (FViewStartIndex > 0) then
begin
var countDelta := newTotalCount - prevTotalCount;
if countDelta > 0 then
countDelta := newTotalCount - prevTotalCount;
if (countDelta > 0) then
begin
// Adjust the start index based on the absolute change in total data points
FViewStartIndex := FViewStartIndex + countDelta;
// Clamp to beginning of dataset
if FViewStartIndex + FViewCount > newTotalCount then
if (FViewStartIndex + FViewCount > newTotalCount) then
FViewStartIndex := newTotalCount - FViewCount;
end;
end;
// finally, check the repaint signal
var repaintSignal := false; // FNeedRepaint.Reset;
repaintSignal := FNeedRepaint.Reset;
if repaintSignal or (seriesRepaint and seriesChanged) then
Repaint;
end;
procedure TMycChart.Paint;
var
rect, panelRect: TRectF;
isButtonVisible: Boolean;
buttonColor: TAlphaColor;
path: TPathData;
masterTotalCount: Int64;
panelHeight: Single;
begin
inherited;
rect := Self.LocalRect;
if (not Assigned(FXAxisSeries)) or (FXAxisSeries.Series.Count <= 1) or (FViewCount <= 1) or (PanelCount = 0) then
begin
Canvas.Fill.Color := TAlphaColors.Gray;
Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center);
Exit;
end;
masterTotalCount := FXAxisSeries.Series.TotalCount;
panelHeight := rect.Height / PanelCount;
for var i := 0 to PanelCount - 1 do
begin
var panel := Panels[i];
panelRect := TRectF.Create(rect.Left, rect.Top + (i * panelHeight), rect.Right, rect.Top + ((i + 1) * panelHeight));
// Draw panel content
panel.Paint(Self.Canvas, panelRect, masterTotalCount, FViewStartIndex, FViewCount);
// Draw separator line
if (i > 0) then
begin
Canvas.Stroke.Color := TAlphaColors.Gray;
Canvas.Stroke.Thickness := 1;
Canvas.DrawLine(PointF(panelRect.Left, panelRect.Top), PointF(panelRect.Right, panelRect.Top), 1);
end;
end;
// --- Draw the "jump to latest" button (code unchanged) ---
isButtonVisible := (FViewStartIndex > 0);
if isButtonVisible then
begin
FJumpButtonRect := TRectF.Create(Self.Width - 44, Self.Height - 44, Self.Width - 10, Self.Height - 10);
if FJumpButtonPressed then
buttonColor := $FF707070
else if FJumpButtonHot then
buttonColor := $FF505050
else
buttonColor := $FF303030;
Canvas.Fill.Color := buttonColor;
Canvas.FillRect(FJumpButtonRect, 4, 4, AllCorners, 0.7);
Canvas.Stroke.Color := TAlphaColors.White;
Canvas.Stroke.Thickness := 1.5;
path := TPathData.Create;
try
var cx := FJumpButtonRect.CenterPoint.X;
var cy := FJumpButtonRect.CenterPoint.Y;
path.MoveTo(PointF(cx - 5, cy - 6));
path.LineTo(PointF(cx, cy));
path.LineTo(PointF(cx - 5, cy + 6));
path.MoveTo(PointF(cx + 2, cy - 6));
path.LineTo(PointF(cx + 7, cy));
path.LineTo(PointF(cx + 2, cy + 6));
Canvas.DrawPath(path, 1);
finally
path.Free;
end;
end
else
begin
FJumpButtonRect := TRectF.Empty;
FJumpButtonPressed := false;
end;
end;
procedure TMycChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
begin
// Check for button press first
@@ -382,153 +471,156 @@ begin
Repaint;
end;
procedure TMycChart.Paint;
{ TMycChart.TLayer }
function CalcView(Series: TSeries; MasterCount: Int64; out First: Int64; out Last: Int64): Boolean;
constructor TMycChart.TLayer.Create(AOwner: TPanel);
begin
inherited Create;
FParent := AOwner;
end;
function TMycChart.TLayer.GetOwner: TMycChart;
begin
Result := FParent.Owner;
end;
procedure TMycChart.TLayer.Repaint;
begin
FParent.Owner.Repaint;
end;
{ TMycChart.TPanel }
constructor TMycChart.TPanel.Create(AOwner: TMycChart);
begin
inherited Create;
FOwner := AOwner;
FSeriesList := TObjectList<TMycChart.TLayer>.Create(true);
end;
destructor TMycChart.TPanel.Destroy;
begin
FSeriesList.Free;
inherited Destroy;
end;
function TMycChart.TPanel.AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor;
const ALineWidth: Single
): TMycChart.TLayer;
begin
Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth);
FSeriesList.Add(Result);
end;
function TMycChart.TPanel.AddOhlcSeries(
const DataProvider: IMycDataProvider<TOhlcItem>;
const AUpColor, ADownColor: TAlphaColor
): TMycChart.TLayer;
begin
Result :=
TChartOhlcLayer.Create(Self, DataProvider, TMutable<TAlphaColor>.Constant(AUpColor), TMutable<TAlphaColor>.Constant(ADownColor));
FSeriesList.Add(Result);
end;
procedure TMycChart.TPanel.Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount, ViewStartIndex, ViewCount: Int64);
var
localMin, localMax, seriesMin, seriesMax, padding: Double;
rangeInitialized: Boolean;
seriesFirst, seriesLast: Int64;
xTransform, yTransform: TFunc<Double, Single>;
function CalcView(Series: TMycChart.TSeries; out First, Last: Int64): Boolean;
var
seriesTotalCount, offset, startIdx: Int64;
begin
var seriesTotalCount := series.TotalCount;
var offset := MasterCount - seriesTotalCount;
var startIdx := FViewStartIndex - offset;
seriesTotalCount := series.TotalCount;
offset := MasterTotalCount - seriesTotalCount;
startIdx := ViewStartIndex - offset;
First := Max(0, startIdx);
Last := Min(series.Count, startIdx + FViewCount) - 1;
Result := First <= Last - 1;
Last := Min(series.Count, startIdx + ViewCount) - 1;
Result := First <= Last;
end;
var
rect: TRectF;
globalMin, globalMax, seriesMin, seriesMax, padding: Double;
rangeInitialized: Boolean;
xTransform: TFunc<Double, Single>;
yTransform: TFunc<Double, Single>;
isButtonVisible: Boolean;
buttonColor: TAlphaColor;
path: TPathData;
masterTotalCount, seriesFirst, seriesLast: Int64;
begin
inherited;
rect := Self.LocalRect;
// The chart requires an X-axis series to be set before it can draw anything.
if not Assigned(FXAxisSeries) or (FXAxisSeries.Series.Count <= 1) or (FViewCount <= 1) then
begin
Canvas.Fill.Color := TAlphaColors.Gray;
Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center);
Exit;
end;
rangeInitialized := false;
masterTotalCount := FXAxisSeries.Series.TotalCount;
// 1. Calculate value range (Y-Axis) for this panel only
for var series in FSeriesList do
begin
if CalcView(series.Series, masterTotalCount, seriesFirst, seriesLast) then
if CalcView(series.Series, seriesFirst, seriesLast) then
begin
if series.GetValueRange(seriesFirst, seriesLast, seriesMin, seriesMax) then
begin
if not rangeInitialized then
begin
globalMin := seriesMin;
globalMax := seriesMax;
localMin := seriesMin;
localMax := seriesMax;
rangeInitialized := true;
end
else
begin
globalMin := Min(globalMin, seriesMin);
globalMax := Max(globalMax, seriesMax);
localMin := Min(localMin, seriesMin);
localMax := Max(localMax, seriesMax);
end;
end;
end;
end;
if not rangeInitialized then
Exit;
Exit; // Nothing to draw in this panel
padding := (globalMax - globalMin) * 0.1;
globalMin := globalMin - padding;
globalMax := globalMax + padding;
padding := (localMax - localMin) * 0.1;
localMin := localMin - padding;
localMax := localMax + padding;
if (globalMax - globalMin) = 0 then
exit;
if (localMax - localMin) = 0 then
localMax := localMin + 1; // Avoid division by zero
// Base transform maps an index from the viewport to a screen coordinate
// 2. Create transforms for this panel
yTransform :=
function(value: Double): Single begin Result := rect.Top + (1 - (value - globalMin) / (globalMax - globalMin)) * rect.Height; end;
function(value: Double): Single
begin
Result := Viewport.Top + (1 - (value - localMin) / (localMax - localMin)) * Viewport.Height;
end;
// --- Draw Series with Offset Compensation ---
// The masterTotalCount is already calculated from the range finding loop
// 3. Paint all layers in this panel
for var series in FSeriesList do
begin
if CalcView(series.Series, masterTotalCount, seriesFirst, seriesLast) then
if CalcView(series.Series, seriesFirst, seriesLast) then
begin
var offset := masterTotalCount - series.Series.TotalCount;
var offset := MasterTotalCount - series.Series.TotalCount;
xTransform :=
function(index: Double): Single
begin
Result := rect.Right - (((index + offset - FViewStartIndex) / (FViewCount - 1)) * rect.Width);
Result := Viewport.Right - (((index + offset - ViewStartIndex) / (ViewCount - 1)) * Viewport.Width);
end;
series.Paint(Self.Canvas, seriesFirst, seriesLast, xTransform, yTransform);
series.Paint(Canvas, seriesFirst, seriesLast, xTransform, yTransform);
end;
end;
// --- Draw the "jump to latest" button ---
isButtonVisible := (FViewStartIndex > 0);
if isButtonVisible then
begin
// Define button position and size
FJumpButtonRect := TRectF.Create(Self.Width - 44, Self.Height - 44, Self.Width - 10, Self.Height - 10);
// Determine color based on state
if FJumpButtonPressed then
buttonColor := $FF707070 // Pressed color
else if FJumpButtonHot then
buttonColor := $FF505050 // Hover color
else
buttonColor := $FF303030; // Default color
// Draw button background
Canvas.Fill.Color := buttonColor;
Canvas.FillRect(FJumpButtonRect, 4, 4, AllCorners, 0.7);
// Draw an icon
Canvas.Stroke.Color := TAlphaColors.White;
Canvas.Stroke.Thickness := 1.5;
path := TPathData.Create;
try
var cx := FJumpButtonRect.CenterPoint.X;
var cy := FJumpButtonRect.CenterPoint.Y;
path.MoveTo(PointF(cx - 5, cy - 6));
path.LineTo(PointF(cx, cy));
path.LineTo(PointF(cx - 5, cy + 6));
path.MoveTo(PointF(cx + 2, cy - 6));
path.LineTo(PointF(cx + 7, cy));
path.LineTo(PointF(cx + 2, cy + 6));
Canvas.DrawPath(path, 1);
finally
path.Free;
end;
end
else
begin
// Reset button state when not visible
FJumpButtonRect := TRectF.Empty;
FJumpButtonPressed := false;
end;
end;
constructor TMycChart.TLayer.Create(AOwner: TMycChart);
procedure TMycChart.TPanel.UpdateAndCheckChanges(MasterTotalCount, ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean);
var
seriesCount: Int64;
begin
inherited Create;
FOwner := AOwner;
end;
SeriesChanged := false;
SeriesRepaint := false;
for var series in FSeriesList do
begin
seriesCount := series.Series.TotalCount;
procedure TMycChart.TLayer.Repaint;
begin
FOwner.Repaint;
if (ViewStartIndex <= MasterTotalCount - seriesCount) then
SeriesRepaint := true;
series.Update;
if (seriesCount <> series.Series.TotalCount) then
SeriesChanged := true;
end;
end;
end.
+2 -36
View File
@@ -5,25 +5,10 @@ interface
uses
System.TimeSpan,
Myc.Signals,
Myc.Core.Notifier;
Myc.Core.Notifier,
Myc.Trade.Types;
type
// A data record for an Ask/Bid price pair.
TAskBidItem = packed record
Ask: Double;
Bid: Double;
constructor Create(AAsk, ABid: Double);
end;
TOhlcItem = record
Open: Double;
High: Double;
Low: Double;
Close: Double;
Volume: Double;
constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double);
end;
// Represents a time-stamped data point in a series.
TDataPoint<T> = record
Time: TDateTime;
@@ -295,25 +280,6 @@ begin
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Volume; end);
end;
{ TAskBidItem }
constructor TAskBidItem.Create(AAsk, ABid: Double);
begin
Ask := AAsk;
Bid := ABid;
end;
{ TOhlcItem }
constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Double);
begin
Open := AOpen;
High := AHigh;
Low := ALow;
Close := AClose;
Volume := AVolume;
end;
{ TDataPoint<T> }
constructor TDataPoint<T>.Create(ATime: TDateTime; const AData: T);
+2 -2
View File
@@ -5,8 +5,8 @@ interface
uses
System.SysUtils,
System.Math,
Myc.Trade.DataArray,
Myc.Trade.DataPoint;
Myc.Trade.Types,
Myc.Trade.DataArray;
type
// Result for the Moving Average Convergence Divergence (MACD) indicator.
+43
View File
@@ -0,0 +1,43 @@
unit Myc.Trade.Types;
interface
type
// A data record for an Ask/Bid price pair.
TAskBidItem = packed record
Ask: Double;
Bid: Double;
constructor Create(AAsk, ABid: Double);
end;
TOhlcItem = record
Open: Double;
High: Double;
Low: Double;
Close: Double;
Volume: Double;
constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Double);
end;
implementation
{ TAskBidItem }
constructor TAskBidItem.Create(AAsk, ABid: Double);
begin
Ask := AAsk;
Bid := ABid;
end;
{ TOhlcItem }
constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Double);
begin
Open := AOpen;
High := AHigh;
Low := ALow;
Close := AClose;
Volume := AVolume;
end;
end.