TLazy + Data-Endpoints refactoring

This commit is contained in:
Michael Schimmel
2025-07-17 00:48:46 +02:00
parent abad66ae52
commit b3359a4d73
8 changed files with 271 additions and 250 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<ProjectVersion>20.3</ProjectVersion> <ProjectVersion>20.3</ProjectVersion>
<FrameworkType>FMX</FrameworkType> <FrameworkType>FMX</FrameworkType>
<Base>True</Base> <Base>True</Base>
<Config Condition="'$(Config)'==''">Release</Config> <Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win64</Platform> <Platform Condition="'$(Platform)'==''">Win64</Platform>
<ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName> <ProjectName Condition="'$(ProjectName)'==''">AuraTrader</ProjectName>
<TargetedPlatforms>3</TargetedPlatforms> <TargetedPlatforms>3</TargetedPlatforms>
+17 -8
View File
@@ -483,13 +483,18 @@ begin
var Highest: Double := Double.MinValue; var Highest: Double := Double.MinValue;
var ATR := Ohlc[0].Chain<Double>(TIndicators.CreateATR(50)).MakeParallel; var ATR := Ohlc[0].Chain<Double>(TIndicators.CreateATR(50)).MakeParallel;
var ATRSeries := TConverter.CreateEndpoint<Double>(ATR.Sender, 5);
var HullSeries := TConverter.CreateEndpoint<Double>(Hull.Sender, 5);
var SmaSeries := TConverter.CreateEndpoint<Double>(Sma.Sender, 5);
// next stage // next stage
var ATREndPoint := TConverter.CreateEndpoint<Double>(ATR.Sender, 5);
var ATRSeries: TSeries<Double>;
var HullEndPoint := TConverter.CreateEndpoint<Double>(Hull.Sender, 5);
var HullSeries: TSeries<Double>;
var SmaEndPoint := TConverter.CreateEndpoint<Double>(Sma.Sender, 5);
var SmaSeries: TSeries<Double>;
var curr: TSignal; var curr: TSignal;
curr.SL := Double.NaN; curr.SL := Double.NaN;
curr.Entry := Double.NaN; curr.Entry := Double.NaN;
@@ -508,7 +513,11 @@ begin
Result.Sig := 0; Result.Sig := 0;
var pnl: double := NaN; var pnl: double := NaN;
if (HullSeries.Value[0] < SmaSeries.Value[0]) and (HullSeries.Value[1] >= SmaSeries.Value[1]) then ATREndPoint.Update(ATRSeries);
HullEndPoint.Update(HullSeries);
SmaEndPoint.Update(SmaSeries);
if (HullSeries[0] < SmaSeries[0]) and (HullSeries[1] >= SmaSeries[1]) then
begin begin
if curr.Sig > 0 then if curr.Sig > 0 then
pnl := Ohlc.Close - curr.Entry; pnl := Ohlc.Close - curr.Entry;
@@ -518,7 +527,7 @@ begin
curr.Entry := Ohlc.Close; curr.Entry := Ohlc.Close;
Result := curr; Result := curr;
end end
else if (HullSeries.Value[0] > SmaSeries.Value[0]) and (HullSeries.Value[1] <= SmaSeries.Value[1]) then else if (HullSeries[0] > SmaSeries[0]) and (HullSeries[1] <= SmaSeries[1]) then
begin begin
if curr.Sig < 0 then if curr.Sig < 0 then
pnl := curr.Entry - Ohlc.Close; pnl := curr.Entry - Ohlc.Close;
@@ -529,7 +538,7 @@ begin
Result := curr; Result := curr;
end; end;
var atr := 15 * ATRSeries.Value[0]; var atr := 15 * ATRSeries[0];
if curr.Sig > 0 then if curr.Sig > 0 then
begin begin
if Ohlc.Close > curr.SL then if Ohlc.Close > curr.SL then
@@ -600,8 +609,8 @@ begin
panel.AddOhlcSeries(Ohlc[0].Sender); panel.AddOhlcSeries(Ohlc[0].Sender);
panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Cornflowerblue, 2); panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Cornflowerblue, 2);
panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Brown, 1.5); panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Brown, 1.5);
panel.AddDoubleSeries(Signal.Field<Double>('SL').Sender, TAlphaColors.Red, 2);
panel.AddDoubleSeries(Signal.Field<Double>('Entry').Sender, TAlphaColors.Green, 1); panel.AddDoubleSeries(Signal.Field<Double>('Entry').Sender, TAlphaColors.Green, 1);
panel.AddDoubleSeries(Signal.Field<Double>('SL').Sender, TAlphaColors.Red, 2);
var pnlChart := TMycChart.Create(Self); var pnlChart := TMycChart.Create(Self);
AlignControl(pnlChart); AlignControl(pnlChart);
+32 -5
View File
@@ -10,7 +10,7 @@ uses
Myc.Mutable; Myc.Mutable;
type type
TMycNullMutable<T> = class(TInterfacedObject, TMutable<T>.IMutable) TMycConstMutable<T> = class(TInterfacedObject, TMutable<T>.IMutable)
private private
FValue: T; FValue: T;
function GetChanged: TSignal; function GetChanged: TSignal;
@@ -64,25 +64,34 @@ type
procedure SetValue(const Value: T); procedure SetValue(const Value: T);
end; end;
TMycConstLazy<T> = class(TInterfacedObject, TLazy<T>.ILazy)
private
FValue: T;
function GetChanged: TState;
function Update(var Value: T): Boolean;
public
constructor Create(AValue: T);
end;
implementation implementation
uses uses
System.Generics.Defaults; System.Generics.Defaults;
constructor TMycNullMutable<T>.Create(AValue: T); constructor TMycConstMutable<T>.Create(AValue: T);
begin begin
inherited Create; inherited Create;
FValue := AValue; FValue := AValue;
end; end;
{ TMycNullMutable<T> } { TMycConstMutable<T> }
function TMycNullMutable<T>.GetChanged: TSignal; function TMycConstMutable<T>.GetChanged: TSignal;
begin begin
Result := TSignal.Null; Result := TSignal.Null;
end; end;
function TMycNullMutable<T>.GetValue: T; function TMycConstMutable<T>.GetValue: T;
begin begin
Result := FValue; Result := FValue;
end; end;
@@ -179,4 +188,22 @@ begin
end; end;
end; end;
constructor TMycConstLazy<T>.Create(AValue: T);
begin
inherited Create;
FValue := AValue;
end;
{ TMycConstLazy<T> }
function TMycConstLazy<T>.GetChanged: TState;
begin
Result := TState.Null;
end;
function TMycConstLazy<T>.Update(var Value: T): Boolean;
begin
Result := false;
end;
end. end.
+56 -92
View File
@@ -15,32 +15,17 @@ uses
Myc.Fmx.Chart; Myc.Fmx.Chart;
type type
TChartSeriesProcessor<T> = class(TMycChart.TSeries)
strict private
FDataSeries: TSeries<T>;
private
FData: TSeries<T>;
FDataProvider: TDataProvider<T>;
FReceiver: TMutable<TSeries<T>>;
protected
function GetCount: Int64; override;
function GetTotalCount: Int64; override;
procedure Update;
public
constructor Create(const ADataProvider: TDataProvider<T>; const ALookback: TMutable<Int64>);
destructor Destroy; override;
property Data: TSeries<T> read FData;
end;
{ TChartCustomLayer } { TChartCustomLayer }
TChartCustomLayer<T> = class abstract(TMycChart.TDataLayer) TChartCustomLayer<T> = class abstract(TMycChart.TDataLayer)
private private
FSeries: TChartSeriesProcessor<T>; FDataProvider: TDataProvider<T>;
FReceiver: TLazy<TSeries<T>>;
FData: TSeries<T>;
protected protected
function GetSeries: TMycChart.TSeries; override; function GetCount: Int64; override;
function GetTotalCount: Int64; override;
function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; abstract; function GetValueRange(First, Last: Int64; out MinValue, MaxValue: Double): Boolean; override; abstract;
procedure Update; override; function Update: Boolean; override;
procedure Paint( procedure Paint(
const Canvas: TCanvas; const Canvas: TCanvas;
First, Last: Int64; First, Last: Int64;
@@ -49,7 +34,7 @@ type
); override; abstract; ); override; abstract;
public public
constructor Create(AParent: TMycChart.TPanel; const ADataProvider: TDataProvider<T>); constructor Create(AParent: TMycChart.TPanel; const ADataProvider: TDataProvider<T>);
destructor Destroy; override; property Data: TSeries<T> read FData;
end; end;
{ TChartOhlcLayer } { TChartOhlcLayer }
@@ -98,14 +83,17 @@ type
{ TChartXAxisLayer } { TChartXAxisLayer }
TChartXAxisLayer<T> = class(TMycChart.TXAxisLayer) TChartXAxisLayer<T> = class(TMycChart.TXAxisLayer)
private private
FSeries: TChartSeriesProcessor<T>; FDataProvider: TDataProvider<T>;
FReceiver: TLazy<TSeries<T>>;
FData: TSeries<T>;
protected protected
function GetSeries: TMycChart.TSeries; override; function Update: Boolean; override;
procedure Update; override;
function GetCaption(Idx: Int64): String; override; function GetCaption(Idx: Int64): String; override;
function GetCount: Int64; override;
function GetTotalCount: Int64; override;
public public
constructor Create(AOwner: TMycChart; const ADataProvider: TDataProvider<T>); constructor Create(AOwner: TMycChart; const ADataProvider: TDataProvider<T>);
destructor Destroy; override; property Data: TSeries<T> read FData;
end; end;
{ TChartXAxisTimestampLayer } { TChartXAxisTimestampLayer }
@@ -126,37 +114,6 @@ uses
System.Math, System.Math,
FMX.Types; FMX.Types;
{ TChartSeriesProcessor<T> }
constructor TChartSeriesProcessor<T>.Create(const ADataProvider: TDataProvider<T>; const ALookback: TMutable<Int64>);
begin
inherited Create;
FDataProvider := ADataProvider;
FData := FDataSeries;
FReceiver := TConverter.CreateEndpoint<T>(FDataProvider, ALookback.Value);
end;
destructor TChartSeriesProcessor<T>.Destroy;
begin
inherited;
end;
function TChartSeriesProcessor<T>.GetCount: Int64;
begin
Result := FData.Count;
end;
function TChartSeriesProcessor<T>.GetTotalCount: Int64;
begin
Result := FData.TotalCount;
end;
procedure TChartSeriesProcessor<T>.Update;
begin
FData := FReceiver.Value;
end;
{ TChartLineLayer } { TChartLineLayer }
constructor TChartLineLayer.Create( constructor TChartLineLayer.Create(
@@ -179,14 +136,16 @@ begin
MaxValue := -MaxDouble; MaxValue := -MaxDouble;
for i := First to Last do for i := First to Last do
begin begin
var v := FSeries.Data.Items[i]; var v := Data.Items[i];
if not IsNaN(v) then if not IsNaN(v) then
begin begin
MinValue := Min(MinValue, v); if v < MinValue then
MaxValue := Max(MaxValue, v); MinValue := v;
if v > MaxValue then
MaxValue := v;
end; end;
end; end;
Result := (MinValue < MaxValue); Result := (MinValue <= MaxValue);
end; end;
procedure TChartLineLayer.Paint( procedure TChartLineLayer.Paint(
@@ -205,18 +164,18 @@ begin
while n < Last do while n < Last do
begin begin
// Skip warmup/gap data // Skip warmup/gap data
while (n <= Last) and (IsNaN(FSeries.Data[n])) do while (n <= Last) and (IsNaN(Data[n])) do
inc(n); inc(n);
if n < Last then if n < Last then
begin begin
points.MoveTo(TPointF.Create(XForm(n), YForm(FSeries.Data[n]))); points.MoveTo(TPointF.Create(XForm(n), YForm(Data[n])));
inc(n); inc(n);
while n <= Last do while n <= Last do
begin begin
if IsNaN(FSeries.Data[n]) then if IsNaN(Data[n]) then
break; break;
points.LineTo(TPointF.Create(XForm(n), YForm(FSeries.Data[n]))); points.LineTo(TPointF.Create(XForm(n), YForm(Data[n])));
inc(n); inc(n);
end; end;
end; end;
@@ -256,11 +215,13 @@ begin
for i := First to Last do for i := First to Last do
begin begin
var item := FSeries.Data.Items[i]; var item := Data.Items[i];
MinValue := Min(MinValue, item.Low); if item.Low < MinValue then
MaxValue := Max(MaxValue, item.High); MinValue := item.Low;
if item.High > MaxValue then
MaxValue := item.High;
end; end;
Result := (MinValue <> MaxDouble); Result := MinValue <= MaxDouble;
end; end;
procedure TChartOhlcLayer.Paint( procedure TChartOhlcLayer.Paint(
@@ -280,7 +241,7 @@ begin
for i := First to Last do for i := First to Last do
begin begin
item := FSeries.Data.Items[i]; item := Data.Items[i];
x := XForm(i); x := XForm(i);
yOpen := YForm(item.Open); yOpen := YForm(item.Open);
yClose := YForm(item.Close); yClose := YForm(item.Close);
@@ -309,23 +270,24 @@ end;
constructor TChartCustomLayer<T>.Create(AParent: TMycChart.TPanel; const ADataProvider: TDataProvider<T>); constructor TChartCustomLayer<T>.Create(AParent: TMycChart.TPanel; const ADataProvider: TDataProvider<T>);
begin begin
inherited Create(AParent); inherited Create(AParent);
FSeries := TChartSeriesProcessor<T>.Create(ADataProvider, AParent.Owner.Lookback.AsMutable); FDataProvider := ADataProvider;
FReceiver := TConverter.CreateEndpoint<T>(FDataProvider, Owner.Lookback.Value);
end; end;
destructor TChartCustomLayer<T>.Destroy; function TChartCustomLayer<T>.GetCount: Int64;
begin begin
FSeries.Free; Result := FData.Count;
inherited;
end; end;
function TChartCustomLayer<T>.GetSeries: TMycChart.TSeries; function TChartCustomLayer<T>.GetTotalCount: Int64;
begin begin
Result := FSeries; Result := FData.TotalCount;
end; end;
procedure TChartCustomLayer<T>.Update; function TChartCustomLayer<T>.Update: Boolean;
begin begin
FSeries.Update; Result := FReceiver.Update(FData);
end; end;
{ TChartXAxisLayer } { TChartXAxisLayer }
@@ -333,30 +295,32 @@ end;
constructor TChartXAxisLayer<T>.Create(AOwner: TMycChart; const ADataProvider: TDataProvider<T>); constructor TChartXAxisLayer<T>.Create(AOwner: TMycChart; const ADataProvider: TDataProvider<T>);
begin begin
inherited Create(AOwner); inherited Create(AOwner);
FSeries := TChartSeriesProcessor<T>.Create(ADataProvider, AOwner.Lookback.AsMutable); FDataProvider := ADataProvider;
end; FReceiver := TConverter.CreateEndpoint<T>(FDataProvider, Owner.Lookback.Value);
destructor TChartXAxisLayer<T>.Destroy;
begin
FSeries.Free;
inherited;
end; end;
function TChartXAxisLayer<T>.GetCaption(Idx: Int64): String; function TChartXAxisLayer<T>.GetCaption(Idx: Int64): String;
begin begin
Result := IntToStr(Idx); Result := IntToStr(Owner.ViewStartIndex + Idx);
end; end;
function TChartXAxisLayer<T>.GetSeries: TMycChart.TSeries; function TChartXAxisLayer<T>.GetCount: Int64;
begin begin
Result := FSeries; Result := FData.Count;
end; end;
procedure TChartXAxisLayer<T>.Update; function TChartXAxisLayer<T>.GetTotalCount: Int64;
begin begin
FSeries.Update; Result := FData.TotalCount;
end; end;
function TChartXAxisLayer<T>.Update: Boolean;
begin
Result := FReceiver.Update(FData);
end;
{ TChartXAxisTimestampLayer }
constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: TDataProvider<TDateTime>); constructor TChartXAxisTimestampLayer.Create(AOwner: TMycChart; ATimeframe: TTimeframe; const ADataProvider: TDataProvider<TDateTime>);
begin begin
inherited Create(AOwner, ADataProvider); inherited Create(AOwner, ADataProvider);
@@ -369,9 +333,9 @@ var
formatStr: string; formatStr: string;
begin begin
Result := ''; Result := '';
if (Idx >= 0) and (Idx < FSeries.Count) then if (Idx >= 0) and (Idx < Data.Count) then
begin begin
dt := FSeries.Data[Idx]; dt := Data[Idx];
// Choose format based on the series timeframe. // Choose format based on the series timeframe.
if FTimeframe >= D then // Daily, Weekly, Monthly, Yearly if FTimeframe >= D then // Daily, Weekly, Monthly, Yearly
+59 -70
View File
@@ -26,26 +26,18 @@ type
type type
TPanel = class; TPanel = class;
// Abstract base for a data series within the chart.
TSeries = class abstract(TObject)
protected
function GetCount: Int64; virtual; abstract;
function GetTotalCount: Int64; virtual; abstract;
public
property Count: Int64 read GetCount;
property TotalCount: Int64 read GetTotalCount;
end;
// Abstract base for a drawable layer in a panel. // Abstract base for a drawable layer in a panel.
TLayer = class abstract(TObject) TLayer = class abstract(TObject)
protected protected
function GetCount: Int64; virtual; abstract;
function GetTotalCount: Int64; virtual; abstract;
function GetOwner: TMycChart; virtual; abstract; function GetOwner: TMycChart; virtual; abstract;
function GetSeries: TSeries; virtual; abstract; function Update: Boolean; virtual; abstract;
procedure Update; virtual; abstract;
public public
procedure Repaint; procedure Repaint;
property Count: Int64 read GetCount;
property TotalCount: Int64 read GetTotalCount;
property Owner: TMycChart read GetOwner; property Owner: TMycChart read GetOwner;
property Series: TSeries read GetSeries;
end; end;
TDataLayer = class abstract(TLayer) TDataLayer = class abstract(TLayer)
@@ -93,12 +85,12 @@ type
TPanel = class(TObject) TPanel = class(TObject)
private private
FOwner: TMycChart; FOwner: TMycChart;
FSeriesList: TObjectList<TMycChart.TDataLayer>; FLayers: TObjectList<TMycChart.TDataLayer>;
FWeight: Single; FWeight: Single;
procedure SetWeight(const Value: Single); procedure SetWeight(const Value: Single);
protected protected
procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount: Int64; ViewStartIndex, ViewCount: Int64); procedure Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount: Int64; ViewStartIndex, ViewCount: Int64);
procedure Update(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean); function Update: Boolean;
public public
constructor Create(AOwner: TMycChart; AWeight: Single); constructor Create(AOwner: TMycChart; AWeight: Single);
@@ -144,6 +136,7 @@ type
protected protected
procedure Paint; override; procedure Paint; override;
procedure DoIdle; procedure DoIdle;
function Update: Boolean;
procedure DoMouseLeave; override; procedure DoMouseLeave; override;
procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override; procedure MouseWheel(Shift: TShiftState; WheelDelta: Integer; var Handled: Boolean); override;
procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override; procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
@@ -168,6 +161,7 @@ type
property JumpButtonRect: TRectF read FJumpButtonRect; property JumpButtonRect: TRectF read FJumpButtonRect;
property ViewCount: Int64 read FViewCount; property ViewCount: Int64 read FViewCount;
property ViewStartIndex: Int64 read FViewStartIndex; property ViewStartIndex: Int64 read FViewStartIndex;
property XAxisSeries: TMycChart.TXAxisLayer read FXAxisSeries;
end; end;
implementation implementation
@@ -230,7 +224,7 @@ begin
Result.Point.X := X; Result.Point.X := X;
Result.Point.Y := Y; Result.Point.Y := Y;
Result.Idx := Round((rect.Right - X) * (FViewCount - 1) / rect.Width); Result.Idx := Round((rect.Right - X) * (FViewCount - 1) / rect.Width);
if (Result.Idx < 0) or (Result.Idx >= FXAxisSeries.Series.Count) then if (Result.Idx < 0) or (Result.Idx >= FXAxisSeries.Count) then
Result.Idx := -1; Result.Idx := -1;
Result.BarX := NaN; Result.BarX := NaN;
if Result.Idx >= 0 then if Result.Idx >= 0 then
@@ -248,34 +242,34 @@ begin
end; end;
procedure TMycChart.DoIdle; procedure TMycChart.DoIdle;
var begin
prevTotalCount, newTotalCount: Int64; if Update then
seriesChanged, seriesRepaint, repaintSignal: Boolean; Repaint;
countDelta: Int64; end;
function TMycChart.Update: Boolean;
begin begin
if not Assigned(FXAxisSeries) then if not Assigned(FXAxisSeries) then
exit; exit(false);
prevTotalCount := FXAxisSeries.Series.TotalCount; var prevTotalCount := FXAxisSeries.TotalCount;
FXAxisSeries.Update; var seriesChanged := false;
newTotalCount := FXAxisSeries.Series.TotalCount;
seriesChanged := prevTotalCount <> newTotalCount; if FXAxisSeries.Update then
seriesRepaint := (FViewStartIndex = 0); seriesChanged := (FViewStartIndex = 0);
for var panel in FPanelList do for var panel in FPanelList do
begin begin
var panelChanged, panelRepaint: Boolean; if panel.Update then
panel.Update(prevTotalCount, FViewStartIndex, panelChanged, panelRepaint); seriesChanged := true;
seriesChanged := seriesChanged or panelChanged;
seriesRepaint := seriesRepaint or panelRepaint;
end; end;
// When we are not displaying live data, adjust start index accordingly // When we are not displaying live data, adjust start index accordingly
if (FViewStartIndex > 0) then if FViewStartIndex > 0 then
begin begin
countDelta := newTotalCount - prevTotalCount; var newTotalCount := FXAxisSeries.TotalCount;
if (countDelta > 0) then var countDelta := newTotalCount - prevTotalCount;
if countDelta > 0 then
begin begin
FViewStartIndex := FViewStartIndex + countDelta; FViewStartIndex := FViewStartIndex + countDelta;
if (FViewStartIndex + FViewCount > newTotalCount) then if (FViewStartIndex + FViewCount > newTotalCount) then
@@ -283,10 +277,9 @@ begin
end; end;
end; end;
repaintSignal := FNeedRepaint.Reset; var repaintSignal := FNeedRepaint.Reset;
if repaintSignal or (seriesRepaint and seriesChanged) then Result := repaintSignal or seriesChanged;
Repaint;
end; end;
procedure TMycChart.DoMouseLeave; procedure TMycChart.DoMouseLeave;
@@ -312,14 +305,14 @@ begin
inherited; inherited;
rect := Self.LocalRect; rect := Self.LocalRect;
if (not Assigned(FXAxisSeries)) or (FXAxisSeries.Series.Count <= 1) or (FViewCount <= 1) or (PanelCount = 0) then if (not Assigned(FXAxisSeries)) or (FXAxisSeries.Count <= 1) or (FViewCount <= 1) or (PanelCount = 0) then
begin begin
Canvas.Fill.Color := TAlphaColors.Gray; Canvas.Fill.Color := TAlphaColors.Gray;
Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center); Canvas.FillText(rect, 'No Data', false, 1, [], TTextAlign.Center, TTextAlign.Center);
Exit; Exit;
end; end;
masterTotalCount := FXAxisSeries.Series.TotalCount; masterTotalCount := FXAxisSeries.TotalCount;
// Calculate total weight for proportional panel height // Calculate total weight for proportional panel height
totalWeight := 0; totalWeight := 0;
@@ -332,6 +325,7 @@ begin
for var i := 0 to PanelCount - 1 do for var i := 0 to PanelCount - 1 do
begin begin
var panel := Panels[i]; var panel := Panels[i];
panelHeight := panel.Weight * rect.Height / totalWeight; panelHeight := panel.Weight * rect.Height / totalWeight;
panelRect := TRectF.Create(rect.Left, currentY, rect.Right, currentY + panelHeight); panelRect := TRectF.Create(rect.Left, currentY, rect.Right, currentY + panelHeight);
@@ -542,7 +536,7 @@ begin
FViewStartIndex := FViewStartIndex - indexDelta; FViewStartIndex := FViewStartIndex - indexDelta;
// Clamp values // Clamp values
maxIndex := FXAxisSeries.Series.Count - FViewCount; maxIndex := FXAxisSeries.Count - FViewCount;
if (maxIndex < 0) then if (maxIndex < 0) then
maxIndex := 0; maxIndex := 0;
if (FViewStartIndex < 0) then if (FViewStartIndex < 0) then
@@ -602,7 +596,7 @@ begin
if not Assigned(FXAxisSeries) then if not Assigned(FXAxisSeries) then
exit; exit;
var xAxisCount := FXAxisSeries.Series.Count; var xAxisCount := FXAxisSeries.Count;
if (xAxisCount = 0) or (Width <= 0) then if (xAxisCount = 0) or (Width <= 0) then
exit; exit;
@@ -667,13 +661,13 @@ constructor TMycChart.TPanel.Create(AOwner: TMycChart; AWeight: Single);
begin begin
inherited Create; inherited Create;
FOwner := AOwner; FOwner := AOwner;
FSeriesList := TObjectList<TMycChart.TDataLayer>.Create(true); FLayers := TObjectList<TMycChart.TDataLayer>.Create(true);
FWeight := AWeight; FWeight := AWeight;
end; end;
destructor TMycChart.TPanel.Destroy; destructor TMycChart.TPanel.Destroy;
begin begin
FSeriesList.Free; FLayers.Free;
inherited Destroy; inherited Destroy;
end; end;
@@ -684,7 +678,7 @@ function TMycChart.TPanel.AddDoubleSeries(
): TMycChart.TDataLayer; ): TMycChart.TDataLayer;
begin begin
Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth); Result := TChartLineLayer.Create(Self, DataProvider, ALineColor, ALineWidth);
FSeriesList.Add(Result); FLayers.Add(Result);
end; end;
function TMycChart.TPanel.AddOhlcSeries( function TMycChart.TPanel.AddOhlcSeries(
@@ -695,7 +689,7 @@ function TMycChart.TPanel.AddOhlcSeries(
begin begin
Result := Result :=
TChartOhlcLayer.Create(Self, DataProvider, TMutable<TAlphaColor>.Constant(AUpColor), TMutable<TAlphaColor>.Constant(ADownColor)); TChartOhlcLayer.Create(Self, DataProvider, TMutable<TAlphaColor>.Constant(AUpColor), TMutable<TAlphaColor>.Constant(ADownColor));
FSeriesList.Add(Result); FLayers.Add(Result);
end; end;
procedure TMycChart.TPanel.Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount, ViewStartIndex, ViewCount: Int64); procedure TMycChart.TPanel.Paint(const Canvas: TCanvas; const Viewport: TRectF; MasterTotalCount, ViewStartIndex, ViewCount: Int64);
@@ -706,16 +700,16 @@ var
xTransform: TFunc<Int64, Single>; xTransform: TFunc<Int64, Single>;
yTransform: TFunc<Double, Single>; yTransform: TFunc<Double, Single>;
function CalcView(Series: TMycChart.TSeries; out First, Last: Int64): Boolean; function CalcView(Layer: TMycChart.TDataLayer; out First, Last: Int64): Boolean;
var var
seriesTotalCount, offset, startIdx: Int64; seriesTotalCount, offset, startIdx: Int64;
begin begin
seriesTotalCount := series.TotalCount; seriesTotalCount := Layer.TotalCount;
offset := MasterTotalCount - seriesTotalCount; offset := MasterTotalCount - seriesTotalCount;
startIdx := ViewStartIndex - offset; startIdx := ViewStartIndex - offset;
First := Max(0, startIdx); First := Max(0, startIdx);
Last := Min(series.Count, startIdx + ViewCount) - 1; Last := Min(Layer.Count, startIdx + ViewCount) - 1;
Result := First <= Last; Result := First <= Last;
end; end;
@@ -724,12 +718,16 @@ begin
localMin := 0; localMin := 0;
localMax := 0; localMax := 0;
// Update data state
for var layer in FLayers do
layer.Update;
// 1. Calculate value range (Y-Axis) for this panel only // 1. Calculate value range (Y-Axis) for this panel only
for var series in FSeriesList do for var layer in FLayers do
begin begin
if CalcView(series.Series, seriesFirst, seriesLast) then if CalcView(layer, seriesFirst, seriesLast) then
begin begin
if series.GetValueRange(seriesFirst, seriesLast, seriesMin, seriesMax) then if layer.GetValueRange(seriesFirst, seriesLast, seriesMin, seriesMax) then
begin begin
if not rangeInitialized then if not rangeInitialized then
begin begin
@@ -760,18 +758,18 @@ begin
yTransform := function(value: Double): Single begin Result := top + (1 - (value - localMin) / range) * height; end; yTransform := function(value: Double): Single begin Result := top + (1 - (value - localMin) / range) * height; end;
// 3. Paint all layers in this panel // 3. Paint all layers in this panel
for var series in FSeriesList do for var layer in FLayers do
begin begin
if CalcView(series.Series, seriesFirst, seriesLast) then if CalcView(layer, seriesFirst, seriesLast) then
begin begin
var offset := MasterTotalCount - series.Series.TotalCount; var offset := MasterTotalCount - layer.TotalCount;
xTransform := xTransform :=
function(index: Int64): Single function(index: Int64): Single
begin begin
Result := Viewport.Right - (((index + offset - ViewStartIndex) / (ViewCount - 1)) * Viewport.Width); Result := Viewport.Right - (((index + offset - ViewStartIndex) / (ViewCount - 1)) * Viewport.Width);
end; end;
series.Paint(Canvas, seriesFirst, seriesLast, xTransform, yTransform); layer.Paint(Canvas, seriesFirst, seriesLast, xTransform, yTransform);
end; end;
end; end;
end; end;
@@ -789,23 +787,14 @@ begin
end; end;
end; end;
procedure TMycChart.TPanel.Update(MasterTotalCount: Int64; ViewStartIndex: Int64; out SeriesChanged, SeriesRepaint: Boolean); function TMycChart.TPanel.Update: Boolean;
var
seriesCount: Int64;
begin begin
SeriesChanged := false; Result := false;
SeriesRepaint := false; for var layer in FLayers do
for var series in FSeriesList do
begin begin
seriesCount := series.Series.TotalCount; if layer.Update then
if (Owner.ViewStartIndex <= Owner.FXAxisSeries.TotalCount - layer.TotalCount) then
if (ViewStartIndex <= MasterTotalCount - seriesCount) then Result := true;
SeriesRepaint := true;
series.Update;
if (seriesCount <> series.Series.TotalCount) then
SeriesChanged := true;
end; end;
end; end;
@@ -846,7 +835,7 @@ var
textSize: TRectF; textSize: TRectF;
begin begin
// Do not draw if no data or view is invalid // Do not draw if no data or view is invalid
if (not Assigned(Series)) or (Series.Count = 0) then if GetCount = 0 then
exit; exit;
// 1. Snap X to the candle's center // 1. Snap X to the candle's center
+63 -19
View File
@@ -73,15 +73,38 @@ type
end; end;
TLazy<T> = record TLazy<T> = record
type
ILazy = interface
{$REGION 'property access'}
function GetChanged: TState;
{$ENDREGION}
function Update(var Value: T): Boolean;
property Changed: TState read GetChanged;
end;
{$REGION 'private'}
strict private
class var
FNull: ILazy;
class constructor CreateClass;
private private
FMutable: TMutable<T>; FLazy: ILazy;
FChanged: TFlag; function GetChanged: TState; inline;
FChangeState: TSignal.TSubscription; {$ENDREGION}
function GetValue: T; inline;
public public
constructor Create(const AMutable: TMutable<T>); constructor Create(const ALazy: ILazy);
function Update: Boolean; class operator Initialize(out Dest: TLazy<T>);
property Value: T read GetValue; class operator Implicit(const A: ILazy): TLazy<T>; overload;
class operator Implicit(const A: TLazy<T>): ILazy; overload;
class property Null: ILazy read FNull;
class function Constant(const Value: T): TLazy<T>; overload; static;
function Update(var Value: T): Boolean; inline;
property Changed: TState read GetChanged;
end; end;
implementation implementation
@@ -99,12 +122,12 @@ end;
class constructor TMutable<T>.CreateClass; class constructor TMutable<T>.CreateClass;
begin begin
FNull := TMycNullMutable<T>.Create(Default(T)); FNull := TMycConstMutable<T>.Create(Default(T));
end; end;
class function TMutable<T>.Constant(const Value: T): TMutable<T>; class function TMutable<T>.Constant(const Value: T): TMutable<T>;
begin begin
Result := TMycNullMutable<T>.Create(Value); Result := TMycConstMutable<T>.Create(Value);
end; end;
class function TMutable<T>.Construct(const Changing: TSignal; const Proc: TFunc<T>): TMutable<T>; class function TMutable<T>.Construct(const Changing: TSignal; const Proc: TFunc<T>): TMutable<T>;
@@ -195,23 +218,44 @@ end;
{ TLazy<T> } { TLazy<T> }
constructor TLazy<T>.Create(const AMutable: TMutable<T>); constructor TLazy<T>.Create(const ALazy: ILazy);
begin begin
FMutable := AMutable; FLazy := ALazy;
FChanged := TFlag.CreateFlag;
FChangeState := AMutable.Changed.Subscribe(FChanged);
end; end;
function TLazy<T>.GetValue: T; class constructor TLazy<T>.CreateClass;
begin begin
Result := FMutable.Value; FNull := TMycConstLazy<T>.Create(Default(T));
end; end;
function TLazy<T>.Update: Boolean; class function TLazy<T>.Constant(const Value: T): TLazy<T>;
begin begin
Result := FChanged.State.IsSet; Result := TMycConstLazy<T>.Create(Value);
if Result then end;
FChanged.Reset;
function TLazy<T>.GetChanged: TState;
begin
Result := FLazy.Changed;
end;
function TLazy<T>.Update(var Value: T): Boolean;
begin
Result := FLazy.Update(Value);
end;
class operator TLazy<T>.Implicit(const A: TLazy<T>): ILazy;
begin
Result := A.FLazy;
end;
class operator TLazy<T>.Implicit(const A: ILazy): TLazy<T>;
begin
Result.FLazy := A;
end;
class operator TLazy<T>.Initialize(out Dest: TLazy<T>);
begin
Dest.FLazy := FNull;
end; end;
end. end.
+41 -53
View File
@@ -157,7 +157,7 @@ type
end; end;
// Endpoint that collects data into a series. // Endpoint that collects data into a series.
TMycDataEndpoint<T> = class(TInterfacedObject, TMutable<TSeries<T>>.IMutable) TMycDataEndpoint<T> = class(TInterfacedObject, TLazy<TSeries<T>>.ILazy)
type type
PItem = ^TItem; PItem = ^TItem;
TItem = record TItem = record
@@ -169,16 +169,16 @@ type
FTag: TDataProvider<T>.TTag; FTag: TDataProvider<T>.TTag;
FDataProvider: TDataProvider<T>; FDataProvider: TDataProvider<T>;
FLookback: Int64; FLookback: Int64;
FData: TSeries<T>; FChanged: TFlag;
FChanged: TEvent;
FLock: TLightweightMREW; FLock: TLightweightMREW;
FFirst: PItem; FFirst: PItem;
function GetChanged: TSignal; FCount: Integer;
function GetValue: TSeries<T>; function GetChanged: TState;
function ProcessData(const Value: T): TState; function ProcessData(const Value: T): TState;
public public
constructor Create(const ADataProvider: TDataProvider<T>; ALookback: Int64); constructor Create(const ADataProvider: TDataProvider<T>; ALookback: Int64);
destructor Destroy; override; destructor Destroy; override;
function Update(var Value: TSeries<T>): Boolean;
end; end;
TTickAggregation = class(TMycConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>) TTickAggregation = class(TMycConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>)
@@ -475,55 +475,13 @@ begin
inherited; inherited;
end; end;
function TMycDataEndpoint<T>.GetChanged: TSignal; function TMycDataEndpoint<T>.GetChanged: TState;
begin begin
Result := FChanged.Signal; Result := FChanged.State
end;
function TMycDataEndpoint<T>.GetValue: TSeries<T>;
begin
FLock.BeginWrite;
try
var cnt := 0;
var tmp: PItem := nil;
var item: PItem;
while FFirst <> nil do
begin
item := FFirst;
FFirst := item.Next;
item.Next := tmp;
tmp := item;
inc(cnt);
end;
if cnt > 0 then
begin
var Arr: TArray<T>;
SetLength(Arr, cnt);
cnt := 0;
while tmp <> nil do
begin
item := tmp;
tmp := item.Next;
Arr[cnt] := item.Value;
inc(cnt);
Dispose(item);
end;
FData := FData.Add(Arr, 0, cnt, FLookback);
end;
Result := FData;
finally
FLock.EndWrite;
end;
end; end;
function TMycDataEndpoint<T>.ProcessData(const Value: T): TState; function TMycDataEndpoint<T>.ProcessData(const Value: T): TState;
begin begin
Result := TState.Null;
FLock.BeginWrite; FLock.BeginWrite;
try try
var P: PItem; var P: PItem;
@@ -531,13 +489,43 @@ begin
P.Next := FFirst; P.Next := FFirst;
FFirst := P; FFirst := P;
P.Value := Value; P.Value := Value;
inc(FCount);
// Add new data point, respecting the lookback period. FChanged.Notify;
// FData := FData.Add(Value, FLookback); finally
FLock.EndWrite;
end;
end;
function TMycDataEndpoint<T>.Update(var Value: TSeries<T>): Boolean;
begin
FLock.BeginWrite;
try
Result := FChanged.Reset;
if Result then
begin
if FCount > 0 then
begin
var item: PItem;
var Arr: TArray<T>;
SetLength(Arr, FCount);
while FFirst <> nil do
begin
item := FFirst;
FFirst := item.Next;
dec(FCount);
Assert(FCount >= 0);
Arr[FCount] := item.Value;
Dispose(item);
end;
Assert(FCount = 0);
Value := Value.Add(Arr, 0, Length(Arr), FLookback);
end;
end;
finally finally
FLock.EndWrite; FLock.EndWrite;
end; end;
FChanged.Notify;
end; end;
{ TTickAggregation } { TTickAggregation }
+2 -2
View File
@@ -104,7 +104,7 @@ type
// Factory for creating specific converter instances. // Factory for creating specific converter instances.
TConverter = record TConverter = record
class function CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TMutable<TSeries<T>>; static; class function CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TLazy<TSeries<T>>; static;
class function CreateCounter<T>: TConverter<T, Int64>; static; class function CreateCounter<T>: TConverter<T, Int64>; static;
class function CreateTicker<T>: TConverter<TArray<T>, T>; static; class function CreateTicker<T>: TConverter<TArray<T>, T>; static;
@@ -277,7 +277,7 @@ begin
); );
end; end;
class function TConverter.CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TMutable<TSeries<T>>; class function TConverter.CreateEndpoint<T>(const DataProvider: TDataProvider<T>; Lookback: Int64): TLazy<TSeries<T>>;
begin begin
Result := TMycDataEndpoint<T>.Create(DataProvider, Lookback); Result := TMycDataEndpoint<T>.Create(DataProvider, Lookback);
end; end;