unit Myc.Trade.DataStream; interface uses System.SysUtils, System.Classes, System.Generics.Collections, System.Generics.Defaults, System.IOUtils, Myc.Futures, Myc.Signals, Myc.Mutable, Myc.Trade.Types, Myc.Data.Pipeline, Myc.Core.FileCache; type // Match C# M1Record (Pack = 1, 48 Bytes) TM1Record = packed record Time: Double; Open: Double; High: Double; Low: Double; Close: Double; Spread: Single; Volume: Integer; end; // Match C# TickRecord (Pack = 1, 24 Bytes) TTickRecord = packed record Time: Double; Ask: Double; Bid: Double; end; // Interface for an instantiable data server. IDataServer = interface procedure ClearCache; function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IConsumer>>): TState; function EnumerateSymbols: TFuture>; end; // Metadata for a monthly data file (SYMBOL_YYYY_MM.EXT) TDataFile = record private FExtension: String; FPath: String; FSymbol: String; FYear: Integer; FMonth: Integer; FBasename: String; function GetIsValid: Boolean; public constructor Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer; const ABasename: String); function GetBaseFileName: string; function GetFullFileName: string; property IsValid: Boolean read GetIsValid; property Extension: String read FExtension; property Path: String read FPath; property Symbol: String read FSymbol; property Year: Integer read FYear; property Month: Integer read FMonth; end; // Generic server base handling the pipeline and preloading. TDataServer = class(TInterfacedObject, IDataServer) private FCachedFiles: TDataFileCache>>>; FPath: String; FSymbols: TFuture>>; function GetPath: String; function ProcessChunks( const DataChunks: TArray>>; Terminated: TState; Processor: IConsumer>> ): TState; function ProcessFile( FileInfo: TDataFile; const DataFile: TFuture>>>; const Terminated: TState; Processor: IConsumer>> ): TState; strict private class var FLoadGate: TLatch; protected // Maps the raw file record to the internal TDataPoint function MapRecord(const RecordBuffer: R): TDataPoint; virtual; abstract; function DoLoad(const FileName: string; const Gate: TLatch): TFuture>>>; function GetNextFile(const Curr: TDataFile): TDataFile; virtual; function ReadCompressedData(const InputStream: TStream): TArray>>; function ReadUncompressedData(const InputStream: TStream): TArray>>; public constructor Create(const APath: String); destructor Destroy; override; procedure AfterConstruction; override; procedure ClearCache; procedure UpdateSymbols; function FindFirstFile(const Symbol: string): TFuture; function ParseFileName(const FileName: string): TDataFile; function EnumerateSymbols: TFuture>; function LoadDataFile(const DataFile: TDataFile): TFuture>>>; function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IConsumer>>): TState; property Path: String read GetPath; end; TTickFileServer = class(TDataServer) protected function MapRecord(const RecordBuffer: TTickRecord): TDataPoint; override; end; TM1FileServer = class(TDataServer) protected function MapRecord(const RecordBuffer: TM1Record): TDataPoint; override; end; implementation uses System.Zip, System.ZLib, System.Math, System.StrUtils, Myc.TaskManager; const ChunkSize = 1024; { TDataFile } constructor TDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer; const ABasename: String); begin FPath := APath; FSymbol := ASymbol; FExtension := AExtension; FYear := AYear; FMonth := AMonth; FBasename := ABasename; end; function TDataFile.GetBaseFileName: string; begin Result := FBasename; end; function TDataFile.GetFullFileName: string; begin Result := TPath.Combine(FPath, FBasename + FExtension); end; function TDataFile.GetIsValid: Boolean; begin Result := (FSymbol <> '') and (FYear > 0) and (FMonth >= 1) and (FMonth <= 12); end; { TDataServer } constructor TDataServer.Create(const APath: String); begin inherited Create; FPath := APath; FCachedFiles := TDataFileCache>>>.Create; end; destructor TDataServer.Destroy; begin ClearCache; FCachedFiles.Free; inherited Destroy; end; procedure TDataServer.AfterConstruction; begin inherited; UpdateSymbols; end; procedure TDataServer.ClearCache; begin FCachedFiles.Clear; end; procedure TDataServer.UpdateSymbols; begin FSymbols := TFuture>>.Construct( FSymbols.Done, function: TDictionary> var fileNames: TArray; tempSymbolMap: TDictionary>; dataFile: TDataFile; symbolList: TList; begin Result := TDictionary>.Create; tempSymbolMap := TDictionary>.Create; try if not TDirectory.Exists(FPath) then exit; fileNames := TDirectory.GetFiles(FPath); for var currentFile in fileNames do begin dataFile := ParseFileName(currentFile); if dataFile.IsValid then begin if not tempSymbolMap.TryGetValue(dataFile.Symbol, symbolList) then begin symbolList := TList.Create; tempSymbolMap.Add(dataFile.Symbol, symbolList); end; symbolList.Add(dataFile); end; end; for var kvp in tempSymbolMap do begin symbolList := kvp.Value; symbolList.Sort( TComparer.Construct( function(const Left, Right: TDataFile): Integer begin if Left.Year <> Right.Year then Result := Left.Year - Right.Year else Result := Left.Month - Right.Month; end ) ); Result.Add(kvp.Key, symbolList.ToArray); end; finally for var kvp in tempSymbolMap do kvp.Value.Free; tempSymbolMap.Free; end; end ); FSymbols.Manage; end; function TDataServer.EnumerateSymbols: TFuture>; begin Result := FSymbols.Chain>( function(const Symbols: TDictionary>): TArray begin Result := Symbols.Keys.ToArray; end ); end; function TDataServer.FindFirstFile(const Symbol: string): TFuture; begin var symName := Symbol; Result := FSymbols.Chain( function(const Symbols: TDictionary>): TDataFile var symbolFiles: TArray; begin if Symbols.TryGetValue(symName, symbolFiles) and (Length(symbolFiles) > 0) then Result := symbolFiles[0] else Result := Default(TDataFile); end ); end; function TDataServer.GetNextFile(const Curr: TDataFile): TDataFile; var allSymbolFiles: TArray; foundIndex: Integer; begin Result := Default(TDataFile); if (not Curr.IsValid) or (not FSymbols.Done.IsSet) then exit; if not FSymbols.Value.TryGetValue(Curr.Symbol, allSymbolFiles) then exit; foundIndex := -1; for var i := 0 to High(allSymbolFiles) do begin if allSymbolFiles[i].GetBaseFileName = Curr.GetBaseFileName then begin foundIndex := i; break; end; end; if (foundIndex <> -1) and (foundIndex < High(allSymbolFiles)) then Result := allSymbolFiles[foundIndex + 1]; end; function TDataServer.GetPath: String; begin Result := FPath; end; function TDataServer.ParseFileName(const FileName: string): TDataFile; var fileNameNoPath, baseName, symbol, ext: string; parts: TArray; year, month: Integer; begin Result := Default(TDataFile); fileNameNoPath := TPath.GetFileName(FileName); ext := TPath.GetExtension(fileNameNoPath); baseName := TPath.GetFileNameWithoutExtension(fileNameNoPath); // Format: SYMBOL_YYYY_MM parts := baseName.Split(['_']); if Length(parts) < 3 then exit; // Use reverse indexing for year and month if (not TryStrToInt(parts[High(parts)], month)) or (not TryStrToInt(parts[High(parts) - 1], year)) then exit; symbol := string.Join('_', Copy(parts, 0, Length(parts) - 2)); Result := TDataFile.Create(TPath.GetDirectoryName(FileName), symbol, ext, year, month, baseName); end; function TDataServer.LoadDataFile(const DataFile: TDataFile): TFuture>>>; begin if DataFile.IsValid then Result := FCachedFiles.GetOrAdd( DataFile.GetFullFileName, function(const Filename: String): TFuture>>> begin Result := DoLoad(Filename, FLoadGate); end ) else Result := TFuture>>>.Construct(TArray>>(nil)); end; function TDataServer.DoLoad(const FileName: string; const Gate: TLatch): TFuture>>>; begin var capFileName := FileName; Result := TFuture .Construct(Gate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end) .Chain>>>( function(Bytes: TBytes): TArray>> var compStream: TBytesStream; begin compStream := TBytesStream.Create(Bytes); try Result := ReadCompressedData(compStream); finally compStream.Free; end; end); end; function TDataServer.ReadCompressedData(const InputStream: TStream): TArray>>; var zip: TZipFile; decompStream: TStream; header: TZipHeader; entryIdx: Integer; begin Result := nil; InputStream.Position := 0; zip := TZipFile.Create; try zip.Open(InputStream, TZipMode.zmRead); entryIdx := -1; // Search for the .bin entry created by C# for var i := 0 to zip.FileCount - 1 do begin if zip.FileNames[i].EndsWith('.bin', true) then begin entryIdx := i; break; end; end; if entryIdx = -1 then exit; zip.Read(entryIdx, decompStream, header); try Result := ReadUncompressedData(decompStream); finally decompStream.Free; end; finally zip.Free; end; end; function TDataServer.ReadUncompressedData(const InputStream: TStream): TArray>>; var totalRecords, chunkCount, i, j, recsInChunk: Integer; rawBuffer: TArray; begin Result := nil; totalRecords := InputStream.Size div sizeof(R); if totalRecords = 0 then exit; // Fast block read into memory SetLength(rawBuffer, totalRecords); InputStream.Position := 0; InputStream.ReadBuffer(rawBuffer[0], InputStream.Size); chunkCount := (totalRecords + ChunkSize - 1) div ChunkSize; SetLength(Result, chunkCount); for i := 0 to chunkCount - 1 do begin recsInChunk := Min(ChunkSize, totalRecords - (i * ChunkSize)); SetLength(Result[i], recsInChunk); for j := 0 to recsInChunk - 1 do begin Result[i][j] := MapRecord(rawBuffer[i * ChunkSize + j]); end; end; end; function TDataServer.ProcessChunks( const DataChunks: TArray>>; Terminated: TState; Processor: IConsumer>> ): TState; begin var callProcess := function(Idx: Integer): TFunc begin var cChunk := DataChunks[Idx]; Result := function: TState begin if not Terminated.IsSet then Result := Processor.Consume(cChunk); end; end; for var i := 0 to High(DataChunks) do begin if not Terminated.IsSet then begin var q := TaskManager.RunTask(Result, callProcess(i)); Result := q; end; end; end; function TDataServer.ProcessFile( FileInfo: TDataFile; const DataFile: TFuture>>>; const Terminated: TState; Processor: IConsumer>> ): TState; begin if (not FileInfo.IsValid) or (Terminated.IsSet) then exit(DataFile.Done); var nextFileInfo := GetNextFile(FileInfo); var nextFile := LoadDataFile(nextFileInfo); var cTerminated := Terminated; Result := DataFile.Chain( function(const DataChunks: TArray>>): TState begin var done := ProcessChunks(DataChunks, cTerminated, Processor); Result := TaskManager .RunTask(done, function: TState begin Result := ProcessFile(nextFileInfo, nextFile, cTerminated, Processor); end); end ); end; function TDataServer.ProcessData( const Symbol: String; const Terminated: TState; const Processor: IConsumer>> ): TState; begin var cProc := Processor; var cTerm := Terminated; Result := FindFirstFile(Symbol) .Chain(function(const Info: TDataFile): TState begin Result := ProcessFile(Info, LoadDataFile(Info), cTerm, cProc); end); end; { TTickFileServer } function TTickFileServer.MapRecord(const RecordBuffer: TTickRecord): TDataPoint; begin Result := TDataPoint.Create(RecordBuffer.Time, RecordBuffer); end; { TM1FileServer } function TM1FileServer.MapRecord(const RecordBuffer: TM1Record): TDataPoint; begin Result := TDataPoint.Create( RecordBuffer.Time, TOhlcItem.Create(RecordBuffer.Open, RecordBuffer.High, RecordBuffer.Low, RecordBuffer.Close, RecordBuffer.Volume) ); end; end.