From 9b747e53783a3bbc5c1da8788739d978d7115463 Mon Sep 17 00:00:00 2001 From: artemiusgreat Date: Sun, 15 Dec 2024 22:36:38 -0500 Subject: [PATCH] Orders --- Core/Domains/Gateway.cs | 45 ++++++ Core/Enums/ActionEnum.cs | 12 +- Core/Enums/CurrencyEnum.cs | 7 +- Core/Enums/InstrumentEnum.cs | 19 +-- Core/Enums/OperationEnum.cs | 6 +- Core/Enums/OptionSideEnum.cs | 8 +- Core/Enums/OrderInstructionEnum.cs | 8 +- Core/Enums/OrderSideEnum.cs | 6 +- Core/Enums/OrderStatusEnum.cs | 10 +- Core/Enums/OrderTimeSpanEnum.cs | 14 +- Core/Enums/OrderTypeEnum.cs | 10 +- Core/Enums/StatusEnum.cs | 8 +- Core/Models/Transactions/OrderModel.cs | 4 +- Gateway/Alpaca/Libs/Adapter.cs | 41 ++++-- Gateway/Alpaca/Libs/Maps/ExternalMap.cs | 41 ++++-- Gateway/Alpaca/Libs/Maps/InternalMap.cs | 23 ++- Gateway/InteractiveBrokers/Libs/Adapter.cs | 45 ++++-- .../Libs/Maps/ExternalMap.cs | 87 ++++++----- .../Libs/Maps/InternalMap.cs | 1 + Gateway/Schwab/Libs/Adapter.cs | 137 +++++++++++------- Gateway/Schwab/Libs/Maps/ExternalMap.cs | 96 ++++++++---- Gateway/Schwab/Libs/Maps/InternalMap.cs | 3 +- Gateway/Simulation/Libs/Adapter.cs | 41 +++--- Terminal/Pages/Gateways/Alpaca.razor.cs | 77 +++++----- .../Gateways/InteractiveBrokers.razor.cs | 76 +++++----- Terminal/Pages/Gateways/Schwab.razor.cs | 79 +++++----- Terminal/Services/TradeService.cs | 12 ++ 27 files changed, 561 insertions(+), 355 deletions(-) diff --git a/Core/Domains/Gateway.cs b/Core/Domains/Gateway.cs index 8da7ff5c5a..a4b1e7d238 100644 --- a/Core/Domains/Gateway.cs +++ b/Core/Domains/Gateway.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Terminal.Core.Enums; using Terminal.Core.Models; @@ -239,5 +240,49 @@ protected virtual IList SetupAccounts(params IAccount[] accounts) return accounts; } + + /// + /// Create separate orders when combo-orders are not supported + /// + /// + /// + protected virtual IList ComposeOrders(OrderModel order) + { + OrderModel merge(OrderModel subOrder, OrderModel group) + { + var nextOrder = subOrder.Clone() as OrderModel; + var groupOrders = group + ?.Orders + ?.Where(o => o.Instruction is InstructionEnum.Brace) + ?.Where(o => Equals(o.Name, nextOrder.Name)) ?? []; + + nextOrder.Price ??= nextOrder.GetOpenEstimate(); + nextOrder.Type ??= group.Type ?? OrderTypeEnum.Market; + nextOrder.TimeSpan ??= group.TimeSpan ?? OrderTimeSpanEnum.Gtc; + nextOrder.Instruction ??= InstructionEnum.Side; + nextOrder.Transaction.Price ??= nextOrder.Price; + nextOrder.Transaction.Time ??= DateTime.Now; + nextOrder.Transaction.CurrentVolume = nextOrder.Transaction.Volume; + nextOrder.Descriptor = group.Descriptor; + nextOrder.Orders = [.. nextOrder.Orders, .. groupOrders]; + + return nextOrder; + } + + var nextOrders = order + .Orders + .Where(o => o.Instruction is InstructionEnum.Side) + .Select(o => merge(o, order)) + .ToList(); + + if (order.Transaction is not null) + { + nextOrders.Add(merge(order, order)); + } + + order.Orders.Clear(); + + return nextOrders; + } } } diff --git a/Core/Enums/ActionEnum.cs b/Core/Enums/ActionEnum.cs index 47cf0df47e..09c1506cab 100644 --- a/Core/Enums/ActionEnum.cs +++ b/Core/Enums/ActionEnum.cs @@ -2,11 +2,11 @@ namespace Terminal.Core.Enums { public enum ActionEnum : byte { - None = 0, - Create = 1, - Update = 2, - Delete = 3, - Connect = 4, - Disconnect = 5 + None, + Create, + Update, + Delete, + Connect, + Disconnect } } diff --git a/Core/Enums/CurrencyEnum.cs b/Core/Enums/CurrencyEnum.cs index cc14ec2544..673fdc459c 100644 --- a/Core/Enums/CurrencyEnum.cs +++ b/Core/Enums/CurrencyEnum.cs @@ -1,8 +1,9 @@ -namespace Terminal.Core.Enums +namespace Terminal.Core.Enums { public enum CurrencyEnum : byte { - USD = 1, - EUR = 2 + None, + USD, + EUR } } diff --git a/Core/Enums/InstrumentEnum.cs b/Core/Enums/InstrumentEnum.cs index 0cffb8aaaf..0361546337 100644 --- a/Core/Enums/InstrumentEnum.cs +++ b/Core/Enums/InstrumentEnum.cs @@ -2,14 +2,15 @@ namespace Terminal.Core.Enums { public enum InstrumentEnum : byte { - None = 0, - Bonds = 1, - Coins = 2, - Shares = 3, - Options = 4, - Futures = 5, - Contracts = 6, - Currencies = 7, - FuturesOptions = 8 + None, + Bonds, + Coins, + Shares, + Indices, + Options, + Futures, + Contracts, + Currencies, + FuturesOptions } } diff --git a/Core/Enums/OperationEnum.cs b/Core/Enums/OperationEnum.cs index 9bf9bb124a..43b68c3c85 100644 --- a/Core/Enums/OperationEnum.cs +++ b/Core/Enums/OperationEnum.cs @@ -2,8 +2,8 @@ namespace Terminal.Core.Enums { public enum OperationEnum : byte { - None = 0, - In = 1, - Out = 2 + None, + In, + Out } } diff --git a/Core/Enums/OptionSideEnum.cs b/Core/Enums/OptionSideEnum.cs index 930c5979cc..54b4c76276 100644 --- a/Core/Enums/OptionSideEnum.cs +++ b/Core/Enums/OptionSideEnum.cs @@ -2,9 +2,9 @@ namespace Terminal.Core.Enums { public enum OptionSideEnum : byte { - None = 0, - Put = 1, - Call = 2, - Share = 3 + None, + Put, + Call, + Share } } diff --git a/Core/Enums/OrderInstructionEnum.cs b/Core/Enums/OrderInstructionEnum.cs index 6185653fd2..26ff4f311c 100644 --- a/Core/Enums/OrderInstructionEnum.cs +++ b/Core/Enums/OrderInstructionEnum.cs @@ -2,9 +2,9 @@ namespace Terminal.Core.Enums { public enum InstructionEnum : byte { - None = 0, - Side = 1, - Brace = 2, - Group = 3 + None, + Side, + Brace, + Group } } diff --git a/Core/Enums/OrderSideEnum.cs b/Core/Enums/OrderSideEnum.cs index 3f31064645..81dcf2c1d9 100644 --- a/Core/Enums/OrderSideEnum.cs +++ b/Core/Enums/OrderSideEnum.cs @@ -2,8 +2,8 @@ namespace Terminal.Core.Enums { public enum OrderSideEnum : byte { - None = 0, - Buy = 1, - Sell = 2 + None, + Buy, + Sell } } diff --git a/Core/Enums/OrderStatusEnum.cs b/Core/Enums/OrderStatusEnum.cs index 0cf482f5a9..b42d3331b4 100644 --- a/Core/Enums/OrderStatusEnum.cs +++ b/Core/Enums/OrderStatusEnum.cs @@ -2,10 +2,10 @@ namespace Terminal.Core.Enums { public enum OrderStatusEnum : byte { - None = 0, - Filled = 1, - Pending = 2, - Canceled = 3, - Partitioned = 4 + None, + Filled, + Pending, + Canceled, + Partitioned } } diff --git a/Core/Enums/OrderTimeSpanEnum.cs b/Core/Enums/OrderTimeSpanEnum.cs index 590ee2a964..bd1a499d19 100644 --- a/Core/Enums/OrderTimeSpanEnum.cs +++ b/Core/Enums/OrderTimeSpanEnum.cs @@ -2,12 +2,12 @@ namespace Terminal.Core.Enums { public enum OrderTimeSpanEnum : byte { - None = 0, - Day = 1, - Fok = 2, - Gtc = 3, - Ioc = 4, - Am = 5, - Pm = 6 + None, + Day, + Fok, + Gtc, + Ioc, + Am, + Pm } } diff --git a/Core/Enums/OrderTypeEnum.cs b/Core/Enums/OrderTypeEnum.cs index 8bf2dc8d5f..617a8c3985 100644 --- a/Core/Enums/OrderTypeEnum.cs +++ b/Core/Enums/OrderTypeEnum.cs @@ -2,10 +2,10 @@ namespace Terminal.Core.Enums { public enum OrderTypeEnum : byte { - None = 0, - Stop = 1, - Market = 2, - Limit = 3, - StopLimit = 4 + None, + Stop, + Market, + Limit, + StopLimit } } diff --git a/Core/Enums/StatusEnum.cs b/Core/Enums/StatusEnum.cs index f41f71cf52..8e2c1bc154 100644 --- a/Core/Enums/StatusEnum.cs +++ b/Core/Enums/StatusEnum.cs @@ -2,9 +2,9 @@ namespace Terminal.Core.Enums { public enum StatusEnum : byte { - None = 0, - Error = 1, - Success = 2, - Progress = 3 + None, + Error, + Success, + Progress } } diff --git a/Core/Models/Transactions/OrderModel.cs b/Core/Models/Transactions/OrderModel.cs index 7a05e584eb..989a132300 100644 --- a/Core/Models/Transactions/OrderModel.cs +++ b/Core/Models/Transactions/OrderModel.cs @@ -94,8 +94,8 @@ public OrderModel() { Orders = []; OrderStream = o => { }; - Id = $"{Guid.NewGuid():N}".ToUpper(); - Descriptor = $"{Guid.NewGuid():N}".ToUpper(); + Id = $"{Guid.NewGuid()}"; + Descriptor = $"{Guid.NewGuid()}"; } /// diff --git a/Gateway/Alpaca/Libs/Adapter.cs b/Gateway/Alpaca/Libs/Adapter.cs index 332df96533..01cc7d2e08 100644 --- a/Gateway/Alpaca/Libs/Adapter.cs +++ b/Gateway/Alpaca/Libs/Adapter.cs @@ -116,6 +116,8 @@ public override async Task> Subscribe(InstrumentModel async Task subscribe() where T : class, IStreamingDataClient { + await Unsubscribe(instrument); + var client = _streamingClients[instrument.Type.Value] as T; var onPointSub = client.GetQuoteSubscription(instrument.Name); var onTradeSub = client.GetTradeSubscription(instrument.Name); @@ -131,8 +133,6 @@ async Task subscribe() where T : class, IStreamingDataClient try { - await Unsubscribe(instrument); - switch (instrument.Type) { case InstrumentEnum.Coins: await subscribe(); break; @@ -448,7 +448,6 @@ public override async Task>> GetPoints(PointScre /// /// public override async Task>> CreateOrders(params OrderModel[] orders) - { var response = new ResponseModel> { Data = [] }; @@ -456,15 +455,12 @@ public override async Task>> CreateOrders(params { try { - Account.Orders[order.Id] = order; - - var exOrder = ExternalMap.GetOrder(order); - var exResponse = await _tradingClient.PostOrderAsync(exOrder); + var inOrders = ComposeOrders(order); - order.Transaction.Id = $"{exResponse.OrderId}"; - order.Transaction.Status = InternalMap.GetStatus(exResponse.OrderStatus); - - response.Data.Add(order); + foreach (var inOrder in inOrders) + { + response.Data.Add((await CreateOrder(inOrder)).Data); + } } catch (Exception e) { @@ -472,6 +468,8 @@ public override async Task>> CreateOrders(params } } + await GetAccount([]); + return response; } @@ -492,6 +490,27 @@ public override async Task>> DeleteOrders(params return response; } + /// + /// Send order + /// + /// + /// + protected virtual async Task> CreateOrder(OrderModel order) + { + Account.Orders[order.Id] = order; + + await Subscribe(order.Transaction.Instrument); + + var exOrder = ExternalMap.GetOrder(order); + var response = new ResponseModel(); + var exResponse = await _tradingClient.PostOrderAsync(exOrder); + + order.Transaction.Id = $"{exResponse.OrderId}"; + order.Transaction.Status = InternalMap.GetStatus(exResponse.OrderStatus); + + return response; + } + /// /// Process quote from the stream /// diff --git a/Gateway/Alpaca/Libs/Maps/ExternalMap.cs b/Gateway/Alpaca/Libs/Maps/ExternalMap.cs index b08eb78e45..ad0386dda7 100644 --- a/Gateway/Alpaca/Libs/Maps/ExternalMap.cs +++ b/Gateway/Alpaca/Libs/Maps/ExternalMap.cs @@ -8,23 +8,23 @@ namespace Alpaca.Mappers public class ExternalMap { /// - /// Send orders + /// Compose sub order /// - /// + /// /// public static NewOrderRequest GetOrder(OrderModel order) { - var instrument = order.Transaction.Instrument; - var name = instrument.Name; - var volume = OrderQuantity.Fractional((decimal)order.Transaction.Volume); + var action = order.Transaction; + var name = action.Instrument.Name; var side = order.Side is OrderSideEnum.Buy ? OrderSide.Buy : OrderSide.Sell; var orderType = GetOrderType(order.Type); - var duration = GetTimeInForce(order.TimeSpan); + var duration = GetTimeSpan(order.TimeSpan); + var volume = OrderQuantity.Fractional((decimal)action.Volume); var exOrder = new NewOrderRequest(name, volume, side, orderType, duration); var braces = order .Orders .Where(o => o.Instruction is InstructionEnum.Brace) - .Where(o => Equals(o.Name, order.Name)); + .Where(o => Equals(o.Name, name)); exOrder.ClientOrderId = order.Id; @@ -40,29 +40,43 @@ public static NewOrderRequest GetOrder(OrderModel order) if (braces.Any()) { - var TP = GetBracePrice(order, order.Side is OrderSideEnum.Buy ? 1 : -1); - var SL = GetBracePrice(order, order.Side is OrderSideEnum.Buy ? -1 : 1); + var TP = GetBracePrice(order, side is OrderSide.Buy ? 1 : -1); + var SL = GetBracePrice(order, side is OrderSide.Buy ? -1 : 1); exOrder.OrderClass = OrderClass.Bracket; - exOrder.StopLossStopPrice = SL is null ? null : (decimal)SL; - exOrder.TakeProfitLimitPrice = TP is null ? null : (decimal)TP; + exOrder.StopLossStopPrice = (decimal?)SL; + exOrder.TakeProfitLimitPrice = (decimal?)TP; } return exOrder; } + /// + /// Get side + /// + /// + /// + public static OrderSide GetSide(OrderSideEnum? side) + { + if (side is OrderSideEnum.Sell) + { + return OrderSide.Sell; + } + + return OrderSide.Buy; + } + /// /// Get order duration /// /// /// - public static TimeInForce GetTimeInForce(OrderTimeSpanEnum? timeSpan) + public static TimeInForce GetTimeSpan(OrderTimeSpanEnum? timeSpan) { switch (timeSpan) { case OrderTimeSpanEnum.Day: return TimeInForce.Day; case OrderTimeSpanEnum.Fok: return TimeInForce.Fok; - case OrderTimeSpanEnum.Gtc: return TimeInForce.Gtc; case OrderTimeSpanEnum.Ioc: return TimeInForce.Ioc; case OrderTimeSpanEnum.Am: return TimeInForce.Opg; case OrderTimeSpanEnum.Pm: return TimeInForce.Cls; @@ -98,6 +112,7 @@ public static OrderType GetOrderType(OrderTypeEnum? orderType) { var nextOrder = order .Orders + .Where(o => Equals(o.Name, order.Name)) .FirstOrDefault(o => (o.Price - order.Price) * direction > 0); return nextOrder?.Price; diff --git a/Gateway/Alpaca/Libs/Maps/InternalMap.cs b/Gateway/Alpaca/Libs/Maps/InternalMap.cs index 5a06ed362b..9ebb8a341e 100644 --- a/Gateway/Alpaca/Libs/Maps/InternalMap.cs +++ b/Gateway/Alpaca/Libs/Maps/InternalMap.cs @@ -83,7 +83,8 @@ public static OrderModel GetOrder(IOrder message) { var instrument = new InstrumentModel { - Name = message.Symbol + Name = message.Symbol, + Type = GetInstrumentType(message.AssetClass) }; var action = new TransactionModel @@ -144,8 +145,9 @@ public static OrderModel GetPosition(IPosition message) var instrument = new InstrumentModel { + Point = point, Name = message.Symbol, - Point = point + Type = GetInstrumentType(message.AssetClass) }; var action = new TransactionModel @@ -266,6 +268,23 @@ public static OrderModel GetPosition(IPosition message) return null; } + /// + /// Asset type + /// + /// + /// + public static InstrumentEnum? GetInstrumentType(AssetClass assetType) + { + switch (assetType) + { + case AssetClass.Crypto: return InstrumentEnum.Coins; + case AssetClass.UsEquity: return InstrumentEnum.Shares; + case AssetClass.UsOption: return InstrumentEnum.Options; + } + + return null; + } + /// /// Get value or default /// diff --git a/Gateway/InteractiveBrokers/Libs/Adapter.cs b/Gateway/InteractiveBrokers/Libs/Adapter.cs index f4f92a4e85..b1caa8bb0b 100644 --- a/Gateway/InteractiveBrokers/Libs/Adapter.cs +++ b/Gateway/InteractiveBrokers/Libs/Adapter.cs @@ -8,8 +8,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.Metrics; +using System.Drawing; using System.Linq; using System.Threading.Tasks; +using System.Xml.Linq; using Terminal.Core.Domains; using Terminal.Core.Enums; using Terminal.Core.Extensions; @@ -127,6 +129,7 @@ public override async Task> Subscribe(InstrumentModel point.Time ??= DateTime.Now; point.Instrument = instrument; + instrument.Point = point; instrument.Points.Add(point); instrument.PointGroups.Add(point, instrument.TimeFrame); @@ -312,9 +315,23 @@ public override async Task>> CreateOrders(params foreach (var order in orders) { - response.Data.Add((await CreateOrder(order)).Data); + try + { + var inOrders = ComposeOrders(order); + + foreach (var inOrder in inOrders) + { + response.Data.Add((await CreateOrder(inOrder)).Data); + } + } + catch (Exception e) + { + response.Errors.Add(new ErrorModel { ErrorMessage = $"{e}" }); + } } + await GetAccount([]); + return response; } @@ -679,10 +696,14 @@ protected virtual Task> CreateReader() /// protected virtual async Task> CreateOrder(OrderModel order) { + Account.Orders[order.Id] = order; + + await Subscribe(order.Transaction.Instrument); + + var orderId = _client.NextOrderId++; var response = new ResponseModel(); var source = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var orderId = _client.NextOrderId++; - var exOrder = ExternalMap.GetOrder(orderId, order, _contracts); + var exOrders = ExternalMap.GetOrders(orderId, order, _contracts); var exResponse = null as OpenOrderMessage; void subscribe(OpenOrderMessage message) @@ -691,6 +712,7 @@ void subscribe(OpenOrderMessage message) { exResponse = message; unsubscribe(); + source.TrySetResult(); } } @@ -698,24 +720,21 @@ void unsubscribe() { _client.OpenOrder -= subscribe; _client.OpenOrderEnd -= unsubscribe; - source.TrySetResult(); } _client.OpenOrder += subscribe; _client.OpenOrderEnd += unsubscribe; - _client.ClientSocket.placeOrder(orderId, exOrder.Contract, exOrder.Order); + + foreach (var exOrder in exOrders) + { + _client.ClientSocket.placeOrder(exOrder.Order.OrderId, exOrder.Contract, exOrder.Order); + } await await Task.WhenAny(source.Task, Task.Delay(Timeout)); response.Data = order; - - if (string.Equals(exResponse.OrderState.Status, "Submitted", StringComparison.InvariantCultureIgnoreCase)) - { - response.Data.Transaction.Id = $"{orderId}"; - response.Data.Transaction.Status = InternalMap.GetOrderStatus(exResponse.OrderState.Status); - - Account.Orders[order.Id] = order; - } + response.Data.Transaction.Id = $"{exResponse.OrderId}"; + response.Data.Transaction.Status = InternalMap.GetOrderStatus(exResponse.OrderState.Status); return response; } diff --git a/Gateway/InteractiveBrokers/Libs/Maps/ExternalMap.cs b/Gateway/InteractiveBrokers/Libs/Maps/ExternalMap.cs index 6d8cd4b750..01781f69cf 100644 --- a/Gateway/InteractiveBrokers/Libs/Maps/ExternalMap.cs +++ b/Gateway/InteractiveBrokers/Libs/Maps/ExternalMap.cs @@ -3,6 +3,10 @@ using InteractiveBrokers.Messages; using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Linq; +using System.Xml.Linq; using Terminal.Core.Domains; using Terminal.Core.Enums; using Terminal.Core.Extensions; @@ -19,18 +23,40 @@ public class ExternalMap /// /// /// - public static OpenOrderMessage GetOrder(int orderId, OrderModel orderModel, IDictionary contracts) + public static IList GetOrders(int orderId, OrderModel orderModel, IDictionary contracts) { - var order = GetSubOrder(orderId, orderModel); + var response = new List(); + var order = new Order(); var action = orderModel.Transaction; var instrument = action.Instrument; var contract = contracts.Get(instrument.Name) ?? GetContract(action.Instrument); - return new OpenOrderMessage + order.OrderId = orderId; + order.Action = GetSide(orderModel.Side); + order.Tif = GetTimeSpan(orderModel.TimeSpan); + order.OrderType = GetOrderType(orderModel.Type); + order.TotalQuantity = (decimal)orderModel.Transaction.Volume; + + switch (orderModel.Type) { - Order = order, + case OrderTypeEnum.Stop: order.AuxPrice = orderModel.Price.Value; break; + case OrderTypeEnum.Limit: order.LmtPrice = orderModel.Price.Value; break; + case OrderTypeEnum.StopLimit: + order.LmtPrice = orderModel.Price.Value; + order.AuxPrice = orderModel.ActivationPrice.Value; + break; + } + + var TP = GetBracePrice(orderModel, orderModel.Side is OrderSideEnum.Buy ? 1 : -1); + var SL = GetBracePrice(orderModel, orderModel.Side is OrderSideEnum.Buy ? -1 : 1); + + response = [.. GetBraces(order, SL, TP).Select(o => new OpenOrderMessage + { + Order = o, Contract = contract - }; + })]; + + return response; } /// @@ -132,6 +158,7 @@ public static string GetInstrumentType(InstrumentEnum? message) { case InstrumentEnum.Bonds: return "BOND"; case InstrumentEnum.Shares: return "STK"; + case InstrumentEnum.Indices: return "IND"; case InstrumentEnum.Options: return "OPT"; case InstrumentEnum.Futures: return "FUT"; case InstrumentEnum.Contracts: return "CFD"; @@ -172,42 +199,14 @@ public static string GetExpiration(DateTime? date) return null; } - /// - /// Basic order - /// - /// - /// - /// - public static Order GetSubOrder(int id, OrderModel orderModel) - { - var order = new Order(); - - order.OrderId = id; - order.Action = GetSide(orderModel.Side); - order.OrderType = GetOrderType(orderModel.Type); - order.TotalQuantity = (decimal)orderModel.Transaction.Volume; - - switch (orderModel.Type) - { - case OrderTypeEnum.Stop: order.AuxPrice = orderModel.Price.Value; break; - case OrderTypeEnum.Limit: order.LmtPrice = orderModel.Price.Value; break; - case OrderTypeEnum.StopLimit: - order.LmtPrice = orderModel.Price.Value; - order.AuxPrice = orderModel.ActivationPrice.Value; - break; - } - - return order; - } - /// /// Bracket template /// /// - /// - /// + /// + /// /// - public static List GetBraces(Order order, double? stopPrice, double? takePrice) + public static IList GetBraces(Order order, double? stopPrice, double? takePrice) { var orders = new List { order }; @@ -247,5 +246,21 @@ public static List GetBraces(Order order, double? stopPrice, double? take return orders; } + + /// + /// Get price for brackets + /// + /// + /// + /// + public static double? GetBracePrice(OrderModel order, double direction) + { + var nextOrder = order + .Orders + .Where(o => Equals(o.Name, order.Name)) + .FirstOrDefault(o => (o.Price - order.Price) * direction > 0); + + return nextOrder?.Price; + } } } diff --git a/Gateway/InteractiveBrokers/Libs/Maps/InternalMap.cs b/Gateway/InteractiveBrokers/Libs/Maps/InternalMap.cs index 222365bfa4..2345201e57 100644 --- a/Gateway/InteractiveBrokers/Libs/Maps/InternalMap.cs +++ b/Gateway/InteractiveBrokers/Libs/Maps/InternalMap.cs @@ -174,6 +174,7 @@ public static OrderModel GetPosition(PositionMultiMessage message) { case "BOND": return InstrumentEnum.Bonds; case "STK": return InstrumentEnum.Shares; + case "IND": return InstrumentEnum.Indices; case "OPT": return InstrumentEnum.Options; case "FUT": return InstrumentEnum.Futures; case "CFD": return InstrumentEnum.Contracts; diff --git a/Gateway/Schwab/Libs/Adapter.cs b/Gateway/Schwab/Libs/Adapter.cs index 6c633cf123..54759062ca 100644 --- a/Gateway/Schwab/Libs/Adapter.cs +++ b/Gateway/Schwab/Libs/Adapter.cs @@ -5,7 +5,6 @@ using Schwab.Messages; using System; using System.Collections; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -115,6 +114,9 @@ public override async Task> Connect() _sender = new Service(); await UpdateToken("/v1/oauth/token"); + + _accountCode = (await GetAccountCode()).Data; + await GetAccount([]); _streamer = await GetConnection(ws, scheduler); @@ -147,14 +149,16 @@ public override async Task> Connect() /// public override async Task> Subscribe(InstrumentModel instrument) { - var response = new ResponseModel(); + var response = new ResponseModel + { + Data = StatusEnum.Success + }; try { - await Unsubscribe(instrument); - var streamData = _userData.Streamer.FirstOrDefault(); + await Unsubscribe(instrument); await SendStream(_streamer, new StreamInputMessage { Requestid = ++_counter, @@ -168,11 +172,10 @@ public override async Task> Subscribe(InstrumentModel Fields = string.Join(",", Enumerable.Range(0, 10)) } }); - - response.Data = StatusEnum.Success; } catch (Exception e) { + response.Data = default; response.Errors.Add(new ErrorModel { ErrorMessage = $"{e}" }); } @@ -239,16 +242,13 @@ public override async Task>> GetOptions(Opt var optionResponse = await SendData($"/marketdata/v1/chains?{props}"); - if (optionResponse.Data is not null) - { - response.Data = optionResponse - .Data - .PutExpDateMap - ?.Concat(optionResponse.Data.CallExpDateMap) - ?.SelectMany(dateMap => dateMap.Value.SelectMany(o => o.Value)) - ?.Select(option => InternalMap.GetOption(option, optionResponse.Data)) - ?.ToList() ?? []; - } + response.Data = optionResponse + .Data + .PutExpDateMap + ?.Concat(optionResponse.Data.CallExpDateMap) + ?.SelectMany(dateMap => dateMap.Value.SelectMany(o => o.Value)) + ?.Select(option => InternalMap.GetOption(option, optionResponse.Data)) + ?.ToList() ?? []; } catch (Exception e) { @@ -343,7 +343,14 @@ public override async Task>> CreateOrders(params foreach (var order in orders) { - response.Data.Add((await CreateOrder(order)).Data); + try + { + response.Data.Add((await CreateOrder(order)).Data); + } + catch (Exception e) + { + response.Errors.Add(new ErrorModel { ErrorMessage = $"{e}" }); + } } return response; @@ -360,7 +367,14 @@ public override async Task>> DeleteOrders(params foreach (var order in orders) { - response.Data.Add((await DeleteOrder(order)).Data); + try + { + response.Data.Add((await DeleteOrder(order)).Data); + } + catch (Exception e) + { + response.Errors.Add(new ErrorModel { ErrorMessage = $"{e}" }); + } } return response; @@ -378,10 +392,6 @@ public override async Task> GetAccount(Hashtable criteri try { var accountProps = new Hashtable { ["fields"] = "positions" }; - var accountNumbers = await SendData("/trader/v1/accounts/accountNumbers"); - - _accountCode = accountNumbers.Data.First(o => Equals(o.AccountNumber, Account.Descriptor)).HashValue; - var account = await SendData($"/trader/v1/accounts/{_accountCode}?{accountProps.Query()}"); var orders = await GetOrders(null, criteria); var positions = await GetPositions(null, criteria); @@ -426,9 +436,12 @@ public override async Task>> GetOrders(OrderScre }.Merge(criteria); - var items = await SendData($"/trader/v1/accounts/{_accountCode}/orders?{props}"); + var orders = await SendData($"/trader/v1/accounts/{_accountCode}/orders?{props}"); - response.Data = [.. items.Data.Where(o => o.CloseTime is null).Select(InternalMap.GetOrder)]; + response.Data = [.. orders + .Data + .Where(o => o.CloseTime is null) + .Select(InternalMap.GetOrder)]; } catch (Exception e) { @@ -467,6 +480,29 @@ public override async Task>> GetPositions(Positi return response; } + /// + /// Sync open balance, order, and positions + /// + /// + /// + protected virtual async Task> GetAccountCode() + { + var response = new ResponseModel(); + + try + { + var accountNumbers = await SendData("/trader/v1/accounts/accountNumbers"); + + response.Data = accountNumbers.Data.First(o => Equals(o.AccountNumber, Account.Descriptor)).HashValue; + } + catch (Exception e) + { + response.Errors = [new ErrorModel { ErrorMessage = $"{e}" }]; + } + + return response; + } + /// /// Send data to web socket stream /// @@ -533,17 +569,11 @@ protected virtual async Task GetConnection(ClientWebSocket ws, }); var adminResponse = await ReceiveStream(ws); - var adminCode = adminResponse?.Response?.FirstOrDefault()?.Content?.Code; + var adminCode = adminResponse.Response.FirstOrDefault().Content.Code; var pointMap = Account .Instruments .ToDictionary(o => o.Key, o => new PointModel()); - if (adminCode is not 0) - { - InstanceService.Instance.OnMessage(new MessageModel { Message = "No stream" }); - return ws; - } - scheduler.Send(async () => { while (ws.State is WebSocketState.Open) @@ -623,7 +653,7 @@ protected virtual void OnPoint(IDictionary pointMap, IEnumer /// /// /// - /// + /// /// protected virtual async Task> SendData(string source, HttpMethod verb = null, object content = null) { @@ -689,6 +719,11 @@ protected async Task> GetUserData() var response = new ResponseModel(); var userResponse = await SendData($"/trader/v1/userPreference"); + if (string.IsNullOrEmpty(userResponse.Error) is false) + { + response.Errors = [new ErrorModel { ErrorMessage = userResponse.Error }]; + } + _userData = response.Data = userResponse.Data; return response; @@ -701,31 +736,31 @@ protected async Task> GetUserData() /// protected virtual async Task> CreateOrder(OrderModel order) { - var inResponse = new ResponseModel(); - Account.Orders[order.Id] = order; + await Subscribe(order.Transaction.Instrument); + var exOrder = ExternalMap.GetOrder(order); + var response = new ResponseModel(); var exResponse = await SendData($"/trader/v1/accounts/{_accountCode}/orders", HttpMethod.Post, exOrder); - inResponse.Data = order; - if (exResponse.Message.Headers.TryGetValues("Location", out var orderData)) { var orderItem = orderData.First(); - var orderId = $"{orderItem[(orderItem.LastIndexOf('/') + 1)..]}"; - if (string.IsNullOrEmpty(orderId)) - { - inResponse.Errors.Add(new ErrorModel { ErrorMessage = $"{exResponse.Message.StatusCode}" }); - return inResponse; - } + response.Data = order; + response.Data.Transaction.Status = OrderStatusEnum.Filled; + response.Data.Transaction.Id = $"{orderItem[(orderItem.LastIndexOf('/') + 1)..]}"; + } - inResponse.Data.Transaction.Id = orderId; - inResponse.Data.Transaction.Status = OrderStatusEnum.Filled; + if (string.IsNullOrEmpty(response?.Data?.Transaction?.Id)) + { + response.Errors.Add(new ErrorModel { ErrorMessage = $"{exResponse.Message.StatusCode}" }); } - return inResponse; + await GetAccount([]); + + return response; } /// @@ -735,19 +770,19 @@ protected virtual async Task> CreateOrder(OrderModel o /// protected virtual async Task> DeleteOrder(OrderModel order) { - var inResponse = new ResponseModel(); + var response = new ResponseModel(); var exResponse = await SendData($"/trader/v1/accounts/{_accountCode}/orders/{order.Transaction.Id}", HttpMethod.Delete); if ((int)exResponse.Message.StatusCode >= 400) { - inResponse.Errors.Add(new ErrorModel { ErrorMessage = $"{exResponse.Message.StatusCode}" }); - return inResponse; + response.Errors.Add(new ErrorModel { ErrorMessage = $"{exResponse.Message.StatusCode}" }); + return response; } - inResponse.Data = order; - inResponse.Data.Transaction.Status = OrderStatusEnum.Canceled; + response.Data = order; + response.Data.Transaction.Status = OrderStatusEnum.Canceled; - return inResponse; + return response; } } } diff --git a/Gateway/Schwab/Libs/Maps/ExternalMap.cs b/Gateway/Schwab/Libs/Maps/ExternalMap.cs index 77f233f57a..4bbf0916f8 100644 --- a/Gateway/Schwab/Libs/Maps/ExternalMap.cs +++ b/Gateway/Schwab/Libs/Maps/ExternalMap.cs @@ -18,26 +18,17 @@ public static OrderMessage GetOrder(OrderModel order) var action = order.Transaction; var message = new OrderMessage { - Duration = "DAY", Session = "NORMAL", - OrderType = "MARKET", - OrderStrategyType = "SINGLE" + OrderStrategyType = "SINGLE", + OrderType = GetOrderType(order.Type), + Duration = GetTimeSpan(order.TimeSpan) }; switch (order.Type) { - case OrderTypeEnum.Stop: - message.OrderType = "STOP"; - message.StopPrice = order.Price; - break; - - case OrderTypeEnum.Limit: - message.OrderType = "LIMIT"; - message.Price = order.Price; - break; - + case OrderTypeEnum.Stop: message.StopPrice = order.Price; break; + case OrderTypeEnum.Limit: message.Price = order.Price; break; case OrderTypeEnum.StopLimit: - message.OrderType = "STOP_LIMIT"; message.Price = order.Price; message.StopPrice = order.ActivationPrice; break; @@ -46,7 +37,7 @@ public static OrderMessage GetOrder(OrderModel order) message.OrderLegCollection = order .Orders .Where(o => o.Instruction is InstructionEnum.Side) - .Select(GetOrderItem) + .Select(o => GetSubOrder(o, order)) .ToList(); message.ChildOrderStrategies = order @@ -56,14 +47,14 @@ public static OrderMessage GetOrder(OrderModel order) .Select(o => { var subOrder = GetOrder(o); - subOrder.OrderLegCollection = [GetOrderItem(o)]; + subOrder.OrderLegCollection = [GetSubOrder(o, order)]; return subOrder; }) .ToList(); if (order?.Transaction?.Volume is not 0) { - message.OrderLegCollection.Add(GetOrderItem(order)); + message.OrderLegCollection.Add(GetSubOrder(order)); } if (message.ChildOrderStrategies.Count is not 0) @@ -74,6 +65,23 @@ public static OrderMessage GetOrder(OrderModel order) return message; } + /// + /// Order side + /// + /// + /// + public static string GetOrderType(OrderTypeEnum? message) + { + switch (message) + { + case OrderTypeEnum.Stop: return "STOP"; + case OrderTypeEnum.Limit: return "LIMIT"; + case OrderTypeEnum.StopLimit: return "STOP_LIMIT"; + } + + return "MARKET"; + } + /// /// Service name based on asset type /// @@ -93,41 +101,73 @@ public static string GetStreamingService(InstrumentModel instrument) return null; } + /// + /// Convert local time in force to remote + /// + /// + /// + public static string GetTimeSpan(OrderTimeSpanEnum? span) + { + switch (span) + { + case OrderTimeSpanEnum.Day: return "DAY"; + case OrderTimeSpanEnum.Fok: return "FILL_OR_KILL"; + } + + return "GOOD_TILL_CANCEL"; + } + /// /// Create leg in a combo-order /// /// + /// /// - public static OrderLegMessage GetOrderItem(OrderModel order) + public static OrderLegMessage GetSubOrder(OrderModel order, OrderModel group = null) { + var action = order?.Transaction ?? group?.Transaction; + var assetType = action?.Instrument?.Type ?? group?.Transaction?.Instrument?.Type; + var side = order?.Side ?? group?.Side; var instrument = new InstrumentMessage { - AssetType = GetInstrumentType(order.Transaction.Instrument.Type), - Symbol = order.Transaction.Instrument.Name + AssetType = GetInstrumentType(assetType), + Symbol = action.Instrument.Name }; var response = new OrderLegMessage { Instrument = instrument, Quantity = order.Transaction.Volume, + Instruction = GetSide(assetType, side) }; - switch (order.Side) + return response; + } + + /// + /// Get external instrument type + /// + /// + /// + /// + public static string GetSide(InstrumentEnum? assetType, OrderSideEnum? side) + { + switch (side) { - case OrderSideEnum.Buy: response.Instruction = "BUY"; break; - case OrderSideEnum.Sell: response.Instruction = "SELL"; break; + case OrderSideEnum.Buy: return "BUY"; + case OrderSideEnum.Sell: return "SELL"; } - if (order.Transaction.Instrument.Type is InstrumentEnum.Options) + if (assetType is InstrumentEnum.Options) { - switch (order.Side) + switch (side) { - case OrderSideEnum.Buy: response.Instruction = "BUY_TO_OPEN"; break; - case OrderSideEnum.Sell: response.Instruction = "SELL_TO_OPEN"; break; + case OrderSideEnum.Buy: return "BUY_TO_OPEN"; + case OrderSideEnum.Sell: return "SELL_TO_OPEN"; } } - return response; + return null; } /// diff --git a/Gateway/Schwab/Libs/Maps/InternalMap.cs b/Gateway/Schwab/Libs/Maps/InternalMap.cs index 81d99d47f9..094aeef2de 100644 --- a/Gateway/Schwab/Libs/Maps/InternalMap.cs +++ b/Gateway/Schwab/Libs/Maps/InternalMap.cs @@ -5,7 +5,6 @@ using Terminal.Core.Domains; using Terminal.Core.Enums; using Terminal.Core.Models; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Schwab.Mappers { @@ -59,12 +58,12 @@ public static IDictionary GetStreamMap(string assetType) switch (assetType) { case "ETF": - case "INDEX": case "EQUITY": case "EXTENDED": case "INDICATOR": case "FUNDAMENTAL": case "MUTUAL_FUND": return InstrumentEnum.Shares; + case "INDEX": return InstrumentEnum.Indices; case "BOND": return InstrumentEnum.Bonds; case "FOREX": return InstrumentEnum.Currencies; case "FUTURE": return InstrumentEnum.Futures; diff --git a/Gateway/Simulation/Libs/Adapter.cs b/Gateway/Simulation/Libs/Adapter.cs index f19bfce524..3ba9663a53 100644 --- a/Gateway/Simulation/Libs/Adapter.cs +++ b/Gateway/Simulation/Libs/Adapter.cs @@ -193,14 +193,14 @@ public override Task>> CreateOrders(params Order .Errors .Select(error => new ErrorModel { ErrorMessage = error.ErrorMessage }))]; - if (response.Errors.Count > 0) + if (response.Errors.Count is not 0) { return Task.FromResult(response); } foreach (var order in orders) { - var nextOrders = CreateGroup(order); + var nextOrders = ComposeOrders(order); foreach (var nextOrder in nextOrders) { @@ -297,40 +297,39 @@ protected virtual OrderModel SendOrder(OrderModel order) /// /// /// - protected virtual IList CreateGroup(OrderModel order) + protected virtual IList ComposeOrders(OrderModel order) { - OrderModel updateOrder(OrderModel o, OrderModel group = null) + OrderModel merge(OrderModel o, OrderModel group) { var nextOrder = o.Clone() as OrderModel; - nextOrder.Price = nextOrder.GetOpenEstimate(); - nextOrder.Type ??= group?.Type ?? OrderTypeEnum.Market; - nextOrder.TimeSpan ??= group?.TimeSpan ?? OrderTimeSpanEnum.Gtc; + nextOrder.Price ??= nextOrder.GetOpenEstimate(); + nextOrder.Type ??= group.Type ?? OrderTypeEnum.Market; + nextOrder.TimeSpan ??= group.TimeSpan ?? OrderTimeSpanEnum.Gtc; nextOrder.Instruction ??= InstructionEnum.Side; nextOrder.Transaction.Time ??= DateTime.Now; + nextOrder.Transaction.Price ??= nextOrder.Price; nextOrder.Transaction.Status = OrderStatusEnum.Filled; nextOrder.Transaction.CurrentVolume = nextOrder.Transaction.Volume; - nextOrder.Transaction.Price = nextOrder.Price; + nextOrder.Descriptor = group.Descriptor; return nextOrder; } - var nextOrders = new List(); + var nextOrders = order + .Orders + .Where(o => o.Instruction is InstructionEnum.Side) + .Select(o => merge(o, order)) + .ToList(); - if (order.Transaction is not null) - { - nextOrders.Add(updateOrder(order)); - } + order + .Orders + .Where(o => o.Instruction is InstructionEnum.Brace) + .ForEach(o => SendPendingOrder(o)); - foreach (var o in order.Orders) + if (order.Transaction is not null) { - o.Descriptor = order.Descriptor; - - switch (o.Instruction) - { - case InstructionEnum.Side: nextOrders.Add(updateOrder(o, order)); break; - case InstructionEnum.Brace: SendPendingOrder(o); break; - } + nextOrders.Add(merge(order, order)); } order.Orders.Clear(); diff --git a/Terminal/Pages/Gateways/Alpaca.razor.cs b/Terminal/Pages/Gateways/Alpaca.razor.cs index a648db7f67..2889ffea38 100644 --- a/Terminal/Pages/Gateways/Alpaca.razor.cs +++ b/Terminal/Pages/Gateways/Alpaca.razor.cs @@ -12,6 +12,7 @@ using Terminal.Core.Enums; using Terminal.Core.Indicators; using Terminal.Core.Models; +using Terminal.Services; namespace Terminal.Pages.Gateways { @@ -19,13 +20,11 @@ public partial class Alpaca { [Inject] IConfiguration Configuration { get; set; } - protected virtual int Counter { get; set; } - protected virtual PageComponent View { get; set; } - protected virtual PerformanceIndicator Performance { get; set; } - protected virtual InstrumentModel Instrument { get; set; } = new InstrumentModel + protected PageComponent View { get; set; } + protected PerformanceIndicator Performance { get; set; } + protected InstrumentModel Instrument { get; set; } = new InstrumentModel { - Name = "BTC/USD", - Exchange = "SMART", + Name = "DOGE/USD", Type = InstrumentEnum.Coins, TimeFrame = TimeSpan.FromMinutes(1) }; @@ -89,23 +88,30 @@ protected virtual void CreateAccounts() }); } - private async Task OnData(PointModel point) + protected async Task OnData(PointModel point) { - Counter = (Counter + 1) % 100; - + var name = Instrument.Name.Replace("/", string.Empty); var account = View.Adapters["Prime"].Account; var instrument = account.Instruments[Instrument.Name]; var performance = Performance.Calculate([account]); + var openOrders = account.Orders.Values.Where(o => Equals(o.Name, name)); + var openPositions = account.Positions.Values.Where(o => Equals(o.Name, name)); - if (account.Orders.IsEmpty && account.Positions.IsEmpty) + if (openOrders.IsEmpty() && openPositions.IsEmpty()) { await OpenPositions(Instrument, 1); - } + await TradeService.Done(async () => + { + var position = account + .Positions + .Values + .Where(o => Equals(o.Name, name)) + .First(); - if (Counter is 0 && account.Positions.IsEmpty is false) - { - await ClosePositions(); - await OpenPositions(Instrument, account.Positions.First().Value.Side is OrderSideEnum.Buy ? -1 : 1); + await ClosePositions(name); + await OpenPositions(Instrument, position.Side is OrderSideEnum.Buy ? -1 : 1); + + }, 10000); } View.ChartsView.UpdateItems(point.Time.Value.Ticks, "Prices", "Bars", View.ChartsView.GetShape(point)); @@ -116,39 +122,32 @@ private async Task OnData(PointModel point) View.PositionsView.UpdateItems(account.Positions.Values); } - private double? GetPrice(double direction) => direction > 0 ? + protected double? GetPrice(double direction) => direction > 0 ? Instrument.Point.Ask : Instrument.Point.Bid; - private async Task OpenPositions(InstrumentModel instrument, double direction) + protected async Task OpenPositions(InstrumentModel instrument, double direction) { + var adapter = View.Adapters["Prime"]; var side = direction > 0 ? OrderSideEnum.Buy : OrderSideEnum.Sell; var stopSide = direction < 0 ? OrderSideEnum.Buy : OrderSideEnum.Sell; - var adapter = View.Adapters["Prime"]; + var TP = new OrderModel { - Price = GetPrice(direction) + 15 * direction, Side = stopSide, Type = OrderTypeEnum.Limit, Instruction = InstructionEnum.Brace, - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Price = GetPrice(direction) + 15 * direction, + Transaction = new() { Volume = 10, Instrument = instrument } }; var SL = new OrderModel { - Price = GetPrice(-direction) - 15 * direction, Side = stopSide, Type = OrderTypeEnum.Stop, Instruction = InstructionEnum.Brace, - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Price = GetPrice(-direction) - 15 * direction, + Transaction = new() { Volume = 10, Instrument = instrument } }; var order = new OrderModel @@ -156,32 +155,28 @@ private async Task OpenPositions(InstrumentModel instrument, double direction) Price = GetPrice(direction), Side = side, Type = OrderTypeEnum.Market, - //Orders = [SL, TP], - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Transaction = new() { Volume = 10, Instrument = instrument } + //Orders = [SL, TP] }; await adapter.CreateOrders(order); } - private async Task ClosePositions() + protected async Task ClosePositions(string name) { var adapter = View.Adapters["Prime"]; - foreach (var position in adapter.Account.Positions) + foreach (var position in adapter.Account.Positions.Values.Where(o => Equals(name, o.Name))) { - var side = position.Value.Side is OrderSideEnum.Buy ? OrderSideEnum.Sell : OrderSideEnum.Buy; + var side = position.Side is OrderSideEnum.Buy ? OrderSideEnum.Sell : OrderSideEnum.Buy; var order = new OrderModel { Side = side, Type = OrderTypeEnum.Market, Transaction = new() { - Volume = position.Value.Transaction.Volume, - Instrument = position.Value.Transaction.Instrument + Volume = position.Transaction.Volume, + Instrument = position.Transaction.Instrument } }; diff --git a/Terminal/Pages/Gateways/InteractiveBrokers.razor.cs b/Terminal/Pages/Gateways/InteractiveBrokers.razor.cs index f7b657a483..7006fece0e 100644 --- a/Terminal/Pages/Gateways/InteractiveBrokers.razor.cs +++ b/Terminal/Pages/Gateways/InteractiveBrokers.razor.cs @@ -1,7 +1,7 @@ using Canvas.Core.Shapes; +using InteractiveBrokers; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Configuration; -using InteractiveBrokers; using System; using System.Collections.Concurrent; using System.Linq; @@ -11,6 +11,7 @@ using Terminal.Core.Enums; using Terminal.Core.Indicators; using Terminal.Core.Models; +using Terminal.Services; namespace Terminal.Pages.Gateways { @@ -18,9 +19,8 @@ public partial class InteractiveBrokers { [Inject] IConfiguration Configuration { get; set; } - protected virtual int Counter { get; set; } - protected virtual PageComponent View { get; set; } - protected virtual PerformanceIndicator Performance { get; set; } + protected PageComponent View { get; set; } + protected PerformanceIndicator Performance { get; set; } protected virtual InstrumentModel Instrument { get; set; } = new InstrumentModel { Id = "495512557", @@ -85,23 +85,30 @@ protected virtual void CreateAccounts() }); } - private async Task OnData(PointModel point) + protected async Task OnData(PointModel point) { - Counter = (Counter + 1) % 100; - + var name = Instrument.Name; var account = View.Adapters["Prime"].Account; - var instrument = account.Instruments[Instrument.Name]; + var instrument = account.Instruments[name]; var performance = Performance.Calculate([account]); + var openOrders = account.Orders.Values.Where(o => Equals(o.Name, name)); + var openPositions = account.Positions.Values.Where(o => Equals(o.Name, name)); - if (account.Orders.IsEmpty && account.Positions.IsEmpty) + if (openOrders.IsEmpty() && openPositions.IsEmpty()) { await OpenPositions(Instrument, 1); - } + await TradeService.Done(async () => + { + var position = account + .Positions + .Values + .Where(o => Equals(o.Name, name)) + .First(); - if (Counter is 0 && account.Positions.IsEmpty is false) - { - await ClosePositions(); - await OpenPositions(Instrument, account.Positions.First().Value.Side is OrderSideEnum.Buy ? -1 : 1); + await ClosePositions(name); + await OpenPositions(Instrument, position.Side is OrderSideEnum.Buy ? -1 : 1); + + }, 10000); } View.ChartsView.UpdateItems(point.Time.Value.Ticks, "Prices", "Bars", View.ChartsView.GetShape(point)); @@ -112,39 +119,32 @@ private async Task OnData(PointModel point) View.PositionsView.UpdateItems(account.Positions.Values); } - private double? GetPrice(double direction) => direction > 0 ? + protected double? GetPrice(double direction) => direction > 0 ? Instrument.Point.Ask : Instrument.Point.Bid; - private async Task OpenPositions(InstrumentModel instrument, double direction) + protected async Task OpenPositions(InstrumentModel instrument, double direction) { + var adapter = View.Adapters["Prime"]; var side = direction > 0 ? OrderSideEnum.Buy : OrderSideEnum.Sell; var stopSide = direction < 0 ? OrderSideEnum.Buy : OrderSideEnum.Sell; - var adapter = View.Adapters["Prime"]; + var TP = new OrderModel { - Price = GetPrice(direction) + 15 * direction, Side = stopSide, Type = OrderTypeEnum.Limit, Instruction = InstructionEnum.Brace, - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Price = GetPrice(direction) + 15 * direction, + Transaction = new() { Volume = 1, Instrument = instrument } }; var SL = new OrderModel { - Price = GetPrice(-direction) - 15 * direction, Side = stopSide, Type = OrderTypeEnum.Stop, Instruction = InstructionEnum.Brace, - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Price = GetPrice(-direction) - 15 * direction, + Transaction = new() { Volume = 1, Instrument = instrument } }; var order = new OrderModel @@ -152,32 +152,28 @@ private async Task OpenPositions(InstrumentModel instrument, double direction) Price = GetPrice(direction), Side = side, Type = OrderTypeEnum.Market, - Orders = [SL, TP], - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Transaction = new() { Volume = 1, Instrument = instrument }, + Orders = [SL, TP] }; await adapter.CreateOrders(order); } - private async Task ClosePositions() + protected async Task ClosePositions(string name) { var adapter = View.Adapters["Prime"]; - foreach (var position in adapter.Account.Positions) + foreach (var position in adapter.Account.Positions.Values.Where(o => Equals(name, o.Name))) { - var side = position.Value.Side is OrderSideEnum.Buy ? OrderSideEnum.Sell : OrderSideEnum.Buy; + var side = position.Side is OrderSideEnum.Buy ? OrderSideEnum.Sell : OrderSideEnum.Buy; var order = new OrderModel { Side = side, Type = OrderTypeEnum.Market, Transaction = new() { - Volume = position.Value.Transaction.Volume, - Instrument = position.Value.Transaction.Instrument + Volume = position.Transaction.Volume, + Instrument = position.Transaction.Instrument } }; diff --git a/Terminal/Pages/Gateways/Schwab.razor.cs b/Terminal/Pages/Gateways/Schwab.razor.cs index 0d33dbab69..a733b87fab 100644 --- a/Terminal/Pages/Gateways/Schwab.razor.cs +++ b/Terminal/Pages/Gateways/Schwab.razor.cs @@ -11,6 +11,7 @@ using Terminal.Core.Enums; using Terminal.Core.Indicators; using Terminal.Core.Models; +using Terminal.Services; namespace Terminal.Pages.Gateways { @@ -18,13 +19,11 @@ public partial class Schwab { [Inject] IConfiguration Configuration { get; set; } - protected virtual int Counter { get; set; } - protected virtual PageComponent View { get; set; } - protected virtual PerformanceIndicator Performance { get; set; } - protected virtual InstrumentModel Instrument { get; set; } = new InstrumentModel + protected PageComponent View { get; set; } + protected PerformanceIndicator Performance { get; set; } + protected InstrumentModel Instrument { get; set; } = new InstrumentModel { Name = "SPY", - Exchange = "SMART", Type = InstrumentEnum.Shares, TimeFrame = TimeSpan.FromMinutes(1) }; @@ -89,26 +88,33 @@ protected virtual void CreateAccounts() }); } - private async Task OnData(PointModel point) + protected async Task OnData(PointModel point) { - Counter = (Counter + 1) % 100; - + var name = Instrument.Name; var account = View.Adapters["Prime"].Account; - var instrument = account.Instruments[Instrument.Name]; + var instrument = account.Instruments[name]; var performance = Performance.Calculate([account]); + var openOrders = account.Orders.Values.Where(o => Equals(o.Name, name)); + var openPositions = account.Positions.Values.Where(o => Equals(o.Name, name)); - if (account.Orders.IsEmpty && account.Positions.IsEmpty) + if (openOrders.IsEmpty() && openPositions.IsEmpty()) { await OpenPositions(Instrument, 1); - } + await TradeService.Done(async () => + { + var position = account + .Positions + .Values + .Where(o => Equals(o.Name, name)) + .First(); - if (Counter is 0 && account.Positions.IsEmpty is false) - { - await ClosePositions(); - await OpenPositions(Instrument, account.Positions.First().Value.Side is OrderSideEnum.Buy ? -1 : 1); + await ClosePositions(name); + await OpenPositions(Instrument, position.Side is OrderSideEnum.Buy ? -1 : 1); + + }, 10000); } - View.ChartsView.UpdateItems(point.Time.Value.Ticks, "Prices", "Bars", View.ChartsView.GetShape(point)); + View.ChartsView.UpdateItems(point.Time.Value.Ticks, "Prices", "Bars", View.ChartsView.GetShape(point)); View.ReportsView.UpdateItems(point.Time.Value.Ticks, "Performance", "Balance", new AreaShape { Y = account.Balance }); View.ReportsView.UpdateItems(point.Time.Value.Ticks, "Performance", "PnL", new LineShape { Y = performance.Point.Last }); View.DealsView.UpdateItems(account.Deals); @@ -116,39 +122,32 @@ private async Task OnData(PointModel point) View.PositionsView.UpdateItems(account.Positions.Values); } - private double? GetPrice(double direction) => direction > 0 ? + protected double? GetPrice(double direction) => direction > 0 ? Instrument.Point.Ask : Instrument.Point.Bid; - private async Task OpenPositions(InstrumentModel instrument, double direction) + protected async Task OpenPositions(InstrumentModel instrument, double direction) { + var adapter = View.Adapters["Prime"]; var side = direction > 0 ? OrderSideEnum.Buy : OrderSideEnum.Sell; var stopSide = direction < 0 ? OrderSideEnum.Buy : OrderSideEnum.Sell; - var adapter = View.Adapters["Prime"]; + var TP = new OrderModel { - Price = GetPrice(direction) + 15 * direction, Side = stopSide, Type = OrderTypeEnum.Limit, Instruction = InstructionEnum.Brace, - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Price = GetPrice(direction) + 15 * direction, + Transaction = new() { Volume = 10, Instrument = instrument } }; var SL = new OrderModel { - Price = GetPrice(-direction) - 15 * direction, Side = stopSide, Type = OrderTypeEnum.Stop, Instruction = InstructionEnum.Brace, - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Price = GetPrice(-direction) - 15 * direction, + Transaction = new() { Volume = 10, Instrument = instrument } }; var order = new OrderModel @@ -156,32 +155,28 @@ private async Task OpenPositions(InstrumentModel instrument, double direction) Price = GetPrice(direction), Side = side, Type = OrderTypeEnum.Market, - Orders = [SL, TP], - Transaction = new() - { - Volume = 1, - Instrument = instrument - } + Transaction = new() { Volume = 10, Instrument = instrument }, + Orders = [SL, TP] }; await adapter.CreateOrders(order); } - private async Task ClosePositions() + protected async Task ClosePositions(string name) { var adapter = View.Adapters["Prime"]; - foreach (var position in adapter.Account.Positions) + foreach (var position in adapter.Account.Positions.Values.Where(o => Equals(name, o.Name))) { - var side = position.Value.Side is OrderSideEnum.Buy ? OrderSideEnum.Sell : OrderSideEnum.Buy; + var side = position.Side is OrderSideEnum.Buy ? OrderSideEnum.Sell : OrderSideEnum.Buy; var order = new OrderModel { Side = side, Type = OrderTypeEnum.Market, Transaction = new() { - Volume = position.Value.Transaction.Volume, - Instrument = position.Value.Transaction.Instrument + Volume = position.Transaction.Volume, + Instrument = position.Transaction.Instrument } }; diff --git a/Terminal/Services/TradeService.cs b/Terminal/Services/TradeService.cs index 793b20246f..470a419105 100644 --- a/Terminal/Services/TradeService.cs +++ b/Terminal/Services/TradeService.cs @@ -494,5 +494,17 @@ public static IList GetPmCover(IGateway adapter, PointModel point, O return [order]; } + + /// + /// Run with delay + /// + /// + /// + /// + public static async Task Done(Action action, int interval) + { + await Task.Delay(interval); + action(); + } } }