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