Files
MycLib/Src/Data/Myc.Data.Stream.Pipes.pas
T
Michael Schimmel 8b765487ae Implementing Pipes
2025-12-21 16:30:09 +01:00

288 lines
7.4 KiB
ObjectPascal

(*TODO
; 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"))
(def sma (create-sma 20))
; TRANSFORMATION
; btc-sma ist ein Record-Stream, der {:ma ..} records enthält.
(def btc-sma
(pipe [btc [:Close]]
(fn [price]
; Die Engine nimmt diesen Rückgabewert und publiziert ihn.
{:ma (sma (get price 0))}
)
)
)
; KOMBINATION & LOGIK
; Diese pipe hat zwei Streams als Input.
(def btc-signal
(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 -> optionaler Typ
; Wenn die Bedingung FALSE ist, ist das Ergebnis der Funktion "Void".
; Die Engine erkennt "Void" und sendet NICHTS an die Observer.
; Die ermöglich Aggregation.
(if (> (get price 0) (get ma 0))
{:signal :buy})
)
)
)
*)
unit Myc.Data.Stream.Pipes;
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Collections,
System.SyncObjs,
Myc.Data.Scalar,
Myc.Data.Keyword,
Myc.Data.Stream,
Myc.Core.Notifier;
type
// Forward declaration
TPipeStream = class;
// mapped fields per input stream
TPipeConfig = TArray<TArray<TScalarRecordField>>;
TPipeSource = class(TInterfacedObject, IStreamObserver)
private
[Weak]
FOwner: TPipeStream;
FSource: IStream;
FTag: TSubscriptionTag;
// State for barrier synchronization
FLastSeenCycle: Int64;
procedure OnSignal(const Signal: TStreamSignal);
procedure Destroying;
public
constructor Create(AOwner: TPipeStream; ASource: IStream; const AInputs: TArray<TScalarRecordField>);
property LastSeenCycle: Int64 read FLastSeenCycle;
end;
// --- Runtime: The Pipe ---
// The executable instance of a pipe.
// Acts as IStream (Producer) for downstream consumers and manages internal inputs.
TPipeStream = class(TInterfacedObject, IStream)
type
// 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>;
FSourceSeries: TArray<ISeries>;
FSeries: IWriteableScalarRecordSeries;
// Barrier State
FLastFiredCycleID: Int64;
// Execution Logic
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: TPipeLambda
);
destructor Destroy; override;
// IStream Implementation
function Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
procedure Unsubscribe(Tag: TSubscriptionTag);
end;
implementation
{ TPipeSource }
constructor TPipeSource.Create(AOwner: TPipeStream; ASource: IStream; const AInputs: TArray<TScalarRecordField>);
begin
inherited Create;
FOwner := AOwner;
FSource := ASource;
FLastSeenCycle := -1;
FTag := FSource.Subscribe(Self);
end;
procedure TPipeSource.Destroying;
begin
FSource.Unsubscribe(FTag);
FOwner := nil;
end;
procedure TPipeSource.OnSignal(const Signal: TStreamSignal);
begin
FLastSeenCycle := Signal.CycleID;
if Assigned(FOwner) then
FOwner.CheckBarrierAndFire(FLastSeenCycle);
end;
{ TPipeStream }
constructor TPipeStream.Create(
const AConfig: TPipeConfig;
const ADef: IScalarRecordDefinition;
const ASources: TArray<IStream>;
const ALambda: TPipeLambda
);
begin
inherited Create;
FLambda := ALambda;
FLastFiredCycleID := -1;
Assert(Length(AConfig) = Length(ASources));
FSeries := TScalarRecordSeries.Create(ADef);
// Construct record series definition from source config
SetLength(FSources, Length(ASources));
SetLength(FSourceSeries, Length(ASources));
var n := 0;
for var i := 0 to High(AConfig) do
begin
FSources[i] := TPipeSource.Create(Self, ASources[i], AConfig[i]);
FSources[i]._AddRef;
inc(n, Length(AConfig[i]));
end;
SetLength(FSourceSeries, n);
n := 0;
for var i := 0 to High(AConfig) do
for var j := 0 to High(AConfig[i]) do
begin
FSourceSeries[n] := ASources[i].Series.Fields[AConfig[i][j].Key];
inc(n);
end;
end;
destructor TPipeStream.Destroy;
begin
for var src in FSources do
begin
src.Destroying;
src._Release;
end;
FObservers.Finalize;
inherited;
end;
function TPipeStream.Subscribe(const Observer: IStreamObserver): TSubscriptionTag;
begin
FObservers.Lock;
try
Result := FObservers.Advise(Observer);
finally
FObservers.Release;
end;
end;
procedure TPipeStream.Unsubscribe(Tag: TSubscriptionTag);
begin
FObservers.Lock;
try
FObservers.Unadvise(Tag);
finally
FObservers.Release;
end;
end;
procedure TPipeStream.CheckBarrierAndFire(CurrentCycle: Int64);
begin
FObservers.Lock;
try
// Barrier Check
for var src in FSources do
begin
if src.LastSeenCycle < CurrentCycle then
exit;
end;
// --- Barrier Open ---
if FLastFiredCycleID >= CurrentCycle then
exit;
FLastFiredCycleID := CurrentCycle;
// Execute Lambda
if Assigned(FLambda) then
begin
try
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;
end;
end;
finally
FObservers.Release;
end;
end;
procedure TPipeStream.Emit(const Value: TArray<TScalar.TValue>);
begin
// Add to internal series
FSeries.Add(Value);
// Notify downstream
var signal := TStreamSignal.Create(skData, FLastFiredCycleID);
FObservers.Notify(
function(const Obs: IStreamObserver): Boolean
begin
Obs.OnSignal(signal);
Result := True;
end
);
end;
function TPipeStream.GetSeries: IScalarRecordSeries;
begin
Result := FSeries;
end;
end.