Add MasterBotUserId and MasterAgentName for copy trading support

- Introduced MasterBotUserId and MasterAgentName properties to facilitate copy trading functionality.
- Updated relevant models, controllers, and database entities to accommodate these new properties.
- Enhanced validation logic in StartCopyTradingCommandHandler to ensure proper ownership checks for master strategies.
This commit is contained in:
2025-11-20 00:33:31 +07:00
parent 97103fbfe8
commit ff2df2d9ac
13 changed files with 1852 additions and 20 deletions

View File

@@ -993,6 +993,7 @@ public class DataController : ControllerBase
StartupTime = item.StartupTime,
Name = item.Name,
Ticker = item.Ticker,
MasterAgentName = item.MasterBotUser?.AgentName,
});
}

View File

@@ -81,5 +81,10 @@ namespace Managing.Api.Models.Responses
/// </summary>
[Required]
public Ticker Ticker { get; set; }
/// <summary>
/// The agent name of the master bot's owner (for copy trading bots)
/// </summary>
public string MasterAgentName { get; set; }
}
}

View File

@@ -930,7 +930,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
Pnl = 0,
Roi = 0,
Volume = 0,
Fees = 0
Fees = 0,
MasterBotUserId = _state.State.Config.MasterBotUserId
};
}
else
@@ -993,7 +994,8 @@ public class LiveTradingBotGrain : Grain, ILiveTradingBotGrain, IRemindable
Volume = agentMetrics.TotalVolume,
Fees = agentMetrics.TotalFees,
LongPositionCount = longPositionCount,
ShortPositionCount = shortPositionCount
ShortPositionCount = shortPositionCount,
MasterBotUserId = _state.State.Config.MasterBotUserId
};
}

View File

@@ -8,6 +8,7 @@ using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using MediatR;
using System;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
@@ -47,24 +48,31 @@ namespace Managing.Application.ManageBot
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, masterBot.User.AgentName, StringComparison.OrdinalIgnoreCase) &&
key.Owned >= 1);
// Check if copy trading validation should be bypassed (for testing)
var enableValidation = Environment.GetEnvironmentVariable("ENABLE_COPY_TRADING_VALIDATION")?
.Equals("true", StringComparison.OrdinalIgnoreCase) == true;
if (!hasMasterStrategyKey)
if (enableValidation)
{
// Special validation for Kudai strategy - check staking requirements
if (string.Equals(request.MasterBotIdentifier.ToString(), "Kudai", StringComparison.OrdinalIgnoreCase))
{
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.");
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.");
}
}
}
@@ -156,6 +164,7 @@ namespace Managing.Application.ManageBot
// Set copy trading specific properties
IsForCopyTrading = true,
MasterBotIdentifier = request.MasterBotIdentifier,
MasterBotUserId = masterBot.User.Id,
// Set computed/default properties
IsForBacktest = false,

View File

@@ -28,6 +28,16 @@ namespace Managing.Domain.Bots
public int LongPositionCount { get; set; }
public int ShortPositionCount { get; set; }
/// <summary>
/// The user ID of the master bot's owner when this bot is for copy trading
/// </summary>
public int? MasterBotUserId { get; set; }
/// <summary>
/// The user object of the master bot's owner when this bot is for copy trading
/// </summary>
public User MasterBotUser { get; set; }
/// <summary>
/// Gets the total runtime in seconds, including the current session if the bot is running
/// </summary>

View File

@@ -115,4 +115,10 @@ public class TradingBotConfig
/// </summary>
[Id(22)]
public Guid? MasterBotIdentifier { get; set; }
/// <summary>
/// The user ID of the master bot's owner when IsForCopyTrading is true
/// </summary>
[Id(23)]
public int? MasterBotUserId { get; set; }
}

View File

@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class AddMasterBotUserIdToBots : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "MasterBotUserId",
table: "Bots",
type: "integer",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "MasterBotUserId",
table: "Bots");
}
}
}

View File

@@ -318,6 +318,9 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Property<int>("LongPositionCount")
.HasColumnType("integer");
b.Property<int?>("MasterBotUserId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)

View File

@@ -36,4 +36,15 @@ public class BotEntity
public decimal Fees { get; set; }
public int LongPositionCount { get; set; }
public int ShortPositionCount { get; set; }
/// <summary>
/// The user ID of the master bot's owner when this bot is for copy trading
/// </summary>
[ForeignKey("MasterBotUser")]
public int? MasterBotUserId { get; set; }
/// <summary>
/// Navigation property for the master bot's owner when this bot is for copy trading
/// </summary>
public virtual UserEntity? MasterBotUser { get; set; }
}

View File

@@ -548,6 +548,12 @@ public class ManagingDbContext : DbContext
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Configure relationship with MasterBotUser
entity.HasOne(e => e.MasterBotUser)
.WithMany()
.HasForeignKey(e => e.MasterBotUserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure MoneyManagement entity

View File

@@ -79,6 +79,7 @@ public class PostgreSqlBotRepository : IBotRepository
existingEntity.LastStartTime = bot.LastStartTime;
existingEntity.LastStopTime = bot.LastStopTime;
existingEntity.AccumulatedRunTimeSeconds = bot.AccumulatedRunTimeSeconds;
existingEntity.MasterBotUserId = bot.MasterBotUserId;
await _context.SaveChangesAsync().ConfigureAwait(false);
}
@@ -114,10 +115,26 @@ public class PostgreSqlBotRepository : IBotRepository
var entities = await _context.Bots
.AsNoTracking()
.Include(m => m.User)
.Include(m => m.MasterBotUser)
.Where(b => b.UserId == id)
.ToListAsync()
.ConfigureAwait(false);
return PostgreSqlMappers.Map(entities);
// Map entities to domain objects
var bots = PostgreSqlMappers.Map(entities).ToList();
// Attach master bot users to domain objects
foreach (var entity in entities.Where(e => e.MasterBotUser != null))
{
var bot = bots.FirstOrDefault(b => b.MasterBotUserId == entity.MasterBotUserId);
if (bot != null)
{
// Convert UserEntity to User domain object
bot.MasterBotUser = PostgreSqlMappers.Map(entity.MasterBotUser);
}
}
return bots;
}
public async Task<IEnumerable<Bot>> GetBotsByStatusAsync(BotStatus status)

View File

@@ -753,7 +753,8 @@ public static class PostgreSqlMappers
Volume = entity.Volume,
Fees = entity.Fees,
LongPositionCount = entity.LongPositionCount,
ShortPositionCount = entity.ShortPositionCount
ShortPositionCount = entity.ShortPositionCount,
MasterBotUserId = entity.MasterBotUserId
};
return bot;
@@ -784,6 +785,7 @@ public static class PostgreSqlMappers
Fees = bot.Fees,
LongPositionCount = bot.LongPositionCount,
ShortPositionCount = bot.ShortPositionCount,
MasterBotUserId = bot.MasterBotUserId,
UpdatedAt = DateTime.UtcNow
};
}