Pipes refactoring, Fixes in type checker

This commit is contained in:
Michael Schimmel
2026-02-13 23:34:39 +01:00
parent 19cdb2f004
commit dc46a2dd2d
10 changed files with 341 additions and 285 deletions
File diff suppressed because one or more lines are too long
+17
View File
@@ -8,6 +8,7 @@ object Form1: TForm1
FormFactor.Height = 480 FormFactor.Height = 480
FormFactor.Devices = [Desktop] FormFactor.Devices = [Desktop]
OnCreate = FormCreate OnCreate = FormCreate
OnDestroy = FormDestroy
DesignerMasterStyle = 0 DesignerMasterStyle = 0
object Panel1: TPanel object Panel1: TPanel
Align = MostLeft Align = MostLeft
@@ -286,4 +287,20 @@ object Form1: TForm1
Text = 'Save' Text = 'Save'
OnClick = SaveScriptButtonClick OnClick = SaveScriptButtonClick
end end
object Timer1: TTimer
Interval = 250
OnTimer = Timer1Timer
Left = 992
Top = 216
end
object WatchLabel: TLabel
Anchors = [akRight, akBottom]
Position.X = 1097.000000000000000000
Position.Y = 856.000000000000000000
Size.Width = 289.000000000000000000
Size.Height = 17.000000000000000000
Size.PlatformDefault = False
Text = 'watcher: ..............'
TabOrder = 9
end
end end
+62 -22
View File
@@ -38,6 +38,7 @@ uses
Myc.Ast, Myc.Ast,
Myc.Ast.Visitor, Myc.Ast.Visitor,
Myc.Data.Decimal, Myc.Data.Decimal,
Myc.Data.Stream,
Myc.Ast.Script, Myc.Ast.Script,
Myc.Ast.Types, Myc.Ast.Types,
Myc.Ast.Environment, Myc.Ast.Environment,
@@ -92,6 +93,9 @@ type
CompilerStageBox: TComboBox; CompilerStageBox: TComboBox;
SaveTestBtn: TButton; SaveTestBtn: TButton;
SaveScriptButton: TButton; SaveScriptButton: TButton;
Timer1: TTimer;
WatchLabel: TLabel;
procedure FormDestroy(Sender: TObject);
procedure InnerLambdaButtonClick(Sender: TObject); procedure InnerLambdaButtonClick(Sender: TObject);
procedure ClearButtonClick(Sender: TObject); procedure ClearButtonClick(Sender: TObject);
procedure CompilerStageBoxChange(Sender: TObject); procedure CompilerStageBoxChange(Sender: TObject);
@@ -118,14 +122,17 @@ type
procedure LoadUserLibButtonClick(Sender: TObject); procedure LoadUserLibButtonClick(Sender: TObject);
procedure RTLListViewChange(Sender: TObject); procedure RTLListViewChange(Sender: TObject);
procedure SaveScriptButtonClick(Sender: TObject); procedure SaveScriptButtonClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private private
FCurrUnboundAst: IAstNode; FCurrUnboundAst: IAstNode;
FCurrCompiled: ILambdaExpressionNode; FCurrLambda: ILambdaExpressionNode;
FCurrExec: TCompiledFunction; FCurrCompiled: TCompiledFunction;
FCurrExec: TDataValue;
FEnvironment: TAstEnvironment; FEnvironment: TAstEnvironment;
FAstEditor: TAstEditor; // Die Komponente (Facade) FAstEditor: TAstEditor; // Die Komponente (Facade)
FScriptUpdate: Boolean; FScriptUpdate: Boolean;
FTriggerTest: TAstEnvironment; FTriggerTest: TAstEnvironment;
FSim: IFinanceSimulator;
procedure AstEditorMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single); procedure AstEditorMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Single);
@@ -152,12 +159,18 @@ uses
Myc.Trade.Broker, Myc.Trade.Broker,
System.Diagnostics, // For TStopwatch System.Diagnostics, // For TStopwatch
System.TimeSpan, System.TimeSpan,
Myc.Data.Keyword,
Myc.Ast.Json, // For TAstJson serialization Myc.Ast.Json, // For TAstJson serialization
System.IOUtils, // For TFile System.IOUtils, // For TFile
Myc.Ast.Dumper; // Needed for DumpButtonClick Myc.Ast.Dumper; // Needed for DumpButtonClick
{$R *.fmx} {$R *.fmx}
procedure TForm1.FormDestroy(Sender: TObject);
begin
FSim := nil;
end;
procedure TForm1.FormCreate(Sender: TObject); procedure TForm1.FormCreate(Sender: TObject);
begin begin
// 1. Initialize Environment // 1. Initialize Environment
@@ -262,6 +275,28 @@ begin
end end
); );
Scope.Define(
'watch',
function(const Args: TArray<TDataValue>): TDataValue
var
str: TStringBuilder;
begin
str := TStringBuilder.Create;
try
for var i := 0 to High(Args) do
begin
if Args[i].Kind = vkText then
str.Append(Args[i].AsText)
else
str.Append(Args[i].ToString);
end;
WatchLabel.Text := str.ToString;
finally
str.Free;
end;
end
);
Scope.Define( Scope.Define(
'timestamp', 'timestamp',
function(const Args: TArray<TDataValue>): TDataValue function(const Args: TArray<TDataValue>): TDataValue
@@ -291,13 +326,11 @@ begin
CompilerStageBox.BringToFront; CompilerStageBox.BringToFront;
ClearButtonClick(Self); ClearButtonClick(Self);
FSim := TFinanceSimulator.Create;
FSim.CreateOHLCSeries(FEnvironment.RootScope, 'dax');
if FileExists(SrcName) then if FileExists(SrcName) then
ScriptMemo.Lines.LoadFromFile(SrcName); ScriptMemo.Lines.LoadFromFile(SrcName);
// Memo1.Lines.Add(TAstSchema.GenerateFullSystemPrompt(FEnvironment.RootScope));
// "Dieses JSON-Objekt betest du in das Feld response_schema (Gemini) oder json_schema (OpenAI) ein."
// Memo1.Lines.Add(TAstSchema.GenerateFullSchema.ToJSON);
end; end;
procedure TForm1.ShowVizualization; procedure TForm1.ShowVizualization;
@@ -313,7 +346,7 @@ end;
function TForm1.ExecuteAst(const ANode: IAstNode): TDataValue; function TForm1.ExecuteAst(const ANode: IAstNode): TDataValue;
begin begin
FCurrUnboundAst := ANode; FCurrUnboundAst := ANode;
FCurrExec := Default(TCompiledFunction); FCurrCompiled := Default(TCompiledFunction);
if DebugBox.IsChecked then if DebugBox.IsChecked then
FEnvironment.SetDebugMode(Memo1.Lines, ShowScopeBox.IsChecked) FEnvironment.SetDebugMode(Memo1.Lines, ShowScopeBox.IsChecked)
@@ -323,17 +356,18 @@ begin
try try
// Use Compile directly. Errors will be raised as ECompilationFailed. // Use Compile directly. Errors will be raised as ECompilationFailed.
// We catch them to print to log, but allow flow to continue so UI can be updated. // We catch them to print to log, but allow flow to continue so UI can be updated.
FCurrCompiled := FEnvironment.Compile(ANode); FCurrLambda := FEnvironment.Compile(ANode);
FCurrExec := FEnvironment.Link(FCurrCompiled); FCurrCompiled := FEnvironment.Link(FCurrLambda);
if Assigned(FCurrExec.Func) then if Assigned(FCurrCompiled.Func) then
begin begin
Memo1.Lines.Add( Memo1.Lines.Add(
Format('Compiled. Signature: %s, IsPure=%s', [FCurrExec.StaticType.ToString, BoolToStr(FCurrExec.IsPure, true)]) Format('Compiled. Signature: %s, IsPure=%s', [FCurrCompiled.StaticType.ToString, BoolToStr(FCurrCompiled.IsPure, true)])
); );
end; end;
Result := FCurrExec.Func([]); FCurrExec := FCurrCompiled.Func([]);
Result := FCurrExec;
except except
on E: ECompilationFailed do on E: ECompilationFailed do
@@ -354,7 +388,7 @@ begin
end; end;
on E: Exception do on E: Exception do
begin begin
FCurrExec.Func := nil; FCurrCompiled.Func := nil;
Memo1.Lines.Add('--- RUNTIME ERROR ---'); Memo1.Lines.Add('--- RUNTIME ERROR ---');
Memo1.Lines.Add(E.ClassName + ': ' + E.Message); Memo1.Lines.Add(E.ClassName + ': ' + E.Message);
Result := TDataValue.Void; Result := TDataValue.Void;
@@ -617,8 +651,8 @@ begin
Memo1.Lines.Add('--- Naive recursive fib with AST---'); Memo1.Lines.Add('--- Naive recursive fib with AST---');
FCurrCompiled := TestEnv.Compile(TAst.FunctionCall(TAst.Identifier('fib'), [TAst.Constant(25)])); FCurrLambda := TestEnv.Compile(TAst.FunctionCall(TAst.Identifier('fib'), [TAst.Constant(25)]));
var fibExec := TestEnv.Link(FCurrCompiled).Func; var fibExec := TestEnv.Link(FCurrLambda).Func;
sw := TStopwatch.StartNew; sw := TStopwatch.StartNew;
result := fibExec([]); result := fibExec([]);
@@ -781,8 +815,8 @@ begin
// Die Pipeline nutzt den Environment-Helper (Managed Record) // Die Pipeline nutzt den Environment-Helper (Managed Record)
// Compile akzeptiert IAstNode und erzeugt intern ein Lambda // Compile akzeptiert IAstNode und erzeugt intern ein Lambda
FCurrCompiled := FEnvironment.Compile(FCurrUnboundAst); FCurrLambda := FEnvironment.Compile(FCurrUnboundAst);
FCurrExec := FEnvironment.Link(FCurrCompiled); FCurrCompiled := FEnvironment.Link(FCurrLambda);
Memo1.Lines.Clear; Memo1.Lines.Clear;
Memo1.Lines.Add('AST deserialized and bound successfully from JSON.'); Memo1.Lines.Add('AST deserialized and bound successfully from JSON.');
@@ -1196,8 +1230,8 @@ end;
procedure TForm1.Test1ButtonClick(Sender: TObject); procedure TForm1.Test1ButtonClick(Sender: TObject);
begin begin
FCurrUnboundAst := TAstScript.Parse('(do (print txt))'); FCurrUnboundAst := TAstScript.Parse('(do (print txt))');
FCurrCompiled := FEnvironment.Compile(FCurrUnboundAst, [TAst.Identifier('txt')]); FCurrLambda := FEnvironment.Compile(FCurrUnboundAst, [TAst.Identifier('txt')]);
var fn := FEnvironment.Link(FCurrCompiled).Func; var fn := FEnvironment.Link(FCurrLambda).Func;
fn(['Hello World']); fn(['Hello World']);
FEnvironment.RootScope.Define('fn', fn); FEnvironment.RootScope.Define('fn', fn);
UpdateScript; UpdateScript;
@@ -1280,14 +1314,14 @@ begin
Memo1.Lines.Clear; Memo1.Lines.Clear;
Memo1.Lines.Add('--- AST Dump ---'); Memo1.Lines.Add('--- AST Dump ---');
if FCurrCompiled = nil then if FCurrLambda = nil then
begin begin
Memo1.Lines.Add('No compiled AST available.'); Memo1.Lines.Add('No compiled AST available.');
exit; exit;
end; end;
// Dump the raw unbound AST first // Dump the raw unbound AST first
TAstDumper.Dump(FCurrCompiled, Memo1.Lines); TAstDumper.Dump(FCurrLambda, Memo1.Lines);
end; end;
procedure TForm1.SaveScriptButtonClick(Sender: TObject); procedure TForm1.SaveScriptButtonClick(Sender: TObject);
@@ -1295,6 +1329,12 @@ begin
ScriptMemo.Lines.SaveToFile(SrcName); ScriptMemo.Lines.SaveToFile(SrcName);
end; end;
procedure TForm1.Timer1Timer(Sender: TObject);
begin
if Assigned(FSim) then
FSim.AddRandomCandle(FEnvironment.RootScope, 'dax');
end;
initialization initialization
TAst.RegisterLibrary(RegisterBroker); TAst.RegisterLibrary(RegisterBroker);
+27 -134
View File
@@ -1,139 +1,32 @@
(do (do
;; =========================================================================== (def med (pipe [[dax [:Close :Open]]]
;; 1. DEFINITIONEN (fn [cs os]
;; ===========================================================================
;; ---------------------------------------------------------------------------
;; Factory: create-wma
;; Berechnet WMA. Wartet, bis das Fenster voll ist.
;; Phase 1: Warten (< len)
;; Phase 2: Init (= len) -> Loop berechnung
;; Phase 3: Slide (> len) -> O(1) berechnung
;; ---------------------------------------------------------------------------
(def create-wma
(fn [len]
(do
(def prev-w-sum 0.0)
(def prev-s-sum 0.0)
(def weight-div (/ (* len (+ len 1)) 2))
(fn [src]
(do
(def cnt (count src))
;; PHASE 1: Nicht genug Daten
(if (< cnt len)
NaN
;; Genug Daten vorhanden
(if (= cnt len)
;; PHASE 2: Initialisierung (Das Fenster ist gerade voll geworden)
;; Wir müssen einmalig die Basis-Summen berechnen.
(do
(def init-w 0.0)
(def init-s 0.0)
;; Loop über die Elemente 0 bis len-1
((fn [i]
(if (< i len)
(do
(def val (get src i))
;; Gewichtung: Index 0 ist das neueste -> Gewicht 'len'
(assign init-w (+ init-w (* (- len i) val)))
(assign init-s (+ init-s val))
(recur (+ i 1)))
)) 0)
;; State speichern
(assign prev-w-sum init-w)
(assign prev-s-sum init-s)
(/ init-w weight-div))
;; PHASE 3: Sliding Window (O(1))
;; Wir haben bereits einen gültigen State aus dem vorherigen Schritt.
(do
(def val-new (get src 0)) ; Neuster Wert
(def val-out (get src len)) ; Wert der rausfällt
;; Update Formeln
(def w-sum (+ prev-w-sum (- (* len val-new) prev-s-sum)))
(def s-sum (+ (- prev-s-sum val-out) val-new))
;; State speichern
(assign prev-w-sum w-sum)
(assign prev-s-sum s-sum)
(/ w-sum weight-div))
)
)
))
)))
;; --- HMA Factory ---
(def create-hma
(fn [len]
(do
(def half-len (round (/ len 2)))
(def sqrt-len (round (sqrt len)))
(def wma-half (create-wma half-len))
(def wma-full (create-wma len))
(def wma-smooth (create-wma sqrt-len))
(def diff-series (new-series [[:val :Float]]))
(fn [src]
(do
(def v1 (wma-half src))
(def v2 (wma-full src))
;; WICHTIG: Wir dürfen erst weitermachen, wenn BEIDE WMAs gültig sind.
;; Da v2 das längere Fenster (len) braucht, diktiert es den Takt.
(def diff (- (* 2 v1) v2))
(if (not (is-NaN diff))
(add-item diff-series {:val diff}))
;; Glättung auf dem Resultat
(wma-smooth (.val diff-series))
))
)))
;; --- Repeat Macro ---
(defmacro repeat [n body]
`((fn [cnt]
(if (> cnt 0)
(do
~body
(recur (- cnt 1)))))
~n))
;; ===========================================================================
;; 2. SIMULATION
;; ===========================================================================
(def hma-length 14)
(def simulation-steps 500)
(def hma-calc (create-hma hma-length))
(def prices (new-series [[:close :Float]]))
(def current-price 100.0)
(print "Step | Price | HMA(14)")
(print "-----|----------|----------")
(repeat simulation-steps
(do (do
;; Random Walk (def o (get os 0))
(assign current-price (+ current-price (- (random) 0.5))) (def c (get cs 0))
(add-item prices {:close current-price}) (def mm (* 0.5 (+ o c)))
{:m mm}
;; Berechnung
;; Nutzung von Member Access (.close), da es eine Record-Series ist
(def hma-val (hma-calc (.close prices)))
;; Ausgabe
(if (not (is-NaN hma-val))
(print (count prices) " |" current-price " | " hma-val)
)
) )
) )
))
(def v 0)
(def xxx (pipe [[dax [:High :Low]] [med [:m]]]
(fn [hs ls ms]
(do
(def h (round (get hs 0)))
(def l (round (get ls 0)))
(def m (get ms 0))
(watch "h=" h " l=" l " m=" m)
(assign v (+ v h))
{:ah v}
)
)
))
(pipe [[xxx [:ah]]]
(fn [a] (do (print (get a 0)) ...))
)
) )
+40 -17
View File
@@ -629,16 +629,36 @@ begin
if Length(sig.ParamTypes) <> Length(argTypes) then if Length(sig.ParamTypes) <> Length(argTypes) then
continue; continue;
match := True; match := True;
var sigIsStatic := sig.ReturnType.Kind <> stUnknown;
for j := 0 to High(argTypes) do for j := 0 to High(argTypes) do
begin
if not TTypeRules.CanAssign(sig.ParamTypes[j], argTypes[j]) then if not TTypeRules.CanAssign(sig.ParamTypes[j], argTypes[j]) then
begin begin
match := False; match := False;
break; break;
end; end;
if sig.ParamTypes[j].Kind = stUnknown then
sigIsStatic := false;
end;
if match then if match then
begin begin
bestSig := sig; if not sigIsStatic then
break; begin
if bestSig <> nil then
if Assigned(FLog) then
FLog.AddError(
Format('RTL contains ambiguous signatures for method call on %s', [calleeType.ToString]),
Node
);
bestSig := sig;
end
else
begin
bestSig := sig;
break;
end;
end; end;
end; end;
@@ -763,19 +783,21 @@ begin
baseType := PrepareBaseType(newBase, isOpt); baseType := PrepareBaseType(newBase, isOpt);
resType := TTypes.Unknown; resType := TTypes.Unknown;
if (baseType.Kind = stRecord) or (baseType.Kind = stRecordSeries) then case baseType.Kind of
begin stRecord, stRecordSeries:
idx := baseType.AsRecord.Definition.IndexOf(M.Member.Value);
if idx >= 0 then
begin begin
var fieldType := TTypes.FromScalarKind(baseType.AsRecord.Definition[idx]); idx := baseType.AsRecord.Definition.IndexOf(M.Member.Value);
if baseType.Kind = stRecordSeries then if idx >= 0 then
resType := TTypes.CreateSeries(fieldType) begin
else var fieldType := TTypes.FromScalarKind(baseType.AsRecord.Definition[idx]);
resType := fieldType; if baseType.Kind = stRecordSeries then
end resType := TTypes.CreateSeries(fieldType)
else if Assigned(FLog) then else
FLog.AddError('Member not found', Node); resType := fieldType;
end
else if Assigned(FLog) then
FLog.AddError('Member not found', Node);
end;
end; end;
resType := ApplyOptionality(resType, isOpt); resType := ApplyOptionality(resType, isOpt);
@@ -1081,7 +1103,8 @@ begin
else else
inferredType := TTypes.Ordinal; inferredType := TTypes.Ordinal;
end; end;
paramTypes.Add(inferredType);
paramTypes.Add(TTypes.CreateSeries(inferredType));
end; end;
// Reconstruct the typed Input Tuple [StreamIdent, [Selectors]] // Reconstruct the typed Input Tuple [StreamIdent, [Selectors]]
@@ -1150,7 +1173,7 @@ begin
end end
else else
begin begin
if (lambdaRetType.Kind <> stUnknown) and (lambdaRetType.Kind <> stVoid) then if lambdaRetType.Kind <> stVoid then
begin begin
if Assigned(FLog) then if Assigned(FLog) then
FLog.AddError( FLog.AddError(
@@ -1158,7 +1181,7 @@ begin
lambda lambda
); );
end; end;
pipeType := TTypes.Unknown; pipeType := TTypes.Void;
end; end;
// Reconstruct the Pipe Node with typed Inputs tuple and typed Lambda // Reconstruct the Pipe Node with typed Inputs tuple and typed Lambda
+15 -9
View File
@@ -411,7 +411,7 @@ begin
vkRecordSeries: Result := base.AsRecordSeries.Fields[N.Member.Value]; vkRecordSeries: Result := base.AsRecordSeries.Fields[N.Member.Value];
vkScalarRecord: Result := base.AsScalarRecord.Fields[N.Member.Value]; vkScalarRecord: Result := base.AsScalarRecord.Fields[N.Member.Value];
vkRecord: Result := base.AsRecord.Fields[N.Member.Value]; vkRecord: Result := base.AsRecord.Fields[N.Member.Value];
vkStream: Result := base.AsStream.Series.Fields[N.Member.Value]; // vkStream: Result := base.AsStream.Def.Fields[N.Member.Value];
else else
raise EEvaluatorException.Create('Member error'); raise EEvaluatorException.Create('Member error');
end; end;
@@ -549,7 +549,6 @@ var
sources: TArray<IStream>; sources: TArray<IStream>;
config: TPipeConfig; config: TPipeConfig;
inputVal: TDataValue; inputVal: TDataValue;
sourceSeries: IScalarRecordSeries;
lambdaFunc: TDataValue.TFunc; lambdaFunc: TDataValue.TFunc;
inputsElements: TArray<IAstNode>; inputsElements: TArray<IAstNode>;
@@ -581,8 +580,7 @@ begin
raise EEvaluatorException.Create(Format('Variable "%s" is not a Stream.', [sourceId.Name])); raise EEvaluatorException.Create(Format('Variable "%s" is not a Stream.', [sourceId.Name]));
sources[i] := inputVal.AsStream; sources[i] := inputVal.AsStream;
sourceSeries := sources[i].Series; var srcDef := sources[i].Def;
var srcDef := sourceSeries.Def;
// Build Selectors Config // Build Selectors Config
SetLength(config[i], Length(selectorsElements)); SetLength(config[i], Length(selectorsElements));
@@ -601,11 +599,18 @@ begin
// Compile the transformation function // Compile the transformation function
lambdaFunc := Visit(N.Transformation).AsMethod(); lambdaFunc := Visit(N.Transformation).AsMethod();
// Allow both stRecord and stRecordSeries (as TypeChecker correctly assigns stRecordSeries for Pipe nodes) var outputDef: IScalarRecordDefinition;
if (N.StaticType.Kind <> stRecordSeries) and (N.StaticType.Kind <> stRecord) then
raise EEvaluatorException.Create('Pipe requires Type Checking to determine output structure (RecordDefinition).');
var outputDef := N.StaticType.AsRecord.Definition; case N.StaticType.Kind of
stVoid:
// This is an endpoint. The lambda returns nothing. So we produce "nothing".
outputDef := TScalarRecord.TRegistry.Empty;
stRecord, stRecordSeries:
// Allow both stRecord and stRecordSeries (as TypeChecker correctly assigns stRecordSeries for Pipe nodes)
outputDef := N.StaticType.AsRecord.Definition;
else
raise EEvaluatorException.Create('Pipe requires Type Checking to determine output structure (RecordDefinition).');
end;
var pipeAdapter: TPipeStream.TPipeLambda := var pipeAdapter: TPipeStream.TPipeLambda :=
function(const S: array of ISeries; out R: array of TScalar.TValue): Boolean function(const S: array of ISeries; out R: array of TScalar.TValue): Boolean
@@ -630,7 +635,8 @@ begin
Result := True; Result := True;
end; end;
Result := TPipeStream.Create(config, outputDef, sources, pipeAdapter); //TODO Lookback not evaluated in script!
Result := TPipeStream.Create(config, outputDef, sources, pipeAdapter, 1000);
end; end;
end. end.
+4
View File
@@ -86,12 +86,14 @@ type
class var class var
FLock: TSpinLock; FLock: TSpinLock;
FRegistry: TDictionary<TArray<Integer>, IKeywordMapping<T>>; FRegistry: TDictionary<TArray<Integer>, IKeywordMapping<T>>;
FEmpty: IKeywordMapping<T>;
class constructor Create; class constructor Create;
class destructor Destroy; class destructor Destroy;
public public
// Creates or gets the cached keyword mapping. // Creates or gets the cached keyword mapping.
class function Intern(const AFields: TArray<TPair<IKeyword, T>>): IKeywordMapping<T>; static; class function Intern(const AFields: TArray<TPair<IKeyword, T>>): IKeywordMapping<T>; static;
class property Empty: IKeywordMapping<T> read FEmpty;
end; end;
TGenericRecord<T> = class(TInterfacedObject, IKeywordMapping<T>) TGenericRecord<T> = class(TInterfacedObject, IKeywordMapping<T>)
@@ -270,6 +272,8 @@ class constructor TKeywordMappingRegistry<T>.Create;
begin begin
FRegistry := TDictionary<TArray<Integer>, IKeywordMapping<T>>.Create(TEqualityComparer<TArray<Integer>>.Default); FRegistry := TDictionary<TArray<Integer>, IKeywordMapping<T>>.Create(TEqualityComparer<TArray<Integer>>.Default);
FLock := TSpinLock.Create(False); FLock := TSpinLock.Create(False);
FEmpty := Intern([]);
end; end;
class destructor TKeywordMappingRegistry<T>.Destroy; class destructor TKeywordMappingRegistry<T>.Destroy;
+10
View File
@@ -92,6 +92,7 @@ type
class operator Implicit(const A: TScalar): Int64; overload; inline; class operator Implicit(const A: TScalar): Int64; overload; inline;
class operator Implicit(const A: TScalar): Boolean; overload; inline; class operator Implicit(const A: TScalar): Boolean; overload; inline;
class operator Implicit(const A: TScalar): TDateTime; overload; inline; class operator Implicit(const A: TScalar): TDateTime; overload; inline;
class operator Implicit(const A: TScalar): IKeyword; overload; inline;
class function StringToKind(const AName: string): TKind; static; class function StringToKind(const AName: string): TKind; static;
function ToString: String; function ToString: String;
@@ -750,6 +751,15 @@ begin
raise EArgumentException.Create('Operator "xor" requires Ordinal or Boolean arguments.'); raise EArgumentException.Create('Operator "xor" requires Ordinal or Boolean arguments.');
end; end;
class operator TScalar.Implicit(const A: TScalar): IKeyword;
begin
case A.Kind of
TKind.Keyword: Result := TKeywordRegistry.GetKeyword(A.Value.AsInt64);
else
raise EInvalidCast.CreateFmt('Cannot implicitly convert %s to Keyword', [A.Kind.ToString]);
end;
end;
class operator TScalar.LeftShift(const A, B: TScalar): TScalar; class operator TScalar.LeftShift(const A, B: TScalar): TScalar;
begin begin
if (A.Kind <> TKind.Ordinal) or (B.Kind <> TKind.Ordinal) then if (A.Kind <> TKind.Ordinal) or (B.Kind <> TKind.Ordinal) then
+164 -100
View File
@@ -14,128 +14,143 @@ uses
type type
TSignalKind = (skData, skHeartbeat); TSignalKind = (skData, skHeartbeat);
// TStreamSignal represents the data packet traveling through the graph.
// It carries the CycleID for synchronization and the actual data payload.
TStreamSignal = record TStreamSignal = record
private strict private
FCycleID: Int64; FCycleID: Int64;
FKind: TSignalKind; FKind: TSignalKind;
FData: TArray<TScalar.TValue>;
public public
constructor Create(AKind: TSignalKind; ACycleID: Int64); constructor Create(AKind: TSignalKind; ACycleID: Int64; const AData: TArray<TScalar.TValue> = nil);
function ToString: string; function ToString: string;
property CycleID: Int64 read FCycleID; property CycleID: Int64 read FCycleID;
property Kind: TSignalKind read FKind; property Kind: TSignalKind read FKind;
property Data: TArray<TScalar.TValue> read FData;
end; end;
IStreamObserver = interface TSignalProc = reference to procedure(const Signal: TStreamSignal);
procedure OnSignal(const Signal: TStreamSignal);
end;
TSubscriptionTag = Pointer; TSubscriptionTag = Pointer;
// IStream is now stateless. It defines WHAT is produced (Def), but not
// the history. History must be captured by an observer (Accumulator).
IStream = interface IStream = interface
{$region 'private'} {$region 'private'}
function GetSeries: IScalarRecordSeries; function GetDef: IScalarRecordDefinition;
{$endregion} {$endregion}
function Subscribe(const Observer: IStreamObserver): TSubscriptionTag; function Subscribe(const Observer: TSignalProc): TSubscriptionTag;
procedure Unsubscribe(Tag: TSubscriptionTag); procedure Unsubscribe(Tag: TSubscriptionTag);
// Returns the Series of the records this stream produces. Streams are scalar record series. Always! { Returns the definition (schema) of the records this stream produces. }
property Series: IScalarRecordSeries read GetSeries; property Def: IScalarRecordDefinition read GetDef;
end; end;
// ========================================================================= // Implements broadcasting logic. No internal storage of past values.
// BASE STREAM
// Implements storage (Series) and broadcasting (Observers).
// Does not define WHEN data is emitted (Policy).
// =========================================================================
TCustomDataStream = class(TInterfacedObject, IStream) TCustomDataStream = class(TInterfacedObject, IStream)
strict private strict private
FSeries: IWriteableScalarRecordSeries; FDef: IScalarRecordDefinition;
FObservers: TMycNotifyList<IStreamObserver>; FObservers: TMycNotifyList<TSignalProc>;
function GetSeries: IScalarRecordSeries; function GetDef: IScalarRecordDefinition;
protected protected
// Derived classes call this to write data and notify listeners. { Broadcasts data to all subscribers. Stateless. }
// ACycleID: The cycle this data belongs to.
procedure Emit(const Value: array of TScalar.TValue; ACycleID: Int64); procedure Emit(const Value: array of TScalar.TValue; ACycleID: Int64);
public public
constructor Create(const ADef: IScalarRecordDefinition); constructor Create(const ADef: IScalarRecordDefinition);
destructor Destroy; override; destructor Destroy; override;
// IStream function Subscribe(const Observer: TSignalProc): TSubscriptionTag;
function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
procedure Unsubscribe(Tag: TSubscriptionTag); procedure Unsubscribe(Tag: TSubscriptionTag);
property Series: IScalarRecordSeries read GetSeries; property Def: IScalarRecordDefinition read GetDef;
end; end;
// ========================================================================= // The entry point for external data. Generates the global CycleID (Clock).
// ROOT STREAM (SOURCE)
// Acts as the clock source. Generates new CycleIDs.
// =========================================================================
TRootStream = class(TCustomDataStream) TRootStream = class(TCustomDataStream)
private strict private
FLastCycleID: Int64; FLastCycleID: Int64;
public public
constructor Create(const ADef: IScalarRecordDefinition); constructor Create(const ADef: IScalarRecordDefinition);
// Public API to inject data from Delphi { Injects a new record into the stream system. }
procedure Push(const RowData: TArray<TScalar.TValue>); procedure Push(const RowData: array of TScalar.TValue);
end; end;
// ========================================================================= // Synchronizes multiple inputs via barrier and executes a transformation.
// PIPE STREAM (NODE) TPipeStream = class;
// Reacts to upstream signals using barrier synchronization.
// =========================================================================
TPipeStream = class; // Forward
TPipeConfig = TArray<TArray<TScalarRecordField>>; { TPipeSource acts as a selective accumulator for a single field of a stream. }
TPipeSource = class(TInterfacedObject, ISeries)
TPipeSource = class(TContainedObject, IStreamObserver) public
private type
TSignalProc = reference to procedure(const Signal: TStreamSignal);
strict private
FSource: IStream; FSource: IStream;
FTag: TSubscriptionTag; FTag: TSubscriptionTag;
FOnSignal: TSignalProc;
FFieldIndex: Integer;
FData: IWriteableSeries;
FLastSeenCycle: Int64; FLastSeenCycle: Int64;
procedure OnSignal(const Signal: TStreamSignal); FLookback: Int64;
{ ISeries implementation }
function GetCount: Int64;
function GetItems(Idx: Integer): TScalar;
function GetTotalCount: Int64;
private
procedure HandleSignal(const Signal: TStreamSignal);
protected
procedure Detach;
public public
constructor Create(AOwner: TPipeStream; ASource: IStream); constructor Create(const ASource: IStream; const AField: IKeyword; ALookback: Int64; const AOnSignal: TSignalProc);
destructor Destroy; override; destructor Destroy; override;
function IsReady(RequiredLookback: Int64): Boolean;
property LastSeenCycle: Int64 read FLastSeenCycle; property LastSeenCycle: Int64 read FLastSeenCycle;
end; end;
TPipeConfig = TArray<TArray<TScalarRecordField>>;
TPipeStream = class(TCustomDataStream) TPipeStream = class(TCustomDataStream)
public public
type type
TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean; TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean;
private strict private
FSources: TArray<TPipeSource>; FSources: TArray<ISeries>;
FSourceSeries: TArray<ISeries>; FResults: TArray<TScalar.TValue>;
FLastFiredCycleID: Int64; FLastFiredCycleID: Int64;
FLookback: Int64;
FLambda: TPipeLambda; FLambda: TPipeLambda;
private
procedure CheckBarrierAndFire(CurrentCycle: Int64); procedure CheckBarrierAndFire(CurrentCycle: Int64);
public public
constructor Create( constructor Create(
const AConfig: TPipeConfig; const AConfig: TPipeConfig;
const ADef: IScalarRecordDefinition; const ADef: IScalarRecordDefinition;
const ASources: TArray<IStream>; const ASources: TArray<IStream>;
const ALambda: TPipeLambda const ALambda: TPipeLambda;
ALookback: Int64
); );
destructor Destroy; override; destructor Destroy; override;
property Lookback: Int64 read FLookback;
end; end;
implementation implementation
uses { TStreamSignal }
Winapi.Windows;
constructor TStreamSignal.Create(AKind: TSignalKind; ACycleID: Int64); constructor TStreamSignal.Create(AKind: TSignalKind; ACycleID: Int64; const AData: TArray<TScalar.TValue>);
begin begin
FKind := AKind; FKind := AKind;
FCycleID := ACycleID; FCycleID := ACycleID;
FData := AData;
end; end;
function TStreamSignal.ToString: string; function TStreamSignal.ToString: string;
begin begin
if Kind = skData then if Kind = skData then
Result := Format('Signal(Data, #%d)', [CycleID]) Result := Format('Signal(Data, #%d, Fields: %d)', [CycleID, Length(Data)])
else else
Result := Format('Signal(Heartbeat, #%d)', [CycleID]); Result := Format('Signal(Heartbeat, #%d)', [CycleID]);
end; end;
@@ -145,7 +160,7 @@ end;
constructor TCustomDataStream.Create(const ADef: IScalarRecordDefinition); constructor TCustomDataStream.Create(const ADef: IScalarRecordDefinition);
begin begin
inherited Create; inherited Create;
FSeries := TScalarRecordSeries.Create(ADef); FDef := ADef;
end; end;
destructor TCustomDataStream.Destroy; destructor TCustomDataStream.Destroy;
@@ -154,12 +169,12 @@ begin
inherited; inherited;
end; end;
function TCustomDataStream.GetSeries: IScalarRecordSeries; function TCustomDataStream.GetDef: IScalarRecordDefinition;
begin begin
Result := FSeries; Result := FDef;
end; end;
function TCustomDataStream.Subscribe(const Observer: IStreamObserver): TSubscriptionTag; function TCustomDataStream.Subscribe(const Observer: TSignalProc): TSubscriptionTag;
begin begin
FObservers.Lock; FObservers.Lock;
try try
@@ -171,6 +186,9 @@ end;
procedure TCustomDataStream.Unsubscribe(Tag: TSubscriptionTag); procedure TCustomDataStream.Unsubscribe(Tag: TSubscriptionTag);
begin begin
if Tag = nil then
exit;
FObservers.Lock; FObservers.Lock;
try try
FObservers.Unadvise(Tag); FObservers.Unadvise(Tag);
@@ -181,20 +199,23 @@ end;
procedure TCustomDataStream.Emit(const Value: array of TScalar.TValue; ACycleID: Int64); procedure TCustomDataStream.Emit(const Value: array of TScalar.TValue; ACycleID: Int64);
var var
signal: TStreamSignal; LSignal: TStreamSignal;
LData: TArray<TScalar.TValue>;
I: Integer;
begin begin
// Convert open array to dynamic array for signal payload
SetLength(LData, Length(Value));
for I := 0 to High(Value) do
LData[I] := Value[I];
LSignal := TStreamSignal.Create(skData, ACycleID, LData);
FObservers.Lock; FObservers.Lock;
try try
// 1. Write Data
FSeries.Add(Value);
// 2. Broadcast Signal
signal := TStreamSignal.Create(skData, ACycleID);
FObservers.Notify( FObservers.Notify(
function(const Obs: IStreamObserver): Boolean function(const Obs: TSignalProc): Boolean
begin begin
Obs.OnSignal(signal); Obs(LSignal);
Result := True; Result := True;
end end
); );
@@ -211,116 +232,159 @@ begin
FLastCycleID := 0; FLastCycleID := 0;
end; end;
procedure TRootStream.Push(const RowData: TArray<TScalar.TValue>); procedure TRootStream.Push(const RowData: array of TScalar.TValue);
begin begin
// Root streams increment the global clock if Length(RowData) <> Def.Count then
raise EArgumentException.Create('Push: Data field count mismatch.');
Inc(FLastCycleID); Inc(FLastCycleID);
Emit(RowData, FLastCycleID); Emit(RowData, FLastCycleID);
end; end;
{ TPipeSource } { TPipeSource }
constructor TPipeSource.Create(AOwner: TPipeStream; ASource: IStream); constructor TPipeSource.Create(const ASource: IStream; const AField: IKeyword; ALookback: Int64; const AOnSignal: TSignalProc);
begin begin
inherited Create(AOwner); inherited Create;
FSource := ASource; FSource := ASource;
FOnSignal := AOnSignal;
FLookback := ALookback;
FLastSeenCycle := -1; FLastSeenCycle := -1;
FTag := FSource.Subscribe(Self);
FFieldIndex := ASource.Def.IndexOf(AField);
if (FFieldIndex < 0) then
raise EArgumentException.CreateFmt('Field %s not found in source stream.', [AField.Name]);
FData := TScalarSeries.Create(ASource.Def.Items[FFieldIndex]);
FTag := FSource.Subscribe(procedure(const Signal: TStreamSignal) begin HandleSignal(Signal); end);
end; end;
destructor TPipeSource.Destroy; destructor TPipeSource.Destroy;
begin begin
FSource.Unsubscribe(FTag); Detach;
inherited; inherited;
end; end;
procedure TPipeSource.OnSignal(const Signal: TStreamSignal); procedure TPipeSource.Detach;
begin begin
if Signal.Kind = skData then FSource.Unsubscribe(FTag);
FTag := nil;
FOnSignal := nil;
end;
procedure TPipeSource.HandleSignal(const Signal: TStreamSignal);
begin
if (Signal.Kind = skData) then
begin begin
FLastSeenCycle := Signal.CycleID; FLastSeenCycle := Signal.CycleID;
(Controller as TPipeStream).CheckBarrierAndFire(FLastSeenCycle); FData.Add(Signal.Data[FFieldIndex], FLookback);
if Assigned(FOnSignal) then
FOnSignal(Signal);
end; end;
end; end;
function TPipeSource.IsReady(RequiredLookback: Int64): Boolean;
begin
Result := (FLastSeenCycle >= 0) and (FData.Count >= RequiredLookback);
end;
function TPipeSource.GetCount: Int64;
begin
Result := FData.Count;
end;
function TPipeSource.GetItems(Idx: Integer): TScalar;
begin
Result := FData.Items[Idx];
end;
function TPipeSource.GetTotalCount: Int64;
begin
Result := FData.TotalCount;
end;
{ TPipeStream } { TPipeStream }
constructor TPipeStream.Create( constructor TPipeStream.Create(
const AConfig: TPipeConfig; const AConfig: TPipeConfig;
const ADef: IScalarRecordDefinition; const ADef: IScalarRecordDefinition;
const ASources: TArray<IStream>; const ASources: TArray<IStream>;
const ALambda: TPipeLambda const ALambda: TPipeLambda;
ALookback: Int64
); );
var var
i, j, n: Integer; i, j, n: Integer;
begin begin
// Pass Definition to base class
inherited Create(ADef); inherited Create(ADef);
FLambda := ALambda; FLambda := ALambda;
FLookback := ALookback;
FLastFiredCycleID := -1; FLastFiredCycleID := -1;
SetLength(FSources, Length(ASources)); // Flatten sources from config
// Flatten Sources
n := 0; n := 0;
for i := 0 to High(AConfig) do for i := 0 to High(AConfig) do
inc(n, Length(AConfig[i])); inc(n, Length(AConfig[i]));
SetLength(FSourceSeries, n);
SetLength(FSources, n);
SetLength(FResults, n);
n := 0; n := 0;
for i := 0 to High(ASources) do for i := 0 to High(ASources) do
begin begin
FSources[i] := TPipeSource.Create(Self, ASources[i]);
for j := 0 to High(AConfig[i]) do for j := 0 to High(AConfig[i]) do
begin begin
// Extract the specific column series // Each input is wrapped in an accumulator source
var m := ASources[i].Series.IndexOf(AConfig[i][j].Key); FSources[n] :=
if m >= 0 then TPipeSource.Create(
begin ASources[i],
FSourceSeries[n] := ASources[i].Series[m]; AConfig[i][j].Key,
inc(n); FLookback,
end; procedure(const Signal: TStreamSignal) begin CheckBarrierAndFire(Signal.CycleID); end
);
inc(n);
end; end;
end; end;
end; end;
destructor TPipeStream.Destroy; destructor TPipeStream.Destroy;
begin begin
for var i := High(FSources) downto 0 do for var i := 0 to High(FSources) do
FSources[i].Free; (FSources[i] as TPipeSource).Detach;
FSources := nil;
FSourceSeries := nil;
inherited; inherited;
end; end;
procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64); procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64);
var
resultVal: TArray<TScalar.TValue>;
begin begin
// We assume thread safety is handled by the caller const requiredLookback = 10;
// 1. Check if all sources reached this cycle AND have enough history (Warm-up)
for var src in FSources do for var src in FSources do
if src.LastSeenCycle < CurrentCycle then begin
exit; // Barrier closed if src.Count < requiredLookback then
exit;
var LSource := src as TPipeSource;
if LSource.LastSeenCycle < CurrentCycle then
exit;
end;
if FLastFiredCycleID >= CurrentCycle then if FLastFiredCycleID >= CurrentCycle then
exit; // Already fired Exit;
FLastFiredCycleID := CurrentCycle; FLastFiredCycleID := CurrentCycle;
if Assigned(FLambda) then if Assigned(FLambda) then
begin begin
SetLength(resultVal, Series.Def.Count); // Execute transformation on synchronized window
if FLambda(FSourceSeries, resultVal) then if FLambda(FSources, FResults) then
begin begin
// Pass through CycleID from upstream Emit(FResults, FLastFiredCycleID);
Emit(resultVal, FLastFiredCycleID);
end; end;
end; end;
end; end;
initialization initialization
TMycNotifyList<IStreamObserver>.ReverseOnNotify := False; TMycNotifyList<TSignalProc>.ReverseOnNotify := False;
end. end.
+1 -2
View File
@@ -84,7 +84,6 @@ begin
// Für den Compiler ist es eine RecordSeries (Typ), zur Laufzeit ein Stream (Wert) // Für den Compiler ist es eine RecordSeries (Typ), zur Laufzeit ein Stream (Wert)
StaticType := TTypes.CreateRecordSeries(Def); StaticType := TTypes.CreateRecordSeries(Def);
// UPDATED: Dokumentation für die Stream-Variable hinzufügen
Scope.Define(VarName, Stream, StaticType, 'Financial OHLC Data Stream (Time, Open, High, Low, Close, Vol).'); Scope.Define(VarName, Stream, StaticType, 'Financial OHLC Data Stream (Time, Open, High, Low, Close, Vol).');
end; end;
@@ -118,7 +117,7 @@ begin
raise Exception.CreateFmt('Variable "%s" is a Stream, but not a TRootStream (cannot push manually).', [VarName]); raise Exception.CreateFmt('Variable "%s" is a Stream, but not a TRootStream (cannot push manually).', [VarName]);
Stream := TRootStream(Val.AsStream); Stream := TRootStream(Val.AsStream);
Def := Stream.Series.Def; Def := Stream.Def;
// --- Simulation --- // --- Simulation ---
FLastTime := IncMinute(FLastTime, 5); FLastTime := IncMinute(FLastTime, 5);