Unit refactoring
This commit is contained in:
@@ -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}
|
||||
|
||||
|
||||
@@ -142,8 +142,6 @@
|
||||
<DCCReference Include="..\Src\Myc.FMX.Chart.Series.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Trade.Indicators.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">
|
||||
<Key>Base</Key>
|
||||
</BuildConfiguration>
|
||||
@@ -187,12 +185,6 @@
|
||||
<Overwrite>true</Overwrite>
|
||||
</Platform>
|
||||
</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">
|
||||
<Platform Name="Win64">
|
||||
<RemoteName>AuraTrader.exe</RemoteName>
|
||||
|
||||
@@ -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<Double>, TDataPoint<TOhlcItem>>)
|
||||
|
||||
@@ -30,7 +30,6 @@ uses
|
||||
Myc.Trade.Types,
|
||||
Myc.Trade.DataStream,
|
||||
Myc.Trade.DataPoint,
|
||||
Myc.Trade.DataConverter,
|
||||
Myc.Signals,
|
||||
Myc.Mutable,
|
||||
Myc.Signals.FMX,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
uses
|
||||
Myc.Signals,
|
||||
Myc.Trade.Types,
|
||||
Myc.Trade.DataPoint,
|
||||
Myc.Trade.DataConverter;
|
||||
Myc.Trade.DataPoint;
|
||||
|
||||
type
|
||||
// Null object implementation for IMycConverter
|
||||
@@ -4,6 +4,7 @@ interface
|
||||
|
||||
uses
|
||||
Myc.Signals,
|
||||
Myc.Trade.Types,
|
||||
Myc.Core.Notifier;
|
||||
|
||||
type
|
||||
@@ -97,8 +98,58 @@ type
|
||||
procedure Unlink(Tag: TDataProvider<T>.TTag);
|
||||
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
|
||||
|
||||
uses
|
||||
Myc.Trade.DataPoint.Impl;
|
||||
|
||||
{ TNullDataProvider }
|
||||
|
||||
function TNullDataProvider<T>.Link(const Receiver: IMycProcessor<T>): TDataProvider<T>.TTag;
|
||||
@@ -263,4 +314,86 @@ begin
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -128,7 +128,6 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
||||
<DCCReference Include="..\Src\Myc.Trade.DataPoint.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Trade.Node.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Trade.DataStream.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Test.Trade.DataPoint.pas"/>
|
||||
<DCCReference Include="..\Src\Myc.Mutable.pas"/>
|
||||
<DCCReference Include="Test.Core.Mutable.pas"/>
|
||||
<BuildConfiguration Include="Base">
|
||||
|
||||
Reference in New Issue
Block a user