diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas index c236e08..f9f870e 100644 --- a/AuraTrader/MainForm.pas +++ b/AuraTrader/MainForm.pas @@ -897,7 +897,7 @@ begin exit; var timeframe := TTimeframe.M15; - ExecuteStrategy(Symbol, timeframe, CreateStrategy2(timeframe)); + // ExecuteStrategy(Symbol, timeframe, CreateStrategy2(timeframe)); var tstStrat := StrategyTest.CreateStrategy1(timeframe); ExecuteStrategy(Symbol, timeframe, tstStrat); diff --git a/Src/Myc.Trade.DataArray.pas b/Src/Myc.Trade.DataArray.pas index e4db488..a3d618c 100644 --- a/Src/Myc.Trade.DataArray.pas +++ b/Src/Myc.Trade.DataArray.pas @@ -3,10 +3,7 @@ unit Myc.Trade.DataArray; interface type - // A series is an array of values with the newest ite at index=0. Each series counts the total of added items since creation, but - // it actually may contain less items, because the array size is limited by the lookback parameter, when adding items. - // Series are immutable. - TSeries = record + TChunkArray = record private const ChunkSize = 1024; @@ -14,118 +11,390 @@ type TChunk = TArray; private FChunks: TArray; - FCount: Int64; - FTotalCount: Int64; + FOffset: Integer; + FCount: Integer; + function GetItems(Idx: Integer): T; inline; - function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline; - function GetItems(Idx: Int64): T; inline; public - constructor Create(const AChunks: TArray; ACount, ATotalCount: Int64); + constructor Create(const AChunks: TArray; ACount: Integer; AOffset: Integer); + class operator Initialize(out Dest: TChunkArray); + // Add a single item without creating a temporary array. + procedure Add(const Data: T; MaxCount: Integer); overload; + + // Add items, but ensure that the result has MaxCount items. + procedure Add(const Data: array of T; MaxCount: Integer); overload; + + class function CreateFromArray(const AData: TArray; MaxCount: Integer): TChunkArray; static; + + // Creates a deep copy of the array, optionally truncating it to the last MaxCount items. + function Copy(MaxCount: Integer = -1): TChunkArray; + + property Count: Integer read FCount; + property Items[Idx: Integer]: T read GetItems; default; + end; + + // A series where the last added item has index 0. + TSeries = record + private + FArray: TChunkArray; + FTotalCount: Int64; + function GetCount: Integer; + function GetItems(Idx: Integer): T; + + public + constructor Create(const AArray: TChunkArray; ATotalCount: Int64); class operator Initialize(out Dest: TSeries); - // Add a singe item - function Add(const Data: T; Lookback: Int64 = -1): TSeries; overload; - // Add a ranmge of items - function Add(const Data: array of T; First, Count, Lookback: Int64): TSeries; overload; - // Helper to create a data array from a raw TArray. - class function CreateFromArray(const AData: TArray; First, Count: Integer): TSeries; static; - property Count: Int64 read FCount; + + // Creates a deep copy of the series, optionally truncating it to the last Lookback items. + function Copy(Lookback: Integer = -1): TSeries; + + procedure Add(const Data: T; Lookback: Int64 = -1); overload; + procedure Add(const Data: array of T; Lookback: Integer); overload; + + class function CreateFromArray(const AData: TArray; Lookback: Integer): TSeries; static; + + property Count: Integer read GetCount; property TotalCount: Int64 read FTotalCount; - property Items[Idx: Int64]: T read GetItems; default; + property Items[Idx: Integer]: T read GetItems; default; end; implementation uses + System.Generics.Collections, System.Math; -{ TSeries } +{ TChunkArray } -constructor TSeries.Create(const AChunks: TArray; ACount, ATotalCount: Int64); +constructor TChunkArray.Create(const AChunks: TArray; ACount, AOffset: Integer); begin FChunks := AChunks; FCount := ACount; - FTotalCount := ATotalCount; + FOffset := AOffset; + Assert(FOffset < ChunkSize); end; -class function TSeries.CreateFromArray(const AData: TArray; First, Count: Integer): TSeries; -begin - // Use the Add method on an empty array to perform the chunking logic. - Result.Add(AData, First, Count, Count); -end; - -function TSeries.Add(const Data: array of T; First, Count, Lookback: Int64): TSeries; +procedure TChunkArray.Add(const Data: T; MaxCount: Integer); var - destPhysicalIdx, sourcePhysicalIdx: Int64; - itemsToSkip: Int64; - numNewChunks: Integer; - newChunks: TArray; - destChunkIdx, destSubIdx: Integer; - sumCount, newCount: Int64; + newCount, totalNewOffset: Integer; + oldLogicalEnd, newLogicalEnd, oldStartChunk, newStartChunk, oldEndChunk, newEndChunk, chunksToRemove, chunksToAdd: Integer; begin - if Count < 0 then - Count := Length(Data) - First; - if Count = 0 then - exit(Self); + if MaxCount < 0 then + MaxCount := MaxInt; - Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array'); + if MaxCount <= 0 then + exit; - sumCount := FCount + Count; - newCount := sumCount; - if (Lookback >= 0) and (newCount > Lookback) then - newCount := Lookback; - itemsToSkip := sumCount - newCount; + // 1. Calculate the new logical state (numToAdd is always 1) + newCount := Min(FCount + 1, MaxCount); + totalNewOffset := FOffset + FCount + 1 - newCount; - numNewChunks := 0; + // 2. Determine if the underlying FChunks array needs restructuring + oldLogicalEnd := FOffset + FCount - 1; + newLogicalEnd := totalNewOffset + newCount - 1; + + oldStartChunk := FOffset div ChunkSize; + newStartChunk := totalNewOffset div ChunkSize; + + oldEndChunk := -1; + if FCount > 0 then + oldEndChunk := oldLogicalEnd div ChunkSize; + + newEndChunk := -1; if newCount > 0 then - numNewChunks := (newCount - 1) div ChunkSize + 1; - SetLength(newChunks, numNewChunks); + newEndChunk := newLogicalEnd div ChunkSize; - for destPhysicalIdx := 0 to newCount - 1 do + chunksToRemove := newStartChunk - oldStartChunk; + chunksToAdd := newEndChunk - oldEndChunk; + + // 3. Perform the add operation + if (chunksToRemove > 0) or (chunksToAdd > 0) then begin - destChunkIdx := destPhysicalIdx div ChunkSize; - destSubIdx := destPhysicalIdx mod ChunkSize; + // --- Rebuild Path: Chunks must be added or removed --- + var newChunks: TArray; + var oldChunkCount := Length(FChunks); + var newChunkCount := oldChunkCount - chunksToRemove + chunksToAdd; + SetLength(newChunks, newChunkCount); - if destSubIdx = 0 then - SetLength(newChunks[destChunkIdx], ChunkSize); + var numChunksToKeep := oldChunkCount - chunksToRemove; + if numChunksToKeep > 0 then + TArray.Copy(FChunks, newChunks, chunksToRemove, 0, numChunksToKeep); - sourcePhysicalIdx := itemsToSkip + destPhysicalIdx; + for var i := numChunksToKeep to newChunkCount - 1 do + SetLength(newChunks[i], ChunkSize); - if sourcePhysicalIdx < FCount then + FChunks := newChunks; + + // Copy the new data item into the rebuilt chunks + var writeStartLogicalIdx := FOffset + FCount; + var physicalWriteStartIdx := writeStartLogicalIdx - (chunksToRemove * ChunkSize); + var chunkIdx := physicalWriteStartIdx div ChunkSize; + var idxInChunk := physicalWriteStartIdx mod ChunkSize; + + Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds during rebuild'); + FChunks[chunkIdx][idxInChunk] := Data; + end + else + begin + // --- Fast Path: Window slides within existing chunk allocation --- + var writeStartLogicalIdx := FOffset + FCount; + var chunkIdx := writeStartLogicalIdx div ChunkSize; + var idxInChunk := writeStartLogicalIdx mod ChunkSize; + + Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds on fast path'); + if Length(FChunks[chunkIdx]) = 0 then // Safeguard + SetLength(FChunks[chunkIdx], ChunkSize); + + FChunks[chunkIdx][idxInChunk] := Data; + end; + + // 4. Finalize the new state + FOffset := totalNewOffset mod ChunkSize; + FCount := newCount; +end; + +class function TChunkArray.CreateFromArray(const AData: TArray; MaxCount: Integer): TChunkArray; +begin + Result.Add(AData, MaxCount); +end; + +procedure TChunkArray.Add(const Data: array of T; MaxCount: Integer); +var + dataLen, dataReadOffset, numToAdd, newCount, totalNewOffset: Integer; + oldLogicalEnd, newLogicalEnd, oldStartChunk, newStartChunk, oldEndChunk, newEndChunk, chunksToRemove, chunksToAdd: Integer; +begin + if MaxCount < 0 then + MaxCount := MaxInt; + + dataLen := Length(Data); + if (dataLen = 0) or (MaxCount <= 0) then + exit; + + // 1. Trim input data if it's larger than MaxCount + dataReadOffset := 0; + if dataLen > MaxCount then + dataReadOffset := dataLen - MaxCount; + numToAdd := dataLen - dataReadOffset; + + // 2. Calculate the new logical state (total offset and count) + newCount := Min(FCount + numToAdd, MaxCount); + // Total logical offset of the new window's start + totalNewOffset := FOffset + FCount + numToAdd - newCount; + + // 3. Determine if the underlying FChunks array needs restructuring + oldLogicalEnd := FOffset + FCount - 1; + newLogicalEnd := totalNewOffset + newCount - 1; + + oldStartChunk := FOffset div ChunkSize; + newStartChunk := totalNewOffset div ChunkSize; + + oldEndChunk := -1; + if FCount > 0 then + oldEndChunk := oldLogicalEnd div ChunkSize; + + newEndChunk := -1; + if newCount > 0 then + newEndChunk := newLogicalEnd div ChunkSize; + + chunksToRemove := newStartChunk - oldStartChunk; + chunksToAdd := newEndChunk - oldEndChunk; + + // 4. Perform the add operation + if (chunksToRemove > 0) or (chunksToAdd > 0) then + begin + // --- Rebuild Path: Chunks must be added or removed --- + var newChunks: TArray; + var oldChunkCount := Length(FChunks); + var newChunkCount := oldChunkCount - chunksToRemove + chunksToAdd; + SetLength(newChunks, newChunkCount); + + // Copy references to the chunks that are kept + var numChunksToKeep := oldChunkCount - chunksToRemove; + if numChunksToKeep > 0 then + TArray.Copy(FChunks, newChunks, chunksToRemove, 0, numChunksToKeep); + + // Allocate new chunks at the end + for var i := numChunksToKeep to newChunkCount - 1 do + SetLength(newChunks[i], ChunkSize); + + FChunks := newChunks; + + // Copy the new data into the rebuilt chunks + var writeStartLogicalIdx := FOffset + FCount; + var physicalWriteStartIdx := writeStartLogicalIdx - (chunksToRemove * ChunkSize); + var remainingToAdd := numToAdd; + var currentDataOffset := dataReadOffset; + + while remainingToAdd > 0 do begin - newChunks[destChunkIdx][destSubIdx] := FChunks[sourcePhysicalIdx div ChunkSize][sourcePhysicalIdx mod ChunkSize]; - end - else + var chunkIdx := physicalWriteStartIdx div ChunkSize; + var idxInChunk := physicalWriteStartIdx mod ChunkSize; + var spaceInChunk := ChunkSize - idxInChunk; + var countToCopy := Min(remainingToAdd, spaceInChunk); + + Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds during rebuild'); + TArray.Copy(Data, FChunks[chunkIdx], currentDataOffset, idxInChunk, countToCopy); + + inc(physicalWriteStartIdx, countToCopy); + inc(currentDataOffset, countToCopy); + dec(remainingToAdd, countToCopy); + end; + end + else + begin + // --- Fast Path: Window slides within existing chunk allocation --- + var writeStartLogicalIdx := FOffset + FCount; + var remainingToAdd := numToAdd; + var currentDataOffset := dataReadOffset; + + while remainingToAdd > 0 do begin - newChunks[destChunkIdx][destSubIdx] := Data[First + (sourcePhysicalIdx - FCount)]; + var chunkIdx := writeStartLogicalIdx div ChunkSize; + var idxInChunk := writeStartLogicalIdx mod ChunkSize; + var spaceInChunk := ChunkSize - idxInChunk; + var countToCopy := Min(remainingToAdd, spaceInChunk); + + Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds on fast path'); + if Length(FChunks[chunkIdx]) = 0 then // Should not happen on fast path, but as a safeguard + SetLength(FChunks[chunkIdx], ChunkSize); + + TArray.Copy(Data, FChunks[chunkIdx], currentDataOffset, idxInChunk, countToCopy); + + inc(writeStartLogicalIdx, countToCopy); + inc(currentDataOffset, countToCopy); + dec(remainingToAdd, countToCopy); end; end; - Result := TSeries.Create(newChunks, newCount, FTotalCount + Count); + // 5. Finalize the new state + FOffset := totalNewOffset mod ChunkSize; + FCount := newCount; end; -function TSeries.Add(const Data: T; Lookback: Int64): TSeries; +function TChunkArray.GetItems(Idx: Integer): T; begin - Result := Add([Data], 0, 1, Lookback); + // Convert logical index to physical index inside chunks + var physicalIdx := Idx + FOffset; + Result := FChunks[physicalIdx div ChunkSize][physicalIdx mod ChunkSize]; end; -function TSeries.GetItems(Idx: Int64): T; +function TChunkArray.Copy(MaxCount: Integer): TChunkArray; var - physicalIndex: Int64; + effectiveCount: Integer; + logicalStartIndex: Integer; + physicalStartIndex, physicalEndIndex: Integer; + startChunkIdx, endChunkIdx: Integer; + newOffset: Integer; + newChunkCount: Integer; + i: Integer; begin - Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.'); - physicalIndex := LogicalToPhysicalIndex(Idx); - Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize]; + // 1. Determine the number of items to copy. + effectiveCount := FCount; + if (MaxCount >= 0) and (MaxCount < FCount) then + effectiveCount := MaxCount; + + if effectiveCount = 0 then + begin + // Return an empty array. + Result := Default(TChunkArray); + exit; + end; + + // 2. Calculate physical location of the data to be copied. + logicalStartIndex := FCount - effectiveCount; + physicalStartIndex := FOffset + logicalStartIndex; + physicalEndIndex := physicalStartIndex + effectiveCount - 1; + startChunkIdx := physicalStartIndex div ChunkSize; + endChunkIdx := physicalEndIndex div ChunkSize; + newOffset := physicalStartIndex mod ChunkSize; + + // 3. Create the new chunk array for the result. + newChunkCount := endChunkIdx - startChunkIdx + 1; + SetLength(Result.FChunks, newChunkCount); + + // 4. Copy chunks. Boundary chunks are deep-copied, internal chunks are shared. + if newChunkCount = 1 then + begin + // All data is in a single chunk, which is both a start and end boundary. + // It must always be a deep copy. + Result.FChunks[0] := System.Copy(FChunks[startChunkIdx]); + end + else + begin + // First chunk (start boundary) must be a deep copy. + Result.FChunks[0] := System.Copy(FChunks[startChunkIdx]); + + // Middle, immutable chunks can be shared by reference. + // This loop only runs if there are 3 or more chunks in the copy. + for i := 1 to newChunkCount - 2 do + begin + Result.FChunks[i] := FChunks[startChunkIdx + i]; + end; + + // Last chunk (end boundary) must be a deep copy. + Result.FChunks[newChunkCount - 1] := System.Copy(FChunks[endChunkIdx]); + end; + + // 5. Finalize the new TChunkArray state. + Result.FOffset := newOffset; + Result.FCount := effectiveCount; end; -function TSeries.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; +class operator TChunkArray.Initialize(out Dest: TChunkArray); begin - Result := FCount - LogicalIndex - 1; + Dest.FOffset := 0; + Dest.FCount := 0; +end; + +{ TSeries } + +procedure TSeries.Add(const Data: T; Lookback: Int64); +begin + FArray.Add(Data, Lookback); + inc(FTotalCount); +end; + +procedure TSeries.Add(const Data: array of T; Lookback: Integer); +begin + FArray.Add(Data, Lookback); + inc(FTotalCount, Length(Data)); +end; + +constructor TSeries.Create(const AArray: TChunkArray; ATotalCount: Int64); +begin + FArray := AArray; + FTotalCount := ATotalCount; +end; + +function TSeries.Copy(Lookback: Integer): TSeries; +begin + // Create a copy of the underlying chunk array, optionally truncating it. + Result.FArray := FArray.Copy(Lookback); + // The new series' total count is the number of items it actually contains. + Result.FTotalCount := Result.FArray.Count; +end; + +class function TSeries.CreateFromArray(const AData: TArray; Lookback: Integer): TSeries; +begin + Result.FArray.Add(AData, Lookback); + Result.FTotalCount := Length(AData); +end; + +function TSeries.GetCount: Integer; +begin + Result := FArray.Count; +end; + +function TSeries.GetItems(Idx: Integer): T; +begin + // Access is reversed: series index 0 is the last element in the underlying array + Assert((Idx >= 0) and (Idx < FArray.Count), 'Index out of bounds'); + Result := FArray.Items[FArray.Count - 1 - Idx]; end; class operator TSeries.Initialize(out Dest: TSeries); begin - Dest.FCount := 0; Dest.FTotalCount := 0; end; diff --git a/Src/Myc.Trade.DataPoint.Impl.pas b/Src/Myc.Trade.DataPoint.Impl.pas index b675a73..776ea49 100644 --- a/Src/Myc.Trade.DataPoint.Impl.pas +++ b/Src/Myc.Trade.DataPoint.Impl.pas @@ -554,7 +554,7 @@ begin end; Assert(FCount = 0); - Value := Value.Add(Arr, 0, Length(Arr), FLookback); + Value.Add(Arr, FLookback); end; end; finally diff --git a/Src/Myc.Trade.Indicators.pas b/Src/Myc.Trade.Indicators.pas index 99e0484..ff14828 100644 --- a/Src/Myc.Trade.Indicators.pas +++ b/Src/Myc.Trade.Indicators.pas @@ -216,7 +216,7 @@ begin var stdDev: Double; begin - sourceData := sourceData.Add(Value, Period); + sourceData.Add(Value, Period); Result.MiddleBand := Double.NaN; Result.UpperBand := Double.NaN; Result.LowerBand := Double.NaN; @@ -240,7 +240,7 @@ begin Result := function(const Value: Double): Double begin - sourceData := sourceData.Add(Value, Period); + sourceData.Add(Value, Period); if (sourceData.Count < Period) then begin @@ -281,7 +281,7 @@ begin Result := Double.NaN; // Add new price to the source data array, respecting the lookback period. - sourceData := sourceData.Add(price, Period); + sourceData.Add(price, Period); // Check if there is enough data to start the first stage of calculation. if (sourceData.Count >= Period) then @@ -292,7 +292,7 @@ begin // Calculate the difference and add to the intermediate series. diff := 2 * wmaHalf - wmaFull; - diffSeries := diffSeries.Add(diff, periodSqrt); + diffSeries.Add(diff, periodSqrt); // Check if there is enough intermediate data for the final calculation. if (diffSeries.Count >= periodSqrt) then @@ -352,7 +352,7 @@ begin gainSum, lossSum: Double; i: Integer; begin - sourceData := sourceData.Add(Value, Period + 1); + sourceData.Add(Value, Period + 1); Result := Double.NaN; if (sourceData.Count <= Period) then @@ -404,7 +404,7 @@ begin Result := function(const Value: Double): Double begin - sourceData := sourceData.Add(Value, Period); + sourceData.Add(Value, Period); if (sourceData.Count >= Period) then Result := CalculateSMA(sourceData, Period) else @@ -432,7 +432,7 @@ begin i: Integer; highestHigh, lowestLow: Double; begin - sourceData := sourceData.Add(Value, KPeriod); + sourceData.Add(Value, KPeriod); Result.K := Double.NaN; Result.D := Double.NaN; @@ -477,7 +477,7 @@ begin tr: Double; begin // We only need the previous bar to calculate true range. - sourceData := sourceData.Add(Value, 2); + sourceData.Add(Value, 2); if (sourceData.Count < 2) then begin diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index 81ac9d0..6c9b172 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -30,7 +30,8 @@ uses Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas', Myc.Mutable in '..\Src\Myc.Mutable.pas', Test.Core.Mutable in 'Test.Core.Mutable.pas', - TestDataRecord in 'TestDataRecord.pas'; + TestDataRecord in 'TestDataRecord.pas', + TestDataArray in 'TestDataArray.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT} diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index b6f3939..a39e1fe 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -132,6 +132,7 @@ $(PreBuildEvent)]]> + Base diff --git a/Test/TestDataArray.pas b/Test/TestDataArray.pas new file mode 100644 index 0000000..ab85bed --- /dev/null +++ b/Test/TestDataArray.pas @@ -0,0 +1,210 @@ +unit TestDataArray; + +interface + +uses + System.SysUtils, + DUnitX.TestFramework, + Myc.Trade.DataArray; + +type + [TestFixture] + TMyTestObject = class + private + function GenerateData(Count: Integer): TArray; + public + [Test] + [TestCase('Small_NoSlide', '100, -1')] + [TestCase('Small_WithSlide', '100, 50')] + [TestCase('Medium_NoSlide_CrossChunk', '1500, -1')] + [TestCase('Medium_WithSlide_CrossChunk', '2500, 1500')] + [TestCase('Large_ExactSlide', '5000, 1024')] + procedure TestChunkArray_CreateFromArray(const DataSize, MaxCount: Integer); + + [Test] + procedure TestChunkArray_Add_And_Slide; + + [Test] + procedure TestChunkArray_Copy_IsIndependent; + + [Test] + procedure TestSeries_CreationAndIndexing; + + [Test] + procedure TestSeries_Add_And_Copy; + end; + +implementation + +uses + System.Generics.Collections; + +{ TMyTestObject } + +function TMyTestObject.GenerateData(Count: Integer): TArray; +var + i: Integer; +begin + SetLength(Result, Count); + for i := 0 to Count - 1 do + Result[i] := i; +end; + +procedure TMyTestObject.TestChunkArray_Add_And_Slide; +var + arr: TChunkArray; + newData: TArray; +begin + // Test sliding with single additions + arr := TChunkArray.CreateFromArray([], 5); // MaxCount = 5 + + arr.Add(10, 5); // arr = [10] + Assert.AreEqual(1, arr.Count); + Assert.AreEqual(10, arr[0]); + + arr.Add(20, 5); // arr = [10, 20] + arr.Add(30, 5); // arr = [10, 20, 30] + arr.Add(40, 5); // arr = [10, 20, 30, 40] + arr.Add(50, 5); // arr = [10, 20, 30, 40, 50] + Assert.AreEqual(5, arr.Count); + Assert.AreEqual(10, arr[0]); + Assert.AreEqual(50, arr[4]); + + // Now, slide the window + arr.Add(60, 5); // arr becomes [20, 30, 40, 50, 60] + Assert.AreEqual(5, arr.Count, 'Count should remain at MaxCount'); + Assert.AreEqual(20, arr[0], 'First item should be slided out'); + Assert.AreEqual(60, arr[4], 'Last item should be the new one'); + + // Test sliding with array addition + arr := TChunkArray.CreateFromArray([1, 2, 3], 5); // arr = [1, 2, 3], MaxCount = 5 + newData := [4, 5, 6, 7]; + arr.Add(newData, 5); // arr becomes [3, 4, 5, 6, 7] + Assert.AreEqual(5, arr.Count, 'Count should be MaxCount after adding array'); + Assert.AreEqual(3, arr[0]); + Assert.AreEqual(7, arr[4]); +end; + +procedure TMyTestObject.TestChunkArray_Copy_IsIndependent; +const + DATA_SIZE = 3000; // Approx 3 chunks + MAX_COUNT_COPY = 1500; // Partial copy +var + original, fullCopy, partialCopy: TChunkArray; + data: TArray; + i: Integer; +begin + data := GenerateData(DATA_SIZE); + original := TChunkArray.CreateFromArray(data, -1); + + // 1. Test full copy + fullCopy := original.Copy(-1); + Assert.AreEqual(original.Count, fullCopy.Count, 'Full copy count should match original'); + for i := 0 to original.Count - 1 do + Assert.AreEqual(original[i], fullCopy[i], 'Full copy item should match original'); + + // 2. Test partial copy + partialCopy := original.Copy(MAX_COUNT_COPY); + Assert.AreEqual(MAX_COUNT_COPY, partialCopy.Count, 'Partial copy should have MaxCount items'); + for i := 0 to partialCopy.Count - 1 do + // The partial copy contains the LAST items of the original + Assert.AreEqual(original[i + (DATA_SIZE - MAX_COUNT_COPY)], partialCopy[i], 'Partial copy item should match last part of original'); + + // 3. Test independence after modification + // Add an item to original. This modifies its last chunk. + original.Add(9999, -1); + Assert.AreEqual(DATA_SIZE + 1, original.Count, 'Original count should increment'); + + // Verify the full copy is unchanged. + Assert.AreEqual(DATA_SIZE, fullCopy.Count, 'Full copy count should NOT change'); + Assert.AreNotEqual(9999, fullCopy[fullCopy.Count - 1], 'Last item of full copy should NOT be the new item'); + Assert.AreEqual(data[DATA_SIZE - 1], fullCopy[fullCopy.Count - 1], 'Last item of full copy should be original last item'); + + // Verify the partial copy is unchanged. + Assert.AreEqual(MAX_COUNT_COPY, partialCopy.Count, 'Partial copy count should NOT change'); +end; + +procedure TMyTestObject.TestChunkArray_CreateFromArray(const DataSize, MaxCount: Integer); +var + data, expectedData: TArray; + arr: TChunkArray; + effectiveCount: Integer; + i: Integer; +begin + data := GenerateData(DataSize); + arr := TChunkArray.CreateFromArray(data, MaxCount); + + if (MaxCount < 0) or (MaxCount >= DataSize) then + effectiveCount := DataSize + else + effectiveCount := MaxCount; + + Assert.AreEqual(effectiveCount, arr.Count, 'Count should match effective count'); + + if effectiveCount = 0 then + exit; + + // We only expect the last 'effectiveCount' items from the original data + SetLength(expectedData, effectiveCount); + TArray.Copy(data, expectedData, DataSize - effectiveCount, 0, effectiveCount); + + for i := 0 to effectiveCount - 1 do + Assert.AreEqual(expectedData[i], arr.Items[i], 'Item at index should be correct'); +end; + +procedure TMyTestObject.TestSeries_Add_And_Copy; +var + original, copy: TSeries; +begin + // 1. Create and add + original := TSeries.CreateFromArray([1, 2, 3], 5); // Lookback = 5 + Assert.AreEqual(Int64(3), original.TotalCount); + + original.Add(4, 5); // Series: [4, 3, 2, 1] + original.Add(5, 5); // Series: [5, 4, 3, 2, 1] + Assert.AreEqual(5, original.Count, 'Count should be 5'); + Assert.AreEqual(Int64(5), original.TotalCount, 'TotalCount should be 5'); + Assert.AreEqual(5, original[0], 'Index 0 should be 5'); + + // 2. Test sliding + original.Add(6, 5); // Series: [6, 5, 4, 3, 2] + Assert.AreEqual(5, original.Count, 'Count should remain 5 after slide'); + Assert.AreEqual(Int64(6), original.TotalCount, 'TotalCount should be 6'); + Assert.AreEqual(6, original[0], 'Index 0 should be 6'); + Assert.AreEqual(2, original[4], 'Last item should be 2'); + + // 3. Test copy + copy := original.Copy(); + Assert.AreEqual(5, copy.Count, 'Copy count should match original count'); + Assert.AreEqual(Int64(5), copy.TotalCount, 'Copy TotalCount should match its own count, not original TotalCount'); + Assert.AreEqual(6, copy[0], 'Copy should have same data'); + + // 4. Test independence + original.Add(7, 5); + Assert.AreEqual(7, original[0], 'Original should be updated'); + Assert.AreEqual(6, copy[0], 'Copy should NOT be updated'); +end; + +procedure TMyTestObject.TestSeries_CreationAndIndexing; +var + series: TSeries; + data: TArray; +begin + data := [10, 20, 30, 40, 50]; + series := TSeries.CreateFromArray(data, -1); + + Assert.AreEqual(5, series.Count, 'Series count should be 5'); + Assert.AreEqual(Int64(5), series.TotalCount, 'Series total count should be 5'); + + // Test reversed indexing + Assert.AreEqual(50, series[0], 'Index 0 should be the last element'); + Assert.AreEqual(40, series[1], 'Index 1 should be the second to last element'); + Assert.AreEqual(30, series[2]); + Assert.AreEqual(20, series[3]); + Assert.AreEqual(10, series[4], 'Last index should be the first element'); +end; + +initialization + TDUnitX.RegisterTestFixture(TMyTestObject); + +end.