Chart now solid

This commit is contained in:
Michael Schimmel
2025-07-12 21:19:11 +02:00
parent 45ff69fd92
commit a06640665a
5 changed files with 264 additions and 213 deletions
+4 -37
View File
@@ -24,7 +24,7 @@ type
protected protected
// Broadcasts the given data to all linked processors. // Broadcasts the given data to all linked processors.
procedure Broadcast(const Value: T); procedure Broadcast(const Value: T);
procedure Update; override;
public public
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
@@ -42,33 +42,21 @@ type
constructor Create(const AFunc: TConvertFunc); constructor Create(const AFunc: TConvertFunc);
end; end;
ITicksToTimeframe = interface(IMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>>) TTicksToTimeframe = class(TMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>>)
function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetStateText: TWriteable<String>;
function GetTimeframe: TTimeframe;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property StateText: TWriteable<String> read GetStateText;
property Timeframe: TTimeframe read GetTimeframe;
end;
TTicksToTimeframe = class(TMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>>, ITicksToTimeframe)
private private
FTimeframe: TTimeframe; FTimeframe: TTimeframe;
FStateText: TWriteable<String>;
// Stores the currently aggregating OHLC data. // Stores the currently aggregating OHLC data.
FCurrentBar: TDataPoint<TOhlcItem>; FCurrentBar: TDataPoint<TOhlcItem>;
function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime;
function GetCurrentBar: TDataPoint<TOhlcItem>; function GetCurrentBar: TDataPoint<TOhlcItem>;
function GetStateText: TWriteable<String>;
function GetTimeframe: TTimeframe; function GetTimeframe: TTimeframe;
public public
constructor Create(const ATimeframe: TTimeframe; const AStateText: TWriteable<String>); constructor Create(const ATimeframe: TTimeframe);
// Process new data. This is called concurrently and must not have side effects out of the scope of this class! // Process new data. This is called concurrently and must not have side effects out of the scope of this class!
function ProcessData(const Values: TArray<TDataPoint<TAskBidItem>>): Boolean; override; function ProcessData(const Values: TArray<TDataPoint<TAskBidItem>>): Boolean; override;
property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar; property CurrentBar: TDataPoint<TOhlcItem> read GetCurrentBar;
property StateText: TWriteable<String> read GetStateText;
property Timeframe: TTimeframe read GetTimeframe; property Timeframe: TTimeframe read GetTimeframe;
end; end;
@@ -99,10 +87,9 @@ uses
{ TTicksToTimeframe } { TTicksToTimeframe }
constructor TTicksToTimeframe.Create(const ATimeframe: TTimeframe; const AStateText: TWriteable<String>); constructor TTicksToTimeframe.Create(const ATimeframe: TTimeframe);
begin begin
inherited Create; inherited Create;
FStateText := AStateText;
FTimeframe := ATimeframe; FTimeframe := ATimeframe;
end; end;
@@ -124,11 +111,6 @@ begin
Result := FCurrentBar; Result := FCurrentBar;
end; end;
function TTicksToTimeframe.GetStateText: TWriteable<String>;
begin
Result := FStateText;
end;
function TTicksToTimeframe.GetTimeframe: TTimeframe; function TTicksToTimeframe.GetTimeframe: TTimeframe;
begin begin
Result := FTimeframe; Result := FTimeframe;
@@ -178,10 +160,6 @@ begin
FCurrentBar.Data := currentBar; FCurrentBar.Data := currentBar;
end; end;
end; end;
with FCurrentBar do
FStateText.Value :=
Format('Cuur Bar: O:%.5f H:%.5f L:%.5f C:%.5f V:%.0f', [Data.Open, Data.High, Data.Low, Data.Close, Data.Volume]);
end; end;
{ THullMovingAverage } { THullMovingAverage }
@@ -302,15 +280,4 @@ begin
Result := FSender; Result := FSender;
end; end;
procedure TMycConverter<S, T>.Update;
begin
FSender.Notify(
function(const Processor: IMycProcessor<T>): Boolean
begin
Processor.Update;
Result := true
end
);
end;
end. end.
+12 -13
View File
@@ -156,12 +156,6 @@ begin
if Symbol = '' then if Symbol = '' then
exit; exit;
var stateText := TWriteable<String>.CreateWriteable.Protect;
var stateLabel := TLabel.Create(Self);
AlignControl(stateLabel);
stateLabel.Text := 'Initializing...';
var chart := TMycChart.Create(Self); var chart := TMycChart.Create(Self);
AlignControl(chart); AlignControl(chart);
chart.Height := Layout.ChildrenRect.Width * 9 / 16; chart.Height := Layout.ChildrenRect.Width * 9 / 16;
@@ -169,7 +163,7 @@ begin
///// /////
var OhlcPoint: ITicksToTimeframe := TTicksToTimeframe.Create(H1, stateText); var OhlcPoint: IMycConverter<TArray<TDataPoint<TAskBidItem>>, TDataPoint<TOhlcItem>> := TTicksToTimeframe.Create(M1);
var Ohlc: IMycConverter<TDataPoint<TOhlcItem>, TOhlcItem> := var Ohlc: IMycConverter<TDataPoint<TOhlcItem>, TOhlcItem> :=
TMycGenericConverter<TDataPoint<TOhlcItem>, TOhlcItem> TMycGenericConverter<TDataPoint<TOhlcItem>, TOhlcItem>
@@ -182,18 +176,23 @@ begin
Ohlc.Sender.Link(Closes); Ohlc.Sender.Link(Closes);
var Hull: IMycConverter<Double, Double> := THullMovingAverage.Create(250); for var i := 0 to 200 do
begin
var Hull: IMycConverter<Double, Double> := THullMovingAverage.Create(20 + (20 * i));
Closes.Sender.Link(Hull);
chart.AddDoubleSeries(Hull.Sender, Random(Cardinal.MaxValue));
end;
Closes.Sender.Link(Hull); // var Hull: IMycConverter<Double, Double> := THullMovingAverage.Create(250);
//
chart.AddDoubleSeries(Hull.Sender); // Closes.Sender.Link(Hull);
//
// chart.AddDoubleSeries(Hull.Sender);
var done := ExecuteStrategy(Symbol, OhlcPoint); var done := ExecuteStrategy(Symbol, OhlcPoint);
///// /////
stateLabel.ProcessSignal(stateText.Changed, done, procedure begin stateLabel.Text := stateText.Value; end);
FProcessDone := TState.All([FProcessDone, done]); FProcessDone := TState.All([FProcessDone, done]);
chart.AddOhlcSeries(OhlcPoint.Sender); chart.AddOhlcSeries(OhlcPoint.Sender);
+145 -100
View File
@@ -31,9 +31,10 @@ type
function GetMainSeries: TSeries; function GetMainSeries: TSeries;
protected protected
function GetCount: Int64; virtual; abstract; function GetCount: Int64; virtual; abstract;
function GetTotalCount: Int64; virtual; abstract;
function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract; function GetValueRange(StartIndex, Count: Int64; out Min, Max: Double): Boolean; virtual; abstract;
function Update: Boolean; virtual; abstract; function Update: Boolean; virtual; abstract;
// The signature is changed to support viewport painting // The signature is changed to support viewport painting with an index offset
procedure Paint( procedure Paint(
const ACanvas: TCanvas; const ACanvas: TCanvas;
StartIndex, Count: Int64; StartIndex, Count: Int64;
@@ -44,6 +45,7 @@ type
constructor Create(AOwner: TMycChart); constructor Create(AOwner: TMycChart);
property Count: Int64 read GetCount; property Count: Int64 read GetCount;
property Owner: TMycChart read FOwner; property Owner: TMycChart read FOwner;
property TotalCount: Int64 read GetTotalCount;
end; end;
private private
@@ -68,20 +70,20 @@ type
constructor Create(AOwner: TComponent); override; constructor Create(AOwner: TComponent); override;
destructor Destroy; override; destructor Destroy; override;
// Creates an OHLC candlestick/bar series // Creates an OHLC candlestick/bar series and returns it
procedure AddOhlcSeries( function AddOhlcSeries(
const DataProvider: IMycDataProvider<TDataPoint<TOhlcItem>>; const DataProvider: IMycDataProvider<TDataPoint<TOhlcItem>>;
const AUpColor: TAlphaColor = TAlphaColors.Green; const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red; const ADownColor: TAlphaColor = TAlphaColors.Red;
const AStyle: TCandleStyle = csCandleStick const AStyle: TCandleStyle = csCandleStick
); ): TSeries;
// Creates a simple line series for double values // Creates a simple line series for double values and returns it
procedure AddDoubleSeries( function AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>; const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single = 1.5 const ALineWidth: Single = 1.5
); ): TSeries;
property Lookback: TWriteable<Int64> read FLookback write FLookback; property Lookback: TWriteable<Int64> read FLookback write FLookback;
end; end;
@@ -103,7 +105,6 @@ type
FData: TWriteable<TMycDataArray<T>>; FData: TWriteable<TMycDataArray<T>>;
protected protected
function ProcessData(const Value: T): Boolean; override; function ProcessData(const Value: T): Boolean; override;
procedure Update; override;
public public
constructor Create(const ALookback: TWriteable<Int64>); constructor Create(const ALookback: TWriteable<Int64>);
function GetData(var Data: TMycDataArray<T>): Boolean; function GetData(var Data: TMycDataArray<T>): Boolean;
@@ -121,6 +122,7 @@ type
FReceiverTag: TTag; FReceiverTag: TTag;
protected protected
function GetCount: Int64; override; function GetCount: Int64; override;
function GetTotalCount: Int64; override;
function Update: Boolean; override; function Update: Boolean; override;
public public
@@ -205,29 +207,25 @@ begin
inherited; inherited;
end; end;
procedure TMycChart.AddDoubleSeries( function TMycChart.AddDoubleSeries(
const DataProvider: IMycDataProvider<Double>; const DataProvider: IMycDataProvider<Double>;
const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue; const ALineColor: TAlphaColor = TAlphaColors.Cornflowerblue;
const ALineWidth: Single = 1.5 const ALineWidth: Single = 1.5
); ): TSeries;
var
series: TChartLineSeries;
begin begin
series := TChartLineSeries.Create(Self, DataProvider, ALineColor, ALineWidth); Result := TChartLineSeries.Create(Self, DataProvider, ALineColor, ALineWidth);
FSeriesList.Add(series); FSeriesList.Add(Result);
end; end;
procedure TMycChart.AddOhlcSeries( function TMycChart.AddOhlcSeries(
const DataProvider: IMycDataProvider<TDataPoint<TOhlcItem>>; const DataProvider: IMycDataProvider<TDataPoint<TOhlcItem>>;
const AUpColor: TAlphaColor = TAlphaColors.Green; const AUpColor: TAlphaColor = TAlphaColors.Green;
const ADownColor: TAlphaColor = TAlphaColors.Red; const ADownColor: TAlphaColor = TAlphaColors.Red;
const AStyle: TCandleStyle = csCandleStick const AStyle: TCandleStyle = csCandleStick
); ): TSeries;
var
series: TChartOhlcSeries;
begin begin
series := TChartOhlcSeries.Create(Self, DataProvider, AUpColor, ADownColor, AStyle); Result := TChartOhlcSeries.Create(Self, DataProvider, AUpColor, ADownColor, AStyle);
FSeriesList.Add(series); FSeriesList.Add(Result);
end; end;
procedure TMycChart.DoIdle; procedure TMycChart.DoIdle;
@@ -235,22 +233,18 @@ var
doRepaint: Boolean; doRepaint: Boolean;
mainSeries: TSeries; mainSeries: TSeries;
isLiveView: Boolean; isLiveView: Boolean;
prevCount, newCount, newCandles: Int64; prevTotalCount, newTotalCount, countDelta: Int64;
begin begin
if (FSeriesList.Count = 0) then if (FSeriesList.Count = 0) then
begin
exit; exit;
end;
mainSeries := FSeriesList[0]; mainSeries := FSeriesList[0];
if (mainSeries = nil) then if (mainSeries = nil) then
begin
exit; exit;
end;
// Remember state before update // Remember state before update
isLiveView := (FViewStartIndex = 0); isLiveView := (FViewStartIndex = 0);
prevCount := mainSeries.Count; prevTotalCount := mainSeries.TotalCount;
// Check all series for updates // Check all series for updates
doRepaint := false; doRepaint := false;
@@ -262,20 +256,20 @@ begin
end; end;
end; end;
if doRepaint then if doRepaint and (not isLiveView) then
begin begin
newCount := mainSeries.Count; newTotalCount := mainSeries.TotalCount;
newCandles := newCount - prevCount; countDelta := newTotalCount - prevTotalCount;
// Adjust viewport only if user was not watching the live data if (countDelta > 0) then
if (not isLiveView) and (newCandles > 0) then
begin begin
FViewStartIndex := FViewStartIndex + newCandles; // Adjust the start index based on the absolute change in total data points
FViewStartIndex := FViewStartIndex + countDelta;
end; end;
// If the view was live, it implicitly stays at StartIndex = 0, showing the new data.
Repaint;
end; end;
if doRepaint then
Repaint;
end; end;
procedure TMycChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure TMycChart.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single);
@@ -385,7 +379,9 @@ begin
if (FViewStartIndex > maxIndex) then if (FViewStartIndex > maxIndex) then
FViewStartIndex := maxIndex; FViewStartIndex := maxIndex;
FDragStartPoint := TPointF.Create(X, Y); // Optimized point update
FDragStartPoint.X := X;
FDragStartPoint.Y := Y;
Repaint; Repaint;
end; end;
end; end;
@@ -452,6 +448,24 @@ begin
end; end;
procedure TMycChart.Paint; procedure TMycChart.Paint;
function CalcView(Series: TSeries; MasterCount: Int64; out First: Int64; out Count: Int64): Boolean;
begin
var seriesTotalCount := series.TotalCount;
var offset := MasterCount - seriesTotalCount;
var startIdx := FViewStartIndex - offset;
var effectiveStart := Max(0, startIdx);
var effectiveEnd := Min(series.Count, startIdx + FViewCount) - 1;
Result := effectiveStart <= effectiveEnd - 1;
if Result then
begin
First := effectiveStart;
Count := effectiveEnd - effectiveStart + 1;
end;
end;
var var
rect: TRectF; rect: TRectF;
series: TSeries; series: TSeries;
@@ -462,6 +476,8 @@ var
isButtonVisible: Boolean; isButtonVisible: Boolean;
buttonColor: TAlphaColor; buttonColor: TAlphaColor;
path: TPathData; path: TPathData;
mainSeries: TSeries;
masterTotalCount, seriesStart, seriesCount: Int64;
begin begin
inherited; inherited;
rect := Self.LocalRect; rect := Self.LocalRect;
@@ -474,22 +490,26 @@ begin
end; end;
rangeInitialized := false; rangeInitialized := false;
mainSeries := FSeriesList[0];
// Determine value range for the visible viewport only masterTotalCount := mainSeries.TotalCount;
for series in FSeriesList do for series in FSeriesList do
begin begin
if series.GetValueRange(FViewStartIndex, FViewCount, seriesMin, seriesMax) then if CalcView(series, masterTotalCount, seriesStart, seriesCount) then
begin begin
if not rangeInitialized then if series.GetValueRange(seriesStart, seriesCount, seriesMin, seriesMax) then
begin begin
globalMin := seriesMin; if not rangeInitialized then
globalMax := seriesMax; begin
rangeInitialized := true; globalMin := seriesMin;
end globalMax := seriesMax;
else rangeInitialized := true;
begin end
globalMin := Min(globalMin, seriesMin); else
globalMax := Max(globalMax, seriesMax); begin
globalMin := Min(globalMin, seriesMin);
globalMax := Max(globalMax, seriesMax);
end;
end; end;
end; end;
end; end;
@@ -504,29 +524,29 @@ begin
if (globalMax - globalMin) = 0 then if (globalMax - globalMin) = 0 then
exit; exit;
// Transform maps an index from the viewport to a screen coordinate // Base transform maps an index from the viewport to a screen coordinate
xTransform :=
function(index: Double): Single
begin
if (FViewCount <= 1) then
begin
Result := rect.Left + rect.Width / 2;
exit;
end;
Result := rect.Right - (((index - FViewStartIndex) / (FViewCount - 1)) * rect.Width);
end;
yTransform := yTransform :=
function(value: Double): Single begin Result := rect.Top + (1 - (value - globalMin) / (globalMax - globalMin)) * rect.Height; end; function(value: Double): Single begin Result := rect.Top + (1 - (value - globalMin) / (globalMax - globalMin)) * rect.Height; end;
// --- Draw Series with Offset Compensation ---
// The masterTotalCount is already calculated from the range finding loop
for series in FSeriesList do for series in FSeriesList do
begin begin
series.Paint(Self.Canvas, FViewStartIndex, FViewCount, xTransform, yTransform); if CalcView(series, masterTotalCount, seriesStart, seriesCount) then
begin
var offset := masterTotalCount - series.TotalCount;
xTransform :=
function(index: Double): Single
begin
Result := rect.Right - (((index + offset - FViewStartIndex) / (FViewCount - 1)) * rect.Width);
end;
series.Paint(Self.Canvas, seriesStart, seriesCount, xTransform, yTransform);
end;
end; end;
// --- Draw the "jump to latest" button --- // --- Draw the "jump to latest" button ---
isButtonVisible := (FViewStartIndex > 0); isButtonVisible := (FViewStartIndex > 0);
FJumpButtonHot := FJumpButtonHot and isButtonVisible;
if isButtonVisible then if isButtonVisible then
begin begin
@@ -545,7 +565,7 @@ begin
Canvas.Fill.Color := buttonColor; Canvas.Fill.Color := buttonColor;
Canvas.FillRect(FJumpButtonRect, 4, 4, AllCorners, 0.7); Canvas.FillRect(FJumpButtonRect, 4, 4, AllCorners, 0.7);
// Draw an icon (e.g., a "fast forward" double arrow) // Draw an icon
Canvas.Stroke.Color := TAlphaColors.White; Canvas.Stroke.Color := TAlphaColors.White;
Canvas.Stroke.Thickness := 1.5; Canvas.Stroke.Thickness := 1.5;
@@ -553,11 +573,9 @@ begin
try try
var cx := FJumpButtonRect.CenterPoint.X; var cx := FJumpButtonRect.CenterPoint.X;
var cy := FJumpButtonRect.CenterPoint.Y; var cy := FJumpButtonRect.CenterPoint.Y;
// First arrow
path.MoveTo(PointF(cx - 5, cy - 6)); path.MoveTo(PointF(cx - 5, cy - 6));
path.LineTo(PointF(cx, cy)); path.LineTo(PointF(cx, cy));
path.LineTo(PointF(cx - 5, cy + 6)); path.LineTo(PointF(cx - 5, cy + 6));
// Second arrow
path.MoveTo(PointF(cx + 2, cy - 6)); path.MoveTo(PointF(cx + 2, cy - 6));
path.LineTo(PointF(cx + 7, cy)); path.LineTo(PointF(cx + 7, cy));
path.LineTo(PointF(cx + 2, cy + 6)); path.LineTo(PointF(cx + 2, cy + 6));
@@ -574,6 +592,30 @@ begin
end; end;
end; end;
{ TChartSeriesReceiver<T> }
constructor TChartSeriesReceiver<T>.Create(const ALookback: TWriteable<Int64>);
begin
inherited Create;
FLookback := ALookback;
FCurrData := TMycDataArray<T>.CreateEmpty;
FData := TWriteable<TMycDataArray<T>>.CreateWriteable(FCurrData).Protect;
end;
function TChartSeriesReceiver<T>.GetData(var Data: TMycDataArray<T>): Boolean;
begin
var prevTotalCount := Data.TotalCount;
Data := FData.Value;
Result := prevTotalCount <> Data.TotalCount;
end;
function TChartSeriesReceiver<T>.ProcessData(const Value: T): Boolean;
begin
Result := true;
FCurrData := FCurrData.Add(Value, FLookback.Value);
FData.Value := FCurrData;
end;
{ TChartSeriesProcessor<T> } { TChartSeriesProcessor<T> }
constructor TChartSeriesProcessor<T>.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider<T>); constructor TChartSeriesProcessor<T>.Create(AOwner: TMycChart; const ADataProvider: IMycDataProvider<T>);
@@ -598,6 +640,11 @@ begin
Result := FData.Count; Result := FData.Count;
end; end;
function TChartSeriesProcessor<T>.GetTotalCount: Int64;
begin
Result := FData.TotalCount;
end;
function TChartSeriesProcessor<T>.Update: Boolean; function TChartSeriesProcessor<T>.Update: Boolean;
begin begin
Result := FReceiver.GetData(FData); Result := FReceiver.GetData(FData);
@@ -621,17 +668,30 @@ end;
function TChartOhlcSeries.GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean; function TChartOhlcSeries.GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean;
var var
i: Int64; i: Int64;
lastIndex: Int64; seriesCount: Int64;
effectiveStart, effectiveEnd: Int64;
begin begin
Result := GetCount > 0; seriesCount := GetCount;
// Ensure the request is valid and there's data to check
Result := (seriesCount > 0) and (Count > 0);
if not Result then if not Result then
Exit; Exit;
// Calculate the effective range that is actually available within this series's data
effectiveStart := Max(0, StartIndex);
effectiveEnd := Min(seriesCount - 1, StartIndex + Count - 1);
// If the entire requested range is outside our available data, there's nothing to do.
if (effectiveStart > effectiveEnd) then
begin
Result := false;
Exit;
end;
MinValue := MaxDouble; MinValue := MaxDouble;
MaxValue := -MaxDouble; MaxValue := -MaxDouble;
lastIndex := Min(GetCount - 1, StartIndex + Count - 1);
for i := StartIndex to lastIndex do for i := effectiveStart to effectiveEnd do
begin begin
var dp := Data.Items[i]; var dp := Data.Items[i];
MinValue := Min(MinValue, dp.Data.Low); MinValue := Min(MinValue, dp.Data.Low);
@@ -704,16 +764,29 @@ end;
function TChartLineSeries.GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean; function TChartLineSeries.GetValueRange(StartIndex, Count: Int64; out MinValue, MaxValue: Double): Boolean;
var var
i: Int64; i: Int64;
lastIndex: Int64; seriesCount: Int64;
effectiveStart, effectiveEnd: Int64;
begin begin
Result := GetCount > 0; seriesCount := GetCount;
// Ensure the request is valid and there's data to check
Result := (seriesCount > 0) and (Count > 0);
if not Result then if not Result then
Exit; Exit;
// Calculate the effective range that is actually available within this series's data
effectiveStart := Max(0, StartIndex);
effectiveEnd := Min(seriesCount - 1, StartIndex + Count - 1);
// If the entire requested range is outside our available data, there's nothing to do.
if (effectiveStart > effectiveEnd) then
begin
Result := false;
Exit;
end;
MinValue := MaxDouble; MinValue := MaxDouble;
MaxValue := -MaxDouble; MaxValue := -MaxDouble;
lastIndex := Min(Data.Count - 1, StartIndex + Count - 1); for i := effectiveStart to effectiveEnd do
for i := StartIndex to lastIndex do
begin begin
var v := Data.Items[i]; var v := Data.Items[i];
if not IsNaN(v) then if not IsNaN(v) then
@@ -764,32 +837,4 @@ begin
end; end;
end; end;
{ TChartSeriesReceiver<T> }
constructor TChartSeriesReceiver<T>.Create(const ALookback: TWriteable<Int64>);
begin
inherited Create;
FLookback := ALookback;
FCurrData := TMycDataArray<T>.CreateEmpty;
FData := TWriteable<TMycDataArray<T>>.CreateWriteable(FCurrData).Protect;
end;
function TChartSeriesReceiver<T>.GetData(var Data: TMycDataArray<T>): Boolean;
begin
var prevTotalCount := Data.TotalCount;
Data := FData.Value;
Result := prevTotalCount <> Data.TotalCount;
end;
function TChartSeriesReceiver<T>.ProcessData(const Value: T): Boolean;
begin
Result := true;
FCurrData := FCurrData.Add(Value, FLookback.Value);
end;
procedure TChartSeriesReceiver<T>.Update;
begin
FData.Value := FCurrData;
end;
end. end.
-13
View File
@@ -32,13 +32,11 @@ type
IMycProcessor<T> = interface IMycProcessor<T> = interface
function ProcessData(const Value: T): Boolean; function ProcessData(const Value: T): Boolean;
procedure Update;
end; end;
TMycProcessor<T> = class abstract(TInterfacedObject, IMycProcessor<T>) TMycProcessor<T> = class abstract(TInterfacedObject, IMycProcessor<T>)
protected protected
function ProcessData(const Value: T): Boolean; virtual; abstract; function ProcessData(const Value: T): Boolean; virtual; abstract;
procedure Update; virtual;
end; end;
TTag = Pointer; TTag = Pointer;
@@ -79,7 +77,6 @@ type
private private
FProc: TProc; FProc: TProc;
function ProcessData(const Value: T): Boolean; function ProcessData(const Value: T): Boolean;
procedure Update;
public public
constructor Create(const Controller: IInterface; const AProc: TProc); constructor Create(const Controller: IInterface; const AProc: TProc);
end; end;
@@ -467,11 +464,6 @@ begin
Result := FProc(Value); Result := FProc(Value);
end; end;
procedure TMycContainedProcessor<T>.Update;
begin
end;
{ TMycDataProvider<S, T> } { TMycDataProvider<S, T> }
constructor TMycDataProvider<T>.Create(const Controller: IInterface); constructor TMycDataProvider<T>.Create(const Controller: IInterface);
@@ -516,9 +508,4 @@ begin
end; end;
end; end;
procedure TMycProcessor<T>.Update;
begin
end;
end. end.
+103 -50
View File
@@ -91,17 +91,17 @@ type
// Generic server for loading and managing sequential time-series data from files. // Generic server for loading and managing sequential time-series data from files.
TAuraDataServer<T: record> = class(TInterfacedObject, IDataServer<T>) TAuraDataServer<T: record> = class(TInterfacedObject, IDataServer<T>)
private private
FCachedFiles: TDataFileCache<TArray<TDataPoint<T>>>; FCachedFiles: TDataFileCache<TArray<TArray<TDataPoint<T>>>>;
FPath: String; FPath: String;
FSymbols: TFuture<TDictionary<String, TAuraDataFile>>; FSymbols: TFuture<TDictionary<String, TAuraDataFile>>;
function GetPath: String; function GetPath: String;
// Used by cache to actually load an uncached file. // Used by cache to actually load an uncached file.
function DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>; function DoLoad(const FileName: string): TFuture<TArray<TArray<TDataPoint<T>>>>;
function ProcessFile( function ProcessFile(
FileInfo: TAuraDataFile; FileInfo: TAuraDataFile;
const DataFile: TFuture<TArray<TDataPoint<T>>>; const DataFile: TFuture<TArray<TArray<TDataPoint<T>>>>;
const Terminated: TState; const Terminated: TState;
Processor: IMycProcessor<TArray<TDataPoint<T>>> Processor: IMycProcessor<TArray<TDataPoint<T>>>
): TState; ): TState;
@@ -111,8 +111,10 @@ type
FLoadGate: TLatch; FLoadGate: TLatch;
protected protected
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; virtual; abstract; // Read data from stream and return it chunked.
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; virtual; abstract; class function ReadCompressedData(const InputStream: TStream): TArray<TArray<TDataPoint<T>>>; virtual; abstract;
// Read data from stream and return it chunked.
class function ReadUncompressedData(const InputStream: TStream): TArray<TArray<TDataPoint<T>>>; virtual; abstract;
public public
constructor Create(const APath: String); constructor Create(const APath: String);
@@ -125,7 +127,9 @@ type
function FindFirstFile(const Symbol: string): TFuture<TAuraDataFile>; function FindFirstFile(const Symbol: string): TFuture<TAuraDataFile>;
function ParseFileName(const FileName: string): TAuraDataFile; virtual; abstract; function ParseFileName(const FileName: string): TAuraDataFile; virtual; abstract;
function EnumerateSymbols: TFuture<TArray<String>>; function EnumerateSymbols: TFuture<TArray<String>>;
function LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TDataPoint<T>>>;
// Load data file and split content into chunks.
function LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TArray<TDataPoint<T>>>>;
function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IMycProcessor<TArray<TDataPoint<T>>>): TState; function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IMycProcessor<TArray<TDataPoint<T>>>): TState;
@@ -138,7 +142,7 @@ type
type type
TDataStream = record TDataStream = record
FileInfo: TAuraDataFile; FileInfo: TAuraDataFile;
Data: TFuture<TArray<TDataPoint<T>>>; Data: TFuture<TArray<TArray<TDataPoint<T>>>>;
end; end;
private private
FDataServer: TAuraDataServer<T>; FDataServer: TAuraDataServer<T>;
@@ -147,6 +151,7 @@ type
FNextReady: TFlag; FNextReady: TFlag;
FCurrent: TFuture<TDataStream>; FCurrent: TFuture<TDataStream>;
FNext: TFuture<TDataStream>; FNext: TFuture<TDataStream>;
FCurrChunkInFile: Int64;
FCurrPosInFile: Int64; FCurrPosInFile: Int64;
FLastTimeStamp: TDateTime; FLastTimeStamp: TDateTime;
procedure PreloadNext; procedure PreloadNext;
@@ -172,8 +177,8 @@ type
TAuraTABFileServer = class(TAuraDataServer<TAuraAskBidFileItem>) TAuraTABFileServer = class(TAuraDataServer<TAuraAskBidFileItem>)
protected protected
// Scans a directory and returns the oldest file found for each symbol. // Scans a directory and returns the oldest file found for each symbol.
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>; override; class function ReadCompressedData(const InputStream: TStream): TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>; override;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>; override; class function ReadUncompressedData(const InputStream: TStream): TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>; override;
public public
function CreateStream(const Symbol: String): IDataStream<TAuraAskBidFileItem>; override; function CreateStream(const Symbol: String): IDataStream<TAuraAskBidFileItem>; override;
function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>; function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>;
@@ -188,6 +193,10 @@ uses
System.StrUtils, System.StrUtils,
Myc.TaskManager; Myc.TaskManager;
const
// The number of records per data chunk when loading files.
ChunkSize = 8192;
{ TAuraDataFile } { TAuraDataFile }
constructor TAuraDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer); constructor TAuraDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer);
@@ -252,8 +261,8 @@ begin
FPath := APath; FPath := APath;
FCachedFiles := FCachedFiles :=
TDataFileCache<TArray<TDataPoint<T>>> TDataFileCache<TArray<TArray<TDataPoint<T>>>>
.Create(function(const Filename: String): TFuture<TArray<TDataPoint<T>>> begin Result := DoLoad(Filename); end); .Create(function(const Filename: String): TFuture<TArray<TArray<TDataPoint<T>>>> begin Result := DoLoad(Filename); end);
end; end;
destructor TAuraDataServer<T>.Destroy; destructor TAuraDataServer<T>.Destroy;
@@ -274,9 +283,9 @@ begin
FCachedFiles.Clear; FCachedFiles.Clear;
end; end;
function TAuraDataServer<T>.DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>; function TAuraDataServer<T>.DoLoad(const FileName: string): TFuture<TArray<TArray<TDataPoint<T>>>>;
begin begin
Result := TFuture<TArray<TDataPoint<T>>>.Null; Result := TFuture<TArray<TArray<TDataPoint<T>>>>.Null;
if not TFile.Exists(FileName) then if not TFile.Exists(FileName) then
exit; exit;
@@ -287,8 +296,8 @@ begin
Result := Result :=
TFuture<TBytes> TFuture<TBytes>
.Construct(FLoadGate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end) .Construct(FLoadGate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
.Chain<TArray<TDataPoint<T>>>( .Chain<TArray<TArray<TDataPoint<T>>>>(
function(bytes: TBytes): TArray<TDataPoint<T>> function(bytes: TBytes): TArray<TArray<TDataPoint<T>>>
begin begin
var zipMemoryStream := TBytesStream.Create(bytes); var zipMemoryStream := TBytesStream.Create(bytes);
try try
@@ -302,9 +311,9 @@ begin
else else
begin begin
Result := Result :=
TFuture<TArray<TDataPoint<T>>>.Construct( TFuture<TArray<TArray<TDataPoint<T>>>>.Construct(
FLoadGate.Enqueue, FLoadGate.Enqueue,
function: TArray<TDataPoint<T>> function: TArray<TArray<TDataPoint<T>>>
begin begin
if TFile.Exists(capFileName) then if TFile.Exists(capFileName) then
begin begin
@@ -411,7 +420,7 @@ begin
end); end);
end; end;
function TAuraDataServer<T>.LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TDataPoint<T>>>; function TAuraDataServer<T>.LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TArray<TDataPoint<T>>>>;
begin begin
if DataFile.IsValid then if DataFile.IsValid then
Result := FCachedFiles.GetOrAdd(DataFile.GetFullFileName); Result := FCachedFiles.GetOrAdd(DataFile.GetFullFileName);
@@ -419,7 +428,7 @@ end;
function TAuraDataServer<T>.ProcessFile( function TAuraDataServer<T>.ProcessFile(
FileInfo: TAuraDataFile; FileInfo: TAuraDataFile;
const DataFile: TFuture<TArray<TDataPoint<T>>>; const DataFile: TFuture<TArray<TArray<TDataPoint<T>>>>;
const Terminated: TState; const Terminated: TState;
Processor: IMycProcessor<TArray<TDataPoint<T>>> Processor: IMycProcessor<TArray<TDataPoint<T>>>
): TState; ): TState;
@@ -434,11 +443,20 @@ begin
var cTerminated := Terminated; var cTerminated := Terminated;
Result := Result :=
DataFile.Chain( DataFile.Chain(
function(const Data: TArray<TDataPoint<T>>): TState function(const DataChunks: TArray<TArray<TDataPoint<T>>>): TState
begin begin
if not Processor.ProcessData(Data) then if Assigned(DataChunks) then
exit(TState.Null); begin
Processor.Update; // Process each chunk, checking for termination between chunks.
for var chunk in DataChunks do
begin
if cTerminated.IsSet then
exit(TState.Null);
if not Processor.ProcessData(chunk) then
exit(TState.Null);
end;
end;
Result := ProcessFile(nextFileInfo, nextFile, cTerminated, Processor); Result := ProcessFile(nextFileInfo, nextFile, cTerminated, Processor);
end end
@@ -485,6 +503,7 @@ begin
); );
FLastTimeStamp := 0; FLastTimeStamp := 0;
FCurrChunkInFile := 0;
FCurrPosInFile := 0; FCurrPosInFile := 0;
PreloadNext; PreloadNext;
@@ -502,6 +521,8 @@ end;
function TAuraFileStream<T>.GetChunk(var Data: array of TDataPoint<T>): Integer; function TAuraFileStream<T>.GetChunk(var Data: array of TDataPoint<T>): Integer;
var var
item: TDataPoint<T>; item: TDataPoint<T>;
fileChunks: TArray<TArray<TDataPoint<T>>>;
currentChunk: TArray<TDataPoint<T>>;
label label
loop; loop;
begin begin
@@ -516,19 +537,33 @@ begin
if not FCurrent.Value.FileInfo.IsValid then if not FCurrent.Value.FileInfo.IsValid then
exit; exit;
if FCurrPosInFile >= Length(FCurrent.Value.Data.Value) then fileChunks := FCurrent.Value.Data.Value;
if (fileChunks = nil) or (FCurrChunkInFile >= Length(fileChunks)) then
begin begin
// Finished with current file, move to next
FCurrChunkInFile := 0;
FCurrPosInFile := 0; FCurrPosInFile := 0;
FCurrent := FNext; FCurrent := FNext;
PreloadNext; PreloadNext;
goto loop; goto loop;
end; end;
var currData := FCurrent.Value.Data.Value;
var maxLen := Length(Data); var maxLen := Length(Data);
while (Result < maxLen) and (FCurrPosInFile < Length(currData)) do while (Result < maxLen) do
begin begin
item := currData[FCurrPosInFile]; // Check if we need to advance to the next chunk
if (FCurrChunkInFile < Length(fileChunks)) and (FCurrPosInFile >= Length(fileChunks[FCurrChunkInFile])) then
begin
Inc(FCurrChunkInFile);
FCurrPosInFile := 0;
end;
// Exit loop if all chunks are processed for the current file
if FCurrChunkInFile >= Length(fileChunks) then
break;
currentChunk := fileChunks[FCurrChunkInFile];
item := currentChunk[FCurrPosInFile];
if FLastTimeStamp < item.Time then if FLastTimeStamp < item.Time then
begin begin
FLastTimeStamp := item.Time; FLastTimeStamp := item.Time;
@@ -538,7 +573,8 @@ begin
Inc(FCurrPosInFile); Inc(FCurrPosInFile);
end; end;
if (FCurrPosInFile < Length(currData)) or FNextReady.Reset then // Notify if there is potentially more data
if (FCurrChunkInFile < Length(fileChunks)) or FNextReady.Reset then
begin begin
FHasData.Notify; FHasData.Notify;
end; end;
@@ -593,8 +629,8 @@ end;
function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>; function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>;
var var
loadedState: TState; loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>>; loadedFiles: TArray<TFuture<TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>>>;
liveData: TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>; liveData: TFuture<TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>>;
tabFiles: TList<TAuraDataFile>; tabFiles: TList<TAuraDataFile>;
liveFile: TAuraDataFile; liveFile: TAuraDataFile;
currentFileInfo: TAuraDataFile; currentFileInfo: TAuraDataFile;
@@ -618,7 +654,7 @@ begin
liveFile := ParseFileName(potentialLivePath); liveFile := ParseFileName(potentialLivePath);
end; end;
var loadedFileList := TList<TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>>.Create; var loadedFileList := TList<TFuture<TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>>>.Create;
var loadStates := TList<TState>.Create; var loadStates := TList<TState>.Create;
try try
for var fileInfo in tabFiles do for var fileInfo in tabFiles do
@@ -628,7 +664,7 @@ begin
loadStates.Add(data.Done); loadStates.Add(data.Done);
end; end;
liveData := TFuture<TArray<TDataPoint<TAuraAskBidFileItem>>>.Null; liveData := TFuture<TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>>.Null;
if liveFile.IsValid then if liveFile.IsValid then
begin begin
liveData := LoadDataFile(liveFile); liveData := LoadDataFile(liveFile);
@@ -656,16 +692,18 @@ begin
var overallLastTabTickTime: TDateTime := 0; var overallLastTabTickTime: TDateTime := 0;
var cnt := 0; var cnt := 0;
for var P in loadedFiles do for var P in loadedFiles do
inc(cnt, Length(P.Value)); for var chunk in P.Value do
inc(cnt, Length(chunk));
tickList.Capacity := cnt; tickList.Capacity := cnt;
for var P in loadedFiles do for var P in loadedFiles do
for var iterTickData in P.Value do for var chunk in P.Value do
if overallLastTabTickTime < iterTickData.Time then for var iterTickData in chunk do
begin if overallLastTabTickTime < iterTickData.Time then
tickList.Add(iterTickData); begin
overallLastTabTickTime := iterTickData.Time; tickList.Add(iterTickData);
end; overallLastTabTickTime := iterTickData.Time;
end;
Result := tickList.ToArray; Result := tickList.ToArray;
finally finally
tickList.Free; tickList.Free;
@@ -721,14 +759,14 @@ begin
Result := TAuraDataFile.Create(path, symbol, fileExt, year, month); Result := TAuraDataFile.Create(path, symbol, fileExt, year, month);
end; end;
class function TAuraTABFileServer.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>; class function TAuraTABFileServer.ReadCompressedData(const InputStream: TStream): TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>;
var var
decompressionStream: TStream; decompressionStream: TStream;
localHeader: TZipHeader; localHeader: TZipHeader;
entryIndex, i: Integer; entryIndex, i: Integer;
zipFileInstance: TZipFile; zipFileInstance: TZipFile;
begin begin
SetLength(Result, 0); Result := nil;
decompressionStream := nil; decompressionStream := nil;
zipFileInstance := nil; zipFileInstance := nil;
try try
@@ -760,7 +798,7 @@ begin
end; end;
end; end;
class function TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAuraAskBidFileItem>>; class function TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray<TArray<TDataPoint<TAuraAskBidFileItem>>>;
type type
TFileRecord = packed record TFileRecord = packed record
TimeStamp: TDateTime; TimeStamp: TDateTime;
@@ -768,25 +806,40 @@ type
end; end;
var var
fileSize: Int64; fileSize: Int64;
recordCount, bytesRead: Integer; totalRecordCount, bytesRead, chunkIndex, indexInChunk, recordIndex: Integer;
rec: TFileRecord; rec: TFileRecord;
begin begin
SetLength(Result, 0); Result := nil;
InputStream.Position := 0; InputStream.Position := 0;
fileSize := InputStream.Size; fileSize := InputStream.Size;
if (fileSize = 0) or ((fileSize mod SizeOf(TFileRecord)) <> 0) then if (fileSize = 0) or ((fileSize mod SizeOf(TFileRecord)) <> 0) then
exit; exit;
recordCount := fileSize div SizeOf(TFileRecord); totalRecordCount := fileSize div SizeOf(TFileRecord);
if recordCount > 0 then if totalRecordCount > 0 then
begin begin
SetLength(Result, recordCount); var numChunks := (totalRecordCount + ChunkSize - 1) div ChunkSize;
for var i := 0 to High(Result) do SetLength(Result, numChunks);
chunkIndex := 0;
indexInChunk := 0;
SetLength(Result[chunkIndex], Min(ChunkSize, totalRecordCount));
for recordIndex := 0 to totalRecordCount - 1 do
begin begin
if indexInChunk >= ChunkSize then
begin
Inc(chunkIndex);
indexInChunk := 0;
var remainingRecords := totalRecordCount - (chunkIndex * ChunkSize);
SetLength(Result[chunkIndex], Min(ChunkSize, remainingRecords));
end;
bytesRead := InputStream.Read(rec, SizeOf(TFileRecord)); bytesRead := InputStream.Read(rec, SizeOf(TFileRecord));
if bytesRead <> SizeOf(TFileRecord) then if bytesRead <> SizeOf(TFileRecord) then
raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [fileSize, bytesRead]); raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [SizeOf(TFileRecord), bytesRead]);
Result[i].Create(rec.TimeStamp, rec.Data);
Result[chunkIndex][indexInChunk].Create(rec.TimeStamp, rec.Data);
Inc(indexInChunk);
end; end;
end; end;
end; end;