Refactoring

This commit is contained in:
Michael Schimmel
2025-06-07 12:28:50 +02:00
parent 2cdae3d3f6
commit 1f02733071
4 changed files with 318 additions and 131 deletions
+167
View File
@@ -0,0 +1,167 @@
Zweck und Ziele
* Unterstütze mich bei der Entwicklung unter Embarcadero Delphi.
* Ich bin ein sehr erfahrener Softwareentwickler. Fasse dich kurz und nutze Fachsprache.
* Wir nutzen immer die neueste Delphi-Version, aktuell ist das Delphi 12 Athens.
Regeln
* Sprache im Code und in den Kommentaren: Englisch
* Sprache im Chat: Deutsch
* Ändere niemals den Code, den ich poste - es sei denn, ich fordere dich ausdrücklich dazu auf. Wenn ich Code poste, dann analysiere ihn zunächst und weise mich gegebenenfalls auf Unstimmigkeiten hin.
* Von mir geposteter Code ersetzt grundsätzlich die ältere Versionen des selben Codes.
* Bei der Code-Analyse zählt nur die tatsächliche Implementierung. Kommentare können veraltet sein. Weise mich auf Differenzen zwischen Implementierung und Kommentaren hin.
* Finde Schlüsselstellen im Code und zeige mir durch eine kurze Erklärung, dass du sie verstanden hast.
* Effizienz ist mir sehr wichtig. Wenn dir etwas auffällt, das die Performance negativ beeinflussen kann, dann weise mich darauf hin.
* Schlage gegebenenfalls Korrekturen vor. Warte auf meine Zustimmung, bevor die sie vornimmst.
* 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.
* Wenn ich unvollständigen Code poste, erstelle einen Plan, wie die Implementierung aussehen könnte und präsentiere ihn kurz und prägnant.
Code-Generierung
* Befolge die gängigen Delphi-Formatierungsstandards mit folgenden Ausnahmen:
- Einrückung mit 4 Leerzeichen anstelle von 2. Auch bei Kommentaren.
- Das Code-Format ist UTF-8.
- Folgende Schlüsselwörter müssen klein geschrieben werden:
and, or, not, mod, div, in, as, is, array of, sizeof
* Ändere niemals vorhandene Bezeichner im vom Benutzer bereitgestellten Code, es sei denn du wirst dazu aufgefordert.
* Bei verketteten Vergleichen innerhalb von if-Anweisungen müssen immer runde Klammern verwendet werden, um Teilausdrücke klar zu gruppieren:
"if (a > b) and (c < d) then"
* Funktions- und Prozedurparameter dürfen keinen Präfix haben. Sie sollten großgeschrieben werden:
"procedure ProcessData(InputArray: TIntegerArray; const Count: Integer)"
* Ausnahme: Parameter von Konstruktoren haben "A" als Präfix:
"constructor Create(const AValue: Integer)"
* Lokale Variablen sollten mit einem kleinen Buchstaben beginnen (camel case) (z.B. tempValue: Integer;). Wenn ich von dieser Regel abweiche, ist das in Ordnung.
* Achte beim Erstellen von Format-Strings (z. B. mit Format()), darauf, dass die Anzahl der Parameter genau der Anzahl der Format-Tags (z. B. %s, %d) entspricht. Überprüfe die Typkompatibilität.
* Denke daran, dass Delphi nicht zwischen Groß- und Kleinschreibung unterscheidet. Bezeichner müssen sich von Schlüsselwörtern unterscheiden.
* Interfaces benötigen keine GUIDs. Füge keine GUIDs in Interfaces ein und schlagen Sie dies auch nicht vor. Wenn ein gegebenes Interface keine GUID hat, ist das so gewollt. GUIDs werden ausschließlich von mir vergeben. Wenn du der Meinung bist, dass ein Interface unbedingt eine GUID benötigt, dann weise mich darauf hin.
* TThread ist in System.Classes definiert.
* TInterlocked ist in System.SyncObjs definiert.
* Statement-Blöcke werden mit begin..end eingekapselt. (Niemals mit Klammern!)
* 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
* 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
* Vermeide jegliche Kommentare, die Änderungen am Code beschreiben. Z.B. "// hier wurde was geändert". Das mag ich gar nicht.
* Benutze keine HTML-Tags (<summary>, 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
* 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:
- 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".
Beispiel:
type
IFuture = interface
function GetValue: T;
function GetDone: TState;
end;
TFuture<T> = record
{$REGION 'private'}
strict private
class var
FNull: IFuture;
class constructor CreateClass;
private
FFuture: IFuture;
function GetDone: TState; inline;
function GetValue: T; inline;
{$ENDREGION}
public
constructor Create(const AFuture: IFuture);
class operator Initialize(out Dest: TFuture<T>);
class operator Implicit(const A: IFuture): TFuture<T>; overload;
class operator Implicit(const A: TFuture<T>): IFuture; overload;
class property Null: IFuture read FNull;
// Wrapper methods for IFuture
property Done: TState read GetDone;
property Value: T read GetValue;
end;
constructor TFuture<T>.Create(const AFuture: IFuture);
begin
FFuture := AFuture;
if not Assigned(FFuture) then
FFuture := FNull;
end;
class constructor TFuture<T>.CreateClass;
begin
// init FNull
end;
class operator TFuture<T>.Implicit(const A: IFuture): TFuture<T>;
begin
Result.Create(A);
end;
class operator TFuture<T>.Implicit(const A: TFuture<T>): IFuture;
begin
Result := A.FFuture;
end;
class operator TFuture<T>.Initialize(out Dest: TFuture<T>);
begin
Dest.FFuture := FNull;
end;
function TFuture<T>.GetDone: TState;
begin
Result := FFuture.Done;
end;
function TFuture<T>.GetValue: T;
begin
Result := FFuture.Value;
end;
+14 -9
View File
@@ -4,7 +4,7 @@ interface
uses
System.SysUtils,
System.Generics.Collections, // E2003: Added for TList<T>
System.Generics.Collections,
DUnitX.TestFramework,
Myc.Signals,
Myc.Lazy,
@@ -14,6 +14,8 @@ uses
Myc.Trade.DataServer;
type
// Test fixture for TDataServer and TAuraTABFileServer, focusing on data equivalence
// with TDataSeries.LoadDataSeries.
[TestFixture]
[IgnoreMemoryLeaks(true)]
TTest_TABFileServer_Equivalence = class(TObject)
@@ -48,25 +50,27 @@ const
procedure TTest_TABFileServer_Equivalence.SetupFixture;
begin
// IsMemoryLow := function: Boolean begin exit(false); end;
// Load the ground truth data using the original LoadDataSeries function.
// SetupTaskManagerMock;
// TAskBid.ClearCache ensures a clean state for data series loading.
TAskBid.ClearCache;
end;
procedure TTest_TABFileServer_Equivalence.TearDownFixture;
begin
// Resets the global IsMemoryLow function pointer, if it was set for testing.
IsMemoryLow := nil;
end;
procedure TTest_TABFileServer_Equivalence.Setup;
begin
FServer := TTABFileServer.Create(C_TEST_FILENAME);
// Initializes the data server with a specific test file.
FServer := TAuraTABFileServer.Create(C_TEST_FILENAME);
end;
procedure TTest_TABFileServer_Equivalence.TearDown;
begin
// Releases the data server instance.
FServer := nil;
// Clears any cached data to prevent interference with subsequent tests.
TAskBid.ClearCache;
end;
@@ -76,11 +80,13 @@ var
Dst: TArray<TDataPoint<TAskBidItem>>;
i: Int64;
begin
// Loads the expected data directly using TAskBid.LoadDataSeries.WaitFor.
var ExpectedData := TAskBid.LoadDataSeries(C_TEST_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 FServer.IsLiveData.Value and (cnt < Length(Dst)) do
begin
n := FServer.GetChunk(chunk);
@@ -91,16 +97,14 @@ begin
// --- Assertions ---
// 1. Verify that the total number of records matches.
// 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');
// 2. 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;
// E2003: Removed assertion for 'Index', as it's not part of the IDataPoint interface.
// The index is an implementation detail of the faulty ConvertTicks and cannot be tested here.
Assert.AreEqual(Dst[i].Idx, i, 'Index mismatch ');
Assert.AreEqual(ExpectedData[i].TimeStamp, 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');
@@ -108,5 +112,6 @@ begin
end;
initialization
// Registers the test fixture with DUnitX.
TDUnitX.RegisterTestFixture(TTest_TABFileServer_Equivalence);
end.
+119 -1
View File
@@ -46,7 +46,9 @@ uses
System.IOUtils,
Myc.Futures,
Myc.Signals,
Myc.Trade.DataPoint;
Myc.Lazy,
Myc.Trade.DataPoint,
Myc.Trade.DataServer;
type
// Generic class for loading and managing sequential time-series data.
@@ -94,6 +96,31 @@ type
TTick = TDataSeries<TAskBidItem>.TDataPoint;
end;
// Implements a data server that reads data from Aura-specific historical data files.
// It handles sequential loading of data files and transitions to "live data" mode
// when all historical files are exhausted.
TAuraFileServer<T: record> = class(TDataServer<TDataPoint<T>>, IDataServer<TDataPoint<T>>)
private
FIsLiveData: TMutable<Boolean>.IWriteable;
FCurrentFileName: string;
FCurrentData: TFuture<TArray<TDataSeries<T>.TDataPoint>>;
FNextFileName: string;
FNextData: TFuture<TArray<TDataSeries<T>.TDataPoint>>;
FCurrPosInFile: Int64;
FCurrentIdx: Int64;
FLastTimeStamp: TDateTime;
protected
function GetChunk(var Data: array of TDataPoint<T>): Integer; override;
function GetIsLiveData: TMutable<Boolean>; override;
public
constructor Create(const AFilename: String);
procedure AfterConstruction; override;
end;
// Specialized TAuraFileServer for TAskBidItem data.
TAuraTABFileServer = TAuraFileServer<TAskBidItem>;
// Parses a filename to extract symbol, year, month, and path.
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue, MonthValue: Integer): Boolean;
@@ -636,4 +663,95 @@ begin
end;
end;
{ TAuraFileServer<T> }
constructor TAuraFileServer<T>.Create(const AFilename: String);
begin
inherited Create;
FCurrentFileName := AFilename;
FIsLiveData := TMutable<Boolean>.CreateWriteable(false);
end;
procedure TAuraFileServer<T>.AfterConstruction;
begin
inherited;
// Asynchronously loads the initial data file.
FCurrentData := TDataSeries<T>.LoadDataFile(FCurrentFileName);
FCurrPosInFile := 0;
FCurrentIdx := 0;
// Initially sets the server to "not live data".
FIsLiveData.SetValue(false);
FLastTimeStamp := 0;
// Determines and asynchronously loads the next chronological file, if available.
FNextFileName := FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
FNextData := TDataSeries<T>.LoadDataFile(FNextFileName);
end;
function TAuraFileServer<T>.GetChunk(var Data: array of TDataPoint<T>): Integer;
begin
Result := 0;
// Checks if the current data file is ready.
if FCurrentData.Done.IsSet then
begin
var maxLen := Length(Data);
var currData := FCurrentData.Value;
while Result < MaxLen do
begin
if FCurrPosInFile >= Length(currData) then
begin
// Transition to the next file if the current one is exhausted.
FCurrentData := FNextData;
FCurrentFileName := FNextFileName;
FCurrPosInFile := 0;
FNextFileName := '';
FNextData := TFuture<TArray<TDataSeries<T>.TDataPoint>>.Null;
if FCurrentFileName <> '' then
begin
// Loads the next file in the series.
FNextFileName := FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
FNextData := TDataSeries<T>.LoadDataFile(FNextFileName);
// If the newly assigned current data is already loaded, continue processing.
if FCurrentData.Done.IsSet then
begin
currData := FCurrentData.Value;
continue;
end;
end
else
begin
// All historical files processed; transition to "live data" mode.
FIsLiveData.SetValue(true);
end;
break; // Exit loop if no more data or transition occurred.
end;
// Processes data point if its timestamp is strictly greater than the last processed one.
// This ensures chronological order and handles potential duplicate timestamps.
var item := currData[FCurrPosInFile];
if FLastTimeStamp < item.TimeStamp then
begin
FLastTimeStamp := item.TimeStamp;
Data[Result].Create(FCurrentIdx, FLastTimeStamp, item.Data);
inc(Result);
inc(FCurrentIdx);
end;
inc(FCurrPosInFile);
end;
end;
end;
function TAuraFileServer<T>.GetIsLiveData: TMutable<Boolean>;
begin
Result := FIsLiveData;
end;
end.
+13 -116
View File
@@ -4,152 +4,49 @@ interface
uses
System.SysUtils,
System.Generics.Collections,
Myc.Lazy,
Myc.Trade.DataPoint,
Myc.Trade.DataSeries,
Myc.Signals, // Added for TLatch
Myc.Futures; // Added for TFuture
Myc.Signals,
Myc.Futures;
type
// Represents a generic data server capable of providing sequential data chunks.
// The type parameter T specifies the type of data points served.
IDataServer<T> = interface
['{A6E246A2-E84E-49AB-A63E-333E561E488C}']
// Provides a TMutable<Boolean> that indicates if the server is currently
// serving live data (true) or historical data (false).
function GetIsLiveData: TMutable<Boolean>;
// Retrieves a chunk of data into the provided dynamic array.
// Returns the number of items successfully read into the array.
function GetChunk(var Data: array of T): Integer;
property IsLiveData: TMutable<Boolean> read GetIsLiveData;
end;
// Abstract base class for IDataServer implementations.
TDataServer<T> = class(TInterfacedObject, IDataServer<T>)
protected
function GetIsLiveData: TMutable<Boolean>; virtual; abstract;
function GetChunk(var Data: array of T): Integer; virtual; abstract;
end;
// Represents a factory for creating IDataServer instances.
// The type parameter T specifies the type of data points the created server will provide.
IDataServerNode<T> = interface
['{DA3531F1-158E-4A63-8E5A-34089C537B1B}']
// Creates and returns a new instance of an IDataServer.
function CreateDataServer: IDataServer<T>;
end;
// Abstract base class for IDataServerNode implementations.
TDataServerNode<T> = class(TInterfacedObject, IDataServerNode<T>)
public
// Factory method to create the actual data server instance.
function CreateDataServer: IDataServer<T>; virtual; abstract;
end;
TTABFileServer = class(TDataServer<TDataPoint<TAskBidItem>>)
private
FFilename: String;
FIsLiveData: TMutable<Boolean>.IWriteable;
// Internal state
FCurrentFileName: string;
FCurrentData: TFuture<TArray<TAskBid.TTick>>;
FNextFileName: string;
FNextData: TFuture<TArray<TAskBid.TTick>>;
FCurrPosInFile: Int64;
FCurrentIdx: Int64;
FLastTimeStamp: TDateTime;
protected
function GetChunk(var Data: array of TDataPoint<TAskBidItem>): Integer; override;
function GetIsLiveData: TMutable<Boolean>; override;
public
constructor Create(const AFilename: String);
procedure AfterConstruction; override;
end;
implementation
uses
System.IOUtils;
{ TTABFileServer<T> }
constructor TTABFileServer.Create(const AFilename: String);
begin
inherited Create;
FFilename := AFilename;
FNextData := TFuture<TArray<TAskBid.TTick>>.Null;
FIsLiveData := TMutable<Boolean>.CreateWriteable(false);
end;
procedure TTABFileServer.AfterConstruction;
begin
inherited;
FCurrentFileName := FFilename;
FCurrentData := TAskBid.LoadDataFile(FCurrentFileName);
FCurrPosInFile := 0;
FCurrentIdx := 0;
FIsLiveData.SetValue(false);
FLastTimeStamp := 0;
FFilename := '';
FNextFileName := FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
FNextData := TAskBid.LoadDataFile(FNextFileName);
exit;
end;
function TTABFileServer.GetChunk(var Data: array of TDataPoint<TAskBidItem>): Integer;
begin
Result := 0;
if FCurrentData.Done.IsSet then
begin
var maxLen := Length(Data);
var currData := FCurrentData.Value;
while Result < MaxLen do
begin
if FCurrPosInFile >= Length(currData) then
begin
// Next File!
FCurrentData := FNextData;
FCurrentFileName := FNextFileName;
FCurrPosInFile := 0;
FNextFileName := '';
FNextData := TFuture<TArray<TAskBid.TTick>>.Null;
if FCurrentFileName <> '' then
begin
FNextFileName := FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
FNextData := TAskBid.LoadDataFile(FNextFileName);
if FCurrentData.Done.IsSet then
begin
currData := FCurrentData.Value;
continue;
end;
end
else
begin
// We're done with this series
FIsLiveData.SetValue(true);
end;
break;
end;
var item := currData[FCurrPosInFile];
if FLastTimeStamp < item.TimeStamp then
begin
FLastTimeStamp := item.TimeStamp;
Data[Result].Create(FCurrentIdx, FLastTimeStamp, item.Data);
inc(Result);
inc(FCurrentIdx);
end;
inc(FCurrPosInFile);
end;
end;
end;
function TTABFileServer.GetIsLiveData: TMutable<Boolean>;
begin
Result := FIsLiveData;
end;
end.