unit Myc.Trade.DataPoint; interface uses System.SysUtils; type // A data record for an Ask/Bid price pair. TAskBidItem = packed record Ask: Single; Bid: Single; constructor Create(AAsk, ABid: Single); end; // Represents a time-stamped data point in a series. TDataPoint = record Time: TDateTime; Data: T; constructor Create(ATime: TDateTime; const AData: T); end; // A time-ordered series of data points, optimized for chronological additions. // The most recently added element has the logical index 0. TDataSeries = record private const ChunkSize = 1024; type TChunk = TArray>; private FChunks: TArray; FCount: Int64; FMaxLookback: Int64; FTotalCount: Int64; // Converts a logical index (0=newest) to a physical storage index (0=oldest). function LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; inline; function GetCount: Int64; function GetData(Idx: Int64): T; function GetItems(Idx: Int64): TDataPoint; function GetTime(Idx: Int64): TDateTime; procedure SetData(Idx: Int64; const Value: T); // Checks if a logical index is within the defined limits. function IsIndexInLimits(LogicalIndex: Int64): Boolean; // Removes old chunks that are outside the Lookback limits. procedure Trim; public constructor Create(AMaxLookback: Int64); // Adds a new data point to the series. procedure Add(const Data: TDataPoint); overload; procedure Add(const Data: array of TDataPoint; NumToAdd: Integer = -1); overload; // Clears all data from the series. procedure Clear; // 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 operator Initialize(out Dest: TDataSeries); class operator Finalize(var Dest: TDataSeries); property Count: Int64 read GetCount; // Accesses data points by their logical index. // Index 0 is the newest element, Index (Count - 1) is the oldest. property Items[Idx: Int64]: TDataPoint read GetItems; default; property Time[Idx: Int64]: TDateTime read GetTime; property Data[Idx: Int64]: T read GetData write SetData; // The maximum number of data points to keep (0 = unlimited). property MaxLookback: Int64 read FMaxLookback; // The total number of items ever added to the series (resets on Clear). property TotalCount: Int64 read FTotalCount; end; implementation { TAskBidItem } constructor TAskBidItem.Create(AAsk, ABid: Single); begin Ask := AAsk; Bid := ABid; end; { TDataPoint } constructor TDataPoint.Create(ATime: TDateTime; const AData: T); begin Time := ATime; Data := AData; end; constructor TDataSeries.Create(AMaxLookback: Int64); begin FMaxLookback := AMaxLookback; FCount := 0; FTotalCount := 0; FChunks := nil; end; { TDataSeries } procedure TDataSeries.Add(const Data: TDataPoint); begin // Enforce chronological order for new items. Assert((FCount = 0) or (Data.Time >= GetTime(0)), 'Time stamp older than last item'); var ci := FCount div ChunkSize; var di := FCount mod ChunkSize; if (di = 0) then begin SetLength(FChunks, ci + 1); SetLength(FChunks[ci], ChunkSize); end; FChunks[ci][di] := Data; Inc(FCount); Inc(FTotalCount); // Remove old data that is now out of limits. Trim; end; procedure TDataSeries.Add(const Data: array of TDataPoint; NumToAdd: Integer = -1); var sourceIdx, itemsToCopy, spaceInChunk: Integer; ci, di: Int64; i: Integer; resolvedNumToAdd: Integer; begin if NumToAdd < 0 then resolvedNumToAdd := Length(Data) else resolvedNumToAdd := NumToAdd; if resolvedNumToAdd = 0 then Exit; Assert(resolvedNumToAdd <= Length(Data), 'NumToAdd cannot be larger than the source array'); // Check chronological order of the input array itself for i := 1 to resolvedNumToAdd - 1 do Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted'); // Enforce chronological order for new items against existing series. Assert((FCount = 0) or (Data[0].Time >= GetTime(0)), 'First new item is older than last existing item'); // Increment total count immediately. Inc(FTotalCount, resolvedNumToAdd); var newTotalCount := FCount + resolvedNumToAdd; var requiredChunks := (newTotalCount + ChunkSize - 1) div ChunkSize; if requiredChunks = 0 then // case for newTotalCount = 0 requiredChunks := 1; // Pre-allocate chunk array structure if requiredChunks > Length(FChunks) then SetLength(FChunks, requiredChunks); sourceIdx := 0; while sourceIdx < resolvedNumToAdd do begin ci := FCount div ChunkSize; di := FCount mod ChunkSize; // Allocate the actual chunk if we are at its beginning if di = 0 then SetLength(FChunks[ci], ChunkSize); spaceInChunk := ChunkSize - di; itemsToCopy := resolvedNumToAdd - sourceIdx; if spaceInChunk < itemsToCopy then itemsToCopy := spaceInChunk; // Move a block of data into the available space in the current chunk. System.Move(Data[sourceIdx], FChunks[ci][di], itemsToCopy * SizeOf(TDataPoint)); Inc(FCount, itemsToCopy); Inc(sourceIdx, itemsToCopy); // Remove old data that is now out of limits Trim; end; end; procedure TDataSeries.Clear; begin FChunks := nil; FCount := 0; FTotalCount := 0; end; class operator TDataSeries.Finalize(var Dest: TDataSeries); begin Dest.Clear; end; function TDataSeries.GetCount: Int64; begin // The public Count must always reflect the addressable range. Result := FCount; if (FMaxLookback > 0) and (Result > FMaxLookback) then Result := FMaxLookback; end; function TDataSeries.GetData(Idx: Int64): T; var physicalIndex: Int64; begin Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.'); physicalIndex := LogicalToPhysicalIndex(Idx); Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data; end; function TDataSeries.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 ChunkSize][physicalIndex mod ChunkSize]; end; function TDataSeries.GetTime(Idx: Int64): TDateTime; var physicalIndex: Int64; begin Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.'); physicalIndex := LogicalToPhysicalIndex(Idx); Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Time; end; class operator TDataSeries.Initialize(out Dest: TDataSeries); begin Dest.FCount := 0; Dest.FTotalCount := 0; Dest.FChunks := nil; Dest.FMaxLookback := 0; end; function TDataSeries.IndexOf(TimeStamp: TDateTime): Int64; var low, high, mid: Int64; dataPointTime: TDateTime; begin Result := -1; if Count = 0 then // Use public Count to respect limits Exit; low := 0; high := Count - 1; // Use public Count to respect limits while (low <= high) do begin mid := low + (high - low) div 2; dataPointTime := GetTime(mid); if (dataPointTime = TimeStamp) then begin Result := mid; break; end else if (dataPointTime < TimeStamp) then begin Result := mid; high := mid - 1; end else // dataPointTime > TimeStamp begin low := mid + 1; end; end; end; function TDataSeries.IsIndexInLimits(LogicalIndex: Int64): Boolean; begin // Check Lookback limit Result := (FMaxLookback <= 0) or (LogicalIndex < FMaxLookback); end; function TDataSeries.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; begin Assert((LogicalIndex >= 0) and (LogicalIndex < Count), 'Logical index is out of bounds.'); Result := FCount - LogicalIndex - 1; end; procedure TDataSeries.SetData(Idx: Int64; const Value: T); var physicalIndex: Int64; begin Assert(IsIndexInLimits(Idx), 'Index is outside of the configured Lookback limits.'); physicalIndex := LogicalToPhysicalIndex(Idx); FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data := Value; end; procedure TDataSeries.Trim; var itemsToRemove, chunksToRemove: Int64; begin if (FCount = 0) or (FMaxLookback <= 0) then Exit; itemsToRemove := 0; if FCount > FMaxLookback then begin itemsToRemove := FCount - FMaxLookback; end; if itemsToRemove <= 0 then Exit; // We only free full chunks to avoid expensive partial copy operations. chunksToRemove := itemsToRemove div ChunkSize; if chunksToRemove > 0 then begin var itemsInFreedChunks := chunksToRemove * ChunkSize; FChunks := Copy(FChunks, chunksToRemove, Length(FChunks) - chunksToRemove); FCount := FCount - itemsInFreedChunks; end; end; end.