Adding Pipes

This commit is contained in:
Michael Schimmel
2025-12-20 13:40:29 +01:00
parent 8960a5683e
commit e7fdbc3312
4 changed files with 62 additions and 46 deletions
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -39,7 +39,8 @@ uses
Myc.Fmx.AstEditor.Handlers.Data in '..\Src\AST\Myc.Fmx.AstEditor.Handlers.Data.pas',
Myc.Ast.RTL.TypeRegistry in '..\Src\AST\Myc.Ast.RTL.TypeRegistry.pas',
Myc.Trade.Broker in '..\Src\Myc.Trade.Broker.pas',
Myc.Data.Stream.Pipes in '..\Src\Data\Myc.Data.Stream.Pipes.pas';
Myc.Data.Stream.Pipes in '..\Src\Data\Myc.Data.Stream.Pipes.pas',
Myc.Data.Stream in '..\Src\Data\Myc.Data.Stream.pas';
{$R *.res}
+1
View File
@@ -170,6 +170,7 @@
<DCCReference Include="..\Src\AST\Myc.Ast.RTL.TypeRegistry.pas"/>
<DCCReference Include="..\Src\Myc.Trade.Broker.pas"/>
<DCCReference Include="..\Src\Data\Myc.Data.Stream.Pipes.pas"/>
<DCCReference Include="..\Src\Data\Myc.Data.Stream.pas"/>
<BuildConfiguration Include="Base">
<Key>Base</Key>
</BuildConfiguration>
+58 -44
View File
@@ -1,35 +1,46 @@
(*TODO
Ich möchte ein neues Sprachkonzept einbauen. Folgendes Skript skizziert, was möglich sein soll:
; Record-Streams sind Record-Series, an die "Pipes" andocken können (Observer) um neue Daten zu bekommen
; Es handelt sich um reaktives Producer-Consumer-Pattern
(*
; Record-Streams sind Record-Series, an die "Pipes" andocken können.
; Es handelt sich um ein reaktives Producer-Consumer-Pattern.
;; Beispiel: SMA auf Close
(def btc (create-ticker "BTCUSD")) ; ein Open-High-Low-Close-Record-Stream
(def sma (create-sma 20)) ; erzeugt eine Lambda (sma(price)), die den sma20 berechnet
(def btc (create-ticker "BTCUSD"))
(def sma (create-sma 20))
; Das Ergebnis eines pipe-Befehls ist wieder ein Record-Stream.
; btc-sma ist also ein Record-Stream, der {:ma ..} records enthält:
(def btc-sma 
  (pipe [btc [:Close]]
    (fn [price] 
      ; pipe mappt die Elemente des Record-Streams (hier :Close) auf die Parameter der fn. Price ist als eine ScalarSeries.
      ; (Index=0 ist das neueste Element)
      ; emit ist eine von pipe in den lambda-scope injizierte Funktion, die das neue Element in den Ergebnis-Stream 
      ; pusht (und damit die Observer des Ergebnis-Streams benachrichtigt).
      (emit {:ma (sma (get price 0))})
    )
  )
; TRANSFORMATION
; btc-sma ist ein Record-Stream, der {:ma ..} records enthält.
(def btc-sma
(pipe [btc [:Close]]
(fn [price]
; KEIN emit mehr. Wir geben einfach die Map zurück.
; Die Engine nimmt diesen Rückgabewert und publiziert ihn.
{:ma (sma (get price 0))}
)
)
)
; Diese pipe hat zwei Streams als Input: den Closing-Price und den SMA. Jede pipe kann also N Input Stream haben
; KOMBINATION & LOGIK
; Diese pipe hat zwei Streams als Input.
(def btc-signal
  (pipe [btc [:Close] btc-sma [:ma]]
    (fn [price ma]
(emit {:signal (? (> (get price 0) (get ma 0)) :buy : sell)})
    )
  )
(pipe [btc [:Close] btc-sma [:ma]]
(fn [price ma]
; Hier wird einfach der Record zurückgegeben.
; Da der ternäre Operator (?) immer ein Ergebnis hat (:buy oder :sell),
; feuert dieser Stream bei jedem Tick ein Signal.
{:signal (? (> (get price 0) (get ma 0)) :buy :sell)}
)
)
)
(def btc-buy-only
(pipe [btc [:Close] btc-sma [:ma]]
(fn [price ma]
; Ein IF ohne ELSE.
; Wenn die Bedingung FALSE ist, ist das Ergebnis der Funktion "Void".
; Die Engine erkennt "Void" und sendet NICHTS an die Observer.
(if (> (get price 0) (get ma 0))
{:signal :buy})
)
)
)
*)
@@ -77,8 +88,9 @@ type
// Acts as IStream (Producer) for downstream consumers and manages internal inputs.
TPipeStream = class(TInterfacedObject, IStream)
type
TEmitProc = reference to procedure(const Data: array of TScalar.TValue);
TProc = reference to procedure(const Sources: TArray<ISeries>; const Emit: TEmitProc);
// Functional lambda: Maps inputs (Series) to a result value (TArray<TScalar.TValue>).
// Returns false to filter (emit nothing).
TPipeLambda = reference to function(const Sources: array of ISeries; out Results: array of TScalar.TValue): Boolean;
private
FSources: TArray<TPipeSource>;
FObservers: TMycNotifyList<IStreamObserver>;
@@ -89,24 +101,23 @@ type
FLastFiredCycleID: Int64;
// Execution Logic
// The compiled lambda function representing (fn [inputs...] ...)
FLambda: TProc;
FLambda: TPipeLambda;
procedure CheckBarrierAndFire(CurrentCycle: Int64);
function GetSeries: IScalarRecordSeries;
// Extract values from the lambda result and map them to the stream definition
procedure Emit(const Value: TArray<TScalar.TValue>);
public
constructor Create(
const AConfig: TPipeConfig;
const ADef: IScalarRecordDefinition;
const ASources: TArray<IStream>;
const ALambda: TProc
const ALambda: TPipeLambda
);
destructor Destroy; override;
// Injected into lambda scope as "emit"
procedure Emit(const Data: array of TScalar.TValue);
// IStream Implementation
function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
procedure Unsubscribe(Tag: TSubscriptionTag);
@@ -147,7 +158,7 @@ constructor TPipeStream.Create(
const AConfig: TPipeConfig;
const ADef: IScalarRecordDefinition;
const ASources: TArray<IStream>;
const ALambda: TProc
const ALambda: TPipeLambda
);
begin
inherited Create;
@@ -233,7 +244,14 @@ begin
if Assigned(FLambda) then
begin
try
FLambda(FSourceSeries, Emit);
var resultVal: TArray<TScalar.TValue>;
SetLength(resultVal, FSeries.Def.Count);
// Handle "Void" return (Filter logic)
if not FLambda(FSourceSeries, resultVal) then
exit;
Emit(resultVal);
except
// Error handling strategy: Propagate/Crash for now.
raise;
@@ -244,17 +262,13 @@ begin
end;
end;
procedure TPipeStream.Emit(const Data: array of TScalar.TValue);
procedure TPipeStream.Emit(const Value: TArray<TScalar.TValue>);
begin
Assert(Length(Data) = FSeries.Def.Count);
// Emit has to be called from the Lambda, which is called in CheckBarrierAndFire!
Assert(FObservers.IsLocked);
FSeries.Add(Data);
// Add to internal series
FSeries.Add(Value);
// Notify downstream
var signal := TStreamSignal.Create(skData, FLastFiredCycleID);
FObservers.Notify(
function(const Obs: IStreamObserver): Boolean
begin