Data file handling
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
unit Myc.Core.FileCache;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.IOUtils,
|
||||
System.Generics.Collections,
|
||||
System.Generics.Defaults,
|
||||
System.SyncObjs,
|
||||
Myc.Futures;
|
||||
|
||||
type
|
||||
// Manages a cache for time-series data files loaded asynchronously.
|
||||
// The cache is activated by assigning a function to the IsMemoryLow property.
|
||||
// It evicts least-recently-used items when memory is low.
|
||||
TDataFileCache<T> = class
|
||||
type
|
||||
TLoaderFunc = reference to function(const Filename: String): TFuture<T>;
|
||||
private
|
||||
type
|
||||
// Holds metadata for a cached file.
|
||||
TCachedFile = class
|
||||
public
|
||||
Name: String;
|
||||
Age: TDateTime;
|
||||
LastUsed: TDateTime;
|
||||
Data: TFuture<T>;
|
||||
end;
|
||||
private
|
||||
FCachedFiles: TDictionary<String, TCachedFile>;
|
||||
FLoader: TLoaderFunc;
|
||||
FLock: TCriticalSection;
|
||||
public
|
||||
constructor Create(const ALoader: TLoaderFunc);
|
||||
destructor Destroy; override;
|
||||
|
||||
// Removes all items from the cache, waiting for any pending operations to complete.
|
||||
procedure Clear;
|
||||
|
||||
// Retrieves a data future from the cache or uses the provided loader function to create and cache it.
|
||||
// Caching is only active if the IsMemoryLow property is assigned.
|
||||
function GetOrAdd(const AFileName: string): TFuture<T>;
|
||||
end;
|
||||
|
||||
var
|
||||
IsMemoryLow: TFunc<Boolean>;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
{$IFDEF MSWINDOWS} Winapi.Windows, {$endif}
|
||||
System.Math;
|
||||
|
||||
{ TDataFileCache<T> }
|
||||
|
||||
constructor TDataFileCache<T>.Create(const ALoader: TLoaderFunc);
|
||||
begin
|
||||
inherited Create;
|
||||
FLoader := ALoader;
|
||||
FLock := TCriticalSection.Create;
|
||||
FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
|
||||
end;
|
||||
|
||||
destructor TDataFileCache<T>.Destroy;
|
||||
begin
|
||||
Clear;
|
||||
FCachedFiles.Free;
|
||||
FLock.Free;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TDataFileCache<T>.Clear;
|
||||
begin
|
||||
FLock.Enter;
|
||||
try
|
||||
// Wait for any pending load operations to finish before clearing.
|
||||
for var cachedFile in FCachedFiles.Values do
|
||||
cachedFile.Data.WaitFor;
|
||||
FCachedFiles.Clear;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TDataFileCache<T>.GetOrAdd(const AFileName: string): TFuture<T>;
|
||||
var
|
||||
fileList: TArray<TCachedFile>;
|
||||
i: Integer;
|
||||
cachedFile: TCachedFile;
|
||||
currentFileAge: TDateTime;
|
||||
begin
|
||||
// If no memory check function is assigned, caching is disabled.
|
||||
// Simply execute the loader function and return the result.
|
||||
if not Assigned(IsMemoryLow) then
|
||||
begin
|
||||
Result := FLoader(AFilename);
|
||||
exit;
|
||||
end;
|
||||
|
||||
FLock.Enter;
|
||||
try
|
||||
// Caching is active. First, try to free memory if needed.
|
||||
fileList := FCachedFiles.Values.ToArray;
|
||||
TArray.Sort<TCachedFile>(
|
||||
fileList,
|
||||
TComparer<TCachedFile>.Construct(
|
||||
function(const Left, Right: TCachedFile): Integer
|
||||
begin
|
||||
// Sort by LastUsed timestamp, oldest first.
|
||||
Result := Sign(Left.LastUsed - Right.LastUsed);
|
||||
end
|
||||
)
|
||||
);
|
||||
|
||||
for i := 0 to High(fileList) do
|
||||
begin
|
||||
if not IsMemoryLow() then
|
||||
break; // Stop evicting if memory pressure is relieved.
|
||||
|
||||
if fileList[i].Name <> AFileName then
|
||||
FCachedFiles.Remove(fileList[i].Name);
|
||||
end;
|
||||
|
||||
// Check if a valid entry exists in the cache.
|
||||
if FCachedFiles.TryGetValue(AFileName, cachedFile) then
|
||||
begin
|
||||
if FileAge(AFileName, currentFileAge, true) and (currentFileAge = cachedFile.Age) then
|
||||
begin
|
||||
// Cache hit and file is unchanged. Update usage timestamp and return cached future.
|
||||
cachedFile.LastUsed := Now;
|
||||
exit(cachedFile.Data);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// File is stale or no longer exists. Remove from cache.
|
||||
FCachedFiles.Remove(AFileName);
|
||||
end;
|
||||
end;
|
||||
|
||||
// Cache miss or stale entry. Execute the loader function.
|
||||
Result := FLoader(AFilename);
|
||||
|
||||
// Create a new cache entry for the loaded future.
|
||||
cachedFile := TCachedFile.Create;
|
||||
cachedFile.Name := AFileName;
|
||||
if FileAge(AFileName, cachedFile.Age, true) then
|
||||
begin
|
||||
cachedFile.Data := Result;
|
||||
cachedFile.LastUsed := Now;
|
||||
FCachedFiles.Add(AFileName, cachedFile);
|
||||
end
|
||||
else
|
||||
begin
|
||||
// If we cannot get the file's age, we cannot cache it reliably.
|
||||
cachedFile.Free;
|
||||
end;
|
||||
finally
|
||||
FLock.Leave;
|
||||
end;
|
||||
end;
|
||||
|
||||
initialization
|
||||
{$IFDEF MSWINDOWS}
|
||||
IsMemoryLow :=
|
||||
function: Boolean
|
||||
var
|
||||
memStatusEx: TMemoryStatusEx;
|
||||
begin
|
||||
Result := false;
|
||||
memStatusEx.dwLength := SizeOf(TMemoryStatusEx); // Initialize the structure size
|
||||
if GlobalMemoryStatusEx(memStatusEx) then
|
||||
begin
|
||||
// memStatusEx.dwMemoryLoad indicates the approximate percentage of physical memory that is in use.
|
||||
Result := memStatusEx.dwMemoryLoad >= 80;
|
||||
end;
|
||||
end;
|
||||
{$endif}
|
||||
|
||||
end.
|
||||
@@ -59,7 +59,7 @@ procedure TMycNotifyList<T>.Destroy;
|
||||
begin
|
||||
// Because refcounting is thread-safe, this will always be entered once after all references
|
||||
// to Self are dropped. No locking needed!
|
||||
Assert(not IsLocked);
|
||||
// Assert(not IsLocked);
|
||||
FFirst := FFirst and not 3;
|
||||
UnadviseAll;
|
||||
end;
|
||||
|
||||
@@ -25,7 +25,7 @@ type
|
||||
end;
|
||||
|
||||
// A state that acts as an event. It's state ist always set and it will always Trigger it's subscribers, if it gets notified by itself.
|
||||
TMycEvent = class(TInterfacedObject, TSignal.ISignal, TEvent.IEvent)
|
||||
TMycEvent = class(TInterfacedObject, TSignal.ISubscriber, TSignal.ISignal, TEvent.IEvent)
|
||||
strict private
|
||||
FSubscribers: TMycNotifyList<TSignal.ISubscriber>;
|
||||
protected
|
||||
@@ -49,7 +49,7 @@ type
|
||||
// It is initialized with a count. Calls to its Notify method (as an TSignal.ISubscriber)
|
||||
// decrement this count. When the count reaches zero, the latch transitions to the "set"
|
||||
// state (IsSet becomes true), notifies its current subscribers once, and then remains set.
|
||||
TMycLatch = class(TInterfacedObject, TState.IState, TLatch.ILatch)
|
||||
TMycLatch = class(TInterfacedObject, TSignal.ISubscriber, TSignal.ISignal, TState.IState, TLatch.ILatch)
|
||||
strict private
|
||||
FSubscribers: TMycNotifyList<TSignal.ISubscriber>; // List of subscribers waiting for this latch to be set.
|
||||
private
|
||||
@@ -74,7 +74,7 @@ type
|
||||
function Notify: Boolean;
|
||||
end;
|
||||
|
||||
TMycNullLatch = class(TInterfacedObject, TLatch.ILatch)
|
||||
TMycNullLatch = class(TInterfacedObject, TSignal.ISubscriber, TLatch.ILatch)
|
||||
private
|
||||
function GetState: TState;
|
||||
function Notify: Boolean;
|
||||
@@ -83,7 +83,7 @@ type
|
||||
// TMycFlag implements a resettable "dirty flag".
|
||||
// It can be explicitly set to dirty (via Notify) or reset to clean (via Reset).
|
||||
// Subscribers are notified of relevant state changes.
|
||||
TMycFlag = class(TInterfacedObject, TState.IState, TFlag.IFlag)
|
||||
TMycFlag = class(TInterfacedObject, TSignal.ISubscriber, TSignal.ISignal, TState.IState, TFlag.IFlag)
|
||||
strict private
|
||||
FSubscribers: TMycNotifyList<TSignal.ISubscriber>; // List of subscribers interested in state changes of this dirty flag.
|
||||
private
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
unit Myc.Signals.FMX;
|
||||
|
||||
interface
|
||||
|
||||
uses
|
||||
System.Classes,
|
||||
FMX.Controls,
|
||||
Myc.Signals;
|
||||
|
||||
type
|
||||
TFMXSignalLink = class(TComponent, TSignal.ISubscriber)
|
||||
private
|
||||
[volatile]
|
||||
FInvalidated: Integer;
|
||||
procedure InvalidateControl;
|
||||
|
||||
protected
|
||||
function Notify: Boolean;
|
||||
|
||||
public
|
||||
constructor Create(AOwner: TControl); reintroduce;
|
||||
destructor Destroy; override;
|
||||
function Reset: Boolean;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SyncObjs;
|
||||
|
||||
{ TFMXSignalLink }
|
||||
|
||||
constructor TFMXSignalLink.Create(AOwner: TControl);
|
||||
begin
|
||||
inherited Create(AOwner);
|
||||
FInvalidated := 0;
|
||||
end;
|
||||
|
||||
destructor TFMXSignalLink.Destroy;
|
||||
begin
|
||||
TThread.RemoveQueuedEvents(InvalidateControl);
|
||||
inherited;
|
||||
end;
|
||||
|
||||
procedure TFMXSignalLink.InvalidateControl;
|
||||
begin
|
||||
with TControl(Owner) do
|
||||
InvalidateRect(LocalRect);
|
||||
end;
|
||||
|
||||
function TFMXSignalLink.Notify: Boolean;
|
||||
begin
|
||||
if TInterlocked.Exchange(FInvalidated, 1) = 0 then
|
||||
TThread.Queue(nil, InvalidateControl);
|
||||
exit( true );
|
||||
end;
|
||||
|
||||
function TFMXSignalLink.Reset: Boolean;
|
||||
begin
|
||||
Result := TInterlocked.Exchange(FInvalidated, 0) > 0;
|
||||
end;
|
||||
|
||||
end.
|
||||
+23
-7
@@ -3,7 +3,8 @@ unit Myc.Signals;
|
||||
interface
|
||||
|
||||
uses
|
||||
System.SysUtils;
|
||||
System.SysUtils,
|
||||
System.SyncObjs;
|
||||
|
||||
type
|
||||
TSignal = record
|
||||
@@ -136,6 +137,8 @@ type
|
||||
strict private
|
||||
class var
|
||||
FNull: ILatch;
|
||||
FQueueGate: TLatch;
|
||||
FQueueLock: TSpinLock;
|
||||
class constructor ClassCreate;
|
||||
|
||||
private
|
||||
@@ -150,7 +153,8 @@ type
|
||||
|
||||
class function CreateLatch(Count: Integer): TLatch; static;
|
||||
|
||||
class function Enqueue(var Gate: TLatch; Count: Integer = 1): TState; static;
|
||||
class function Enqueue(var Gate: TLatch): TState; overload; static;
|
||||
class function Enqueue: TState; overload; static;
|
||||
|
||||
class property Null: ILatch read FNull;
|
||||
|
||||
@@ -390,6 +394,7 @@ class constructor TLatch.ClassCreate;
|
||||
begin
|
||||
// Create a singleton null latch instance that is initially (and always) set.
|
||||
FNull := TMycNullLatch.Create;
|
||||
FQueueLock := TSpinLock.Create(false);
|
||||
end;
|
||||
|
||||
constructor TLatch.Create(const ALatch: TLatch.ILatch);
|
||||
@@ -407,12 +412,23 @@ begin
|
||||
Result := FNull;
|
||||
end;
|
||||
|
||||
class function TLatch.Enqueue(var Gate: TLatch; Count: Integer = 1): TState;
|
||||
class function TLatch.Enqueue(var Gate: TLatch): TState;
|
||||
begin
|
||||
var gateState := Gate.State;
|
||||
Gate := TMycLatch.Create(Count);
|
||||
gateState.Subscribe(Gate);
|
||||
exit(Gate.State);
|
||||
var Latch: TLatch.ILatch := TMycLatch.Create(1);
|
||||
|
||||
FQueueLock.Enter;
|
||||
try
|
||||
Gate.State.Subscribe(Latch);
|
||||
Gate := Latch;
|
||||
Result := Latch.State;
|
||||
finally
|
||||
FQueueLock.Exit;
|
||||
end;
|
||||
end;
|
||||
|
||||
class function TLatch.Enqueue: TState;
|
||||
begin
|
||||
Result := Enqueue(FQueueGate);
|
||||
end;
|
||||
|
||||
function TLatch.GetState: TState;
|
||||
|
||||
@@ -59,9 +59,6 @@ end;
|
||||
procedure TTest_TABFileServer_Equivalence.TearDownFixture;
|
||||
begin
|
||||
FServer := nil;
|
||||
|
||||
// Resets the global IsMemoryLow function pointer, if it was set for testing.
|
||||
IsMemoryLow := nil;
|
||||
end;
|
||||
|
||||
procedure TTest_TABFileServer_Equivalence.Setup;
|
||||
@@ -90,7 +87,7 @@ var
|
||||
expectedData: TArray<TDataPoint<TAskBidItem>>;
|
||||
begin
|
||||
// 1. Expected data is loaded directly using LoadDataSeries.
|
||||
filename := TAuraTABFileServer.FindFirstDataFile(C_TEST_PATH, C_TEST_SYMBOL);
|
||||
filename := (FServer as TAuraDataServer<TAskBidItem>).FindFirstDataFile(C_TEST_SYMBOL);
|
||||
Assert.IsNotEmpty(filename, 'Test data file could not be found.');
|
||||
|
||||
expectedData := FServer.LoadDataSeries(filename).WaitFor;
|
||||
|
||||
+306
-341
@@ -26,10 +26,10 @@ uses
|
||||
Myc.Futures,
|
||||
Myc.Signals,
|
||||
Myc.Lazy,
|
||||
Myc.Trade.DataPoint;
|
||||
Myc.Trade.DataPoint,
|
||||
Myc.Core.FileCache;
|
||||
|
||||
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.
|
||||
@@ -39,13 +39,16 @@ type
|
||||
['{A6E246A2-E84E-49AB-A63E-333E561E488C}']
|
||||
function GetHasData: TSignal;
|
||||
function GetChunk(var Data: array of TDataPoint<T>): Integer;
|
||||
function GetSymbol: String;
|
||||
function IsHistory: Boolean;
|
||||
property HasData: TSignal read GetHasData;
|
||||
property Symbol: String read GetSymbol;
|
||||
end;
|
||||
|
||||
// Abstract base class for IDataStream implementations.
|
||||
TDataStream<T> = class(TInterfacedObject, IDataStream<T>)
|
||||
protected
|
||||
function GetSymbol: String; virtual; abstract;
|
||||
function GetHasData: TSignal; virtual; abstract;
|
||||
function GetChunk(var Data: array of TDataPoint<T>): Integer; virtual; abstract;
|
||||
function IsHistory: Boolean; virtual; abstract;
|
||||
@@ -79,31 +82,31 @@ type
|
||||
IAuraDataServer<T: record> = interface(IDataServer<T>)
|
||||
['{481E27DE-DC58-49AF-B8A8-B6316980C423}']
|
||||
function LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
function EnumerateAssetFiles: TArray<String>;
|
||||
function EnumerateAssets: TArray<String>;
|
||||
end;
|
||||
|
||||
// Aura files
|
||||
|
||||
// Generic server for loading and managing sequential time-series data from files.
|
||||
TAuraDataServer<T: record> = class(TInterfacedObject, IDataServer<T>, IAuraDataServer<T>)
|
||||
public type
|
||||
TDataFile = record
|
||||
Extension: String;
|
||||
Path, Symbol: String;
|
||||
Year, Month: Integer;
|
||||
function GetBaseFileName: string;
|
||||
function GetFullFileName: string;
|
||||
function GetIsValid: Boolean;
|
||||
property IsValid: Boolean read GetIsValid;
|
||||
end;
|
||||
|
||||
private
|
||||
type
|
||||
TCachedFile = class
|
||||
public
|
||||
Name: String;
|
||||
Age: TDateTime;
|
||||
LastUsed: TDateTime;
|
||||
Data: TFuture<TArray<TDataPoint<T>>>;
|
||||
end;
|
||||
private
|
||||
// The cache is per-instance.
|
||||
FCachedFiles: TDictionary<String, TCachedFile>;
|
||||
FCachedFiles: TDataFileCache<TArray<TDataPoint<T>>>;
|
||||
FPath: String;
|
||||
function DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
function GetPath: String;
|
||||
|
||||
strict private
|
||||
// The load gate is shared across all instances of TAuraDataServer.
|
||||
class var
|
||||
FLoadGate: TLatch;
|
||||
|
||||
@@ -115,65 +118,58 @@ type
|
||||
constructor Create(const APath: String);
|
||||
destructor Destroy; override;
|
||||
|
||||
class function TryParseFileName(
|
||||
const FileName: string;
|
||||
out PathValue, SymbolValue: string;
|
||||
out YearValue, MonthValue: Integer
|
||||
): Boolean; virtual; abstract;
|
||||
class function TryParseFileName(const FileName: string; out DataFile: TDataFile): Boolean; virtual; abstract;
|
||||
|
||||
class function FindFirstDataFile(const Path, Symbol: string): string;
|
||||
class function FindNextDataFile(const FileName: string): string;
|
||||
class function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
|
||||
function FindFirstDataFile(const Symbol: string): TDataFile;
|
||||
class function FindNextDataFile(const CurrentFile: TDataFile): TDataFile;
|
||||
class function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<TDataFile>;
|
||||
|
||||
// IDataServer<T> implementation
|
||||
function CreateStream(const Symbol: String): IDataStream<T>; virtual; abstract;
|
||||
procedure ClearCache;
|
||||
function EnumerateAssetFiles: TArray<String>;
|
||||
|
||||
function EnumerateAssets: TArray<String>;
|
||||
function LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
|
||||
property Path: String read GetPath;
|
||||
end;
|
||||
|
||||
// Implements a data stream that reads from Aura-specific historical data files.
|
||||
TAuraFileStream<T: record> = class(TDataStream<T>)
|
||||
TAuraFileStream<T: record> = class(TDataStream<T>, IDataStream<T>)
|
||||
private
|
||||
type
|
||||
// Changed: Renamed "File" to "FileInfo" for clarity.
|
||||
TDataStream = record
|
||||
FileInfo: TAuraDataServer<T>.TDataFile;
|
||||
Data: TFuture<TArray<TDataPoint<T>>>;
|
||||
end;
|
||||
private
|
||||
FDataServer: TAuraDataServer<T>;
|
||||
FSymbol: String;
|
||||
FHasData: TEvent;
|
||||
FCurrentFileName: string;
|
||||
FCurrentData: TFuture<TArray<TDataPoint<T>>>;
|
||||
FNextFileName: string;
|
||||
FNextData: TFuture<TArray<TDataPoint<T>>>;
|
||||
FCurrent: TFuture<TDataStream>;
|
||||
FNext: TFuture<TDataStream>;
|
||||
FCurrPosInFile: Int64;
|
||||
FLastTimeStamp: TDateTime;
|
||||
procedure PreloadNext;
|
||||
protected
|
||||
function GetSymbol: String; override;
|
||||
function GetHasData: TSignal; override;
|
||||
function GetChunk(var Data: array of TDataPoint<T>): Integer; override;
|
||||
function IsHistory: Boolean; override;
|
||||
public
|
||||
constructor Create(ADataServer: TAuraDataServer<T>; const AFilename: String);
|
||||
constructor Create(ADataServer: TAuraDataServer<T>; const ASymbol: String);
|
||||
destructor Destroy; override;
|
||||
procedure AfterConstruction; override;
|
||||
procedure BeforeDestruction; override;
|
||||
end;
|
||||
|
||||
// Aura tick data file Ask-Bid
|
||||
|
||||
TAuraTABFileServer = class(TAuraDataServer<TAskBidItem>)
|
||||
public
|
||||
// IDataServer<T> implementation
|
||||
function CreateStream(const Symbol: String): IDataStream<TAskBidItem>; override;
|
||||
|
||||
class function TryParseFileName(
|
||||
const FileName: string;
|
||||
out PathValue, SymbolValue: string;
|
||||
out YearValue, MonthValue: Integer
|
||||
): Boolean; override;
|
||||
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<TAskBidItem>>>;
|
||||
class function TryParseFileName(const FileName: string; out DataFile: TAuraDataServer<TAskBidItem>.TDataFile): Boolean; override;
|
||||
end;
|
||||
|
||||
var
|
||||
IsMemoryLow: TFunc<Boolean>;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
@@ -182,12 +178,33 @@ uses
|
||||
System.StrUtils,
|
||||
Myc.TaskManager;
|
||||
|
||||
{ TAuraDataServer<T>.TDataFile }
|
||||
|
||||
function TAuraDataServer<T>.TDataFile.GetBaseFileName: string;
|
||||
begin
|
||||
Result := Format('%s_%.4d_%.2d', [Symbol, Year, Month]);
|
||||
end;
|
||||
|
||||
function TAuraDataServer<T>.TDataFile.GetFullFileName: string;
|
||||
begin
|
||||
Result := TPath.Combine(Path, GetBaseFileName + Extension);
|
||||
end;
|
||||
|
||||
function TAuraDataServer<T>.TDataFile.GetIsValid: Boolean;
|
||||
begin
|
||||
Result := (Symbol <> '') and (Year > 0);
|
||||
end;
|
||||
|
||||
{ TAuraDataServer<T> }
|
||||
|
||||
constructor TAuraDataServer<T>.Create(const APath: String);
|
||||
begin
|
||||
inherited Create;
|
||||
// Initialize the per-instance cache.
|
||||
FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
|
||||
FPath := APath;
|
||||
|
||||
FCachedFiles :=
|
||||
TDataFileCache<TArray<TDataPoint<T>>>
|
||||
.Create(function(const Filename: String): TFuture<TArray<TDataPoint<T>>> begin Result := DoLoad(Filename); end);
|
||||
end;
|
||||
|
||||
destructor TAuraDataServer<T>.Destroy;
|
||||
@@ -199,78 +216,59 @@ end;
|
||||
|
||||
procedure TAuraDataServer<T>.ClearCache;
|
||||
begin
|
||||
for var cF in FCachedFiles.Values do
|
||||
cF.Data.WaitFor;
|
||||
|
||||
FCachedFiles.Clear;
|
||||
end;
|
||||
|
||||
class function TAuraDataServer<T>.FindFirstDataFile(const Path, Symbol: string): string;
|
||||
function TAuraDataServer<T>.FindFirstDataFile(const Symbol: string): TDataFile;
|
||||
var
|
||||
oldestFiles: TArray<string>;
|
||||
fileName: string;
|
||||
pathValue, symbolValue: string;
|
||||
yearValue, monthValue: Integer;
|
||||
oldestFiles: TArray<TDataFile>;
|
||||
fileInfo: TDataFile;
|
||||
begin
|
||||
oldestFiles := FindOldestFilesPerSymbol(Path);
|
||||
for fileName in oldestFiles do
|
||||
oldestFiles := FindOldestFilesPerSymbol(FPath);
|
||||
for fileInfo in oldestFiles do
|
||||
begin
|
||||
if TryParseFileName(fileName, pathValue, symbolValue, yearValue, monthValue) then
|
||||
begin
|
||||
if SameText(symbolValue, Symbol) then
|
||||
exit(fileName);
|
||||
end;
|
||||
if SameText(fileInfo.Symbol, Symbol) then
|
||||
exit(fileInfo);
|
||||
end;
|
||||
Result := '';
|
||||
Result := Default(TDataFile);
|
||||
end;
|
||||
|
||||
class function TAuraDataServer<T>.FindNextDataFile(const FileName: string): string;
|
||||
class function TAuraDataServer<T>.FindNextDataFile(const CurrentFile: TDataFile): TDataFile;
|
||||
var
|
||||
pathValue, symbolValue: string;
|
||||
yearValue, monthValue: Integer;
|
||||
nextBaseName, compressedPath, uncompressedPath: string;
|
||||
dataFileToProbe: TDataFile;
|
||||
begin
|
||||
if not TryParseFileName(FileName, pathValue, symbolValue, yearValue, monthValue) then
|
||||
exit('');
|
||||
Result := Default(TDataFile);
|
||||
|
||||
Inc(monthValue);
|
||||
if monthValue > 12 then
|
||||
if not CurrentFile.IsValid then
|
||||
exit;
|
||||
|
||||
// Changed: Simplified logic, no reparsing needed.
|
||||
dataFileToProbe := CurrentFile;
|
||||
Inc(dataFileToProbe.Month);
|
||||
if (dataFileToProbe.Month > 12) then
|
||||
begin
|
||||
monthValue := 1;
|
||||
Inc(yearValue);
|
||||
dataFileToProbe.Month := 1;
|
||||
Inc(dataFileToProbe.Year);
|
||||
end;
|
||||
|
||||
nextBaseName := Format('%s_%.4d_%.2d.tab', [symbolValue, yearValue, monthValue]);
|
||||
dataFileToProbe.Extension := '.tab_zip';
|
||||
if TFile.Exists(dataFileToProbe.GetFullFileName) then
|
||||
exit(dataFileToProbe);
|
||||
|
||||
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 := '';
|
||||
dataFileToProbe.Extension := '.tab';
|
||||
if TFile.Exists(dataFileToProbe.GetFullFileName) then
|
||||
exit(dataFileToProbe);
|
||||
end;
|
||||
|
||||
class function TAuraDataServer<T>.FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
|
||||
type
|
||||
TTrackedFileInfo = record
|
||||
FullFileName: string;
|
||||
Year: Integer;
|
||||
Month: Integer;
|
||||
end;
|
||||
class function TAuraDataServer<T>.FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<TDataFile>;
|
||||
var
|
||||
oldestFilesPerSymbol: TDictionary<string, TTrackedFileInfo>;
|
||||
oldestFilesPerSymbol: TDictionary<string, TDataFile>;
|
||||
fileNames: TArray<string>;
|
||||
currentFile: string;
|
||||
parsedPath: string;
|
||||
parsedSymbol: string;
|
||||
parsedYear: Integer;
|
||||
parsedMonth: Integer;
|
||||
trackedInfo: TTrackedFileInfo;
|
||||
dataFile: TDataFile;
|
||||
trackedInfo: TDataFile;
|
||||
begin
|
||||
oldestFilesPerSymbol := TDictionary<string, TTrackedFileInfo>.Create;
|
||||
oldestFilesPerSymbol := TDictionary<string, TDataFile>.Create;
|
||||
try
|
||||
if not TDirectory.Exists(DirectoryPath) then
|
||||
exit(nil);
|
||||
@@ -278,43 +276,37 @@ begin
|
||||
fileNames := TDirectory.GetFiles(DirectoryPath);
|
||||
for currentFile in fileNames do
|
||||
begin
|
||||
if TryParseFileName(currentFile, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
|
||||
if TryParseFileName(currentFile, dataFile) then
|
||||
begin
|
||||
if oldestFilesPerSymbol.TryGetValue(parsedSymbol, trackedInfo) then
|
||||
if oldestFilesPerSymbol.TryGetValue(dataFile.Symbol, trackedInfo) then
|
||||
begin
|
||||
if (parsedYear < trackedInfo.Year) or ((parsedYear = trackedInfo.Year) and (parsedMonth < trackedInfo.Month)) then
|
||||
if (dataFile.Year < trackedInfo.Year) or ((dataFile.Year = trackedInfo.Year) and (dataFile.Month < trackedInfo.Month)) then
|
||||
begin
|
||||
trackedInfo.FullFileName := currentFile;
|
||||
trackedInfo.Year := parsedYear;
|
||||
trackedInfo.Month := parsedMonth;
|
||||
oldestFilesPerSymbol.AddOrSetValue(parsedSymbol, trackedInfo);
|
||||
oldestFilesPerSymbol.AddOrSetValue(dataFile.Symbol, dataFile);
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
trackedInfo.FullFileName := currentFile;
|
||||
trackedInfo.Year := parsedYear;
|
||||
trackedInfo.Month := parsedMonth;
|
||||
oldestFilesPerSymbol.Add(parsedSymbol, trackedInfo);
|
||||
oldestFilesPerSymbol.Add(dataFile.Symbol, dataFile);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
SetLength(Result, oldestFilesPerSymbol.Count);
|
||||
var n := 0;
|
||||
for trackedInfo in oldestFilesPerSymbol.Values do
|
||||
begin
|
||||
Result[n] := trackedInfo.FullFileName;
|
||||
inc(n);
|
||||
end;
|
||||
Result := oldestFilesPerSymbol.Values.ToArray;
|
||||
finally
|
||||
oldestFilesPerSymbol.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TAuraDataServer<T>.EnumerateAssetFiles: TArray<String>;
|
||||
function TAuraDataServer<T>.EnumerateAssets: TArray<String>;
|
||||
var
|
||||
dataFiles: TArray<TDataFile>;
|
||||
i: Integer;
|
||||
begin
|
||||
Result := FindOldestFilesPerSymbol(FPath);
|
||||
dataFiles := FindOldestFilesPerSymbol(FPath);
|
||||
SetLength(Result, Length(dataFiles));
|
||||
for i := 0 to High(dataFiles) do
|
||||
Result[i] := dataFiles[i].Symbol;
|
||||
end;
|
||||
|
||||
function TAuraDataServer<T>.GetPath: String;
|
||||
@@ -322,59 +314,20 @@ begin
|
||||
Result := FPath;
|
||||
end;
|
||||
|
||||
function TAuraDataServer<T>.LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
var
|
||||
parsedPath, parsedSymbol: string;
|
||||
parsedYear, parsedMonth: Integer;
|
||||
function TAuraDataServer<T>.DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
begin
|
||||
Result := TFuture<TArray<TDataPoint<T>>>.Null;
|
||||
|
||||
if not TryParseFileName(FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
|
||||
if not TFile.Exists(FileName) 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
|
||||
TLatch.Enqueue(FLoadGate),
|
||||
function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
|
||||
.Chain<TArray<TDataPoint<T>>>(
|
||||
function(bytes: TBytes): TArray<TDataPoint<T>>
|
||||
@@ -388,11 +341,11 @@ begin
|
||||
end;
|
||||
end);
|
||||
end
|
||||
else if TFile.Exists(FileName) then
|
||||
else
|
||||
begin
|
||||
Result :=
|
||||
TFuture<TArray<TDataPoint<T>>>.Construct(
|
||||
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
|
||||
TLatch.Enqueue(FLoadGate),
|
||||
function: TArray<TDataPoint<T>>
|
||||
begin
|
||||
if TFile.Exists(capFileName) then
|
||||
@@ -412,108 +365,11 @@ begin
|
||||
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 TAuraDataServer<T>.LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
var
|
||||
loadedState: TState;
|
||||
loadedFiles: TArray<TFuture<TArray<TDataPoint<T>>>>;
|
||||
liveData: TFuture<TArray<TDataPoint<T>>>;
|
||||
tabFiles: TList<string>;
|
||||
currentFile: string;
|
||||
liveFilePath: string;
|
||||
pathName, symbolName: string;
|
||||
yearValue, monthValue: Integer;
|
||||
function TAuraDataServer<T>.LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
|
||||
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<TDataPoint<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<TDataPoint<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<TDataPoint<T>>>.Construct(
|
||||
loadedState,
|
||||
function: TArray<TDataPoint<T>>
|
||||
begin
|
||||
var tickList := TList<TDataPoint<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.Time then
|
||||
begin
|
||||
tickList.Add(iterTickData);
|
||||
overallLastTabTickTime := iterTickData.Time;
|
||||
end;
|
||||
Result := tickList.ToArray;
|
||||
finally
|
||||
tickList.Free;
|
||||
end;
|
||||
end
|
||||
);
|
||||
Result := FCachedFiles.GetOrAdd(Filename);
|
||||
end;
|
||||
|
||||
class function TAuraDataServer<T>.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>;
|
||||
@@ -583,14 +439,13 @@ end;
|
||||
|
||||
{ TAuraFileStream<T> }
|
||||
|
||||
constructor TAuraFileStream<T>.Create(ADataServer: TAuraDataServer<T>; const AFilename: String);
|
||||
constructor TAuraFileStream<T>.Create(ADataServer: TAuraDataServer<T>; const ASymbol: String);
|
||||
begin
|
||||
inherited Create;
|
||||
Assert(Assigned(ADataServer));
|
||||
FDataServer := ADataServer;
|
||||
FCurrentFileName := AFilename;
|
||||
// Create an event
|
||||
FHasData := TEvent.CreateEvent; // interface helper benutzen!
|
||||
FSymbol := ASymbol;
|
||||
FHasData := TEvent.CreateEvent;
|
||||
end;
|
||||
|
||||
destructor TAuraFileStream<T>.Destroy;
|
||||
@@ -599,56 +454,69 @@ begin
|
||||
end;
|
||||
|
||||
procedure TAuraFileStream<T>.AfterConstruction;
|
||||
var
|
||||
firstFile: TFuture<TAuraDataServer<T>.TDataFile>;
|
||||
begin
|
||||
inherited;
|
||||
|
||||
firstFile := TFuture<TAuraDataServer<T>.TDataFile>.Construct(function: TAuraDataServer<T>.TDataFile begin Result := FDataServer.FindFirstDataFile(FSymbol); end);
|
||||
|
||||
FCurrent :=
|
||||
firstFile.Chain<TDataStream>(
|
||||
function(const FileInfo: TAuraDataServer<T>.TDataFile): TDataStream
|
||||
begin
|
||||
if FileInfo.IsValid then
|
||||
begin
|
||||
// Changed: Use renamed field "FileInfo".
|
||||
Result.FileInfo := FileInfo;
|
||||
Result.Data := FDataServer.LoadDataFile(FileInfo.GetFullFileName);
|
||||
Result.Data.Done.Subscribe(FHasData);
|
||||
end;
|
||||
end
|
||||
);
|
||||
|
||||
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
|
||||
begin
|
||||
FNextData := FDataServer.LoadDataFile(FNextFileName);
|
||||
FNextData.Done.Subscribe(FHasData);
|
||||
end;
|
||||
PreloadNext;
|
||||
end;
|
||||
|
||||
procedure TAuraFileStream<T>.BeforeDestruction;
|
||||
begin
|
||||
FCurrent.WaitFor;
|
||||
FCurrent.Value.Data.WaitFor;
|
||||
FNext.WaitFor;
|
||||
FNext.Value.Data.WaitFor;
|
||||
inherited;
|
||||
end;
|
||||
|
||||
function TAuraFileStream<T>.GetChunk(var Data: array of TDataPoint<T>): Integer;
|
||||
var
|
||||
item: TDataPoint<T>;
|
||||
currData: TArray<TDataPoint<T>>;
|
||||
label
|
||||
loop;
|
||||
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
|
||||
loop:
|
||||
if not FCurrent.Done.IsSet then
|
||||
exit;
|
||||
if not FCurrent.Value.Data.Done.IsSet then
|
||||
exit;
|
||||
|
||||
if FCurrPosInFile >= Length(FCurrentData.Value) then
|
||||
// Changed: Use renamed field "FileInfo".
|
||||
if not FCurrent.Value.FileInfo.IsValid then
|
||||
exit;
|
||||
|
||||
if FCurrPosInFile >= Length(FCurrent.Value.Data.Value) then
|
||||
begin
|
||||
FCurrentData := FNextData;
|
||||
FCurrentFileName := FNextFileName;
|
||||
FCurrPosInFile := 0;
|
||||
FNextData := TFuture<TArray<TDataPoint<T>>>.Null;
|
||||
|
||||
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;
|
||||
FCurrent := FNext;
|
||||
PreloadNext;
|
||||
goto loop;
|
||||
end;
|
||||
|
||||
currData := FCurrentData.Value;
|
||||
|
||||
var currData := FCurrent.Value.Data.Value;
|
||||
var maxLen := Length(Data);
|
||||
while (Result < maxLen) and (FCurrPosInFile < Length(currData)) do
|
||||
begin
|
||||
@@ -662,8 +530,6 @@ begin
|
||||
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;
|
||||
@@ -675,92 +541,191 @@ begin
|
||||
Result := FHasData.Signal;
|
||||
end;
|
||||
|
||||
function TAuraFileStream<T>.GetSymbol: String;
|
||||
begin
|
||||
Result := FSymbol;
|
||||
end;
|
||||
|
||||
function TAuraFileStream<T>.IsHistory: Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
procedure TAuraFileStream<T>.PreloadNext;
|
||||
begin
|
||||
FNext :=
|
||||
FCurrent.Chain<TDataStream>(
|
||||
function(const Prev: TDataStream): TDataStream
|
||||
var
|
||||
nextFileInfo: TAuraDataServer<T>.TDataFile;
|
||||
begin
|
||||
// Changed: Use renamed field "FileInfo".
|
||||
if Prev.FileInfo.IsValid then
|
||||
begin
|
||||
nextFileInfo := TAuraDataServer<T>.FindNextDataFile(Prev.FileInfo);
|
||||
if nextFileInfo.IsValid then
|
||||
begin
|
||||
Result.FileInfo := nextFileInfo;
|
||||
Result.Data := FDataServer.LoadDataFile(nextFileInfo.GetFullFileName);
|
||||
Result.Data.Done.Subscribe(FHasData);
|
||||
end;
|
||||
end;
|
||||
end
|
||||
);
|
||||
end;
|
||||
|
||||
{ TAuraTABFileServer }
|
||||
|
||||
function TAuraTABFileServer.CreateStream(const Symbol: String): IDataStream<TAskBidItem>;
|
||||
var
|
||||
fileName: string;
|
||||
begin
|
||||
// FindFirstDataFile uses the server's path and the given symbol to locate the initial data file.
|
||||
fileName := FindFirstDataFile(FPath, Symbol);
|
||||
|
||||
// Exit if no file is found or if it is not a .tab file, preventing stream creation for invalid sources.
|
||||
if (fileName = '') or (not TPath.GetExtension(fileName).StartsWith('.tab')) then
|
||||
exit(nil);
|
||||
|
||||
// Creates a new stream instance, passing itself as the data server and the found filename.
|
||||
Result := TAuraFileStream<TAskBidItem>.Create(Self, fileName);
|
||||
Result := TAuraFileStream<TAskBidItem>.Create(Self, Symbol);
|
||||
end;
|
||||
|
||||
class function TAuraTABFileServer.TryParseFileName(
|
||||
const FileName: string;
|
||||
out PathValue, SymbolValue: string;
|
||||
out YearValue, MonthValue: Integer
|
||||
): Boolean;
|
||||
function TAuraTABFileServer.LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<TAskBidItem>>>;
|
||||
var
|
||||
fileNameNoPath: string;
|
||||
nameForParsing: string;
|
||||
loadedState: TState;
|
||||
loadedFiles: TArray<TFuture<TArray<TDataPoint<TAskBidItem>>>>;
|
||||
liveData: TFuture<TArray<TDataPoint<TAskBidItem>>>;
|
||||
tabFiles: TList<string>;
|
||||
liveFilePath: string;
|
||||
currentFileInfo: TDataFile;
|
||||
begin
|
||||
tabFiles := TList<string>.Create;
|
||||
try
|
||||
if TryParseFileName(FileName, currentFileInfo) then
|
||||
begin
|
||||
while currentFileInfo.IsValid do
|
||||
begin
|
||||
tabFiles.Add(currentFileInfo.GetFullFileName);
|
||||
currentFileInfo := FindNextDataFile(currentFileInfo);
|
||||
end;
|
||||
end;
|
||||
|
||||
liveFilePath := '';
|
||||
if tabFiles.Count > 0 then
|
||||
begin
|
||||
var lastTabFile := tabFiles.Last;
|
||||
if TryParseFileName(lastTabFile, currentFileInfo) then
|
||||
begin
|
||||
var liveFileBaseName := Format('%s_%d_%02d.tab-live', [currentFileInfo.Symbol, currentFileInfo.Year, currentFileInfo.Month]);
|
||||
var potentialLivePath := TPath.Combine(currentFileInfo.Path, liveFileBaseName);
|
||||
if TFile.Exists(potentialLivePath) then
|
||||
liveFilePath := potentialLivePath;
|
||||
end;
|
||||
end;
|
||||
|
||||
var loadedFileList := TList<TFuture<TArray<TDataPoint<TAskBidItem>>>>.Create;
|
||||
var loadStates := TList<TState>.Create;
|
||||
try
|
||||
for var fileStr in tabFiles do
|
||||
begin
|
||||
var data := LoadDataFile(fileStr);
|
||||
loadedFileList.Add(data);
|
||||
loadStates.Add(data.Done);
|
||||
end;
|
||||
|
||||
liveData := TFuture<TArray<TDataPoint<TAskBidItem>>>.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<TDataPoint<TAskBidItem>>>.Construct(
|
||||
loadedState,
|
||||
function: TArray<TDataPoint<TAskBidItem>>
|
||||
begin
|
||||
var tickList := TList<TDataPoint<TAskBidItem>>.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.Time then
|
||||
begin
|
||||
tickList.Add(iterTickData);
|
||||
overallLastTabTickTime := iterTickData.Time;
|
||||
end;
|
||||
Result := tickList.ToArray;
|
||||
finally
|
||||
tickList.Free;
|
||||
end;
|
||||
end
|
||||
);
|
||||
end;
|
||||
|
||||
class function TAuraTABFileServer.TryParseFileName(const FileName: string; out DataFile: TAuraDataServer<TAskBidItem>.TDataFile): Boolean;
|
||||
var
|
||||
fileNameNoPath, nameForParsing, baseName, ext: string;
|
||||
parts: TArray<string>;
|
||||
baseName: string;
|
||||
begin
|
||||
Result := False;
|
||||
PathValue := '';
|
||||
SymbolValue := '';
|
||||
YearValue := 0;
|
||||
MonthValue := 0;
|
||||
PathValue := TPath.GetDirectoryName(FileName);
|
||||
DataFile := Default(TDataFile);
|
||||
|
||||
DataFile.Path := TPath.GetDirectoryName(FileName);
|
||||
fileNameNoPath := TPath.GetFileName(FileName);
|
||||
nameForParsing := fileNameNoPath;
|
||||
|
||||
ext := '';
|
||||
if nameForParsing.EndsWith('_zip', True) then
|
||||
begin
|
||||
baseName := nameForParsing.Substring(0, nameForParsing.Length - 4);
|
||||
if TPath.GetExtension(baseName) <> '' then
|
||||
begin
|
||||
nameForParsing := baseName;
|
||||
end;
|
||||
ext := '_zip';
|
||||
nameForParsing := nameForParsing.Substring(0, nameForParsing.Length - 4);
|
||||
end;
|
||||
|
||||
nameForParsing := TPath.GetFileNameWithoutExtension(nameForParsing);
|
||||
parts := nameForParsing.Split(['_']);
|
||||
DataFile.Extension := TPath.GetExtension(nameForParsing) + ext;
|
||||
baseName := TPath.GetFileNameWithoutExtension(nameForParsing);
|
||||
|
||||
parts := baseName.Split(['_']);
|
||||
|
||||
if Length(parts) < 3 then
|
||||
exit;
|
||||
|
||||
if not TryStrToInt(parts[High(parts)], MonthValue) then
|
||||
if not TryStrToInt(parts[High(parts)], DataFile.Month) then
|
||||
exit;
|
||||
|
||||
if (MonthValue < 1) or (MonthValue > 12) then
|
||||
if (DataFile.Month < 1) or (DataFile.Month > 12) then
|
||||
begin
|
||||
MonthValue := 0;
|
||||
DataFile.Month := 0;
|
||||
exit;
|
||||
end;
|
||||
|
||||
if not TryStrToInt(parts[High(parts) - 1], YearValue) then
|
||||
if not TryStrToInt(parts[High(parts) - 1], DataFile.Year) then
|
||||
begin
|
||||
MonthValue := 0;
|
||||
DataFile.Month := 0;
|
||||
exit;
|
||||
end;
|
||||
|
||||
if YearValue <= 0 then
|
||||
if DataFile.Year <= 0 then
|
||||
begin
|
||||
YearValue := 0;
|
||||
MonthValue := 0;
|
||||
DataFile.Year := 0;
|
||||
DataFile.Month := 0;
|
||||
exit;
|
||||
end;
|
||||
|
||||
SymbolValue := string.Join('_', Copy(parts, 0, Length(parts) - 2));
|
||||
DataFile.Symbol := string.Join('_', Copy(parts, 0, Length(parts) - 2));
|
||||
|
||||
if SymbolValue = '' then
|
||||
if DataFile.Symbol = '' then
|
||||
begin
|
||||
PathValue := '';
|
||||
YearValue := 0;
|
||||
MonthValue := 0;
|
||||
DataFile.Path := '';
|
||||
DataFile.Year := 0;
|
||||
DataFile.Month := 0;
|
||||
exit;
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user