Files
managing-apps/src/Managing.Application/ManageBot/StartCopyTradingCommandHandler.cs

204 lines
9.7 KiB
C#

using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
public class StartCopyTradingCommandHandler : IRequestHandler<StartCopyTradingCommand, string>
{
private readonly IAccountService _accountService;
private readonly IGrainFactory _grainFactory;
private readonly IBotService _botService;
private readonly IKaigenService _kaigenService;
private readonly IEvmManager _evmManager;
public StartCopyTradingCommandHandler(
IAccountService accountService, IGrainFactory grainFactory, IBotService botService, IKaigenService kaigenService, IEvmManager evmManager)
{
_accountService = accountService;
_grainFactory = grainFactory;
_botService = botService;
_kaigenService = kaigenService;
_evmManager = evmManager;
}
public async Task<string> Handle(StartCopyTradingCommand request, CancellationToken cancellationToken)
{
// Validate the trading balance
if (request.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
throw new ArgumentException(
$"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
// Get the master bot configuration
var masterBot = await _botService.GetBotByIdentifier(request.MasterBotIdentifier);
if (masterBot == null)
{
throw new ArgumentException($"Master bot with identifier {request.MasterBotIdentifier} not found");
}
// Special validation for Kudai strategy - check staking requirements
if (string.Equals(request.MasterBotIdentifier.ToString(), "Kudai", StringComparison.OrdinalIgnoreCase))
{
await ValidateKudaiStakingRequirements(request.User);
}else {
// Verify the user owns the keys of the master strategy
var ownedKeys = await _kaigenService.GetOwnedKeysAsync(request.User);
var hasMasterStrategyKey = ownedKeys.Items.Any(key =>
string.Equals(key.AgentName, request.MasterBotIdentifier.ToString(), StringComparison.OrdinalIgnoreCase) &&
key.Owned >= 1);
if (!hasMasterStrategyKey)
{
throw new UnauthorizedAccessException(
$"You don't own the keys for the master strategy '{request.MasterBotIdentifier}'. " +
"You must own at least 1 key for this strategy to copy trade from it.");
}
}
// Get the master bot configuration
var masterConfig = await _botService.GetBotConfig(request.MasterBotIdentifier);
if (masterConfig == null)
{
throw new InvalidOperationException($"Could not retrieve configuration for master bot {request.MasterBotIdentifier}");
}
// Check if user already has a bot on this ticker (same as master bot)
var hasExistingBotOnTicker = await _botService.HasUserBotOnTickerAsync(request.User.Id, masterConfig.Ticker);
if (hasExistingBotOnTicker)
{
throw new InvalidOperationException(
$"You already have a strategy running or saved on ticker {masterConfig.Ticker}. " +
"You cannot create multiple strategies on the same ticker.");
}
// Get account information from the requesting user's accounts
var userAccounts = await _accountService.GetAccountsByUserAsync(request.User, true, true);
var firstAccount = userAccounts.FirstOrDefault();
if (firstAccount == null)
{
throw new InvalidOperationException($"User '{request.User.Name}' has no accounts configured.");
}
Account account = firstAccount;
// Check balances for EVM/GMX V2 accounts before starting
if (account.Exchange == TradingExchanges.Evm || account.Exchange == TradingExchanges.GmxV2)
{
var balanceCheckResult = await _botService.CheckAccountBalancesAsync(account);
if (!balanceCheckResult.IsSuccessful)
{
throw new InvalidOperationException(balanceCheckResult.Message);
}
}
Balance usdcBalance = null;
// For other exchanges, keep the original USDC balance check
if (account.Exchange != TradingExchanges.Evm && account.Exchange != TradingExchanges.GmxV2)
{
usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
if (usdcBalance == null ||
usdcBalance.Value < Constants.GMX.Config.MinimumPositionAmount ||
usdcBalance.Value < request.BotTradingBalance)
{
throw new Exception(
$"Account {masterConfig.AccountName} has no USDC balance or not enough balance for copy trading");
}
}
// Enforce allocation limit across all bots using the same account
var availableAllocation = await _botService.GetAvailableAllocationUsdAsync(account, default);
if (request.BotTradingBalance > availableAllocation)
{
throw new InvalidOperationException(
$"Insufficient available allocation. Requested: {request.BotTradingBalance:F2} USDC, " +
$"Balance : {usdcBalance?.Value:F2 ?? 0} Available: {availableAllocation:F2} USDC.");
}
// Create copy trading configuration based on master bot
var copyTradingConfig = new TradingBotConfig
{
// Copy all master configuration
AccountName = account.Name,
MoneyManagement = masterConfig.MoneyManagement,
Ticker = masterConfig.Ticker,
Scenario = masterConfig.Scenario,
ScenarioName = masterConfig.ScenarioName,
Timeframe = masterConfig.Timeframe,
IsForWatchingOnly = masterConfig.IsForWatchingOnly,
BotTradingBalance = request.BotTradingBalance, // Use the provided trading balance
CooldownPeriod = masterConfig.CooldownPeriod,
MaxLossStreak = masterConfig.MaxLossStreak,
MaxPositionTimeHours = masterConfig.MaxPositionTimeHours,
FlipOnlyWhenInProfit = masterConfig.FlipOnlyWhenInProfit,
CloseEarlyWhenProfitable = masterConfig.CloseEarlyWhenProfitable,
UseSynthApi = masterConfig.UseSynthApi,
UseForPositionSizing = masterConfig.UseForPositionSizing,
UseForSignalFiltering = masterConfig.UseForSignalFiltering,
UseForDynamicStopLoss = masterConfig.UseForDynamicStopLoss,
FlipPosition = masterConfig.FlipPosition,
// Set copy trading specific properties
IsForCopyTrading = true,
MasterBotIdentifier = request.MasterBotIdentifier,
// Set computed/default properties
IsForBacktest = false,
Name = masterConfig.Name
};
try
{
var botGrain = _grainFactory.GetGrain<ILiveTradingBotGrain>(Guid.NewGuid());
await botGrain.CreateAsync(copyTradingConfig, request.User);
// Start the copy trading bot immediately
await botGrain.StartAsync();
return $"Copy trading bot started successfully, following master bot '{masterConfig.Name}' with {request.BotTradingBalance:F2} USDC balance";
}
catch (Exception ex)
{
throw new Exception($"Failed to start copy trading bot: {ex.Message}, {ex.StackTrace}");
}
}
private async Task ValidateKudaiStakingRequirements(User user)
{
// Use the user's wallet address directly
if (string.IsNullOrEmpty(user.OwnerWalletAddress))
{
throw new InvalidOperationException(
"To copy trade the Kudai strategy, you must have a wallet address configured in your profile.");
}
// Check KUDAI staked balance
var kudaiStakedBalance = await _evmManager.GetKudaiStakedBalance(user.OwnerWalletAddress);
// Check GBC staked NFT count
var gbcStakedCount = await _evmManager.GetGbcStakedCount(user.OwnerWalletAddress);
// Requirements: 100 million KUDAI OR 10 GBC NFTs
const decimal requiredKudaiAmount = 100_000_000m; // 100 million
const int requiredGbcCount = 10;
if (kudaiStakedBalance < requiredKudaiAmount && gbcStakedCount < requiredGbcCount)
{
throw new UnauthorizedAccessException(
$"To copy trade the Kudai strategy, you must have either:\n" +
$"• {requiredKudaiAmount:N0} KUDAI tokens staked (currently have {kudaiStakedBalance:N0})\n" +
$"• OR {requiredGbcCount} GBC NFTs staked (currently have {gbcStakedCount})\n\n" +
$"Please stake the required amount in the Kudai staking contract to copy this strategy.");
}
}
}
}