diff --git a/src/Managing.Application/LLM/McpService.cs b/src/Managing.Application/LLM/McpService.cs deleted file mode 100644 index 49235d64..00000000 --- a/src/Managing.Application/LLM/McpService.cs +++ /dev/null @@ -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; - -/// -/// Service for executing Model Context Protocol (MCP) tools -/// -public class McpService : IMcpService -{ - private readonly BacktestTools _backtestTools; - private readonly ILogger _logger; - - public McpService(BacktestTools backtestTools, ILogger logger) - { - _backtestTools = backtestTools; - _logger = logger; - } - - public async Task ExecuteToolAsync(User user, string toolName, Dictionary? 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> GetAvailableToolsAsync() - { - var tools = new List - { - 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 - { - ["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>(tools); - } - - private async Task ExecuteGetBacktestsPaginated(User user, Dictionary? parameters) - { - var page = GetParameterValue(parameters, "page", 1); - var pageSize = GetParameterValue(parameters, "pageSize", 50); - var sortByString = GetParameterValue(parameters, "sortBy", "Score"); - var sortOrder = GetParameterValue(parameters, "sortOrder", "desc"); - var scoreMin = GetParameterValue(parameters, "scoreMin", null); - var scoreMax = GetParameterValue(parameters, "scoreMax", null); - var winrateMin = GetParameterValue(parameters, "winrateMin", null); - var winrateMax = GetParameterValue(parameters, "winrateMax", null); - var maxDrawdownMax = GetParameterValue(parameters, "maxDrawdownMax", null); - var tickers = GetParameterValue(parameters, "tickers", null); - var indicators = GetParameterValue(parameters, "indicators", null); - var durationMinDays = GetParameterValue(parameters, "durationMinDays", null); - var durationMaxDays = GetParameterValue(parameters, "durationMaxDays", null); - var name = GetParameterValue(parameters, "name", null); - var tradingTypeString = GetParameterValue(parameters, "tradingType", null); - - // Parse sortBy enum - if (!Enum.TryParse(sortByString, true, out var sortBy)) - { - sortBy = BacktestSortableColumn.Score; - } - - // Parse tradingType enum - TradingType? tradingType = null; - if (!string.IsNullOrWhiteSpace(tradingTypeString) && - Enum.TryParse(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(Dictionary? 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; - } - } -} diff --git a/src/Managing.Bootstrap/ApiBootstrap.cs b/src/Managing.Bootstrap/ApiBootstrap.cs index 30e3ee5a..5b45d543 100644 --- a/src/Managing.Bootstrap/ApiBootstrap.cs +++ b/src/Managing.Bootstrap/ApiBootstrap.cs @@ -427,8 +427,19 @@ public static class ApiBootstrap // LLM and MCP services services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + + // MCP Tools (underlying implementations) services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // MCP Tool Wrappers (with tool definitions) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } diff --git a/src/Managing.Mcp/McpService.cs b/src/Managing.Mcp/McpService.cs new file mode 100644 index 00000000..535a7ed6 --- /dev/null +++ b/src/Managing.Mcp/McpService.cs @@ -0,0 +1,103 @@ +using Managing.Application.Abstractions.Services; +using Managing.Mcp.McpTools; +using Managing.Domain.Users; +using Microsoft.Extensions.Logging; + +namespace Managing.Mcp; + +/// +/// Service for executing Model Context Protocol (MCP) tools +/// +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 _logger; + + public McpService( + BacktestMcpTools backtestMcpTools, + DataMcpTools dataMcpTools, + // BotMcpTools botMcpTools, // TODO: Fix BotMcpTools implementation + IndicatorMcpTools indicatorMcpTools, + ILogger logger) + { + _backtestMcpTools = backtestMcpTools; + _dataMcpTools = dataMcpTools; + // _botMcpTools = botMcpTools; // TODO: Fix BotMcpTools implementation + _indicatorMcpTools = indicatorMcpTools; + _logger = logger; + } + + public async Task ExecuteToolAsync(User user, string toolName, Dictionary? 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> GetAvailableToolsAsync() + { + var allTools = new List(); + + // 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>(allTools); + } +} diff --git a/src/Managing.Mcp/McpTools/BacktestMcpTools.cs b/src/Managing.Mcp/McpTools/BacktestMcpTools.cs new file mode 100644 index 00000000..2b3b57e3 --- /dev/null +++ b/src/Managing.Mcp/McpTools/BacktestMcpTools.cs @@ -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; + +/// +/// MCP tools for backtest operations +/// +public class BacktestMcpTools : BaseMcpTool +{ + private readonly BacktestTools _backtestTools; + + public BacktestMcpTools(BacktestTools backtestTools) + { + _backtestTools = backtestTools; + } + + public override IEnumerable GetToolDefinitions() + { + return new List + { + 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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 ExecuteGetBacktestsPaginated(User user, Dictionary? parameters) + { + var page = GetParameterValue(parameters, "page", 1); + var pageSize = GetParameterValue(parameters, "pageSize", 50); + var sortByString = GetParameterValue(parameters, "sortBy", "Score"); + var sortOrder = GetParameterValue(parameters, "sortOrder", "desc"); + var scoreMin = GetParameterValue(parameters, "scoreMin", null); + var scoreMax = GetParameterValue(parameters, "scoreMax", null); + var winrateMin = GetParameterValue(parameters, "winrateMin", null); + var winrateMax = GetParameterValue(parameters, "winrateMax", null); + var maxDrawdownMax = GetParameterValue(parameters, "maxDrawdownMax", null); + var tickers = GetParameterValue(parameters, "tickers", null); + var indicators = GetParameterValue(parameters, "indicators", null); + var durationMinDays = GetParameterValue(parameters, "durationMinDays", null); + var durationMaxDays = GetParameterValue(parameters, "durationMaxDays", null); + var name = GetParameterValue(parameters, "name", null); + var tradingTypeString = GetParameterValue(parameters, "tradingType", null); + + // Parse sortBy enum + if (!Enum.TryParse(sortByString, true, out var sortBy)) + { + sortBy = BacktestSortableColumn.Score; + } + + // Parse tradingType enum + TradingType? tradingType = null; + if (!string.IsNullOrWhiteSpace(tradingTypeString) && + Enum.TryParse(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 ExecuteGetBacktestById(User user, Dictionary? parameters) + { + var id = GetParameterValue(parameters, "id", string.Empty); + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Backtest ID is required"); + } + + return await _backtestTools.GetBacktestById(user, id); + } + + public async Task ExecuteDeleteBacktest(User user, Dictionary? parameters) + { + var id = GetParameterValue(parameters, "id", string.Empty); + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentException("Backtest ID is required"); + } + + return await _backtestTools.DeleteBacktest(user, id); + } + + public async Task ExecuteDeleteBacktestsByIds(User user, Dictionary? parameters) + { + var idsString = GetParameterValue(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 ExecuteDeleteBacktestsByFilters(User user, Dictionary? parameters) + { + var scoreMin = GetParameterValue(parameters, "scoreMin", null); + var scoreMax = GetParameterValue(parameters, "scoreMax", null); + var winrateMin = GetParameterValue(parameters, "winrateMin", null); + var winrateMax = GetParameterValue(parameters, "winrateMax", null); + var name = GetParameterValue(parameters, "name", null); + + return await _backtestTools.DeleteBacktestsByFilters(user, scoreMin, scoreMax, winrateMin, winrateMax, name); + } + + public async Task ExecuteGetBundleBacktestsPaginated(User user, Dictionary? parameters) + { + var page = GetParameterValue(parameters, "page", 1); + var pageSize = GetParameterValue(parameters, "pageSize", 50); + var sortByString = GetParameterValue(parameters, "sortBy", "CreatedAt"); + var sortOrder = GetParameterValue(parameters, "sortOrder", "desc"); + var statusString = GetParameterValue(parameters, "status", null); + var name = GetParameterValue(parameters, "name", null); + + return await _backtestTools.GetBundleBacktestsPaginated(user, page, pageSize, sortByString, sortOrder, statusString, name); + } + + public async Task ExecuteGetBundleBacktestById(User user, Dictionary? parameters) + { + var idString = GetParameterValue(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 ExecuteDeleteBundleBacktest(User user, Dictionary? parameters) + { + var idString = GetParameterValue(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 ExecuteRunBacktest(User user, Dictionary? parameters) + { + var ticker = GetParameterValue(parameters, "ticker", string.Empty); + var timeframe = GetParameterValue(parameters, "timeframe", string.Empty); + var startDateString = GetParameterValue(parameters, "startDate", string.Empty); + var endDateString = GetParameterValue(parameters, "endDate", string.Empty); + var balance = GetParameterValue(parameters, "balance", 0); + var scenarioName = GetParameterValue(parameters, "scenarioName", null); + var moneyManagementName = GetParameterValue(parameters, "moneyManagementName", null); + var stopLoss = GetParameterValue(parameters, "stopLoss", null); + var takeProfit = GetParameterValue(parameters, "takeProfit", null); + var leverage = GetParameterValue(parameters, "leverage", null); + var name = GetParameterValue(parameters, "name", null); + var save = GetParameterValue(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); + } +} diff --git a/src/Managing.Mcp/McpTools/BaseMcpTool.cs b/src/Managing.Mcp/McpTools/BaseMcpTool.cs new file mode 100644 index 00000000..b6a8c88d --- /dev/null +++ b/src/Managing.Mcp/McpTools/BaseMcpTool.cs @@ -0,0 +1,50 @@ +using Managing.Application.Abstractions.Services; + +namespace Managing.Mcp.McpTools; + +/// +/// Base class for MCP tools providing common functionality +/// +public abstract class BaseMcpTool +{ + /// + /// Gets the tool definitions for this tool + /// + public abstract IEnumerable GetToolDefinitions(); + + /// + /// Helper method to extract parameter values with type conversion and default values + /// + protected T GetParameterValue(Dictionary? 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; + } + } +} diff --git a/src/Managing.Mcp/McpTools/BotMcpTools.cs b/src/Managing.Mcp/McpTools/BotMcpTools.cs new file mode 100644 index 00000000..bdaccafc --- /dev/null +++ b/src/Managing.Mcp/McpTools/BotMcpTools.cs @@ -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; + +/// +/// MCP tools for bot operations +/// +public class BotMcpTools : BaseMcpTool +{ + private readonly BotTools _botTools; + + public BotMcpTools(BotTools botTools) + { + _botTools = botTools; + } + + public override IEnumerable GetToolDefinitions() + { + return new List + { + 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 + { + ["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 + { + ["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 + // { + // ["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 + { + ["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 + { + ["identifier"] = new McpParameterDefinition + { + Type = "string", + Description = "The identifier (GUID) of the bot to delete", + Required = true + } + } + } + }; + } + + public async Task ExecuteGetBotsPaginated(User user, Dictionary? parameters) + { + var pageNumber = GetParameterValue(parameters, "pageNumber", 1); + var pageSize = GetParameterValue(parameters, "pageSize", 10); + var statusString = GetParameterValue(parameters, "status", null); + var name = GetParameterValue(parameters, "name", null); + var ticker = GetParameterValue(parameters, "ticker", null); + var agentName = GetParameterValue(parameters, "agentName", null); + var minBalance = GetParameterValue(parameters, "minBalance", null); + var maxBalance = GetParameterValue(parameters, "maxBalance", null); + var sortByString = GetParameterValue(parameters, "sortBy", "CreateDate"); + var sortDirectionString = GetParameterValue(parameters, "sortDirection", "Desc"); + var showOnlyProfitable = GetParameterValue(parameters, "showOnlyProfitable", false); + + // Parse status enum + BotStatus? status = null; + if (!string.IsNullOrWhiteSpace(statusString) && + Enum.TryParse(statusString, true, out var parsedStatus)) + { + status = parsedStatus; + } + + // Parse sortBy enum + if (!Enum.TryParse(sortByString, true, out var sortBy)) + { + sortBy = BotSortableColumn.CreateDate; + } + + // Parse sortDirection enum + if (!Enum.TryParse(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 ExecuteGetBotById(User user, Dictionary? parameters) + { + var identifierString = GetParameterValue(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 ExecuteGetBotConfig(User user, Dictionary? parameters) + // { + // var identifierString = GetParameterValue(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 ExecuteGetBotsByStatus(User user, Dictionary? parameters) + { + var statusString = GetParameterValue(parameters, "status", string.Empty); + if (string.IsNullOrWhiteSpace(statusString) || !Enum.TryParse(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 ExecuteDeleteBot(User user, Dictionary? parameters) + { + var identifierString = GetParameterValue(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); + } +} + diff --git a/src/Managing.Mcp/McpTools/DataMcpTools.cs b/src/Managing.Mcp/McpTools/DataMcpTools.cs new file mode 100644 index 00000000..56b3a2cc --- /dev/null +++ b/src/Managing.Mcp/McpTools/DataMcpTools.cs @@ -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; + +/// +/// MCP tools for data operations (tickers, candles, statistics, agents) +/// +public class DataMcpTools : BaseMcpTool +{ + private readonly DataTools _dataTools; + + public DataMcpTools(DataTools dataTools) + { + _dataTools = dataTools; + } + + public override IEnumerable GetToolDefinitions() + { + return new List + { + new McpToolDefinition + { + Name = "get_tickers", + Description = "Retrieves available tickers for a given timeframe.", + Parameters = new Dictionary + { + ["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 + { + ["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() + }, + new McpToolDefinition + { + Name = "get_agent_balances", + Description = "Retrieves balance history for a specific agent within a date range.", + Parameters = new Dictionary + { + ["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() + }, + new McpToolDefinition + { + Name = "get_agents_paginated", + Description = "Retrieves paginated agent summaries with filtering and sorting capabilities.", + Parameters = new Dictionary + { + ["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 ExecuteGetTickers(User user, Dictionary? parameters) + { + var timeframeString = GetParameterValue(parameters, "timeframe", string.Empty); + if (string.IsNullOrWhiteSpace(timeframeString) || + !Enum.TryParse(timeframeString, true, out var timeframe)) + { + throw new ArgumentException("Valid timeframe is required"); + } + + return await _dataTools.GetTickers(timeframe); + } + + public async Task ExecuteGetCandles(User user, Dictionary? parameters) + { + var tickerString = GetParameterValue(parameters, "ticker", string.Empty); + var startDateString = GetParameterValue(parameters, "startDate", string.Empty); + var endDateString = GetParameterValue(parameters, "endDate", string.Empty); + var timeframeString = GetParameterValue(parameters, "timeframe", string.Empty); + + if (string.IsNullOrWhiteSpace(tickerString) || + !Enum.TryParse(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(timeframeString, true, out var timeframe)) + { + throw new ArgumentException("Valid timeframe is required"); + } + + return await _dataTools.GetCandles(ticker, startDate, endDate, timeframe); + } + + public async Task ExecuteGetSpotlight(User user, Dictionary? parameters) + { + return await _dataTools.GetSpotlight(); + } + + public async Task ExecuteGetAgentBalances(User user, Dictionary? parameters) + { + var agentName = GetParameterValue(parameters, "agentName", string.Empty); + var startDateString = GetParameterValue(parameters, "startDate", string.Empty); + var endDateString = GetParameterValue(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 ExecuteGetOnlineAgents(User user, Dictionary? parameters) + { + return await _dataTools.GetOnlineAgents(); + } + + public async Task ExecuteGetAgentsPaginated(User user, Dictionary? parameters) + { + var page = GetParameterValue(parameters, "page", 1); + var pageSize = GetParameterValue(parameters, "pageSize", 10); + var sortBy = GetParameterValue(parameters, "sortBy", "NetPnL"); + var sortOrder = GetParameterValue(parameters, "sortOrder", "desc"); + var agentNames = GetParameterValue(parameters, "agentNames", null); + var showOnlyProfitable = GetParameterValue(parameters, "showOnlyProfitable", false); + + return await _dataTools.GetAgentsPaginated(page, pageSize, sortBy, sortOrder, agentNames, showOnlyProfitable); + } +} diff --git a/src/Managing.Mcp/McpTools/IndicatorMcpTools.cs b/src/Managing.Mcp/McpTools/IndicatorMcpTools.cs new file mode 100644 index 00000000..2c90d3d3 --- /dev/null +++ b/src/Managing.Mcp/McpTools/IndicatorMcpTools.cs @@ -0,0 +1,222 @@ +using Managing.Application.Abstractions.Services; +using Managing.Domain.Users; +using Managing.Mcp.Tools; + +namespace Managing.Mcp.McpTools; + +/// +/// MCP tools for indicator operations (list, explain, recommend, refine, compare) +/// +public class IndicatorMcpTools : BaseMcpTool +{ + private readonly IndicatorTools _indicatorTools; + + public IndicatorMcpTools(IndicatorTools indicatorTools) + { + _indicatorTools = indicatorTools; + } + + public override IEnumerable GetToolDefinitions() + { + return new List + { + 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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["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 + { + ["indicatorTypes"] = new McpParameterDefinition + { + Type = "string", + Description = "Comma-separated list of indicator types to compare (e.g., 'RsiDivergence,MacdCross,EmaCross')", + Required = true + } + } + } + }; + } + + public async Task ExecuteListIndicators(User user, Dictionary? parameters) + { + var signalType = GetParameterValue(parameters, "signalType", null); + var category = GetParameterValue(parameters, "category", null); + + return await Task.FromResult(_indicatorTools.ListIndicators(signalType, category)); + } + + public async Task ExecuteGetIndicatorInfo(User user, Dictionary? parameters) + { + var indicatorType = GetParameterValue(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 ExecuteExplainIndicator(User user, Dictionary? parameters) + { + var indicatorType = GetParameterValue(parameters, "indicatorType", string.Empty); + if (string.IsNullOrWhiteSpace(indicatorType)) + { + throw new ArgumentException("Indicator type is required"); + } + + var includeExamples = GetParameterValue(parameters, "includeExamples", true); + + return await Task.FromResult(_indicatorTools.ExplainIndicator(indicatorType, includeExamples)); + } + + public async Task ExecuteRecommendIndicators(User user, Dictionary? parameters) + { + var tradingStyle = GetParameterValue(parameters, "tradingStyle", null); + var marketCondition = GetParameterValue(parameters, "marketCondition", null); + var goal = GetParameterValue(parameters, "goal", null); + var maxRecommendations = GetParameterValue(parameters, "maxRecommendations", null); + + return await Task.FromResult(_indicatorTools.RecommendIndicators( + tradingStyle, marketCondition, goal, maxRecommendations)); + } + + public async Task ExecuteRefineIndicatorParameters(User user, Dictionary? parameters) + { + var indicatorType = GetParameterValue(parameters, "indicatorType", string.Empty); + if (string.IsNullOrWhiteSpace(indicatorType)) + { + throw new ArgumentException("Indicator type is required"); + } + + var tradingStyle = GetParameterValue(parameters, "tradingStyle", null); + var marketVolatility = GetParameterValue(parameters, "marketVolatility", null); + + return await Task.FromResult(_indicatorTools.RefineIndicatorParameters( + indicatorType, tradingStyle, marketVolatility)); + } + + public async Task ExecuteCompareIndicators(User user, Dictionary? parameters) + { + var indicatorTypes = GetParameterValue(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)); + } +} + diff --git a/src/Managing.Mcp/Tools/BacktestTools.cs b/src/Managing.Mcp/Tools/BacktestTools.cs index 0f717e56..dbbb0625 100644 --- a/src/Managing.Mcp/Tools/BacktestTools.cs +++ b/src/Managing.Mcp/Tools/BacktestTools.cs @@ -1,5 +1,8 @@ using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Shared; +using Managing.Domain.Backtests; +using Managing.Domain.Bots; +using Managing.Domain.MoneyManagements; using Managing.Domain.Users; using Microsoft.Extensions.Logging; using static Managing.Common.Enums; @@ -12,11 +15,16 @@ namespace Managing.Mcp.Tools; public class BacktestTools { private readonly IBacktester _backtester; + private readonly IAccountService _accountService; private readonly ILogger _logger; - public BacktestTools(IBacktester backtester, ILogger logger) + public BacktestTools( + IBacktester backtester, + IAccountService accountService, + ILogger logger) { _backtester = backtester; + _accountService = accountService; _logger = logger; } @@ -134,4 +142,359 @@ public class BacktestTools throw new InvalidOperationException($"Failed to retrieve backtests: {ex.Message}", ex); } } + + /// + /// Retrieves a specific backtest by ID for the user + /// + public async Task 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); + } + } + + /// + /// Deletes a specific backtest by ID for the user + /// + public async Task 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); + } + } + + /// + /// Deletes multiple backtests by their IDs for the user + /// + public async Task DeleteBacktestsByIds(User user, IEnumerable 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); + } + } + + /// + /// Deletes backtests matching specified filters for the user + /// + public async Task 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); + } + } + + /// + /// Retrieves paginated bundle backtest requests for the user + /// + public async Task 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(sortByString, true, out var sortBy)) + { + sortBy = BundleBacktestRequestSortableColumn.CreatedAt; + } + + // Parse status enum + BundleBacktestRequestStatus? status = null; + if (!string.IsNullOrWhiteSpace(statusString) && + Enum.TryParse(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); + } + } + + /// + /// Retrieves a specific bundle backtest request by ID + /// + public async Task 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); + } + } + + /// + /// Deletes a bundle backtest request and all associated backtests + /// + public async Task 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); + } + } + + /// + /// Runs a new backtest with the specified configuration + /// + public async Task 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, true, out var parsedTicker)) + { + throw new ArgumentException($"Invalid ticker: {ticker}"); + } + + // Parse timeframe enum + if (!Enum.TryParse(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); + } + } } \ No newline at end of file diff --git a/src/Managing.Mcp/Tools/BotTools.cs b/src/Managing.Mcp/Tools/BotTools.cs new file mode 100644 index 00000000..6f9dccab --- /dev/null +++ b/src/Managing.Mcp/Tools/BotTools.cs @@ -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; + +/// +/// MCP tools for bot operations +/// +public class BotTools +{ + private readonly IBotRepository _botRepository; + private readonly ILogger _logger; + + public BotTools(IBotRepository botRepository, ILogger logger) + { + _botRepository = botRepository; + _logger = logger; + } + + /// + /// Retrieves paginated bots for a user with filtering and sorting capabilities + /// + public async Task 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); + } + } + + /// + /// Retrieves a specific bot by identifier for the user + /// + public async Task 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); + } + } + + + /// + /// Retrieves bots by status for the user + /// + public async Task 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); + } + } + + /// + /// Deletes a bot by identifier for the user + /// + public async Task 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); + } + } +} + diff --git a/src/Managing.Mcp/Tools/DataTools.cs b/src/Managing.Mcp/Tools/DataTools.cs new file mode 100644 index 00000000..29dccd8f --- /dev/null +++ b/src/Managing.Mcp/Tools/DataTools.cs @@ -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; + +/// +/// MCP tools for data operations (tickers, candles, statistics) +/// +public class DataTools +{ + private readonly IExchangeService _exchangeService; + private readonly IStatisticService _statisticService; + private readonly IAgentService _agentService; + private readonly ICacheService _cacheService; + private readonly ILogger _logger; + + public DataTools( + IExchangeService exchangeService, + IStatisticService statisticService, + IAgentService agentService, + ICacheService cacheService, + ILogger logger) + { + _exchangeService = exchangeService; + _statisticService = statisticService; + _agentService = agentService; + _cacheService = cacheService; + _logger = logger; + } + + /// + /// Retrieves available tickers for a given timeframe + /// + public async Task 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); + } + } + + /// + /// Retrieves candles for a specific ticker and date range + /// + public async Task 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); + } + } + + /// + /// Retrieves platform statistics and spotlight data + /// + public async Task 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); + } + } + + /// + /// Retrieves agent balance history within a date range + /// + public async Task 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); + } + } + + /// + /// Retrieves list of online agents + /// + public async Task 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); + } + } + + /// + /// Retrieves paginated agent summaries with filtering and sorting + /// + public async Task 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); + } + } +} diff --git a/src/Managing.Mcp/Tools/IndicatorTools.cs b/src/Managing.Mcp/Tools/IndicatorTools.cs new file mode 100644 index 00000000..df8afee3 --- /dev/null +++ b/src/Managing.Mcp/Tools/IndicatorTools.cs @@ -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; + +/// +/// MCP tools for indicator operations (list, explain, recommend, refine, compare) +/// +public class IndicatorTools +{ + private readonly ILogger _logger; + private readonly Dictionary _indicatorInfoCache; + + public IndicatorTools(ILogger logger) + { + _logger = logger; + _indicatorInfoCache = LoadIndicatorInfo(); + } + + /// + /// Lists all available indicators with their types and basic information + /// + public object ListIndicators(string? signalType = null, string? category = null) + { + try + { + var indicators = Enum.GetValues() + .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, 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); + } + } + + /// + /// Gets detailed information about a specific indicator + /// + public object GetIndicatorInfo(string indicatorType) + { + try + { + if (!Enum.TryParse(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); + } + } + + /// + /// Explains how an indicator works, when to use it, and provides examples + /// + public object ExplainIndicator(string indicatorType, bool includeExamples = true) + { + try + { + if (!Enum.TryParse(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); + } + } + + /// + /// Recommends indicators based on trading style, market conditions, and goals + /// + public object RecommendIndicators( + string? tradingStyle = null, + string? marketCondition = null, + string? goal = null, + int? maxRecommendations = null) + { + try + { + var recommendations = new List(); + + foreach (var type in Enum.GetValues()) + { + 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); + } + } + + /// + /// Suggests parameter refinements for an indicator based on trading style + /// + public object RefineIndicatorParameters( + string indicatorType, + string? tradingStyle = null, + string? marketVolatility = null) + { + try + { + if (!Enum.TryParse(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(); + + 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); + } + } + + /// + /// Compares multiple indicators side by side + /// + public object CompareIndicators(string indicatorTypes) + { + try + { + var types = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(t => t.Trim()) + .Where(t => Enum.TryParse(t, true, out _)) + .Select(t => Enum.Parse(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(), + Parameters = info?.Parameters.Select(p => p.Name).ToList() ?? new List(), + BestFor = GetBestForDescription(type, info) + }; + }).ToList(); + + return new + { + Indicators = comparisons, + Count = comparisons.Count, + ComparisonSummary = GenerateComparisonSummary(comparisons.Cast().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 LoadIndicatorInfo() + { + var info = new Dictionary(); + 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 ParseIndicatorInfoFromReadme(string content) + { + var info = new Dictionary(); + + // 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()) + { + 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().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 ExtractListItems(string section, string header) + { + var items = new List(); + 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().Select(m => m.Groups[1].Value.Trim())); + } + + return items; + } + + private List ExtractParameters(string section) + { + var parameters = new List(); + 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 ExtractRecommendedParameters(string section) + { + var recommended = new Dictionary(); + 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 ExtractUseCases(string section, IndicatorType type) + { + var useCases = new List(); + 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 ExtractTradingStyleRecommendations(string section, IndicatorType type) + { + var recommendations = new Dictionary(); + + // 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 GetRecommendationReasons( + IndicatorType type, + IndicatorInfo info, + string? tradingStyle, + string? marketCondition, + string? goal) + { + var reasons = new List(); + 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 GetRefinedParameterValues( + IndicatorType type, + string parameterName, + string? tradingStyle, + string? marketVolatility) + { + var refined = new Dictionary(); + + // 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(); + + 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(); + + 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 GetIndicatorExamples(IndicatorType type) + { + return new List + { + $"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 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 LoadDefaultIndicatorInfo() + { + var info = new Dictionary(); + + foreach (var type in Enum.GetValues()) + { + 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 { "Generates long signals based on indicator logic" }, + TriggersShort = new List { "Generates short signals based on indicator logic" }, + Parameters = new List(), + RecommendedParameters = new Dictionary(), + UseCases = ExtractUseCases("", type), + TradingStyleRecommendations = new Dictionary() + }; + } + + #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 TriggersLong { get; set; } = new(); + public List TriggersShort { get; set; } = new(); + public List Parameters { get; set; } = new(); + public Dictionary RecommendedParameters { get; set; } = new(); + public List UseCases { get; set; } = new(); + public Dictionary 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 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 RefinedValues { get; set; } = new(); + public string Reasoning { get; set; } = string.Empty; + } + + #endregion +} +