unit Myc.Trade.Core.DataPoint; interface uses System.Generics.Collections, System.TimeSpan, Myc.Trade.DataPoint; type TMycDataArray = record private const ChunkSize = 1024; type TChunk = TArray>; private FChunks: TArray; FCount: Int64; function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline; function GetItems(Idx: Int64): TDataPoint; inline; public constructor Create(const AChunks: TArray; ACount: Int64); function Add(const Data: array of TDataPoint; First, Count, Lookback: Int64): TMycDataArray; class function CreateEmpty: TMycDataArray; static; // Helper to create a data array from a raw TArray. class function CreateFromArray(const AData: TArray>; First, Count: Integer): TMycDataArray; static; property Count: Int64 read FCount; property Items[Idx: Int64]: TDataPoint read GetItems; default; end; // The implementation class for IDataSeries. TMycDataSeries = class(TInterfacedObject, IDataSeries) private FData: TMycDataArray; FLookback: Int64; FTotalCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; public constructor Create(ALookback: Int64; const AData: TMycDataArray; ATotalCount: Int64); destructor Destroy; override; function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class function CreateDataSeries(Lookback: Int64; const AData: TMycDataArray; ATotalCount: Int64): IDataSeries; static; end; // Null object implementation for IDataSeries TNullDataSeries = class(TInterfacedObject, IDataSeries) strict private class var FNull: IDataSeries; private FTotalCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetTotalCount: Int64; function GetLookback: Int64; class constructor CreateClass; public constructor Create(ATotalCount: Int64); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class property Null: IDataSeries read FNull; end; // A virtual series that combines a base series and an array of new data without copying. TCompositeDataSeries = class(TInterfacedObject, IDataSeries) private FBaseSeries: IDataSeries; FAddedData: TMycDataArray; FLookback: Int64; FCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; public constructor Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; class function CreateComposite( const BaseSeries: IDataSeries; const Data: TArray>; First, Count: Integer ): IDataSeries; static; end; TConvertSeries = class(TInterfacedObject, IDataSeries) private FSource: IDataSeries; FConvertFunc: TDataSeries.TConvertFunc; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; public constructor Create(const ASource: IDataSeries; const AConvertFunc: TDataSeries.TConvertFunc); function Add(const Data: TArray>; First, Count: Integer): IDataSeries; end; TAggregateDataSeries = class(TInterfacedObject, IDataSeries) public // Defines the function signature for aggregating a set of source data points into a single target value. type TAggregateFunc = reference to function(const ASourcePoints: TArray>): S; private FSource: IDataSeries; FTimeFrame: TDateTime; FAggregateFunc: TAggregateFunc; FCachedItems: TDictionary>; FBaseTime: TDateTime; FAggregatedCount: Int64; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; function GetLookback: Int64; function GetTotalCount: Int64; procedure CalculateAggregatedCount; public constructor Create(const ASource: IDataSeries; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc); destructor Destroy; override; function Add(const Data: TArray>; First, Count: Integer): IDataSeries; end; implementation uses System.SysUtils, System.Math; { TMycDataArray } constructor TMycDataArray.Create(const AChunks: TArray; ACount: Int64); begin FChunks := AChunks; FCount := ACount; end; class function TMycDataArray.CreateEmpty: TMycDataArray; begin Result.FChunks := nil; Result.FCount := 0; end; class function TMycDataArray.CreateFromArray(const AData: TArray>; First, Count: Integer): TMycDataArray; begin // Use the Add method on an empty array to perform the chunking logic. Result := CreateEmpty.Add(AData, First, Count, Count); end; function TMycDataArray.Add(const Data: array of TDataPoint; First, Count, Lookback: Int64): TMycDataArray; var destPhysicalIdx, sourcePhysicalIdx: Int64; itemsToSkip: Int64; numNewChunks: Integer; newChunks: TArray; destChunkIdx, destSubIdx: Integer; sumCount, newCount: Int64; begin if Count < 0 then Count := Length(Data) - First; if (Lookback <= 0) or (Count = 0) then exit(Self); Assert(Count <= (Length(Data) - First), 'Count cannot be larger than the source array'); for var i := First + 1 to First + Count - 1 do Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted'); if FCount > 0 then Assert(Data[First].Time >= Self.Items[0].Time, 'First new item is older than last existing item'); sumCount := FCount + Count; newCount := sumCount; if (Lookback > 0) and (newCount > Lookback) then newCount := Lookback; itemsToSkip := sumCount - newCount; numNewChunks := 0; if newCount > 0 then numNewChunks := (newCount - 1) div ChunkSize + 1; SetLength(newChunks, numNewChunks); for destPhysicalIdx := 0 to newCount - 1 do begin destChunkIdx := destPhysicalIdx div ChunkSize; destSubIdx := destPhysicalIdx mod ChunkSize; if destSubIdx = 0 then SetLength(newChunks[destChunkIdx], ChunkSize); sourcePhysicalIdx := itemsToSkip + destPhysicalIdx; if sourcePhysicalIdx < FCount then begin newChunks[destChunkIdx][destSubIdx] := FChunks[sourcePhysicalIdx div ChunkSize][sourcePhysicalIdx mod ChunkSize]; end else begin newChunks[destChunkIdx][destSubIdx] := Data[First + (sourcePhysicalIdx - FCount)]; end; end; Result := TMycDataArray.Create(newChunks, newCount); end; function TMycDataArray.GetItems(Idx: Int64): TDataPoint; var physicalIndex: Int64; begin Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.'); physicalIndex := LogicalToPhysicalIndex(Idx); Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize]; end; function TMycDataArray.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; begin Result := FCount - LogicalIndex - 1; end; { TMycDataSeries } constructor TMycDataSeries.Create(ALookback: Int64; const AData: TMycDataArray; ATotalCount: Int64); begin inherited Create; FLookback := ALookback; FData := AData; FTotalCount := ATotalCount; end; destructor TMycDataSeries.Destroy; begin inherited; end; class function TMycDataSeries.CreateDataSeries(Lookback: Int64; const AData: TMycDataArray; ATotalCount: Int64): IDataSeries; begin if Lookback > 0 then Result := TMycDataSeries.Create(Lookback, AData, ATotalCount) else Result := TNullDataSeries.Null; end; function TMycDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; var newData: TMycDataArray; newTotalCount: Int64; begin if Count < 0 then Count := Length(Data) - First; newData := FData.Add(Data, First, Count, FLookback); newTotalCount := FTotalCount + Count; Result := TMycDataSeries.Create(FLookback, newData, newTotalCount); end; function TMycDataSeries.GetCount: Int64; begin Result := FData.Count; end; function TMycDataSeries.GetItems(Idx: Int64): TDataPoint; begin Assert((Idx >= 0) and (Idx < FData.Count), 'Index is out of bounds.'); Result := FData.Items[Idx]; end; function TMycDataSeries.GetLookback: Int64; begin Result := FLookback; end; function TMycDataSeries.GetTotalCount: Int64; begin Result := FTotalCount; end; { TNullDataSeries } class constructor TNullDataSeries.CreateClass; begin FNull := TNullDataSeries.Create(0); end; constructor TNullDataSeries.Create(ATotalCount: Int64); begin inherited Create; FTotalCount := ATotalCount; end; function TNullDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; begin if Count < 0 then Count := Length(Data) - First; if Count > 0 then Result := TNullDataSeries.Create(FTotalCount + Count) else Result := Self; end; function TNullDataSeries.GetCount: Int64; begin Result := 0; end; function TNullDataSeries.GetItems(Idx: Int64): TDataPoint; begin Assert(false, 'Data series is empty.'); Result := Default(TDataPoint); end; function TNullDataSeries.GetLookback: Int64; begin Result := 0; end; function TNullDataSeries.GetTotalCount: Int64; begin Result := FTotalCount; end; { TCompositeDataSeries } constructor TCompositeDataSeries.Create(const ABaseSeries: IDataSeries; const AAddedData: TMycDataArray); begin inherited Create; FBaseSeries := ABaseSeries; FAddedData := AAddedData; FLookback := FBaseSeries.Lookback; Assert(FLookback > 0); FCount := FBaseSeries.Count + FAddedData.Count; if FCount > FLookback then FCount := FLookback; end; function TCompositeDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; var newAddedData: TMycDataArray; itemsInBase: Int64; lookbackForAdd: Int64; begin if Count < 0 then Count := Length(Data) - First; if Count = 0 then exit(Self); itemsInBase := FBaseSeries.Count; lookbackForAdd := FLookback - itemsInBase; if lookbackForAdd < 0 then lookbackForAdd := 0; newAddedData := FAddedData.Add(Data, First, Count, lookbackForAdd); // Optimization: If the added data fills the entire lookback window, // the base series is no longer relevant. We can return a simpler TMycDataSeries. if newAddedData.Count >= FLookback then begin Result := TMycDataSeries.Create(FLookback, newAddedData, GetTotalCount + Count); end else begin Result := TCompositeDataSeries.Create(FBaseSeries, newAddedData); end; end; class function TCompositeDataSeries.CreateComposite( const BaseSeries: IDataSeries; const Data: TArray>; First, Count: Integer ): IDataSeries; begin if Count < 0 then Count := Length(Data) - First; if Count = 0 then exit(BaseSeries); Result := TCompositeDataSeries.Create(BaseSeries, TMycDataArray.CreateFromArray(Data, First, Count)); end; function TCompositeDataSeries.GetCount: Int64; begin Result := FCount; end; function TCompositeDataSeries.GetItems(Idx: Int64): TDataPoint; var addedCount: Int64; begin Assert((Idx >= 0) and (Idx < FCount), 'Logical index is out of bounds.'); addedCount := FAddedData.Count; if Idx < addedCount then begin Result := FAddedData.Items[Idx]; end else begin Result := FBaseSeries.Items[Idx - addedCount]; end; end; function TCompositeDataSeries.GetLookback: Int64; begin Result := FLookback; end; function TCompositeDataSeries.GetTotalCount: Int64; begin Result := FBaseSeries.TotalCount + FAddedData.Count; end; { TConvertSeries } constructor TConvertSeries.Create(const ASource: IDataSeries; const AConvertFunc: TDataSeries.TConvertFunc); begin inherited Create; FSource := ASource; FConvertFunc := AConvertFunc; end; function TConvertSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; begin Result := TCompositeDataSeries.CreateComposite(Self, Data, First, Count); end; function TConvertSeries.GetCount: Int64; begin Result := FSource.Count; end; function TConvertSeries.GetItems(Idx: Int64): TDataPoint; var P: TDataPoint; begin P := FSource[Idx]; Result.Create(P.Time, FConvertFunc(P)); end; function TConvertSeries.GetLookback: Int64; begin Result := FSource.Lookback; end; function TConvertSeries.GetTotalCount: Int64; begin Result := FSource.TotalCount; end; { TAggregateDataSeries } constructor TAggregateDataSeries.Create(const ASource: IDataSeries; ATimeFrame: TDateTime; const AAggregateFunc: TAggregateFunc); begin inherited Create; FSource := ASource; FTimeFrame := ATimeFrame; FAggregateFunc := AAggregateFunc; FCachedItems := TDictionary>.Create; FAggregatedCount := -1; // -1 indicates that it has not been calculated yet end; destructor TAggregateDataSeries.Destroy; begin FCachedItems.Free; inherited; end; procedure TAggregateDataSeries.CalculateAggregatedCount; var totalTimeSpan: Double; begin if FSource.Count = 0 then begin FBaseTime := 0; FAggregatedCount := 0; end else begin // The timestamp of the oldest element serves as the anchor for our time grid. FBaseTime := FSource.Items[FSource.Count - 1].Time; // Total duration covered by the source series. totalTimeSpan := FSource.Items[0].Time - FBaseTime; if totalTimeSpan >= 0 then FAggregatedCount := Trunc(totalTimeSpan / FTimeFrame) + 1 else FAggregatedCount := 0; end; end; function TAggregateDataSeries.Add(const Data: TArray>; First, Count: Integer): IDataSeries; begin // Adding to an aggregated series is complex. // The most straightforward approach is to create a composite series. Result := TCompositeDataSeries.CreateComposite(Self, Data, First, Count); end; function TAggregateDataSeries.GetCount: Int64; begin if FAggregatedCount = -1 then CalculateAggregatedCount; Result := FAggregatedCount; end; function TAggregateDataSeries.GetItems(Idx: Int64): TDataPoint; var startTime, endTime: TDateTime; sourcePoints: TList>; sourceIdx: Int64; begin Assert((Idx >= 0) and (Idx < GetCount), 'Index is out of bounds.'); if FCachedItems.TryGetValue(Idx, Result) then exit; // Calculate the time window for the requested aggregated data point. // Index 0 is the newest, so we calculate backwards from the total count. endTime := FBaseTime + (FAggregatedCount - Idx) * FTimeFrame; startTime := endTime - FTimeFrame; sourcePoints := TList>.Create; try // Find all source data points that fall into this time window. // We can optimize the start of the search using the IndexOf function. sourceIdx := TDataSeries(FSource).IndexOf(endTime); if sourceIdx = -1 then sourceIdx := 0; // If endTime is after the last element, start at the newest. while (sourceIdx < FSource.Count) do begin var P := FSource.Items[sourceIdx]; if P.Time < startTime then break; // We have moved past our time window if P.Time < endTime then // Time is within [startTime, endTime) begin sourcePoints.Add(P); end; Inc(sourceIdx); end; // The aggregation function expects the data in chronological order (oldest first), // but we collected it in reverse. So we reverse the list. sourcePoints.Reverse; Result.Create(endTime, FAggregateFunc(sourcePoints.ToArray)); finally sourcePoints.Free; end; // Cache the result for future calls FCachedItems.Add(Idx, Result); end; function TAggregateDataSeries.GetLookback: Int64; begin Result := FSource.Lookback; // The lookback is defined by the source series. end; function TAggregateDataSeries.GetTotalCount: Int64; begin // The total count of aggregated items is simply its current count, as it's a view. Result := GetCount; end; end.