Beispiel #1
0
        /// <summary>
        /// Extends a candle by another one candle. These two candles must be compatible by TimeStamp, TimeInterval and PriceType.
        /// </summary>
        public static ICandle ExtendBy(this ICandle self, ICandle newCandle)
        {
            if (self.Timestamp != newCandle.Timestamp)
            {
                throw new InvalidOperationException("It's impossible to extend a candle by another one with the different time stamp.");
            }

            if (self.TimeInterval != newCandle.TimeInterval)
            {
                throw new InvalidOperationException($"It's impossible to extend a candle with time interval {self.TimeInterval} by another one with {newCandle.TimeInterval}.");
            }

            if (self.PriceType != newCandle.PriceType)
            {
                throw new InvalidOperationException($"It's impossible to extend a candle eith price type {self.PriceType} by another one with {newCandle.PriceType}");
            }

            var selfIsOlder = self.LastUpdateTimestamp <= newCandle.LastUpdateTimestamp;

            return(Candle.Create(
                       self.AssetPairId,
                       self.PriceType,
                       self.TimeInterval,
                       self.Timestamp,
                       selfIsOlder ? self.Open : newCandle.Open,
                       selfIsOlder ? newCandle.Close : self.Close,
                       Math.Max(self.High, newCandle.High),
                       Math.Min(self.Low, newCandle.Low),
                       Convert.ToDouble((decimal)self.TradingVolume + (decimal)newCandle.TradingVolume), // It's a bit messy, but really allows to keep precision in addiction operation.
                       Convert.ToDouble((decimal)self.TradingOppositeVolume + (decimal)newCandle.TradingOppositeVolume),
                       selfIsOlder ? newCandle.LastTradePrice : self.LastTradePrice,
                       selfIsOlder ? newCandle.LastUpdateTimestamp : self.LastUpdateTimestamp
                       ));
        }
        private static ICandle DeserializeCandle(byte[] value, string assetPairId, CandlePriceType priceType, CandleTimeInterval timeInterval)
        {
            // value is:
            // 0 .. TimestampFormat.Length - 1 bytes: timestamp as yyyyMMddHHmmss in ASCII
            // TimestampFormat.Length .. end bytes: serialized RedistCachedCandle

            var timestampLength = TimestampFormat.Length;
            var timestampString = Encoding.ASCII.GetString(value, 0, timestampLength);
            var timestamp       = DateTime.ParseExact(timestampString, TimestampFormat, CultureInfo.InvariantCulture);

            using (var stream = new MemoryStream(value, timestampLength, value.Length - timestampLength, writable: false))
            {
                var cachedCandle = MessagePack.MessagePackSerializer.Deserialize <RedisCachedCandle>(stream);

                return(Candle.Create(
                           assetPairId,
                           priceType,
                           timeInterval,
                           timestamp,
                           cachedCandle.Open,
                           cachedCandle.Close,
                           cachedCandle.High,
                           cachedCandle.Low,
                           cachedCandle.TradingVolume,
                           cachedCandle.TradingOppositVolume,
                           cachedCandle.LastTradePrice,
                           cachedCandle.LastUpdateTimestamp));
            }
        }
Beispiel #3
0
        private ICandle NormalizeCandlePrices(ICandle candle)
        {
            var lastNonZeroPrice = GetLastNonZeroPrice(candle.AssetPairId, candle.PriceType);
            var open             = ConvertToDecimal(candle.Open);
            var close            = ConvertToDecimal(candle.Close);
            var high             = ConvertToDecimal(candle.High);
            var low = ConvertToDecimal(candle.Low);

            if (open == 0 || close == 0 || high == 0 || low == 0)
            {
                open  = open == 0 ? lastNonZeroPrice : open;
                close = close == 0 ? lastNonZeroPrice : close;
                high  = high == 0 ? lastNonZeroPrice : high;
                low   = low == 0 ? lastNonZeroPrice : low;

                return(Candle.Create(candle.AssetPairId, candle.PriceType, candle.TimeInterval, candle.Timestamp,
                                     (double)open,
                                     (double)close,
                                     (double)high,
                                     (double)low,
                                     0,
                                     0,
                                     0,
                                     candle.Timestamp));
            }

            return(candle);
        }
        /// <summary>
        /// Creates mid candle of two candles (ask and bid)
        /// </summary>
        /// <param name="askCandle">first candle</param>
        /// <param name="bidCandle">second candle</param>
        public static ICandle Create(ICandle askCandle, ICandle bidCandle)
        {
            if (askCandle == null || bidCandle == null)
            {
                return(askCandle ?? bidCandle);
            }

            if (askCandle.AssetPairId != bidCandle.AssetPairId)
            {
                throw new InvalidOperationException($"Can't create mid candle of different asset pairs. candle1={askCandle.ToJson()}, candle2={bidCandle.ToJson()}");
            }

            if (askCandle.PriceType != CandlePriceType.Ask)
            {
                throw new InvalidOperationException($"Ask candle should has according price type. candle={askCandle.ToJson()}");
            }

            if (bidCandle.PriceType != CandlePriceType.Bid)
            {
                throw new InvalidOperationException($"Bid candle should has according price type. candle={bidCandle.ToJson()}");
            }

            if (askCandle.TimeInterval != bidCandle.TimeInterval)
            {
                throw new InvalidOperationException($"Can't create mid candle of different time intervals. candle1={askCandle.ToJson()}, candle2={bidCandle.ToJson()}");
            }

            if (askCandle.Timestamp != bidCandle.Timestamp)
            {
                throw new InvalidOperationException($"Can't create mid candle from candles with different timestamps. candle1={askCandle.ToJson()}, candle2={bidCandle.ToJson()}");
            }

            return(Candle.Create(
                       open: (askCandle.Open + bidCandle.Open) / 2,
                       close: (askCandle.Close + bidCandle.Close) / 2,
                       high: (askCandle.High + bidCandle.High) / 2,
                       low: (askCandle.Low + bidCandle.Low) / 2,
                       assetPair: askCandle.AssetPairId,
                       priceType: CandlePriceType.Mid,
                       timeInterval: askCandle.TimeInterval,
                       timestamp: askCandle.Timestamp,
                       tradingVolume: askCandle.LastUpdateTimestamp > bidCandle.LastUpdateTimestamp
                    ? askCandle.TradingVolume
                    : bidCandle.TradingVolume,
                       tradingOppositeVolume: askCandle.LastUpdateTimestamp > bidCandle.LastUpdateTimestamp
                    ? askCandle.TradingOppositeVolume
                    : bidCandle.TradingOppositeVolume,
                       lastTradePrice: askCandle.LastUpdateTimestamp > bidCandle.LastUpdateTimestamp
                    ? askCandle.LastTradePrice
                    : bidCandle.LastTradePrice,
                       lastUpdateTimestamp: askCandle.LastUpdateTimestamp > bidCandle.LastUpdateTimestamp
                    ? askCandle.LastUpdateTimestamp
                    : bidCandle.LastUpdateTimestamp));
        }
Beispiel #5
0
        public MigrationCandleMergeResult Merge(string assetPair, CandlePriceType priceType, CandleTimeInterval timeInterval, DateTime timestamp, double open, double close, double low, double high)
        {
            var key = GetKey(assetPair, timeInterval, priceType);

            Candle oldCandle = null;
            var    newCandle = _candles.AddOrUpdate(key,
                                                    addValueFactory: k => Candle.Create(
                                                        assetPair: assetPair,
                                                        priceType: priceType,
                                                        timeInterval: timeInterval,
                                                        timestamp: timestamp,
                                                        open: open,
                                                        close: close,
                                                        high: high,
                                                        low: low,
                                                        tradingVolume: 0,
                                                        tradingOppositeVolume: 0,
                                                        lastTradePrice: 0,
                                                        lastUpdateTimestamp: timestamp),
                                                    updateValueFactory: (k, old) =>
            {
                oldCandle = old;

                // Start new candle?
                var intervalTimestamp = timestamp.TruncateTo(timeInterval);
                if (oldCandle.Timestamp != intervalTimestamp)
                {
                    return(Candle.Create(
                               assetPair: assetPair,
                               priceType: priceType,
                               timeInterval: timeInterval,
                               timestamp: intervalTimestamp,
                               open: open,
                               close: close,
                               high: high,
                               low: low,
                               tradingVolume: 0,
                               tradingOppositeVolume: 0,
                               lastTradePrice: 0,
                               lastUpdateTimestamp: timestamp));
                }

                return(oldCandle.Update(close, low, high, 0, 0, 0, timestamp));
            });

            return(new MigrationCandleMergeResult(newCandle, !newCandle.Equals(oldCandle)));
        }
        private async Task ProcessCandlesUpdatedEventAsync(CandlesUpdatedEvent candlesUpdate)
        {
            try
            {
                if (_cacheInitalizationService.InitializationState != CacheInitializationState.Idle)
                {
                    await Task.Delay(5000);

                    throw new InvalidOperationException("Initialization in progress");
                }

                var validationErrors = ValidateQuote(candlesUpdate);

                if (validationErrors.Any())
                {
                    var message = string.Join("\r\n", validationErrors);
                    _log.Warning(nameof(ProcessCandlesUpdatedEventAsync), message, context: candlesUpdate.ToJson());

                    return;
                }

                var candles = candlesUpdate.Candles
                              .Where(candleUpdate =>
                                     _candlesChecker.CanHandleAssetPair(candleUpdate.AssetPairId))
                              .Select(candleUpdate => Candle.Create(
                                          priceType: candleUpdate.PriceType,
                                          assetPair: candleUpdate.AssetPairId,
                                          timeInterval: candleUpdate.TimeInterval,
                                          timestamp: candleUpdate.CandleTimestamp,
                                          open: candleUpdate.Open,
                                          close: candleUpdate.Close,
                                          low: candleUpdate.Low,
                                          high: candleUpdate.High,
                                          tradingVolume: candleUpdate.TradingVolume,
                                          tradingOppositeVolume: candleUpdate.TradingOppositeVolume,
                                          lastTradePrice: candleUpdate.LastTradePrice,
                                          lastUpdateTimestamp: candleUpdate.ChangeTimestamp))
                              .ToArray();

                await _candlesManager.ProcessCandlesAsync(candles);
            }
            catch (Exception)
            {
                _log.Warning(nameof(ProcessCandlesUpdatedEventAsync), "Failed to process candle", context: candlesUpdate.ToJson());
                throw;
            }
        }
Beispiel #7
0
 public static ICandle CreateCandle(this TradeHistoryItem trade, string assetPairId, CandlePriceType priceType, CandleTimeInterval interval, decimal volumeMultiplier = 1.0M)
 {
     return(Candle.Create(
                assetPairId,
                priceType,
                interval,
                trade.DateTime.TruncateTo(interval),
                (double)trade.Price,
                (double)trade.Price,
                (double)trade.Price,
                (double)trade.Price,
                Convert.ToDouble((trade.IsStraight ? trade.Volume : trade.OppositeVolume) * volumeMultiplier),
                Convert.ToDouble((trade.IsStraight ? trade.OppositeVolume : trade.Volume) * volumeMultiplier),
                0, // Last Trade Price is enforced to be = 0
                trade.DateTime
                ));
 }
Beispiel #8
0
 /// <summary>
 /// Creates a new candle with all of the parameter values from self but with new time interval.
 /// </summary>
 public static ICandle RebaseToInterval(this ICandle self, CandleTimeInterval newInterval)
 {
     return(Candle.Create(
                self.AssetPairId,
                self.PriceType,
                newInterval,
                self.Timestamp.TruncateTo(newInterval),
                self.Open,
                self.Close,
                self.High,
                self.Low,
                self.TradingVolume,
                self.TradingOppositeVolume,
                self.LastTradePrice,
                self.LastUpdateTimestamp
                ));
 }
Beispiel #9
0
        public ICandle ToCandle(string assetPairId, CandlePriceType priceType, DateTime baseTime)
        {
            var timeStamp = baseTime.AddSeconds(Tick);

            return(Candle.Create(
                       open: Open,
                       close: Close,
                       high: High,
                       low: Low,
                       assetPair: assetPairId,
                       priceType: priceType,
                       timeInterval: CandleTimeInterval.Sec,
                       timestamp: timeStamp,
                       tradingVolume: 0,
                       tradingOppositeVolume: 0,
                       lastTradePrice: 0,
                       lastUpdateTimestamp: timeStamp));
        }
        public ICandle ToCandle(string assetPairId, CandlePriceType priceType, DateTime baseTime, CandleTimeInterval timeInterval)
        {
            var normalizedTick = Tick - GetIntervalTickOrigin(timeInterval);

            return(Candle.Create
                   (
                       open: Open,
                       close: Close,
                       high: High,
                       low: Low,
                       assetPair: assetPairId,
                       priceType: priceType,
                       timeInterval: timeInterval,
                       timestamp: baseTime.AddIntervalTicks(normalizedTick, timeInterval),
                       tradingVolume: TradingVolume,
                       tradingOppositeVolume: TradingOppositeVolume,
                       lastTradePrice: LastTradePrice,
                       lastUpdateTimestamp: LastUpdateTimestamp
                   ));
        }
        private async Task ProcessCandlesUpdatedEventAsync(CandlesUpdatedEvent candlesUpdate)
        {
            try
            {
                var validationErrors = ValidateQuote(candlesUpdate);
                if (validationErrors.Any())
                {
                    var message = string.Join("\r\n", validationErrors);
                    await _log.WriteWarningAsync(nameof(CandlesSubscriber), nameof(CandlesUpdatedEvent), candlesUpdate.ToJson(), message);

                    return;
                }

                var candles = candlesUpdate.Candles
                              .Where(candleUpdate =>
                                     Constants.StoredIntervals.Contains(candleUpdate.TimeInterval) &&
                                     _candlesChecker.CanHandleAssetPair(candleUpdate.AssetPairId))
                              .Select(candleUpdate => Candle.Create(
                                          priceType: candleUpdate.PriceType,
                                          assetPair: candleUpdate.AssetPairId,
                                          timeInterval: candleUpdate.TimeInterval,
                                          timestamp: candleUpdate.CandleTimestamp,
                                          open: candleUpdate.Open,
                                          close: candleUpdate.Close,
                                          low: candleUpdate.Low,
                                          high: candleUpdate.High,
                                          tradingVolume: candleUpdate.TradingVolume,
                                          tradingOppositeVolume: candleUpdate.TradingOppositeVolume,
                                          lastTradePrice: candleUpdate.LastTradePrice,
                                          lastUpdateTimestamp: candleUpdate.ChangeTimestamp))
                              .ToArray();

                await _candlesManager.ProcessCandlesAsync(candles);
            }
            catch (Exception)
            {
                await _log.WriteWarningAsync(nameof(CandlesSubscriber), nameof(ProcessCandlesUpdatedEventAsync), candlesUpdate.ToJson(), "Failed to process candle");

                throw;
            }
        }
        private Candle FixMidCandleOrDefault(ICandle midCandle, ICandle askCandle, ICandle bidCandle, int assetPairAccuracy)
        {
            var openPrice  = Math.Round((askCandle.Open + bidCandle.Open) / 2, assetPairAccuracy);
            var closePrice = Math.Round((askCandle.Close + bidCandle.Close) / 2, assetPairAccuracy);
            var highPrice  = Math.Round((askCandle.High + bidCandle.High) / 2, assetPairAccuracy);
            var lowPrice   = Math.Round((askCandle.Low + bidCandle.Low) / 2, assetPairAccuracy);

            var delta = Math.Pow(10, -assetPairAccuracy);

            bool isCandleCorrupted = Math.Abs(midCandle.Open - openPrice) >= delta || Math.Abs(midCandle.Close - closePrice) >= delta ||
                                     Math.Abs(midCandle.High - highPrice) >= delta || Math.Abs(midCandle.Low - lowPrice) >= delta;

            if (isCandleCorrupted)
            {
                return(Candle.Create(midCandle.AssetPairId, midCandle.PriceType, midCandle.TimeInterval, midCandle.Timestamp,
                                     openPrice, closePrice, highPrice, lowPrice, midCandle.TradingVolume, midCandle.TradingOppositeVolume,
                                     midCandle.LastTradePrice, midCandle.LastUpdateTimestamp));
            }

            return(null);
        }
        public void Test_candles_merged_to_bigger_interval()
        {
            const string assetPair = "BTCUSD";
            var          date      = new DateTime(2019, 03, 04, 0, 1, 0);

            var candles = new List <Candle>
            {
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date, 4000, 4100, 4100, 4000,
                              0, 0, 0, date),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(1), 4000, 4100,
                              4100, 4000, 0, 0, 0, date.AddMinutes(1)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(2), 3900, 4100,
                              4200, 3800, 0, 0, 0, date.AddMinutes(2)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(3), 4000, 4150,
                              4100, 4000, 0, 0, 0, date.AddMinutes(3)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(4), 3850, 4100,
                              4100, 4000, 0, 0, 0, date.AddMinutes(4)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(5), 4000, 4100,
                              4100, 4000, 0, 0, 0, date.AddMinutes(5)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(6), 4000, 4100,
                              4300, 3700, 0, 0, 0, date.AddMinutes(6)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(7), 4000, 4100,
                              4100, 4000, 0, 0, 0, date.AddMinutes(7)),
                Candle.Create(assetPair, CandlePriceType.Ask, CandleTimeInterval.Minute, date.AddMinutes(8), 4000, 4180,
                              4100, 4000, 0, 0, 0, date.AddMinutes(8))
            };

            var newCandles = CandlesMerger.MergeIntoBiggerIntervals(candles, CandleTimeInterval.Min5).ToArray();

            Assert.AreEqual(2, newCandles.Length);
            Assert.IsTrue(newCandles.All(x => x.TimeInterval == CandleTimeInterval.Min5));
            Assert.AreEqual(4000, newCandles[0].Open);
            Assert.AreEqual(4150, newCandles[0].Close);
            Assert.AreEqual(4200, newCandles[0].High);
            Assert.AreEqual(3800, newCandles[0].Low);
            Assert.AreEqual(3850, newCandles[1].Open);
            Assert.AreEqual(4180, newCandles[1].Close);
            Assert.AreEqual(4300, newCandles[1].High);
            Assert.AreEqual(3700, newCandles[1].Low);
        }
Beispiel #14
0
        private IEnumerable <ICandle> GenerateCandles(
            AssetPair assetPair,
            CandlePriceType priceType,
            DateTime exclusiveStartDate,
            DateTime exclusiveEndDate,
            decimal exclusiveStartPrice,
            decimal exclusiveEndPrice,
            decimal spread)
        {
            var start = exclusiveStartDate.AddSeconds(1);
            var end   = exclusiveEndDate.AddSeconds(-1);

            if (exclusiveEndDate - exclusiveStartDate <= TimeSpan.FromSeconds(1))
            {
                yield break;
            }

            var duration  = (decimal)(exclusiveEndDate - exclusiveStartDate).TotalSeconds;
            var prevClose = exclusiveStartPrice;
            var trendSign = exclusiveStartPrice < exclusiveEndPrice ? 1 : -1;
            // Absolute start to end price change in % of start price
            var totalPriceChange = exclusiveStartPrice != 0m
                ? Math.Abs((exclusiveEndPrice - exclusiveStartPrice) / exclusiveStartPrice)
                : Math.Abs(exclusiveEndPrice - exclusiveStartPrice);
            var stepPriceChange = totalPriceChange / duration;
            var effectiveSpread = spread != 0
                ? Math.Abs(spread)
                : totalPriceChange * 0.2m;
            // Start in opposite dirrection
            var currentTrendSign = -trendSign;

            if (effectiveSpread == 0)
            {
                if (exclusiveStartPrice != 0)
                {
                    effectiveSpread = exclusiveStartPrice * 0.2m;
                }
                else if (exclusiveEndPrice != 0)
                {
                    effectiveSpread = exclusiveEndPrice * 0.2m;
                }
                else
                {
                    effectiveSpread = (decimal)Math.Pow(10, -assetPair.Accuracy / 4d);
                }
            }

            if (stepPriceChange == 0)
            {
                stepPriceChange = effectiveSpread / duration;
            }

            var backupMid = exclusiveStartPrice;

            if (backupMid == 0)
            {
                backupMid = exclusiveEndPrice != 0 ? exclusiveEndPrice : effectiveSpread;
            }

            //File.WriteAllLines(@"C:\temp\candles.csv", new[]
            //{
            //    "timestamp,t,mid,min,max,open,close,low,high,closePriceMaxDeviation,rangeMinMaxDeviationFactor,height"
            //});

            for (var timestamp = start; timestamp <= end; timestamp = timestamp.AddSeconds(1))
            {
                // Interpolation parameter (0..1)
                var t = (decimal)(timestamp - exclusiveStartDate).TotalSeconds / duration;

                // Lineary interpolated price for current candle
                var mid = MathEx.Lerp(exclusiveStartPrice, exclusiveEndPrice, t);

                if (mid <= 0)
                {
                    mid = backupMid;
                }

                var halfSpread = effectiveSpread * 0.5m;

                // Next candle opens at prev candle close and from 5% up to 50% of stepPriceChange,
                // direction is dependent of trend sign
                var open = prevClose + stepPriceChange * _rnd.NextDecimal(0.05m, 0.5m) * currentTrendSign;

                if (open <= 0)
                {
                    open = mid;
                }

                // Lets candles goes from 10% near the generated range boundaries and
                // up to 100% of the spread in the middle of the generated range,
                // and only inside the spread at the range boundaries
                var rangeMinMaxDeviationFactor = MathEx.Lerp(0.1m, 1m, 2m * (0.5m - Math.Abs(0.5m - t)));
                var min = mid - halfSpread * rangeMinMaxDeviationFactor;
                var max = mid + halfSpread * rangeMinMaxDeviationFactor;

                if (min <= 0)
                {
                    min = mid;
                }

                var maxClosePriceDeviation = CalculateMaxClosePriceDeviation(currentTrendSign, open, mid, min, max);

                // Candle can be closed from the 10% and up to closePriceMaxDeviation% of price change from the open price
                // But only inside min/max and if it touched min/max, the sign will be changed, and close price will be regenerated

                decimal GenerateClose(decimal sign)
                {
                    return(open + stepPriceChange * _rnd.NextDecimal(0.05m, maxClosePriceDeviation) * sign);
                }

                var close = GenerateClose(currentTrendSign);

                if (close >= max && currentTrendSign > 0 ||
                    close <= min && currentTrendSign < 0)
                {
                    currentTrendSign = -currentTrendSign;

                    close = GenerateClose(currentTrendSign);
                }

                if (close <= 0)
                {
                    close = mid;
                }

                // Max low/high deviation from open/close is 20% - 50% of candle height, depending of current trend sign
                var height = Math.Abs(open - close);
                var high   = Math.Max(open, close) + _rnd.NextDecimal(0m, currentTrendSign > 0 ? 0.5m : 0.2m) * height;
                var low    = Math.Min(open, close) - _rnd.NextDecimal(0m, currentTrendSign < 0 ? 0.5m : 0.2m) * height;

                if (low <= 0)
                {
                    low = Math.Min(open, close);
                }

                if (high <= 0)
                {
                    high = Math.Max(open, close);
                }

                //File.AppendAllLines(@"C:\temp\candles.csv", new []
                //{
                //    $"{timestamp},{t},{mid},{min},{max},{open},{close},{low},{high},{maxClosePriceDeviation},{rangeMinMaxDeviationFactor},{height}"
                //});

                var newCandle = Candle.Create(
                    assetPair.Id,
                    priceType,
                    CandleTimeInterval.Sec,
                    timestamp,
                    (double)Math.Round(open, assetPair.Accuracy),
                    (double)Math.Round(close, assetPair.Accuracy),
                    (double)Math.Round(high, assetPair.Accuracy),
                    (double)Math.Round(low, assetPair.Accuracy),
                    0,
                    0,
                    0,
                    timestamp);

                if (open == 0 || close == 0 || high == 0 || low == 0)
                {
                    var context = new
                    {
                        AssetPair = new
                        {
                            assetPair.Id,
                            assetPair.Accuracy
                        },
                        exclusiveStartDate,
                        exclusiveEndDate,
                        start,
                        end,
                        exclusiveStartPrice,
                        exclusiveEndPrice,
                        duration,
                        spread,
                        effectiveSpread,
                        prevClose,
                        trendSign,
                        totalPriceChange,
                        timestamp,
                        t,
                        mid,
                        halfSpread,
                        currentTrendSign,
                        rangeMinMaxDeviationFactor,
                        min,
                        max,
                        height
                    };

                    throw new InvalidOperationException($"Generated candle {newCandle.ToJson()} has zero prices. Context: {context.ToJson()}");
                }

                prevClose = close;

                yield return(newCandle);
            }
        }
        /// <summary>
        /// Merges all of candles placed in chronological order
        /// </summary>
        /// <param name="candles">Candles in hronological order</param>
        /// <param name="interval">new candle time interval</param>
        /// <param name="newTimestamp">
        /// <see cref="ICandle.Timestamp"/> of merged candle, if not specified,
        /// then <see cref="ICandle.Timestamp"/> of all candles should be equals,
        /// and it will be used as merged candle <see cref="ICandle.Timestamp"/>
        /// </param>
        /// <returns>Merged candle, or null, if no candles to merge</returns>
        private static ICandle MergeAll(IEnumerable <ICandle> candles, CandleTimeInterval interval, DateTime?newTimestamp = null)
        {
            if (candles == null)
            {
                return(null);
            }

            var open                  = 0d;
            var close                 = 0d;
            var high                  = 0d;
            var low                   = 0d;
            var tradingVolume         = 0d;
            var tradingOppositeVolume = 0d;
            var lastTradePrice        = 0d;
            var assetPairId           = string.Empty;
            var priceType             = CandlePriceType.Unspecified;
            var timeInterval          = CandleTimeInterval.Unspecified;
            var timestamp             = DateTime.MinValue;
            var lastUpdateTimestamp   = DateTime.MinValue;
            var count                 = 0;

            using (var enumerator = candles.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    var candle = enumerator.Current;

                    if (count == 0)
                    {
                        open                  = candle.Open;
                        close                 = candle.Close;
                        high                  = candle.High;
                        low                   = candle.Low;
                        tradingVolume         = candle.TradingVolume;
                        tradingOppositeVolume = candle.TradingOppositeVolume;
                        lastTradePrice        = candle.LastTradePrice;
                        assetPairId           = candle.AssetPairId;
                        priceType             = candle.PriceType;
                        timeInterval          = candle.TimeInterval;
                        timestamp             = candle.Timestamp;
                        lastUpdateTimestamp   = candle.LastUpdateTimestamp;
                    }
                    else
                    {
                        if (assetPairId != candle.AssetPairId)
                        {
                            throw new InvalidOperationException($"Can't merge candles of different asset pairs. Current candle={candle.ToJson()}");
                        }

                        if (priceType != candle.PriceType)
                        {
                            throw new InvalidOperationException($"Can't merge candles of different price types. Current candle={candle.ToJson()}");
                        }

                        if (timeInterval != candle.TimeInterval)
                        {
                            throw new InvalidOperationException($"Can't merge candles of different time intervals. Current candle={candle.ToJson()}");
                        }

                        if (!newTimestamp.HasValue && timestamp != candle.Timestamp)
                        {
                            throw new InvalidOperationException($"Can't merge candles with different timestamps. Current candle={candle.ToJson()}");
                        }

                        close                  = candle.Close;
                        high                   = Math.Max(high, candle.High);
                        low                    = Math.Min(low, candle.Low);
                        tradingVolume         += candle.TradingVolume;
                        tradingOppositeVolume += candle.TradingOppositeVolume;
                        lastUpdateTimestamp    = candle.LastUpdateTimestamp > lastUpdateTimestamp
                            ? candle.LastUpdateTimestamp
                            : lastUpdateTimestamp;
                        lastTradePrice = candle.LastUpdateTimestamp > lastUpdateTimestamp
                            ? candle.LastTradePrice
                            : lastTradePrice;
                    }

                    count++;
                }
            }

            if (count > 0)
            {
                return(Candle.Create(
                           open: open,
                           close: close,
                           high: high,
                           low: low,
                           assetPair: assetPairId,
                           priceType: priceType,
                           timeInterval: interval,
                           timestamp: newTimestamp ?? timestamp,
                           tradingVolume: tradingVolume,
                           tradingOppositeVolume: tradingOppositeVolume,
                           lastTradePrice: lastTradePrice,
                           lastUpdateTimestamp: lastUpdateTimestamp));
            }

            return(null);
        }