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,
|
Identifier = strategy.Identifier,
|
||||||
WalletBalances = walletBalances,
|
WalletBalances = walletBalances,
|
||||||
Ticker = strategy.Ticker,
|
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();
|
var availableTools = await _mcpService.GetAvailableToolsAsync();
|
||||||
request.Tools = availableTools.ToList();
|
request.Tools = availableTools.ToList();
|
||||||
|
|
||||||
// Add system message to clarify that tools are optional and the LLM can respond directly
|
// Add or prepend system message to ensure LLM knows it can respond directly
|
||||||
// Check if a system message already exists
|
// Remove any existing system messages first to ensure our directive is clear
|
||||||
var hasSystemMessage = request.Messages.Any(m => m.Role == "system");
|
var existingSystemMessages = request.Messages.Where(m => m.Role == "system").ToList();
|
||||||
if (!hasSystemMessage)
|
foreach (var msg in existingSystemMessages)
|
||||||
{
|
{
|
||||||
|
request.Messages.Remove(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add explicit system message at the beginning
|
||||||
var systemMessage = new LlmMessage
|
var systemMessage = new LlmMessage
|
||||||
{
|
{
|
||||||
Role = "system",
|
Role = "system",
|
||||||
Content = "You are a helpful AI assistant with expertise in quantitative finance, algorithmic trading, and financial mathematics. " +
|
Content = "You are an expert AI assistant specializing in quantitative finance, algorithmic trading, and financial mathematics. " +
|
||||||
"You can answer questions directly using your knowledge. " +
|
"You have full knowledge and can answer ANY question directly using your training data and expertise. " +
|
||||||
"Tools are available for specific operations (backtesting, agent management, market data retrieval, etc.) but are optional. " +
|
"IMPORTANT: You MUST answer general questions, explanations, calculations, and discussions directly without using tools. " +
|
||||||
"Use tools only when they are needed for the specific task. " +
|
"Tools are ONLY for specific system operations like backtesting, agent management, or retrieving real-time market data. " +
|
||||||
"For general questions, explanations, calculations, or discussions, respond directly without using tools."
|
"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);
|
request.Messages.Insert(0, systemMessage);
|
||||||
}
|
|
||||||
|
|
||||||
// Send chat request to LLM
|
// Send chat request to LLM
|
||||||
var response = await _llmService.ChatAsync(user, request);
|
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)
|
/// The agent name of the master bot's owner (for copy trading bots)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MasterAgentName { get; set; }
|
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),
|
"get_bundle_backtest_by_id" => await _backtestMcpTools.ExecuteGetBundleBacktestById(user, parameters),
|
||||||
"delete_bundle_backtest" => await _backtestMcpTools.ExecuteDeleteBundleBacktest(user, parameters),
|
"delete_bundle_backtest" => await _backtestMcpTools.ExecuteDeleteBundleBacktest(user, parameters),
|
||||||
"run_backtest" => await _backtestMcpTools.ExecuteRunBacktest(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
|
// Data tools
|
||||||
"get_tickers" => await _dataMcpTools.ExecuteGetTickers(user, parameters),
|
"get_tickers" => await _dataMcpTools.ExecuteGetTickers(user, parameters),
|
||||||
|
|||||||
@@ -359,6 +359,95 @@ public class BacktestMcpTools : BaseMcpTool
|
|||||||
DefaultValue = true
|
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,
|
name,
|
||||||
save);
|
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.MoneyManagements;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Text.Json;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
namespace Managing.Mcp.Tools;
|
namespace Managing.Mcp.Tools;
|
||||||
@@ -288,7 +289,8 @@ public class BacktestTools
|
|||||||
var filter = new BundleBacktestRequestsFilter
|
var filter = new BundleBacktestRequestsFilter
|
||||||
{
|
{
|
||||||
Status = status,
|
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(
|
var (bundleRequests, totalCount) = await _backtester.GetBundleBacktestRequestsPaginatedAsync(
|
||||||
@@ -497,4 +499,328 @@ public class BacktestTools
|
|||||||
throw new InvalidOperationException($"Failed to run backtest: {ex.Message}", ex);
|
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 (pageNumber < 1) pageNumber = 1;
|
||||||
if (pageSize < 1 || pageSize > 100) pageSize = 10;
|
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(
|
var (bots, totalCount) = await _botRepository.GetBotsPaginatedAsync(
|
||||||
pageNumber,
|
pageNumber,
|
||||||
pageSize,
|
pageSize,
|
||||||
status,
|
status,
|
||||||
name,
|
name,
|
||||||
ticker,
|
ticker,
|
||||||
agentName,
|
userAgentName, // Filter by user's agentName to ensure security
|
||||||
minBalance,
|
minBalance,
|
||||||
maxBalance,
|
maxBalance,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
|||||||
Reference in New Issue
Block a user