diff --git a/KI/Prompt-Delphi-Entwicklung.txt b/KI/Prompt-Delphi-Entwicklung.md similarity index 69% rename from KI/Prompt-Delphi-Entwicklung.txt rename to KI/Prompt-Delphi-Entwicklung.md index 7b343ab..a0dc41b 100644 --- a/KI/Prompt-Delphi-Entwicklung.txt +++ b/KI/Prompt-Delphi-Entwicklung.md @@ -1,4 +1,4 @@ -Zweck und Ziele +# Zweck und Ziele * Unterstütze mich bei der Entwicklung unter Embarcadero Delphi. * Ich bin ein sehr erfahrener Softwareentwickler. Fasse dich kurz und nutze Fachsprache. @@ -6,7 +6,7 @@ Zweck und Ziele -Regeln +# Allgemeine Regeln * Sprache im Code und in den Kommentaren: Englisch * Sprache im Chat: Deutsch @@ -25,14 +25,21 @@ Regeln * Erkläre niemals grundlegende Syntax, es sei denn ich frage ausdrücklich danach. -* Fasse dich kurz. - -* Behalte den Kontext während der gesamten Konversation bei. Alle Ideen und Antworten sollen mit der vorherigen Diskussion in Verbindung stehen. Schweife nicht ab. +* Fasse dich kurz. Behalte den Kontext während der gesamten Konversation bei. Alle Ideen und Antworten sollen mit der vorherigen Diskussion in Verbindung stehen. Schweife nicht ab. * Wenn ich unvollständigen Code poste, erstelle einen Plan, wie die Implementierung aussehen könnte und präsentiere ihn kurz und prägnant. +# TODO -Code-Generierung +* Wenn ich Code poste, der einen TODO-Eintrag enthält, dann implementiere die dort spezifizierten Anforderungen. + +* Ändere *nicht* den umliegenden Code. Nutze den vorhandenen Kontext um die Anforderung zu erledigen. + +* Dokumentiere die Änderung knapp direkt im Code. + +* Gib mir als Ergebnis den vollständigen Codeblock zurück. + +# Code-Generierung * Befolge die gängigen Delphi-Formatierungsstandards mit folgenden Ausnahmen: @@ -67,35 +74,58 @@ Code-Generierung * begin und end stehen am Anfang einer neuen Zeile. then steht nie am Anfang einer neuen Zeile. * RECORDs, die einen Initialize-Operator haben, sind Managed Records. Sie benötigen also kein explizites Create. -Unit-Tests +# Unit-Tests * Verwende nur statische Strings für Log-Einträge und Asserts. Keine Format(), ToString usw. (Das verursacht Speicherlecks außerhalb des Test-Gültigkeitsbereichs, sodass ein Leak vom Memory Manager gemeldet wird.) * Verwende das Attribut für parametrische Tests, z. B. [TestCase('TestName', 'Parameter1,Parameter2,...')] -Kommentare im Code +# Kommentare im Code -* Vermeide jegliche Kommentare, die Änderungen am Code beschreiben. Z.B. "// hier wurde was geändert". Das mag ich gar nicht. +* Vermeide jegliche Kommentare, die Änderungen am Code beschreiben. Z.B. `// hier wurde was geändert`. Das mag ich gar nicht. -* Benutze keine HTML-Tags (, etc.)! -* Benutze "//" oder "(* *)" und fasse dich extrem kurz. Meistens genügen Einzeiler vor den Deklarationen. +* Benutze keine HTML-Tags (``, etc.)! +* Benutze `//` oder `(* *)` und fasse dich extrem kurz. Meistens genügen Einzeiler vor den Deklarationen. * Kommentare in der interface-Sektion einer Unit sollen die Schnittstelle dokumentieren. Dokumentiere ausschließlich Elemente, die auch von außen zugänglich sind. Und beziehe dich nur auf Elemente, die von außen zugänglich sind. Im Interface wird beschrieben, WAS eine Funktion macht. Es wird nicht beschrieben WIE sie es macht! * Kommentare im Implementation-Teil sollten sehr sparsam eingesetzt werden. Sie sind nur nötig, wenn etwas wirklich kompliziertes Beschrieben werden muss und auch nur, wenn sich die Funktion nicht aus dem Quelltext ergibt. -Interface helper +# Projektplan -* Ein "interface helper" ist ein record, das immer nur genau ein Interface referenziert. Wenn ich dich dazu auffordere sollst du ihn so weit wie möglich selbst erzeugen, oder einen unvollständigen helper ergänzen. Auf jeden Fall enthält ein interface helper: +* **Wenn ich dich darum bitte**, erzeuge eine Zusammenfassung der Ergebnisse unserer Unterhaltung. Diese möchte ich in einen externen Projektplan kopieren. + + - Formatiere in Markdown. Gib als Antwort nur den Projektplan aus (damit ich ihn direkt kopieren kann). + - Füge Datum und Uhrzeit hinzu. + - Gliedere in der Reihenfolge: Motivation - Ziel - Ergebnis + - Fass dich kurz + + +# Interface helper + +**Interface helper** sind ein Konzept, dass *nicht* explizit in Delphi/Pascal verankert ist. Es werden stattdessen managed records benutzt um ein Interface zu kapseln und die zugrundeliegende Implementierung vollständig zu verbergen. + + - Ein "interface helper" ist ein managed record, das immer nur **genau ein** Interface referenziert. + + - Es verbirgt die verschiedenen Implementierungen des Interface und fungiert als generische "Instanz" des interface. + + - interface helper unterstützen das **null object pattern**. Der Sinn dieses Patterns ist es, nil-Prüfungen überflüssig zu machen und stattdessen ein Null-Objekt mit definiertem "leerem" Verhalten zu haben. + + - Die Implementierungen des Interfaces finden sich oft in "Core"-Klassen, oder im implementation-Teil der Unit. Diese Implementierungen sollen von Benutzern nicht direkt eingebunden werden (außer zum Testen.) + +* Wenn ich dich dazu auffordere sollst du ihn so weit wie möglich selbst erzeugen, oder einen unvollständigen helper ergänzen. Auf jeden Fall enthält ein interface helper: - einen Konstruktor - zwei implicit-operatoren, die vom helper zum interface casten (und umgekehrt) - Wrapper für die Methoden und Properties des Interface. - - ein optionales class property "Null". + - ein class property "Null". -Beispiel: +* Immer wenn ein interface helper für ein interface vorhanden wird, soll er auch benutzt werden. Greife nicht direkt auf die Implementierung zu, lasse den helper das erledigen. +## Beispiel: + +``` type IFuture = interface function GetValue: T; @@ -124,7 +154,7 @@ type class property Null: IFuture read FNull; - // Wrapper methods for IFuture + // Wrapper methods for IFuture property Done: TState read GetDone; property Value: T read GetValue; end; @@ -165,3 +195,5 @@ function TFuture.GetValue: T; begin Result := FFuture.Value; end; +``` + diff --git a/Src/Myc.Test.Trade.DataPoint.pas b/Src/Myc.Test.Trade.DataPoint.pas index ec1089d..27fed3b 100644 --- a/Src/Myc.Test.Trade.DataPoint.pas +++ b/Src/Myc.Test.Trade.DataPoint.pas @@ -170,7 +170,11 @@ begin 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'); + 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. @@ -183,7 +187,11 @@ begin 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'); + 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. @@ -209,7 +217,11 @@ begin 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'); + 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. diff --git a/Src/Myc.Test.Trade.DataStream.pas b/Src/Myc.Test.Trade.DataStream.pas index ef752be..f9348e2 100644 --- a/Src/Myc.Test.Trade.DataStream.pas +++ b/Src/Myc.Test.Trade.DataStream.pas @@ -81,42 +81,62 @@ end; procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries; var chunk: array[0..C_MAX_FETCH - 1] of TDataPoint; - Dst: TArray>; + dst: TArray>; i: Int64; + n: Integer; + cnt: Int64; + timeout: Integer; + filename: string; + expectedData: TArray>; begin - // Loads the expected data directly using TAskBid.LoadDataSeries.WaitFor. + // 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.'); - var Filename := TAuraTABFileServer.FindFirstDataFile(C_TEST_PATH, C_TEST_SYMBOL); + expectedData := FServer.LoadDataSeries(filename).WaitFor; + SetLength(dst, Length(expectedData)); - Assert.IsNotEmpty(Filename); - - var ExpectedData := FServer.LoadDataSeries(Filename).WaitFor; - SetLength(Dst, Length(ExpectedData)); - - var cnt: Int64 := 0; - var n: Integer; - // Fetches data chunks from the server until all data is retrieved or server indicates live data. - while not FStream.IsLiveData.Value and (cnt < Length(Dst)) do + // 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 - Move(chunk[0], dst[cnt], n * sizeof(TDataPoint)); - inc(cnt, n); + begin + Move(chunk[0], dst[cnt], n * SizeOf(TDataPoint)); + 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 --- - // 1. Verify that the total number of records fetched from the server matches the expected count. - Assert.AreEqual(Length(ExpectedData), cnt, 'Data record count mismatch'); + // 4. Verify that the total number of records fetched matches the expected count. + Assert.AreEqual(Length(expectedData), cnt, 'Data record count mismatch.'); - // 2. Verify that the content of each record is identical. + // 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 + for n := 0 to High(expectedData) div 1000 do begin i := n * 1000; - Assert.AreEqual(ExpectedData[i].Time, Dst[i].Time, 'Timestamp mismatch'); - Assert.AreEqual(ExpectedData[i].Data.Ask, Dst[i].Data.Ask, 'Ask price mismatch'); - Assert.AreEqual(ExpectedData[i].Data.Bid, Dst[i].Data.Bid, 'Bid price mismatch'); + 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; diff --git a/Src/Myc.Trade.DataPoint.pas b/Src/Myc.Trade.DataPoint.pas index fbf51d5..ac498af 100644 --- a/Src/Myc.Trade.DataPoint.pas +++ b/Src/Myc.Trade.DataPoint.pas @@ -22,7 +22,7 @@ type // A time-ordered series of data points, optimized for chronological additions. // The most recently added element has the logical index 0. - TDataSeries = record + TDataSeries = record private const ChunkSize = 1024; @@ -31,12 +31,18 @@ type private FChunks: TArray; 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; + 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); - + // 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. @@ -47,10 +53,11 @@ type 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; end; implementation @@ -75,17 +82,14 @@ end; 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'); + // 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 + if (di = 0) then begin - Assert(ci = Length(FChunks)); - Assert(di = 0); SetLength(FChunks, ci + 1); SetLength(FChunks[ci], ChunkSize); end; @@ -94,10 +98,15 @@ begin Inc(FCount); end; +procedure TDataSeries.Clear; +begin + FChunks := nil; + FCount := 0; +end; + class operator TDataSeries.Finalize(var Dest: TDataSeries); begin - Dest.FChunks := nil; - Dest.FCount := 0; + Dest.Clear; end; function TDataSeries.GetCount: Int64; @@ -105,12 +114,28 @@ begin Result := FCount; end; -function TDataSeries.GetItems(Idx: Int64): TDataPoint; +function TDataSeries.GetData(Idx: Int64): T; +var + physicalIndex: Int64; 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]; + physicalIndex := LogicalToPhysicalIndex(Idx); + Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data; +end; + +function TDataSeries.GetItems(Idx: Int64): TDataPoint; +var + physicalIndex: Int64; +begin + physicalIndex := LogicalToPhysicalIndex(Idx); + Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize]; +end; + +function TDataSeries.GetTime(Idx: Int64): TDateTime; +var + physicalIndex: Int64; +begin + physicalIndex := LogicalToPhysicalIndex(Idx); + Result := FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Time; end; class operator TDataSeries.Initialize(out Dest: TDataSeries); @@ -134,7 +159,7 @@ begin while (low <= high) do begin mid := low + (high - low) div 2; - dataPointTime := GetItems(mid).Time; + dataPointTime := GetTime(mid); if (dataPointTime = TimeStamp) then begin @@ -153,4 +178,18 @@ begin end; end; +function TDataSeries.LogicalToPhysicalIndex(LogicalIndex: Int64): Int64; +begin + Assert((LogicalIndex >= 0) and (LogicalIndex < FCount), 'Logical index is out of bounds.'); + Result := FCount - LogicalIndex - 1; +end; + +procedure TDataSeries.SetData(Idx: Int64; const Value: T); +var + physicalIndex: Int64; +begin + physicalIndex := LogicalToPhysicalIndex(Idx); + FChunks[physicalIndex div ChunkSize][physicalIndex mod ChunkSize].Data := Value; +end; + end. diff --git a/Src/Myc.Trade.DataStream.pas b/Src/Myc.Trade.DataStream.pas index ffb53b8..72101a3 100644 --- a/Src/Myc.Trade.DataStream.pas +++ b/Src/Myc.Trade.DataStream.pas @@ -31,18 +31,24 @@ uses type // Represents a generic data stream capable of providing sequential data chunks. + // IsHistory: + // - true, if this stream is a history stream. Once HasData becomes false, it reached it's end and will not provide more data. + // - false, we expect more Data to come. This stream has no end. + // HasData: set, if a call to GetChunk will return new data. IDataStream = interface ['{A6E246A2-E84E-49AB-A63E-333E561E488C}'] - function GetIsLiveData: TMutable; + function GetHasData: TSignal; function GetChunk(var Data: array of TDataPoint): Integer; - property IsLiveData: TMutable read GetIsLiveData; + function IsHistory: Boolean; + property HasData: TSignal read GetHasData; end; // Abstract base class for IDataStream implementations. TDataStream = class(TInterfacedObject, IDataStream) protected - function GetIsLiveData: TMutable; virtual; abstract; + function GetHasData: TSignal; virtual; abstract; function GetChunk(var Data: array of TDataPoint): Integer; virtual; abstract; + function IsHistory: Boolean; virtual; abstract; end; // Represents a factory for creating IDataStream instances. @@ -131,10 +137,10 @@ type end; // Implements a data stream that reads from Aura-specific historical data files. - TAuraFileStream = class(TDataStream, IDataStream) + TAuraFileStream = class(TDataStream) private FDataServer: TAuraDataServer; - FIsLiveData: TMutable.IWriteable; + FHasData: TEvent; FCurrentFileName: string; FCurrentData: TFuture>>; FNextFileName: string; @@ -142,10 +148,12 @@ type FCurrPosInFile: Int64; FLastTimeStamp: TDateTime; protected + function GetHasData: TSignal; override; function GetChunk(var Data: array of TDataPoint): Integer; override; - function GetIsLiveData: TMutable; override; + function IsHistory: Boolean; override; public constructor Create(ADataServer: TAuraDataServer; const AFilename: String); + destructor Destroy; override; procedure AfterConstruction; override; end; @@ -581,21 +589,30 @@ begin Assert(Assigned(ADataServer)); FDataServer := ADataServer; FCurrentFileName := AFilename; - FIsLiveData := TMutable.CreateWriteable(false); + // Create an event + FHasData := TEvent.CreateEvent; // interface helper benutzen! +end; + +destructor TAuraFileStream.Destroy; +begin + inherited; end; procedure TAuraFileStream.AfterConstruction; begin inherited; - FCurrentData := FDataServer.LoadDataFile(FCurrentFileName); - FCurrPosInFile := 0; - FIsLiveData.SetValue(false); FLastTimeStamp := 0; + FCurrPosInFile := 0; + FCurrentData := FDataServer.LoadDataFile(FCurrentFileName); + // Forward all future done events, because this means there is new data. + FCurrentData.Done.Subscribe( FHasData ); + FNextFileName := FDataServer.FindNextDataFile(FCurrentFileName); if FNextFileName <> '' then - FNextData := FDataServer.LoadDataFile(FNextFileName) - else - FNextData := nil; + begin + FNextData := FDataServer.LoadDataFile(FNextFileName); + FNextData.Done.Subscribe( FHasData ); + end; end; function TAuraFileStream.GetChunk(var Data: array of TDataPoint): Integer; @@ -604,39 +621,37 @@ var currData: TArray>; begin Result := 0; + + // This is an asynchronous operation! We don't wait for data. It's totally valid to result nothing, if there is nothing. if not FCurrentData.Done.IsSet then exit; - var maxLen := Length(Data); - currData := FCurrentData.Value; - while Result < maxLen do + if FCurrPosInFile >= Length(FCurrentData.Value) then begin - if FCurrPosInFile >= Length(currData) then - begin - FCurrentData := FNextData; - FCurrentFileName := FNextFileName; - FCurrPosInFile := 0; - if FCurrentFileName <> '' then - begin - FNextFileName := FDataServer.FindNextDataFile(FCurrentFileName); - if FNextFileName <> '' then - FNextData := FDataServer.LoadDataFile(FNextFileName) - else - FNextData := nil; + FCurrentData := FNextData; + FCurrentFileName := FNextFileName; + FCurrPosInFile := 0; + FNextData := TFuture>>.Null; - if FCurrentData.Done.IsSet then - begin - currData := FCurrentData.Value; - continue; - end; - end - else - begin - FIsLiveData.SetValue(true); - end; - break; + if FCurrentFileName = '' then + exit; + + FNextFileName := FDataServer.FindNextDataFile(FCurrentFileName); + if FNextFileName <> '' then + begin + FNextData := FDataServer.LoadDataFile(FNextFileName); + FNextData.Done.Subscribe( FHasData ); end; + if not FCurrentData.Done.IsSet then + exit; + end; + + currData := FCurrentData.Value; + + var maxLen := Length(Data); + while (Result < maxLen) and (FCurrPosInFile < Length(currData)) do + begin item := currData[FCurrPosInFile]; if FLastTimeStamp < item.Time then begin @@ -646,11 +661,23 @@ begin end; Inc(FCurrPosInFile); end; + + // If there is data left in the current file, signal new data. If it is finished, we do nothing, because + // the next signal will come from the next future being done. + if FCurrPosInFile < Length(currData) then + begin + FHasData.Notify; + end; end; -function TAuraFileStream.GetIsLiveData: TMutable; +function TAuraFileStream.GetHasData: TSignal; begin - Result := FIsLiveData; + Result := FHasData.Signal; +end; + +function TAuraFileStream.IsHistory: Boolean; +begin + Result := True; end; { TAuraTABFileServer } diff --git a/Test/MycTests.res b/Test/MycTests.res index e30ea80..333684a 100644 Binary files a/Test/MycTests.res and b/Test/MycTests.res differ