unit Myc.Test.Trade.DataPoint; interface uses System.SysUtils, System.Classes, DUnitX.TestFramework, Myc.Trade.DataPoint; type [TestFixture] TTestDataArray = class(TObject) private FArray: TDataSeries; 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; 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.CreateWriteable(0); end; procedure TTestDataArray.Teardown; begin FArray := nil; end; procedure TTestDataArray.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0); var i: Integer; DataPoint: TDataPoint; baseTime: TDateTime; begin FArray := TDataSeries.CreateWriteable(MaxLookBack); baseTime := EncodeDate(2020, 7, 7); // Add data points with increasing timestamps. for i := 0 to ItemCount - 1 do begin DataPoint := TDataPoint.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; newTime: TDateTime; begin SetupSeriesWithData; newTime := EncodeDate(2020, 7, 17); Assert.AreEqual(Int64(10), FArray.Count, 'Initial count should be 10'); DataPoint := TDataPoint.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.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; 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>; 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.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>; begin SetupSeriesWithData(10); SetLength(newData, 2); newData[0] := TDataPoint.Create(Now + 1, TAskBidItem.Create(1, 1)); newData[1] := TDataPoint.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>; olderTime: TDateTime; begin SetupSeriesWithData(10); // Newest is baseTime + 9 olderTime := EncodeDate(2020, 7, 7) + 8; SetLength(newData, 1); newData[0] := TDataPoint.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>; 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.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>; 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.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>; i: Integer; begin Assert.AreEqual(Int64(0), FArray.TotalCount, 'TotalCount should be 0 on creation'); // Test single add FArray.Add(TDataPoint.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.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.CreateWriteable(0); end; procedure TTestDataSeries.Teardown; begin FSeries := Default(TDataSeries); end; procedure TTestDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0); var i: Integer; DataPoint: TDataPoint; baseTime: TDateTime; begin FSeries := TDataSeries.CreateWriteable(MaxLookBack); baseTime := EncodeDate(2020, 7, 7); // Add data points with increasing timestamps. for i := 0 to ItemCount - 1 do begin DataPoint := TDataPoint.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1)); FSeries.Add(DataPoint); end; end; procedure TTestDataSeries.TestCopyHasSameContent; var copy: TDataSeries; 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; originalCount, originalTotalCount: Int64; newPoint: TDataPoint; 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.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; newPoint: TDataPoint; 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.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; testTime: TDateTime; begin // Use default setup, but add one item testTime := EncodeDate(2025, 1, 1); DataPoint := TDataPoint.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.