Files
MycLib/Src/Myc.Trade.DataStream.pas
T
Michael Schimmel 4d67e587ba # Projektplan: Meilenstein Daten-Infrastruktur
*Datum: 13. Juni 2025*

## Status: Design-Phase abgeschlossen, Basis-Implementierung erfolgt

---

## Stufe 1: Asynchrone Datenstrom-Schnittstelle (`IDataStream<T>`) - **ABGESCHLOSSEN**

* **Motivation**: Das ursprüngliche, zustandsbehaftete Design war für die Verarbeitung asynchron eintreffender Daten (z.B. von `TFuture`-Objekten) zu komplex und fehleranfällig.

* **Ziel**: Die Schaffung eines robusten, ereignisgesteuerten und non-blocking Modells, das den Datenproduzenten sauber vom Konsumenten entkoppelt.

* **Ergebnis**:
    * Die `IDataStream<T>`-Schnittstelle wurde überarbeitet. Die `HasData`-Eigenschaft liefert nun ein `TSignal`, das Konsumenten aktiv über potenziell neue Daten informiert.
    * Die Referenzimplementierung `TAuraFileStream<T>` wurde erfolgreich angepasst und nutzt eine saubere Ereignis-Kopplung (`Subscribe`) für eine robuste und wartungsarme Logik.
    * Die korrekte Funktionalität wurde durch eine angepasste DUnitX-Test-Suite verifiziert.

---

## Stufe 2: Abstraktion für Handelssysteme (`IDataSeriesProvider`) - **ENTWORFEN**

* **Motivation**: Ein Handelssystem benötigt einen stets validen und kontinuierlichen Daten-Lookback. Ein roher `IDataStream<T>` kann dies nicht garantieren, da Lücken in den Daten auftreten können (z.B. beim Übergang von Historie zu Live).

* **Ziel**: Die Konzeption einer übergeordneten Abstraktionsschicht, die diese komplexe Anforderung kapselt, die Datenintegrität sicherstellt und dem Handelssystem eine einfache, sichere Schnittstelle bietet.

* **Ergebnis**:
    Entworfen wurde der `IDataSeriesProvider`, der als "Black Box" für das Handelssystem fungiert und die Komplexität der Datenbeschaffung vollständig verbirgt. Er wurde mit zwei unterschiedlichen, vom Anwender wählbaren Betriebsmodi konzipiert:

    ### Modus 1: "Live-Handel"
    * **Motivation**: Um einen echten **"Sofort-Start"** im Live-Handel zu ermöglichen, muss die Lücke zwischen den statischen, lokalen Historiendaten und dem aktuellen Zeitpunkt geschlossen werden.
    * **Ziel**: Ein lückenloser, tagesaktueller Start des Handelssystems ohne manuelles Eingreifen oder lange Wartezeiten für den Nutzer.
    * **Ergebnis**: Das Design einer **Drei-Phasen-Synchronisation**: (1) Lokale History laden, (2) "Catch-up"-Daten vom Broker holen, (3) auf den Live-Stream umschalten.

    ### Modus 2: "Simulation & Backtest"
    * **Motivation**: Für Entwicklung, Test und Analyse muss die Software **völlig autonom** und ohne Abhängigkeit von einer externen, potenziell nicht verfügbaren Broker-API lauffähig sein.
    * **Ziel**: Einen "Sofort-Start" für Backtests zu jedem beliebigen Zeitpunkt in der Vergangenheit zu ermöglichen, der rein auf lokalen Dateien basiert.
    * **Ergebnis**: Ein Design, bei dem der relevante Datenkontext in den Speicher geladen wird, um von dort aus einen schnellen Start und ein "Playback" der Daten zu ermöglichen.
2025-06-13 10:50:40 +02:00

771 lines
25 KiB
ObjectPascal

unit Myc.Trade.DataStream;
(*
Myc.Trade.DataStream
Provides a data server for loading time-series data and streaming it.
This unit contains the core components for handling historical data:
- TAuraFileDataRecord: A generic record for a single time-stamped data entry.
- IDataServer/TAuraDataServer: A server component responsible for loading and caching
series of data files from disk. Each server instance manages its own cache,
but all instances share a central load gate.
- IDataStream/TDataStream: An interface representing a stream of data points that
can be consumed sequentially.
- TAuraFileStream: A concrete implementation of IDataStream that is fed by an
IDataServer instance.
*)
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.Generics.Defaults,
System.IOUtils,
Myc.Futures,
Myc.Signals,
Myc.Lazy,
Myc.Trade.DataPoint;
type
// Represents a generic data stream capable of providing sequential data chunks.
// IsHistory:
// - true, if this stream is a history stream. Once HasData becomes false, it reached it's end and will not provide more data.
// - false, we expect more Data to come. This stream has no end.
// HasData: set, if a call to GetChunk will return new data.
IDataStream<T> = interface
['{A6E246A2-E84E-49AB-A63E-333E561E488C}']
function GetHasData: TSignal;
function GetChunk(var Data: array of TDataPoint<T>): Integer;
function IsHistory: Boolean;
property HasData: TSignal read GetHasData;
end;
// Abstract base class for IDataStream implementations.
TDataStream<T> = class(TInterfacedObject, IDataStream<T>)
protected
function GetHasData: TSignal; virtual; abstract;
function GetChunk(var Data: array of TDataPoint<T>): Integer; virtual; abstract;
function IsHistory: Boolean; virtual; abstract;
end;
// Represents a factory for creating IDataStream instances.
IDataStreamNode<T> = interface
['{DA3531F1-158E-4A63-8E5A-34089C537B1B}']
function CreateDataStream: IDataStream<T>;
end;
// Abstract base class for IDataStreamNode implementations.
TDataStreamNode<T> = class(TInterfacedObject, IDataStreamNode<T>)
public
function CreateDataStream: IDataStream<T>; virtual; abstract;
end;
// Interface for an instantiable data server.
IDataServer<T: record> = interface
['{1F8E5A9D-E92A-44C1-9F3F-C4B82A6E94B3}']
function CreateStream(const Symbol: String): IDataStream<T>;
procedure ClearCache;
end;
// A generic record for a single time-stamped data entry.
TAuraFileDataRecord<T: record> = packed record
TimeStamp: TDateTime;
Data: T;
end;
IAuraDataServer<T: record> = interface(IDataServer<T>)
['{481E27DE-DC58-49AF-B8A8-B6316980C423}']
function LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
function EnumerateAssetFiles: TArray<String>;
end;
// Aura files
// Generic server for loading and managing sequential time-series data from files.
TAuraDataServer<T: record> = class(TInterfacedObject, IDataServer<T>, IAuraDataServer<T>)
private
type
TCachedFile = class
public
Name: String;
Age: TDateTime;
LastUsed: TDateTime;
Data: TFuture<TArray<TDataPoint<T>>>;
end;
private
// The cache is per-instance.
FCachedFiles: TDictionary<String, TCachedFile>;
FPath: String;
function GetPath: String;
strict private
// The load gate is shared across all instances of TAuraDataServer.
class var
FLoadGate: TLatch;
protected
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; static;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; static;
public
constructor Create(const APath: String);
destructor Destroy; override;
class function TryParseFileName(
const FileName: string;
out PathValue, SymbolValue: string;
out YearValue, MonthValue: Integer
): Boolean; virtual; abstract;
class function FindFirstDataFile(const Path, Symbol: string): string;
class function FindNextDataFile(const FileName: string): string;
class function FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
// IDataServer<T> implementation
function CreateStream(const Symbol: String): IDataStream<T>; virtual; abstract;
procedure ClearCache;
function EnumerateAssetFiles: TArray<String>;
function LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
function LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
property Path: String read GetPath;
end;
// Implements a data stream that reads from Aura-specific historical data files.
TAuraFileStream<T: record> = class(TDataStream<T>)
private
FDataServer: TAuraDataServer<T>;
FHasData: TEvent;
FCurrentFileName: string;
FCurrentData: TFuture<TArray<TDataPoint<T>>>;
FNextFileName: string;
FNextData: TFuture<TArray<TDataPoint<T>>>;
FCurrPosInFile: Int64;
FLastTimeStamp: TDateTime;
protected
function GetHasData: TSignal; override;
function GetChunk(var Data: array of TDataPoint<T>): Integer; override;
function IsHistory: Boolean; override;
public
constructor Create(ADataServer: TAuraDataServer<T>; const AFilename: String);
destructor Destroy; override;
procedure AfterConstruction; override;
end;
// Aura tick data file Ask-Bid
TAuraTABFileServer = class(TAuraDataServer<TAskBidItem>)
public
// IDataServer<T> implementation
function CreateStream(const Symbol: String): IDataStream<TAskBidItem>; override;
class function TryParseFileName(
const FileName: string;
out PathValue, SymbolValue: string;
out YearValue, MonthValue: Integer
): Boolean; override;
end;
var
IsMemoryLow: TFunc<Boolean>;
implementation
uses
System.Zip,
System.Math,
System.StrUtils,
Myc.TaskManager;
constructor TAuraDataServer<T>.Create(const APath: String);
begin
inherited Create;
// Initialize the per-instance cache.
FCachedFiles := TObjectDictionary<String, TCachedFile>.Create([doOwnsValues]);
FPath := APath;
end;
destructor TAuraDataServer<T>.Destroy;
begin
ClearCache;
FCachedFiles.Free;
inherited Destroy;
end;
procedure TAuraDataServer<T>.ClearCache;
begin
for var cF in FCachedFiles.Values do
cF.Data.WaitFor;
FCachedFiles.Clear;
end;
class function TAuraDataServer<T>.FindFirstDataFile(const Path, Symbol: string): string;
var
oldestFiles: TArray<string>;
fileName: string;
pathValue, symbolValue: string;
yearValue, monthValue: Integer;
begin
oldestFiles := FindOldestFilesPerSymbol(Path);
for fileName in oldestFiles do
begin
if TryParseFileName(fileName, pathValue, symbolValue, yearValue, monthValue) then
begin
if SameText(symbolValue, Symbol) then
exit(fileName);
end;
end;
Result := '';
end;
class function TAuraDataServer<T>.FindNextDataFile(const FileName: string): string;
var
pathValue, symbolValue: string;
yearValue, monthValue: Integer;
nextBaseName, compressedPath, uncompressedPath: string;
begin
if not TryParseFileName(FileName, pathValue, symbolValue, yearValue, monthValue) then
exit('');
Inc(monthValue);
if monthValue > 12 then
begin
monthValue := 1;
Inc(yearValue);
end;
nextBaseName := Format('%s_%.4d_%.2d.tab', [symbolValue, yearValue, monthValue]);
compressedPath := TPath.Combine(pathValue, nextBaseName + '_zip');
if TFile.Exists(compressedPath) then
exit(compressedPath);
uncompressedPath := TPath.Combine(pathValue, nextBaseName);
if TFile.Exists(uncompressedPath) then
exit(uncompressedPath);
Result := '';
end;
class function TAuraDataServer<T>.FindOldestFilesPerSymbol(const DirectoryPath: string): TArray<String>;
type
TTrackedFileInfo = record
FullFileName: string;
Year: Integer;
Month: Integer;
end;
var
oldestFilesPerSymbol: TDictionary<string, TTrackedFileInfo>;
fileNames: TArray<string>;
currentFile: string;
parsedPath: string;
parsedSymbol: string;
parsedYear: Integer;
parsedMonth: Integer;
trackedInfo: TTrackedFileInfo;
begin
oldestFilesPerSymbol := TDictionary<string, TTrackedFileInfo>.Create;
try
if not TDirectory.Exists(DirectoryPath) then
exit(nil);
fileNames := TDirectory.GetFiles(DirectoryPath);
for currentFile in fileNames do
begin
if TryParseFileName(currentFile, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
begin
if oldestFilesPerSymbol.TryGetValue(parsedSymbol, trackedInfo) then
begin
if (parsedYear < trackedInfo.Year) or ((parsedYear = trackedInfo.Year) and (parsedMonth < trackedInfo.Month)) then
begin
trackedInfo.FullFileName := currentFile;
trackedInfo.Year := parsedYear;
trackedInfo.Month := parsedMonth;
oldestFilesPerSymbol.AddOrSetValue(parsedSymbol, trackedInfo);
end;
end
else
begin
trackedInfo.FullFileName := currentFile;
trackedInfo.Year := parsedYear;
trackedInfo.Month := parsedMonth;
oldestFilesPerSymbol.Add(parsedSymbol, trackedInfo);
end;
end;
end;
SetLength(Result, oldestFilesPerSymbol.Count);
var n := 0;
for trackedInfo in oldestFilesPerSymbol.Values do
begin
Result[n] := trackedInfo.FullFileName;
inc(n);
end;
finally
oldestFilesPerSymbol.Free;
end;
end;
function TAuraDataServer<T>.EnumerateAssetFiles: TArray<String>;
begin
Result := FindOldestFilesPerSymbol(FPath);
end;
function TAuraDataServer<T>.GetPath: String;
begin
Result := FPath;
end;
function TAuraDataServer<T>.LoadDataFile(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
var
parsedPath, parsedSymbol: string;
parsedYear, parsedMonth: Integer;
begin
Result := TFuture<TArray<TDataPoint<T>>>.Null;
if not TryParseFileName(FileName, parsedPath, parsedSymbol, parsedYear, parsedMonth) then
exit;
if Assigned(IsMemoryLow) then
begin
if IsMemoryLow() then
begin
var fL := FCachedFiles.Values.ToArray;
TArray.Sort<TCachedFile>(
fL,
TComparer<TCachedFile>
.Construct(function(const Left, Right: TCachedFile): Integer begin Result := Sign(Left.LastUsed - Right.LastUsed); end)
);
for var i := 0 to High(fL) do
begin
if not IsMemoryLow() then
break;
if fL[i].Name <> FileName then
FCachedFiles.Remove(fL[i].Name);
end;
end;
var cachedFile: TCachedFile;
if FCachedFiles.TryGetValue(Filename, cachedFile) then
begin
var age: TDateTime;
if FileAge(Filename, age, true) then
begin
if age = cachedFile.Age then
begin
cachedFile.LastUsed := Now;
exit(cachedFile.Data);
end
else
FCachedFiles.Remove(FileName);
end;
end;
end;
var capFileName := FileName;
if FileName.EndsWith('_zip', True) then
begin
Result :=
TFuture<TBytes>
.Construct(
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
.Chain<TArray<TDataPoint<T>>>(
function(bytes: TBytes): TArray<TDataPoint<T>>
begin
var zipMemoryStream := TBytesStream.Create(bytes);
try
zipMemoryStream.Position := 0;
Result := ReadCompressedData(zipMemoryStream);
finally
zipMemoryStream.Free;
end;
end);
end
else if TFile.Exists(FileName) then
begin
Result :=
TFuture<TArray<TDataPoint<T>>>.Construct(
TLatch.Enqueue(FLoadGate), // Use shared class var FLoadGate
function: TArray<TDataPoint<T>>
begin
if TFile.Exists(capFileName) then
begin
var stream := TFileStream.Create(capFileName, fmOpenRead or fmShareDenyWrite);
try
try
Result := ReadUncompressedData(stream);
except
on E: EReadError do
raise EReadError.CreateFmt('File %s: %s', [capFileName, E.Message]);
end;
finally
stream.Free;
end;
end;
end
);
end;
if Assigned(IsMemoryLow) then
begin
var cachedFile := TCachedFile.Create;
cachedFile.Name := Filename;
if FileAge(Filename, cachedFile.Age, true) then
begin
cachedFile.Data := Result;
cachedFile.LastUsed := Now;
FCachedFiles.Add(Filename, cachedFile);
end;
end;
end;
function TAuraDataServer<T>.LoadDataSeries(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
var
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataPoint<T>>>>;
liveData: TFuture<TArray<TDataPoint<T>>>;
tabFiles: TList<string>;
currentFile: string;
liveFilePath: string;
pathName, symbolName: string;
yearValue, monthValue: Integer;
begin
tabFiles := TList<string>.Create;
try
currentFile := FileName;
if TFile.Exists(currentFile) then
while currentFile <> '' do
begin
tabFiles.Add(currentFile);
currentFile := FindNextDataFile(currentFile);
end;
liveFilePath := '';
if tabFiles.Count > 0 then
begin
var lastTabFile := tabFiles.Last;
if TryParseFileName(lastTabFile, pathName, symbolName, yearValue, monthValue) then
begin
var liveFileBaseName := Format('%s_%d_%02d.tab-live', [symbolName, yearValue, monthValue]);
var potentialLivePath := TPath.Combine(pathName, liveFileBaseName);
if TFile.Exists(potentialLivePath) then
liveFilePath := potentialLivePath;
end;
end;
var loadedFileList := TList<TFuture<TArray<TDataPoint<T>>>>.Create;
var loadStates := TList<TState>.Create;
try
for currentFile in tabFiles do
begin
var data := LoadDataFile(currentFile);
loadedFileList.Add(data);
loadStates.Add(data.Done);
end;
liveData := TFuture<TArray<TDataPoint<T>>>.Null;
if liveFilePath <> '' then
begin
liveData := LoadDataFile(liveFilePath);
loadedFileList.Add(liveData);
loadStates.Add(liveData.Done);
end;
loadedState := TState.All(loadStates.ToArray);
loadedFiles := loadedFileList.ToArray;
finally
loadStates.Free;
loadedFileList.Free;
end;
finally
tabFiles.Free;
end;
Result :=
TFuture<TArray<TDataPoint<T>>>.Construct(
loadedState,
function: TArray<TDataPoint<T>>
begin
var tickList := TList<TDataPoint<T>>.Create;
try
var overallLastTabTickTime: TDateTime := 0;
var cnt := 0;
for var P in loadedFiles do
inc(cnt, Length(P.Value));
tickList.Capacity := cnt;
for var P in loadedFiles do
for var iterTickData in P.Value do
if overallLastTabTickTime < iterTickData.Time then
begin
tickList.Add(iterTickData);
overallLastTabTickTime := iterTickData.Time;
end;
Result := tickList.ToArray;
finally
tickList.Free;
end;
end
);
end;
class function TAuraDataServer<T>.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>;
var
decompressionStream: TStream;
localHeader: TZipHeader;
entryIndex, i: Integer;
zipFileInstance: TZipFile;
begin
SetLength(Result, 0);
decompressionStream := nil;
zipFileInstance := nil;
try
InputStream.Position := 0;
zipFileInstance := TZipFile.Create;
zipFileInstance.Open(InputStream, TZipMode.zmRead);
if zipFileInstance.FileCount = 0 then
exit;
entryIndex := -1;
for i := 0 to zipFileInstance.FileCount - 1 do
if zipFileInstance.FileNames[i].EndsWith('.tab', True) then
begin
entryIndex := i;
break;
end;
if entryIndex = -1 then
entryIndex := 0;
zipFileInstance.Read(entryIndex, decompressionStream, localHeader);
if not Assigned(decompressionStream) then
exit;
Result := ReadUncompressedData(decompressionStream);
finally
decompressionStream.Free;
zipFileInstance.Free;
end;
end;
class function TAuraDataServer<T>.ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<T>>;
var
fileSize: Int64;
recordCount, bytesRead: Integer;
rec: TAuraFileDataRecord<T>;
begin
SetLength(Result, 0);
InputStream.Position := 0;
fileSize := InputStream.Size;
if (fileSize = 0) or ((fileSize mod SizeOf(TAuraFileDataRecord<T>)) <> 0) then
exit;
recordCount := fileSize div SizeOf(TAuraFileDataRecord<T>);
if recordCount > 0 then
begin
SetLength(Result, recordCount);
for var i := 0 to High(Result) do
begin
bytesRead := InputStream.Read(rec, SizeOf(TAuraFileDataRecord<T>));
if bytesRead <> SizeOf(TAuraFileDataRecord<T>) then
raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [fileSize, bytesRead]);
Result[i].Create(rec.TimeStamp, rec.Data);
end;
end;
end;
{ TAuraFileStream<T> }
constructor TAuraFileStream<T>.Create(ADataServer: TAuraDataServer<T>; const AFilename: String);
begin
inherited Create;
Assert(Assigned(ADataServer));
FDataServer := ADataServer;
FCurrentFileName := AFilename;
// Create an event
FHasData := TEvent.CreateEvent; // interface helper benutzen!
end;
destructor TAuraFileStream<T>.Destroy;
begin
inherited;
end;
procedure TAuraFileStream<T>.AfterConstruction;
begin
inherited;
FLastTimeStamp := 0;
FCurrPosInFile := 0;
FCurrentData := FDataServer.LoadDataFile(FCurrentFileName);
// Forward all future done events, because this means there is new data.
FCurrentData.Done.Subscribe( FHasData );
FNextFileName := FDataServer.FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
begin
FNextData := FDataServer.LoadDataFile(FNextFileName);
FNextData.Done.Subscribe( FHasData );
end;
end;
function TAuraFileStream<T>.GetChunk(var Data: array of TDataPoint<T>): Integer;
var
item: TDataPoint<T>;
currData: TArray<TDataPoint<T>>;
begin
Result := 0;
// This is an asynchronous operation! We don't wait for data. It's totally valid to result nothing, if there is nothing.
if not FCurrentData.Done.IsSet then
exit;
if FCurrPosInFile >= Length(FCurrentData.Value) then
begin
FCurrentData := FNextData;
FCurrentFileName := FNextFileName;
FCurrPosInFile := 0;
FNextData := TFuture<TArray<TDataPoint<T>>>.Null;
if FCurrentFileName = '' then
exit;
FNextFileName := FDataServer.FindNextDataFile(FCurrentFileName);
if FNextFileName <> '' then
begin
FNextData := FDataServer.LoadDataFile(FNextFileName);
FNextData.Done.Subscribe( FHasData );
end;
if not FCurrentData.Done.IsSet then
exit;
end;
currData := FCurrentData.Value;
var maxLen := Length(Data);
while (Result < maxLen) and (FCurrPosInFile < Length(currData)) do
begin
item := currData[FCurrPosInFile];
if FLastTimeStamp < item.Time then
begin
FLastTimeStamp := item.Time;
Data[Result].Create(FLastTimeStamp, item.Data);
Inc(Result);
end;
Inc(FCurrPosInFile);
end;
// If there is data left in the current file, signal new data. If it is finished, we do nothing, because
// the next signal will come from the next future being done.
if FCurrPosInFile < Length(currData) then
begin
FHasData.Notify;
end;
end;
function TAuraFileStream<T>.GetHasData: TSignal;
begin
Result := FHasData.Signal;
end;
function TAuraFileStream<T>.IsHistory: Boolean;
begin
Result := True;
end;
{ TAuraTABFileServer }
function TAuraTABFileServer.CreateStream(const Symbol: String): IDataStream<TAskBidItem>;
var
fileName: string;
begin
// FindFirstDataFile uses the server's path and the given symbol to locate the initial data file.
fileName := FindFirstDataFile(FPath, Symbol);
// Exit if no file is found or if it is not a .tab file, preventing stream creation for invalid sources.
if (fileName = '') or (not TPath.GetExtension(fileName).StartsWith('.tab')) then
exit(nil);
// Creates a new stream instance, passing itself as the data server and the found filename.
Result := TAuraFileStream<TAskBidItem>.Create(Self, fileName);
end;
class function TAuraTABFileServer.TryParseFileName(
const FileName: string;
out PathValue, SymbolValue: string;
out YearValue, MonthValue: Integer
): Boolean;
var
fileNameNoPath: string;
nameForParsing: string;
parts: TArray<string>;
baseName: string;
begin
Result := False;
PathValue := '';
SymbolValue := '';
YearValue := 0;
MonthValue := 0;
PathValue := TPath.GetDirectoryName(FileName);
fileNameNoPath := TPath.GetFileName(FileName);
nameForParsing := fileNameNoPath;
if nameForParsing.EndsWith('_zip', True) then
begin
baseName := nameForParsing.Substring(0, nameForParsing.Length - 4);
if TPath.GetExtension(baseName) <> '' then
begin
nameForParsing := baseName;
end;
end;
nameForParsing := TPath.GetFileNameWithoutExtension(nameForParsing);
parts := nameForParsing.Split(['_']);
if Length(parts) < 3 then
exit;
if not TryStrToInt(parts[High(parts)], MonthValue) then
exit;
if (MonthValue < 1) or (MonthValue > 12) then
begin
MonthValue := 0;
exit;
end;
if not TryStrToInt(parts[High(parts) - 1], YearValue) then
begin
MonthValue := 0;
exit;
end;
if YearValue <= 0 then
begin
YearValue := 0;
MonthValue := 0;
exit;
end;
SymbolValue := string.Join('_', Copy(parts, 0, Length(parts) - 2));
if SymbolValue = '' then
begin
PathValue := '';
YearValue := 0;
MonthValue := 0;
exit;
end;
Result := True;
end;
end.