Chart
This commit is contained in:
@@ -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)'==''">Debug</Config>
|
<Config Condition="'$(Config)'==''">Release</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>
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ object Form1: TForm1
|
|||||||
StyleLookup = ''
|
StyleLookup = ''
|
||||||
TabOrder = 0
|
TabOrder = 0
|
||||||
Text = 'Modules'
|
Text = 'Modules'
|
||||||
|
ExplicitSize.cx = 66.00000000000000000
|
||||||
|
ExplicitSize.cy = 26.00000000000000000
|
||||||
object TreeView: TTreeView
|
object TreeView: TTreeView
|
||||||
Align = Client
|
Align = Client
|
||||||
Size.Width = 169.00000000000000000
|
Size.Width = 169.00000000000000000
|
||||||
|
|||||||
@@ -130,10 +130,6 @@ begin
|
|||||||
tab.Text := ws.Caption;
|
tab.Text := ws.Caption;
|
||||||
tab.Tag := NativeInt(ws);
|
tab.Tag := NativeInt(ws);
|
||||||
|
|
||||||
var tabBtn := TSpeedButton.Create(Self);
|
|
||||||
tabBtn.Parent := tab;
|
|
||||||
tabBtn.Align := TAlignLayout.Right;
|
|
||||||
|
|
||||||
var scrollbox := TVertScrollBox.Create(Self);
|
var scrollbox := TVertScrollBox.Create(Self);
|
||||||
scrollbox.Parent := tab;
|
scrollbox.Parent := tab;
|
||||||
scrollbox.Align := TAlignLayout.Client;
|
scrollbox.Align := TAlignLayout.Client;
|
||||||
@@ -152,8 +148,6 @@ end;
|
|||||||
|
|
||||||
procedure TForm1.TreeViewDblClick(Sender: TObject);
|
procedure TForm1.TreeViewDblClick(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
var addnew := false;
|
|
||||||
|
|
||||||
var sel := TreeView.Selected as TTreeViewItem;
|
var sel := TreeView.Selected as TTreeViewItem;
|
||||||
|
|
||||||
var parent := sel.ParentItem;
|
var parent := sel.ParentItem;
|
||||||
@@ -372,14 +366,14 @@ begin
|
|||||||
nil,
|
nil,
|
||||||
function: TState
|
function: TState
|
||||||
begin
|
begin
|
||||||
var Prices := TDataSeries<TAskBidItem>.CreateWriteable(width);
|
var Prices := TDataSeries<TAskBidItem>.CreateDataSeries(width);
|
||||||
Result :=
|
Result :=
|
||||||
FServer.ProcessData(
|
FServer.ProcessData(
|
||||||
Symbol,
|
Symbol,
|
||||||
FTerminate.Signal,
|
FTerminate.Signal,
|
||||||
procedure(const Values: TArray<TDataPoint<TAskBidItem>>; const Terminated: TState)
|
procedure(const Values: TArray<TDataPoint<TAskBidItem>>; const Terminated: TState)
|
||||||
begin
|
begin
|
||||||
Prices.Add(Values);
|
Prices := Prices.Add(Values);
|
||||||
currLog.Value := Prices.TotalCount.ToString;
|
currLog.Value := Prices.TotalCount.ToString;
|
||||||
|
|
||||||
var PathData := TPathData.Create;
|
var PathData := TPathData.Create;
|
||||||
|
|||||||
+590
-229
File diff suppressed because it is too large
Load Diff
+3
-19
@@ -413,21 +413,17 @@ type
|
|||||||
private
|
private
|
||||||
FName: TWriteable<String>;
|
FName: TWriteable<String>;
|
||||||
function GetName: TWriteable<String>;
|
function GetName: TWriteable<String>;
|
||||||
procedure Serialize(const Write: TJsonWriter);
|
procedure Serialize(const Write: TJsonWriter); virtual;
|
||||||
public
|
public
|
||||||
constructor Create(const AName: string);
|
constructor Create(const AName: string);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Generic implementation for a collection of child nodes.
|
// Generic implementation for a collection of child nodes.
|
||||||
TMycAuraNode = class(TInterfacedObject, IAuraNode)
|
TMycAuraNode = class(TMycAuraObject, IAuraNode)
|
||||||
private
|
private
|
||||||
FName: TWriteable<String>;
|
|
||||||
function GetName: TWriteable<String>;
|
|
||||||
protected
|
protected
|
||||||
function GetCaption: string; virtual;
|
function GetCaption: string; virtual;
|
||||||
procedure Serialize(const Write: TJsonWriter);
|
procedure Serialize(const Write: TJsonWriter); override;
|
||||||
public
|
|
||||||
constructor Create(const AName: string);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Generic implementation for a collection of child nodes.
|
// Generic implementation for a collection of child nodes.
|
||||||
@@ -502,23 +498,11 @@ end;
|
|||||||
|
|
||||||
{ TMycAuraNode }
|
{ TMycAuraNode }
|
||||||
|
|
||||||
constructor TMycAuraNode.Create(const AName: string);
|
|
||||||
begin
|
|
||||||
inherited Create;
|
|
||||||
FName := TWriteable<String>.CreateWriteable;
|
|
||||||
FName.Value := AName;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TMycAuraNode.GetCaption: string;
|
function TMycAuraNode.GetCaption: string;
|
||||||
begin
|
begin
|
||||||
Result := FName.Value;
|
Result := FName.Value;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TMycAuraNode.GetName: TWriteable<String>;
|
|
||||||
begin
|
|
||||||
Result := FName;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TMycAuraNode.Serialize(const Write: TJsonWriter);
|
procedure TMycAuraNode.Serialize(const Write: TJsonWriter);
|
||||||
begin
|
begin
|
||||||
Write.WriteStartObject;
|
Write.WriteStartObject;
|
||||||
|
|||||||
@@ -18,9 +18,13 @@ type
|
|||||||
end;
|
end;
|
||||||
|
|
||||||
TMycNullFuture<T> = class(TMycFuture<T>)
|
TMycNullFuture<T> = class(TMycFuture<T>)
|
||||||
|
private
|
||||||
|
FValue: T;
|
||||||
protected
|
protected
|
||||||
function GetValue: T; override;
|
function GetValue: T; override;
|
||||||
function GetDone: TState; override;
|
function GetDone: TState; override;
|
||||||
|
public
|
||||||
|
constructor Create(const AValue: T);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TMycGateFuncFuture<T> = class(TMycFuture<T>)
|
TMycGateFuncFuture<T> = class(TMycFuture<T>)
|
||||||
@@ -56,6 +60,12 @@ begin
|
|||||||
Assert(GetDone.IsSet, 'Trying to destroy an unfinished future');
|
Assert(GetDone.IsSet, 'Trying to destroy an unfinished future');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
constructor TMycNullFuture<T>.Create(const AValue: T);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FValue := AValue;
|
||||||
|
end;
|
||||||
|
|
||||||
{ TMycNullFuture<T> }
|
{ TMycNullFuture<T> }
|
||||||
|
|
||||||
function TMycNullFuture<T>.GetDone: TState;
|
function TMycNullFuture<T>.GetDone: TState;
|
||||||
@@ -65,7 +75,7 @@ end;
|
|||||||
|
|
||||||
function TMycNullFuture<T>.GetValue: T;
|
function TMycNullFuture<T>.GetValue: T;
|
||||||
begin
|
begin
|
||||||
Result := Default(T);
|
Result := FValue;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TMycGateFuncFuture<T> }
|
{ TMycGateFuncFuture<T> }
|
||||||
|
|||||||
+58
-1
@@ -45,6 +45,7 @@ type
|
|||||||
|
|
||||||
class function Construct(const Proc: TFunc<T>): TFuture<T>; overload; static;
|
class function Construct(const Proc: TFunc<T>): TFuture<T>; overload; static;
|
||||||
class function Construct(const Gate: TState; const Proc: TFunc<T>): TFuture<T>; overload; static;
|
class function Construct(const Gate: TState; const Proc: TFunc<T>): TFuture<T>; overload; static;
|
||||||
|
class function Construct(const ConstVal: T): TFuture<T>; overload; static;
|
||||||
|
|
||||||
class property Null: IFuture read FNull;
|
class property Null: IFuture read FNull;
|
||||||
|
|
||||||
@@ -55,10 +56,17 @@ type
|
|||||||
function Chain(const Proc: TProcConst<T, TState>): TState; overload;
|
function Chain(const Proc: TProcConst<T, TState>): TState; overload;
|
||||||
function WaitFor: T;
|
function WaitFor: T;
|
||||||
|
|
||||||
|
class function WhenAll<S>(const Futures: TArray<IFuture>; const Func: TFuncConst<TArray<T>, S>): TFuture<S>; static; experimental;
|
||||||
|
|
||||||
property Done: TState.IState read GetDone;
|
property Done: TState.IState read GetDone;
|
||||||
property Value: T read GetValue;
|
property Value: T read GetValue;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
TFuture = record
|
||||||
|
public
|
||||||
|
class function FromArray<T>(const Arr: TArray<TFuture<T>>): TFuture<TArray<T>>; static;
|
||||||
|
end;
|
||||||
|
|
||||||
IObjectRef<T: class> = interface
|
IObjectRef<T: class> = interface
|
||||||
function GetObj: T;
|
function GetObj: T;
|
||||||
function Pop: T;
|
function Pop: T;
|
||||||
@@ -89,7 +97,7 @@ end;
|
|||||||
|
|
||||||
class constructor TFuture<T>.CreateClass;
|
class constructor TFuture<T>.CreateClass;
|
||||||
begin
|
begin
|
||||||
FNull := TMycNullFuture<T>.Create;
|
FNull := TMycNullFuture<T>.Create(Default(T));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TFuture<T>.Chain(const Proc: TProcConst<T, TState>): TState;
|
function TFuture<T>.Chain(const Proc: TProcConst<T, TState>): TState;
|
||||||
@@ -121,6 +129,11 @@ begin
|
|||||||
Result := TMycGateFuncFuture<T>.Create(TaskManager, Gate, Proc);
|
Result := TMycGateFuncFuture<T>.Create(TaskManager, Gate, Proc);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
class function TFuture<T>.Construct(const ConstVal: T): TFuture<T>;
|
||||||
|
begin
|
||||||
|
Result := TMycNullFuture<T>.Create(ConstVal);
|
||||||
|
end;
|
||||||
|
|
||||||
function TFuture<T>.GetDone: TState.IState;
|
function TFuture<T>.GetDone: TState.IState;
|
||||||
begin
|
begin
|
||||||
Result := FFuture.Done;
|
Result := FFuture.Done;
|
||||||
@@ -143,6 +156,29 @@ begin
|
|||||||
Result := FFuture.Value;
|
Result := FFuture.Value;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
class function TFuture<T>.WhenAll<S>(const Futures: TArray<IFuture>; const Func: TFuncConst<TArray<T>, S>): TFuture<S>;
|
||||||
|
var
|
||||||
|
DoneStates: TArray<TState>;
|
||||||
|
begin
|
||||||
|
SetLength(DoneStates, Length(Futures));
|
||||||
|
for var i := 0 to High(DoneStates) do
|
||||||
|
DoneStates[i] := Futures[i].Done;
|
||||||
|
var cFutures := Futures;
|
||||||
|
Result :=
|
||||||
|
TFuture<S>.Construct(
|
||||||
|
TState.All(DoneStates),
|
||||||
|
function: S
|
||||||
|
var
|
||||||
|
Vals: TArray<T>;
|
||||||
|
begin
|
||||||
|
SetLength(Vals, Length(cFutures));
|
||||||
|
for var i := 0 to High(Vals) do
|
||||||
|
Vals[i] := Futures[i].Value;
|
||||||
|
Result := Func(Vals);
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
class operator TFuture<T>.Implicit(const A: IFuture): TFuture<T>;
|
class operator TFuture<T>.Implicit(const A: IFuture): TFuture<T>;
|
||||||
begin
|
begin
|
||||||
Result.Create(A);
|
Result.Create(A);
|
||||||
@@ -181,4 +217,25 @@ begin
|
|||||||
FreeAndNil(FObj);
|
FreeAndNil(FObj);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
class function TFuture.FromArray<T>(const Arr: TArray<TFuture<T>>): TFuture<TArray<T>>;
|
||||||
|
var
|
||||||
|
DoneStates: TArray<TState>;
|
||||||
|
begin
|
||||||
|
var cFutures := Arr;
|
||||||
|
SetLength(DoneStates, Length(cFutures));
|
||||||
|
for var i := 0 to High(DoneStates) do
|
||||||
|
DoneStates[i] := cFutures[i].Done;
|
||||||
|
|
||||||
|
Result :=
|
||||||
|
TFuture<TArray<T>>.Construct(
|
||||||
|
TState.All(DoneStates),
|
||||||
|
function: TArray<T>
|
||||||
|
begin
|
||||||
|
SetLength(Result, Length(cFutures));
|
||||||
|
for var i := 0 to High(Result) do
|
||||||
|
Result[i] := cFutures[i].Value;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+370
-250
@@ -9,12 +9,11 @@ uses
|
|||||||
Myc.Trade.DataPoint;
|
Myc.Trade.DataPoint;
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
TTestDataArray = class(TObject)
|
TTestDataSeries = class(TObject)
|
||||||
private
|
private
|
||||||
FArray: TDataSeries<TAskBidItem>;
|
FSeries: TDataSeries<TAskBidItem>;
|
||||||
procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
|
procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 10);
|
||||||
public
|
public
|
||||||
[Setup]
|
[Setup]
|
||||||
procedure Setup;
|
procedure Setup;
|
||||||
@@ -32,15 +31,6 @@ type
|
|||||||
[Test]
|
[Test]
|
||||||
procedure TestGetItemsIndexing;
|
procedure TestGetItemsIndexing;
|
||||||
|
|
||||||
// Tests for MaxLookback
|
|
||||||
[Test]
|
|
||||||
procedure TestCountIsCappedByMaxLookback_NoTrim;
|
|
||||||
[Test]
|
|
||||||
procedure TestCountIsCappedByMaxLookback_WithTrim;
|
|
||||||
[Test]
|
|
||||||
[IgnoreMemoryLeaks]
|
|
||||||
procedure TestLoopIsSafeWithMaxLookback;
|
|
||||||
|
|
||||||
// Tests for bulk Add
|
// Tests for bulk Add
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestBulkAddIncreasesCount;
|
procedure TestBulkAddIncreasesCount;
|
||||||
@@ -61,19 +51,7 @@ type
|
|||||||
[Test]
|
[Test]
|
||||||
procedure TestTotalCountIgnoresTrimming;
|
procedure TestTotalCountIgnoresTrimming;
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestTotalCountResetsOnClear;
|
procedure TestTotalCountResetsOnRecreate;
|
||||||
end;
|
|
||||||
|
|
||||||
[TestFixture]
|
|
||||||
TTestDataSeries = class(TObject)
|
|
||||||
private
|
|
||||||
FSeries: TDataSeries<TAskBidItem>;
|
|
||||||
procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
|
|
||||||
public
|
|
||||||
[Setup]
|
|
||||||
procedure Setup;
|
|
||||||
[Teardown]
|
|
||||||
procedure Teardown;
|
|
||||||
|
|
||||||
// Tests for IndexOf
|
// Tests for IndexOf
|
||||||
[Test]
|
[Test]
|
||||||
@@ -94,48 +72,65 @@ type
|
|||||||
procedure TestIndexOfSingleItemSeries;
|
procedure TestIndexOfSingleItemSeries;
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestIndexOfRespectsMaxLookback;
|
procedure TestIndexOfRespectsMaxLookback;
|
||||||
|
[Test]
|
||||||
|
procedure TestIndexOfWithTrimmedData;
|
||||||
|
|
||||||
// Tests for Copy
|
// Tests for Copy (Immutability)
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestCopyHasSameContent;
|
procedure TestCopyHasSameContent;
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestCopyIsImmutableAfterOriginalChanges;
|
procedure TestCopyIsImmutableAfterOriginalChanges;
|
||||||
[Test]
|
[Test]
|
||||||
procedure TestCopyRespectsMaxLookback;
|
procedure TestCopyRespectsMaxLookback;
|
||||||
|
|
||||||
|
// Tests for Lookback
|
||||||
|
[Test]
|
||||||
|
procedure TestCountIsCappedByMaxLookback_NoTrim;
|
||||||
|
[Test]
|
||||||
|
procedure TestCountIsCappedByMaxLookback_WithTrim;
|
||||||
|
[Test]
|
||||||
|
[IgnoreMemoryLeaks]
|
||||||
|
procedure TestLoopIsSafeWithMaxLookback;
|
||||||
|
[Test]
|
||||||
|
procedure TestDataAndTimePropertiesRespectLookback;
|
||||||
|
[Test]
|
||||||
|
procedure TestLookbackZero;
|
||||||
|
[Test]
|
||||||
|
procedure TestLookbackOne;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{ TTestDataArray }
|
{ TTestDataSeries }
|
||||||
|
|
||||||
procedure TTestDataArray.Setup;
|
procedure TTestDataSeries.Setup;
|
||||||
begin
|
begin
|
||||||
FArray := TDataSeries<TAskBidItem>.CreateWriteable(0);
|
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(1);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.Teardown;
|
procedure TTestDataSeries.Teardown;
|
||||||
begin
|
begin
|
||||||
FArray := nil;
|
FSeries := Default(TDataSeries<TAskBidItem>);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
|
procedure TTestDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 10);
|
||||||
var
|
var
|
||||||
i: Integer;
|
i: Integer;
|
||||||
DataPoint: TDataPoint<TAskBidItem>;
|
DataPoint: TDataPoint<TAskBidItem>;
|
||||||
baseTime: TDateTime;
|
baseTime: TDateTime;
|
||||||
begin
|
begin
|
||||||
FArray := TDataSeries<TAskBidItem>.CreateWriteable(MaxLookBack);
|
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(MaxLookBack);
|
||||||
baseTime := EncodeDate(2020, 7, 7);
|
baseTime := EncodeDate(2020, 7, 7);
|
||||||
|
|
||||||
// Add data points with increasing timestamps.
|
// Add data points with increasing timestamps.
|
||||||
for i := 0 to ItemCount - 1 do
|
for i := 0 to ItemCount - 1 do
|
||||||
begin
|
begin
|
||||||
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
|
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
|
||||||
FArray.Add(DataPoint);
|
FSeries := FSeries.Add([DataPoint], 0, 1);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestSetupSeriesWithDataVerification;
|
procedure TTestDataSeries.TestSetupSeriesWithDataVerification;
|
||||||
var
|
var
|
||||||
i: Integer;
|
i: Integer;
|
||||||
baseTime, expectedTime: TDateTime;
|
baseTime, expectedTime: TDateTime;
|
||||||
@@ -144,38 +139,39 @@ begin
|
|||||||
SetupSeriesWithData;
|
SetupSeriesWithData;
|
||||||
baseTime := EncodeDate(2020, 7, 7);
|
baseTime := EncodeDate(2020, 7, 7);
|
||||||
|
|
||||||
Assert.AreEqual(Int64(10), FArray.Count, 'Setup should create exactly 10 items');
|
Assert.AreEqual(Int64(10), FSeries.Count, 'Setup should create exactly 10 items');
|
||||||
|
|
||||||
// Check all items to ensure correct reverse chronological order.
|
// Check all items to ensure correct reverse chronological order.
|
||||||
for i := 0 to FArray.Count - 1 do
|
for i := 0 to FSeries.Count - 1 do
|
||||||
begin
|
begin
|
||||||
expectedTime := baseTime + (9 - i);
|
expectedTime := baseTime + (9 - i);
|
||||||
expectedAsk := Single(9 - i);
|
expectedAsk := Single(9 - i);
|
||||||
Assert.AreEqual(expectedTime, FArray[i].Time, 'Item at logical index should have correct reversed timestamp');
|
Assert.AreEqual(expectedTime, FSeries[i].Time, 'Item at logical index should have correct reversed timestamp');
|
||||||
Assert.AreEqual(expectedAsk, FArray[i].Data.Ask, 'Item at logical index should have correct reversed data');
|
Assert.AreEqual(expectedAsk, FSeries[i].Data.Ask, 'Item at logical index should have correct reversed data');
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestAddAndCount;
|
procedure TTestDataSeries.TestAddAndCount;
|
||||||
var
|
var
|
||||||
DataPoint: TDataPoint<TAskBidItem>;
|
DataPoint: TDataPoint<TAskBidItem>;
|
||||||
newTime: TDateTime;
|
newTime: TDateTime;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData;
|
SetupSeriesWithData(10, 11);
|
||||||
newTime := EncodeDate(2020, 7, 17);
|
newTime := EncodeDate(2020, 7, 17);
|
||||||
|
|
||||||
Assert.AreEqual(Int64(10), FArray.Count, 'Initial count should be 10');
|
Assert.AreEqual(Int64(10), FSeries.Count, 'Initial count should be 10');
|
||||||
|
|
||||||
DataPoint := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(100.0, 100.1));
|
DataPoint := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(100.0, 100.1));
|
||||||
FArray.Add(DataPoint);
|
FSeries := FSeries.Add([DataPoint], 0, 1);
|
||||||
|
|
||||||
Assert.AreEqual(Int64(11), FArray.Count, 'Count should be 11 after adding one more');
|
Assert.AreEqual(Int64(11), FSeries.Count, 'Count should be 11 after adding one more');
|
||||||
Assert.AreEqual(newTime, FArray.Items[0].Time, 'Newest item should be at index 0');
|
Assert.AreEqual(newTime, FSeries.Items[0].Time, 'Newest item should be at index 0');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestAddOrderAssertion;
|
procedure TTestDataSeries.TestAddOrderAssertion;
|
||||||
var
|
var
|
||||||
olderTime: TDateTime;
|
olderTime: TDateTime;
|
||||||
|
DataPoint: TDataPoint<TAskBidItem>;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData; // Newest item is at 2020-07-16
|
SetupSeriesWithData; // Newest item is at 2020-07-16
|
||||||
olderTime := EncodeDate(2020, 7, 15);
|
olderTime := EncodeDate(2020, 7, 15);
|
||||||
@@ -183,15 +179,15 @@ begin
|
|||||||
Assert.WillRaise(
|
Assert.WillRaise(
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
var DataPoint := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(0.0, 0.0));
|
DataPoint := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(0.0, 0.0));
|
||||||
FArray.Add(DataPoint);
|
FSeries.Add([DataPoint], 0, 1);
|
||||||
end,
|
end,
|
||||||
EAssertionFailed,
|
EAssertionFailed,
|
||||||
'Adding item with older timestamp should raise an assert error'
|
'Adding item with older timestamp should raise an assert error'
|
||||||
);
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestGetItemsIndexing;
|
procedure TTestDataSeries.TestGetItemsIndexing;
|
||||||
var
|
var
|
||||||
i: Integer;
|
i: Integer;
|
||||||
expectedTime: TDateTime;
|
expectedTime: TDateTime;
|
||||||
@@ -203,68 +199,22 @@ begin
|
|||||||
for i := 0 to 9 do
|
for i := 0 to 9 do
|
||||||
begin
|
begin
|
||||||
expectedTime := baseTime + (9 - i);
|
expectedTime := baseTime + (9 - i);
|
||||||
Assert.AreEqual(expectedTime, FArray.Items[i].Time, 'Item at logical index should have reversed chronological time');
|
Assert.AreEqual(expectedTime, FSeries.Items[i].Time, 'Item at logical index should have reversed chronological time');
|
||||||
Assert.AreEqual(Single(9 - i), FArray.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
|
Assert.AreEqual(Single(9 - i), FSeries.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestCountIsCappedByMaxLookback_NoTrim;
|
//--------------------------------------------------------------------------------------------------
|
||||||
begin
|
// Tests for bulk Add
|
||||||
SetupSeriesWithData(500, 400); // Less than one chunk
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
// The public Count must now be the MaxLookback value, even though
|
procedure TTestDataSeries.TestBulkAddIncreasesCount;
|
||||||
// no chunk was freed and FCount is still 500 internally.
|
|
||||||
Assert.AreEqual(Int64(400), FArray.Count, 'Public count should be capped by MaxLookback even if no chunk is freed');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataArray.TestCountIsCappedByMaxLookback_WithTrim;
|
|
||||||
const
|
|
||||||
ITEMS_TO_ADD = 2000;
|
|
||||||
LOOKBACK_LIMIT = 900;
|
|
||||||
begin
|
|
||||||
SetupSeriesWithData(ITEMS_TO_ADD, LOOKBACK_LIMIT);
|
|
||||||
|
|
||||||
// Internally, FCount will be 976 after one chunk is freed.
|
|
||||||
// The public Count should still be capped at the lookback limit.
|
|
||||||
Assert.AreEqual(Int64(LOOKBACK_LIMIT), FArray.Count, 'Public count should be capped by MaxLookback after trimming');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataArray.TestLoopIsSafeWithMaxLookback;
|
|
||||||
var
|
|
||||||
dummy: TDataPoint<TAskBidItem>;
|
|
||||||
begin
|
|
||||||
SetupSeriesWithData(500, 400);
|
|
||||||
Assert.AreEqual(Int64(400), FArray.Count, 'Pre-condition: Count must be capped at 400');
|
|
||||||
|
|
||||||
// This test proves that a standard loop based on the public Count is safe
|
|
||||||
// and will not access an invalid index.
|
|
||||||
Assert.WillNotRaise(
|
|
||||||
procedure
|
|
||||||
begin
|
|
||||||
for var i := 0 to FArray.Count - 1 do
|
|
||||||
begin
|
|
||||||
dummy := FArray[i];
|
|
||||||
end;
|
|
||||||
end,
|
|
||||||
nil,
|
|
||||||
'Looping up to the public Count should not raise an exception'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Also test the assert when going one item beyond the public count
|
|
||||||
Assert.WillRaise(
|
|
||||||
procedure begin dummy := FArray[400]; end,
|
|
||||||
EAssertionFailed,
|
|
||||||
'Accessing index at the limit of MaxLookback should raise an exception'
|
|
||||||
);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataArray.TestBulkAddIncreasesCount;
|
|
||||||
var
|
var
|
||||||
newData: TArray<TDataPoint<TAskBidItem>>;
|
newData: TArray<TDataPoint<TAskBidItem>>;
|
||||||
baseTime, newTime: TDateTime;
|
baseTime, newTime: TDateTime;
|
||||||
i: Integer;
|
i: Integer;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData(10);
|
SetupSeriesWithData(10, 15);
|
||||||
baseTime := EncodeDate(2020, 7, 7);
|
baseTime := EncodeDate(2020, 7, 7);
|
||||||
|
|
||||||
SetLength(newData, 5);
|
SetLength(newData, 5);
|
||||||
@@ -274,13 +224,13 @@ begin
|
|||||||
newData[i] := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(i, i));
|
newData[i] := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(i, i));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
FArray.Add(newData);
|
FSeries := FSeries.Add(newData, 0, Length(newData));
|
||||||
|
|
||||||
Assert.AreEqual(Int64(15), FArray.Count, 'Count should be increased by the number of items in the bulk add');
|
Assert.AreEqual(Int64(15), FSeries.Count, 'Count should be increased by the number of items in the bulk add');
|
||||||
Assert.AreEqual(baseTime + 10 + High(newData), FArray[0].Time, 'Newest item should be the last item from the added array');
|
Assert.AreEqual(baseTime + 10 + High(newData), FSeries[0].Time, 'Newest item should be the last item from the added array');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestBulkAddChronologicalAssertion_Internal;
|
procedure TTestDataSeries.TestBulkAddChronologicalAssertion_Internal;
|
||||||
var
|
var
|
||||||
newData: TArray<TDataPoint<TAskBidItem>>;
|
newData: TArray<TDataPoint<TAskBidItem>>;
|
||||||
begin
|
begin
|
||||||
@@ -290,13 +240,13 @@ begin
|
|||||||
newData[1] := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(2, 2)); // Not sorted
|
newData[1] := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(2, 2)); // Not sorted
|
||||||
|
|
||||||
Assert.WillRaise(
|
Assert.WillRaise(
|
||||||
procedure begin FArray.Add(newData); end,
|
procedure begin FSeries.Add(newData, 0, Length(newData)); end,
|
||||||
EAssertionFailed,
|
EAssertionFailed,
|
||||||
'Bulk Add should fail if the input array is not chronologically sorted'
|
'Bulk Add should fail if the input array is not chronologically sorted'
|
||||||
);
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestBulkAddChronologicalAssertion_External;
|
procedure TTestDataSeries.TestBulkAddChronologicalAssertion_External;
|
||||||
var
|
var
|
||||||
newData: TArray<TDataPoint<TAskBidItem>>;
|
newData: TArray<TDataPoint<TAskBidItem>>;
|
||||||
olderTime: TDateTime;
|
olderTime: TDateTime;
|
||||||
@@ -308,13 +258,13 @@ begin
|
|||||||
newData[0] := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(1, 1));
|
newData[0] := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(1, 1));
|
||||||
|
|
||||||
Assert.WillRaise(
|
Assert.WillRaise(
|
||||||
procedure begin FArray.Add(newData); end,
|
procedure begin FSeries.Add(newData, 0, Length(newData)); end,
|
||||||
EAssertionFailed,
|
EAssertionFailed,
|
||||||
'Bulk Add should fail if its first item is older than the series last item'
|
'Bulk Add should fail if its first item is older than the series last item'
|
||||||
);
|
);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestBulkAddSpanningMultipleChunks;
|
procedure TTestDataSeries.TestBulkAddSpanningMultipleChunks;
|
||||||
const
|
const
|
||||||
CHUNK_SIZE = 1024;
|
CHUNK_SIZE = 1024;
|
||||||
var
|
var
|
||||||
@@ -322,8 +272,8 @@ var
|
|||||||
lastTimeBeforeAdd, firstNewTime, lastNewTime: TDateTime;
|
lastTimeBeforeAdd, firstNewTime, lastNewTime: TDateTime;
|
||||||
i: Integer;
|
i: Integer;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData(CHUNK_SIZE - 4); // Almost fill the first chunk
|
SetupSeriesWithData(CHUNK_SIZE - 4, CHUNK_SIZE + 8); // Almost fill the first chunk
|
||||||
lastTimeBeforeAdd := FArray[0].Time;
|
lastTimeBeforeAdd := FSeries[0].Time;
|
||||||
|
|
||||||
SetLength(newData, 8); // Add 8 items, which will span the chunk boundary
|
SetLength(newData, 8); // Add 8 items, which will span the chunk boundary
|
||||||
for i := 0 to High(newData) do
|
for i := 0 to High(newData) do
|
||||||
@@ -334,20 +284,20 @@ begin
|
|||||||
firstNewTime := newData[0].Time;
|
firstNewTime := newData[0].Time;
|
||||||
lastNewTime := newData[High(newData)].Time;
|
lastNewTime := newData[High(newData)].Time;
|
||||||
|
|
||||||
FArray.Add(newData);
|
FSeries := FSeries.Add(newData, 0, Length(newData));
|
||||||
Assert.AreEqual(Int64(CHUNK_SIZE + 4), FArray.Count, 'Count should be correct after spanning a chunk');
|
Assert.AreEqual(Int64(CHUNK_SIZE + 4), FSeries.Count, 'Count should be correct after spanning a chunk');
|
||||||
Assert.AreEqual(lastNewTime, FArray[0].Time, 'Newest item should be correct');
|
Assert.AreEqual(lastNewTime, FSeries[0].Time, 'Newest item should be correct');
|
||||||
Assert.AreEqual(firstNewTime, FArray[7].Time, 'Oldest of the new items should be at the correct logical index');
|
Assert.AreEqual(firstNewTime, FSeries[7].Time, 'Oldest of the new items should be at the correct logical index');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestBulkAddWithMaxLookback;
|
procedure TTestDataSeries.TestBulkAddWithMaxLookback;
|
||||||
var
|
var
|
||||||
newData: TArray<TDataPoint<TAskBidItem>>;
|
newData: TArray<TDataPoint<TAskBidItem>>;
|
||||||
i: Integer;
|
i: Integer;
|
||||||
baseTime: TDateTime;
|
baseTime: TDateTime;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData(400, 500);
|
SetupSeriesWithData(400, 500);
|
||||||
baseTime := FArray[0].Time;
|
baseTime := FSeries[0].Time;
|
||||||
|
|
||||||
SetLength(newData, 200);
|
SetLength(newData, 200);
|
||||||
for i := 0 to High(newData) do
|
for i := 0 to High(newData) do
|
||||||
@@ -355,157 +305,65 @@ begin
|
|||||||
newData[i] := TDataPoint<TAskBidItem>.Create(baseTime + 1 + i, TAskBidItem.Create(i, i));
|
newData[i] := TDataPoint<TAskBidItem>.Create(baseTime + 1 + i, TAskBidItem.Create(i, i));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
FArray.Add(newData);
|
FSeries := FSeries.Add(newData, 0, Length(newData));
|
||||||
|
|
||||||
// Total items would be 600, but MaxLookback is 500.
|
// Total items would be 600, but MaxLookback is 500.
|
||||||
// Trim does not free chunks (100 to remove < 1024).
|
|
||||||
// The public Count must be capped at 500.
|
// The public Count must be capped at 500.
|
||||||
Assert.AreEqual(Int64(500), FArray.Count, 'Count should be capped by MaxLookback after bulk add');
|
Assert.AreEqual(Int64(500), FSeries.Count, 'Count should be capped by MaxLookback after bulk add');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestTotalCountIncrementsCorrectly;
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
// Tests for TotalCount
|
||||||
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestTotalCountIncrementsCorrectly;
|
||||||
var
|
var
|
||||||
newData: TArray<TDataPoint<TAskBidItem>>;
|
newData: TArray<TDataPoint<TAskBidItem>>;
|
||||||
i: Integer;
|
i: Integer;
|
||||||
|
DataPoint: TDataPoint<TAskBidItem>;
|
||||||
begin
|
begin
|
||||||
Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 on creation');
|
Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 on creation');
|
||||||
|
|
||||||
// Test single add
|
// Test single add
|
||||||
FArray.Add(TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(1, 1)));
|
DataPoint := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(1, 1));
|
||||||
Assert.AreEqual(Int64(1), FArray.TotalCount, 'TotalCount should be 1 after single add');
|
FSeries := FSeries.Add([DataPoint], 0, 1);
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.TotalCount, 'TotalCount should be 1 after single add');
|
||||||
|
|
||||||
// Test bulk add
|
// Test bulk add
|
||||||
SetLength(newData, 10);
|
SetLength(newData, 10);
|
||||||
for i := 0 to High(newData) do
|
for i := 0 to High(newData) do
|
||||||
newData[i] := TDataPoint<TAskBidItem>.Create(Now + 1 + i, TAskBidItem.Create(i, i));
|
newData[i] := TDataPoint<TAskBidItem>.Create(Now + 1 + i, TAskBidItem.Create(i, i));
|
||||||
|
|
||||||
FArray.Add(newData);
|
FSeries := FSeries.Add(newData, 0, Length(newData));
|
||||||
Assert.AreEqual(Int64(11), FArray.TotalCount, 'TotalCount should be 11 after bulk add');
|
Assert.AreEqual(Int64(11), FSeries.TotalCount, 'TotalCount should be 11 after bulk add');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestTotalCountIgnoresTrimming;
|
procedure TTestDataSeries.TestTotalCountIgnoresTrimming;
|
||||||
begin
|
begin
|
||||||
// Create series with a lookback that will cause trimming
|
// Create series with a lookback that will cause trimming
|
||||||
SetupSeriesWithData(20, 10);
|
SetupSeriesWithData(20, 10);
|
||||||
|
|
||||||
// The visible count is capped by MaxLookback
|
// The visible count is capped by MaxLookback
|
||||||
Assert.AreEqual(Int64(10), FArray.Count, 'Count should be capped by MaxLookback');
|
Assert.AreEqual(Int64(10), FSeries.Count, 'Count should be capped by MaxLookback');
|
||||||
// The total count should reflect all items that were ever added
|
// The total count should reflect all items that were ever added
|
||||||
Assert.AreEqual(Int64(20), FArray.TotalCount, 'TotalCount must not be affected by trimming');
|
Assert.AreEqual(Int64(20), FSeries.TotalCount, 'TotalCount must not be affected by trimming');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataArray.TestTotalCountResetsOnClear;
|
procedure TTestDataSeries.TestTotalCountResetsOnRecreate;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData(15);
|
SetupSeriesWithData(15);
|
||||||
Assert.IsTrue(FArray.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0');
|
Assert.IsTrue(FSeries.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0');
|
||||||
|
|
||||||
FArray.Clear;
|
// Re-create the series to clear it
|
||||||
|
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(0);
|
||||||
|
|
||||||
Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 after Clear');
|
Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 after re-creation');
|
||||||
Assert.AreEqual(Int64(0), FArray.Count, 'Count should be 0 after Clear');
|
Assert.AreEqual(Int64(0), FSeries.Count, 'Count should be 0 after re-creation');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TTestDataSeries }
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
// Tests for IndexOf
|
||||||
procedure TTestDataSeries.Setup;
|
//--------------------------------------------------------------------------------------------------
|
||||||
begin
|
|
||||||
FSeries := TDataSeries<TAskBidItem>.CreateWriteable(0);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataSeries.Teardown;
|
|
||||||
begin
|
|
||||||
FSeries := Default(TDataSeries<TAskBidItem>);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
|
|
||||||
var
|
|
||||||
i: Integer;
|
|
||||||
DataPoint: TDataPoint<TAskBidItem>;
|
|
||||||
baseTime: TDateTime;
|
|
||||||
begin
|
|
||||||
FSeries := TDataSeries<TAskBidItem>.CreateWriteable(MaxLookBack);
|
|
||||||
baseTime := EncodeDate(2020, 7, 7);
|
|
||||||
|
|
||||||
// Add data points with increasing timestamps.
|
|
||||||
for i := 0 to ItemCount - 1 do
|
|
||||||
begin
|
|
||||||
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
|
|
||||||
FSeries.Add(DataPoint);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataSeries.TestCopyHasSameContent;
|
|
||||||
var
|
|
||||||
copy: TDataSeries<TAskBidItem>;
|
|
||||||
i: Integer;
|
|
||||||
begin
|
|
||||||
SetupSeriesWithData(15);
|
|
||||||
|
|
||||||
copy := FSeries.Immutable;
|
|
||||||
|
|
||||||
Assert.AreEqual(FSeries.Count, copy.Count, 'Copy must have the same count as the original');
|
|
||||||
Assert.AreEqual(FSeries.TotalCount, copy.TotalCount, 'Copy must have the same total count as the original');
|
|
||||||
|
|
||||||
for i := 0 to FSeries.Count - 1 do
|
|
||||||
begin
|
|
||||||
Assert.AreEqual(FSeries[i].Time, copy[i].Time, 'Copied item timestamp must match original');
|
|
||||||
Assert.AreEqual(FSeries[i].Data.Ask, copy[i].Data.Ask, 'Copied item data must match original');
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataSeries.TestCopyIsImmutableAfterOriginalChanges;
|
|
||||||
var
|
|
||||||
copy: TDataSeries<TAskBidItem>;
|
|
||||||
originalCount, originalTotalCount: Int64;
|
|
||||||
newPoint: TDataPoint<TAskBidItem>;
|
|
||||||
lastTime: TDateTime;
|
|
||||||
begin
|
|
||||||
SetupSeriesWithData(10);
|
|
||||||
|
|
||||||
// Create the copy
|
|
||||||
copy := FSeries.Immutable;
|
|
||||||
originalCount := copy.Count;
|
|
||||||
originalTotalCount := copy.TotalCount;
|
|
||||||
|
|
||||||
// Modify the original series
|
|
||||||
lastTime := FSeries[0].Time;
|
|
||||||
newPoint := TDataPoint<TAskBidItem>.Create(lastTime + 1, TAskBidItem.Create(99, 99.1));
|
|
||||||
FSeries.Add(newPoint);
|
|
||||||
|
|
||||||
Assert.AreEqual(Int64(11), FSeries.Count, 'Original array count should have increased');
|
|
||||||
|
|
||||||
// Verify the copy remains unchanged
|
|
||||||
Assert.AreEqual(originalCount, copy.Count, 'Copy count must not change after original is modified');
|
|
||||||
Assert.AreEqual(originalTotalCount, copy.TotalCount, 'Copy total count must not change after original is modified');
|
|
||||||
Assert.AreNotEqual(FSeries[0].Time, copy[0].Time, 'Newest item in copy should not be the new item from original');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataSeries.TestCopyRespectsMaxLookback;
|
|
||||||
var
|
|
||||||
copy: TDataSeries<TAskBidItem>;
|
|
||||||
newPoint: TDataPoint<TAskBidItem>;
|
|
||||||
lastTime: TDateTime;
|
|
||||||
begin
|
|
||||||
SetupSeriesWithData(20, 15); // 20 items total, but series count is capped at 15
|
|
||||||
|
|
||||||
Assert.AreEqual(Int64(15), FSeries.Count, 'Pre-condition: Series count should be capped by MaxLookback');
|
|
||||||
Assert.AreEqual(Int64(20), FSeries.TotalCount, 'Pre-condition: Series total count should be 20');
|
|
||||||
|
|
||||||
copy := FSeries.Immutable;
|
|
||||||
|
|
||||||
// Verify the copy reflects the MaxLookback state
|
|
||||||
Assert.AreEqual(Int64(15), copy.Count, 'Copy count should be the MaxLookback value of the original');
|
|
||||||
Assert.AreEqual(Int64(20), copy.TotalCount, 'Copy total count should be the total items added to the original');
|
|
||||||
|
|
||||||
// Modify original
|
|
||||||
lastTime := FSeries[0].Time;
|
|
||||||
newPoint := TDataPoint<TAskBidItem>.Create(lastTime + 1, TAskBidItem.Create(99, 99.1));
|
|
||||||
FSeries.Add(newPoint);
|
|
||||||
|
|
||||||
// Verify the copy is still unchanged
|
|
||||||
Assert.AreEqual(Int64(15), copy.Count, 'Copy count must remain unchanged after original is modified');
|
|
||||||
Assert.AreEqual(Int64(20), copy.TotalCount, 'Copy total count must remain unchanged after original is modified');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TTestDataSeries.TestIndexOfExistingTimeStamp;
|
procedure TTestDataSeries.TestIndexOfExistingTimeStamp;
|
||||||
var
|
var
|
||||||
@@ -513,14 +371,10 @@ var
|
|||||||
expectedIndex: Int64;
|
expectedIndex: Int64;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData;
|
SetupSeriesWithData;
|
||||||
ATimeStamp := EncodeDate(2020, 7, 7); // Oldest item
|
ATimeStamp := EncodeDate(2020, 7, 7) + 9; // Newest item
|
||||||
expectedIndex := 9;
|
expectedIndex := 0;
|
||||||
|
|
||||||
Assert.AreEqual(
|
Assert.AreEqual(expectedIndex, FSeries.IndexOf(ATimeStamp), 'IndexOf for the newest existing item timestamp should return 0');
|
||||||
expectedIndex,
|
|
||||||
FSeries.IndexOf(ATimeStamp),
|
|
||||||
'IndexOf for the oldest existing item timestamp should return its correct index'
|
|
||||||
);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataSeries.TestIndexOfNonExistingBetween;
|
procedure TTestDataSeries.TestIndexOfNonExistingBetween;
|
||||||
@@ -529,8 +383,11 @@ var
|
|||||||
expectedIndex: Int64;
|
expectedIndex: Int64;
|
||||||
begin
|
begin
|
||||||
SetupSeriesWithData;
|
SetupSeriesWithData;
|
||||||
ATimeStamp := EncodeDate(2020, 7, 7) + 0.5; // 12:00 on the day of the oldest item
|
// Time is between item 8 (2020-07-15) and 9 (2020-07-16)
|
||||||
expectedIndex := Int64(9); // Should find the item from 00:00
|
ATimeStamp := EncodeDate(2020, 7, 15) + 0.5;
|
||||||
|
// Should return the index of the preceding item, which is the one at 2020-07-15.
|
||||||
|
// Its logical index is 1 (0 is newest at 2020-07-16)
|
||||||
|
expectedIndex := Int64(1);
|
||||||
|
|
||||||
Assert.AreEqual(
|
Assert.AreEqual(
|
||||||
expectedIndex,
|
expectedIndex,
|
||||||
@@ -602,7 +459,7 @@ begin
|
|||||||
// Use default setup, but add one item
|
// Use default setup, but add one item
|
||||||
testTime := EncodeDate(2025, 1, 1);
|
testTime := EncodeDate(2025, 1, 1);
|
||||||
DataPoint := TDataPoint<TAskBidItem>.Create(testTime, TAskBidItem.Create(1.0, 2.0));
|
DataPoint := TDataPoint<TAskBidItem>.Create(testTime, TAskBidItem.Create(1.0, 2.0));
|
||||||
FSeries.Add(DataPoint);
|
FSeries := FSeries.Add([DataPoint], 0, 1);
|
||||||
|
|
||||||
Assert.AreEqual(Int64(1), FSeries.Count);
|
Assert.AreEqual(Int64(1), FSeries.Count);
|
||||||
Assert.AreEqual(Int64(0), FSeries.IndexOf(testTime), 'IndexOf for exact single item should be 0');
|
Assert.AreEqual(Int64(0), FSeries.IndexOf(testTime), 'IndexOf for exact single item should be 0');
|
||||||
@@ -620,19 +477,282 @@ begin
|
|||||||
Assert.AreEqual(Int64(400), FSeries.Count, 'Pre-condition: Count must be 400');
|
Assert.AreEqual(Int64(400), FSeries.Count, 'Pre-condition: Count must be 400');
|
||||||
|
|
||||||
// The oldest *physically present* item has a timestamp of baseTime+0.
|
// The oldest *physically present* item has a timestamp of baseTime+0.
|
||||||
// This item is at logical index 499, which is outside the MaxLookback range.
|
// This item is outside the MaxLookback range.
|
||||||
// IndexOf must not find it.
|
// IndexOf must not find it.
|
||||||
searchTime := baseTime; // Time of oldest physical item.
|
searchTime := baseTime; // Time of oldest physical item.
|
||||||
Assert.AreEqual(Int64(-1), FSeries.IndexOf(searchTime), 'IndexOf should not find items outside the MaxLookback range');
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(searchTime), 'IndexOf should not find items outside the MaxLookback range');
|
||||||
|
|
||||||
// The oldest *logically valid* item is at index 399.
|
// The oldest *logically valid* item is at index 399.
|
||||||
// Its physical index is 500 - 399 - 1 = 100.
|
// This item corresponds to the 100th item added (i=100), since 500-400=100 items are trimmed from the start.
|
||||||
// Its timestamp is baseTime + 100.
|
// Its timestamp is baseTime + 100.
|
||||||
searchTime := baseTime + 100;
|
searchTime := baseTime + 100;
|
||||||
Assert.AreEqual(Int64(399), FSeries.IndexOf(searchTime), 'IndexOf should find the oldest valid item at the edge of MaxLookback');
|
Assert.AreEqual(Int64(399), FSeries.IndexOf(searchTime), 'IndexOf should find the oldest valid item at the edge of MaxLookback');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestIndexOfWithTrimmedData;
|
||||||
|
var
|
||||||
|
baseTime, searchTime: TDateTime;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(500, 400); // Oldest visible item has time baseTime+100
|
||||||
|
baseTime := EncodeDate(2020, 7, 7);
|
||||||
|
|
||||||
|
// Search for an exact timestamp that was trimmed
|
||||||
|
searchTime := baseTime + 50; // Added, but now outside the lookback window
|
||||||
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(searchTime), 'IndexOf should not find an exact timestamp that has been trimmed');
|
||||||
|
|
||||||
|
// Search for a non-exact timestamp in the trimmed range
|
||||||
|
searchTime := baseTime + 99.5; // This would resolve to item 99, which is trimmed.
|
||||||
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(searchTime), 'IndexOf should return -1 when the preceding item is trimmed');
|
||||||
|
|
||||||
|
// Search for a non-exact timestamp that should resolve to the oldest visible item
|
||||||
|
searchTime := baseTime + 100.5; // This should resolve to item 100 (logical index 399)
|
||||||
|
Assert.AreEqual(
|
||||||
|
Int64(399),
|
||||||
|
FSeries.IndexOf(searchTime),
|
||||||
|
'IndexOf should find the oldest visible item when searching just after its timestamp'
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
// Tests for Copy (Immutability)
|
||||||
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestCopyHasSameContent;
|
||||||
|
var
|
||||||
|
copy: TDataSeries<TAskBidItem>;
|
||||||
|
i: Integer;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(15);
|
||||||
|
|
||||||
|
copy := FSeries;
|
||||||
|
|
||||||
|
Assert.AreEqual(FSeries.Count, copy.Count, 'Copy must have the same count as the original');
|
||||||
|
Assert.AreEqual(FSeries.TotalCount, copy.TotalCount, 'Copy must have the same total count as the original');
|
||||||
|
|
||||||
|
for i := 0 to FSeries.Count - 1 do
|
||||||
|
begin
|
||||||
|
Assert.AreEqual(FSeries[i].Time, copy[i].Time, 'Copied item timestamp must match original');
|
||||||
|
Assert.AreEqual(FSeries[i].Data.Ask, copy[i].Data.Ask, 'Copied item data must match original');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestCopyIsImmutableAfterOriginalChanges;
|
||||||
|
var
|
||||||
|
copy: TDataSeries<TAskBidItem>;
|
||||||
|
originalCount, originalTotalCount: Int64;
|
||||||
|
newPoint: TDataPoint<TAskBidItem>;
|
||||||
|
lastTime: TDateTime;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(10, 11);
|
||||||
|
|
||||||
|
// Create the copy
|
||||||
|
copy := FSeries;
|
||||||
|
originalCount := copy.Count;
|
||||||
|
originalTotalCount := copy.TotalCount;
|
||||||
|
|
||||||
|
// Modify the original series by creating a new one
|
||||||
|
lastTime := FSeries[0].Time;
|
||||||
|
newPoint := TDataPoint<TAskBidItem>.Create(lastTime + 1, TAskBidItem.Create(99, 99.1));
|
||||||
|
FSeries := FSeries.Add([newPoint], 0, 1);
|
||||||
|
|
||||||
|
Assert.AreEqual(Int64(11), FSeries.Count, 'Original series count should have increased');
|
||||||
|
|
||||||
|
// Verify the copy remains unchanged
|
||||||
|
Assert.AreEqual(originalCount, copy.Count, 'Copy count must not change after original is modified');
|
||||||
|
Assert.AreEqual(originalTotalCount, copy.TotalCount, 'Copy total count must not change after original is modified');
|
||||||
|
Assert.AreNotEqual(FSeries[0].Time, copy[0].Time, 'Newest item in copy should not be the new item from original');
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestCopyRespectsMaxLookback;
|
||||||
|
var
|
||||||
|
copy: TDataSeries<TAskBidItem>;
|
||||||
|
newPoint: TDataPoint<TAskBidItem>;
|
||||||
|
lastTime: TDateTime;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(20, 15); // 20 items total, but series count is capped at 15
|
||||||
|
|
||||||
|
Assert.AreEqual(Int64(15), FSeries.Count, 'Pre-condition: Series count should be capped by MaxLookback');
|
||||||
|
Assert.AreEqual(Int64(20), FSeries.TotalCount, 'Pre-condition: Series total count should be 20');
|
||||||
|
|
||||||
|
copy := FSeries;
|
||||||
|
|
||||||
|
// Verify the copy reflects the MaxLookback state
|
||||||
|
Assert.AreEqual(Int64(15), copy.Count, 'Copy count should be the MaxLookback value of the original');
|
||||||
|
Assert.AreEqual(Int64(20), copy.TotalCount, 'Copy total count should be the total items added to the original');
|
||||||
|
|
||||||
|
// Modify original
|
||||||
|
lastTime := FSeries[0].Time;
|
||||||
|
newPoint := TDataPoint<TAskBidItem>.Create(lastTime + 1, TAskBidItem.Create(99, 99.1));
|
||||||
|
FSeries := FSeries.Add([newPoint], 0, 1);
|
||||||
|
|
||||||
|
// Verify the copy is still unchanged
|
||||||
|
Assert.AreEqual(Int64(15), copy.Count, 'Copy count must remain unchanged after original is modified');
|
||||||
|
Assert.AreEqual(Int64(20), copy.TotalCount, 'Copy total count must remain unchanged after original is modified');
|
||||||
|
end;
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
// Tests for Lookback
|
||||||
|
//--------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestCountIsCappedByMaxLookback_NoTrim;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(500, 400); // Less than one chunk
|
||||||
|
|
||||||
|
// The public Count must now be the MaxLookback value, even though
|
||||||
|
// no chunk was freed and FCount is still 500 internally.
|
||||||
|
Assert.AreEqual(Int64(400), FSeries.Count, 'Public count should be capped by MaxLookback even if no chunk is freed');
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestCountIsCappedByMaxLookback_WithTrim;
|
||||||
|
const
|
||||||
|
ITEMS_TO_ADD = 2000;
|
||||||
|
LOOKBACK_LIMIT = 900;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(ITEMS_TO_ADD, LOOKBACK_LIMIT);
|
||||||
|
|
||||||
|
// Internally, the item count will be trimmed.
|
||||||
|
// The public Count should be capped at the lookback limit.
|
||||||
|
Assert.AreEqual(Int64(LOOKBACK_LIMIT), FSeries.Count, 'Public count should be capped by MaxLookback after trimming');
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestLoopIsSafeWithMaxLookback;
|
||||||
|
var
|
||||||
|
dummy: TDataPoint<TAskBidItem>;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(500, 400);
|
||||||
|
Assert.AreEqual(Int64(400), FSeries.Count, 'Pre-condition: Count must be capped at 400');
|
||||||
|
|
||||||
|
// This test proves that a standard loop based on the public Count is safe
|
||||||
|
// and will not access an invalid index.
|
||||||
|
Assert.WillNotRaise(
|
||||||
|
procedure
|
||||||
|
begin
|
||||||
|
for var i := 0 to FSeries.Count - 1 do
|
||||||
|
begin
|
||||||
|
dummy := FSeries[i];
|
||||||
|
end;
|
||||||
|
end,
|
||||||
|
EAssertionFailed, // Using EAssertionFailed here to catch potential internal range checks
|
||||||
|
'Looping up to the public Count should not raise an exception'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also test the assert when going one item beyond the public count
|
||||||
|
Assert.WillRaise(
|
||||||
|
procedure begin dummy := FSeries[400]; end,
|
||||||
|
EAssertionFailed,
|
||||||
|
'Accessing index at the limit of MaxLookback should raise an exception'
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestDataAndTimePropertiesRespectLookback;
|
||||||
|
var
|
||||||
|
baseTime: TDateTime;
|
||||||
|
expectedOldestTime: TDateTime;
|
||||||
|
expectedOldestAsk: Single;
|
||||||
|
expectedNewestTime: TDateTime;
|
||||||
|
expectedNewestAsk: Single;
|
||||||
|
begin
|
||||||
|
SetupSeriesWithData(20, 15); // Add 20 items, lookback 15.
|
||||||
|
// Visible items are those originally at index 5 through 19.
|
||||||
|
baseTime := EncodeDate(2020, 7, 7);
|
||||||
|
|
||||||
|
// Newest item in series is from i=19. Time = baseTime + 19. Logical index = 0.
|
||||||
|
expectedNewestTime := baseTime + 19;
|
||||||
|
expectedNewestAsk := 19.0;
|
||||||
|
// Oldest visible item is from i=5. Time = baseTime + 5. Logical index = 14.
|
||||||
|
expectedOldestTime := baseTime + 5;
|
||||||
|
expectedOldestAsk := 5.0;
|
||||||
|
|
||||||
|
Assert.AreEqual(Int64(15), FSeries.Count, 'Pre-condition: Count must be 15');
|
||||||
|
|
||||||
|
// Check Data and Time properties at the boundaries
|
||||||
|
Assert.AreEqual(expectedNewestTime, FSeries.Time[0], 'Time[0] should be the newest visible item''s time');
|
||||||
|
Assert.AreEqual(expectedNewestAsk, FSeries.Data[0].Ask, 'Data[0] should be the newest visible item''s data');
|
||||||
|
|
||||||
|
Assert.AreEqual(expectedOldestTime, FSeries.Time[14], 'Time[Count-1] should be the oldest visible item''s time');
|
||||||
|
Assert.AreEqual(expectedOldestAsk, FSeries.Data[14].Ask, 'Data[Count-1] should be the oldest visible item''s data');
|
||||||
|
|
||||||
|
// Check access will fail beyond the lookback-limited count
|
||||||
|
Assert.WillRaise(
|
||||||
|
procedure begin var dummy := FSeries.Data[15]; end,
|
||||||
|
EAssertionFailed,
|
||||||
|
'Accessing Data property beyond public count should fail'
|
||||||
|
);
|
||||||
|
Assert.WillRaise(
|
||||||
|
procedure begin var dummy := FSeries.Time[15]; end,
|
||||||
|
EAssertionFailed,
|
||||||
|
'Accessing Time property beyond public count should fail'
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestLookbackZero;
|
||||||
|
var
|
||||||
|
DataPoint: TDataPoint<TAskBidItem>;
|
||||||
|
begin
|
||||||
|
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(0);
|
||||||
|
Assert.AreEqual(Int64(0), FSeries.Lookback, 'Lookback should be 0');
|
||||||
|
Assert.AreEqual(Int64(0), FSeries.Count, 'Count on new series should be 0');
|
||||||
|
Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount on new series should be 0');
|
||||||
|
|
||||||
|
// Add an item
|
||||||
|
DataPoint := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(1, 1.1));
|
||||||
|
FSeries := FSeries.Add([DataPoint], 0, 1);
|
||||||
|
|
||||||
|
// Count should still be 0, but TotalCount should be 1
|
||||||
|
Assert.AreEqual(Int64(0), FSeries.Count, 'Count must remain 0 when Lookback is 0');
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.TotalCount, 'TotalCount should increment even with Lookback 0');
|
||||||
|
|
||||||
|
// Accessing any item should fail
|
||||||
|
Assert.WillRaise(
|
||||||
|
procedure begin var dummy := FSeries[0]; end,
|
||||||
|
EAssertionFailed,
|
||||||
|
'Accessing item[0] on a series with Lookback=0 should raise an error'
|
||||||
|
);
|
||||||
|
|
||||||
|
// IndexOf should also find nothing
|
||||||
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(Now), 'IndexOf on a series with Lookback=0 should return -1');
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TTestDataSeries.TestLookbackOne;
|
||||||
|
var
|
||||||
|
DataPoint1, DataPoint2, DataPoint3: TDataPoint<TAskBidItem>;
|
||||||
|
time1, time2, time3: TDateTime;
|
||||||
|
begin
|
||||||
|
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(1);
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.Lookback, 'Lookback should be 1');
|
||||||
|
|
||||||
|
// Add first item
|
||||||
|
time1 := Now;
|
||||||
|
DataPoint1 := TDataPoint<TAskBidItem>.Create(time1, TAskBidItem.Create(1, 1.1));
|
||||||
|
FSeries := FSeries.Add([DataPoint1], 0, 1);
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.Count, 'Count should be 1 after one add');
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.TotalCount, 'TotalCount should be 1');
|
||||||
|
Assert.AreEqual(time1, FSeries[0].Time, 'The first item should be at index 0');
|
||||||
|
|
||||||
|
// Add second item
|
||||||
|
time2 := time1 + EncodeTime(0, 0, 1, 0);
|
||||||
|
DataPoint2 := TDataPoint<TAskBidItem>.Create(time2, TAskBidItem.Create(2, 2.1));
|
||||||
|
FSeries := FSeries.Add([DataPoint2], 0, 1);
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.Count, 'Count must be capped at 1');
|
||||||
|
Assert.AreEqual(Int64(2), FSeries.TotalCount, 'TotalCount should be 2');
|
||||||
|
Assert.AreEqual(time2, FSeries[0].Time, 'The newest item should now be at index 0');
|
||||||
|
|
||||||
|
// Add third item
|
||||||
|
time3 := time2 + EncodeTime(0, 0, 1, 0);
|
||||||
|
DataPoint3 := TDataPoint<TAskBidItem>.Create(time3, TAskBidItem.Create(3, 3.1));
|
||||||
|
FSeries := FSeries.Add([DataPoint3], 0, 1);
|
||||||
|
Assert.AreEqual(Int64(1), FSeries.Count, 'Count must still be capped at 1');
|
||||||
|
Assert.AreEqual(Int64(3), FSeries.TotalCount, 'TotalCount should be 3');
|
||||||
|
Assert.AreEqual(time3, FSeries[0].Time, 'The third item should now be at index 0');
|
||||||
|
|
||||||
|
// Accessing index 1 should fail
|
||||||
|
Assert.WillRaise(
|
||||||
|
procedure begin var dummy := FSeries[1]; end,
|
||||||
|
EAssertionFailed,
|
||||||
|
'Accessing item[1] on a series with Lookback=1 should raise an error'
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
initialization
|
initialization
|
||||||
TDUnitX.RegisterTestFixture(TTestDataArray);
|
|
||||||
TDUnitX.RegisterTestFixture(TTestDataSeries);
|
TDUnitX.RegisterTestFixture(TTestDataSeries);
|
||||||
end.
|
end.
|
||||||
|
|||||||
+436
-218
@@ -3,11 +3,13 @@ unit Myc.Trade.Core.DataPoint;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
|
System.Generics.Collections,
|
||||||
|
System.TimeSpan,
|
||||||
Myc.Trade.DataPoint;
|
Myc.Trade.DataPoint;
|
||||||
|
|
||||||
type
|
type
|
||||||
// The implementation class for IDataSeries<T>.
|
TMycDataArray<T> = record
|
||||||
TDataArray<T> = class(TInterfacedObject, IDataSeries<T>, IDataSeriesWriter<T>)
|
private
|
||||||
const
|
const
|
||||||
ChunkSize = 1024;
|
ChunkSize = 1024;
|
||||||
type
|
type
|
||||||
@@ -15,26 +17,34 @@ type
|
|||||||
private
|
private
|
||||||
FChunks: TArray<TChunk>;
|
FChunks: TArray<TChunk>;
|
||||||
FCount: Int64;
|
FCount: Int64;
|
||||||
FLookback: Int64;
|
|
||||||
FTotalCount: Int64;
|
|
||||||
|
|
||||||
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
|
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
|
||||||
function IsIndexInLimits(LogicalIndex: Int64): Boolean;
|
function GetItems(Idx: Int64): TDataPoint<T>; inline;
|
||||||
procedure Trim;
|
public
|
||||||
|
constructor Create(const AChunks: TArray<TChunk>; ACount: Int64);
|
||||||
|
function Add(const Data: array of TDataPoint<T>; First, Count, Lookback: Int64): TMycDataArray<T>;
|
||||||
|
class function CreateEmpty: TMycDataArray<T>; static;
|
||||||
|
// Helper to create a data array from a raw TArray.
|
||||||
|
class function CreateFromArray(const AData: TArray<TDataPoint<T>>; First, Count: Integer): TMycDataArray<T>; static;
|
||||||
|
property Count: Int64 read FCount;
|
||||||
|
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
||||||
|
end;
|
||||||
|
|
||||||
// IDataSeries<T> implementation
|
// The implementation class for IDataSeries<T>.
|
||||||
|
TMycDataSeries<T> = class(TInterfacedObject, IDataSeries<T>)
|
||||||
|
private
|
||||||
|
FData: TMycDataArray<T>;
|
||||||
|
FLookback: Int64;
|
||||||
|
FTotalCount: Int64;
|
||||||
function GetCount: Int64;
|
function GetCount: Int64;
|
||||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
function GetLookback: Int64;
|
function GetLookback: Int64;
|
||||||
function GetTotalCount: Int64;
|
function GetTotalCount: Int64;
|
||||||
function GetWriter: IDataSeriesWriter<T>;
|
|
||||||
public
|
public
|
||||||
constructor Create(ALookback: Int64);
|
constructor Create(ALookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64);
|
||||||
procedure Add(const Data: TDataPoint<T>); overload;
|
destructor Destroy; override;
|
||||||
procedure Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1); overload;
|
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
procedure Clear;
|
class function CreateDataSeries(Lookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64): IDataSeries<T>; static;
|
||||||
function Immutable: IDataSeries<T>;
|
|
||||||
property Lookback: Int64 read GetLookback;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Null object implementation for IDataSeries<T>
|
// Null object implementation for IDataSeries<T>
|
||||||
@@ -43,217 +53,247 @@ type
|
|||||||
class var
|
class var
|
||||||
FNull: IDataSeries<T>;
|
FNull: IDataSeries<T>;
|
||||||
private
|
private
|
||||||
class constructor CreateClass;
|
FTotalCount: Int64;
|
||||||
function GetWriter: IDataSeriesWriter<T>;
|
|
||||||
public
|
|
||||||
function GetCount: Int64;
|
function GetCount: Int64;
|
||||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
function GetTotalCount: Int64;
|
function GetTotalCount: Int64;
|
||||||
function IndexOf(TimeStamp: TDateTime): Int64;
|
function GetLookback: Int64;
|
||||||
function Immutable: IDataSeries<T>;
|
class constructor CreateClass;
|
||||||
|
public
|
||||||
|
constructor Create(ATotalCount: Int64);
|
||||||
|
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
class property Null: IDataSeries<T> read FNull;
|
class property Null: IDataSeries<T> read FNull;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// The implementation class for IDataSeries<T>.
|
// A virtual series that combines a base series and an array of new data without copying.
|
||||||
TStaticDataSeries<T> = class(TInterfacedObject, IDataSeries<T>)
|
TCompositeDataSeries<T> = class(TInterfacedObject, IDataSeries<T>)
|
||||||
private
|
private
|
||||||
FChunks: TArray<TDataArray<T>.TChunk>;
|
FBaseSeries: IDataSeries<T>;
|
||||||
FCount: Int64;
|
FAddedData: TMycDataArray<T>;
|
||||||
FLookback: Int64;
|
FLookback: Int64;
|
||||||
FTotalCount: Int64;
|
FCount: Int64;
|
||||||
|
|
||||||
function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline;
|
|
||||||
function IsIndexInLimits(LogicalIndex: Int64): Boolean;
|
|
||||||
|
|
||||||
// IDataSeries<T> implementation
|
|
||||||
function GetCount: Int64;
|
function GetCount: Int64;
|
||||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
function GetLookback: Int64;
|
function GetLookback: Int64;
|
||||||
function GetTotalCount: Int64;
|
function GetTotalCount: Int64;
|
||||||
function GetWriter: IDataSeriesWriter<T>;
|
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create(const AChunks: TArray<TDataArray<T>.TChunk>; ACount, ALookback, ATotalCount: Int64);
|
constructor Create(const ABaseSeries: IDataSeries<T>; const AAddedData: TMycDataArray<T>);
|
||||||
function Immutable: IDataSeries<T>;
|
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
property Lookback: Int64 read GetLookback;
|
class function CreateComposite(
|
||||||
|
const BaseSeries: IDataSeries<T>;
|
||||||
|
const Data: TArray<TDataPoint<T>>;
|
||||||
|
First, Count: Integer
|
||||||
|
): IDataSeries<T>; static;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TConvertSeries<T, S> = class(TInterfacedObject, IDataSeries<S>)
|
||||||
|
private
|
||||||
|
FSource: IDataSeries<T>;
|
||||||
|
FConvertFunc: TDataSeries<T>.TConvertFunc<S>;
|
||||||
|
function GetCount: Int64;
|
||||||
|
function GetItems(Idx: Int64): TDataPoint<S>;
|
||||||
|
function GetLookback: Int64;
|
||||||
|
function GetTotalCount: Int64;
|
||||||
|
public
|
||||||
|
constructor Create(const ASource: IDataSeries<T>; const AConvertFunc: TDataSeries<T>.TConvertFunc<S>);
|
||||||
|
function Add(const Data: TArray<TDataPoint<S>>; First, Count: Integer): IDataSeries<S>;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TAggregateDataSeries<T, S> = class(TInterfacedObject, IDataSeries<S>)
|
||||||
|
public
|
||||||
|
// Defines the function signature for aggregating a set of source data points into a single target value.
|
||||||
|
type
|
||||||
|
TAggregateFunc = reference to function(const ASourcePoints: TArray<TDataPoint<T>>): S;
|
||||||
|
private
|
||||||
|
FSource: IDataSeries<T>;
|
||||||
|
FTimeFrame: TDateTime;
|
||||||
|
FAggregateFunc: TAggregateFunc;
|
||||||
|
FCachedItems: TDictionary<Int64, TDataPoint<S>>;
|
||||||
|
FBaseTime: TDateTime;
|
||||||
|
FAggregatedCount: Int64;
|
||||||
|
function GetCount: Int64;
|
||||||
|
function GetItems(Idx: Int64): TDataPoint<S>;
|
||||||
|
function GetLookback: Int64;
|
||||||
|
function GetTotalCount: Int64;
|
||||||
|
procedure CalculateAggregatedCount;
|
||||||
|
public
|
||||||
|
constructor Create(const ASource: IDataSeries<T>; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc);
|
||||||
|
destructor Destroy; override;
|
||||||
|
function Add(const Data: TArray<TDataPoint<S>>; First, Count: Integer): IDataSeries<S>;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{ TDataArray<T> }
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
System.Math;
|
||||||
|
|
||||||
constructor TDataArray<T>.Create(ALookback: Int64);
|
{ TMycDataArray<T> }
|
||||||
|
|
||||||
|
constructor TMycDataArray<T>.Create(const AChunks: TArray<TChunk>; ACount: Int64);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
FChunks := AChunks;
|
||||||
FLookback := ALookback;
|
FCount := ACount;
|
||||||
FCount := 0;
|
|
||||||
FTotalCount := 0;
|
|
||||||
FChunks := nil;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataArray<T>.Add(const Data: TDataPoint<T>);
|
class function TMycDataArray<T>.CreateEmpty: TMycDataArray<T>;
|
||||||
begin
|
begin
|
||||||
Assert((FCount = 0) or (Data.Time >= GetItems(0).Time), 'Time stamp older than last item');
|
Result.FChunks := nil;
|
||||||
|
Result.FCount := 0;
|
||||||
var ci := FCount div ChunkSize;
|
|
||||||
var di := FCount mod ChunkSize;
|
|
||||||
|
|
||||||
if (di = 0) then
|
|
||||||
begin
|
|
||||||
SetLength(FChunks, ci + 1);
|
|
||||||
SetLength(FChunks[ci], ChunkSize);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
FChunks[ci][di] := Data;
|
class function TMycDataArray<T>.CreateFromArray(const AData: TArray<TDataPoint<T>>; First, Count: Integer): TMycDataArray<T>;
|
||||||
Inc(FCount);
|
begin
|
||||||
Inc(FTotalCount);
|
// Use the Add method on an empty array to perform the chunking logic.
|
||||||
|
Result := CreateEmpty.Add(AData, First, Count, Count);
|
||||||
Trim;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataArray<T>.Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1);
|
function TMycDataArray<T>.Add(const Data: array of TDataPoint<T>; First, Count, Lookback: Int64): TMycDataArray<T>;
|
||||||
var
|
var
|
||||||
sourceIdx, itemsToCopy, spaceInChunk: Integer;
|
destPhysicalIdx, sourcePhysicalIdx: Int64;
|
||||||
ci, di: Int64;
|
itemsToSkip: Int64;
|
||||||
i: Integer;
|
numNewChunks: Integer;
|
||||||
resolvedNumToAdd: Integer;
|
newChunks: TArray<TChunk>;
|
||||||
|
destChunkIdx, destSubIdx: Integer;
|
||||||
|
sumCount, newCount: Int64;
|
||||||
begin
|
begin
|
||||||
if NumToAdd < 0 then
|
if Count < 0 then
|
||||||
resolvedNumToAdd := Length(Data)
|
Count := Length(Data) - First;
|
||||||
else
|
if (Lookback <= 0) or (Count = 0) then
|
||||||
resolvedNumToAdd := NumToAdd;
|
exit(Self);
|
||||||
|
|
||||||
if resolvedNumToAdd = 0 then
|
Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array');
|
||||||
Exit;
|
for var i := First + 1 to First + Count - 1 do
|
||||||
|
|
||||||
Assert(resolvedNumToAdd <= Length(Data), 'NumToAdd cannot be larger than the source array');
|
|
||||||
|
|
||||||
for i := 1 to resolvedNumToAdd - 1 do
|
|
||||||
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
|
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
|
||||||
|
if FCount > 0 then
|
||||||
|
Assert(Data[First].Time >= Self.Items[0].Time, 'First new item is older than last existing item');
|
||||||
|
|
||||||
Assert((FCount = 0) or (Data[0].Time >= GetItems(0).Time), 'First new item is older than last existing item');
|
sumCount := FCount + Count;
|
||||||
|
newCount := sumCount;
|
||||||
|
if (Lookback > 0) and (newCount > Lookback) then
|
||||||
|
newCount := Lookback;
|
||||||
|
itemsToSkip := sumCount - newCount;
|
||||||
|
|
||||||
Inc(FTotalCount, resolvedNumToAdd);
|
numNewChunks := 0;
|
||||||
|
if newCount > 0 then
|
||||||
|
numNewChunks := (newCount - 1) div ChunkSize + 1;
|
||||||
|
SetLength(newChunks, numNewChunks);
|
||||||
|
|
||||||
var newTotalCount := FCount + resolvedNumToAdd;
|
for destPhysicalIdx := 0 to newCount - 1 do
|
||||||
var requiredChunks := (newTotalCount + ChunkSize - 1) div ChunkSize;
|
|
||||||
if requiredChunks = 0 then
|
|
||||||
requiredChunks := 1;
|
|
||||||
|
|
||||||
if requiredChunks > Length(FChunks) then
|
|
||||||
SetLength(FChunks, requiredChunks);
|
|
||||||
|
|
||||||
sourceIdx := 0;
|
|
||||||
while sourceIdx < resolvedNumToAdd do
|
|
||||||
begin
|
begin
|
||||||
ci := FCount div ChunkSize;
|
destChunkIdx := destPhysicalIdx div ChunkSize;
|
||||||
di := FCount mod ChunkSize;
|
destSubIdx := destPhysicalIdx mod ChunkSize;
|
||||||
|
|
||||||
if di = 0 then
|
if destSubIdx = 0 then
|
||||||
SetLength(FChunks[ci], ChunkSize);
|
SetLength(newChunks[destChunkIdx], ChunkSize);
|
||||||
|
|
||||||
spaceInChunk := ChunkSize - di;
|
sourcePhysicalIdx := itemsToSkip + destPhysicalIdx;
|
||||||
itemsToCopy := resolvedNumToAdd - sourceIdx;
|
|
||||||
if spaceInChunk < itemsToCopy then
|
|
||||||
itemsToCopy := spaceInChunk;
|
|
||||||
|
|
||||||
System.Move(Data[sourceIdx], FChunks[ci][di], itemsToCopy * SizeOf(TDataPoint<T>));
|
if sourcePhysicalIdx < FCount then
|
||||||
|
|
||||||
Inc(FCount, itemsToCopy);
|
|
||||||
Inc(sourceIdx, itemsToCopy);
|
|
||||||
|
|
||||||
Trim;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataArray<T>.Clear;
|
|
||||||
begin
|
begin
|
||||||
FChunks := nil;
|
newChunks[destChunkIdx][destSubIdx] := FChunks[sourcePhysicalIdx div ChunkSize][sourcePhysicalIdx mod ChunkSize];
|
||||||
FCount := 0;
|
end
|
||||||
FTotalCount := 0;
|
else
|
||||||
end;
|
|
||||||
|
|
||||||
function TDataArray<T>.Immutable: IDataSeries<T>;
|
|
||||||
begin
|
begin
|
||||||
Result := TStaticDataSeries<T>.Create(FChunks, FCount, FLookback, FTotalCount);
|
newChunks[destChunkIdx][destSubIdx] := Data[First + (sourcePhysicalIdx - FCount)];
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataArray<T>.GetCount: Int64;
|
Result := TMycDataArray<T>.Create(newChunks, newCount);
|
||||||
begin
|
|
||||||
Result := FCount;
|
|
||||||
if (FLookback > 0) and (Result > FLookback) then
|
|
||||||
Result := FLookback;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataArray<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
function TMycDataArray<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
var
|
var
|
||||||
physicalIndex: Int64;
|
physicalIndex: Int64;
|
||||||
begin
|
begin
|
||||||
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
|
Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.');
|
||||||
physicalIndex := LogicalToPhysicalIndex(Idx);
|
physicalIndex := LogicalToPhysicalIndex(Idx);
|
||||||
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
|
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataArray<T>.GetLookback: Int64;
|
function TMycDataArray<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
|
||||||
|
begin
|
||||||
|
Result := FCount - LogicalIndex - 1;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TMycDataSeries<T> }
|
||||||
|
|
||||||
|
constructor TMycDataSeries<T>.Create(ALookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FLookback := ALookback;
|
||||||
|
FData := AData;
|
||||||
|
FTotalCount := ATotalCount;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TMycDataSeries<T>.Destroy;
|
||||||
|
begin
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TMycDataSeries<T>.CreateDataSeries(Lookback: Int64; const AData: TMycDataArray<T>; ATotalCount: Int64): IDataSeries<T>;
|
||||||
|
begin
|
||||||
|
if Lookback > 0 then
|
||||||
|
Result := TMycDataSeries<T>.Create(Lookback, AData, ATotalCount)
|
||||||
|
else
|
||||||
|
Result := TNullDataSeries<T>.Null;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TMycDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
|
var
|
||||||
|
newData: TMycDataArray<T>;
|
||||||
|
newTotalCount: Int64;
|
||||||
|
begin
|
||||||
|
if Count < 0 then
|
||||||
|
Count := Length(Data) - First;
|
||||||
|
newData := FData.Add(Data, First, Count, FLookback);
|
||||||
|
newTotalCount := FTotalCount + Count;
|
||||||
|
Result := TMycDataSeries<T>.Create(FLookback, newData, newTotalCount);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TMycDataSeries<T>.GetCount: Int64;
|
||||||
|
begin
|
||||||
|
Result := FData.Count;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TMycDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
|
begin
|
||||||
|
Assert((Idx >= 0) and (Idx < FData.Count), 'Index is out of bounds.');
|
||||||
|
Result := FData.Items[Idx];
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TMycDataSeries<T>.GetLookback: Int64;
|
||||||
begin
|
begin
|
||||||
Result := FLookback;
|
Result := FLookback;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataArray<T>.GetTotalCount: Int64;
|
function TMycDataSeries<T>.GetTotalCount: Int64;
|
||||||
begin
|
begin
|
||||||
Result := FTotalCount;
|
Result := FTotalCount;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataArray<T>.GetWriter: IDataSeriesWriter<T>;
|
{ TNullDataSeries<T> }
|
||||||
begin
|
|
||||||
Result := Self;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TDataArray<T>.IsIndexInLimits(LogicalIndex: Int64): Boolean;
|
|
||||||
begin
|
|
||||||
Result := (FLookback <= 0) or (LogicalIndex < FLookback);
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TDataArray<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
|
|
||||||
begin
|
|
||||||
Assert((LogicalIndex >= 0) and (LogicalIndex < FCount), 'Logical index is out of bounds.');
|
|
||||||
Result := FCount - LogicalIndex - 1;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataArray<T>.Trim;
|
|
||||||
var
|
|
||||||
itemsToRemove, chunksToRemove: Int64;
|
|
||||||
begin
|
|
||||||
if (FCount = 0) or (FLookback <= 0) then
|
|
||||||
Exit;
|
|
||||||
|
|
||||||
itemsToRemove := 0;
|
|
||||||
if FCount > FLookback then
|
|
||||||
itemsToRemove := FCount - FLookback;
|
|
||||||
|
|
||||||
if itemsToRemove <= 0 then
|
|
||||||
Exit;
|
|
||||||
|
|
||||||
chunksToRemove := itemsToRemove div ChunkSize;
|
|
||||||
|
|
||||||
if chunksToRemove > 0 then
|
|
||||||
begin
|
|
||||||
var itemsInFreedChunks := chunksToRemove * ChunkSize;
|
|
||||||
FChunks := Copy(FChunks, chunksToRemove, Length(FChunks) - chunksToRemove);
|
|
||||||
FCount := FCount - itemsInFreedChunks;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TNullDataSeries<T> Interface Helper }
|
|
||||||
|
|
||||||
class constructor TNullDataSeries<T>.CreateClass;
|
class constructor TNullDataSeries<T>.CreateClass;
|
||||||
begin
|
begin
|
||||||
FNull := TNullDataSeries<T>.Create;
|
FNull := TNullDataSeries<T>.Create(0);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TNullDataSeries<T>.Immutable: IDataSeries<T>;
|
constructor TNullDataSeries<T>.Create(ATotalCount: Int64);
|
||||||
begin
|
begin
|
||||||
Result := FNull;
|
inherited Create;
|
||||||
|
FTotalCount := ATotalCount;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TNullDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
|
begin
|
||||||
|
if Count < 0 then
|
||||||
|
Count := Length(Data) - First;
|
||||||
|
|
||||||
|
if Count > 0 then
|
||||||
|
Result := TNullDataSeries<T>.Create(FTotalCount + Count)
|
||||||
|
else
|
||||||
|
Result := Self;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TNullDataSeries<T>.GetCount: Int64;
|
function TNullDataSeries<T>.GetCount: Int64;
|
||||||
@@ -263,81 +303,259 @@ end;
|
|||||||
|
|
||||||
function TNullDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
function TNullDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
begin
|
begin
|
||||||
Assert(false, 'Index out of bounds.');
|
Assert(false, 'Data series is empty.');
|
||||||
Result := Default(TDataPoint<T>);
|
Result := Default(TDataPoint<T>);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TNullDataSeries<T>.GetTotalCount: Int64;
|
function TNullDataSeries<T>.GetLookback: Int64;
|
||||||
begin
|
begin
|
||||||
Result := 0;
|
Result := 0;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TNullDataSeries<T>.GetWriter: IDataSeriesWriter<T>;
|
function TNullDataSeries<T>.GetTotalCount: Int64;
|
||||||
begin
|
|
||||||
Result := nil;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TNullDataSeries<T>.IndexOf(TimeStamp: TDateTime): Int64;
|
|
||||||
begin
|
|
||||||
Result := -1;
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TStaticDataSeries<T> }
|
|
||||||
|
|
||||||
constructor TStaticDataSeries<T>.Create(const AChunks: TArray<TDataArray<T>.TChunk>; ACount, ALookback, ATotalCount: Int64);
|
|
||||||
begin
|
|
||||||
inherited Create;
|
|
||||||
FChunks := AChunks;
|
|
||||||
FCount := ACount;
|
|
||||||
FLookback := ALookback;
|
|
||||||
FTotalCount := ATotalCount;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TStaticDataSeries<T>.Immutable: IDataSeries<T>;
|
|
||||||
begin
|
|
||||||
Result := Self;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TStaticDataSeries<T>.GetCount: Int64;
|
|
||||||
begin
|
|
||||||
Result := FCount;
|
|
||||||
if (FLookback > 0) and (Result > FLookback) then
|
|
||||||
Result := FLookback;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TStaticDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
|
||||||
var
|
|
||||||
physicalIndex: Int64;
|
|
||||||
begin
|
|
||||||
Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.');
|
|
||||||
physicalIndex := LogicalToPhysicalIndex(Idx);
|
|
||||||
Result := FChunks[physicalIndex div TDataArray<T>.ChunkSize][physicalIndex mod TDataArray<T>.ChunkSize];
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TStaticDataSeries<T>.GetLookback: Int64;
|
|
||||||
begin
|
|
||||||
Result := FLookback;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TStaticDataSeries<T>.GetTotalCount: Int64;
|
|
||||||
begin
|
begin
|
||||||
Result := FTotalCount;
|
Result := FTotalCount;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TStaticDataSeries<T>.GetWriter: IDataSeriesWriter<T>;
|
{ TCompositeDataSeries<T> }
|
||||||
|
|
||||||
|
constructor TCompositeDataSeries<T>.Create(const ABaseSeries: IDataSeries<T>; const AAddedData: TMycDataArray<T>);
|
||||||
begin
|
begin
|
||||||
Result := nil;
|
inherited Create;
|
||||||
|
FBaseSeries := ABaseSeries;
|
||||||
|
FAddedData := AAddedData;
|
||||||
|
FLookback := FBaseSeries.Lookback;
|
||||||
|
Assert(FLookback > 0);
|
||||||
|
|
||||||
|
FCount := FBaseSeries.Count + FAddedData.Count;
|
||||||
|
if FCount > FLookback then
|
||||||
|
FCount := FLookback;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TStaticDataSeries<T>.IsIndexInLimits(LogicalIndex: Int64): Boolean;
|
function TCompositeDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
|
var
|
||||||
|
newAddedData: TMycDataArray<T>;
|
||||||
|
itemsInBase: Int64;
|
||||||
|
lookbackForAdd: Int64;
|
||||||
begin
|
begin
|
||||||
Result := (FLookback <= 0) or (LogicalIndex < FLookback);
|
if Count < 0 then
|
||||||
|
Count := Length(Data) - First;
|
||||||
|
if Count = 0 then
|
||||||
|
exit(Self);
|
||||||
|
|
||||||
|
itemsInBase := FBaseSeries.Count;
|
||||||
|
lookbackForAdd := FLookback - itemsInBase;
|
||||||
|
if lookbackForAdd < 0 then
|
||||||
|
lookbackForAdd := 0;
|
||||||
|
|
||||||
|
newAddedData := FAddedData.Add(Data, First, Count, lookbackForAdd);
|
||||||
|
|
||||||
|
// Optimization: If the added data fills the entire lookback window,
|
||||||
|
// the base series is no longer relevant. We can return a simpler TMycDataSeries.
|
||||||
|
if newAddedData.Count >= FLookback then
|
||||||
|
begin
|
||||||
|
Result := TMycDataSeries<T>.Create(FLookback, newAddedData, GetTotalCount + Count);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Result := TCompositeDataSeries<T>.Create(FBaseSeries, newAddedData);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TStaticDataSeries<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
|
class function TCompositeDataSeries<T>.CreateComposite(
|
||||||
|
const BaseSeries: IDataSeries<T>;
|
||||||
|
const Data: TArray<TDataPoint<T>>;
|
||||||
|
First, Count: Integer
|
||||||
|
): IDataSeries<T>;
|
||||||
begin
|
begin
|
||||||
Assert((LogicalIndex >= 0) and (LogicalIndex < FCount), 'Logical index is out of bounds.');
|
if Count < 0 then
|
||||||
Result := FCount - LogicalIndex - 1;
|
Count := Length(Data) - First;
|
||||||
|
if Count = 0 then
|
||||||
|
exit(BaseSeries);
|
||||||
|
|
||||||
|
Result := TCompositeDataSeries<T>.Create(BaseSeries, TMycDataArray<T>.CreateFromArray(Data, First, Count));
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCompositeDataSeries<T>.GetCount: Int64;
|
||||||
|
begin
|
||||||
|
Result := FCount;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCompositeDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
|
var
|
||||||
|
addedCount: Int64;
|
||||||
|
begin
|
||||||
|
Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.');
|
||||||
|
addedCount := FAddedData.Count;
|
||||||
|
if Idx < addedCount then
|
||||||
|
begin
|
||||||
|
Result := FAddedData.Items[Idx];
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Result := FBaseSeries.Items[Idx - addedCount];
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCompositeDataSeries<T>.GetLookback: Int64;
|
||||||
|
begin
|
||||||
|
Result := FLookback;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TCompositeDataSeries<T>.GetTotalCount: Int64;
|
||||||
|
begin
|
||||||
|
Result := FBaseSeries.TotalCount + FAddedData.Count;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TConvertSeries<T, S> }
|
||||||
|
|
||||||
|
constructor TConvertSeries<T, S>.Create(const ASource: IDataSeries<T>; const AConvertFunc: TDataSeries<T>.TConvertFunc<S>);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FSource := ASource;
|
||||||
|
FConvertFunc := AConvertFunc;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TConvertSeries<T, S>.Add(const Data: TArray<TDataPoint<S>>; First, Count: Integer): IDataSeries<S>;
|
||||||
|
begin
|
||||||
|
Result := TCompositeDataSeries<S>.CreateComposite(Self, Data, First, Count);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TConvertSeries<T, S>.GetCount: Int64;
|
||||||
|
begin
|
||||||
|
Result := FSource.Count;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TConvertSeries<T, S>.GetItems(Idx: Int64): TDataPoint<S>;
|
||||||
|
var
|
||||||
|
P: TDataPoint<T>;
|
||||||
|
begin
|
||||||
|
P := FSource[Idx];
|
||||||
|
Result.Create(P.Time, FConvertFunc(P));
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TConvertSeries<T, S>.GetLookback: Int64;
|
||||||
|
begin
|
||||||
|
Result := FSource.Lookback;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TConvertSeries<T, S>.GetTotalCount: Int64;
|
||||||
|
begin
|
||||||
|
Result := FSource.TotalCount;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TAggregateDataSeries<T, S> }
|
||||||
|
|
||||||
|
constructor TAggregateDataSeries<T, S>.Create(const ASource: IDataSeries<T>; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FSource := ASource;
|
||||||
|
FTimeFrame := ATimeFrame;
|
||||||
|
FAggregateFunc := AAggregateFunc;
|
||||||
|
FCachedItems := TDictionary<Int64, TDataPoint<S>>.Create;
|
||||||
|
FAggregatedCount := -1; // -1 indicates that it has not been calculated yet
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TAggregateDataSeries<T, S>.Destroy;
|
||||||
|
begin
|
||||||
|
FCachedItems.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TAggregateDataSeries<T, S>.CalculateAggregatedCount;
|
||||||
|
var
|
||||||
|
totalTimeSpan: Double;
|
||||||
|
begin
|
||||||
|
if FSource.Count = 0 then
|
||||||
|
begin
|
||||||
|
FBaseTime := 0;
|
||||||
|
FAggregatedCount := 0;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
// The timestamp of the oldest element serves as the anchor for our time grid.
|
||||||
|
FBaseTime := FSource.Items[FSource.Count - 1].Time;
|
||||||
|
// Total duration covered by the source series.
|
||||||
|
totalTimeSpan := FSource.Items[0].Time - FBaseTime;
|
||||||
|
if totalTimeSpan >= 0 then
|
||||||
|
FAggregatedCount := Trunc(totalTimeSpan / FTimeFrame) + 1
|
||||||
|
else
|
||||||
|
FAggregatedCount := 0;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAggregateDataSeries<T, S>.Add(const Data: TArray<TDataPoint<S>>; First, Count: Integer): IDataSeries<S>;
|
||||||
|
begin
|
||||||
|
// Adding to an aggregated series is complex.
|
||||||
|
// The most straightforward approach is to create a composite series.
|
||||||
|
Result := TCompositeDataSeries<S>.CreateComposite(Self, Data, First, Count);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAggregateDataSeries<T, S>.GetCount: Int64;
|
||||||
|
begin
|
||||||
|
if FAggregatedCount = -1 then
|
||||||
|
CalculateAggregatedCount;
|
||||||
|
Result := FAggregatedCount;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAggregateDataSeries<T, S>.GetItems(Idx: Int64): TDataPoint<S>;
|
||||||
|
var
|
||||||
|
startTime, endTime: TDateTime;
|
||||||
|
sourcePoints: TList<TDataPoint<T>>;
|
||||||
|
sourceIdx: Int64;
|
||||||
|
begin
|
||||||
|
Assert((Idx >= 0) and (Idx < GetCount), 'Index is out of bounds.');
|
||||||
|
|
||||||
|
if FCachedItems.TryGetValue(Idx, Result) then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
// Calculate the time window for the requested aggregated data point.
|
||||||
|
// Index 0 is the newest, so we calculate backwards from the total count.
|
||||||
|
endTime := FBaseTime + (FAggregatedCount - Idx) * FTimeFrame;
|
||||||
|
startTime := endTime - FTimeFrame;
|
||||||
|
|
||||||
|
sourcePoints := TList<TDataPoint<T>>.Create;
|
||||||
|
try
|
||||||
|
// Find all source data points that fall into this time window.
|
||||||
|
// We can optimize the start of the search using the IndexOf function.
|
||||||
|
sourceIdx := TDataSeries<T>(FSource).IndexOf(endTime);
|
||||||
|
if sourceIdx = -1 then
|
||||||
|
sourceIdx := 0; // If endTime is after the last element, start at the newest.
|
||||||
|
|
||||||
|
while (sourceIdx < FSource.Count) do
|
||||||
|
begin
|
||||||
|
var P := FSource.Items[sourceIdx];
|
||||||
|
if P.Time < startTime then
|
||||||
|
break; // We have moved past our time window
|
||||||
|
|
||||||
|
if P.Time < endTime then // Time is within [startTime, endTime)
|
||||||
|
begin
|
||||||
|
sourcePoints.Add(P);
|
||||||
|
end;
|
||||||
|
Inc(sourceIdx);
|
||||||
|
end;
|
||||||
|
|
||||||
|
// The aggregation function expects the data in chronological order (oldest first),
|
||||||
|
// but we collected it in reverse. So we reverse the list.
|
||||||
|
sourcePoints.Reverse;
|
||||||
|
Result.Create(endTime, FAggregateFunc(sourcePoints.ToArray));
|
||||||
|
finally
|
||||||
|
sourcePoints.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Cache the result for future calls
|
||||||
|
FCachedItems.Add(Idx, Result);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAggregateDataSeries<T, S>.GetLookback: Int64;
|
||||||
|
begin
|
||||||
|
Result := FSource.Lookback; // The lookback is defined by the source series.
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TAggregateDataSeries<T, S>.GetTotalCount: Int64;
|
||||||
|
begin
|
||||||
|
// The total count of aggregated items is simply its current count, as it's a view.
|
||||||
|
Result := GetCount;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+157
-32
@@ -2,6 +2,9 @@ unit Myc.Trade.DataPoint;
|
|||||||
|
|
||||||
interface
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
System.TimeSpan;
|
||||||
|
|
||||||
type
|
type
|
||||||
// A data record for an Ask/Bid price pair.
|
// A data record for an Ask/Bid price pair.
|
||||||
TAskBidItem = packed record
|
TAskBidItem = packed record
|
||||||
@@ -10,6 +13,15 @@ type
|
|||||||
constructor Create(AAsk, ABid: Single);
|
constructor Create(AAsk, ABid: Single);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
TOhlcItem = record
|
||||||
|
Open: Single;
|
||||||
|
High: Single;
|
||||||
|
Low: Single;
|
||||||
|
Close: Single;
|
||||||
|
Volume: Single;
|
||||||
|
constructor Create(AOpen, AHigh, ALow, AClose, AVolume: Single);
|
||||||
|
end;
|
||||||
|
|
||||||
// Represents a time-stamped data point in a series.
|
// Represents a time-stamped data point in a series.
|
||||||
TDataPoint<T> = record
|
TDataPoint<T> = record
|
||||||
Time: TDateTime;
|
Time: TDateTime;
|
||||||
@@ -17,41 +29,38 @@ type
|
|||||||
constructor Create(ATime: TDateTime; const AData: T);
|
constructor Create(ATime: TDateTime; const AData: T);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
IDataSeriesWriter<T> = interface
|
|
||||||
procedure Add(const Data: TDataPoint<T>); overload;
|
|
||||||
procedure Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1); overload;
|
|
||||||
procedure Clear;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// A time-ordered series of data points, optimized for chronological additions.
|
// A time-ordered series of data points, optimized for chronological additions.
|
||||||
// The most recently added element has the logical index 0.
|
// The most recently added element has the logical index 0.
|
||||||
IDataSeries<T> = interface
|
IDataSeries<T> = interface
|
||||||
function GetCount: Int64;
|
function GetCount: Int64;
|
||||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
function GetTotalCount: Int64;
|
function GetTotalCount: Int64;
|
||||||
function Immutable: IDataSeries<T>;
|
function GetLookback: Int64;
|
||||||
function GetWriter: IDataSeriesWriter<T>;
|
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||||
property Count: Int64 read GetCount;
|
property Count: Int64 read GetCount;
|
||||||
// Accesses data points by their logical index.
|
// Accesses data points by their logical index.
|
||||||
// Index 0 is the newest element, Index (Count - 1) is the oldest.
|
// Index 0 is the newest element, Index (Count - 1) is the oldest.
|
||||||
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
||||||
|
// The maximum number of adressable items. Count will never be bigger than the Lookback.
|
||||||
|
property Lookback: Int64 read GetLookback;
|
||||||
// The total number of items ever added to the series.
|
// The total number of items ever added to the series.
|
||||||
property TotalCount: Int64 read GetTotalCount;
|
property TotalCount: Int64 read GetTotalCount;
|
||||||
property Writer: IDataSeriesWriter<T> read GetWriter;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Interface Helper for IDataSeries<T>.
|
// Interface Helper for IDataSeries<T>.
|
||||||
// Provides a safe, value-type-like wrapper around the interface.
|
// Provides a safe, value-type-like wrapper around the interface.
|
||||||
TDataSeries<T> = record
|
TDataSeries<T> = record
|
||||||
|
type
|
||||||
|
TConvertFunc<S> = reference to function(const Val: TDataPoint<T>): S;
|
||||||
private
|
private
|
||||||
FDataSeries: IDataSeries<T>;
|
FDataSeries: IDataSeries<T>;
|
||||||
function GetCount: Int64;
|
function GetCount: Int64;
|
||||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||||
function GetData(Idx: Int64): T;
|
function GetData(Idx: Int64): T;
|
||||||
function GetIsWriteable: Boolean;
|
|
||||||
function GetTime(Idx: Int64): TDateTime;
|
function GetTime(Idx: Int64): TDateTime;
|
||||||
class function GetNull: IDataSeries<T>; static;
|
|
||||||
function GetTotalCount: Int64;
|
function GetTotalCount: Int64;
|
||||||
|
function GetLookback: Int64;
|
||||||
|
class function GetNull: IDataSeries<T>; static;
|
||||||
public
|
public
|
||||||
constructor Create(ADataSeries: IDataSeries<T>);
|
constructor Create(ADataSeries: IDataSeries<T>);
|
||||||
|
|
||||||
@@ -61,14 +70,12 @@ type
|
|||||||
class operator Implicit(const A: TDataSeries<T>): IDataSeries<T>;
|
class operator Implicit(const A: TDataSeries<T>): IDataSeries<T>;
|
||||||
class operator Implicit(const A: IDataSeries<T>): TDataSeries<T>;
|
class operator Implicit(const A: IDataSeries<T>): TDataSeries<T>;
|
||||||
|
|
||||||
class function CreateWriteable(MaxLookback: Int64): TDataSeries<T>; static;
|
class function CreateDataSeries(Lookback: Int64; const Data: TArray<TDataPoint<T>> = nil): TDataSeries<T>; static;
|
||||||
|
|
||||||
// Create an immutable version of the given series.
|
function Add(const Data: TArray<TDataPoint<T>>): TDataSeries<T>; overload;
|
||||||
function Immutable: TDataSeries<T>;
|
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): TDataSeries<T>; overload;
|
||||||
|
|
||||||
// Writing (only if IsWriteable=true)
|
function Convert<S>(const Func: TConvertFunc<S>): TDataSeries<S>;
|
||||||
procedure Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1);
|
|
||||||
procedure Clear;
|
|
||||||
|
|
||||||
// Searches for a data point by its timestamp.
|
// Searches for a data point by its timestamp.
|
||||||
// Returns the logical index of the matching item.
|
// Returns the logical index of the matching item.
|
||||||
@@ -76,20 +83,111 @@ type
|
|||||||
// Returns -1 if the timestamp is before the oldest item in the series.
|
// Returns -1 if the timestamp is before the oldest item in the series.
|
||||||
function IndexOf(TimeStamp: TDateTime): Int64;
|
function IndexOf(TimeStamp: TDateTime): Int64;
|
||||||
|
|
||||||
|
function ToArray: TArray<TDataPoint<T>>;
|
||||||
|
function ToDataArray: TArray<T>;
|
||||||
|
|
||||||
class property Null: IDataSeries<T> read GetNull;
|
class property Null: IDataSeries<T> read GetNull;
|
||||||
property Count: Int64 read GetCount;
|
property Count: Int64 read GetCount;
|
||||||
|
property TotalCount: Int64 read GetTotalCount;
|
||||||
|
property Lookback: Int64 read GetLookback;
|
||||||
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
||||||
property Data[Idx: Int64]: T read GetData;
|
property Data[Idx: Int64]: T read GetData;
|
||||||
property IsWriteable: Boolean read GetIsWriteable;
|
|
||||||
property Time[Idx: Int64]: TDateTime read GetTime;
|
property Time[Idx: Int64]: TDateTime read GetTime;
|
||||||
property TotalCount: Int64 read GetTotalCount;
|
end;
|
||||||
|
|
||||||
|
TDataSeriesDoubleHelper = record helper for TDataSeries<Double>
|
||||||
|
function ToOhlc(TimeFrame: TTimeSpan): TDataSeries<TOhlcItem>;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
|
System.SysUtils,
|
||||||
|
System.Math,
|
||||||
|
System.Generics.Collections,
|
||||||
Myc.Trade.Core.DataPoint;
|
Myc.Trade.Core.DataPoint;
|
||||||
|
|
||||||
|
// Optimized helper function using direct TTimeSpan features and integer arithmetic.
|
||||||
|
function CeilToTimeSpan(const ATime: TDateTime; const ATimeSpan: TTimeSpan): TDateTime;
|
||||||
|
var
|
||||||
|
timeSinceMidnight: TTimeSpan;
|
||||||
|
timeSpanTicks: Int64;
|
||||||
|
numIntervals, ceiledTicks: Int64;
|
||||||
|
begin
|
||||||
|
timeSpanTicks := ATimeSpan.Ticks;
|
||||||
|
Assert(timeSpanTicks > 0, 'TimeSpan must be positive.');
|
||||||
|
|
||||||
|
// Get the time portion of ATime directly as a TTimeSpan.
|
||||||
|
timeSinceMidnight := TTimeSpan.Subtract(ATime, Trunc(ATime));
|
||||||
|
|
||||||
|
// Using integer arithmetic to find the ceiling is robust.
|
||||||
|
// This is a standard formula for integer ceiling division: (numerator + denominator - 1) / denominator
|
||||||
|
numIntervals := (timeSinceMidnight.Ticks + timeSpanTicks - 1) div timeSpanTicks;
|
||||||
|
|
||||||
|
ceiledTicks := numIntervals * timeSpanTicks;
|
||||||
|
|
||||||
|
// Construct the final DateTime from the date part and the new, aligned time part.
|
||||||
|
Result := Trunc(ATime) + TTimeSpan.FromTicks(ceiledTicks);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDataSeriesDoubleHelper.ToOhlc(TimeFrame: TTimeSpan): TDataSeries<TOhlcItem>;
|
||||||
|
var
|
||||||
|
ohlcPoints: TList<TDataPoint<TOhlcItem>>;
|
||||||
|
currentBar: TOhlcItem;
|
||||||
|
windowEndTime: TDateTime;
|
||||||
|
sourceIdx: Int64;
|
||||||
|
firstPointInBar: Boolean;
|
||||||
|
begin
|
||||||
|
if Self.Count = 0 then
|
||||||
|
exit(TDataSeries<TOhlcItem>.Create(TNullDataSeries<TOhlcItem>.Null));
|
||||||
|
|
||||||
|
ohlcPoints := TList<TDataPoint<TOhlcItem>>.Create;
|
||||||
|
try
|
||||||
|
if TimeFrame.Ticks <= 0 then
|
||||||
|
raise EArgumentException.Create('Invalid TimeFrame for OHLC aggregation.');
|
||||||
|
|
||||||
|
sourceIdx := Self.Count - 1; // Start with the oldest data point
|
||||||
|
firstPointInBar := True;
|
||||||
|
windowEndTime := 0;
|
||||||
|
|
||||||
|
// Iterate through all source points chronologically (oldest to newest)
|
||||||
|
while sourceIdx >= 0 do
|
||||||
|
begin
|
||||||
|
var P := Self.Items[sourceIdx];
|
||||||
|
|
||||||
|
if firstPointInBar then
|
||||||
|
begin
|
||||||
|
windowEndTime := CeilToTimeSpan(P.Time, TimeFrame);
|
||||||
|
currentBar.Create(P.Data, P.Data, P.Data, P.Data, 0);
|
||||||
|
firstPointInBar := False;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if P.Time >= windowEndTime then
|
||||||
|
begin
|
||||||
|
ohlcPoints.Add(TDataPoint<TOhlcItem>.Create(windowEndTime, currentBar));
|
||||||
|
firstPointInBar := True;
|
||||||
|
Continue; // Re-evaluate the same point for the next bar
|
||||||
|
end;
|
||||||
|
|
||||||
|
currentBar.High := Max(currentBar.High, P.Data);
|
||||||
|
currentBar.Low := Min(currentBar.Low, P.Data);
|
||||||
|
currentBar.Close := P.Data;
|
||||||
|
currentBar.Volume := currentBar.Volume + 1;
|
||||||
|
|
||||||
|
Dec(sourceIdx);
|
||||||
|
end;
|
||||||
|
|
||||||
|
if not firstPointInBar then
|
||||||
|
ohlcPoints.Add(TDataPoint<TOhlcItem>.Create(windowEndTime, currentBar));
|
||||||
|
|
||||||
|
var dataArray := TMycDataArray<TOhlcItem>.CreateFromArray(ohlcPoints.ToArray, 0, ohlcPoints.Count);
|
||||||
|
var seriesImpl := TMycDataSeries<TOhlcItem>.Create(ohlcPoints.Count, dataArray, ohlcPoints.Count);
|
||||||
|
Result := TDataSeries<TOhlcItem>.Create(seriesImpl);
|
||||||
|
finally
|
||||||
|
ohlcPoints.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
{ TAskBidItem }
|
{ TAskBidItem }
|
||||||
|
|
||||||
constructor TAskBidItem.Create(AAsk, ABid: Single);
|
constructor TAskBidItem.Create(AAsk, ABid: Single);
|
||||||
@@ -98,6 +196,17 @@ begin
|
|||||||
Bid := ABid;
|
Bid := ABid;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ TOhlcItem }
|
||||||
|
|
||||||
|
constructor TOhlcItem.Create(AOpen, AHigh, ALow, AClose, AVolume: Single);
|
||||||
|
begin
|
||||||
|
Open := AOpen;
|
||||||
|
High := AHigh;
|
||||||
|
Low := ALow;
|
||||||
|
Close := AClose;
|
||||||
|
Volume := AVolume;
|
||||||
|
end;
|
||||||
|
|
||||||
{ TDataPoint<T> }
|
{ TDataPoint<T> }
|
||||||
|
|
||||||
constructor TDataPoint<T>.Create(ATime: TDateTime; const AData: T);
|
constructor TDataPoint<T>.Create(ATime: TDateTime; const AData: T);
|
||||||
@@ -114,26 +223,24 @@ begin
|
|||||||
FDataSeries := Null;
|
FDataSeries := Null;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataSeries<T>.Add(const Data: array of TDataPoint<T>; NumToAdd: Integer = -1);
|
function TDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): TDataSeries<T>;
|
||||||
begin
|
begin
|
||||||
Assert(IsWriteable);
|
Result := FDataSeries.Add(Data, First, Count);
|
||||||
FDataSeries.Writer.Add(Data, NumToAdd);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataSeries<T>.Clear;
|
function TDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>): TDataSeries<T>;
|
||||||
begin
|
begin
|
||||||
Assert(IsWriteable);
|
Result := FDataSeries.Add(Data, 0, Length(Data));
|
||||||
FDataSeries.Writer.Clear;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataSeries<T>.Immutable: TDataSeries<T>;
|
function TDataSeries<T>.Convert<S>(const Func: TConvertFunc<S>): TDataSeries<S>;
|
||||||
begin
|
begin
|
||||||
Result := FDataSeries.Immutable;
|
Result := TConvertSeries<T, S>.Create(FDataSeries, Func);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
class function TDataSeries<T>.CreateWriteable(MaxLookback: Int64): TDataSeries<T>;
|
class function TDataSeries<T>.CreateDataSeries(Lookback: Int64; const Data: TArray<TDataPoint<T>> = nil): TDataSeries<T>;
|
||||||
begin
|
begin
|
||||||
Result := TDataArray<T>.Create(MaxLookback);
|
Result := TMycDataSeries<T>.CreateDataSeries(Lookback, TMycDataArray<T>.CreateFromArray(Data, 0, Length(Data)), Length(Data));
|
||||||
end;
|
end;
|
||||||
|
|
||||||
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
|
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
|
||||||
@@ -171,9 +278,9 @@ begin
|
|||||||
Result := FDataSeries[Idx].Data;
|
Result := FDataSeries[Idx].Data;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataSeries<T>.GetIsWriteable: Boolean;
|
function TDataSeries<T>.GetLookback: Int64;
|
||||||
begin
|
begin
|
||||||
Result := Assigned(FDataSeries.Writer);
|
Result := FDataSeries.Lookback;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataSeries<T>.GetTime(Idx: Int64): TDateTime;
|
function TDataSeries<T>.GetTime(Idx: Int64): TDateTime;
|
||||||
@@ -206,7 +313,7 @@ begin
|
|||||||
while (low <= high) do
|
while (low <= high) do
|
||||||
begin
|
begin
|
||||||
mid := low + (high - low) div 2;
|
mid := low + (high - low) div 2;
|
||||||
dataPointTime := GetItems(mid).Time; // Use GetItems to stay within the class
|
dataPointTime := FDataSeries[mid].Time;
|
||||||
|
|
||||||
if (dataPointTime = TimeStamp) then
|
if (dataPointTime = TimeStamp) then
|
||||||
begin
|
begin
|
||||||
@@ -225,4 +332,22 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TDataSeries<T>.ToArray: TArray<TDataPoint<T>>;
|
||||||
|
begin
|
||||||
|
var n := FDataSeries.Count;
|
||||||
|
SetLength(Result, n);
|
||||||
|
dec(n);
|
||||||
|
for var i := 0 to n do
|
||||||
|
Result[i] := FDataSeries[n - i];
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDataSeries<T>.ToDataArray: TArray<T>;
|
||||||
|
begin
|
||||||
|
var n := FDataSeries.Count;
|
||||||
|
SetLength(Result, n);
|
||||||
|
dec(n);
|
||||||
|
for var i := 0 to n do
|
||||||
|
Result[i] := FDataSeries[n - i].Data;
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ begin
|
|||||||
SetLength(FChunk, AMaxChunkSize);
|
SetLength(FChunk, AMaxChunkSize);
|
||||||
FStream := AStream;
|
FStream := AStream;
|
||||||
FNewDataAvailable := TFlag.CreateObserver(FStream.HasData);
|
FNewDataAvailable := TFlag.CreateObserver(FStream.HasData);
|
||||||
FDataSeries := TDataSeries<T>.CreateWriteable(ALookback);
|
FDataSeries := TDataSeries<T>.CreateDataSeries(ALookback);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TDataStreamProvider<T>.Destroy;
|
destructor TDataStreamProvider<T>.Destroy;
|
||||||
@@ -57,7 +57,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
var cnt := FStream.GetChunk(FChunk);
|
var cnt := FStream.GetChunk(FChunk);
|
||||||
if cnt > 0 then
|
if cnt > 0 then
|
||||||
FDataSeries.Add(FChunk, cnt);
|
FDataSeries := FDataSeries.Add(FChunk, 0, cnt);
|
||||||
end;
|
end;
|
||||||
Result := FDataSeries;
|
Result := FDataSeries;
|
||||||
end;
|
end;
|
||||||
|
|||||||
Reference in New Issue
Block a user