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; function GetCount: Int64; function GetItems(Idx: Int64): TDataPoint; public // Adds a new data point to the series. procedure Add(const Data: TDataPoint); // 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; 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; { TDataSeries } procedure TDataSeries.Add(const Data: TDataPoint); begin // Enforce chronological order: new items cannot be older than the newest existing item. // This is a prerequisite for the binary search in IndexOf to work correctly. Assert((Length(FChunks) = 0) or (Data.Time >= GetItems(0).Time), 'Time stamp older than last item'); var ci := FCount div ChunkSize; var di := FCount mod ChunkSize; if di = 0 then begin Assert(ci = Length(FChunks)); Assert(di = 0); SetLength(FChunks, ci + 1); SetLength(FChunks[ci], ChunkSize); end; FChunks[ci][di] := Data; Inc(FCount); end; class operator TDataSeries.Finalize(var Dest: TDataSeries); begin Dest.FChunks := nil; Dest.FCount := 0; end; function TDataSeries.GetCount: Int64; begin Result := FCount; end; function TDataSeries.GetItems(Idx: Int64): TDataPoint; begin Assert((Idx >= 0) and (Idx < FCount)); // Convert logical index (0 = newest) to physical index (0 = oldest). Idx := FCount - Idx - 1; Result := FChunks[Idx div ChunkSize][Idx mod ChunkSize]; end; class operator TDataSeries.Initialize(out Dest: TDataSeries); begin Dest.FCount := 0; Dest.FChunks := nil; end; function TDataSeries.IndexOf(TimeStamp: TDateTime): Int64; var low, high, mid: Int64; dataPointTime: TDateTime; begin Result := -1; if FCount = 0 then Exit; low := 0; high := FCount - 1; while (low <= high) do begin mid := low + (high - low) div 2; dataPointTime := GetItems(mid).Time; if (dataPointTime = TimeStamp) then begin Result := mid; Exit; end else if (dataPointTime < TimeStamp) then begin Result := mid; high := mid - 1; end else // dataPointTime > TimeStamp begin low := mid + 1; end; end; end; end.