Unit refactoring

This commit is contained in:
Michael Schimmel
2025-07-15 09:13:40 +02:00
parent 3e0628ad57
commit ce0cba720a
12 changed files with 138 additions and 927 deletions
+1 -3
View File
@@ -13,9 +13,7 @@ uses
Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas', Myc.Trade.DataArray in '..\Src\Myc.Trade.DataArray.pas',
Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas', Myc.FMX.Chart.Series in '..\Src\Myc.FMX.Chart.Series.pas',
Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas', Myc.Trade.Indicators in '..\Src\Myc.Trade.Indicators.pas',
Myc.Trade.Types in '..\Src\Myc.Trade.Types.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';
{$R *.res} {$R *.res}
-8
View File
@@ -142,8 +142,6 @@
<DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/> <DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Indicators.pas"/> <DCCReference Include="..\Src\Myc.Trade.Indicators.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Types.pas"/> <DCCReference Include="..\Src\Myc.Trade.Types.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataConverter.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Core.DataConverter.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
@@ -187,12 +185,6 @@
<Overwrite>true</Overwrite> <Overwrite>true</Overwrite>
</Platform> </Platform>
</DeployFile> </DeployFile>
<DeployFile LocalName="Win64\Debug\AuraTrader.rsm" Configuration="Debug" Class="DebugSymbols">
<Platform Name="Win64">
<RemoteName>AuraTrader.rsm</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployFile LocalName="Win64\Release\AuraTrader.exe" Configuration="Release" Class="ProjectOutput"> <DeployFile LocalName="Win64\Release\AuraTrader.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win64"> <Platform Name="Win64">
<RemoteName>AuraTrader.exe</RemoteName> <RemoteName>AuraTrader.exe</RemoteName>
+1 -2
View File
@@ -11,8 +11,7 @@ uses
Myc.Trade.Types, Myc.Trade.Types,
Myc.Trade.DataPoint, Myc.Trade.DataPoint,
Myc.Trade.DataArray, Myc.Trade.DataArray,
Myc.Trade.DataConverter, Myc.Trade.DataPoint.Impl;
Myc.Trade.Core.DataConverter;
type type
TTickAggregation = class(TMycConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>) TTickAggregation = class(TMycConverter<TDataPoint<Double>, TDataPoint<TOhlcItem>>)
-1
View File
@@ -30,7 +30,6 @@ uses
Myc.Trade.Types, Myc.Trade.Types,
Myc.Trade.DataStream, Myc.Trade.DataStream,
Myc.Trade.DataPoint, Myc.Trade.DataPoint,
Myc.Trade.DataConverter,
Myc.Signals, Myc.Signals,
Myc.Mutable, Myc.Mutable,
Myc.Signals.FMX, Myc.Signals.FMX,
-2
View File
@@ -11,8 +11,6 @@ uses
Myc.Trade.Types, Myc.Trade.Types,
Myc.Trade.DataArray, Myc.Trade.DataArray,
Myc.Trade.DataPoint, Myc.Trade.DataPoint,
Myc.Trade.DataConverter,
Myc.Trade.Core.DataConverter,
Myc.Fmx.Chart; Myc.Fmx.Chart;
type type
-759
View File
@@ -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<TAskBidItem>;
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<TAskBidItem>.CreateDataSeries(1);
end;
procedure TTestDataSeries.Teardown;
begin
FSeries := Default(TDataSeries<TAskBidItem>);
end;
procedure TTestDataSeries.SetupSeriesWithData(ItemCount: Integer = 10; MaxLookBack: Int64 = 10);
var
i: Integer;
DataPoint: TDataPoint<TAskBidItem>;
baseTime: TDateTime;
begin
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(MaxLookBack);
baseTime := EncodeDate(2020, 7, 7);
// Add data points with increasing timestamps.
for i := 0 to ItemCount - 1 do
begin
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
FSeries := 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<TAskBidItem>;
newTime: TDateTime;
begin
SetupSeriesWithData(10, 11);
newTime := EncodeDate(2020, 7, 17);
Assert.AreEqual(Int64(10), FSeries.Count, 'Initial count should be 10');
DataPoint := TDataPoint<TAskBidItem>.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<TAskBidItem>;
begin
SetupSeriesWithData; // Newest item is at 2020-07-16
olderTime := EncodeDate(2020, 7, 15);
Assert.WillRaise(
procedure
begin
DataPoint := TDataPoint<TAskBidItem>.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<TDataPoint<TAskBidItem>>;
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<TAskBidItem>.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<TDataPoint<TAskBidItem>>;
begin
SetupSeriesWithData(10);
SetLength(newData, 2);
newData[0] := TDataPoint<TAskBidItem>.Create(Now + 1, TAskBidItem.Create(1, 1));
newData[1] := TDataPoint<TAskBidItem>.Create(Now, TAskBidItem.Create(2, 2)); // Not sorted
Assert.WillRaise(
procedure begin 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<TDataPoint<TAskBidItem>>;
olderTime: TDateTime;
begin
SetupSeriesWithData(10); // Newest is baseTime + 9
olderTime := EncodeDate(2020, 7, 7) + 8;
SetLength(newData, 1);
newData[0] := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(1, 1));
Assert.WillRaise(
procedure begin 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<TDataPoint<TAskBidItem>>;
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<TAskBidItem>.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<TDataPoint<TAskBidItem>>;
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<TAskBidItem>.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<TDataPoint<TAskBidItem>>;
i: Integer;
DataPoint: TDataPoint<TAskBidItem>;
begin
Assert.AreEqual(Int64(0), FSeries.TotalCount, 'TotalCount should be 0 on creation');
// Test single add
DataPoint := TDataPoint<TAskBidItem>.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<TAskBidItem>.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<TAskBidItem>.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<TAskBidItem>;
testTime: TDateTime;
begin
// Use default setup, but add one item
testTime := EncodeDate(2025, 1, 1);
DataPoint := TDataPoint<TAskBidItem>.Create(testTime, TAskBidItem.Create(1.0, 2.0));
FSeries := 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<TAskBidItem>;
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<TAskBidItem>;
originalCount, originalTotalCount: Int64;
newPoint: TDataPoint<TAskBidItem>;
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<TAskBidItem>.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<TAskBidItem>;
newPoint: TDataPoint<TAskBidItem>;
lastTime: TDateTime;
begin
SetupSeriesWithData(20, 15); // 20 items total, but series count is capped at 15
Assert.AreEqual(Int64(15), FSeries.Count, 'Pre-condition: Series count should be capped by MaxLookback');
Assert.AreEqual(Int64(20), FSeries.TotalCount, 'Pre-condition: Series total count should be 20');
copy := FSeries;
// Verify the copy reflects the MaxLookback state
Assert.AreEqual(Int64(15), copy.Count, 'Copy count should be the MaxLookback value of the original');
Assert.AreEqual(Int64(20), copy.TotalCount, 'Copy total count should be the total items added to the original');
// Modify original
lastTime := FSeries[0].Time;
newPoint := TDataPoint<TAskBidItem>.Create(lastTime + 1, TAskBidItem.Create(99, 99.1));
FSeries := 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<TAskBidItem>;
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<TAskBidItem>;
begin
FSeries := TDataSeries<TAskBidItem>.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<TAskBidItem>.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<TAskBidItem>;
time1, time2, time3: TDateTime;
begin
FSeries := TDataSeries<TAskBidItem>.CreateDataSeries(1);
Assert.AreEqual(Int64(1), FSeries.Lookback, 'Lookback should be 1');
// Add first item
time1 := Now;
DataPoint1 := TDataPoint<TAskBidItem>.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<TAskBidItem>.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<TAskBidItem>.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.
-145
View File
@@ -1,145 +0,0 @@
unit Myc.Trade.DataConverter;
interface
uses
Myc.Signals,
Myc.Trade.Types,
Myc.Trade.DataPoint;
type
// Interface helper for IMycConverter<S,T> providing the null object pattern.
TConverter<S, T> = record
type
IConverter = interface(IMycProcessor<S>)
function GetSender: TDataProvider<T>.IDataProvider;
property Sender: TDataProvider<T>.IDataProvider read GetSender;
end;
{$REGION 'private'}
strict private
class var
FNull: IConverter;
class constructor CreateClass;
private
FConverter: IConverter;
function GetSender: TDataProvider<T>; inline;
{$ENDREGION}
public
constructor Create(const AConverter: IConverter);
// Managed record operators
class operator Initialize(out Dest: TConverter<S, T>);
class operator Implicit(const A: IConverter): TConverter<S, T>; overload;
class operator Implicit(const A: TConverter<S, T>): IConverter; overload;
class function CreateGeneric(const Func: TConstFunc<S, T>): TConverter<S, T>; static;
// Wrapper for IMycProcessor.ProcessData
function ProcessData(const Value: S): TState; inline;
function Chain<R>(const Next: TConverter<T, R>): TConverter<T, R>; overload; inline;
function Chain<R>(const Func: TConstFunc<T, R>): TConverter<T, R>; overload; inline;
function Field<R>(const FieldName: String): TConverter<T, R>; overload; inline;
// Provides access to the null object instance.
class property Null: IConverter read FNull;
// Wrapper for IMycConverter.Sender
property Sender: TDataProvider<T> read GetSender;
end;
TConverter = record
class function CreateCounter<T>: TConverter<T, Int64>; static;
class function CreateTicker<T>: TConverter<TArray<T>, T>; static;
class function CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>; static;
end;
implementation
uses
Myc.Trade.Core.DataConverter;
{ TConverter<S, T> }
class constructor TConverter<S, T>.CreateClass;
begin
// Create the singleton null object instance.
FNull := TNullConverter<S, T>.Create;
end;
constructor TConverter<S, T>.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<S, T>.Chain<R>(const Next: TConverter<T, R>): TConverter<T, R>;
begin
FConverter.Sender.Link(Next);
Result := Next;
end;
function TConverter<S, T>.Chain<R>(const Func: TConstFunc<T, R>): TConverter<T, R>;
begin
Result := Chain<R>(TMycGenericConverter<T, R>.Create(Func));
end;
class function TConverter<S, T>.CreateGeneric(const Func: TConstFunc<S, T>): TConverter<S, T>;
begin
Result := TMycGenericConverter<S, T>.Create(Func);
end;
function TConverter<S, T>.Field<R>(const FieldName: String): TConverter<T, R>;
begin
Result := Chain<R>(TMycRecordFieldReader<T, R>.Create(FieldName));
end;
function TConverter<S, T>.GetSender: TDataProvider<T>;
begin
// Forward the call to the wrapped interface.
Result := FConverter.Sender;
end;
class operator TConverter<S, T>.Initialize(out Dest: TConverter<S, T>);
begin
// Initialize new record instances with the null object.
Dest.FConverter := FNull;
end;
class operator TConverter<S, T>.Implicit(const A: IConverter): TConverter<S, T>;
begin
// Allow implicit conversion from the interface to the helper.
Result.Create(A);
end;
class operator TConverter<S, T>.Implicit(const A: TConverter<S, T>): IConverter;
begin
// Allow implicit conversion from the helper to the interface.
Result := A.FConverter;
end;
function TConverter<S, T>.ProcessData(const Value: S): TState;
begin
// Forward the call to the wrapped interface.
Result := FConverter.ProcessData(Value);
end;
class function TConverter.CreateCounter<T>: TConverter<T, Int64>;
begin
Result := TMycDataCounter<T>.Create;
end;
class function TConverter.CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>;
begin
Result := TMycRecordFieldReader<S, T>.Create(FieldName);
end;
class function TConverter.CreateTicker<T>: TConverter<TArray<T>, T>;
begin
Result := TMycTicker<T>.Create;
end;
end.
@@ -1,12 +1,11 @@
unit Myc.Trade.Core.DataConverter; unit Myc.Trade.DataPoint.Impl;
interface interface
uses uses
Myc.Signals, Myc.Signals,
Myc.Trade.Types, Myc.Trade.Types,
Myc.Trade.DataPoint, Myc.Trade.DataPoint;
Myc.Trade.DataConverter;
type type
// Null object implementation for IMycConverter // Null object implementation for IMycConverter
+133
View File
@@ -4,6 +4,7 @@ interface
uses uses
Myc.Signals, Myc.Signals,
Myc.Trade.Types,
Myc.Core.Notifier; Myc.Core.Notifier;
type type
@@ -97,8 +98,58 @@ type
procedure Unlink(Tag: TDataProvider<T>.TTag); procedure Unlink(Tag: TDataProvider<T>.TTag);
end; end;
// Interface helper for IMycConverter<S,T> providing the null object pattern.
TConverter<S, T> = record
type
IConverter = interface(IMycProcessor<S>)
function GetSender: TDataProvider<T>.IDataProvider;
property Sender: TDataProvider<T>.IDataProvider read GetSender;
end;
{$REGION 'private'}
strict private
class var
FNull: IConverter;
class constructor CreateClass;
private
FConverter: IConverter;
function GetSender: TDataProvider<T>; inline;
{$ENDREGION}
public
constructor Create(const AConverter: IConverter);
// Managed record operators
class operator Initialize(out Dest: TConverter<S, T>);
class operator Implicit(const A: IConverter): TConverter<S, T>; overload;
class operator Implicit(const A: TConverter<S, T>): IConverter; overload;
class function CreateGeneric(const Func: TConstFunc<S, T>): TConverter<S, T>; static;
// Wrapper for IMycProcessor.ProcessData
function ProcessData(const Value: S): TState; inline;
function Chain<R>(const Next: TConverter<T, R>): TConverter<T, R>; overload; inline;
function Chain<R>(const Func: TConstFunc<T, R>): TConverter<T, R>; overload; inline;
function Field<R>(const FieldName: String): TConverter<T, R>; overload; inline;
// Provides access to the null object instance.
class property Null: IConverter read FNull;
// Wrapper for IMycConverter.Sender
property Sender: TDataProvider<T> read GetSender;
end;
TConverter = record
class function CreateCounter<T>: TConverter<T, Int64>; static;
class function CreateTicker<T>: TConverter<TArray<T>, T>; static;
class function CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>; static;
end;
implementation implementation
uses
Myc.Trade.DataPoint.Impl;
{ TNullDataProvider } { TNullDataProvider }
function TNullDataProvider<T>.Link(const Receiver: IMycProcessor<T>): TDataProvider<T>.TTag; function TNullDataProvider<T>.Link(const Receiver: IMycProcessor<T>): TDataProvider<T>.TTag;
@@ -263,4 +314,86 @@ begin
end; end;
end; end;
{ TConverter<S, T> }
class constructor TConverter<S, T>.CreateClass;
begin
// Create the singleton null object instance.
FNull := TNullConverter<S, T>.Create;
end;
constructor TConverter<S, T>.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<S, T>.Chain<R>(const Next: TConverter<T, R>): TConverter<T, R>;
begin
FConverter.Sender.Link(Next);
Result := Next;
end;
function TConverter<S, T>.Chain<R>(const Func: TConstFunc<T, R>): TConverter<T, R>;
begin
Result := Chain<R>(TMycGenericConverter<T, R>.Create(Func));
end;
class function TConverter<S, T>.CreateGeneric(const Func: TConstFunc<S, T>): TConverter<S, T>;
begin
Result := TMycGenericConverter<S, T>.Create(Func);
end;
function TConverter<S, T>.Field<R>(const FieldName: String): TConverter<T, R>;
begin
Result := Chain<R>(TMycRecordFieldReader<T, R>.Create(FieldName));
end;
function TConverter<S, T>.GetSender: TDataProvider<T>;
begin
// Forward the call to the wrapped interface.
Result := FConverter.Sender;
end;
class operator TConverter<S, T>.Initialize(out Dest: TConverter<S, T>);
begin
// Initialize new record instances with the null object.
Dest.FConverter := FNull;
end;
class operator TConverter<S, T>.Implicit(const A: IConverter): TConverter<S, T>;
begin
// Allow implicit conversion from the interface to the helper.
Result.Create(A);
end;
class operator TConverter<S, T>.Implicit(const A: TConverter<S, T>): IConverter;
begin
// Allow implicit conversion from the helper to the interface.
Result := A.FConverter;
end;
function TConverter<S, T>.ProcessData(const Value: S): TState;
begin
// Forward the call to the wrapped interface.
Result := FConverter.ProcessData(Value);
end;
class function TConverter.CreateCounter<T>: TConverter<T, Int64>;
begin
Result := TMycDataCounter<T>.Create;
end;
class function TConverter.CreateRecordField<S, T>(const FieldName: String): TConverter<S, T>;
begin
Result := TMycRecordFieldReader<S, T>.Create(FieldName);
end;
class function TConverter.CreateTicker<T>: TConverter<TArray<T>, T>;
begin
Result := TMycTicker<T>.Create;
end;
end. end.
+1 -2
View File
@@ -6,8 +6,7 @@ uses
System.SysUtils, System.SysUtils,
System.Math, System.Math,
Myc.Trade.Types, Myc.Trade.Types,
Myc.Trade.DataArray, Myc.Trade.DataArray;
Myc.Trade.DataConverter;
type type
// Result for the Moving Average Convergence Divergence (MACD) indicator. // Result for the Moving Average Convergence Divergence (MACD) indicator.
-1
View File
@@ -28,7 +28,6 @@ uses
Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas', Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas',
Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas', Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas',
Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.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', Myc.Mutable in '..\Src\Myc.Mutable.pas',
Test.Core.Mutable in 'Test.Core.Mutable.pas'; Test.Core.Mutable in 'Test.Core.Mutable.pas';
-1
View File
@@ -128,7 +128,6 @@ $(PreBuildEvent)]]></PreBuildEvent>
<DCCReference Include="..\Src\Myc.Trade.DataPoint.pas"/> <DCCReference Include="..\Src\Myc.Trade.DataPoint.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Node.pas"/> <DCCReference Include="..\Src\Myc.Trade.Node.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataStream.pas"/> <DCCReference Include="..\Src\Myc.Trade.DataStream.pas"/>
<DCCReference Include="..\Src\Myc.Test.Trade.DataPoint.pas"/>
<DCCReference Include="..\Src\Myc.Mutable.pas"/> <DCCReference Include="..\Src\Myc.Mutable.pas"/>
<DCCReference Include="Test.Core.Mutable.pas"/> <DCCReference Include="Test.Core.Mutable.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">