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:
@@ -993,6 +993,7 @@ public class DataController : ControllerBase
|
||||
StartupTime = item.StartupTime,
|
||||
Name = item.Name,
|
||||
Ticker = item.Ticker,
|
||||
MasterAgentName = item.MasterBotUser?.AgentName,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
1732
src/Managing.Infrastructure.Database/Migrations/20251119165943_AddMasterBotUserIdToBots.Designer.cs
generated
Normal file
1732
src/Managing.Infrastructure.Database/Migrations/20251119165943_AddMasterBotUserIdToBots.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user