unit Myc.Trade.DataPoint.Impl; interface uses System.SysUtils, System.SyncObjs, Myc.Signals, Myc.Mutable, Myc.Core.Notifier, Myc.Trade.Types, Myc.Trade.DataArray, Myc.Trade.DataPoint; type // Abstract base class for data consumers. TMycProcessor = class abstract(TInterfacedObject, IMycProcessor) protected function Execute(const Value: T; const Proc: TConstFunc): TState; function ProcessData(const Value: T): TState; virtual; abstract; end; // Concrete data provider that manages a list of processors (listeners). TMycContainedDataProvider = class abstract(TContainedObject, TDataProvider.IDataProvider) private FListeners: TMycNotifyList>; public constructor Create(const Controller: IInterface); destructor Destroy; override; // Notifies all linked processors. function Broadcast(const Value: T): TState; // Link a Processor function Link(const Processor: IMycProcessor): TDataProvider.TTag; // Unlink a linked strategy procedure Unlink(Tag: TDataProvider.TTag); end; TMycSequence = class(TMycProcessor, IMycDataSequence) private FDataProviders: TArray>; function GetCount: Integer; function GetDataProvider(Idx: Integer): TDataProvider; protected function ProcessData(const Value: T): TState; override; function ProcessDataProvider(Idx: Integer; const Value: T): TState; public constructor Create(ACount: Integer); destructor Destroy; override; end; // Null object implementation for IDataProvider. TNullDataProvider = class(TInterfacedObject, TDataProvider.IDataProvider) public function Link(const Receiver: IMycProcessor): TDataProvider.TTag; procedure Unlink(Tag: TDataProvider.TTag); end; // Abstract base class for components that process data of type S and provide data of type T. TMycConverter = class abstract(TMycProcessor, TConverter.IConverter) private FSender: TMycContainedDataProvider; function GetSender: TDataProvider.IDataProvider; protected function ProcessData(const Value: S): TState; override; abstract; // Broadcasts the given data to all linked processors. function Broadcast(const Value: T): TState; public constructor Create; destructor Destroy; override; property Sender: TDataProvider.IDataProvider read GetSender; end; // Null object implementation for IConverter. TNullConverter = class(TInterfacedObject, TConverter.IConverter) private function GetSender: TDataProvider.IDataProvider; public function ProcessData(const Value: S): TState; end; // A generic converter that uses a function reference for the conversion logic. TMycGenericConverter = class(TMycConverter) private FFunc: TConstFunc; protected function ProcessData(const Value: S): TState; override; public constructor Create(const AFunc: TConstFunc); end; TMycGenericParallelConverter = class(TMycConverter) private FFunc: TConstFunc; FQueue: TState; protected function ProcessData(const Value: S): TState; override; public constructor Create(const AFunc: TConstFunc); end; TMycIdentityConverter = class(TMycConverter) protected function ProcessData(const Value: T): TState; override; final; end; // A converter specialized for calculating indicators. TMycIndicator = class(TMycConverter) protected function ProcessData(const Value: S): TState; override; final; function Calculate(const Value: S): T; virtual; abstract; end; // A converter that counts incoming data points and outputs the current count. TMycDataCounter = class(TMycConverter) private FCount: Int64; protected function ProcessData(const Value: T): TState; override; public constructor Create; end; // A converter that takes an array and broadcasts each element individually. TMycTicker = class(TMycConverter, T>) public function ProcessData(const Values: TArray): TState; override; end; // A converter that reads a specific field from a record using RTTI. TMycRecordFieldReader = class(TMycConverter) private FOffset: Integer; public constructor Create(const AFieldName: String); function ProcessData(const Values: S): TState; override; end; // A generic processor implementation that uses a function reference. TMycGenericProcessor = class(TMycProcessor) private FProc: TConstFunc; protected function ProcessData(const Value: T): TState; override; final; public constructor Create(const AProc: TConstFunc); end; // A processor implementation that is owned by a controller. TMycContainedProcessor = class(TContainedObject, IMycProcessor) type TProc = function(const Value: T): TState of object; private FProc: TProc; function ProcessData(const Value: T): TState; public constructor Create(const Controller: IInterface; const AProc: TProc); end; // Endpoint that collects data into a series. TMycDataEndpoint = class(TInterfacedObject, TLazy>.ILazy) type PItem = ^TItem; TItem = record Next: PItem; Value: T; end; private FProcessor: TMycContainedProcessor; FTag: TDataProvider.TTag; FDataProvider: TDataProvider; FLookback: Int64; FChanged: TFlag; FLock: TLightweightMREW; FFirst: PItem; FCount: Integer; function GetChanged: TState; function ProcessData(const Value: T): TState; public constructor Create(const ADataProvider: TDataProvider; ALookback: Int64); destructor Destroy; override; function Update(var Value: TSeries): Boolean; end; TTickAggregation = class(TMycConverter, TDataPoint>) private FTimeframe: TTimeframe; FCurrentBar: TDataPoint; function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; function GetCurrentBar: TDataPoint; function GetTimeframe: TTimeframe; public constructor Create(const ATimeframe: TTimeframe); function ProcessData(const Value: TDataPoint): TState; override; property CurrentBar: TDataPoint read GetCurrentBar; property Timeframe: TTimeframe read GetTimeframe; end; TMycParallelConverter = class(TMycConverter) private FQueue: TState; protected function ProcessData(const Value: T): TState; override; final; end; TOhlcAggregation = class(TMycConverter, TDataPoint>) private FTimeframe: TTimeframe; FCurrentBar: TDataPoint; function GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; function GetCurrentBar: TDataPoint; function GetTimeframe: TTimeframe; public constructor Create(const ATimeframe: TTimeframe); function ProcessData(const Value: TDataPoint): TState; override; property CurrentBar: TDataPoint read GetCurrentBar; property Timeframe: TTimeframe read GetTimeframe; end; implementation uses System.TypInfo, System.RTTI, System.DateUtils, System.Math, Myc.TaskManager; { TMycProcessor } function TMycProcessor.Execute(const Value: T; const Proc: TConstFunc): TState; begin var cValue := Value; Result := TaskManager.RunTask(function: TState begin Result := Proc(cValue); end); end; { TMycContainedDataProvider } constructor TMycContainedDataProvider.Create(const Controller: IInterface); begin inherited Create(Controller); end; destructor TMycContainedDataProvider.Destroy; begin FListeners.Finalize; inherited Destroy; end; function TMycContainedDataProvider.Broadcast(const Value: T): TState; begin FListeners.Lock; try var item := FListeners.First; if item = nil then exit(TState.Null); var i := 1; while item.Next <> nil do begin inc(i); item := item.Next; end; var done := TLatch.CreateLatch(i); while item <> nil do begin item.Receiver.ProcessData(Value).Signal.Subscribe(done); item := item.Prev; end; Result := done.State; finally FListeners.Release; end; end; function TMycContainedDataProvider.Link(const Processor: IMycProcessor): TDataProvider.TTag; begin // Add the Processor to the notification list FListeners.Lock; try Result := FListeners.Advise(Processor); finally FListeners.Release; end; end; procedure TMycContainedDataProvider.Unlink(Tag: TDataProvider.TTag); begin FListeners.Lock; try FListeners.Unadvise(Tag); finally FListeners.Release; end; end; { TNullDataProvider } function TNullDataProvider.Link(const Receiver: IMycProcessor): TDataProvider.TTag; begin Result := nil; end; procedure TNullDataProvider.Unlink(Tag: TDataProvider.TTag); begin // Do nothing in the null implementation. end; { TMycConverter } constructor TMycConverter.Create; begin inherited Create; FSender := TMycContainedDataProvider.Create(Self); end; destructor TMycConverter.Destroy; begin FSender.Free; inherited Destroy; end; function TMycConverter.Broadcast(const Value: T): TState; begin Result := FSender.Broadcast(Value); end; function TMycConverter.GetSender: TDataProvider.IDataProvider; begin Result := FSender; end; { TNullConverter } function TNullConverter.GetSender: TDataProvider.IDataProvider; begin Result := TDataProvider.Null; end; function TNullConverter.ProcessData(const Value: S): TState; begin Result := TState.Null; end; { TMycGenericConverter } constructor TMycGenericConverter.Create(const AFunc: TConstFunc); begin inherited Create; FFunc := AFunc; end; function TMycGenericConverter.ProcessData(const Value: S): TState; begin Result := Broadcast(FFunc(Value)); end; { TMycIndicator } function TMycIndicator.ProcessData(const Value: S): TState; begin Result := Execute(Value, function(const Value: S): TState begin Result := Broadcast(Calculate(Value)); end); end; { TMycDataCounter } constructor TMycDataCounter.Create; begin inherited Create; FCount := 0; end; function TMycDataCounter.ProcessData(const Value: T): TState; begin Result := Broadcast(FCount); inc(FCount); end; { TMycTicker } function TMycTicker.ProcessData(const Values: TArray): TState; begin var done := TLatch.CreateLatch(Length(Values)); // Process each incoming data point for var i := 0 to High(Values) do Broadcast(Values[i]).Signal.Subscribe(done); Result := done.State; end; { TMycRecordFieldReader } constructor TMycRecordFieldReader.Create(const AFieldName: String); begin inherited Create; var Context := TRttiContext.Create; var Field := Context.GetType(TypeInfo(S)).GetField(AFieldName); var TypeT := Context.GetType(TypeInfo(T)); var Fields := Context.GetType(TypeInfo(S)).GetFields; var name := ''; if AFieldName = 'Time' then for var i := 0 to High(Fields) do begin name := name + ' ' + Fields[i].Name; end; Assert(Assigned(Field), 'Field ' + AFieldName + ' not found'); Assert(Field.FieldType.TypeKind = TypeT.TypeKind, 'Incorrect type'); if Assigned(Field) and (Field.FieldType.TypeKind = TypeT.TypeKind) then FOffset := Field.Offset else FOffset := -1; end; function TMycRecordFieldReader.ProcessData(const Values: S): TState; type PT = ^T; begin if FOffset < 0 then exit(TState.Null); var fieldPtr := PByte(@Values); inc(fieldPtr, FOffset); Result := Broadcast(PT(fieldPtr)^); end; { TMycGenericProcessor } constructor TMycGenericProcessor.Create(const AProc: TConstFunc); begin inherited Create; FProc := AProc; end; function TMycGenericProcessor.ProcessData(const Value: T): TState; begin Result := FProc(Value); end; { TMycContainedProcessor } constructor TMycContainedProcessor.Create(const Controller: IInterface; const AProc: TProc); begin inherited Create(Controller); FProc := AProc; end; function TMycContainedProcessor.ProcessData(const Value: T): TState; begin Result := FProc(Value); end; { TMycDataEndpoint } constructor TMycDataEndpoint.Create(const ADataProvider: TDataProvider; ALookback: Int64); begin inherited Create; FDataProvider := ADataProvider; FLookback := ALookback; FProcessor := TMycContainedProcessor.Create(Self, ProcessData); FTag := FDataProvider.Link(FProcessor); end; destructor TMycDataEndpoint.Destroy; begin FDataProvider.Unlink(FTag); FProcessor.Free; inherited; end; function TMycDataEndpoint.GetChanged: TState; begin Result := FChanged.State end; function TMycDataEndpoint.ProcessData(const Value: T): TState; begin FLock.BeginWrite; try var P: PItem; New(P); P.Next := FFirst; FFirst := P; P.Value := Value; inc(FCount); FChanged.Notify; finally FLock.EndWrite; end; end; function TMycDataEndpoint.Update(var Value: TSeries): Boolean; begin FLock.BeginWrite; try Result := FChanged.Reset; if Result then begin if FCount > 0 then begin var item: PItem; var Arr: TArray; SetLength(Arr, FCount); while FFirst <> nil do begin item := FFirst; FFirst := item.Next; dec(FCount); Assert(FCount >= 0); Arr[FCount] := item.Value; Dispose(item); end; Assert(FCount = 0); Value := Value.Add(Arr, 0, Length(Arr), FLookback); end; end; finally FLock.EndWrite; end; end; { TTickAggregation } constructor TTickAggregation.Create(const ATimeframe: TTimeframe); begin inherited Create; FTimeframe := ATimeframe; end; function TTickAggregation.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; var baseTime: TDateTime; begin // Align the time grid to UTC 0:00 using functions from System.DateUtils baseTime := RecodeMilliSecond(TimeStamp, 0); case Timeframe of S: Result := baseTime; S5: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 5)); S15: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 15)); S30: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 30)); M: Result := RecodeSecond(baseTime, 0); M2: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 2)); M3: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 3)); M5: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 5)); M10: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 10)); M15: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 15)); M30: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 30)); H: Result := RecodeMinute(RecodeSecond(baseTime, 0), 0); H2: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 2)); H3: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 3)); H4: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 4)); H8: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 8)); H12: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 12)); D: Result := StartOfTheDay(TimeStamp); // D2, D3 are uncommon; this is a simple modulo-based approach relative to TDateTime's epoch. D2: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 2); D3: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 3); W: Result := TimeStamp.StartOfTheWeek; MN: Result := TimeStamp.StartOfTheMonth; // Quarter alignment MN3: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 3 * 3 + 1); // Half-year alignment MN6: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 6 * 6 + 1); Y: Result := TimeStamp.StartOfTheYear; else // Fallback for any undefined timeframe Result := 0; end; end; function TTickAggregation.GetCurrentBar: TDataPoint; begin Result := FCurrentBar; end; function TTickAggregation.GetTimeframe: TTimeframe; begin Result := FTimeframe; end; function TTickAggregation.ProcessData(const Value: TDataPoint): TState; var barStartTime: TDateTime; lastBarTime: TDateTime; begin // Update bar for the strategy's timeframe barStartTime := GetBarStartTime(Value.Time, FTimeframe); lastBarTime := FCurrentBar.Time; if (barStartTime > lastBarTime) then begin // A new bar starts, so the previous one is now complete. if (lastBarTime > 0) then begin Result := Broadcast(FCurrentBar); end; // Start a new bar, Volume is 1 because this is the first tick. FCurrentBar.Data.Open := Value.Data; FCurrentBar.Data.High := Value.Data; FCurrentBar.Data.Low := Value.Data; FCurrentBar.Data.Close := Value.Data; FCurrentBar.Data.Volume := 1; FCurrentBar.Time := barStartTime; end else begin // Update the currently aggregating bar if Value.Data > FCurrentBar.Data.High then FCurrentBar.Data.High := Value.Data; if Value.Data < FCurrentBar.Data.Low then FCurrentBar.Data.Low := Value.Data; FCurrentBar.Data.Close := Value.Data; // Volume is the number of ticks needed to build the complete bar. FCurrentBar.Data.Volume := FCurrentBar.Data.Volume + 1; end; end; { TMycSequence } constructor TMycSequence.Create(ACount: Integer); begin inherited Create; SetLength(FDataProviders, ACount); for var i := 0 to High(FDataProviders) do FDataProviders[i] := TMycContainedDataProvider.Create(Self); end; destructor TMycSequence.Destroy; begin for var i := High(FDataProviders) downto 0 do FDataProviders[i].Free; inherited; end; function TMycSequence.GetCount: Integer; begin Result := Length(FDataProviders); end; function TMycSequence.GetDataProvider(Idx: Integer): TDataProvider; begin Result := FDataProviders[Idx]; end; function TMycSequence.ProcessData(const Value: T): TState; begin Result := ProcessDataProvider(0, Value); end; function TMycSequence.ProcessDataProvider(Idx: Integer; const Value: T): TState; begin if Idx >= Length(FDataProviders) then exit; Result := TaskManager .RunTask(FDataProviders[idx].Broadcast(Value), function: TState begin Result := ProcessDataProvider(1 + idx, Value); end); end; function TMycIdentityConverter.ProcessData(const Value: T): TState; begin Result := Broadcast(Value); end; { TMycGenericParallelConverter } constructor TMycGenericParallelConverter.Create(const AFunc: TConstFunc); begin inherited Create; FFunc := AFunc; end; function TMycGenericParallelConverter.ProcessData(const Value: S): TState; begin var cValue := Value; Result := TaskManager.RunTask(FQueue, function: TState begin Result := Broadcast(FFunc(cValue)); end); FQueue := Result; end; function TMycParallelConverter.ProcessData(const Value: T): TState; begin var cValue := Value; Result := TaskManager.RunTask(FQueue, function: TState begin Result := Broadcast(cValue); end); FQueue := Result; end; { TOhlcAggregation } constructor TOhlcAggregation.Create(const ATimeframe: TTimeframe); begin inherited Create; FTimeframe := ATimeframe; end; function TOhlcAggregation.GetBarStartTime(const TimeStamp: TDateTime; const Timeframe: TTimeframe): TDateTime; var baseTime: TDateTime; begin // Align the time grid to UTC 0:00 using functions from System.DateUtils baseTime := RecodeMilliSecond(TimeStamp, 0); case Timeframe of S: Result := baseTime; S5: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 5)); S15: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 15)); S30: Result := RecodeSecond(baseTime, SecondOf(TimeStamp) - (SecondOf(TimeStamp) mod 30)); M: Result := RecodeSecond(baseTime, 0); M2: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 2)); M3: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 3)); M5: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 5)); M10: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 10)); M15: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 15)); M30: Result := RecodeMinute(RecodeSecond(baseTime, 0), MinuteOf(TimeStamp) - (MinuteOf(TimeStamp) mod 30)); H: Result := RecodeMinute(RecodeSecond(baseTime, 0), 0); H2: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 2)); H3: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 3)); H4: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 4)); H8: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 8)); H12: Result := RecodeHour(RecodeMinute(RecodeSecond(baseTime, 0), 0), HourOf(TimeStamp) - (HourOf(TimeStamp) mod 12)); D: Result := StartOfTheDay(TimeStamp); // D2, D3 are uncommon; this is a simple modulo-based approach relative to TDateTime's epoch. D2: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 2); D3: Result := Floor(TimeStamp) - (Floor(TimeStamp) mod 3); W: Result := TimeStamp.StartOfTheWeek; MN: Result := TimeStamp.StartOfTheMonth; // Quarter alignment MN3: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 3 * 3 + 1); // Half-year alignment MN6: Result := RecodeMonth(TimeStamp.StartOfTheMonth, (MonthOf(TimeStamp) - 1) div 6 * 6 + 1); Y: Result := TimeStamp.StartOfTheYear; else // Fallback for any undefined timeframe Result := 0; end; end; function TOhlcAggregation.GetCurrentBar: TDataPoint; begin Result := FCurrentBar; end; function TOhlcAggregation.GetTimeframe: TTimeframe; begin Result := FTimeframe; end; function TOhlcAggregation.ProcessData(const Value: TDataPoint): TState; var barStartTime: TDateTime; lastBarTime: TDateTime; begin // Update bar for the strategy's timeframe barStartTime := GetBarStartTime(Value.Time, FTimeframe); lastBarTime := FCurrentBar.Time; if (barStartTime > lastBarTime) then begin // A new bar starts, so the previous one is now complete. if (lastBarTime > 0) then begin Result := Broadcast(FCurrentBar); end; // Start a new bar, Volume is 1 because this is the first tick. FCurrentBar.Data := Value.Data; FCurrentBar.Time := barStartTime; end else begin // Update the currently aggregating bar if Value.Data.High > FCurrentBar.Data.High then FCurrentBar.Data.High := Value.Data.High; if Value.Data.Low < FCurrentBar.Data.Low then FCurrentBar.Data.Low := Value.Data.Low; FCurrentBar.Data.Close := Value.Data.Close; // Volume is the number of ticks needed to build the complete bar. FCurrentBar.Data.Volume := FCurrentBar.Data.Volume + Value.Data.Volume; end; end; end.