private static decimal?MatchBestPriceForOrder(ExternalOrderBook externalOrderbook, IOrder order, bool isOpening) { var direction = isOpening ? order.GetOrderType() : order.GetCloseType(); var volume = Math.Abs(order.Volume); return(externalOrderbook.GetMatchedPrice(volume, direction)); }
private bool CheckZeroQuote(ExternalOrderBook orderbook, bool isEodOrderbook) { // TODO: the code below supposes we have only one quote in orderbook var hasZeroVolume = orderbook.Asks[0].Volume == 0 || orderbook.Bids[0].Volume == 0; var hasZeroPrice = orderbook.Asks[0].Price == 0 || orderbook.Bids[0].Price == 0; var isOrderbookValid = !hasZeroVolume && !hasZeroPrice; var assetPair = _assetPairsCache.GetAssetPairByIdOrDefault(orderbook.AssetPairId); if (assetPair == null) { return(isOrderbookValid); } //EOD quotes should not change asset pair state if (isEodOrderbook) { return(isOrderbookValid); } if (!isOrderbookValid) { // suspend instrument in case of zero volumes only if (!assetPair.IsSuspended && hasZeroVolume) { assetPair.IsSuspended = true;//todo apply changes to trading engine _cqrsSender.SendCommandToSettingsService(new ChangeProductSuspendedStatusCommand() { ProductId = assetPair.Id, OperationId = _identityGenerator.GenerateGuid(), Timestamp = _dateService.Now(), IsSuspended = true, }); _log.Info($"Suspending instrument {assetPair.Id}", context: orderbook.ToContextData()?.ToJson()); } } else { if (assetPair.IsSuspended) { assetPair.IsSuspended = false;//todo apply changes to trading engine _cqrsSender.SendCommandToSettingsService(new ChangeProductSuspendedStatusCommand { ProductId = assetPair.Id, OperationId = _identityGenerator.GenerateGuid(), Timestamp = _dateService.Now(), IsSuspended = false, }); _log.Info($"Un-suspending instrument {assetPair.Id}", context: orderbook.ToContextData()?.ToJson()); } } return(isOrderbookValid); }
public async Task UpdateAsync(string exchange, string assetPair, DateTime timestamp, IReadOnlyList <OrderBookLevel> sellLevels, IReadOnlyList <OrderBookLevel> buyLevels) { ExchangeSettings exchangeSettings = await _exchangeSettingsService.GetByNameAsync(exchange); if (exchangeSettings == null) { throw new FailedOperationException("Exchange setting not found"); } AssetPairModel assetPairSettings = _marketInstrumentService.GetAssetPair(assetPair, exchange); if (assetPairSettings == null) { throw new FailedOperationException("Asset pair not found"); } var externalOrderBook = new ExternalOrderBook { Exchange = exchange, AssetPair = assetPair, Timestamp = timestamp, SellLevels = sellLevels.Select(o => new ExternalOrderBookLevel { Price = (o.Price * (1 + exchangeSettings.MarketFee + exchangeSettings.TransactionFee)) .TruncateDecimalPlaces(assetPairSettings.PriceAccuracy, true), Volume = o.Volume, Markup = exchangeSettings.MarketFee + exchangeSettings.TransactionFee, OriginalPrice = o.Price }) .OrderBy(o => o.Price) .ToList(), BuyLevels = buyLevels.Select(o => new ExternalOrderBookLevel { Price = (o.Price * (1 - exchangeSettings.MarketFee - exchangeSettings.TransactionFee)) .TruncateDecimalPlaces(assetPairSettings.PriceAccuracy), Volume = o.Volume, Markup = exchangeSettings.MarketFee + exchangeSettings.TransactionFee, OriginalPrice = o.Price }) .OrderByDescending(o => o.Price) .ToList() }; lock (_sync) { if (!_orderBooks.ContainsKey(exchange)) { _orderBooks[exchange] = new Dictionary <string, ExternalOrderBook>(); } _orderBooks[exchange][assetPair] = externalOrderBook; } }
internal static ExternalOrderBookContract ToContract(this ExternalOrderBook orderBook) { return(new ExternalOrderBookContract { ExchangeName = orderBook.ExchangeName, AssetPairId = orderBook.AssetPairId, Timestamp = orderBook.Timestamp, ReceiveTimestamp = orderBook.ReceiveTimestamp, Asks = orderBook.Asks.ToContracts(), Bids = orderBook.Bids.ToContracts(), }); }
public void Test_ExternalOrderBooksList_Set_InvalidOrderbook(ExternalOrderBook orderBook, string expectedErrorMessage) { //Arrange var orderbooks = GetNewOrderbooksList(); //Act orderbooks.SetOrderbook(orderBook); //Assert _logMock.Verify( log => log.WriteErrorAsync(It.IsAny <string>(), It.IsAny <string>(), It.Is <Exception>(ex => ex.Message.Contains(expectedErrorMessage)), null), Times.Once); }
public void SetOrderbook(ExternalOrderBook orderbook) { var isEodOrderbook = orderbook.ExchangeName == ExternalOrderbookService.EodExternalExchange; var instrumentTradingStatus = _assetPairDayOffService.IsAssetTradingDisabled(orderbook.AssetPairId); if (!isEodOrderbook && !instrumentTradingStatus.TradingEnabled && instrumentTradingStatus.Reason == InstrumentTradingDisabledReason.InstrumentTradingDisabled) { return; } if (_orderbookValidation.ValidateInstrumentStatusForEodQuotes && isEodOrderbook || _orderbookValidation.ValidateInstrumentStatusForTradingQuotes && !isEodOrderbook) { // we should process normal orderbook only if instrument is currently tradable if (_orderbookValidation.ValidateInstrumentStatusForTradingQuotes && instrumentTradingStatus && !isEodOrderbook) { return; } // and process EOD orderbook only if instrument is currently not tradable if (_orderbookValidation.ValidateInstrumentStatusForEodQuotes && !instrumentTradingStatus && isEodOrderbook) { //log current schedule for the instrument var schedule = _scheduleSettingsCache.GetMarketTradingScheduleByAssetPair(orderbook.AssetPairId); _log.WriteWarning("EOD quotes processing", $"Current schedule for the instrument's market: {schedule.ToJson()}", $"EOD quote for {orderbook.AssetPairId} is skipped, because instrument is within trading hours"); return; } } if (!CheckZeroQuote(orderbook, isEodOrderbook)) { return; } orderbook.ApplyExchangeIdFromSettings(_defaultExternalExchangeId); var bba = orderbook.GetBestPrice(); _orderbooks.AddOrUpdate(orderbook.AssetPairId, a => orderbook, (s, book) => orderbook); _bestPriceChangeEventChannel.SendEvent(this, new BestPriceChangeEventArgs(bba, isEodOrderbook)); }
public static object ToContextData(this ExternalOrderBook orderbook) { if (orderbook == null) { return(null); } return(new { assetPairId = orderbook.AssetPairId, timestamp = orderbook.Timestamp, bestAsk = orderbook.Asks[0], bestBid = orderbook.Bids[0], exchangeName = orderbook.ExchangeName }); }
public void SetOrderbook(ExternalOrderBook orderbook) { if (!ValidateOrderbook(orderbook)) { return; } var bba = new InstrumentBidAskPair { Bid = 0, Ask = decimal.MaxValue, Date = _dateService.Now(), Instrument = orderbook.AssetPairId }; Dictionary <string, ExternalOrderBook> UpdateOrderbooksDictionary(string assetPairId, Dictionary <string, ExternalOrderBook> dict) { dict[orderbook.ExchangeName] = orderbook; foreach (var pair in dict.Values.RequiredNotNullOrEmptyCollection(nameof(dict))) { // guaranteed to be sorted best first var bestBid = pair.Bids.First().Price; var bestAsk = pair.Asks.First().Price; if (bestBid > bba.Bid) { bba.Bid = bestBid; } if (bestAsk < bba.Ask) { bba.Ask = bestAsk; } } return(dict); } _orderbooks.AddOrUpdate(orderbook.AssetPairId, k => UpdateOrderbooksDictionary(k, new Dictionary <string, ExternalOrderBook>()), UpdateOrderbooksDictionary); _bestPriceChangeEventChannel.SendEvent(this, new BestPriceChangeEventArgs(bba)); }
private static bool AreEqual(ExternalOrderBook a, ExternalOrderBook b) { if (a == null && b == null) { return(true); } if (a == null || b == null) { return(false); } if (a.AssetPair != b.AssetPair || a.Exchange != b.Exchange) { return(false); } return(AreEqual(a.SellLevels, b.SellLevels) && AreEqual(a.BuyLevels, b.BuyLevels)); }
private static decimal?MatchBestPriceForOrderExecution(ExternalOrderBook externalOrderBook, decimal volume, bool validateOppositeDirectionVolume) { var direction = volume.GetOrderDirection(); var price = externalOrderBook.GetMatchedPrice(volume, direction); if (price != null && validateOppositeDirectionVolume) { var closePrice = externalOrderBook.GetMatchedPrice(volume, direction.GetOpositeDirection()); //if no liquidity for close, should not use price for open if (closePrice == null) { return(null); } } return(price); }
public void SetOrderbook(ExternalOrderBook orderbook) { if (!CheckZeroQuote(orderbook)) { return; } var isDayOff = _assetPairDayOffService.IsDayOff(orderbook.AssetPairId); var isEodOrderbook = orderbook.ExchangeName == ExternalOrderbookService.EodExternalExchange; // we should process normal orderbook only if instrument is currently tradable if (isDayOff && !isEodOrderbook) { return; } // and process EOD orderbook only if instrument is currently not tradable if (!isDayOff && isEodOrderbook) { //log current schedule for the instrument var schedule = _scheduleSettingsCache.GetCompiledScheduleSettings( orderbook.AssetPairId, _dateService.Now(), TimeSpan.Zero); _log.WriteWarning("EOD quotes processing", $"Current schedule: {schedule.ToJson()}", $"EOD quote for {orderbook.AssetPairId} is skipped, because instrument is within trading hours"); return; } orderbook.ApplyExchangeIdFromSettings(_defaultExternalExchangeId); var bba = orderbook.GetBestPrice(); _orderbooks.AddOrUpdate(orderbook.AssetPairId, a => orderbook, (s, book) => orderbook); _bestPriceChangeEventChannel.SendEvent(this, new BestPriceChangeEventArgs(bba)); }
private bool CheckZeroQuote(ExternalOrderBook orderbook) { var isOrderbookValid = orderbook.Asks[0].Volume != 0 && orderbook.Bids[0].Volume != 0; var assetPair = _assetPairsCache.GetAssetPairByIdOrDefault(orderbook.AssetPairId); if (assetPair == null) { return(isOrderbookValid); } if (!isOrderbookValid) { if (!assetPair.IsSuspended) { assetPair.IsSuspended = true;//todo apply changes to trading engine _cqrsSender.SendCommandToSettingsService(new SuspendAssetPairCommand { AssetPairId = assetPair.Id, OperationId = _identityGenerator.GenerateGuid(), }); } } else { if (assetPair.IsSuspended) { assetPair.IsSuspended = false;//todo apply changes to trading engine _cqrsSender.SendCommandToSettingsService(new UnsuspendAssetPairCommand { AssetPairId = assetPair.Id, OperationId = _identityGenerator.GenerateGuid(), }); } } return(isOrderbookValid); }
private bool ValidateOrderbook(ExternalOrderBook orderbook) { try { orderbook.AssetPairId.RequiredNotNullOrWhiteSpace("orderbook.AssetPairId"); orderbook.ExchangeName.RequiredNotNullOrWhiteSpace("orderbook.ExchangeName"); orderbook.RequiredNotNull(nameof(orderbook)); orderbook.Bids.RequiredNotNullOrEmpty("orderbook.Bids"); orderbook.Bids.RemoveAll(e => e == null || e.Price <= 0 || e.Volume == 0); ValidatePricesSorted(orderbook.Bids, false); orderbook.Asks.RequiredNotNullOrEmpty("orderbook.Asks"); orderbook.Asks.RemoveAll(e => e == null || e.Price <= 0 || e.Volume == 0); ValidatePricesSorted(orderbook.Asks, true); return(true); } catch (Exception e) { _log.WriteError(nameof(ExternalOrderBooksList), orderbook.ToJson(), e); return(false); } }
private static decimal?MatchBestPriceForPositionClose(ExternalOrderBook externalOrderBook, decimal volume) { var direction = volume.GetClosePositionOrderDirection(); return(externalOrderBook.GetMatchedPrice(Math.Abs(volume), direction)); }
public Task <ExecutionReport> ExecuteOrder(OrderModel orderModel, CancellationToken cancellationToken) { if (orderModel == null || orderModel.Volume == 0) { return(Task.FromResult(new ExecutionReport { Success = false, ExecutionStatus = OrderExecutionStatus.Rejected, FailureType = OrderStatusUpdateFailureType.ConnectorError, })); } ExecutionReport result; try { _chaosKitty.Meow(nameof(FakeExchangeConnectorClient)); ExternalOrderBook orderbook; decimal? currentPrice; if (orderModel.Modality == TradeRequestModality.Liquidation_CorporateAction || orderModel.Modality == TradeRequestModality.Liquidation_MarginCall) { if (orderModel.Price == null) { throw new InvalidOperationException("Order should have price specified in case of special liquidation"); } currentPrice = (decimal?)orderModel.Price; orderbook = new ExternalOrderBook(MatchingEngineConstants.DefaultSpecialLiquidation, orderModel.Instrument, _dateService.Now(), new [] { new VolumePrice { Price = currentPrice.Value, Volume = (decimal)orderModel.Volume } }, new [] { new VolumePrice { Price = currentPrice.Value, Volume = (decimal)orderModel.Volume } }); } else { orderbook = _orderbookService.GetOrderBook(orderModel.Instrument); if (orderbook == null) { throw new InvalidOperationException("Orderbook was not found"); } currentPrice = orderbook.GetMatchedPrice((decimal)orderModel.Volume, orderModel.TradeType == TradeType.Buy ? OrderDirection.Buy : OrderDirection.Sell); } result = new ExecutionReport( type: orderModel.TradeType, time: DateTime.UtcNow, price: (double)(currentPrice ?? 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)); _cqrsSender.PublishEvent(new OrderExecutionOrderBookContract { OrderId = orderModel.OrderId, Volume = (decimal)orderModel.Volume, ExternalOrderId = result.ExchangeOrderId, OrderBook = new ExternalOrderBookContract { AssetPairId = orderbook.AssetPairId, ExchangeName = orderbook.ExchangeName, Timestamp = orderbook.Timestamp, ReceiveTimestamp = _dateService.Now(), Asks = orderbook.Asks.Select(a => new VolumePriceContract { Price = a.Price, Volume = a.Volume }).ToList(), Bids = orderbook.Bids.Select(a => new VolumePriceContract { Price = a.Price, Volume = a.Volume }).ToList() } }, _settings.Cqrs.ContextNames.Gavel); } catch (Exception ex) { _log.WriteErrorAsync(nameof(FakeExchangeConnectorClient), nameof(ExecuteOrder), orderModel.ToJson(), ex); result = new ExecutionReport( type: orderModel.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)); }
public async Task Create_Order_Book() { // arrange string assetPair = "BTCUSD"; var exchangeSettings = new ExchangeSettings { Name = "exchange1", MarketFee = .1m, TransactionFee = .01m }; _exchangeSettings.Add(exchangeSettings); var assetPairSettings = new AssetPairModel { Exchange = exchangeSettings.Name, Name = assetPair, PriceAccuracy = 3 }; _assetPairs.Add(assetPairSettings); var sellLevels = new List <OrderBookLevel> { new OrderBookLevel { Price = 1.732235m, Volume = 3 }, new OrderBookLevel { Price = 0.833000m, Volume = 1 } }; var buyLevels = new List <OrderBookLevel> { new OrderBookLevel { Price = 0.712397m, Volume = 1 }, new OrderBookLevel { Price = 0.111000m, Volume = 5 } }; var expectedExternalOrderBook = new ExternalOrderBook { Exchange = exchangeSettings.Name, Timestamp = DateTime.UtcNow, AssetPair = assetPair, SellLevels = sellLevels .Select(o => new ExternalOrderBookLevel { Price = (o.Price * (1 + exchangeSettings.MarketFee + exchangeSettings.TransactionFee)) .TruncateDecimalPlaces(assetPairSettings.PriceAccuracy, true), Volume = o.Volume, Markup = exchangeSettings.MarketFee + exchangeSettings.TransactionFee, OriginalPrice = o.Price }) .ToList(), BuyLevels = buyLevels .Select(o => new ExternalOrderBookLevel { Price = (o.Price * (1 - exchangeSettings.MarketFee - exchangeSettings.TransactionFee)) .TruncateDecimalPlaces(assetPairSettings.PriceAccuracy), Volume = o.Volume, Markup = exchangeSettings.MarketFee + exchangeSettings.TransactionFee, OriginalPrice = o.Price }) .ToList() }; // act await _service.UpdateAsync(exchangeSettings.Name, assetPair, DateTime.UtcNow, sellLevels, buyLevels); ExternalOrderBook actualExternalOrderBook = _service.GetByAssetPair(assetPair).Single(); // assert Assert.IsTrue(AreEqual(expectedExternalOrderBook, actualExternalOrderBook)); }