494 lines
19 KiB
C#
494 lines
19 KiB
C#
using Managing.Application.Abstractions.Repositories;
|
|
using Managing.Application.Abstractions.Services;
|
|
using Managing.Domain.Accounts;
|
|
using Managing.Domain.Bots;
|
|
using Managing.Domain.Indicators;
|
|
using Managing.Domain.Scenarios;
|
|
using Managing.Domain.Shared.Helpers;
|
|
using Managing.Domain.Statistics;
|
|
using Managing.Domain.Strategies;
|
|
using Managing.Domain.Synth.Models;
|
|
using Managing.Domain.Trades;
|
|
using Managing.Domain.Users;
|
|
using Managing.Infrastructure.Evm.Models.Privy;
|
|
using Microsoft.Extensions.Logging;
|
|
using static Managing.Common.Enums;
|
|
|
|
namespace Managing.Application.Trading;
|
|
|
|
public class TradingService : ITradingService
|
|
{
|
|
private readonly ITradingRepository _tradingRepository;
|
|
private readonly IExchangeService _exchangeService;
|
|
private readonly IAccountService _accountService;
|
|
private readonly IAccountRepository _accountRepository;
|
|
private readonly ICacheService _cacheService;
|
|
private readonly IMessengerService _messengerService;
|
|
private readonly IStatisticRepository _statisticRepository;
|
|
private readonly IEvmManager _evmManager;
|
|
private readonly ILogger<TradingService> _logger;
|
|
private readonly ISynthPredictionService _synthPredictionService;
|
|
private readonly IWeb3ProxyService _web3ProxyService;
|
|
|
|
public TradingService(
|
|
ITradingRepository tradingRepository,
|
|
IExchangeService exchangeService,
|
|
ILogger<TradingService> logger,
|
|
IAccountService accountService,
|
|
IAccountRepository accountRepository,
|
|
ICacheService cacheService,
|
|
IMessengerService messengerService,
|
|
IStatisticRepository statisticRepository,
|
|
IEvmManager evmManager,
|
|
ISynthPredictionService synthPredictionService,
|
|
IWeb3ProxyService web3ProxyService)
|
|
{
|
|
_tradingRepository = tradingRepository;
|
|
_exchangeService = exchangeService;
|
|
_logger = logger;
|
|
_accountService = accountService;
|
|
_accountRepository = accountRepository;
|
|
_cacheService = cacheService;
|
|
_messengerService = messengerService;
|
|
_statisticRepository = statisticRepository;
|
|
_evmManager = evmManager;
|
|
_synthPredictionService = synthPredictionService;
|
|
_web3ProxyService = web3ProxyService;
|
|
}
|
|
|
|
public async Task DeleteScenarioAsync(string name)
|
|
{
|
|
await _tradingRepository.DeleteScenarioAsync(name);
|
|
}
|
|
|
|
public async Task DeleteIndicatorAsync(string name)
|
|
{
|
|
await _tradingRepository.DeleteIndicatorAsync(name);
|
|
}
|
|
|
|
public async Task<Position> GetPositionByIdentifierAsync(Guid identifier)
|
|
{
|
|
return await _tradingRepository.GetPositionByIdentifierAsync(identifier);
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetPositionsAsync(PositionInitiator positionInitiator)
|
|
{
|
|
return await _tradingRepository.GetPositionsAsync(positionInitiator);
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetPositionsByStatusAsync(PositionStatus postionStatus)
|
|
{
|
|
return await _tradingRepository.GetPositionsByStatusAsync(postionStatus);
|
|
}
|
|
|
|
|
|
public async Task<Scenario> GetScenarioByNameAsync(string scenario)
|
|
{
|
|
return await _tradingRepository.GetScenarioByNameAsync(scenario);
|
|
}
|
|
|
|
public async Task<IEnumerable<Scenario>> GetScenariosAsync()
|
|
{
|
|
return await _tradingRepository.GetScenariosAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<Scenario>> GetScenariosByUserAsync(User user)
|
|
{
|
|
return await _tradingRepository.GetScenariosByUserAsync(user);
|
|
}
|
|
|
|
public async Task<IEnumerable<IndicatorBase>> GetIndicatorsAsync()
|
|
{
|
|
return await _tradingRepository.GetStrategiesAsync();
|
|
}
|
|
|
|
public async Task<IndicatorBase> GetIndicatorByNameAsync(string strategy)
|
|
{
|
|
return await _tradingRepository.GetStrategyByNameAsync(strategy);
|
|
}
|
|
|
|
public async Task InsertPositionAsync(Position position)
|
|
{
|
|
await _tradingRepository.InsertPositionAsync(position);
|
|
}
|
|
|
|
public async Task InsertScenarioAsync(Scenario scenario)
|
|
{
|
|
await _tradingRepository.InsertScenarioAsync(scenario);
|
|
}
|
|
|
|
public async Task InsertIndicatorAsync(IndicatorBase indicatorBase)
|
|
{
|
|
await _tradingRepository.InsertIndicatorAsync(indicatorBase);
|
|
}
|
|
|
|
public async Task<Position> ManagePosition(Account account, Position position)
|
|
{
|
|
var lastPrice = await _exchangeService.GetPrice(account, position.Ticker, DateTime.UtcNow);
|
|
var quantityInPosition = await _exchangeService.GetQuantityInPosition(account, position.Ticker);
|
|
var orders = await _exchangeService.GetOpenOrders(account, position.Ticker);
|
|
|
|
if (quantityInPosition > 0)
|
|
{
|
|
// Position still open
|
|
position.ProfitAndLoss =
|
|
TradingBox.GetProfitAndLoss(position, position.Open.Quantity, lastPrice, position.Open.Leverage);
|
|
_logger.LogInformation($"Position is still open - PNL : {position.ProfitAndLoss.Realized} $");
|
|
_logger.LogInformation($"Requested trades : {orders.Count}");
|
|
}
|
|
else
|
|
{
|
|
// No quantity in position = SL/TP hit
|
|
if (orders.All(o => o.TradeType != TradeType.StopLoss))
|
|
{
|
|
// SL hit
|
|
_logger.LogInformation($"Stop loss is filled on exchange.");
|
|
position.StopLoss.SetStatus(TradeStatus.Filled);
|
|
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.StopLoss.Quantity,
|
|
position.StopLoss.Price, position.Open.Leverage);
|
|
_ = _exchangeService.CancelOrder(account, position.Ticker);
|
|
}
|
|
else if (orders.All(o => o.TradeType != TradeType.TakeProfit))
|
|
{
|
|
// TP Hit
|
|
if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit2 != null)
|
|
{
|
|
position.TakeProfit2.SetStatus(TradeStatus.Filled);
|
|
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.TakeProfit2.Quantity,
|
|
position.TakeProfit2.Price, 1);
|
|
_logger.LogInformation($"TakeProfit 2 is filled on exchange.");
|
|
}
|
|
else
|
|
{
|
|
position.TakeProfit1.SetStatus(TradeStatus.Filled);
|
|
position.ProfitAndLoss = TradingBox.GetProfitAndLoss(position, position.TakeProfit1.Quantity,
|
|
position.TakeProfit1.Price, 1);
|
|
_logger.LogInformation($"TakeProfit 1 is filled on exchange.");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation(
|
|
$"Position closed manually or forced close by exchange because quantity in position is below 0.");
|
|
position.Status = PositionStatus.Finished;
|
|
|
|
if (orders.Any()) await _exchangeService.CancelOrder(account, position.Ticker);
|
|
}
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
public async Task UpdatePositionAsync(Position position)
|
|
{
|
|
await _tradingRepository.UpdatePositionAsync(position);
|
|
}
|
|
|
|
public async Task WatchTrader()
|
|
{
|
|
var availableTickers = new List<Ticker> { Ticker.BTC, Ticker.ETH, Ticker.UNI, Ticker.LINK };
|
|
var watchAccount = await GetTradersWatch();
|
|
var key = $"AccountsQuantityInPosition";
|
|
var aqip = _cacheService.GetValue<List<TraderFollowup>>(key);
|
|
|
|
if (aqip == null)
|
|
{
|
|
aqip = GetAccountsQuantityInPosition(watchAccount);
|
|
}
|
|
else
|
|
{
|
|
foreach (var a in watchAccount.Where(w => !aqip.Any(a => a.Account.Address == w.Address)))
|
|
{
|
|
var newAccount = SetupFollowUp(a);
|
|
aqip.Add(newAccount);
|
|
}
|
|
|
|
foreach (var a in aqip)
|
|
{
|
|
await ManageTrader(a, availableTickers);
|
|
}
|
|
}
|
|
|
|
_cacheService.SaveValue(key, aqip, TimeSpan.FromMinutes(10));
|
|
}
|
|
|
|
public async Task<IEnumerable<Trader>> GetTradersWatch()
|
|
{
|
|
var watchAccount = await _statisticRepository.GetBestTradersAsync();
|
|
var customWatchAccount = (await _accountService.GetAccountsAsync(true, false))
|
|
.Where(a => a.Type == AccountType.Watch)
|
|
.ToList().MapToTraders();
|
|
watchAccount.AddRange(customWatchAccount.Where(a =>
|
|
!watchAccount.Any(w => w.Address.Equals(a.Address, StringComparison.InvariantCultureIgnoreCase))));
|
|
return watchAccount;
|
|
}
|
|
|
|
public void UpdateDeltaNeutralOpportunities()
|
|
{
|
|
var fundingRates = _exchangeService.GetFundingRates();
|
|
}
|
|
|
|
public async Task UpdateScenarioAsync(Scenario scenario)
|
|
{
|
|
await _tradingRepository.UpdateScenarioAsync(scenario);
|
|
}
|
|
|
|
public async Task UpdateIndicatorAsync(IndicatorBase indicatorBase)
|
|
{
|
|
await _tradingRepository.UpdateStrategyAsync(indicatorBase);
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetBrokerPositions(Account account)
|
|
{
|
|
return await _exchangeService.GetBrokerPositions(account);
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetAllDatabasePositionsAsync()
|
|
{
|
|
return await _tradingRepository.GetAllPositionsAsync();
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier)
|
|
{
|
|
return await _tradingRepository.GetPositionsByInitiatorIdentifierAsync(initiatorIdentifier);
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetPositionsByInitiatorIdentifiersAsync(
|
|
IEnumerable<Guid> initiatorIdentifiers)
|
|
{
|
|
return await _tradingRepository.GetPositionsByInitiatorIdentifiersAsync(initiatorIdentifiers);
|
|
}
|
|
|
|
public async Task DeletePositionsByInitiatorIdentifierAsync(Guid initiatorIdentifier)
|
|
{
|
|
await _tradingRepository.DeletePositionsByInitiatorIdentifierAsync(initiatorIdentifier);
|
|
}
|
|
|
|
public async Task<decimal> GetGlobalPnLFromPositionsAsync()
|
|
{
|
|
return await _tradingRepository.GetGlobalPnLFromPositionsAsync();
|
|
}
|
|
|
|
private async Task ManageTrader(TraderFollowup a, List<Ticker> tickers)
|
|
{
|
|
var shortAddress = a.Account.Address.Substring(0, 6);
|
|
|
|
foreach (var ticker in tickers)
|
|
{
|
|
try
|
|
{
|
|
var newTrade = await _exchangeService.GetTrade(a.Account.Address, "", ticker);
|
|
var oldTrade = a.Trades.SingleOrDefault(t => t.Ticker == ticker);
|
|
if (newTrade == null)
|
|
{
|
|
if (oldTrade != null)
|
|
{
|
|
_logger.LogInformation(
|
|
$"[{shortAddress}][{ticker}] Trader previously got a position open but the position was close by trader");
|
|
// TODO : Send position closed
|
|
a.Trades.Remove(oldTrade);
|
|
}
|
|
}
|
|
else if ((newTrade != null && oldTrade == null) || (newTrade.Quantity > oldTrade.Quantity))
|
|
{
|
|
_logger.LogInformation(
|
|
$"[{shortAddress}][{ticker}] Trader increase {newTrade.Direction} by {newTrade.Quantity - (oldTrade?.Quantity ?? 0)} with leverage {newTrade.Leverage} at {newTrade.Price} leverage.");
|
|
|
|
var index = a.Trades.IndexOf(oldTrade);
|
|
if (index != -1)
|
|
{
|
|
a.Trades[index] = newTrade;
|
|
}
|
|
else
|
|
{
|
|
a.Trades.Add(newTrade);
|
|
}
|
|
|
|
// Open position
|
|
await _messengerService.SendIncreasePosition(a.Account.Address, newTrade, "Test6", oldTrade);
|
|
// Save position to cache
|
|
}
|
|
else if (newTrade.Quantity < oldTrade.Quantity && newTrade.Quantity > 0)
|
|
{
|
|
var decreaseAmount = oldTrade.Quantity - newTrade.Quantity;
|
|
var index = a.Trades.IndexOf(oldTrade);
|
|
a.Trades[index] = newTrade;
|
|
_logger.LogInformation(
|
|
$"[{a.Account.Address.Substring(0, 6)}][{ticker}] Trader decrease position but didnt close it {decreaseAmount}");
|
|
await _messengerService.SendDecreasePosition(a.Account.Address, newTrade, decreaseAmount);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation(
|
|
$"[{shortAddress}][{ticker}] No change - Quantity still {newTrade.Quantity}");
|
|
}
|
|
}
|
|
catch (Exception)
|
|
{
|
|
_logger.LogError($"[{shortAddress}][{ticker}] Impossible to fetch trader");
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<TraderFollowup> GetAccountsQuantityInPosition(IEnumerable<Trader> watchAccount)
|
|
{
|
|
var result = new List<TraderFollowup>();
|
|
foreach (var account in watchAccount)
|
|
{
|
|
var trader = SetupFollowUp(account);
|
|
result.Add(trader);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static TraderFollowup SetupFollowUp(Trader account)
|
|
{
|
|
var trader = new TraderFollowup
|
|
{
|
|
Account = account,
|
|
Trades = new List<Trade>(),
|
|
PositionIdentifiers = new List<string>()
|
|
};
|
|
|
|
return trader;
|
|
}
|
|
|
|
public class TraderFollowup
|
|
{
|
|
public Trader Account { get; set; }
|
|
public List<Trade> Trades { get; set; }
|
|
public List<string> PositionIdentifiers { get; set; }
|
|
}
|
|
|
|
public async Task<PrivyInitAddressResponse> InitPrivyWallet(string publicAddress, TradingExchanges tradingExchange)
|
|
{
|
|
try
|
|
{
|
|
if (string.IsNullOrEmpty(publicAddress))
|
|
{
|
|
_logger.LogWarning("Attempted to initialize Privy wallet with null or empty public address");
|
|
return new PrivyInitAddressResponse
|
|
{ Success = false, Error = "Public address cannot be null or empty" };
|
|
}
|
|
|
|
// Check if the account is already initialized
|
|
var account = await _accountRepository.GetAccountByKeyAsync(publicAddress);
|
|
if (account != null && account.IsGmxInitialized)
|
|
{
|
|
_logger.LogInformation("Account with address {PublicAddress} is already initialized for GMX",
|
|
publicAddress);
|
|
return new PrivyInitAddressResponse
|
|
{
|
|
Success = true,
|
|
Address = publicAddress,
|
|
IsAlreadyInitialized = true
|
|
};
|
|
}
|
|
|
|
PrivyInitAddressResponse initResult;
|
|
switch (tradingExchange)
|
|
{
|
|
case TradingExchanges.GmxV2:
|
|
initResult = await _evmManager.InitAddressForGMX(publicAddress);
|
|
break;
|
|
default:
|
|
initResult = await _evmManager.InitAddressForGMX(publicAddress);
|
|
break;
|
|
}
|
|
|
|
// If initialization was successful, update the account's initialization status
|
|
if (initResult.Success && account != null)
|
|
{
|
|
account.IsGmxInitialized = true;
|
|
await _accountRepository.UpdateAccountAsync(account);
|
|
_logger.LogInformation("Updated account {AccountName} GMX initialization status to true", account.Name);
|
|
}
|
|
|
|
return initResult;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error initializing Privy wallet for address {PublicAddress}", publicAddress);
|
|
return new PrivyInitAddressResponse { Success = false, Error = ex.Message };
|
|
}
|
|
}
|
|
|
|
// Synth API integration methods
|
|
public async Task<SignalValidationResult> ValidateSynthSignalAsync(LightSignal signal, decimal currentPrice,
|
|
TradingBotConfig botConfig, bool isBacktest)
|
|
{
|
|
return await _synthPredictionService.ValidateSignalAsync(signal, currentPrice, botConfig, isBacktest);
|
|
}
|
|
|
|
public async Task<bool> AssessSynthPositionRiskAsync(Ticker ticker, TradeDirection direction, decimal currentPrice,
|
|
TradingBotConfig botConfig, bool isBacktest)
|
|
{
|
|
return await _synthPredictionService.AssessPositionRiskAsync(ticker, direction, currentPrice,
|
|
botConfig, isBacktest);
|
|
}
|
|
|
|
public async Task<SynthRiskResult> MonitorSynthPositionRiskAsync(Ticker ticker, TradeDirection direction,
|
|
decimal currentPrice, decimal liquidationPrice, Guid positionIdentifier, TradingBotConfig botConfig)
|
|
{
|
|
return await _synthPredictionService.MonitorPositionRiskAsync(ticker, direction, currentPrice, liquidationPrice,
|
|
positionIdentifier, botConfig);
|
|
}
|
|
|
|
|
|
public async Task<IndicatorBase?> GetIndicatorByNameUserAsync(string name, User user)
|
|
{
|
|
return await _tradingRepository.GetStrategyByNameUserAsync(name, user);
|
|
}
|
|
|
|
public async Task<Scenario?> GetScenarioByNameUserAsync(string scenarioName, User user)
|
|
{
|
|
return await _tradingRepository.GetScenarioByNameUserAsync(scenarioName, user);
|
|
}
|
|
|
|
public async Task<IEnumerable<Position>> GetPositionByUserIdAsync(int userId)
|
|
{
|
|
return await _tradingRepository.GetPositionByUserIdAsync(userId);
|
|
}
|
|
|
|
public async Task<SwapInfos> SwapGmxTokensAsync(User user, string accountName, Ticker fromTicker, Ticker toTicker,
|
|
double amount, string orderType = "market", double? triggerRatio = null, double allowedSlippage = 0.5)
|
|
{
|
|
// Get the account for the user
|
|
var account = await _accountService.GetAccountByUser(user, accountName, true, false);
|
|
|
|
if (account == null)
|
|
{
|
|
throw new ArgumentException($"Account '{accountName}' not found for user '{user.Name}'");
|
|
}
|
|
|
|
// Ensure the account has a valid address/key
|
|
if (string.IsNullOrEmpty(account.Key))
|
|
{
|
|
throw new ArgumentException($"Account '{accountName}' does not have a valid address");
|
|
}
|
|
|
|
try
|
|
{
|
|
// Call the Web3ProxyService to swap GMX tokens
|
|
var swapInfos = await _web3ProxyService.SwapGmxTokensAsync(
|
|
account.Key,
|
|
fromTicker,
|
|
toTicker,
|
|
amount,
|
|
orderType,
|
|
triggerRatio,
|
|
allowedSlippage
|
|
);
|
|
|
|
return swapInfos;
|
|
}
|
|
catch (Exception ex) when (!(ex is ArgumentException || ex is InvalidOperationException))
|
|
{
|
|
_logger.LogError(ex, "Error swapping GMX tokens for account {AccountName} and user {UserName}",
|
|
accountName, user.Name);
|
|
SentrySdk.CaptureException(ex);
|
|
throw new InvalidOperationException($"Failed to swap GMX tokens: {ex.Message}", ex);
|
|
}
|
|
}
|
|
} |