public async Task Getting_enabled_pair_returns_enabled_pair()
        {
            // Arrange
            _assetsServiceMock
            .Setup(s => s.TryGetAssetPairAsync(It.Is <string>(a => a == "EURUSD"), It.IsAny <CancellationToken>()))
            .ReturnsAsync((string a, CancellationToken t) => new AssetPair {
                Id = a, IsDisabled = false
            });

            // Act
            var pair = await _manager.TryGetEnabledPairAsync("EURUSD");

            // Assert
            Assert.IsNotNull(pair);
            Assert.AreEqual("EURUSD", pair.Id);
        }
        public LongTaskLaunchResult Filtrate(ICandlesFiltrationRequest request, bool analyzeOnly)
        {
            // We should not run filtration multiple times before the first attempt ends.
            if (Health != null && Health.State == CandlesFiltrationState.InProgress)
            {
                return(LongTaskLaunchResult.AlreadyInProgress);
            }

            // And also we should check if the specified asset pair is enabled.
            var storedAssetPair = _assetPairsManager.TryGetEnabledPairAsync(request.AssetPairId).GetAwaiter().GetResult();

            if (storedAssetPair == null || !_candlesHistoryRepository.CanStoreAssetPair(request.AssetPairId))
            {
                return(LongTaskLaunchResult.AssetPairNotSupported);
            }

            var epsilon = Math.Pow(10, -storedAssetPair.Accuracy);

            _log.Info(nameof(Filtrate), $"Starting candles with extreme price filtration for {request.AssetPairId}...");

            Health = new CandlesFiltrationHealthReport(request.AssetPairId, request.LimitLow, request.LimitHigh, analyzeOnly);

            var priceTypeTasks = new List <Task>();

            if (request.PriceType.HasValue)
            {
                priceTypeTasks.Add(
                    DoFiltrateAsync(request.AssetPairId, request.LimitLow, request.LimitHigh, request.PriceType.Value, epsilon,
                                    analyzeOnly));
            }
            else
            {
                foreach (var priceType in Constants.StoredPriceTypes)
                {
                    priceTypeTasks.Add(
                        DoFiltrateAsync(request.AssetPairId, request.LimitLow, request.LimitHigh, priceType, epsilon,
                                        analyzeOnly));
                }
            }

            Task.WhenAll(priceTypeTasks.ToArray()).ContinueWith(t =>
            {
                Health.State = CandlesFiltrationState.Finished;

                if (analyzeOnly)
                {
                    _log.Info(nameof(Filtrate),
                              $"Filtration for {request.AssetPairId} finished: analyze only. Total amount of candles to delete: {Health.DeletedCandlesCount.Values.Sum()}, " +
                              $"total amount of candles to replace: {Health.ReplacedCandlesCount.Values.Sum()}. Errors count: {Health.Errors.Count}.");
                }
                else
                {
                    _log.Info(nameof(Filtrate),
                              $"Filtration for {request.AssetPairId} finished. Total amount of deleted Sec candles: {Health.DeletedCandlesCount.Values.Sum()}, " +
                              $"total amount of replaced bigger candles: {Health.ReplacedCandlesCount.Values.Sum()}. Errors count: {Health.Errors.Count}.");
                }
            });

            return(LongTaskLaunchResult.Started);
        }
Beispiel #3
0
        public bool Migrate(DateTime?removeByDate, string[] assetPairIds)
        {
            if (!MigrationEnabled)
            {
                return(false);
            }

            // We should not run migration multiple times before the first attempt ends.
            if (!_tradesMigrationHealthService.CanStartMigration)
            {
                return(false);
            }

            // First of all, we will check if we can store the requested asset pairs. Additionally, let's
            // generate asset search tokens for using it in TradesSqlHistoryRepository (which has no access
            // to AssetPairsManager).
            var assetSearchTokens = new List <(string AssetPairId, string SearchToken, string ReverseSearchToken)>();

            foreach (var assetPairId in assetPairIds)
            {
                var storedAssetPair = _assetPairsManager.TryGetEnabledPairAsync(assetPairId).GetAwaiter().GetResult();
                if (storedAssetPair == null)
                {
                    _log.WriteInfoAsync(nameof(TradesMigrationManager), nameof(Migrate),
                                        $"Trades migration: Asset pair {assetPairId} is not currently enabled. Skipping.").GetAwaiter().GetResult();
                    continue;
                }

                assetSearchTokens.Add((
                                          AssetPairId: assetPairId,
                                          SearchToken: storedAssetPair.BaseAssetId + storedAssetPair.QuotingAssetId,
                                          ReverseSearchToken: storedAssetPair.QuotingAssetId + storedAssetPair.BaseAssetId));
            }

            // Creating a blank health report
            _tradesMigrationHealthService.Prepare(_sqlQueryBatchSize, removeByDate);

            // If there is nothing to migrate, just return "success".
            if (!assetSearchTokens.Any())
            {
                _tradesMigrationHealthService.State = TradesMigrationState.Finished;
                _log.WriteInfoAsync(nameof(TradesMigrationManager), nameof(Migrate),
                                    $"Trades migration: None of the requested asset pairs can be stored. Migration canceled.").GetAwaiter().GetResult();
                return(true);
            }

            // 1. We do not parallel the migration of different asset pairs consciously.
            // 2. If we have no upper date-time limit for migration, we migrate everything.
            // 3. We do not await while the migration process is finished for it may take a lot of time. Immediately return to a caller code instead.
            _tradesMigrationService.MigrateTradesCandlesAsync(removeByDate, assetSearchTokens);

            return(true);
        }
        public async Task <LongTaskLaunchResult> RunFixMidPricesAsync(string assetPairId, bool analyzeOnly)
        {
            // We should not run filtration multiple times before the first attempt ends.
            if (Health != null && Health.State == CandlesFiltrationState.InProgress)
            {
                return(LongTaskLaunchResult.AlreadyInProgress);
            }

            // And also we should check if the specified asset pair is enabled.
            var storedAssetPair = await _assetPairsManager.TryGetEnabledPairAsync(assetPairId);

            if (storedAssetPair == null || !_candlesHistoryRepository.CanStoreAssetPair(assetPairId))
            {
                return(LongTaskLaunchResult.AssetPairNotSupported);
            }

            _log.Info($"Starting mid candles price fix for {assetPairId}...");

            Health = new MidPricesFixHealthReport(assetPairId, analyzeOnly);

#pragma warning disable 4014
            Task.Run(async() =>
#pragma warning restore 4014
            {
                await FixMidPricesAsync(assetPairId, storedAssetPair.Accuracy, analyzeOnly);

                Health.State = CandlesFiltrationState.Finished;

                if (analyzeOnly)
                {
                    _log.Info($"Mid candle prices fix for {assetPairId} finished: analyze only. Total amount of corrupted candles: {Health.CorruptedCandlesCount}, " +
                              $" errors count: {Health.Errors.Count}.");
                }
                else
                {
                    _log.Info($"Mid candle prices fix for {assetPairId} finished. Total amount of corrupted candles: {Health.CorruptedCandlesCount}, " +
                              $"errors count: {Health.Errors.Count}.");
                }
            });

            return(LongTaskLaunchResult.Started);
        }
        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.");
                    }
                }
            }
        }
        public async Task ProcessMtQuoteAsync(MtQuoteDto mtQuote)
        {
            if (mtQuote == null)
            {
                throw new ArgumentNullException(nameof(mtQuote));
            }

            var assetPair = await _assetPairsManager.TryGetEnabledPairAsync(mtQuote.AssetPair?.Trim());

            if (assetPair == null)
            {
                return;
            }

            var changedUpdates = new ConcurrentBag <CandleUpdateResult>();
            var midPriceQuote  = _midPriceQuoteGenerator.TryGenerate(
                mtQuote.AssetPair,
                mtQuote.Ask,
                mtQuote.Bid,
                mtQuote.Timestamp,
                assetPair.Accuracy);

            try
            {
                // Updates all intervals in parallel

                var processingTasks = _intervals
                                      .Select(timeInterval => Task.Factory.StartNew(() =>
                {
                    ProcessQuoteInterval(
                        mtQuote.AssetPair,
                        mtQuote.Timestamp,
                        mtQuote.Ask,
                        false,
                        timeInterval,
                        midPriceQuote,
                        changedUpdates);
                }))
                                      .Concat(_intervals
                                              .Select(timeInterval => Task.Factory.StartNew(() =>
                {
                    ProcessQuoteInterval(
                        mtQuote.AssetPair,
                        mtQuote.Timestamp,
                        mtQuote.Bid,
                        true,
                        timeInterval,
                        null,
                        changedUpdates);
                })));

                await Task.WhenAll(processingTasks);

                // Publishes updated candles

                if (!changedUpdates.IsEmpty)
                {
                    var publisher = await _candlesPublisherProvider.GetForAssetPair(assetPair.Id);

                    await publisher.PublishAsync(changedUpdates);
                }
            }
            catch (Exception)
            {
                // Failed to publish one or several candles, so processing should be cancelled

                foreach (var updateResult in changedUpdates)
                {
                    _candlesGenerator.Undo(updateResult);
                }

                throw;
            }
        }