Files
MycLib/Src/Myc.Test.Trade.DataPoint.pas
T
2025-06-25 15:00:51 +02:00

639 lines
21 KiB
ObjectPascal

unit Myc.Test.Trade.DataPoint;
interface
uses
System.SysUtils,
System.Classes,
DUnitX.TestFramework,
Myc.Trade.DataPoint;
type
[TestFixture]
TTestDataArray = class(TObject)
private
FArray: TDataSeries<TAskBidItem>;
procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
public
[Setup]
procedure Setup;
[Teardown]
procedure Teardown;
// Tests for Setup and basic Add/Count
[Test]
procedure TestSetupSeriesWithDataVerification;
[Test]
procedure TestAddAndCount;
[Test]
[IgnoreMemoryLeaks]
procedure TestAddOrderAssertion;
[Test]
procedure TestGetItemsIndexing;
// Tests for MaxLookback
[Test]
procedure TestCountIsCappedByMaxLookback_NoTrim;
[Test]
procedure TestCountIsCappedByMaxLookback_WithTrim;
[Test]
[IgnoreMemoryLeaks]
procedure TestLoopIsSafeWithMaxLookback;
// Tests for bulk Add
[Test]
procedure TestBulkAddIncreasesCount;
[Test]
[IgnoreMemoryLeaks]
procedure TestBulkAddChronologicalAssertion_Internal;
[Test]
[IgnoreMemoryLeaks]
procedure TestBulkAddChronologicalAssertion_External;
[Test]
procedure TestBulkAddSpanningMultipleChunks;
[Test]
procedure TestBulkAddWithMaxLookback;
// Tests for TotalCount
[Test]
procedure TestTotalCountIncrementsCorrectly;
[Test]
procedure TestTotalCountIgnoresTrimming;
[Test]
procedure TestTotalCountResetsOnClear;
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
[Test]
procedure TestIndexOfExistingTimeStamp;
[Test]
procedure TestIndexOfNonExistingBetween;
[Test]
procedure TestIndexOfBeforeFirstItem;
[Test]
procedure TestIndexOfAfterLastItem;
[Test]
procedure TestIndexOfExactOldestItem;
[Test]
procedure TestIndexOfExactNewestItem;
[Test]
procedure TestIndexOfEmptySeries;
[Test]
procedure TestIndexOfSingleItemSeries;
[Test]
procedure TestIndexOfRespectsMaxLookback;
// Tests for Copy
[Test]
procedure TestCopyHasSameContent;
[Test]
procedure TestCopyIsImmutableAfterOriginalChanges;
[Test]
procedure TestCopyRespectsMaxLookback;
end;
implementation
{ TTestDataArray }
procedure TTestDataArray.Setup;
begin
FArray := TDataSeries<TAskBidItem>.CreateWriteable(0);
end;
procedure TTestDataArray.Teardown;
begin
FArray := nil;
end;
procedure TTestDataArray.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0);
var
i: Integer;
DataPoint: TDataPoint<TAskBidItem>;
baseTime: TDateTime;
begin
FArray := 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));
FArray.Add(DataPoint);
end;
end;
procedure TTestDataArray.TestSetupSeriesWithDataVerification;
var
i: Integer;
baseTime, expectedTime: TDateTime;
expectedAsk: Single;
begin
SetupSeriesWithData;
baseTime := EncodeDate(2020, 7, 7);
Assert.AreEqual(Int64(10), FArray.Count, 'Setup should create exactly 10 items');
// Check all items to ensure correct reverse chronological order.
for i := 0 to FArray.Count - 1 do
begin
expectedTime := baseTime + (9 - i);
expectedAsk := Single(9 - i);
Assert.AreEqual(expectedTime, FArray[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');
end;
end;
procedure TTestDataArray.TestAddAndCount;
var
DataPoint: TDataPoint<TAskBidItem>;
newTime: TDateTime;
begin
SetupSeriesWithData;
newTime := EncodeDate(2020, 7, 17);
Assert.AreEqual(Int64(10), FArray.Count, 'Initial count should be 10');
DataPoint := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(100.0, 100.1));
FArray.Add(DataPoint);
Assert.AreEqual(Int64(11), FArray.Count, 'Count should be 11 after adding one more');
Assert.AreEqual(newTime, FArray.Items[0].Time, 'Newest item should be at index 0');
end;
procedure TTestDataArray.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));
FArray.Add(DataPoint);
end,
EAssertionFailed,
'Adding item with older timestamp should raise an assert error'
);
end;
procedure TTestDataArray.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, FArray.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');
end;
end;
procedure TTestDataArray.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), 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
newData: TArray<TDataPoint<TAskBidItem>>;
baseTime, newTime: TDateTime;
i: Integer;
begin
SetupSeriesWithData(10);
baseTime := EncodeDate(2020, 7, 7);
SetLength(newData, 5);
for i := 0 to High(newData) do
begin
newTime := baseTime + 10 + i;
newData[i] := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(i, i));
end;
FArray.Add(newData);
Assert.AreEqual(Int64(15), FArray.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');
end;
procedure TTestDataArray.TestBulkAddChronologicalAssertion_Internal;
var
newData: TArray<TDataPoint<TAskBidItem>>;
begin
SetupSeriesWithData(10);
SetLength(newData, 2);
newData[0] := TDataPoint<TAskBidItem>.Create(Now + 1, TAskBidItem.Create(1, 1));
newData[1] := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(2, 2)); // Not sorted
Assert.WillRaise(
procedure begin FArray.Add(newData); end,
EAssertionFailed,
'Bulk Add should fail if the input array is not chronologically sorted'
);
end;
procedure TTestDataArray.TestBulkAddChronologicalAssertion_External;
var
newData: TArray<TDataPoint<TAskBidItem>>;
olderTime: TDateTime;
begin
SetupSeriesWithData(10); // Newest is baseTime + 9
olderTime := EncodeDate(2020, 7, 7) + 8;
SetLength(newData, 1);
newData[0] := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(1, 1));
Assert.WillRaise(
procedure begin FArray.Add(newData); end,
EAssertionFailed,
'Bulk Add should fail if its first item is older than the series last item'
);
end;
procedure TTestDataArray.TestBulkAddSpanningMultipleChunks;
const
CHUNK_SIZE = 1024;
var
newData: TArray<TDataPoint<TAskBidItem>>;
lastTimeBeforeAdd, firstNewTime, lastNewTime: TDateTime;
i: Integer;
begin
SetupSeriesWithData(CHUNK_SIZE - 4); // Almost fill the first chunk
lastTimeBeforeAdd := FArray[0].Time;
SetLength(newData, 8); // Add 8 items, which will span the chunk boundary
for i := 0 to High(newData) do
begin
newData[i] := TDataPoint<TAskBidItem>.Create(lastTimeBeforeAdd + 1 + i, TAskBidItem.Create(i, i));
end;
firstNewTime := newData[0].Time;
lastNewTime := newData[High(newData)].Time;
FArray.Add(newData);
Assert.AreEqual(Int64(CHUNK_SIZE + 4), FArray.Count, 'Count should be correct after spanning a chunk');
Assert.AreEqual(lastNewTime, FArray[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');
end;
procedure TTestDataArray.TestBulkAddWithMaxLookback;
var
newData: TArray<TDataPoint<TAskBidItem>>;
i: Integer;
baseTime: TDateTime;
begin
SetupSeriesWithData(400, 500);
baseTime := FArray[0].Time;
SetLength(newData, 200);
for i := 0 to High(newData) do
begin
newData[i] := TDataPoint<TAskBidItem>.Create(baseTime + 1 + i, TAskBidItem.Create(i, i));
end;
FArray.Add(newData);
// 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.
Assert.AreEqual(Int64(500), FArray.Count, 'Count should be capped by MaxLookback after bulk add');
end;
procedure TTestDataArray.TestTotalCountIncrementsCorrectly;
var
newData: TArray<TDataPoint<TAskBidItem>>;
i: Integer;
begin
Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 on creation');
// Test single add
FArray.Add(TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(1, 1)));
Assert.AreEqual(Int64(1), FArray.TotalCount, 'TotalCount should be 1 after single add');
// Test bulk add
SetLength(newData, 10);
for i := 0 to High(newData) do
newData[i] := TDataPoint<TAskBidItem>.Create(Now + 1 + i, TAskBidItem.Create(i, i));
FArray.Add(newData);
Assert.AreEqual(Int64(11), FArray.TotalCount, 'TotalCount should be 11 after bulk add');
end;
procedure TTestDataArray.TestTotalCountIgnoresTrimming;
begin
// Create series with a lookback that will cause trimming
SetupSeriesWithData(20, 10);
// The visible count is capped by MaxLookback
Assert.AreEqual(Int64(10), FArray.Count, 'Count should be capped by MaxLookback');
// The total count should reflect all items that were ever added
Assert.AreEqual(Int64(20), FArray.TotalCount, 'TotalCount must not be affected by trimming');
end;
procedure TTestDataArray.TestTotalCountResetsOnClear;
begin
SetupSeriesWithData(15);
Assert.IsTrue(FArray.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0');
FArray.Clear;
Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 after Clear');
Assert.AreEqual(Int64(0), FArray.Count, 'Count should be 0 after Clear');
end;
{ TTestDataSeries }
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;
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;
procedure TTestDataSeries.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;
procedure TTestDataSeries.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;
procedure TTestDataSeries.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;
procedure TTestDataSeries.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;
procedure TTestDataSeries.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;
procedure TTestDataSeries.TestIndexOfEmptySeries;
begin
// Use default setup with no data
Assert.AreEqual(Int64(0), FSeries.Count);
Assert.AreEqual(Int64(-1), FSeries.IndexOf(Now), 'IndexOf on empty series should return -1');
end;
procedure TTestDataSeries.TestIndexOfSingleItemSeries;
var
DataPoint: TDataPoint<TAskBidItem>;
testTime: TDateTime;
begin
// Use default setup, but add one item
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;
procedure TTestDataSeries.TestIndexOfRespectsMaxLookback;
var
baseTime, searchTime: TDateTime;
begin
SetupSeriesWithData(500, 400); // Items from baseTime+0 to baseTime+499
baseTime := EncodeDate(2020, 7, 7);
Assert.AreEqual(Int64(400), FSeries.Count, 'Pre-condition: Count must be 400');
// The oldest *physically present* item has a timestamp of baseTime+0.
// This item is at logical index 499, which is outside the MaxLookback range.
// IndexOf must not find it.
searchTime := baseTime; // Time of oldest physical item.
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.
// Its physical index is 500 - 399 - 1 = 100.
// Its timestamp is baseTime + 100.
searchTime := baseTime + 100;
Assert.AreEqual(Int64(399), FSeries.IndexOf(searchTime), 'IndexOf should find the oldest valid item at the edge of MaxLookback');
end;
initialization
TDUnitX.RegisterTestFixture(TTestDataArray);
TDUnitX.RegisterTestFixture(TTestDataSeries);
end.