Enhance trading bot functionality and LLM system message clarity
- Added BotTradingBalance property to UserStrategyDetailsViewModel for better tracking of bot allocations. - Updated DataController to include BotTradingBalance in the response model. - Improved LlmController by refining the system message to ensure LLM understands its response capabilities and tool usage. - Introduced new MCP tools for running and analyzing bundle backtests, enhancing backtesting capabilities for users. - Implemented security measures in BotTools to ensure users can only access their own bots, improving data privacy.
This commit is contained in:
@@ -632,7 +632,8 @@ public class DataController : ControllerBase
|
||||
Identifier = strategy.Identifier,
|
||||
WalletBalances = walletBalances,
|
||||
Ticker = strategy.Ticker,
|
||||
MasterAgentName = strategy.MasterBotUser?.AgentName
|
||||
MasterAgentName = strategy.MasterBotUser?.AgentName,
|
||||
BotTradingBalance = strategy.BotTradingBalance
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -57,22 +57,27 @@ public class LlmController : BaseController
|
||||
var availableTools = await _mcpService.GetAvailableToolsAsync();
|
||||
request.Tools = availableTools.ToList();
|
||||
|
||||
// Add system message to clarify that tools are optional and the LLM can respond directly
|
||||
// Check if a system message already exists
|
||||
var hasSystemMessage = request.Messages.Any(m => m.Role == "system");
|
||||
if (!hasSystemMessage)
|
||||
// Add or prepend system message to ensure LLM knows it can respond directly
|
||||
// Remove any existing system messages first to ensure our directive is clear
|
||||
var existingSystemMessages = request.Messages.Where(m => m.Role == "system").ToList();
|
||||
foreach (var msg in existingSystemMessages)
|
||||
{
|
||||
request.Messages.Remove(msg);
|
||||
}
|
||||
|
||||
// Add explicit system message at the beginning
|
||||
var systemMessage = new LlmMessage
|
||||
{
|
||||
Role = "system",
|
||||
Content = "You are a helpful AI assistant with expertise in quantitative finance, algorithmic trading, and financial mathematics. " +
|
||||
"You can answer questions directly using your knowledge. " +
|
||||
"Tools are available for specific operations (backtesting, agent management, market data retrieval, etc.) but are optional. " +
|
||||
"Use tools only when they are needed for the specific task. " +
|
||||
"For general questions, explanations, calculations, or discussions, respond directly without using tools."
|
||||
Content = "You are an expert AI assistant specializing in quantitative finance, algorithmic trading, and financial mathematics. " +
|
||||
"You have full knowledge and can answer ANY question directly using your training data and expertise. " +
|
||||
"IMPORTANT: You MUST answer general questions, explanations, calculations, and discussions directly without using tools. " +
|
||||
"Tools are ONLY for specific system operations like backtesting, agent management, or retrieving real-time market data. " +
|
||||
"For questions about financial concepts, mathematical formulas (like Black-Scholes), trading strategies, or any theoretical knowledge, " +
|
||||
"you MUST provide a direct answer using your knowledge. Do NOT refuse to answer or claim you can only use tools. " +
|
||||
"Only use tools when the user explicitly needs to perform a system operation (e.g., 'run a backtest', 'get market data', 'manage agents')."
|
||||
};
|
||||
request.Messages.Insert(0, systemMessage);
|
||||
}
|
||||
|
||||
// Send chat request to LLM
|
||||
var response = await _llmService.ChatAsync(user, request);
|
||||
|
||||
@@ -112,5 +112,11 @@ namespace Managing.Api.Models.Responses
|
||||
/// The agent name of the master bot's owner (for copy trading bots)
|
||||
/// </summary>
|
||||
public string MasterAgentName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The trading balance allocated to this bot
|
||||
/// </summary>
|
||||
[Required]
|
||||
public decimal BotTradingBalance { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ public class McpService : IMcpService
|
||||
"get_bundle_backtest_by_id" => await _backtestMcpTools.ExecuteGetBundleBacktestById(user, parameters),
|
||||
"delete_bundle_backtest" => await _backtestMcpTools.ExecuteDeleteBundleBacktest(user, parameters),
|
||||
"run_backtest" => await _backtestMcpTools.ExecuteRunBacktest(user, parameters),
|
||||
"run_bundle_backtest" => await _backtestMcpTools.ExecuteRunBundleBacktest(user, parameters),
|
||||
"analyze_bundle_backtest" => await _backtestMcpTools.ExecuteAnalyzeBundleBacktest(user, parameters),
|
||||
|
||||
// Data tools
|
||||
"get_tickers" => await _dataMcpTools.ExecuteGetTickers(user, parameters),
|
||||
|
||||
@@ -359,6 +359,95 @@ public class BacktestMcpTools : BaseMcpTool
|
||||
DefaultValue = true
|
||||
}
|
||||
}
|
||||
},
|
||||
new McpToolDefinition
|
||||
{
|
||||
Name = "run_bundle_backtest",
|
||||
Description = "Runs a new bundle backtest with multiple tickers and money management variants. Creates a bundle backtest request that will generate multiple backtest jobs.",
|
||||
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||
{
|
||||
["name"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Display name for the bundle backtest",
|
||||
Required = true
|
||||
},
|
||||
["timeframe"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "The timeframe for the backtest (e.g., 'FiveMinutes', 'FifteenMinutes', 'OneHour', 'FourHours', 'OneDay')",
|
||||
Required = true
|
||||
},
|
||||
["startDate"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Start date for the backtest in ISO 8601 format (e.g., '2024-01-01T00:00:00Z')",
|
||||
Required = true
|
||||
},
|
||||
["endDate"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "End date for the backtest in ISO 8601 format (e.g., '2024-12-31T23:59:59Z')",
|
||||
Required = true
|
||||
},
|
||||
["balance"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "number",
|
||||
Description = "Starting balance for the backtest (must be greater than zero)",
|
||||
Required = true
|
||||
},
|
||||
["tickers"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Comma-separated list of tickers to test (e.g., 'BTC,ETH,SOL')",
|
||||
Required = true
|
||||
},
|
||||
["moneyManagementVariants"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "JSON array of money management variants. Each variant should have a 'moneyManagement' object with 'stopLoss', 'takeProfit', 'leverage', 'name', and 'timeframe' properties. Example: [{\"moneyManagement\":{\"stopLoss\":2.5,\"takeProfit\":5.0,\"leverage\":1,\"name\":\"Conservative\",\"timeframe\":\"OneHour\"}}]",
|
||||
Required = true
|
||||
},
|
||||
["scenarioName"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "The name of the trading scenario/strategy to use (alternative to scenarioJson)",
|
||||
Required = false
|
||||
},
|
||||
["scenarioJson"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "JSON object representing the scenario configuration with 'name', 'indicators' array, and optional 'lookbackPeriod' (alternative to scenarioName)",
|
||||
Required = false
|
||||
},
|
||||
["tradingType"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Trading type (Spot, Futures, BacktestSpot, BacktestFutures, Paper). Defaults to BacktestSpot",
|
||||
Required = false
|
||||
},
|
||||
["saveAsTemplate"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "boolean",
|
||||
Description = "Whether to save only as a template without running (defaults to false)",
|
||||
Required = false,
|
||||
DefaultValue = false
|
||||
}
|
||||
}
|
||||
},
|
||||
new McpToolDefinition
|
||||
{
|
||||
Name = "analyze_bundle_backtest",
|
||||
Description = "Analyzes a bundle backtest by retrieving all associated backtests and aggregating statistics including averages, best/worst performers, and breakdown by ticker.",
|
||||
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||
{
|
||||
["id"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "The ID of the bundle backtest request to analyze",
|
||||
Required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -543,4 +632,82 @@ public class BacktestMcpTools : BaseMcpTool
|
||||
name,
|
||||
save);
|
||||
}
|
||||
|
||||
public async Task<object> ExecuteRunBundleBacktest(User user, Dictionary<string, object>? parameters)
|
||||
{
|
||||
var name = GetParameterValue<string>(parameters, "name", string.Empty);
|
||||
var timeframe = GetParameterValue<string>(parameters, "timeframe", string.Empty);
|
||||
var startDateString = GetParameterValue<string>(parameters, "startDate", string.Empty);
|
||||
var endDateString = GetParameterValue<string>(parameters, "endDate", string.Empty);
|
||||
var balance = GetParameterValue<decimal>(parameters, "balance", 0);
|
||||
var tickers = GetParameterValue<string>(parameters, "tickers", string.Empty);
|
||||
var moneyManagementVariants = GetParameterValue<string>(parameters, "moneyManagementVariants", string.Empty);
|
||||
var scenarioName = GetParameterValue<string?>(parameters, "scenarioName", null);
|
||||
var scenarioJson = GetParameterValue<string?>(parameters, "scenarioJson", null);
|
||||
var tradingTypeString = GetParameterValue<string?>(parameters, "tradingType", null);
|
||||
var saveAsTemplate = GetParameterValue<bool>(parameters, "saveAsTemplate", false);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Bundle name is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(timeframe))
|
||||
{
|
||||
throw new ArgumentException("Timeframe is required");
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(startDateString, out var startDate))
|
||||
{
|
||||
throw new ArgumentException("Valid start date is required");
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(endDateString, out var endDate))
|
||||
{
|
||||
throw new ArgumentException("Valid end date is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tickers))
|
||||
{
|
||||
throw new ArgumentException("At least one ticker is required");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(moneyManagementVariants))
|
||||
{
|
||||
throw new ArgumentException("At least one money management variant is required");
|
||||
}
|
||||
|
||||
// Parse tradingType enum
|
||||
TradingType? tradingType = null;
|
||||
if (!string.IsNullOrWhiteSpace(tradingTypeString) &&
|
||||
Enum.TryParse<TradingType>(tradingTypeString, true, out var parsedTradingType))
|
||||
{
|
||||
tradingType = parsedTradingType;
|
||||
}
|
||||
|
||||
return await _backtestTools.RunBundleBacktest(
|
||||
user,
|
||||
name,
|
||||
timeframe,
|
||||
startDate,
|
||||
endDate,
|
||||
balance,
|
||||
tickers,
|
||||
moneyManagementVariants,
|
||||
scenarioName,
|
||||
scenarioJson,
|
||||
tradingType,
|
||||
saveAsTemplate);
|
||||
}
|
||||
|
||||
public async Task<object> ExecuteAnalyzeBundleBacktest(User user, Dictionary<string, object>? parameters)
|
||||
{
|
||||
var idString = GetParameterValue<string>(parameters, "id", string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(idString) || !Guid.TryParse(idString, out var id))
|
||||
{
|
||||
throw new ArgumentException("Valid bundle backtest ID is required");
|
||||
}
|
||||
|
||||
return await _backtestTools.AnalyzeBundleBacktest(user, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Managing.Domain.Bots;
|
||||
using Managing.Domain.MoneyManagements;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Mcp.Tools;
|
||||
@@ -288,7 +289,8 @@ public class BacktestTools
|
||||
var filter = new BundleBacktestRequestsFilter
|
||||
{
|
||||
Status = status,
|
||||
NameContains = name
|
||||
NameContains = name,
|
||||
UserId = user.Id // Ensure we only get bundle backtests for the current user
|
||||
};
|
||||
|
||||
var (bundleRequests, totalCount) = await _backtester.GetBundleBacktestRequestsPaginatedAsync(
|
||||
@@ -497,4 +499,328 @@ public class BacktestTools
|
||||
throw new InvalidOperationException($"Failed to run backtest: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a new bundle backtest with the specified configuration
|
||||
/// </summary>
|
||||
public async Task<object> RunBundleBacktest(
|
||||
User user,
|
||||
string name,
|
||||
string timeframe,
|
||||
DateTime startDate,
|
||||
DateTime endDate,
|
||||
decimal balance,
|
||||
string tickers,
|
||||
string moneyManagementVariants,
|
||||
string? scenarioName = null,
|
||||
string? scenarioJson = null,
|
||||
TradingType? tradingType = null,
|
||||
bool saveAsTemplate = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse timeframe enum
|
||||
if (!Enum.TryParse<Timeframe>(timeframe, true, out var parsedTimeframe))
|
||||
{
|
||||
throw new ArgumentException($"Invalid timeframe: {timeframe}");
|
||||
}
|
||||
|
||||
// Validate dates
|
||||
if (startDate >= endDate)
|
||||
{
|
||||
throw new ArgumentException("Start date must be before end date");
|
||||
}
|
||||
|
||||
if (balance <= 0)
|
||||
{
|
||||
throw new ArgumentException("Balance must be greater than zero");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentException("Bundle name is required");
|
||||
}
|
||||
|
||||
// Parse tickers
|
||||
var tickerList = string.IsNullOrWhiteSpace(tickers)
|
||||
? new List<Ticker>()
|
||||
: tickers.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => Enum.TryParse<Ticker>(t, true, out var ticker) ? ticker : throw new ArgumentException($"Invalid ticker: {t}"))
|
||||
.ToList();
|
||||
|
||||
if (tickerList.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one ticker is required");
|
||||
}
|
||||
|
||||
// Parse money management variants (JSON array)
|
||||
List<MoneyManagementVariant> mmVariants;
|
||||
if (string.IsNullOrWhiteSpace(moneyManagementVariants))
|
||||
{
|
||||
throw new ArgumentException("At least one money management variant is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mmVariants = JsonSerializer.Deserialize<List<MoneyManagementVariant>>(moneyManagementVariants);
|
||||
if (mmVariants == null || mmVariants.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("At least one money management variant is required");
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw new ArgumentException($"Invalid money management variants JSON: {ex.Message}");
|
||||
}
|
||||
|
||||
// Get user's first account
|
||||
var accounts = await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, getBalance: false);
|
||||
var firstAccount = accounts.FirstOrDefault();
|
||||
|
||||
if (firstAccount == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No accounts found for user {user.AgentName}");
|
||||
}
|
||||
|
||||
// Build universal config
|
||||
var universalConfig = new BundleBacktestUniversalConfig
|
||||
{
|
||||
Timeframe = parsedTimeframe,
|
||||
IsForWatchingOnly = false,
|
||||
BotTradingBalance = balance,
|
||||
BotName = name,
|
||||
TradingType = tradingType ?? TradingType.BacktestSpot,
|
||||
FlipPosition = false,
|
||||
CooldownPeriod = 0,
|
||||
MaxLossStreak = 0,
|
||||
ScenarioName = scenarioName,
|
||||
Save = true
|
||||
};
|
||||
|
||||
// Parse scenario if provided as JSON
|
||||
if (!string.IsNullOrWhiteSpace(scenarioJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
var scenario = JsonSerializer.Deserialize<ScenarioRequest>(scenarioJson);
|
||||
if (scenario != null)
|
||||
{
|
||||
universalConfig.Scenario = scenario;
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to parse scenario JSON, using scenario name instead");
|
||||
}
|
||||
}
|
||||
|
||||
// Create date time ranges (single range for now, can be extended)
|
||||
var dateTimeRanges = new List<DateTimeRange>
|
||||
{
|
||||
new DateTimeRange
|
||||
{
|
||||
StartDate = startDate,
|
||||
EndDate = endDate
|
||||
}
|
||||
};
|
||||
|
||||
// Create the bundle backtest request
|
||||
var bundleRequest = new BundleBacktestRequest
|
||||
{
|
||||
User = user,
|
||||
UniversalConfigJson = JsonSerializer.Serialize(universalConfig),
|
||||
DateTimeRangesJson = JsonSerializer.Serialize(dateTimeRanges),
|
||||
MoneyManagementVariantsJson = JsonSerializer.Serialize(mmVariants),
|
||||
TickerVariantsJson = JsonSerializer.Serialize(tickerList),
|
||||
TotalBacktests = dateTimeRanges.Count * mmVariants.Count * tickerList.Count,
|
||||
CompletedBacktests = 0,
|
||||
FailedBacktests = 0,
|
||||
Status = saveAsTemplate
|
||||
? BundleBacktestRequestStatus.Saved
|
||||
: BundleBacktestRequestStatus.Pending,
|
||||
Name = name
|
||||
};
|
||||
|
||||
// Save bundle request
|
||||
await _backtester.SaveBundleBacktestRequestAsync(user, bundleRequest);
|
||||
|
||||
// If not saving as template, create jobs
|
||||
if (!saveAsTemplate)
|
||||
{
|
||||
await _backtester.CreateJobsForBundleRequestAsync(bundleRequest);
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
Success = true,
|
||||
BundleRequestId = bundleRequest.RequestId,
|
||||
Message = saveAsTemplate
|
||||
? $"Bundle backtest template saved successfully. Bundle ID: {bundleRequest.RequestId}"
|
||||
: $"Bundle backtest job created successfully. Bundle ID: {bundleRequest.RequestId}",
|
||||
TotalBacktests = bundleRequest.TotalBacktests,
|
||||
Config = new
|
||||
{
|
||||
Name = name,
|
||||
Timeframe = parsedTimeframe.ToString(),
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
Balance = balance,
|
||||
TickerCount = tickerList.Count,
|
||||
MoneyManagementVariantCount = mmVariants.Count,
|
||||
ScenarioName = scenarioName,
|
||||
TradingType = universalConfig.TradingType.ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error running bundle backtest for user {UserId}", user.Id);
|
||||
throw new InvalidOperationException($"Failed to run bundle backtest: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes a bundle backtest by retrieving all associated backtests and aggregating statistics
|
||||
/// </summary>
|
||||
public async Task<object> AnalyzeBundleBacktest(User user, Guid bundleRequestId)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the bundle request
|
||||
var bundleRequest = await _backtester.GetBundleBacktestRequestByIdForUserAsync(user, bundleRequestId);
|
||||
if (bundleRequest == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Bundle backtest request with ID '{bundleRequestId}' not found");
|
||||
}
|
||||
|
||||
// Get all backtests for this bundle
|
||||
var backtests = await _backtester.GetBacktestsByRequestIdAsync(bundleRequestId);
|
||||
var backtestList = backtests.ToList();
|
||||
|
||||
if (backtestList.Count == 0)
|
||||
{
|
||||
return new
|
||||
{
|
||||
BundleRequestId = bundleRequestId,
|
||||
BundleName = bundleRequest.Name,
|
||||
Status = bundleRequest.Status.ToString(),
|
||||
TotalBacktests = bundleRequest.TotalBacktests,
|
||||
CompletedBacktests = bundleRequest.CompletedBacktests,
|
||||
FailedBacktests = bundleRequest.FailedBacktests,
|
||||
Message = "No backtests found for this bundle yet",
|
||||
Statistics = (object?)null
|
||||
};
|
||||
}
|
||||
|
||||
// Aggregate statistics
|
||||
// Backtest has non-nullable Score, WinRate, GrowthPercentage
|
||||
var completedBacktests = backtestList.ToList();
|
||||
var totalCount = completedBacktests.Count;
|
||||
|
||||
if (totalCount == 0)
|
||||
{
|
||||
return new
|
||||
{
|
||||
BundleRequestId = bundleRequestId,
|
||||
BundleName = bundleRequest.Name,
|
||||
Status = bundleRequest.Status.ToString(),
|
||||
TotalBacktests = bundleRequest.TotalBacktests,
|
||||
CompletedBacktests = bundleRequest.CompletedBacktests,
|
||||
FailedBacktests = bundleRequest.FailedBacktests,
|
||||
Message = "No completed backtests found yet",
|
||||
Statistics = (object?)null
|
||||
};
|
||||
}
|
||||
|
||||
var avgScore = completedBacktests.Average(b => b.Score);
|
||||
var avgWinRate = completedBacktests.Average(b => b.WinRate);
|
||||
var avgGrowthPercentage = completedBacktests.Average(b => (double)b.GrowthPercentage);
|
||||
// PerformanceMetrics from Exilion.TradingAtomics
|
||||
// Both SharpeRatio and MaxDrawdown appear to be non-nullable
|
||||
var backtestsWithStats = completedBacktests.Where(b => b.Statistics != null).ToList();
|
||||
var statsCount = backtestsWithStats.Count;
|
||||
|
||||
double avgSharpeRatio = 0.0;
|
||||
double avgMaxDrawdown = 0.0;
|
||||
|
||||
if (statsCount > 0)
|
||||
{
|
||||
// Both are non-nullable, so use directly
|
||||
avgSharpeRatio = backtestsWithStats.Average(b => (double)b.Statistics!.SharpeRatio);
|
||||
avgMaxDrawdown = backtestsWithStats.Average(b => (double)b.Statistics!.MaxDrawdown);
|
||||
}
|
||||
var totalFinalPnl = completedBacktests.Sum(b => b.FinalPnl);
|
||||
var totalNetPnl = completedBacktests.Sum(b => b.NetPnl);
|
||||
var totalFees = completedBacktests.Sum(b => b.Fees);
|
||||
var totalPositions = completedBacktests.Sum(b => b.PositionCount);
|
||||
|
||||
var bestBacktest = completedBacktests.OrderByDescending(b => b.Score).FirstOrDefault();
|
||||
var worstBacktest = completedBacktests.OrderBy(b => b.Score).FirstOrDefault();
|
||||
|
||||
// Group by ticker
|
||||
var byTicker = completedBacktests
|
||||
.GroupBy(b => b.Config?.Ticker.ToString() ?? "Unknown")
|
||||
.Select(g => new
|
||||
{
|
||||
Ticker = g.Key,
|
||||
Count = g.Count(),
|
||||
AvgScore = g.Average(b => b.Score),
|
||||
AvgWinRate = g.Average(b => b.WinRate),
|
||||
AvgGrowth = g.Average(b => (double)b.GrowthPercentage),
|
||||
TotalPnl = g.Sum(b => b.FinalPnl)
|
||||
})
|
||||
.OrderByDescending(x => x.AvgScore)
|
||||
.ToList();
|
||||
|
||||
return new
|
||||
{
|
||||
BundleRequestId = bundleRequestId,
|
||||
BundleName = bundleRequest.Name,
|
||||
Status = bundleRequest.Status.ToString(),
|
||||
TotalBacktests = bundleRequest.TotalBacktests,
|
||||
CompletedBacktests = bundleRequest.CompletedBacktests,
|
||||
FailedBacktests = bundleRequest.FailedBacktests,
|
||||
ProgressPercentage = bundleRequest.ProgressPercentage,
|
||||
CreatedAt = bundleRequest.CreatedAt,
|
||||
CompletedAt = bundleRequest.CompletedAt,
|
||||
Statistics = new
|
||||
{
|
||||
TotalCompleted = totalCount,
|
||||
AverageScore = Math.Round(avgScore, 2),
|
||||
AverageWinRate = Math.Round(avgWinRate, 2),
|
||||
AverageGrowthPercentage = Math.Round(avgGrowthPercentage, 2),
|
||||
AverageSharpeRatio = Math.Round(avgSharpeRatio, 3),
|
||||
AverageMaxDrawdown = Math.Round(avgMaxDrawdown, 2),
|
||||
TotalFinalPnl = Math.Round(totalFinalPnl, 2),
|
||||
TotalNetPnl = Math.Round(totalNetPnl, 2),
|
||||
TotalFees = Math.Round(totalFees, 2),
|
||||
TotalPositions = totalPositions,
|
||||
BestBacktest = bestBacktest != null ? new
|
||||
{
|
||||
bestBacktest.Id,
|
||||
Score = bestBacktest.Score,
|
||||
WinRate = bestBacktest.WinRate,
|
||||
GrowthPercentage = (double)bestBacktest.GrowthPercentage,
|
||||
FinalPnl = bestBacktest.FinalPnl,
|
||||
Ticker = bestBacktest.Config?.Ticker.ToString()
|
||||
} : null,
|
||||
WorstBacktest = worstBacktest != null ? new
|
||||
{
|
||||
worstBacktest.Id,
|
||||
Score = worstBacktest.Score,
|
||||
WinRate = worstBacktest.WinRate,
|
||||
GrowthPercentage = (double)worstBacktest.GrowthPercentage,
|
||||
FinalPnl = worstBacktest.FinalPnl,
|
||||
Ticker = worstBacktest.Config?.Ticker.ToString()
|
||||
} : null,
|
||||
ByTicker = byTicker
|
||||
}
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error analyzing bundle backtest {BundleId} for user {UserId}", bundleRequestId, user.Id);
|
||||
throw new InvalidOperationException($"Failed to analyze bundle backtest: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,13 +43,23 @@ public class BotTools
|
||||
if (pageNumber < 1) pageNumber = 1;
|
||||
if (pageSize < 1 || pageSize > 100) pageSize = 10;
|
||||
|
||||
// Ensure we only get bots for the current user by filtering by user's agentName
|
||||
// If agentName parameter is provided, verify it matches the user's agentName
|
||||
if (!string.IsNullOrWhiteSpace(agentName) && !agentName.Equals(user.AgentName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new UnauthorizedAccessException("You can only access bots for your own agent");
|
||||
}
|
||||
|
||||
// Use user's agentName to filter bots
|
||||
var userAgentName = user.AgentName;
|
||||
|
||||
var (bots, totalCount) = await _botRepository.GetBotsPaginatedAsync(
|
||||
pageNumber,
|
||||
pageSize,
|
||||
status,
|
||||
name,
|
||||
ticker,
|
||||
agentName,
|
||||
userAgentName, // Filter by user's agentName to ensure security
|
||||
minBalance,
|
||||
maxBalance,
|
||||
sortBy,
|
||||
|
||||
Reference in New Issue
Block a user