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; IDataServer = interface procedure ClearCache; function EnumerateSymbols: TFuture>; end; // Interface for an instantiable data server. IDataServer = interface(IDataServer) function ProcessData(const Symbol: String; const Terminated: TState; const Processor: IConsumer>>): TState; end; // Metadata for a monthly data file (SYMBOL_YYYY_MM.EXT) TDataFile = record private FPath: String; FSymbol: String; // Includes extension, e.g. "EURUSD.M1" FYear: Integer; FMonth: Integer; FBasename: String; function GetIsValid: Boolean; public constructor Create(const APath, ASymbol: String; AYear, AMonth: Integer; const ABasename: String); function GetBaseFileName: string; function GetFullFileName: string; property IsValid: Boolean read GetIsValid; 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, IDataServer) private // Cache stores binary blobs of the MAPPED data (TDataPoint) FCachedFiles: IDataFileCache>; FPath: String; FSymbols: TFuture>>; function GetPath: String; function ProcessChunks(const MappedBlobs: 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; class constructor CreateClass; protected // Maps the raw file record to the internal TDataPoint function MapRecord(const RecordBuffer: R): TDataPoint; virtual; abstract; // Returns Future of Processed Blobs (Serialized TDataPoint) 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 // Cache is injected. It stores TArray. constructor Create(const ACache: IDataFileCache>; 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; ITickDataServer = interface(IDataServer) ['{A6DE8B82-B0FB-48DB-A39A-CE91D861C5D2}'] end; TTickFileServer = class(TDataServer, ITickDataServer) protected function MapRecord(const RecordBuffer: TTickRecord): TDataPoint; override; end; IM1DataServer = interface(IDataServer) ['{7BB980D9-1969-49A4-991F-908F2DDAC1A0}'] end; TM1FileServer = class(TDataServer, IM1DataServer) 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: String; AYear, AMonth: Integer; const ABasename: String); begin FPath := APath; FSymbol := ASymbol; FYear := AYear; FMonth := AMonth; FBasename := ABasename; end; function TDataFile.GetBaseFileName: string; begin Result := FBasename; end; function TDataFile.GetFullFileName: string; var ext: string; begin ext := TPath.GetExtension(FSymbol); Result := TPath.Combine(FPath, FBasename + ext); end; function TDataFile.GetIsValid: Boolean; begin Result := (FSymbol <> '') and (FYear > 0) and (FMonth >= 1) and (FMonth <= 12); end; { TDataServer } class constructor TDataServer.CreateClass; begin FLoadGate := TLatch.CreateLatch(0); end; constructor TDataServer.Create(const ACache: IDataFileCache>; const APath: String); begin inherited Create; FPath := APath; FCachedFiles := ACache; end; destructor TDataServer.Destroy; begin inherited Destroy; end; procedure TDataServer.AfterConstruction; begin inherited; UpdateSymbols; end; procedure TDataServer.ClearCache; begin if Assigned(FCachedFiles) then 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); parts := baseName.Split(['_']); if (Length(parts) < 3) then exit; 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)) + ext; Result := TDataFile.Create(TPath.GetDirectoryName(FileName), symbol, 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; inputRecordSize, outputPointSize: Integer; chunkBuffer: TArray>; rawBufferChunk: TArray; // Only buffer one chunk at a time begin Result := nil; inputRecordSize := SizeOf(R); outputPointSize := SizeOf(TDataPoint); // Ensure we are working with complete records if (InputStream.Size mod inputRecordSize <> 0) then raise Exception.Create('Stream size is not a multiple of record size.'); totalRecords := InputStream.Size div inputRecordSize; if (totalRecords = 0) then exit; chunkCount := (totalRecords + ChunkSize - 1) div ChunkSize; SetLength(Result, chunkCount); SetLength(rawBufferChunk, ChunkSize); // Reuse buffer InputStream.Position := 0; for i := 0 to chunkCount - 1 do begin recsInChunk := Min(ChunkSize, totalRecords - (i * ChunkSize)); // Read only needed bytes for this chunk InputStream.ReadBuffer(rawBufferChunk[0], recsInChunk * inputRecordSize); // Temp buffer for mapped objects SetLength(chunkBuffer, recsInChunk); for j := 0 to recsInChunk - 1 do begin chunkBuffer[j] := MapRecord(rawBufferChunk[j]); end; // Serialize mapped data to TBytes SetLength(Result[i], recsInChunk * outputPointSize); Move(chunkBuffer[0], Result[i][0], recsInChunk * outputPointSize); end; end; function TDataServer.ProcessChunks( const MappedBlobs: TArray; Terminated: TState; Processor: IConsumer>> ): TState; begin var callProcess := function(Idx: Integer): TFunc begin // Capture the blob var cBlob := MappedBlobs[Idx]; Result := function: TState var dataChunk: TArray>; count: Integer; begin if not Terminated.IsSet then begin // Quick deserialize (Mem Copy) count := Length(cBlob) div SizeOf(TDataPoint); SetLength(dataChunk, count); if count > 0 then Move(cBlob[0], dataChunk[0], Length(cBlob)); Result := Processor.Consume(dataChunk); end; end; end; for var i := 0 to High(MappedBlobs) 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 MappedBlobs: TArray): TState begin var done := ProcessChunks(MappedBlobs, 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.