Refactoring to new TDataSeries

This commit is contained in:
Michael Schimmel
2025-06-05 14:37:00 +02:00
parent f571965762
commit d9c9aacff3
5 changed files with 862 additions and 1030 deletions
+18 -8
View File
@@ -16,14 +16,14 @@ object ChartForm: TChartForm
900
835)
TextHeight = 15
object FileSelectButton: TSpeedButton
Left = 647
Top = 17
object PathSelectButton: TSpeedButton
Left = 423
Top = 18
Width = 23
Height = 22
Height = 23
Anchors = [akTop, akRight]
Caption = '...'
OnClick = FileSelectButtonClick
OnClick = PathSelectButtonClick
end
object MouseHoverDataLabel: TLabel
Left = 32
@@ -34,14 +34,14 @@ object ChartForm: TChartForm
AutoSize = False
ExplicitTop = 359
end
object FileNameEdit: TEdit
object DataPathEdit: TEdit
Left = 32
Top = 17
Width = 609
Width = 385
Height = 23
Anchors = [akLeft, akTop, akRight]
TabOrder = 0
Text = '\\COFFEE\TickData\Pepperstone\EURUSD_2017_02.tab_zip'
Text = '\\COFFEE\TickData\Pepperstone'
end
object LoadButton: TButton
Left = 686
@@ -116,4 +116,14 @@ object ChartForm: TChartForm
ExplicitHeight = 97
end
end
object SymbolComboBox: TComboBox
Left = 452
Top = 18
Width = 228
Height = 23
Anchors = [akTop, akRight]
TabOrder = 7
Text = 'GER40'
OnChange = SymbolComboBoxChange
end
end
File diff suppressed because it is too large Load Diff
+7 -16
View File
@@ -5,7 +5,7 @@ interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.StrUtils,
Myc.Futures,
Myc.TickLoader, Myc.OHLCCache; // OHLCCache for TOHLC, IGenericCandleBuilder, TF_ constants
Myc.Trade.DataSeries, Myc.OHLCCache;
type
TTimeframeSetting = record
@@ -16,7 +16,7 @@ type
TChartDataController = class
private
FTickDataArray: TArray<TTickData>;
FTickDataArray: TArray<TAskBid.TTick>;
FOHLCCacheList: TList<TOHLC>;
FTimeframeSettingsArray: TArray<TTimeframeSetting>; // Now includes registered generic builders
FMemoLog: TStrings;
@@ -39,9 +39,8 @@ type
procedure ClearAllDataAndCaches;
function GetTickDataCount: Integer;
function GetTickDataArrayPtr: PTTickData;
function GetTickDataLength: Integer;
function GetTickDataItem(AIndex: Integer): TTickData;
function GetTickDataItem(AIndex: Integer): TAskBid.TTick;
function HasData: Boolean;
// ABuilderCaption is the unique key used for FTimeframeSettingsArray.Caption and FGenericBuilders key.
@@ -55,7 +54,7 @@ type
implementation
uses
System.DateUtils; // For FormatDateTime in logging
System.DateUtils;
const
dcSecsPerDayConst = 24 * 60 * 60;
@@ -211,7 +210,7 @@ procedure TChartDataController.LoadTickDataSeries(const AFileName: string);
begin
ClearAllDataAndCaches();
try
FTickDataArray := Myc.TickLoader.LoadTickDataSeries(AFileName).WaitFor;
FTickDataArray := TAskBid.LoadDataSeries(AFileName).WaitFor();
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Loading tick records from series: %s',
[AFileName]));
@@ -401,20 +400,12 @@ begin
Result := Length(FTickDataArray);
end;
function TChartDataController.GetTickDataArrayPtr: PTTickData;
begin
if Length(FTickDataArray) > 0 then
Result := @FTickDataArray[0]
else
Result := nil;
end;
function TChartDataController.GetTickDataLength: Integer;
begin
Result := Length(FTickDataArray);
end;
function TChartDataController.GetTickDataItem(AIndex: Integer): TTickData;
function TChartDataController.GetTickDataItem(AIndex: Integer): TAskBid.TTick;
begin
if (AIndex >= 0) and (AIndex < Length(FTickDataArray)) then
Result := FTickDataArray[AIndex]
@@ -422,7 +413,7 @@ begin
begin
if FMemoLog <> nil then
FMemoLog.Add(Format('DataController: Attempted to access invalid tick data index %d. Count is %d.', [AIndex, Length(FTickDataArray)]));
Result := Default(TTickData);
Result := Default(TAskBid.TTick);
end;
end;
+44 -45
View File
@@ -35,7 +35,7 @@ interface
uses
System.Classes, System.SysUtils, System.Math,
System.Generics.Collections, System.DateUtils,
Myc.TickLoader;
Myc.Trade.DataSeries;
const
TF_AUTO_AGGREGATION = 0;
@@ -56,9 +56,8 @@ type
// Interface for generic candle building logic
IGenericCandleBuilder = interface
['{B4F6A9C1-0A7E-42D3-847B-170705A8CC8D}'] // Added GUID
function Init(const ADataSet: TArray<TTickData>): Boolean; // Changed DataSet to ADataSet for clarity
function IsNewBar(AIndex: Integer; const ATick: TTickData): Boolean; // Changed Idx to AIndex, Tick to ATick
function Init(const ADataSet: TArray<TAskBid.TTick>): Boolean; // Changed DataSet to ADataSet for clarity
function IsNewBar(AIndex: Integer; const ATick: TAskBid.TTick): Boolean; // Changed Idx to AIndex, Tick to ATick
function GetCaption: string; // Method for the Caption property
property Caption: string read GetCaption;
end;
@@ -81,7 +80,7 @@ type
public
constructor Create;
procedure ClearCache;
procedure Build(const ATickData: TArray<TTickData>;
procedure Build(const ATickData: TArray<TAskBid.TTick>;
ASelectedTimeframeSeconds: Int64;
AAutoAggCandleWidth: Integer;
AAutoAggCandleSpacing: Integer;
@@ -189,7 +188,7 @@ begin
Result := Length(FCachedCandles);
end;
procedure TOHLC.Build(const ATickData: TArray<TTickData>;
procedure TOHLC.Build(const ATickData: TArray<TAskBid.TTick>;
ASelectedTimeframeSeconds: Int64;
AAutoAggCandleWidth: Integer; AAutoAggCandleSpacing: Integer; AAutoAggPaintBoxClientWidth: Integer;
AMemoForLogging: TStrings; AGenericCandleBuilder: IGenericCandleBuilder = nil); // Parameter changed
@@ -201,7 +200,7 @@ var
CandleList: TList<TCachedCandle>;
TempCandle: TCachedCandle;
LogTimePerCandleStr: string;
CurrentTick: TTickData;
CurrentTick: TAskBid.TTick;
begin
if AMemoForLogging <> nil then
AMemoForLogging.Add(FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + Format(' - TOHLC.Build: TF Secs: %d...',
@@ -217,14 +216,14 @@ begin
Exit;
end;
FGlobalMinY := ATickData[0].Ask;
FGlobalMaxY := ATickData[0].Ask;
FGlobalMinY := ATickData[0].Data.Ask; // Changed
FGlobalMaxY := ATickData[0].Data.Ask; // Changed
for I := 1 to High(ATickData) do
begin
if ATickData[I].Ask < FGlobalMinY then
FGlobalMinY := ATickData[I].Ask;
if ATickData[I].Ask > FGlobalMaxY then
FGlobalMaxY := ATickData[I].Ask;
if ATickData[I].Data.Ask < FGlobalMinY then // Changed
FGlobalMinY := ATickData[I].Data.Ask; // Changed
if ATickData[I].Data.Ask > FGlobalMaxY then // Changed
FGlobalMaxY := ATickData[I].Data.Ask; // Changed
end;
if FGlobalMaxY = FGlobalMinY then
begin
@@ -257,10 +256,10 @@ begin
if NumDataPoints > 0 then
begin
DataIndexStart := 0;
OpenPrice := ATickData[0].Ask;
HighPrice := ATickData[0].Ask;
LowPrice := ATickData[0].Ask;
ClosePrice := ATickData[0].Ask;
OpenPrice := ATickData[0].Data.Ask; // Changed
HighPrice := ATickData[0].Data.Ask; // Changed
LowPrice := ATickData[0].Data.Ask; // Changed
ClosePrice := ATickData[0].Data.Ask; // Changed
for I := 0 to NumDataPoints - 1 do
begin
@@ -272,7 +271,7 @@ begin
TempCandle.OpenPrice := OpenPrice;
TempCandle.HighPrice := HighPrice;
TempCandle.LowPrice := LowPrice;
TempCandle.ClosePrice := ATickData[I - 1].Ask; // Close of previous candle is the tick before the new bar starts
TempCandle.ClosePrice := ATickData[I - 1].Data.Ask; // Close of previous candle is the tick before the new bar starts // Changed
TempCandle.OriginalDataIndexStart := DataIndexStart;
TempCandle.OriginalDataIndexEnd := I - 1;
TempCandle.TickVolume := (I - 1) - DataIndexStart + 1;
@@ -280,27 +279,27 @@ begin
// Current tick 'I' starts a new candle
DataIndexStart := I;
OpenPrice := CurrentTick.Ask;
HighPrice := CurrentTick.Ask;
LowPrice := CurrentTick.Ask;
ClosePrice := CurrentTick.Ask;
OpenPrice := CurrentTick.Data.Ask; // Changed
HighPrice := CurrentTick.Data.Ask; // Changed
LowPrice := CurrentTick.Data.Ask; // Changed
ClosePrice := CurrentTick.Data.Ask; // Changed
end
else // Current tick continues the existing candle OR is the first tick of the very first candle
begin
if I = DataIndexStart then // This is the first tick of the current (or overall first) candle
begin
OpenPrice := CurrentTick.Ask; // Open is set
HighPrice := CurrentTick.Ask; // Initial High
LowPrice := CurrentTick.Ask; // Initial Low
OpenPrice := CurrentTick.Data.Ask; // Open is set // Changed
HighPrice := CurrentTick.Data.Ask; // Initial High // Changed
LowPrice := CurrentTick.Data.Ask; // Initial Low // Changed
end
else // Update High/Low for ongoing candle
begin
if CurrentTick.Ask > HighPrice then
HighPrice := CurrentTick.Ask;
if CurrentTick.Ask < LowPrice then
LowPrice := CurrentTick.Ask;
if CurrentTick.Data.Ask > HighPrice then // Changed
HighPrice := CurrentTick.Data.Ask; // Changed
if CurrentTick.Data.Ask < LowPrice then // Changed
LowPrice := CurrentTick.Data.Ask; // Changed
end;
ClosePrice := CurrentTick.Ask; // Always update close to current tick
ClosePrice := CurrentTick.Data.Ask; // Always update close to current tick // Changed
end;
// If this is the last tick in the dataset, finalize the current candle
@@ -379,15 +378,15 @@ begin
Continue;
end;
OpenPrice := ATickData[DataIndexStart].Ask;
ClosePrice := ATickData[DataIndexEnd].Ask;
OpenPrice := ATickData[DataIndexStart].Data.Ask; // Changed
ClosePrice := ATickData[DataIndexEnd].Data.Ask; // Changed
HighPrice := OpenPrice; // Initialize with Open
LowPrice := OpenPrice; // Initialize with Open
for J := DataIndexStart to DataIndexEnd do
begin
if ATickData[J].Ask > HighPrice then HighPrice := ATickData[J].Ask;
if ATickData[J].Ask < LowPrice then LowPrice := ATickData[J].Ask;
if ATickData[J].Data.Ask > HighPrice then HighPrice := ATickData[J].Data.Ask; // Changed
if ATickData[J].Data.Ask < LowPrice then LowPrice := ATickData[J].Data.Ask; // Changed
end;
// Ensure High/Low encompass Open/Close after loop
HighPrice := Max(HighPrice, Max(OpenPrice, ClosePrice));
@@ -409,10 +408,10 @@ begin
SetLength(FCachedCandles, NumDataPoints);
for I := 0 to NumDataPoints - 1 do
begin
FCachedCandles[I].OpenPrice := ATickData[I].Ask;
FCachedCandles[I].HighPrice := ATickData[I].Ask;
FCachedCandles[I].LowPrice := ATickData[I].Ask;
FCachedCandles[I].ClosePrice := ATickData[I].Ask;
FCachedCandles[I].OpenPrice := ATickData[I].Data.Ask; // Changed
FCachedCandles[I].HighPrice := ATickData[I].Data.Ask; // Changed
FCachedCandles[I].LowPrice := ATickData[I].Data.Ask; // Changed
FCachedCandles[I].ClosePrice := ATickData[I].Data.Ask; // Changed
FCachedCandles[I].OriginalDataIndexStart := I;
FCachedCandles[I].OriginalDataIndexEnd := I;
FCachedCandles[I].TickVolume := 1;
@@ -438,10 +437,10 @@ begin
BucketEndTimeOADate := BucketStartTimeOADate + TimeframeIntervalOADays;
DataIndexStart := CurrentTickIndex;
OpenPrice := ATickData[CurrentTickIndex].Ask;
HighPrice := ATickData[CurrentTickIndex].Ask;
LowPrice := ATickData[CurrentTickIndex].Ask;
ClosePrice := ATickData[CurrentTickIndex].Ask; // Initialize close with open
OpenPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Initialize close with open // Changed
DataIndexEnd := CurrentTickIndex; // Initialize end with start
// Iterate through ticks that fall into the current bucket
@@ -449,9 +448,9 @@ begin
(ATickData[CurrentTickIndex].OADateTime >= BucketStartTimeOADate) and
(ATickData[CurrentTickIndex].OADateTime < BucketEndTimeOADate) do
begin
if ATickData[CurrentTickIndex].Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Ask;
if ATickData[CurrentTickIndex].Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Ask;
ClosePrice := ATickData[CurrentTickIndex].Ask; // Continuously update close price
if ATickData[CurrentTickIndex].Data.Ask > HighPrice then HighPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
if ATickData[CurrentTickIndex].Data.Ask < LowPrice then LowPrice := ATickData[CurrentTickIndex].Data.Ask; // Changed
ClosePrice := ATickData[CurrentTickIndex].Data.Ask; // Continuously update close price // Changed
DataIndexEnd := CurrentTickIndex; // Mark the last tick included in this candle
Inc(CurrentTickIndex);
end;
-454
View File
@@ -1,454 +0,0 @@
unit Myc.TickLoader;
(* ------------------------------------------------------------------------------
Unit:         Myc.TickLoader
Author:       Delphi Algo Trading (Assistant)
Date:         May 27, 2025
Description:
This unit provides functions to load tick data (TTickData) from
binary files. The data originates from a C# application
and has the structure OADateTime (Double), Ask (Single), and Bid (Single).
The files follow the naming 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:
1. All .tab files (regular or _zip) are processed chronologically by
month and year. Unique ticks are loaded, and the timestamp of the
latest tick across all .tab files is determined (overallLastTabTickTime).
Assumes timestamps are unique across all .tab files.
2. All .tab-live files (which are never compressed), for months/years
corresponding to found .tab files, are processed. Ticks from a
.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,
it is deleted after processing.
The loading function automatically detects subsequent monthly/yearly files.
Main Function:
LoadTickDataSeries - Loads a series of tick data files.
------------------------------------------------------------------------------ *)
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.IOUtils,
Myc.Futures, // Added for TFuture in ReadFileTickData
Myc.Signals; // Added for IMycState (dependency of TFuture)
type
TTickData = packed record
OADateTime: Double;
Ask: Single;
Bid: Single;
end;
PTTickData = ^TTickData;
// TryParseFileName parses filenames like "Symbol_Year_MM.Extension" or "Symbol_Year_MM.Extension_zip"
// to extract Path, Symbol, Year, and Month.
function TryParseFileName( const FileName: string;
out PathValue, SymbolValue: string;
out YearValue, MonthValue: Integer ): Boolean;
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
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
fileNameNoPath: string;
nameForParsing: string;
parts: TArray<string>;
begin
Result := False; // Default to failure
PathValue := ''; // Initialize all out parameters
SymbolValue := '';
YearValue := 0;
MonthValue := 0;
PathValue := TPath.GetDirectoryName( FileName );
fileNameNoPath := TPath.GetFileName( FileName );
nameForParsing := fileNameNoPath;
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;
end;
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
initialYear, initialMonth: Integer;
currentTabYear, currentTabMonth: Integer;
pathName, symbolName: string;
tabFileEntryRecord: TPhase1FileEntry; // Variable to construct record instances
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TTickData>>>;
liveData: TFuture<TArray<TTickData>>;
begin // Start of LoadTickDataSeries
if not TryParseFileName( FileName, pathName, symbolName, initialYear, initialMonth ) then
begin
raise EArgumentException.CreateFmt(
'Invalid initial file name format: %s. Expected Symbol_Year_MM.Extension or Symbol_Year_MM.Extension_zip', [FileName] );
end;
currentTabYear := initialYear;
currentTabMonth := initialMonth;
var tabBaseNameFormat: string;
var compressedTabPath, uncompressedTabPath: string;
var actualTabFileToLoad: string;
var maxTabYearFound := -1;
var maxTabMonthFound := -1;
var filesToLoadPhase1 := TList<TPhase1FileEntry>.Create; // Create list with the named record type
try
while True do
begin
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
actualTabFileToLoad := compressedTabPath;
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;
// Now load and process identified .tab files
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
loadStates.Free;
loadedFileList.Free;
end;
finally
filesToLoadPhase1.Free;
end;
Result := TFuture<TArray<TTickData>>.Construct(
loadedState,
function: TArray<TTickData>
begin
var tickList := TList<TTickData>.Create;
try
var totalTicks := Length(liveData.Result);
var overallLastTabTickTime := 0.0;
for var P in loadedFiles do
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;
Result := tickList.ToArray;
finally
tickList.Free;
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.