Chart now solid
This commit is contained in:
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user