From ce0cba720a2ad76815220ba41208eaecfb6beee9 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Tue, 15 Jul 2025 09:13:40 +0200 Subject: [PATCH] Unit refactoring --- AuraTrader/AuraTrader.dpr | 4 +- AuraTrader/AuraTrader.dproj | 8 - AuraTrader/FirstStrategy.pas | 3 +- AuraTrader/MainForm.pas | 1 - Src/Myc.Fmx.Chart.Series.pas | 2 - Src/Myc.Test.Trade.DataPoint.pas | 759 ------------------ Src/Myc.Trade.DataConverter.pas | 145 ---- ...erter.pas => Myc.Trade.DataPoint.Impl.pas} | 5 +- Src/Myc.Trade.DataPoint.pas | 133 +++ Src/Myc.Trade.Indicators.pas | 3 +- Test/MycTests.dpr | 1 - Test/MycTests.dproj | 1 - 12 files changed, 138 insertions(+), 927 deletions(-) delete mode 100644 Src/Myc.Test.Trade.DataPoint.pas delete mode 100644 Src/Myc.Trade.DataConverter.pas rename Src/{Myc.Trade.Core.DataConverter.pas => Myc.Trade.DataPoint.Impl.pas} (94%) diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr index 3a2f499..1add917 100644 --- a/AuraTrader/AuraTrader.dpr +++ b/AuraTrader/AuraTrader.dpr @@ -13,9 +13,7 @@ uses Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas', Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas', - Myc.Trade.Types in '..\Src\Myc.Trade.Types.pas', - Myc.Trade.DataConverter in '..\Src\Myc.Trade.DataConverter.pas', - Myc.Trade.Core.DataConverter in '..\Src\Myc.Trade.Core.DataConverter.pas'; + Myc.Trade.Types in '..\Src\Myc.Trade.Types.pas'; {$R *.res} diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj index 8c03150..bc46de6 100644 --- a/AuraTrader/AuraTrader.dproj +++ b/AuraTrader/AuraTrader.dproj @@ -142,8 +142,6 @@ - - Base @@ -187,12 +185,6 @@ true - - - AuraTrader.rsm - true - - AuraTrader.exe diff --git a/AuraTrader/FirstStrategy.pas b/AuraTrader/FirstStrategy.pas index 8f14917..249c9a0 100644 --- a/AuraTrader/FirstStrategy.pas +++ b/AuraTrader/FirstStrategy.pas @@ -11,8 +11,7 @@ uses Myc.Trade.Types, Myc.Trade.DataPoint, Myc.Trade.DataArray, - Myc.Trade.DataConverter, - Myc.Trade.Core.DataConverter; + Myc.Trade.DataPoint.Impl; type TTickAggregation = class(TMycConverter, TDataPoint>) diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index c451cf5..0c37bf8 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -30,7 +30,6 @@ uses Myc.Trade.Types, Myc.Trade.DataStream, Myc.Trade.DataPoint, - Myc.Trade.DataConverter, Myc.Signals, Myc.Mutable, Myc.Signals.FMX, diff --git a/Src/Myc.Fmx.Chart.Series.pas b/Src/Myc.Fmx.Chart.Series.pas index 653176c..2ac309b 100644 --- a/Src/Myc.Fmx.Chart.Series.pas +++ b/Src/Myc.Fmx.Chart.Series.pas @@ -11,8 +11,6 @@ uses Myc.Trade.Types, Myc.Trade.DataArray, Myc.Trade.DataPoint, - Myc.Trade.DataConverter, - Myc.Trade.Core.DataConverter, Myc.Fmx.Chart; type diff --git a/Src/Myc.Test.Trade.DataPoint.pas b/Src/Myc.Test.Trade.DataPoint.pas deleted file mode 100644 index afc68ee..0000000 --- a/Src/Myc.Test.Trade.DataPoint.pas +++ /dev/null @@ -1,759 +0,0 @@ -unit Myc.Test.Trade.DataPoint; - -interface - -uses - System.SysUtils, - System.Classes, - DUnitX.TestFramework, - Myc.Trade.Types, - Myc.Trade.DataPoint; - -type - [TestFixture] - TTestDataSeries = class(TObject) - private - FSeries: TDataSeries; - procedure SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 10); - 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 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 TestTotalCountResetsOnRecreate; - - // 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; - [Test] - procedure TestIndexOfWithTrimmedData; - - // Tests for Copy (Immutability) - [Test] - procedure TestCopyHasSameContent; - [Test] - procedure TestCopyIsImmutableAfterOriginalChanges; - [Test] - procedure TestCopyRespectsMaxLookback; - - // Tests for Lookback - [Test] - procedure TestCountIsCappedByMaxLookback_NoTrim; - [Test] - procedure TestCountIsCappedByMaxLookback_WithTrim; - [Test] - [IgnoreMemoryLeaks] - procedure TestLoopIsSafeWithMaxLookback; - [Test] - procedure TestDataAndTimePropertiesRespectLookback; - [Test] - procedure TestLookbackZero; - [Test] - procedure TestLookbackOne; - end; - -implementation - -{ TTestDataSeries } - -procedure TTestDataSeries.Setup; -begin - FSeries := TDataSeries.CreateDataSeries(1); -end; - -procedure TTestDataSeries.Teardown; -begin - FSeries := Default(TDataSeries); -end; - -procedure TTestDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 10); -var - i: Integer; - DataPoint: TDataPoint; - baseTime: TDateTime; -begin - FSeries := TDataSeries.CreateDataSeries(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 := FSeries.Add([DataPoint], 0, 1); - end; -end; - -procedure TTestDataSeries.TestSetupSeriesWithDataVerification; -var - i: Integer; - baseTime, expectedTime: TDateTime; - expectedAsk: Double; -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; - -procedure TTestDataSeries.TestAddAndCount; -var - DataPoint: TDataPoint; - newTime: TDateTime; -begin - SetupSeriesWithData(10, 11); - 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 := FSeries.Add([DataPoint], 0, 1); - - 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; - -procedure TTestDataSeries.TestAddOrderAssertion; -var - olderTime: TDateTime; - DataPoint: TDataPoint; -begin - SetupSeriesWithData; // Newest item is at 2020-07-16 - olderTime := EncodeDate(2020, 7, 15); - - Assert.WillRaise( - procedure - begin - DataPoint := TDataPoint.Create(olderTime, TAskBidItem.Create(0.0, 0.0)); - FSeries.Add([DataPoint], 0, 1); - end, - EAssertionFailed, - 'Adding item with older timestamp should raise an assert error' - ); -end; - -procedure TTestDataSeries.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(Double(9 - i), FSeries.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order'); - end; -end; - -//-------------------------------------------------------------------------------------------------- -// Tests for bulk Add -//-------------------------------------------------------------------------------------------------- - -procedure TTestDataSeries.TestBulkAddIncreasesCount; -var - newData: TArray>; - baseTime, newTime: TDateTime; - i: Integer; -begin - SetupSeriesWithData(10, 15); - 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 := FSeries.Add(newData, 0, Length(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[0].Time, 'Newest item should be the last item from the added array'); -end; - -procedure TTestDataSeries.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, 0, Length(newData)); end, - EAssertionFailed, - 'Bulk Add should fail if the input array is not chronologically sorted' - ); -end; - -procedure TTestDataSeries.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, 0, Length(newData)); end, - EAssertionFailed, - 'Bulk Add should fail if its first item is older than the series last item' - ); -end; - -procedure TTestDataSeries.TestBulkAddSpanningMultipleChunks; -const - CHUNK_SIZE = 1024; -var - newData: TArray>; - lastTimeBeforeAdd, firstNewTime, lastNewTime: TDateTime; - i: Integer; -begin - SetupSeriesWithData(CHUNK_SIZE - 4, CHUNK_SIZE + 8); // Almost fill the first chunk - lastTimeBeforeAdd := FSeries[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; - - FSeries := FSeries.Add(newData, 0, Length(newData)); - Assert.AreEqual(Int64(CHUNK_SIZE + 4), FSeries.Count, 'Count should be correct after spanning a chunk'); - Assert.AreEqual(lastNewTime, FSeries[0].Time, 'Newest item should be correct'); - Assert.AreEqual(firstNewTime, FSeries[7].Time, 'Oldest of the new items should be at the correct logical index'); -end; - -procedure TTestDataSeries.TestBulkAddWithMaxLookback; -var - newData: TArray>; - i: Integer; - baseTime: TDateTime; -begin - SetupSeriesWithData(400, 500); - baseTime := FSeries[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; - - FSeries := FSeries.Add(newData, 0, Length(newData)); - - // Total items would be 600, but MaxLookback is 500. - // The public Count must be capped at 500. - Assert.AreEqual(Int64(500), FSeries.Count, 'Count should be capped by MaxLookback after bulk add'); -end; - -//-------------------------------------------------------------------------------------------------- -// Tests for TotalCount -//-------------------------------------------------------------------------------------------------- - -procedure TTestDataSeries.TestTotalCountIncrementsCorrectly; -var - newData: TArray>; - i: Integer; - DataPoint: TDataPoint; -begin - Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 on creation'); - - // Test single add - DataPoint := TDataPoint.Create(Now, TAskBidItem.Create(1, 1)); - FSeries := FSeries.Add([DataPoint], 0, 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 := FSeries.Add(newData, 0, Length(newData)); - Assert.AreEqual(Int64(11), FSeries.TotalCount, 'TotalCount should be 11 after bulk add'); -end; - -procedure TTestDataSeries.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 TTestDataSeries.TestTotalCountResetsOnRecreate; -begin - SetupSeriesWithData(15); - Assert.IsTrue(FSeries.TotalCount > 0, 'Pre-condition: TotalCount should be greater than 0'); - - // Re-create the series to clear it - FSeries := TDataSeries.CreateDataSeries(0); - - Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 after re-creation'); - Assert.AreEqual(Int64(0), FSeries.Count, 'Count should be 0 after re-creation'); -end; - -//-------------------------------------------------------------------------------------------------- -// Tests for IndexOf -//-------------------------------------------------------------------------------------------------- - -procedure TTestDataSeries.TestIndexOfExistingTimeStamp; -var - ATimeStamp: TDateTime; - expectedIndex: Int64; -begin - SetupSeriesWithData; - ATimeStamp := EncodeDate(2020, 7, 7) + 9; // Newest item - expectedIndex := 0; - - Assert.AreEqual(expectedIndex, FSeries.IndexOf(ATimeStamp), 'IndexOf for the newest existing item timestamp should return 0'); -end; - -procedure TTestDataSeries.TestIndexOfNonExistingBetween; -var - ATimeStamp: TDateTime; - expectedIndex: Int64; -begin - SetupSeriesWithData; - // Time is between item 8 (2020-07-15) and 9 (2020-07-16) - ATimeStamp := EncodeDate(2020, 7, 15) + 0.5; - // Should return the index of the preceding item, which is the one at 2020-07-15. - // Its logical index is 1 (0 is newest at 2020-07-16) - expectedIndex := Int64(1); - - 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 := FSeries.Add([DataPoint], 0, 1); - - 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 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. - // This item corresponds to the 100th item added (i=100), since 500-400=100 items are trimmed from the start. - // 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 TTestDataSeries.TestIndexOfWithTrimmedData; -var - baseTime, searchTime: TDateTime; -begin - SetupSeriesWithData(500, 400); // Oldest visible item has time baseTime+100 - baseTime := EncodeDate(2020, 7, 7); - - // Search for an exact timestamp that was trimmed - searchTime := baseTime + 50; // Added, but now outside the lookback window - Assert.AreEqual(Int64(-1), FSeries.IndexOf(searchTime), 'IndexOf should not find an exact timestamp that has been trimmed'); - - // Search for a non-exact timestamp in the trimmed range - searchTime := baseTime + 99.5; // This would resolve to item 99, which is trimmed. - Assert.AreEqual(Int64(-1), FSeries.IndexOf(searchTime), 'IndexOf should return -1 when the preceding item is trimmed'); - - // Search for a non-exact timestamp that should resolve to the oldest visible item - searchTime := baseTime + 100.5; // This should resolve to item 100 (logical index 399) - Assert.AreEqual( - Int64(399), - FSeries.IndexOf(searchTime), - 'IndexOf should find the oldest visible item when searching just after its timestamp' - ); -end; - -//-------------------------------------------------------------------------------------------------- -// Tests for Copy (Immutability) -//-------------------------------------------------------------------------------------------------- - -procedure TTestDataSeries.TestCopyHasSameContent; -var - copy: TDataSeries; - i: Integer; -begin - SetupSeriesWithData(15); - - copy := FSeries; - - 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, 11); - - // Create the copy - copy := FSeries; - originalCount := copy.Count; - originalTotalCount := copy.TotalCount; - - // Modify the original series by creating a new one - lastTime := FSeries[0].Time; - newPoint := TDataPoint.Create(lastTime + 1, TAskBidItem.Create(99, 99.1)); - FSeries := FSeries.Add([newPoint], 0, 1); - - Assert.AreEqual(Int64(11), FSeries.Count, 'Original series 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; - - // 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 := FSeries.Add([newPoint], 0, 1); - - // 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; - -//-------------------------------------------------------------------------------------------------- -// Tests for Lookback -//-------------------------------------------------------------------------------------------------- - -procedure TTestDataSeries.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 TTestDataSeries.TestCountIsCappedByMaxLookback_WithTrim; -const - ITEMS_TO_ADD = 2000; - LOOKBACK_LIMIT = 900; -begin - SetupSeriesWithData(ITEMS_TO_ADD, LOOKBACK_LIMIT); - - // Internally, the item count will be trimmed. - // The public Count should be capped at the lookback limit. - Assert.AreEqual(Int64(LOOKBACK_LIMIT), FSeries.Count, 'Public count should be capped by MaxLookback after trimming'); -end; - -procedure TTestDataSeries.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, - EAssertionFailed, // Using EAssertionFailed here to catch potential internal range checks - '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 TTestDataSeries.TestDataAndTimePropertiesRespectLookback; -var - baseTime: TDateTime; - expectedOldestTime: TDateTime; - expectedOldestAsk: Double; - expectedNewestTime: TDateTime; - expectedNewestAsk: Double; -begin - SetupSeriesWithData(20, 15); // Add 20 items, lookback 15. - // Visible items are those originally at index 5 through 19. - baseTime := EncodeDate(2020, 7, 7); - - // Newest item in series is from i=19. Time = baseTime + 19. Logical index = 0. - expectedNewestTime := baseTime + 19; - expectedNewestAsk := 19.0; - // Oldest visible item is from i=5. Time = baseTime + 5. Logical index = 14. - expectedOldestTime := baseTime + 5; - expectedOldestAsk := 5.0; - - Assert.AreEqual(Int64(15), FSeries.Count, 'Pre-condition: Count must be 15'); - - // Check Data and Time properties at the boundaries - Assert.AreEqual(expectedNewestTime, FSeries.Time[0], 'Time[0] should be the newest visible item''s time'); - Assert.AreEqual(expectedNewestAsk, FSeries.Data[0].Ask, 'Data[0] should be the newest visible item''s data'); - - Assert.AreEqual(expectedOldestTime, FSeries.Time[14], 'Time[Count-1] should be the oldest visible item''s time'); - Assert.AreEqual(expectedOldestAsk, FSeries.Data[14].Ask, 'Data[Count-1] should be the oldest visible item''s data'); - - // Check access will fail beyond the lookback-limited count - Assert.WillRaise( - procedure begin var dummy := FSeries.Data[15]; end, - EAssertionFailed, - 'Accessing Data property beyond public count should fail' - ); - Assert.WillRaise( - procedure begin var dummy := FSeries.Time[15]; end, - EAssertionFailed, - 'Accessing Time property beyond public count should fail' - ); -end; - -procedure TTestDataSeries.TestLookbackZero; -var - DataPoint: TDataPoint; -begin - FSeries := TDataSeries.CreateDataSeries(0); - Assert.AreEqual(Int64(0), FSeries.Lookback, 'Lookback should be 0'); - Assert.AreEqual(Int64(0), FSeries.Count, 'Count on new series should be 0'); - Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount on new series should be 0'); - - // Add an item - DataPoint := TDataPoint.Create(Now, TAskBidItem.Create(1, 1.1)); - FSeries := FSeries.Add([DataPoint], 0, 1); - - // Count should still be 0, but TotalCount should be 1 - Assert.AreEqual(Int64(0), FSeries.Count, 'Count must remain 0 when Lookback is 0'); - Assert.AreEqual(Int64(1), FSeries.TotalCount, 'TotalCount should increment even with Lookback 0'); - - // Accessing any item should fail - Assert.WillRaise( - procedure begin var dummy := FSeries[0]; end, - EAssertionFailed, - 'Accessing item[0] on a series with Lookback=0 should raise an error' - ); - - // IndexOf should also find nothing - Assert.AreEqual(Int64(-1), FSeries.IndexOf(Now), 'IndexOf on a series with Lookback=0 should return -1'); -end; - -procedure TTestDataSeries.TestLookbackOne; -var - DataPoint1, DataPoint2, DataPoint3: TDataPoint; - time1, time2, time3: TDateTime; -begin - FSeries := TDataSeries.CreateDataSeries(1); - Assert.AreEqual(Int64(1), FSeries.Lookback, 'Lookback should be 1'); - - // Add first item - time1 := Now; - DataPoint1 := TDataPoint.Create(time1, TAskBidItem.Create(1, 1.1)); - FSeries := FSeries.Add([DataPoint1], 0, 1); - Assert.AreEqual(Int64(1), FSeries.Count, 'Count should be 1 after one add'); - Assert.AreEqual(Int64(1), FSeries.TotalCount, 'TotalCount should be 1'); - Assert.AreEqual(time1, FSeries[0].Time, 'The first item should be at index 0'); - - // Add second item - time2 := time1 + EncodeTime(0, 0, 1, 0); - DataPoint2 := TDataPoint.Create(time2, TAskBidItem.Create(2, 2.1)); - FSeries := FSeries.Add([DataPoint2], 0, 1); - Assert.AreEqual(Int64(1), FSeries.Count, 'Count must be capped at 1'); - Assert.AreEqual(Int64(2), FSeries.TotalCount, 'TotalCount should be 2'); - Assert.AreEqual(time2, FSeries[0].Time, 'The newest item should now be at index 0'); - - // Add third item - time3 := time2 + EncodeTime(0, 0, 1, 0); - DataPoint3 := TDataPoint.Create(time3, TAskBidItem.Create(3, 3.1)); - FSeries := FSeries.Add([DataPoint3], 0, 1); - Assert.AreEqual(Int64(1), FSeries.Count, 'Count must still be capped at 1'); - Assert.AreEqual(Int64(3), FSeries.TotalCount, 'TotalCount should be 3'); - Assert.AreEqual(time3, FSeries[0].Time, 'The third item should now be at index 0'); - - // Accessing index 1 should fail - Assert.WillRaise( - procedure begin var dummy := FSeries[1]; end, - EAssertionFailed, - 'Accessing item[1] on a series with Lookback=1 should raise an error' - ); -end; - -initialization - TDUnitX.RegisterTestFixture(TTestDataSeries); -end. diff --git a/Src/Myc.Trade.DataConverter.pas b/Src/Myc.Trade.DataConverter.pas deleted file mode 100644 index 2340881..0000000 --- a/Src/Myc.Trade.DataConverter.pas +++ /dev/null @@ -1,145 +0,0 @@ -unit Myc.Trade.DataConverter; - -interface - -uses - Myc.Signals, - Myc.Trade.Types, - Myc.Trade.DataPoint; - -type - // Interface helper for IMycConverter providing the null object pattern. - TConverter = record - type - IConverter = interface(IMycProcessor) - function GetSender: TDataProvider.IDataProvider; - property Sender: TDataProvider.IDataProvider read GetSender; - end; - - {$REGION 'private'} - strict private - class var - FNull: IConverter; - class constructor CreateClass; - private - FConverter: IConverter; - function GetSender: TDataProvider; inline; - {$ENDREGION} - public - constructor Create(const AConverter: IConverter); - - // Managed record operators - class operator Initialize(out Dest: TConverter); - class operator Implicit(const A: IConverter): TConverter; overload; - class operator Implicit(const A: TConverter): IConverter; overload; - - class function CreateGeneric(const Func: TConstFunc): TConverter; static; - - // Wrapper for IMycProcessor.ProcessData - function ProcessData(const Value: S): TState; inline; - - function Chain(const Next: TConverter): TConverter; overload; inline; - function Chain(const Func: TConstFunc): TConverter; overload; inline; - - function Field(const FieldName: String): TConverter; overload; inline; - - // Provides access to the null object instance. - class property Null: IConverter read FNull; - // Wrapper for IMycConverter.Sender - property Sender: TDataProvider read GetSender; - end; - - TConverter = record - class function CreateCounter: TConverter; static; - class function CreateTicker: TConverter, T>; static; - class function CreateRecordField(const FieldName: String): TConverter; static; - end; - -implementation - -uses - Myc.Trade.Core.DataConverter; - -{ TConverter } - -class constructor TConverter.CreateClass; -begin - // Create the singleton null object instance. - FNull := TNullConverter.Create; -end; - -constructor TConverter.Create(const AConverter: IConverter); -begin - FConverter := AConverter; - // Ensure that the internal interface is never nil. - if not Assigned(FConverter) then - FConverter := FNull; -end; - -function TConverter.Chain(const Next: TConverter): TConverter; -begin - FConverter.Sender.Link(Next); - Result := Next; -end; - -function TConverter.Chain(const Func: TConstFunc): TConverter; -begin - Result := Chain(TMycGenericConverter.Create(Func)); -end; - -class function TConverter.CreateGeneric(const Func: TConstFunc): TConverter; -begin - Result := TMycGenericConverter.Create(Func); -end; - -function TConverter.Field(const FieldName: String): TConverter; -begin - Result := Chain(TMycRecordFieldReader.Create(FieldName)); -end; - -function TConverter.GetSender: TDataProvider; -begin - // Forward the call to the wrapped interface. - Result := FConverter.Sender; -end; - -class operator TConverter.Initialize(out Dest: TConverter); -begin - // Initialize new record instances with the null object. - Dest.FConverter := FNull; -end; - -class operator TConverter.Implicit(const A: IConverter): TConverter; -begin - // Allow implicit conversion from the interface to the helper. - Result.Create(A); -end; - -class operator TConverter.Implicit(const A: TConverter): IConverter; -begin - // Allow implicit conversion from the helper to the interface. - Result := A.FConverter; -end; - -function TConverter.ProcessData(const Value: S): TState; -begin - // Forward the call to the wrapped interface. - Result := FConverter.ProcessData(Value); -end; - -class function TConverter.CreateCounter: TConverter; -begin - Result := TMycDataCounter.Create; -end; - -class function TConverter.CreateRecordField(const FieldName: String): TConverter; -begin - Result := TMycRecordFieldReader.Create(FieldName); -end; - -class function TConverter.CreateTicker: TConverter, T>; -begin - Result := TMycTicker.Create; -end; - -end. diff --git a/Src/Myc.Trade.Core.DataConverter.pas b/Src/Myc.Trade.DataPoint.Impl.pas similarity index 94% rename from Src/Myc.Trade.Core.DataConverter.pas rename to Src/Myc.Trade.DataPoint.Impl.pas index f84d46d..dba38ec 100644 --- a/Src/Myc.Trade.Core.DataConverter.pas +++ b/Src/Myc.Trade.DataPoint.Impl.pas @@ -1,12 +1,11 @@ -unit Myc.Trade.Core.DataConverter; +unit Myc.Trade.DataPoint.Impl; interface uses Myc.Signals, Myc.Trade.Types, - Myc.Trade.DataPoint, - Myc.Trade.DataConverter; + Myc.Trade.DataPoint; type // Null object implementation for IMycConverter diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 9598165..fed9483 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -4,6 +4,7 @@ interface uses Myc.Signals, + Myc.Trade.Types, Myc.Core.Notifier; type @@ -97,8 +98,58 @@ type procedure Unlink(Tag: TDataProvider.TTag); end; + // Interface helper for IMycConverter providing the null object pattern. + TConverter = record + type + IConverter = interface(IMycProcessor) + function GetSender: TDataProvider.IDataProvider; + property Sender: TDataProvider.IDataProvider read GetSender; + end; + + {$REGION 'private'} + strict private + class var + FNull: IConverter; + class constructor CreateClass; + private + FConverter: IConverter; + function GetSender: TDataProvider; inline; + {$ENDREGION} + public + constructor Create(const AConverter: IConverter); + + // Managed record operators + class operator Initialize(out Dest: TConverter); + class operator Implicit(const A: IConverter): TConverter; overload; + class operator Implicit(const A: TConverter): IConverter; overload; + + class function CreateGeneric(const Func: TConstFunc): TConverter; static; + + // Wrapper for IMycProcessor.ProcessData + function ProcessData(const Value: S): TState; inline; + + function Chain(const Next: TConverter): TConverter; overload; inline; + function Chain(const Func: TConstFunc): TConverter; overload; inline; + + function Field(const FieldName: String): TConverter; overload; inline; + + // Provides access to the null object instance. + class property Null: IConverter read FNull; + // Wrapper for IMycConverter.Sender + property Sender: TDataProvider read GetSender; + end; + + TConverter = record + class function CreateCounter: TConverter; static; + class function CreateTicker: TConverter, T>; static; + class function CreateRecordField(const FieldName: String): TConverter; static; + end; + implementation +uses + Myc.Trade.DataPoint.Impl; + { TNullDataProvider } function TNullDataProvider.Link(const Receiver: IMycProcessor): TDataProvider.TTag; @@ -263,4 +314,86 @@ begin end; end; +{ TConverter } + +class constructor TConverter.CreateClass; +begin + // Create the singleton null object instance. + FNull := TNullConverter.Create; +end; + +constructor TConverter.Create(const AConverter: IConverter); +begin + FConverter := AConverter; + // Ensure that the internal interface is never nil. + if not Assigned(FConverter) then + FConverter := FNull; +end; + +function TConverter.Chain(const Next: TConverter): TConverter; +begin + FConverter.Sender.Link(Next); + Result := Next; +end; + +function TConverter.Chain(const Func: TConstFunc): TConverter; +begin + Result := Chain(TMycGenericConverter.Create(Func)); +end; + +class function TConverter.CreateGeneric(const Func: TConstFunc): TConverter; +begin + Result := TMycGenericConverter.Create(Func); +end; + +function TConverter.Field(const FieldName: String): TConverter; +begin + Result := Chain(TMycRecordFieldReader.Create(FieldName)); +end; + +function TConverter.GetSender: TDataProvider; +begin + // Forward the call to the wrapped interface. + Result := FConverter.Sender; +end; + +class operator TConverter.Initialize(out Dest: TConverter); +begin + // Initialize new record instances with the null object. + Dest.FConverter := FNull; +end; + +class operator TConverter.Implicit(const A: IConverter): TConverter; +begin + // Allow implicit conversion from the interface to the helper. + Result.Create(A); +end; + +class operator TConverter.Implicit(const A: TConverter): IConverter; +begin + // Allow implicit conversion from the helper to the interface. + Result := A.FConverter; +end; + +function TConverter.ProcessData(const Value: S): TState; +begin + // Forward the call to the wrapped interface. + Result := FConverter.ProcessData(Value); +end; + +class function TConverter.CreateCounter: TConverter; +begin + Result := TMycDataCounter.Create; +end; + +class function TConverter.CreateRecordField(const FieldName: String): TConverter; +begin + Result := TMycRecordFieldReader.Create(FieldName); +end; + +class function TConverter.CreateTicker: TConverter, T>; +begin + Result := TMycTicker.Create; +end; + end. diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas index bd18d32..049b767 100644 --- a/Src/Myc.Trade.Indicators.pas +++ b/Src/Myc.Trade.Indicators.pas @@ -6,8 +6,7 @@ uses System.SysUtils, System.Math, Myc.Trade.Types, - Myc.Trade.DataArray, - Myc.Trade.DataConverter; + Myc.Trade.DataArray; type // Result for the Moving Average Convergence Divergence (MACD) indicator. diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index 0893423..d3fd943 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -28,7 +28,6 @@ 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.DataPoint in '..\Src\Myc.Test.Trade.DataPoint.pas', Myc.Mutable in '..\Src\Myc.Mutable.pas', Test.Core.Mutable in 'Test.Core.Mutable.pas'; diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index 4a5fd2b..c4a08d8 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -128,7 +128,6 @@ $(PreBuildEvent)]]> -