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
+
+
+
+
+
+ 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.