Функционал покупки продажи
test / deploy_trader_prod (push) Successful in 1m19s
Details
test / deploy_trader_prod (push) Successful in 1m19s
Details
parent
859fba68ed
commit
9114dda27f
|
@ -3,14 +3,8 @@
|
||||||
public enum TradeCommandType
|
public enum TradeCommandType
|
||||||
{
|
{
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
|
|
||||||
MarketBuy = 1,
|
MarketBuy = 1,
|
||||||
|
|
||||||
|
|
||||||
MarketSell = 101,
|
MarketSell = 101,
|
||||||
SoftClosePosition = 110,
|
|
||||||
|
|
||||||
ForceClosePosition = 201,
|
|
||||||
|
|
||||||
UpdatePortfolio = 10000,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +1,14 @@
|
||||||
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums;
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Enums;
|
||||||
|
using KLHZ.Trader.Core.Contracts.Messaging.Dtos.Interfaces;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
|
namespace KLHZ.Trader.Core.Contracts.Messaging.Dtos
|
||||||
{
|
{
|
||||||
public class TradeCommand
|
public class TradeCommand : ITradeCommand
|
||||||
{
|
{
|
||||||
public TradeCommandType CommandType { get; init; }
|
public TradeCommandType CommandType { get; init; }
|
||||||
public string? Figi { get; init; }
|
public required string Figi { get; init; }
|
||||||
public string? Ticker { get; init; }
|
|
||||||
public decimal? RecomendPrice { get; init; }
|
public decimal? RecomendPrice { get; init; }
|
||||||
public decimal? Count { get; init; }
|
public long Count { get; init; }
|
||||||
public decimal? LotsCount { get; init; }
|
public required string AccountId { get; init; }
|
||||||
public string? AccountId { get; init; }
|
|
||||||
public bool IsNeedBigCashOnAccount { get; init; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
using System.Threading.Channels;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces
|
namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces
|
||||||
|
@ -9,11 +8,11 @@ namespace KLHZ.Trader.Core.Contracts.Messaging.Interfaces
|
||||||
public bool AddChannel(string key, Channel<IOrderbook> channel);
|
public bool AddChannel(string key, Channel<IOrderbook> channel);
|
||||||
public bool AddChannel(string key, Channel<IProcessedPrice> channel);
|
public bool AddChannel(string key, Channel<IProcessedPrice> channel);
|
||||||
public bool AddChannel(string key, Channel<INewPrice> channel);
|
public bool AddChannel(string key, Channel<INewPrice> channel);
|
||||||
public bool AddChannel(string key, Channel<TradeCommand> channel);
|
public bool AddChannel(string key, Channel<ITradeCommand> channel);
|
||||||
public bool AddChannel(string key, Channel<IMessage> channel);
|
public bool AddChannel(string key, Channel<IMessage> channel);
|
||||||
public bool AddChannel(string key, Channel<INewCandle> channel);
|
public bool AddChannel(string key, Channel<INewCandle> channel);
|
||||||
public Task Broadcast(INewPrice newPriceMessage);
|
public Task Broadcast(INewPrice newPriceMessage);
|
||||||
public Task Broadcast(TradeCommand command);
|
public Task Broadcast(ITradeCommand command);
|
||||||
public Task Broadcast(INewCandle command);
|
public Task Broadcast(INewCandle command);
|
||||||
public Task Broadcast(IProcessedPrice command);
|
public Task Broadcast(IProcessedPrice command);
|
||||||
public Task Broadcast(IOrderbook orderbook);
|
public Task Broadcast(IOrderbook orderbook);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using KLHZ.Trader.Core.Exchange.Models.Trading;
|
||||||
using KLHZ.Trader.Core.Exchange.Utils;
|
using KLHZ.Trader.Core.Exchange.Utils;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Tests
|
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 dt = new DateTime(2025, 9, 6, 0, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.Close);
|
Assert.IsTrue(res == ExchangeState.Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -17,7 +18,7 @@ namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
var dt = new DateTime(2025, 9, 5, 7, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2025, 9, 5, 7, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.Open);
|
Assert.IsTrue(res == ExchangeState.Open);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -25,7 +26,7 @@ namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
var dt = new DateTime(2025, 9, 5, 6, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2025, 9, 5, 6, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.Close);
|
Assert.IsTrue(res == ExchangeState.Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -33,7 +34,7 @@ namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
var dt = new DateTime(2025, 9, 5, 11, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2025, 9, 5, 11, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.ClearingTime);
|
Assert.IsTrue(res == ExchangeState.ClearingTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -41,7 +42,7 @@ namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
var dt = new DateTime(2025, 9, 7, 11, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2025, 9, 7, 11, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.Open);
|
Assert.IsTrue(res == ExchangeState.Open);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -49,7 +50,7 @@ namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
var dt = new DateTime(2025, 9, 5, 16, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2025, 9, 5, 16, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.ClearingTime);
|
Assert.IsTrue(res == ExchangeState.ClearingTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -57,7 +58,7 @@ namespace KLHZ.Trader.Core.Tests
|
||||||
{
|
{
|
||||||
var dt = new DateTime(2025, 9, 7, 15, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2025, 9, 7, 15, 0, 0, DateTimeKind.Utc);
|
||||||
var res = ExchangeScheduler.GetCurrentState(dt);
|
var res = ExchangeScheduler.GetCurrentState(dt);
|
||||||
Assert.IsTrue(res == Exchange.Models.ExchangeState.Open);
|
Assert.IsTrue(res == ExchangeState.Open);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
|
@ -13,7 +12,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
|
||||||
private readonly ConcurrentDictionary<string, Channel<INewCandle>> _candlesChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<INewCandle>> _candlesChannels = new();
|
||||||
private readonly ConcurrentDictionary<string, Channel<INewPrice>> _priceChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<INewPrice>> _priceChannels = new();
|
||||||
private readonly ConcurrentDictionary<string, Channel<IProcessedPrice>> _processedPricesChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<IProcessedPrice>> _processedPricesChannels = new();
|
||||||
private readonly ConcurrentDictionary<string, Channel<TradeCommand>> _commandChannels = new();
|
private readonly ConcurrentDictionary<string, Channel<ITradeCommand>> _commandChannels = new();
|
||||||
|
|
||||||
public bool AddChannel(string key, Channel<IProcessedPrice> channel)
|
public bool AddChannel(string key, Channel<IProcessedPrice> channel)
|
||||||
{
|
{
|
||||||
|
@ -35,7 +34,7 @@ namespace KLHZ.Trader.Core.Common.Messaging.Services
|
||||||
return _candlesChannels.TryAdd(key, channel);
|
return _candlesChannels.TryAdd(key, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool AddChannel(string key, Channel<TradeCommand> channel)
|
public bool AddChannel(string key, Channel<ITradeCommand> channel)
|
||||||
{
|
{
|
||||||
return _commandChannels.TryAdd(key, 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)
|
foreach (var channel in _commandChannels.Values)
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
{
|
{
|
||||||
Unknown = 0,
|
Unknown = 0,
|
||||||
Common = 1,
|
Common = 1,
|
||||||
Future = 2
|
Future = 2,
|
||||||
|
Currency = 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using KLHZ.Trader.Core.Exchange.Models.Assets;
|
using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Exchange.Extentions
|
namespace KLHZ.Trader.Core.Exchange.Extentions
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
namespace KLHZ.Trader.Core.Exchange.Models.Assets
|
|
||||||
{
|
|
||||||
public enum AssetType
|
|
||||||
{
|
|
||||||
Unknown = 0,
|
|
||||||
Currency = 1,
|
|
||||||
Common = 2,
|
|
||||||
Futures = 3,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace KLHZ.Trader.Core.Exchange.Models.Assets
|
|
||||||
{
|
|
||||||
public enum PositionType
|
|
||||||
{
|
|
||||||
Unknown = 0,
|
|
||||||
Long = 1,
|
|
||||||
Short = 2
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
namespace KLHZ.Trader.Core.Exchange.Models.Assets
|
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
|
||||||
{
|
{
|
||||||
public class Asset
|
public class Asset
|
||||||
{
|
{
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
|
||||||
|
{
|
||||||
|
public enum AssetType
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Common = 1,
|
||||||
|
Futures = 2,
|
||||||
|
Currency = 3,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
|
||||||
|
{
|
||||||
|
public enum DealDirection
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Buy = 1,
|
||||||
|
Sell = 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<string, Asset> 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<DealResult> 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<DealResult> 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();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace KLHZ.Trader.Core.Exchange.Models.AssetsAccounting
|
||||||
|
{
|
||||||
|
public enum PositionType
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Long = 1,//Пока считаем, что маржинального лонга для обычных акций не существует.
|
||||||
|
Short = 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
namespace KLHZ.Trader.Core.Exchange
|
namespace KLHZ.Trader.Core.Exchange.Models.Configs
|
||||||
{
|
{
|
||||||
public class ExchangeConfig
|
public class ExchangeConfig
|
||||||
{
|
{
|
||||||
|
@ -12,5 +12,6 @@
|
||||||
public string[] DataRecievingInstrumentsFigis { get; set; } = [];
|
public string[] DataRecievingInstrumentsFigis { get; set; } = [];
|
||||||
public string[] TradingInstrumentsFigis { get; set; } = [];
|
public string[] TradingInstrumentsFigis { get; set; } = [];
|
||||||
public string[] ManagingAccountNamePatterns { get; set; } = [];
|
public string[] ManagingAccountNamePatterns { get; set; } = [];
|
||||||
|
public InstrumentSettings[] InstrumentsSettings { get; set; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
namespace KLHZ.Trader.Core.Exchange.Models
|
namespace KLHZ.Trader.Core.Exchange.Models.Trading
|
||||||
{
|
{
|
||||||
internal class DeferredTrade
|
internal class DeferredTrade
|
||||||
{
|
{
|
|
@ -1,4 +1,4 @@
|
||||||
namespace KLHZ.Trader.Core.Exchange.Models
|
namespace KLHZ.Trader.Core.Exchange.Models.Trading
|
||||||
{
|
{
|
||||||
internal enum ExchangeState
|
internal enum ExchangeState
|
||||||
{
|
{
|
|
@ -6,6 +6,7 @@ using KLHZ.Trader.Core.DataLayer.Entities.Orders;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Trades;
|
using KLHZ.Trader.Core.DataLayer.Entities.Trades;
|
||||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
using KLHZ.Trader.Core.Exchange.Extentions;
|
||||||
|
using KLHZ.Trader.Core.Exchange.Models.Configs;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
@ -18,6 +19,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
{
|
{
|
||||||
public class ExchangeDataReader : IHostedService
|
public class ExchangeDataReader : IHostedService
|
||||||
{
|
{
|
||||||
|
private readonly TradeDataProvider _tradeDataProvider;
|
||||||
private readonly InvestApiClient _investApiClient;
|
private readonly InvestApiClient _investApiClient;
|
||||||
private readonly string[] _instrumentsFigis = [];
|
private readonly string[] _instrumentsFigis = [];
|
||||||
private readonly string[] _managedAccountNamePatterns;
|
private readonly string[] _managedAccountNamePatterns;
|
||||||
|
@ -27,7 +29,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly IDataBus _eventBus;
|
private readonly IDataBus _eventBus;
|
||||||
private readonly bool _exchangeDataRecievingEnabled;
|
private readonly bool _exchangeDataRecievingEnabled;
|
||||||
public ExchangeDataReader(InvestApiClient investApiClient, IDataBus eventBus,
|
public ExchangeDataReader(InvestApiClient investApiClient, IDataBus eventBus, TradeDataProvider tradeDataProvider,
|
||||||
IOptions<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory,
|
IOptions<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory,
|
||||||
ILogger<ExchangeDataReader> logger)
|
ILogger<ExchangeDataReader> logger)
|
||||||
{
|
{
|
||||||
|
@ -38,36 +40,16 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
_instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray();
|
_instrumentsFigis = options.Value.DataRecievingInstrumentsFigis.ToArray();
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_managedAccountNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
|
_managedAccountNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
|
||||||
|
_tradeDataProvider = tradeDataProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Инициализация приемника данных с биржи");
|
_logger.LogInformation("Инициализация приемника данных с биржи");
|
||||||
var accounts = await _investApiClient.GetAccounts(_managedAccountNamePatterns);
|
var accounts = await _investApiClient.GetAccounts(_managedAccountNamePatterns);
|
||||||
await InitCache();
|
|
||||||
_ = CycleSubscribtion(accounts);
|
_ = 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)
|
private async Task CycleSubscribtion(string[] accounts)
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
|
@ -154,7 +136,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
var message = new PriceChange()
|
var message = new PriceChange()
|
||||||
{
|
{
|
||||||
Figi = response.LastPrice.Figi,
|
Figi = response.LastPrice.Figi,
|
||||||
Ticker = GetTickerByFigi(response.LastPrice.Figi),
|
Ticker = _tradeDataProvider.GetTickerByFigi(response.LastPrice.Figi),
|
||||||
Time = response.LastPrice.Time.ToDateTime().ToUniversalTime(),
|
Time = response.LastPrice.Time.ToDateTime().ToUniversalTime(),
|
||||||
Value = response.LastPrice.Price,
|
Value = response.LastPrice.Price,
|
||||||
IsHistoricalData = false,
|
IsHistoricalData = false,
|
||||||
|
@ -169,7 +151,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
{
|
{
|
||||||
Figi = response.Trade.Figi,
|
Figi = response.Trade.Figi,
|
||||||
BoughtAt = response.Trade.Time.ToDateTime().ToUniversalTime(),
|
BoughtAt = response.Trade.Time.ToDateTime().ToUniversalTime(),
|
||||||
Ticker = GetTickerByFigi(response.Trade.Figi),
|
Ticker = _tradeDataProvider.GetTickerByFigi(response.Trade.Figi),
|
||||||
Price = response.Trade.Price,
|
Price = response.Trade.Price,
|
||||||
Count = response.Trade.Quantity,
|
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,
|
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()
|
var asksSummary10 = new OrderbookItem()
|
||||||
{
|
{
|
||||||
Figi = response.Orderbook.Figi,
|
Figi = response.Orderbook.Figi,
|
||||||
Ticker = GetTickerByFigi(response.Orderbook.Figi),
|
Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi),
|
||||||
Count = response.Orderbook.Asks.Sum(a => (int)a.Quantity),
|
Count = response.Orderbook.Asks.Sum(a => (int)a.Quantity),
|
||||||
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary10,
|
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary10,
|
||||||
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
||||||
|
@ -190,7 +172,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
var asksSummary4 = new OrderbookItem()
|
var asksSummary4 = new OrderbookItem()
|
||||||
{
|
{
|
||||||
Figi = response.Orderbook.Figi,
|
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),
|
Count = response.Orderbook.Asks.Take(4).Sum(a => (int)a.Quantity),
|
||||||
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary4,
|
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.AsksSummary4,
|
||||||
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
||||||
|
@ -199,7 +181,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
var bidsSummary10 = new OrderbookItem()
|
var bidsSummary10 = new OrderbookItem()
|
||||||
{
|
{
|
||||||
Figi = response.Orderbook.Figi,
|
Figi = response.Orderbook.Figi,
|
||||||
Ticker = GetTickerByFigi(response.Orderbook.Figi),
|
Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi),
|
||||||
Count = response.Orderbook.Bids.Sum(a => (int)a.Quantity),
|
Count = response.Orderbook.Bids.Sum(a => (int)a.Quantity),
|
||||||
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary10,
|
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary10,
|
||||||
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
||||||
|
@ -208,7 +190,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
var bidsSummary4 = new OrderbookItem()
|
var bidsSummary4 = new OrderbookItem()
|
||||||
{
|
{
|
||||||
Figi = response.Orderbook.Figi,
|
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),
|
Count = response.Orderbook.Bids.Take(4).Sum(a => (int)a.Quantity),
|
||||||
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary4,
|
ItemType = DataLayer.Entities.Orders.Enums.OrderbookItemType.BidsSummary4,
|
||||||
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
||||||
|
@ -221,7 +203,7 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
|
|
||||||
var message = new NewOrderbookMessage()
|
var message = new NewOrderbookMessage()
|
||||||
{
|
{
|
||||||
Ticker = GetTickerByFigi(response.Orderbook.Figi),
|
Ticker = _tradeDataProvider.GetTickerByFigi(response.Orderbook.Figi),
|
||||||
Figi = response.Orderbook.Figi,
|
Figi = response.Orderbook.Figi,
|
||||||
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
Time = response.Orderbook.Time.ToDateTime().ToUniversalTime(),
|
||||||
AsksCount = asksSummary10.Count,
|
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)
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
|
|
|
@ -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<TradeCommand> _channel = Channel.CreateUnbounded<TradeCommand>();
|
|
||||||
|
|
||||||
#region Поля, собираемые из контейнера DI
|
|
||||||
private readonly InvestApiClient _investApiClient;
|
|
||||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
|
||||||
private readonly ILogger<ManagedAccount> _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<string, Models.Assets.Asset> Assets = new();
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public ManagedAccount(InvestApiClient investApiClient, IDataBus dataBus, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<ManagedAccount> 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<DealResult> 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<DealResult> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<TraderDbContext> _dbContextFactory;
|
||||||
|
private readonly ILogger<ManagedAccount> _logger;
|
||||||
|
private readonly IDataBus _dataBus;
|
||||||
|
private readonly string[] _managedAccountsNamePatterns = [];
|
||||||
|
private readonly string[] _instrumentsFigis = [];
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, InstrumentSettings> _instrumentsSettings = new();
|
||||||
|
private readonly ConcurrentDictionary<string, string> _tickersCache = new();
|
||||||
|
private readonly ConcurrentDictionary<string, AssetType> _assetTypesCache = new();
|
||||||
|
internal readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
|
||||||
|
public TradeDataProvider(InvestApiClient investApiClient, IOptions<ExchangeConfig> options, IDbContextFactory<TraderDbContext> dbContextFactory, ILogger<ManagedAccount> 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<ManagedAccount>();
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,44 +1,41 @@
|
||||||
using KLHZ.Trader.Core.Common;
|
using KLHZ.Trader.Core.Common;
|
||||||
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
|
using KLHZ.Trader.Core.Contracts.Declisions.Dtos.Enums;
|
||||||
using KLHZ.Trader.Core.Contracts.Declisions.Interfaces;
|
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.Dtos.Interfaces;
|
||||||
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
using KLHZ.Trader.Core.DataLayer;
|
using KLHZ.Trader.Core.DataLayer;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
using KLHZ.Trader.Core.DataLayer.Entities.Declisions;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums;
|
using KLHZ.Trader.Core.DataLayer.Entities.Declisions.Enums;
|
||||||
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
using KLHZ.Trader.Core.DataLayer.Entities.Prices;
|
||||||
using KLHZ.Trader.Core.Exchange.Extentions;
|
using KLHZ.Trader.Core.Exchange.Models.AssetsAccounting;
|
||||||
using KLHZ.Trader.Core.Exchange.Models;
|
using KLHZ.Trader.Core.Exchange.Models.Configs;
|
||||||
using KLHZ.Trader.Core.Exchange.Models.Assets;
|
using KLHZ.Trader.Core.Exchange.Models.Trading;
|
||||||
using KLHZ.Trader.Core.Exchange.Utils;
|
using KLHZ.Trader.Core.Exchange.Utils;
|
||||||
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
|
using KLHZ.Trader.Core.Math.Declisions.Services.Cache;
|
||||||
using KLHZ.Trader.Core.Math.Declisions.Utils;
|
using KLHZ.Trader.Core.Math.Declisions.Utils;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using Tinkoff.InvestApi;
|
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
|
namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
{
|
{
|
||||||
public class Trader : IHostedService
|
public class Trader : IHostedService
|
||||||
{
|
{
|
||||||
private readonly InvestApiClient _investApiClient;
|
|
||||||
private readonly IServiceProvider _provider;
|
|
||||||
private readonly IDataBus _dataBus;
|
private readonly IDataBus _dataBus;
|
||||||
private readonly BotModeSwitcher _botModeSwitcher;
|
private readonly BotModeSwitcher _botModeSwitcher;
|
||||||
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
private readonly IDbContextFactory<TraderDbContext> _dbContextFactory;
|
||||||
|
private readonly TradeDataProvider _tradeDataProvider;
|
||||||
|
private readonly ILogger<Trader> _logger;
|
||||||
private readonly ConcurrentDictionary<string, DeferredTrade> DeferredLongOpens = new();
|
private readonly ConcurrentDictionary<string, DeferredTrade> DeferredLongOpens = new();
|
||||||
private readonly ConcurrentDictionary<string, DeferredTrade> DeferredLongCloses = new();
|
private readonly ConcurrentDictionary<string, DeferredTrade> DeferredLongCloses = new();
|
||||||
private readonly ConcurrentDictionary<string, DateTime> OpeningStops = new();
|
private readonly ConcurrentDictionary<string, DateTime> OpeningStops = new();
|
||||||
private readonly ConcurrentDictionary<string, ManagedAccount> Accounts = new();
|
private readonly ConcurrentDictionary<string, InstrumentSettings> Leverages = new();
|
||||||
private readonly ConcurrentDictionary<string, IPriceHistoryCacheUnit> _historyCash = new();
|
private readonly ConcurrentDictionary<string, IPriceHistoryCacheUnit> _historyCash = new();
|
||||||
private readonly ILogger<Trader> _logger;
|
|
||||||
|
|
||||||
|
|
||||||
private readonly double _buyStopLength;
|
private readonly double _buyStopLength;
|
||||||
|
@ -47,7 +44,6 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
private readonly decimal _accountCashPart;
|
private readonly decimal _accountCashPart;
|
||||||
private readonly decimal _accountCashPartFutures;
|
private readonly decimal _accountCashPartFutures;
|
||||||
private readonly decimal _defaultBuyPartOfAccount;
|
private readonly decimal _defaultBuyPartOfAccount;
|
||||||
private readonly string[] _managedAccountsNamePatterns = [];
|
|
||||||
private readonly string[] _tradingInstrumentsFigis = [];
|
private readonly string[] _tradingInstrumentsFigis = [];
|
||||||
|
|
||||||
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
|
private readonly Channel<INewPrice> _pricesChannel = Channel.CreateUnbounded<INewPrice>();
|
||||||
|
@ -60,14 +56,13 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
IOptions<ExchangeConfig> options,
|
IOptions<ExchangeConfig> options,
|
||||||
IDataBus dataBus,
|
IDataBus dataBus,
|
||||||
IDbContextFactory<TraderDbContext> dbContextFactory,
|
IDbContextFactory<TraderDbContext> dbContextFactory,
|
||||||
|
TradeDataProvider tradeDataProvider,
|
||||||
InvestApiClient investApiClient)
|
InvestApiClient investApiClient)
|
||||||
{
|
{
|
||||||
|
_tradeDataProvider = tradeDataProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_botModeSwitcher = botModeSwitcher;
|
_botModeSwitcher = botModeSwitcher;
|
||||||
_dataBus = dataBus;
|
_dataBus = dataBus;
|
||||||
_provider = provider;
|
|
||||||
_investApiClient = investApiClient;
|
|
||||||
_managedAccountsNamePatterns = options.Value.ManagingAccountNamePatterns.ToArray();
|
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_futureComission = options.Value.FutureComission;
|
_futureComission = options.Value.FutureComission;
|
||||||
_shareComission = options.Value.ShareComission;
|
_shareComission = options.Value.ShareComission;
|
||||||
|
@ -76,29 +71,16 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
_defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount;
|
_defaultBuyPartOfAccount = options.Value.DefaultBuyPartOfAccount;
|
||||||
_tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis;
|
_tradingInstrumentsFigis = options.Value.TradingInstrumentsFigis;
|
||||||
_buyStopLength = (double)options.Value.StopBuyLengthMinuts;
|
_buyStopLength = (double)options.Value.StopBuyLengthMinuts;
|
||||||
|
|
||||||
|
foreach (var lev in options.Value.InstrumentsSettings)
|
||||||
|
{
|
||||||
|
Leverages.TryAdd(lev.Figi, lev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync(CancellationToken cancellationToken)
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
//await InitStops();
|
await _tradeDataProvider.Init();
|
||||||
var accounts = await _investApiClient.GetAccounts(_managedAccountsNamePatterns);
|
|
||||||
var accountsList = new List<ManagedAccount>();
|
|
||||||
int i = 0;
|
|
||||||
foreach (var accountId in accounts)
|
|
||||||
{
|
|
||||||
var acc = _provider.GetKeyedService<ManagedAccount>(i);
|
|
||||||
if (acc != null)
|
|
||||||
{
|
|
||||||
await acc.Init(accountId);
|
|
||||||
Accounts[accountId] = acc;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
|
_dataBus.AddChannel(nameof(Trader), _pricesChannel);
|
||||||
_dataBus.AddChannel(nameof(Trader), _ordersbookChannel);
|
_dataBus.AddChannel(nameof(Trader), _ordersbookChannel);
|
||||||
_ = ProcessPrices();
|
_ = ProcessPrices();
|
||||||
|
@ -284,9 +266,9 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
await context.Trades
|
await context.Trades
|
||||||
.Where(t => t.Figi == newPrice.Figi && t.ArchiveStatus == 0 && t.Asset == DataLayer.Entities.Trades.Enums.AssetType.Future)
|
.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));
|
.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)
|
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);
|
return System.Math.Floor(balance * _defaultBuyPartOfAccount / boutPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -361,10 +368,10 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
{
|
{
|
||||||
if (!_botModeSwitcher.CanPurchase()) return false;
|
if (!_botModeSwitcher.CanPurchase()) return false;
|
||||||
|
|
||||||
var balance = Accounts[accountId].Balance;
|
var balance = _tradeDataProvider.Accounts[accountId].Balance;
|
||||||
var total = Accounts[accountId].Total;
|
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 (futures != null || needBigCash)
|
||||||
{
|
{
|
||||||
if ((balance - boutPrice * count) / total < _accountCashPartFutures) return false;
|
if ((balance - boutPrice * count) / total < _accountCashPartFutures) return false;
|
||||||
|
@ -376,26 +383,5 @@ namespace KLHZ.Trader.Core.Exchange.Services
|
||||||
|
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<TradingCommandsExecutor> _logger;
|
||||||
|
private readonly Channel<ITradeCommand> _channel = Channel.CreateUnbounded<ITradeCommand>();
|
||||||
|
|
||||||
|
public TradingCommandsExecutor(InvestApiClient investApiClient, IDataBus dataBus, ILogger<TradingCommandsExecutor> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
using KLHZ.Trader.Core.Exchange.Models;
|
using KLHZ.Trader.Core.Exchange.Models.Trading;
|
||||||
|
|
||||||
namespace KLHZ.Trader.Core.Exchange.Utils
|
namespace KLHZ.Trader.Core.Exchange.Utils
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,37 +74,27 @@ namespace KLHZ.Trader.Core.TG.Services
|
||||||
await botClient.SendMessage(update.Message.Chat, "Покупки остановлены!");
|
await botClient.SendMessage(update.Message.Chat, "Покупки остановлены!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "сбросить сбер":
|
case "продать IMOEXF":
|
||||||
{
|
|
||||||
var command = new TradeCommand()
|
|
||||||
{
|
|
||||||
CommandType = TradeCommandType.ForceClosePosition,
|
|
||||||
RecomendPrice = null,
|
|
||||||
Figi = "BBG004730N88",
|
|
||||||
};
|
|
||||||
await _eventBus.Broadcast(command);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "продать сбер":
|
|
||||||
{
|
{
|
||||||
var command = new TradeCommand()
|
var command = new TradeCommand()
|
||||||
{
|
{
|
||||||
|
AccountId = "2274189208",
|
||||||
CommandType = TradeCommandType.MarketSell,
|
CommandType = TradeCommandType.MarketSell,
|
||||||
RecomendPrice = null,
|
RecomendPrice = null,
|
||||||
Figi = "BBG004730N88",
|
Figi = "FUTIMOEXF000",
|
||||||
Count = 1,
|
Count = 1,
|
||||||
LotsCount = 1,
|
|
||||||
};
|
};
|
||||||
await _eventBus.Broadcast(command);
|
await _eventBus.Broadcast(command);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "купить сбер":
|
case "купить IMOEXF":
|
||||||
{
|
{
|
||||||
var command = new TradeCommand()
|
var command = new TradeCommand()
|
||||||
{
|
{
|
||||||
|
AccountId = "2274189208",
|
||||||
CommandType = TradeCommandType.MarketBuy,
|
CommandType = TradeCommandType.MarketBuy,
|
||||||
RecomendPrice = null,
|
RecomendPrice = null,
|
||||||
Figi = "BBG004730N88",
|
Figi = "FUTIMOEXF000",
|
||||||
Count = 1
|
Count = 1
|
||||||
};
|
};
|
||||||
await _eventBus.Broadcast(command);
|
await _eventBus.Broadcast(command);
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
{
|
{
|
||||||
public required string Token { get; set; }
|
public required string Token { get; set; }
|
||||||
|
|
||||||
public required long[] Admins = [];
|
public long[] Admins { get; set; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
using KLHZ.Trader.Core.DataLayer;
|
using KLHZ.Trader.Core.DataLayer;
|
||||||
using KLHZ.Trader.Core.Exchange;
|
using KLHZ.Trader.Core.Exchange.Models.Configs;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@ using KLHZ.Trader.Core.Common;
|
||||||
using KLHZ.Trader.Core.Common.Messaging.Services;
|
using KLHZ.Trader.Core.Common.Messaging.Services;
|
||||||
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
using KLHZ.Trader.Core.Contracts.Messaging.Interfaces;
|
||||||
using KLHZ.Trader.Core.DataLayer;
|
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.Exchange.Services;
|
||||||
using KLHZ.Trader.Core.TG;
|
using KLHZ.Trader.Core.TG;
|
||||||
using KLHZ.Trader.Core.TG.Services;
|
using KLHZ.Trader.Core.TG.Services;
|
||||||
|
@ -44,11 +45,14 @@ builder.Services.AddDbContextFactory<TraderDbContext>(options =>
|
||||||
builder.Services.AddHostedService<BotStarter>();
|
builder.Services.AddHostedService<BotStarter>();
|
||||||
builder.Services.AddHostedService<ExchangeDataReader>();
|
builder.Services.AddHostedService<ExchangeDataReader>();
|
||||||
builder.Services.AddHostedService<Trader>();
|
builder.Services.AddHostedService<Trader>();
|
||||||
|
builder.Services.AddHostedService<TradingCommandsExecutor>();
|
||||||
//builder.Services.AddHostedService<ProcessedPricesLogger>();
|
//builder.Services.AddHostedService<ProcessedPricesLogger>();
|
||||||
|
|
||||||
//builder.Services.AddHostedService<KalmanPredictor>();
|
//builder.Services.AddHostedService<KalmanPredictor>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<IUpdateHandler, BotMessagesHandler>();
|
builder.Services.AddSingleton<IUpdateHandler, BotMessagesHandler>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<TradeDataProvider>();
|
||||||
builder.Services.AddSingleton<BotModeSwitcher>();
|
builder.Services.AddSingleton<BotModeSwitcher>();
|
||||||
builder.Services.AddSingleton<IDataBus, DataBus>();
|
builder.Services.AddSingleton<IDataBus, DataBus>();
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,15 @@
|
||||||
"ShareComission": 0.0004,
|
"ShareComission": 0.0004,
|
||||||
"AccountCashPart": 0.05,
|
"AccountCashPart": 0.05,
|
||||||
"AccountCashPartFutures": 0.5,
|
"AccountCashPartFutures": 0.5,
|
||||||
"DefaultBuyPartOfAccount": 0.3333
|
"DefaultBuyPartOfAccount": 0.3333,
|
||||||
|
"InstrumentsSettings": [
|
||||||
|
{
|
||||||
|
"Figi": "FUTIMOEXF000",
|
||||||
|
"LongLeverage": 10.3,
|
||||||
|
"ShortLeverage": 7.9,
|
||||||
|
"PriceToRubConvertationCoefficient" : 0.1
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
|
|
|
@ -25,8 +25,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "graphana", "graphana", "{4A
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{174A800A-6040-40CF-B331-8603E097CBAC}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "postrgres", "postrgres", "{174A800A-6040-40CF-B331-8603E097CBAC}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
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\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\migration2.sql = KLHZ.Trader.Infrastructure\postgres\migration2.sql
|
||||||
KLHZ.Trader.Infrastructure\postgres\migration3.sql = KLHZ.Trader.Infrastructure\postgres\migration3.sql
|
KLHZ.Trader.Infrastructure\postgres\migration3.sql = KLHZ.Trader.Infrastructure\postgres\migration3.sql
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
|
|
Loading…
Reference in New Issue