integrated TFuture

This commit is contained in:
Michael Schimmel
2025-06-04 01:55:24 +02:00
parent 06ec0db105
commit f571965762
8 changed files with 1223 additions and 227 deletions
+1 -3
View File
@@ -41,9 +41,7 @@ object ChartForm: TChartForm
Height = 23 Height = 23
Anchors = [akLeft, akTop, akRight] Anchors = [akLeft, akTop, akRight]
TabOrder = 0 TabOrder = 0
Text = Text = '\\COFFEE\TickData\Pepperstone\EURUSD_2017_02.tab_zip'
'C:\Users\Brummel\Documents\cAlgo\TickData\Pepperstone\EURUSD_201' +
'7.tab'
end end
object LoadButton: TButton object LoadButton: TButton
Left = 686 Left = 686
+4 -3
View File
@@ -543,12 +543,13 @@ var
InitialDirectory: string; InitialDirectory: string;
SelectedPath, SelectedSymbol: string; SelectedPath, SelectedSymbol: string;
SelectedYear: Integer; SelectedYear: Integer;
SelectedMonth: Integer;
IsValidSyntax: Boolean; IsValidSyntax: Boolean;
begin begin
OpenDialog := TOpenDialog.Create( Self ); OpenDialog := TOpenDialog.Create( Self );
try try
OpenDialog.Title := 'Open Tick Data File'; OpenDialog.Title := 'Open Tick Data File';
OpenDialog.Filter := 'Tick Data Files (*.tab)|*.TAB|All Files (*.*)|*.*'; OpenDialog.Filter := 'Tick Data Files (*.tab)|*.tab;*.tab_zip;*.tab_live|All Files (*.*)|*.*';
OpenDialog.DefaultExt := 'tab'; OpenDialog.DefaultExt := 'tab';
OpenDialog.Options := [ofFileMustExist, ofPathMustExist, ofHideReadOnly, ofEnableSizing]; OpenDialog.Options := [ofFileMustExist, ofPathMustExist, ofHideReadOnly, ofEnableSizing];
@@ -574,11 +575,11 @@ begin
Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - File selected via dialog: ' + OpenDialog.FileName ); Memo.Lines.Add( FormatDateTime( 'yyyy-mm-dd hh:nn:ss', Now ) + ' - File selected via dialog: ' + OpenDialog.FileName );
IsValidSyntax := Myc.TickLoader.TryParseFileName( OpenDialog.FileName, IsValidSyntax := Myc.TickLoader.TryParseFileName( OpenDialog.FileName,
SelectedPath, SelectedSymbol, SelectedYear ); SelectedPath, SelectedSymbol, SelectedYear, SelectedMonth );
if not IsValidSyntax then if not IsValidSyntax then
begin begin
MessageDlg( Format( 'Warning: The selected filename ''%s'' does not match the expected format (Prefix_Symbol_Year.tab).', MessageDlg( Format( 'Warning: The selected filename ''%s'' does not match the expected format (Prefix_Symbol_Year_Month.tab).',
[System.IOUtils.TPath.GetFileName( OpenDialog.FileName )] ), mtWarning, [mbOK], 0 ); [System.IOUtils.TPath.GetFileName( OpenDialog.FileName )] ), mtWarning, [mbOK], 0 );
Memo.Lines.Add( 'Warning: Incorrect filename syntax chosen by user.' ); Memo.Lines.Add( 'Warning: Incorrect filename syntax chosen by user.' );
end; end;
+3 -1
View File
@@ -1,10 +1,12 @@
program ChartDisplayProject; program ChartDisplayProject;
uses uses
FastMM5,
Vcl.Forms, Vcl.Forms,
ChartDisplayMain in 'ChartDisplayMain.pas' {ChartForm}, ChartDisplayMain in 'ChartDisplayMain.pas' {ChartForm},
Myc.OHLCCache in '..\common\Myc.OHLCCache.pas', Myc.OHLCCache in '..\common\Myc.OHLCCache.pas',
Myc.ChartDataController in '..\common\Myc.ChartDataController.pas'; Myc.ChartDataController in '..\common\Myc.ChartDataController.pas',
DataStreamer in 'DataStreamer.pas';
{$R *.res} {$R *.res}
+8 -10
View File
@@ -71,7 +71,7 @@
<UWP_DelphiLogo44>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png</UWP_DelphiLogo44> <UWP_DelphiLogo44>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png</UWP_DelphiLogo44>
<UWP_DelphiLogo150>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png</UWP_DelphiLogo150> <UWP_DelphiLogo150>$(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png</UWP_DelphiLogo150>
<SanitizedProjectName>ChartDisplayProject</SanitizedProjectName> <SanitizedProjectName>ChartDisplayProject</SanitizedProjectName>
<DCC_UnitSearchPath>..\common;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> <DCC_UnitSearchPath>..\common;T:\Myc\Src;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Locale>1031</VerInfo_Locale> <VerInfo_Locale>1031</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments=</VerInfo_Keys>
</PropertyGroup> </PropertyGroup>
@@ -110,6 +110,9 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''"> <PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
<DCC_RunTimeTypeInfo>true</DCC_RunTimeTypeInfo>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_2)'!=''"> <PropertyGroup Condition="'$(Cfg_2)'!=''">
<DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols> <DCC_LocalDebugSymbols>false</DCC_LocalDebugSymbols>
@@ -129,10 +132,10 @@
</DelphiCompile> </DelphiCompile>
<DCCReference Include="ChartDisplayMain.pas"> <DCCReference Include="ChartDisplayMain.pas">
<Form>ChartForm</Form> <Form>ChartForm</Form>
<FormType>dfm</FormType>
</DCCReference> </DCCReference>
<DCCReference Include="..\common\Myc.OHLCCache.pas"/> <DCCReference Include="..\common\Myc.OHLCCache.pas"/>
<DCCReference Include="..\common\Myc.ChartDataController.pas"/> <DCCReference Include="..\common\Myc.ChartDataController.pas"/>
<DCCReference Include="DataStreamer.pas"/>
<BuildConfiguration Include="Base"> <BuildConfiguration Include="Base">
<Key>Base</Key> <Key>Base</Key>
</BuildConfiguration> </BuildConfiguration>
@@ -153,12 +156,7 @@
<Source> <Source>
<Source Name="MainSource">ChartDisplayProject.dpr</Source> <Source Name="MainSource">ChartDisplayProject.dpr</Source>
</Source> </Source>
<Excluded_Packages> <Excluded_Packages/>
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k290.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp290.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k290.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
<Excluded_Packages Name="$(BDSBIN)\dclofficexp290.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
</Excluded_Packages>
</Delphi.Personality> </Delphi.Personality>
<Deployment Version="5"> <Deployment Version="5">
<DeployFile LocalName="Win32\Debug\ChartDisplayProject.exe" Configuration="Debug" Class="ProjectOutput"> <DeployFile LocalName="Win32\Debug\ChartDisplayProject.exe" Configuration="Debug" Class="ProjectOutput">
@@ -173,9 +171,9 @@
<Overwrite>true</Overwrite> <Overwrite>true</Overwrite>
</Platform> </Platform>
</DeployFile> </DeployFile>
<DeployFile LocalName="Win64\Debug\ChartDisplayProject.rsm" Configuration="Debug" Class="DebugSymbols"> <DeployFile LocalName="Win64\Release\ChartDisplayProject.exe" Configuration="Release" Class="ProjectOutput">
<Platform Name="Win64"> <Platform Name="Win64">
<RemoteName>ChartDisplayProject.rsm</RemoteName> <RemoteName>ChartDisplayProject.exe</RemoteName>
<Overwrite>true</Overwrite> <Overwrite>true</Overwrite>
</Platform> </Platform>
</DeployFile> </DeployFile>
+671
View File
@@ -0,0 +1,671 @@
unit DataStreamer;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.IOUtils, System.Threading,
System.Zip;
type
IDataPage = interface
['{B3E3C5B7-7C1A-4A8B-8B7E-15C9D9E0F4A6}']
function GetStream: TStream;
function GetSourceIdentifier: string;
function GetLastModificationTime: TDateTime;
end;
IRecordStreamReader<T: record> = interface
['{C1B7E8F0-5A0A-4F3B-BD28-18D5E5A3F7A0}']
function GetCurrentRecord: T;
function MoveToNextRecord: Boolean;
procedure Reset;
function GetCurrentRecordIndex: Int64;
function Seek(RecordIndex: Int64): Int64;
property CurrentRecord: T read GetCurrentRecord;
property CurrentRecordIndex: Int64 read GetCurrentRecordIndex;
end;
TLocalFilePage = class(TInterfacedObject, IDataPage)
private
FActualFilePath: string;
FMemoryStream: TMemoryStream;
FLastModifiedOnDisk: TDateTime;
function GetStream: TStream;
function GetSourceIdentifier: string;
function GetLastModificationTime: TDateTime;
public
constructor Create(const AActualFilePath: string; ADataStream: TMemoryStream; ALastModifiedTimeOnDisk: TDateTime);
destructor Destroy; override;
class function IsFileModified(const AFileName: string; KnownLastModified: TDateTime): Boolean;
end;
TStreamingFileRecordReader<T: record> = class(TInterfacedObject, IRecordStreamReader<T>)
private
FDirectoryPath: string;
FDataSetName: string;
FExtension: string;
FFilePaths: TStringList;
FCachedPages: TObjectDictionary<string, IDataPage>;
FPageCacheQueue: TQueue<string>;
FMaxCacheSize: Integer;
FPreloadPageTask: ITask;
FLockObject: TObject;
FCurrentPage: IDataPage;
FCurrentPageStream: TStream;
FCurrentPageIndexInFileList: Integer;
FRecordSize: Integer;
FCurrentRecord: T;
FRecordsInCurrentPage: Int64;
FRecordsReadFromCurrentPage: Int64;
FGlobalRecordIndex: Int64;
procedure ScanDirectoryAndMatchFiles;
function LoadPage(ActualFilePath: string): IDataPage;
procedure EnsurePageInCache(ActualFilePath: string; IsPreload: Boolean = False);
procedure EvictOldestPage;
procedure DoPreloadWork(const APathToPreload: string);
procedure StartPreloadNextPage;
procedure InvalidatePageIfModified(PageToCheck: IDataPage; IsCurrentPage: Boolean);
function ActivatePage(PageIndexInFileList: Integer): Boolean;
function GetRecordCountForFile(ActualFilePath: string): Int64;
function GetCurrentRecord: T;
function MoveToNextRecord: Boolean;
procedure Reset;
function GetCurrentRecordIndex: Int64;
function Seek(RecordIndex: Int64): Int64;
public
constructor Create(const ADirectoryPath: string; const ADataSetName: string; const AExtension: string; AMaxCacheSize: Integer = 2);
destructor Destroy; override;
end;
implementation
uses
System.DateUtils, System.Types, System.StrUtils,
System.Math;
{ TLocalFilePage }
constructor TLocalFilePage.Create(const AActualFilePath: string; ADataStream: TMemoryStream; ALastModifiedTimeOnDisk: TDateTime);
begin
inherited Create;
FActualFilePath := AActualFilePath;
FMemoryStream := ADataStream;
FMemoryStream.Position := 0;
FLastModifiedOnDisk := ALastModifiedTimeOnDisk;
end;
destructor TLocalFilePage.Destroy;
begin
FMemoryStream.Free;
inherited Destroy;
end;
function TLocalFilePage.GetStream: TStream;
begin
FMemoryStream.Position := 0;
result := FMemoryStream;
end;
function TLocalFilePage.GetSourceIdentifier: string;
begin
result := FActualFilePath;
end;
function TLocalFilePage.GetLastModificationTime: TDateTime;
begin
result := FLastModifiedOnDisk;
end;
class function TLocalFilePage.IsFileModified(const AFileName: string; KnownLastModified: TDateTime): Boolean;
var
currentModTime: TDateTime;
begin
if TFile.Exists(AFileName) then
begin
currentModTime := TFile.GetLastWriteTime(AFileName);
result := Abs(MilliSecondsBetween(currentModTime, KnownLastModified)) > 1000;
end
else
begin
result := True;
end;
end;
{ TStreamingFileRecordReader<T> }
constructor TStreamingFileRecordReader<T>.Create(const ADirectoryPath: string; const ADataSetName: string; const AExtension: string; AMaxCacheSize: Integer);
begin
inherited Create;
FLockObject := TObject.Create;
if not TDirectory.Exists(ADirectoryPath) then
raise EDirectoryNotFoundException.Create('Directory not found: ' + ADirectoryPath);
if Trim(ADataSetName) = '' then
raise EArgumentException.Create('DataSetName cannot be empty.');
if Trim(AExtension) = '' then
raise EArgumentException.Create('Extension cannot be empty.');
FDirectoryPath := IncludeTrailingPathDelimiter(ADirectoryPath);
FDataSetName := ADataSetName;
FExtension := AExtension;
if StartsText('.', FExtension) then
FExtension := Copy(FExtension, 2, Length(FExtension) -1);
FFilePaths := TStringList.Create;
FFilePaths.Sorted := True;
FCachedPages := TObjectDictionary<string, IDataPage>.Create([doOwnsValues]);
FPageCacheQueue := TQueue<string>.Create;
FMaxCacheSize := System.Math.Max(2, AMaxCacheSize);
FRecordSize := SizeOf(T);
if FRecordSize = 0 then
raise EArgumentException.Create('Record size for type T cannot be zero.');
FCurrentPageIndexInFileList := -1;
FGlobalRecordIndex := -1;
FRecordsReadFromCurrentPage := 0;
FRecordsInCurrentPage := 0;
FCurrentPage := nil;
FCurrentPageStream := nil;
ScanDirectoryAndMatchFiles;
Reset;
end;
destructor TStreamingFileRecordReader<T>.Destroy;
var
taskToWaitFor: ITask;
begin
taskToWaitFor := nil;
TMonitor.Enter(FLockObject);
try
taskToWaitFor := FPreloadPageTask;
FPreloadPageTask := nil;
finally
TMonitor.Exit(FLockObject);
end;
if (taskToWaitFor <> nil) and (taskToWaitFor.Status in [TTaskStatus.Created, TTaskStatus.WaitingToRun, TTaskStatus.Running]) then
begin
// Consider taskToWaitFor.Cancel; taskToWaitFor.Wait;
end;
FFilePaths.Free;
FCachedPages.Free;
FPageCacheQueue.Free;
FLockObject.Free;
inherited Destroy;
end;
procedure TStreamingFileRecordReader<T>.ScanDirectoryAndMatchFiles;
var
allFiles: TStringDynArray;
fileNameOnly, filePath: string;
expectedPrefix, baseExtensionPattern, zipExtensionPattern: string;
begin
TMonitor.Enter(FLockObject);
try
FFilePaths.Clear;
allFiles := TDirectory.GetFiles(FDirectoryPath);
expectedPrefix := FDataSetName + '_';
baseExtensionPattern := '.' + FExtension;
zipExtensionPattern := baseExtensionPattern + '_zip';
for filePath in allFiles do
begin
fileNameOnly := ExtractFileName(filePath);
if StartsText(expectedPrefix, fileNameOnly) then
begin
if EndsText(zipExtensionPattern, fileNameOnly.ToLower) or EndsText(baseExtensionPattern, fileNameOnly.ToLower) then
begin
FFilePaths.Add(filePath);
end;
end;
end;
finally
TMonitor.Exit(FLockObject);
end;
end;
function TStreamingFileRecordReader<T>.LoadPage(ActualFilePath: string): IDataPage;
var
dataStream: TMemoryStream;
fileLastModified: TDateTime;
zipFile: TZipFile;
isCompressed: Boolean;
fileExtensionOnDisk: string;
decompressionStream: TStream;
localHeader: TZipHeader;
fileNameInZip: string;
begin
dataStream := TMemoryStream.Create;
try
fileLastModified := TFile.GetLastWriteTime(ActualFilePath);
fileExtensionOnDisk := ExtractFileExt(ActualFilePath);
isCompressed := EndsText('_zip', fileExtensionOnDisk.ToLower);
if isCompressed then
begin
zipFile := TZipFile.Create;
try
zipFile.Open(ActualFilePath, zmRead);
if zipFile.FileCount > 0 then
begin
fileNameInZip := zipFile.FileNames[0];
zipFile.Read(fileNameInZip, decompressionStream, localHeader);
try
dataStream.CopyFrom(decompressionStream, 0);
finally
decompressionStream.Free;
end;
end
else
raise Exception.Create('Compressed file is empty: ' + ActualFilePath);
finally
zipFile.Free;
end;
end
else
dataStream.LoadFromFile(ActualFilePath);
dataStream.Position := 0;
result := TLocalFilePage.Create(ActualFilePath, dataStream, fileLastModified);
except
dataStream.Free;
raise;
end;
end;
procedure TStreamingFileRecordReader<T>.EnsurePageInCache(ActualFilePath: string; IsPreload: Boolean);
var
page: IDataPage;
newlyLoadedPage: IDataPage;
begin
// Assumes FLockObject is held by the caller
if not FCachedPages.TryGetValue(ActualFilePath, page) then
begin
if FCachedPages.Count >= FMaxCacheSize then
EvictOldestPage;
newlyLoadedPage := LoadPage(ActualFilePath);
FCachedPages.Add(ActualFilePath, newlyLoadedPage);
FPageCacheQueue.Enqueue(ActualFilePath);
end
else
begin
if not IsPreload then
InvalidatePageIfModified(page, page = FCurrentPage);
end;
end;
procedure TStreamingFileRecordReader<T>.EvictOldestPage;
var
oldestFilePath: string;
begin
// Assumes FLockObject is held by the caller
if FPageCacheQueue.Count > 0 then
begin
oldestFilePath := FPageCacheQueue.Dequeue;
if FCachedPages.ContainsKey(oldestFilePath) then
FCachedPages.Remove(oldestFilePath);
end;
end;
procedure TStreamingFileRecordReader<T>.InvalidatePageIfModified(PageToCheck: IDataPage; IsCurrentPage: Boolean);
var
actualFilePath: string;
reloadedPage: IDataPage;
begin
// Assumes FLockObject is held by the caller
if PageToCheck = nil then exit;
actualFilePath := PageToCheck.GetSourceIdentifier;
if TLocalFilePage.IsFileModified(actualFilePath, PageToCheck.GetLastModificationTime) then
begin
FCachedPages.Remove(actualFilePath);
if IsCurrentPage then
begin
FCurrentPage := nil;
FCurrentPageStream := nil;
reloadedPage := LoadPage(actualFilePath);
FCachedPages.Add(actualFilePath, reloadedPage);
FCurrentPage := reloadedPage;
FCurrentPageStream := FCurrentPage.GetStream;
if (FCurrentPageStream <> nil) and (FCurrentPageStream.Size >= FRecordSize) then
FRecordsInCurrentPage := FCurrentPageStream.Size div FRecordSize
else
FRecordsInCurrentPage := 0;
if (FRecordsReadFromCurrentPage * FRecordSize < FCurrentPageStream.Size) and (FRecordsReadFromCurrentPage <= FRecordsInCurrentPage) then
FCurrentPageStream.Position := FRecordsReadFromCurrentPage * FRecordSize
else
begin
FCurrentPageStream.Position := 0;
FRecordsReadFromCurrentPage := 0;
end;
end;
end;
end;
procedure TStreamingFileRecordReader<T>.DoPreloadWork(const APathToPreload: string);
begin
try
TMonitor.Enter(FLockObject);
try
if not FCachedPages.ContainsKey(APathToPreload) then
EnsurePageInCache(APathToPreload, True);
finally
TMonitor.Exit(FLockObject);
end;
except
on E: Exception do
begin
// Optional: Log error E.Message
end;
end;
end;
procedure TStreamingFileRecordReader<T>.StartPreloadNextPage;
var
nextPageIndexToLoad: Integer;
nextFilePath: string;
currentTask: ITask;
begin
TMonitor.Enter(FLockObject);
try
currentTask := FPreloadPageTask;
if (currentTask <> nil) and (currentTask.Status in [TTaskStatus.Created, TTaskStatus.WaitingToRun, TTaskStatus.Running]) then
exit;
nextPageIndexToLoad := FCurrentPageIndexInFileList + 1;
if nextPageIndexToLoad < FFilePaths.Count then
begin
nextFilePath := FFilePaths[nextPageIndexToLoad];
if FCachedPages.ContainsKey(nextFilePath) then
exit;
FPreloadPageTask := TTask.Run(
procedure
begin
DoPreloadWork(nextFilePath);
end
);
end;
finally
TMonitor.Exit(FLockObject);
end;
end;
function TStreamingFileRecordReader<T>.ActivatePage(PageIndexInFileList: Integer): Boolean;
var
actualFilePath: string;
pageFromCache: IDataPage;
begin
result := False;
if (PageIndexInFileList < 0) or (PageIndexInFileList >= FFilePaths.Count) then
begin FCurrentPage := nil; FCurrentPageStream := nil; exit; end;
actualFilePath := FFilePaths[PageIndexInFileList];
TMonitor.Enter(FLockObject);
try
EnsurePageInCache(actualFilePath, False);
if not FCachedPages.TryGetValue(actualFilePath, pageFromCache) then
begin FCurrentPage := nil; FCurrentPageStream := nil; exit; end;
FCurrentPage := pageFromCache;
FCurrentPageStream := FCurrentPage.GetStream;
FCurrentPageIndexInFileList := PageIndexInFileList;
FRecordsReadFromCurrentPage := 0;
if (FCurrentPageStream <> nil) and (FCurrentPageStream.Size >= FRecordSize) then
FRecordsInCurrentPage := FCurrentPageStream.Size div FRecordSize
else
FRecordsInCurrentPage := 0;
if FRecordsInCurrentPage > 0 then
begin
result := True;
StartPreloadNextPage;
end;
finally
TMonitor.Exit(FLockObject);
end;
end;
function TStreamingFileRecordReader<T>.GetRecordCountForFile(ActualFilePath: string): Int64;
var
page: IDataPage;
stream: TStream;
begin
result := 0;
EnsurePageInCache(ActualFilePath, False); // Assumes FLockObject is held by caller
if FCachedPages.TryGetValue(ActualFilePath, page) then
begin
stream := page.GetStream;
if (stream <> nil) and (stream.Size >= FRecordSize) then
result := stream.Size div FRecordSize;
end;
end;
function TStreamingFileRecordReader<T>.GetCurrentRecord: T;
begin
if FGlobalRecordIndex < 0 then
Result := System.Default(T)
else
Result := FCurrentRecord;
end;
function TStreamingFileRecordReader<T>.GetCurrentRecordIndex: Int64;
begin
result := FGlobalRecordIndex;
end;
function TStreamingFileRecordReader<T>.MoveToNextRecord: Boolean;
var
bytesRead: NativeInt; // ReadData<T> returns NativeInt
attemptedNextPageActivation: Boolean;
begin
result := False;
attemptedNextPageActivation := False;
TMonitor.Enter(FLockObject);
try
while True do
begin
if FCurrentPageStream = nil then
begin
if attemptedNextPageActivation or (FCurrentPageIndexInFileList >= FFilePaths.Count - 1) then
begin FGlobalRecordIndex := -1; exit; end;
if not ActivatePage(FCurrentPageIndexInFileList + 1) then
begin FGlobalRecordIndex := -1; exit; end;
attemptedNextPageActivation := True;
end;
if FRecordsReadFromCurrentPage >= FRecordsInCurrentPage then
begin
if attemptedNextPageActivation or (FCurrentPageIndexInFileList >= FFilePaths.Count - 1) then
begin FGlobalRecordIndex := -1; exit; end;
FCurrentPageStream := nil;
if not ActivatePage(FCurrentPageIndexInFileList + 1) then
begin FGlobalRecordIndex := -1; exit; end;
attemptedNextPageActivation := True;
continue;
end;
FCurrentPageStream.Position := FRecordsReadFromCurrentPage * FRecordSize;
if (FCurrentPageStream.Size - FCurrentPageStream.Position) < FRecordSize then
begin
FRecordsReadFromCurrentPage := FRecordsInCurrentPage;
attemptedNextPageActivation := False;
FCurrentPageStream := nil;
continue;
end;
// Verwende die generische TStream.ReadData<T> Methode
bytesRead := FCurrentPageStream.ReadData<T>(FCurrentRecord); // User line 518 context
if bytesRead = FRecordSize then // FRecordSize is SizeOf(T)
begin
Inc(FRecordsReadFromCurrentPage);
if FGlobalRecordIndex < 0 then
FGlobalRecordIndex := 0
else
Inc(FGlobalRecordIndex); // << User line 518 (E2010), hopefully fixed by ReadData<T>
result := True; exit;
end
else
begin
FRecordsReadFromCurrentPage := FRecordsInCurrentPage;
attemptedNextPageActivation := False;
FCurrentPageStream := nil;
continue;
end;
end;
finally
TMonitor.Exit(FLockObject);
end;
end;
procedure TStreamingFileRecordReader<T>.Reset;
var
taskToCheck: ITask;
begin
TMonitor.Enter(FLockObject);
try
taskToCheck := FPreloadPageTask;
if (taskToCheck <> nil) and (taskToCheck.Status in [TTaskStatus.Created, TTaskStatus.WaitingToRun, TTaskStatus.Running]) then
begin
// Cancellation logic
end;
FPreloadPageTask := nil;
FCurrentPageIndexInFileList := -1;
FGlobalRecordIndex := -1;
FRecordsReadFromCurrentPage := 0;
FRecordsInCurrentPage := 0;
FCurrentPage := nil;
FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
if FFilePaths.Count > 0 then
if not ActivatePage(0) then;
finally
TMonitor.Exit(FLockObject);
end;
end;
function TStreamingFileRecordReader<T>.Seek(RecordIndex: Int64): Int64;
var
pageIdx: Integer;
cumulativeRecordOffset: Int64;
recordsInCurrentFile: Int64;
recordOffsetInPage: Int64;
effectiveTargetIdx: Int64;
i: Integer;
sumRecordsBeforeThisPage: Int64;
bytesRead: NativeInt; // For ReadData<T>
begin
TMonitor.Enter(FLockObject);
try
if FFilePaths.Count = 0 then
begin
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
end;
effectiveTargetIdx := RecordIndex;
if effectiveTargetIdx < 0 then effectiveTargetIdx := 0;
cumulativeRecordOffset := 0;
for pageIdx := 0 to FFilePaths.Count - 1 do
begin
recordsInCurrentFile := GetRecordCountForFile(FFilePaths[pageIdx]);
if effectiveTargetIdx < cumulativeRecordOffset + recordsInCurrentFile then
begin
if not ActivatePage(pageIdx) then
begin
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
end;
recordOffsetInPage := effectiveTargetIdx - cumulativeRecordOffset;
FCurrentPageStream.Position := recordOffsetInPage * FRecordSize;
// Verwende die generische TStream.ReadData<T> Methode
bytesRead := FCurrentPageStream.ReadData<T>(FCurrentRecord);
if bytesRead = FRecordSize then
begin
FGlobalRecordIndex := effectiveTargetIdx; FRecordsReadFromCurrentPage := recordOffsetInPage + 1;
result := FGlobalRecordIndex; exit;
end
else
begin // User line ~604 context (E2008)
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
end;
end;
inc(cumulativeRecordOffset, recordsInCurrentFile);
end;
if cumulativeRecordOffset = 0 then // User line ~616 context (E2029) for assignment after this block
begin
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
end;
for pageIdx := FFilePaths.Count - 1 downto 0 do
begin
recordsInCurrentFile := GetRecordCountForFile(FFilePaths[pageIdx]);
if recordsInCurrentFile > 0 then
begin
if not ActivatePage(pageIdx) then
begin
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
end;
recordOffsetInPage := recordsInCurrentFile - 1;
FCurrentPageStream.Position := recordOffsetInPage * FRecordSize;
// Verwende die generische TStream.ReadData<T> Methode
bytesRead := FCurrentPageStream.ReadData<T>(FCurrentRecord);
if bytesRead = FRecordSize then
begin
sumRecordsBeforeThisPage := 0;
for i := 0 to pageIdx - 1 do
sumRecordsBeforeThisPage := sumRecordsBeforeThisPage + GetRecordCountForFile(FFilePaths[i]);
FGlobalRecordIndex := sumRecordsBeforeThisPage + recordOffsetInPage; // User line 616 context (E2029/E2014)
FRecordsReadFromCurrentPage := recordOffsetInPage + 1;
result := FGlobalRecordIndex; exit;
end
else
begin // User line ~639 context (E2008)
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
end;
end;
end;
FCurrentPageIndexInFileList := -1; FGlobalRecordIndex := -1; FCurrentPage := nil; FCurrentPageStream := nil;
FCurrentRecord := System.Default(T);
result := -1; exit;
finally
TMonitor.Exit(FLockObject);
end;
end;
end.
+127
View File
@@ -0,0 +1,127 @@
<map version="freeplane 1.12.1">
<!--To view this file, download free mind mapping software Freeplane from https://www.freeplane.org -->
<node TEXT="Data loader&#xa;" FOLDED="false" ID="1" CREATED="1748275449538" MODIFIED="1748275581071"><hook NAME="MapStyle">
<properties edgeColorConfiguration="#808080ff,#ff0000ff,#0000ffff,#00ff00ff,#ff00ffff,#00ffffff,#7c0000ff,#00007cff,#007c00ff,#7c007cff,#007c7cff,#7c7c00ff" show_icon_for_attributes="true" show_tags="UNDER_NODES" show_note_icons="true" fit_to_viewport="false" show_icons="BESIDE_NODES" showTagCategories="false"/>
<tags category_separator="::"/>
<map_styles>
<stylenode LOCALIZED_TEXT="styles.root_node" STYLE="oval" UNIFORM_SHAPE="true" VGAP_QUANTITY="24 pt">
<font SIZE="24"/>
<stylenode LOCALIZED_TEXT="styles.predefined" POSITION="bottom_or_right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="default" ID="ID_271890427" ICON_SIZE="12 pt" COLOR="#000000" STYLE="fork">
<arrowlink SHAPE="CUBIC_CURVE" COLOR="#000000" WIDTH="2" TRANSPARENCY="200" DASH="" FONT_SIZE="9" FONT_FAMILY="SansSerif" DESTINATION="ID_271890427" STARTARROW="NONE" ENDARROW="DEFAULT"/>
<font NAME="SansSerif" SIZE="10" BOLD="false" ITALIC="false"/>
<richcontent TYPE="DETAILS" CONTENT-TYPE="plain/auto"/>
<richcontent TYPE="NOTE" CONTENT-TYPE="plain/auto"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.details"/>
<stylenode LOCALIZED_TEXT="defaultstyle.tags">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.attributes">
<font SIZE="9"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.note" COLOR="#000000" BACKGROUND_COLOR="#ffffff" TEXT_ALIGN="LEFT"/>
<stylenode LOCALIZED_TEXT="defaultstyle.floating">
<edge STYLE="hide_edge"/>
<cloud COLOR="#f0f0f0" SHAPE="ROUND_RECT"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.selection" BACKGROUND_COLOR="#afd3f7" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#afd3f7"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.user-defined" POSITION="bottom_or_right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="styles.topic" COLOR="#18898b" STYLE="fork">
<font NAME="Liberation Sans" SIZE="10" BOLD="true"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.subtopic" COLOR="#cc3300" STYLE="fork">
<font NAME="Liberation Sans" SIZE="10" BOLD="true"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.subsubtopic" COLOR="#669900">
<font NAME="Liberation Sans" SIZE="10" BOLD="true"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.important" ID="ID_67550811">
<icon BUILTIN="yes"/>
<arrowlink COLOR="#003399" TRANSPARENCY="255" DESTINATION="ID_67550811"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.flower" COLOR="#ffffff" BACKGROUND_COLOR="#255aba" STYLE="oval" TEXT_ALIGN="CENTER" BORDER_WIDTH_LIKE_EDGE="false" BORDER_WIDTH="22 pt" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#f9d71c" BORDER_DASH_LIKE_EDGE="false" BORDER_DASH="CLOSE_DOTS" MAX_WIDTH="6 cm" MIN_WIDTH="3 cm"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.AutomaticLayout" POSITION="bottom_or_right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="AutomaticLayout.level.root" COLOR="#000000" STYLE="oval" SHAPE_HORIZONTAL_MARGIN="10 pt" SHAPE_VERTICAL_MARGIN="10 pt">
<font SIZE="18"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,1" COLOR="#0033ff">
<font SIZE="16"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,2" COLOR="#00b439">
<font SIZE="14"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,3" COLOR="#990000">
<font SIZE="12"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,4" COLOR="#111111">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,5"/>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,6"/>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,7"/>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,8"/>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,9"/>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,10"/>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,11"/>
</stylenode>
</stylenode>
</map_styles>
</hook>
<node TEXT="IDataPageStreamReader" POSITION="bottom_or_right" ID="24" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Zeigt auf den aktuellen Datenpunkt" ID="25" CREATED="1748275449538" MODIFIED="1748275449538"/>
<node TEXT="Nummer des Datenpunkts" ID="28" CREATED="1748275449538" MODIFIED="1748275449538"/>
<node TEXT="MoveNext springt zum nächsten Datenpunkt." ID="30" CREATED="1748275449538" MODIFIED="1748275449538"/>
<node TEXT="Seek(Idx)-Funktion springt zum Datenpunkt am gegebenen Index, relativ zum Anfang des Datensatzes." ID="40" CREATED="1748275449538" MODIFIED="1748275539454">
<hook NAME="AlwaysUnfoldedNode"/>
<node TEXT="Liefert den Index des tatsächlich erreichten Datensatzes zurück." ID="42" CREATED="1748275449538" MODIFIED="1748275449538"/>
</node>
<node TEXT="Die Gesamtzahl der Datensätze kann NICHT geliefert werden." ID="44" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Dafür müssten alle Dateien geöffnet werden, das wollen wir nicht." ID="45" CREATED="1748275449538" MODIFIED="1748275449538"/>
</node>
</node>
<node TEXT="Streaming API für extrem große Datenpakete" POSITION="bottom_or_right" ID="3" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Daten sind lokal abgespeichert" ID="4" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Alle in einem Verzeichnis" ID="5" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Namensschema nach Zeitraum sortiert." ID="31" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Monate" ID="32" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Dateiname: &quot;DatenSatzName_JJJJ_MM.Extension&quot;" ID="33" CREATED="1748275449538" MODIFIED="1748275449538"/>
</node>
<node TEXT="Jahre" ID="34" CREATED="1748275449538" MODIFIED="1748275449538">
<node TEXT="Dateiname: &quot;DatenSatzName_JJJJ.Extension&quot;" ID="35" CREATED="1748275449539" MODIFIED="1748275449539"/>
</node>
<node TEXT="Komprimierte Dateien" ID="37" CREATED="1748275449539" MODIFIED="1748275449539">
<node TEXT="Hier wird an die Extension ein &quot;_zip&quot; angehängt." ID="38" CREATED="1748275449539" MODIFIED="1748275449539"/>
</node>
</node>
<node TEXT="Der gesamte Datensatz wird durch DatenSatzName und Extension identifiziert." ID="36" CREATED="1748275449539" MODIFIED="1748275449539">
<node TEXT="Der Loader muss die richtigen Dateinamen selbst herausfinden. Sie können entweder nach Monaten oder Jahren sortiert sein und sie können zusätzlich komprimiert sein." ID="39" CREATED="1748275449539" MODIFIED="1748275449539"/>
</node>
</node>
<node TEXT="Paging algorhytmus" ID="6" CREATED="1748275449539" MODIFIED="1748275449539">
<node TEXT="Es wird eine Reader benötigt, der sequentiell die Daten einließt." ID="10" CREATED="1748275449539" MODIFIED="1748275449539">
<node TEXT="Reader ließt immer die aktuelle Page und im Hintergrund die potentiell nächste Page ein." ID="11" CREATED="1748275449539" MODIFIED="1748275449539">
<node TEXT="Möglich wenig Latenz beim Zugriff." ID="16" CREATED="1748275449539" MODIFIED="1748275449539"/>
<node TEXT="Es werden immer nur n Pages im Speicher gehalten. Wobei n &gt;= 2 ist." ID="17" CREATED="1748275449539" MODIFIED="1748275449539"/>
</node>
<node TEXT="Dateien könnten extern aktualisiert worden sein." ID="19" CREATED="1748275449539" MODIFIED="1748275449539">
<node TEXT="Die eingelesene Page muss dann verworfen werden." ID="20" CREATED="1748275449540" MODIFIED="1748275449540"/>
</node>
</node>
<node TEXT="Jede Datei repräsentiert eine &quot;Page&quot;" ID="12" CREATED="1748275449540" MODIFIED="1748275449540">
<node TEXT="Dateien werden also immer komplett eingelesen." ID="13" CREATED="1748275449540" MODIFIED="1748275449540"/>
<node TEXT="Dateien müssen also nicht geblockt werden" ID="14" CREATED="1748275449540" MODIFIED="1748275449540">
<node TEXT="Besser für Netzwerkverkehr und unterstützt Caching auf Betriebssystemebene." ID="15" CREATED="1748275449540" MODIFIED="1748275449540"/>
</node>
</node>
</node>
</node>
<node TEXT="Es handelt sich um Sequenzen von gleichgroßen Datenpunkten, also um Records." ID="21" CREATED="1748275449540" MODIFIED="1748275449540">
<node TEXT="Der Loader bekommt den Typ des Records als generischen Parameter T mit." ID="22" CREATED="1748275449540" MODIFIED="1748275449540"/>
</node>
</node>
</node>
</map>
+4 -5
View File
@@ -4,6 +4,7 @@ interface
uses uses
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils, System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
Myc.Futures,
Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants
type type
@@ -79,7 +80,6 @@ begin
FOHLCCacheList.Add(nil); // Add a nil item to the list FOHLCCacheList.Add(nil); // Add a nil item to the list
end; end;
SetLength(FTickDataArray, 0);
if FMemoLog <> nil then if FMemoLog <> nil then
FMemoLog.Add('TChartDataController instance created. Base timeframes initialized.'); FMemoLog.Add('TChartDataController instance created. Base timeframes initialized.');
end; end;
@@ -211,16 +211,15 @@ procedure TChartDataController.LoadTickDataSeries(const AFileName: string);
begin begin
ClearAllDataAndCaches(); ClearAllDataAndCaches();
try try
FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName); FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName).WaitFor;
if FMemoLog <> nil then if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Loaded %d tick records from series: %s', FMemoLog.Add(Format('DataController: Loading tick records from series: %s',
[Length(FTickDataArray), AFileName])); [AFileName]));
except except
on E: Exception do on E: Exception do
begin begin
if FMemoLog <> nil then if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Error loading tick data from %s - %s', [AFileName, E.Message])); FMemoLog.Add(Format('DataController: Error loading tick data from %s - %s', [AFileName, E.Message]));
SetLength(FTickDataArray, 0);
end; end;
end; end;
end; end;
+380 -180
View File
@@ -1,33 +1,42 @@
unit Myc.TickLoader; unit Myc.TickLoader;
(*------------------------------------------------------------------------------ (* ------------------------------------------------------------------------------
Unit: Myc.TickLoader Unit:         Myc.TickLoader
Author: Delphi Algo Trading (Assistant) Author:       Delphi Algo Trading (Assistant)
Date: May 22, 2025 Date:         May 27, 2025
Description: Description:
This unit provides functions to load tick data (TTickData) from This unit provides functions to load tick data (TTickData) from
binary files. The data originates from a C# application binary files. The data originates from a C# application
and has the structure OADateTime (Double), Ask (Single), and Bid (Single). and has the structure OADateTime (Double), Ask (Single), and Bid (Single).
The files now follow the naming convention: Symbol_Year.tab. The files follow the naming convention:
(Prefix has been removed from the convention). For .tab files:      Symbol_Year_MM.tab OR Symbol_Year_MM.tab_zip
For .tab-live files: Symbol_Year_MM.tab-live (NEVER compressed)
(MM represents the two-digit month, e.g., 01 for January, 12 for December)
If both compressed (.tab_zip) and uncompressed (.tab) versions of a .tab
file exist, the compressed version is preferred. Files ending with _zip
are expected to be ZIP archives containing the actual data file
(e.g., Symbol_Year_MM.tab).
Loading occurs in two phases: Loading occurs in two phases:
1. All .tab files are processed chronologically. Unique ticks are loaded, 1. All .tab files (regular or _zip) are processed chronologically by
and the timestamp of the latest tick across all .tab files is determined month and year. Unique ticks are loaded, and the timestamp of the
(overallLastTabTickTime). Assumes timestamps are unique across all .tab files. latest tick across all .tab files is determined (overallLastTabTickTime).
2. All .tab-live files (for years corresponding to found .tab files) are Assumes timestamps are unique across all .tab files.
processed. Ticks from a .tab-live file are only integrated if their 2. All .tab-live files (which are never compressed), for months/years
timestamp is strictly later than overallLastTabTickTime. Assumes such corresponding to found .tab files, are processed. Ticks from a
ticks are globally unique. .tab-live file are only integrated if their timestamp is strictly later
than overallLastTabTickTime. Assumes such ticks are globally unique.
If a .tab-live file results in no new data being added under these rules, If a .tab-live file results in no new data being added under these rules,
it is deleted after processing. it is deleted after processing.
The loading function automatically detects subsequent yearly .tab files. The loading function automatically detects subsequent monthly/yearly files.
Main Function: Main Function:
LoadTickDataSeries - Loads a series of tick data files. LoadTickDataSeries - Loads a series of tick data files.
------------------------------------------------------------------------------*) ------------------------------------------------------------------------------ *)
interface interface
@@ -35,7 +44,9 @@ uses
System.SysUtils, System.SysUtils,
System.Classes, System.Classes,
System.Generics.Collections, System.Generics.Collections,
System.IOUtils; System.IOUtils,
Myc.Futures, // Added for TFuture in ReadFileTickData
Myc.Signals; // Added for IMycState (dependency of TFuture)
type type
TTickData = packed record TTickData = packed record
@@ -43,204 +54,221 @@ type
Ask: Single; Ask: Single;
Bid: Single; Bid: Single;
end; end;
PTTickData = ^TTickData; PTTickData = ^TTickData;
// TryParseFileName now expects filenames like "Symbol_Year" (or "Symbol_Part2_Year") // TryParseFileName parses filenames like "Symbol_Year_MM.Extension" or "Symbol_Year_MM.Extension_zip"
// PrefixValue will be returned as an empty string. // to extract Path, Symbol, Year, and Month.
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean; function TryParseFileName( const FileName: string;
function LoadTickDataSeries(const FileName: string): TArray<TTickData>; out PathValue, SymbolValue: string;
out YearValue, MonthValue: Integer ): Boolean;
function LoadTickDataSeries(const FileName: string): TFuture<TArray<TTickData>>;
// Asynchronously reads tick data from a single specified file.
// Returns a TFuture that will provide an array of TTickData.
function ReadFileTickData( var LoadGate: TLatch; const FileName: string ): TFuture<TArray<TTickData>>;
implementation implementation
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean; uses
System.Zip,
// Myc.Futures, Myc.Signals already in interface uses for TFuture type visibility
Myc.TaskManager; // Added for the global TaskManager used by TFuture.Construct
function TryParseFileName( const FileName: string;
out PathValue, SymbolValue: string;
out YearValue, MonthValue: Integer ): Boolean;
var var
fileNameOnly: string; fileNameNoPath: string;
nameForParsing: string;
parts: TArray<string>; parts: TArray<string>;
begin begin
Result := False; Result := False; // Default to failure
PathValue := TPath.GetDirectoryName(FileName); PathValue := ''; // Initialize all out parameters
fileNameOnly := TPath.GetFileNameWithoutExtension(FileName); // Handles any extension for parsing basename
parts := fileNameOnly.Split(['_']);
// Expected format is Symbol_Year or SymbolPart1_SymbolPart2_Year, so at least 2 parts.
if Length(parts) < 2 then
begin
SymbolValue := ''; YearValue := 0; // Clear out params on minimum parts failure
Exit;
end;
// Year is the last part.
if not TryStrToInt(parts[High(parts)], YearValue) then
begin
SymbolValue := ''; // Clear SymbolValue as well on year parse failure
Exit;
end;
// Symbol consists of all parts before the year part.
// Copy all parts except the last one (which is the Year).
// The second parameter of Copy (for TArray<string>) is the starting index,
// the third parameter is the count of elements to copy.
// We want to copy elements from index 0 up to (but not including) the last element.
// So, the count is High(parts) (which is Length(parts) - 1).
if High(parts) > 0 then // Ensure there's at least one part for the symbol
SymbolValue := string.Join('_', Copy(parts, 0, High(parts)))
else if Length(parts) = 1 then // Edge case: File is "Year.tab" - this is invalid by new rule "Symbol_Year"
begin SymbolValue := ''; Exit; end // Or handle as Symbol = parts[0] if "Year" itself is a symbol
else // Should not happen if Length(parts) < 2 already exited
SymbolValue := ''; SymbolValue := '';
YearValue := 0;
MonthValue := 0;
PathValue := TPath.GetDirectoryName( FileName );
fileNameNoPath := TPath.GetFileName( FileName );
nameForParsing := fileNameNoPath;
if nameForParsing.EndsWith( '_zip', True ) then
begin
nameForParsing := nameForParsing.Substring( 0, nameForParsing.Length - 4 );
end;
nameForParsing := TPath.GetFileNameWithoutExtension( nameForParsing ); // e.g., "EURUSD_2023_01"
parts := nameForParsing.Split( ['_'] );
if Length( parts ) < 3 then // Need at least Symbol_Year_Month
begin
exit;
end;
// 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;
// Symbol consists of all parts before Year and Month. Count = Length(parts) - 2
SymbolValue := string.Join( '_', Copy( parts, 0, Length( parts ) - 2 ) );
if SymbolValue = '' then // Ensure symbol is not empty
begin
exit;
end;
Result := True; Result := True;
end; end;
function LoadTickDataSeries(const FileName: string): TArray<TTickData>; function LoadTickDataSeries(const FileName: string): TFuture<TArray<TTickData>>;
// Local type declaration for storing file information for phase 1 processing
type
TPhase1FileEntry = record
Path: string;
Year: Integer;
Month: Integer;
end;
var var
tickList: TList<TTickData>; initialYear, initialMonth: Integer;
initialYear, currentTabYear, liveProcessingYear: Integer; currentTabYear, currentTabMonth: Integer;
pathName, symbolName: string; // prefixNameOut will be an empty out param pathName, symbolName: string;
tabFileName, liveFileName: string; tabFileEntryRecord: TPhase1FileEntry; // Variable to construct record instances
overallLastTabTickTime: Double; loadedState: TState;
maxTabYearFound: Integer; loadedFiles: TArray<TFuture<TArray<TTickData>>>;
liveData: TFuture<TArray<TTickData>>;
procedure ReadFileContentAndGetMaxTime(const AFileName: string;
out Records: TArray<TTickData>;
out ActualMaxTickTimeInFile: Double);
var
fs: TFileStream;
fileSize: Int64;
recordCount: Integer;
bytesRead: Integer;
iterTickData: TTickData;
currentFileMaxOA: Double;
begin
SetLength(Records, 0);
ActualMaxTickTimeInFile := 0.0;
currentFileMaxOA := 0.0;
if not TFile.Exists(AFileName) then // Added check for file existence
begin
// Silently exit or log if preferred, but don't try to open non-existent file.
Exit;
end;
fs := TFileStream.Create(AFileName, fmOpenRead or fmShareDenyWrite);
try
fileSize := fs.Size;
if fileSize = 0 then Exit;
if (fileSize mod SizeOf(TTickData)) <> 0 then
raise EReadError.CreateFmt('File %s: Corrupted. Size %d is not a multiple of record size %d.',
[AFileName, fileSize, SizeOf(TTickData)]);
recordCount := fileSize div SizeOf(TTickData);
if recordCount > 0 then
begin
SetLength(Records, recordCount);
bytesRead := fs.Read(Records[0], fileSize);
if bytesRead <> fileSize then
raise EReadError.CreateFmt('File %s: Read error. Expected to read %d bytes, but actually read %d bytes.',
[AFileName, fileSize, bytesRead]);
for iterTickData in Records do
begin
if iterTickData.OADateTime > currentFileMaxOA then
currentFileMaxOA := iterTickData.OADateTime;
end;
ActualMaxTickTimeInFile := currentFileMaxOA;
end;
finally
fs.Free;
end;
end;
begin // Start of LoadTickDataSeries begin // Start of LoadTickDataSeries
tickList := TList<TTickData>.Create; if not TryParseFileName( FileName, pathName, symbolName, initialYear, initialMonth ) then
overallLastTabTickTime := 0.0;
maxTabYearFound := -1;
try
// Initial FileName must be the path to the first .tab file (e.g., Symbol_Year.tab)
// prefixNameOut will be an out parameter from TryParseFileName but will be empty.
if not TryParseFileName(FileName, pathName, symbolName, initialYear) then
begin begin
raise EArgumentException.CreateFmt('Invalid initial file name format: %s. Expected Symbol_Year.tab (or Symbol_Part2_Year.tab)', [FileName]); raise EArgumentException.CreateFmt(
'Invalid initial file name format: %s. Expected Symbol_Year_MM.Extension or Symbol_Year_MM.Extension_zip', [FileName] );
end; end;
// --- PHASE 1: Load all .tab files and determine overallLastTabTickTime ---
currentTabYear := initialYear; currentTabYear := initialYear;
// Construct the first .tab file name using the parsed components (symbol, year). currentTabMonth := initialMonth;
// Prefix is no longer used in the format string. var tabBaseNameFormat: string;
tabFileName := TPath.Combine(pathName, Format('%s_%d.tab', [symbolName, currentTabYear])); var compressedTabPath, uncompressedTabPath: string;
var actualTabFileToLoad: string;
var maxTabYearFound := -1;
var maxTabMonthFound := -1;
var tabFileRecords: TArray<TTickData>; var filesToLoadPhase1 := TList<TPhase1FileEntry>.Create; // Create list with the named record type
var maxTimeInThisTabFile: Double;
while TFile.Exists(tabFileName) do
begin
maxTabYearFound := currentTabYear;
ReadFileContentAndGetMaxTime(tabFileName, tabFileRecords, maxTimeInThisTabFile);
if Length(tabFileRecords) > 0 then
begin
tickList.AddRange(tabFileRecords);
end;
if maxTimeInThisTabFile > overallLastTabTickTime then
overallLastTabTickTime := maxTimeInThisTabFile;
Inc(currentTabYear);
// Construct next tab filename without prefix
tabFileName := TPath.Combine(pathName, Format('%s_%d.tab', [symbolName, currentTabYear]));
end;
// --- PHASE 2: Load .tab-live files ---
if maxTabYearFound >= initialYear then
begin
for liveProcessingYear := initialYear to maxTabYearFound do
begin
// Construct .tab-live filename without prefix
liveFileName := TPath.Combine(pathName, Format('%s_%d.tab-live', [symbolName, liveProcessingYear]));
if TFile.Exists(liveFileName) then
begin
var liveFileRecords: TArray<TTickData>;
var maxTimeInThisLiveFile: Double;
var ticksToActuallyAddFromLive: TList<TTickData>;
var liveDataAddedThisFile: Boolean;
var iterLiveTickData: TTickData;
ReadFileContentAndGetMaxTime(liveFileName, liveFileRecords, maxTimeInThisLiveFile);
liveDataAddedThisFile := False;
if Length(liveFileRecords) > 0 then
begin
ticksToActuallyAddFromLive := TList<TTickData>.Create;
try try
for iterLiveTickData in liveFileRecords do while True do
begin begin
if iterLiveTickData.OADateTime > overallLastTabTickTime then tabBaseNameFormat := Format( '%s_%.4d_%.2d.tab', [symbolName, currentTabYear, currentTabMonth] );
compressedTabPath := TPath.Combine( pathName, tabBaseNameFormat + '_zip' );
uncompressedTabPath := TPath.Combine( pathName, tabBaseNameFormat );
actualTabFileToLoad := '';
if TFile.Exists( compressedTabPath ) then
begin begin
ticksToActuallyAddFromLive.Add(iterLiveTickData); actualTabFileToLoad := compressedTabPath;
liveDataAddedThisFile := True; end
else if TFile.Exists( uncompressedTabPath ) then
begin
actualTabFileToLoad := uncompressedTabPath;
end;
if actualTabFileToLoad = '' then
break; // No more sequential .tab files
// Construct the record and add it to the list
tabFileEntryRecord.Path := actualTabFileToLoad;
tabFileEntryRecord.Year := currentTabYear;
tabFileEntryRecord.Month := currentTabMonth;
filesToLoadPhase1.Add( tabFileEntryRecord );
// Advance to the next month
Inc( currentTabMonth );
if currentTabMonth > 12 then
begin
currentTabMonth := 1;
Inc( currentTabYear );
end; end;
end; end;
if ticksToActuallyAddFromLive.Count > 0 then // Now load and process identified .tab files
tickList.AddRange(ticksToActuallyAddFromLive); var loadedFileList := TList<TFuture<TArray<TTickData>>>.Create;
var loadStates := TList<TState>.Create;
try
var LoadGate: TLatch;
for var tabFileEntry: TPhase1FileEntry in filesToLoadPhase1 do // Iterate with the correct type
begin
var data := ReadFileTickData( LoadGate, tabFileEntry.Path );
loadedFileList.Add( TFuture<TArray<TTickData>>.Create( data ) );
loadStates.Add( data.Done );
if tabFileEntry.Year > maxTabYearFound then
begin
maxTabYearFound := tabFileEntry.Year;
maxTabMonthFound := tabFileEntry.Month;
end
else if ( tabFileEntry.Year = maxTabYearFound ) and ( tabFileEntry.Month > maxTabMonthFound ) then
begin
maxTabMonthFound := tabFileEntry.Month;
end;
end;
var liveFileBaseName := Format( '%s_%d_%02d.tab-live', [symbolName, maxTabYearFound, maxTabMonthFound] );
liveData := ReadFileTickData( LoadGate, TPath.Combine( pathName, liveFileBaseName ) );
loadStates.Add( liveData.Done );
loadedFileList.Add( liveData );
loadedState := TState.All( loadStates.ToArray );
loadedFiles := loadedFileList.ToArray;
finally finally
ticksToActuallyAddFromLive.Free; loadStates.Free;
loadedFileList.Free;
end; end;
finally
filesToLoadPhase1.Free;
end; end;
if not liveDataAddedThisFile then Result := TFuture<TArray<TTickData>>.Construct(
loadedState,
function: TArray<TTickData>
begin begin
var tickList := TList<TTickData>.Create;
try try
TFile.Delete(liveFileName); var totalTicks := Length(liveData.Result);
except var overallLastTabTickTime := 0.0;
on E: Exception do {/* Optional: Log error, e.g., using a global logger or passed logger */}
end; for var P in loadedFiles do
end; inc( totalTicks, Length(P.Result) );
tickList.Capacity := totalTicks;
for var P in loadedFiles do
begin
if Length(P.Result) > 0 then
begin
inc( totalTicks, Length(P.Result) );
for var iterTickData in P.Result do
if iterTickData.OADateTime > overallLastTabTickTime then
begin
tickList.Add( iterTickData );
overallLastTabTickTime := iterTickData.OADateTime;
end; end;
end; end;
end; end;
@@ -249,6 +277,178 @@ begin // Start of LoadTickDataSeries
finally finally
tickList.Free; tickList.Free;
end; end;
end );
end;
function LoadSingleFileTickDataFromStream(const InputStream: TStream): TArray<TTickData>;
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(TTickData)) <> 0 then
raise EReadError.CreateFmt('File corrupted. Size %d is not a multiple of record size %d.',
[fileSize, SizeOf(TTickData)]);
recordCount := fileSize div SizeOf(TTickData);
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;
function LoadUncompressedSingleFileTickData(const FullFileName: string): TArray<TTickData>;
var
inputStream: TStream;
begin
// Initialize Result record
SetLength(Result, 0);
if not TFile.Exists(FullFileName) then
begin
exit;
end;
inputStream := TFileStream.Create(FullFileName, fmOpenRead or fmShareDenyWrite);
try
try
Result := LoadSingleFileTickDataFromStream( inputStream );
except
on E: EReadError do
begin
raise EReadError.CreateFmt('File %s: %s', [FullFileName, E.Message]);
end;
end;
finally
inputStream.Free;
end;
end;
function UncompressTickDataFromStream(const InputStream: TStream): TArray<TTickData>;
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 := LoadSingleFileTickDataFromStream( 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;
function ReadFileTickData( var LoadGate: TLatch; const FileName: string ): TFuture<TArray<TTickData>>;
var
parsedPath, parsedSymbol: string;
parsedYear, parsedMonth: Integer;
begin
Result := TFuture<TArray<TTickData>>.Null;
// Attempt to parse year and month from the filename
if not TryParseFileName( FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth ) then
begin
// If filename parsing fails, return a future that resolves to an empty array.
exit;
end;
if FileName.EndsWith('_zip', True) then
begin
var zipFileBytes := TFile.ReadAllBytes(FileName); // Read all bytes of the .zip file
if Length(zipFileBytes) = 0 then // Handle empty zip file case
exit;
var capFileName := FileName;
var blob := TFuture<TBytes>.Construct(
TLatch.Enqueue( LoadGate ),
function: TBytes
begin
Result := TFile.ReadAllBytes(capFileName); // Read all bytes of the .zip file
end );
Result := blob.Chain<TArray<TTickData>>(
function( bytes: TBytes ): TArray<TTickData>
begin
var zipMemoryStream := TBytesStream.Create( zipFileBytes ); // Create a memory stream to hold the zip file content
try
zipMemoryStream.Position := 0;
Result := UncompressTickDataFromStream( zipMemoryStream );
finally
zipMemoryStream.Free;
end;
end );
end
else
begin
var capFileName := FileName;
Result := TFuture<TArray<TTickData>>.Construct(
function: TArray<TTickData>
begin
Result := LoadUncompressedSingleFileTickData( capFileName );
end );
end;
end; end;
end. end.