AuraTrader Sample

This commit is contained in:
Michael Schimmel
2025-06-17 21:49:09 +02:00
parent f81337c98d
commit a9aff8c41a
13 changed files with 2093 additions and 306 deletions
+15
View File
@@ -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.
File diff suppressed because it is too large Load Diff
+224
View File
@@ -0,0 +1,224 @@
<map version="freeplane 1.12.1">
<!--To view this file, download free mind mapping software Freeplane from https://www.freeplane.org -->
<node TEXT="AuraTrader" LOCALIZED_STYLE_REF="AutomaticLayout.level.root" FOLDED="false" ID="ID_1090958577" CREATED="1409300609620" MODIFIED="1749137964610" VGAP_QUANTITY="3 pt"><hook NAME="MapStyle" background="#d6e8e8ff" zoom="0.826">
<properties show_icon_for_attributes="true" edgeColorConfiguration="#808080ff,#ff0000ff,#0000ffff,#00ff00ff,#ff00ffff,#00ffffff,#7c0000ff,#00007cff,#007c00ff,#7c007cff,#007c7cff,#7c7c00ff" show_tags="UNDER_NODES" show_note_icons="true" associatedTemplateLocation="template:/light_sky_element_template.mm" fit_to_viewport="false" show_icons="BESIDE_NODES" showTagCategories="false"/>
<tags category_separator="::"/>
<map_styles>
<stylenode LOCALIZED_TEXT="styles.root_node" STYLE="oval" UNIFORM_SHAPE="true" VGAP_QUANTITY="24 pt">
<font SIZE="24"/>
<stylenode LOCALIZED_TEXT="styles.predefined" POSITION="bottom_or_right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="default" ID="ID_4172522" ICON_SIZE="12 pt" FORMAT_AS_HYPERLINK="false" COLOR="#051552" BACKGROUND_COLOR="#5cd5e8" STYLE="bubble" SHAPE_HORIZONTAL_MARGIN="8 pt" SHAPE_VERTICAL_MARGIN="5 pt" BORDER_WIDTH_LIKE_EDGE="false" BORDER_WIDTH="1.7 px" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#1164b0" BORDER_DASH_LIKE_EDGE="true" BORDER_DASH="SOLID" VGAP_QUANTITY="3 pt">
<arrowlink SHAPE="CUBIC_CURVE" COLOR="#000000" WIDTH="2" TRANSPARENCY="200" DASH="" FONT_SIZE="9" FONT_FAMILY="SansSerif" DESTINATION="ID_4172522" STARTINCLINATION="116.25 pt;0 pt;" ENDINCLINATION="116.25 pt;28.5 pt;" STARTARROW="NONE" ENDARROW="DEFAULT"/>
<font NAME="SansSerif" SIZE="10" BOLD="false" STRIKETHROUGH="false" ITALIC="false"/>
<edge STYLE="bezier" COLOR="#051552" WIDTH="2" DASH="SOLID"/>
<richcontent TYPE="DETAILS" CONTENT-TYPE="plain/auto"/>
<richcontent TYPE="NOTE" CONTENT-TYPE="plain/auto"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.details" COLOR="#fff024" BACKGROUND_COLOR="#000000"/>
<stylenode LOCALIZED_TEXT="defaultstyle.tags">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.attributes">
<font SIZE="9"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.note" COLOR="#000000" BACKGROUND_COLOR="#f6f9a1" TEXT_ALIGN="LEFT">
<icon BUILTIN="clock2"/>
<font SIZE="10" ITALIC="true"/>
<edge COLOR="#000000"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.floating">
<edge STYLE="hide_edge"/>
<cloud COLOR="#f0f0f0" SHAPE="ROUND_RECT"/>
</stylenode>
<stylenode LOCALIZED_TEXT="defaultstyle.selection" COLOR="#ffffff" BACKGROUND_COLOR="#cc7212" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#1164b0"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.user-defined" POSITION="bottom_or_right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="styles.important" ID="ID_1823054225" COLOR="#fff024" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#9d5e19">
<icon BUILTIN="yes"/>
<arrowlink COLOR="#9d5e19" TRANSPARENCY="255" DESTINATION="ID_1823054225"/>
<font SIZE="11" BOLD="true"/>
<edge COLOR="#9d5e19"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.flower" COLOR="#ffffff" BACKGROUND_COLOR="#255aba" STYLE="oval" TEXT_ALIGN="CENTER" BORDER_WIDTH_LIKE_EDGE="false" BORDER_WIDTH="22 pt" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#f9d71c" BORDER_DASH_LIKE_EDGE="false" BORDER_DASH="CLOSE_DOTS" MAX_WIDTH="6 cm" MIN_WIDTH="3 cm"/>
</stylenode>
<stylenode LOCALIZED_TEXT="styles.AutomaticLayout" POSITION="bottom_or_right" STYLE="bubble">
<stylenode LOCALIZED_TEXT="AutomaticLayout.level.root" COLOR="#ffffff" BACKGROUND_COLOR="#053d8b" STYLE="bubble" SHAPE_HORIZONTAL_MARGIN="10 pt" SHAPE_VERTICAL_MARGIN="10 pt" BORDER_COLOR_LIKE_EDGE="false" BORDER_COLOR="#2c2b29" BORDER_DASH_LIKE_EDGE="true">
<font SIZE="18"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,1" COLOR="#ffffff" BACKGROUND_COLOR="#1164b0" STYLE="bubble" SHAPE_HORIZONTAL_MARGIN="8 pt" SHAPE_VERTICAL_MARGIN="5 pt" BORDER_COLOR="#2c2b29">
<font SIZE="16"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,2" COLOR="#ffffff" BACKGROUND_COLOR="#298bc8" STYLE="bubble" SHAPE_HORIZONTAL_MARGIN="8 pt" SHAPE_VERTICAL_MARGIN="5 pt" BORDER_COLOR_LIKE_EDGE="true" BORDER_COLOR="#f0f0f0">
<font SIZE="14"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,3" COLOR="#ffffff" BACKGROUND_COLOR="#3fb7db" STYLE="bubble" SHAPE_HORIZONTAL_MARGIN="8 pt" SHAPE_VERTICAL_MARGIN="5 pt" BORDER_COLOR_LIKE_EDGE="true" BORDER_COLOR="#f0f0f0">
<font SIZE="12"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,4" COLOR="#051552" BACKGROUND_COLOR="#5cd5e8" BORDER_COLOR_LIKE_EDGE="true" BORDER_COLOR="#f0f0f0">
<font SIZE="11"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,5" BORDER_COLOR_LIKE_EDGE="true" BORDER_COLOR="#f0f0f0">
<font SIZE="11"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,6" BORDER_COLOR_LIKE_EDGE="true" BORDER_COLOR="#f0f0f0">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,7" BORDER_COLOR="#f0f0f0">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,8" BORDER_COLOR="#f0f0f0">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,9" BORDER_COLOR="#f0f0f0">
<font SIZE="10"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,10" BORDER_COLOR="#f0f0f0">
<font SIZE="9"/>
</stylenode>
<stylenode LOCALIZED_TEXT="AutomaticLayout.level,11" BORDER_COLOR="#f0f0f0">
<font SIZE="9"/>
</stylenode>
</stylenode>
</stylenode>
</map_styles>
</hook>
<hook NAME="accessories/plugins/AutomaticLayout.properties" VALUE="ALL"/>
<font BOLD="true"/>
<node TEXT="Konzept" POSITION="bottom_or_right" ID="ID_351347589" CREATED="1749138140790" MODIFIED="1749138143372">
<node TEXT="Graph" ID="ID_1387256995" CREATED="1749138153896" MODIFIED="1749138171293">
<node TEXT="Node" ID="ID_259083893" CREATED="1749197003119" MODIFIED="1749197028434">
<node TEXT="Hat eine Signatur" POSITION="bottom_or_right" ID="ID_1518342419" CREATED="1749138332065" MODIFIED="1749198016791">
<node TEXT="Eingänge" ID="ID_225862940" CREATED="1749198017375" MODIFIED="1749198021478">
<node TEXT="Lazy&lt;T&gt;" ID="ID_1288547800" CREATED="1749198026579" MODIFIED="1749198040009"/>
</node>
<node TEXT="Ausgänge" ID="ID_661812861" CREATED="1749198022539" MODIFIED="1749198024513">
<node TEXT="Mutable&lt;T&gt;" ID="ID_1005406898" CREATED="1749198042405" MODIFIED="1749198082198"/>
</node>
</node>
<node TEXT="Caption" POSITION="bottom_or_right" ID="ID_1196798317" CREATED="1749197994222" MODIFIED="1749197997264"/>
<node TEXT="Grafische Repräsentation" POSITION="bottom_or_right" ID="ID_1686858497" CREATED="1749198609593" MODIFIED="1749198635728"/>
<node TEXT="Jeder Node ist eine Factory" POSITION="bottom_or_right" ID="ID_1175294085" CREATED="1749199661343" MODIFIED="1749199751197">
<node TEXT="Die eigentliche Komponente wird erst beim Init der Strategie erzeugt" ID="ID_691361700" CREATED="1749199752043" MODIFIED="1749199765349"/>
<node TEXT="So ist es möglich, dass z.B. ein Chart mit mehreren Eingängen verknüpft wird" ID="ID_1680171724" CREATED="1749199770731" MODIFIED="1749199814332"/>
</node>
<node TEXT="abstract templates" ID="ID_1144603697" CREATED="1749197934224" MODIFIED="1749197953583">
<node TEXT="DataServer" POSITION="bottom_or_right" ID="ID_291421820" CREATED="1749138179173" MODIFIED="1749202649741">
<node TEXT="Können Preisdaten aus beliebigen Quellen sammeln und stellen sie als Stream zur verfügung" ID="ID_827558062" CREATED="1749138204738" MODIFIED="1749138284430"/>
<node TEXT="Können sowohl Live- als auch historische Daten liefern" ID="ID_896603589" CREATED="1749193159987" MODIFIED="1749193216833">
<node TEXT="Für den Consumer darf das (eigentlich) nicht transparent sein" ID="ID_554512865" CREATED="1749193217206" MODIFIED="1749196862117"/>
</node>
<node TEXT="Signatur" ID="ID_1267368219" CREATED="1749195431793" MODIFIED="1749195436151">
<node TEXT="Keine Eingänge!" POSITION="bottom_or_right" ID="ID_65784960" CREATED="1749200853295" MODIFIED="1749200862455">
<node TEXT="Registriert sich als IDataProvider und publiziert so seine Fetch-Routine" POSITION="bottom_or_right" ID="ID_351312947" CREATED="1749200902001" MODIFIED="1749200934037"/>
<node TEXT="Fetch - signalisiert, dass neue Daten abgerufen werden können" POSITION="bottom_or_right" ID="ID_1521536523" CREATED="1749193885863" MODIFIED="1749194845452">
<node TEXT="Live" ID="ID_1178415156" CREATED="1749196109094" MODIFIED="1749196123441">
<node TEXT="Asynchron Daten von der externen Quelle abrufen" ID="ID_26813979" CREATED="1749196129271" MODIFIED="1749201012068"/>
<node TEXT="Es werden nach einem Fetch immer alle bis dahin enfangenen Daten verarbeitet" ID="ID_1279384359" CREATED="1749196364669" MODIFIED="1749196388802"/>
</node>
<node TEXT="Backtest" ID="ID_1462183661" CREATED="1749196123907" MODIFIED="1749196127688">
<node TEXT="Einlesen der historischen Daten aus lokaler Quelle" ID="ID_766072914" CREATED="1749196215155" MODIFIED="1749196299263"/>
<node TEXT="Wie viele Daten sollen hier gelesen werden?" ID="ID_955008466" CREATED="1749196316474" MODIFIED="1749196340955">
<node TEXT="Der Backtest könnte theoretisch alle Daten in einem Fetch liefern" ID="ID_783811796" CREATED="1749196423022" MODIFIED="1749196468608"/>
<node TEXT="Stattdessen sollen &quot;sinnvolle&quot; Häppchen geliefert werden" ID="ID_1420632589" CREATED="1749196470465" MODIFIED="1749196493915"/>
<node TEXT="Z.B. in dem die Timestamps der Daten analysiert werden." ID="ID_994363528" CREATED="1749196495078" MODIFIED="1749196523145">
<node TEXT="Ticks die sehr schnell nacheinender kommen, könnten zusammengefasst werden." ID="ID_262920625" CREATED="1749196523651" MODIFIED="1749196546182"/>
</node>
</node>
</node>
</node>
</node>
<node TEXT="Ausgang" POSITION="bottom_or_right" ID="ID_354621746" CREATED="1749193952447" MODIFIED="1749193957977">
<node TEXT="Array" ID="ID_1992679898" CREATED="1749196558592" MODIFIED="1749196561177">
<node TEXT="Enthält alle Datenpunkte, die seit dem letzten Durchlauf empfangen wurden" POSITION="bottom_or_right" ID="ID_1560263964" CREATED="1749194019659" MODIFIED="1749196612914"/>
<node TEXT="Datenpunkt" ID="ID_1319987980" CREATED="1749196616408" MODIFIED="1749196620117">
<node TEXT="Index seit Start der Strategy" POSITION="bottom_or_right" ID="ID_1416903711" CREATED="1749194191668" MODIFIED="1749194208147"/>
<node TEXT="Index seit dem letzten Session Break (wenn die Datenquelle das unterstützt)" POSITION="bottom_or_right" ID="ID_1894209537" CREATED="1749194208989" MODIFIED="1749194250436"/>
<node TEXT="Daten" ID="ID_508077608" CREATED="1749196736643" MODIFIED="1749196745736">
<node TEXT="Timestamp" POSITION="bottom_or_right" ID="ID_1600370587" CREATED="1749195711666" MODIFIED="1749195717838"/>
<node TEXT="Volume" POSITION="bottom_or_right" ID="ID_719731480" CREATED="1749196709681" MODIFIED="1749196711684"/>
<node TEXT="abstrakt" POSITION="bottom_or_right" ID="ID_1867165552" CREATED="1749196643858" MODIFIED="1749196675549">
<node TEXT="OHLC" POSITION="bottom_or_right" ID="ID_400927533" CREATED="1749195379156" MODIFIED="1749196820207"/>
<node TEXT="Tick" POSITION="bottom_or_right" ID="ID_1133389846" CREATED="1749195375327" MODIFIED="1749196821669">
<node TEXT="Ask-Bid" POSITION="bottom_or_right" ID="ID_127401459" CREATED="1749194269527" MODIFIED="1749196722182"/>
</node>
</node>
</node>
</node>
</node>
<node TEXT="IsLiveData" ID="ID_1043765011" CREATED="1749196881074" MODIFIED="1749196892932"/>
</node>
</node>
</node>
<node TEXT="Trader" POSITION="bottom_or_right" ID="ID_1544725450" CREATED="1749197183355" MODIFIED="1749197191427">
<node TEXT="Implementiert, wie mit Signalen umgegangen wird" ID="ID_102160159" CREATED="1749197357878" MODIFIED="1749197388131"/>
<node TEXT="Signatur" ID="ID_1668381928" CREATED="1749198186867" MODIFIED="1749198190179">
<node TEXT="Eingang" POSITION="bottom_or_right" ID="ID_967519634" CREATED="1749197096963" MODIFIED="1749197103083">
<node TEXT="Buy-Signal" ID="ID_1120078634" CREATED="1749197103978" MODIFIED="1749197398085"/>
<node TEXT="Sell-Signal" ID="ID_1074440473" CREATED="1749197140015" MODIFIED="1749197402441"/>
</node>
<node TEXT="Ausgang" POSITION="bottom_or_right" ID="ID_1984669993" CREATED="1749197229378" MODIFIED="1749197231751">
<node TEXT="Orders" ID="ID_84017955" CREATED="1749197256250" MODIFIED="1749197267082"/>
<node TEXT="Positions" ID="ID_106757702" CREATED="1749197270413" MODIFIED="1749197273907">
<node TEXT="Deals" ID="ID_1873504915" CREATED="1749197303336" MODIFIED="1749197305157"/>
<node TEXT="IsOpen" ID="ID_111000501" CREATED="1749197318276" MODIFIED="1749197322652"/>
</node>
</node>
</node>
</node>
<node TEXT="Indicator" ID="ID_1041206949" CREATED="1749198254012" MODIFIED="1749198258932">
<node TEXT="Frei programmierbare Module" ID="ID_1046851781" CREATED="1749198260161" MODIFIED="1749198292019"/>
</node>
<node TEXT="Diagramme" ID="ID_1728273110" CREATED="1749198882498" MODIFIED="1749199486967">
<node TEXT="abstract" POSITION="bottom_or_right" ID="ID_1648724212" CREATED="1749198794903" MODIFIED="1749198800455">
<node TEXT="Tick-Chart" ID="ID_1017072631" CREATED="1749198800457" MODIFIED="1749198831938"/>
<node TEXT="OHLC-Chart" ID="ID_629656972" CREATED="1749198805040" MODIFIED="1749198837689"/>
<node TEXT="Histogram" ID="ID_34977684" CREATED="1749198841247" MODIFIED="1749199015989"/>
<node TEXT="Diagram" ID="ID_840651195" CREATED="1749199016656" MODIFIED="1749199108308"/>
</node>
<node TEXT="Draw-Plane mit X/Y Achse" ID="ID_587693864" CREATED="1749198904253" MODIFIED="1749199081316"/>
<node TEXT="Charts" ID="ID_382545852" CREATED="1749199466839" MODIFIED="1749199491297">
<node TEXT="Eingang" POSITION="bottom_or_right" ID="ID_1587680882" CREATED="1749199129247" MODIFIED="1749199394732">
<node TEXT="Klar definierte Dateneingänge" ID="ID_548619408" CREATED="1749199135233" MODIFIED="1749199152222">
<node TEXT="z.B. TickData für Tick-Chart" ID="ID_383484888" CREATED="1749199152224" MODIFIED="1749199163416"/>
</node>
<node TEXT="Eingänge dynamisch erzeugen." ID="ID_710689831" CREATED="1749199168396" MODIFIED="1749199223960">
<node TEXT="z.B. ein EMA auf einem OHLC-Chart" ID="ID_288893788" CREATED="1749199223961" MODIFIED="1749199254594">
<node TEXT="Ausgang des EMA liefert nur einen Wert" ID="ID_1879052051" CREATED="1749199255287" MODIFIED="1749199283462"/>
<node TEXT="Muss über einen im Chart-Node definierbaren Eingang verknüpft werden" ID="ID_1048634781" CREATED="1749199285281" MODIFIED="1749199324815">
<node TEXT="Definiert den Style des EMA" ID="ID_1401266298" CREATED="1749199343035" MODIFIED="1749199359978"/>
</node>
</node>
<node TEXT="AddChartInput()" ID="ID_851859706" CREATED="1749199909158" MODIFIED="1749200050028">
<node TEXT="Liefert einen neuen Eingang mit eigenen Style-Parametern" ID="ID_1326484717" CREATED="1749200057661" MODIFIED="1749200088602"/>
</node>
</node>
</node>
<node TEXT="Ausgang" POSITION="bottom_or_right" ID="ID_1838167422" CREATED="1749199387596" MODIFIED="1749199390976">
<node TEXT="Sichtbarer Datenbereich" ID="ID_149531054" CREATED="1749199425386" MODIFIED="1749199530548">
<node TEXT="Damit können andere Diagramme ihren Sichtbereich an den Chart anpassen." ID="ID_21321688" CREATED="1749199531020" MODIFIED="1749199559573"/>
</node>
<node TEXT="Last Value" ID="ID_179046743" CREATED="1749199567850" MODIFIED="1749199578133"/>
</node>
</node>
<node TEXT="Histogram" ID="ID_1926471247" CREATED="1749199595197" MODIFIED="1749199599389"/>
</node>
<node TEXT="Chart" ID="ID_1485297910" CREATED="1749198734818" MODIFIED="1749198738034">
<node TEXT="Tick" ID="ID_1989326003" CREATED="1749198752908" MODIFIED="1749198768413">
<node TEXT="" ID="ID_276822419" CREATED="1749198770321" MODIFIED="1749198770321"/>
</node>
<node TEXT="OHLC" ID="ID_1040780755" CREATED="1749198757224" MODIFIED="1749198762424"/>
<node TEXT="Eingang" ID="ID_1311557583" CREATED="1749198785094" MODIFIED="1749198793621"/>
</node>
</node>
</node>
</node>
<node TEXT="Broker" POSITION="bottom_or_right" ID="ID_1666096793" CREATED="1749138364769" MODIFIED="1749138404806">
<node TEXT="Order- und Positionsmanagement" ID="ID_982862918" CREATED="1749138407782" MODIFIED="1749138443566"/>
<node TEXT="Reduziert auf das absolute Minimum" ID="ID_620096980" CREATED="1749138462663" MODIFIED="1749138472587"/>
<node TEXT="Plazieren von Limit- und Market-Orders" ID="ID_325256813" CREATED="1749138473278" MODIFIED="1749138498552"/>
<node TEXT="Verschiedene Ausführungsmodelle" ID="ID_1368393579" CREATED="1749138525992" MODIFIED="1749138537994"/>
</node>
</node>
</node>
</map>
Binary file not shown.
+68
View File
@@ -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
+172
View File
@@ -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<TAskBidItem>;
FSymbolsValid: TFMXValidationState;
FStreamUpdate: TFMXValidationState;
FSymbols: TFuture<TArray<String>>;
FStream: IDataStream<TAskBidItem>;
FRandomStream: array[0..19] of IDataStream<TAskBidItem>;
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<TAskBidItem>;
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<TDataPoint<TAskBidItem>>;
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.
+74
View File
@@ -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<T>`) - **ABGESCHLOSSEN**
* **Motivation**: Das ursprüngliche, zustandsbehaftete Design war für die Verarbeitung asynchron eintreffender Daten (z.B. von `TFuture`-Objekten) zu komplex und fehleranfällig.
* **Ziel**: Die Schaffung eines robusten, ereignisgesteuerten und non-blocking Modells, das den Datenproduzenten sauber vom Konsumenten entkoppelt.
* **Ergebnis**:
* Die `IDataStream<T>`-Schnittstelle wurde überarbeitet. Die `HasData`-Eigenschaft liefert nun ein `TSignal`, das Konsumenten aktiv über potenziell neue Daten informiert.
* Die Referenzimplementierung `TAuraFileStream<T>` wurde erfolgreich angepasst und nutzt eine saubere Ereignis-Kopplung (`Subscribe`) für eine robuste und wartungsarme Logik.
* Die korrekte Funktionalität wurde durch eine angepasste DUnitX-Test-Suite verifiziert.
---
## Stufe 2: Abstraktion für Handelssysteme (`IDataSeriesProvider`) - **ENTWORFEN**
* **Motivation**: Ein Handelssystem benötigt einen stets validen und kontinuierlichen Daten-Lookback. Ein roher `IDataStream<T>` kann dies nicht garantieren, da Lücken in den Daten auftreten können (z.B. beim Übergang von Historie zu Live).
* **Ziel**: Die Konzeption einer übergeordneten Abstraktionsschicht, die diese komplexe Anforderung kapselt, die Datenintegrität sicherstellt und dem Handelssystem eine einfache, sichere Schnittstelle bietet.
* **Ergebnis**:
Entworfen wurde der `IDataSeriesProvider`, der als "Black Box" für das Handelssystem fungiert und die Komplexität der Datenbeschaffung vollständig verbirgt. Er wurde mit zwei unterschiedlichen, vom Anwender wählbaren Betriebsmodi konzipiert:
### Modus 1: "Live-Handel"
* **Motivation**: Um einen echten **"Sofort-Start"** im Live-Handel zu ermöglichen, muss die Lücke zwischen den statischen, lokalen Historiendaten und dem aktuellen Zeitpunkt geschlossen werden.
* **Ziel**: Ein lückenloser, tagesaktueller Start des Handelssystems ohne manuelles Eingreifen oder lange Wartezeiten für den Nutzer.
* **Ergebnis**: Das Design einer **Drei-Phasen-Synchronisation**: (1) Lokale History laden, (2) "Catch-up"-Daten vom Broker holen, (3) auf den Live-Stream umschalten.
### Modus 2: "Simulation & Backtest"
* **Motivation**: Für Entwicklung, Test und Analyse muss die Software **völlig autonom** und ohne Abhängigkeit von einer externen, potenziell nicht verfügbaren Broker-API lauffähig sein.
* **Ziel**: Einen "Sofort-Start" für Backtests zu jedem beliebigen Zeitpunkt in der Vergangenheit zu ermöglichen, der rein auf lokalen Dateien basiert.
* **Ergebnis**: Ein Design, bei dem der relevante Datenkontext in den Speicher geladen wird, um von dort aus einen schnellen Start und ein "Playback" der Daten zu ermöglichen.
# TODO-Einträge: Dateninfrastruktur
## Stufe 2: `IDataSeriesProvider`
- [ ] **Interface und Basis-Implementierung erstellen**
* Definiere das `IDataSeriesProvider<T>`-Interface.
* Definiere die `TProviderStatus`-Enumeration (`psInitializing`, `psReadyContinuous`, `psReadyWithGap`, `psFaulted`).
* Implementiere eine abstrakte Basisklasse `TDataSeriesProvider<T>`, die die grundlegenden Felder (z.B. für die interne `TDataSeries`) und Methoden bereitstellt.
- [ ] **Simulations-Modus implementieren**
* Erstelle eine konkrete Implementierung (z.B. `TSimulationSeriesProvider<T>`).
* 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<T>`), 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.
+5 -3
View File
@@ -49,7 +49,9 @@ var
implementation
uses
{$IFDEF MSWINDOWS} Winapi.Windows, {$endif}
{$IFDEF MSWINDOWS}
Winapi.Windows,
{$endif}
System.Math;
{ TDataFileCache<T> }
@@ -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.
+51 -2
View File
@@ -13,6 +13,8 @@ type
protected
function GetValue: T; virtual; abstract;
function GetDone: TState; virtual; abstract;
public
procedure BeforeDestruction; override;
end;
TMycNullFuture<T> = class(TMycFuture<T>)
@@ -34,8 +36,27 @@ type
destructor Destroy; override;
end;
TMycFutureManaged<T> = class(TMycFuture<T>)
private
FFuture: TFuture<T>.IFuture;
protected
function GetValue: T; override;
function GetDone: TState; override;
public
constructor Create(const AFuture: TFuture<T>.IFuture);
destructor Destroy; override;
end;
implementation
{ TMycFuture<T> }
procedure TMycFuture<T>.BeforeDestruction;
begin
inherited;
Assert(GetDone.IsSet, 'Trying to destroy an unfinished future');
end;
{ TMycNullFuture<T> }
function TMycNullFuture<T>.GetDone: TState;
@@ -79,8 +100,6 @@ end;
destructor TMycGateFuncFuture<T>.Destroy;
begin
Assert(FDone.State.IsSet, 'Future not done');
FInit.Unsubscribe;
inherited Destroy;
end;
@@ -96,4 +115,34 @@ begin
Result := FResult;
end;
{ TMycFutureManaged<T> }
constructor TMycFutureManaged<T>.Create(const AFuture: TFuture<T>.IFuture);
begin
inherited Create;
FFuture := AFuture;
end;
destructor TMycFutureManaged<T>.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<T>.GetDone: TState;
begin
Result := FFuture.Done;
end;
function TMycFutureManaged<T>.GetValue: T;
begin
Result := FFuture.Value;
end;
end.
+8
View File
@@ -49,6 +49,8 @@ type
class property Null: IFuture read FNull;
procedure Manage;
function Chain<S>(const Proc: TFunc<T, S>): TFuture<S>; overload;
function Chain<S>(const Proc: TFuncConst<T, S>): TFuture<S>; overload;
function WaitFor: T;
@@ -113,6 +115,12 @@ begin
Result := FFuture.Value;
end;
procedure TFuture<T>.Manage;
begin
if GetTypeKind(T) = tkClass then
FFuture := TMycFutureManaged<T>.Create(FFuture);
end;
function TFuture<T>.WaitFor: T;
begin
TaskManager.WaitFor(FFuture.Done);
+33 -20
View File
@@ -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.
+33 -12
View File
@@ -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;
+264 -269
View File
@@ -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<TDataFile>; 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<T>;
procedure ClearCache;
function EnumerateSymbols: TArray<String>;
function EnumerateSymbols: TFuture<TArray<String>>;
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<T: record> = class(TInterfacedObject, IDataServer<T>)
private
FCachedFiles: TDataFileCache<TArray<TDataPoint<T>>>;
FPath: String;
function DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
FSymbols: TFuture<TDictionary<String, TAuraDataFile>>;
function GetPath: String;
// Used by cache to actually load an uncached file.
function DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
strict private
class var
FLoadGate: TLatch;
protected
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; static;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; static;
// Scans a directory to find the oldest file for a specific symbol.
function FindFirstFile(const Symbol: string): TFuture<TAuraDataFile>;
function ParseFileName(const FileName: string): TAuraDataFile; virtual; abstract;
class function ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; virtual; abstract;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<T>>; virtual; abstract;
public
constructor Create(const APath: String);
destructor Destroy; override;
procedure AfterConstruction; override;
function CreateStream(const Symbol: String): IDataStream<T>; virtual; abstract;
procedure ClearCache;
function EnumerateSymbols: TArray<String>;
function LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TDataPoint<T>>>;
procedure UpdateSymbols;
function EnumerateSymbols: TFuture<TArray<String>>;
function LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TDataPoint<T>>>;
property Path: String read GetPath;
end;
@@ -135,7 +139,7 @@ type
private
type
TDataStream = record
FileInfo: TDataFile;
FileInfo: TAuraDataFile;
Data: TFuture<TArray<TDataPoint<T>>>;
end;
private
@@ -162,9 +166,14 @@ type
// Aura tick data file Ask-Bid
TAuraTABFileServer = class(TAuraDataServer<TAskBidItem>)
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<TDataPoint<TAskBidItem>>; override;
class function ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<TAskBidItem>>; override;
public
function CreateStream(const Symbol: String): IDataStream<TAskBidItem>; override;
function LoadDataSeries(const InitialFile: TDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
function LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
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<string>;
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<string>;
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<TDataFile>;
var
oldestFilesPerSymbol: TDictionary<string, TDataFile>;
fileNames: TArray<string>;
currentFile: string;
dataFile: TDataFile;
trackedInfo: TDataFile;
begin
oldestFilesPerSymbol := TDictionary<string, TDataFile>.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<T> }
constructor TAuraDataServer<T>.Create(const APath: String);
@@ -368,27 +259,17 @@ begin
inherited Destroy;
end;
procedure TAuraDataServer<T>.AfterConstruction;
begin
inherited;
UpdateSymbols;
end;
procedure TAuraDataServer<T>.ClearCache;
begin
FCachedFiles.Clear;
end;
function TAuraDataServer<T>.EnumerateSymbols: TArray<String>;
var
dataFiles: TArray<TDataFile>;
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<T>.GetPath: String;
begin
Result := FPath;
end;
function TAuraDataServer<T>.DoLoad(const FileName: string): TFuture<TArray<TDataPoint<T>>>;
begin
Result := TFuture<TArray<TDataPoint<T>>>.Null;
@@ -401,9 +282,7 @@ begin
begin
Result :=
TFuture<TBytes>
.Construct(
TLatch.Enqueue(FLoadGate),
function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
.Construct(FLoadGate.Enqueue, function: TBytes begin Result := TFile.ReadAllBytes(capFileName); end)
.Chain<TArray<TDataPoint<T>>>(
function(bytes: TBytes): TArray<TDataPoint<T>>
begin
@@ -420,7 +299,7 @@ begin
begin
Result :=
TFuture<TArray<TDataPoint<T>>>.Construct(
TLatch.Enqueue(FLoadGate),
FLoadGate.Enqueue,
function: TArray<TDataPoint<T>>
begin
if TFile.Exists(capFileName) then
@@ -442,81 +321,82 @@ begin
end;
end;
function TAuraDataServer<T>.LoadDataFile(const DataFile: TDataFile): TFuture<TArray<TDataPoint<T>>>;
procedure TAuraDataServer<T>.UpdateSymbols;
begin
FSymbols :=
TFuture<TDictionary<String, TAuraDataFile>>.Construct(
FSymbols.Done,
function: TDictionary<String, TAuraDataFile>
var
fileNames: TArray<string>;
currentFile: string;
dataFile: TAuraDataFile;
trackedInfo: TAuraDataFile;
begin
Result := TDictionary<string, TAuraDataFile>.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<T>.EnumerateSymbols: TFuture<TArray<String>>;
var
dataFiles: TArray<TAuraDataFile>;
i: Integer;
begin
Result :=
FSymbols.Chain<TArray<String>>(
function(Symbols: TDictionary<String, TAuraDataFile>): TArray<String> begin Result := Symbols.Keys.ToArray; end
);
end;
function TAuraDataServer<T>.FindFirstFile(const Symbol: string): TFuture<TAuraDataFile>;
begin
var symName := Symbol;
Result :=
FSymbols.Chain<TAuraDataFile>(
function(Symbols: TDictionary<String, TAuraDataFile>): TAuraDataFile
begin
if not Symbols.TryGetValue(symName, Result) then
Result := Default(TAuraDataFile);
end
);
end;
function TAuraDataServer<T>.GetPath: String;
begin
Result := FPath;
end;
function TAuraDataServer<T>.LoadDataFile(const DataFile: TAuraDataFile): TFuture<TArray<TDataPoint<T>>>;
begin
Result := FCachedFiles.GetOrAdd(DataFile.GetFullFileName);
end;
class function TAuraDataServer<T>.ReadCompressedData(const InputStream: TStream): TArray<TDataPoint<T>>;
var
decompressionStream: TStream;
localHeader: TZipHeader;
entryIndex, i: Integer;
zipFileInstance: TZipFile;
begin
SetLength(Result, 0);
decompressionStream := nil;
zipFileInstance := nil;
try
InputStream.Position := 0;
zipFileInstance := TZipFile.Create;
zipFileInstance.Open(InputStream, TZipMode.zmRead);
if zipFileInstance.FileCount = 0 then
exit;
entryIndex := -1;
for i := 0 to zipFileInstance.FileCount - 1 do
if zipFileInstance.FileNames[i].EndsWith('.tab', True) then
begin
entryIndex := i;
break;
end;
if entryIndex = -1 then
entryIndex := 0;
zipFileInstance.Read(entryIndex, decompressionStream, localHeader);
if not Assigned(decompressionStream) then
exit;
Result := ReadUncompressedData(decompressionStream);
finally
decompressionStream.Free;
zipFileInstance.Free;
end;
end;
class function TAuraDataServer<T>.ReadUncompressedData(const InputStream: TStream): TArray<TDataPoint<T>>;
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<T> }
constructor TAuraFileStream<T>.Create(ADataServer: TAuraDataServer<T>; const ASymbol: String);
@@ -535,18 +415,16 @@ end;
procedure TAuraFileStream<T>.AfterConstruction;
var
firstFile: TFuture<TDataFile>;
serverPath: string;
firstFile: TFuture<TAuraDataFile>;
begin
inherited;
// We need the path from the server instance to find the file.
serverPath := (FDataServer as TAuraDataServer<T>).Path;
firstFile := TFuture<TDataFile>.Construct(function: TDataFile begin Result := TDataFile.FindFirst(serverPath, FSymbol); end);
firstFile := FDataServer.FindFirstFile(FSymbol);
FCurrent :=
firstFile.Chain<TDataStream>(
function(const FileInfo: TDataFile): TDataStream
function(const FileInfo: TAuraDataFile): TDataStream
begin
if FileInfo.IsValid then
begin
@@ -638,7 +516,7 @@ begin
FCurrent.Chain<TDataStream>(
function(const Prev: TDataStream): TDataStream
var
nextFileInfo: TDataFile;
nextFileInfo: TAuraDataFile;
begin
if Prev.FileInfo.IsValid then
begin
@@ -661,16 +539,16 @@ begin
Result := TAuraFileStream<TAskBidItem>.Create(Self, Symbol);
end;
function TAuraTABFileServer.LoadDataSeries(const InitialFile: TDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
function TAuraTABFileServer.LoadDataSeries(const InitialFile: TAuraDataFile): TFuture<TArray<TDataPoint<TAskBidItem>>>;
var
loadedState: TState;
loadedFiles: TArray<TFuture<TArray<TDataPoint<TAskBidItem>>>>;
liveData: TFuture<TArray<TDataPoint<TAskBidItem>>>;
tabFiles: TList<TDataFile>;
liveFile: TDataFile;
currentFileInfo: TDataFile;
tabFiles: TList<TAuraDataFile>;
liveFile: TAuraDataFile;
currentFileInfo: TAuraDataFile;
begin
tabFiles := TList<TDataFile>.Create;
tabFiles := TList<TAuraDataFile>.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<TFuture<TArray<TDataPoint<TAskBidItem>>>>.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<string>;
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<TDataPoint<TAskBidItem>>;
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<TDataPoint<TAskBidItem>>;
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.