Files
cTrader-DataExport/common/Myc.TickLoader.pas
T
Michael Schimmel 06ec0db105 1st commit
2025-05-23 12:51:32 +02:00

255 lines
10 KiB
ObjectPascal

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<TTickData>;
implementation
function TryParseFileName(const FileName: string; out PathValue, SymbolValue: string; out YearValue: Integer): Boolean;
var
fileNameOnly: string;
parts: TArray<string>;
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<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 := '';
Result := True;
end;
function LoadTickDataSeries(const FileName: string): TArray<TTickData>;
var
tickList: TList<TTickData>;
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<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
tickList := TList<TTickData>.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<TTickData>;
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
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.