4d67e587ba
*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.
277 lines
8.6 KiB
ObjectPascal
277 lines
8.6 KiB
ObjectPascal
unit Myc.Test.Trade.DataPoint;
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils,
|
|
System.Classes,
|
|
DUnitX.TestFramework,
|
|
Myc.Trade.DataPoint;
|
|
|
|
type
|
|
|
|
[TestFixture]
|
|
TTestTDataSeries = class(TObject)
|
|
private
|
|
FSeries: TDataSeries<TAskBidItem>;
|
|
procedure SetupSeriesWithData;
|
|
public
|
|
[Setup]
|
|
procedure Setup;
|
|
[Teardown]
|
|
procedure Teardown;
|
|
|
|
[Test]
|
|
procedure TestSetupSeriesWithDataVerification;
|
|
[Test]
|
|
procedure TestAddAndCount;
|
|
[Test]
|
|
[IgnoreMemoryLeaks]
|
|
procedure TestAddOrderAssertion;
|
|
[Test]
|
|
procedure TestGetItemsIndexing;
|
|
[Test]
|
|
procedure TestIndexOfExistingTimeStamp;
|
|
[Test]
|
|
procedure TestIndexOfNonExistingBetween;
|
|
[Test]
|
|
procedure TestIndexOfBeforeFirstItem;
|
|
[Test]
|
|
procedure TestIndexOfAfterLastItem;
|
|
[Test]
|
|
procedure TestIndexOfExactOldestItem;
|
|
[Test]
|
|
procedure TestIndexOfExactNewestItem;
|
|
[Test]
|
|
procedure TestIndexOfEmptySeries;
|
|
[Test]
|
|
procedure TestIndexOfSingleItemSeries;
|
|
end;
|
|
|
|
implementation
|
|
|
|
{ TTestTDataSeries }
|
|
|
|
procedure TTestTDataSeries.Setup;
|
|
begin
|
|
// Managed record is implicitly initialized.
|
|
end;
|
|
|
|
procedure TTestTDataSeries.Teardown;
|
|
begin
|
|
// Ensure managed record is finalized to prevent false leak reports.
|
|
FSeries := Default(TDataSeries<TAskBidItem>);
|
|
end;
|
|
|
|
procedure TTestTDataSeries.SetupSeriesWithData;
|
|
var
|
|
i: Integer;
|
|
DataPoint: TDataPoint<TAskBidItem>;
|
|
baseTime: TDateTime;
|
|
begin
|
|
FSeries := Default(TDataSeries<TAskBidItem>);
|
|
baseTime := EncodeDate(2020, 7, 7);
|
|
|
|
// Add 10 data points with increasing timestamps.
|
|
for i := 0 to 9 do
|
|
begin
|
|
DataPoint := TDataPoint<TAskBidItem>.Create(baseTime + i, TAskBidItem.Create(i * 1.0, i * 1.0 + 0.1));
|
|
FSeries.Add(DataPoint);
|
|
end;
|
|
// Resulting state:
|
|
// Newest item: Time = baseTime + 9, Logical Index = 0
|
|
// Oldest item: Time = baseTime + 0, Logical Index = 9
|
|
end;
|
|
|
|
// Verifies that the test data setup helper works as expected.
|
|
procedure TTestTDataSeries.TestSetupSeriesWithDataVerification;
|
|
var
|
|
i: Integer;
|
|
baseTime, expectedTime: TDateTime;
|
|
expectedAsk: Single;
|
|
begin
|
|
SetupSeriesWithData;
|
|
baseTime := EncodeDate(2020, 7, 7);
|
|
|
|
Assert.AreEqual(Int64(10), FSeries.Count, 'Setup should create exactly 10 items');
|
|
|
|
// Check all items to ensure correct reverse chronological order.
|
|
for i := 0 to FSeries.Count - 1 do
|
|
begin
|
|
expectedTime := baseTime + (9 - i);
|
|
expectedAsk := Single(9 - i);
|
|
Assert.AreEqual(expectedTime, FSeries[i].Time, 'Item at logical index should have correct reversed timestamp');
|
|
Assert.AreEqual(expectedAsk, FSeries[i].Data.Ask, 'Item at logical index should have correct reversed data');
|
|
end;
|
|
end;
|
|
|
|
// Tests adding new items and the resulting count.
|
|
procedure TTestTDataSeries.TestAddAndCount;
|
|
var
|
|
DataPoint: TDataPoint<TAskBidItem>;
|
|
newTime: TDateTime;
|
|
begin
|
|
SetupSeriesWithData;
|
|
newTime := EncodeDate(2020, 7, 17);
|
|
|
|
Assert.AreEqual(Int64(10), FSeries.Count, 'Initial count should be 10');
|
|
|
|
DataPoint := TDataPoint<TAskBidItem>.Create(newTime, TAskBidItem.Create(100.0, 100.1));
|
|
FSeries.Add(DataPoint);
|
|
|
|
Assert.AreEqual(Int64(11), FSeries.Count, 'Count should be 11 after adding one more');
|
|
Assert.AreEqual(newTime, FSeries.Items[0].Time, 'Newest item should be at index 0');
|
|
end;
|
|
|
|
// Ensures that adding an item with an older timestamp raises an assertion.
|
|
procedure TTestTDataSeries.TestAddOrderAssertion;
|
|
var
|
|
olderTime: TDateTime;
|
|
begin
|
|
SetupSeriesWithData; // Newest item is at 2020-07-16
|
|
olderTime := EncodeDate(2020, 7, 15);
|
|
|
|
Assert.WillRaise(
|
|
procedure
|
|
begin
|
|
var DataPoint := TDataPoint<TAskBidItem>.Create(olderTime, TAskBidItem.Create(0.0, 0.0));
|
|
FSeries.Add(DataPoint);
|
|
end,
|
|
EAssertionFailed,
|
|
'Adding item with older timestamp should raise an assert error'
|
|
);
|
|
end;
|
|
|
|
// Verifies that the logical-to-physical index mapping is correct.
|
|
procedure TTestTDataSeries.TestGetItemsIndexing;
|
|
var
|
|
i: Integer;
|
|
expectedTime: TDateTime;
|
|
baseTime: TDateTime;
|
|
begin
|
|
SetupSeriesWithData;
|
|
baseTime := EncodeDate(2020, 7, 7);
|
|
|
|
for i := 0 to 9 do
|
|
begin
|
|
expectedTime := baseTime + (9 - i);
|
|
Assert.AreEqual(expectedTime, FSeries.Items[i].Time, 'Item at logical index should have reversed chronological time');
|
|
Assert.AreEqual(Single(9 - i), FSeries.Items[i].Data.Ask, 'Item data at logical index should match reversed insertion order');
|
|
end;
|
|
end;
|
|
|
|
// Tests finding an existing item (in this case, the oldest).
|
|
procedure TTestTDataSeries.TestIndexOfExistingTimeStamp;
|
|
var
|
|
ATimeStamp: TDateTime;
|
|
expectedIndex: Int64;
|
|
begin
|
|
SetupSeriesWithData;
|
|
ATimeStamp := EncodeDate(2020, 7, 7); // Oldest item
|
|
expectedIndex := 9;
|
|
|
|
Assert.AreEqual(
|
|
expectedIndex,
|
|
FSeries.IndexOf(ATimeStamp),
|
|
'IndexOf for the oldest existing item timestamp should return its correct index'
|
|
);
|
|
end;
|
|
|
|
// Tests finding the index for a timestamp that falls between two existing items.
|
|
procedure TTestTDataSeries.TestIndexOfNonExistingBetween;
|
|
var
|
|
ATimeStamp: TDateTime;
|
|
expectedIndex: Int64;
|
|
begin
|
|
SetupSeriesWithData;
|
|
ATimeStamp := EncodeDate(2020, 7, 7) + 0.5; // 12:00 on the day of the oldest item
|
|
expectedIndex := Int64(9); // Should find the item from 00:00
|
|
|
|
Assert.AreEqual(
|
|
expectedIndex,
|
|
FSeries.IndexOf(ATimeStamp),
|
|
'IndexOf for a non-existing timestamp should return the index of the immediately preceding item'
|
|
);
|
|
end;
|
|
|
|
// Tests finding a timestamp that is older than any item in the series.
|
|
procedure TTestTDataSeries.TestIndexOfBeforeFirstItem;
|
|
var
|
|
ATimeStamp: TDateTime;
|
|
baseTime: TDateTime;
|
|
begin
|
|
SetupSeriesWithData;
|
|
baseTime := EncodeDate(2020, 7, 7);
|
|
ATimeStamp := baseTime - 1; // A day before the oldest item
|
|
|
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(ATimeStamp), 'IndexOf for a timestamp before the oldest item should return -1');
|
|
end;
|
|
|
|
// Tests finding a timestamp that is newer than any item in the series.
|
|
procedure TTestTDataSeries.TestIndexOfAfterLastItem;
|
|
var
|
|
ATimeStamp: TDateTime;
|
|
baseTime: TDateTime;
|
|
begin
|
|
SetupSeriesWithData;
|
|
baseTime := EncodeDate(2020, 7, 7);
|
|
ATimeStamp := baseTime + 10; // A day after the newest item
|
|
|
|
Assert.AreEqual(
|
|
Int64(0),
|
|
FSeries.IndexOf(ATimeStamp),
|
|
'IndexOf for a timestamp after the newest item should return the index of the newest item'
|
|
);
|
|
end;
|
|
|
|
// Specifically tests finding the exact oldest item.
|
|
procedure TTestTDataSeries.TestIndexOfExactOldestItem;
|
|
var
|
|
ATimeStamp: TDateTime;
|
|
begin
|
|
SetupSeriesWithData;
|
|
ATimeStamp := EncodeDate(2020, 7, 7);
|
|
|
|
Assert.AreEqual(Int64(9), FSeries.IndexOf(ATimeStamp), 'IndexOf for the exact oldest timestamp should return the last index');
|
|
end;
|
|
|
|
// Specifically tests finding the exact newest item.
|
|
procedure TTestTDataSeries.TestIndexOfExactNewestItem;
|
|
var
|
|
ATimeStamp: TDateTime;
|
|
begin
|
|
SetupSeriesWithData;
|
|
ATimeStamp := EncodeDate(2020, 7, 16);
|
|
|
|
Assert.AreEqual(Int64(0), FSeries.IndexOf(ATimeStamp), 'IndexOf for the exact newest timestamp should return index 0');
|
|
end;
|
|
|
|
// Tests IndexOf on an empty series.
|
|
procedure TTestTDataSeries.TestIndexOfEmptySeries;
|
|
begin
|
|
FSeries := Default(TDataSeries<TAskBidItem>);
|
|
Assert.AreEqual(Int64(0), FSeries.Count);
|
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(Now), 'IndexOf on empty series should return -1');
|
|
end;
|
|
|
|
// Tests IndexOf on a series with only one item.
|
|
procedure TTestTDataSeries.TestIndexOfSingleItemSeries;
|
|
var
|
|
DataPoint: TDataPoint<TAskBidItem>;
|
|
testTime: TDateTime;
|
|
begin
|
|
FSeries := Default(TDataSeries<TAskBidItem>);
|
|
testTime := EncodeDate(2025, 1, 1);
|
|
DataPoint := TDataPoint<TAskBidItem>.Create(testTime, TAskBidItem.Create(1.0, 2.0));
|
|
FSeries.Add(DataPoint);
|
|
|
|
Assert.AreEqual(Int64(1), FSeries.Count);
|
|
Assert.AreEqual(Int64(0), FSeries.IndexOf(testTime), 'IndexOf for exact single item should be 0');
|
|
Assert.AreEqual(Int64(0), FSeries.IndexOf(testTime + 0.5), 'IndexOf for time after single item should be 0');
|
|
Assert.AreEqual(Int64(-1), FSeries.IndexOf(testTime - 1), 'IndexOf for time before single item should be -1');
|
|
end;
|
|
|
|
initialization
|
|
TDUnitX.RegisterTestFixture(TTestTDataSeries);
|
|
end.
|