Add copy trading functionality with StartCopyTrading endpoint and related models. Implemented position copying from master bot and subscription to copy trading stream in LiveTradingBotGrain. Updated TradingBotConfig to support copy trading parameters.

This commit is contained in:
2025-11-16 14:54:17 +07:00
parent 428e36d744
commit 1e15d5f23b
10 changed files with 711 additions and 173 deletions

View File

@@ -0,0 +1,147 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
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;
public StartCopyTradingCommandHandler(
IAccountService accountService, IGrainFactory grainFactory, IBotService botService)
{
_accountService = accountService;
_grainFactory = grainFactory;
_botService = botService;
}
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");
}
// Verify the user owns the master bot keys
// if (masterBot.User?.Name != request.User.Name)
// {
// throw new UnauthorizedAccessException("You don't have permission to copy trades from this bot");
// }
// 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}");
}
// 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}");
}
}
}
}