Files
MycLib/Src/Myc.Trade.DataPoint.pas
T
Michael Schimmel 4d67e587ba # Projektplan: Meilenstein Daten-Infrastruktur
*Datum: 13. Juni 2025*

## Status: Design-Phase abgeschlossen, Basis-Implementierung erfolgt

---

## Stufe 1: Asynchrone Datenstrom-Schnittstelle (`IDataStream<T>`) - **ABGESCHLOSSEN**

* **Motivation**: Das ursprüngliche, zustandsbehaftete Design war für die Verarbeitung asynchron eintreffender Daten (z.B. von `TFuture`-Objekten) zu komplex und fehleranfällig.

* **Ziel**: Die Schaffung eines robusten, ereignisgesteuerten und non-blocking Modells, das den Datenproduzenten sauber vom Konsumenten entkoppelt.

* **Ergebnis**:
    * Die `IDataStream<T>`-Schnittstelle wurde überarbeitet. Die `HasData`-Eigenschaft liefert nun ein `TSignal`, das Konsumenten aktiv über potenziell neue Daten informiert.
    * Die Referenzimplementierung `TAuraFileStream<T>` wurde erfolgreich angepasst und nutzt eine saubere Ereignis-Kopplung (`Subscribe`) für eine robuste und wartungsarme Logik.
    * Die korrekte Funktionalität wurde durch eine angepasste DUnitX-Test-Suite verifiziert.

---

## Stufe 2: Abstraktion für Handelssysteme (`IDataSeriesProvider`) - **ENTWORFEN**

* **Motivation**: Ein Handelssystem benötigt einen stets validen und kontinuierlichen Daten-Lookback. Ein roher `IDataStream<T>` kann dies nicht garantieren, da Lücken in den Daten auftreten können (z.B. beim Übergang von Historie zu Live).

* **Ziel**: Die Konzeption einer übergeordneten Abstraktionsschicht, die diese komplexe Anforderung kapselt, die Datenintegrität sicherstellt und dem Handelssystem eine einfache, sichere Schnittstelle bietet.

* **Ergebnis**:
    Entworfen wurde der `IDataSeriesProvider`, der als "Black Box" für das Handelssystem fungiert und die Komplexität der Datenbeschaffung vollständig verbirgt. Er wurde mit zwei unterschiedlichen, vom Anwender wählbaren Betriebsmodi konzipiert:

    ### Modus 1: "Live-Handel"
    * **Motivation**: Um einen echten **"Sofort-Start"** im Live-Handel zu ermöglichen, muss die Lücke zwischen den statischen, lokalen Historiendaten und dem aktuellen Zeitpunkt geschlossen werden.
    * **Ziel**: Ein lückenloser, tagesaktueller Start des Handelssystems ohne manuelles Eingreifen oder lange Wartezeiten für den Nutzer.
    * **Ergebnis**: Das Design einer **Drei-Phasen-Synchronisation**: (1) Lokale History laden, (2) "Catch-up"-Daten vom Broker holen, (3) auf den Live-Stream umschalten.

    ### Modus 2: "Simulation & Backtest"
    * **Motivation**: Für Entwicklung, Test und Analyse muss die Software **völlig autonom** und ohne Abhängigkeit von einer externen, potenziell nicht verfügbaren Broker-API lauffähig sein.
    * **Ziel**: Einen "Sofort-Start" für Backtests zu jedem beliebigen Zeitpunkt in der Vergangenheit zu ermöglichen, der rein auf lokalen Dateien basiert.
    * **Ergebnis**: Ein Design, bei dem der relevante Datenkontext in den Speicher geladen wird, um von dort aus einen schnellen Start und ein "Playback" der Daten zu ermöglichen.
2025-06-13 10:50:40 +02:00

196 lines
5.3 KiB
ObjectPascal

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<T> = 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<T: record> = record
private
const
ChunkSize = 1024;
type
TChunk = TArray<TDataPoint<T>>;
private
FChunks: TArray<TChunk>;
FCount: 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<T>;
function GetTime(Idx: Int64): TDateTime;
procedure SetData(Idx: Int64; const Value: T);
public
// Adds a new data point to the series.
procedure Add(const Data: TDataPoint<T>);
// 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<T>);
class operator Finalize(var Dest: TDataSeries<T>);
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<T> read GetItems; default;
property Time[Idx: Int64]: TDateTime read GetTime;
property Data[Idx: Int64]: T read GetData write SetData;
end;
implementation
{ TAskBidItem }
constructor TAskBidItem.Create(AAsk, ABid: Single);
begin
Ask := AAsk;
Bid := ABid;
end;
{ TDataPoint<T> }
constructor TDataPoint<T>.Create(ATime: TDateTime; const AData: T);
begin
Time := ATime;
Data := AData;
end;
{ TDataSeries<T> }
procedure TDataSeries<T>.Add(const Data: TDataPoint<T>);
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);
end;
procedure TDataSeries<T>.Clear;
begin
FChunks := nil;
FCount := 0;
end;
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
begin
Dest.Clear;
end;
function TDataSeries<T>.GetCount: Int64;
begin
Result := FCount;
end;
function TDataSeries<T>.GetData(Idx: Int64): T;
var
physicalIndex: Int64;
begin
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data;
end;
function TDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
var
physicalIndex: Int64;
begin
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize];
end;
function TDataSeries<T>.GetTime(Idx: Int64): TDateTime;
var
physicalIndex: Int64;
begin
physicalIndex := LogicalToPhysicalIndex(Idx);
Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Time;
end;
class operator TDataSeries<T>.Initialize(out Dest: TDataSeries<T>);
begin
Dest.FCount := 0;
Dest.FChunks := nil;
end;
function TDataSeries<T>.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 := GetTime(mid);
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;
function TDataSeries<T>.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64;
begin
Assert((LogicalIndex >= 0) and (LogicalIndex < FCount), 'Logical index is out of bounds.');
Result := FCount - LogicalIndex - 1;
end;
procedure TDataSeries<T>.SetData(Idx: Int64; const Value: T);
var
physicalIndex: Int64;
begin
physicalIndex := LogicalToPhysicalIndex(Idx);
FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data := Value;
end;
end.