unit Myc.TickLoader; (*------------------------------------------------------------------------------ Unit: Myc.TickLoader Author: Delphi Algo Trading (Assistant) Date: May 22, 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 now follow the naming convention: Symbol_Year.tab. (Prefix has been removed from the convention). Loading occurs in two phases: 1. All .tab files are processed chronologically. 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 (for 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 yearly .tab files. Main Function: LoadTickDataSeries - Loads a series of tick data files. ------------------------------------------------------------------------------*) interface uses System.SysUtils, System.Classes, System.Generics.Collections, System.IOUtils; type TTickData = packed record OADateTime: Double; Ask: Single; Bid: Single; end; PTTickData = ^TTickData; // TryParseFileName now expects filenames like "Symbol_Year" (or "Symbol_Part2_Year") // PrefixValue will be returned as an empty string. function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean; function LoadTickDataSeries(const FileName: string): TArray; implementation function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean; var fileNameOnly: string; parts: TArray; begin Result := False; PathValue := TPath.GetDirectoryName(FileName); 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) 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 := ''; Result := True; end; function LoadTickDataSeries(const FileName: string): TArray; var tickList: TList; initialYear, currentTabYear, liveProcessingYear: Integer; pathName, symbolName: string; // prefixNameOut will be an empty out param tabFileName, liveFileName: string; overallLastTabTickTime: Double; maxTabYearFound: Integer; procedure ReadFileContentAndGetMaxTime(const AFileName: string; out Records: TArray; 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 tickList := TList.Create; 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 raise EArgumentException.CreateFmt('Invalid initial file name format: %s. Expected Symbol_Year.tab (or Symbol_Part2_Year.tab)', [FileName]); end; // --- PHASE 1: Load all .tab files and determine overallLastTabTickTime --- currentTabYear := initialYear; // Construct the first .tab file name using the parsed components (symbol, year). // Prefix is no longer used in the format string. tabFileName := TPath.Combine(pathName, Format('%s_%d.tab', [symbolName, currentTabYear])); var tabFileRecords: TArray; 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; var maxTimeInThisLiveFile: Double; var ticksToActuallyAddFromLive: TList; var liveDataAddedThisFile: Boolean; var iterLiveTickData: TTickData; ReadFileContentAndGetMaxTime(liveFileName, liveFileRecords, maxTimeInThisLiveFile); liveDataAddedThisFile := False; if Length(liveFileRecords) > 0 then begin ticksToActuallyAddFromLive := TList.Create; try for iterLiveTickData in liveFileRecords do begin if iterLiveTickData.OADateTime > overallLastTabTickTime then begin ticksToActuallyAddFromLive.Add(iterLiveTickData); liveDataAddedThisFile := True; end; end; if ticksToActuallyAddFromLive.Count > 0 then tickList.AddRange(ticksToActuallyAddFromLive); finally ticksToActuallyAddFromLive.Free; end; end; if not liveDataAddedThisFile then begin try TFile.Delete(liveFileName); except on E: Exception do {/* Optional: Log error, e.g., using a global logger or passed logger */} end; end; end; end; end; Result := tickList.ToArray; finally tickList.Free; end; end; end.