unit Myc.Test.Trade.DataPoint; interface uses System.SysUtils, System.Classes, DUnitX.TestFramework, Myc.Trade.DataPoint; type [TestFixture] TTestTDataSeries = class(TObject) private FSeries: TDataSeries; procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0); 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; // Tests for MaxLookback [Test] procedure TestCountIsCappedByMaxLookback_NoTrim; [Test] procedure TestCountIsCappedByMaxLookback_WithTrim; [Test] [IgnoreMemoryLeaks] procedure TestLoopIsSafeWithMaxLookback; [Test] procedure TestIndexOfRespectsMaxLookback; // 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; 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); end; procedure TTestTDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 0); var i: Integer; DataPoint: TDataPoint; baseTime: TDateTime; begin FSeries := TDataSeries.Create( 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; // 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; newTime: TDateTime; begin SetupSeriesWithData; newTime := EncodeDate(2020, 7, 17); Assert.AreEqual(Int64(10), FSeries.Count, 'Initial count should be 10'); DataPoint := TDataPoint.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.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); 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; testTime: TDateTime; begin FSeries := Default(TDataSeries); 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 TTestTDataSeries.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 TTestTDataSeries.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), FSeries.Count, 'Public count should be capped by MaxLookback after trimming'); end; procedure TTestTDataSeries.TestLoopIsSafeWithMaxLookback; var dummy: TDataPoint; 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, 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 := FSeries[400]; end, EAssertionFailed, 'Accessing index at the limit of MaxLookback should raise an exception' ); end; procedure TTestTDataSeries.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; procedure TTestTDataSeries.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; FSeries.Add(newData); 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), FSeries.Time[0], 'Newest item should be the last item from the added array'); end; procedure TTestTDataSeries.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 FSeries.Add(newData); end, EAssertionFailed, 'Bulk Add should fail if the input array is not chronologically sorted' ); end; procedure TTestTDataSeries.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 FSeries.Add(newData); end, EAssertionFailed, 'Bulk Add should fail if its first item is older than the series last item' ); end; procedure TTestTDataSeries.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 := FSeries.Time[0]; 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; FSeries.Add(newData); Assert.AreEqual(Int64(CHUNK_SIZE + 4), FSeries.Count, 'Count should be correct after spanning a chunk'); Assert.AreEqual(lastNewTime, FSeries.Time[0], 'Newest item should be correct'); Assert.AreEqual(firstNewTime, FSeries.Time[7], 'Oldest of the new items should be at the correct logical index'); end; procedure TTestTDataSeries.TestBulkAddWithMaxLookback; var newData: TArray>; i: Integer; baseTime: TDateTime; begin SetupSeriesWithData(400, 500); baseTime := FSeries.Time[0]; SetLength(newData, 200); for i := 0 to High(newData) do begin newData[i] := TDataPoint.Create(baseTime + 1 + i, TAskBidItem.Create(i, i)); end; FSeries.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), FSeries.Count, 'Count should be capped by MaxLookback after bulk add'); end; procedure TTestTDataSeries.TestTotalCountIncrementsCorrectly; var newData: TArray>; i: Integer; begin FSeries := TDataSeries.Create( 0 ); Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 on creation'); // Test single add FSeries.Add(TDataPoint.Create(Now, TAskBidItem.Create(1,1))); Assert.AreEqual(Int64(1), FSeries.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)); FSeries.Add(newData); Assert.AreEqual(Int64(11), FSeries.TotalCount, 'TotalCount should be 11 after bulk add'); end; procedure TTestTDataSeries.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), FSeries.Count, 'Count should be capped by MaxLookback'); // The total count should reflect all items that were ever added Assert.AreEqual(Int64(20), FSeries.TotalCount, 'TotalCount must not be affected by trimming'); end; procedure TTestTDataSeries.TestTotalCountResetsOnClear; begin SetupSeriesWithData(15); Assert.IsTrue(FSeries.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0'); FSeries.Clear; Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 after Clear'); Assert.AreEqual(Int64(0), FSeries.Count, 'Count should be 0 after Clear'); end; initialization TDUnitX.RegisterTestFixture(TTestTDataSeries); end.