Remove McpService and refactor dependency injection for MCP tools

- Deleted the McpService class, which was previously responsible for executing Model Context Protocol (MCP) tools.
- Updated the ApiBootstrap class to change the registration of IMcpService to the new Managing.Mcp.McpService implementation.
- Added new MCP tool implementations for DataTools, BotTools, and IndicatorTools to enhance functionality.
This commit is contained in:
2026-01-03 22:55:27 +07:00
parent 6f55566db3
commit 8ce7650bbf
12 changed files with 3234 additions and 238 deletions

View File

@@ -1,236 +0,0 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Managing.Mcp.Tools;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.LLM;
/// <summary>
/// Service for executing Model Context Protocol (MCP) tools
/// </summary>
public class McpService : IMcpService
{
private readonly BacktestTools _backtestTools;
private readonly ILogger<McpService> _logger;
public McpService(BacktestTools backtestTools, ILogger<McpService> logger)
{
_backtestTools = backtestTools;
_logger = logger;
}
public async Task<object> ExecuteToolAsync(User user, string toolName, Dictionary<string, object>? parameters = null)
{
_logger.LogInformation("Executing MCP tool: {ToolName} for user: {UserId}", toolName, user.Id);
try
{
return toolName.ToLowerInvariant() switch
{
"get_backtests_paginated" => await ExecuteGetBacktestsPaginated(user, parameters),
_ => throw new InvalidOperationException($"Unknown tool: {toolName}")
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing MCP tool {ToolName} for user {UserId}", toolName, user.Id);
throw;
}
}
public Task<IEnumerable<McpToolDefinition>> GetAvailableToolsAsync()
{
var tools = new List<McpToolDefinition>
{
new McpToolDefinition
{
Name = "get_backtests_paginated",
Description = "Retrieves paginated backtests with filtering and sorting capabilities. Supports filters for score, winrate, drawdown, tickers, indicators, duration, and trading type.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["page"] = new McpParameterDefinition
{
Type = "integer",
Description = "Page number (defaults to 1)",
Required = false,
DefaultValue = 1
},
["pageSize"] = new McpParameterDefinition
{
Type = "integer",
Description = "Number of items per page (defaults to 50, max 100)",
Required = false,
DefaultValue = 50
},
["sortBy"] = new McpParameterDefinition
{
Type = "string",
Description = "Field to sort by (Score, WinRate, GrowthPercentage, MaxDrawdown, SharpeRatio, FinalPnl, StartDate, EndDate, PositionCount)",
Required = false,
DefaultValue = "Score"
},
["sortOrder"] = new McpParameterDefinition
{
Type = "string",
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
Required = false,
DefaultValue = "desc"
},
["scoreMin"] = new McpParameterDefinition
{
Type = "number",
Description = "Minimum score filter (0-100)",
Required = false
},
["scoreMax"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum score filter (0-100)",
Required = false
},
["winrateMin"] = new McpParameterDefinition
{
Type = "integer",
Description = "Minimum winrate filter (0-100)",
Required = false
},
["winrateMax"] = new McpParameterDefinition
{
Type = "integer",
Description = "Maximum winrate filter (0-100)",
Required = false
},
["maxDrawdownMax"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum drawdown filter",
Required = false
},
["tickers"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of tickers to filter by (e.g., 'BTC,ETH,SOL')",
Required = false
},
["indicators"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of indicators to filter by",
Required = false
},
["durationMinDays"] = new McpParameterDefinition
{
Type = "number",
Description = "Minimum duration in days",
Required = false
},
["durationMaxDays"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum duration in days",
Required = false
},
["name"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by name (contains search)",
Required = false
},
["tradingType"] = new McpParameterDefinition
{
Type = "string",
Description = "Trading type filter (Spot, Futures, BacktestSpot, BacktestFutures, Paper)",
Required = false
}
}
}
};
return Task.FromResult<IEnumerable<McpToolDefinition>>(tools);
}
private async Task<object> ExecuteGetBacktestsPaginated(User user, Dictionary<string, object>? parameters)
{
var page = GetParameterValue<int>(parameters, "page", 1);
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
var sortByString = GetParameterValue<string>(parameters, "sortBy", "Score");
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
var maxDrawdownMax = GetParameterValue<decimal?>(parameters, "maxDrawdownMax", null);
var tickers = GetParameterValue<string?>(parameters, "tickers", null);
var indicators = GetParameterValue<string?>(parameters, "indicators", null);
var durationMinDays = GetParameterValue<double?>(parameters, "durationMinDays", null);
var durationMaxDays = GetParameterValue<double?>(parameters, "durationMaxDays", null);
var name = GetParameterValue<string?>(parameters, "name", null);
var tradingTypeString = GetParameterValue<string?>(parameters, "tradingType", null);
// Parse sortBy enum
if (!Enum.TryParse<BacktestSortableColumn>(sortByString, true, out var sortBy))
{
sortBy = BacktestSortableColumn.Score;
}
// Parse tradingType enum
TradingType? tradingType = null;
if (!string.IsNullOrWhiteSpace(tradingTypeString) &&
Enum.TryParse<TradingType>(tradingTypeString, true, out var parsedTradingType))
{
tradingType = parsedTradingType;
}
return await _backtestTools.GetBacktestsPaginated(
user,
page,
pageSize,
sortBy,
sortOrder,
scoreMin,
scoreMax,
winrateMin,
winrateMax,
maxDrawdownMax,
tickers,
indicators,
durationMinDays,
durationMaxDays,
name,
tradingType);
}
private T GetParameterValue<T>(Dictionary<string, object>? parameters, string key, T defaultValue)
{
if (parameters == null || !parameters.ContainsKey(key))
{
return defaultValue;
}
try
{
var value = parameters[key];
if (value == null)
{
return defaultValue;
}
// Handle nullable types
var targetType = typeof(T);
var underlyingType = Nullable.GetUnderlyingType(targetType);
if (underlyingType != null)
{
// It's a nullable type
return (T)Convert.ChangeType(value, underlyingType);
}
return (T)Convert.ChangeType(value, targetType);
}
catch
{
return defaultValue;
}
}
}

View File

@@ -427,8 +427,19 @@ public static class ApiBootstrap
// LLM and MCP services
services.AddScoped<ILlmService, Managing.Application.LLM.LlmService>();
services.AddScoped<IMcpService, Managing.Application.LLM.McpService>();
services.AddScoped<IMcpService, Managing.Mcp.McpService>();
// MCP Tools (underlying implementations)
services.AddScoped<Managing.Mcp.Tools.BacktestTools>();
services.AddScoped<Managing.Mcp.Tools.DataTools>();
services.AddScoped<Managing.Mcp.Tools.BotTools>();
services.AddScoped<Managing.Mcp.Tools.IndicatorTools>();
// MCP Tool Wrappers (with tool definitions)
services.AddScoped<Managing.Mcp.McpTools.BacktestMcpTools>();
services.AddScoped<Managing.Mcp.McpTools.DataMcpTools>();
services.AddScoped<Managing.Mcp.McpTools.BotMcpTools>();
services.AddScoped<Managing.Mcp.McpTools.IndicatorMcpTools>();
return services;
}

View File

@@ -0,0 +1,103 @@
using Managing.Application.Abstractions.Services;
using Managing.Mcp.McpTools;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
namespace Managing.Mcp;
/// <summary>
/// Service for executing Model Context Protocol (MCP) tools
/// </summary>
public class McpService : IMcpService
{
private readonly BacktestMcpTools _backtestMcpTools;
private readonly DataMcpTools _dataMcpTools;
// private readonly BotMcpTools _botMcpTools; // TODO: Fix BotMcpTools implementation
private readonly IndicatorMcpTools _indicatorMcpTools;
private readonly ILogger<McpService> _logger;
public McpService(
BacktestMcpTools backtestMcpTools,
DataMcpTools dataMcpTools,
// BotMcpTools botMcpTools, // TODO: Fix BotMcpTools implementation
IndicatorMcpTools indicatorMcpTools,
ILogger<McpService> logger)
{
_backtestMcpTools = backtestMcpTools;
_dataMcpTools = dataMcpTools;
// _botMcpTools = botMcpTools; // TODO: Fix BotMcpTools implementation
_indicatorMcpTools = indicatorMcpTools;
_logger = logger;
}
public async Task<object> ExecuteToolAsync(User user, string toolName, Dictionary<string, object>? parameters = null)
{
_logger.LogInformation("Executing MCP tool: {ToolName} for user: {UserId}", toolName, user.Id);
try
{
return toolName.ToLowerInvariant() switch
{
// Backtest tools
"get_backtests_paginated" => await _backtestMcpTools.ExecuteGetBacktestsPaginated(user, parameters),
"get_backtest_by_id" => await _backtestMcpTools.ExecuteGetBacktestById(user, parameters),
"delete_backtest" => await _backtestMcpTools.ExecuteDeleteBacktest(user, parameters),
"delete_backtests_by_ids" => await _backtestMcpTools.ExecuteDeleteBacktestsByIds(user, parameters),
"delete_backtests_by_filters" => await _backtestMcpTools.ExecuteDeleteBacktestsByFilters(user, parameters),
"get_bundle_backtests_paginated" => await _backtestMcpTools.ExecuteGetBundleBacktestsPaginated(user, parameters),
"get_bundle_backtest_by_id" => await _backtestMcpTools.ExecuteGetBundleBacktestById(user, parameters),
"delete_bundle_backtest" => await _backtestMcpTools.ExecuteDeleteBundleBacktest(user, parameters),
"run_backtest" => await _backtestMcpTools.ExecuteRunBacktest(user, parameters),
// Data tools
"get_tickers" => await _dataMcpTools.ExecuteGetTickers(user, parameters),
"get_candles" => await _dataMcpTools.ExecuteGetCandles(user, parameters),
"get_spotlight" => await _dataMcpTools.ExecuteGetSpotlight(user, parameters),
"get_agent_balances" => await _dataMcpTools.ExecuteGetAgentBalances(user, parameters),
"get_online_agents" => await _dataMcpTools.ExecuteGetOnlineAgents(user, parameters),
"get_agents_paginated" => await _dataMcpTools.ExecuteGetAgentsPaginated(user, parameters),
// Bot tools (TODO: Fix BotMcpTools implementation)
// "get_bots_paginated" => await _botMcpTools.ExecuteGetBotsPaginated(user, parameters),
// "get_bot_by_id" => await _botMcpTools.ExecuteGetBotById(user, parameters),
// "get_bot_config" => await _botMcpTools.ExecuteGetBotConfig(user, parameters),
// "get_bots_by_status" => await _botMcpTools.ExecuteGetBotsByStatus(user, parameters),
// "delete_bot" => await _botMcpTools.ExecuteDeleteBot(user, parameters),
// Indicator tools
"list_indicators" => await _indicatorMcpTools.ExecuteListIndicators(user, parameters),
"get_indicator_info" => await _indicatorMcpTools.ExecuteGetIndicatorInfo(user, parameters),
"explain_indicator" => await _indicatorMcpTools.ExecuteExplainIndicator(user, parameters),
"recommend_indicators" => await _indicatorMcpTools.ExecuteRecommendIndicators(user, parameters),
"refine_indicator_parameters" => await _indicatorMcpTools.ExecuteRefineIndicatorParameters(user, parameters),
"compare_indicators" => await _indicatorMcpTools.ExecuteCompareIndicators(user, parameters),
_ => throw new InvalidOperationException($"Unknown tool: {toolName}")
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error executing MCP tool {ToolName} for user {UserId}", toolName, user.Id);
throw;
}
}
public Task<IEnumerable<McpToolDefinition>> GetAvailableToolsAsync()
{
var allTools = new List<McpToolDefinition>();
// Add backtest tools
allTools.AddRange(_backtestMcpTools.GetToolDefinitions());
// Add data tools
allTools.AddRange(_dataMcpTools.GetToolDefinitions());
// Add bot tools (TODO: Fix BotMcpTools implementation)
// allTools.AddRange(_botMcpTools.GetToolDefinitions());
// Add indicator tools
allTools.AddRange(_indicatorMcpTools.GetToolDefinitions());
return Task.FromResult<IEnumerable<McpToolDefinition>>(allTools);
}
}

View File

@@ -0,0 +1,546 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Managing.Mcp.Tools;
using static Managing.Common.Enums;
namespace Managing.Mcp.McpTools;
/// <summary>
/// MCP tools for backtest operations
/// </summary>
public class BacktestMcpTools : BaseMcpTool
{
private readonly BacktestTools _backtestTools;
public BacktestMcpTools(BacktestTools backtestTools)
{
_backtestTools = backtestTools;
}
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
{
return new List<McpToolDefinition>
{
new McpToolDefinition
{
Name = "get_backtests_paginated",
Description = "Retrieves paginated backtests with filtering and sorting capabilities. Supports filters for score, winrate, drawdown, tickers, indicators, duration, and trading type.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["page"] = new McpParameterDefinition
{
Type = "integer",
Description = "Page number (defaults to 1)",
Required = false,
DefaultValue = 1
},
["pageSize"] = new McpParameterDefinition
{
Type = "integer",
Description = "Number of items per page (defaults to 50, max 100)",
Required = false,
DefaultValue = 50
},
["sortBy"] = new McpParameterDefinition
{
Type = "string",
Description = "Field to sort by (Score, WinRate, GrowthPercentage, MaxDrawdown, SharpeRatio, FinalPnl, StartDate, EndDate, PositionCount)",
Required = false,
DefaultValue = "Score"
},
["sortOrder"] = new McpParameterDefinition
{
Type = "string",
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
Required = false,
DefaultValue = "desc"
},
["scoreMin"] = new McpParameterDefinition
{
Type = "number",
Description = "Minimum score filter (0-100)",
Required = false
},
["scoreMax"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum score filter (0-100)",
Required = false
},
["winrateMin"] = new McpParameterDefinition
{
Type = "integer",
Description = "Minimum winrate filter (0-100)",
Required = false
},
["winrateMax"] = new McpParameterDefinition
{
Type = "integer",
Description = "Maximum winrate filter (0-100)",
Required = false
},
["maxDrawdownMax"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum drawdown filter",
Required = false
},
["tickers"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of tickers to filter by (e.g., 'BTC,ETH,SOL')",
Required = false
},
["indicators"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of indicators to filter by",
Required = false
},
["durationMinDays"] = new McpParameterDefinition
{
Type = "number",
Description = "Minimum duration in days",
Required = false
},
["durationMaxDays"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum duration in days",
Required = false
},
["name"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by name (contains search)",
Required = false
},
["tradingType"] = new McpParameterDefinition
{
Type = "string",
Description = "Trading type filter (Spot, Futures, BacktestSpot, BacktestFutures, Paper)",
Required = false
}
}
},
new McpToolDefinition
{
Name = "get_backtest_by_id",
Description = "Retrieves a specific backtest by its ID for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["id"] = new McpParameterDefinition
{
Type = "string",
Description = "The ID of the backtest to retrieve",
Required = true
}
}
},
new McpToolDefinition
{
Name = "delete_backtest",
Description = "Deletes a specific backtest by its ID for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["id"] = new McpParameterDefinition
{
Type = "string",
Description = "The ID of the backtest to delete",
Required = true
}
}
},
new McpToolDefinition
{
Name = "delete_backtests_by_ids",
Description = "Deletes multiple backtests by their IDs for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["ids"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of backtest IDs to delete",
Required = true
}
}
},
new McpToolDefinition
{
Name = "delete_backtests_by_filters",
Description = "Deletes backtests matching the specified filters for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["scoreMin"] = new McpParameterDefinition
{
Type = "number",
Description = "Minimum score filter (0-100)",
Required = false
},
["scoreMax"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum score filter (0-100)",
Required = false
},
["winrateMin"] = new McpParameterDefinition
{
Type = "integer",
Description = "Minimum winrate filter (0-100)",
Required = false
},
["winrateMax"] = new McpParameterDefinition
{
Type = "integer",
Description = "Maximum winrate filter (0-100)",
Required = false
},
["name"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by name (contains search)",
Required = false
}
}
},
new McpToolDefinition
{
Name = "get_bundle_backtests_paginated",
Description = "Retrieves paginated bundle backtest requests with filtering and sorting capabilities.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["page"] = new McpParameterDefinition
{
Type = "integer",
Description = "Page number (defaults to 1)",
Required = false,
DefaultValue = 1
},
["pageSize"] = new McpParameterDefinition
{
Type = "integer",
Description = "Number of items per page (defaults to 50, max 100)",
Required = false,
DefaultValue = 50
},
["sortBy"] = new McpParameterDefinition
{
Type = "string",
Description = "Field to sort by (CreatedAt, UpdatedAt, BotName)",
Required = false,
DefaultValue = "CreatedAt"
},
["sortOrder"] = new McpParameterDefinition
{
Type = "string",
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
Required = false,
DefaultValue = "desc"
},
["status"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by status (Pending, Running, Completed, Failed, Cancelled)",
Required = false
},
["name"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by bot name (contains search)",
Required = false
}
}
},
new McpToolDefinition
{
Name = "get_bundle_backtest_by_id",
Description = "Retrieves a specific bundle backtest request by its ID.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["id"] = new McpParameterDefinition
{
Type = "string",
Description = "The ID of the bundle backtest request to retrieve",
Required = true
}
}
},
new McpToolDefinition
{
Name = "delete_bundle_backtest",
Description = "Deletes a bundle backtest request and all associated backtests.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["id"] = new McpParameterDefinition
{
Type = "string",
Description = "The ID of the bundle backtest request to delete",
Required = true
}
}
},
new McpToolDefinition
{
Name = "run_backtest",
Description = "Runs a new backtest with the specified configuration. Creates a backtest job that will be processed asynchronously.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["ticker"] = new McpParameterDefinition
{
Type = "string",
Description = "The ticker symbol to backtest (e.g., 'BTC', 'ETH', 'SOL')",
Required = true
},
["timeframe"] = new McpParameterDefinition
{
Type = "string",
Description = "The timeframe for the backtest (e.g., 'FiveMinutes', 'FifteenMinutes', 'OneHour', 'FourHours', 'OneDay')",
Required = true
},
["startDate"] = new McpParameterDefinition
{
Type = "string",
Description = "Start date for the backtest in ISO 8601 format (e.g., '2024-01-01T00:00:00Z')",
Required = true
},
["endDate"] = new McpParameterDefinition
{
Type = "string",
Description = "End date for the backtest in ISO 8601 format (e.g., '2024-12-31T23:59:59Z')",
Required = true
},
["balance"] = new McpParameterDefinition
{
Type = "number",
Description = "Starting balance for the backtest (must be greater than zero)",
Required = true
},
["scenarioName"] = new McpParameterDefinition
{
Type = "string",
Description = "The name of the trading scenario/strategy to use",
Required = false
},
["moneyManagementName"] = new McpParameterDefinition
{
Type = "string",
Description = "The name of the money management configuration to use",
Required = false
},
["stopLoss"] = new McpParameterDefinition
{
Type = "number",
Description = "Stop loss percentage (e.g., 2.5 for 2.5%)",
Required = false
},
["takeProfit"] = new McpParameterDefinition
{
Type = "number",
Description = "Take profit percentage (e.g., 5.0 for 5%)",
Required = false
},
["leverage"] = new McpParameterDefinition
{
Type = "number",
Description = "Leverage multiplier (e.g., 1, 2, 5, 10)",
Required = false
},
["name"] = new McpParameterDefinition
{
Type = "string",
Description = "Optional name for the backtest (auto-generated if not provided)",
Required = false
},
["save"] = new McpParameterDefinition
{
Type = "boolean",
Description = "Whether to save the backtest results (defaults to true)",
Required = false,
DefaultValue = true
}
}
}
};
}
public async Task<object> ExecuteGetBacktestsPaginated(User user, Dictionary<string, object>? parameters)
{
var page = GetParameterValue<int>(parameters, "page", 1);
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
var sortByString = GetParameterValue<string>(parameters, "sortBy", "Score");
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
var maxDrawdownMax = GetParameterValue<decimal?>(parameters, "maxDrawdownMax", null);
var tickers = GetParameterValue<string?>(parameters, "tickers", null);
var indicators = GetParameterValue<string?>(parameters, "indicators", null);
var durationMinDays = GetParameterValue<double?>(parameters, "durationMinDays", null);
var durationMaxDays = GetParameterValue<double?>(parameters, "durationMaxDays", null);
var name = GetParameterValue<string?>(parameters, "name", null);
var tradingTypeString = GetParameterValue<string?>(parameters, "tradingType", null);
// Parse sortBy enum
if (!Enum.TryParse<BacktestSortableColumn>(sortByString, true, out var sortBy))
{
sortBy = BacktestSortableColumn.Score;
}
// Parse tradingType enum
TradingType? tradingType = null;
if (!string.IsNullOrWhiteSpace(tradingTypeString) &&
Enum.TryParse<TradingType>(tradingTypeString, true, out var parsedTradingType))
{
tradingType = parsedTradingType;
}
return await _backtestTools.GetBacktestsPaginated(
user,
page,
pageSize,
sortBy,
sortOrder,
scoreMin,
scoreMax,
winrateMin,
winrateMax,
maxDrawdownMax,
tickers,
indicators,
durationMinDays,
durationMaxDays,
name,
tradingType);
}
public async Task<object> ExecuteGetBacktestById(User user, Dictionary<string, object>? parameters)
{
var id = GetParameterValue<string>(parameters, "id", string.Empty);
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentException("Backtest ID is required");
}
return await _backtestTools.GetBacktestById(user, id);
}
public async Task<object> ExecuteDeleteBacktest(User user, Dictionary<string, object>? parameters)
{
var id = GetParameterValue<string>(parameters, "id", string.Empty);
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentException("Backtest ID is required");
}
return await _backtestTools.DeleteBacktest(user, id);
}
public async Task<object> ExecuteDeleteBacktestsByIds(User user, Dictionary<string, object>? parameters)
{
var idsString = GetParameterValue<string>(parameters, "ids", string.Empty);
if (string.IsNullOrWhiteSpace(idsString))
{
throw new ArgumentException("At least one backtest ID is required");
}
var ids = idsString.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return await _backtestTools.DeleteBacktestsByIds(user, ids);
}
public async Task<object> ExecuteDeleteBacktestsByFilters(User user, Dictionary<string, object>? parameters)
{
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
var name = GetParameterValue<string?>(parameters, "name", null);
return await _backtestTools.DeleteBacktestsByFilters(user, scoreMin, scoreMax, winrateMin, winrateMax, name);
}
public async Task<object> ExecuteGetBundleBacktestsPaginated(User user, Dictionary<string, object>? parameters)
{
var page = GetParameterValue<int>(parameters, "page", 1);
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
var sortByString = GetParameterValue<string>(parameters, "sortBy", "CreatedAt");
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
var statusString = GetParameterValue<string?>(parameters, "status", null);
var name = GetParameterValue<string?>(parameters, "name", null);
return await _backtestTools.GetBundleBacktestsPaginated(user, page, pageSize, sortByString, sortOrder, statusString, name);
}
public async Task<object> ExecuteGetBundleBacktestById(User user, Dictionary<string, object>? parameters)
{
var idString = GetParameterValue<string>(parameters, "id", string.Empty);
if (string.IsNullOrWhiteSpace(idString) || !Guid.TryParse(idString, out var id))
{
throw new ArgumentException("Valid bundle backtest ID is required");
}
return await _backtestTools.GetBundleBacktestById(user, id);
}
public async Task<object> ExecuteDeleteBundleBacktest(User user, Dictionary<string, object>? parameters)
{
var idString = GetParameterValue<string>(parameters, "id", string.Empty);
if (string.IsNullOrWhiteSpace(idString) || !Guid.TryParse(idString, out var id))
{
throw new ArgumentException("Valid bundle backtest ID is required");
}
return await _backtestTools.DeleteBundleBacktest(user, id);
}
public async Task<object> ExecuteRunBacktest(User user, Dictionary<string, object>? parameters)
{
var ticker = GetParameterValue<string>(parameters, "ticker", string.Empty);
var timeframe = GetParameterValue<string>(parameters, "timeframe", string.Empty);
var startDateString = GetParameterValue<string>(parameters, "startDate", string.Empty);
var endDateString = GetParameterValue<string>(parameters, "endDate", string.Empty);
var balance = GetParameterValue<decimal>(parameters, "balance", 0);
var scenarioName = GetParameterValue<string?>(parameters, "scenarioName", null);
var moneyManagementName = GetParameterValue<string?>(parameters, "moneyManagementName", null);
var stopLoss = GetParameterValue<decimal?>(parameters, "stopLoss", null);
var takeProfit = GetParameterValue<decimal?>(parameters, "takeProfit", null);
var leverage = GetParameterValue<decimal?>(parameters, "leverage", null);
var name = GetParameterValue<string?>(parameters, "name", null);
var save = GetParameterValue<bool>(parameters, "save", true);
if (string.IsNullOrWhiteSpace(ticker))
{
throw new ArgumentException("Ticker is required");
}
if (string.IsNullOrWhiteSpace(timeframe))
{
throw new ArgumentException("Timeframe is required");
}
if (!DateTime.TryParse(startDateString, out var startDate))
{
throw new ArgumentException("Valid start date is required");
}
if (!DateTime.TryParse(endDateString, out var endDate))
{
throw new ArgumentException("Valid end date is required");
}
return await _backtestTools.RunBacktest(
user,
ticker,
timeframe,
startDate,
endDate,
balance,
scenarioName,
moneyManagementName,
stopLoss,
takeProfit,
leverage,
name,
save);
}
}

View File

@@ -0,0 +1,50 @@
using Managing.Application.Abstractions.Services;
namespace Managing.Mcp.McpTools;
/// <summary>
/// Base class for MCP tools providing common functionality
/// </summary>
public abstract class BaseMcpTool
{
/// <summary>
/// Gets the tool definitions for this tool
/// </summary>
public abstract IEnumerable<McpToolDefinition> GetToolDefinitions();
/// <summary>
/// Helper method to extract parameter values with type conversion and default values
/// </summary>
protected T GetParameterValue<T>(Dictionary<string, object>? parameters, string key, T defaultValue)
{
if (parameters == null || !parameters.ContainsKey(key))
{
return defaultValue;
}
try
{
var value = parameters[key];
if (value == null)
{
return defaultValue;
}
// Handle nullable types
var targetType = typeof(T);
var underlyingType = Nullable.GetUnderlyingType(targetType);
if (underlyingType != null)
{
// It's a nullable type
return (T)Convert.ChangeType(value, underlyingType);
}
return (T)Convert.ChangeType(value, targetType);
}
catch
{
return defaultValue;
}
}
}

View File

@@ -0,0 +1,257 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Managing.Mcp.Tools;
using static Managing.Common.Enums;
namespace Managing.Mcp.McpTools;
/// <summary>
/// MCP tools for bot operations
/// </summary>
public class BotMcpTools : BaseMcpTool
{
private readonly BotTools _botTools;
public BotMcpTools(BotTools botTools)
{
_botTools = botTools;
}
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
{
return new List<McpToolDefinition>
{
new McpToolDefinition
{
Name = "get_bots_paginated",
Description = "Retrieves paginated bots with filtering and sorting capabilities. Supports filters for status, name, ticker, agent name, and balance range.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["pageNumber"] = new McpParameterDefinition
{
Type = "integer",
Description = "Page number (defaults to 1)",
Required = false,
DefaultValue = 1
},
["pageSize"] = new McpParameterDefinition
{
Type = "integer",
Description = "Number of items per page (defaults to 10, max 100)",
Required = false,
DefaultValue = 10
},
["status"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by bot status (Saved, Stopped, Running)",
Required = false
},
["name"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by bot name (partial match, case-insensitive)",
Required = false
},
["ticker"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by ticker (partial match, case-insensitive)",
Required = false
},
["agentName"] = new McpParameterDefinition
{
Type = "string",
Description = "Filter by agent name (partial match, case-insensitive)",
Required = false
},
["minBalance"] = new McpParameterDefinition
{
Type = "number",
Description = "Minimum bot trading balance filter",
Required = false
},
["maxBalance"] = new McpParameterDefinition
{
Type = "number",
Description = "Maximum bot trading balance filter",
Required = false
},
["sortBy"] = new McpParameterDefinition
{
Type = "string",
Description = "Field to sort by (CreateDate, Name, Ticker, Status, StartupTime, Roi, Pnl, WinRate, AgentName, BotTradingBalance)",
Required = false,
DefaultValue = "CreateDate"
},
["sortDirection"] = new McpParameterDefinition
{
Type = "string",
Description = "Sort direction - 'Asc' or 'Desc' (defaults to 'Desc')",
Required = false,
DefaultValue = "Desc"
},
["showOnlyProfitable"] = new McpParameterDefinition
{
Type = "boolean",
Description = "Show only profitable bots (ROI > 0) (defaults to false)",
Required = false,
DefaultValue = false
}
}
},
new McpToolDefinition
{
Name = "get_bot_by_id",
Description = "Retrieves a specific bot by its identifier for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["identifier"] = new McpParameterDefinition
{
Type = "string",
Description = "The identifier (GUID) of the bot to retrieve",
Required = true
}
}
},
// TODO: Implement GetBotConfig in BotTools
// new McpToolDefinition
// {
// Name = "get_bot_config",
// Description = "Retrieves the configuration of a specific bot by its identifier for the authenticated user.",
// Parameters = new Dictionary<string, McpParameterDefinition>
// {
// ["identifier"] = new McpParameterDefinition
// {
// Type = "string",
// Description = "The identifier (GUID) of the bot to get configuration for",
// Required = true
// }
// }
// },
new McpToolDefinition
{
Name = "get_bots_by_status",
Description = "Retrieves all bots with a specific status for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["status"] = new McpParameterDefinition
{
Type = "string",
Description = "Bot status filter (Saved, Stopped, Running)",
Required = true
}
}
},
new McpToolDefinition
{
Name = "delete_bot",
Description = "Deletes a specific bot by its identifier for the authenticated user.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["identifier"] = new McpParameterDefinition
{
Type = "string",
Description = "The identifier (GUID) of the bot to delete",
Required = true
}
}
}
};
}
public async Task<object> ExecuteGetBotsPaginated(User user, Dictionary<string, object>? parameters)
{
var pageNumber = GetParameterValue<int>(parameters, "pageNumber", 1);
var pageSize = GetParameterValue<int>(parameters, "pageSize", 10);
var statusString = GetParameterValue<string?>(parameters, "status", null);
var name = GetParameterValue<string?>(parameters, "name", null);
var ticker = GetParameterValue<string?>(parameters, "ticker", null);
var agentName = GetParameterValue<string?>(parameters, "agentName", null);
var minBalance = GetParameterValue<decimal?>(parameters, "minBalance", null);
var maxBalance = GetParameterValue<decimal?>(parameters, "maxBalance", null);
var sortByString = GetParameterValue<string>(parameters, "sortBy", "CreateDate");
var sortDirectionString = GetParameterValue<string>(parameters, "sortDirection", "Desc");
var showOnlyProfitable = GetParameterValue<bool>(parameters, "showOnlyProfitable", false);
// Parse status enum
BotStatus? status = null;
if (!string.IsNullOrWhiteSpace(statusString) &&
Enum.TryParse<BotStatus>(statusString, true, out var parsedStatus))
{
status = parsedStatus;
}
// Parse sortBy enum
if (!Enum.TryParse<BotSortableColumn>(sortByString, true, out var sortBy))
{
sortBy = BotSortableColumn.CreateDate;
}
// Parse sortDirection enum
if (!Enum.TryParse<SortDirection>(sortDirectionString, true, out var sortDirection))
{
sortDirection = SortDirection.Desc;
}
return await _botTools.GetBotsPaginated(
user,
pageNumber,
pageSize,
status,
name,
ticker,
agentName,
minBalance,
maxBalance,
sortBy,
sortDirection,
showOnlyProfitable);
}
public async Task<object> ExecuteGetBotById(User user, Dictionary<string, object>? parameters)
{
var identifierString = GetParameterValue<string>(parameters, "identifier", string.Empty);
if (string.IsNullOrWhiteSpace(identifierString) || !Guid.TryParse(identifierString, out var identifier))
{
throw new ArgumentException("Valid bot identifier (GUID) is required");
}
return await _botTools.GetBotById(user, identifier);
}
// TODO: Implement GetBotConfig in BotTools
// public async Task<object> ExecuteGetBotConfig(User user, Dictionary<string, object>? parameters)
// {
// var identifierString = GetParameterValue<string>(parameters, "identifier", string.Empty);
// if (string.IsNullOrWhiteSpace(identifierString) || !Guid.TryParse(identifierString, out var identifier))
// {
// throw new ArgumentException("Valid bot identifier (GUID) is required");
// }
//
// return await _botTools.GetBotConfig(user, identifier);
// }
public async Task<object> ExecuteGetBotsByStatus(User user, Dictionary<string, object>? parameters)
{
var statusString = GetParameterValue<string>(parameters, "status", string.Empty);
if (string.IsNullOrWhiteSpace(statusString) || !Enum.TryParse<BotStatus>(statusString, true, out var status))
{
throw new ArgumentException("Valid bot status is required (Saved, Stopped, Running)");
}
return await _botTools.GetBotsByStatus(user, status);
}
public async Task<object> ExecuteDeleteBot(User user, Dictionary<string, object>? parameters)
{
var identifierString = GetParameterValue<string>(parameters, "identifier", string.Empty);
if (string.IsNullOrWhiteSpace(identifierString) || !Guid.TryParse(identifierString, out var identifier))
{
throw new ArgumentException("Valid bot identifier (GUID) is required");
}
return await _botTools.DeleteBot(user, identifier);
}
}

View File

@@ -0,0 +1,250 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Managing.Mcp.Tools;
using static Managing.Common.Enums;
namespace Managing.Mcp.McpTools;
/// <summary>
/// MCP tools for data operations (tickers, candles, statistics, agents)
/// </summary>
public class DataMcpTools : BaseMcpTool
{
private readonly DataTools _dataTools;
public DataMcpTools(DataTools dataTools)
{
_dataTools = dataTools;
}
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
{
return new List<McpToolDefinition>
{
new McpToolDefinition
{
Name = "get_tickers",
Description = "Retrieves available tickers for a given timeframe.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["timeframe"] = new McpParameterDefinition
{
Type = "string",
Description = "The timeframe (e.g., 'FiveMinutes', 'FifteenMinutes', 'OneHour', 'FourHours', 'OneDay')",
Required = true
}
}
},
new McpToolDefinition
{
Name = "get_candles",
Description = "Retrieves historical candle data for a specific ticker and date range.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["ticker"] = new McpParameterDefinition
{
Type = "string",
Description = "The ticker symbol (e.g., 'BTC', 'ETH', 'SOL')",
Required = true
},
["startDate"] = new McpParameterDefinition
{
Type = "string",
Description = "Start date in ISO 8601 format (e.g., '2024-01-01T00:00:00Z')",
Required = true
},
["endDate"] = new McpParameterDefinition
{
Type = "string",
Description = "End date in ISO 8601 format (e.g., '2024-12-31T23:59:59Z')",
Required = true
},
["timeframe"] = new McpParameterDefinition
{
Type = "string",
Description = "The timeframe (e.g., 'FiveMinutes', 'FifteenMinutes', 'OneHour', 'FourHours', 'OneDay')",
Required = true
}
}
},
new McpToolDefinition
{
Name = "get_spotlight",
Description = "Retrieves platform statistics and spotlight data showing market overview.",
Parameters = new Dictionary<string, McpParameterDefinition>()
},
new McpToolDefinition
{
Name = "get_agent_balances",
Description = "Retrieves balance history for a specific agent within a date range.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["agentName"] = new McpParameterDefinition
{
Type = "string",
Description = "The name of the agent",
Required = true
},
["startDate"] = new McpParameterDefinition
{
Type = "string",
Description = "Start date in ISO 8601 format",
Required = true
},
["endDate"] = new McpParameterDefinition
{
Type = "string",
Description = "Optional end date in ISO 8601 format (defaults to current time)",
Required = false
}
}
},
new McpToolDefinition
{
Name = "get_online_agents",
Description = "Retrieves list of currently online agents.",
Parameters = new Dictionary<string, McpParameterDefinition>()
},
new McpToolDefinition
{
Name = "get_agents_paginated",
Description = "Retrieves paginated agent summaries with filtering and sorting capabilities.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["page"] = new McpParameterDefinition
{
Type = "integer",
Description = "Page number (defaults to 1)",
Required = false,
DefaultValue = 1
},
["pageSize"] = new McpParameterDefinition
{
Type = "integer",
Description = "Number of items per page (defaults to 10, max 100)",
Required = false,
DefaultValue = 10
},
["sortBy"] = new McpParameterDefinition
{
Type = "string",
Description = "Field to sort by (NetPnL, TotalPnL, TotalROI, Wins, Losses, AgentName, TotalVolume, TotalBalance)",
Required = false,
DefaultValue = "NetPnL"
},
["sortOrder"] = new McpParameterDefinition
{
Type = "string",
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
Required = false,
DefaultValue = "desc"
},
["agentNames"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of agent names to filter by",
Required = false
},
["showOnlyProfitable"] = new McpParameterDefinition
{
Type = "boolean",
Description = "Show only profitable agents (defaults to false)",
Required = false,
DefaultValue = false
}
}
}
};
}
public async Task<object> ExecuteGetTickers(User user, Dictionary<string, object>? parameters)
{
var timeframeString = GetParameterValue<string>(parameters, "timeframe", string.Empty);
if (string.IsNullOrWhiteSpace(timeframeString) ||
!Enum.TryParse<Timeframe>(timeframeString, true, out var timeframe))
{
throw new ArgumentException("Valid timeframe is required");
}
return await _dataTools.GetTickers(timeframe);
}
public async Task<object> ExecuteGetCandles(User user, Dictionary<string, object>? parameters)
{
var tickerString = GetParameterValue<string>(parameters, "ticker", string.Empty);
var startDateString = GetParameterValue<string>(parameters, "startDate", string.Empty);
var endDateString = GetParameterValue<string>(parameters, "endDate", string.Empty);
var timeframeString = GetParameterValue<string>(parameters, "timeframe", string.Empty);
if (string.IsNullOrWhiteSpace(tickerString) ||
!Enum.TryParse<Ticker>(tickerString, true, out var ticker))
{
throw new ArgumentException("Valid ticker is required");
}
if (!DateTime.TryParse(startDateString, out var startDate))
{
throw new ArgumentException("Valid start date is required");
}
if (!DateTime.TryParse(endDateString, out var endDate))
{
throw new ArgumentException("Valid end date is required");
}
if (string.IsNullOrWhiteSpace(timeframeString) ||
!Enum.TryParse<Timeframe>(timeframeString, true, out var timeframe))
{
throw new ArgumentException("Valid timeframe is required");
}
return await _dataTools.GetCandles(ticker, startDate, endDate, timeframe);
}
public async Task<object> ExecuteGetSpotlight(User user, Dictionary<string, object>? parameters)
{
return await _dataTools.GetSpotlight();
}
public async Task<object> ExecuteGetAgentBalances(User user, Dictionary<string, object>? parameters)
{
var agentName = GetParameterValue<string>(parameters, "agentName", string.Empty);
var startDateString = GetParameterValue<string>(parameters, "startDate", string.Empty);
var endDateString = GetParameterValue<string?>(parameters, "endDate", null);
if (string.IsNullOrWhiteSpace(agentName))
{
throw new ArgumentException("Agent name is required");
}
if (!DateTime.TryParse(startDateString, out var startDate))
{
throw new ArgumentException("Valid start date is required");
}
DateTime? endDate = null;
if (!string.IsNullOrWhiteSpace(endDateString) && DateTime.TryParse(endDateString, out var parsedEndDate))
{
endDate = parsedEndDate;
}
return await _dataTools.GetAgentBalances(agentName, startDate, endDate);
}
public async Task<object> ExecuteGetOnlineAgents(User user, Dictionary<string, object>? parameters)
{
return await _dataTools.GetOnlineAgents();
}
public async Task<object> ExecuteGetAgentsPaginated(User user, Dictionary<string, object>? parameters)
{
var page = GetParameterValue<int>(parameters, "page", 1);
var pageSize = GetParameterValue<int>(parameters, "pageSize", 10);
var sortBy = GetParameterValue<string>(parameters, "sortBy", "NetPnL");
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
var agentNames = GetParameterValue<string?>(parameters, "agentNames", null);
var showOnlyProfitable = GetParameterValue<bool>(parameters, "showOnlyProfitable", false);
return await _dataTools.GetAgentsPaginated(page, pageSize, sortBy, sortOrder, agentNames, showOnlyProfitable);
}
}

View File

@@ -0,0 +1,222 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Managing.Mcp.Tools;
namespace Managing.Mcp.McpTools;
/// <summary>
/// MCP tools for indicator operations (list, explain, recommend, refine, compare)
/// </summary>
public class IndicatorMcpTools : BaseMcpTool
{
private readonly IndicatorTools _indicatorTools;
public IndicatorMcpTools(IndicatorTools indicatorTools)
{
_indicatorTools = indicatorTools;
}
public override IEnumerable<McpToolDefinition> GetToolDefinitions()
{
return new List<McpToolDefinition>
{
new McpToolDefinition
{
Name = "list_indicators",
Description = "Lists all available indicators with their types, categories, and basic information. Can filter by signal type (Signal, Trend, Context) or category.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["signalType"] = new McpParameterDefinition
{
Type = "string",
Description = "Optional filter by signal type: 'Signal', 'Trend', or 'Context'",
Required = false
},
["category"] = new McpParameterDefinition
{
Type = "string",
Description = "Optional filter by category (e.g., 'RSI', 'EMA', 'MACD', 'Volatility')",
Required = false
}
}
},
new McpToolDefinition
{
Name = "get_indicator_info",
Description = "Gets detailed information about a specific indicator including description, triggers, parameters, recommended values, use cases, and trading style recommendations.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["indicatorType"] = new McpParameterDefinition
{
Type = "string",
Description = "The indicator type (e.g., 'RsiDivergence', 'MacdCross', 'EmaCross', 'SuperTrend', 'StDev')",
Required = true
}
}
},
new McpToolDefinition
{
Name = "explain_indicator",
Description = "Provides a comprehensive explanation of how an indicator works, when to use it, what triggers it, parameter recommendations, and examples. Useful for understanding indicator mechanics and best practices.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["indicatorType"] = new McpParameterDefinition
{
Type = "string",
Description = "The indicator type to explain (e.g., 'RsiDivergence', 'MacdCross', 'EmaCross')",
Required = true
},
["includeExamples"] = new McpParameterDefinition
{
Type = "boolean",
Description = "Whether to include usage examples (defaults to true)",
Required = false,
DefaultValue = true
}
}
},
new McpToolDefinition
{
Name = "recommend_indicators",
Description = "Recommends indicators based on trading style (scalping, swing trading, position trading), market conditions (trending, volatile, ranging), and goals (entry signals, trend confirmation, risk filtering). Returns scored recommendations with reasoning.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["tradingStyle"] = new McpParameterDefinition
{
Type = "string",
Description = "Trading style: 'scalping', 'day trading', 'swing trading', 'position trading', or 'any'",
Required = false
},
["marketCondition"] = new McpParameterDefinition
{
Type = "string",
Description = "Market condition: 'trending', 'ranging', 'volatile', 'high volatility', 'low volatility', or 'any'",
Required = false
},
["goal"] = new McpParameterDefinition
{
Type = "string",
Description = "Trading goal: 'entry signals', 'exit signals', 'trend confirmation', 'risk filtering', 'market context', or 'any'",
Required = false
},
["maxRecommendations"] = new McpParameterDefinition
{
Type = "integer",
Description = "Maximum number of recommendations to return (defaults to all)",
Required = false
}
}
},
new McpToolDefinition
{
Name = "refine_indicator_parameters",
Description = "Suggests parameter refinements for an indicator based on trading style and market volatility. Provides optimized parameter values for different scenarios (short-term, medium-term, long-term trading, high/low volatility markets).",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["indicatorType"] = new McpParameterDefinition
{
Type = "string",
Description = "The indicator type to refine parameters for (e.g., 'RsiDivergence', 'EmaCross', 'MacdCross')",
Required = true
},
["tradingStyle"] = new McpParameterDefinition
{
Type = "string",
Description = "Trading style: 'scalping', 'day trading', 'swing trading', 'position trading', or leave empty for general recommendations",
Required = false
},
["marketVolatility"] = new McpParameterDefinition
{
Type = "string",
Description = "Market volatility: 'high', 'low', 'normal', or leave empty for general recommendations",
Required = false
}
}
},
new McpToolDefinition
{
Name = "compare_indicators",
Description = "Compares multiple indicators side by side, showing their types, categories, use cases, parameters, and best use scenarios. Useful for selecting between similar indicators.",
Parameters = new Dictionary<string, McpParameterDefinition>
{
["indicatorTypes"] = new McpParameterDefinition
{
Type = "string",
Description = "Comma-separated list of indicator types to compare (e.g., 'RsiDivergence,MacdCross,EmaCross')",
Required = true
}
}
}
};
}
public async Task<object> ExecuteListIndicators(User user, Dictionary<string, object>? parameters)
{
var signalType = GetParameterValue<string?>(parameters, "signalType", null);
var category = GetParameterValue<string?>(parameters, "category", null);
return await Task.FromResult(_indicatorTools.ListIndicators(signalType, category));
}
public async Task<object> ExecuteGetIndicatorInfo(User user, Dictionary<string, object>? parameters)
{
var indicatorType = GetParameterValue<string>(parameters, "indicatorType", string.Empty);
if (string.IsNullOrWhiteSpace(indicatorType))
{
throw new ArgumentException("Indicator type is required");
}
return await Task.FromResult(_indicatorTools.GetIndicatorInfo(indicatorType));
}
public async Task<object> ExecuteExplainIndicator(User user, Dictionary<string, object>? parameters)
{
var indicatorType = GetParameterValue<string>(parameters, "indicatorType", string.Empty);
if (string.IsNullOrWhiteSpace(indicatorType))
{
throw new ArgumentException("Indicator type is required");
}
var includeExamples = GetParameterValue<bool>(parameters, "includeExamples", true);
return await Task.FromResult(_indicatorTools.ExplainIndicator(indicatorType, includeExamples));
}
public async Task<object> ExecuteRecommendIndicators(User user, Dictionary<string, object>? parameters)
{
var tradingStyle = GetParameterValue<string?>(parameters, "tradingStyle", null);
var marketCondition = GetParameterValue<string?>(parameters, "marketCondition", null);
var goal = GetParameterValue<string?>(parameters, "goal", null);
var maxRecommendations = GetParameterValue<int?>(parameters, "maxRecommendations", null);
return await Task.FromResult(_indicatorTools.RecommendIndicators(
tradingStyle, marketCondition, goal, maxRecommendations));
}
public async Task<object> ExecuteRefineIndicatorParameters(User user, Dictionary<string, object>? parameters)
{
var indicatorType = GetParameterValue<string>(parameters, "indicatorType", string.Empty);
if (string.IsNullOrWhiteSpace(indicatorType))
{
throw new ArgumentException("Indicator type is required");
}
var tradingStyle = GetParameterValue<string?>(parameters, "tradingStyle", null);
var marketVolatility = GetParameterValue<string?>(parameters, "marketVolatility", null);
return await Task.FromResult(_indicatorTools.RefineIndicatorParameters(
indicatorType, tradingStyle, marketVolatility));
}
public async Task<object> ExecuteCompareIndicators(User user, Dictionary<string, object>? parameters)
{
var indicatorTypes = GetParameterValue<string>(parameters, "indicatorTypes", string.Empty);
if (string.IsNullOrWhiteSpace(indicatorTypes))
{
throw new ArgumentException("At least one indicator type is required");
}
return await Task.FromResult(_indicatorTools.CompareIndicators(indicatorTypes));
}
}

View File

@@ -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<BacktestTools> _logger;
public BacktestTools(IBacktester backtester, ILogger<BacktestTools> logger)
public BacktestTools(
IBacktester backtester,
IAccountService accountService,
ILogger<BacktestTools> logger)
{
_backtester = backtester;
_accountService = accountService;
_logger = logger;
}
@@ -134,4 +142,359 @@ public class BacktestTools
throw new InvalidOperationException($"Failed to retrieve backtests: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves a specific backtest by ID for the user
/// </summary>
public async Task<object> GetBacktestById(User user, string id)
{
try
{
var backtest = await _backtester.GetBacktestByIdForUserAsync(user, id);
if (backtest == null)
{
throw new InvalidOperationException($"Backtest with ID '{id}' not found");
}
return new
{
backtest.Id,
backtest.Config,
backtest.FinalPnl,
backtest.WinRate,
backtest.GrowthPercentage,
backtest.HodlPercentage,
backtest.StartDate,
backtest.EndDate,
MaxDrawdown = backtest.Statistics?.MaxDrawdown,
backtest.Fees,
SharpeRatio = backtest.Statistics?.SharpeRatio,
backtest.Score,
backtest.ScoreMessage,
backtest.InitialBalance,
backtest.NetPnl,
backtest.PositionCount,
TradingType = backtest.Config.TradingType,
backtest.Positions
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting backtest {BacktestId} for user {UserId}", id, user.Id);
throw new InvalidOperationException($"Failed to retrieve backtest: {ex.Message}", ex);
}
}
/// <summary>
/// Deletes a specific backtest by ID for the user
/// </summary>
public async Task<object> DeleteBacktest(User user, string id)
{
try
{
var success = await _backtester.DeleteBacktestByUserAsync(user, id);
return new { Success = success, Message = success ? "Backtest deleted successfully" : "Failed to delete backtest" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting backtest {BacktestId} for user {UserId}", id, user.Id);
throw new InvalidOperationException($"Failed to delete backtest: {ex.Message}", ex);
}
}
/// <summary>
/// Deletes multiple backtests by their IDs for the user
/// </summary>
public async Task<object> DeleteBacktestsByIds(User user, IEnumerable<string> ids)
{
try
{
var success = await _backtester.DeleteBacktestsByIdsForUserAsync(user, ids);
var count = ids.Count();
return new { Success = success, Message = success ? $"{count} backtests deleted successfully" : "Failed to delete backtests" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting backtests for user {UserId}", user.Id);
throw new InvalidOperationException($"Failed to delete backtests: {ex.Message}", ex);
}
}
/// <summary>
/// Deletes backtests matching specified filters for the user
/// </summary>
public async Task<object> DeleteBacktestsByFilters(
User user,
double? scoreMin = null,
double? scoreMax = null,
int? winrateMin = null,
int? winrateMax = null,
string? name = null)
{
try
{
var filter = new BacktestsFilter
{
NameContains = name,
ScoreMin = scoreMin,
ScoreMax = scoreMax,
WinrateMin = winrateMin,
WinrateMax = winrateMax
};
var deletedCount = await _backtester.DeleteBacktestsByFiltersAsync(user, filter);
return new { Success = true, DeletedCount = deletedCount, Message = $"{deletedCount} backtests deleted successfully" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting backtests by filters for user {UserId}", user.Id);
throw new InvalidOperationException($"Failed to delete backtests by filters: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves paginated bundle backtest requests for the user
/// </summary>
public async Task<object> GetBundleBacktestsPaginated(
User user,
int page = 1,
int pageSize = 50,
string sortByString = "CreatedAt",
string sortOrder = "desc",
string? statusString = null,
string? name = null)
{
try
{
// Validate inputs
if (page < 1) page = 1;
if (pageSize < 1 || pageSize > 100) pageSize = 50;
if (sortOrder != "asc" && sortOrder != "desc") sortOrder = "desc";
// Parse sortBy enum
if (!Enum.TryParse<BundleBacktestRequestSortableColumn>(sortByString, true, out var sortBy))
{
sortBy = BundleBacktestRequestSortableColumn.CreatedAt;
}
// Parse status enum
BundleBacktestRequestStatus? status = null;
if (!string.IsNullOrWhiteSpace(statusString) &&
Enum.TryParse<BundleBacktestRequestStatus>(statusString, true, out var parsedStatus))
{
status = parsedStatus;
}
var filter = new BundleBacktestRequestsFilter
{
Status = status,
NameContains = name
};
var (bundleRequests, totalCount) = await _backtester.GetBundleBacktestRequestsPaginatedAsync(
page,
pageSize,
sortBy,
sortOrder,
filter);
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
return new
{
BundleRequests = bundleRequests.Select(br => new
{
br.RequestId,
br.Name,
br.Status,
br.CreatedAt,
br.UpdatedAt,
br.User.AgentName,
br.TotalBacktests
}),
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting paginated bundle backtests for user {UserId}", user.Id);
throw new InvalidOperationException($"Failed to retrieve bundle backtests: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves a specific bundle backtest request by ID
/// </summary>
public async Task<object> GetBundleBacktestById(User user, Guid id)
{
try
{
var bundleRequest = await _backtester.GetBundleBacktestRequestByIdForUserAsync(user, id);
if (bundleRequest == null)
{
throw new InvalidOperationException($"Bundle backtest request with ID '{id}' not found");
}
return new
{
bundleRequest.RequestId,
bundleRequest.Name,
bundleRequest.Status,
bundleRequest.CreatedAt,
bundleRequest.UpdatedAt,
bundleRequest.User.AgentName,
bundleRequest.TotalBacktests,
bundleRequest.CompletedBacktests,
bundleRequest.FailedBacktests,
bundleRequest.UniversalConfigJson,
bundleRequest.DateTimeRangesJson,
bundleRequest.MoneyManagementVariantsJson,
bundleRequest.TickerVariantsJson
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting bundle backtest {BundleId} for user {UserId}", id, user.Id);
throw new InvalidOperationException($"Failed to retrieve bundle backtest: {ex.Message}", ex);
}
}
/// <summary>
/// Deletes a bundle backtest request and all associated backtests
/// </summary>
public async Task<object> DeleteBundleBacktest(User user, Guid id)
{
try
{
await _backtester.DeleteBundleBacktestRequestByIdForUserAsync(user, id);
// Also delete associated backtests
await _backtester.DeleteBacktestsByRequestIdAsync(id);
return new { Success = true, Message = "Bundle backtest and associated backtests deleted successfully" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting bundle backtest {BundleId} for user {UserId}", id, user.Id);
throw new InvalidOperationException($"Failed to delete bundle backtest: {ex.Message}", ex);
}
}
/// <summary>
/// Runs a new backtest with the specified configuration
/// </summary>
public async Task<object> RunBacktest(
User user,
string ticker,
string timeframe,
DateTime startDate,
DateTime endDate,
decimal balance,
string? scenarioName = null,
string? moneyManagementName = null,
decimal? stopLoss = null,
decimal? takeProfit = null,
decimal? leverage = null,
string? name = null,
bool save = true)
{
try
{
// Parse ticker enum
if (!Enum.TryParse<Ticker>(ticker, true, out var parsedTicker))
{
throw new ArgumentException($"Invalid ticker: {ticker}");
}
// Parse timeframe enum
if (!Enum.TryParse<Timeframe>(timeframe, true, out var parsedTimeframe))
{
throw new ArgumentException($"Invalid timeframe: {timeframe}");
}
// Validate dates
if (startDate >= endDate)
{
throw new ArgumentException("Start date must be before end date");
}
if (balance <= 0)
{
throw new ArgumentException("Balance must be greater than zero");
}
// Get user's first account
var accounts = await _accountService.GetAccountsByUserAsync(user, hideSecrets: true, getBalance: false);
var firstAccount = accounts.FirstOrDefault();
if (firstAccount == null)
{
throw new InvalidOperationException($"No accounts found for user {user.AgentName}");
}
// Build the config
var config = new TradingBotConfig
{
AccountName = firstAccount.Name,
Ticker = parsedTicker,
Timeframe = parsedTimeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
Name = name ?? $"Backtest_{parsedTicker}_{DateTime.UtcNow:yyyyMMddHHmmss}",
FlipPosition = false,
CooldownPeriod = 0,
MaxLossStreak = 0,
ScenarioName = scenarioName,
TradingType = TradingType.BacktestSpot
};
// Add money management if parameters provided
if (stopLoss.HasValue || takeProfit.HasValue || leverage.HasValue)
{
config.MoneyManagement = new LightMoneyManagement
{
Name = "Custom",
Timeframe = parsedTimeframe,
StopLoss = stopLoss ?? 0,
TakeProfit = takeProfit ?? 0,
Leverage = leverage ?? 1
};
}
// Run the backtest (creates a job)
var result = await _backtester.RunTradingBotBacktest(
config,
startDate,
endDate,
user,
save: save,
withCandles: false);
return new
{
Success = true,
JobId = result.Id,
Message = $"Backtest job created successfully. Job ID: {result.Id}",
Config = new
{
Ticker = parsedTicker.ToString(),
Timeframe = parsedTimeframe.ToString(),
StartDate = startDate,
EndDate = endDate,
Balance = balance,
ScenarioName = scenarioName,
MoneyManagementName = moneyManagementName
},
result.ScoreMessage
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error running backtest for user {UserId}", user.Id);
throw new InvalidOperationException($"Failed to run backtest: {ex.Message}", ex);
}
}
}

View File

@@ -0,0 +1,214 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Bots;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Mcp.Tools;
/// <summary>
/// MCP tools for bot operations
/// </summary>
public class BotTools
{
private readonly IBotRepository _botRepository;
private readonly ILogger<BotTools> _logger;
public BotTools(IBotRepository botRepository, ILogger<BotTools> logger)
{
_botRepository = botRepository;
_logger = logger;
}
/// <summary>
/// Retrieves paginated bots for a user with filtering and sorting capabilities
/// </summary>
public async Task<object> GetBotsPaginated(
User user,
int pageNumber = 1,
int pageSize = 10,
BotStatus? status = null,
string? name = null,
string? ticker = null,
string? agentName = null,
decimal? minBalance = null,
decimal? maxBalance = null,
BotSortableColumn sortBy = BotSortableColumn.CreateDate,
SortDirection sortDirection = SortDirection.Desc,
bool showOnlyProfitable = false)
{
try
{
// Validate inputs
if (pageNumber < 1) pageNumber = 1;
if (pageSize < 1 || pageSize > 100) pageSize = 10;
var (bots, totalCount) = await _botRepository.GetBotsPaginatedAsync(
pageNumber,
pageSize,
status,
name,
ticker,
agentName,
minBalance,
maxBalance,
sortBy,
sortDirection,
showOnlyProfitable);
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
return new
{
Bots = bots.Select(b => new
{
b.Identifier,
b.Name,
b.Ticker,
b.Status,
b.CreateDate,
b.StartupTime,
b.Pnl,
b.Roi,
WinRate = (b.TradeWins + b.TradeLosses) != 0
? (double)b.TradeWins / (b.TradeWins + b.TradeLosses)
: 0.0,
b.BotTradingBalance,
b.TradingType,
AgentName = b.User?.AgentName,
MasterAgentName = b.MasterBotUser?.AgentName
}),
TotalCount = totalCount,
CurrentPage = pageNumber,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = pageNumber < totalPages,
HasPreviousPage = pageNumber > 1
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting paginated bots for user {UserId}", user.Id);
throw new InvalidOperationException($"Failed to retrieve bots: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves a specific bot by identifier for the user
/// </summary>
public async Task<object> GetBotById(User user, Guid identifier)
{
try
{
var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
if (bot == null)
{
throw new InvalidOperationException($"Bot with identifier '{identifier}' not found");
}
// Verify user owns the bot
if (bot.User?.Name != user.Name)
{
throw new UnauthorizedAccessException("You don't have permission to access this bot");
}
return new
{
bot.Identifier,
bot.Name,
bot.Ticker,
bot.Status,
bot.CreateDate,
bot.StartupTime,
bot.Pnl,
bot.Roi,
WinRate = (bot.TradeWins + bot.TradeLosses) != 0
? (double)bot.TradeWins / (bot.TradeWins + bot.TradeLosses)
: 0.0,
bot.BotTradingBalance,
bot.TradingType,
AgentName = bot.User?.AgentName,
MasterAgentName = bot.MasterBotUser?.AgentName,
TradeWins = bot.TradeWins,
TradeLosses = bot.TradeLosses
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting bot {BotId} for user {UserId}", identifier, user.Id);
throw new InvalidOperationException($"Failed to retrieve bot: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves bots by status for the user
/// </summary>
public async Task<object> GetBotsByStatus(User user, BotStatus status)
{
try
{
var bots = await _botRepository.GetBotsByUserIdAsync(user.Id);
var filteredBots = bots.Where(b => b.Status == status);
return new
{
Status = status.ToString(),
Bots = filteredBots.Select(b => new
{
b.Identifier,
b.Name,
b.Ticker,
b.Status,
b.CreateDate,
b.StartupTime,
b.Pnl,
b.Roi,
WinRate = (b.TradeWins + b.TradeLosses) != 0
? (double)b.TradeWins / (b.TradeWins + b.TradeLosses)
: 0.0,
b.BotTradingBalance,
b.TradingType,
AgentName = b.User?.AgentName
}),
Count = filteredBots.Count()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting bots by status {Status} for user {UserId}", status, user.Id);
throw new InvalidOperationException($"Failed to retrieve bots: {ex.Message}", ex);
}
}
/// <summary>
/// Deletes a bot by identifier for the user
/// </summary>
public async Task<object> DeleteBot(User user, Guid identifier)
{
try
{
var bot = await _botRepository.GetBotByIdentifierAsync(identifier);
if (bot == null)
{
throw new InvalidOperationException($"Bot with identifier '{identifier}' not found");
}
// Verify user owns the bot
if (bot.User?.Name != user.Name)
{
throw new UnauthorizedAccessException("You don't have permission to delete this bot");
}
await _botRepository.DeleteBot(identifier);
var success = true;
return new { Success = success, Message = success ? "Bot deleted successfully" : "Failed to delete bot" };
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting bot {BotId} for user {UserId}", identifier, user.Id);
throw new InvalidOperationException($"Failed to delete bot: {ex.Message}", ex);
}
}
}

View File

@@ -0,0 +1,243 @@
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Mcp.Tools;
/// <summary>
/// MCP tools for data operations (tickers, candles, statistics)
/// </summary>
public class DataTools
{
private readonly IExchangeService _exchangeService;
private readonly IStatisticService _statisticService;
private readonly IAgentService _agentService;
private readonly ICacheService _cacheService;
private readonly ILogger<DataTools> _logger;
public DataTools(
IExchangeService exchangeService,
IStatisticService statisticService,
IAgentService agentService,
ICacheService cacheService,
ILogger<DataTools> logger)
{
_exchangeService = exchangeService;
_statisticService = statisticService;
_agentService = agentService;
_cacheService = cacheService;
_logger = logger;
}
/// <summary>
/// Retrieves available tickers for a given timeframe
/// </summary>
public async Task<object> GetTickers(Timeframe timeframe)
{
try
{
var tickers = await _exchangeService.GetTickers(timeframe);
return new
{
Tickers = tickers.Select(t => new
{
Ticker = t.ToString(),
Timeframe = timeframe.ToString()
}),
TotalCount = tickers.Count
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting tickers for timeframe {Timeframe}", timeframe);
throw new InvalidOperationException($"Failed to retrieve tickers: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves candles for a specific ticker and date range
/// </summary>
public async Task<object> GetCandles(
Ticker ticker,
DateTime startDate,
DateTime endDate,
Timeframe timeframe)
{
try
{
var candles = await _exchangeService.GetCandlesInflux(
TradingExchanges.Evm,
ticker,
startDate,
timeframe,
endDate);
return new
{
Ticker = ticker.ToString(),
Timeframe = timeframe.ToString(),
StartDate = startDate,
EndDate = endDate,
Candles = candles.Select(c => new
{
c.Date,
c.Open,
c.High,
c.Low,
c.Close,
c.Volume
}).OrderBy(c => c.Date),
TotalCount = candles.Count
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting candles for {Ticker} from {StartDate} to {EndDate}",
ticker, startDate, endDate);
throw new InvalidOperationException($"Failed to retrieve candles: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves platform statistics and spotlight data
/// </summary>
public async Task<object> GetSpotlight()
{
try
{
var spotlight = await _statisticService.GetLastSpotlight(DateTime.Now.AddDays(-2));
return spotlight;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting spotlight data");
throw new InvalidOperationException($"Failed to retrieve spotlight data: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves agent balance history within a date range
/// </summary>
public async Task<object> GetAgentBalances(
string agentName,
DateTime startDate,
DateTime? endDate = null)
{
try
{
var balances = await _agentService.GetAgentBalances(agentName, startDate, endDate);
return balances;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting agent balances for {AgentName} from {StartDate} to {EndDate}",
agentName, startDate, endDate);
throw new InvalidOperationException($"Failed to retrieve agent balances: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves list of online agents
/// </summary>
public async Task<object> GetOnlineAgents()
{
try
{
var agentNames = await _agentService.GetAllOnlineAgents();
return new
{
Agents = agentNames.Select(name => new
{
AgentName = name,
IsOnline = true
}),
TotalCount = agentNames.Count()
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting online agents");
throw new InvalidOperationException($"Failed to retrieve online agents: {ex.Message}", ex);
}
}
/// <summary>
/// Retrieves paginated agent summaries with filtering and sorting
/// </summary>
public async Task<object> GetAgentsPaginated(
int page = 1,
int pageSize = 10,
string sortBy = "NetPnL",
string sortOrder = "desc",
string? agentNames = null,
bool showOnlyProfitable = false)
{
try
{
// Validate inputs
if (page < 1) page = 1;
if (pageSize < 1 || pageSize > 100) pageSize = 10;
if (sortOrder != "asc" && sortOrder != "desc") sortOrder = "desc";
// Get all agent summaries
var allAgents = await _agentService.GetAllAgentSummaries();
// Filter by agent names if provided
if (!string.IsNullOrWhiteSpace(agentNames))
{
var agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(name => name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
allAgents = allAgents.Where(a => agentNamesList.Contains(a.AgentName));
}
// Filter profitable agents if requested
if (showOnlyProfitable)
{
allAgents = allAgents.Where(a => a.NetPnL > 0);
}
var totalCount = allAgents.Count();
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Apply pagination
var paginatedAgents = allAgents
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
return new
{
Agents = paginatedAgents.Select(a => new
{
a.AgentName,
a.TotalPnL,
a.NetPnL,
a.TotalROI,
a.Wins,
a.Losses,
WinRate = (a.Wins + a.Losses) > 0 ? (a.Wins * 100) / (a.Wins + a.Losses) : 0,
a.ActiveStrategiesCount,
a.TotalVolume,
a.TotalBalance,
a.TotalFees,
a.BacktestCount
}),
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting paginated agents");
throw new InvalidOperationException($"Failed to retrieve agents: {ex.Message}", ex);
}
}
}

View File

@@ -0,0 +1,973 @@
using System.Text.RegularExpressions;
using Managing.Domain.Indicators;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Mcp.Tools;
/// <summary>
/// MCP tools for indicator operations (list, explain, recommend, refine, compare)
/// </summary>
public class IndicatorTools
{
private readonly ILogger<IndicatorTools> _logger;
private readonly Dictionary<IndicatorType, IndicatorInfo> _indicatorInfoCache;
public IndicatorTools(ILogger<IndicatorTools> logger)
{
_logger = logger;
_indicatorInfoCache = LoadIndicatorInfo();
}
/// <summary>
/// Lists all available indicators with their types and basic information
/// </summary>
public object ListIndicators(string? signalType = null, string? category = null)
{
try
{
var indicators = Enum.GetValues<IndicatorType>()
.Where(it => it != IndicatorType.Composite) // Exclude Composite as it's special
.Select(type => new
{
Type = type.ToString(),
SignalType = GetSignalType(type).ToString(),
Category = GetCategory(type),
Name = GetIndicatorName(type),
Description = GetShortDescription(type)
})
.ToList();
// Filter by signal type if provided
if (!string.IsNullOrWhiteSpace(signalType) &&
Enum.TryParse<SignalType>(signalType, true, out var signalTypeEnum))
{
indicators = indicators.Where(i => i.SignalType == signalTypeEnum.ToString()).ToList();
}
// Filter by category if provided
if (!string.IsNullOrWhiteSpace(category))
{
indicators = indicators.Where(i =>
i.Category.Contains(category, StringComparison.OrdinalIgnoreCase)).ToList();
}
return new
{
Indicators = indicators,
TotalCount = indicators.Count,
SignalTypeFilter = signalType,
CategoryFilter = category
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error listing indicators");
throw new InvalidOperationException($"Failed to list indicators: {ex.Message}", ex);
}
}
/// <summary>
/// Gets detailed information about a specific indicator
/// </summary>
public object GetIndicatorInfo(string indicatorType)
{
try
{
if (!Enum.TryParse<IndicatorType>(indicatorType, true, out var type))
{
throw new ArgumentException($"Invalid indicator type: {indicatorType}");
}
if (!_indicatorInfoCache.TryGetValue(type, out var info))
{
throw new ArgumentException($"Information not available for indicator: {indicatorType}");
}
return new
{
Type = type.ToString(),
SignalType = GetSignalType(type).ToString(),
Category = GetCategory(type),
Name = info.Name,
Description = info.Description,
LongDescription = info.LongDescription,
TriggersLong = info.TriggersLong,
TriggersShort = info.TriggersShort,
Parameters = info.Parameters,
RecommendedParameters = info.RecommendedParameters,
UseCases = info.UseCases,
TradingStyleRecommendations = info.TradingStyleRecommendations
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting indicator info for {IndicatorType}", indicatorType);
throw new InvalidOperationException($"Failed to get indicator info: {ex.Message}", ex);
}
}
/// <summary>
/// Explains how an indicator works, when to use it, and provides examples
/// </summary>
public object ExplainIndicator(string indicatorType, bool includeExamples = true)
{
try
{
if (!Enum.TryParse<IndicatorType>(indicatorType, true, out var type))
{
throw new ArgumentException($"Invalid indicator type: {indicatorType}");
}
if (!_indicatorInfoCache.TryGetValue(type, out var info))
{
throw new ArgumentException($"Information not available for indicator: {indicatorType}");
}
var explanation = new
{
Type = type.ToString(),
SignalType = GetSignalType(type).ToString(),
Category = GetCategory(type),
Name = info.Name,
HowItWorks = info.LongDescription,
WhenToUse = info.UseCases,
TriggersLong = info.TriggersLong,
TriggersShort = info.TriggersShort,
Parameters = info.Parameters.Select(p => new
{
p.Name,
p.Description,
RecommendedValue = p.RecommendedValue,
Range = p.Range
}),
RecommendedParameters = info.RecommendedParameters,
TradingStyleRecommendations = info.TradingStyleRecommendations,
Examples = includeExamples ? GetIndicatorExamples(type) : null
};
return explanation;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error explaining indicator {IndicatorType}", indicatorType);
throw new InvalidOperationException($"Failed to explain indicator: {ex.Message}", ex);
}
}
/// <summary>
/// Recommends indicators based on trading style, market conditions, and goals
/// </summary>
public object RecommendIndicators(
string? tradingStyle = null,
string? marketCondition = null,
string? goal = null,
int? maxRecommendations = null)
{
try
{
var recommendations = new List<IndicatorRecommendation>();
foreach (var type in Enum.GetValues<IndicatorType>())
{
if (type == IndicatorType.Composite) continue;
if (!_indicatorInfoCache.TryGetValue(type, out var info)) continue;
var score = CalculateRecommendationScore(
type, info, tradingStyle, marketCondition, goal);
if (score > 0)
{
recommendations.Add(new IndicatorRecommendation
{
IndicatorType = type.ToString(),
SignalType = GetSignalType(type).ToString(),
Category = GetCategory(type),
Name = info.Name,
Score = score,
Reasons = GetRecommendationReasons(type, info, tradingStyle, marketCondition, goal)
});
}
}
// Sort by score descending
recommendations = recommendations.OrderByDescending(r => r.Score).ToList();
// Limit results if requested
if (maxRecommendations.HasValue && maxRecommendations.Value > 0)
{
recommendations = recommendations.Take(maxRecommendations.Value).ToList();
}
return new
{
Recommendations = recommendations,
TotalCount = recommendations.Count,
Filters = new
{
TradingStyle = tradingStyle ?? "Any",
MarketCondition = marketCondition ?? "Any",
Goal = goal ?? "Any"
}
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error recommending indicators");
throw new InvalidOperationException($"Failed to recommend indicators: {ex.Message}", ex);
}
}
/// <summary>
/// Suggests parameter refinements for an indicator based on trading style
/// </summary>
public object RefineIndicatorParameters(
string indicatorType,
string? tradingStyle = null,
string? marketVolatility = null)
{
try
{
if (!Enum.TryParse<IndicatorType>(indicatorType, true, out var type))
{
throw new ArgumentException($"Invalid indicator type: {indicatorType}");
}
if (!_indicatorInfoCache.TryGetValue(type, out var info))
{
throw new ArgumentException($"Information not available for indicator: {indicatorType}");
}
var refinements = new List<ParameterRefinement>();
foreach (var param in info.Parameters)
{
var refinement = new ParameterRefinement
{
ParameterName = param.Name,
Description = param.Description,
CurrentRecommended = param.RecommendedValue,
RefinedValues = GetRefinedParameterValues(
type, param.Name, tradingStyle, marketVolatility),
Reasoning = GetParameterRefinementReasoning(
type, param.Name, tradingStyle, marketVolatility)
};
refinements.Add(refinement);
}
return new
{
IndicatorType = type.ToString(),
SignalType = GetSignalType(type).ToString(),
TradingStyle = tradingStyle ?? "General",
MarketVolatility = marketVolatility ?? "Normal",
ParameterRefinements = refinements,
GeneralAdvice = GetGeneralParameterAdvice(type, tradingStyle, marketVolatility)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refining parameters for {IndicatorType}", indicatorType);
throw new InvalidOperationException($"Failed to refine parameters: {ex.Message}", ex);
}
}
/// <summary>
/// Compares multiple indicators side by side
/// </summary>
public object CompareIndicators(string indicatorTypes)
{
try
{
var types = indicatorTypes.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(t => t.Trim())
.Where(t => Enum.TryParse<IndicatorType>(t, true, out _))
.Select(t => Enum.Parse<IndicatorType>(t, true))
.Distinct()
.ToList();
if (types.Count == 0)
{
throw new ArgumentException("At least one valid indicator type is required");
}
var comparisons = types.Select(type =>
{
_indicatorInfoCache.TryGetValue(type, out var info);
return new
{
Type = type.ToString(),
SignalType = GetSignalType(type).ToString(),
Category = GetCategory(type),
Name = info?.Name ?? type.ToString(),
Description = info?.Description ?? "No description available",
UseCases = info?.UseCases ?? new List<string>(),
Parameters = info?.Parameters.Select(p => p.Name).ToList() ?? new List<string>(),
BestFor = GetBestForDescription(type, info)
};
}).ToList();
return new
{
Indicators = comparisons,
Count = comparisons.Count,
ComparisonSummary = GenerateComparisonSummary(comparisons.Cast<dynamic>().ToList())
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error comparing indicators");
throw new InvalidOperationException($"Failed to compare indicators: {ex.Message}", ex);
}
}
#region Helper Methods
private SignalType GetSignalType(IndicatorType type)
{
return type switch
{
IndicatorType.EmaTrend or IndicatorType.StochRsiTrend or IndicatorType.IchimokuKumoTrend => SignalType.Trend,
IndicatorType.StDev or IndicatorType.BollingerBandsVolatilityProtection => SignalType.Context,
_ => SignalType.Signal
};
}
private string GetCategory(IndicatorType type)
{
return type switch
{
IndicatorType.RsiDivergence or IndicatorType.RsiDivergenceConfirm => "RSI",
IndicatorType.EmaCross or IndicatorType.EmaTrend or IndicatorType.DualEmaCross or IndicatorType.SuperTrendCrossEma => "EMA",
IndicatorType.MacdCross => "MACD",
IndicatorType.SuperTrend or IndicatorType.ChandelierExit => "Trend Following",
IndicatorType.Stc or IndicatorType.LaggingStc => "STC",
IndicatorType.StochRsiTrend or IndicatorType.StochasticCross => "Stochastic",
IndicatorType.ThreeWhiteSoldiers => "Candlestick",
IndicatorType.StDev or IndicatorType.BollingerBandsVolatilityProtection => "Volatility",
IndicatorType.BollingerBandsPercentBMomentumBreakout => "Bollinger Bands",
IndicatorType.IchimokuKumoTrend => "Ichimoku",
_ => "Other"
};
}
private string GetIndicatorName(IndicatorType type)
{
return type switch
{
IndicatorType.RsiDivergence => "RSI Divergence",
IndicatorType.RsiDivergenceConfirm => "RSI Divergence Confirm",
IndicatorType.MacdCross => "MACD Cross",
IndicatorType.EmaCross => "EMA Cross",
IndicatorType.DualEmaCross => "Dual EMA Cross",
IndicatorType.EmaTrend => "EMA Trend",
IndicatorType.SuperTrend => "SuperTrend",
IndicatorType.SuperTrendCrossEma => "SuperTrend Cross EMA",
IndicatorType.ChandelierExit => "Chandelier Exit",
IndicatorType.Stc => "STC (Schaff Trend Cycle)",
IndicatorType.LaggingStc => "Lagging STC",
IndicatorType.StochRsiTrend => "Stochastic RSI Trend",
IndicatorType.StochasticCross => "Stochastic Cross",
IndicatorType.ThreeWhiteSoldiers => "Three White Soldiers",
IndicatorType.StDev => "Standard Deviation Context",
IndicatorType.BollingerBandsPercentBMomentumBreakout => "Bollinger Bands Momentum Breakout",
IndicatorType.BollingerBandsVolatilityProtection => "Bollinger Bands Volatility Protection",
IndicatorType.IchimokuKumoTrend => "Ichimoku Kumo Trend",
_ => type.ToString()
};
}
private string GetShortDescription(IndicatorType type)
{
return _indicatorInfoCache.TryGetValue(type, out var info)
? info.Description
: "No description available";
}
private Dictionary<IndicatorType, IndicatorInfo> LoadIndicatorInfo()
{
var info = new Dictionary<IndicatorType, IndicatorInfo>();
var readmePath = Path.Combine(
AppDomain.CurrentDomain.BaseDirectory,
"..", "..", "..", "..", "..",
"src", "Managing.Domain", "Indicators", "README-Indicators.md");
// Try multiple paths
if (!File.Exists(readmePath))
{
readmePath = Path.Combine(
Directory.GetCurrentDirectory(),
"src", "Managing.Domain", "Indicators", "README-Indicators.md");
}
if (!File.Exists(readmePath))
{
_logger.LogWarning("README-Indicators.md not found, using default indicator info");
return LoadDefaultIndicatorInfo();
}
try
{
var content = File.ReadAllText(readmePath);
return ParseIndicatorInfoFromReadme(content);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading indicator info from README, using defaults");
return LoadDefaultIndicatorInfo();
}
}
private Dictionary<IndicatorType, IndicatorInfo> ParseIndicatorInfoFromReadme(string content)
{
var info = new Dictionary<IndicatorType, IndicatorInfo>();
// Parse each indicator section
var sections = Regex.Split(content, @"^##\s+", RegexOptions.Multiline);
foreach (var section in sections)
{
if (string.IsNullOrWhiteSpace(section)) continue;
var lines = section.Split('\n');
if (lines.Length == 0) continue;
var title = lines[0].Trim();
var indicatorType = FindIndicatorTypeByTitle(title);
if (indicatorType == null) continue;
var indicatorInfo = ParseIndicatorSection(section, indicatorType.Value);
info[indicatorType.Value] = indicatorInfo;
}
// Fill in any missing indicators with defaults
foreach (var type in Enum.GetValues<IndicatorType>())
{
if (type == IndicatorType.Composite) continue;
if (!info.ContainsKey(type))
{
info[type] = GetDefaultIndicatorInfo(type);
}
}
return info;
}
private IndicatorType? FindIndicatorTypeByTitle(string title)
{
var titleLower = title.ToLowerInvariant();
return Enum.GetValues<IndicatorType>().FirstOrDefault(type =>
{
var name = GetIndicatorName(type).ToLowerInvariant();
return titleLower.Contains(name.ToLowerInvariant()) ||
titleLower.Contains(type.ToString().ToLowerInvariant());
});
}
private IndicatorInfo ParseIndicatorSection(string section, IndicatorType type)
{
var info = new IndicatorInfo
{
Name = GetIndicatorName(type),
Description = ExtractField(section, "Description:"),
LongDescription = ExtractField(section, "Description:") ?? "",
TriggersLong = ExtractListItems(section, "Trigger a Long"),
TriggersShort = ExtractListItems(section, "Trigger a Short"),
Parameters = ExtractParameters(section),
RecommendedParameters = ExtractRecommendedParameters(section),
UseCases = ExtractUseCases(section, type),
TradingStyleRecommendations = ExtractTradingStyleRecommendations(section, type)
};
return info;
}
private string? ExtractField(string section, string fieldName)
{
var pattern = $@"{Regex.Escape(fieldName)}\s*\n\s*\*\*(.*?)\*\*";
var match = Regex.Match(section, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
return match.Success ? match.Groups[1].Value.Trim() : null;
}
private List<string> ExtractListItems(string section, string header)
{
var items = new List<string>();
var pattern = $@"{Regex.Escape(header)}.*?\n((?:\s*•\s*.*?\n?)+)";
var match = Regex.Match(section, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (match.Success)
{
var itemsText = match.Groups[1].Value;
var itemMatches = Regex.Matches(itemsText, @"•\s*(.+?)(?=\n\s*•|\n\n|$)");
items.AddRange(itemMatches.Cast<Match>().Select(m => m.Groups[1].Value.Trim()));
}
return items;
}
private List<ParameterInfo> ExtractParameters(string section)
{
var parameters = new List<ParameterInfo>();
var pattern = @"\*\*Parameters\*\*\s*\n((?:\s*•\s*.*?\n?)+)";
var match = Regex.Match(section, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (match.Success)
{
var paramsText = match.Groups[1].Value;
var paramMatches = Regex.Matches(paramsText, @"•\s*(\w+)\s*\(recommended:\s*([^)]+)\)\s*-\s*(.+?)(?=\n\s*•|\n\n|$)");
foreach (Match m in paramMatches)
{
parameters.Add(new ParameterInfo
{
Name = m.Groups[1].Value.Trim(),
RecommendedValue = m.Groups[2].Value.Trim(),
Description = m.Groups[3].Value.Trim(),
Range = ExtractRange(m.Groups[2].Value)
});
}
}
return parameters;
}
private string? ExtractRange(string recommendedValue)
{
if (recommendedValue.Contains("-"))
{
return recommendedValue;
}
return null;
}
private Dictionary<string, string> ExtractRecommendedParameters(string section)
{
var recommended = new Dictionary<string, string>();
var paramMatches = Regex.Matches(section, @"(\w+)\s*\(recommended:\s*([^)]+)\)");
foreach (Match m in paramMatches)
{
recommended[m.Groups[1].Value.Trim()] = m.Groups[2].Value.Trim();
}
return recommended;
}
private List<string> ExtractUseCases(string section, IndicatorType type)
{
var useCases = new List<string>();
var signalType = GetSignalType(type);
switch (signalType)
{
case SignalType.Signal:
useCases.Add("Generate precise entry/exit signals");
useCases.Add("Identify high-probability trade entries");
break;
case SignalType.Trend:
useCases.Add("Identify overall market direction");
useCases.Add("Confirm market momentum");
useCases.Add("Avoid counter-trend trades");
break;
case SignalType.Context:
useCases.Add("Provide market context");
useCases.Add("Filter trades in unfavorable conditions");
useCases.Add("Risk management");
break;
}
return useCases;
}
private Dictionary<string, string> ExtractTradingStyleRecommendations(string section, IndicatorType type)
{
var recommendations = new Dictionary<string, string>();
// Extract from README sections about trading styles
if (section.Contains("Short-term") || section.Contains("Scalping"))
{
recommendations["Short-term"] = "Suitable for quick signals";
}
if (section.Contains("Medium-term") || section.Contains("Swing"))
{
recommendations["Medium-term"] = "Good for swing trading";
}
if (section.Contains("Long-term") || section.Contains("Position"))
{
recommendations["Long-term"] = "Ideal for position trading";
}
return recommendations;
}
private int CalculateRecommendationScore(
IndicatorType type,
IndicatorInfo info,
string? tradingStyle,
string? marketCondition,
string? goal)
{
var score = 0;
// Base score
score += 10;
// Trading style matching
if (!string.IsNullOrWhiteSpace(tradingStyle))
{
var styleLower = tradingStyle.ToLowerInvariant();
if (styleLower.Contains("scalp") || styleLower.Contains("short"))
{
if (GetSignalType(type) == SignalType.Signal)
score += 20;
}
else if (styleLower.Contains("swing") || styleLower.Contains("medium"))
{
if (GetSignalType(type) == SignalType.Trend || GetSignalType(type) == SignalType.Signal)
score += 20;
}
else if (styleLower.Contains("position") || styleLower.Contains("long"))
{
if (GetSignalType(type) == SignalType.Trend)
score += 20;
}
}
// Market condition matching
if (!string.IsNullOrWhiteSpace(marketCondition))
{
var conditionLower = marketCondition.ToLowerInvariant();
if (conditionLower.Contains("volatile") || conditionLower.Contains("high volatility"))
{
if (type == IndicatorType.StDev || type == IndicatorType.BollingerBandsVolatilityProtection)
score += 15;
}
else if (conditionLower.Contains("trend") || conditionLower.Contains("trending"))
{
if (GetSignalType(type) == SignalType.Trend)
score += 15;
}
}
// Goal matching
if (!string.IsNullOrWhiteSpace(goal))
{
var goalLower = goal.ToLowerInvariant();
if (goalLower.Contains("entry") || goalLower.Contains("signal"))
{
if (GetSignalType(type) == SignalType.Signal)
score += 15;
}
else if (goalLower.Contains("filter") || goalLower.Contains("context"))
{
if (GetSignalType(type) == SignalType.Context)
score += 15;
}
}
return score;
}
private List<string> GetRecommendationReasons(
IndicatorType type,
IndicatorInfo info,
string? tradingStyle,
string? marketCondition,
string? goal)
{
var reasons = new List<string>();
var signalType = GetSignalType(type);
if (!string.IsNullOrWhiteSpace(tradingStyle))
{
reasons.Add($"Suitable for {tradingStyle} trading style");
}
if (signalType == SignalType.Signal)
{
reasons.Add("Generates precise entry/exit signals");
}
else if (signalType == SignalType.Trend)
{
reasons.Add("Identifies market direction and momentum");
}
else if (signalType == SignalType.Context)
{
reasons.Add("Provides market context and risk filtering");
}
return reasons;
}
private Dictionary<string, string> GetRefinedParameterValues(
IndicatorType type,
string parameterName,
string? tradingStyle,
string? marketVolatility)
{
var refined = new Dictionary<string, string>();
// Default recommendations based on trading style
if (tradingStyle != null)
{
var styleLower = tradingStyle.ToLowerInvariant();
if (styleLower.Contains("scalp") || styleLower.Contains("short"))
{
refined["Short-term"] = GetShortTermParameterValue(type, parameterName);
}
else if (styleLower.Contains("swing") || styleLower.Contains("medium"))
{
refined["Medium-term"] = GetMediumTermParameterValue(type, parameterName);
}
else if (styleLower.Contains("position") || styleLower.Contains("long"))
{
refined["Long-term"] = GetLongTermParameterValue(type, parameterName);
}
}
// Volatility adjustments
if (marketVolatility != null)
{
var volLower = marketVolatility.ToLowerInvariant();
if (volLower.Contains("high"))
{
refined["High Volatility"] = GetHighVolatilityParameterValue(type, parameterName);
}
else if (volLower.Contains("low"))
{
refined["Low Volatility"] = GetLowVolatilityParameterValue(type, parameterName);
}
}
return refined;
}
private string GetShortTermParameterValue(IndicatorType type, string parameterName)
{
// Short-term trading recommendations
return parameterName.ToLowerInvariant() switch
{
"period" => type switch
{
IndicatorType.RsiDivergence => "3-5",
IndicatorType.EmaCross => "10-20",
IndicatorType.EmaTrend => "10-20",
_ => "5-10"
},
"lookbackperiod" => "3-5",
_ => "Shorter periods for responsiveness"
};
}
private string GetMediumTermParameterValue(IndicatorType type, string parameterName)
{
return parameterName.ToLowerInvariant() switch
{
"period" => type switch
{
IndicatorType.RsiDivergence => "5-10",
IndicatorType.EmaCross => "20-50",
IndicatorType.EmaTrend => "20-50",
_ => "10-20"
},
"lookbackperiod" => "5-10",
_ => "Standard periods for balanced performance"
};
}
private string GetLongTermParameterValue(IndicatorType type, string parameterName)
{
return parameterName.ToLowerInvariant() switch
{
"period" => type switch
{
IndicatorType.RsiDivergence => "10-15",
IndicatorType.EmaCross => "50-100",
IndicatorType.EmaTrend => "50-100",
_ => "20-30"
},
"lookbackperiod" => "10-15",
_ => "Longer periods for stability"
};
}
private string GetHighVolatilityParameterValue(IndicatorType type, string parameterName)
{
return "Longer periods to reduce noise";
}
private string GetLowVolatilityParameterValue(IndicatorType type, string parameterName)
{
return "Shorter periods to capture subtle patterns";
}
private string GetParameterRefinementReasoning(
IndicatorType type,
string parameterName,
string? tradingStyle,
string? marketVolatility)
{
var reasoning = new List<string>();
if (!string.IsNullOrWhiteSpace(tradingStyle))
{
reasoning.Add($"Adjusted for {tradingStyle} trading style");
}
if (!string.IsNullOrWhiteSpace(marketVolatility))
{
reasoning.Add($"Optimized for {marketVolatility} market conditions");
}
return string.Join(". ", reasoning);
}
private string GetGeneralParameterAdvice(IndicatorType type, string? tradingStyle, string? marketVolatility)
{
var advice = new List<string>();
if (tradingStyle != null)
{
var styleLower = tradingStyle.ToLowerInvariant();
if (styleLower.Contains("scalp"))
{
advice.Add("Use shorter periods for quick signals");
}
else if (styleLower.Contains("swing"))
{
advice.Add("Use medium periods for balanced performance");
}
else if (styleLower.Contains("position"))
{
advice.Add("Use longer periods for trend confirmation");
}
}
if (marketVolatility != null)
{
var volLower = marketVolatility.ToLowerInvariant();
if (volLower.Contains("high"))
{
advice.Add("Consider longer periods to filter out noise");
}
else if (volLower.Contains("low"))
{
advice.Add("Shorter periods can capture subtle movements");
}
}
return string.Join(" ", advice);
}
private List<string> GetIndicatorExamples(IndicatorType type)
{
return new List<string>
{
$"Example: Use {GetIndicatorName(type)} to identify entry points",
$"Combine with trend indicators for confirmation",
$"Backtest with different parameters to find optimal settings"
};
}
private string GetBestForDescription(IndicatorType type, IndicatorInfo? info)
{
var signalType = GetSignalType(type);
return signalType switch
{
SignalType.Signal => "Precise entry/exit signals",
SignalType.Trend => "Trend identification and confirmation",
SignalType.Context => "Market context and risk filtering",
_ => "General trading analysis"
};
}
private string GenerateComparisonSummary(List<dynamic> comparisons)
{
var signalTypes = comparisons.Select(c => c.SignalType).Distinct().ToList();
var categories = comparisons.Select(c => c.Category).Distinct().ToList();
return $"Comparing {comparisons.Count} indicators across {signalTypes.Count} signal types and {categories.Count} categories";
}
private Dictionary<IndicatorType, IndicatorInfo> LoadDefaultIndicatorInfo()
{
var info = new Dictionary<IndicatorType, IndicatorInfo>();
foreach (var type in Enum.GetValues<IndicatorType>())
{
if (type == IndicatorType.Composite) continue;
info[type] = GetDefaultIndicatorInfo(type);
}
return info;
}
private IndicatorInfo GetDefaultIndicatorInfo(IndicatorType type)
{
return new IndicatorInfo
{
Name = GetIndicatorName(type),
Description = $"{GetIndicatorName(type)} indicator",
LongDescription = $"The {GetIndicatorName(type)} indicator provides trading signals based on technical analysis.",
TriggersLong = new List<string> { "Generates long signals based on indicator logic" },
TriggersShort = new List<string> { "Generates short signals based on indicator logic" },
Parameters = new List<ParameterInfo>(),
RecommendedParameters = new Dictionary<string, string>(),
UseCases = ExtractUseCases("", type),
TradingStyleRecommendations = new Dictionary<string, string>()
};
}
#endregion
#region Helper Classes
private class IndicatorInfo
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string LongDescription { get; set; } = string.Empty;
public List<string> TriggersLong { get; set; } = new();
public List<string> TriggersShort { get; set; } = new();
public List<ParameterInfo> Parameters { get; set; } = new();
public Dictionary<string, string> RecommendedParameters { get; set; } = new();
public List<string> UseCases { get; set; } = new();
public Dictionary<string, string> TradingStyleRecommendations { get; set; } = new();
}
private class ParameterInfo
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string RecommendedValue { get; set; } = string.Empty;
public string? Range { get; set; }
}
private class IndicatorRecommendation
{
public string IndicatorType { get; set; } = string.Empty;
public string SignalType { get; set; } = string.Empty;
public string Category { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int Score { get; set; }
public List<string> Reasons { get; set; } = new();
}
private class ParameterRefinement
{
public string ParameterName { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string CurrentRecommended { get; set; } = string.Empty;
public Dictionary<string, string> RefinedValues { get; set; } = new();
public string Reasoning { get; set; } = string.Empty;
}
#endregion
}