diff --git a/AuraTrader/MainForm.fmx b/AuraTrader/MainForm.fmx index b2f7f61..a0c0ed2 100644 --- a/AuraTrader/MainForm.fmx +++ b/AuraTrader/MainForm.fmx @@ -3,7 +3,7 @@ object Form1: TForm1 Top = 0 Caption = 'Form1' ClientHeight = 840 - ClientWidth = 808 + ClientWidth = 797 FormFactor.Width = 320 FormFactor.Height = 480 FormFactor.Devices = [Desktop] @@ -12,7 +12,7 @@ object Form1: TForm1 DesignerMasterStyle = 0 object Panel1: TPanel Align = Top - Size.Width = 808.00000000000000000 + Size.Width = 797.00000000000000000 Size.Height = 529.00000000000000000 Size.PlatformDefault = False TabOrder = 2 @@ -28,13 +28,13 @@ object Form1: TForm1 Anchors = [akLeft, akTop, akRight] Position.X = 8.00000000000000000 Position.Y = 8.00000000000000000 - Size.Width = 273.00000000000000000 + Size.Width = 262.00000000000000000 Size.Height = 513.00000000000000000 Size.PlatformDefault = False end object SymbolsComboBox: TComboBox Anchors = [akTop, akRight] - Position.X = 505.00000000000000000 + Position.X = 494.00000000000000000 Position.Y = 8.00000000000000000 Size.Width = 217.00000000000000000 Size.Height = 22.00000000000000000 @@ -43,7 +43,7 @@ object Form1: TForm1 end object LoadButton: TButton Anchors = [akTop, akRight] - Position.X = 730.00000000000000000 + Position.X = 719.00000000000000000 Position.Y = 8.00000000000000000 Size.Width = 47.00000000000000000 Size.Height = 22.00000000000000000 @@ -69,11 +69,11 @@ object Form1: TForm1 Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] DataDetectorTypes = [] Align = Client - Size.Width = 808.00000000000000000 + Size.Width = 797.00000000000000000 Size.Height = 311.00000000000000000 Size.PlatformDefault = False TabOrder = 1 - Viewport.Width = 804.00000000000000000 + Viewport.Width = 793.00000000000000000 Viewport.Height = 307.00000000000000000 end end diff --git a/Src/Myc.Test.Trade.DataPoint.pas b/Src/Myc.Test.Trade.DataPoint.pas index ba048c3..77cd56f 100644 --- a/Src/Myc.Test.Trade.DataPoint.pas +++ b/Src/Myc.Test.Trade.DataPoint.pas @@ -95,6 +95,14 @@ type procedure TestIndexOfSingleItemSeries; [Test] procedure TestIndexOfRespectsMaxLookback; + + // Tests for Copy + [Test] + procedure TestCopyHasSameContent; + [Test] + procedure TestCopyIsImmutableAfterOriginalChanges; + [Test] + procedure TestCopyRespectsMaxLookback; end; implementation @@ -403,7 +411,7 @@ end; procedure TTestDataSeries.Setup; begin FArray := TDataSeries.CreateArray(0); - FSeries.Create(FArray); + FSeries := FArray; end; procedure TTestDataSeries.Teardown; @@ -419,7 +427,7 @@ var baseTime: TDateTime; begin FArray := TDataSeries.CreateArray(MaxLookBack); - FSeries.Create(FArray); + FSeries := FArray; baseTime := EncodeDate(2020, 7, 7); // Add data points with increasing timestamps. @@ -430,6 +438,79 @@ begin end; end; +procedure TTestDataSeries.TestCopyHasSameContent; +var + copy: TDataSeries; + i: Integer; +begin + SetupSeriesWithData(15); + + copy := FSeries.Copy; + + 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); + + // Create the copy + copy := FSeries.Copy; + originalCount := copy.Count; + originalTotalCount := copy.TotalCount; + + // Modify the original series + lastTime := FArray[0].Time; + newPoint := TDataPoint.Create(lastTime + 1, TAskBidItem.Create(99, 99.1)); + FArray.Add(newPoint); + + Assert.AreEqual(Int64(11), FArray.Count, 'Original array 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(FArray[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.Copy; + + // 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 := FArray[0].Time; + newPoint := TDataPoint.Create(lastTime + 1, TAskBidItem.Create(99, 99.1)); + FArray.Add(newPoint); + + // 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; + procedure TTestDataSeries.TestIndexOfExistingTimeStamp; var ATimeStamp: TDateTime; diff --git a/Src/Myc.Trade.Core.DataPoint.pas b/Src/Myc.Trade.Core.DataPoint.pas index 69aaab7..e8b8116 100644 --- a/Src/Myc.Trade.Core.DataPoint.pas +++ b/Src/Myc.Trade.Core.DataPoint.pas @@ -8,13 +8,13 @@ uses type // The implementation class for IDataSeries. TDataArray = class(TInterfacedObject, IDataSeries, IDataArray) - private const ChunkSize = 1024; type TChunk = TArray>; private FChunks: TArray; + FChunks1: TArray; FCount: Int64; FMaxLookback: Int64; FTotalCount: Int64; @@ -33,17 +33,47 @@ type procedure Add(const Data: TDataPoint); overload; procedure Add(const Data: array of TDataPoint; NumToAdd: Integer = -1); overload; procedure Clear; + function Copy: IDataSeries; property MaxLookback: Int64 read GetMaxLookback; end; -type // Null object implementation for IDataSeries TNullDataSeries = class(TInterfacedObject, IDataSeries) + strict private + class var + FNull: IDataSeries; + private + class constructor CreateClass; public function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; function IndexOf(TimeStamp: TDateTime): Int64; + function Copy: IDataSeries; + class property Null: IDataSeries read FNull; + end; + + // The implementation class for IDataSeries. + TStaticDataSeries = class(TInterfacedObject, IDataSeries) + private + FChunks: TArray.TChunk>; + FCount: Int64; + FMaxLookback: Int64; + FTotalCount: Int64; + + function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline; + function IsIndexInLimits(LogicalIndex: Int64): Boolean; + + // IDataSeries implementation + function GetCount: Int64; + function GetItems(Idx: Int64): TDataPoint; + function GetMaxLookback: Int64; + function GetTotalCount: Int64; + + public + constructor Create(const AChunks: TArray.TChunk>; ACount, AMaxLookback, ATotalCount: Int64); + function Copy: IDataSeries; + property MaxLookback: Int64 read GetMaxLookback; end; implementation @@ -141,6 +171,11 @@ begin FTotalCount := 0; end; +function TDataArray.Copy: IDataSeries; +begin + Result := TStaticDataSeries.Create( FChunks, FCount, FMaxLookback, FTotalCount ); +end; + function TDataArray.GetCount: Int64; begin Result := FCount; @@ -197,11 +232,23 @@ begin if chunksToRemove > 0 then begin var itemsInFreedChunks := chunksToRemove * ChunkSize; - FChunks := Copy(FChunks, chunksToRemove, Length(FChunks) - chunksToRemove); + FChunks := System.Copy(FChunks, chunksToRemove, Length(FChunks) - chunksToRemove); FCount := FCount - itemsInFreedChunks; end; end; +{ TNullDataSeries Interface Helper } + +class constructor TNullDataSeries.CreateClass; +begin + FNull := TNullDataSeries.Create; +end; + +function TNullDataSeries.Copy: IDataSeries; +begin + Result := FNull; +end; + function TNullDataSeries.GetCount: Int64; begin Result := 0; @@ -223,4 +270,57 @@ begin Result := -1; end; +{ TStaticDataSeries } + +constructor TStaticDataSeries.Create(const AChunks: TArray.TChunk>; ACount, AMaxLookback, ATotalCount: Int64); +begin + inherited Create; + FChunks := AChunks; + FCount := ACount; + FMaxLookback := AMaxLookback; + FTotalCount := ATotalCount; +end; + +function TStaticDataSeries.Copy: IDataSeries; +begin + Result := Self; +end; + +function TStaticDataSeries.GetCount: Int64; +begin + Result := FCount; + if (FMaxLookback > 0) and (Result > FMaxLookback) then + Result := FMaxLookback; +end; + +function TStaticDataSeries.GetItems(Idx: Int64): TDataPoint; +var + physicalIndex: Int64; +begin + Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.'); + physicalIndex := LogicalToPhysicalIndex(Idx); + Result := FChunks[physicalIndex div TDataArray.ChunkSize][physicalIndex mod TDataArray.ChunkSize]; +end; + +function TStaticDataSeries.GetMaxLookback: Int64; +begin + Result := FMaxLookback; +end; + +function TStaticDataSeries.GetTotalCount: Int64; +begin + Result := FTotalCount; +end; + +function TStaticDataSeries.IsIndexInLimits(LogicalIndex: Int64): Boolean; +begin + Result := (FMaxLookback <= 0) or (LogicalIndex < FMaxLookback); +end; + +function TStaticDataSeries.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; +begin + Assert((LogicalIndex >= 0) and (LogicalIndex < FCount), 'Logical index is out of bounds.'); + Result := FCount - LogicalIndex - 1; +end; + end. diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index 932a389..cf09b02 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -23,6 +23,7 @@ type function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; + function Copy: IDataSeries; property Count: Int64 read GetCount; // Accesses data points by their logical index. // Index 0 is the newest element, Index (Count - 1) is the oldest. @@ -40,18 +41,15 @@ type // Interface Helper for IDataSeries. // Provides a safe, value-type-like wrapper around the interface. TDataSeries = record - strict private - class var - FNull: IDataSeries; private FDataSeries: IDataSeries; FMaxLookback: Int64; - class constructor CreateClass; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetData(Idx: Int64): T; function GetTime(Idx: Int64): TDateTime; function GetMaxLookback: Int64; + class function GetNull: IDataSeries; static; function GetTotalCount: Int64; public constructor Create(ADataSeries: IDataSeries); @@ -64,13 +62,16 @@ type class function CreateArray(MaxLookback: Int64): IDataArray; static; + // Create a guaranteed immutable version of the given series. + function Copy: TDataSeries; + // Searches for a data point by its timestamp. // Returns the logical index of the matching item. // If no exact match, returns the index of the item immediately preceding the timestamp. // Returns -1 if the timestamp is before the oldest item in the series. function IndexOf(TimeStamp: TDateTime): Int64; - class property Null: IDataSeries read FNull; + class property Null: IDataSeries read GetNull; property Count: Int64 read GetCount; property Items[Idx: Int64]: TDataPoint read GetItems; default; property Data[Idx: Int64]: T read GetData; @@ -100,19 +101,17 @@ begin Data := AData; end; -{ TDataSeries Interface Helper } - -class constructor TDataSeries.CreateClass; -begin - FNull := TNullDataSeries.Create; -end; - constructor TDataSeries.Create(ADataSeries: IDataSeries); begin if Assigned(ADataSeries) then FDataSeries := ADataSeries else - FDataSeries := FNull; + FDataSeries := Null; +end; + +function TDataSeries.Copy: TDataSeries; +begin + Result := FDataSeries.Copy; end; class function TDataSeries.CreateArray(MaxLookback: Int64): IDataArray; @@ -127,7 +126,7 @@ end; class operator TDataSeries.Initialize(out Dest: TDataSeries); begin - Dest.FDataSeries := FNull; + Dest.FDataSeries := Null; end; class operator TDataSeries.Implicit(const A: TDataSeries): IDataSeries; @@ -165,6 +164,11 @@ begin Result := FMaxLookback; end; +class function TDataSeries.GetNull: IDataSeries; +begin + Result := TNullDataSeries.Null; +end; + function TDataSeries.GetTotalCount: Int64; begin Result := FDataSeries.GetTotalCount;