Remove McpService and refactor dependency injection for MCP tools
- Deleted the McpService class, which was previously responsible for executing Model Context Protocol (MCP) tools. - Updated the ApiBootstrap class to change the registration of IMcpService to the new Managing.Mcp.McpService implementation. - Added new MCP tool implementations for DataTools, BotTools, and IndicatorTools to enhance functionality.
This commit is contained in:
@@ -1,236 +0,0 @@
|
|||||||
using Managing.Application.Abstractions.Services;
|
|
||||||
using Managing.Domain.Users;
|
|
||||||
using Managing.Mcp.Tools;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static Managing.Common.Enums;
|
|
||||||
|
|
||||||
namespace Managing.Application.LLM;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Service for executing Model Context Protocol (MCP) tools
|
|
||||||
/// </summary>
|
|
||||||
public class McpService : IMcpService
|
|
||||||
{
|
|
||||||
private readonly BacktestTools _backtestTools;
|
|
||||||
private readonly ILogger<McpService> _logger;
|
|
||||||
|
|
||||||
public McpService(BacktestTools backtestTools, ILogger<McpService> logger)
|
|
||||||
{
|
|
||||||
_backtestTools = backtestTools;
|
|
||||||
_logger = logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> ExecuteToolAsync(User user, string toolName, Dictionary<string, object>? parameters = null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Executing MCP tool: {ToolName} for user: {UserId}", toolName, user.Id);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return toolName.ToLowerInvariant() switch
|
|
||||||
{
|
|
||||||
"get_backtests_paginated" => await ExecuteGetBacktestsPaginated(user, parameters),
|
|
||||||
_ => throw new InvalidOperationException($"Unknown tool: {toolName}")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error executing MCP tool {ToolName} for user {UserId}", toolName, user.Id);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IEnumerable<McpToolDefinition>> GetAvailableToolsAsync()
|
|
||||||
{
|
|
||||||
var tools = new List<McpToolDefinition>
|
|
||||||
{
|
|
||||||
new McpToolDefinition
|
|
||||||
{
|
|
||||||
Name = "get_backtests_paginated",
|
|
||||||
Description = "Retrieves paginated backtests with filtering and sorting capabilities. Supports filters for score, winrate, drawdown, tickers, indicators, duration, and trading type.",
|
|
||||||
Parameters = new Dictionary<string, McpParameterDefinition>
|
|
||||||
{
|
|
||||||
["page"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "integer",
|
|
||||||
Description = "Page number (defaults to 1)",
|
|
||||||
Required = false,
|
|
||||||
DefaultValue = 1
|
|
||||||
},
|
|
||||||
["pageSize"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "integer",
|
|
||||||
Description = "Number of items per page (defaults to 50, max 100)",
|
|
||||||
Required = false,
|
|
||||||
DefaultValue = 50
|
|
||||||
},
|
|
||||||
["sortBy"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "string",
|
|
||||||
Description = "Field to sort by (Score, WinRate, GrowthPercentage, MaxDrawdown, SharpeRatio, FinalPnl, StartDate, EndDate, PositionCount)",
|
|
||||||
Required = false,
|
|
||||||
DefaultValue = "Score"
|
|
||||||
},
|
|
||||||
["sortOrder"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "string",
|
|
||||||
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
|
|
||||||
Required = false,
|
|
||||||
DefaultValue = "desc"
|
|
||||||
},
|
|
||||||
["scoreMin"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "number",
|
|
||||||
Description = "Minimum score filter (0-100)",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["scoreMax"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "number",
|
|
||||||
Description = "Maximum score filter (0-100)",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["winrateMin"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "integer",
|
|
||||||
Description = "Minimum winrate filter (0-100)",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["winrateMax"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "integer",
|
|
||||||
Description = "Maximum winrate filter (0-100)",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["maxDrawdownMax"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "number",
|
|
||||||
Description = "Maximum drawdown filter",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["tickers"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "string",
|
|
||||||
Description = "Comma-separated list of tickers to filter by (e.g., 'BTC,ETH,SOL')",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["indicators"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "string",
|
|
||||||
Description = "Comma-separated list of indicators to filter by",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["durationMinDays"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "number",
|
|
||||||
Description = "Minimum duration in days",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["durationMaxDays"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "number",
|
|
||||||
Description = "Maximum duration in days",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["name"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "string",
|
|
||||||
Description = "Filter by name (contains search)",
|
|
||||||
Required = false
|
|
||||||
},
|
|
||||||
["tradingType"] = new McpParameterDefinition
|
|
||||||
{
|
|
||||||
Type = "string",
|
|
||||||
Description = "Trading type filter (Spot, Futures, BacktestSpot, BacktestFutures, Paper)",
|
|
||||||
Required = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return Task.FromResult<IEnumerable<McpToolDefinition>>(tools);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<object> ExecuteGetBacktestsPaginated(User user, Dictionary<string, object>? parameters)
|
|
||||||
{
|
|
||||||
var page = GetParameterValue<int>(parameters, "page", 1);
|
|
||||||
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
|
|
||||||
var sortByString = GetParameterValue<string>(parameters, "sortBy", "Score");
|
|
||||||
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
|
|
||||||
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
|
|
||||||
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
|
|
||||||
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
|
|
||||||
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
|
|
||||||
var maxDrawdownMax = GetParameterValue<decimal?>(parameters, "maxDrawdownMax", null);
|
|
||||||
var tickers = GetParameterValue<string?>(parameters, "tickers", null);
|
|
||||||
var indicators = GetParameterValue<string?>(parameters, "indicators", null);
|
|
||||||
var durationMinDays = GetParameterValue<double?>(parameters, "durationMinDays", null);
|
|
||||||
var durationMaxDays = GetParameterValue<double?>(parameters, "durationMaxDays", null);
|
|
||||||
var name = GetParameterValue<string?>(parameters, "name", null);
|
|
||||||
var tradingTypeString = GetParameterValue<string?>(parameters, "tradingType", null);
|
|
||||||
|
|
||||||
// Parse sortBy enum
|
|
||||||
if (!Enum.TryParse<BacktestSortableColumn>(sortByString, true, out var sortBy))
|
|
||||||
{
|
|
||||||
sortBy = BacktestSortableColumn.Score;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse tradingType enum
|
|
||||||
TradingType? tradingType = null;
|
|
||||||
if (!string.IsNullOrWhiteSpace(tradingTypeString) &&
|
|
||||||
Enum.TryParse<TradingType>(tradingTypeString, true, out var parsedTradingType))
|
|
||||||
{
|
|
||||||
tradingType = parsedTradingType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await _backtestTools.GetBacktestsPaginated(
|
|
||||||
user,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
sortBy,
|
|
||||||
sortOrder,
|
|
||||||
scoreMin,
|
|
||||||
scoreMax,
|
|
||||||
winrateMin,
|
|
||||||
winrateMax,
|
|
||||||
maxDrawdownMax,
|
|
||||||
tickers,
|
|
||||||
indicators,
|
|
||||||
durationMinDays,
|
|
||||||
durationMaxDays,
|
|
||||||
name,
|
|
||||||
tradingType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private T GetParameterValue<T>(Dictionary<string, object>? parameters, string key, T defaultValue)
|
|
||||||
{
|
|
||||||
if (parameters == null || !parameters.ContainsKey(key))
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var value = parameters[key];
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle nullable types
|
|
||||||
var targetType = typeof(T);
|
|
||||||
var underlyingType = Nullable.GetUnderlyingType(targetType);
|
|
||||||
|
|
||||||
if (underlyingType != null)
|
|
||||||
{
|
|
||||||
// It's a nullable type
|
|
||||||
return (T)Convert.ChangeType(value, underlyingType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (T)Convert.ChangeType(value, targetType);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -427,8 +427,19 @@ public static class ApiBootstrap
|
|||||||
|
|
||||||
// LLM and MCP services
|
// LLM and MCP services
|
||||||
services.AddScoped<ILlmService, Managing.Application.LLM.LlmService>();
|
services.AddScoped<ILlmService, Managing.Application.LLM.LlmService>();
|
||||||
services.AddScoped<IMcpService, Managing.Application.LLM.McpService>();
|
services.AddScoped<IMcpService, Managing.Mcp.McpService>();
|
||||||
|
|
||||||
|
// MCP Tools (underlying implementations)
|
||||||
services.AddScoped<Managing.Mcp.Tools.BacktestTools>();
|
services.AddScoped<Managing.Mcp.Tools.BacktestTools>();
|
||||||
|
services.AddScoped<Managing.Mcp.Tools.DataTools>();
|
||||||
|
services.AddScoped<Managing.Mcp.Tools.BotTools>();
|
||||||
|
services.AddScoped<Managing.Mcp.Tools.IndicatorTools>();
|
||||||
|
|
||||||
|
// MCP Tool Wrappers (with tool definitions)
|
||||||
|
services.AddScoped<Managing.Mcp.McpTools.BacktestMcpTools>();
|
||||||
|
services.AddScoped<Managing.Mcp.McpTools.DataMcpTools>();
|
||||||
|
services.AddScoped<Managing.Mcp.McpTools.BotMcpTools>();
|
||||||
|
services.AddScoped<Managing.Mcp.McpTools.IndicatorMcpTools>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
103
src/Managing.Mcp/McpService.cs
Normal file
103
src/Managing.Mcp/McpService.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Mcp.McpTools;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Managing.Mcp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for executing Model Context Protocol (MCP) tools
|
||||||
|
/// </summary>
|
||||||
|
public class McpService : IMcpService
|
||||||
|
{
|
||||||
|
private readonly BacktestMcpTools _backtestMcpTools;
|
||||||
|
private readonly DataMcpTools _dataMcpTools;
|
||||||
|
// private readonly BotMcpTools _botMcpTools; // TODO: Fix BotMcpTools implementation
|
||||||
|
private readonly IndicatorMcpTools _indicatorMcpTools;
|
||||||
|
private readonly ILogger<McpService> _logger;
|
||||||
|
|
||||||
|
public McpService(
|
||||||
|
BacktestMcpTools backtestMcpTools,
|
||||||
|
DataMcpTools dataMcpTools,
|
||||||
|
// BotMcpTools botMcpTools, // TODO: Fix BotMcpTools implementation
|
||||||
|
IndicatorMcpTools indicatorMcpTools,
|
||||||
|
ILogger<McpService> logger)
|
||||||
|
{
|
||||||
|
_backtestMcpTools = backtestMcpTools;
|
||||||
|
_dataMcpTools = dataMcpTools;
|
||||||
|
// _botMcpTools = botMcpTools; // TODO: Fix BotMcpTools implementation
|
||||||
|
_indicatorMcpTools = indicatorMcpTools;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteToolAsync(User user, string toolName, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Executing MCP tool: {ToolName} for user: {UserId}", toolName, user.Id);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return toolName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
// Backtest tools
|
||||||
|
"get_backtests_paginated" => await _backtestMcpTools.ExecuteGetBacktestsPaginated(user, parameters),
|
||||||
|
"get_backtest_by_id" => await _backtestMcpTools.ExecuteGetBacktestById(user, parameters),
|
||||||
|
"delete_backtest" => await _backtestMcpTools.ExecuteDeleteBacktest(user, parameters),
|
||||||
|
"delete_backtests_by_ids" => await _backtestMcpTools.ExecuteDeleteBacktestsByIds(user, parameters),
|
||||||
|
"delete_backtests_by_filters" => await _backtestMcpTools.ExecuteDeleteBacktestsByFilters(user, parameters),
|
||||||
|
"get_bundle_backtests_paginated" => await _backtestMcpTools.ExecuteGetBundleBacktestsPaginated(user, parameters),
|
||||||
|
"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),
|
||||||
|
|
||||||
|
// Data tools
|
||||||
|
"get_tickers" => await _dataMcpTools.ExecuteGetTickers(user, parameters),
|
||||||
|
"get_candles" => await _dataMcpTools.ExecuteGetCandles(user, parameters),
|
||||||
|
"get_spotlight" => await _dataMcpTools.ExecuteGetSpotlight(user, parameters),
|
||||||
|
"get_agent_balances" => await _dataMcpTools.ExecuteGetAgentBalances(user, parameters),
|
||||||
|
"get_online_agents" => await _dataMcpTools.ExecuteGetOnlineAgents(user, parameters),
|
||||||
|
"get_agents_paginated" => await _dataMcpTools.ExecuteGetAgentsPaginated(user, parameters),
|
||||||
|
|
||||||
|
// Bot tools (TODO: Fix BotMcpTools implementation)
|
||||||
|
// "get_bots_paginated" => await _botMcpTools.ExecuteGetBotsPaginated(user, parameters),
|
||||||
|
// "get_bot_by_id" => await _botMcpTools.ExecuteGetBotById(user, parameters),
|
||||||
|
// "get_bot_config" => await _botMcpTools.ExecuteGetBotConfig(user, parameters),
|
||||||
|
// "get_bots_by_status" => await _botMcpTools.ExecuteGetBotsByStatus(user, parameters),
|
||||||
|
// "delete_bot" => await _botMcpTools.ExecuteDeleteBot(user, parameters),
|
||||||
|
|
||||||
|
// Indicator tools
|
||||||
|
"list_indicators" => await _indicatorMcpTools.ExecuteListIndicators(user, parameters),
|
||||||
|
"get_indicator_info" => await _indicatorMcpTools.ExecuteGetIndicatorInfo(user, parameters),
|
||||||
|
"explain_indicator" => await _indicatorMcpTools.ExecuteExplainIndicator(user, parameters),
|
||||||
|
"recommend_indicators" => await _indicatorMcpTools.ExecuteRecommendIndicators(user, parameters),
|
||||||
|
"refine_indicator_parameters" => await _indicatorMcpTools.ExecuteRefineIndicatorParameters(user, parameters),
|
||||||
|
"compare_indicators" => await _indicatorMcpTools.ExecuteCompareIndicators(user, parameters),
|
||||||
|
|
||||||
|
_ => throw new InvalidOperationException($"Unknown tool: {toolName}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error executing MCP tool {ToolName} for user {UserId}", toolName, user.Id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IEnumerable<McpToolDefinition>> GetAvailableToolsAsync()
|
||||||
|
{
|
||||||
|
var allTools = new List<McpToolDefinition>();
|
||||||
|
|
||||||
|
// Add backtest tools
|
||||||
|
allTools.AddRange(_backtestMcpTools.GetToolDefinitions());
|
||||||
|
|
||||||
|
// Add data tools
|
||||||
|
allTools.AddRange(_dataMcpTools.GetToolDefinitions());
|
||||||
|
|
||||||
|
// Add bot tools (TODO: Fix BotMcpTools implementation)
|
||||||
|
// allTools.AddRange(_botMcpTools.GetToolDefinitions());
|
||||||
|
|
||||||
|
// Add indicator tools
|
||||||
|
allTools.AddRange(_indicatorMcpTools.GetToolDefinitions());
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<McpToolDefinition>>(allTools);
|
||||||
|
}
|
||||||
|
}
|
||||||
546
src/Managing.Mcp/McpTools/BacktestMcpTools.cs
Normal file
546
src/Managing.Mcp/McpTools/BacktestMcpTools.cs
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Managing.Mcp.Tools;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.McpTools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for backtest operations
|
||||||
|
/// </summary>
|
||||||
|
public class BacktestMcpTools : BaseMcpTool
|
||||||
|
{
|
||||||
|
private readonly BacktestTools _backtestTools;
|
||||||
|
|
||||||
|
public BacktestMcpTools(BacktestTools backtestTools)
|
||||||
|
{
|
||||||
|
_backtestTools = backtestTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
|
||||||
|
{
|
||||||
|
return new List<McpToolDefinition>
|
||||||
|
{
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_backtests_paginated",
|
||||||
|
Description = "Retrieves paginated backtests with filtering and sorting capabilities. Supports filters for score, winrate, drawdown, tickers, indicators, duration, and trading type.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["page"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Page number (defaults to 1)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 1
|
||||||
|
},
|
||||||
|
["pageSize"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Number of items per page (defaults to 50, max 100)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 50
|
||||||
|
},
|
||||||
|
["sortBy"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Field to sort by (Score, WinRate, GrowthPercentage, MaxDrawdown, SharpeRatio, FinalPnl, StartDate, EndDate, PositionCount)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "Score"
|
||||||
|
},
|
||||||
|
["sortOrder"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "desc"
|
||||||
|
},
|
||||||
|
["scoreMin"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Minimum score filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["scoreMax"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Maximum score filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["winrateMin"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Minimum winrate filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["winrateMax"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Maximum winrate filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["maxDrawdownMax"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Maximum drawdown filter",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["tickers"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Comma-separated list of tickers to filter by (e.g., 'BTC,ETH,SOL')",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["indicators"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Comma-separated list of indicators to filter by",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["durationMinDays"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Minimum duration in days",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["durationMaxDays"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Maximum duration in days",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["name"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by name (contains search)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["tradingType"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Trading type filter (Spot, Futures, BacktestSpot, BacktestFutures, Paper)",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_backtest_by_id",
|
||||||
|
Description = "Retrieves a specific backtest by its ID for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["id"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The ID of the backtest to retrieve",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "delete_backtest",
|
||||||
|
Description = "Deletes a specific backtest by its ID for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["id"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The ID of the backtest to delete",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "delete_backtests_by_ids",
|
||||||
|
Description = "Deletes multiple backtests by their IDs for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["ids"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Comma-separated list of backtest IDs to delete",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "delete_backtests_by_filters",
|
||||||
|
Description = "Deletes backtests matching the specified filters for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["scoreMin"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Minimum score filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["scoreMax"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Maximum score filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["winrateMin"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Minimum winrate filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["winrateMax"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Maximum winrate filter (0-100)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["name"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by name (contains search)",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_bundle_backtests_paginated",
|
||||||
|
Description = "Retrieves paginated bundle backtest requests with filtering and sorting capabilities.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["page"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Page number (defaults to 1)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 1
|
||||||
|
},
|
||||||
|
["pageSize"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Number of items per page (defaults to 50, max 100)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 50
|
||||||
|
},
|
||||||
|
["sortBy"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Field to sort by (CreatedAt, UpdatedAt, BotName)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "CreatedAt"
|
||||||
|
},
|
||||||
|
["sortOrder"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "desc"
|
||||||
|
},
|
||||||
|
["status"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by status (Pending, Running, Completed, Failed, Cancelled)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["name"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by bot name (contains search)",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_bundle_backtest_by_id",
|
||||||
|
Description = "Retrieves a specific bundle backtest request by its ID.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["id"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The ID of the bundle backtest request to retrieve",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "delete_bundle_backtest",
|
||||||
|
Description = "Deletes a bundle backtest request and all associated backtests.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["id"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The ID of the bundle backtest request to delete",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "run_backtest",
|
||||||
|
Description = "Runs a new backtest with the specified configuration. Creates a backtest job that will be processed asynchronously.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["ticker"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The ticker symbol to backtest (e.g., 'BTC', 'ETH', 'SOL')",
|
||||||
|
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
|
||||||
|
},
|
||||||
|
["scenarioName"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The name of the trading scenario/strategy to use",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["moneyManagementName"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The name of the money management configuration to use",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["stopLoss"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Stop loss percentage (e.g., 2.5 for 2.5%)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["takeProfit"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Take profit percentage (e.g., 5.0 for 5%)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["leverage"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Leverage multiplier (e.g., 1, 2, 5, 10)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["name"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Optional name for the backtest (auto-generated if not provided)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["save"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "boolean",
|
||||||
|
Description = "Whether to save the backtest results (defaults to true)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBacktestsPaginated(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var page = GetParameterValue<int>(parameters, "page", 1);
|
||||||
|
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
|
||||||
|
var sortByString = GetParameterValue<string>(parameters, "sortBy", "Score");
|
||||||
|
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
|
||||||
|
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
|
||||||
|
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
|
||||||
|
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
|
||||||
|
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
|
||||||
|
var maxDrawdownMax = GetParameterValue<decimal?>(parameters, "maxDrawdownMax", null);
|
||||||
|
var tickers = GetParameterValue<string?>(parameters, "tickers", null);
|
||||||
|
var indicators = GetParameterValue<string?>(parameters, "indicators", null);
|
||||||
|
var durationMinDays = GetParameterValue<double?>(parameters, "durationMinDays", null);
|
||||||
|
var durationMaxDays = GetParameterValue<double?>(parameters, "durationMaxDays", null);
|
||||||
|
var name = GetParameterValue<string?>(parameters, "name", null);
|
||||||
|
var tradingTypeString = GetParameterValue<string?>(parameters, "tradingType", null);
|
||||||
|
|
||||||
|
// Parse sortBy enum
|
||||||
|
if (!Enum.TryParse<BacktestSortableColumn>(sortByString, true, out var sortBy))
|
||||||
|
{
|
||||||
|
sortBy = BacktestSortableColumn.Score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse tradingType enum
|
||||||
|
TradingType? tradingType = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(tradingTypeString) &&
|
||||||
|
Enum.TryParse<TradingType>(tradingTypeString, true, out var parsedTradingType))
|
||||||
|
{
|
||||||
|
tradingType = parsedTradingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _backtestTools.GetBacktestsPaginated(
|
||||||
|
user,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
scoreMin,
|
||||||
|
scoreMax,
|
||||||
|
winrateMin,
|
||||||
|
winrateMax,
|
||||||
|
maxDrawdownMax,
|
||||||
|
tickers,
|
||||||
|
indicators,
|
||||||
|
durationMinDays,
|
||||||
|
durationMaxDays,
|
||||||
|
name,
|
||||||
|
tradingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBacktestById(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var id = GetParameterValue<string>(parameters, "id", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Backtest ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _backtestTools.GetBacktestById(user, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteDeleteBacktest(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var id = GetParameterValue<string>(parameters, "id", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Backtest ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _backtestTools.DeleteBacktest(user, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteDeleteBacktestsByIds(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var idsString = GetParameterValue<string>(parameters, "ids", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(idsString))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one backtest ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = idsString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
return await _backtestTools.DeleteBacktestsByIds(user, ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteDeleteBacktestsByFilters(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
|
||||||
|
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
|
||||||
|
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
|
||||||
|
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
|
||||||
|
var name = GetParameterValue<string?>(parameters, "name", null);
|
||||||
|
|
||||||
|
return await _backtestTools.DeleteBacktestsByFilters(user, scoreMin, scoreMax, winrateMin, winrateMax, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBundleBacktestsPaginated(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var page = GetParameterValue<int>(parameters, "page", 1);
|
||||||
|
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
|
||||||
|
var sortByString = GetParameterValue<string>(parameters, "sortBy", "CreatedAt");
|
||||||
|
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
|
||||||
|
var statusString = GetParameterValue<string?>(parameters, "status", null);
|
||||||
|
var name = GetParameterValue<string?>(parameters, "name", null);
|
||||||
|
|
||||||
|
return await _backtestTools.GetBundleBacktestsPaginated(user, page, pageSize, sortByString, sortOrder, statusString, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBundleBacktestById(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.GetBundleBacktestById(user, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteDeleteBundleBacktest(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.DeleteBundleBacktest(user, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteRunBacktest(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var ticker = GetParameterValue<string>(parameters, "ticker", 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 scenarioName = GetParameterValue<string?>(parameters, "scenarioName", null);
|
||||||
|
var moneyManagementName = GetParameterValue<string?>(parameters, "moneyManagementName", null);
|
||||||
|
var stopLoss = GetParameterValue<decimal?>(parameters, "stopLoss", null);
|
||||||
|
var takeProfit = GetParameterValue<decimal?>(parameters, "takeProfit", null);
|
||||||
|
var leverage = GetParameterValue<decimal?>(parameters, "leverage", null);
|
||||||
|
var name = GetParameterValue<string?>(parameters, "name", null);
|
||||||
|
var save = GetParameterValue<bool>(parameters, "save", true);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ticker))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Ticker 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _backtestTools.RunBacktest(
|
||||||
|
user,
|
||||||
|
ticker,
|
||||||
|
timeframe,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
balance,
|
||||||
|
scenarioName,
|
||||||
|
moneyManagementName,
|
||||||
|
stopLoss,
|
||||||
|
takeProfit,
|
||||||
|
leverage,
|
||||||
|
name,
|
||||||
|
save);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/Managing.Mcp/McpTools/BaseMcpTool.cs
Normal file
50
src/Managing.Mcp/McpTools/BaseMcpTool.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.McpTools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for MCP tools providing common functionality
|
||||||
|
/// </summary>
|
||||||
|
public abstract class BaseMcpTool
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the tool definitions for this tool
|
||||||
|
/// </summary>
|
||||||
|
public abstract IEnumerable<McpToolDefinition> GetToolDefinitions();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to extract parameter values with type conversion and default values
|
||||||
|
/// </summary>
|
||||||
|
protected T GetParameterValue<T>(Dictionary<string, object>? parameters, string key, T defaultValue)
|
||||||
|
{
|
||||||
|
if (parameters == null || !parameters.ContainsKey(key))
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = parameters[key];
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle nullable types
|
||||||
|
var targetType = typeof(T);
|
||||||
|
var underlyingType = Nullable.GetUnderlyingType(targetType);
|
||||||
|
|
||||||
|
if (underlyingType != null)
|
||||||
|
{
|
||||||
|
// It's a nullable type
|
||||||
|
return (T)Convert.ChangeType(value, underlyingType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T)Convert.ChangeType(value, targetType);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
257
src/Managing.Mcp/McpTools/BotMcpTools.cs
Normal file
257
src/Managing.Mcp/McpTools/BotMcpTools.cs
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Managing.Mcp.Tools;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.McpTools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for bot operations
|
||||||
|
/// </summary>
|
||||||
|
public class BotMcpTools : BaseMcpTool
|
||||||
|
{
|
||||||
|
private readonly BotTools _botTools;
|
||||||
|
|
||||||
|
public BotMcpTools(BotTools botTools)
|
||||||
|
{
|
||||||
|
_botTools = botTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
|
||||||
|
{
|
||||||
|
return new List<McpToolDefinition>
|
||||||
|
{
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_bots_paginated",
|
||||||
|
Description = "Retrieves paginated bots with filtering and sorting capabilities. Supports filters for status, name, ticker, agent name, and balance range.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["pageNumber"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Page number (defaults to 1)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 1
|
||||||
|
},
|
||||||
|
["pageSize"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Number of items per page (defaults to 10, max 100)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 10
|
||||||
|
},
|
||||||
|
["status"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by bot status (Saved, Stopped, Running)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["name"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by bot name (partial match, case-insensitive)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["ticker"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by ticker (partial match, case-insensitive)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["agentName"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Filter by agent name (partial match, case-insensitive)",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["minBalance"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Minimum bot trading balance filter",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["maxBalance"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "number",
|
||||||
|
Description = "Maximum bot trading balance filter",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["sortBy"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Field to sort by (CreateDate, Name, Ticker, Status, StartupTime, Roi, Pnl, WinRate, AgentName, BotTradingBalance)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "CreateDate"
|
||||||
|
},
|
||||||
|
["sortDirection"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Sort direction - 'Asc' or 'Desc' (defaults to 'Desc')",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "Desc"
|
||||||
|
},
|
||||||
|
["showOnlyProfitable"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "boolean",
|
||||||
|
Description = "Show only profitable bots (ROI > 0) (defaults to false)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_bot_by_id",
|
||||||
|
Description = "Retrieves a specific bot by its identifier for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["identifier"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The identifier (GUID) of the bot to retrieve",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// TODO: Implement GetBotConfig in BotTools
|
||||||
|
// new McpToolDefinition
|
||||||
|
// {
|
||||||
|
// Name = "get_bot_config",
|
||||||
|
// Description = "Retrieves the configuration of a specific bot by its identifier for the authenticated user.",
|
||||||
|
// Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
// {
|
||||||
|
// ["identifier"] = new McpParameterDefinition
|
||||||
|
// {
|
||||||
|
// Type = "string",
|
||||||
|
// Description = "The identifier (GUID) of the bot to get configuration for",
|
||||||
|
// Required = true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_bots_by_status",
|
||||||
|
Description = "Retrieves all bots with a specific status for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["status"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Bot status filter (Saved, Stopped, Running)",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "delete_bot",
|
||||||
|
Description = "Deletes a specific bot by its identifier for the authenticated user.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["identifier"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The identifier (GUID) of the bot to delete",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBotsPaginated(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var pageNumber = GetParameterValue<int>(parameters, "pageNumber", 1);
|
||||||
|
var pageSize = GetParameterValue<int>(parameters, "pageSize", 10);
|
||||||
|
var statusString = GetParameterValue<string?>(parameters, "status", null);
|
||||||
|
var name = GetParameterValue<string?>(parameters, "name", null);
|
||||||
|
var ticker = GetParameterValue<string?>(parameters, "ticker", null);
|
||||||
|
var agentName = GetParameterValue<string?>(parameters, "agentName", null);
|
||||||
|
var minBalance = GetParameterValue<decimal?>(parameters, "minBalance", null);
|
||||||
|
var maxBalance = GetParameterValue<decimal?>(parameters, "maxBalance", null);
|
||||||
|
var sortByString = GetParameterValue<string>(parameters, "sortBy", "CreateDate");
|
||||||
|
var sortDirectionString = GetParameterValue<string>(parameters, "sortDirection", "Desc");
|
||||||
|
var showOnlyProfitable = GetParameterValue<bool>(parameters, "showOnlyProfitable", false);
|
||||||
|
|
||||||
|
// Parse status enum
|
||||||
|
BotStatus? status = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(statusString) &&
|
||||||
|
Enum.TryParse<BotStatus>(statusString, true, out var parsedStatus))
|
||||||
|
{
|
||||||
|
status = parsedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse sortBy enum
|
||||||
|
if (!Enum.TryParse<BotSortableColumn>(sortByString, true, out var sortBy))
|
||||||
|
{
|
||||||
|
sortBy = BotSortableColumn.CreateDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse sortDirection enum
|
||||||
|
if (!Enum.TryParse<SortDirection>(sortDirectionString, true, out var sortDirection))
|
||||||
|
{
|
||||||
|
sortDirection = SortDirection.Desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _botTools.GetBotsPaginated(
|
||||||
|
user,
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ticker,
|
||||||
|
agentName,
|
||||||
|
minBalance,
|
||||||
|
maxBalance,
|
||||||
|
sortBy,
|
||||||
|
sortDirection,
|
||||||
|
showOnlyProfitable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBotById(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var identifierString = GetParameterValue<string>(parameters, "identifier", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(identifierString) || !Guid.TryParse(identifierString, out var identifier))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid bot identifier (GUID) is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _botTools.GetBotById(user, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Implement GetBotConfig in BotTools
|
||||||
|
// public async Task<object> ExecuteGetBotConfig(User user, Dictionary<string, object>? parameters)
|
||||||
|
// {
|
||||||
|
// var identifierString = GetParameterValue<string>(parameters, "identifier", string.Empty);
|
||||||
|
// if (string.IsNullOrWhiteSpace(identifierString) || !Guid.TryParse(identifierString, out var identifier))
|
||||||
|
// {
|
||||||
|
// throw new ArgumentException("Valid bot identifier (GUID) is required");
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return await _botTools.GetBotConfig(user, identifier);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetBotsByStatus(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var statusString = GetParameterValue<string>(parameters, "status", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(statusString) || !Enum.TryParse<BotStatus>(statusString, true, out var status))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid bot status is required (Saved, Stopped, Running)");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _botTools.GetBotsByStatus(user, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteDeleteBot(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var identifierString = GetParameterValue<string>(parameters, "identifier", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(identifierString) || !Guid.TryParse(identifierString, out var identifier))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid bot identifier (GUID) is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _botTools.DeleteBot(user, identifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
250
src/Managing.Mcp/McpTools/DataMcpTools.cs
Normal file
250
src/Managing.Mcp/McpTools/DataMcpTools.cs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Managing.Mcp.Tools;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.McpTools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for data operations (tickers, candles, statistics, agents)
|
||||||
|
/// </summary>
|
||||||
|
public class DataMcpTools : BaseMcpTool
|
||||||
|
{
|
||||||
|
private readonly DataTools _dataTools;
|
||||||
|
|
||||||
|
public DataMcpTools(DataTools dataTools)
|
||||||
|
{
|
||||||
|
_dataTools = dataTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
|
||||||
|
{
|
||||||
|
return new List<McpToolDefinition>
|
||||||
|
{
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_tickers",
|
||||||
|
Description = "Retrieves available tickers for a given timeframe.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["timeframe"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The timeframe (e.g., 'FiveMinutes', 'FifteenMinutes', 'OneHour', 'FourHours', 'OneDay')",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_candles",
|
||||||
|
Description = "Retrieves historical candle data for a specific ticker and date range.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["ticker"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The ticker symbol (e.g., 'BTC', 'ETH', 'SOL')",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["startDate"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Start date in ISO 8601 format (e.g., '2024-01-01T00:00:00Z')",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["endDate"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "End date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z')",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["timeframe"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The timeframe (e.g., 'FiveMinutes', 'FifteenMinutes', 'OneHour', 'FourHours', 'OneDay')",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_spotlight",
|
||||||
|
Description = "Retrieves platform statistics and spotlight data showing market overview.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>()
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_agent_balances",
|
||||||
|
Description = "Retrieves balance history for a specific agent within a date range.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["agentName"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The name of the agent",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["startDate"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Start date in ISO 8601 format",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["endDate"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Optional end date in ISO 8601 format (defaults to current time)",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_online_agents",
|
||||||
|
Description = "Retrieves list of currently online agents.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>()
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_agents_paginated",
|
||||||
|
Description = "Retrieves paginated agent summaries with filtering and sorting capabilities.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["page"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Page number (defaults to 1)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 1
|
||||||
|
},
|
||||||
|
["pageSize"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Number of items per page (defaults to 10, max 100)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = 10
|
||||||
|
},
|
||||||
|
["sortBy"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Field to sort by (NetPnL, TotalPnL, TotalROI, Wins, Losses, AgentName, TotalVolume, TotalBalance)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "NetPnL"
|
||||||
|
},
|
||||||
|
["sortOrder"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = "desc"
|
||||||
|
},
|
||||||
|
["agentNames"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Comma-separated list of agent names to filter by",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["showOnlyProfitable"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "boolean",
|
||||||
|
Description = "Show only profitable agents (defaults to false)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetTickers(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var timeframeString = GetParameterValue<string>(parameters, "timeframe", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(timeframeString) ||
|
||||||
|
!Enum.TryParse<Timeframe>(timeframeString, true, out var timeframe))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid timeframe is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _dataTools.GetTickers(timeframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetCandles(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var tickerString = GetParameterValue<string>(parameters, "ticker", string.Empty);
|
||||||
|
var startDateString = GetParameterValue<string>(parameters, "startDate", string.Empty);
|
||||||
|
var endDateString = GetParameterValue<string>(parameters, "endDate", string.Empty);
|
||||||
|
var timeframeString = GetParameterValue<string>(parameters, "timeframe", string.Empty);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(tickerString) ||
|
||||||
|
!Enum.TryParse<Ticker>(tickerString, true, out var ticker))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid ticker 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(timeframeString) ||
|
||||||
|
!Enum.TryParse<Timeframe>(timeframeString, true, out var timeframe))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid timeframe is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _dataTools.GetCandles(ticker, startDate, endDate, timeframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetSpotlight(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
return await _dataTools.GetSpotlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetAgentBalances(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var agentName = GetParameterValue<string>(parameters, "agentName", string.Empty);
|
||||||
|
var startDateString = GetParameterValue<string>(parameters, "startDate", string.Empty);
|
||||||
|
var endDateString = GetParameterValue<string?>(parameters, "endDate", null);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(agentName))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Agent name is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DateTime.TryParse(startDateString, out var startDate))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Valid start date is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime? endDate = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(endDateString) && DateTime.TryParse(endDateString, out var parsedEndDate))
|
||||||
|
{
|
||||||
|
endDate = parsedEndDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _dataTools.GetAgentBalances(agentName, startDate, endDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetOnlineAgents(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
return await _dataTools.GetOnlineAgents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetAgentsPaginated(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var page = GetParameterValue<int>(parameters, "page", 1);
|
||||||
|
var pageSize = GetParameterValue<int>(parameters, "pageSize", 10);
|
||||||
|
var sortBy = GetParameterValue<string>(parameters, "sortBy", "NetPnL");
|
||||||
|
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
|
||||||
|
var agentNames = GetParameterValue<string?>(parameters, "agentNames", null);
|
||||||
|
var showOnlyProfitable = GetParameterValue<bool>(parameters, "showOnlyProfitable", false);
|
||||||
|
|
||||||
|
return await _dataTools.GetAgentsPaginated(page, pageSize, sortBy, sortOrder, agentNames, showOnlyProfitable);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/Managing.Mcp/McpTools/IndicatorMcpTools.cs
Normal file
222
src/Managing.Mcp/McpTools/IndicatorMcpTools.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Managing.Mcp.Tools;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.McpTools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for indicator operations (list, explain, recommend, refine, compare)
|
||||||
|
/// </summary>
|
||||||
|
public class IndicatorMcpTools : BaseMcpTool
|
||||||
|
{
|
||||||
|
private readonly IndicatorTools _indicatorTools;
|
||||||
|
|
||||||
|
public IndicatorMcpTools(IndicatorTools indicatorTools)
|
||||||
|
{
|
||||||
|
_indicatorTools = indicatorTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
|
||||||
|
{
|
||||||
|
return new List<McpToolDefinition>
|
||||||
|
{
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "list_indicators",
|
||||||
|
Description = "Lists all available indicators with their types, categories, and basic information. Can filter by signal type (Signal, Trend, Context) or category.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["signalType"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Optional filter by signal type: 'Signal', 'Trend', or 'Context'",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["category"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Optional filter by category (e.g., 'RSI', 'EMA', 'MACD', 'Volatility')",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "get_indicator_info",
|
||||||
|
Description = "Gets detailed information about a specific indicator including description, triggers, parameters, recommended values, use cases, and trading style recommendations.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["indicatorType"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The indicator type (e.g., 'RsiDivergence', 'MacdCross', 'EmaCross', 'SuperTrend', 'StDev')",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "explain_indicator",
|
||||||
|
Description = "Provides a comprehensive explanation of how an indicator works, when to use it, what triggers it, parameter recommendations, and examples. Useful for understanding indicator mechanics and best practices.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["indicatorType"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The indicator type to explain (e.g., 'RsiDivergence', 'MacdCross', 'EmaCross')",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["includeExamples"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "boolean",
|
||||||
|
Description = "Whether to include usage examples (defaults to true)",
|
||||||
|
Required = false,
|
||||||
|
DefaultValue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "recommend_indicators",
|
||||||
|
Description = "Recommends indicators based on trading style (scalping, swing trading, position trading), market conditions (trending, volatile, ranging), and goals (entry signals, trend confirmation, risk filtering). Returns scored recommendations with reasoning.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["tradingStyle"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Trading style: 'scalping', 'day trading', 'swing trading', 'position trading', or 'any'",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["marketCondition"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Market condition: 'trending', 'ranging', 'volatile', 'high volatility', 'low volatility', or 'any'",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["goal"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Trading goal: 'entry signals', 'exit signals', 'trend confirmation', 'risk filtering', 'market context', or 'any'",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["maxRecommendations"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "integer",
|
||||||
|
Description = "Maximum number of recommendations to return (defaults to all)",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "refine_indicator_parameters",
|
||||||
|
Description = "Suggests parameter refinements for an indicator based on trading style and market volatility. Provides optimized parameter values for different scenarios (short-term, medium-term, long-term trading, high/low volatility markets).",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["indicatorType"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "The indicator type to refine parameters for (e.g., 'RsiDivergence', 'EmaCross', 'MacdCross')",
|
||||||
|
Required = true
|
||||||
|
},
|
||||||
|
["tradingStyle"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Trading style: 'scalping', 'day trading', 'swing trading', 'position trading', or leave empty for general recommendations",
|
||||||
|
Required = false
|
||||||
|
},
|
||||||
|
["marketVolatility"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Market volatility: 'high', 'low', 'normal', or leave empty for general recommendations",
|
||||||
|
Required = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new McpToolDefinition
|
||||||
|
{
|
||||||
|
Name = "compare_indicators",
|
||||||
|
Description = "Compares multiple indicators side by side, showing their types, categories, use cases, parameters, and best use scenarios. Useful for selecting between similar indicators.",
|
||||||
|
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||||
|
{
|
||||||
|
["indicatorTypes"] = new McpParameterDefinition
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Description = "Comma-separated list of indicator types to compare (e.g., 'RsiDivergence,MacdCross,EmaCross')",
|
||||||
|
Required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteListIndicators(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var signalType = GetParameterValue<string?>(parameters, "signalType", null);
|
||||||
|
var category = GetParameterValue<string?>(parameters, "category", null);
|
||||||
|
|
||||||
|
return await Task.FromResult(_indicatorTools.ListIndicators(signalType, category));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteGetIndicatorInfo(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var indicatorType = GetParameterValue<string>(parameters, "indicatorType", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(indicatorType))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Indicator type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(_indicatorTools.GetIndicatorInfo(indicatorType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteExplainIndicator(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var indicatorType = GetParameterValue<string>(parameters, "indicatorType", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(indicatorType))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Indicator type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
var includeExamples = GetParameterValue<bool>(parameters, "includeExamples", true);
|
||||||
|
|
||||||
|
return await Task.FromResult(_indicatorTools.ExplainIndicator(indicatorType, includeExamples));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteRecommendIndicators(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var tradingStyle = GetParameterValue<string?>(parameters, "tradingStyle", null);
|
||||||
|
var marketCondition = GetParameterValue<string?>(parameters, "marketCondition", null);
|
||||||
|
var goal = GetParameterValue<string?>(parameters, "goal", null);
|
||||||
|
var maxRecommendations = GetParameterValue<int?>(parameters, "maxRecommendations", null);
|
||||||
|
|
||||||
|
return await Task.FromResult(_indicatorTools.RecommendIndicators(
|
||||||
|
tradingStyle, marketCondition, goal, maxRecommendations));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteRefineIndicatorParameters(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var indicatorType = GetParameterValue<string>(parameters, "indicatorType", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(indicatorType))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Indicator type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tradingStyle = GetParameterValue<string?>(parameters, "tradingStyle", null);
|
||||||
|
var marketVolatility = GetParameterValue<string?>(parameters, "marketVolatility", null);
|
||||||
|
|
||||||
|
return await Task.FromResult(_indicatorTools.RefineIndicatorParameters(
|
||||||
|
indicatorType, tradingStyle, marketVolatility));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<object> ExecuteCompareIndicators(User user, Dictionary<string, object>? parameters)
|
||||||
|
{
|
||||||
|
var indicatorTypes = GetParameterValue<string>(parameters, "indicatorTypes", string.Empty);
|
||||||
|
if (string.IsNullOrWhiteSpace(indicatorTypes))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one indicator type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(_indicatorTools.CompareIndicators(indicatorTypes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
using Managing.Application.Abstractions.Services;
|
using Managing.Application.Abstractions.Services;
|
||||||
using Managing.Application.Abstractions.Shared;
|
using Managing.Application.Abstractions.Shared;
|
||||||
|
using Managing.Domain.Backtests;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.MoneyManagements;
|
||||||
using Managing.Domain.Users;
|
using Managing.Domain.Users;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
@@ -12,11 +15,16 @@ namespace Managing.Mcp.Tools;
|
|||||||
public class BacktestTools
|
public class BacktestTools
|
||||||
{
|
{
|
||||||
private readonly IBacktester _backtester;
|
private readonly IBacktester _backtester;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
private readonly ILogger<BacktestTools> _logger;
|
private readonly ILogger<BacktestTools> _logger;
|
||||||
|
|
||||||
public BacktestTools(IBacktester backtester, ILogger<BacktestTools> logger)
|
public BacktestTools(
|
||||||
|
IBacktester backtester,
|
||||||
|
IAccountService accountService,
|
||||||
|
ILogger<BacktestTools> logger)
|
||||||
{
|
{
|
||||||
_backtester = backtester;
|
_backtester = backtester;
|
||||||
|
_accountService = accountService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,4 +142,359 @@ public class BacktestTools
|
|||||||
throw new InvalidOperationException($"Failed to retrieve backtests: {ex.Message}", ex);
|
throw new InvalidOperationException($"Failed to retrieve backtests: {ex.Message}", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a specific backtest by ID for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetBacktestById(User user, string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var backtest = await _backtester.GetBacktestByIdForUserAsync(user, id);
|
||||||
|
if (backtest == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Backtest with ID '{id}' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
backtest.Id,
|
||||||
|
backtest.Config,
|
||||||
|
backtest.FinalPnl,
|
||||||
|
backtest.WinRate,
|
||||||
|
backtest.GrowthPercentage,
|
||||||
|
backtest.HodlPercentage,
|
||||||
|
backtest.StartDate,
|
||||||
|
backtest.EndDate,
|
||||||
|
MaxDrawdown = backtest.Statistics?.MaxDrawdown,
|
||||||
|
backtest.Fees,
|
||||||
|
SharpeRatio = backtest.Statistics?.SharpeRatio,
|
||||||
|
backtest.Score,
|
||||||
|
backtest.ScoreMessage,
|
||||||
|
backtest.InitialBalance,
|
||||||
|
backtest.NetPnl,
|
||||||
|
backtest.PositionCount,
|
||||||
|
TradingType = backtest.Config.TradingType,
|
||||||
|
backtest.Positions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting backtest {BacktestId} for user {UserId}", id, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve backtest: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a specific backtest by ID for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> DeleteBacktest(User user, string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await _backtester.DeleteBacktestByUserAsync(user, id);
|
||||||
|
return new { Success = success, Message = success ? "Backtest deleted successfully" : "Failed to delete backtest" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting backtest {BacktestId} for user {UserId}", id, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to delete backtest: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes multiple backtests by their IDs for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> DeleteBacktestsByIds(User user, IEnumerable<string> ids)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var success = await _backtester.DeleteBacktestsByIdsForUserAsync(user, ids);
|
||||||
|
var count = ids.Count();
|
||||||
|
return new { Success = success, Message = success ? $"{count} backtests deleted successfully" : "Failed to delete backtests" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting backtests for user {UserId}", user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to delete backtests: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes backtests matching specified filters for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> DeleteBacktestsByFilters(
|
||||||
|
User user,
|
||||||
|
double? scoreMin = null,
|
||||||
|
double? scoreMax = null,
|
||||||
|
int? winrateMin = null,
|
||||||
|
int? winrateMax = null,
|
||||||
|
string? name = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var filter = new BacktestsFilter
|
||||||
|
{
|
||||||
|
NameContains = name,
|
||||||
|
ScoreMin = scoreMin,
|
||||||
|
ScoreMax = scoreMax,
|
||||||
|
WinrateMin = winrateMin,
|
||||||
|
WinrateMax = winrateMax
|
||||||
|
};
|
||||||
|
|
||||||
|
var deletedCount = await _backtester.DeleteBacktestsByFiltersAsync(user, filter);
|
||||||
|
return new { Success = true, DeletedCount = deletedCount, Message = $"{deletedCount} backtests deleted successfully" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting backtests by filters for user {UserId}", user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to delete backtests by filters: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves paginated bundle backtest requests for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetBundleBacktestsPaginated(
|
||||||
|
User user,
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 50,
|
||||||
|
string sortByString = "CreatedAt",
|
||||||
|
string sortOrder = "desc",
|
||||||
|
string? statusString = null,
|
||||||
|
string? name = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate inputs
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1 || pageSize > 100) pageSize = 50;
|
||||||
|
if (sortOrder != "asc" && sortOrder != "desc") sortOrder = "desc";
|
||||||
|
|
||||||
|
// Parse sortBy enum
|
||||||
|
if (!Enum.TryParse<BundleBacktestRequestSortableColumn>(sortByString, true, out var sortBy))
|
||||||
|
{
|
||||||
|
sortBy = BundleBacktestRequestSortableColumn.CreatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse status enum
|
||||||
|
BundleBacktestRequestStatus? status = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(statusString) &&
|
||||||
|
Enum.TryParse<BundleBacktestRequestStatus>(statusString, true, out var parsedStatus))
|
||||||
|
{
|
||||||
|
status = parsedStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter = new BundleBacktestRequestsFilter
|
||||||
|
{
|
||||||
|
Status = status,
|
||||||
|
NameContains = name
|
||||||
|
};
|
||||||
|
|
||||||
|
var (bundleRequests, totalCount) = await _backtester.GetBundleBacktestRequestsPaginatedAsync(
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
filter);
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
BundleRequests = bundleRequests.Select(br => new
|
||||||
|
{
|
||||||
|
br.RequestId,
|
||||||
|
br.Name,
|
||||||
|
br.Status,
|
||||||
|
br.CreatedAt,
|
||||||
|
br.UpdatedAt,
|
||||||
|
br.User.AgentName,
|
||||||
|
br.TotalBacktests
|
||||||
|
}),
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
HasNextPage = page < totalPages,
|
||||||
|
HasPreviousPage = page > 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting paginated bundle backtests for user {UserId}", user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve bundle backtests: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a specific bundle backtest request by ID
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetBundleBacktestById(User user, Guid id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bundleRequest = await _backtester.GetBundleBacktestRequestByIdForUserAsync(user, id);
|
||||||
|
if (bundleRequest == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Bundle backtest request with ID '{id}' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
bundleRequest.RequestId,
|
||||||
|
bundleRequest.Name,
|
||||||
|
bundleRequest.Status,
|
||||||
|
bundleRequest.CreatedAt,
|
||||||
|
bundleRequest.UpdatedAt,
|
||||||
|
bundleRequest.User.AgentName,
|
||||||
|
bundleRequest.TotalBacktests,
|
||||||
|
bundleRequest.CompletedBacktests,
|
||||||
|
bundleRequest.FailedBacktests,
|
||||||
|
bundleRequest.UniversalConfigJson,
|
||||||
|
bundleRequest.DateTimeRangesJson,
|
||||||
|
bundleRequest.MoneyManagementVariantsJson,
|
||||||
|
bundleRequest.TickerVariantsJson
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting bundle backtest {BundleId} for user {UserId}", id, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve bundle backtest: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a bundle backtest request and all associated backtests
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> DeleteBundleBacktest(User user, Guid id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _backtester.DeleteBundleBacktestRequestByIdForUserAsync(user, id);
|
||||||
|
// Also delete associated backtests
|
||||||
|
await _backtester.DeleteBacktestsByRequestIdAsync(id);
|
||||||
|
return new { Success = true, Message = "Bundle backtest and associated backtests deleted successfully" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting bundle backtest {BundleId} for user {UserId}", id, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to delete bundle backtest: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs a new backtest with the specified configuration
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> RunBacktest(
|
||||||
|
User user,
|
||||||
|
string ticker,
|
||||||
|
string timeframe,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
decimal balance,
|
||||||
|
string? scenarioName = null,
|
||||||
|
string? moneyManagementName = null,
|
||||||
|
decimal? stopLoss = null,
|
||||||
|
decimal? takeProfit = null,
|
||||||
|
decimal? leverage = null,
|
||||||
|
string? name = null,
|
||||||
|
bool save = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Parse ticker enum
|
||||||
|
if (!Enum.TryParse<Ticker>(ticker, true, out var parsedTicker))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid ticker: {ticker}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 the config
|
||||||
|
var config = new TradingBotConfig
|
||||||
|
{
|
||||||
|
AccountName = firstAccount.Name,
|
||||||
|
Ticker = parsedTicker,
|
||||||
|
Timeframe = parsedTimeframe,
|
||||||
|
IsForWatchingOnly = false,
|
||||||
|
BotTradingBalance = balance,
|
||||||
|
Name = name ?? $"Backtest_{parsedTicker}_{DateTime.UtcNow:yyyyMMddHHmmss}",
|
||||||
|
FlipPosition = false,
|
||||||
|
CooldownPeriod = 0,
|
||||||
|
MaxLossStreak = 0,
|
||||||
|
ScenarioName = scenarioName,
|
||||||
|
TradingType = TradingType.BacktestSpot
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add money management if parameters provided
|
||||||
|
if (stopLoss.HasValue || takeProfit.HasValue || leverage.HasValue)
|
||||||
|
{
|
||||||
|
config.MoneyManagement = new LightMoneyManagement
|
||||||
|
{
|
||||||
|
Name = "Custom",
|
||||||
|
Timeframe = parsedTimeframe,
|
||||||
|
StopLoss = stopLoss ?? 0,
|
||||||
|
TakeProfit = takeProfit ?? 0,
|
||||||
|
Leverage = leverage ?? 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the backtest (creates a job)
|
||||||
|
var result = await _backtester.RunTradingBotBacktest(
|
||||||
|
config,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
user,
|
||||||
|
save: save,
|
||||||
|
withCandles: false);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
JobId = result.Id,
|
||||||
|
Message = $"Backtest job created successfully. Job ID: {result.Id}",
|
||||||
|
Config = new
|
||||||
|
{
|
||||||
|
Ticker = parsedTicker.ToString(),
|
||||||
|
Timeframe = parsedTimeframe.ToString(),
|
||||||
|
StartDate = startDate,
|
||||||
|
EndDate = endDate,
|
||||||
|
Balance = balance,
|
||||||
|
ScenarioName = scenarioName,
|
||||||
|
MoneyManagementName = moneyManagementName
|
||||||
|
},
|
||||||
|
result.ScoreMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error running backtest for user {UserId}", user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to run backtest: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
214
src/Managing.Mcp/Tools/BotTools.cs
Normal file
214
src/Managing.Mcp/Tools/BotTools.cs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
using Managing.Application.Abstractions.Repositories;
|
||||||
|
using Managing.Domain.Bots;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for bot operations
|
||||||
|
/// </summary>
|
||||||
|
public class BotTools
|
||||||
|
{
|
||||||
|
private readonly IBotRepository _botRepository;
|
||||||
|
private readonly ILogger<BotTools> _logger;
|
||||||
|
|
||||||
|
public BotTools(IBotRepository botRepository, ILogger<BotTools> logger)
|
||||||
|
{
|
||||||
|
_botRepository = botRepository;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves paginated bots for a user with filtering and sorting capabilities
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetBotsPaginated(
|
||||||
|
User user,
|
||||||
|
int pageNumber = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
BotStatus? status = null,
|
||||||
|
string? name = null,
|
||||||
|
string? ticker = null,
|
||||||
|
string? agentName = null,
|
||||||
|
decimal? minBalance = null,
|
||||||
|
decimal? maxBalance = null,
|
||||||
|
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
|
||||||
|
SortDirection sortDirection = SortDirection.Desc,
|
||||||
|
bool showOnlyProfitable = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate inputs
|
||||||
|
if (pageNumber < 1) pageNumber = 1;
|
||||||
|
if (pageSize < 1 || pageSize > 100) pageSize = 10;
|
||||||
|
|
||||||
|
var (bots, totalCount) = await _botRepository.GetBotsPaginatedAsync(
|
||||||
|
pageNumber,
|
||||||
|
pageSize,
|
||||||
|
status,
|
||||||
|
name,
|
||||||
|
ticker,
|
||||||
|
agentName,
|
||||||
|
minBalance,
|
||||||
|
maxBalance,
|
||||||
|
sortBy,
|
||||||
|
sortDirection,
|
||||||
|
showOnlyProfitable);
|
||||||
|
|
||||||
|
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Bots = bots.Select(b => new
|
||||||
|
{
|
||||||
|
b.Identifier,
|
||||||
|
b.Name,
|
||||||
|
b.Ticker,
|
||||||
|
b.Status,
|
||||||
|
b.CreateDate,
|
||||||
|
b.StartupTime,
|
||||||
|
b.Pnl,
|
||||||
|
b.Roi,
|
||||||
|
WinRate = (b.TradeWins + b.TradeLosses) != 0
|
||||||
|
? (double)b.TradeWins / (b.TradeWins + b.TradeLosses)
|
||||||
|
: 0.0,
|
||||||
|
b.BotTradingBalance,
|
||||||
|
b.TradingType,
|
||||||
|
AgentName = b.User?.AgentName,
|
||||||
|
MasterAgentName = b.MasterBotUser?.AgentName
|
||||||
|
}),
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = pageNumber,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
HasNextPage = pageNumber < totalPages,
|
||||||
|
HasPreviousPage = pageNumber > 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting paginated bots for user {UserId}", user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve bots: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a specific bot by identifier for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetBotById(User user, Guid identifier)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Bot with identifier '{identifier}' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user owns the bot
|
||||||
|
if (bot.User?.Name != user.Name)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("You don't have permission to access this bot");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
bot.Identifier,
|
||||||
|
bot.Name,
|
||||||
|
bot.Ticker,
|
||||||
|
bot.Status,
|
||||||
|
bot.CreateDate,
|
||||||
|
bot.StartupTime,
|
||||||
|
bot.Pnl,
|
||||||
|
bot.Roi,
|
||||||
|
WinRate = (bot.TradeWins + bot.TradeLosses) != 0
|
||||||
|
? (double)bot.TradeWins / (bot.TradeWins + bot.TradeLosses)
|
||||||
|
: 0.0,
|
||||||
|
bot.BotTradingBalance,
|
||||||
|
bot.TradingType,
|
||||||
|
AgentName = bot.User?.AgentName,
|
||||||
|
MasterAgentName = bot.MasterBotUser?.AgentName,
|
||||||
|
TradeWins = bot.TradeWins,
|
||||||
|
TradeLosses = bot.TradeLosses
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting bot {BotId} for user {UserId}", identifier, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve bot: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves bots by status for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetBotsByStatus(User user, BotStatus status)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bots = await _botRepository.GetBotsByUserIdAsync(user.Id);
|
||||||
|
var filteredBots = bots.Where(b => b.Status == status);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Status = status.ToString(),
|
||||||
|
Bots = filteredBots.Select(b => new
|
||||||
|
{
|
||||||
|
b.Identifier,
|
||||||
|
b.Name,
|
||||||
|
b.Ticker,
|
||||||
|
b.Status,
|
||||||
|
b.CreateDate,
|
||||||
|
b.StartupTime,
|
||||||
|
b.Pnl,
|
||||||
|
b.Roi,
|
||||||
|
WinRate = (b.TradeWins + b.TradeLosses) != 0
|
||||||
|
? (double)b.TradeWins / (b.TradeWins + b.TradeLosses)
|
||||||
|
: 0.0,
|
||||||
|
b.BotTradingBalance,
|
||||||
|
b.TradingType,
|
||||||
|
AgentName = b.User?.AgentName
|
||||||
|
}),
|
||||||
|
Count = filteredBots.Count()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting bots by status {Status} for user {UserId}", status, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve bots: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes a bot by identifier for the user
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> DeleteBot(User user, Guid identifier)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
|
||||||
|
if (bot == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Bot with identifier '{identifier}' not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify user owns the bot
|
||||||
|
if (bot.User?.Name != user.Name)
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException("You don't have permission to delete this bot");
|
||||||
|
}
|
||||||
|
|
||||||
|
await _botRepository.DeleteBot(identifier);
|
||||||
|
var success = true;
|
||||||
|
return new { Success = success, Message = success ? "Bot deleted successfully" : "Failed to delete bot" };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error deleting bot {BotId} for user {UserId}", identifier, user.Id);
|
||||||
|
throw new InvalidOperationException($"Failed to delete bot: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
243
src/Managing.Mcp/Tools/DataTools.cs
Normal file
243
src/Managing.Mcp/Tools/DataTools.cs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
using Managing.Application.Abstractions.Services;
|
||||||
|
using Managing.Domain.Users;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for data operations (tickers, candles, statistics)
|
||||||
|
/// </summary>
|
||||||
|
public class DataTools
|
||||||
|
{
|
||||||
|
private readonly IExchangeService _exchangeService;
|
||||||
|
private readonly IStatisticService _statisticService;
|
||||||
|
private readonly IAgentService _agentService;
|
||||||
|
private readonly ICacheService _cacheService;
|
||||||
|
private readonly ILogger<DataTools> _logger;
|
||||||
|
|
||||||
|
public DataTools(
|
||||||
|
IExchangeService exchangeService,
|
||||||
|
IStatisticService statisticService,
|
||||||
|
IAgentService agentService,
|
||||||
|
ICacheService cacheService,
|
||||||
|
ILogger<DataTools> logger)
|
||||||
|
{
|
||||||
|
_exchangeService = exchangeService;
|
||||||
|
_statisticService = statisticService;
|
||||||
|
_agentService = agentService;
|
||||||
|
_cacheService = cacheService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves available tickers for a given timeframe
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetTickers(Timeframe timeframe)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tickers = await _exchangeService.GetTickers(timeframe);
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Tickers = tickers.Select(t => new
|
||||||
|
{
|
||||||
|
Ticker = t.ToString(),
|
||||||
|
Timeframe = timeframe.ToString()
|
||||||
|
}),
|
||||||
|
TotalCount = tickers.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting tickers for timeframe {Timeframe}", timeframe);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve tickers: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves candles for a specific ticker and date range
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetCandles(
|
||||||
|
Ticker ticker,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime endDate,
|
||||||
|
Timeframe timeframe)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var candles = await _exchangeService.GetCandlesInflux(
|
||||||
|
TradingExchanges.Evm,
|
||||||
|
ticker,
|
||||||
|
startDate,
|
||||||
|
timeframe,
|
||||||
|
endDate);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Ticker = ticker.ToString(),
|
||||||
|
Timeframe = timeframe.ToString(),
|
||||||
|
StartDate = startDate,
|
||||||
|
EndDate = endDate,
|
||||||
|
Candles = candles.Select(c => new
|
||||||
|
{
|
||||||
|
c.Date,
|
||||||
|
c.Open,
|
||||||
|
c.High,
|
||||||
|
c.Low,
|
||||||
|
c.Close,
|
||||||
|
c.Volume
|
||||||
|
}).OrderBy(c => c.Date),
|
||||||
|
TotalCount = candles.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting candles for {Ticker} from {StartDate} to {EndDate}",
|
||||||
|
ticker, startDate, endDate);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve candles: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves platform statistics and spotlight data
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetSpotlight()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var spotlight = await _statisticService.GetLastSpotlight(DateTime.Now.AddDays(-2));
|
||||||
|
return spotlight;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting spotlight data");
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve spotlight data: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves agent balance history within a date range
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetAgentBalances(
|
||||||
|
string agentName,
|
||||||
|
DateTime startDate,
|
||||||
|
DateTime? endDate = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var balances = await _agentService.GetAgentBalances(agentName, startDate, endDate);
|
||||||
|
return balances;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting agent balances for {AgentName} from {StartDate} to {EndDate}",
|
||||||
|
agentName, startDate, endDate);
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve agent balances: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves list of online agents
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetOnlineAgents()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var agentNames = await _agentService.GetAllOnlineAgents();
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Agents = agentNames.Select(name => new
|
||||||
|
{
|
||||||
|
AgentName = name,
|
||||||
|
IsOnline = true
|
||||||
|
}),
|
||||||
|
TotalCount = agentNames.Count()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting online agents");
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve online agents: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves paginated agent summaries with filtering and sorting
|
||||||
|
/// </summary>
|
||||||
|
public async Task<object> GetAgentsPaginated(
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
string sortBy = "NetPnL",
|
||||||
|
string sortOrder = "desc",
|
||||||
|
string? agentNames = null,
|
||||||
|
bool showOnlyProfitable = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Validate inputs
|
||||||
|
if (page < 1) page = 1;
|
||||||
|
if (pageSize < 1 || pageSize > 100) pageSize = 10;
|
||||||
|
if (sortOrder != "asc" && sortOrder != "desc") sortOrder = "desc";
|
||||||
|
|
||||||
|
// Get all agent summaries
|
||||||
|
var allAgents = await _agentService.GetAllAgentSummaries();
|
||||||
|
|
||||||
|
// Filter by agent names if provided
|
||||||
|
if (!string.IsNullOrWhiteSpace(agentNames))
|
||||||
|
{
|
||||||
|
var agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(name => name.Trim())
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
allAgents = allAgents.Where(a => agentNamesList.Contains(a.AgentName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter profitable agents if requested
|
||||||
|
if (showOnlyProfitable)
|
||||||
|
{
|
||||||
|
allAgents = allAgents.Where(a => a.NetPnL > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCount = allAgents.Count();
|
||||||
|
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||||
|
|
||||||
|
// Apply pagination
|
||||||
|
var paginatedAgents = allAgents
|
||||||
|
.Skip((page - 1) * pageSize)
|
||||||
|
.Take(pageSize)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Agents = paginatedAgents.Select(a => new
|
||||||
|
{
|
||||||
|
a.AgentName,
|
||||||
|
a.TotalPnL,
|
||||||
|
a.NetPnL,
|
||||||
|
a.TotalROI,
|
||||||
|
a.Wins,
|
||||||
|
a.Losses,
|
||||||
|
WinRate = (a.Wins + a.Losses) > 0 ? (a.Wins * 100) / (a.Wins + a.Losses) : 0,
|
||||||
|
a.ActiveStrategiesCount,
|
||||||
|
a.TotalVolume,
|
||||||
|
a.TotalBalance,
|
||||||
|
a.TotalFees,
|
||||||
|
a.BacktestCount
|
||||||
|
}),
|
||||||
|
TotalCount = totalCount,
|
||||||
|
CurrentPage = page,
|
||||||
|
PageSize = pageSize,
|
||||||
|
TotalPages = totalPages,
|
||||||
|
HasNextPage = page < totalPages,
|
||||||
|
HasPreviousPage = page > 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting paginated agents");
|
||||||
|
throw new InvalidOperationException($"Failed to retrieve agents: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
973
src/Managing.Mcp/Tools/IndicatorTools.cs
Normal file
973
src/Managing.Mcp/Tools/IndicatorTools.cs
Normal file
@@ -0,0 +1,973 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Managing.Domain.Indicators;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
|
namespace Managing.Mcp.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP tools for indicator operations (list, explain, recommend, refine, compare)
|
||||||
|
/// </summary>
|
||||||
|
public class IndicatorTools
|
||||||
|
{
|
||||||
|
private readonly ILogger<IndicatorTools> _logger;
|
||||||
|
private readonly Dictionary<IndicatorType, IndicatorInfo> _indicatorInfoCache;
|
||||||
|
|
||||||
|
public IndicatorTools(ILogger<IndicatorTools> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_indicatorInfoCache = LoadIndicatorInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lists all available indicators with their types and basic information
|
||||||
|
/// </summary>
|
||||||
|
public object ListIndicators(string? signalType = null, string? category = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var indicators = Enum.GetValues<IndicatorType>()
|
||||||
|
.Where(it => it != IndicatorType.Composite) // Exclude Composite as it's special
|
||||||
|
.Select(type => new
|
||||||
|
{
|
||||||
|
Type = type.ToString(),
|
||||||
|
SignalType = GetSignalType(type).ToString(),
|
||||||
|
Category = GetCategory(type),
|
||||||
|
Name = GetIndicatorName(type),
|
||||||
|
Description = GetShortDescription(type)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Filter by signal type if provided
|
||||||
|
if (!string.IsNullOrWhiteSpace(signalType) &&
|
||||||
|
Enum.TryParse<SignalType>(signalType, true, out var signalTypeEnum))
|
||||||
|
{
|
||||||
|
indicators = indicators.Where(i => i.SignalType == signalTypeEnum.ToString()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by category if provided
|
||||||
|
if (!string.IsNullOrWhiteSpace(category))
|
||||||
|
{
|
||||||
|
indicators = indicators.Where(i =>
|
||||||
|
i.Category.Contains(category, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Indicators = indicators,
|
||||||
|
TotalCount = indicators.Count,
|
||||||
|
SignalTypeFilter = signalType,
|
||||||
|
CategoryFilter = category
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error listing indicators");
|
||||||
|
throw new InvalidOperationException($"Failed to list indicators: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets detailed information about a specific indicator
|
||||||
|
/// </summary>
|
||||||
|
public object GetIndicatorInfo(string indicatorType)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<IndicatorType>(indicatorType, true, out var type))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid indicator type: {indicatorType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_indicatorInfoCache.TryGetValue(type, out var info))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Information not available for indicator: {indicatorType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Type = type.ToString(),
|
||||||
|
SignalType = GetSignalType(type).ToString(),
|
||||||
|
Category = GetCategory(type),
|
||||||
|
Name = info.Name,
|
||||||
|
Description = info.Description,
|
||||||
|
LongDescription = info.LongDescription,
|
||||||
|
TriggersLong = info.TriggersLong,
|
||||||
|
TriggersShort = info.TriggersShort,
|
||||||
|
Parameters = info.Parameters,
|
||||||
|
RecommendedParameters = info.RecommendedParameters,
|
||||||
|
UseCases = info.UseCases,
|
||||||
|
TradingStyleRecommendations = info.TradingStyleRecommendations
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting indicator info for {IndicatorType}", indicatorType);
|
||||||
|
throw new InvalidOperationException($"Failed to get indicator info: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Explains how an indicator works, when to use it, and provides examples
|
||||||
|
/// </summary>
|
||||||
|
public object ExplainIndicator(string indicatorType, bool includeExamples = true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<IndicatorType>(indicatorType, true, out var type))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid indicator type: {indicatorType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_indicatorInfoCache.TryGetValue(type, out var info))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Information not available for indicator: {indicatorType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var explanation = new
|
||||||
|
{
|
||||||
|
Type = type.ToString(),
|
||||||
|
SignalType = GetSignalType(type).ToString(),
|
||||||
|
Category = GetCategory(type),
|
||||||
|
Name = info.Name,
|
||||||
|
HowItWorks = info.LongDescription,
|
||||||
|
WhenToUse = info.UseCases,
|
||||||
|
TriggersLong = info.TriggersLong,
|
||||||
|
TriggersShort = info.TriggersShort,
|
||||||
|
Parameters = info.Parameters.Select(p => new
|
||||||
|
{
|
||||||
|
p.Name,
|
||||||
|
p.Description,
|
||||||
|
RecommendedValue = p.RecommendedValue,
|
||||||
|
Range = p.Range
|
||||||
|
}),
|
||||||
|
RecommendedParameters = info.RecommendedParameters,
|
||||||
|
TradingStyleRecommendations = info.TradingStyleRecommendations,
|
||||||
|
Examples = includeExamples ? GetIndicatorExamples(type) : null
|
||||||
|
};
|
||||||
|
|
||||||
|
return explanation;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error explaining indicator {IndicatorType}", indicatorType);
|
||||||
|
throw new InvalidOperationException($"Failed to explain indicator: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recommends indicators based on trading style, market conditions, and goals
|
||||||
|
/// </summary>
|
||||||
|
public object RecommendIndicators(
|
||||||
|
string? tradingStyle = null,
|
||||||
|
string? marketCondition = null,
|
||||||
|
string? goal = null,
|
||||||
|
int? maxRecommendations = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var recommendations = new List<IndicatorRecommendation>();
|
||||||
|
|
||||||
|
foreach (var type in Enum.GetValues<IndicatorType>())
|
||||||
|
{
|
||||||
|
if (type == IndicatorType.Composite) continue;
|
||||||
|
|
||||||
|
if (!_indicatorInfoCache.TryGetValue(type, out var info)) continue;
|
||||||
|
|
||||||
|
var score = CalculateRecommendationScore(
|
||||||
|
type, info, tradingStyle, marketCondition, goal);
|
||||||
|
|
||||||
|
if (score > 0)
|
||||||
|
{
|
||||||
|
recommendations.Add(new IndicatorRecommendation
|
||||||
|
{
|
||||||
|
IndicatorType = type.ToString(),
|
||||||
|
SignalType = GetSignalType(type).ToString(),
|
||||||
|
Category = GetCategory(type),
|
||||||
|
Name = info.Name,
|
||||||
|
Score = score,
|
||||||
|
Reasons = GetRecommendationReasons(type, info, tradingStyle, marketCondition, goal)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by score descending
|
||||||
|
recommendations = recommendations.OrderByDescending(r => r.Score).ToList();
|
||||||
|
|
||||||
|
// Limit results if requested
|
||||||
|
if (maxRecommendations.HasValue && maxRecommendations.Value > 0)
|
||||||
|
{
|
||||||
|
recommendations = recommendations.Take(maxRecommendations.Value).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Recommendations = recommendations,
|
||||||
|
TotalCount = recommendations.Count,
|
||||||
|
Filters = new
|
||||||
|
{
|
||||||
|
TradingStyle = tradingStyle ?? "Any",
|
||||||
|
MarketCondition = marketCondition ?? "Any",
|
||||||
|
Goal = goal ?? "Any"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error recommending indicators");
|
||||||
|
throw new InvalidOperationException($"Failed to recommend indicators: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Suggests parameter refinements for an indicator based on trading style
|
||||||
|
/// </summary>
|
||||||
|
public object RefineIndicatorParameters(
|
||||||
|
string indicatorType,
|
||||||
|
string? tradingStyle = null,
|
||||||
|
string? marketVolatility = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Enum.TryParse<IndicatorType>(indicatorType, true, out var type))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Invalid indicator type: {indicatorType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_indicatorInfoCache.TryGetValue(type, out var info))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Information not available for indicator: {indicatorType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var refinements = new List<ParameterRefinement>();
|
||||||
|
|
||||||
|
foreach (var param in info.Parameters)
|
||||||
|
{
|
||||||
|
var refinement = new ParameterRefinement
|
||||||
|
{
|
||||||
|
ParameterName = param.Name,
|
||||||
|
Description = param.Description,
|
||||||
|
CurrentRecommended = param.RecommendedValue,
|
||||||
|
RefinedValues = GetRefinedParameterValues(
|
||||||
|
type, param.Name, tradingStyle, marketVolatility),
|
||||||
|
Reasoning = GetParameterRefinementReasoning(
|
||||||
|
type, param.Name, tradingStyle, marketVolatility)
|
||||||
|
};
|
||||||
|
|
||||||
|
refinements.Add(refinement);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
IndicatorType = type.ToString(),
|
||||||
|
SignalType = GetSignalType(type).ToString(),
|
||||||
|
TradingStyle = tradingStyle ?? "General",
|
||||||
|
MarketVolatility = marketVolatility ?? "Normal",
|
||||||
|
ParameterRefinements = refinements,
|
||||||
|
GeneralAdvice = GetGeneralParameterAdvice(type, tradingStyle, marketVolatility)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error refining parameters for {IndicatorType}", indicatorType);
|
||||||
|
throw new InvalidOperationException($"Failed to refine parameters: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compares multiple indicators side by side
|
||||||
|
/// </summary>
|
||||||
|
public object CompareIndicators(string indicatorTypes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var types = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(t => t.Trim())
|
||||||
|
.Where(t => Enum.TryParse<IndicatorType>(t, true, out _))
|
||||||
|
.Select(t => Enum.Parse<IndicatorType>(t, true))
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (types.Count == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one valid indicator type is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
var comparisons = types.Select(type =>
|
||||||
|
{
|
||||||
|
_indicatorInfoCache.TryGetValue(type, out var info);
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Type = type.ToString(),
|
||||||
|
SignalType = GetSignalType(type).ToString(),
|
||||||
|
Category = GetCategory(type),
|
||||||
|
Name = info?.Name ?? type.ToString(),
|
||||||
|
Description = info?.Description ?? "No description available",
|
||||||
|
UseCases = info?.UseCases ?? new List<string>(),
|
||||||
|
Parameters = info?.Parameters.Select(p => p.Name).ToList() ?? new List<string>(),
|
||||||
|
BestFor = GetBestForDescription(type, info)
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
Indicators = comparisons,
|
||||||
|
Count = comparisons.Count,
|
||||||
|
ComparisonSummary = GenerateComparisonSummary(comparisons.Cast<dynamic>().ToList())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error comparing indicators");
|
||||||
|
throw new InvalidOperationException($"Failed to compare indicators: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Helper Methods
|
||||||
|
|
||||||
|
private SignalType GetSignalType(IndicatorType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
IndicatorType.EmaTrend or IndicatorType.StochRsiTrend or IndicatorType.IchimokuKumoTrend => SignalType.Trend,
|
||||||
|
IndicatorType.StDev or IndicatorType.BollingerBandsVolatilityProtection => SignalType.Context,
|
||||||
|
_ => SignalType.Signal
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCategory(IndicatorType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
IndicatorType.RsiDivergence or IndicatorType.RsiDivergenceConfirm => "RSI",
|
||||||
|
IndicatorType.EmaCross or IndicatorType.EmaTrend or IndicatorType.DualEmaCross or IndicatorType.SuperTrendCrossEma => "EMA",
|
||||||
|
IndicatorType.MacdCross => "MACD",
|
||||||
|
IndicatorType.SuperTrend or IndicatorType.ChandelierExit => "Trend Following",
|
||||||
|
IndicatorType.Stc or IndicatorType.LaggingStc => "STC",
|
||||||
|
IndicatorType.StochRsiTrend or IndicatorType.StochasticCross => "Stochastic",
|
||||||
|
IndicatorType.ThreeWhiteSoldiers => "Candlestick",
|
||||||
|
IndicatorType.StDev or IndicatorType.BollingerBandsVolatilityProtection => "Volatility",
|
||||||
|
IndicatorType.BollingerBandsPercentBMomentumBreakout => "Bollinger Bands",
|
||||||
|
IndicatorType.IchimokuKumoTrend => "Ichimoku",
|
||||||
|
_ => "Other"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetIndicatorName(IndicatorType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
IndicatorType.RsiDivergence => "RSI Divergence",
|
||||||
|
IndicatorType.RsiDivergenceConfirm => "RSI Divergence Confirm",
|
||||||
|
IndicatorType.MacdCross => "MACD Cross",
|
||||||
|
IndicatorType.EmaCross => "EMA Cross",
|
||||||
|
IndicatorType.DualEmaCross => "Dual EMA Cross",
|
||||||
|
IndicatorType.EmaTrend => "EMA Trend",
|
||||||
|
IndicatorType.SuperTrend => "SuperTrend",
|
||||||
|
IndicatorType.SuperTrendCrossEma => "SuperTrend Cross EMA",
|
||||||
|
IndicatorType.ChandelierExit => "Chandelier Exit",
|
||||||
|
IndicatorType.Stc => "STC (Schaff Trend Cycle)",
|
||||||
|
IndicatorType.LaggingStc => "Lagging STC",
|
||||||
|
IndicatorType.StochRsiTrend => "Stochastic RSI Trend",
|
||||||
|
IndicatorType.StochasticCross => "Stochastic Cross",
|
||||||
|
IndicatorType.ThreeWhiteSoldiers => "Three White Soldiers",
|
||||||
|
IndicatorType.StDev => "Standard Deviation Context",
|
||||||
|
IndicatorType.BollingerBandsPercentBMomentumBreakout => "Bollinger Bands Momentum Breakout",
|
||||||
|
IndicatorType.BollingerBandsVolatilityProtection => "Bollinger Bands Volatility Protection",
|
||||||
|
IndicatorType.IchimokuKumoTrend => "Ichimoku Kumo Trend",
|
||||||
|
_ => type.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetShortDescription(IndicatorType type)
|
||||||
|
{
|
||||||
|
return _indicatorInfoCache.TryGetValue(type, out var info)
|
||||||
|
? info.Description
|
||||||
|
: "No description available";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<IndicatorType, IndicatorInfo> LoadIndicatorInfo()
|
||||||
|
{
|
||||||
|
var info = new Dictionary<IndicatorType, IndicatorInfo>();
|
||||||
|
var readmePath = Path.Combine(
|
||||||
|
AppDomain.CurrentDomain.BaseDirectory,
|
||||||
|
"..", "..", "..", "..", "..",
|
||||||
|
"src", "Managing.Domain", "Indicators", "README-Indicators.md");
|
||||||
|
|
||||||
|
// Try multiple paths
|
||||||
|
if (!File.Exists(readmePath))
|
||||||
|
{
|
||||||
|
readmePath = Path.Combine(
|
||||||
|
Directory.GetCurrentDirectory(),
|
||||||
|
"src", "Managing.Domain", "Indicators", "README-Indicators.md");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(readmePath))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("README-Indicators.md not found, using default indicator info");
|
||||||
|
return LoadDefaultIndicatorInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(readmePath);
|
||||||
|
return ParseIndicatorInfoFromReadme(content);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error loading indicator info from README, using defaults");
|
||||||
|
return LoadDefaultIndicatorInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<IndicatorType, IndicatorInfo> ParseIndicatorInfoFromReadme(string content)
|
||||||
|
{
|
||||||
|
var info = new Dictionary<IndicatorType, IndicatorInfo>();
|
||||||
|
|
||||||
|
// Parse each indicator section
|
||||||
|
var sections = Regex.Split(content, @"^##\s+", RegexOptions.Multiline);
|
||||||
|
|
||||||
|
foreach (var section in sections)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(section)) continue;
|
||||||
|
|
||||||
|
var lines = section.Split('\n');
|
||||||
|
if (lines.Length == 0) continue;
|
||||||
|
|
||||||
|
var title = lines[0].Trim();
|
||||||
|
var indicatorType = FindIndicatorTypeByTitle(title);
|
||||||
|
|
||||||
|
if (indicatorType == null) continue;
|
||||||
|
|
||||||
|
var indicatorInfo = ParseIndicatorSection(section, indicatorType.Value);
|
||||||
|
info[indicatorType.Value] = indicatorInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill in any missing indicators with defaults
|
||||||
|
foreach (var type in Enum.GetValues<IndicatorType>())
|
||||||
|
{
|
||||||
|
if (type == IndicatorType.Composite) continue;
|
||||||
|
if (!info.ContainsKey(type))
|
||||||
|
{
|
||||||
|
info[type] = GetDefaultIndicatorInfo(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndicatorType? FindIndicatorTypeByTitle(string title)
|
||||||
|
{
|
||||||
|
var titleLower = title.ToLowerInvariant();
|
||||||
|
return Enum.GetValues<IndicatorType>().FirstOrDefault(type =>
|
||||||
|
{
|
||||||
|
var name = GetIndicatorName(type).ToLowerInvariant();
|
||||||
|
return titleLower.Contains(name.ToLowerInvariant()) ||
|
||||||
|
titleLower.Contains(type.ToString().ToLowerInvariant());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndicatorInfo ParseIndicatorSection(string section, IndicatorType type)
|
||||||
|
{
|
||||||
|
var info = new IndicatorInfo
|
||||||
|
{
|
||||||
|
Name = GetIndicatorName(type),
|
||||||
|
Description = ExtractField(section, "Description:"),
|
||||||
|
LongDescription = ExtractField(section, "Description:") ?? "",
|
||||||
|
TriggersLong = ExtractListItems(section, "Trigger a Long"),
|
||||||
|
TriggersShort = ExtractListItems(section, "Trigger a Short"),
|
||||||
|
Parameters = ExtractParameters(section),
|
||||||
|
RecommendedParameters = ExtractRecommendedParameters(section),
|
||||||
|
UseCases = ExtractUseCases(section, type),
|
||||||
|
TradingStyleRecommendations = ExtractTradingStyleRecommendations(section, type)
|
||||||
|
};
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ExtractField(string section, string fieldName)
|
||||||
|
{
|
||||||
|
var pattern = $@"{Regex.Escape(fieldName)}\s*\n\s*\*\*(.*?)\*\*";
|
||||||
|
var match = Regex.Match(section, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||||
|
return match.Success ? match.Groups[1].Value.Trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> ExtractListItems(string section, string header)
|
||||||
|
{
|
||||||
|
var items = new List<string>();
|
||||||
|
var pattern = $@"{Regex.Escape(header)}.*?\n((?:\s*•\s*.*?\n?)+)";
|
||||||
|
var match = Regex.Match(section, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var itemsText = match.Groups[1].Value;
|
||||||
|
var itemMatches = Regex.Matches(itemsText, @"•\s*(.+?)(?=\n\s*•|\n\n|$)");
|
||||||
|
items.AddRange(itemMatches.Cast<Match>().Select(m => m.Groups[1].Value.Trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ParameterInfo> ExtractParameters(string section)
|
||||||
|
{
|
||||||
|
var parameters = new List<ParameterInfo>();
|
||||||
|
var pattern = @"\*\*Parameters\*\*\s*\n((?:\s*•\s*.*?\n?)+)";
|
||||||
|
var match = Regex.Match(section, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
var paramsText = match.Groups[1].Value;
|
||||||
|
var paramMatches = Regex.Matches(paramsText, @"•\s*(\w+)\s*\(recommended:\s*([^)]+)\)\s*-\s*(.+?)(?=\n\s*•|\n\n|$)");
|
||||||
|
|
||||||
|
foreach (Match m in paramMatches)
|
||||||
|
{
|
||||||
|
parameters.Add(new ParameterInfo
|
||||||
|
{
|
||||||
|
Name = m.Groups[1].Value.Trim(),
|
||||||
|
RecommendedValue = m.Groups[2].Value.Trim(),
|
||||||
|
Description = m.Groups[3].Value.Trim(),
|
||||||
|
Range = ExtractRange(m.Groups[2].Value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ExtractRange(string recommendedValue)
|
||||||
|
{
|
||||||
|
if (recommendedValue.Contains("-"))
|
||||||
|
{
|
||||||
|
return recommendedValue;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> ExtractRecommendedParameters(string section)
|
||||||
|
{
|
||||||
|
var recommended = new Dictionary<string, string>();
|
||||||
|
var paramMatches = Regex.Matches(section, @"(\w+)\s*\(recommended:\s*([^)]+)\)");
|
||||||
|
|
||||||
|
foreach (Match m in paramMatches)
|
||||||
|
{
|
||||||
|
recommended[m.Groups[1].Value.Trim()] = m.Groups[2].Value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommended;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> ExtractUseCases(string section, IndicatorType type)
|
||||||
|
{
|
||||||
|
var useCases = new List<string>();
|
||||||
|
var signalType = GetSignalType(type);
|
||||||
|
|
||||||
|
switch (signalType)
|
||||||
|
{
|
||||||
|
case SignalType.Signal:
|
||||||
|
useCases.Add("Generate precise entry/exit signals");
|
||||||
|
useCases.Add("Identify high-probability trade entries");
|
||||||
|
break;
|
||||||
|
case SignalType.Trend:
|
||||||
|
useCases.Add("Identify overall market direction");
|
||||||
|
useCases.Add("Confirm market momentum");
|
||||||
|
useCases.Add("Avoid counter-trend trades");
|
||||||
|
break;
|
||||||
|
case SignalType.Context:
|
||||||
|
useCases.Add("Provide market context");
|
||||||
|
useCases.Add("Filter trades in unfavorable conditions");
|
||||||
|
useCases.Add("Risk management");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return useCases;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> ExtractTradingStyleRecommendations(string section, IndicatorType type)
|
||||||
|
{
|
||||||
|
var recommendations = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
// Extract from README sections about trading styles
|
||||||
|
if (section.Contains("Short-term") || section.Contains("Scalping"))
|
||||||
|
{
|
||||||
|
recommendations["Short-term"] = "Suitable for quick signals";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.Contains("Medium-term") || section.Contains("Swing"))
|
||||||
|
{
|
||||||
|
recommendations["Medium-term"] = "Good for swing trading";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (section.Contains("Long-term") || section.Contains("Position"))
|
||||||
|
{
|
||||||
|
recommendations["Long-term"] = "Ideal for position trading";
|
||||||
|
}
|
||||||
|
|
||||||
|
return recommendations;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateRecommendationScore(
|
||||||
|
IndicatorType type,
|
||||||
|
IndicatorInfo info,
|
||||||
|
string? tradingStyle,
|
||||||
|
string? marketCondition,
|
||||||
|
string? goal)
|
||||||
|
{
|
||||||
|
var score = 0;
|
||||||
|
|
||||||
|
// Base score
|
||||||
|
score += 10;
|
||||||
|
|
||||||
|
// Trading style matching
|
||||||
|
if (!string.IsNullOrWhiteSpace(tradingStyle))
|
||||||
|
{
|
||||||
|
var styleLower = tradingStyle.ToLowerInvariant();
|
||||||
|
if (styleLower.Contains("scalp") || styleLower.Contains("short"))
|
||||||
|
{
|
||||||
|
if (GetSignalType(type) == SignalType.Signal)
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
else if (styleLower.Contains("swing") || styleLower.Contains("medium"))
|
||||||
|
{
|
||||||
|
if (GetSignalType(type) == SignalType.Trend || GetSignalType(type) == SignalType.Signal)
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
else if (styleLower.Contains("position") || styleLower.Contains("long"))
|
||||||
|
{
|
||||||
|
if (GetSignalType(type) == SignalType.Trend)
|
||||||
|
score += 20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Market condition matching
|
||||||
|
if (!string.IsNullOrWhiteSpace(marketCondition))
|
||||||
|
{
|
||||||
|
var conditionLower = marketCondition.ToLowerInvariant();
|
||||||
|
if (conditionLower.Contains("volatile") || conditionLower.Contains("high volatility"))
|
||||||
|
{
|
||||||
|
if (type == IndicatorType.StDev || type == IndicatorType.BollingerBandsVolatilityProtection)
|
||||||
|
score += 15;
|
||||||
|
}
|
||||||
|
else if (conditionLower.Contains("trend") || conditionLower.Contains("trending"))
|
||||||
|
{
|
||||||
|
if (GetSignalType(type) == SignalType.Trend)
|
||||||
|
score += 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goal matching
|
||||||
|
if (!string.IsNullOrWhiteSpace(goal))
|
||||||
|
{
|
||||||
|
var goalLower = goal.ToLowerInvariant();
|
||||||
|
if (goalLower.Contains("entry") || goalLower.Contains("signal"))
|
||||||
|
{
|
||||||
|
if (GetSignalType(type) == SignalType.Signal)
|
||||||
|
score += 15;
|
||||||
|
}
|
||||||
|
else if (goalLower.Contains("filter") || goalLower.Contains("context"))
|
||||||
|
{
|
||||||
|
if (GetSignalType(type) == SignalType.Context)
|
||||||
|
score += 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetRecommendationReasons(
|
||||||
|
IndicatorType type,
|
||||||
|
IndicatorInfo info,
|
||||||
|
string? tradingStyle,
|
||||||
|
string? marketCondition,
|
||||||
|
string? goal)
|
||||||
|
{
|
||||||
|
var reasons = new List<string>();
|
||||||
|
var signalType = GetSignalType(type);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tradingStyle))
|
||||||
|
{
|
||||||
|
reasons.Add($"Suitable for {tradingStyle} trading style");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signalType == SignalType.Signal)
|
||||||
|
{
|
||||||
|
reasons.Add("Generates precise entry/exit signals");
|
||||||
|
}
|
||||||
|
else if (signalType == SignalType.Trend)
|
||||||
|
{
|
||||||
|
reasons.Add("Identifies market direction and momentum");
|
||||||
|
}
|
||||||
|
else if (signalType == SignalType.Context)
|
||||||
|
{
|
||||||
|
reasons.Add("Provides market context and risk filtering");
|
||||||
|
}
|
||||||
|
|
||||||
|
return reasons;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> GetRefinedParameterValues(
|
||||||
|
IndicatorType type,
|
||||||
|
string parameterName,
|
||||||
|
string? tradingStyle,
|
||||||
|
string? marketVolatility)
|
||||||
|
{
|
||||||
|
var refined = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
// Default recommendations based on trading style
|
||||||
|
if (tradingStyle != null)
|
||||||
|
{
|
||||||
|
var styleLower = tradingStyle.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (styleLower.Contains("scalp") || styleLower.Contains("short"))
|
||||||
|
{
|
||||||
|
refined["Short-term"] = GetShortTermParameterValue(type, parameterName);
|
||||||
|
}
|
||||||
|
else if (styleLower.Contains("swing") || styleLower.Contains("medium"))
|
||||||
|
{
|
||||||
|
refined["Medium-term"] = GetMediumTermParameterValue(type, parameterName);
|
||||||
|
}
|
||||||
|
else if (styleLower.Contains("position") || styleLower.Contains("long"))
|
||||||
|
{
|
||||||
|
refined["Long-term"] = GetLongTermParameterValue(type, parameterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Volatility adjustments
|
||||||
|
if (marketVolatility != null)
|
||||||
|
{
|
||||||
|
var volLower = marketVolatility.ToLowerInvariant();
|
||||||
|
if (volLower.Contains("high"))
|
||||||
|
{
|
||||||
|
refined["High Volatility"] = GetHighVolatilityParameterValue(type, parameterName);
|
||||||
|
}
|
||||||
|
else if (volLower.Contains("low"))
|
||||||
|
{
|
||||||
|
refined["Low Volatility"] = GetLowVolatilityParameterValue(type, parameterName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return refined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetShortTermParameterValue(IndicatorType type, string parameterName)
|
||||||
|
{
|
||||||
|
// Short-term trading recommendations
|
||||||
|
return parameterName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"period" => type switch
|
||||||
|
{
|
||||||
|
IndicatorType.RsiDivergence => "3-5",
|
||||||
|
IndicatorType.EmaCross => "10-20",
|
||||||
|
IndicatorType.EmaTrend => "10-20",
|
||||||
|
_ => "5-10"
|
||||||
|
},
|
||||||
|
"lookbackperiod" => "3-5",
|
||||||
|
_ => "Shorter periods for responsiveness"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetMediumTermParameterValue(IndicatorType type, string parameterName)
|
||||||
|
{
|
||||||
|
return parameterName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"period" => type switch
|
||||||
|
{
|
||||||
|
IndicatorType.RsiDivergence => "5-10",
|
||||||
|
IndicatorType.EmaCross => "20-50",
|
||||||
|
IndicatorType.EmaTrend => "20-50",
|
||||||
|
_ => "10-20"
|
||||||
|
},
|
||||||
|
"lookbackperiod" => "5-10",
|
||||||
|
_ => "Standard periods for balanced performance"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLongTermParameterValue(IndicatorType type, string parameterName)
|
||||||
|
{
|
||||||
|
return parameterName.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"period" => type switch
|
||||||
|
{
|
||||||
|
IndicatorType.RsiDivergence => "10-15",
|
||||||
|
IndicatorType.EmaCross => "50-100",
|
||||||
|
IndicatorType.EmaTrend => "50-100",
|
||||||
|
_ => "20-30"
|
||||||
|
},
|
||||||
|
"lookbackperiod" => "10-15",
|
||||||
|
_ => "Longer periods for stability"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetHighVolatilityParameterValue(IndicatorType type, string parameterName)
|
||||||
|
{
|
||||||
|
return "Longer periods to reduce noise";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLowVolatilityParameterValue(IndicatorType type, string parameterName)
|
||||||
|
{
|
||||||
|
return "Shorter periods to capture subtle patterns";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetParameterRefinementReasoning(
|
||||||
|
IndicatorType type,
|
||||||
|
string parameterName,
|
||||||
|
string? tradingStyle,
|
||||||
|
string? marketVolatility)
|
||||||
|
{
|
||||||
|
var reasoning = new List<string>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(tradingStyle))
|
||||||
|
{
|
||||||
|
reasoning.Add($"Adjusted for {tradingStyle} trading style");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(marketVolatility))
|
||||||
|
{
|
||||||
|
reasoning.Add($"Optimized for {marketVolatility} market conditions");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(". ", reasoning);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetGeneralParameterAdvice(IndicatorType type, string? tradingStyle, string? marketVolatility)
|
||||||
|
{
|
||||||
|
var advice = new List<string>();
|
||||||
|
|
||||||
|
if (tradingStyle != null)
|
||||||
|
{
|
||||||
|
var styleLower = tradingStyle.ToLowerInvariant();
|
||||||
|
if (styleLower.Contains("scalp"))
|
||||||
|
{
|
||||||
|
advice.Add("Use shorter periods for quick signals");
|
||||||
|
}
|
||||||
|
else if (styleLower.Contains("swing"))
|
||||||
|
{
|
||||||
|
advice.Add("Use medium periods for balanced performance");
|
||||||
|
}
|
||||||
|
else if (styleLower.Contains("position"))
|
||||||
|
{
|
||||||
|
advice.Add("Use longer periods for trend confirmation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marketVolatility != null)
|
||||||
|
{
|
||||||
|
var volLower = marketVolatility.ToLowerInvariant();
|
||||||
|
if (volLower.Contains("high"))
|
||||||
|
{
|
||||||
|
advice.Add("Consider longer periods to filter out noise");
|
||||||
|
}
|
||||||
|
else if (volLower.Contains("low"))
|
||||||
|
{
|
||||||
|
advice.Add("Shorter periods can capture subtle movements");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", advice);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GetIndicatorExamples(IndicatorType type)
|
||||||
|
{
|
||||||
|
return new List<string>
|
||||||
|
{
|
||||||
|
$"Example: Use {GetIndicatorName(type)} to identify entry points",
|
||||||
|
$"Combine with trend indicators for confirmation",
|
||||||
|
$"Backtest with different parameters to find optimal settings"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetBestForDescription(IndicatorType type, IndicatorInfo? info)
|
||||||
|
{
|
||||||
|
var signalType = GetSignalType(type);
|
||||||
|
return signalType switch
|
||||||
|
{
|
||||||
|
SignalType.Signal => "Precise entry/exit signals",
|
||||||
|
SignalType.Trend => "Trend identification and confirmation",
|
||||||
|
SignalType.Context => "Market context and risk filtering",
|
||||||
|
_ => "General trading analysis"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateComparisonSummary(List<dynamic> comparisons)
|
||||||
|
{
|
||||||
|
var signalTypes = comparisons.Select(c => c.SignalType).Distinct().ToList();
|
||||||
|
var categories = comparisons.Select(c => c.Category).Distinct().ToList();
|
||||||
|
|
||||||
|
return $"Comparing {comparisons.Count} indicators across {signalTypes.Count} signal types and {categories.Count} categories";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<IndicatorType, IndicatorInfo> LoadDefaultIndicatorInfo()
|
||||||
|
{
|
||||||
|
var info = new Dictionary<IndicatorType, IndicatorInfo>();
|
||||||
|
|
||||||
|
foreach (var type in Enum.GetValues<IndicatorType>())
|
||||||
|
{
|
||||||
|
if (type == IndicatorType.Composite) continue;
|
||||||
|
info[type] = GetDefaultIndicatorInfo(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IndicatorInfo GetDefaultIndicatorInfo(IndicatorType type)
|
||||||
|
{
|
||||||
|
return new IndicatorInfo
|
||||||
|
{
|
||||||
|
Name = GetIndicatorName(type),
|
||||||
|
Description = $"{GetIndicatorName(type)} indicator",
|
||||||
|
LongDescription = $"The {GetIndicatorName(type)} indicator provides trading signals based on technical analysis.",
|
||||||
|
TriggersLong = new List<string> { "Generates long signals based on indicator logic" },
|
||||||
|
TriggersShort = new List<string> { "Generates short signals based on indicator logic" },
|
||||||
|
Parameters = new List<ParameterInfo>(),
|
||||||
|
RecommendedParameters = new Dictionary<string, string>(),
|
||||||
|
UseCases = ExtractUseCases("", type),
|
||||||
|
TradingStyleRecommendations = new Dictionary<string, string>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Helper Classes
|
||||||
|
|
||||||
|
private class IndicatorInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string LongDescription { get; set; } = string.Empty;
|
||||||
|
public List<string> TriggersLong { get; set; } = new();
|
||||||
|
public List<string> TriggersShort { get; set; } = new();
|
||||||
|
public List<ParameterInfo> Parameters { get; set; } = new();
|
||||||
|
public Dictionary<string, string> RecommendedParameters { get; set; } = new();
|
||||||
|
public List<string> UseCases { get; set; } = new();
|
||||||
|
public Dictionary<string, string> TradingStyleRecommendations { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ParameterInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string RecommendedValue { get; set; } = string.Empty;
|
||||||
|
public string? Range { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class IndicatorRecommendation
|
||||||
|
{
|
||||||
|
public string IndicatorType { get; set; } = string.Empty;
|
||||||
|
public string SignalType { get; set; } = string.Empty;
|
||||||
|
public string Category { get; set; } = string.Empty;
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public int Score { get; set; }
|
||||||
|
public List<string> Reasons { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ParameterRefinement
|
||||||
|
{
|
||||||
|
public string ParameterName { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string CurrentRecommended { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, string> RefinedValues { get; set; } = new();
|
||||||
|
public string Reasoning { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user