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)]]>
-