273 lines
7.3 KiB
ObjectPascal
273 lines
7.3 KiB
ObjectPascal
(*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
|
||
|
||
;; 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
|
||
|
||
|
||
; 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))})
|
||
)
|
||
)
|
||
)
|
||
|
||
; Diese pipe hat zwei Streams als Input: den Closing-Price und den SMA. Jede pipe kann also N Input Stream haben
|
||
(def btc-signal
|
||
(pipe [btc [:Close] btc-sma [:ma]]
|
||
(fn [price ma]
|
||
(emit {:signal (? (> (get price 0) (get ma 0)) :buy : sell)})
|
||
)
|
||
)
|
||
)
|
||
*)
|
||
|
||
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
|
||
TEmitProc = reference to procedure(const Data: array of TScalar.TValue);
|
||
TProc = reference to procedure(const Sources: TArray<ISeries>; const Emit: TEmitProc);
|
||
private
|
||
FSources: TArray<TPipeSource>;
|
||
FObservers: TMycNotifyList<IStreamObserver>;
|
||
FSourceSeries: TArray<ISeries>;
|
||
FSeries: IWriteableScalarRecordSeries;
|
||
|
||
// Barrier State
|
||
FLastFiredCycleID: Int64;
|
||
|
||
// Execution Logic
|
||
// The compiled lambda function representing (fn [inputs...] ...)
|
||
FLambda: TProc;
|
||
|
||
procedure CheckBarrierAndFire(CurrentCycle: Int64);
|
||
function GetSeries: IScalarRecordSeries;
|
||
|
||
public
|
||
constructor Create(
|
||
const AConfig: TPipeConfig;
|
||
const ADef: IScalarRecordDefinition;
|
||
const ASources: TArray<IStream>;
|
||
const ALambda: TProc
|
||
);
|
||
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);
|
||
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: TProc
|
||
);
|
||
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
|
||
FLambda(FSourceSeries, Emit);
|
||
except
|
||
// Error handling strategy: Propagate/Crash for now.
|
||
raise;
|
||
end;
|
||
end;
|
||
finally
|
||
FObservers.Release;
|
||
end;
|
||
end;
|
||
|
||
procedure TPipeStream.Emit(const Data: array of 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);
|
||
|
||
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.
|