diff --git a/AuraTrader/AuraTrader.dpr b/AuraTrader/AuraTrader.dpr new file mode 100644 index 0000000..3ee392a --- /dev/null +++ b/AuraTrader/AuraTrader.dpr @@ -0,0 +1,15 @@ +program AuraTrader; + +uses + FastMM5, + System.StartUpCopy, + FMX.Forms, + MainForm in 'MainForm.pas' {Form1}; + +{$R *.res} + +begin + Application.Initialize; + Application.CreateForm(TForm1, Form1); + Application.Run; +end. diff --git a/AuraTrader/AuraTrader.dproj b/AuraTrader/AuraTrader.dproj new file mode 100644 index 0000000..d7d0f0e --- /dev/null +++ b/AuraTrader/AuraTrader.dproj @@ -0,0 +1,1146 @@ + + + {9EC42A5B-872B-410F-AF42-A0A62DBF91E0} + 20.3 + FMX + True + Debug + Win64 + AuraTrader + 3 + Application + AuraTrader.dpr + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_1 + true + true + + + true + Cfg_1 + true + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + .\$(Platform)\$(Config) + .\$(Platform)\$(Config) + false + false + false + false + false + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + $(BDS)\bin\delphi_PROJECTICON.ico + $(BDS)\bin\delphi_PROJECTICNS.icns + AuraTrader + T:\Myc\Src;$(DCC_UnitSearchPath) + 1031 + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;IndyProtocols;vclx;Skia.Package.RTL;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;Skia.Package.FMX;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + Debug + true + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + $(BDS)\bin\default_app.manifest + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + + + vclwinx;fmx;vclie;DbxCommonDriver;bindengine;IndyIPCommon;VCLRESTComponents;FireDACCommonODBC;FireDACCommonDriver;IndyProtocols;vclx;IndyIPClient;dbxcds;vcledge;bindcompvclwinx;FmxTeeUI;bindcompfmx;inetdb;FireDACSqliteDriver;DbxClientDriver;Tee;soapmidas;vclactnband;TeeUI;fmxFireDAC;dbexpress;DBXMySQLDriver;VclSmp;inet;vcltouch;fmxase;dbrtl;fmxdae;TeeDB;FireDACMSAccDriver;CustomIPTransport;vcldsnap;DBXInterBaseDriver;IndySystem;Skia.Package.VCL;vcldb;vclFireDAC;bindcomp;FireDACCommon;inetstn;IndyCore;RESTBackendComponents;bindcompdbx;rtl;FireDACMySQLDriver;FireDACADSDriver;RESTComponents;DBXSqliteDriver;vcl;IndyIPServer;dsnapxml;dsnapcon;adortl;vclimg;FireDACPgDriver;FireDAC;inetdbxpress;xmlrtl;tethering;bindcompvcl;dsnap;CloudService;fmxobj;bindcompvclsmp;FMXTee;soaprtl;soapserver;FireDACIBDriver;$(DCC_UsePackage) + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + Debug + true + 1033 + $(BDS)\bin\default_app.manifest + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_44.png + $(BDS)\bin\Artwork\Windows\UWP\delphi_UwpDefault_150.png + + + DEBUG;$(DCC_Define) + true + false + true + true + true + true + true + + + false + PerMonitorV2 + + + PerMonitorV2 + true + 1033 + + + false + RELEASE;$(DCC_Define) + 0 + 0 + + + PerMonitorV2 + + + PerMonitorV2 + + + + MainSource + + +
Form1
+
+ + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + +
+ + Delphi.Personality.12 + Application + + + + AuraTrader.dpr + + + + + + true + + + + + true + + + + + true + + + + + AuraTrader.exe + true + + + + + AuraTrader.rsm + true + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + res\xml + 1 + + + res\xml + 1 + + + + + library\lib\armeabi + 1 + + + library\lib\armeabi + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + library\lib\mips + 1 + + + library\lib\mips + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + + + library\lib\armeabi-v7a + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v21 + 1 + + + res\drawable-anydpi-v21 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-v21 + 1 + + + res\values-v21 + 1 + + + + + res\values-v31 + 1 + + + res\values-v31 + 1 + + + + + res\values-v35 + 1 + + + res\values-v35 + 1 + + + + + res\drawable-anydpi-v26 + 1 + + + res\drawable-anydpi-v26 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-anydpi-v33 + 1 + + + res\drawable-anydpi-v33 + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\values-night-v21 + 1 + + + res\values-night-v21 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-ldpi + 1 + + + res\drawable-ldpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-mdpi + 1 + + + res\drawable-mdpi + 1 + + + + + res\drawable-hdpi + 1 + + + res\drawable-hdpi + 1 + + + + + res\drawable-xhdpi + 1 + + + res\drawable-xhdpi + 1 + + + + + res\drawable-xxhdpi + 1 + + + res\drawable-xxhdpi + 1 + + + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + + + + res\drawable-small + 1 + + + res\drawable-small + 1 + + + + + res\drawable-normal + 1 + + + res\drawable-normal + 1 + + + + + res\drawable-large + 1 + + + res\drawable-large + 1 + + + + + res\drawable-xlarge + 1 + + + res\drawable-xlarge + 1 + + + + + res\values + 1 + + + res\values + 1 + + + + + res\drawable-anydpi-v24 + 1 + + + res\drawable-anydpi-v24 + 1 + + + + + res\drawable + 1 + + + res\drawable + 1 + + + + + res\drawable-night-anydpi-v21 + 1 + + + res\drawable-night-anydpi-v21 + 1 + + + + + res\drawable-anydpi-v31 + 1 + + + res\drawable-anydpi-v31 + 1 + + + + + res\drawable-night-anydpi-v31 + 1 + + + res\drawable-night-anydpi-v31 + 1 + + + + + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + Contents\MacOS + 1 + .framework + + + 0 + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .dll;.bpl + + + + + 1 + .dylib + + + 1 + .dylib + + + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + Contents\MacOS + 1 + .dylib + + + 0 + .bpl + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + Contents\Resources\StartUp\ + 0 + + + 0 + + + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + Contents + 1 + + + Contents + 1 + + + Contents + 1 + + + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + Contents\Resources + 1 + + + + + library\lib\armeabi-v7a + 1 + + + library\lib\arm64-v8a + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + Contents\MacOS + 1 + + + 0 + + + + + library\lib\armeabi-v7a + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + ..\$(PROJECTNAME).app.dSYM\Contents\Resources\DWARF + 1 + + + + + ..\ + 1 + + + ..\ + 1 + + + ..\ + 1 + + + + + 1 + + + 1 + + + 1 + + + + + ..\$(PROJECTNAME).launchscreen + 64 + + + ..\$(PROJECTNAME).launchscreen + 64 + + + + + 1 + + + 1 + + + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + Assets + 1 + + + Assets + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + + + + + + + + + + + + + True + True + + + 12 + + + + +
diff --git a/AuraTrader/AuraTrader.mm b/AuraTrader/AuraTrader.mm new file mode 100644 index 0000000..82e44a4 --- /dev/null +++ b/AuraTrader/AuraTrader.mm @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AuraTrader/AuraTrader.res b/AuraTrader/AuraTrader.res new file mode 100644 index 0000000..5c25dbd Binary files /dev/null and b/AuraTrader/AuraTrader.res differ diff --git a/AuraTrader/MainForm.fmx b/AuraTrader/MainForm.fmx new file mode 100644 index 0000000..ce121c7 --- /dev/null +++ b/AuraTrader/MainForm.fmx @@ -0,0 +1,68 @@ +object Form1: TForm1 + Left = 0 + Top = 0 + Caption = 'Form1' + ClientHeight = 840 + ClientWidth = 808 + FormFactor.Width = 320 + FormFactor.Height = 480 + FormFactor.Devices = [Desktop] + OnCreate = FormCreate + OnDestroy = FormDestroy + DesignerMasterStyle = 0 + object Panel1: TPanel + Align = Top + Size.Width = 808.00000000000000000 + Size.Height = 529.00000000000000000 + Size.PlatformDefault = False + TabOrder = 2 + object RandomButton: TButton + Position.X = 697.00000000000000000 + Position.Y = 38.00000000000000000 + TabOrder = 0 + Text = 'Random' + TextSettings.Trimming = None + OnClick = RandomButtonClick + end + object Path1: TPath + Anchors = [akLeft, akTop, akRight] + Position.X = 8.00000000000000000 + Position.Y = 8.00000000000000000 + Size.Width = 489.00000000000000000 + Size.Height = 513.00000000000000000 + Size.PlatformDefault = False + end + object SymbolsComboBox: TComboBox + Anchors = [akTop, akRight] + Position.X = 505.00000000000000000 + Position.Y = 8.00000000000000000 + Size.Width = 217.00000000000000000 + Size.Height = 22.00000000000000000 + Size.PlatformDefault = False + TabOrder = 2 + end + object LoadButton: TButton + Anchors = [akTop, akRight] + Position.X = 730.00000000000000000 + Position.Y = 8.00000000000000000 + Size.Width = 47.00000000000000000 + Size.Height = 22.00000000000000000 + Size.PlatformDefault = False + TabOrder = 3 + Text = 'Load' + TextSettings.Trimming = None + OnClick = LoadButtonClick + end + end + object LogMemo: TMemo + Touch.InteractiveGestures = [Pan, LongTap, DoubleTap] + DataDetectorTypes = [] + Align = Client + Size.Width = 808.00000000000000000 + Size.Height = 311.00000000000000000 + Size.PlatformDefault = False + TabOrder = 1 + Viewport.Width = 804.00000000000000000 + Viewport.Height = 307.00000000000000000 + end +end diff --git a/AuraTrader/MainForm.pas b/AuraTrader/MainForm.pas new file mode 100644 index 0000000..d2fdf64 --- /dev/null +++ b/AuraTrader/MainForm.pas @@ -0,0 +1,172 @@ +unit MainForm; + +interface + +uses + System.SysUtils, + System.Types, + System.UITypes, + System.Classes, + System.Variants, + System.DateUtils, + FMX.Types, + FMX.Controls, + FMX.Forms, + FMX.Graphics, + FMX.Dialogs, + FMX.Controls.Presentation, + FMX.StdCtrls, + FMX.ListView.Types, + FMX.ListView.Appearances, + FMX.ListView.Adapters.Base, + FMX.ListView, + FMX.Memo.Types, + FMX.ScrollBox, + FMX.Memo, + FMX.Objects, + Myc.Futures, + Myc.Trade.DataStream, + Myc.Trade.DataPoint, + Myc.Signals, + Myc.Signals.FMX, + FMX.ListBox; + +type + TForm1 = class(TForm) + LogMemo: TMemo; + Panel1: TPanel; + RandomButton: TButton; + Path1: TPath; + SymbolsComboBox: TComboBox; + LoadButton: TButton; + procedure RandomButtonClick(Sender: TObject); + procedure FormCreate(Sender: TObject); + procedure FormDestroy(Sender: TObject); + procedure LoadButtonClick(Sender: TObject); + private + FOnEvent: TNotifyEvent; + { Private declarations } + FServer: IDataServer; + FSymbolsValid: TFMXValidationState; + FStreamUpdate: TFMXValidationState; + FSymbols: TFuture>; + FStream: IDataStream; + FRandomStream: array[0..19] of IDataStream; + FStreamIdx: Integer; + procedure DoIdle(Sender: TObject; var Done: Boolean); + public + { Public declarations } + published + property OnEvent: TNotifyEvent read FOnEvent write FOnEvent; + end; + +var + Form1: TForm1; + +implementation + +{$R *.fmx} + +procedure TForm1.FormCreate(Sender: TObject); +begin + // Create an instance of the TAuraTABFileServer. The server can be reused for multiple stream creations. [364] + FServer := TAuraTABFileServer.Create('\\COFFEE\TickData\Pepperstone'); + + SymbolsComboBox.Enabled := false; + + FSymbols := FServer.EnumerateSymbols; + + FSymbolsValid := + TFMXValidationState.Create( + SymbolsComboBox, + FSymbols.Done, + procedure + begin + SymbolsComboBox.BeginUpdate; + try + SymbolsComboBox.Items.Clear; + SymbolsComboBox.Items.AddStrings(FSymbols.WaitFor); + if SymbolsComboBox.Items.Count > 0 then + begin + if SymbolsComboBox.ItemIndex < 0 then + SymbolsComboBox.ItemIndex := SymbolsComboBox.Items.IndexOf('GER40'); + SymbolsComboBox.Enabled := true; + end; + finally + SymbolsComboBox.EndUpdate; + end; + end + ); + + Application.OnIdle := DoIdle; +end; + +procedure TForm1.FormDestroy(Sender: TObject); +begin + Application.OnIdle := nil; + FSymbolsValid.Free; +end; + +procedure TForm1.LoadButtonClick(Sender: TObject); +begin + FStream := FServer.CreateStream(FSymbols.WaitFor[SymbolsComboBox.ItemIndex]); + FStreamUpdate := TFMXValidationState.Create(Path1, FStream.HasData, + procedure + var + Data: array [0..5] of TDataPoint; + begin + var cnt := FStream.GetChunk(Data); + if cnt = 0 then exit; + + for var i:=0 to cnt-1 do + Path1.Data.LineTo( PointF(Path1.Data.Count, Data[i].Data.Ask) ); + end); +end; + +procedure TForm1.RandomButtonClick(Sender: TObject); +begin + var ass := FSymbols.WaitFor; + FRandomStream[FStreamIdx] := FServer.CreateStream(ass[Random(Length(ass))]); + + for var i := 0 to High(FRandomStream) do + if LogMemo.Lines.Count = i then + LogMemo.Lines.Add(''); + LogMemo.Lines[FStreamIdx] := FRandomStream[FStreamIdx].Symbol + ' loading...'; + + FStreamIdx := (FStreamIdx + 1) mod Length(FRandomStream); +end; + +procedure TForm1.DoIdle(Sender: TObject; var Done: Boolean); +var + Items: TArray>; +begin + SetLength(Items, 200); + + LogMemo.BeginUpdate; + try + for var i := 0 to High(FRandomStream) do + begin + var str := ''; + if LogMemo.Lines.Count > i then + str := LogMemo.Lines[i] + else + LogMemo.Lines.Add(''); + + if Assigned(FRandomStream[i]) then + begin + var cnt := FRandomStream[i].GetChunk(Items); + if cnt > 0 then + begin + var d := Items[cnt - 1]; + str := FRandomStream[i].Symbol + ' ' + cnt.ToString + ' ' + d.Time.ToString + ': ' + d.Data.Ask.ToString; + end; + end; + + LogMemo.Lines[i] := str; + end; + finally + LogMemo.EndUpdate; + end; +end; + +end. diff --git a/AuraTrader/Projektplan.md b/AuraTrader/Projektplan.md new file mode 100644 index 0000000..85d8f0a --- /dev/null +++ b/AuraTrader/Projektplan.md @@ -0,0 +1,74 @@ +# Projektplan: Meilenstein Daten-Infrastruktur + +*Datum: 13. Juni 2025* + +## Status: Design-Phase abgeschlossen, Basis-Implementierung erfolgt + +--- + +## Stufe 1: Asynchrone Datenstrom-Schnittstelle (`IDataStream`) - **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`-Schnittstelle wurde überarbeitet. Die `HasData`-Eigenschaft liefert nun ein `TSignal`, das Konsumenten aktiv über potenziell neue Daten informiert. + * Die Referenzimplementierung `TAuraFileStream` 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` 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. + +# TODO-Einträge: Dateninfrastruktur + +## Stufe 2: `IDataSeriesProvider` + +- [ ] **Interface und Basis-Implementierung erstellen** + * Definiere das `IDataSeriesProvider`-Interface. + * Definiere die `TProviderStatus`-Enumeration (`psInitializing`, `psReadyContinuous`, `psReadyWithGap`, `psFaulted`). + * Implementiere eine abstrakte Basisklasse `TDataSeriesProvider`, die die grundlegenden Felder (z.B. für die interne `TDataSeries`) und Methoden bereitstellt. + +- [ ] **Simulations-Modus implementieren** + * Erstelle eine konkrete Implementierung (z.B. `TSimulationSeriesProvider`). + * Implementiere die Start-Logik: + * Laden des gesamten relevanten Datenkontexts via `TAuraDataServer.LoadDataSeries`. + * Positionierung auf einen wählbaren Startzeitpunkt via `TDataSeries.IndexOf`. + * Extrahieren des initialen Lookback-Puffers. + * Implementiere das "Playback" der nachfolgenden Datenpunkte aus der im Speicher gehaltenen Serie. + * Setze den `Status` korrekt auf `psReadyContinuous`, da im Simulationsmodus per Definition keine Lücke zur "Gegenwart" existiert. + +- [ ] **Live-Handel Modus implementieren** + * **Voraussetzung**: Definiere eine Schnittstelle für Live-Datenquellen (z.B. `ILiveDataSource`), die eine Methode zur Abfrage rezenter historischer Daten (`GetCatchUpData`) enthält. + * **Start-Logik (Drei-Phasen-Synchronisation)**: + * Implementiere den parallelen Start der asynchronen Operationen: + 1. Laden der lokalen Historie. + 2. Herstellen der Broker-Verbindung und Abrufen der "Catch-up"-Daten. + * Implementiere die "Splicing"-Logik, die beide Datenquellen zu einem nahtlosen Lookback-Puffer verbindet. + * Implementiere die Prüfung auf ein "Markt-Gap" beim Übergang, um den initialen Status korrekt auf `psReadyContinuous` oder `psReadyWithGap` zu setzen. + * **Laufzeit-Logik**: + * Implementiere das kontinuierliche Verarbeiten des Live-Streams. + * Implementiere die Fehlerbehandlung für Laufzeit-Unterbrechungen (Verbindungsverlust), inklusive des Wechsels in den `psFaulted`-Status und des sicheren "Re-Priming". + +- [ ] **Session-Kalender entwerfen und implementieren** + * **Motivation**: Notwendig für die Unterscheidung zwischen "Markt-Gaps" (natürlich) und "Daten-Lücken" (Fehler) im Live-Betrieb. + * **Anforderung**: Muss pro Asset konfigurierbar sein (Handelszeiten, Feiertage). + * **Implementierung**: Erstelle eine Klasse, die für ein gegebenes Zeitintervall prüfen kann, ob der Markt geöffnet oder geschlossen war. Integriere diese Prüfung in die Laufzeit-Logik des `TDataSeriesProvider` im Live-Modus. \ No newline at end of file diff --git a/Src/Myc.Core.FileCache.pas b/Src/Myc.Core.FileCache.pas index f790ba9..675e7a2 100644 --- a/Src/Myc.Core.FileCache.pas +++ b/Src/Myc.Core.FileCache.pas @@ -49,7 +49,9 @@ var implementation uses - {$IFDEF MSWINDOWS} Winapi.Windows, {$endif} +{$IFDEF MSWINDOWS} + Winapi.Windows, +{$endif} System.Math; { TDataFileCache } @@ -170,7 +172,7 @@ begin end; initialization - {$IFDEF MSWINDOWS} +{$IFDEF MSWINDOWS} IsMemoryLow := function: Boolean var @@ -184,6 +186,6 @@ initialization Result := memStatusEx.dwMemoryLoad >= 80; end; end; - {$endif} +{$endif} end. diff --git a/Src/Myc.Core.Futures.pas b/Src/Myc.Core.Futures.pas index 42e699a..27ca3c2 100644 --- a/Src/Myc.Core.Futures.pas +++ b/Src/Myc.Core.Futures.pas @@ -13,6 +13,8 @@ type protected function GetValue: T; virtual; abstract; function GetDone: TState; virtual; abstract; + public + procedure BeforeDestruction; override; end; TMycNullFuture = class(TMycFuture) @@ -34,8 +36,27 @@ type destructor Destroy; override; end; + TMycFutureManaged = class(TMycFuture) + private + FFuture: TFuture.IFuture; + protected + function GetValue: T; override; + function GetDone: TState; override; + public + constructor Create(const AFuture: TFuture.IFuture); + destructor Destroy; override; + end; + implementation +{ TMycFuture } + +procedure TMycFuture.BeforeDestruction; +begin + inherited; + Assert(GetDone.IsSet, 'Trying to destroy an unfinished future'); +end; + { TMycNullFuture } function TMycNullFuture.GetDone: TState; @@ -79,8 +100,6 @@ end; destructor TMycGateFuncFuture.Destroy; begin - Assert(FDone.State.IsSet, 'Future not done'); - FInit.Unsubscribe; inherited Destroy; end; @@ -96,4 +115,34 @@ begin Result := FResult; end; +{ TMycFutureManaged } + +constructor TMycFutureManaged.Create(const AFuture: TFuture.IFuture); +begin + inherited Create; + FFuture := AFuture; +end; + +destructor TMycFutureManaged.Destroy; +begin + if GetTypeKind(T) = tkClass then + begin + var val := GetValue; + var obj := TObject(PPointer(@val)^); + if obj <> nil then + obj.Free; + end; + inherited; +end; + +function TMycFutureManaged.GetDone: TState; +begin + Result := FFuture.Done; +end; + +function TMycFutureManaged.GetValue: T; +begin + Result := FFuture.Value; +end; + end. diff --git a/Src/Myc.Futures.pas b/Src/Myc.Futures.pas index 67cfe74..4d0754e 100644 --- a/Src/Myc.Futures.pas +++ b/Src/Myc.Futures.pas @@ -49,6 +49,8 @@ type class property Null: IFuture read FNull; + procedure Manage; + function Chain(const Proc: TFunc): TFuture; overload; function Chain(const Proc: TFuncConst): TFuture; overload; function WaitFor: T; @@ -113,6 +115,12 @@ begin Result := FFuture.Value; end; +procedure TFuture.Manage; +begin + if GetTypeKind(T) = tkClass then + FFuture := TMycFutureManaged.Create(FFuture); +end; + function TFuture.WaitFor: T; begin TaskManager.WaitFor(FFuture.Done); diff --git a/Src/Myc.Signals.FMX.pas b/Src/Myc.Signals.FMX.pas index 3cf79d8..aa78c16 100644 --- a/Src/Myc.Signals.FMX.pas +++ b/Src/Myc.Signals.FMX.pas @@ -4,23 +4,27 @@ interface uses System.Classes, + System.SysUtils, FMX.Controls, Myc.Signals; type - TFMXSignalLink = class(TComponent, TSignal.ISubscriber) + TFMXValidationState = class(TComponent, TSignal.ISubscriber) private [volatile] FInvalidated: Integer; - procedure InvalidateControl; - + FSignal: TSignal.ISignal; + FSubscription: TSignal.TSubscriptionTag; + FProc: TProc; + procedure CallProc; protected function Notify: Boolean; public - constructor Create(AOwner: TControl); reintroduce; + constructor Create(AOwner: TControl; const ASignal: TSignal.ISignal; const AProc: TProc); reintroduce; destructor Destroy; override; - function Reset: Boolean; + procedure AfterConstruction; override; + procedure BeforeDestruction; override; end; implementation @@ -28,36 +32,45 @@ implementation uses System.SyncObjs; -{ TFMXSignalLink } +{ TFMXValidationState } -constructor TFMXSignalLink.Create(AOwner: TControl); +constructor TFMXValidationState.Create(AOwner: TControl; const ASignal: TSignal.ISignal; const AProc: TProc); begin inherited Create(AOwner); FInvalidated := 0; + FSignal := ASignal; + FProc := AProc; end; -destructor TFMXSignalLink.Destroy; +destructor TFMXValidationState.Destroy; begin - TThread.RemoveQueuedEvents(InvalidateControl); + TThread.RemoveQueuedEvents(CallProc); inherited; end; -procedure TFMXSignalLink.InvalidateControl; +procedure TFMXValidationState.AfterConstruction; begin - with TControl(Owner) do - InvalidateRect(LocalRect); + inherited; + FSubscription := FSignal.Subscribe(Self); end; -function TFMXSignalLink.Notify: Boolean; +procedure TFMXValidationState.BeforeDestruction; +begin + FSignal.Unsubscribe(FSubscription); + inherited; +end; + +procedure TFMXValidationState.CallProc; +begin + if TInterlocked.Exchange(FInvalidated, 0) > 0 then + FProc; +end; + +function TFMXValidationState.Notify: Boolean; begin if TInterlocked.Exchange(FInvalidated, 1) = 0 then - TThread.Queue(nil, InvalidateControl); - exit( true ); -end; - -function TFMXSignalLink.Reset: Boolean; -begin - Result := TInterlocked.Exchange(FInvalidated, 0) > 0; + TThread.Queue(nil, CallProc); + exit(true); end; end. diff --git a/Src/Myc.Signals.pas b/Src/Myc.Signals.pas index 8303edf..dfd9855 100644 --- a/Src/Myc.Signals.pas +++ b/Src/Myc.Signals.pas @@ -3,8 +3,7 @@ unit Myc.Signals; interface uses - System.SysUtils, - System.SyncObjs; + System.SysUtils; type TSignal = record @@ -137,8 +136,7 @@ type strict private class var FNull: ILatch; - FQueueGate: TLatch; - FQueueLock: TSpinLock; + FQueueLock: Integer; class constructor ClassCreate; private @@ -153,8 +151,8 @@ type class function CreateLatch(Count: Integer): TLatch; static; - class function Enqueue(var Gate: TLatch): TState; overload; static; - class function Enqueue: TState; overload; static; + class function Enqueue1(var Gate: TLatch): TState; overload; static; + function Enqueue: TState; overload; class property Null: ILatch read FNull; @@ -394,7 +392,7 @@ class constructor TLatch.ClassCreate; begin // Create a singleton null latch instance that is initially (and always) set. FNull := TMycNullLatch.Create; - FQueueLock := TSpinLock.Create(false); + FQueueLock := 0; end; constructor TLatch.Create(const ALatch: TLatch.ILatch); @@ -412,23 +410,46 @@ begin Result := FNull; end; -class function TLatch.Enqueue(var Gate: TLatch): TState; +class function TLatch.Enqueue1(var Gate: TLatch): TState; begin var Latch: TLatch.ILatch := TMycLatch.Create(1); - FQueueLock.Enter; + repeat + if FQueueLock > 0 then + begin + YieldProcessor; + continue; + end; + until AtomicCmpExchange(FQueueLock, 1, 0) = 0; + try Gate.State.Subscribe(Latch); Gate := Latch; Result := Latch.State; finally - FQueueLock.Exit; + AtomicDecrement(FQueueLock); end; end; -class function TLatch.Enqueue: TState; +function TLatch.Enqueue: TState; begin - Result := Enqueue(FQueueGate); + var Latch: ILatch := TMycLatch.Create(1); + + repeat + if FQueueLock > 0 then + begin + YieldProcessor; + continue; + end; + until AtomicCmpExchange(FQueueLock, 1, 0) = 0; + + try + FLatch.State.Subscribe(Latch); + FLatch := Latch; + Result := FLatch.State; + finally + AtomicDecrement(FQueueLock); + end; end; function TLatch.GetState: TState; diff --git a/Src/Myc.Trade.DataStream.pas b/Src/Myc.Trade.DataStream.pas index 06571a9..789c1db 100644 --- a/Src/Myc.Trade.DataStream.pas +++ b/Src/Myc.Trade.DataStream.pas @@ -30,34 +30,6 @@ uses Myc.Core.FileCache; type - // Represents metadata for a single data file. - TDataFile = record - private - FExtension: String; - FPath: String; - FSymbol: String; - FYear: Integer; - FMonth: Integer; - function GetIsValid: Boolean; - public - constructor Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer); - function GetBaseFileName: string; - function GetFullFileName: string; - // Gets the next consecutive data file, if it exists on disk. - function GetNextFile: TDataFile; - class function ParseFileName(const FileName: string): TDataFile; static; - // Scans a directory to find the oldest file for a specific symbol. - class function FindFirst(const DirectoryPath, Symbol: string): TDataFile; static; - // Scans a directory and returns the oldest file found for each symbol. - class function FindOldestPerSymbol(const DirectoryPath: string): TArray; static; - 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; - // 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. @@ -99,34 +71,66 @@ type ['{1F8E5A9D-E92A-44C1-9F3F-C4B82A6E94B3}'] function CreateStream(const Symbol: String): IDataStream; procedure ClearCache; - function EnumerateSymbols: TArray; + function EnumerateSymbols: TFuture>; end; // Aura files + // Represents metadata for a single data file. + TAuraDataFile = record + private + FExtension: String; + FPath: String; + FSymbol: String; + FYear: Integer; + FMonth: Integer; + function GetIsValid: Boolean; + public + constructor Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer); + function GetBaseFileName: string; + function GetFullFileName: string; + // Gets the next consecutive data file, if it exists on disk. + function GetNextFile: TAuraDataFile; + 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 for loading and managing sequential time-series data from files. TAuraDataServer = class(TInterfacedObject, IDataServer) private FCachedFiles: TDataFileCache>>; FPath: String; - function DoLoad(const FileName: string): TFuture>>; + FSymbols: TFuture>; function GetPath: String; + // Used by cache to actually load an uncached file. + function DoLoad(const FileName: string): TFuture>>; + strict private class var FLoadGate: TLatch; protected - class function ReadCompressedData(const InputStream: TStream): TArray>; static; - class function ReadUncompressedData(const InputStream: TStream): TArray>; static; + // Scans a directory to find the oldest file for a specific symbol. + function FindFirstFile(const Symbol: string): TFuture; + function ParseFileName(const FileName: string): TAuraDataFile; virtual; abstract; + + class function ReadCompressedData(const InputStream: TStream): TArray>; virtual; abstract; + class function ReadUncompressedData(const InputStream: TStream): TArray>; virtual; abstract; public constructor Create(const APath: String); destructor Destroy; override; + procedure AfterConstruction; override; function CreateStream(const Symbol: String): IDataStream; virtual; abstract; procedure ClearCache; - function EnumerateSymbols: TArray; - function LoadDataFile(const DataFile: TDataFile): TFuture>>; + procedure UpdateSymbols; + function EnumerateSymbols: TFuture>; + function LoadDataFile(const DataFile: TAuraDataFile): TFuture>>; property Path: String read GetPath; end; @@ -135,7 +139,7 @@ type private type TDataStream = record - FileInfo: TDataFile; + FileInfo: TAuraDataFile; Data: TFuture>>; end; private @@ -162,9 +166,14 @@ type // Aura tick data file Ask-Bid TAuraTABFileServer = class(TAuraDataServer) + protected + function ParseFileName(const FileName: string): TAuraDataFile; override; + // Scans a directory and returns the oldest file found for each symbol. + class function ReadCompressedData(const InputStream: TStream): TArray>; override; + class function ReadUncompressedData(const InputStream: TStream): TArray>; override; public function CreateStream(const Symbol: String): IDataStream; override; - function LoadDataSeries(const InitialFile: TDataFile): TFuture>>; + function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture>>; end; implementation @@ -175,9 +184,9 @@ uses System.StrUtils, Myc.TaskManager; -{ TDataFile } +{ TAuraDataFile } -constructor TDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer); +constructor TAuraDataFile.Create(const APath, ASymbol, AExtension: String; AYear, AMonth: Integer); begin FPath := APath; FSymbol := ASymbol; @@ -186,28 +195,28 @@ begin FMonth := AMonth; end; -function TDataFile.GetBaseFileName: string; +function TAuraDataFile.GetBaseFileName: string; begin Result := Format('%s_%.4d_%.2d', [FSymbol, FYear, FMonth]); end; -function TDataFile.GetFullFileName: string; +function TAuraDataFile.GetFullFileName: string; begin Result := TPath.Combine(FPath, GetBaseFileName + FExtension); end; -function TDataFile.GetIsValid: Boolean; +function TAuraDataFile.GetIsValid: Boolean; begin Result := (FSymbol <> '') and (FYear > 0); end; -function TDataFile.GetNextFile: TDataFile; +function TAuraDataFile.GetNextFile: TAuraDataFile; var nextMonth, nextYear: Integer; - nextFile: TDataFile; + nextFile: TAuraDataFile; begin if not IsValid then - exit(Default(TDataFile)); + exit(Default(TAuraDataFile)); nextMonth := FMonth + 1; nextYear := FYear; @@ -218,137 +227,19 @@ begin end; // Probe for zipped file first - nextFile := TDataFile.Create(FPath, FSymbol, '.tab_zip', nextYear, nextMonth); + nextFile := TAuraDataFile.Create(FPath, FSymbol, '.tab_zip', nextYear, nextMonth); if TFile.Exists(nextFile.GetFullFileName) then exit(nextFile); // Probe for uncompressed file - nextFile := TDataFile.Create(FPath, FSymbol, '.tab', nextYear, nextMonth); + nextFile := TAuraDataFile.Create(FPath, FSymbol, '.tab', nextYear, nextMonth); if TFile.Exists(nextFile.GetFullFileName) then exit(nextFile); // No next file found - Result := Default(TDataFile); + Result := Default(TAuraDataFile); end; -class function TDataFile.ParseFileName(const FileName: string): TDataFile; -var - fileNameNoPath, nameForParsing, baseName, ext, path, symbol: string; - year, month: Integer; - parts: TArray; -begin - Result := Default(TDataFile); - - path := TPath.GetDirectoryName(FileName); - fileNameNoPath := TPath.GetFileName(FileName); - nameForParsing := fileNameNoPath; - - ext := ''; - if nameForParsing.EndsWith('_zip', True) then - begin - ext := '_zip'; - nameForParsing := nameForParsing.Substring(0, nameForParsing.Length - 4); - end; - - var fileExt := TPath.GetExtension(nameForParsing) + ext; - baseName := TPath.GetFileNameWithoutExtension(nameForParsing); - - parts := baseName.Split(['_']); - - if Length(parts) < 3 then - exit; - - if not TryStrToInt(parts[High(parts)], month) then - exit; - - if (month < 1) or (month > 12) then - exit; - - if not TryStrToInt(parts[High(parts) - 1], year) then - exit; - - if year <= 0 then - exit; - - symbol := string.Join('_', Copy(parts, 0, Length(parts) - 2)); - - if symbol = '' then - exit; - - Result := TDataFile.Create(path, symbol, fileExt, year, month); -end; - -class function TDataFile.FindFirst(const DirectoryPath, Symbol: string): TDataFile; -var - fileNames: TArray; - currentFileName: string; - parsedFile: TDataFile; -begin - Result := Default(TDataFile); - - if not TDirectory.Exists(DirectoryPath) then - exit; - - fileNames := TDirectory.GetFiles(DirectoryPath); - for currentFileName in fileNames do - begin - parsedFile := TDataFile.ParseFileName(currentFileName); - if parsedFile.IsValid then - begin - if SameText(parsedFile.Symbol, Symbol) then - begin - // If it's the first match or older than the current result, update. - if (not Result.IsValid) or - (parsedFile.Year < Result.Year) or - ((parsedFile.Year = Result.Year) and (parsedFile.Month < Result.Month)) then - begin - Result := parsedFile; - end; - end; - end; - end; -end; - -class function TDataFile.FindOldestPerSymbol(const DirectoryPath: string): TArray; -var - oldestFilesPerSymbol: TDictionary; - fileNames: TArray; - currentFile: string; - dataFile: TDataFile; - trackedInfo: TDataFile; -begin - oldestFilesPerSymbol := TDictionary.Create; - try - if not TDirectory.Exists(DirectoryPath) then - exit(nil); - - fileNames := TDirectory.GetFiles(DirectoryPath); - for currentFile in fileNames do - begin - dataFile := TDataFile.ParseFileName(currentFile); - if dataFile.IsValid then - begin - if oldestFilesPerSymbol.TryGetValue(dataFile.Symbol, trackedInfo) then - begin - if (dataFile.Year < trackedInfo.Year) or ((dataFile.Year = trackedInfo.Year) and (dataFile.Month < trackedInfo.Month)) then - begin - oldestFilesPerSymbol.AddOrSetValue(dataFile.Symbol, dataFile); - end; - end - else - begin - oldestFilesPerSymbol.Add(dataFile.Symbol, dataFile); - end; - end; - end; - - Result := oldestFilesPerSymbol.Values.ToArray; - finally - oldestFilesPerSymbol.Free; - end; -end; - - { TAuraDataServer } constructor TAuraDataServer.Create(const APath: String); @@ -368,27 +259,17 @@ begin inherited Destroy; end; +procedure TAuraDataServer.AfterConstruction; +begin + inherited; + UpdateSymbols; +end; + procedure TAuraDataServer.ClearCache; begin FCachedFiles.Clear; end; -function TAuraDataServer.EnumerateSymbols: TArray; -var - dataFiles: TArray; - i: Integer; -begin - dataFiles := TDataFile.FindOldestPerSymbol(FPath); - SetLength(Result, Length(dataFiles)); - for i := 0 to High(dataFiles) do - Result[i] := dataFiles[i].Symbol; -end; - -function TAuraDataServer.GetPath: String; -begin - Result := FPath; -end; - function TAuraDataServer.DoLoad(const FileName: string): TFuture>>; begin Result := TFuture>>.Null; @@ -401,9 +282,7 @@ begin begin Result := TFuture - .Construct( - TLatch.Enqueue(FLoadGate), - function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end) + .Construct(FLoadGate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end) .Chain>>( function(bytes: TBytes): TArray> begin @@ -420,7 +299,7 @@ begin begin Result := TFuture>>.Construct( - TLatch.Enqueue(FLoadGate), + FLoadGate.Enqueue, function: TArray> begin if TFile.Exists(capFileName) then @@ -442,81 +321,82 @@ begin end; end; -function TAuraDataServer.LoadDataFile(const DataFile: TDataFile): TFuture>>; +procedure TAuraDataServer.UpdateSymbols; +begin + FSymbols := + TFuture>.Construct( + FSymbols.Done, + function: TDictionary + var + fileNames: TArray; + currentFile: string; + dataFile: TAuraDataFile; + trackedInfo: TAuraDataFile; + begin + Result := TDictionary.Create; + if not TDirectory.Exists(FPath) then + exit; + + fileNames := TDirectory.GetFiles(FPath); + for currentFile in fileNames do + begin + dataFile := ParseFileName(currentFile); + if dataFile.IsValid then + begin + if Result.TryGetValue(dataFile.Symbol, trackedInfo) then + begin + if (dataFile.Year < trackedInfo.Year) + or ((dataFile.Year = trackedInfo.Year) and (dataFile.Month < trackedInfo.Month)) then + begin + Result.AddOrSetValue(dataFile.Symbol, dataFile); + end; + end + else + begin + Result.Add(dataFile.Symbol, dataFile); + end; + end; + end; + end + ); + + FSymbols.Manage; +end; + +function TAuraDataServer.EnumerateSymbols: TFuture>; +var + dataFiles: TArray; + i: Integer; +begin + Result := + FSymbols.Chain>( + function(Symbols: TDictionary): TArray begin Result := Symbols.Keys.ToArray; end + ); +end; + +function TAuraDataServer.FindFirstFile(const Symbol: string): TFuture; +begin + var symName := Symbol; + Result := + FSymbols.Chain( + function(Symbols: TDictionary): TAuraDataFile + begin + if not Symbols.TryGetValue(symName, Result) then + Result := Default(TAuraDataFile); + end + ); +end; + +function TAuraDataServer.GetPath: String; +begin + Result := FPath; +end; + +function TAuraDataServer.LoadDataFile(const DataFile: TAuraDataFile): TFuture>>; begin Result := FCachedFiles.GetOrAdd(DataFile.GetFullFileName); end; -class function TAuraDataServer.ReadCompressedData(const InputStream: TStream): TArray>; -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.ReadUncompressedData(const InputStream: TStream): TArray>; -type - TFileRecord = packed record - TimeStamp: TDateTime; - Data: T; - end; -var - fileSize: Int64; - recordCount, bytesRead: Integer; - rec: TFileRecord; -begin - SetLength(Result, 0); - InputStream.Position := 0; - fileSize := InputStream.Size; - if (fileSize = 0) or ((fileSize mod SizeOf(TFileRecord)) <> 0) then - exit; - - recordCount := fileSize div SizeOf(TFileRecord); - if recordCount > 0 then - begin - SetLength(Result, recordCount); - for var i := 0 to High(Result) do - begin - bytesRead := InputStream.Read(rec, SizeOf(TFileRecord)); - if bytesRead <> SizeOf(TFileRecord) then - raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [fileSize, bytesRead]); - Result[i].Create(rec.TimeStamp, rec.Data); - end; - end; -end; - { TAuraFileStream } constructor TAuraFileStream.Create(ADataServer: TAuraDataServer; const ASymbol: String); @@ -535,18 +415,16 @@ end; procedure TAuraFileStream.AfterConstruction; var - firstFile: TFuture; - serverPath: string; + firstFile: TFuture; begin inherited; // We need the path from the server instance to find the file. - serverPath := (FDataServer as TAuraDataServer).Path; - firstFile := TFuture.Construct(function: TDataFile begin Result := TDataFile.FindFirst(serverPath, FSymbol); end); + firstFile := FDataServer.FindFirstFile(FSymbol); FCurrent := firstFile.Chain( - function(const FileInfo: TDataFile): TDataStream + function(const FileInfo: TAuraDataFile): TDataStream begin if FileInfo.IsValid then begin @@ -638,7 +516,7 @@ begin FCurrent.Chain( function(const Prev: TDataStream): TDataStream var - nextFileInfo: TDataFile; + nextFileInfo: TAuraDataFile; begin if Prev.FileInfo.IsValid then begin @@ -661,16 +539,16 @@ begin Result := TAuraFileStream.Create(Self, Symbol); end; -function TAuraTABFileServer.LoadDataSeries(const InitialFile: TDataFile): TFuture>>; +function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture>>; var loadedState: TState; loadedFiles: TArray>>>; liveData: TFuture>>; - tabFiles: TList; - liveFile: TDataFile; - currentFileInfo: TDataFile; + tabFiles: TList; + liveFile: TAuraDataFile; + currentFileInfo: TAuraDataFile; begin - tabFiles := TList.Create; + tabFiles := TList.Create; try currentFileInfo := InitialFile; while currentFileInfo.IsValid do @@ -679,14 +557,14 @@ begin currentFileInfo := currentFileInfo.GetNextFile; end; - liveFile := Default(TDataFile); + liveFile := Default(TAuraDataFile); if tabFiles.Count > 0 then begin var lastTabFile := tabFiles.Last; var liveFileBaseName := Format('%s_%d_%02d.tab-live', [lastTabFile.Symbol, lastTabFile.Year, lastTabFile.Month]); var potentialLivePath := TPath.Combine(lastTabFile.Path, liveFileBaseName); if TFile.Exists(potentialLivePath) then - liveFile := TDataFile.ParseFileName(potentialLivePath); + liveFile := ParseFileName(potentialLivePath); end; var loadedFileList := TList>>>.Create; @@ -745,4 +623,121 @@ begin ); end; +function TAuraTABFileServer.ParseFileName(const FileName: string): TAuraDataFile; +var + fileNameNoPath, nameForParsing, baseName, ext, path, symbol: string; + year, month: Integer; + parts: TArray; +begin + Result := Default(TAuraDataFile); + + path := TPath.GetDirectoryName(FileName); + fileNameNoPath := TPath.GetFileName(FileName); + nameForParsing := fileNameNoPath; + + ext := ''; + if nameForParsing.EndsWith('_zip', True) then + begin + ext := '_zip'; + nameForParsing := nameForParsing.Substring(0, nameForParsing.Length - 4); + end; + + var fileExt := TPath.GetExtension(nameForParsing) + ext; + baseName := TPath.GetFileNameWithoutExtension(nameForParsing); + + parts := baseName.Split(['_']); + + if Length(parts) < 3 then + exit; + + if not TryStrToInt(parts[High(parts)], month) then + exit; + + if (month < 1) or (month > 12) then + exit; + + if not TryStrToInt(parts[High(parts) - 1], year) then + exit; + + if year <= 0 then + exit; + + symbol := string.Join('_', Copy(parts, 0, Length(parts) - 2)); + + if symbol = '' then + exit; + + Result := TAuraDataFile.Create(path, symbol, fileExt, year, month); +end; + +class function TAuraTABFileServer.ReadCompressedData(const InputStream: TStream): TArray>; +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 TAuraTABFileServer.ReadUncompressedData(const InputStream: TStream): TArray>; +type + TFileRecord = packed record + TimeStamp: TDateTime; + Data: TAskBidItem; + end; +var + fileSize: Int64; + recordCount, bytesRead: Integer; + rec: TFileRecord; +begin + SetLength(Result, 0); + InputStream.Position := 0; + fileSize := InputStream.Size; + if (fileSize = 0) or ((fileSize mod SizeOf(TFileRecord)) <> 0) then + exit; + + recordCount := fileSize div SizeOf(TFileRecord); + if recordCount > 0 then + begin + SetLength(Result, recordCount); + for var i := 0 to High(Result) do + begin + bytesRead := InputStream.Read(rec, SizeOf(TFileRecord)); + if bytesRead <> SizeOf(TFileRecord) then + raise EReadError.CreateFmt('Read error. Expected %d bytes, read %d.', [fileSize, bytesRead]); + Result[i].Create(rec.TimeStamp, rec.Data); + end; + end; +end; + end.