From 9114dda27f32bded2cea05a72037592cf2b45a42 Mon Sep 17 00:00:00 2001 From: vlad zverzhkhovskiy Date: Fri, 5 Sep 2025 12:41:05 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=20=D0=BF=D0=BE=D0=BA=D1=83=D0=BF=D0=BA=D0=B8?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0=D0=B6=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Messaging/Dtos/Enums/TradeCommandType.cs | 8 +- .../Dtos/Interfaces/ITradeCommand.cs | 13 + .../Messaging/Dtos/TradeCommand.cs | 12 +- .../Messaging/Interfaces/IDataBus.cs | 7 +- .../ExchangeSchedulerTests.cs | 15 +- .../Common/Messaging/Services/DataBus.cs | 9 +- .../Entities/Trades/Enums/AssetType.cs | 3 +- .../Exchange/Extentions/StringExtensions.cs | 2 +- .../Exchange/Models/Assets/AssetType.cs | 10 - .../Exchange/Models/Assets/PositionType.cs | 9 - .../{Assets => AssetsAccounting}/Asset.cs | 2 +- .../Models/AssetsAccounting/AssetType.cs | 10 + .../Models/AssetsAccounting/DealDirection.cs | 9 + .../Models/AssetsAccounting/DealResult.cs | 12 + .../Models/AssetsAccounting/ManagedAccount.cs | 258 ++++++++++++++++ .../Models/AssetsAccounting/PositionType.cs | 9 + .../{ => Models/Configs}/ExchangeConfig.cs | 3 +- .../Models/Configs/InstrumentSettings.cs | 10 + .../Exchange/Models/DealResult.cs | 9 - .../Models/{ => Trading}/DeferredTrade.cs | 2 +- .../Models/{ => Trading}/ExchangeState.cs | 2 +- .../Exchange/Services/ExchangeDataReader.cs | 45 +-- .../Exchange/Services/ManagedAccount.cs | 292 ------------------ .../Exchange/Services/TradeDataProvider.cs | 241 +++++++++++++++ KLHZ.Trader.Core/Exchange/Services/Trader.cs | 106 +++---- .../Services/TradingCommandsExecutor.cs | 97 ++++++ .../Exchange/Utils/ExchangeScheduler.cs | 2 +- .../Exchange/Utils/TradingCalculator.cs | 11 + .../TG/Services/BotMessagesHandler.cs | 22 +- KLHZ.Trader.Core/TG/TgBotConfig.cs | 2 +- KLHZ.Trader.HistoryLoader/Program.cs | 2 +- KLHZ.Trader.Service/Program.cs | 6 +- KLHZ.Trader.Service/appsettings.json | 10 +- KLHZ.Trader.sln | 2 +- 34 files changed, 780 insertions(+), 472 deletions(-) create mode 100644 KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs delete mode 100644 KLHZ.Trader.Core/Exchange/Models/Assets/AssetType.cs delete mode 100644 KLHZ.Trader.Core/Exchange/Models/Assets/PositionType.cs rename KLHZ.Trader.Core/Exchange/Models/{Assets => AssetsAccounting}/Asset.cs (87%) create mode 100644 KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/AssetType.cs create mode 100644 KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealDirection.cs create mode 100644 KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealResult.cs create mode 100644 KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs create mode 100644 KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/PositionType.cs rename KLHZ.Trader.Core/Exchange/{ => Models/Configs}/ExchangeConfig.cs (81%) create mode 100644 KLHZ.Trader.Core/Exchange/Models/Configs/InstrumentSettings.cs delete mode 100644 KLHZ.Trader.Core/Exchange/Models/DealResult.cs rename KLHZ.Trader.Core/Exchange/Models/{ => Trading}/DeferredTrade.cs (74%) rename KLHZ.Trader.Core/Exchange/Models/{ => Trading}/ExchangeState.cs (63%) delete mode 100644 KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs create mode 100644 KLHZ.Trader.Core/Exchange/Services/TradeDataProvider.cs create mode 100644 KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs create mode 100644 KLHZ.Trader.Core/Exchange/Utils/TradingCalculator.cs diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs index 4d4431a..38991d5 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Enums/TradeCommandType.cs @@ -3,14 +3,8 @@ public enum TradeCommandType { Unknown = 0, + MarketBuy = 1, - - MarketSell = 101, - SoftClosePosition = 110, - - ForceClosePosition = 201, - - UpdatePortfolio = 10000, } } diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs new file mode 100644 index 0000000..f34b63e --- /dev/null +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/Interfaces/ITradeCommand.cs @@ -0,0 +1,13 @@ +using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums; + +namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces +{ + public interface ITradeCommand + { + public TradeCommandType CommandType { get; } + public string Figi { get; } + public decimal? RecomendPrice { get; } + public long Count { get; } + public string AccountId { get; } + } +} diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs index 4720f10..3530952 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Dtos/TradeCommand.cs @@ -1,16 +1,14 @@ using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums; +using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos { - public class TradeCommand + public class TradeCommand : ITradeCommand { public TradeCommandType CommandType { get; init; } - public string? Figi { get; init; } - public string? Ticker { get; init; } + public required string Figi { get; init; } public decimal? RecomendPrice { get; init; } - public decimal? Count { get; init; } - public decimal? LotsCount { get; init; } - public string? AccountId { get; init; } - public bool IsNeedBigCashOnAccount { get; init; } + public long Count { get; init; } + public required string AccountId { get; init; } } } diff --git a/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs b/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs index baf6f86..2b7d17c 100644 --- a/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs +++ b/KLHZ.Trader.Core.Contracts/Messaging/Interfaces/IDataBus.cs @@ -1,5 +1,4 @@ -using KLHZ.Trader.Core.Contracts.Messaging.Dtos; -using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; +using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; using System.Threading.Channels; namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces @@ -9,11 +8,11 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces public bool AddChannel(string key, Channel channel); public bool AddChannel(string key, Channel channel); public bool AddChannel(string key, Channel channel); - public bool AddChannel(string key, Channel channel); + public bool AddChannel(string key, Channel channel); public bool AddChannel(string key, Channel channel); public bool AddChannel(string key, Channel channel); public Task Broadcast(INewPrice newPriceMessage); - public Task Broadcast(TradeCommand command); + public Task Broadcast(ITradeCommand command); public Task Broadcast(INewCandle command); public Task Broadcast(IProcessedPrice command); public Task Broadcast(IOrderbook orderbook); diff --git a/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs b/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs index 3bd24bd..6b15063 100644 --- a/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs +++ b/KLHZ.Trader.Core.Tests/ExchangeSchedulerTests.cs @@ -1,3 +1,4 @@ +using KLHZ.Trader.Core.Exchange.Models.Trading; using KLHZ.Trader.Core.Exchange.Utils; namespace KLHZ.Trader.Core.Tests @@ -9,7 +10,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 6, 0, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.Close); + Assert.IsTrue(res == ExchangeState.Close); } [Test] @@ -17,7 +18,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 5, 7, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.Open); + Assert.IsTrue(res == ExchangeState.Open); } [Test] @@ -25,7 +26,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 5, 6, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.Close); + Assert.IsTrue(res == ExchangeState.Close); } [Test] @@ -33,7 +34,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 5, 11, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.ClearingTime); + Assert.IsTrue(res == ExchangeState.ClearingTime); } [Test] @@ -41,7 +42,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 7, 11, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.Open); + Assert.IsTrue(res == ExchangeState.Open); } [Test] @@ -49,7 +50,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 5, 16, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.ClearingTime); + Assert.IsTrue(res == ExchangeState.ClearingTime); } [Test] @@ -57,7 +58,7 @@ namespace KLHZ.Trader.Core.Tests { var dt = new DateTime(2025, 9, 7, 15, 0, 0, DateTimeKind.Utc); var res = ExchangeScheduler.GetCurrentState(dt); - Assert.IsTrue(res == Exchange.Models.ExchangeState.Open); + Assert.IsTrue(res == ExchangeState.Open); } } } \ No newline at end of file diff --git a/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs b/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs index 49a009b..cdfc995 100644 --- a/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs +++ b/KLHZ.Trader.Core/Common/Messaging/Services/DataBus.cs @@ -1,5 +1,4 @@ -using KLHZ.Trader.Core.Contracts.Messaging.Dtos; -using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; +using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using System.Collections.Concurrent; using System.Threading.Channels; @@ -13,7 +12,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services private readonly ConcurrentDictionary> _candlesChannels = new(); private readonly ConcurrentDictionary> _priceChannels = new(); private readonly ConcurrentDictionary> _processedPricesChannels = new(); - private readonly ConcurrentDictionary> _commandChannels = new(); + private readonly ConcurrentDictionary> _commandChannels = new(); public bool AddChannel(string key, Channel channel) { @@ -35,7 +34,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services return _candlesChannels.TryAdd(key, channel); } - public bool AddChannel(string key, Channel channel) + public bool AddChannel(string key, Channel channel) { return _commandChannels.TryAdd(key, channel); } @@ -69,7 +68,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services } } - public async Task Broadcast(TradeCommand command) + public async Task Broadcast(ITradeCommand command) { foreach (var channel in _commandChannels.Values) { diff --git a/KLHZ.Trader.Core/DataLayer/Entities/Trades/Enums/AssetType.cs b/KLHZ.Trader.Core/DataLayer/Entities/Trades/Enums/AssetType.cs index 7f148c6..5da2b5a 100644 --- a/KLHZ.Trader.Core/DataLayer/Entities/Trades/Enums/AssetType.cs +++ b/KLHZ.Trader.Core/DataLayer/Entities/Trades/Enums/AssetType.cs @@ -4,6 +4,7 @@ { Unknown = 0, Common = 1, - Future = 2 + Future = 2, + Currency = 3, } } diff --git a/KLHZ.Trader.Core/Exchange/Extentions/StringExtensions.cs b/KLHZ.Trader.Core/Exchange/Extentions/StringExtensions.cs index 625ce00..a038c38 100644 --- a/KLHZ.Trader.Core/Exchange/Extentions/StringExtensions.cs +++ b/KLHZ.Trader.Core/Exchange/Extentions/StringExtensions.cs @@ -1,4 +1,4 @@ -using KLHZ.Trader.Core.Exchange.Models.Assets; +using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; namespace KLHZ.Trader.Core.Exchange.Extentions { diff --git a/KLHZ.Trader.Core/Exchange/Models/Assets/AssetType.cs b/KLHZ.Trader.Core/Exchange/Models/Assets/AssetType.cs deleted file mode 100644 index 9000294..0000000 --- a/KLHZ.Trader.Core/Exchange/Models/Assets/AssetType.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace KLHZ.Trader.Core.Exchange.Models.Assets -{ - public enum AssetType - { - Unknown = 0, - Currency = 1, - Common = 2, - Futures = 3, - } -} diff --git a/KLHZ.Trader.Core/Exchange/Models/Assets/PositionType.cs b/KLHZ.Trader.Core/Exchange/Models/Assets/PositionType.cs deleted file mode 100644 index 549c1e0..0000000 --- a/KLHZ.Trader.Core/Exchange/Models/Assets/PositionType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace KLHZ.Trader.Core.Exchange.Models.Assets -{ - public enum PositionType - { - Unknown = 0, - Long = 1, - Short = 2 - } -} diff --git a/KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/Asset.cs similarity index 87% rename from KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs rename to KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/Asset.cs index a0ada88..af38b82 100644 --- a/KLHZ.Trader.Core/Exchange/Models/Assets/Asset.cs +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/Asset.cs @@ -1,4 +1,4 @@ -namespace KLHZ.Trader.Core.Exchange.Models.Assets +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting { public class Asset { diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/AssetType.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/AssetType.cs new file mode 100644 index 0000000..95817f5 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/AssetType.cs @@ -0,0 +1,10 @@ +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting +{ + public enum AssetType + { + Unknown = 0, + Common = 1, + Futures = 2, + Currency = 3, + } +} diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealDirection.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealDirection.cs new file mode 100644 index 0000000..df7f64e --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealDirection.cs @@ -0,0 +1,9 @@ +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting +{ + public enum DealDirection + { + Unknown = 0, + Buy = 1, + Sell = 2 + } +} diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealResult.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealResult.cs new file mode 100644 index 0000000..407c400 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/DealResult.cs @@ -0,0 +1,12 @@ +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting +{ + public class DealResult + { + public required string AccountId { get; set; } + public required string Figi { get; set; } + public decimal Price { get; set; } + public decimal Count { get; set; } + public bool Success { get; set; } + public DealDirection Direction { get; set; } + } +} diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs new file mode 100644 index 0000000..4b8f822 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/ManagedAccount.cs @@ -0,0 +1,258 @@ +using System.Collections.Concurrent; + +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting +{ + public class ManagedAccount + { + public readonly string AccountId; + private readonly object _locker = new(); + private decimal _balance = 0; + private decimal _total = 0; + + internal decimal Balance + { + get + { + lock (_locker) + return _balance; + } + set + { + lock (_locker) + _balance = value; + } + } + internal decimal Total + { + get + { + lock (_locker) + return _total; + } + set + { + lock (_locker) + _total = value; + } + } + + internal readonly ConcurrentDictionary Assets = new(); + + public ManagedAccount(string accountId) + { + AccountId = accountId; + } + + // private async Task ProcessCommands() + // { + // while (await _channel.Reader.WaitToReadAsync()) + // { + // var command = await _channel.Reader.ReadAsync(); + // try + // { + // await ProcessMarketCommand(command); + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Ошибка при обработке команды."); + // } + // } + // } + + // internal async Task SyncPortfolio() + // { + // try + // { + // //await _semaphoreSlim.WaitAsync(); + // var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() + // { + // AccountId = AccountId, + // }); + + // var oldAssets = Assets.Keys.ToHashSet(); + // using var context = await _dbContextFactory.CreateDbContextAsync(); + // context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + // var trades = await context.Trades + // .Where(t => t.AccountId == AccountId && t.ArchiveStatus == 0) + // .ToListAsync(); + // foreach (var position in portfolio.Positions) + // { + // decimal price = 0; + // var trade = trades.FirstOrDefault(t => t.Figi == position.Figi); + + // if (trade != null) + // { + // trades.Remove(trade); + // price = trade.Price; + // } + // else + // { + // price = position.AveragePositionPrice; + // } + + //#pragma warning disable CS0612 // Тип или член устарел + // var asset = new Models.Assets.Asset() + // { + // TradeId = trade?.Id, + // AccountId = AccountId, + // Figi = position.Figi, + // Ticker = position.Ticker, + // BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow, + // BoughtPrice = price, + // Type = position.InstrumentType.ParseInstrumentType(), + // Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short, + // BlockedItems = position.BlockedLots, + // Count = position.Quantity, + // CountLots = position.QuantityLots, + // }; + //#pragma warning restore CS0612 // Тип или член устарел + // Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset); + // oldAssets.Remove(asset.Figi); + // } + + // Total = portfolio.TotalAmountPortfolio; + // Balance = portfolio.TotalAmountCurrencies; + + // foreach (var asset in oldAssets) + // { + // Assets.TryRemove(asset, out _); + // } + + // var ids = trades.Select(t => t.Id).ToArray(); + // await context.Trades + // .Where(t => ids.Contains(t.Id)) + // .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", AccountId); + // } + // finally + // { + // //_semaphoreSlim.Release(); + // } + + // } + + // internal async Task ClosePosition(string figi) + // { + // if (!string.IsNullOrEmpty(figi) && Assets.TryGetValue(figi, out var asset)) + // { + // try + // { + // var req = new PostOrderRequest() + // { + // AccountId = AccountId, + // InstrumentId = figi, + // }; + // if (asset != null) + // { + // req.Direction = OrderDirection.Sell; + // req.OrderType = OrderType.Market; + // req.Quantity = (long)asset.Count; + // req.ConfirmMarginTrade = true; + // var res = await _investApiClient.Orders.PostOrderAsync(req); + // return new DealResult + // { + // Count = res.LotsExecuted, + // Price = res.ExecutedOrderPrice, + // Success = true, + // }; + // } + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Ошибка при закрытии позиции по счёту {acc}. figi: {figi}", AccountId, figi); + // } + // } + // return new DealResult + // { + // Count = 0, + // Price = 0, + // Success = false, + // }; + // } + + // internal async Task BuyAsset(string figi, decimal count, string? ticker = null, decimal? recommendedPrice = null) + // { + // try + // { + // var req = new PostOrderRequest() + // { + // AccountId = AccountId, + // InstrumentId = figi, + // Direction = OrderDirection.Buy, + // OrderType = OrderType.Market, + // Quantity = (long)count, + // }; + + // var res = await _investApiClient.Orders.PostOrderAsync(req); + + // using var context = await _dbContextFactory.CreateDbContextAsync(); + // context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + // var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == figi); + // if (trade == null) + // { + // var newTrade = new DataLayer.Entities.Trades.Trade() + // { + // AccountId = AccountId, + // Figi = figi, + // Ticker = ticker ?? string.Empty, + // BoughtAt = DateTime.UtcNow, + // Count = res.LotsExecuted, + // Price = res.ExecutedOrderPrice, + // Position = DataLayer.Entities.Trades.Enums.PositionType.Long, + // Direction = DataLayer.Entities.Trades.Enums.TradeDirection.Buy, + // Asset = DataLayer.Entities.Trades.Enums.AssetType.Common, + // }; + + // await context.Trades.AddAsync(newTrade); + // } + // else + // { + // var oldAmount = trade.Price * trade.Count; + // var newAmount = res.ExecutedOrderPrice * res.LotsExecuted; + // trade.Count = res.LotsExecuted + trade.Count; + // trade.Price = (oldAmount + newAmount) / trade.Count; + // context.Trades.Update(trade); + // } + + // await context.SaveChangesAsync(); + // return new DealResult + // { + // Count = res.LotsExecuted, + // Price = res.ExecutedOrderPrice, + // Success = true, + // }; + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", AccountId, figi); + // } + // return new DealResult + // { + // Count = 0, + // Price = 0, + // Success = false, + // }; + // } + + // private async Task ProcessMarketCommand(TradeCommand command) + // { + // if (string.IsNullOrWhiteSpace(command.Figi)) return; + // if (command.CommandType == TradeCommandType.MarketBuy) + // { + // await BuyAsset(command.Figi, command.Count ?? 1, command.Ticker, command.RecomendPrice); + // } + // else if (command.CommandType == TradeCommandType.ForceClosePosition) + // { + // await ClosePosition(command.Figi); + // } + // else return; + + // await SyncPortfolio(); + // } + } +} diff --git a/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/PositionType.cs b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/PositionType.cs new file mode 100644 index 0000000..2effc5a --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/AssetsAccounting/PositionType.cs @@ -0,0 +1,9 @@ +namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting +{ + public enum PositionType + { + Unknown = 0, + Long = 1,//Пока считаем, что маржинального лонга для обычных акций не существует. + Short = 2 + } +} diff --git a/KLHZ.Trader.Core/Exchange/ExchangeConfig.cs b/KLHZ.Trader.Core/Exchange/Models/Configs/ExchangeConfig.cs similarity index 81% rename from KLHZ.Trader.Core/Exchange/ExchangeConfig.cs rename to KLHZ.Trader.Core/Exchange/Models/Configs/ExchangeConfig.cs index 35020d7..2546973 100644 --- a/KLHZ.Trader.Core/Exchange/ExchangeConfig.cs +++ b/KLHZ.Trader.Core/Exchange/Models/Configs/ExchangeConfig.cs @@ -1,4 +1,4 @@ -namespace KLHZ.Trader.Core.Exchange +namespace KLHZ.Trader.Core.Exchange.Models.Configs { public class ExchangeConfig { @@ -12,5 +12,6 @@ public string[] DataRecievingInstrumentsFigis { get; set; } = []; public string[] TradingInstrumentsFigis { get; set; } = []; public string[] ManagingAccountNamePatterns { get; set; } = []; + public InstrumentSettings[] InstrumentsSettings { get; set; } = []; } } diff --git a/KLHZ.Trader.Core/Exchange/Models/Configs/InstrumentSettings.cs b/KLHZ.Trader.Core/Exchange/Models/Configs/InstrumentSettings.cs new file mode 100644 index 0000000..43b73dd --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Models/Configs/InstrumentSettings.cs @@ -0,0 +1,10 @@ +namespace KLHZ.Trader.Core.Exchange.Models.Configs +{ + public class InstrumentSettings + { + public required string Figi { get; init; } + public decimal ShortLeverage { get; init; } + public decimal LongLeverage { get; init; } + public decimal PriceToRubConvertationCoefficient { get; init; } = 1; + } +} diff --git a/KLHZ.Trader.Core/Exchange/Models/DealResult.cs b/KLHZ.Trader.Core/Exchange/Models/DealResult.cs deleted file mode 100644 index 4b09c1d..0000000 --- a/KLHZ.Trader.Core/Exchange/Models/DealResult.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace KLHZ.Trader.Core.Exchange.Models -{ - public class DealResult - { - public decimal Price { get; set; } - public decimal Count { get; set; } - public bool Success { get; set; } - } -} diff --git a/KLHZ.Trader.Core/Exchange/Models/DeferredTrade.cs b/KLHZ.Trader.Core/Exchange/Models/Trading/DeferredTrade.cs similarity index 74% rename from KLHZ.Trader.Core/Exchange/Models/DeferredTrade.cs rename to KLHZ.Trader.Core/Exchange/Models/Trading/DeferredTrade.cs index 15aabb5..ec9aabf 100644 --- a/KLHZ.Trader.Core/Exchange/Models/DeferredTrade.cs +++ b/KLHZ.Trader.Core/Exchange/Models/Trading/DeferredTrade.cs @@ -1,4 +1,4 @@ -namespace KLHZ.Trader.Core.Exchange.Models +namespace KLHZ.Trader.Core.Exchange.Models.Trading { internal class DeferredTrade { diff --git a/KLHZ.Trader.Core/Exchange/Models/ExchangeState.cs b/KLHZ.Trader.Core/Exchange/Models/Trading/ExchangeState.cs similarity index 63% rename from KLHZ.Trader.Core/Exchange/Models/ExchangeState.cs rename to KLHZ.Trader.Core/Exchange/Models/Trading/ExchangeState.cs index 6f24191..1afd49f 100644 --- a/KLHZ.Trader.Core/Exchange/Models/ExchangeState.cs +++ b/KLHZ.Trader.Core/Exchange/Models/Trading/ExchangeState.cs @@ -1,4 +1,4 @@ -namespace KLHZ.Trader.Core.Exchange.Models +namespace KLHZ.Trader.Core.Exchange.Models.Trading { internal enum ExchangeState { diff --git a/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs b/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs index bf48882..9c0e538 100644 --- a/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/ExchangeDataReader.cs @@ -6,6 +6,7 @@ using KLHZ.Trader.Core.DataLayer.Entities.Orders; using KLHZ.Trader.Core.DataLayer.Entities.Prices; using KLHZ.Trader.Core.DataLayer.Entities.Trades; using KLHZ.Trader.Core.Exchange.Extentions; +using KLHZ.Trader.Core.Exchange.Models.Configs; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -18,6 +19,7 @@ namespace KLHZ.Trader.Core.Exchange.Services { public class ExchangeDataReader : IHostedService { + private readonly TradeDataProvider _tradeDataProvider; private readonly InvestApiClient _investApiClient; private readonly string[] _instrumentsFigis = []; private readonly string[] _managedAccountNamePatterns; @@ -27,7 +29,7 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly CancellationTokenSource _cts = new(); private readonly IDataBus _eventBus; private readonly bool _exchangeDataRecievingEnabled; - public ExchangeDataReader(InvestApiClient investApiClient, IDataBus eventBus, + public ExchangeDataReader(InvestApiClient investApiClient, IDataBus eventBus, TradeDataProvider tradeDataProvider, IOptions options, IDbContextFactory dbContextFactory, ILogger logger) { @@ -38,36 +40,16 @@ namespace KLHZ.Trader.Core.Exchange.Services _instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray(); _logger = logger; _managedAccountNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray(); + _tradeDataProvider = tradeDataProvider; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Инициализация приемника данных с биржи"); var accounts = await _investApiClient.GetAccounts(_managedAccountNamePatterns); - await InitCache(); _ = CycleSubscribtion(accounts); } - private async Task InitCache() - { - var shares = await _investApiClient.Instruments.SharesAsync(); - foreach (var share in shares.Instruments) - { - if (_instrumentsFigis.Contains(share.Figi)) - { - _tickersCache.TryAdd(share.Figi, share.Ticker); - } - } - var futures = await _investApiClient.Instruments.FuturesAsync(); - foreach (var future in futures.Instruments) - { - if (_instrumentsFigis.Contains(future.Figi)) - { - _tickersCache.TryAdd(future.Figi, future.Ticker); - } - } - } - private async Task CycleSubscribtion(string[] accounts) { while (true) @@ -154,7 +136,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var message = new PriceChange() { Figi = response.LastPrice.Figi, - Ticker = GetTickerByFigi(response.LastPrice.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.LastPrice.Figi), Time = response.LastPrice.Time.ToDateTime().ToUniversalTime(), Value = response.LastPrice.Price, IsHistoricalData = false, @@ -169,7 +151,7 @@ namespace KLHZ.Trader.Core.Exchange.Services { Figi = response.Trade.Figi, BoughtAt = response.Trade.Time.ToDateTime().ToUniversalTime(), - Ticker = GetTickerByFigi(response.Trade.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.Trade.Figi), Price = response.Trade.Price, Count = response.Trade.Quantity, Direction = response.Trade.Direction == Tinkoff.InvestApi.V1.TradeDirection.Sell ? DataLayer.Entities.Trades.Enums.TradeDirection.Sell : DataLayer.Entities.Trades.Enums.TradeDirection.Buy, @@ -181,7 +163,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var asksSummary10 = new OrderbookItem() { Figi = response.Orderbook.Figi, - Ticker = GetTickerByFigi(response.Orderbook.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi), Count = response.Orderbook.Asks.Sum(a => (int)a.Quantity), ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary10, Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(), @@ -190,7 +172,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var asksSummary4 = new OrderbookItem() { Figi = response.Orderbook.Figi, - Ticker = GetTickerByFigi(response.Orderbook.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi), Count = response.Orderbook.Asks.Take(4).Sum(a => (int)a.Quantity), ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary4, Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(), @@ -199,7 +181,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var bidsSummary10 = new OrderbookItem() { Figi = response.Orderbook.Figi, - Ticker = GetTickerByFigi(response.Orderbook.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi), Count = response.Orderbook.Bids.Sum(a => (int)a.Quantity), ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary10, Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(), @@ -208,7 +190,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var bidsSummary4 = new OrderbookItem() { Figi = response.Orderbook.Figi, - Ticker = GetTickerByFigi(response.Orderbook.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi), Count = response.Orderbook.Bids.Take(4).Sum(a => (int)a.Quantity), ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary4, Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(), @@ -221,7 +203,7 @@ namespace KLHZ.Trader.Core.Exchange.Services var message = new NewOrderbookMessage() { - Ticker = GetTickerByFigi(response.Orderbook.Figi), + Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi), Figi = response.Orderbook.Figi, Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(), AsksCount = asksSummary10.Count, @@ -262,11 +244,6 @@ namespace KLHZ.Trader.Core.Exchange.Services } } - private string GetTickerByFigi(string figi) - { - return _tickersCache.TryGetValue(figi, out var ticker) ? ticker : string.Empty; - } - public Task StopAsync(CancellationToken cancellationToken) { _cts.Cancel(); diff --git a/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs b/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs deleted file mode 100644 index dc6c119..0000000 --- a/KLHZ.Trader.Core/Exchange/Services/ManagedAccount.cs +++ /dev/null @@ -1,292 +0,0 @@ -using KLHZ.Trader.Core.Contracts.Messaging.Dtos; -using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums; -using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; -using KLHZ.Trader.Core.DataLayer; -using KLHZ.Trader.Core.Exchange.Extentions; -using KLHZ.Trader.Core.Exchange.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; -using System.Threading.Channels; -using Tinkoff.InvestApi; -using Tinkoff.InvestApi.V1; -using PositionType = KLHZ.Trader.Core.Exchange.Models.Assets.PositionType; - -namespace KLHZ.Trader.Core.Exchange.Services -{ - public class ManagedAccount - { - public string AccountId { get; private set; } = string.Empty; - private readonly Channel _channel = Channel.CreateUnbounded(); - - #region Поля, собираемые из контейнера DI - private readonly InvestApiClient _investApiClient; - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - private readonly IDataBus _dataBus; - - #endregion - - #region Кеш рабочих данных - private readonly object _locker = new(); - private decimal _balance = 0; - private decimal _total = 0; - internal decimal Balance - { - get - { - lock (_locker) - return _balance; - } - set - { - lock (_locker) - _balance = value; - } - } - internal decimal Total - { - get - { - lock (_locker) - return _total; - } - set - { - lock (_locker) - _total = value; - } - } - - internal readonly ConcurrentDictionary Assets = new(); - - #endregion - - public ManagedAccount(InvestApiClient investApiClient, IDataBus dataBus, IDbContextFactory dbContextFactory, ILogger logger) - { - _dataBus = dataBus; - _investApiClient = investApiClient; - _dbContextFactory = dbContextFactory; - _logger = logger; - } - - public async Task Init(string accountId) - { - AccountId = accountId; - await SyncPortfolio(); - _dataBus.AddChannel(accountId, _channel); - _ = ProcessCommands(); - } - - private async Task ProcessCommands() - { - while (await _channel.Reader.WaitToReadAsync()) - { - var command = await _channel.Reader.ReadAsync(); - try - { - await ProcessMarketCommand(command); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при обработке команды."); - } - } - } - - internal async Task SyncPortfolio() - { - try - { - //await _semaphoreSlim.WaitAsync(); - var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() - { - AccountId = AccountId, - }); - - var oldAssets = Assets.Keys.ToHashSet(); - using var context = await _dbContextFactory.CreateDbContextAsync(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - - var trades = await context.Trades - .Where(t => t.AccountId == AccountId && t.ArchiveStatus == 0) - .ToListAsync(); - foreach (var position in portfolio.Positions) - { - decimal price = 0; - var trade = trades.FirstOrDefault(t => t.Figi == position.Figi); - - if (trade != null) - { - trades.Remove(trade); - price = trade.Price; - } - else - { - price = position.AveragePositionPrice; - } -#pragma warning disable CS0612 // Тип или член устарел - var asset = new Models.Assets.Asset() - { - TradeId = trade?.Id, - AccountId = AccountId, - Figi = position.Figi, - Ticker = position.Ticker, - BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow, - BoughtPrice = price, - Type = position.InstrumentType.ParseInstrumentType(), - Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short, - BlockedItems = position.BlockedLots, - Count = position.Quantity, - CountLots = position.QuantityLots, - }; -#pragma warning restore CS0612 // Тип или член устарел - Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset); - oldAssets.Remove(asset.Figi); - } - - Total = portfolio.TotalAmountPortfolio; - Balance = portfolio.TotalAmountCurrencies; - - foreach (var asset in oldAssets) - { - Assets.TryRemove(asset, out _); - } - - var ids = trades.Select(t => t.Id).ToArray(); - await context.Trades - .Where(t => ids.Contains(t.Id)) - .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", AccountId); - } - finally - { - //_semaphoreSlim.Release(); - } - - } - - internal async Task ClosePosition(string figi) - { - if (!string.IsNullOrEmpty(figi) && Assets.TryGetValue(figi, out var asset)) - { - try - { - var req = new PostOrderRequest() - { - AccountId = AccountId, - InstrumentId = figi, - }; - if (asset != null) - { - req.Direction = OrderDirection.Sell; - req.OrderType = OrderType.Market; - req.Quantity = (long)asset.Count; - - var res = await _investApiClient.Orders.PostOrderAsync(req); - return new DealResult - { - Count = res.LotsExecuted, - Price = res.ExecutedOrderPrice, - Success = true, - }; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при закрытии позиции по счёту {acc}. figi: {figi}", AccountId, figi); - } - } - return new DealResult - { - Count = 0, - Price = 0, - Success = false, - }; - } - - internal async Task BuyAsset(string figi, decimal count, string? ticker = null, decimal? recommendedPrice = null) - { - try - { - var req = new PostOrderRequest() - { - AccountId = AccountId, - InstrumentId = figi, - Direction = OrderDirection.Buy, - OrderType = OrderType.Market, - Quantity = (long)count, - }; - - var res = await _investApiClient.Orders.PostOrderAsync(req); - - using var context = await _dbContextFactory.CreateDbContextAsync(); - context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - - var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == figi); - if (trade == null) - { - var newTrade = new DataLayer.Entities.Trades.Trade() - { - AccountId = AccountId, - Figi = figi, - Ticker = ticker ?? string.Empty, - BoughtAt = DateTime.UtcNow, - Count = res.LotsExecuted, - Price = res.ExecutedOrderPrice, - Position = DataLayer.Entities.Trades.Enums.PositionType.Long, - Direction = DataLayer.Entities.Trades.Enums.TradeDirection.Buy, - Asset = DataLayer.Entities.Trades.Enums.AssetType.Common, - }; - - await context.Trades.AddAsync(newTrade); - } - else - { - var oldAmount = trade.Price * trade.Count; - var newAmount = res.ExecutedOrderPrice * res.LotsExecuted; - trade.Count = res.LotsExecuted + trade.Count; - trade.Price = (oldAmount + newAmount) / trade.Count; - context.Trades.Update(trade); - } - - await context.SaveChangesAsync(); - return new DealResult - { - Count = res.LotsExecuted, - Price = res.ExecutedOrderPrice, - Success = true, - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", AccountId, figi); - } - return new DealResult - { - Count = 0, - Price = 0, - Success = false, - }; - } - - private async Task ProcessMarketCommand(TradeCommand command) - { - if (string.IsNullOrWhiteSpace(command.Figi)) return; - if (command.CommandType == TradeCommandType.MarketBuy) - { - await BuyAsset(command.Figi, command.Count ?? 1, command.Ticker, command.RecomendPrice); - } - else if (command.CommandType == TradeCommandType.ForceClosePosition) - { - await ClosePosition(command.Figi); - } - else return; - - await SyncPortfolio(); - } - } -} diff --git a/KLHZ.Trader.Core/Exchange/Services/TradeDataProvider.cs b/KLHZ.Trader.Core/Exchange/Services/TradeDataProvider.cs new file mode 100644 index 0000000..910e558 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Services/TradeDataProvider.cs @@ -0,0 +1,241 @@ +using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; +using KLHZ.Trader.Core.DataLayer; +using KLHZ.Trader.Core.Exchange.Extentions; +using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; +using KLHZ.Trader.Core.Exchange.Models.Configs; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Collections.Concurrent; +using Tinkoff.InvestApi; +using Tinkoff.InvestApi.V1; +using Asset = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.Asset; +using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; + +namespace KLHZ.Trader.Core.Exchange.Services +{ + //todo перенести сюда весь кэш и всю работу по сохранению данных. + public class TradeDataProvider + { + private readonly InvestApiClient _investApiClient; + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + private readonly IDataBus _dataBus; + private readonly string[] _managedAccountsNamePatterns = []; + private readonly string[] _instrumentsFigis = []; + + private readonly ConcurrentDictionary _instrumentsSettings = new(); + private readonly ConcurrentDictionary _tickersCache = new(); + private readonly ConcurrentDictionary _assetTypesCache = new(); + internal readonly ConcurrentDictionary Accounts = new(); + public TradeDataProvider(InvestApiClient investApiClient, IOptions options, IDbContextFactory dbContextFactory, ILogger logger, IDataBus dataBus) + { + _investApiClient = investApiClient; + _dbContextFactory = dbContextFactory; + _logger = logger; + _dataBus = dataBus; + _managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray(); + _instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray(); + + + foreach (var lev in options.Value.InstrumentsSettings) + { + _instrumentsSettings.TryAdd(lev.Figi, lev); + } + } + + public async Task Init() + { + var shares = await _investApiClient.Instruments.SharesAsync(); + foreach (var share in shares.Instruments) + { + if (_instrumentsFigis.Contains(share.Figi)) + { + _tickersCache.TryAdd(share.Figi, share.Ticker); + _assetTypesCache.TryAdd(share.Figi, AssetType.Common); + } + } + var futures = await _investApiClient.Instruments.FuturesAsync(); + foreach (var future in futures.Instruments) + { + if (_instrumentsFigis.Contains(future.Figi)) + { + _tickersCache.TryAdd(future.Figi, future.Ticker); + _assetTypesCache.TryAdd(future.Figi, AssetType.Common); + } + } + + var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns); + var accountsList = new List(); + int i = 0; + foreach (var accountId in accounts) + { + var acc = new ManagedAccount(accountId); + await SyncPortfolio(acc); + Accounts[accountId] = acc; + } + } + + public string GetTickerByFigi(string figi) + { + return _tickersCache.TryGetValue(figi, out var ticker) ? ticker : string.Empty; + } + + public AssetType GetAssetTypeByFigi(string figi) + { + return _assetTypesCache.TryGetValue(figi, out var t) ? t : AssetType.Unknown; + } + + internal async Task SyncPortfolio(ManagedAccount account) + { + try + { + //await _semaphoreSlim.WaitAsync(); + var portfolio = await _investApiClient.Operations.GetPortfolioAsync(new PortfolioRequest() + { + AccountId = account.AccountId, + }); + + var oldAssets = account.Assets.Keys.ToHashSet(); + using var context = await _dbContextFactory.CreateDbContextAsync(); + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + + var trades = await context.Trades + .Where(t => t.AccountId == account.AccountId && t.ArchiveStatus == 0) + .ToListAsync(); + foreach (var position in portfolio.Positions) + { + decimal price = 0; + var trade = trades.FirstOrDefault(t => t.Figi == position.Figi); + + if (trade != null) + { + trades.Remove(trade); + price = trade.Price; + } + else + { + price = position.AveragePositionPrice; + } + +#pragma warning disable CS0612 // Тип или член устарел + var asset = new Models.AssetsAccounting.Asset() + { + TradeId = trade?.Id, + AccountId = account.AccountId, + Figi = position.Figi, + Ticker = position.Ticker, + BoughtAt = trade?.BoughtAt ?? DateTime.UtcNow, + BoughtPrice = price, + Type = position.InstrumentType.ParseInstrumentType(), + Position = position.Quantity > 0 ? PositionType.Long : PositionType.Short, + BlockedItems = position.BlockedLots, + Count = position.Quantity, + CountLots = position.QuantityLots, + }; +#pragma warning restore CS0612 // Тип или член устарел + account.Assets.AddOrUpdate(asset.Figi, asset, (k, v) => asset); + oldAssets.Remove(asset.Figi); + } + + account.Total = portfolio.TotalAmountPortfolio; + account.Balance = portfolio.TotalAmountCurrencies; + + foreach (var asset in oldAssets) + { + account.Assets.TryRemove(asset, out _); + } + + var ids = trades.Select(t => t.Id).ToArray(); + await context.Trades + .Where(t => ids.Contains(t.Id)) + .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при синхранизации портфеля счёта {accountId}", account.AccountId); + } + finally + { + //_semaphoreSlim.Release(); + } + } + + public async Task LogDeal(DealResult dealResult) + { + using var context = await _dbContextFactory.CreateDbContextAsync(); + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + var priceCoeff = 1m; + if (_instrumentsSettings.TryGetValue(dealResult.Figi, out var se)) + { + priceCoeff = se.PriceToRubConvertationCoefficient; + } + var trade = await context.Trades.FirstOrDefaultAsync(t => t.ArchiveStatus == 0 && t.Figi == dealResult.Figi && t.AccountId == dealResult.AccountId); + if (trade == null) + { + var newTrade = new DataLayer.Entities.Trades.Trade() + { + AccountId = dealResult.AccountId, + Figi = dealResult.Figi, + Ticker = GetTickerByFigi(dealResult.Figi), + BoughtAt = DateTime.UtcNow, + Count = dealResult.Count, + Price = dealResult.Price * priceCoeff, + Position = dealResult.Count > 0 ? DataLayer.Entities.Trades.Enums.PositionType.Long : DataLayer.Entities.Trades.Enums.PositionType.Short, + Direction = (DataLayer.Entities.Trades.Enums.TradeDirection)(int)dealResult.Direction, + Asset = (DataLayer.Entities.Trades.Enums.AssetType)(int)GetAssetTypeByFigi(dealResult.Figi) + }; + + await context.Trades.AddAsync(newTrade); + await context.SaveChangesAsync(); + } + else + { + var oldAmount = trade.Price * trade.Count; + var newAmount = dealResult.Price * priceCoeff * dealResult.Count; + var oldCount = trade.Count; + trade.Count = trade.Count + dealResult.Count; + if (trade.Count != 0 && System.Math.Abs(oldCount) < System.Math.Abs(trade.Count))// Если суммарное количество элементов позиции сокращается - пересчитывать цену не нужно. + { + trade.Price = (oldAmount + newAmount) / trade.Count; + } + + if (Accounts.TryGetValue(dealResult.AccountId, out var account)) + { + if (account.Assets.TryGetValue(dealResult.Figi, out var asset)) + { + if (trade.Count == 0) + { + await context.Trades.Where(t => t.Id == trade.Id && t.ArchiveStatus == 0) + .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.ArchiveStatus, 1)); + account.Assets.TryRemove(dealResult.Figi, out _); + return; + } + else + { + context.Trades.Update(trade); + await context.SaveChangesAsync(); + var newAsset = new Asset() + { + AccountId = asset.AccountId, + Figi = asset.Figi, + Ticker = asset.Ticker, + BlockedItems = asset.BlockedItems, + BoughtAt = DateTime.UtcNow, + BoughtPrice = trade.Price, + Count = trade.Count, + Position = trade.Count > 0 ? PositionType.Long : PositionType.Short, + Type = asset.Type, + TradeId = asset.TradeId, + }; + account.Assets[dealResult.Figi] = newAsset; + return; + } + } + } + } + await SyncPortfolio(Accounts[dealResult.AccountId]); + } + + } +} diff --git a/KLHZ.Trader.Core/Exchange/Services/Trader.cs b/KLHZ.Trader.Core/Exchange/Services/Trader.cs index 83694f9..4670e08 100644 --- a/KLHZ.Trader.Core/Exchange/Services/Trader.cs +++ b/KLHZ.Trader.Core/Exchange/Services/Trader.cs @@ -1,44 +1,41 @@ using KLHZ.Trader.Core.Common; using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums; using KLHZ.Trader.Core.Contracts.Declisions.Interfaces; -using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums; using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.DataLayer; using KLHZ.Trader.Core.DataLayer.Entities.Declisions; using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums; 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.Models.AssetsAccounting; +using KLHZ.Trader.Core.Exchange.Models.Configs; +using KLHZ.Trader.Core.Exchange.Models.Trading; using KLHZ.Trader.Core.Exchange.Utils; using KLHZ.Trader.Core.Math.Declisions.Services.Cache; using KLHZ.Trader.Core.Math.Declisions.Utils; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Threading.Channels; using Tinkoff.InvestApi; -using AssetType = KLHZ.Trader.Core.Exchange.Models.Assets.AssetType; +using AssetType = KLHZ.Trader.Core.Exchange.Models.AssetsAccounting.AssetType; namespace KLHZ.Trader.Core.Exchange.Services { public class Trader : IHostedService { - private readonly InvestApiClient _investApiClient; - private readonly IServiceProvider _provider; private readonly IDataBus _dataBus; private readonly BotModeSwitcher _botModeSwitcher; private readonly IDbContextFactory _dbContextFactory; + private readonly TradeDataProvider _tradeDataProvider; + private readonly ILogger _logger; private readonly ConcurrentDictionary DeferredLongOpens = new(); private readonly ConcurrentDictionary DeferredLongCloses = new(); private readonly ConcurrentDictionary OpeningStops = new(); - private readonly ConcurrentDictionary Accounts = new(); + private readonly ConcurrentDictionary Leverages = new(); private readonly ConcurrentDictionary _historyCash = new(); - private readonly ILogger _logger; private readonly double _buyStopLength; @@ -47,7 +44,6 @@ namespace KLHZ.Trader.Core.Exchange.Services private readonly decimal _accountCashPart; private readonly decimal _accountCashPartFutures; private readonly decimal _defaultBuyPartOfAccount; - private readonly string[] _managedAccountsNamePatterns = []; private readonly string[] _tradingInstrumentsFigis = []; private readonly Channel _pricesChannel = Channel.CreateUnbounded(); @@ -60,14 +56,13 @@ namespace KLHZ.Trader.Core.Exchange.Services IOptions options, IDataBus dataBus, IDbContextFactory dbContextFactory, + TradeDataProvider tradeDataProvider, InvestApiClient investApiClient) { + _tradeDataProvider = tradeDataProvider; _logger = logger; _botModeSwitcher = botModeSwitcher; _dataBus = dataBus; - _provider = provider; - _investApiClient = investApiClient; - _managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray(); _dbContextFactory = dbContextFactory; _futureComission = options.Value.FutureComission; _shareComission = options.Value.ShareComission; @@ -76,29 +71,16 @@ namespace KLHZ.Trader.Core.Exchange.Services _defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount; _tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis; _buyStopLength = (double)options.Value.StopBuyLengthMinuts; + + foreach (var lev in options.Value.InstrumentsSettings) + { + Leverages.TryAdd(lev.Figi, lev); + } } public async Task StartAsync(CancellationToken cancellationToken) { - //await InitStops(); - var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns); - var accountsList = new List(); - int i = 0; - foreach (var accountId in accounts) - { - var acc = _provider.GetKeyedService(i); - if (acc != null) - { - await acc.Init(accountId); - Accounts[accountId] = acc; - i++; - } - else - { - break; - } - } - + await _tradeDataProvider.Init(); _dataBus.AddChannel(nameof(Trader), _pricesChannel); _dataBus.AddChannel(nameof(Trader), _ordersbookChannel); _ = ProcessPrices(); @@ -284,9 +266,9 @@ namespace KLHZ.Trader.Core.Exchange.Services await context.Trades .Where(t => t.Figi == newPrice.Figi && t.ArchiveStatus == 0 && t.Asset == DataLayer.Entities.Trades.Enums.AssetType.Future) .ExecuteUpdateAsync(t => t.SetProperty(tr => tr.Price, newPriceValue)); - foreach (var account in Accounts.Values) + foreach (var account in _tradeDataProvider.Accounts.Values) { - await account.SyncPortfolio(); + await _tradeDataProvider.SyncPortfolio(account); } } @@ -351,9 +333,34 @@ namespace KLHZ.Trader.Core.Exchange.Services } } + private decimal CalcProfit(string accountId, string figi, decimal closePrice) + { + if (_tradeDataProvider.Accounts.TryGetValue(accountId, out var account)) + { + if (account.Assets.TryGetValue(figi, out var asset)) + { + var leverageValue = 1m; + var isShort = asset.Position == PositionType.Short; + if (Leverages.TryGetValue(figi, out var leverage)) + { + if (asset.Type == AssetType.Futures && !isShort) + { + leverageValue = leverage.LongLeverage; + } + else if (isShort) + { + leverageValue = leverage.ShortLeverage; + } + } + return TradingCalculator.CaclProfit(asset.BoughtPrice, closePrice, GetComission(asset.Type), leverageValue, isShort); + } + } + return 0; + } + private decimal GetCount(string accountId, decimal boutPrice) { - var balance = Accounts[accountId].Balance; + var balance = _tradeDataProvider.Accounts[accountId].Balance; return System.Math.Floor(balance * _defaultBuyPartOfAccount / boutPrice); } @@ -361,10 +368,10 @@ namespace KLHZ.Trader.Core.Exchange.Services { if (!_botModeSwitcher.CanPurchase()) return false; - var balance = Accounts[accountId].Balance; - var total = Accounts[accountId].Total; + var balance = _tradeDataProvider.Accounts[accountId].Balance; + var total = _tradeDataProvider.Accounts[accountId].Total; - var futures = Accounts[accountId].Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures); + var futures = _tradeDataProvider.Accounts[accountId].Assets.Values.FirstOrDefault(v => v.Type == AssetType.Futures); if (futures != null || needBigCash) { if ((balance - boutPrice * count) / total < _accountCashPartFutures) return false; @@ -376,26 +383,5 @@ namespace KLHZ.Trader.Core.Exchange.Services return true; } - - private bool IsSellAllowed(AssetType assetType, PositionType positionType, decimal boutPrice, decimal? requiredPrice, TradeCommandType commandType) - { - if (commandType >= TradeCommandType.MarketSell && commandType < TradeCommandType.ForceClosePosition && requiredPrice.HasValue) - { - var comission = GetComission(assetType); - if (positionType == PositionType.Long) - { - return requiredPrice.Value * (1 - comission) > boutPrice * (1 + comission); - } - else if (positionType == PositionType.Short) - { - return requiredPrice.Value * (1 + comission) < boutPrice * (1 - comission); - } - } - - if (commandType == TradeCommandType.ForceClosePosition) return true; - - return false; - } - } } diff --git a/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs new file mode 100644 index 0000000..ba44046 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Services/TradingCommandsExecutor.cs @@ -0,0 +1,97 @@ +using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces; +using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; +using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Threading.Channels; +using Tinkoff.InvestApi; +using Tinkoff.InvestApi.V1; + +namespace KLHZ.Trader.Core.Exchange.Services +{ + public class TradingCommandsExecutor : IHostedService + { + private readonly TradeDataProvider _tradeDataProvider; + private readonly InvestApiClient _investApiClient; + private readonly IDataBus _dataBus; + private readonly ILogger _logger; + private readonly Channel _channel = Channel.CreateUnbounded(); + + public TradingCommandsExecutor(InvestApiClient investApiClient, IDataBus dataBus, ILogger logger, TradeDataProvider tradeDataProvider) + { + _investApiClient = investApiClient; + _dataBus = dataBus; + _dataBus.AddChannel(nameof(TradingCommandsExecutor), _channel); + _logger = logger; + _tradeDataProvider = tradeDataProvider; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _ = ProcessCommands(); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + internal async Task ExecuteCommand(ITradeCommand tradeCommand) + { + try + { + var dir = OrderDirection.Unspecified; + var dealDirection = DealDirection.Unknown; + var sign = 1; + if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketBuy) + { + dir = OrderDirection.Buy; + dealDirection = DealDirection.Buy; + } + else if (tradeCommand.CommandType == Contracts.Messaging.Dtos.Enums.TradeCommandType.MarketSell) + { + sign = -1; + dir = OrderDirection.Sell; + dealDirection = DealDirection.Sell; + } + + var req = new PostOrderRequest() + { + AccountId = tradeCommand.AccountId, + InstrumentId = tradeCommand.Figi, + Direction = dir, + OrderType = OrderType.Market, + Quantity = tradeCommand.Count, + ConfirmMarginTrade = true, + }; + + var res = await _investApiClient.Orders.PostOrderAsync(req); + + var result = new DealResult + { + Count = sign * res.LotsExecuted, + Price = res.ExecutedOrderPrice, + Success = true, + Direction = dealDirection, + AccountId = tradeCommand.AccountId, + Figi = tradeCommand.Figi, + }; + await _tradeDataProvider.LogDeal(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Ошибка при покупке актива на счёт {acc}. figi: {figi}", tradeCommand.AccountId, tradeCommand.Figi); + } + } + + private async Task ProcessCommands() + { + while (await _channel.Reader.WaitToReadAsync()) + { + var command = await _channel.Reader.ReadAsync(); + await ExecuteCommand(command); + } + } + } +} diff --git a/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs b/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs index dff3e64..714c944 100644 --- a/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs +++ b/KLHZ.Trader.Core/Exchange/Utils/ExchangeScheduler.cs @@ -1,4 +1,4 @@ -using KLHZ.Trader.Core.Exchange.Models; +using KLHZ.Trader.Core.Exchange.Models.Trading; namespace KLHZ.Trader.Core.Exchange.Utils { diff --git a/KLHZ.Trader.Core/Exchange/Utils/TradingCalculator.cs b/KLHZ.Trader.Core/Exchange/Utils/TradingCalculator.cs new file mode 100644 index 0000000..81e4393 --- /dev/null +++ b/KLHZ.Trader.Core/Exchange/Utils/TradingCalculator.cs @@ -0,0 +1,11 @@ +namespace KLHZ.Trader.Core.Exchange.Utils +{ + internal static class TradingCalculator + { + public static decimal CaclProfit(decimal openPrice, decimal closePrice, decimal comission, decimal leverage, bool isShort) + { + var diff = ((isShort ? (closePrice - openPrice) : (openPrice - closePrice)) - closePrice * comission - openPrice * comission) * leverage; + return diff; + } + } +} diff --git a/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs b/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs index 1cdab06..7743b3f 100644 --- a/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs +++ b/KLHZ.Trader.Core/TG/Services/BotMessagesHandler.cs @@ -74,37 +74,27 @@ namespace KLHZ.Trader.Core.TG.Services await botClient.SendMessage(update.Message.Chat, "Покупки остановлены!"); break; } - case "сбросить сбер": - { - var command = new TradeCommand() - { - CommandType = TradeCommandType.ForceClosePosition, - RecomendPrice = null, - Figi = "BBG004730N88", - }; - await _eventBus.Broadcast(command); - break; - } - case "продать сбер": + case "продать IMOEXF": { var command = new TradeCommand() { + AccountId = "2274189208", CommandType = TradeCommandType.MarketSell, RecomendPrice = null, - Figi = "BBG004730N88", + Figi = "FUTIMOEXF000", Count = 1, - LotsCount = 1, }; await _eventBus.Broadcast(command); break; } - case "купить сбер": + case "купить IMOEXF": { var command = new TradeCommand() { + AccountId = "2274189208", CommandType = TradeCommandType.MarketBuy, RecomendPrice = null, - Figi = "BBG004730N88", + Figi = "FUTIMOEXF000", Count = 1 }; await _eventBus.Broadcast(command); diff --git a/KLHZ.Trader.Core/TG/TgBotConfig.cs b/KLHZ.Trader.Core/TG/TgBotConfig.cs index 3c253fc..72bb6e8 100644 --- a/KLHZ.Trader.Core/TG/TgBotConfig.cs +++ b/KLHZ.Trader.Core/TG/TgBotConfig.cs @@ -4,6 +4,6 @@ { public required string Token { get; set; } - public required long[] Admins = []; + public long[] Admins { get; set; } = []; } } diff --git a/KLHZ.Trader.HistoryLoader/Program.cs b/KLHZ.Trader.HistoryLoader/Program.cs index c9871b8..e0862eb 100644 --- a/KLHZ.Trader.HistoryLoader/Program.cs +++ b/KLHZ.Trader.HistoryLoader/Program.cs @@ -1,5 +1,5 @@ using KLHZ.Trader.Core.DataLayer; -using KLHZ.Trader.Core.Exchange; +using KLHZ.Trader.Core.Exchange.Models.Configs; using Microsoft.EntityFrameworkCore; diff --git a/KLHZ.Trader.Service/Program.cs b/KLHZ.Trader.Service/Program.cs index 30a0081..f49b8a6 100644 --- a/KLHZ.Trader.Service/Program.cs +++ b/KLHZ.Trader.Service/Program.cs @@ -2,7 +2,8 @@ using KLHZ.Trader.Core.Common; using KLHZ.Trader.Core.Common.Messaging.Services; using KLHZ.Trader.Core.Contracts.Messaging.Interfaces; using KLHZ.Trader.Core.DataLayer; -using KLHZ.Trader.Core.Exchange; +using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting; +using KLHZ.Trader.Core.Exchange.Models.Configs; using KLHZ.Trader.Core.Exchange.Services; using KLHZ.Trader.Core.TG; using KLHZ.Trader.Core.TG.Services; @@ -44,11 +45,14 @@ builder.Services.AddDbContextFactory(options => builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); //builder.Services.AddHostedService(); //builder.Services.AddHostedService(); builder.Services.AddSingleton(); + +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/KLHZ.Trader.Service/appsettings.json b/KLHZ.Trader.Service/appsettings.json index 0da5351..0380441 100644 --- a/KLHZ.Trader.Service/appsettings.json +++ b/KLHZ.Trader.Service/appsettings.json @@ -18,7 +18,15 @@ "ShareComission": 0.0004, "AccountCashPart": 0.05, "AccountCashPartFutures": 0.5, - "DefaultBuyPartOfAccount": 0.3333 + "DefaultBuyPartOfAccount": 0.3333, + "InstrumentsSettings": [ + { + "Figi": "FUTIMOEXF000", + "LongLeverage": 10.3, + "ShortLeverage": 7.9, + "PriceToRubConvertationCoefficient" : 0.1 + } + ] }, "Logging": { "LogLevel": { diff --git a/KLHZ.Trader.sln b/KLHZ.Trader.sln index 0b05253..5b4c94a 100644 --- a/KLHZ.Trader.sln +++ b/KLHZ.Trader.sln @@ -25,8 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphana", "graphana", "{4A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{174A800A-6040-40CF-B331-8603E097CBAC}" ProjectSection(SolutionItems) = preProject - KLHZ.Trader.Infrastructure\postgres\init.sql = KLHZ.Trader.Infrastructure\postgres\init.sql KLHZ.Trader.Infrastructure\postgres\migration1.sql = KLHZ.Trader.Infrastructure\postgres\migration1.sql + KLHZ.Trader.Infrastructure\postgres\init.sql = KLHZ.Trader.Infrastructure\postgres\init.sql KLHZ.Trader.Infrastructure\postgres\migration2.sql = KLHZ.Trader.Infrastructure\postgres\migration2.sql KLHZ.Trader.Infrastructure\postgres\migration3.sql = KLHZ.Trader.Infrastructure\postgres\migration3.sql EndProjectSection