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>; // 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>; 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; 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>; // 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>>; liveData: TFuture>; 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.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>>.Create; var loadStates := TList.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>.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>.Construct( loadedState, function: TArray begin var tickList := TList.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; 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; 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; 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>; var parsedPath, parsedSymbol: string; parsedYear, parsedMonth: Integer; begin Result := TFuture>.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.Construct( TLatch.Enqueue( LoadGate ), function: TBytes begin Result := TFile.ReadAllBytes(capFileName); // Read all bytes of the .zip file end ); Result := blob.Chain>( function( bytes: TBytes ): TArray 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>.Construct( function: TArray begin Result := LoadUncompressedSingleFileTickData( capFileName ); end ); end; end; end.