diff --git a/src/MarginTrading.Backend.Core/Extensions/InstrumentBidAskPairExtensions.cs b/src/MarginTrading.Backend.Core/Extensions/InstrumentBidAskPairExtensions.cs new file mode 100644 index 000000000..606bc20b5 --- /dev/null +++ b/src/MarginTrading.Backend.Core/Extensions/InstrumentBidAskPairExtensions.cs @@ -0,0 +1,23 @@ +namespace MarginTrading.Backend.Core.Extensions +{ + public static class InstrumentBidAskPairExtensions + { + /// + /// Calculates a hash for the bid/ask pair, which is used only to determine + /// if the quote has already been warned about being stale. Probably, the + /// implementation entirely correct only for the stale usage scenario. + /// + /// + /// + public static int GetStaleHash(this InstrumentBidAskPair bidAskPair) + { + unchecked + { + int hash = 23; + hash = hash * 31 + (bidAskPair?.Instrument.GetHashCode() ?? 0); + hash = hash * 31 + (bidAskPair?.Date.GetHashCode() ?? 0); + return hash; + } + } + } +} \ No newline at end of file diff --git a/src/MarginTrading.Backend.Core/Settings/MarginTradingSettings.cs b/src/MarginTrading.Backend.Core/Settings/MarginTradingSettings.cs index 88063dfa1..1c15bda5a 100644 --- a/src/MarginTrading.Backend.Core/Settings/MarginTradingSettings.cs +++ b/src/MarginTrading.Backend.Core/Settings/MarginTradingSettings.cs @@ -122,5 +122,8 @@ public class MarginTradingSettings [Optional] public bool LogBlockedMarginCalculation { get; set; } + + [Optional] + public MonitoringSettings Monitoring { get; set; } } } \ No newline at end of file diff --git a/src/MarginTrading.Backend.Core/Settings/MonitoringSettings.cs b/src/MarginTrading.Backend.Core/Settings/MonitoringSettings.cs new file mode 100644 index 000000000..0299537f9 --- /dev/null +++ b/src/MarginTrading.Backend.Core/Settings/MonitoringSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2019 Lykke Corp. +// See the LICENSE file in the project root for more information. + +using Lykke.SettingsReader.Attributes; + +namespace MarginTrading.Backend.Core.Settings +{ + public class MonitoringSettings + { + [Optional] + public QuotesMonitoringSettings Quotes { get; set; } + } +} \ No newline at end of file diff --git a/src/MarginTrading.Backend.Core/Settings/QuotesMonitoringSettings.cs b/src/MarginTrading.Backend.Core/Settings/QuotesMonitoringSettings.cs new file mode 100644 index 000000000..e7ba1f40e --- /dev/null +++ b/src/MarginTrading.Backend.Core/Settings/QuotesMonitoringSettings.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2019 Lykke Corp. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MarginTrading.Backend.Core.Settings +{ + public class QuotesMonitoringSettings + { + public bool IsEnabled { get; set; } + public TimeSpan ConsiderQuoteStalePeriod { get; set; } + } +} \ No newline at end of file diff --git a/src/MarginTrading.Backend.Services/MarginTrading.Backend.Services.csproj b/src/MarginTrading.Backend.Services/MarginTrading.Backend.Services.csproj index b42dfe156..347133eba 100644 --- a/src/MarginTrading.Backend.Services/MarginTrading.Backend.Services.csproj +++ b/src/MarginTrading.Backend.Services/MarginTrading.Backend.Services.csproj @@ -50,8 +50,7 @@ - - <_Parameter1>MarginTradingTests - + + \ No newline at end of file diff --git a/src/MarginTrading.Backend.Services/Modules/ServicesModule.cs b/src/MarginTrading.Backend.Services/Modules/ServicesModule.cs index 2c0b3d7a1..79495224e 100644 --- a/src/MarginTrading.Backend.Services/Modules/ServicesModule.cs +++ b/src/MarginTrading.Backend.Services/Modules/ServicesModule.cs @@ -33,6 +33,13 @@ namespace MarginTrading.Backend.Services.Modules { public class ServicesModule : Module { + private readonly MarginTradingSettings _settings; + + public ServicesModule(MarginTradingSettings settings) + { + _settings = settings; + } + protected override void Load(ContainerBuilder builder) { builder.RegisterType() @@ -40,7 +47,12 @@ protected override void Load(ContainerBuilder builder) .As() .As>() .SingleInstance(); - + + if (_settings.Monitoring?.Quotes?.IsEnabled ?? false) + { + builder.RegisterDecorator(); + } + builder.RegisterType() .AsSelf() .As() diff --git a/src/MarginTrading.Backend.Services/Quotes/QuoteCacheInspector.cs b/src/MarginTrading.Backend.Services/Quotes/QuoteCacheInspector.cs new file mode 100644 index 000000000..1ac610b24 --- /dev/null +++ b/src/MarginTrading.Backend.Services/Quotes/QuoteCacheInspector.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2019 Lykke Corp. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using MarginTrading.Backend.Core; +using MarginTrading.Backend.Core.Extensions; +using MarginTrading.Backend.Core.Quotes; +using MarginTrading.Backend.Core.Settings; +using MarginTrading.Common.Services; +using Microsoft.Extensions.Logging; + +namespace MarginTrading.Backend.Services.Quotes +{ + /// + /// Inspects the quotes returned from the cache. + /// Logs a warning if the quote is not up to date (Is older than 5 seconds). + /// + internal sealed class QuoteCacheInspector : IQuoteCacheService + { + private readonly IQuoteCacheService _decoratee; + private readonly IDateService _dateService; + private readonly ILogger _logger; + private readonly TimeSpan _quoteStalePeriod; + private readonly ConcurrentDictionary _warnedQuotes = new ConcurrentDictionary(); + + public QuoteCacheInspector(IQuoteCacheService decoratee, + IDateService dateService, + ILogger logger, + MarginTradingSettings settings) + { + _decoratee = decoratee; + _dateService = dateService; + _logger = logger; + _quoteStalePeriod = settings.Monitoring?.Quotes?.ConsiderQuoteStalePeriod ?? TimeSpan.FromSeconds(5); + + _logger.LogInformation("Quote Cache Inspector is activated with stale period {stalePeriod}", + _quoteStalePeriod); + } + + public InstrumentBidAskPair GetQuote(string instrument) + { + var quote = _decoratee.GetQuote(instrument); + + try + { + if (CanWarn(quote)) + WarnOnStaleQuote(quote); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to inspect quote for {instrument}", instrument); + } + + return quote; + } + + public Dictionary GetAllQuotes() + { + return _decoratee.GetAllQuotes(); + } + + public bool TryGetQuoteById(string instrument, out InstrumentBidAskPair result) + { + var success = _decoratee.TryGetQuoteById(instrument, out var quote); + + try + { + if (CanWarn(quote)) + WarnOnStaleQuote(quote); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to inspect quote for {instrument}", instrument); + } + + result = quote; + return success; + } + + public RemoveQuoteError RemoveQuote(string assetPairId) + { + return _decoratee.RemoveQuote(assetPairId); + } + + public void WarnOnStaleQuote(InstrumentBidAskPair quote) + { + if (quote == null) + return; + + _logger.LogWarning("Quote for {instrument} is stale. Quote date: {quoteDate}, now: {now}", + quote.Instrument, quote.Date, _dateService.Now()); + + _warnedQuotes.TryAdd(quote.GetStaleHash(), 0); + } + + public bool CanWarn(InstrumentBidAskPair quote) + { + if (quote == null) + return false; + + var current = _dateService.Now(); + if (IsQuoteStale(quote.Date, current, _quoteStalePeriod)) + { + var hash = quote.GetStaleHash(); + return !_warnedQuotes.ContainsKey(hash); + } + + return false; + } + + public static bool IsQuoteStale(DateTime quoteDateTime, DateTime now, TimeSpan stalePeriod) + { + return now.Subtract(quoteDateTime) > stalePeriod; + } + } +} \ No newline at end of file diff --git a/src/MarginTrading.Backend.Services/Services/OrderValidator.cs b/src/MarginTrading.Backend.Services/Services/OrderValidator.cs index a0e1db735..5620cec96 100644 --- a/src/MarginTrading.Backend.Services/Services/OrderValidator.cs +++ b/src/MarginTrading.Backend.Services/Services/OrderValidator.cs @@ -436,13 +436,6 @@ private void ValidateBaseOrderPrice(Order order, decimal? orderPrice) throw new OrderRejectionException(OrderRejectReason.NoLiquidity, "Quote not found"); } - //TODO: implement in MTC-155 -// if (_assetDayOffService.ArePendingOrdersDisabled(order.AssetPairId)) -// { -// throw new ValidateOrderException(OrderRejectReason.NoLiquidity, -// "Trades for instrument are not available"); -// } - if (order.OrderType == OrderType.Limit) { if (order.Direction == OrderDirection.Buy && quote.Ask <= orderPrice) diff --git a/src/MarginTrading.Backend.Services/Workflow/SpecialLiquidation/SpecialLiquidationSaga.cs b/src/MarginTrading.Backend.Services/Workflow/SpecialLiquidation/SpecialLiquidationSaga.cs index 65c7a28b8..0c420bb5a 100644 --- a/src/MarginTrading.Backend.Services/Workflow/SpecialLiquidation/SpecialLiquidationSaga.cs +++ b/src/MarginTrading.Backend.Services/Workflow/SpecialLiquidation/SpecialLiquidationSaga.cs @@ -2,7 +2,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Common.Log; @@ -455,16 +454,6 @@ await InternalRetryPriceRequest(e.CreationTime, sender, executionInfo, } } - private decimal GetActualNetPositionCloseVolume(ICollection positionIds, string accountId) - { - var netPositionVolume = _ordersCache.GetPositions() - .Where(x => positionIds.Contains(x.Id) - && (string.IsNullOrEmpty(accountId) || x.AccountId == accountId)) - .Sum(x => x.Volume); - - return -netPositionVolume; - } - private void RequestPrice(ICommandSender sender, IOperationExecutionInfo executionInfo) { diff --git a/src/MarginTrading.Backend/Startup.cs b/src/MarginTrading.Backend/Startup.cs index f304c7947..68713775b 100644 --- a/src/MarginTrading.Backend/Startup.cs +++ b/src/MarginTrading.Backend/Startup.cs @@ -223,7 +223,7 @@ private static void RegisterModules( builder.RegisterModule(new EventModule()); builder.RegisterModule(new CacheModule()); builder.RegisterModule(new ManagersModule()); - builder.RegisterModule(new ServicesModule()); + builder.RegisterModule(new ServicesModule(settings.CurrentValue)); builder.RegisterModule(new BackendServicesModule( mtSettings.CurrentValue, settings.CurrentValue, diff --git a/tests/MarginTradingTests/BaseTests.cs b/tests/MarginTradingTests/BaseTests.cs index 3c2830fa3..1f117be20 100644 --- a/tests/MarginTradingTests/BaseTests.cs +++ b/tests/MarginTradingTests/BaseTests.cs @@ -32,6 +32,7 @@ using MarginTrading.Backend.Services.AssetPairs; using MarginTrading.Backend.Services.Quotes; using MarginTradingTests.Modules; +using Microsoft.Extensions.Logging; using Moq; namespace MarginTradingTests @@ -83,6 +84,9 @@ private void RegisterDependenciesCore(bool mockEvents = false) OvernightMargin = overnightMarginSettings }; + // register mocks of loggers + builder.RegisterInstance(Mock.Of>()); + builder.RegisterInstance(marginSettings).SingleInstance(); builder.RegisterInstance(PositionHistoryEvents).As>().SingleInstance(); builder.RegisterInstance(overnightMarginSettings).SingleInstance(); @@ -123,7 +127,7 @@ private void RegisterDependenciesCore(bool mockEvents = false) } builder.RegisterModule(new CacheModule()); - builder.RegisterModule(new ServicesModule()); + builder.RegisterModule(new ServicesModule(marginSettings)); builder.RegisterModule(new ManagersModule()); builder.RegisterType>() diff --git a/tests/MarginTradingTests/QuoteInspectorTests.cs b/tests/MarginTradingTests/QuoteInspectorTests.cs new file mode 100644 index 000000000..c2e430a1b --- /dev/null +++ b/tests/MarginTradingTests/QuoteInspectorTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) 2019 Lykke Corp. +// See the LICENSE file in the project root for more information. + +using System; +using FluentAssertions; +using MarginTrading.Backend.Core; +using MarginTrading.Backend.Core.Extensions; +using MarginTrading.Backend.Core.Settings; +using MarginTrading.Backend.Services.Quotes; +using MarginTrading.Common.Services; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace MarginTradingTests +{ + [TestFixture] + public class QuoteInspectorTests + { + private IDateService _dateService; + private static readonly TimeSpan StalePeriod = TimeSpan.FromSeconds(5); + + + [SetUp] + public void SetUp() + { + _dateService = new DateService(); + } + + [Test] + [TestCaseSource(nameof(IsQuoteStaleTestCases))] + public void IsQuoteStale_WhenQuoteIsStale_ShouldReturnTrue(string quoteTimestamp, + string now, + string stalePeriod) + { + var quoteTimestampParsed = DateTime.Parse(quoteTimestamp); + var nowParsed = DateTime.Parse(now); + var stalePeriodParsed = TimeSpan.Parse(stalePeriod); + + QuoteCacheInspector.IsQuoteStale(quoteTimestampParsed, nowParsed, stalePeriodParsed).Should().BeTrue(); + } + + [Test] + public void CanWarn_WhenQuoteIsNull_ShouldReturnFalse() + { + var sut = CreateSut(); + + sut.CanWarn(null).Should().BeFalse(); + } + + [TestCase(1)] + [TestCase(100)] + [TestCase(100000)] + public void CanWarn_WhenQuoteIsStale_ShouldReturnTrue(int addSeconds) + { + var sut = CreateSut(); + + var staleQuoteDateTime = _dateService.Now() - StalePeriod.Add(TimeSpan.FromSeconds(addSeconds)); + + var canWarn = sut.CanWarn(new InstrumentBidAskPair + { Instrument = "whatever", Date = staleQuoteDateTime }); + + canWarn.Should().BeTrue(); + } + + [Test] + public void CanWarn_WhenSameQuote_ShouldReturnFalse() + { + var sut = CreateSut(); + + var staleQuote = new InstrumentBidAskPair { Instrument = "whatever", Date = DateTime.MinValue }; + + var canWarnFirstTime = sut.CanWarn(staleQuote); + + canWarnFirstTime.Should().BeTrue(); + sut.WarnOnStaleQuote(staleQuote); + + var canWarnSecondTime = sut.CanWarn(staleQuote); + + canWarnSecondTime.Should().BeFalse(); + } + + [Test] + public void GetQuoteHash_Same_For_Instrument_And_Date() + { + var quoteDate = DateTime.MinValue; + + var quote1 = new InstrumentBidAskPair { Instrument = "whatever", Date = quoteDate }; + var quote2 = new InstrumentBidAskPair { Instrument = "whatever", Date = quoteDate, Ask = 1 }; + + Assert.AreEqual(quote1.GetStaleHash(), quote2.GetStaleHash()); + } + + [Test] + public void GetQuoteHash_Different_When_Date_Different() + { + var quoteDate = DateTime.MinValue; + + var quote1 = new InstrumentBidAskPair { Instrument = "whatever", Date = quoteDate }; + var quote2 = new InstrumentBidAskPair { Instrument = "whatever", Date = quoteDate.AddTicks(1) }; + + Assert.AreNotEqual(quote1.GetStaleHash(), quote2.GetStaleHash()); + } + + public static object[] IsQuoteStaleTestCases = + { + new object[] { "2000-01-01", "2001-01-01", "00:00:05" }, + new object[] { "2000-01-01", "2000-01-01 00:00:06", "00:00:05" }, + new object[] { "2000-01-01 12:12:12", "2000-01-01 12:13:00", "00:00:05" }, + new object[] { "2000-01-01 12:00:00", "2000-01-11 12:00:00", "5.0:00:00" }, + new object[] { "2000-01-01", "2000-01-01 00:00:05.001", "00:00:05" }, + }; + + private QuoteCacheInspector CreateSut() + { + return new QuoteCacheInspector(Mock.Of(), + _dateService, + Mock.Of>(), + new MarginTradingSettings + { + Monitoring = new MonitoringSettings + { + Quotes = new QuotesMonitoringSettings { ConsiderQuoteStalePeriod = StalePeriod } + } + } + ); + } + } +} \ No newline at end of file