private async Task ProcessLimitTradesAsync(LimitOrdersMessage message) { if (message.Orders == null || !message.Orders.Any()) { return; } var limitOrderIds = message.Orders .Select(o => o.Order.Id) .ToHashSet(); foreach (var orderMessage in message.Orders) { if (orderMessage.Trades == null) { continue; } var assetPair = await _assetPairsManager.TryGetEnabledPairAsync(orderMessage.Order.AssetPairId); foreach (var tradeMessage in orderMessage.Trades.OrderBy(t => t.Timestamp).ThenBy(t => t.Index)) { // If both orders of the trade are limit, then both of them should be contained in the single message, // this is by design. var isOppositeOrderIsLimit = limitOrderIds.Contains(tradeMessage.OppositeOrderId); // If opposite order is market order, then unconditionally takes the given limit order. // But if both of orders are limit orders, we should take only one of them. if (isOppositeOrderIsLimit) { var isBuyOrder = orderMessage.Order.Volume > 0; // Takes trade only for the sell limit orders if (isBuyOrder) { continue; } } // Volumes in the asset pair base and quoting assets double baseVolume; double quotingVolume; if (tradeMessage.Asset == assetPair.BaseAssetId) { baseVolume = tradeMessage.Volume; quotingVolume = tradeMessage.OppositeVolume; } else { baseVolume = tradeMessage.OppositeVolume; quotingVolume = tradeMessage.Volume; } // Just discarding trades with negative prices and\or volumes. It's better to do it here instead of // at the first line of foreach 'case we have some additional trade selection logic in the begining. // ReSharper disable once InvertIf if (tradeMessage.Price > 0 && baseVolume > 0 && quotingVolume > 0) { var trade = new Trade( orderMessage.Order.AssetPairId, tradeMessage.Timestamp, baseVolume, quotingVolume, tradeMessage.Price ); await _candlesManager.ProcessTradeAsync(trade); } else { await _log.WriteWarningAsync(nameof(ProcessLimitTradesAsync), tradeMessage.ToJson(), "Got a Spot trade with non-positive price or volume value."); } } } }
private async Task ProcessLimitOrdersAsync(LimitOrdersMessage message) { if (message.Orders == null || !message.Orders.Any()) { return; } HashSet <string> limitOrderIds = message.Orders .Select(o => o.Order.Id) .ToHashSet(); foreach (var orderMessage in message.Orders) { if (orderMessage.Trades == null || !orderMessage.Trades.Any()) { continue; } string assetPairId = orderMessage.Order.AssetPairId; AssetPair assetPair = _assetPairsRepository.TryGet(assetPairId); if (assetPair == null) { _log.Error($"Asset pair {assetPairId} not found"); continue; } List <LimitOrdersMessage.Trade> allTrades = message.Orders.SelectMany(x => x.Trades).ToList(); string marketDataKey = RedisService.GetMarketDataKey(assetPairId); string baseVolumeKey = RedisService.GetMarketDataBaseVolumeKey(assetPairId); string quoteVolumeKey = RedisService.GetMarketDataQuoteVolumeKey(assetPairId); string priceKey = RedisService.GetMarketDataPriceKey(assetPairId); foreach (var tradeMessage in orderMessage.Trades.OrderBy(t => t.Timestamp).ThenBy(t => t.Index)) { long maxIndex = allTrades .Where(x => x.OppositeOrderId == tradeMessage.OppositeOrderId) .Max(t => t.Index); var price = (decimal)tradeMessage.Price; string priceString = price.ToString(CultureInfo.InvariantCulture); var nowDate = tradeMessage.Timestamp; var nowTradeDate = nowDate.AddMilliseconds(tradeMessage.Index); await Task.WhenAll( _database.HashSetAsync(marketDataKey, nameof(MarketSlice.LastPrice), priceString), _database.SortedSetAddAsync(priceKey, RedisExtensions.SerializeWithTimestamp(priceString, nowTradeDate), nowTradeDate.ToUnixTime()) ); var isOppositeOrderIsLimit = limitOrderIds.Contains(tradeMessage.OppositeOrderId); // If opposite order is market order, then unconditionally takes the given limit order. // But if both of orders are limit orders, we should take only one of them. if (isOppositeOrderIsLimit) { var isBuyOrder = orderMessage.Order.Volume > 0; // Takes trade only for the sell limit orders if (isBuyOrder) { continue; } } decimal baseVolume; decimal quotingVolume; if (tradeMessage.Asset == assetPair.BaseAssetId) { baseVolume = (decimal)tradeMessage.Volume; quotingVolume = (decimal)tradeMessage.OppositeVolume; } else { baseVolume = (decimal)tradeMessage.OppositeVolume; quotingVolume = (decimal)tradeMessage.Volume; } if (tradeMessage.Price > 0 && baseVolume > 0 && quotingVolume > 0) { double now = nowDate.ToUnixTime(); double from = (nowDate - _marketDataInterval).ToUnixTime(); decimal baseVolumeSum = baseVolume; decimal quoteVolumeSum = quotingVolume; decimal priceChange = 0; decimal highValue = (decimal)tradeMessage.Price; decimal lowValue = (decimal)tradeMessage.Price; var tasks = new List <Task>(); var baseVolumesDataTask = _database.SortedSetRangeByScoreAsync(baseVolumeKey, from, now); var quoteVolumesDataTask = _database.SortedSetRangeByScoreAsync(quoteVolumeKey, from, now); var priceDataTask = _database.SortedSetRangeByScoreAsync(priceKey, from, now); await Task.WhenAll(baseVolumesDataTask, quoteVolumesDataTask, priceDataTask); baseVolumeSum += baseVolumesDataTask.Result .Where(x => x.HasValue) .Sum(x => RedisExtensions.DeserializeTimestamped <decimal>(x)); quoteVolumeSum += quoteVolumesDataTask.Result .Where(x => x.HasValue) .Sum(x => RedisExtensions.DeserializeTimestamped <decimal>(x)); var currentHigh = priceDataTask.Result.Any(x => x.HasValue) ? priceDataTask.Result .Where(x => x.HasValue) .Max(x => RedisExtensions.DeserializeTimestamped <decimal>(x)) : (decimal?)null; if (currentHigh.HasValue && currentHigh.Value > highValue) { highValue = currentHigh.Value; } var currentLow = priceDataTask.Result.Any(x => x.HasValue) ? priceDataTask.Result .Where(x => x.HasValue) .Min(x => RedisExtensions.DeserializeTimestamped <decimal>(x)) : (decimal?)null; if (currentLow.HasValue && currentLow.Value < lowValue) { lowValue = currentLow.Value; } var pricesData = priceDataTask.Result; if (pricesData.Any() && pricesData[0].HasValue) { decimal openPrice = RedisExtensions.DeserializeTimestamped <decimal>(pricesData[0]); if (openPrice > 0) { priceChange = ((decimal)tradeMessage.Price - openPrice) / openPrice; } } tasks.Add(_database.SortedSetAddAsync(baseVolumeKey, RedisExtensions.SerializeWithTimestamp(baseVolume, nowTradeDate), now)); tasks.Add(_database.SortedSetAddAsync(quoteVolumeKey, RedisExtensions.SerializeWithTimestamp(quotingVolume, nowTradeDate), now)); await Task.WhenAll(tasks); //send event only for the last trade in the order if (tradeMessage.Index == maxIndex) { var evt = new MarketDataChangedEvent { AssetPairId = assetPairId, VolumeBase = baseVolumeSum, VolumeQuote = quoteVolumeSum, PriceChange = priceChange, LastPrice = (decimal)tradeMessage.Price, High = highValue, Low = lowValue }; _cqrsEngine.PublishEvent(evt, MarketDataBoundedContext.Name); try { await _tickerWriter.InsertOrReplaceAsync(new Ticker(assetPairId) { VolumeBase = baseVolumeSum, VolumeQuote = quoteVolumeSum, PriceChange = priceChange, LastPrice = (decimal)tradeMessage.Price, High = highValue, Low = lowValue, UpdatedDt = nowTradeDate }); } catch (Exception ex) { _log.Error(ex, "Error sending ticker to MyNySqlServer", context: evt.ToJson()); } } } } } }