Refactoring DataStream

This commit is contained in:
Michael Schimmel
2025-06-07 16:41:46 +02:00
parent 590e98d614
commit 776067d0a8
9 changed files with 708 additions and 1028 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ Code-Generierung
- Einrückung mit 4 Leerzeichen anstelle von 2. Auch bei Kommentaren. - Einrückung mit 4 Leerzeichen anstelle von 2. Auch bei Kommentaren.
- Das Code-Format ist UTF-8. - Das Code-Format ist UTF-8.
- Folgende Schlüsselwörter müssen klein geschrieben werden: - Folgende Schlüsselwörter müssen klein geschrieben werden:
and, or, not, mod, div, in, as, is, array of, sizeof and, or, not, mod, div, in, as, is, array of, sizeof(), inc(), dec()
* Ändere niemals vorhandene Bezeichner im vom Benutzer bereitgestellten Code, es sei denn du wirst dazu aufgefordert. * Ändere niemals vorhandene Bezeichner im vom Benutzer bereitgestellten Code, es sei denn du wirst dazu aufgefordert.
-172
View File
@@ -1,172 +0,0 @@
unit Myc.Core.DataCache;
interface
uses
System.SysUtils,
System.Generics.Collections,
System.Generics.Defaults,
Myc.Futures, // Required for TFuture
Myc.Signals; // Required for TLatch
type
// Represents a single cached file entry with its metadata and loaded data future.
// TArrayItemType is the type of a single item within the TArray that is loaded by the future.
TCachedFile<TArrayItemType> = class
Name: String; // The full path and name of the cached file.
Age: TDateTime; // The modification timestamp of the file at the time of caching.
LastUsed: TDateTime; // The last time this cached entry was accessed.
Data: TFuture<TArray<TArrayItemType>>; // A future representing the asynchronously loaded data.
end;
// Generic in-memory cache for data files, supporting asynchronous loading
// and eviction based on memory pressure and last usage.
// TArrayItemType is the type of a single item within the TArray that is loaded by the future,
// e.g., TDataPoint<TAskBidItem>.
TDataCache<TArrayItemType> = class
strict private
FCachedFiles: TDictionary<String, TCachedFile<TArrayItemType>>;
FLoadGate: TLatch;
FIsMemoryLow: TFunc<Boolean>;
private
// Manages cache size by evicting least recently used items if memory is low.
procedure ManageCacheSize(const CurrentFileNameBeingLoaded: string);
public
constructor Create(const ALoadGate: TLatch; const AIsMemoryLow: TFunc<Boolean>);
destructor Destroy; override;
// Clears all entries from the cache.
procedure Clear;
// Attempts to retrieve data for a given file from the cache.
// If found and not stale, updates LastUsed and returns true.
// Otherwise, returns false.
function TryGetValue(const FileName: string; out ACachedFile: TCachedFile<TArrayItemType>): Boolean;
// Adds a new or updates an existing entry in the cache.
procedure AddOrUpdate(const FileName: string; const AAge: TDateTime; const AData: TFuture<TArray<TArrayItemType>>);
// Returns the TLoadGate associated with this cache, for coordination of load operations.
function GetLoadGate: TLatch; inline;
property LoadGate: TLatch read GetLoadGate;
end;
implementation
uses
System.Math;
{ TDataCache<TArrayItemType> }
constructor TDataCache<TArrayItemType>.Create(const ALoadGate: TLatch; const AIsMemoryLow: TFunc<Boolean>);
begin
inherited Create;
FCachedFiles := TObjectDictionary<String, TCachedFile<TArrayItemType>>.Create([doOwnsValues]);
FLoadGate := ALoadGate;
FIsMemoryLow := AIsMemoryLow;
end;
destructor TDataCache<TArrayItemType>.Destroy;
begin
FCachedFiles.Free;
inherited;
end;
procedure TDataCache<TArrayItemType>.Clear;
begin
FCachedFiles.Clear;
end;
procedure TDataCache<TArrayItemType>.AddOrUpdate(
const FileName: string;
const AAge: TDateTime;
const AData: TFuture<TArray<TArrayItemType>>
);
var
cachedFile: TCachedFile<TArrayItemType>;
begin
if FCachedFiles.TryGetValue(FileName, cachedFile) then
begin
// Update existing entry
cachedFile.Age := AAge;
cachedFile.Data := AData; // This will replace the old future, potentially freeing old data
cachedFile.LastUsed := Now;
end
else
begin
// Add new entry
cachedFile := TCachedFile<TArrayItemType>.Create;
cachedFile.Name := FileName;
cachedFile.Age := AAge;
cachedFile.Data := AData;
cachedFile.LastUsed := Now;
FCachedFiles.Add(FileName, cachedFile);
end;
end;
function TDataCache<TArrayItemType>.GetLoadGate: TLatch;
begin
Result := FLoadGate;
end;
procedure TDataCache<TArrayItemType>.ManageCacheSize(const CurrentFileNameBeingLoaded: string);
begin
if Assigned(FIsMemoryLow) and FIsMemoryLow() then
begin
var fL := FCachedFiles.Values.ToArray;
// Sorts the cached files by LastUsed in ascending order (least recently used first).
TArray.Sort<TCachedFile<TArrayItemType>>(
fL,
TComparer<TCachedFile<TArrayItemType>>.Construct(
function(const Left, Right: TCachedFile<TArrayItemType>): Integer begin Result := Sign(Left.LastUsed - Right.LastUsed) end
)
);
for var i := 0 to High(fL) do
begin
if not FIsMemoryLow() then // Stop evicting if memory pressure is relieved
break;
// Do not remove the file that is currently being loaded.
if fL[i].Name <> CurrentFileNameBeingLoaded then
FCachedFiles.Remove(fL[i].Name);
end;
end;
end;
function TDataCache<TArrayItemType>.TryGetValue(const FileName: string; out ACachedFile: TCachedFile<TArrayItemType>): Boolean;
var
age: TDateTime;
begin
ManageCacheSize( FileName );
Result := FCachedFiles.TryGetValue(FileName, ACachedFile);
if Result then
begin
// Check if the file on disk has changed since it was cached.
if FileAge(FileName, age, true) then
begin
if age = ACachedFile.Age then
begin
// Cache hit and not stale, update LastUsed.
ACachedFile.LastUsed := Now;
end
else
begin
// File is stale, remove from cache.
FCachedFiles.Remove(FileName);
Result := False;
end;
end
else
begin
// File no longer exists, remove from cache.
FCachedFiles.Remove(FileName);
Result := False;
end;
end;
end;
end.
+1 -1
View File
@@ -22,7 +22,7 @@ type
property Done: TState read GetDone; property Done: TState read GetDone;
end; end;
TFuncConst<T, TResult> = reference to function(const Arg1: T): TResult; TFuncConst<S, TResult> = reference to function(const Arg1: S): TResult;
{$REGION 'private'} {$REGION 'private'}
strict private strict private
+13 -10
View File
@@ -10,8 +10,7 @@ uses
Myc.Lazy, Myc.Lazy,
Myc.Futures, Myc.Futures,
Myc.Trade.DataPoint, Myc.Trade.DataPoint,
Myc.Trade.DataSeries, Myc.Trade.DataStream;
Myc.Trade.DataServer;
type type
// Test fixture for TDataServer and TAuraTABFileServer, focusing on data equivalence // Test fixture for TDataServer and TAuraTABFileServer, focusing on data equivalence
@@ -20,7 +19,8 @@ type
[IgnoreMemoryLeaks(true)] [IgnoreMemoryLeaks(true)]
TTest_TABFileServer_Equivalence = class(TObject) TTest_TABFileServer_Equivalence = class(TObject)
private private
FServer: IDataServer<TDataPoint<TAskBidItem>>; FServer: IDataServer<TAskBidItem>;
FStream: IDataStream<TDataPoint<TAskBidItem>>;
public public
[SetupFixture] [SetupFixture]
procedure SetupFixture; procedure SetupFixture;
@@ -50,12 +50,15 @@ const
procedure TTest_TABFileServer_Equivalence.SetupFixture; procedure TTest_TABFileServer_Equivalence.SetupFixture;
begin begin
FServer := TAskBidServer.Create;
// TAskBid.ClearCache ensures a clean state for data series loading. // TAskBid.ClearCache ensures a clean state for data series loading.
TAskBid.ClearCache; FServer.ClearCache;
end; end;
procedure TTest_TABFileServer_Equivalence.TearDownFixture; procedure TTest_TABFileServer_Equivalence.TearDownFixture;
begin begin
FServer := nil;
// Resets the global IsMemoryLow function pointer, if it was set for testing. // Resets the global IsMemoryLow function pointer, if it was set for testing.
IsMemoryLow := nil; IsMemoryLow := nil;
end; end;
@@ -63,15 +66,15 @@ end;
procedure TTest_TABFileServer_Equivalence.Setup; procedure TTest_TABFileServer_Equivalence.Setup;
begin begin
// Initializes the data server with a specific test file. // Initializes the data server with a specific test file.
FServer := TAuraTABFileServer.Create(C_TEST_FILENAME); FStream := TAuraTABFileStream.Create(C_TEST_FILENAME, FServer);
end; end;
procedure TTest_TABFileServer_Equivalence.TearDown; procedure TTest_TABFileServer_Equivalence.TearDown;
begin begin
// Releases the data server instance. // Releases the data server instance.
FServer := nil; FStream := nil;
// Clears any cached data to prevent interference with subsequent tests. // Clears any cached data to prevent interference with subsequent tests.
TAskBid.ClearCache; FServer.ClearCache;
end; end;
procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries; procedure TTest_TABFileServer_Equivalence.Test_ServerReturnsSameDataAs_LoadDataSeries;
@@ -81,15 +84,15 @@ var
i: Int64; i: Int64;
begin begin
// Loads the expected data directly using TAskBid.LoadDataSeries.WaitFor. // Loads the expected data directly using TAskBid.LoadDataSeries.WaitFor.
var ExpectedData := TAskBid.LoadDataSeries(C_TEST_FILENAME).WaitFor; var ExpectedData := FServer.LoadDataSeries(C_TEST_FILENAME).WaitFor;
SetLength(Dst, Length(ExpectedData)); SetLength(Dst, Length(ExpectedData));
var cnt: Int64 := 0; var cnt: Int64 := 0;
var n: Integer; var n: Integer;
// Fetches data chunks from the server until all data is retrieved or server indicates live data. // 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 while not FStream.IsLiveData.Value and (cnt < Length(Dst)) do
begin begin
n := FServer.GetChunk(chunk); n := FStream.GetChunk(chunk);
if n > 0 then if n > 0 then
Move(chunk[0], dst[cnt], n * sizeof(TDataPoint<TAskBidItem>)); Move(chunk[0], dst[cnt], n * sizeof(TDataPoint<TAskBidItem>));
inc(cnt, n); inc(cnt, n);
-757
View File
@@ -1,757 +0,0 @@
unit Myc.Trade.DataSeries;
(*
@unit Myc.Trade.DataSeries
@brief Provides functionality to load time-series data from binary files.
This unit offers a generic class `TDataSeries<T>` and helper functions to handle
loading and processing of sequential data files, which are often used for
historical market data.
**File Naming Convention**
The functions in this unit expect files to follow a specific naming pattern:
- Historical data: `Symbol_YYYY_MM.tab` (uncompressed) or `Symbol_YYYY_MM.tab_zip` (compressed)
- Live/update data: `Symbol_YYYY_MM.tab-live` (never compressed)
`YYYY` represents the four-digit year and `MM` the two-digit month.
If both a compressed (`_zip`) and an uncompressed version of a `.tab` file exist,
the compressed version is always preferred.
**Core Functionality**
- `TDataSeries<T>.LoadDataSeries`: The main entry point to load a complete series.
Given a starting file, it discovers all subsequent chronological `.tab` files by
calling `FindNextDataFile`. It then looks for a corresponding `.tab-live` file
for the last month in the series. All discovered files are loaded asynchronously
and merged into a single, chronologically sorted array of unique data points.
- `TDataSeries<T>.LoadDataFile`: Loads a single data file asynchronously. This
function includes a caching mechanism to avoid rereading unchanged files.
**Helper Functions**
- `FindNextDataFile`: Given a filename, it finds and returns the path to the
next file in the chronological sequence, if it exists.
- `FindOldestFilesPerSymbol`: Scans a directory and finds the oldest (first)
data file for each symbol based on year and month.
- `TryParseFileName`: A utility function to parse filenames that adhere to the
specified naming convention.
*)
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.Generics.Defaults,
System.IOUtils,
Myc.Futures,
Myc.Signals,
Myc.Lazy,
Myc.Trade.DataPoint,
Myc.Trade.DataServer;
type
// Generic class for loading and managing sequential time-series data.
TDataSeries<T: record> = class
public
type
// Represents a single data point with a timestamp and generic data.
TDataPoint = packed record
TimeStamp: TDateTime;
Data: T;
end;
strict private
type
TCachedFile = class
Name: String;
Age: TDateTime;
LastUsed: TDateTime;
Data: TFuture<TArray<TDataPoint>>;
end;
class var
FCachedFiles: TDictionary<String, TCachedFile>;
FLoadGate: TLatch;
class constructor CreateClass;
class destructor DestroyClass;
protected
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint>; static;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint>; static;
public
class procedure ClearCache;
// Asynchronously loads a single data file with caching support.
class function LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint>>; static;
// Asynchronously loads a complete chronological series of data files.
class function LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint>>; static;
end;
// A specialized TDataSeries for handling Ask/Bid tick data.
TAskBid = class(TDataSeries<TAskBidItem>)
public
type
// Represents a single tick with Ask/Bid data.
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;
// Scans a directory and finds the oldest data file for each symbol.
function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
// Finds the next chronological file in a data series, if it exists.
function FindNextDataFile(const FileName: string): string;
var
// A function pointer to a callback that indicates if the system is low on memory.
IsMemoryLow: TFunc<Boolean>;
implementation
uses
System.Zip,
System.Math,
Myc.TaskManager;
// TryParseFileName parses filenames like "Symbol_Year_MM.Extension" or "Symbol_Year_MM.Extension_zip"
// to extract Path, Symbol, Year, and Month.
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue, MonthValue: Integer): Boolean;
var
fileNameNoPath: string;
nameForParsing: string;
parts: TArray<string>;
baseName: string;
begin
Result := False; // Default to failure
PathValue := ''; // Initialize all out parameters
SymbolValue := '';
YearValue := 0;
MonthValue := 0;
PathValue := TPath.GetDirectoryName(FileName);
fileNameNoPath := TPath.GetFileName(FileName);
nameForParsing := fileNameNoPath;
// Handle cases like "Symbol_Year_MM.Extension_zip" by removing the trailing "_zip" before extension removal
if nameForParsing.EndsWith('_zip', True) then
begin
// Get the part before "_zip"
baseName := nameForParsing.Substring(0, nameForParsing.Length - 4);
// Get the extension of the original name (e.g. ".csv_zip")
// and check if baseName still contains an extension that needs to be removed by GetFileNameWithoutExtension
if TPath.GetExtension(baseName) <> '' then
begin
nameForParsing := baseName;
end
else
begin
// If baseName (e.g. "EURUSD_2023_01") has no extension, it is the correct parsing base
// However, TPath.GetFileNameWithoutExtension would also work correctly here,
// but this handles a potential case where a file might be named "Symbol_Year_MM_zip" (no dot before zip)
end;
end;
// Remove the actual extension (e.g., ".csv", ".dat") from "Symbol_Year_MM.Extension" or the modified "Symbol_Year_MM.Extension" part of "_zip"
nameForParsing := TPath.GetFileNameWithoutExtension(nameForParsing); // e.g., "EURUSD_2023_01"
parts := nameForParsing.Split(['_']);
if Length(parts) < 3 then // Need at least Symbol_Year_Month
exit;
// Month is the last part.
if not TryStrToInt(parts[High(parts)], MonthValue) then
begin
exit;
end;
if (MonthValue < 1) or (MonthValue > 12) then // Validate month range
begin
MonthValue := 0; // Set to invalid if out of range
exit;
end;
// Year is the second to last part.
if not TryStrToInt(parts[High(parts) - 1], YearValue) then
begin
MonthValue := 0; // Also invalidate month if year parsing fails
exit;
end;
// Ensure YearValue is plausible, e.g. not 0 or negative, adjust as needed for your data's year range
if YearValue <= 0 then
begin
YearValue := 0;
MonthValue := 0;
exit;
end;
// Symbol consists of all parts before Year and Month.
SymbolValue := string.Join('_', Copy(parts, 0, Length(parts) - 2));
if SymbolValue = '' then // Ensure symbol is not empty
begin
// Reset all out parameters on failure before exit
PathValue := '';
YearValue := 0;
MonthValue := 0;
exit;
end;
Result := True;
end;
// Finds the oldest file for each symbol in the given directory.
// The file extension is ignored for the purpose of identifying the symbol, year, and month.
function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
type
TTrackedFileInfo = record
FullFileName: string;
Year: Integer;
Month: Integer;
end;
var
oldestFilesPerSymbol: TDictionary<string, TTrackedFileInfo>;
fileNames: TArray<string>;
currentFile: string;
parsedPath: string;
parsedSymbol: string;
parsedYear: Integer;
parsedMonth: Integer;
trackedInfo: TTrackedFileInfo;
begin
oldestFilesPerSymbol := TDictionary<string, TTrackedFileInfo>.Create; // Dictionary to track the oldest file per symbol
try
if not TDirectory.Exists(DirectoryPath) then
begin
// Or raise an exception, depending on desired error handling
exit;
end;
fileNames := TDirectory.GetFiles(DirectoryPath); // Get all files in the directory
for currentFile in fileNames do
begin
// Attempt to parse the file name
if TryParseFileName(currentFile, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
begin
// Check if this symbol is already tracked or if this file is older
if oldestFilesPerSymbol.TryGetValue(parsedSymbol, trackedInfo) then
begin
// If current file's year is less, or year is same and month is less, it's older
if (parsedYear < trackedInfo.Year) or ((parsedYear = trackedInfo.Year) and (parsedMonth < trackedInfo.Month)) then
begin
trackedInfo.FullFileName := currentFile; // Store full path to the file
trackedInfo.Year := parsedYear;
trackedInfo.Month := parsedMonth;
oldestFilesPerSymbol.AddOrSetValue(parsedSymbol, trackedInfo); // Update the entry for this symbol
end;
end
else
begin
// First time we see this symbol, so it's the oldest so far
trackedInfo.FullFileName := currentFile; // Store full path to the file
trackedInfo.Year := parsedYear;
trackedInfo.Month := parsedMonth;
oldestFilesPerSymbol.Add(parsedSymbol, trackedInfo); // Add new entry for this symbol
end;
end;
end;
// Populate the result array with the full names of the oldest files
SetLength(Result, oldestFilesPerSymbol.Count);
var n := 0;
for trackedInfo in oldestFilesPerSymbol.Values do
begin
Result[n] := trackedInfo.FullFileName;
inc(n);
end;
finally
oldestFilesPerSymbol.Free; // Free the dictionary
end;
end;
function FindNextDataFile(const FileName: string): string;
var
pathValue, symbolValue: string;
yearValue, monthValue: Integer;
nextBaseName, compressedPath, uncompressedPath: string;
begin
// Parse the input filename to extract its components.
if not TryParseFileName(FileName, pathValue, symbolValue, yearValue, monthValue) then
begin
exit(''); // If parsing fails, there is no 'next' file.
end;
// Calculate the next month and year.
Inc(monthValue);
if monthValue > 12 then
begin
monthValue := 1;
Inc(yearValue);
end;
// Construct the base name for the next file.
nextBaseName := Format('%s_%.4d_%.2d.tab', [symbolValue, yearValue, monthValue]);
// Check for the existence of the next file, preferring the compressed version.
compressedPath := TPath.Combine(pathValue, nextBaseName + '_zip');
if TFile.Exists(compressedPath) then
begin
exit(compressedPath);
end;
uncompressedPath := TPath.Combine(pathValue, nextBaseName);
if TFile.Exists(uncompressedPath) then
begin
exit(uncompressedPath);
end;
// If no next file is found, return an empty string.
Result := '';
end;
class destructor TDataSeries<T>.CreateClass;
begin
FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
end;
class destructor TDataSeries<T>.DestroyClass;
begin
FCachedFiles.Free;
end;
class procedure TDataSeries<T>.ClearCache;
begin
FCachedFiles.Clear;
end;
// Asynchronously reads tick data from a single specified file.
// Returns a TFuture that will provide an array of TAskBidSeries.
class function TDataSeries<T>.LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint>>;
var
parsedPath, parsedSymbol: string;
parsedYear, parsedMonth: Integer;
begin
Result := TFuture<TArray<TDataPoint>>.Null;
// Attempt to parse year and month from the filename
if not TryParseFileName(FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
exit;
if Assigned(IsMemoryLow) then
begin
if IsMemoryLow then
begin
var fL := FCachedFiles.Values.ToArray;
TArray.Sort<TCachedFile>(
fL,
TComparer<TCachedFile>
.Construct(function(const Left, Right: TCachedFile): Integer begin Result := Sign(Left.LastUsed - Right.LastUsed); end)
);
for var i := 0 to High(fL) do
begin
if not IsMemoryLow then
break;
if fL[i].Name <> FileName then
FCachedFiles.Remove(fL[i].Name);
end;
end;
var cachedFile: TCachedFile;
if FCachedFiles.TryGetValue(Filename, cachedFile) then
begin
var age: TDateTime;
if FileAge(Filename, age, true) then
begin
if age = cachedFile.Age then
begin
cachedFile.LastUsed := Now;
exit(cachedFile.Data);
end
else
FCachedFiles.Remove(FileName);
end;
end;
end;
if FileName.EndsWith('_zip', True) then
begin
var capFileName := FileName;
Result :=
TFuture<TBytes>
.Construct(
TLatch.Enqueue(FLoadGate),
function: TBytes
begin
Result := TFile.ReadAllBytes(capFileName); // Read all bytes of the .zip file
end)
.Chain<TArray<TDataPoint>>(
function(bytes: TBytes): TArray<TDataPoint>
begin
var zipMemoryStream := TBytesStream.Create(bytes); // Create a memory stream to hold the zip file content
try
zipMemoryStream.Position := 0;
Result := ReadCompressedData(zipMemoryStream);
finally
zipMemoryStream.Free;
end;
end);
end
else if TFile.Exists(FileName) then
begin
var capFileName := FileName;
Result :=
TFuture<TArray<TDataPoint>>.Construct(
TLatch.Enqueue(FLoadGate),
function: TArray<TDataPoint>
begin
if TFile.Exists(capFileName) then
begin
var stream := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite);
try
try
Result := ReadUncompressedData(stream);
except
on E: EReadError do
begin
raise EReadError.CreateFmt('File %s: %s', [capFileName, E.Message]);
end;
end;
finally
stream.Free;
end;
end;
end
);
end;
if Assigned(IsMemoryLow) then
begin
var cachedFile := TCachedFile.Create;
cachedFile.Name := Filename;
if FileAge(Filename, cachedFile.Age, true) then
begin
cachedFile.Data := Result;
cachedFile.LastUsed := Now;
FCachedFiles.Add(Filename, cachedFile);
end;
end;
end;
class function TDataSeries<T>.LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint>>;
var
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataPoint>>>;
liveData: TFuture<TArray<TDataPoint>>;
tabFiles: TList<string>;
currentFile: string;
liveFilePath: string;
pathName, symbolName: string;
yearValue, monthValue: Integer;
LoadGate: TLatch;
begin
// 1. Discover all sequential .tab/.tab_zip files using the new helper function.
tabFiles := TList<string>.Create;
try
currentFile := FileName;
if TFile.Exists(currentFile) then
begin
// Start with the given file and find all subsequent files in the series.
while currentFile <> '' do
begin
tabFiles.Add(currentFile);
currentFile := FindNextDataFile(currentFile);
end;
end;
// 2. Determine the corresponding .tab-live file based on the last found .tab file.
liveFilePath := '';
if tabFiles.Count > 0 then
begin
var lastTabFile := tabFiles.Last;
if TryParseFileName(lastTabFile, pathName, symbolName, yearValue, monthValue) then
begin
var liveFileBaseName := Format('%s_%d_%02d.tab-live', [symbolName, yearValue, monthValue]);
var potentialLivePath := TPath.Combine(pathName, liveFileBaseName);
if TFile.Exists(potentialLivePath) then
begin
liveFilePath := potentialLivePath;
end;
end;
end;
// 3. Asynchronously load all discovered files.
var loadedFileList := TList<TFuture<TArray<TDataPoint>>>.Create;
var loadStates := TList<TState>.Create;
try
// Create load tasks for the historical .tab files.
for currentFile in tabFiles do
begin
var data := LoadDataFile(currentFile);
loadedFileList.Add(data);
loadStates.Add(data.Done);
end;
// Create a load task for the .tab-live file, if it exists.
liveData := TFuture<TArray<TDataPoint>>.Null;
if liveFilePath <> '' then
begin
liveData := LoadDataFile(liveFilePath);
loadedFileList.Add(liveData);
loadStates.Add(liveData.Done);
end;
loadedState := TState.All(loadStates.ToArray);
loadedFiles := loadedFileList.ToArray;
finally
loadStates.Free;
loadedFileList.Free;
end;
finally
tabFiles.Free;
end;
// 4. Construct the final future that merges the results. This logic remains unchanged.
Result :=
TFuture<TArray<TDataPoint>>.Construct(
loadedState,
function: TArray<TDataPoint>
begin
var tickList := TList<TDataPoint>.Create;
try
var totalTicks := Length(liveData.Value);
var overallLastTabTickTime: TDateTime := 0;
for var P in loadedFiles do
Inc(totalTicks, Length(P.Value));
tickList.Capacity := totalTicks;
for var P in loadedFiles do
begin
for var iterTickData in P.Value do
if overallLastTabTickTime < iterTickData.TimeStamp then
begin
tickList.Add(iterTickData);
overallLastTabTickTime := iterTickData.TimeStamp;
end;
end;
Result := tickList.ToArray;
finally
tickList.Free;
end;
end
);
end;
class function TDataSeries<T>.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint>;
var
decompressionStream: TStream; // Holds uncompressed data of a single zip entry
localHeader: TZipHeader;
entryIndex: Integer;
i: Integer;
zipFileInstance: TZipFile;
begin
// Initialize Result record
SetLength(Result, 0);
decompressionStream := nil;
zipFileInstance := nil;
try
InputStream.Position := 0; // Reset stream position to the beginning for TZipFile
// Open TZipFile from this memory stream
zipFileInstance := TZipFile.Create;
zipFileInstance.Open(InputStream, TZipMode.zmRead); // Open the zip from the memory stream
if zipFileInstance.FileCount = 0 then
begin
// If TZipFile opens an empty or invalid stream successfully but finds no files
exit; // FileSuccessfullyLoaded remains False
end;
entryIndex := -1;
for i := 0 to zipFileInstance.FileCount - 1 do
begin
if zipFileInstance.FileNames[i].EndsWith('.tab', True) then
begin
entryIndex := i;
break;
end;
end;
if entryIndex = -1 then // If specific name not found, default to the first entry
begin
if zipFileInstance.FileCount > 0 then
entryIndex := 0
else
exit; // Should not happen if FileCount > 0 check passed
end;
// Decompress the specific entry from the in-memory zip file.
// TZipFile.Read creates and populates decompressionStream with uncompressed data.
zipFileInstance.Read(entryIndex, decompressionStream, localHeader);
if not Assigned(decompressionStream) then
exit; // Exit if no valid stream could be prepared
Result := ReadUncompressedData(decompressionStream);
finally
// Ensure all potentially created objects are freed
if Assigned(decompressionStream) then // Holds uncompressed data from a zip entry
decompressionStream.Free;
if Assigned(zipFileInstance) then
zipFileInstance.Free;
end;
end;
class function TDataSeries<T>.ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint>;
var
fileSize: Int64;
recordCount: Integer;
bytesRead: Integer;
begin
// Initialize Result record
SetLength(Result, 0);
InputStream.Position := 0;
fileSize := InputStream.Size;
if fileSize = 0 then
exit;
if (fileSize mod SizeOf(TDataPoint)) <> 0 then
raise EReadError.CreateFmt('File corrupted. Size %d is not a multiple of record size %d.', [fileSize, SizeOf(TDataPoint)]);
recordCount := fileSize div SizeOf(TDataPoint);
if recordCount > 0 then
begin
SetLength(Result, recordCount);
bytesRead := InputStream.Read(Result[0], fileSize);
if bytesRead <> fileSize then
raise EReadError.CreateFmt('Read error. Expected to read %d bytes, but actually read %d bytes.', [fileSize, bytesRead]);
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.
-52
View File
@@ -1,52 +0,0 @@
unit Myc.Trade.DataServer;
interface
uses
System.SysUtils,
Myc.Lazy,
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;
implementation
uses
System.IOUtils;
end.
+654
View File
@@ -0,0 +1,654 @@
unit Myc.Trade.DataStream;
(*
Myc.Trade.DataStream
Provides a data server for loading time-series data and streaming it.
This unit contains the core components for handling historical data:
- TDataRecord: A generic record for a single time-stamped data entry.
- IDataServer/TDataServer: A server component responsible for loading and caching
series of data files from disk. Each server instance manages its own cache,
but all instances share a central load gate.
- IDataStream/TDataStream: An interface representing a stream of data points that
can be consumed sequentially.
- TAuraFileStream: A concrete implementation of IDataStream that is fed by an
IDataServer instance.
*)
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.Generics.Defaults,
System.IOUtils,
Myc.Futures,
Myc.Signals,
Myc.Lazy,
Myc.Trade.DataPoint;
type
// A generic record for a single time-stamped data entry.
TDataRecord<T: record> = packed record
TimeStamp: TDateTime;
Data: T;
end;
// Represents a generic data stream capable of providing sequential data chunks.
IDataStream<T> = interface
['{A6E246A2-E84E-49AB-A63E-333E561E488C}']
function GetIsLiveData: TMutable<Boolean>;
function GetChunk(var Data: array of T): Integer;
property IsLiveData: TMutable<Boolean> read GetIsLiveData;
end;
// Abstract base class for IDataStream implementations.
TDataStream<T> = class(TInterfacedObject, IDataStream<T>)
protected
function GetIsLiveData: TMutable<Boolean>; virtual; abstract;
function GetChunk(var Data: array of T): Integer; virtual; abstract;
end;
// Represents a factory for creating IDataStream instances.
IDataStreamNode<T> = interface
['{DA3531F1-158E-4A63-8E5A-34089C537B1B}']
function CreateDataStream: IDataStream<T>;
end;
// Abstract base class for IDataStreamNode implementations.
TDataStreamNode<T> = class(TInterfacedObject, IDataStreamNode<T>)
public
function CreateDataStream: IDataStream<T>; virtual; abstract;
end;
// Interface for an instantiable data server.
IDataServer<T: record> = interface
['{1F8E5A9D-E92A-44C1-9F3F-C4B82A6E94B3}']
function LoadDataFile(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
procedure ClearCache;
end;
// Generic server for loading and managing sequential time-series data from files.
TDataServer<T: record> = class(TInterfacedObject, IDataServer<T>)
strict private
type
TCachedFile = class
Name: String;
Age: TDateTime;
LastUsed: TDateTime;
Data: TFuture<TArray<TDataRecord<T>>>;
end;
private
// The cache is per-instance.
FCachedFiles: TDictionary<String, TCachedFile>;
strict private
// The load gate is shared across all instances of TDataServer.
class var FLoadGate: TLatch;
protected
class function ReadCompressedData(const InputStream: TStream): TArray<TDataRecord<T>>; static;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataRecord<T>>; static;
public
constructor Create;
destructor Destroy; override;
// IDataServer<T> implementation
procedure ClearCache;
function LoadDataFile(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
end;
// A specialized TDataServer for handling Ask/Bid tick data.
TAskBidServer = class(TDataServer<TAskBidItem>)
end;
// Implements a data stream that reads data from Aura-specific historical data files.
TAuraFileStream<T: record> = class(TDataStream<TDataPoint<T>>, IDataStream<TDataPoint<T>>)
private
FDataServer: IDataServer<T>;
FIsLiveData: TMutable<Boolean>.IWriteable;
FCurrentFileName: string;
FCurrentData: TFuture<TArray<TDataRecord<T>>>;
FNextFileName: string;
FNextData: TFuture<TArray<TDataRecord<T>>>;
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; const ADataServer: IDataServer<T>);
procedure AfterConstruction; override;
end;
// Specialized TAuraFileStream for TAskBidItem data.
TAuraTABFileStream = TAuraFileStream<TAskBidItem>;
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue, MonthValue: Integer): Boolean;
function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
function FindNextDataFile(const FileName: string): string;
var
IsMemoryLow: TFunc<Boolean>;
implementation
uses
System.Zip,
System.Math,
Myc.TaskManager;
constructor TDataServer<T>.Create;
begin
inherited Create;
// Initialize the per-instance cache.
FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
end;
destructor TDataServer<T>.Destroy;
begin
FCachedFiles.Free;
inherited Destroy;
end;
procedure TDataServer<T>.ClearCache;
begin
FCachedFiles.Clear;
end;
function TDataServer<T>.LoadDataFile(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
var
parsedPath, parsedSymbol: string;
parsedYear, parsedMonth: Integer;
begin
Result := TFuture<TArray<TDataRecord<T>>>.Null;
if not TryParseFileName(FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
exit;
if Assigned(IsMemoryLow) then
begin
if IsMemoryLow() then
begin
var fL := FCachedFiles.Values.ToArray;
TArray.Sort<TCachedFile>(fL,
TComparer<TCachedFile>.Construct(function(const Left, Right: TCachedFile): Integer
begin Result := Sign(Left.LastUsed - Right.LastUsed); end)
);
for var i := 0 to High(fL) do
begin
if not IsMemoryLow() then
break;
if fL[i].Name <> FileName then
FCachedFiles.Remove(fL[i].Name);
end;
end;
var cachedFile: TCachedFile;
if FCachedFiles.TryGetValue(Filename, cachedFile) then
begin
var age: TDateTime;
if FileAge(Filename, age, true) then
begin
if age = cachedFile.Age then
begin
cachedFile.LastUsed := Now;
exit(cachedFile.Data);
end
else
FCachedFiles.Remove(FileName);
end;
end;
end;
var capFileName := FileName;
if FileName.EndsWith('_zip', True) then
begin
Result := TFuture<TBytes>.Construct(
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
function: TBytes
begin
Result := TFile.ReadAllBytes(capFileName);
end
).Chain<TArray<TDataRecord<T>>>(
function(bytes: TBytes): TArray<TDataRecord<T>>
begin
var zipMemoryStream := TBytesStream.Create(bytes);
try
zipMemoryStream.Position := 0;
Result := ReadCompressedData(zipMemoryStream);
finally
zipMemoryStream.Free;
end;
end
);
end
else if TFile.Exists(FileName) then
begin
Result := TFuture<TArray<TDataRecord<T>>>.Construct(
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
function: TArray<TDataRecord<T>>
begin
if TFile.Exists(capFileName) then
begin
var stream := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite);
try
try
Result := ReadUncompressedData(stream);
except
on E: EReadError do
raise EReadError.CreateFmt('File %s: %s', [capFileName, E.Message]);
end;
finally
stream.Free;
end;
end;
end
);
end;
if Assigned(IsMemoryLow) then
begin
var cachedFile := TCachedFile.Create;
cachedFile.Name := Filename;
if FileAge(Filename, cachedFile.Age, true) then
begin
cachedFile.Data := Result;
cachedFile.LastUsed := Now;
FCachedFiles.Add(Filename, cachedFile);
end;
end;
end;
function TDataServer<T>.LoadDataSeries(const FileName: string): TFuture<TArray<TDataRecord<T>>>;
var
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataRecord<T>>>>;
liveData: TFuture<TArray<TDataRecord<T>>>;
tabFiles: TList<string>;
currentFile: string;
liveFilePath: string;
pathName, symbolName: string;
yearValue, monthValue: Integer;
begin
tabFiles := TList<string>.Create;
try
currentFile := FileName;
if TFile.Exists(currentFile) then
while currentFile <> '' do
begin
tabFiles.Add(currentFile);
currentFile := FindNextDataFile(currentFile);
end;
liveFilePath := '';
if tabFiles.Count > 0 then
begin
var lastTabFile := tabFiles.Last;
if TryParseFileName(lastTabFile, pathName, symbolName, yearValue, monthValue) then
begin
var liveFileBaseName := Format('%s_%d_%02d.tab-live', [symbolName, yearValue, monthValue]);
var potentialLivePath := TPath.Combine(pathName, liveFileBaseName);
if TFile.Exists(potentialLivePath) then
liveFilePath := potentialLivePath;
end;
end;
var loadedFileList := TList<TFuture<TArray<TDataRecord<T>>>>.Create;
var loadStates := TList<TState>.Create;
try
for currentFile in tabFiles do
begin
var data := LoadDataFile(currentFile);
loadedFileList.Add(data);
loadStates.Add(data.Done);
end;
liveData := TFuture<TArray<TDataRecord<T>>>.Null;
if liveFilePath <> '' then
begin
liveData := LoadDataFile(liveFilePath);
loadedFileList.Add(liveData);
loadStates.Add(liveData.Done);
end;
loadedState := TState.All(loadStates.ToArray);
loadedFiles := loadedFileList.ToArray;
finally
loadStates.Free;
loadedFileList.Free;
end;
finally
tabFiles.Free;
end;
Result := TFuture<TArray<TDataRecord<T>>>.Construct(
loadedState,
function: TArray<TDataRecord<T>>
begin
var tickList := TList<TDataRecord<T>>.Create;
try
var overallLastTabTickTime: TDateTime := 0;
var cnt := 0;
for var P in loadedFiles do
inc(cnt, Length(P.Value));
tickList.Capacity := cnt;
for var P in loadedFiles do
for var iterTickData in P.Value do
if overallLastTabTickTime < iterTickData.TimeStamp then
begin
tickList.Add(iterTickData);
overallLastTabTickTime := iterTickData.TimeStamp;
end;
Result := tickList.ToArray;
finally
tickList.Free;
end;
end
);
end;
class function TDataServer<T>.ReadCompressedData(const InputStream: TStream): TArray<TDataRecord<T>>;
var
decompressionStream: TStream;
localHeader: TZipHeader;
entryIndex, i: Integer;
zipFileInstance: TZipFile;
begin
SetLength(Result, 0);
decompressionStream := nil;
zipFileInstance := nil;
try
InputStream.Position := 0;
zipFileInstance := TZipFile.Create;
zipFileInstance.Open(InputStream, TZipMode.zmRead);
if zipFileInstance.FileCount = 0 then
exit;
entryIndex := -1;
for i := 0 to zipFileInstance.FileCount - 1 do
if zipFileInstance.FileNames[i].EndsWith('.tab', True) then
begin
entryIndex := i;
break;
end;
if entryIndex = -1 then
entryIndex := 0;
zipFileInstance.Read(entryIndex, decompressionStream, localHeader);
if not Assigned(decompressionStream) then
exit;
Result := ReadUncompressedData(decompressionStream);
finally
decompressionStream.Free;
zipFileInstance.Free;
end;
end;
class function TDataServer<T>.ReadUncompressedData(const InputStream: TStream): TArray<TDataRecord<T>>;
var
fileSize: Int64;
recordCount, bytesRead: Integer;
begin
SetLength(Result, 0);
InputStream.Position := 0;
fileSize := InputStream.Size;
if (fileSize = 0) or ((fileSize mod SizeOf(TDataRecord<T>)) <> 0) then
exit;
recordCount := fileSize div SizeOf(TDataRecord<T>);
if recordCount > 0 then
begin
SetLength(Result, recordCount);
bytesRead := InputStream.Read(Result[0], fileSize);
if bytesRead <> fileSize then
raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [fileSize, bytesRead]);
end;
end;
{ TAuraFileStream<T> }
constructor TAuraFileStream<T>.Create(const AFilename: String; const ADataServer: IDataServer<T>);
begin
inherited Create;
Assert(Assigned(ADataServer));
FDataServer := ADataServer;
FCurrentFileName := AFilename;
FIsLiveData := TMutable<Boolean>.CreateWriteable(false);
end;
procedure TAuraFileStream<T>.AfterConstruction;
begin
inherited;
FCurrentData := FDataServer.LoadDataFile(FCurrentFileName);
FCurrPosInFile := 0;
FCurrentIdx := 0;
FIsLiveData.SetValue(false);
FLastTimeStamp := 0;
FNextFileName := FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
FNextData := FDataServer.LoadDataFile(FNextFileName)
else
FNextData := nil;
end;
function TAuraFileStream<T>.GetChunk(var Data: array of TDataPoint<T>): Integer;
var
item: TDataRecord<T>;
currData: TArray<TDataRecord<T>>;
begin
Result := 0;
if not FCurrentData.Done.IsSet then
exit;
var maxLen := Length(Data);
currData := FCurrentData.Value;
while Result < maxLen do
begin
if FCurrPosInFile >= Length(currData) then
begin
FCurrentData := FNextData;
FCurrentFileName := FNextFileName;
FCurrPosInFile := 0;
if FCurrentFileName <> '' then
begin
FNextFileName := FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
FNextData := FDataServer.LoadDataFile(FNextFileName)
else
FNextData := nil;
if FCurrentData.Done.IsSet then
begin
currData := FCurrentData.Value;
continue;
end;
end
else
begin
FIsLiveData.SetValue(true);
end;
break;
end;
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;
function TAuraFileStream<T>.GetIsLiveData: TMutable<Boolean>;
begin
Result := FIsLiveData;
end;
{ Helper Functions }
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue, MonthValue: Integer): Boolean;
var
fileNameNoPath: string;
nameForParsing: string;
parts: TArray<string>;
baseName: string;
begin
Result := False;
PathValue := '';
SymbolValue := '';
YearValue := 0;
MonthValue := 0;
PathValue := TPath.GetDirectoryName(FileName);
fileNameNoPath := TPath.GetFileName(FileName);
nameForParsing := fileNameNoPath;
if nameForParsing.EndsWith('_zip', True) then
begin
baseName := nameForParsing.Substring(0, nameForParsing.Length - 4);
if TPath.GetExtension(baseName) <> '' then
begin
nameForParsing := baseName;
end;
end;
nameForParsing := TPath.GetFileNameWithoutExtension(nameForParsing);
parts := nameForParsing.Split(['_']);
if Length(parts) < 3 then
exit;
if not TryStrToInt(parts[High(parts)], MonthValue) then
exit;
if (MonthValue < 1) or (MonthValue > 12) then
begin
MonthValue := 0;
exit;
end;
if not TryStrToInt(parts[High(parts) - 1], YearValue) then
begin
MonthValue := 0;
exit;
end;
if YearValue <= 0 then
begin
YearValue := 0;
MonthValue := 0;
exit;
end;
SymbolValue := string.Join('_', Copy(parts, 0, Length(parts) - 2));
if SymbolValue = '' then
begin
PathValue := '';
YearValue := 0;
MonthValue := 0;
exit;
end;
Result := True;
end;
function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
type
TTrackedFileInfo = record
FullFileName: string;
Year: Integer;
Month: Integer;
end;
var
oldestFilesPerSymbol: TDictionary<string, TTrackedFileInfo>;
fileNames: TArray<string>;
currentFile: string;
parsedPath: string;
parsedSymbol: string;
parsedYear: Integer;
parsedMonth: Integer;
trackedInfo: TTrackedFileInfo;
begin
oldestFilesPerSymbol := TDictionary<string, TTrackedFileInfo>.Create;
try
if not TDirectory.Exists(DirectoryPath) then
exit(nil);
fileNames := TDirectory.GetFiles(DirectoryPath);
for currentFile in fileNames do
begin
if TryParseFileName(currentFile, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
begin
if oldestFilesPerSymbol.TryGetValue(parsedSymbol, trackedInfo) then
begin
if (parsedYear < trackedInfo.Year) or ((parsedYear = trackedInfo.Year) and (parsedMonth < trackedInfo.Month)) then
begin
trackedInfo.FullFileName := currentFile;
trackedInfo.Year := parsedYear;
trackedInfo.Month := parsedMonth;
oldestFilesPerSymbol.AddOrSetValue(parsedSymbol, trackedInfo);
end;
end
else
begin
trackedInfo.FullFileName := currentFile;
trackedInfo.Year := parsedYear;
trackedInfo.Month := parsedMonth;
oldestFilesPerSymbol.Add(parsedSymbol, trackedInfo);
end;
end;
end;
SetLength(Result, oldestFilesPerSymbol.Count);
var n := 0;
for trackedInfo in oldestFilesPerSymbol.Values do
begin
Result[n] := trackedInfo.FullFileName;
inc(n);
end;
finally
oldestFilesPerSymbol.Free;
end;
end;
function FindNextDataFile(const FileName: string): string;
var
pathValue, symbolValue: string;
yearValue, monthValue: Integer;
nextBaseName, compressedPath, uncompressedPath: string;
begin
if not TryParseFileName(FileName, pathValue, symbolValue, yearValue, monthValue) then
exit('');
Inc(monthValue);
if monthValue > 12 then
begin
monthValue := 1;
Inc(yearValue);
end;
nextBaseName := Format('%s_%.4d_%.2d.tab', [symbolValue, yearValue, monthValue]);
compressedPath := TPath.Combine(pathValue, nextBaseName + '_zip');
if TFile.Exists(compressedPath) then
exit(compressedPath);
uncompressedPath := TPath.Combine(pathValue, nextBaseName);
if TFile.Exists(uncompressedPath) then
exit(uncompressedPath);
Result := '';
end;
end.
+31 -32
View File
@@ -5,38 +5,37 @@ program MycTests;
{$ENDIF} {$ENDIF}
{$STRONGLINKTYPES ON} {$STRONGLINKTYPES ON}
uses uses
FastMM5, FastMM5,
DUnitX.MemoryLeakMonitor.FastMM5, DUnitX.MemoryLeakMonitor.FastMM5,
System.SysUtils, System.SysUtils,
{$IFDEF TESTINSIGHT} {$IFDEF TESTINSIGHT}
TestInsight.DUnitX, TestInsight.DUnitX,
{$ELSE} {$ELSE}
DUnitX.Loggers.Console, DUnitX.Loggers.Console,
{$ENDIF } {$ENDIF }
DUnitX.TestFramework, DUnitX.TestFramework,
TestNotifier in 'TestNotifier.pas', TestNotifier in 'TestNotifier.pas',
TestNotifier_Threading in 'TestNotifier_Threading.pas' {/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',}, TestNotifier_Threading in 'TestNotifier_Threading.pas' {/TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',},
TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas', TestNotifier_ChaosStress in 'TestNotifier_ChaosStress.pas',
Myc.Core.Tasks in '..\Src\Myc.Core.Tasks.pas', Myc.Core.Tasks in '..\Src\Myc.Core.Tasks.pas',
TestTasks in 'TestTasks.pas', TestTasks in 'TestTasks.pas',
Myc.Futures in '..\Src\Myc.Futures.pas', Myc.Futures in '..\Src\Myc.Futures.pas',
TestCoreFutures in 'TestCoreFutures.pas', TestCoreFutures in 'TestCoreFutures.pas',
Myc.TaskManager in '..\Src\Myc.TaskManager.pas', Myc.TaskManager in '..\Src\Myc.TaskManager.pas',
Myc.Core.Futures in '..\Src\Myc.Core.Futures.pas', Myc.Core.Futures in '..\Src\Myc.Core.Futures.pas',
Myc.Core.Signals in '..\Src\Myc.Core.Signals.pas', Myc.Core.Signals in '..\Src\Myc.Core.Signals.pas',
TestFutures in 'TestFutures.pas', TestFutures in 'TestFutures.pas',
Myc.Lazy in '..\Src\Myc.Lazy.pas', Myc.Lazy in '..\Src\Myc.Lazy.pas',
Myc.Core.Lazy in '..\Src\Myc.Core.Lazy.pas', Myc.Core.Lazy in '..\Src\Myc.Core.Lazy.pas',
Myc.Test.Core.Lazy in '..\Src\Myc.Test.Core.Lazy.pas', Myc.Test.Core.Lazy in '..\Src\Myc.Test.Core.Lazy.pas',
Myc.Test.Lazy in '..\Src\Myc.Test.Lazy.pas', Myc.Test.Lazy in '..\Src\Myc.Test.Lazy.pas',
Myc.Test.Core.Atomic in '..\Src\Myc.Test.Core.Atomic.pas', Myc.Test.Core.Atomic in '..\Src\Myc.Test.Core.Atomic.pas',
Myc.Trade.Ticker in '..\Src\Myc.Trade.Ticker.pas', Myc.Trade.Ticker in '..\Src\Myc.Trade.Ticker.pas',
Myc.Test.Signals.Latch in '..\Src\Myc.Test.Signals.Latch.pas', Myc.Test.Signals.Latch in '..\Src\Myc.Test.Signals.Latch.pas',
Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas', Myc.Test.Signals.Dirty in '..\Src\Myc.Test.Signals.Dirty.pas',
Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas', Myc.Trade.DataPoint in '..\Src\Myc.Trade.DataPoint.pas',
Myc.Trade.DataServer in '..\Src\Myc.Trade.DataServer.pas', Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas',
Myc.Trade.Node in '..\Src\Myc.Trade.Node.pas', Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas';
Myc.Test.Trade.DataServer in '..\Src\Myc.Test.Trade.DataServer.pas';
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit } { keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
{$IFNDEF TESTINSIGHT} {$IFNDEF TESTINSIGHT}
+8 -3
View File
@@ -4,7 +4,7 @@
<ProjectVersion>20.3</ProjectVersion> <ProjectVersion>20.3</ProjectVersion>
<FrameworkType>None</FrameworkType> <FrameworkType>None</FrameworkType>
<Base>True</Base> <Base>True</Base>
<Config Condition="'$(Config)'==''">Release</Config> <Config Condition="'$(Config)'==''">Debug</Config>
<Platform Condition="'$(Platform)'==''">Win64</Platform> <Platform Condition="'$(Platform)'==''">Win64</Platform>
<ProjectName Condition="'$(ProjectName)'==''">MycTests</ProjectName> <ProjectName Condition="'$(ProjectName)'==''">MycTests</ProjectName>
<TargetedPlatforms>3</TargetedPlatforms> <TargetedPlatforms>3</TargetedPlatforms>
@@ -134,9 +134,8 @@ $(PreBuildEvent)]]></PreBuildEvent>
<DCCReference Include="..\Src\Myc.Test.Signals.Latch.pas"/> <DCCReference Include="..\Src\Myc.Test.Signals.Latch.pas"/>
<DCCReference Include="..\Src\Myc.Test.Signals.Dirty.pas"/> <DCCReference Include="..\Src\Myc.Test.Signals.Dirty.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataPoint.pas"/> <DCCReference Include="..\Src\Myc.Trade.DataPoint.pas"/>
<DCCReference Include="..\Src\Myc.Trade.DataServer.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Node.pas"/> <DCCReference Include="..\Src\Myc.Trade.Node.pas"/>
<DCCReference Include="..\Src\Myc.Test.Trade.DataServer.pas"/> <DCCReference Include="..\Src\Myc.Trade.DataStream.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
@@ -192,6 +191,12 @@ $(PreBuildEvent)]]></PreBuildEvent>
<Overwrite>true</Overwrite> <Overwrite>true</Overwrite>
</Platform> </Platform>
</DeployFile> </DeployFile>
<DeployFile LocalName="Win64\Release\MycTests.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win64">
<RemoteName>MycTests.exe</RemoteName>
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployClass Name="AdditionalDebugSymbols"> <DeployClass Name="AdditionalDebugSymbols">
<Platform Name="iOSSimulator"> <Platform Name="iOSSimulator">
<Operation>1</Operation> <Operation>1</Operation>