Files
MycLib/Src/Myc.Test.Trade.DataStream.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

147 lines
4.6 KiB
ObjectPascal

unit Myc.Test.Trade.DataStream;
interface
uses
System.SysUtils,
System.Generics.Collections,
DUnitX.TestFramework,
Myc.Signals,
Myc.Lazy,
Myc.Futures,
Myc.Trade.DataPoint,
Myc.Trade.DataStream;
type
// Test fixture for TDataServer and TAuraTABFileServer, focusing on data equivalence
// with TDataSeries.LoadDataSeries.
[TestFixture]
[IgnoreMemoryLeaks(true)]
TTest_TABFileServer_Equivalence = class(TObject)
private
FServer: IAuraDataServer<TAskBidItem>;
FStream: IDataStream<TAskBidItem>;
public
[SetupFixture]
procedure SetupFixture;
[Setup]
procedure Setup;
[TearDown]
procedure TearDown;
[TearDownFixture]
procedure TearDownFixture;
[Test]
procedure Test_ServerReturnsSameDataAs_LoadDataSeries;
end;
implementation
uses
Myc.TaskManager;
const
// The reference data file for this test.
C_TEST_PATH = '\\COFFEE\TickData\Pepperstone';
C_TEST_SYMBOL = 'GER40';
// The chunk size for fetching data from the server.
C_MAX_FETCH = 200;
{ TTest_TABFileServer_Equivalence }
procedure TTest_TABFileServer_Equivalence.SetupFixture;
begin
FServer := TAuraTABFileServer.Create(C_TEST_PATH);
// TAskBid.ClearCache ensures a clean state for data series loading.
FServer.ClearCache;
end;
procedure TTest_TABFileServer_Equivalence.TearDownFixture;
begin
FServer := nil;
// Resets the global IsMemoryLow function pointer, if it was set for testing.
IsMemoryLow := nil;
end;
procedure TTest_TABFileServer_Equivalence.Setup;
begin
// Initializes the data server with a specific test file.
FStream := FServer.CreateStream(C_TEST_SYMBOL);
end;
procedure TTest_TABFileServer_Equivalence.TearDown;
begin
// Releases the data server instance.
FStream := nil;
// Clears any cached data to prevent interference with subsequent tests.
FServer.ClearCache;
end;
procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries;
var
chunk: array[0..C_MAX_FETCH - 1] of TDataPoint<TAskBidItem>;
dst: TArray<TDataPoint<TAskBidItem>>;
i: Int64;
n: Integer;
cnt: Int64;
timeout: Integer;
filename: string;
expectedData: TArray<TDataPoint<TAskBidItem>>;
begin
// 1. Expected data is loaded directly using LoadDataSeries.
filename := TAuraTABFileServer.FindFirstDataFile(C_TEST_PATH, C_TEST_SYMBOL);
Assert.IsNotEmpty(filename, 'Test data file could not be found.');
expectedData := FServer.LoadDataSeries(filename).WaitFor;
SetLength(dst, Length(expectedData));
// 2. Fetch data chunks from the stream until all data is retrieved.
cnt := 0;
timeout := 0;
while (cnt < Length(expectedData)) do
begin
n := FStream.GetChunk(chunk);
if n > 0 then
begin
Move(chunk[0], dst[cnt], n * SizeOf(TDataPoint<TAskBidItem>));
Inc(cnt, n);
timeout := 0; // Reset timeout on progress
end
else
begin
// If GetChunk returns 0, the stream might be waiting for async I/O.
// A small sleep prevents a tight loop from consuming 100% CPU.
Sleep(1);
Inc(timeout);
Assert.IsTrue(timeout < 5000, 'Test timed out waiting for data from stream.');
end;
end;
// 3. A final call should return 0, as the stream must be depleted.
n := FStream.GetChunk(chunk);
Assert.AreEqual(0, n, 'Stream returned data after it should have been depleted.');
n := FStream.GetChunk(chunk);
Assert.AreEqual(0, n, 'Stream returned data after it should have been depleted.');
// --- Assertions ---
// 4. Verify that the total number of records fetched matches the expected count.
Assert.AreEqual(Length(expectedData), cnt, 'Data record count mismatch.');
// 5. Verify that the content of each record is identical.
// The loop checks every 1000th element for efficiency.
for n := 0 to High(expectedData) div 1000 do
begin
i := n * 1000;
Assert.AreEqual(expectedData[i].Time, dst[i].Time, 'Timestamp mismatch at index ' + i.ToString);
Assert.AreEqual(expectedData[i].Data.Ask, dst[i].Data.Ask, 'Ask price mismatch at index ' + i.ToString);
Assert.AreEqual(expectedData[i].Data.Bid, dst[i].Data.Bid, 'Bid price mismatch at index ' + i.ToString);
end;
end;
initialization
// Registers the test fixture with DUnitX.
TDUnitX.RegisterTestFixture(TTest_TABFileServer_Equivalence);
end.