Refactoring
This commit is contained in:
@@ -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,65 +50,68 @@ 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;
|
||||
|
||||
procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries;
|
||||
var
|
||||
chunk: array[0..C_MAX_FETCH-1] of TDataPoint<TAskBidItem>;
|
||||
chunk: array[0..C_MAX_FETCH - 1] of TDataPoint<TAskBidItem>;
|
||||
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 ) );
|
||||
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);
|
||||
if n > 0 then
|
||||
Move( chunk[0], dst[cnt], n * sizeof( TDataPoint<TAskBidItem> ) );
|
||||
inc( cnt, n );
|
||||
Move(chunk[0], dst[cnt], n * sizeof(TDataPoint<TAskBidItem>));
|
||||
inc(cnt, n);
|
||||
end;
|
||||
|
||||
// --- 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].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');
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
// Registers the test fixture with DUnitX.
|
||||
TDUnitX.RegisterTestFixture(TTest_TABFileServer_Equivalence);
|
||||
end.
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user