private async Task LoadStoredCandles(CandleMonitorData candleMonitorData, DateTime?from, CandleSource candleSource)
        {
            // Pre load anything we've already got and fire it off on the event
            var series = await candleMonitorData.DataSource.Load(from);

            // Fire the new candles event
            _logger.LogTrace("{settings} CandleMonitor.StartupAsync firing CandlesReceivedEvent for source {candleSource}", candleMonitorData.Settings, candleSource);

            try
            {
                _candleProducer.Send(new CandlesReceivedEventArgs(candleMonitorData.Settings, series, candleSource));
            }
            catch (Exception e)
            {
                _logger.LogError(e, "{settings} CandleMonitor.StartupAsync.OnCandlesReceived threw an error", candleMonitorData.Settings);
            }
        }
        public override async Task StartupAsync()
        {
            await Task.WhenAll(
                _startupWorkflow.OverlayMonitor.Task,  // Allow overlay runners to be setup, ready to receive events;
                _startupWorkflow.StrategyMonitor.Task, // Allow the strategy runners to be setup, ready to receive events
                _startupWorkflow.SocketMonitor.Task    // Wait till the socket events are all fired up;
                );

            _logger.LogDebug("CandleMonitor Startup Running");

            // The Overlay & Strategy Runners are all Setup, ready to receive Ticks via the .CandlesReceived event
            // This proc tells us all the Markets to monitor periodically, and the ones with a TradeFromUtc should be
            // polled live. This TradeFromUtc is the MIN(_) data should there be multiple runners for the same market.
            // If a Strategy requires a secondary market (for overlay data into ML model) then the secondary market is
            // populated as live data as well (even if there is no runner with it as a primary market.
            // It also returns the markets with *active* overlay data, to ensure we poll to collect it (either live or periodically)

            foreach (var item in _candleMonitorFeedProvider.GetFeeds())
            {
                var candleMonitorData = new CandleMonitorData(item, _provider, _appSetting);

                // TradeFrom.HasValue is the "fast" markets being trades, so pre-load their data
                if (item.TradeFromUtc.HasValue)
                {
                    await LoadStoredCandles(candleMonitorData, item.TradeFromUtc.Value, CandleSource.DatabaseStrategyPreLoad);
                }

                if (item.HasOverlay)
                {
                    // Pre-load everything to ensure calculations work out ok
                    var allDataPointTime = new DateTime(2015, 1, 1).AsUtc();
                    await LoadStoredCandles(candleMonitorData, allDataPointTime, CandleSource.DatabaseOverlayHistoric);
                }

                _state.AddOrUpdate(item.MarketFeedSettings, candleMonitorData, (s, d) => candleMonitorData);

                _logger.LogTrace("CandleMonitor.StartupAsync for {settings} have completed pre-loading candles", item.MarketFeedSettings);
            }

            _logger.LogDebug("CandleMonitor Startup Complete");
            _startupWorkflow.CandleMonitor.TrySetResult(true);
        }
        private async Task RunForGranularity(CandleMonitorData currentState)
        {
            if (currentState.NextRunUtc >= DateTime.UtcNow)
            {
                _logger.LogTrace("CandleMonitor delaying {settings}, next run date is  {NextRunDate}", currentState.Settings, currentState.NextRunUtc);
                return;
            }

            if (currentState.IsRunning)
            {
                _logger.LogTrace("{settings} is still running, skipping this heartbeat", currentState.Settings);
                return;
            }

            currentState.SetRunning();

            var utcNow         = DateTime.UtcNow; // Store this so we're using the same "now" value for longer running batches
            var periodStartUtc = currentState.DataSource.LastUpdatedUtc;

            if (periodStartUtc.Kind != DateTimeKind.Utc)
            {
                throw new ArgumentNotUtcException(nameof(periodStartUtc), periodStartUtc);
            }

            _logger.LogTrace("CandleMonitor {settings} has data up to {lastUpdated}, getting more...", currentState.Settings, periodStartUtc);

            var maxBatch     = currentState.BatchSize;
            var periodEndUtc = periodStartUtc.AddSeconds((maxBatch - 1) * 300 * currentState.Settings.GranularitySeconds);

            // Bring large ranges back to now..
            if (periodEndUtc >= utcNow)
            {
                periodEndUtc = utcNow;
                // Then check to ensure there is at least one full candle of data to ask for
                if (periodStartUtc.AddSeconds(currentState.Settings.GranularitySeconds) > periodEndUtc)
                {
                    _logger.LogDebug("{settings} Skipping... no full candle before utcNow", currentState.Settings);
                    currentState.StopRunning(periodEndUtc);
                    return;
                }
            }

            IList <Candle> candles;

            try
            {
                _logger.LogDebug("{settings} calling GetHistoricRates REST for {periodStartUtc:O} till {periodEndUtc:O}", currentState.Settings, periodStartUtc, periodEndUtc);
                candles = await _client.ProductsService.GetHistoricRatesAsync(currentState.Settings.ProductId, periodStartUtc, periodEndUtc, currentState.Settings.Granularity);
            }
            catch (Exception e)
            {
                if (e.InnerException is CoinbaseProHttpException inner)
                {
                    _logger.LogError(inner, "{settings} HTTP Error {StatusCode}", currentState.Settings, inner.StatusCode);
                }
                else
                {
                    _logger.LogError(e, "{settings} Error", currentState.Settings);
                }
                currentState.StopRunning(periodEndUtc);
                return;
            }

            // Exclude candles that end in the future, to ensure we only store complete candle data
            var series = BuildTimeSeries(currentState.Settings.ProductId,
                                         candles,
                                         currentState.Settings.GranularitySeconds,
                                         periodStartUtc, utcNow);

            if (series.TickCount == 0)
            {
                _logger.LogDebug("{settings} No candles in TimeSeries", currentState.Settings);

                if (periodEndUtc > DateTime.UtcNow.AddDays(-1))
                {
                    _logger.LogTrace("{settings} Finishing as caught up to utcNow minus 1 day", currentState.Settings);
                    currentState.StopRunning(periodEndUtc);
                    return;
                }

                // Allow running through void periods when catching up, or first run etc.
                _logger.LogTrace("{settings} Try next period in case of a void period", currentState.Settings);

                currentState.DataSource.LastUpdatedUtc = periodEndUtc; // This saves the data to db
                currentState.StopRunning(periodEndUtc);
                await DoPeriodicWorkAsync();

                return;
            }

            // Save out the TimeSeries values to the data store (CSV or SQL), including the MarketDataFeed.LastUpdatedDateTime value
            currentState.DataSource.Save(series);

            _logger.LogInformation("{settings} Saved for {TickCount} candles, ending {utc:O}", currentState.Settings, series.TickCount, series.LastTick.EndTime.InUtc().ToDateTimeUtc());

            // Fire the new candles event-------------
            _logger.LogTrace("{settings} CandleMonitor firing event start", currentState.Settings);

            try
            {
                _candleProducer.Send(new CandlesReceivedEventArgs(currentState.Settings, series, CandleSource.RestCall));
            }
            catch (Exception e)
            {
                _logger.LogError(e, "{settings} CandleMonitor.RunForGranularity.OnCandlesReceived threw an error", currentState.Settings);
            }

            _logger.LogTrace("{settings} CandleMonitor firing event end", currentState.Settings);
            // ---------------------------------------

            currentState.StopRunning(periodEndUtc);
        }