public void Is_Quote_Returned() { const string instrument = "EURUSD"; _bestPriceConsumer.SendEvent(this, new BestPriceChangeEventArgs(new InstrumentBidAskPair { Instrument = instrument, Ask = 1.05M, Bid = 1.04M })); var quote = _quoteCacheService.GetQuote(instrument); Assert.IsNotNull(quote); Assert.AreEqual(1.04, quote.Bid); Assert.AreEqual(1.05, quote.Ask); }
public void ChangeOrderLimits(string orderId, decimal?stopLoss, decimal?takeProfit, decimal?expectedOpenPrice) { using (_contextFactory.GetWriteSyncContext($"{nameof(TradingEngine)}.{nameof(ChangeOrderLimits)}")) { var order = _ordersCache.GetOrderById(orderId); if (order.Status != OrderStatus.WaitingForExecution && expectedOpenPrice > 0) { return; } var quote = _quoteCashService.GetQuote(order.Instrument); var tp = takeProfit == 0 ? null : takeProfit; var sl = stopLoss == 0 ? null : stopLoss; var expOpenPrice = expectedOpenPrice == 0 ? null : expectedOpenPrice; var accountAsset = _accountAssetsCacheService.GetAccountAsset(order.TradingConditionId, order.AccountAssetId, order.Instrument); _validateOrderService.ValidateOrderStops(order.GetOrderType(), quote, accountAsset.DeltaBid, accountAsset.DeltaAsk, tp, sl, expOpenPrice, order.AssetAccuracy); order.TakeProfit = tp.HasValue ? Math.Round(tp.Value, order.AssetAccuracy) : (decimal?)null; order.StopLoss = sl.HasValue ? Math.Round(sl.Value, order.AssetAccuracy) : (decimal?)null; order.ExpectedOpenPrice = expOpenPrice.HasValue ? Math.Round(expOpenPrice.Value, order.AssetAccuracy) : (decimal?)null; _orderLimitsChangesEventChannel.SendEvent(this, new OrderLimitsChangedEventArgs(order)); } }
public decimal GetQuoteRateForBaseAsset(string accountAssetId, string assetPairId, string legalEntity, bool useAsk) { var assetPair = _assetPairsCache.GetAssetPairById(assetPairId); // two step transform: base -> quote from QuoteCache, quote -> account from FxCache // if accountAssetId == assetPair.BaseAssetId, rate != 1, because trading and fx rates can be different var assetPairQuote = _quoteCacheService.GetQuote(assetPairId); var tradingRate = useAsk ? assetPairQuote.Ask : assetPairQuote.Bid; if (assetPair.QuoteAssetId == accountAssetId) { return(tradingRate); } var fxPair = _assetPairsCache.FindAssetPair(assetPair.QuoteAssetId, accountAssetId, legalEntity); var fxQuote = _fxRateCacheService.GetQuote(fxPair.Id); var rate = fxPair.BaseAssetId == assetPair.QuoteAssetId ? fxQuote.Ask * tradingRate : 1 / fxQuote.Bid * tradingRate; return(rate); }
public void ApprovePriceRequest(string operationId, decimal?price) { if (_specialLiquidationSettings.FakePriceRequestAutoApproval) { _log.WriteWarning(nameof(ManualRfqService), nameof(ApprovePriceRequest), $"Most probably, the price request for {operationId} has already been automatically approved according to configuration"); } if (!_requests.TryGetValue(operationId, out var command)) { throw new InvalidOperationException($"Command with operation ID {operationId} does not exist"); } if (price == null) { var quote = _quoteCacheService.GetQuote(command.Instrument); price = (command.Volume > 0 ? quote.Ask : quote.Bid) * _specialLiquidationSettings.FakePriceMultiplier; } _cqrsSender.PublishEvent(new PriceForSpecialLiquidationCalculatedEvent { OperationId = operationId, CreationTime = _dateService.Now(), Price = price.Value, }, _cqrsContextNamesSettings.Gavel); _requests.TryRemove(operationId, out _); }
public decimal GetQuoteRateForBaseAsset(string accountAssetId, string instrument) { var asset = _assetPairsCache.GetAssetPairById(instrument); var baseAssetId = asset.BaseAssetId; if (accountAssetId == baseAssetId) { return(1); } var inst = _assetPairsCache.FindAssetPair(baseAssetId, accountAssetId); var quote = _quoteCacheService.GetQuote(inst.Id); if (inst.BaseAssetId == baseAssetId) { return(quote.Ask); } return(1.0M / quote.Bid); }
public decimal GetQuoteRateForBaseAsset(string accountAssetId, string assetPairId, string legalEntity, bool metricIsPositive = true) { var assetPair = _assetPairsCache.GetAssetPairById(assetPairId); if (accountAssetId == assetPair.BaseAssetId) { return(1); } var assetPairSubst = _assetPairsCache.FindAssetPair(assetPair.BaseAssetId, accountAssetId, legalEntity); var rate = metricIsPositive ? assetPairSubst.BaseAssetId == assetPair.BaseAssetId ? _quoteCacheService.GetQuote(assetPairSubst.Id).Ask : 1 / _quoteCacheService.GetQuote(assetPairSubst.Id).Bid : assetPairSubst.BaseAssetId == assetPair.BaseAssetId ? _quoteCacheService.GetQuote(assetPairSubst.Id).Bid : 1 / _quoteCacheService.GetQuote(assetPairSubst.Id).Ask; return(rate); }
public void CheckIsEnoughBalance(Order order, IMatchingEngineBase matchingEngine) { var orderMargin = _fplService.GetInitMarginForOrder(order); var accountMarginAvailable = _accountsCacheService.Get(order.AccountId).GetMarginAvailable(); var quote = _quoteCacheService.GetQuote(order.AssetPairId); var openPrice = order.Price ?? 0; var closePrice = 0m; var directionForClose = order.Volume.GetClosePositionOrderDirection(); if (quote.GetVolumeForOrderDirection(order.Direction) >= Math.Abs(order.Volume) && quote.GetVolumeForOrderDirection(directionForClose) >= Math.Abs(order.Volume)) { closePrice = quote.GetPriceForOrderDirection(directionForClose); if (openPrice == 0) { openPrice = quote.GetPriceForOrderDirection(order.Direction); } } else { var openPriceInfo = matchingEngine.GetBestPriceForOpen(order.AssetPairId, order.Volume); var closePriceInfo = matchingEngine.GetPriceForClose(order.AssetPairId, order.Volume, openPriceInfo.externalProviderId); if (openPriceInfo.price == null || closePriceInfo == null) { throw new ValidateOrderException(OrderRejectReason.NoLiquidity, "Price for open/close can not be calculated"); } closePrice = closePriceInfo.Value; if (openPrice == 0) { openPrice = openPriceInfo.price.Value; } } var pnlInTradingCurrency = (closePrice - openPrice) * order.Volume; var fxRate = _cfdCalculatorService.GetQuoteRateForQuoteAsset(order.AccountAssetId, order.AssetPairId, order.LegalEntity, pnlInTradingCurrency > 0); var pnl = pnlInTradingCurrency * fxRate; // just in case... is should be always negative if (pnl > 0) { _log.WriteWarning(nameof(CheckIsEnoughBalance), order.ToJson(), $"Theoretical PnL at the moment of order execution is positive"); pnl = 0; } if (accountMarginAvailable + pnl < orderMargin) { throw new ValidateOrderException(OrderRejectReason.NotEnoughBalance, MtMessages.Validation_NotEnoughBalance, $"Account available margin: {accountMarginAvailable}, order margin: {orderMargin}, pnl: {pnl} " + $"(open price: {openPrice}, close price: {closePrice}, fx rate: {fxRate})"); } }
public void CheckIsEnoughBalance(Order order, IMatchingEngineBase matchingEngine, decimal additionalMargin) { _log.WriteInfo(nameof(CheckIsEnoughBalance), new { Order = order, additionalMargin }.ToJson(), "Start checking if account balance is enough ..."); var orderMargin = _fplService.GetInitMarginForOrder(order); _log.WriteInfo(nameof(CheckIsEnoughBalance), new { Order = order, orderMargin }.ToJson(), "Order margin calculated"); var account = _accountsProvider.GetAccountById(order.AccountId); var accountMarginAvailable = account.GetMarginAvailable() + additionalMargin; _log.WriteInfo(nameof(CheckIsEnoughBalance), new { Order = order, Account = account, accountMarginAvailable }.ToJson(), "Account margin available calculated"); var quote = _quoteCacheService.GetQuote(order.AssetPairId); decimal openPrice; decimal closePrice; var directionForClose = order.Volume.GetClosePositionOrderDirection(); if (quote.GetVolumeForOrderDirection(order.Direction) >= Math.Abs(order.Volume) && quote.GetVolumeForOrderDirection(directionForClose) >= Math.Abs(order.Volume)) { closePrice = quote.GetPriceForOrderDirection(directionForClose); openPrice = quote.GetPriceForOrderDirection(order.Direction); } else { var openPriceInfo = matchingEngine.GetBestPriceForOpen(order.AssetPairId, order.Volume); var closePriceInfo = matchingEngine.GetPriceForClose(order.AssetPairId, order.Volume, openPriceInfo.externalProviderId); if (openPriceInfo.price == null || closePriceInfo == null) { throw new ValidateOrderException(OrderRejectReason.NoLiquidity, "Price for open/close can not be calculated"); } closePrice = closePriceInfo.Value; openPrice = openPriceInfo.price.Value; } _log.WriteInfo(nameof(CheckIsEnoughBalance), new { Order = order, Quote = quote, openPrice, closePrice }.ToJson(), "Open and close prices calculated"); var pnlInTradingCurrency = (closePrice - openPrice) * order.Volume; var fxRate = _cfdCalculatorService.GetQuoteRateForQuoteAsset(order.AccountAssetId, order.AssetPairId, order.LegalEntity, pnlInTradingCurrency > 0); var pnl = pnlInTradingCurrency * fxRate; // just in case... is should be always negative if (pnl > 0) { _log.WriteWarning(nameof(CheckIsEnoughBalance), order.ToJson(), $"Theoretical PnL at the moment of order execution is positive"); pnl = 0; } _log.WriteInfo(nameof(CheckIsEnoughBalance), new { Order = order, pnlInTradingCurrency, fxRate, pnl }.ToJson(), "PNL calculated"); var assetType = _assetPairsCache.GetAssetPairById(order.AssetPairId).AssetType; if (!_clientProfileSettingsCache.TryGetValue(account.TradingConditionId, assetType, out var clientProfileSettings)) { throw new InvalidOperationException($"Client profile settings for [{account.TradingConditionId}] and asset type [{assetType}] were not found in cache"); } var tradingInstrument = _tradingInstrumentsCache.GetTradingInstrument(account.TradingConditionId, order.AssetPairId); var entryCost = CostHelper.CalculateEntryCost( order.Price, order.Direction == OrderDirection.Buy ? Lykke.Snow.Common.Costs.OrderDirection.Buy : Lykke.Snow.Common.Costs.OrderDirection.Sell, quote.Ask, quote.Bid, fxRate, tradingInstrument.Spread, tradingInstrument.HedgeCost, _marginTradingSettings.BrokerDefaultCcVolume, _marginTradingSettings.BrokerDonationShare); _log.WriteInfo(nameof(CheckIsEnoughBalance), new { OrderPrice = order.Price, OrderDirection = order.Direction, quote.Ask, quote.Bid, fxRate, tradingInstrument.Spread, tradingInstrument.HedgeCost, _marginTradingSettings.BrokerDefaultCcVolume, _marginTradingSettings.BrokerDonationShare, CalculatedEntryCost = entryCost }.ToJson(), "Entry cost calculated"); var exitCost = CostHelper.CalculateExitCost( order.Price, order.Direction == OrderDirection.Buy ? Lykke.Snow.Common.Costs.OrderDirection.Buy : Lykke.Snow.Common.Costs.OrderDirection.Sell, quote.Ask, quote.Bid, fxRate, tradingInstrument.Spread, tradingInstrument.HedgeCost, _marginTradingSettings.BrokerDefaultCcVolume, _marginTradingSettings.BrokerDonationShare); _log.WriteInfo(nameof(CheckIsEnoughBalance), new { OrderPrice = order.Price, OrderDirection = order.Direction, quote.Ask, quote.Bid, fxRate, tradingInstrument.Spread, tradingInstrument.HedgeCost, _marginTradingSettings.BrokerDefaultCcVolume, _marginTradingSettings.BrokerDonationShare, CalculatedExitCost = exitCost }.ToJson(), "Exit cost calculated"); if (accountMarginAvailable + pnl - entryCost - exitCost < orderMargin) { throw new ValidateOrderException(OrderRejectReason.NotEnoughBalance, MtMessages.Validation_NotEnoughBalance, $"Account available margin: {accountMarginAvailable}, order margin: {orderMargin}, pnl: {pnl}, entry cost: {entryCost}, exit cost: {exitCost} " + $"(open price: {openPrice}, close price: {closePrice}, fx rate: {fxRate})"); } _log.WriteInfo(nameof(CheckIsEnoughBalance), new { Order = order, accountMarginAvailable, pnl, entryCost, exitCost, orderMargin }.ToJson(), "Account balance is enough, validation succeeded."); }
public async Task <MatchedOrderCollection> MatchOrderAsync(Order order, bool shouldOpenNewPosition, OrderModality modality = OrderModality.Regular) { List <(string source, decimal?price)> prices = null; if (!string.IsNullOrEmpty(_marginTradingSettings.DefaultExternalExchangeId)) { var quote = _quoteCacheService.GetQuote(order.AssetPairId); if (quote.GetVolumeForOrderDirection(order.Direction) >= Math.Abs(order.Volume)) { prices = new List <(string source, decimal?price)> { (_marginTradingSettings .DefaultExternalExchangeId, quote.GetPriceForOrderDirection(order.Direction)) }; } } if (prices == null) { prices = _externalOrderbookService.GetOrderedPricesForExecution(order.AssetPairId, order.Volume, shouldOpenNewPosition); if (prices == null || !prices.Any()) { return(new MatchedOrderCollection()); } } var assetPair = _assetPairsCache.GetAssetPairByIdOrDefault(order.AssetPairId); var externalAssetPair = assetPair?.BasePairId ?? order.AssetPairId; foreach (var(source, price) in prices .Where(x => string.IsNullOrEmpty(order.ExternalProviderId) || x.source == order.ExternalProviderId)) { var externalOrderModel = new OrderModel(); var orderType = order.OrderType == Core.Orders.OrderType.Limit || order.OrderType == Core.Orders.OrderType.TakeProfit ? Core.Orders.OrderType.Limit : Core.Orders.OrderType.Market; var isCancellationTrade = order.AdditionalInfo.IsCancellationTrade(out var cancellationTradeExternalId); var targetPrice = order.OrderType != Core.Orders.OrderType.Market || isCancellationTrade ? (double?)order.Price : (double?)price; try { externalOrderModel = new OrderModel( tradeType: order.Direction.ToType <TradeType>(), orderType: orderType.ToType <OrderType>(), timeInForce: TimeInForce.FillOrKill, volume: (double)Math.Abs(order.Volume), dateTime: _dateService.Now(), exchangeName: source, instrument: externalAssetPair, price: targetPrice, orderId: order.Id, modality: modality.ToType <TradeRequestModality>(), isCancellationTrade: isCancellationTrade, cancellationTradeExternalId: cancellationTradeExternalId); var cts = new CancellationTokenSource(); cts.CancelAfter(_marginTradingSettings.GavelTimeout); var executionResult = await _exchangeConnectorClient.ExecuteOrder(externalOrderModel, cts.Token); if (!executionResult.Success) { var ex = new Exception( $"External order was not executed. Status: {executionResult.ExecutionStatus}. Failure: {executionResult.FailureType}"); LogOrderExecutionException(order, externalOrderModel, ex); } else { var executedPrice = Math.Abs(executionResult.Price) > 0 ? (decimal)executionResult.Price : price.Value; if (executedPrice.EqualsZero()) { var ex = new Exception($"Have got execution price from Gavel equal to 0. Ignoring."); LogOrderExecutionException(order, externalOrderModel, ex); } else { var matchedOrders = new MatchedOrderCollection { new MatchedOrder { MarketMakerId = source, MatchedDate = _dateService.Now(), OrderId = executionResult.ExchangeOrderId, Price = CalculatePriceWithMarkups(assetPair, order.Direction, executedPrice), Volume = (decimal)executionResult.Volume, IsExternal = true } }; await _rabbitMqNotifyService.ExternalOrder(executionResult); _operationsLogService.AddLog("external order executed", order.AccountId, externalOrderModel.ToJson(), executionResult.ToJson()); return(matchedOrders); } } } catch (Exception ex) { LogOrderExecutionException(order, externalOrderModel, ex); throw new OrderExecutionTechnicalException(); } } return(new MatchedOrderCollection()); }
public async Task <MatchedOrderCollection> MatchOrderAsync(Order order, bool shouldOpenNewPosition, OrderModality modality = OrderModality.Regular) { List <(string source, decimal?price)> prices = null; if (!string.IsNullOrEmpty(_marginTradingSettings.DefaultExternalExchangeId)) { var quote = _quoteCacheService.GetQuote(order.AssetPairId); if (quote.GetVolumeForOrderDirection(order.Direction) >= Math.Abs(order.Volume)) { prices = new List <(string source, decimal?price)> { (_marginTradingSettings .DefaultExternalExchangeId, quote.GetPriceForOrderDirection(order.Direction)) }; } } if (prices == null) { prices = _externalOrderbookService.GetOrderedPricesForExecution(order.AssetPairId, order.Volume, shouldOpenNewPosition); if (prices == null || !prices.Any()) { return(new MatchedOrderCollection()); } } var assetPair = _assetPairsCache.GetAssetPairByIdOrDefault(order.AssetPairId); var externalAssetPair = assetPair?.BasePairId ?? order.AssetPairId; foreach (var(source, price) in prices .Where(x => string.IsNullOrEmpty(order.ExternalProviderId) || x.source == order.ExternalProviderId)) { var externalOrderModel = new OrderModel(); var orderType = order.OrderType == Core.Orders.OrderType.Limit || order.OrderType == Core.Orders.OrderType.TakeProfit ? OrderType.Limit : OrderType.Market; var targetPrice = order.OrderType == Core.Orders.OrderType.Market ? (double?)price : (double?)order.Price; try { externalOrderModel = new OrderModel( tradeType: order.Direction.ToType <TradeType>(), orderType: orderType, timeInForce: TimeInForce.FillOrKill, volume: (double)Math.Abs(order.Volume), dateTime: _dateService.Now(), exchangeName: source, instrument: externalAssetPair, price: targetPrice, orderId: order.Id, modality: modality.ToType <TradeRequestModality>()); var cts = new CancellationTokenSource(); cts.CancelAfter(_marginTradingSettings.GavelTimeout); var executionResult = await _exchangeConnectorService.CreateOrderAsync(externalOrderModel, cts.Token); if (!executionResult.Success) { throw new Exception( $"External order was not executed. Status: {executionResult.ExecutionStatus}. Failure: {executionResult.FailureType}"); } var executedPrice = Math.Abs(executionResult.Price) > 0 ? (decimal)executionResult.Price : price.Value; var matchedOrders = new MatchedOrderCollection { new MatchedOrder { MarketMakerId = source, MatchedDate = _dateService.Now(), OrderId = executionResult.ExchangeOrderId, Price = CalculatePriceWithMarkups(assetPair, order.Direction, executedPrice), Volume = (decimal)executionResult.Volume, IsExternal = true } }; await _rabbitMqNotifyService.ExternalOrder(executionResult); _operationsLogService.AddLog("external order executed", order.AccountId, externalOrderModel.ToJson(), executionResult.ToJson()); return(matchedOrders); } catch (Exception e) { var connector = _marginTradingSettings.ExchangeConnector == ExchangeConnectorType.FakeExchangeConnector ? "Fake" : _exchangeConnectorService.BaseUri.OriginalString; _log.WriteError( $"{nameof(StpMatchingEngine)}:{nameof(MatchOrderAsync)}:{connector}", $"Internal order: {order.ToJson()}, External order model: {externalOrderModel.ToJson()}", e); } } return(new MatchedOrderCollection()); }
public Task <HttpOperationResponse <ExecutionReport> > CreateOrderWithHttpMessagesAsync(OrderModel orderModel = null, Dictionary <string, List <string> > customHeaders = null, CancellationToken cancellationToken = new CancellationToken()) { if (orderModel == null || orderModel.Volume == 0) { var report = new HttpOperationResponse <ExecutionReport> { Response = new HttpResponseMessage() { Content = new StringContent("Bad model"), StatusCode = HttpStatusCode.BadRequest, } }; return(Task.FromResult(report)); } var quote = _quoteService.GetQuote(orderModel.Instrument); var result = new HttpOperationResponse <ExecutionReport>(); try { _chaosKitty.Meow(nameof(FakeExchangeConnectorService)); result.Body = new ExecutionReport( type: orderModel.TradeType.ToType <TradeType>(), time: DateTime.UtcNow, price: orderModel.Price ?? (double?)(orderModel.TradeType == TradeType.Buy ? quote?.Ask : quote?.Bid) ?? throw new Exception("No price"), volume: orderModel.Volume, fee: 0, success: true, executionStatus: OrderExecutionStatus.Fill, failureType: OrderStatusUpdateFailureType.None, orderType: orderModel.OrderType, execType: ExecType.Trade, clientOrderId: Guid.NewGuid().ToString(), exchangeOrderId: Guid.NewGuid().ToString(), instrument: new Instrument(orderModel.Instrument, orderModel.ExchangeName)); } catch { result.Body = new ExecutionReport( type: orderModel.TradeType.ToType <TradeType>(), time: DateTime.UtcNow, price: 0, volume: 0, fee: 0, success: false, executionStatus: OrderExecutionStatus.Rejected, failureType: OrderStatusUpdateFailureType.ExchangeError, orderType: orderModel.OrderType, execType: ExecType.Trade, clientOrderId: null, exchangeOrderId: null, instrument: new Instrument(orderModel.Instrument, orderModel.ExchangeName)); } return(Task.FromResult(result)); }