private async Task SaveMarketDataAsync(List <MarketSlice> marketData, Dictionary <string, IList <Candle> > prices) { var tasks = new List <Task>(); List <string> assetPairIds = prices.Keys.ToList(); var pricesValue = new Dictionary <string, SortedSetEntry[]>(); var baseVolumesValue = new Dictionary <string, SortedSetEntry[]>(); var quoteVolumesValue = new Dictionary <string, SortedSetEntry[]>(); tasks.Add(_database.SortedSetAddAsync(RedisService.GetAssetPairsKey(), marketData.Select(x => new SortedSetEntry(x.AssetPairId, 0)).ToArray())); foreach (var assetPairId in assetPairIds) { pricesValue.Add(assetPairId, prices[assetPairId].Select(x => new SortedSetEntry(RedisExtensions.SerializeWithTimestamp((decimal)x.Open, x.DateTime), x.DateTime.ToUnixTime())).ToArray()); baseVolumesValue.Add(assetPairId, prices[assetPairId].Select(x => new SortedSetEntry(RedisExtensions.SerializeWithTimestamp((decimal)x.TradingVolume, x.DateTime), x.DateTime.ToUnixTime())).ToArray()); quoteVolumesValue.Add(assetPairId, prices[assetPairId].Select(x => new SortedSetEntry(RedisExtensions.SerializeWithTimestamp((decimal)x.TradingOppositeVolume, x.DateTime), x.DateTime.ToUnixTime())).ToArray()); } foreach (MarketSlice marketSlice in marketData) { tasks.Add(_database.HashSetAsync(RedisService.GetMarketDataKey(marketSlice.AssetPairId), marketSlice.ToMarketSliceHash())); } await Task.WhenAll(tasks); tasks = new List <Task>(); foreach (MarketSlice marketSlice in marketData) { tasks.Add(_priceWriter.InsertOrReplaceAsync(marketSlice.ToPrice())); tasks.Add(_tickerWriter.InsertOrReplaceAsync(marketSlice.ToTicker())); } await Task.WhenAll(tasks); tasks = new List <Task>(); foreach (var assetPairId in assetPairIds) { tasks.Add(_database.SortedSetAddAsync(RedisService.GetMarketDataPriceKey(assetPairId), pricesValue[assetPairId])); tasks.Add(_database.SortedSetAddAsync(RedisService.GetMarketDataBaseVolumeKey(assetPairId), baseVolumesValue[assetPairId])); tasks.Add(_database.SortedSetAddAsync(RedisService.GetMarketDataQuoteVolumeKey(assetPairId), quoteVolumesValue[assetPairId])); } await Task.WhenAll(tasks); }
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()); } } } } } }