219 lines
10 KiB
C#
219 lines
10 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");
|
|
}
|
|
|
|
// Check if copy trading validation should be bypassed (for testing)
|
|
var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")?
|
|
.Equals("true", StringComparison.OrdinalIgnoreCase) ?? true;
|
|
|
|
if (enableValidation)
|
|
{
|
|
// 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, masterBot.User.AgentName, 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,
|
|
MasterBotUserId = masterBot.User.Id,
|
|
|
|
// 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.");
|
|
}
|
|
}
|
|
}
|
|
} |