Files
managing-apps/src/Managing.Application/Trading/TradingService.cs

668 lines
26 KiB
C#

using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
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 = null)
{
// 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
{
// Get user's config to access default slippage value
var config = TradingBox.CreateConfigFromUserSettings(user);
// Use provided allowedSlippage if specified, otherwise use user's GmxSlippage setting, or default from IndicatorComboConfig
var slippageToUse = (double)(allowedSlippage.HasValue
? (decimal)allowedSlippage.Value
: (user.GmxSlippage ?? config.GmxSlippage));
// Call the Web3ProxyService to swap GMX tokens
var swapInfos = await _web3ProxyService.SwapGmxTokensAsync(
account.Key,
fromTicker,
toTicker,
amount,
orderType,
triggerRatio,
slippageToUse
);
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);
}
}
/// <summary>
/// Calculates indicator values and generates signals for a given ticker, timeframe, and date range with selected indicators.
/// </summary>
public async Task<RefineIndicatorsResult> RefineIndicatorsAsync(
Ticker ticker,
Timeframe timeframe,
DateTime startDate,
DateTime endDate,
List<IndicatorRequest> indicators)
{
// Get candles for the specified period
var candles = await _exchangeService.GetCandlesInflux(
TradingExchanges.Evm,
ticker,
startDate,
timeframe,
endDate);
if (candles == null || candles.Count == 0)
{
return new RefineIndicatorsResult
{
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>(),
Signals = new List<LightSignal>()
};
}
// Convert to ordered List to preserve chronological order for indicators
var candlesList = candles.OrderBy(c => c.Date).ToList();
// Map request indicators to domain Scenario
var scenario = MapRefineIndicatorsToScenario(indicators);
// Calculate indicators values
var indicatorsValues = TradingBox.CalculateIndicatorsValues(scenario, candlesList);
// Generate signals for the date range using rolling window approach
var signals = GenerateSignalsForDateRange(candlesList, scenario, indicatorsValues);
return new RefineIndicatorsResult
{
IndicatorsValues = indicatorsValues,
Signals = signals
};
}
/// <summary>
/// Maps IndicatorRequest list to a domain Scenario object.
/// </summary>
private Scenario MapRefineIndicatorsToScenario(List<IndicatorRequest> indicators)
{
var scenario = new Scenario("RefineIndicators", 1);
foreach (var indicatorRequest in indicators)
{
var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
Period = indicatorRequest.Period,
FastPeriods = indicatorRequest.FastPeriods,
SlowPeriods = indicatorRequest.SlowPeriods,
SignalPeriods = indicatorRequest.SignalPeriods,
Multiplier = indicatorRequest.Multiplier,
StDev = indicatorRequest.StDev,
SmoothPeriods = indicatorRequest.SmoothPeriods,
StochPeriods = indicatorRequest.StochPeriods,
CyclePeriods = indicatorRequest.CyclePeriods,
KFactor = indicatorRequest.KFactor,
DFactor = indicatorRequest.DFactor,
TenkanPeriods = indicatorRequest.TenkanPeriods,
KijunPeriods = indicatorRequest.KijunPeriods,
SenkouBPeriods = indicatorRequest.SenkouBPeriods,
OffsetPeriods = indicatorRequest.OffsetPeriods,
SenkouOffset = indicatorRequest.SenkouOffset,
ChikouOffset = indicatorRequest.ChikouOffset
};
scenario.AddIndicator(indicator);
}
return scenario;
}
/// <summary>
/// Generates signals for a date range using a rolling window approach to capture signals throughout the entire range.
/// This is necessary because some indicators (like STC) only process recent candles, so we need to process incrementally.
/// </summary>
private List<LightSignal> GenerateSignalsForDateRange(
List<Candle> candles,
Scenario scenario,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues)
{
var allSignals = new List<LightSignal>();
var seenSignalIdentifiers = new HashSet<string>();
var lightScenario = LightScenario.FromScenario(scenario);
// Use rolling window approach to process candles incrementally
// This ensures we capture signals throughout the entire date range, not just recent ones
const int RollingWindowSize = 600;
var rollingWindowCandles = new List<Candle>(RollingWindowSize);
// Process each candle with a rolling window
foreach (var candle in candles)
{
// Maintain rolling window
if (rollingWindowCandles.Count >= RollingWindowSize)
{
rollingWindowCandles.RemoveAt(0);
}
rollingWindowCandles.Add(candle);
// Only process if we have enough candles for indicators
if (rollingWindowCandles.Count < 2)
{
continue;
}
// Process each indicator individually for this rolling window position
foreach (var lightIndicator in lightScenario.Indicators)
{
// Build the indicator instance (create fresh instance for each indicator to avoid state issues)
var indicatorInstance = lightIndicator.ToInterface();
// Use pre-calculated indicator values if available
List<LightSignal> indicatorSignals;
if (preCalculatedIndicatorValues != null && preCalculatedIndicatorValues.ContainsKey(lightIndicator.Type))
{
// Use pre-calculated values to avoid recalculating indicators
indicatorSignals = indicatorInstance.Run(rollingWindowCandles, preCalculatedIndicatorValues[lightIndicator.Type]);
}
else
{
// Normal path: calculate indicators on the fly
indicatorSignals = indicatorInstance.Run(rollingWindowCandles);
}
// Add new signals from this indicator to the collection
if (indicatorSignals != null && indicatorSignals.Count > 0)
{
// Filter signals to only include those within the requested date range
var firstCandleDate = candles.First().Date;
var lastCandleDate = candles.Last().Date;
foreach (var signal in indicatorSignals)
{
// Only add signals within the date range and avoid duplicates
if (signal.Date >= firstCandleDate &&
signal.Date <= lastCandleDate &&
!seenSignalIdentifiers.Contains(signal.Identifier))
{
allSignals.Add(signal);
seenSignalIdentifiers.Add(signal.Identifier);
}
}
}
}
}
// Sort by date and return
return allSignals.OrderBy(s => s.Date).ToList();
}
}