TSeries
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
unit Myc.Test.Trade.DataPoint;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.Classes,
|
||||
DUnitX.TestFramework,
|
||||
Myc.Trade.DataPoint;
|
||||
|
||||
type
|
||||
|
||||
[TestFixture]
|
||||
TTestTDataSeries = class(TObject)
|
||||
private
|
||||
FSeries: TDataSeries<TAskBidItem>;
|
||||
procedure SetupSeriesWithData;
|
||||
public
|
||||
[Setup]
|
||||
procedure Setup;
|
||||
[Teardown]
|
||||
procedure Teardown;
|
||||
|
||||
[Test]
|
||||
procedure TestSetupSeriesWithDataVerification;
|
||||
[Test]
|
||||
procedure TestAddAndCount;
|
||||
[Test]
|
||||
[IgnoreMemoryLeaks]
|
||||
procedure TestAddOrderAssertion;
|
||||
[Test]
|
||||
procedure TestGetItemsIndexing;
|
||||
[Test]
|
||||
procedure TestIndexOfExistingTimeStamp;
|
||||
[Test]
|
||||
procedure TestIndexOfNonExistingBetween;
|
||||
[Test]
|
||||
procedure TestIndexOfBeforeFirstItem;
|
||||
[Test]
|
||||
procedure TestIndexOfAfterLastItem;
|
||||
[Test]
|
||||
procedure TestIndexOfExactOldestItem;
|
||||
[Test]
|
||||
procedure TestIndexOfExactNewestItem;
|
||||
[Test]
|
||||
procedure TestIndexOfEmptySeries;
|
||||
[Test]
|
||||
procedure TestIndexOfSingleItemSeries;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
{ TTestTDataSeries }
|
||||
|
||||
procedure TTestTDataSeries.Setup;
|
||||
begin
|
||||
// Managed record is implicitly initialized.
|
||||
end;
|
||||
|
||||
procedure TTestTDataSeries.Teardown;
|
||||
begin
|
||||
// Ensure managed record is finalized to prevent false leak reports.
|
||||
FSeries := Default(TDataSeries<TAskBidItem>);
|
||||
end;
|
||||
|
||||
procedure TTestTDataSeries.SetupSeriesWithData;
|
||||
var
|
||||
i: Integer;
|
||||
DataPoint: TDataPoint<TAskBidItem>;
|
||||
baseTime: TDateTime;
|
||||
begin
|
||||
FSeries := Default(TDataSeries<TAskBidItem>);
|
||||
baseTime := EncodeDate(2020, 7, 7);
|
||||
|
||||
// Add 10 data points with increasing timestamps.
|
||||
for i := 0 to 9 do
|
||||
begin
|
||||
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
|
||||
FSeries.Add(DataPoint);
|
||||
end;
|
||||
// Resulting state:
|
||||
// Newest item: Time = baseTime + 9, Logical Index = 0
|
||||
// Oldest item: Time = baseTime + 0, Logical Index = 9
|
||||
end;
|
||||
|
||||
// Verifies that the test data setup helper works as expected.
|
||||
procedure TTestTDataSeries.TestSetupSeriesWithDataVerification;
|
||||
var
|
||||
i: Integer;
|
||||
baseTime, expectedTime: TDateTime;
|
||||
expectedAsk: Single;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
baseTime := EncodeDate(2020, 7, 7);
|
||||
|
||||
Assert.AreEqual(Int64(10), FSeries.Count, 'Setup should create exactly 10 items');
|
||||
|
||||
// Check all items to ensure correct reverse chronological order.
|
||||
for i := 0 to FSeries.Count - 1 do
|
||||
begin
|
||||
expectedTime := baseTime + (9 - i);
|
||||
expectedAsk := Single(9 - i);
|
||||
Assert.AreEqual(expectedTime, FSeries[i].Time, 'Item at logical index should have correct reversed timestamp');
|
||||
Assert.AreEqual(expectedAsk, FSeries[i].Data.Ask, 'Item at logical index should have correct reversed data');
|
||||
end;
|
||||
end;
|
||||
|
||||
// Tests adding new items and the resulting count.
|
||||
procedure TTestTDataSeries.TestAddAndCount;
|
||||
var
|
||||
DataPoint: TDataPoint<TAskBidItem>;
|
||||
newTime: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
newTime := EncodeDate(2020, 7, 17);
|
||||
|
||||
Assert.AreEqual(Int64(10), FSeries.Count, 'Initial count should be 10');
|
||||
|
||||
DataPoint := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(100.0, 100.1));
|
||||
FSeries.Add(DataPoint);
|
||||
|
||||
Assert.AreEqual(Int64(11), FSeries.Count, 'Count should be 11 after adding one more');
|
||||
Assert.AreEqual(newTime, FSeries.Items[0].Time, 'Newest item should be at index 0');
|
||||
end;
|
||||
|
||||
// Ensures that adding an item with an older timestamp raises an assertion.
|
||||
procedure TTestTDataSeries.TestAddOrderAssertion;
|
||||
var
|
||||
olderTime: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData; // Newest item is at 2020-07-16
|
||||
olderTime := EncodeDate(2020, 7, 15);
|
||||
|
||||
Assert.WillRaise(
|
||||
procedure
|
||||
begin
|
||||
var DataPoint := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(0.0, 0.0));
|
||||
FSeries.Add(DataPoint);
|
||||
end,
|
||||
EAssertionFailed,
|
||||
'Adding item with older timestamp should raise an assert error'
|
||||
);
|
||||
end;
|
||||
|
||||
// Verifies that the logical-to-physical index mapping is correct.
|
||||
procedure TTestTDataSeries.TestGetItemsIndexing;
|
||||
var
|
||||
i: Integer;
|
||||
expectedTime: TDateTime;
|
||||
baseTime: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
baseTime := EncodeDate(2020, 7, 7);
|
||||
|
||||
for i := 0 to 9 do
|
||||
begin
|
||||
expectedTime := baseTime + (9 - i);
|
||||
Assert.AreEqual(expectedTime, FSeries.Items[i].Time, 'Item at logical index should have reversed chronological time');
|
||||
Assert.AreEqual(Single(9 - i), FSeries.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
|
||||
end;
|
||||
end;
|
||||
|
||||
// Tests finding an existing item (in this case, the oldest).
|
||||
procedure TTestTDataSeries.TestIndexOfExistingTimeStamp;
|
||||
var
|
||||
ATimeStamp: TDateTime;
|
||||
expectedIndex: Int64;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
ATimeStamp := EncodeDate(2020, 7, 7); // Oldest item
|
||||
expectedIndex := 9;
|
||||
|
||||
Assert.AreEqual(expectedIndex, FSeries.IndexOf(ATimeStamp), 'IndexOf for the oldest existing item timestamp should return its correct index');
|
||||
end;
|
||||
|
||||
// Tests finding the index for a timestamp that falls between two existing items.
|
||||
procedure TTestTDataSeries.TestIndexOfNonExistingBetween;
|
||||
var
|
||||
ATimeStamp: TDateTime;
|
||||
expectedIndex: Int64;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
ATimeStamp := EncodeDate(2020, 7, 7) + 0.5; // 12:00 on the day of the oldest item
|
||||
expectedIndex := Int64(9); // Should find the item from 00:00
|
||||
|
||||
Assert.AreEqual(expectedIndex, FSeries.IndexOf(ATimeStamp), 'IndexOf for a non-existing timestamp should return the index of the immediately preceding item');
|
||||
end;
|
||||
|
||||
// Tests finding a timestamp that is older than any item in the series.
|
||||
procedure TTestTDataSeries.TestIndexOfBeforeFirstItem;
|
||||
var
|
||||
ATimeStamp: TDateTime;
|
||||
baseTime: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
baseTime := EncodeDate(2020, 7, 7);
|
||||
ATimeStamp := baseTime - 1; // A day before the oldest item
|
||||
|
||||
Assert.AreEqual(Int64(-1), FSeries.IndexOf(ATimeStamp), 'IndexOf for a timestamp before the oldest item should return -1');
|
||||
end;
|
||||
|
||||
// Tests finding a timestamp that is newer than any item in the series.
|
||||
procedure TTestTDataSeries.TestIndexOfAfterLastItem;
|
||||
var
|
||||
ATimeStamp: TDateTime;
|
||||
baseTime: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
baseTime := EncodeDate(2020, 7, 7);
|
||||
ATimeStamp := baseTime + 10; // A day after the newest item
|
||||
|
||||
Assert.AreEqual(Int64(0), FSeries.IndexOf(ATimeStamp), 'IndexOf for a timestamp after the newest item should return the index of the newest item');
|
||||
end;
|
||||
|
||||
// Specifically tests finding the exact oldest item.
|
||||
procedure TTestTDataSeries.TestIndexOfExactOldestItem;
|
||||
var
|
||||
ATimeStamp: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
ATimeStamp := EncodeDate(2020, 7, 7);
|
||||
|
||||
Assert.AreEqual(Int64(9), FSeries.IndexOf(ATimeStamp), 'IndexOf for the exact oldest timestamp should return the last index');
|
||||
end;
|
||||
|
||||
// Specifically tests finding the exact newest item.
|
||||
procedure TTestTDataSeries.TestIndexOfExactNewestItem;
|
||||
var
|
||||
ATimeStamp: TDateTime;
|
||||
begin
|
||||
SetupSeriesWithData;
|
||||
ATimeStamp := EncodeDate(2020, 7, 16);
|
||||
|
||||
Assert.AreEqual(Int64(0), FSeries.IndexOf(ATimeStamp), 'IndexOf for the exact newest timestamp should return index 0');
|
||||
end;
|
||||
|
||||
// Tests IndexOf on an empty series.
|
||||
procedure TTestTDataSeries.TestIndexOfEmptySeries;
|
||||
begin
|
||||
FSeries := Default(TDataSeries<TAskBidItem>);
|
||||
Assert.AreEqual(Int64(0), FSeries.Count);
|
||||
Assert.AreEqual(Int64(-1), FSeries.IndexOf(Now), 'IndexOf on empty series should return -1');
|
||||
end;
|
||||
|
||||
// Tests IndexOf on a series with only one item.
|
||||
procedure TTestTDataSeries.TestIndexOfSingleItemSeries;
|
||||
var
|
||||
DataPoint: TDataPoint<TAskBidItem>;
|
||||
testTime: TDateTime;
|
||||
begin
|
||||
FSeries := Default(TDataSeries<TAskBidItem>);
|
||||
testTime := EncodeDate(2025, 1, 1);
|
||||
DataPoint := TDataPoint<TAskBidItem>.Create(testTime, TAskBidItem.Create(1.0, 2.0));
|
||||
FSeries.Add(DataPoint);
|
||||
|
||||
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 + 0.5), 'IndexOf for time after single item should be 0');
|
||||
Assert.AreEqual(Int64(-1), FSeries.IndexOf(testTime - 1), 'IndexOf for time before single item should be -1');
|
||||
end;
|
||||
|
||||
initialization
|
||||
TDUnitX.RegisterTestFixture(TTestTDataSeries);
|
||||
end.
|
||||
@@ -114,7 +114,7 @@ begin
|
||||
for n := 0 to High(ExpectedData) div 1000 do
|
||||
begin
|
||||
i := n * 1000;
|
||||
Assert.AreEqual(ExpectedData[i].TimeStamp, Dst[i].Time, 'Timestamp mismatch');
|
||||
Assert.AreEqual(ExpectedData[i].Time, Dst[i].Time, 'Timestamp mismatch');
|
||||
Assert.AreEqual(ExpectedData[i].Data.Ask, Dst[i].Data.Ask, 'Ask price mismatch');
|
||||
Assert.AreEqual(ExpectedData[i].Data.Bid, Dst[i].Data.Bid, 'Bid price mismatch');
|
||||
end;
|
||||
|
||||
+134
-1
@@ -7,23 +7,57 @@ uses
|
||||
|
||||
type
|
||||
// A specific data record structure for Ask and Bid prices.
|
||||
// Contains Ask and Bid prices as Single-precision floating-point numbers.
|
||||
TAskBidItem = packed record
|
||||
Ask: Single;
|
||||
Bid: Single;
|
||||
constructor Create(AAsk, ABid: Single);
|
||||
end;
|
||||
|
||||
// Represents a single data point in a time series.
|
||||
// Combines a timestamp with generic data.
|
||||
TDataPoint<T> = record
|
||||
Time: TDateTime;
|
||||
Data: T;
|
||||
constructor Create(ATime: TDateTime; const AData: T);
|
||||
end;
|
||||
|
||||
IMycStream<T: record> = interface
|
||||
// A time-ordered array whose most recently added element has index 0.
|
||||
// This managed record stores a series of TDataPoint<T> elements.
|
||||
TDataSeries<T: record> = record
|
||||
const
|
||||
ChunkSize = 1024;
|
||||
type
|
||||
TChunk = TArray<TDataPoint<T>>;
|
||||
private
|
||||
FChunks: TArray<TChunk>;
|
||||
FCount: Int64;
|
||||
function GetCount: Int64;
|
||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||
public
|
||||
// Adds a new data point to the series.
|
||||
// Asserts that the new data point's time is not older than the current newest item.
|
||||
procedure Add(const Data: TDataPoint<T>);
|
||||
// Searches for a data point by its timestamp.
|
||||
// Returns the logical index of the matching item.
|
||||
// If no exact match, returns the index of the item immediately preceding the timestamp.
|
||||
// Returns -1 if the timestamp is before the oldest item in the series.
|
||||
function IndexOf(TimeStamp: TDateTime): Int64;
|
||||
// Initializes the TDataSeries record.
|
||||
class operator Initialize(out Dest: TDataSeries<T>);
|
||||
class operator Finalize(var Dest: TDataSeries<T>);
|
||||
// The total number of data points in the series.
|
||||
property Count: Int64 read GetCount;
|
||||
// Accesses data points by their logical index.
|
||||
// Index 0 is the newest element, Index (Count - 1) is the oldest.
|
||||
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
Winapi.Windows;
|
||||
|
||||
{ TDataPoint }
|
||||
|
||||
constructor TDataPoint<T>.Create(ATime: TDateTime; const AData: T);
|
||||
@@ -32,4 +66,103 @@ begin
|
||||
Data := AData;
|
||||
end;
|
||||
|
||||
{ TDataSeries<T> }
|
||||
|
||||
procedure TDataSeries<T>.Add(const Data: TDataPoint<T>);
|
||||
begin
|
||||
// Assert that the new data point's time is NOT OLDER than the current newest item.
|
||||
// This means data must be added in chronologically ascending order (or equal timestamp).
|
||||
Assert((Length(FChunks) = 0) or (Data.Time >= GetItems(0).Time), 'Time stamp older than last item');
|
||||
|
||||
var ci := FCount div ChunkSize;
|
||||
var di := FCount mod ChunkSize;
|
||||
|
||||
if di = 0 then
|
||||
begin
|
||||
Assert(ci = Length(FChunks));
|
||||
Assert(di = 0);
|
||||
SetLength(FChunks, ci + 1);
|
||||
SetLength(FChunks[ci], ChunkSize);
|
||||
end;
|
||||
|
||||
FChunks[ci][di] := Data;
|
||||
inc(FCount);
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetCount: Int64;
|
||||
begin
|
||||
Result := FCount;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
||||
begin
|
||||
Assert((Idx >= 0) and (Idx < FCount));
|
||||
Idx := FCount - Idx - 1;
|
||||
Result := FChunks[Idx div ChunkSize][Idx mod ChunkSize];
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.IndexOf(TimeStamp: TDateTime): Int64;
|
||||
var
|
||||
low, high, mid: Int64;
|
||||
dataPointTime: TDateTime;
|
||||
debugStr: string;
|
||||
begin
|
||||
Result := -1;
|
||||
if FCount = 0 then
|
||||
Exit;
|
||||
|
||||
low := 0;
|
||||
high := FCount - 1;
|
||||
|
||||
// Start der Debug-Ausgabe
|
||||
OutputDebugString(PChar(Format('--- IndexOf Search for %.15f ---', [TimeStamp])));
|
||||
|
||||
while (low <= high) do
|
||||
begin
|
||||
mid := low + (high - low) div 2;
|
||||
dataPointTime := GetItems(mid).Time;
|
||||
|
||||
// Ausgabe der Werte in jeder Iteration
|
||||
debugStr := Format('low: %d, high: %d, mid: %d, dataPointTime: %.15f',
|
||||
[low, high, mid, dataPointTime]);
|
||||
OutputDebugString(PChar(debugStr));
|
||||
|
||||
if (dataPointTime = TimeStamp) then
|
||||
begin
|
||||
OutputDebugString(' -> Match found!');
|
||||
Result := mid;
|
||||
Exit;
|
||||
end
|
||||
else if (dataPointTime < TimeStamp) then
|
||||
begin
|
||||
OutputDebugString(' -> dataPointTime < TimeStamp');
|
||||
Result := mid;
|
||||
high := mid - 1;
|
||||
end
|
||||
else // dataPointTime > TimeStamp
|
||||
begin
|
||||
OutputDebugString(' -> dataPointTime > TimeStamp');
|
||||
low := mid + 1;
|
||||
end;
|
||||
end;
|
||||
OutputDebugString(PChar('--- Search Finished ---'));
|
||||
end;
|
||||
|
||||
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
|
||||
begin
|
||||
Dest.FChunks := nil;
|
||||
Dest.FCount := 0;
|
||||
end;
|
||||
|
||||
class operator TDataSeries<T>.Initialize(out Dest: TDataSeries<T>);
|
||||
begin
|
||||
Dest.FCount := 0;
|
||||
end;
|
||||
|
||||
constructor TAskBidItem.Create(AAsk, ABid: Single);
|
||||
begin
|
||||
Ask := AAsk;
|
||||
Bid := ABid;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
@@ -563,12 +563,12 @@ begin
|
||||
if recordCount > 0 then
|
||||
begin
|
||||
SetLength(Result, recordCount);
|
||||
for var i:=0 to High(Result) do
|
||||
for var i := 0 to High(Result) do
|
||||
begin
|
||||
bytesRead := InputStream.Read(rec, SizeOf(TAuraFileDataRecord<T>));
|
||||
if bytesRead <> SizeOf(TAuraFileDataRecord<T>) then
|
||||
raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [fileSize, bytesRead]);
|
||||
Result[i].Create( rec.TimeStamp, rec.Data );
|
||||
Result[i].Create(rec.TimeStamp, rec.Data);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@ uses
|
||||
Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas',
|
||||
Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas',
|
||||
Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas',
|
||||
Myc.Test.Trade.DataStream in '..\Src\Myc.Test.Trade.DataStream.pas';
|
||||
Myc.Test.Trade.DataStream in '..\Src\Myc.Test.Trade.DataStream.pas',
|
||||
Myc.Test.Trade.DataPoint in '..\Src\Myc.Test.Trade.DataPoint.pas';
|
||||
|
||||
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
|
||||
{$IFNDEF TESTINSIGHT}
|
||||
|
||||
@@ -137,6 +137,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
||||
<DCCReference Include="..\Src\Myc.Trade.Node.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Trade.DataStream.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Test.Trade.DataStream.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Test.Trade.DataPoint.pas"/>
|
||||
<BuildConfiguration Include="Base">
|
||||
<Key>Base</Key>
|
||||
</BuildConfiguration>
|
||||
|
||||
Reference in New Issue
Block a user