diff --git a/Core/Collections/ObservableGroupCollection.cs b/Core/Collections/ObservableGroupCollection.cs new file mode 100644 index 000000000..aa99658d8 --- /dev/null +++ b/Core/Collections/ObservableGroupCollection.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Terminal.Core.Collections +{ + public interface IGroup + { + /// + /// Group index + /// + /// + long GetIndex(); + + /// + /// Grouping implementation + /// + /// + /// + IGroup Update(IGroup previous); + } + + public class ObservableGroupCollection : ObservableCollection where T : IGroup + { + /// + /// Groups + /// + protected virtual IDictionary Groups { get; set; } + + /// + /// Constructor + /// + public ObservableGroupCollection() + { + Groups = new Dictionary(); + } + + /// + /// Grouping implementation + /// + /// + /// + public virtual void Add(IGroup item, TimeSpan? span) + { + var index = item.GetIndex(); + + if (Groups.TryGetValue(index, out var position)) + { + this[position] = (T)item.Update(this[position]); + return; + } + + Groups[index] = Count; + + Add((T)item.Update(null)); + } + } +} diff --git a/Core/Collections/ObservableTimeCollection.cs b/Core/Collections/ObservableTimeCollection.cs deleted file mode 100644 index b05e643ea..000000000 --- a/Core/Collections/ObservableTimeCollection.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Terminal.Core.Extensions; -using Terminal.Core.Models; - -namespace Terminal.Core.Collections -{ - /// - /// Collection with aggregation by date and time - /// - /// - public class ObservableTimeCollection : ObservableCollection where T : PointModel - { - /// - /// Internal tracker to identify new or existing point in time - /// - protected virtual IDictionary Indices { get; set; } - - /// - /// Constructor - /// - public ObservableTimeCollection() - { - Indices = new Dictionary(); - } - - /// - /// Update or add item to the collection depending on its date and time - /// - /// - /// - public virtual void Add(T item, TimeSpan? span) - { - var previous = this.LastOrDefault(); - - if (span is not null && previous is not null) - { - var nextTime = item.Time.Round(span); - var previousTime = previous.Time.Round(span); - - if (Equals(previousTime, nextTime)) - { - this[Count - 1] = item; - return; - } - } - - Add(item); - } - - /// - /// Update or add item to the collection depending on its date and time - /// - /// - /// - /// - public virtual void Add(T item, TimeSpan? span, bool combine) - { - if (span is null) - { - Add(item); - return; - } - - var currentTime = item.Time.Round(span); - var previousTime = (item.Time - span.Value).Round(span); - var currentGroup = Indices.TryGetValue(currentTime.Value.Ticks, out int currentIndex) ? this[currentIndex] : default; - var previousGroup = Indices.TryGetValue(previousTime.Value.Ticks, out int previousIndex) ? this[previousIndex] : default; - var group = CreateGroup(currentGroup, item, previousGroup, span); - - if (group is not null) - { - if (currentGroup is not null) - { - this[currentIndex] = group; - return; - } - - Add(group); - - Indices[currentTime.Value.Ticks] = Count - 1; - } - } - - /// - /// Group items by time - /// - /// - /// - /// - /// - /// - protected virtual T CreateGroup(T currentPoint, T nextPoint, T previousPoint, TimeSpan? span) - { - if (nextPoint.Ask is null && nextPoint.Bid is null) - { - return default; - } - - var nextGroup = (T)nextPoint.Clone(); - - if (currentPoint is not null) - { - nextGroup.AskSize += currentPoint.AskSize ?? 0.0; - nextGroup.BidSize += currentPoint.BidSize ?? 0.0; - nextGroup.Bar.Open = currentPoint.Bar.Open; - } - - nextGroup.Ask = nextPoint.Ask ?? nextPoint.Bid; - nextGroup.Bid = nextPoint.Bid ?? nextPoint.Ask; - nextGroup.Last = nextGroup.Bid ?? nextGroup.Ask; - - nextGroup.Bar.Close = nextGroup.Last; - nextGroup.Bar.Open = nextGroup.Bar.Open ?? previousPoint?.Last ?? nextGroup.Last; - nextGroup.Bar.Low = Math.Min((nextGroup.Bar.Low ?? nextGroup.Last).Value, nextGroup.Last.Value); - nextGroup.Bar.High = Math.Max((nextGroup.Bar.High ?? nextGroup.Last).Value, nextGroup.Last.Value); - - nextGroup.TimeFrame = span; - nextGroup.Time = nextPoint.Time.Round(span); - - return nextGroup; - } - } -} diff --git a/Core/Domains/Instrument.cs b/Core/Domains/Instrument.cs index 1eaeb38bc..b14198e1f 100644 --- a/Core/Domains/Instrument.cs +++ b/Core/Domains/Instrument.cs @@ -49,12 +49,12 @@ public interface IInstrument /// /// List of all ticks from the server /// - ObservableTimeCollection Points { get; set; } + ObservableGroupCollection Points { get; set; } /// /// List of all ticks from the server aggregated into bars /// - ObservableTimeCollection PointGroups { get; set; } + ObservableGroupCollection PointGroups { get; set; } } public class Instrument : IInstrument @@ -102,12 +102,12 @@ public class Instrument : IInstrument /// /// List of all ticks from the server /// - public virtual ObservableTimeCollection Points { get; set; } + public virtual ObservableGroupCollection Points { get; set; } /// /// List of all ticks from the server aggregated into bars /// - public virtual ObservableTimeCollection PointGroups { get; set; } + public virtual ObservableGroupCollection PointGroups { get; set; } /// /// Constructor diff --git a/Core/Models/Points/PointModel.cs b/Core/Models/Points/PointModel.cs index 36c725a07..82559fddc 100644 --- a/Core/Models/Points/PointModel.cs +++ b/Core/Models/Points/PointModel.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using Terminal.Core.Collections; using Terminal.Core.Domains; +using Terminal.Core.Extensions; namespace Terminal.Core.Models { - public class PointModel : ICloneable + public class PointModel : ICloneable, IGroup { /// /// Bid @@ -77,5 +79,39 @@ public virtual object Clone() return clone; } + + /// + /// Grouping index + /// + /// + public virtual long GetIndex() => Time.Value.Ticks; + + /// + /// Grouping implementation + /// + /// + /// + public virtual IGroup Update(IGroup previous) + { + if (previous is not null) + { + var o = previous as PointModel; + var price = (Last ?? Bid ?? Ask ?? o.Last ?? o.Bid ?? o.Ask).Value; + + Ask ??= o.Ask ?? price; + Bid ??= o.Bid ?? price; + AskSize += o.AskSize ?? 0.0; + BidSize += o.BidSize ?? 0.0; + Bar ??= new BarModel(); + Bar.Close = Last = price; + Bar.Open = o.Bar?.Open ?? Bar.Open ?? price; + Bar.Low = Math.Min(Bar.Low ?? price, o.Bar?.Low ?? price); + Bar.High = Math.Max(Bar.High ?? price, o.Bar?.High ?? price); + TimeFrame ??= o.TimeFrame; + Time = Time.Round(TimeFrame) ?? o.Time; + } + + return this; + } } } diff --git a/Core/Services/NotificationService.cs b/Core/Services/NotificationService.cs new file mode 100644 index 000000000..4f2fdc168 --- /dev/null +++ b/Core/Services/NotificationService.cs @@ -0,0 +1,9 @@ +using System; + +namespace Terminal.Core.Services +{ + public class NotificationService + { + public virtual Action OnMessage { get; set; } + } +} diff --git a/Gateways/Alpaca/Libs/Adapter.cs b/Gateways/Alpaca/Libs/Adapter.cs index a29ebf67a..c1956c838 100644 --- a/Gateways/Alpaca/Libs/Adapter.cs +++ b/Gateways/Alpaca/Libs/Adapter.cs @@ -223,13 +223,16 @@ public async Task GetAccountData() var positions = await SendData("/v2/positions"); var orders = await SendData("/v2/orders"); - Account.Balance = account.Data.Equity; - Account.Descriptor = account.Data.AccountNumber; - Account.ActiveOrders = orders.Data.Select(GetInternalOrder).ToDictionary(o => o.Transaction.Id, o => o); - Account.ActivePositions = positions.Data.Select(GetInternalPosition).ToDictionary(o => o.Order.Transaction.Id, o => o); + if (account.Data is not null) + { + Account.Balance = account.Data.Equity; + Account.Descriptor = account.Data.AccountNumber; + Account.ActiveOrders = orders.Data.Select(GetInternalOrder).ToDictionary(o => o.Transaction.Id, o => o); + Account.ActivePositions = positions.Data.Select(GetInternalPosition).ToDictionary(o => o.Order.Transaction.Id, o => o); - Account.ActiveOrders.ForEach(async o => await Subscribe(o.Value.Transaction.Instrument.Name)); - Account.ActivePositions.ForEach(async o => await Subscribe(o.Value.Order.Transaction.Instrument.Name)); + Account.ActiveOrders.ForEach(async o => await Subscribe(o.Value.Transaction.Instrument.Name)); + Account.ActivePositions.ForEach(async o => await Subscribe(o.Value.Order.Transaction.Instrument.Name)); + } } /// diff --git a/Gateways/Simulation/Libs/Adapter.cs b/Gateways/Simulation/Libs/Adapter.cs index 46ecbeeb6..868b396a3 100644 --- a/Gateways/Simulation/Libs/Adapter.cs +++ b/Gateways/Simulation/Libs/Adapter.cs @@ -104,7 +104,7 @@ public override async Task> Subscribe(string name) point.TimeFrame = instrument.TimeFrame; instrument.Points.Add(point); - instrument.PointGroups.Add(point, instrument.TimeFrame, true); + instrument.PointGroups.Add(point, instrument.TimeFrame); } interval.Enabled = true; diff --git a/Gateways/Simulation/Tests/SendPendingOrder.cs b/Gateways/Simulation/Tests/SendPendingOrder.cs index 8751cfba2..9f5ed60e3 100644 --- a/Gateways/Simulation/Tests/SendPendingOrder.cs +++ b/Gateways/Simulation/Tests/SendPendingOrder.cs @@ -44,7 +44,7 @@ public void CreatePendingOrder( Instrument = new Instrument() { Name = "X", - Points = new ObservableTimeCollection + Points = new ObservableGroupCollection { new() { Bid = price, Ask = price } }