This commit is contained in:
Michael Schimmel
2025-06-11 13:33:46 +02:00
parent d282d2c40d
commit 7f6672db24
6 changed files with 404 additions and 5 deletions
+264
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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.
+2 -2
View File
@@ -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
View File
@@ -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}
+1
View File
@@ -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>