diff --git a/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs b/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs new file mode 100644 index 0000000..3bd24bd --- /dev/null +++ b/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs @@ -0,0 +1,63 @@ +using KLHZ.Trader.Core.Exchange.Utils; + +namespace KLHZ.Trader.Core.Tests +{ + public class ExchangeSchedulerTests + { + [Test] + public void Test1() + { + var dt = new DateTime(2025, 9, 6, 0, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.Close); + } + + [Test] + public void Test2() + { + var dt = new DateTime(2025, 9, 5, 7, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.Open); + } + + [Test] + public void Test3() + { + var dt = new DateTime(2025, 9, 5, 6, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.Close); + } + + [Test] + public void Test4() + { + var dt = new DateTime(2025, 9, 5, 11, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.ClearingTime); + } + + [Test] + public void Test5() + { + var dt = new DateTime(2025, 9, 7, 11, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.Open); + } + + [Test] + public void Test6() + { + var dt = new DateTime(2025, 9, 5, 16, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.ClearingTime); + } + + [Test] + public void Test7() + { + var dt = new DateTime(2025, 9, 7, 15, 0, 0, DateTimeKind.Utc); + var res = ExchangeScheduler.GetCurrentState(dt); + Assert.IsTrue(res == Exchange.Models.ExchangeState.Open); + } + } +} \ No newline at end of file diff --git a/KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs b/KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs index e42f69a..a0ada88 100644 --- a/KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs +++ b/KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs @@ -2,6 +2,7 @@ { public class Asset { + public long? TradeId { get; init; } public decimal BlockedItems { get; init; } public AssetType Type { get; init; } public PositionType Position { get; init; } diff --git a/KLHZ.Trader.Core/Exchange/Models/ExchangeState.cs b/KLHZ.Trader.Core/Exchange/Models/ExchangeState.cs new file mode 100644 index 0000000..6f24191 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/ExchangeState.cs @@ -0,0 +1,10 @@ +namespace KLHZ.Trader.Core.Exchange.Models +{ + internal enum ExchangeState + { + None, + Open, + Close, + ClearingTime + } +} diff --git a/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs b/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs index 38b1f9f..dc6c119 100644 --- a/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs +++ b/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs @@ -128,6 +128,7 @@ namespace KLHZ.Trader.Core.Exchange.Services #pragma warning disable CS0612 // Тип или член устарел var asset = new Models.Assets.Asset() { + TradeId = trade?.Id, AccountId = AccountId, Figi = position.Figi, Ticker = position.Ticker, diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index d33e40f..1f4739c 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -11,6 +11,7 @@ using KLHZ.Trader.Core.DataLayer.Entities.Prices; using KLHZ.Trader.Core.Exchange.Extentions; using KLHZ.Trader.Core.Exchange.Models; using KLHZ.Trader.Core.Exchange.Models.Assets; +using KLHZ.Trader.Core.Exchange.Utils; using KLHZ.Trader.Core.Math.Declisions.Services.Cache; using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.EntityFrameworkCore; @@ -110,11 +111,14 @@ namespace KLHZ.Trader.Core.Exchange.Services var processedPrices = new List(); while (await _pricesChannel.Reader.WaitToReadAsync()) { + var bigWindowProcessor = nameof(Trader) + "_big"; var smallWindowProcessor = nameof(Trader) + "_small"; var message = await _pricesChannel.Reader.ReadAsync(); + if (_tradingInstrumentsFigis.Contains(message.Figi)) { + var currentTime = message.IsHistoricalData ? message.Time : DateTime.UtcNow; if (_historyCash.TryGetValue(message.Figi, out var unit)) { await unit.AddData(message); @@ -128,12 +132,11 @@ namespace KLHZ.Trader.Core.Exchange.Services { if (message.Figi == "FUTIMOEXF000") { - DeferredTrade? longOpen; DeferredLongOpens.TryGetValue(message.Figi, out longOpen); if (longOpen != null) { - var t = message.IsHistoricalData ? message.Time : DateTime.UtcNow; + var t = currentTime; if (longOpen.Time <= t && t - longOpen.Time < TimeSpan.FromMinutes(3)) { @@ -149,7 +152,7 @@ namespace KLHZ.Trader.Core.Exchange.Services DeferredLongCloses.TryGetValue(message.Figi, out longClose); if (longClose != null) { - if (longClose.Time <= (message.IsHistoricalData ? message.Time : DateTime.UtcNow)) + if (longClose.Time <= currentTime) { DeferredLongCloses.TryRemove(message.Figi, out _); if (longClose.Price - message.Value < 1) @@ -161,9 +164,18 @@ namespace KLHZ.Trader.Core.Exchange.Services var windowMaxSize = 100; var data = await unit.GetData(windowMaxSize); + var state = ExchangeScheduler.GetCurrentState(message.Time); + + if (state == ExchangeState.ClearingTime + && data.timestamps.Length > 1 + && (data.timestamps[data.timestamps.Length - 1] - data.timestamps[data.timestamps.Length - 2]) > TimeSpan.FromMinutes(3)) + { + await UpdateFuturesPrice(message, data.prices[data.prices.Length - 2]); + } + if (OpeningStops.TryGetValue(message.Figi, out var dt)) { - if (dt < (message.IsHistoricalData ? message.Time : DateTime.UtcNow)) + if (dt < currentTime) { OpeningStops.TryRemove(message.Figi, out _); } @@ -171,7 +183,7 @@ namespace KLHZ.Trader.Core.Exchange.Services if ((unit.BidsCount / unit.AsksCount) < 0.5m || (unit.BidsCount / unit.AsksCount) > 2m) { - var stopTo = (message.IsHistoricalData ? message.Time : DateTime.UtcNow).AddMinutes(3); + var stopTo = currentTime.AddMinutes(3); //OpeningStops.AddOrUpdate(message.Figi, stopTo, (k, v) => stopTo); //LogDeclision(declisionsForSave, DeclisionTradeAction.StopBuyShortTime, message); } @@ -203,6 +215,7 @@ namespace KLHZ.Trader.Core.Exchange.Services if ((res & TradingEvent.UptrendStart) == TradingEvent.UptrendStart && !OpeningStops.TryGetValue(message.Figi, out _) + && state == ExchangeState.Open && data.timestamps.Length > 1 && (data.timestamps[data.timestamps.Length - 1] - data.timestamps[data.timestamps.Length - 2] < TimeSpan.FromMinutes(1))) { @@ -265,6 +278,18 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + private async Task UpdateFuturesPrice(INewPrice newPrice, decimal newPriceValue) + { + using var context = await _dbContextFactory.CreateDbContextAsync(); + await context.Trades + .Where(t => t.Figi == newPrice.Figi && t.ArchiveStatus == 0) + .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.Price, newPriceValue)); + foreach (var account in Accounts.Values) + { + await account.SyncPortfolio(); + } + } + private static void LogPrice(List prices, INewPrice message, string processor, decimal value) { prices.Add(new ProcessedPrice() diff --git a/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs b/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs new file mode 100644 index 0000000..dff3e64 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs @@ -0,0 +1,48 @@ +using KLHZ.Trader.Core.Exchange.Models; + +namespace KLHZ.Trader.Core.Exchange.Utils +{ + internal static class ExchangeScheduler + { + private readonly static TimeOnly _openTimeMain = new(6, 10); + private readonly static TimeOnly _closeTimeMain = new(20, 45); + + private readonly static TimeOnly _openTimeHoliday = new(7, 10); + private readonly static TimeOnly _closeTimeHoliday = new(17, 45); + + private readonly static TimeOnly _firstClearingStart = new(10, 55); + private readonly static TimeOnly _firstClearingEnd = new(11, 10); + + private readonly static TimeOnly _mainClearingStart = new(15, 50); + private readonly static TimeOnly _mainClearingEnd = new(16, 5); + + internal static ExchangeState GetCurrentState(DateTime? currentDt = null) + { + var dt = currentDt ?? DateTime.UtcNow; + var day = dt.DayOfWeek; + + var time = TimeOnly.FromDateTime(dt); + if (day == DayOfWeek.Sunday || day == DayOfWeek.Saturday) + { + if (time > _openTimeHoliday && time < _closeTimeHoliday) + { + return ExchangeState.Open; + } + } + else + { + if (time > _openTimeMain && time < _closeTimeMain) + { + + if (time > _firstClearingStart && time < _firstClearingEnd || time > _mainClearingStart && time < _mainClearingEnd) + { + return ExchangeState.ClearingTime; + } + return ExchangeState.Open; + } + } + + return ExchangeState.Close; + } + } +}