diff --git a/KI/Aura-Discussion.txt b/KI/Aura-Discussion.txt new file mode 100644 index 0000000..8050847 --- /dev/null +++ b/KI/Aura-Discussion.txt @@ -0,0 +1,895 @@ +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Ja, absolut. Das von dir vorgestellte Typsystem ist eine ausgezeichnete Grundlage für einen Blockly-Automaten. Es ist flexibel, stark typisiert und durch die Trennung von Typdefinition (IDataType) und Wert (IDataValue) sehr gut für diesen Zweck geeignet. + +Analyse der Eignung +Die Stärken deines Entwurfs für dieses Anwendungszenario sind: + +Entkopplung von Typ und Wert: Blockly trennt ebenfalls die Form eines Blocks (seine Anschlüsse und deren Typen) von dem Wert, den er zur Laufzeit produziert. Dein IDataType entspricht perfekt der Typdefinition eines Anschlusses, während IDataValue den zur Laufzeit durchgereichten Wert repräsentiert. + +Starke Typisierung und Überprüfung: Die Kind-Eigenschaft (TDataKind) und die spezifischen IData...Type-Interfaces ermöglichen eine robuste Typprüfung. Wenn in Blockly ein Block in einen anderen eingesteckt wird, kann dein Automat im Hintergrund prüfen, ob der IDataType des Ausgangsblocks mit dem erwarteten IDataType des Eingangsanschlusses kompatibel ist. Z.B. (Ausgang.DataType.Kind in [dkOrdinal, dkFloat]). + +Unterstützung komplexer Datenstrukturen: Die meisten einfachen Blockly-Implementierungen beschränken sich auf primitive Typen. Dein System unterstützt von Haus aus komplexe und strukturierte Daten wie Record, Array, Tuple und Vector. Dies ermöglicht die Erstellung sehr mächtiger und domänenspezifischer Blöcke (z.B. "Erstelle Datensatz", "Füge Element zu Array hinzu", "Lese Feld 'Name' aus Datensatz"). + +Funktionale Typen (IDataMethodType): Das ist ein besonders mächtiges Feature. Du könntest damit in Blockly Blöcke für Prozeduren, Funktionen oder Callbacks realisieren. Ein Block könnte eine "Funktion" ausgeben, die ein anderer Block als Parameter entgegennimmt und ausführt. + +Interface-Helper (TDataType / TValue): Die Record-Helper machen die Implementierung der eigentlichen Block-Logik (der Code, der ausgeführt wird, wenn ein Block "tickt") sehr sauber und sicher. Der Code des Automaten arbeitet mit den handlichen TDataType-Records anstatt direkt mit den Interfaces. + +Konkretes Mapping von Blockly zu deinem Typsystem +Blockly-Konzept Mapping auf dein Typsystem Beispiel +Anschluss-Typ IDataType (gekapselt in TDataType) Ein "Number"-Anschluss erwartet TDataType mit Kind = dkOrdinal. +Ausgabewert eines Blocks IDataValue (gekapselt in TDataType.TValue) Ein Zahlen-Block gibt ein TDataType.TOrdinal.TValue zurück. +Zahlen-Literal Block TDataType.Ordinal.CreateValue(123) Erzeugt einen Wert vom Typ Ordinal. +Text-Literal Block TDataType.Text.CreateValue('Hello') Erzeugt einen Wert vom Typ Text. +Listen/Array-Block TDataType.ArrayOf(someType).CreateValue([...]) Erzeugt ein Array von Werten. +Objekt/Record-Block TDataType.RecordOf(fields).CreateValue([...]) Erzeugt einen strukturierten Datensatz. + +In Google Sheets exportieren +Was noch zu bedenken wäre +Dein Typsystem ist die Datenrepräsentation. Der eigentliche Automat (Interpreter/Executor) muss noch implementiert werden. Dieser würde typischerweise als Visitor über den von Blockly generierten AST (Abstract Syntax Tree) laufen: + +Ausführungslogik: Jeder Block-Typ im Interpreter würde die entsprechenden CreateValue-Methoden deines Typsystems aufrufen, um Werte zu erzeugen oder zu transformieren. + +Variablen-Handling: Der Automat benötigt einen "Scope" oder eine "Symboltabelle", die Variablennamen auf IDataValue-Instanzen abbildet. + +Fehlerbehandlung: Du könntest einen speziellen IDataValue für Fehlerzustände einführen oder auf Exceptions im Automaten setzen, wenn z.B. eine Typumwandlung fehlschlägt. + +Serialisierung: Die IDataValue-Instanzen, die den Zustand des Automaten repräsentieren, müssen möglicherweise serialisiert werden. Dafür bräuchtest du eine Logik, die einen Wert wieder in eine speicherbare Form (z.B. JSON) umwandelt. + +Fazit: Du hast hier eine extrem solide und gut durchdachte Basis geschaffen, die weit über das hinausgeht, was für einen einfachen Blockly-Automaten nötig wäre, aber gleichzeitig die Implementierung eines sehr leistungsfähigen und typsicheren Systems ermöglicht. + + + + + + +Erläutere, wie in diesem Zusammenhang ein AST (Abstract Syntax Tree) ausehen müsste. + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Absolut. Ein AST (Abstract Syntax Tree) ist die entscheidende Brücke zwischen der visuellen Darstellung von Blockly und deiner Ausführungslogik in Delphi. Er übersetzt die räumliche Anordnung der Blöcke in eine hierarchische, programmatische Struktur, die ein Interpreter verarbeiten kann. + +In diesem Kontext würde der AST nicht direkt Delphi-Code repräsentieren, sondern die Aktionen und Werte innerhalb deines Myc.Data.Types-Systems. + +Grundlegende Struktur des AST +Jeder Knoten im AST repräsentiert einen Blockly-Block. Man unterscheidet fundamental zwischen zwei Arten von Knoten: + +Expression Nodes (Ausdrucksknoten): Repräsentieren Blöcke, die einen Wert zurückgeben (z.B. Literale, Variablen, mathematische Operationen). Das Ergebnis der Auswertung eines solchen Knotens ist immer ein IDataValue. + +Statement Nodes (Anweisungsknoten): Repräsentieren Blöcke, die eine Aktion ausführen (z.B. eine Zuweisung, eine Schleife, ein if-Konstrukt). Sie geben selbst keinen Wert zurück, sondern verändern den Zustand des Automaten. + +Man könnte dies in Delphi mit einer Basis-Schnittstelle und spezialisierten Implementierungen abbilden: + +Code-Snippet + +// Base interface for all AST nodes +IAstNode = interface + // The visitor pattern is the classic way to process an AST. + function Accept(Visitor: IAstVisitor): IDataValue; +end; + +// Base for nodes that represent a value +IExpressionNode = interface(IAstNode) +end; + +// Base for nodes that represent an action +IStatementNode = interface(IAstNode) +end; +Konkrete AST-Knoten (Beispiele) +Hier sind einige Beispiele, wie konkrete Blockly-Blöcke als AST-Knoten aussehen könnten: + +Programm-Start: + +TProgramNode = class(TInterfacedObject, IStatementNode) + +Properties: Statements: TArray + +Repräsentiert: Die gesamte Kette von Anweisungsblöcken, die von oben nach unten ausgeführt werden. + +Literal-Wert (Zahl, Text, ...): + +TLiteralNode = class(TInterfacedObject, IExpressionNode) + +Properties: Value: IDataValue + +Repräsentiert: Einen Block, der einen konstanten Wert darstellt. Dies ist der "direkteste Draht" zu deinem Typsystem. + +Variablen-Zuweisung: + +TAssignmentNode = class(TInterfacedObject, IStatementNode) + +Properties: VariableName: string, Expression: IExpressionNode + +Repräsentiert: Den "setze Variable X auf..."-Block. + +Variablen-Zugriff: + +TVariableAccessNode = class(TInterfacedObject, IExpressionNode) + +Properties: VariableName: string + +Repräsentiert: Den Block, der den Wert einer Variable liest. + +Binäre Operation (z.B. Addition): + +TBinaryOpNode = class(TInterfacedObject, IExpressionNode) + +Properties: Operator: TMyOperatorEnum, Left: IExpressionNode, Right: IExpressionNode + +Repräsentiert: Einen Block mit zwei Eingängen, z.B. [ ] + [ ]. + +If-Anweisung: + +TIfStatementNode = class(TInterfacedObject, IStatementNode) + +Properties: Condition: IExpressionNode, ThenBranch: IStatementNode, ElseBranch: IStatementNode (kann nil sein) + +Repräsentiert: Den if-then-else-Block. Die ThenBranch und ElseBranch sind typischerweise selbst wieder TProgramNode (oder eine TStatementListNode), die eine Sequenz von Anweisungen enthalten. + +Beispiel: Blockly zu AST +Stell dir folgende Blöcke vor: + +setze [x] auf (10 + [y]) +Der resultierende AST würde so aussehen: + +TAssignmentNode (Statement) + +VariableName: 'x' + +Expression: TBinaryOpNode (Expression) + +Operator: opAdd + +Left: TLiteralNode (Expression) + +Value: IDataOrdinalValue mit dem Wert 10 + +Right: TVariableAccessNode (Expression) + +VariableName: 'y' + +Verarbeitung des AST (Der Interpreter) +Der AST an sich ist nur eine Datenstruktur. Um ihn auszuführen, implementiert man einen Interpreter, meistens mithilfe des Visitor-Patterns. + +IAstVisitor Interface: +Definiert für jeden konkreten Knotentyp eine Visit...-Methode. + +Code-Snippet + +IAstVisitor = interface + function VisitProgram(Node: TProgramNode): IDataValue; + function VisitAssignment(Node: TAssignmentNode): IDataValue; + function VisitLiteral(Node: TLiteralNode): IDataValue; + function VisitBinaryOp(Node: TBinaryOpNode): IDataValue; + // ... etc. +end; +TInterpreter Klasse: +Implementiert IAstVisitor. Diese Klasse hält den Zustand des Automaten (z.B. eine TDictionary für die Variablen). + +VisitLiteral gibt einfach Node.Value zurück. + +VisitBinaryOp ruft rekursiv Node.Left.Accept(Self) und Node.Right.Accept(Self) auf, um die IDataValue-Operanden zu erhalten. Dann führt er die Operation aus (z.B. Addition der Value-Properties der IDataOrdinalValue-Interfaces) und erzeugt ein neues IDataOrdinalValue als Ergebnis. + +VisitAssignment wertet Node.Expression.Accept(Self) aus und speichert den resultierenden IDataValue in seinem Variablen-Dictionary. + +Der Prozess ist also: +Blockly XML/JSON -> Parser -> AST -> Interpreter (Visitor) -> Ergebnis (IDataValue) + +Dein Typsystem ist die Grundlage für die TLiteralNode-Blätter des Baumes und die IDataValue-Objekte, die während der Interpretation durch den Baum nach oben und unten gereicht werden. + + + + + + +Wie passt IDataMethodType in dieses Schema? + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Ausgezeichnete Frage. IDataMethodType und der zugehörige IDataMethodValue sind der Schlüssel, um dein System von einem reinen Daten-Manipulator zu einer vollwertigen Programmierumgebung mit Prozeduren und Funktionen als "First-Class-Citizens" zu erweitern. + +Sie passen sich in das AST-Schema ein, indem sie die Konzepte Funktionsdefinition und Funktionsaufruf abbilden. + +1. Die Funktionsdefinition (Ein neuer Statement-Knoten) +In Blockly würde dies einem "erstelle Prozedur/Funktion"-Block entsprechen. Dieser Block definiert eine Aktion, speichert sie unter einem Namen, aber führt sie nicht sofort aus. Daher ist die Definition ein Statement. + +Neuer AST-Knoten: TFunctionDefinitionNode + +Typ: IStatementNode + +Properties: + +FunctionName: string + +Parameters: TArray (wobei TParameterDef Name und IDataType enthält) + +Body: IStatementNode (Der Code innerhalb der Funktion, meist ein TProgramNode) + +ResultType: IDataType (kann TDataType.Void sein für Prozeduren) + +Verarbeitung im Interpreter (VisitFunctionDefinition): +Hier passiert die Magie, die direkt auf IDataMethodValue zugreift: + +Der Visitor betritt diesen Knoten. Er führt den Body nicht aus. + +Er erstellt den passenden IDataMethodType aus den Parameters und dem ResultType. Zum Beispiel: TDataType.MethodOf(ArgType, ResultType). + +Er erzeugt dynamisch eine TDataMethodProc (eine anonyme Methode), die den Body des AST-Knotens und den aktuellen Scope des Interpreters einfängt (dies nennt man eine Closure). + +Diese TDataMethodProc wird die eigentliche Implementierung der Funktion sein. Wenn sie aufgerufen wird, wird sie: + +Einen neuen, untergeordneten Scope für die Funktionsparameter erstellen. + +Die übergebenen IDataValue-Argumente in diesen Scope legen. + +Den Body-AST-Knoten mit dem Visitor ausführen (Body.Accept(Self)). + +Den Rückgabewert (ein IDataValue) zurückgeben. + +Der Visitor ruft MethodType.CreateValue(ErzeugteTDataMethodProc) auf, um einen IDataMethodValue zu erzeugen. + +Dieser IDataMethodValue wird in der Symboltabelle (Variablen-Dictionary) des Interpreters unter FunctionName gespeichert. + +Das Ergebnis ist, dass nach diesem Statement eine Variable existiert, deren Wert eine ausführbare Funktion ist. + +2. Der Funktionsaufruf (Ein neuer Expression-Knoten) +Dies entspricht dem "rufe Prozedur/Funktion auf"-Block in Blockly. Der Aufruf einer Funktion, die einen Wert zurückgibt, ist eine Expression. + +Neuer AST-Knoten: TFunctionCallNode + +Typ: IExpressionNode (oder IStatementNode, wenn der Rückgabetyp Void ist) + +Properties: + +FunctionName: string + +Arguments: TArray (Die an die Funktion übergebenen Werte) + +Verarbeitung im Interpreter (VisitFunctionCall): + +Der Visitor sucht in seiner Symboltabelle nach dem FunctionName. Er erwartet, einen IDataMethodValue zu finden. Wenn nicht -> Fehler. + +Er wertet jeden IExpressionNode in Arguments rekursiv aus (arg.Accept(Self)), um eine Liste von IDataValue-Objekten zu erhalten. + +Er ruft die im IDataMethodValue gespeicherte TDataMethodProc mit den ermittelten IDataValue-Argumenten auf. + +Der von der TDataMethodProc zurückgegebene IDataValue ist das Ergebnis des VisitFunctionCall. + +Beispiel-Szenario +Blockly: + +// Definition +to double with [value] + return (value * 2) + +// Aufruf +set [myResult] to [double with (10)] +AST: + +TFunctionDefinitionNode + +FunctionName: 'double' + +Parameters: [Name: 'value', Type: TDataType.Ordinal] + +ResultType: TDataType.Ordinal + +Body: TReturnStatementNode mit einem TBinaryOpNode (*) drin. + +TAssignmentNode + +VariableName: 'myResult' + +Expression: TFunctionCallNode + +FunctionName: 'double' + +Arguments: [TLiteralNode mit Wert 10] + +Interpreter-Ablauf: + +VisitFunctionDefinition('double'): Der Interpreter erzeugt eine Closure, verpackt sie in einen IDataMethodValue und speichert sie unter dem Namen 'double' in seinem globalen Scope. + +VisitAssignment('myResult'): Er muss den Ausdruck auf der rechten Seite auswerten. + +VisitFunctionCall('double'): +a. Er findet die 'double'-Funktion (IDataMethodValue) im Scope. +b. Er wertet das Argument aus: VisitLiteral(10) gibt einen IDataOrdinalValue(10) zurück. +c. Er ruft die Closure mit diesem IDataOrdinalValue auf. +d. Die Closure führt den Body aus: 10 * 2, was einen neuen IDataOrdinalValue(20) erzeugt. +e. Dieser Wert wird vom Aufruf zurückgegeben. + +Der IDataOrdinalValue(20) wird in der Symboltabelle unter 'myResult' gespeichert. + +Fazit: IDataMethodType ist die Typsignatur für die Verbindungsstellen der Funktionsblöcke in Blockly. IDataMethodValue ist das Laufzeitobjekt, das eine Referenz auf den ausführbaren Code (den Body-Teil des AST) enthält und im Scope des Interpreters wie eine normale Variable gespeichert wird. Dieses Muster integriert sich perfekt in das AST/Visitor-Schema. + + + + + + +Dann könnte man auch eine Faktory deklareieren, die ihrerseits Funktionen basierend auf Parametern erzeugt? + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Ja, exakt. Das ist die logische und sehr mächtige Konsequenz aus dem bisherigen Entwurf. + +Was du beschreibst, ist das Konzept von Higher-Order Functions: Funktionen, die andere Funktionen als Parameter entgegennehmen oder – wie in deinem Fall – als Ergebnis zurückgeben. Dein System kann das abbilden, weil IDataMethodValue einfach nur ein weiterer Wert (IDataValue) ist. + +Eine Funktion, die eine andere Funktion erzeugt, ist im Grunde eine Function Factory. + +So würde es im etablierten Schema funktionieren: +Nehmen wir ein klassisches Beispiel: Eine Factory createAdder, die eine Zahl x entgegennimmt und eine neue Funktion zurückgibt. Diese neue Funktion addiert bei jedem Aufruf die ursprüngliche Zahl x zu ihrem eigenen Argument. + +1. Die Darstellung in Blockly: + +Du würdest eine Funktion createAdder mit einem Parameter amountToAdd definieren. +Innerhalb des do-Blocks dieser Funktion gäbe es nur einen einzigen Block: einen return-Block. +In den return-Block würdest du einen anonymen Funktionsblock (Lambda) einfügen. Dieser Block definiert einen eigenen Parameter, z.B. inputValue, und sein Rumpf wäre die Berechnung inputValue + amountToAdd. + +2. Die Repräsentation im AST: + +Der AST für die Factory createAdder würde so aussehen: + +TFunctionDefinitionNode + +FunctionName: 'createAdder' + +Parameters: [Name: 'amountToAdd', Type: TDataType.Ordinal] + +ResultType: TDataType.MethodOf(TDataType.Ordinal, TDataType.Ordinal) (Das ist der entscheidende Punkt: der Rückgabetyp ist selbst ein Funktionstyp!) + +Body: TReturnStatementNode + +Expression: TLambdaNode (die erzeugte, anonyme Funktion) + +Parameters: [Name: 'inputValue', Type: TDataType.Ordinal] + +ResultType: TDataType.Ordinal + +Body: TBinaryOpNode (Operator +) + +Left: TVariableAccessNode ('inputValue') + +Right: TVariableAccessNode ('amountToAdd') + +3. Die Verarbeitung im Interpreter (Die Magie der Closures): + +Hier zeigt sich die Stärke deines Designs: + +Aufruf der Factory: Dein Skript führt aus: set [add5] to [createAdder with (5)]. + +Der Interpreter ruft die Funktion createAdder auf. Es wird ein Scope erzeugt, in dem amountToAdd den IDataOrdinalValue(5) hat. + +Der Interpreter wertet den Rumpf aus und stößt auf den TLambdaNode. + +Wie zuvor beschrieben, erzeugt der Visitor für diesen TLambdaNode eine Closure. + +Das Wichtigste: Diese Closure friert ihren Erstellungskontext ein. Sie "erinnert" sich daran, dass in ihrem übergeordneten Scope amountToAdd = 5 war. + +Die Factory createAdder gibt den IDataMethodValue dieser neuen Closure zurück. + +Dieser IDataMethodValue wird in der Variable add5 gespeichert. Die Funktion createAdder ist nun beendet, ihr Scope ist weg, aber die Closure lebt weiter. + +Aufruf der erzeugten Funktion: Dein Skript führt aus: set [result] to [add5 with (3)]. + +Der Interpreter ruft den in add5 gespeicherten IDataMethodValue auf. + +Die Closure wird mit dem Argument inputValue = IDataOrdinalValue(3) aktiviert. Es wird ein neuer Scope für diesen Aufruf erzeugt. + +Der Interpreter führt den Körper der Closure aus: inputValue + amountToAdd. + +Er findet inputValue (Wert 3) in seinem direkten Scope. + +Er findet amountToAdd nicht in seinem direkten Scope. Er schaut daher in den "eingefrorenen" Scope, den er bei seiner Erzeugung mitbekommen hat, und findet dort den Wert 5. + +Er berechnet 3 + 5 und gibt IDataOrdinalValue(8) zurück. + +Fazit: + +Ja, das ist nicht nur möglich, sondern es ist eine natürliche und elegante Eigenschaft des Systems, das wir besprochen haben. Es erfordert keine speziellen Tricks, sondern ergibt sich direkt aus der Fähigkeit, Funktionen als erstklassige Werte zu behandeln und dem korrekten Management von Scopes durch Closures im Interpreter. Das macht den Automaten extrem ausdrucksstark. + + + + + + +Ich möchte damit Handellssysteme erzeugen. Diese bestehen auf einer Strategie, die für einen bestimmten Zeitraum auf Kurse angewdandt wird. Das Ergebnis ist eine Equity-Curve. Diese Strategie muss durch eine Parametrisierbare Factory erzeugt werden können, damit sie an einzelne backtests oder sogar für Walk-Forward-Analyse benutzt werden kann. Und sie soll am Ende auch Live traden können. Ist das Denkbar? + + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Ja, das ist absolut denkbar. Tatsächlich ist das ein exzellenter und sehr anspruchsvoller Anwendungsfall für die Architektur, die wir entworfen haben. Das System aus Blockly, AST und dem typsicheren Interpreter bildet den perfekten Kern einer solchen Handelsplattform. + +Lassen Sie uns Ihre Anforderungen auf die besprochene Architektur abbilden: + +1. Die parametrisierbare Strategie-Factory +Dies ist exakt der Anwendungsfall für eine Higher-Order Function, den wir zuletzt besprochen haben. + +Blockly-Implementierung: Sie erstellen eine Funktion in Blockly, z.B. CreateMACrossoverStrategy. Diese Funktion hat Parameter wie FastMAPeriod, SlowMAPeriod, RiskPerTrade, etc. + +Rückgabewert: Diese Factory-Funktion gibt eine andere Funktion zurück (einen IDataMethodValue). Nennen wir diese die "Strategie-Funktion". + +Strategie-Funktion: Diese zurückgegebene Funktion hat eine feste Signatur, die vom "Harness" (dem Backtester oder Live-Trader) erwartet wird, z.B. function(CurrentBar: IDataRecordValue, Portfolio: IPortfolioApi): TSignal. Sie hat die Parameter der Factory (z.B. FastMAPeriod = 10, SlowMAPeriod = 50) in ihrer Closure "eingebacken". + +2. Anwendung auf Kurse (Backtesting & Live-Trading) +Der Interpreter allein reicht hier nicht. Sie benötigen ein umgebendes "Harness", das die Strategie ausführt. Dieses Harness wäre für die verschiedenen Modi (Backtest, Live) austauschbar. + +Datenmodellierung: Ihre Myc.Data.Types Unit ist hierfür ideal. + +Ein einzelner Kursbalken (OHLC) wäre ein TDataType.TRecord. + +Die gesamte Kurshistorie wäre ein TDataType.TArray dieser Records. + +Das "Harness": Dies ist eine Delphi-Anwendung, die: + +Die Blockly-Definition lädt und den AST erzeugt. + +Den Interpreter startet, um die Strategie-Factory aufzurufen und eine konkrete Strategie-Instanz (IDataMethodValue) mit den gewünschten Parametern zu erzeugen. + +Eine Schleife startet, die über die historischen (Backtest) oder live eintreffenden (Live-Handel) Kursdaten iteriert. + +In jedem Schleifendurchlauf wird die Strategie-Funktion (IDataMethodValue) aufgerufen und ihr der aktuelle Kursbalken übergeben. + +Broker-Abstraktion: Die Strategie muss Orders platzieren können. Dies geschieht über eine API, die dem Interpreter zur Verfügung gestellt wird. + +Sie definieren ein Delphi-Interface IBrokerAPI mit Methoden wie BuyMarket(Amount), SellLimit(Price, Amount), GetCurrentPositionSize(), etc. + +Im Backtest: Sie übergeben eine TSimulatedBroker-Klasse, die IBrokerAPI implementiert. Diese Klasse führt ein simuliertes Portfolio, berechnet Slippage/Kommissionen und erzeugt die Equity-Curve. + +Im Live-Handel: Sie übergeben eine TRealBroker-Klasse, die sich mit der API Ihres echten Brokers (z.B. Interactive Brokers, Binance) verbindet. + +Die in Blockly erstellte Strategie bleibt exakt dieselbe. Nur die Implementierung der IBrokerAPI wird ausgetauscht. + +3. Walk-Forward-Analyse +Die Architektur unterstützt dies perfekt. Eine Walk-Forward-Analyse ist lediglich eine übergeordnete Schleife, die das Backtesting-Harness steuert: + +Optimierungsphase: Die übergeordnete Schleife ruft das Backtest-Harness wiederholt für einen "In-Sample"-Zeitraum auf. Bei jedem Durchlauf wird die Strategie-Factory mit anderen Parametern (FastMAPeriod, SlowMAPeriod, ...) aufgerufen, um die profitabelsten Einstellungen zu finden. + +Validierungsphase: Die übergeordnete Schleife nimmt die besten Parameter aus der Optimierungsphase und ruft das Backtest-Harness einmalig für den nachfolgenden "Out-of-Sample"-Zeitraum auf, um die Performance auf unbekannten Daten zu messen. + +Dieser Prozess wird für gleitende Zeitfenster wiederholt. + +Mögliche Herausforderungen und Erweiterungen +Performance: Die Interpretation eines AST für jeden einzelnen Tausender von Kursbalken kann bei der Optimierung (Millionen von Durchläufen) zu einem Engpass führen. + +Lösung: Kritische Indikatoren (Moving Average, RSI, etc.) werden nicht in Blockly implementiert, sondern als hochoptimierte Delphi-Funktionen. Diese werden dem Interpreter als "native Funktionen" zur Verfügung gestellt, die von speziellen TNativeFunctionCallNode im AST aufgerufen werden können. + +Komplexität des Zustands: Eine Strategie hat einen Zustand (z.B. "bin ich gerade in einem Trade?"). Dieser Zustand wird über Variablen im Scope des Interpreters gehalten. + +Daten-Feeds: Sie benötigen eine robuste Anbindung an historische Datenquellen und Live-Feeds. Dies ist Teil des Harness, nicht des Interpreters. + +Fazit +Ja, es ist absolut denkbar und ein sehr passender Anwendungsfall. Das entworfene System bietet eine saubere Trennung zwischen: + +Strategie-Logik (Blockly/AST): Definiert von Analysten oder Tradern ohne tiefgehende Programmierkenntnisse. + +Kern-Infrastruktur (Delphi Interpreter & Harness): Die hochperformante und robuste Basis, die von Entwicklern erstellt wird (Indikatoren, Broker-Anbindung, Daten-Feeds). + +Sie schaffen damit eine domänenspezifische Sprache (DSL) für Handelsstrategien, die visuell, flexibel und durch die Delphi-Basis extrem leistungsfähig ist. + + + + + + +Der Harness ist der Knackpunkt. Wäre es denkbar auch das Backtesting und die Analyse über Blockly steuern zu können. Z.B. über spezialisierte Blöcke "Backtest" und "WFA"? + + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Ja, das ist nicht nur denkbar, sondern ein konzeptionell sehr eleganter und mächtiger Schritt. Damit heben Sie das gesamte System von einer reinen "Strategie-Entwicklungsumgebung" auf die Ebene einer vollwertigen, visuellen "Quantitative-Research-Umgebung". + +Der Knackpunkt, den Sie identifiziert haben, wird dabei elegant gelöst: Das "Harness" wird zu einer API, die innerhalb der Blockly-Umgebung über spezialisierte Blöcke zugänglich gemacht wird. + +Die Architektur mit zwei Ebenen +Sie würden effektiv mit zwei Ebenen von Skripten arbeiten: + +Ebene 1: Das Strategie-Skript (wie bisher besprochen) + +Dies ist eine Funktion (erzeugt von einer Factory), die die Handelslogik enthält. + +Sie nimmt typischerweise Kursdaten und den Portfolio-Zustand als Input und gibt Handelssignale als Output. + +Diese Ebene weiß nichts von Backtesting oder Live-Handel. Sie ist agnostisch. + +Ebene 2: Das Kontroll- oder Analyse-Skript + +Dies ist ein übergeordnetes Blockly-Skript, das den gesamten Forschungs- oder Handelsprozess steuert. + +Es verwendet die Strategie-Funktion (den IDataMethodValue) von Ebene 1 als Parameter für die neuen, spezialisierten Harness-Blöcke. + +Spezialisierte Blöcke für das "Harness" +Hier sind die Blöcke, die Sie erwähnt haben, und wie sie sich einfügen würden: + +[Load Price Data]-Block (Expression) + +Inputs: Symbol, Zeitrahmen, Start-Datum, End-Datum. + +Output: Ein IDataArrayValue mit den Kursdaten (ein Array von Records). + +Implementierung: Ein nativer Delphi-Aufruf, der Daten aus einer Datenbank oder Datei lädt. + +[Backtest]-Block (Expression oder Statement) + +Inputs: + +Price Data: Der IDataArrayValue vom Load-Block. + +Strategy: Ein IDataMethodValue! Hier stecken Sie die von Ihrer Factory erzeugte Strategie-Instanz hinein. + +Initial Capital: Ein IDataOrdinalValue oder IDataDecimalValue. + +Commission: Ein IDataFloatValue. + +Output: Ein IDataRecordValue, das alle Ergebnisse enthält: EquityCurve (ein Array), TradeList (ein Array), Statistics (ein weiteres Record). + +Implementierung: Ein mächtiger, nativer Delphi-Aufruf. Der Interpreter ruft eine einzelne Delphi-Funktion ExecuteBacktest auf und übergibt ihr die IDataValue-Parameter. Diese Funktion führt dann die hochoptimierte Backtesting-Schleife aus und ruft bei jedem Takt die übergebene Strategie-Funktion (Strategy.Value(...)) auf. + +[Walk Forward Analysis]-Block (Expression oder Statement) + +Inputs: + +Price Data. + +Strategy Factory: Hier wird die Factory selbst (IDataMethodValue) übergeben, nicht eine Instanz. Der WFA-Block muss ja selbst Instanzen mit verschiedenen Parametern erzeugen können. + +Parameter Ranges: Ein IDataRecordValue, das die zu optimierenden Parameter definiert (z.B. {FastMA: {start:10, end:100, step:5}}). + +Weitere Parameter wie In-Sample-Länge, Out-of-Sample-Länge. + +Output: Ein IDataArrayValue, das die Ergebnisse jeder einzelnen Walk-Forward-Periode enthält. + +Implementierung: Ein weiterer komplexer, nativer Delphi-Aufruf, der die gesamte WFA-Logik kapselt. + +Beispiel eines Analyse-Skripts in Blockly +Stellen Sie sich vor, Sie würden folgendes Skript in Blockly zusammenklicken: + +// Lade die nötigen Daten +set [eurusd_h1] to [Load Price Data symbol:"EURUSD", timeframe:"H1", from:"2020-01-01", to:"2025-01-01"] + +// Hole einen Verweis auf die Strategie-Factory +// (angenommen, die Factory "createMACrossover" wurde in einem anderen Tab definiert) +set [myStrategyFactory] to [get function handle of "createMACrossover"] + +// Erzeuge eine spezifische Instanz der Strategie für den Test +set [strategy_10_50] to [call function [myStrategyFactory] with FastMA:10, SlowMA:50] + +// Führe den Backtest aus +set [testResult] to [Backtest data:[eurusd_h1], strategy:[strategy_10_50], capital:10000] + +// Analysiere und visualisiere die Ergebnisse +set [stats] to [get field "Statistics" from [testResult]] +print("Sharpe Ratio: " + [get field "SharpeRatio" from [stats]]) + +[Plot Equity Curve from: [get field "EquityCurve" from [testResult]]] +Die Vorteile dieses Ansatzes +Maximale Flexibilität: Der Anwender kann nicht nur die Strategie, sondern den gesamten Forschungs-Prozess visuell gestalten und automatisieren. Komplexe Vergleiche (Strategie A vs. Strategie B auf denselben Daten) werden trivial. + +Performance: Die rechenintensiven Operationen (Backtesting-Schleife, WFA-Optimierung) laufen als kompilierter, nativer Delphi-Code. Nur die übergeordnete Orchestrierung und die Strategie-Logik selbst werden interpretiert. Das ist der "Best of both worlds"-Ansatz. + +Kapselung: Die Komplexität der Harness-Implementierung ist hinter einfachen Blöcken verborgen. Der Anwender muss nicht wissen, wie ein Backtest funktioniert, sondern nur, dass er ihn mit einer Strategie und Daten füttern muss. + +Protokollierung und Reproduzierbarkeit: Das gesamte Analyse-Skript kann als einzelnes Blockly-XML gespeichert werden, was jeden Forschungsschritt perfekt dokumentiert und reproduzierbar macht. + +Fazit: Ja, das ist der logische nächste Schritt, um ein extrem mächtiges und benutzerfreundliches Werkzeug zu schaffen. Die Harness-Funktionalität wird zu einer "nativen API" innerhalb Ihrer visuellen Programmierumgebung. + + + + + + +So in etwa habe ich mir das vorgestellt. Zu den Preisdaten: die können nicht als Array zur Verfügung gestellt werden. Schließlich handelt es sich (im Live-Modus) um Echtzeitdaten, also Ticks oder 1M Bars. Wie würde man dieses Problem lösen? + + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Das ist ein exzellenter und entscheidender Punkt. Sie haben vollkommen recht, das batch-orientierte Array-Modell ist nur für Backtests geeignet. Für den Live-Handel benötigen wir ein ereignisgesteuertes (event-driven) Modell. + +Die gute Nachricht ist, dass unsere Architektur dies mit einer eleganten Abstraktion lösen kann, die es sogar erlaubt, die Logik für Backtesting und Live-Handel nahezu identisch zu halten. + +Die Lösung liegt darin, nicht die Daten selbst, sondern eine Abstraktion der Datenquelle (einen "Data Provider") zu übergeben und auf dessen Ereignisse zu reagieren. + +1. Einführung des "Data Provider"-Konzepts +Wir führen einen neuen Typ in unserem System ein: den DataProvider. Dies ist im Hintergrund ein Delphi-Objekt, das eine Datenquelle repräsentiert, und in Blockly wird es durch einen speziellen IDataValue (z.B. IDataProviderValue) dargestellt. Es ist ein "Handle" zur Datenquelle. + +Ein DataProvider hat eine Hauptaufgabe: Er emittiert ein OnNewData-Ereignis, sobald neue Daten (ein Tick oder ein neuer Balken) verfügbar sind. + +2. Neue Blöcke für den Live-Modus +Um dies in Blockly zu steuern, benötigen wir zwei neue Arten von Blöcken im "Kontroll-Skript" (Ebene 2): + +[Create Live Data Provider]-Block (Expression) + +Inputs: Symbol, Zeitrahmen, Broker/Feed-API. + +Output: Ein DataProvider-Handle. + +Implementierung: Dieser native Delphi-Block erzeugt eine Instanz eines Live-Providers, der sich z.B. per WebSocket mit einer Börse verbindet. Er startet aber noch nicht den Datenfluss. + +[On New Bar]-Block (Event Handler) + +Dies ist der wichtigste Block. Es ist ein Ereignis-Handler, kein normaler sequenzieller Block. + +Er hat einen Eingangs-Slot für ein DataProvider-Handle. + +Er definiert eine lokale Variable (z.B. newBar), die bei jedem Ereignis den neuen Kursbalken (IDataRecordValue) enthält. + +Er hat einen "do"-Bereich, in den die Logik eingefügt wird, die bei jedem eintreffenden Balken ausgeführt werden soll. + +3. Der Arbeitsablauf im Live-Modus +Ihr Kontroll-Skript für den Live-Handel würde nun so aussehen: + +// --- Initialisierungsphase --- + +// Erzeuge eine Instanz der Strategie +set [myStrategy] to [call function [get function handle of "createMACrossover"] with FastMA:10, SlowMA:50] + +// Erzeuge und konfiguriere den Live-Datenstrom +set [liveFeed] to [Create Live Data Provider symbol:"BTCUSDT", timeframe:"1m", broker:"Binance"] + +// Erzeuge eine Instanz des echten Brokers +set [realBroker] to [Create Broker API for account:"MyLiveAccount"] + + +// --- Reaktive Phase (Ereignis-Schleife) --- + +[On New Bar [currentBar] from [liveFeed] do + // Diese Logik wird bei jedem neuen 1-Minuten-Balken ausgeführt + + // Rufe die eigentliche Strategie-Funktion auf + set [signal] to [call function [myStrategy] with bar:[currentBar], broker:[realBroker]] + + // (Hier könnte noch zusätzliche Logik stehen, z.B. Logging) +] + +// Starte den Datenstrom (dieser Block beendet sich sofort, die Events laufen im Hintergrund) +[Start Data Provider [liveFeed]] +4. Die Vereinheitlichung: Backtesting als simulierter Stream +Jetzt kommt der eleganteste Teil: Wir können das Backtesting so umgestalten, dass es denselben ereignisgesteuerten Mechanismus verwendet. + +Der [Backtest]-Block, den wir zuvor besprochen haben, ist im Grunde ein Wrapper, der Folgendes tut: + +Er lädt die historischen Daten in ein Array (wie gehabt). + +Er erstellt einen SimulatedDataProvider. + +Dieser simulierte Provider hat ebenfalls ein Start-Kommando. Wenn es aufgerufen wird, iteriert er intern durch das Array und feuert für jeden Balken exakt dasselbe OnNewData-Ereignis wie der Live-Provider. + +Wenn das Array-Ende erreicht ist, feuert er ein OnFinished-Ereignis. + +Der Backtest-Block führt die Logik aus, wartet auf das OnFinished-Ereignis und gibt dann das gesammelte Ergebnis zurück. + +Der entscheidende Vorteil: Die Logik innerhalb des [On New Bar ... do ...]-Blocks muss nicht wissen, ob die Balken aus einem Live-Feed oder einem Simulator kommen. Sie kann für beide Szenarien identisch sein. Dies eliminiert einen der größten Risikofaktoren im algorithmischen Handel: Diskrepanzen zwischen Backtest- und Live-Implementierung. + +Fazit: + +Sie lösen das Problem, indem Sie von einem daten-zentrierten (Übergabe eines Arrays) zu einem quell-zentrierten (Übergabe eines Providers) Modell wechseln und eine ereignisgesteuerte Verarbeitung einführen. Das ist die Standard-Architektur für reaktive Systeme und passt perfekt zum Live-Handel. + + + + + + +Könnte man dem Provider das [On New Bar...] nicht auch einfach als closure-Parameter mitgeben? + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Ja, absolut. Das ist eine hervorragende Beobachtung und eine konzeptionell sehr saubere, funktionale Alternative zum deklarativen Event-Handler-Block. Anstatt auf ein globales Ereignis zu lauschen, würden Sie die Verarbeitungslogik direkt als Parameter übergeben. + +Dieser Ansatz passt perfekt zum Higher-Order-Function-Paradigma, das wir bereits für die Strategie-Factories etabliert haben. + +So würde der Ansatz mit dem Closure-Parameter aussehen: +Sie würden den [On New Bar]-Block durch einen "ausführenden" Block ersetzen, der die Logik als Argument entgegennimmt. + +Neuer Block: [Subscribe to Provider] oder [Run Provider] (Statement) + +Inputs: + +Provider: Das DataProvider-Handle (wie gehabt). + +onNewBar: Ein IDataMethodValue. Dies ist der entscheidende Input – ein Slot, der eine Funktion erwartet. Die Signatur dieser Funktion wäre function(bar: IDataRecordValue). + +onError (optional): Ein weiterer IDataMethodValue für die Fehlerbehandlung. + +onFinished (optional): Ein IDataMethodValue, der aufgerufen wird, wenn der Stream endet (relevant für Backtests). + +Implementierung: Dieser native Delphi-Block würde den Provider starten und die übergebene onNewBar-Closure als Callback für das interne OnNewData-Ereignis des Providers registrieren. + +Beispiel-Skript im neuen Stil +Das Live-Trading-Skript würde sich leicht verändern. Die Logik, die vorher im "do"-Bereich des Event-Handlers stand, wird nun in einen anonymen Funktionsblock (Lambda) gepackt und direkt übergeben: + +// --- Initialisierungsphase (bleibt gleich) --- +set [myStrategy] to [...] +set [liveFeed] to [...] +set [realBroker] to [...] + +// --- Start der Verarbeitung mit Closure als Callback --- + +[Subscribe to Provider [liveFeed] onNewBar: [ + // Dies ist ein anonymer Funktionsblock (Lambda), + // der als IDataMethodValue in den Slot gereicht wird. + + function with [currentBar] do + call function [myStrategy] with bar:[currentBar], broker:[realBroker] + end function +]] +Vergleich der beiden Ansätze +Beide Ansätze sind valide und führen zum Ziel. Es ist eine Design-Entscheidung, welche Metapher Sie in Ihrer visuellen Sprache bevorzugen. + +Eigenschaft Ansatz 1: [On New Bar ...] Ansatz 2: Closure-Parameter +Metapher Abonnement / Event-Listener Callback / Higher-Order Function +Stil Deklarativ, ereignisgesteuert. Funktional, imperativ. +Vorteile - Visuell sehr intuitiv, ähnlich wie UI-Programmierung (button.onClick).
- Einfache Anbindung mehrerer, unabhängiger Handler an dieselbe Quelle. - Konzeptionell extrem sauber und in sich geschlossen.
- Expliziter Daten- und Kontrollfluss.
- Konsistent mit dem "Alles ist ein Wert"-Prinzip. +Nachteile - Der Kontrollfluss ist weniger explizit; die Verbindung zwischen Quelle und Handler ist implizit.
- Wirkt weniger wie ein einzelnes, zusammenhängendes Programm. - Visuell etwas "verschachtelter" durch den Lambda-Block.
- Anbindung mehrerer Handler erfordert eine manuelle Dispatcher-Funktion. +AST-Knoten TEventHandlerNode TFunctionCallNode (für "Subscribe"), der einen TLambdaNode als Argument nimmt. + +In Google Sheets exportieren +Fazit +Ihr Vorschlag ist exzellent. Er ist aus Programmierersicht oft die "reinere" Lösung, da er Abhängigkeiten explizit macht. Für eine visuelle DSL, die vielleicht auch von Nicht-Programmierern genutzt werden soll, kann der [On New Bar]-Ansatz manchmal eingängiger sein. + +Die Schönheit Ihrer Architektur ist, dass die zugrundeliegende Delphi-Implementierung des DataProvider (mit einem simplen TEvent OnNewData) problemlos beide Block-Varianten unterstützen kann. Sie könnten sogar beide Blöcke anbieten und dem Benutzer die Wahl lassen, welchen Stil er bevorzugt. + + + + + + +Schwierig. Ich möchte z.B. in der Lage sein, aus einem Feed mehrere Signale in verschiedenen Timeframes zu generieren und diese dann zu gewichten. Und es soll auch möglich sein, mehrere verschieden Feeds zu analysieren um einee Handelsentscheidung zu treffen. + + + +D +Delphi Coding +Benutzerdefiniertes Gem + +Gedankengang anzeigen +Das ist eine exzellente, praxisnahe Anforderung. Und hier zeigt sich, meiner Meinung nach, die klare Stärke des ereignisgesteuerten Ansatzes ([On New Bar ...]) gegenüber dem funktionalen Callback-Ansatz. + +Ihr Vorschlag mit der Closure ist elegant für einen einzelnen Datenstrom. Aber für die parallele Verarbeitung mehrerer asynchroner Datenströme, deren Ergebnisse zu einem Zeitpunkt X zusammengeführt werden müssen, ist das Abonnement-Modell (Subscription) oft wesentlich klarer und flexibler. + +Ihre Anforderung lässt sich mit folgenden Konzepten abbilden: + +DataProvider-Ketten (Resampler): Ein Baustein, der einen DataProvider als Input nimmt und einen neuen DataProvider als Output liefert. + +Parallele Event-Handler: Mehrere [On New Bar]-Blöcke, die unabhängig voneinander auf verschiedene Datenquellen lauschen. + +Geteilter Zustand (Shared State): Globale Variablen im Kontroll-Skript, die die Signale der einzelnen Quellen speichern. + +Aggregationslogik: Eine Prozedur, die immer dann aufgerufen wird, wenn sich einer der Inputs ändert, um eine finale Handelsentscheidung zu treffen. + +Szenario 1: Mehrere Timeframes aus einem Feed +Ziel: Aus einem 1-Minuten-Feed Signale für 1M, 5M und 15M generieren und gewichten. + +Lösung: Wir führen einen neuen "intelligenten" Provider-Block ein, den Resampler. + +Neuer Block: [Aggregate Timeframe] (oder [Create Resampled Feed]) + +Input: + +Source Provider: Der DataProvider der feineren Zeiteinheit (z.B. der 1M-Live-Feed). + +New Timeframe: z.B. "5m", "15m", "1h". + +Output: Ein neuer DataProvider-Handle, der nur dann ein OnNewData-Ereignis auslöst, wenn ein vollständiger Balken der neuen Zeiteinheit fertig ist. + +Implementierung: Ein nativer Delphi-Wrapper, der die ankommenden 1M-Balken sammelt und zu 5M-Balken zusammenfügt. + +Ihr Kontroll-Skript würde dann so aussehen: + +// --- 1. Initialisierung der Datenquellen --- +set [live_1m_feed] to [Create Live Data Provider symbol:"BTCUSDT", timeframe:"1m"] + +// Erzeuge die aggregierten Feeds aus der 1M-Quelle +set [feed_5m] to [Aggregate Timeframe from:[live_1m_feed] to:"5m"] +set [feed_15m] to [Aggregate Timeframe from:[live_1m_feed] to:"15m"] + + +// --- 2. Initialisierung des geteilten Zustands --- +set [signal_1m] to 0 +set [signal_5m] to 0 +set [signal_15m] to 0 + + +// --- 3. Parallele Verarbeitung der Feeds (Event Handlers) --- + +[On New Bar [bar] from [live_1m_feed] do + set [signal_1m] to [call function [calculateSignal_1m] with data:[bar]] + call function [MakeFinalDecision] // Prüfe bei jedem 1M-Tick neu +] + +[On New Bar [bar] from [feed_5m] do + set [signal_5m] to [call function [calculateSignal_5m] with data:[bar]] + // Eine Neubewertung ist hier nicht zwingend nötig, da sie schon durch den 1M-Feed getriggert wird. +] + +[On New Bar [bar] from [feed_15m] do + set [signal_15m] to [call function [calculateSignal_15m] with data:[bar]] +] + + +// --- 4. Aggregationslogik --- +to MakeFinalDecision do + // Diese Funktion liest den geteilten Zustand und trifft die Entscheidung + set [weighted_signal] to ([signal_1m] * 0.5) + ([signal_5m] * 0.3) + ([signal_15m] * 0.2) + + if [weighted_signal] > 0.8 then + [Buy Market on broker:[myBroker] amount:1] + else if [weighted_signal] < -0.8 then + [Sell Market on broker:[myBroker] amount:1] + end if +end function +Szenario 2: Mehrere verschiedene Feeds +Ziel: Den BTC/USDT-Feed und zusätzlich einen S&P500-Feed zur Stimmungsanalyse nutzen. + +Architektonisch ist dies exakt derselbe Anwendungsfall. Anstatt die Feeds durch Resampling zu erzeugen, erstellen Sie einfach zwei unabhängige Live Data Provider. + +// --- 1. Initialisierung der Datenquellen --- +set [btc_feed] to [Create Live Data Provider symbol:"BTCUSDT", timeframe:"1h"] +set [sp500_feed] to [Create Live Data Provider symbol:"SPY", timeframe:"1h"] + + +// --- 2. Initialisierung des geteilten Zustands --- +set [btc_trend_signal] to 0 +set [market_sentiment] to "NEUTRAL" // Kann Text, Zahl, etc. sein + + +// --- 3. Parallele Verarbeitung der Feeds --- + +[On New Bar [bar] from [btc_feed] do + set [btc_trend_signal] to [call function [calculateBTCTrend] with data:[bar]] + call function [MakeFinalBTCDecision] // Nur hier wird eine Handelsentscheidung für BTC getroffen +] + +[On New Bar [bar] from [sp500_feed] do + set [market_sentiment] to [call function [calculateSentiment] with data:[bar]] + // Hier wird nur der Zustand aktualisiert, keine Order platziert +] + + +// --- 4. Aggregationslogik --- +to MakeFinalBTCDecision do + // Diese Funktion berücksichtigt jetzt den Zustand beider Feeds + if ([btc_trend_signal] > 0.7) and ([market_sentiment] = "BULLISH") then + [Buy Market on broker:[myBroker] for symbol:"BTCUSDT" amount:1] + end if +end function +Fazit +Die ereignisgesteuerte Architektur mit parallelen On New Bar-Handlern ist für diese komplexen, realen Szenarien wie geschaffen. Sie ermöglicht: + +Entkopplung: Jeder "Signal-Generator" ist eine eigenständige, isolierte Einheit. + +Klarheit: Das visuelle Layout der parallelen Blöcke spiegelt die tatsächliche parallele Datenverarbeitung wider. + +Flexibilität: Das Hinzufügen einer weiteren Datenquelle (z.B. Zinsdaten) bedeutet lediglich das Hinzufügen eines weiteren DataProvider- und On New Bar-Blockpaars und die Anpassung der finalen Entscheidungslogik. + +Der Versuch, dies mit verschachtelten Callbacks (dem Closure-Ansatz) abzubilden, würde schnell zu einem unübersichtlichen und schwer wartbaren Code führen (bekannt als "Callback Hell" oder "Pyramid of Doom"). Hier ist das deklarative Abo-Modell klar überlegen. \ No newline at end of file diff --git a/Src/AST/Myc.Ast.pas b/Src/AST/Myc.Ast.pas new file mode 100644 index 0000000..b0d2aae --- /dev/null +++ b/Src/AST/Myc.Ast.pas @@ -0,0 +1,882 @@ +unit Myc.Ast; + +interface + +uses + System.SysUtils, + System.Generics.Collections, + Myc.Data.Types; + +type + // Operators are now type-safe enums + TBinaryOperator = (boAdd, boSubtract, boMultiply, boDivide); + TUnaryOperator = (uoNegate, uoNot); + + // Forward declarations for interfaces + IAstVisitor = interface; + IAstNode = interface; + IExpressionNode = interface; + IStatementNode = interface; + IConstantNode = interface; + IIdentifierNode = interface; + IBinaryExpressionNode = interface; + IUnaryExpressionNode = interface; + IIfExpressionNode = interface; + ILambdaExpressionNode = interface; + IFunctionCallNode = interface; + IBlockStatementNode = interface; + IVariableDeclarationStatementNode = interface; + IExpressionStatementNode = interface; + + // --- Abstract Node Interfaces --- + + // Base interface for all AST nodes + IAstNode = interface(IInterface) + // Accept is a function that returns the result of the visit. + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + // Abstract interface for all nodes that evaluate to a value. + IExpressionNode = interface(IAstNode) + end; + + // Abstract interface for all nodes that perform an action. + IStatementNode = interface(IAstNode) + end; + + // --- Concrete Expression Node Interfaces --- + + IConstantNode = interface(IExpressionNode) + {$region 'private'} + function GetValue: IDataValue; + {$endregion} + property Value: IDataValue read GetValue; + end; + + IIdentifierNode = interface(IExpressionNode) + {$region 'private'} + function GetName: string; + {$endregion} + property Name: string read GetName; + end; + + IBinaryExpressionNode = interface(IExpressionNode) + {$region 'private'} + function GetLeft: IExpressionNode; + function GetOperator: TBinaryOperator; + function GetRight: IExpressionNode; + {$endregion} + property Left: IExpressionNode read GetLeft; + property Operator: TBinaryOperator read GetOperator; + property Right: IExpressionNode read GetRight; + end; + + IUnaryExpressionNode = interface(IExpressionNode) + {$region 'private'} + function GetOperator: TUnaryOperator; + function GetRight: IExpressionNode; + {$endregion} + property Operator: TUnaryOperator read GetOperator; + property Right: IExpressionNode read GetRight; + end; + + IIfExpressionNode = interface(IExpressionNode) + {$region 'private'} + function GetCondition: IExpressionNode; + function GetThenBranch: IExpressionNode; + function GetElseBranch: IExpressionNode; + {$endregion} + property Condition: IExpressionNode read GetCondition; + property ThenBranch: IExpressionNode read GetThenBranch; + property ElseBranch: IExpressionNode read GetElseBranch; + end; + + ILambdaExpressionNode = interface(IExpressionNode) + {$region 'private'} + function GetParameters: TList; + function GetBody: IAstNode; // Can be an expression or a block statement + {$endregion} + property Parameters: TList read GetParameters; + property Body: IAstNode read GetBody; + end; + + IFunctionCallNode = interface(IExpressionNode) + {$region 'private'} + function GetCallee: IExpressionNode; + function GetArguments: TList; + {$endregion} + property Callee: IExpressionNode read GetCallee; + property Arguments: TList read GetArguments; + end; + + // --- Concrete Statement Node Interfaces --- + + IBlockStatementNode = interface(IStatementNode) + {$region 'private'} + function GetStatements: TList; + {$endregion} + property Statements: TList read GetStatements; + end; + + IVariableDeclarationStatementNode = interface(IStatementNode) + {$region 'private'} + function GetIdentifier: IIdentifierNode; + function GetInitializer: IExpressionNode; // Can be nil + {$endregion} + property Identifier: IIdentifierNode read GetIdentifier; + property Initializer: IExpressionNode read GetInitializer; + end; + + IExpressionStatementNode = interface(IStatementNode) + {$region 'private'} + function GetExpression: IExpressionNode; + {$endregion} + property Expression: IExpressionNode read GetExpression; + end; + + // All visitor methods are functions returning a value. + IAstVisitor = interface + ['{5F4110E9-0158-41E9-A512-E57A843E8A5A}'] + // Expression visitors + function VisitConstant(const Node: IConstantNode): IDataValue; + function VisitIdentifier(const Node: IIdentifierNode): IDataValue; + function VisitBinaryExpression(const Node: IBinaryExpressionNode): IDataValue; + function VisitUnaryExpression(const Node: IUnaryExpressionNode): IDataValue; + function VisitIfExpression(const Node: IIfExpressionNode): IDataValue; + function VisitLambdaExpression(const Node: ILambdaExpressionNode): IDataValue; + function VisitFunctionCall(const Node: IFunctionCallNode): IDataValue; + + // Statement visitors + function VisitBlockStatement(const Node: IBlockStatementNode): IDataValue; + function VisitVariableDeclarationStatement(const Node: IVariableDeclarationStatementNode): IDataValue; + function VisitExpressionStatement(const Node: IExpressionStatementNode): IDataValue; + end; + + // Record acting as a namespace for the factory functions. + TAst = record + // Expressions + class function Constant(AValue: IDataValue): IConstantNode; static; + class function Identifier(AName: string): IIdentifierNode; static; + class function BinaryExpr( + ALeft: IExpressionNode; + AOperator: TBinaryOperator; + ARight: IExpressionNode + ): IBinaryExpressionNode; static; + class function UnaryExpr(AOperator: TUnaryOperator; ARight: IExpressionNode): IUnaryExpressionNode; static; + class function IfExpr(ACondition: IExpressionNode; AThenBranch, AElseBranch: IExpressionNode): IIfExpressionNode; static; + class function LambdaExpr(const AParameters: array of IIdentifierNode; ABody: IAstNode): ILambdaExpressionNode; static; + class function FunctionCall(ACallee: IExpressionNode; const AArguments: array of IExpressionNode): IFunctionCallNode; static; + // Statements + class function Block(const AStatements: array of IStatementNode): IBlockStatementNode; static; + class function VarDecl(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode): IVariableDeclarationStatementNode; static; + class function ExprStmt(AExpression: IExpressionNode): IExpressionStatementNode; static; + end; + + // Manages the scope of execution, holding variables and their values. + TExecutionScope = class + private + FParent: TExecutionScope; + FVariables: TDictionary; + public + constructor Create(AParent: TExecutionScope = nil); + destructor Destroy; override; + function FindValue(const Name: string; out Value: IDataValue): Boolean; + procedure SetValue(const Name: string; const Value: IDataValue); + end; + + // TEvaluatorVisitor is stateless and thread-safe. + TEvaluatorVisitor = class(TInterfacedObject, IAstVisitor) + private + FScope: TExecutionScope; + function IsTruthy(AValue: IDataValue): Boolean; + public + constructor Create(AScope: TExecutionScope); + // Expression visitors + function VisitConstant(const Node: IConstantNode): IDataValue; + function VisitIdentifier(const Node: IIdentifierNode): IDataValue; + function VisitBinaryExpression(const Node: IBinaryExpressionNode): IDataValue; + function VisitUnaryExpression(const Node: IUnaryExpressionNode): IDataValue; + function VisitIfExpression(const Node: IIfExpressionNode): IDataValue; + function VisitLambdaExpression(const Node: ILambdaExpressionNode): IDataValue; + function VisitFunctionCall(const Node: IFunctionCallNode): IDataValue; + + // Statement visitors + function VisitBlockStatement(const Node: IBlockStatementNode): IDataValue; + function VisitVariableDeclarationStatement(const Node: IVariableDeclarationStatementNode): IDataValue; + function VisitExpressionStatement(const Node: IExpressionStatementNode): IDataValue; + end; + +implementation + +type + TConstantNodeImpl = class(TInterfacedObject, IConstantNode) + private + FValue: IDataValue; + function GetValue: IDataValue; + public + constructor Create(AValue: IDataValue); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TIdentifierNodeImpl = class(TInterfacedObject, IIdentifierNode) + private + FName: string; + function GetName: string; + public + constructor Create(AName: string); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TBinaryExpressionNodeImpl = class(TInterfacedObject, IBinaryExpressionNode) + private + FLeft: IExpressionNode; + FOperator: TBinaryOperator; + FRight: IExpressionNode; + function GetLeft: IExpressionNode; + function GetOperator: TBinaryOperator; + function GetRight: IExpressionNode; + public + constructor Create(ALeft: IExpressionNode; AOperator: TBinaryOperator; ARight: IExpressionNode); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TUnaryExpressionNodeImpl = class(TInterfacedObject, IUnaryExpressionNode) + private + FOperator: TUnaryOperator; + FRight: IExpressionNode; + function GetOperator: TUnaryOperator; + function GetRight: IExpressionNode; + public + constructor Create(AOperator: TUnaryOperator; ARight: IExpressionNode); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TIfExpressionNodeImpl = class(TInterfacedObject, IIfExpressionNode) + private + FCondition: IExpressionNode; + FThenBranch: IExpressionNode; + FElseBranch: IExpressionNode; + function GetCondition: IExpressionNode; + function GetThenBranch: IExpressionNode; + function GetElseBranch: IExpressionNode; + public + constructor Create(ACondition, AThenBranch, AElseBranch: IExpressionNode); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TLambdaExpressionNodeImpl = class(TInterfacedObject, ILambdaExpressionNode) + private + FParameters: TList; + FBody: IAstNode; + function GetParameters: TList; + function GetBody: IAstNode; + public + constructor Create(AParameters: TList; ABody: IAstNode); + destructor Destroy; override; + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TFunctionCallNodeImpl = class(TInterfacedObject, IFunctionCallNode) + private + FCallee: IExpressionNode; + FArguments: TList; + function GetCallee: IExpressionNode; + function GetArguments: TList; + public + constructor Create(ACallee: IExpressionNode; AArguments: TList); + destructor Destroy; override; + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TBlockStatementNodeImpl = class(TInterfacedObject, IBlockStatementNode) + private + FStatements: TList; + function GetStatements: TList; + public + constructor Create(AStatements: TList); + destructor Destroy; override; + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TVariableDeclarationStatementNodeImpl = class(TInterfacedObject, IVariableDeclarationStatementNode) + private + FIdentifier: IIdentifierNode; + FInitializer: IExpressionNode; + function GetIdentifier: IIdentifierNode; + function GetInitializer: IExpressionNode; + public + constructor Create(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + + TExpressionStatementNodeImpl = class(TInterfacedObject, IExpressionStatementNode) + private + FExpression: IExpressionNode; + function GetExpression: IExpressionNode; + public + constructor Create(AExpression: IExpressionNode); + function Accept(const Visitor: IAstVisitor): IDataValue; + end; + +{ TAst - Factory Function Implementations } + +class function TAst.Constant(AValue: IDataValue): IConstantNode; +begin + Result := TConstantNodeImpl.Create(AValue); +end; + +class function TAst.Identifier(AName: string): IIdentifierNode; +begin + Result := TIdentifierNodeImpl.Create(AName); +end; + +class function TAst.BinaryExpr(ALeft: IExpressionNode; AOperator: TBinaryOperator; ARight: IExpressionNode): IBinaryExpressionNode; +begin + Result := TBinaryExpressionNodeImpl.Create(ALeft, AOperator, ARight); +end; + +class function TAst.UnaryExpr(AOperator: TUnaryOperator; ARight: IExpressionNode): IUnaryExpressionNode; +begin + Result := TUnaryExpressionNodeImpl.Create(AOperator, ARight); +end; + +class function TAst.IfExpr(ACondition: IExpressionNode; AThenBranch, AElseBranch: IExpressionNode): IIfExpressionNode; +begin + Result := TIfExpressionNodeImpl.Create(ACondition, AThenBranch, AElseBranch); +end; + +class function TAst.LambdaExpr(const AParameters: array of IIdentifierNode; ABody: IAstNode): ILambdaExpressionNode; +var + paramList: TList; + param: IIdentifierNode; +begin + paramList := TList.Create; + for param in AParameters do + paramList.Add(param); + Result := TLambdaExpressionNodeImpl.Create(paramList, ABody); +end; + +class function TAst.FunctionCall(ACallee: IExpressionNode; const AArguments: array of IExpressionNode): IFunctionCallNode; +var + argList: TList; + arg: IExpressionNode; +begin + argList := TList.Create; + for arg in AArguments do + argList.Add(arg); + Result := TFunctionCallNodeImpl.Create(ACallee, argList); +end; + +class function TAst.Block(const AStatements: array of IStatementNode): IBlockStatementNode; +var + stmtList: TList; + stmt: IStatementNode; +begin + stmtList := TList.Create; + for stmt in AStatements do + stmtList.Add(stmt); + Result := TBlockStatementNodeImpl.Create(stmtList); +end; + +class function TAst.VarDecl(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode): IVariableDeclarationStatementNode; +begin + Result := TVariableDeclarationStatementNodeImpl.Create(AIdentifier, AInitializer); +end; + +class function TAst.ExprStmt(AExpression: IExpressionNode): IExpressionStatementNode; +begin + Result := TExpressionStatementNodeImpl.Create(AExpression); +end; + +{ TConstantNodeImpl } + +constructor TConstantNodeImpl.Create(AValue: IDataValue); +begin + inherited Create; + FValue := AValue; +end; + +function TConstantNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitConstant(Self); +end; + +function TConstantNodeImpl.GetValue: IDataValue; +begin + Result := FValue; +end; + +{ TIdentifierNodeImpl } + +constructor TIdentifierNodeImpl.Create(AName: string); +begin + inherited Create; + FName := AName; +end; + +function TIdentifierNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitIdentifier(Self); +end; + +function TIdentifierNodeImpl.GetName: string; +begin + Result := FName; +end; + +{ TBinaryExpressionNodeImpl } + +constructor TBinaryExpressionNodeImpl.Create(ALeft: IExpressionNode; AOperator: TBinaryOperator; ARight: IExpressionNode); +begin + inherited Create; + FLeft := ALeft; + FOperator := AOperator; + FRight := ARight; +end; + +function TBinaryExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitBinaryExpression(Self); +end; + +function TBinaryExpressionNodeImpl.GetLeft: IExpressionNode; +begin + Result := FLeft; +end; + +function TBinaryExpressionNodeImpl.GetOperator: TBinaryOperator; +begin + Result := FOperator; +end; + +function TBinaryExpressionNodeImpl.GetRight: IExpressionNode; +begin + Result := FRight; +end; + +{ TUnaryExpressionNodeImpl } + +constructor TUnaryExpressionNodeImpl.Create(AOperator: TUnaryOperator; ARight: IExpressionNode); +begin + inherited Create; + FOperator := AOperator; + FRight := ARight; +end; + +function TUnaryExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitUnaryExpression(Self); +end; + +function TUnaryExpressionNodeImpl.GetOperator: TUnaryOperator; +begin + Result := FOperator; +end; + +function TUnaryExpressionNodeImpl.GetRight: IExpressionNode; +begin + Result := FRight; +end; + +{ TIfExpressionNodeImpl } + +constructor TIfExpressionNodeImpl.Create(ACondition, AThenBranch, AElseBranch: IExpressionNode); +begin + inherited Create; + FCondition := ACondition; + FThenBranch := AThenBranch; + FElseBranch := AElseBranch; +end; + +function TIfExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitIfExpression(Self); +end; + +function TIfExpressionNodeImpl.GetCondition: IExpressionNode; +begin + Result := FCondition; +end; + +function TIfExpressionNodeImpl.GetElseBranch: IExpressionNode; +begin + Result := FElseBranch; +end; + +function TIfExpressionNodeImpl.GetThenBranch: IExpressionNode; +begin + Result := FThenBranch; +end; + +{ TLambdaExpressionNodeImpl } + +constructor TLambdaExpressionNodeImpl.Create(AParameters: TList; ABody: IAstNode); +begin + inherited Create; + FParameters := AParameters; + FBody := ABody; +end; + +destructor TLambdaExpressionNodeImpl.Destroy; +begin + FParameters.Free; + inherited Destroy; +end; + +function TLambdaExpressionNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitLambdaExpression(Self); +end; + +function TLambdaExpressionNodeImpl.GetBody: IAstNode; +begin + Result := FBody; +end; + +function TLambdaExpressionNodeImpl.GetParameters: TList; +begin + Result := FParameters; +end; + +{ TFunctionCallNodeImpl } + +constructor TFunctionCallNodeImpl.Create(ACallee: IExpressionNode; AArguments: TList); +begin + inherited Create; + FCallee := ACallee; + FArguments := AArguments; +end; + +destructor TFunctionCallNodeImpl.Destroy; +begin + FArguments.Free; + inherited Destroy; +end; + +function TFunctionCallNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitFunctionCall(Self); +end; + +function TFunctionCallNodeImpl.GetArguments: TList; +begin + Result := FArguments; +end; + +function TFunctionCallNodeImpl.GetCallee: IExpressionNode; +begin + Result := FCallee; +end; + +{ TBlockStatementNodeImpl } + +constructor TBlockStatementNodeImpl.Create(AStatements: TList); +begin + inherited Create; + FStatements := AStatements; +end; + +destructor TBlockStatementNodeImpl.Destroy; +begin + FStatements.Free; + inherited Destroy; +end; + +function TBlockStatementNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitBlockStatement(Self); +end; + +function TBlockStatementNodeImpl.GetStatements: TList; +begin + Result := FStatements; +end; + +{ TVariableDeclarationStatementNodeImpl } + +constructor TVariableDeclarationStatementNodeImpl.Create(AIdentifier: IIdentifierNode; AInitializer: IExpressionNode); +begin + inherited Create; + FIdentifier := AIdentifier; + FInitializer := AInitializer; +end; + +function TVariableDeclarationStatementNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitVariableDeclarationStatement(Self); +end; + +function TVariableDeclarationStatementNodeImpl.GetIdentifier: IIdentifierNode; +begin + Result := FIdentifier; +end; + +function TVariableDeclarationStatementNodeImpl.GetInitializer: IExpressionNode; +begin + Result := FInitializer; +end; + +{ TExpressionStatementNodeImpl } + +constructor TExpressionStatementNodeImpl.Create(AExpression: IExpressionNode); +begin + inherited Create; + FExpression := AExpression; +end; + +function TExpressionStatementNodeImpl.Accept(const Visitor: IAstVisitor): IDataValue; +begin + Result := Visitor.VisitExpressionStatement(Self); +end; + +function TExpressionStatementNodeImpl.GetExpression: IExpressionNode; +begin + Result := FExpression; +end; + +{ TExecutionScope } + +constructor TExecutionScope.Create(AParent: TExecutionScope = nil); +begin + inherited Create; + FParent := AParent; + FVariables := TDictionary.Create; +end; + +destructor TExecutionScope.Destroy; +begin + FVariables.Free; + inherited Destroy; +end; + +function TExecutionScope.FindValue(const Name: string; out Value: IDataValue): Boolean; +begin + Result := FVariables.TryGetValue(Name, Value); + if not Result and Assigned(FParent) then + begin + Result := FParent.FindValue(Name, Value); + end; +end; + +procedure TExecutionScope.SetValue(const Name: string; const Value: IDataValue); +begin + // This defines a variable in the current scope. It can shadow a parent variable. + FVariables.AddOrSetValue(Name, Value); +end; + +{ TEvaluatorVisitor } + +constructor TEvaluatorVisitor.Create(AScope: TExecutionScope); +begin + inherited Create; + Assert(Assigned(AScope)); + FScope := AScope; +end; + +function TEvaluatorVisitor.IsTruthy(AValue: IDataValue): Boolean; +begin + // Defines the language's concept of "truthiness". + // For now, only ordinals can be conditions. 0 is false, everything else is true. + if not Assigned(AValue) then + Exit(False); + + case AValue.DataType.Kind of + dkOrdinal: Result := (TDataType.TValue(AValue).AsOrdinal.Value <> 0); + else + Result := False; + end; +end; + +function TEvaluatorVisitor.VisitConstant(const Node: IConstantNode): IDataValue; +begin + Result := Node.Value; +end; + +function TEvaluatorVisitor.VisitIdentifier(const Node: IIdentifierNode): IDataValue; +var + val: IDataValue; +begin + if FScope.FindValue(Node.Name, val) then + Result := val + else + raise EArgumentException.CreateFmt('Identifier not found: "%s"', [Node.Name]); +end; + +function TEvaluatorVisitor.VisitBinaryExpression(const Node: IBinaryExpressionNode): IDataValue; +var + leftValue, rightValue: IDataValue; +begin + leftValue := Node.Left.Accept(Self); + rightValue := Node.Right.Accept(Self); + + if (leftValue.DataType.Kind <> rightValue.DataType.Kind) then + raise ENotSupportedException.CreateFmt( + 'Binary operations on different types (%s and %s) are not supported', + [leftValue.DataType.Name, rightValue.DataType.Name]); + + case leftValue.DataType.Kind of + dkOrdinal: + begin + var leftOrdinal := TDataType.TValue(leftValue).AsOrdinal; + var rightOrdinal := TDataType.TValue(rightValue).AsOrdinal; + var resultVal: Int64; + + case Node.Operator of + boAdd: resultVal := leftOrdinal.Value + rightOrdinal.Value; + boSubtract: resultVal := leftOrdinal.Value - rightOrdinal.Value; + boMultiply: resultVal := leftOrdinal.Value * rightOrdinal.Value; + boDivide: resultVal := leftOrdinal.Value div rightOrdinal.Value; + else + raise ENotSupportedException.Create('Operator not supported for Ordinal type'); + end; + Result := TDataType.Ordinal.CreateValue(resultVal); + end; + dkText: + begin + if (Node.Operator = boAdd) then + begin + var leftText := TDataType.TValue(leftValue).AsText; + var rightText := TDataType.TValue(rightValue).AsText; + Result := TDataType.Text.CreateValue(leftText.Value + rightText.Value); + end + else + raise ENotSupportedException.Create('Operator not supported for Text type'); + end; + else + raise ENotSupportedException.CreateFmt('Binary operation not supported for type %s', [leftValue.DataType.Name]); + end; +end; + +function TEvaluatorVisitor.VisitUnaryExpression(const Node: IUnaryExpressionNode): IDataValue; +var + rightValue: IDataValue; + ordinalVal: IDataOrdinalValue; +begin + rightValue := Node.Right.Accept(Self); + + case Node.Operator of + uoNegate: + begin + if (rightValue.DataType.Kind = dkOrdinal) then + begin + ordinalVal := TDataType.TValue(rightValue).AsOrdinal; + Result := TDataType.Ordinal.CreateValue(-ordinalVal.Value); + end + else + raise ENotSupportedException.CreateFmt('Unary "-" not supported for type %s', [rightValue.DataType.Name]); + end; + uoNot: raise ENotImplemented.Create('Unary "not" operator is not yet implemented'); + else + raise ENotSupportedException.Create('Unary operator not supported'); + end; +end; + +function TEvaluatorVisitor.VisitIfExpression(const Node: IIfExpressionNode): IDataValue; +var + conditionValue: IDataValue; +begin + conditionValue := Node.Condition.Accept(Self); + + if IsTruthy(conditionValue) then + Result := Node.ThenBranch.Accept(Self) + else + Result := Node.ElseBranch.Accept(Self); +end; + +function TEvaluatorVisitor.VisitLambdaExpression(const Node: ILambdaExpressionNode): IDataValue; +var + lambdaBody: IAstNode; + lambdaParams: TList; + closureScope: TExecutionScope; + methodType: IDataMethodType; +begin + closureScope := FScope; + lambdaBody := Node.Body; + lambdaParams := Node.Parameters; + methodType := TDataType.MethodOf(TDataType.Ordinal, TDataType.Ordinal); + + Result := + methodType.CreateValue( + function(const AValue: IDataValue): IDataValue + var + callScope: TExecutionScope; + innerVisitor: IAstVisitor; + begin + callScope := TExecutionScope.Create(closureScope); + try + if (lambdaParams.Count <> 1) then + raise EArgumentException.Create('This simple implementation only supports single-parameter lambdas.'); + + callScope.SetValue(lambdaParams[0].Name, AValue); + + innerVisitor := TEvaluatorVisitor.Create(callScope); + Result := lambdaBody.Accept(innerVisitor); + finally + callScope.Free; + end; + end + ); +end; + +function TEvaluatorVisitor.VisitFunctionCall(const Node: IFunctionCallNode): IDataValue; +var + calleeValue, argValue: IDataValue; + methodProc: TDataMethodProc; + arguments: TList; +begin + calleeValue := Node.Callee.Accept(Self); + + if (calleeValue.DataType.Kind <> dkMethod) then + raise EArgumentException.Create('Expression is not callable.'); + + arguments := Node.Arguments; + if (arguments.Count <> 1) then + raise EArgumentException.Create('This simple implementation only supports single-argument calls.'); + + argValue := arguments[0].Accept(Self); + + methodProc := TDataType.TValue(calleeValue).AsMethod.Value; + Result := methodProc(argValue); +end; + +// --- Statement Visitor Implementations --- + +function TEvaluatorVisitor.VisitBlockStatement(const Node: IBlockStatementNode): IDataValue; +var + statement: IStatementNode; +begin + // Execute all statements in the block sequentially. + for statement in Node.Statements do + begin + statement.Accept(Self); // The result is ignored. + end; + Result := TDataType.Void.Value; +end; + +function TEvaluatorVisitor.VisitVariableDeclarationStatement(const Node: IVariableDeclarationStatementNode): IDataValue; +var + varName: string; + initValue: IDataValue; +begin + varName := Node.Identifier.Name; + + // Evaluate the initializer expression, if it exists. + if Assigned(Node.Initializer) then + initValue := Node.Initializer.Accept(Self) + else + initValue := TDataType.Void.Value; // Default value if no initializer is provided. + + // Define the variable in the current scope. + FScope.SetValue(varName, initValue); + + Result := TDataType.Void.Value; +end; + +function TEvaluatorVisitor.VisitExpressionStatement(const Node: IExpressionStatementNode): IDataValue; +begin + // Evaluate the expression for its side-effects and discard the result. + Node.Expression.Accept(Self); + Result := TDataType.Void.Value; +end; + +end. diff --git a/Src/AST/TestAst.pas b/Src/AST/TestAst.pas new file mode 100644 index 0000000..97a6100 --- /dev/null +++ b/Src/AST/TestAst.pas @@ -0,0 +1,102 @@ +unit TestAst; + +interface + +uses + Myc.Ast, + DUnitX.TestFramework; + +type + [TestFixture] + TTestAst = class + private + FScope: TExecutionScope; + FVisitor: IAstVisitor; + public + [Setup] + procedure Setup; + [TearDown] + procedure TearDown; + + [Test] + procedure TestHelloWorld; + [Test] + procedure TestVariableDeclarationAndUsage; + end; + +implementation + +uses + System.Generics.Collections, + Myc.Data.Types; + +{ TTestAst } + +procedure TTestAst.Setup; +begin + // Create scope and visitor once for each test + FScope := TExecutionScope.Create; + FVisitor := TEvaluatorVisitor.Create(FScope); +end; + +procedure TTestAst.TearDown; +begin + FVisitor := nil; + FScope.Free; +end; + +procedure TTestAst.TestHelloWorld; +var + ast: IExpressionNode; + resultValue: IDataValue; + textValue: IDataTextValue; +begin + // 1. Build the AST for the expression: "hello " + "world" + ast := TAst.BinaryExpr(TAst.Constant(TDataType.Text.CreateValue('hello ')), boAdd, TAst.Constant(TDataType.Text.CreateValue('world'))); + + // 2. Execute (evaluate) the AST + resultValue := ast.Accept(FVisitor); + + // 3. Assert the result + Assert.IsNotNull(resultValue, 'The evaluation must return a value.'); + Assert.AreEqual(dkText, resultValue.DataType.Kind, 'The result must be of text kind.'); + + textValue := TDataType.TValue(resultValue).AsText; + Assert.AreEqual('hello world', textValue.Value, 'The string concatenation result is wrong.'); +end; + +procedure TTestAst.TestVariableDeclarationAndUsage; +var + varDeclStmt: IStatementNode; + blockStmt: IStatementNode; + identifierExpr: IExpressionNode; + resultValue: IDataValue; + ordinalValue: IDataOrdinalValue; +begin + // 1. Build the AST for the statement: let myVar = 42; + varDeclStmt := TAst.VarDecl(TAst.Identifier('myVar'), TAst.Constant(TDataType.Ordinal.CreateValue(42))); + + // It's good practice to wrap statements in a block + blockStmt := TAst.Block([varDeclStmt]); + + // 2. Build the AST for the expression that uses the variable: myVar + identifierExpr := TAst.Identifier('myVar'); + + // 3. Execute the statement to declare and assign the variable + blockStmt.Accept(FVisitor); + + // 4. Execute the expression to retrieve the variable's value + resultValue := identifierExpr.Accept(FVisitor); + + // 5. Assert the result + Assert.IsNotNull(resultValue, 'The evaluation must return a value.'); + Assert.AreEqual(dkOrdinal, resultValue.DataType.Kind, 'The result must be of ordinal kind.'); + + ordinalValue := TDataType.TValue(resultValue).AsOrdinal; + Assert.AreEqual(Int64(42), ordinalValue.Value, 'The variable value is wrong.'); +end; + +initialization + TDUnitX.RegisterTestFixture(TTestAst); + +end. diff --git a/Test/MycTests.dpr b/Test/MycTests.dpr index 17b0903..686af59 100644 --- a/Test/MycTests.dpr +++ b/Test/MycTests.dpr @@ -33,7 +33,9 @@ uses TestDataTypes in '..\Src\Data\TestDataTypes.pas', Myc.Data.Types.Method in '..\Src\Data\Myc.Data.Types.Method.pas', Myc.Data.Types.Void in '..\Src\Data\Myc.Data.Types.Void.pas', - Myc.Data.Types.Decimal in '..\Src\Data\Myc.Data.Types.Decimal.pas'; + Myc.Data.Types.Decimal in '..\Src\Data\Myc.Data.Types.Decimal.pas', + Myc.Ast in '..\Src\AST\Myc.Ast.pas', + TestAst in '..\Src\AST\TestAst.pas'; { keep comment here to protect the following conditional from being removed by the IDE when adding a unit } {$IFNDEF TESTINSIGHT} diff --git a/Test/MycTests.dproj b/Test/MycTests.dproj index fa420c4..221d7e1 100644 --- a/Test/MycTests.dproj +++ b/Test/MycTests.dproj @@ -58,7 +58,7 @@ true $(BDS)\bin\delphi_PROJECTICON.ico $(BDS)\bin\delphi_PROJECTICNS.icns - $(DUnitX);T:\Myc\Src;T:\Myc\Src\Data;$(DCC_UnitSearchPath) + $(DUnitX);T:\Myc\Src;T:\Myc\Src\Data;T:\Myc\Src\Ast;$(DCC_UnitSearchPath) MycTests 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= @@ -135,6 +135,8 @@ $(PreBuildEvent)]]> + + Base