Trading bot grain (#33)

* Trading bot Grain

* Fix a bit more of the trading bot

* Advance on the tradingbot grain

* Fix build

* Fix db script

* Fix user login

* Fix a bit backtest

* Fix cooldown and backtest

* start fixing bot start

* Fix startup

* Setup local db

* Fix build and update candles and scenario

* Add bot registry

* Add reminder

* Updateing the grains

* fix bootstraping

* Save stats on tick

* Save bot data every tick

* Fix serialization

* fix save bot stats

* Fix get candles

* use dict instead of list for position

* Switch hashset to dict

* Fix a bit

* Fix bot launch and bot view

* add migrations

* Remove the tolist

* Add agent grain

* Save agent summary

* clean

* Add save bot

* Update get bots

* Add get bots

* Fix stop/restart

* fix Update config

* Update scanner table on new backtest saved

* Fix backtestRowDetails.tsx

* Fix agentIndex

* Update agentIndex

* Fix more things

* Update user cache

* Fix

* Fix account load/start/restart/run
This commit is contained in:
Oda
2025-08-04 23:07:06 +02:00
committed by GitHub
parent cd378587aa
commit 082ae8714b
215 changed files with 9562 additions and 14028 deletions

View File

@@ -340,7 +340,7 @@ public class BacktestController : BaseController
// Convert IndicatorRequest objects to Indicator domain objects
foreach (var indicatorRequest in request.Config.Scenario.Indicators)
{
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,
@@ -706,7 +706,6 @@ public class BacktestController : BaseController
}
public MoneyManagement Map(MoneyManagementRequest moneyManagementRequest)
{
return new MoneyManagement

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
using Managing.Api.Models.Requests;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
@@ -8,7 +7,6 @@ using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
@@ -244,7 +242,7 @@ public class DataController : ControllerBase
{
return Ok(new CandlesWithIndicatorsResponse
{
Candles = new List<Candle>(),
Candles = new HashSet<Candle>(),
IndicatorsValues = new Dictionary<IndicatorType, IndicatorsResultBase>()
});
}
@@ -290,8 +288,8 @@ public class DataController : ControllerBase
}
// Get active bots
var activeBots = await _mediator.Send(new GetActiveBotsCommand());
var currentCount = activeBots.Count;
var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Up));
var currentCount = activeBots.Count();
// Get previous count from cache
var previousCount = _cacheService.GetValue<int>(previousCountKey);
@@ -343,11 +341,11 @@ public class DataController : ControllerBase
}
// Get active bots
var activeBots = await _mediator.Send(new GetActiveBotsCommand());
var activeBots = await _mediator.Send(new GetBotsByStatusCommand(BotStatus.Up));
// Calculate PnL for each bot once and store in a list of tuples
var botsWithPnL = activeBots
.Select(bot => new { Bot = bot, PnL = bot.GetProfitAndLoss() })
.Select(bot => new { Bot = bot, PnL = bot.Pnl })
.OrderByDescending(item => item.PnL)
.Take(3)
.ToList();
@@ -441,55 +439,42 @@ public class DataController : ControllerBase
/// </summary>
/// <param name="strategy">The trading bot to map</param>
/// <returns>A view model with detailed strategy information</returns>
private UserStrategyDetailsViewModel MapStrategyToViewModel(ITradingBot strategy)
private UserStrategyDetailsViewModel MapStrategyToViewModel(Bot strategy)
{
// Get the runtime directly from the bot
TimeSpan runtimeSpan = strategy.GetRuntime();
// Get the startup time from the bot's internal property
// If bot is not running, we use MinValue as a placeholder
DateTime startupTime = DateTime.MinValue;
if (strategy is Bot bot && bot.StartupTime != DateTime.MinValue)
{
startupTime = bot.StartupTime;
}
// Calculate ROI percentage based on PnL relative to account value
decimal pnl = strategy.GetProfitAndLoss();
decimal pnl = strategy.Pnl;
// If we had initial investment amount, we could calculate ROI like:
decimal initialInvestment = 1000; // Example placeholder, ideally should come from the account
decimal roi = pnl != 0 ? (pnl / initialInvestment) * 100 : 0;
// Calculate volume statistics
decimal totalVolume = TradingBox.GetTotalVolumeTraded(strategy.Positions);
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(strategy.Positions);
decimal totalVolume = strategy.Volume;
decimal volumeLast24h = strategy.Volume;
// Calculate win/loss statistics
(int wins, int losses) = TradingBox.GetWinLossCount(strategy.Positions);
(int wins, int losses) = (strategy.TradeWins, strategy.TradeLosses);
int winRate = wins + losses > 0 ? (wins * 100) / (wins + losses) : 0;
// Calculate ROI for last 24h
decimal roiLast24h = TradingBox.GetLast24HROI(strategy.Positions);
decimal roiLast24h = strategy.Roi;
return new UserStrategyDetailsViewModel
{
Name = strategy.Name,
ScenarioName = strategy.Config.ScenarioName,
State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
State = strategy.Status.ToString(),
PnL = pnl,
ROIPercentage = roi,
ROILast24H = roiLast24h,
Runtime = startupTime,
WinRate = strategy.GetWinRate(),
Runtime = strategy.StartupTime,
WinRate = winRate,
TotalVolumeTraded = totalVolume,
VolumeLast24H = volumeLast24h,
Wins = wins,
Losses = losses,
Positions = strategy.Positions.OrderByDescending(p => p.Date)
.ToList(), // Include sorted positions with most recent first
Identifier = strategy.Identifier,
WalletBalances = strategy.WalletBalances,
Positions = new Dictionary<Guid, Position>(),
Identifier = strategy.Identifier.ToString(),
WalletBalances = new Dictionary<DateTime, decimal>(),
};
}
@@ -544,18 +529,17 @@ public class DataController : ControllerBase
continue; // Skip agents with no strategies
}
// Combine all positions from all strategies
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
// TODO: Add this calculation into repository for better performance
var globalPnL = strategies.Sum(s => s.Pnl);
var globalVolume = strategies.Sum(s => s.Volume);
var globalVolumeLast24h = strategies.Sum(s => s.Volume);
// Calculate agent metrics for platform totals
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
// Add to platform totals
totalPlatformPnL += totalPnL;
totalPlatformVolume += totalVolume;
totalPlatformVolumeLast24h += volumeLast24h;
totalPlatformPnL += globalPnL;
totalPlatformVolume += globalVolume;
totalPlatformVolumeLast24h += globalVolumeLast24h;
}
// Set the platform totals
@@ -569,128 +553,25 @@ public class DataController : ControllerBase
return Ok(summary);
}
/// <summary>
/// Retrieves a list of agent summaries for the agent index page
/// </summary>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <returns>A list of agent summaries sorted by performance</returns>
[HttpGet("GetAgentIndex")]
public async Task<ActionResult<AgentIndexViewModel>> GetAgentIndex(string timeFilter = "Total")
{
// Validate time filter
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
if (!validTimeFilters.Contains(timeFilter))
{
timeFilter = "Total"; // Default to Total if invalid
}
string cacheKey = $"AgentIndex_{timeFilter}";
// Check if the agent index is already cached
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
if (cachedIndex != null)
{
return Ok(cachedIndex);
}
// Get all agents and their strategies
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
// Create the agent index response
var agentIndex = new AgentIndexViewModel
{
TimeFilter = timeFilter
};
// Create summaries for each agent
foreach (var agent in agentsWithStrategies)
{
var user = agent.Key;
var strategies = agent.Value;
if (strategies.Count == 0)
{
continue; // Skip agents with no strategies
}
// Combine all positions from all strategies
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
// Calculate agent metrics
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
(int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
// Calculate trading volumes
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
// Calculate win rate
int averageWinRate = 0;
if (wins + losses > 0)
{
averageWinRate = (wins * 100) / (wins + losses);
}
// Add to agent summaries
var agentSummary = new AgentSummaryViewModel
{
AgentName = user.AgentName,
TotalPnL = totalPnL,
PnLLast24h = pnlLast24h,
TotalROI = totalROI,
ROILast24h = roiLast24h,
Wins = wins,
Losses = losses,
AverageWinRate = averageWinRate,
ActiveStrategiesCount = strategies.Count,
TotalVolume = totalVolume,
VolumeLast24h = volumeLast24h
};
agentIndex.AgentSummaries.Add(agentSummary);
}
// Sort agent summaries by total PnL (highest first)
agentIndex.AgentSummaries = agentIndex.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList();
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
return Ok(agentIndex);
}
/// <summary>
/// Retrieves a paginated list of agent summaries for the agent index page
/// </summary>
/// <param name="timeFilter">Time filter to apply (24H, 3D, 1W, 1M, 1Y, Total)</param>
/// <param name="page">Page number (defaults to 1)</param>
/// <param name="pageSize">Number of items per page (defaults to 10, max 100)</param>
/// <param name="sortBy">Field to sort by (TotalPnL, PnLLast24h, TotalROI, ROILast24h, Wins, Losses, AverageWinRate, ActiveStrategiesCount, TotalVolume, VolumeLast24h)</param>
/// <param name="sortBy">Field to sort by (TotalPnL, TotalROI, Wins, Losses, AgentName, CreatedAt, UpdatedAt)</param>
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
/// <param name="agentNames">Optional comma-separated list of agent names to filter by</param>
/// <returns>A paginated list of agent summaries sorted by the specified field</returns>
[HttpGet("GetAgentIndexPaginated")]
public async Task<ActionResult<PaginatedAgentIndexResponse>> GetAgentIndexPaginated(
string timeFilter = "Total",
int page = 1,
int pageSize = 10,
string sortBy = "TotalPnL",
SortableFields sortBy = SortableFields.TotalPnL,
string sortOrder = "desc",
string? agentNames = null)
{
// Validate time filter
var validTimeFilters = new[] { "24H", "3D", "1W", "1M", "1Y", "Total" };
if (!validTimeFilters.Contains(timeFilter))
{
timeFilter = "Total"; // Default to Total if invalid
}
// Validate pagination parameters
if (page < 1)
{
@@ -708,177 +589,59 @@ public class DataController : ControllerBase
return BadRequest("Sort order must be 'asc' or 'desc'");
}
// Validate sort by field
var validSortFields = new[]
{
"TotalPnL", "PnLLast24h", "TotalROI", "ROILast24h", "Wins", "Losses", "AverageWinRate",
"ActiveStrategiesCount", "TotalVolume", "VolumeLast24h"
};
if (!validSortFields.Contains(sortBy))
{
sortBy = "TotalPnL"; // Default to TotalPnL if invalid
}
// Create cache key that includes agent names filter
var agentNamesForCache = !string.IsNullOrWhiteSpace(agentNames)
? string.Join("_", agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(name => name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.OrderBy(name => name))
: "all";
string cacheKey = $"AgentIndex_{timeFilter}_{agentNamesForCache}";
// Check if the agent index is already cached
var cachedIndex = _cacheService.GetValue<AgentIndexViewModel>(cacheKey);
List<AgentSummaryViewModel> allAgentSummaries;
if (cachedIndex != null)
{
allAgentSummaries = cachedIndex.AgentSummaries.ToList();
}
else
{
// Get all agents and their strategies
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
allAgentSummaries = new List<AgentSummaryViewModel>();
// Create summaries for each agent
foreach (var agent in agentsWithStrategies)
{
var user = agent.Key;
var strategies = agent.Value;
if (strategies.Count == 0)
{
continue; // Skip agents with no strategies
}
// Combine all positions from all strategies
var allPositions = strategies.SelectMany<ITradingBot, Position>(s => s.Positions).ToList();
// Calculate agent metrics
decimal totalPnL = TradingBox.GetPnLInTimeRange(allPositions, timeFilter);
decimal pnlLast24h = TradingBox.GetPnLInTimeRange(allPositions, "24H");
decimal totalROI = TradingBox.GetROIInTimeRange(allPositions, timeFilter);
decimal roiLast24h = TradingBox.GetROIInTimeRange(allPositions, "24H");
(int wins, int losses) = TradingBox.GetWinLossCountInTimeRange(allPositions, timeFilter);
// Calculate trading volumes
decimal totalVolume = TradingBox.GetTotalVolumeTraded(allPositions);
decimal volumeLast24h = TradingBox.GetLast24HVolumeTraded(allPositions);
// Calculate win rate
int averageWinRate = 0;
if (wins + losses > 0)
{
averageWinRate = (wins * 100) / (wins + losses);
}
// Add to agent summaries
var agentSummary = new AgentSummaryViewModel
{
AgentName = user.AgentName,
TotalPnL = totalPnL,
PnLLast24h = pnlLast24h,
TotalROI = totalROI,
ROILast24h = roiLast24h,
Wins = wins,
Losses = losses,
AverageWinRate = averageWinRate,
ActiveStrategiesCount = strategies.Count,
TotalVolume = totalVolume,
VolumeLast24h = volumeLast24h
};
allAgentSummaries.Add(agentSummary);
}
// Cache the results for 5 minutes
var agentIndex = new AgentIndexViewModel
{
TimeFilter = timeFilter,
AgentSummaries = allAgentSummaries
};
_cacheService.SaveValue(cacheKey, agentIndex, TimeSpan.FromMinutes(5));
}
// Apply agent name filtering if specified
// Parse agent names filter
IEnumerable<string>? agentNamesList = null;
if (!string.IsNullOrWhiteSpace(agentNames))
{
var agentNameList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
agentNamesList = agentNames.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(name => name.Trim())
.Where(name => !string.IsNullOrWhiteSpace(name))
.ToList();
if (agentNameList.Any())
{
allAgentSummaries = allAgentSummaries
.Where(agent => agentNameList.Contains(agent.AgentName, StringComparer.OrdinalIgnoreCase))
.ToList();
}
}
// Apply sorting
var sortedSummaries = sortBy switch
// Get paginated results from database
var command = new GetPaginatedAgentSummariesCommand(page, pageSize, sortBy, sortOrder, agentNamesList);
var result = await _mediator.Send(command);
var agentSummaries = result.Results;
var totalCount = result.TotalCount;
// Map to view models
var agentSummaryViewModels = new List<AgentSummaryViewModel>();
foreach (var agentSummary in agentSummaries)
{
"TotalPnL" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
: allAgentSummaries.OrderBy(a => a.TotalPnL),
"PnLLast24h" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.PnLLast24h)
: allAgentSummaries.OrderBy(a => a.PnLLast24h),
"TotalROI" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.TotalROI)
: allAgentSummaries.OrderBy(a => a.TotalROI),
"ROILast24h" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.ROILast24h)
: allAgentSummaries.OrderBy(a => a.ROILast24h),
"Wins" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.Wins)
: allAgentSummaries.OrderBy(a => a.Wins),
"Losses" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.Losses)
: allAgentSummaries.OrderBy(a => a.Losses),
"AverageWinRate" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.AverageWinRate)
: allAgentSummaries.OrderBy(a => a.AverageWinRate),
"ActiveStrategiesCount" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.ActiveStrategiesCount)
: allAgentSummaries.OrderBy(a => a.ActiveStrategiesCount),
"TotalVolume" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.TotalVolume)
: allAgentSummaries.OrderBy(a => a.TotalVolume),
"VolumeLast24h" => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.VolumeLast24h)
: allAgentSummaries.OrderBy(a => a.VolumeLast24h),
_ => sortOrder == "desc"
? allAgentSummaries.OrderByDescending(a => a.TotalPnL)
: allAgentSummaries.OrderBy(a => a.TotalPnL)
};
// Calculate win rate
int averageWinRate = 0;
if (agentSummary.Wins + agentSummary.Losses > 0)
{
averageWinRate = (agentSummary.Wins * 100) / (agentSummary.Wins + agentSummary.Losses);
}
// Map to view model
var agentSummaryViewModel = new AgentSummaryViewModel
{
AgentName = agentSummary.AgentName,
TotalPnL = agentSummary.TotalPnL,
TotalROI = agentSummary.TotalROI,
Wins = agentSummary.Wins,
Losses = agentSummary.Losses,
ActiveStrategiesCount = agentSummary.ActiveStrategiesCount,
TotalVolume = agentSummary.TotalVolume,
};
agentSummaryViewModels.Add(agentSummaryViewModel);
}
var totalCount = allAgentSummaries.Count;
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
// Apply pagination
var paginatedSummaries = sortedSummaries
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
var response = new PaginatedAgentIndexResponse
{
AgentSummaries = paginatedSummaries,
AgentSummaries = agentSummaryViewModels,
TotalCount = totalCount,
CurrentPage = page,
PageSize = pageSize,
TotalPages = totalPages,
HasNextPage = page < totalPages,
HasPreviousPage = page > 1,
TimeFilter = timeFilter,
SortBy = sortBy,
SortOrder = sortOrder,
FilteredAgentNames = agentNames
@@ -970,7 +733,7 @@ public class DataController : ControllerBase
foreach (var indicatorRequest in scenarioRequest.Indicators)
{
var indicator = new Indicator(indicatorRequest.Name, indicatorRequest.Type)
var indicator = new IndicatorBase(indicatorRequest.Name, indicatorRequest.Type)
{
SignalType = indicatorRequest.SignalType,
MinimumHistory = indicatorRequest.MinimumHistory,

View File

@@ -197,23 +197,23 @@ public class ScenarioController : BaseController
};
}
private static IndicatorViewModel MapToIndicatorViewModel(Indicator indicator)
private static IndicatorViewModel MapToIndicatorViewModel(IndicatorBase indicatorBase)
{
return new IndicatorViewModel
{
Name = indicator.Name,
Type = indicator.Type,
SignalType = indicator.SignalType,
MinimumHistory = indicator.MinimumHistory,
Period = indicator.Period,
FastPeriods = indicator.FastPeriods,
SlowPeriods = indicator.SlowPeriods,
SignalPeriods = indicator.SignalPeriods,
Multiplier = indicator.Multiplier,
SmoothPeriods = indicator.SmoothPeriods,
StochPeriods = indicator.StochPeriods,
CyclePeriods = indicator.CyclePeriods,
UserName = indicator.User?.Name
Name = indicatorBase.Name,
Type = indicatorBase.Type,
SignalType = indicatorBase.SignalType,
MinimumHistory = indicatorBase.MinimumHistory,
Period = indicatorBase.Period,
FastPeriods = indicatorBase.FastPeriods,
SlowPeriods = indicatorBase.SlowPeriods,
SignalPeriods = indicatorBase.SignalPeriods,
Multiplier = indicatorBase.Multiplier,
SmoothPeriods = indicatorBase.SmoothPeriods,
StochPeriods = indicatorBase.StochPeriods,
CyclePeriods = indicatorBase.CyclePeriods,
UserName = indicatorBase.User?.Name
};
}
}

View File

@@ -85,7 +85,7 @@ public class TradingController : BaseController
/// <param name="identifier">The unique identifier of the position to close.</param>
/// <returns>The closed position.</returns>
[HttpPost("ClosePosition")]
public async Task<ActionResult<Position>> ClosePosition(string identifier)
public async Task<ActionResult<Position>> ClosePosition(Guid identifier)
{
var position = await _tradingService.GetPositionByIdentifierAsync(identifier);
var result = await _closeTradeCommandHandler.Handle(new ClosePositionCommand(position));

View File

@@ -26,7 +26,8 @@ public class UserController : BaseController
/// <param name="userService">Service for user-related operations.</param>
/// <param name="jwtUtils">Utility for JWT token operations.</param>
/// <param name="webhookService">Service for webhook operations.</param>
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils, IWebhookService webhookService)
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils,
IWebhookService webhookService)
: base(userService)
{
_config = config;
@@ -40,7 +41,7 @@ public class UserController : BaseController
/// <param name="login">The login request containing user credentials.</param>
/// <returns>A JWT token if authentication is successful; otherwise, an Unauthorized result.</returns>
[AllowAnonymous]
[HttpPost]
[HttpPost("create-token")]
public async Task<ActionResult<string>> CreateToken([FromBody] LoginRequest login)
{
var user = await _userService.Authenticate(login.Name, login.Address, login.Message, login.Signature);
@@ -52,16 +53,18 @@ public class UserController : BaseController
}
return Unauthorized();
}
}
/// <summary>
/// Gets the current user's information.
/// </summary>
/// <returns>The current user's information.</returns>
[Authorize]
[HttpGet]
public async Task<ActionResult<User>> GetCurrentUser()
{
var user = await base.GetUser();
user = await _userService.GetUserByName(user.Name);
return Ok(user);
}
@@ -70,6 +73,7 @@ public class UserController : BaseController
/// </summary>
/// <param name="agentName">The new agent name to set.</param>
/// <returns>The updated user with the new agent name.</returns>
[Authorize]
[HttpPut("agent-name")]
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
{
@@ -83,6 +87,7 @@ public class UserController : BaseController
/// </summary>
/// <param name="avatarUrl">The new avatar URL to set.</param>
/// <returns>The updated user with the new avatar URL.</returns>
[Authorize]
[HttpPut("avatar")]
public async Task<ActionResult<User>> UpdateAvatarUrl([FromBody] string avatarUrl)
{
@@ -96,35 +101,37 @@ public class UserController : BaseController
/// </summary>
/// <param name="telegramChannel">The new Telegram channel to set.</param>
/// <returns>The updated user with the new Telegram channel.</returns>
[Authorize]
[HttpPut("telegram-channel")]
public async Task<ActionResult<User>> UpdateTelegramChannel([FromBody] string telegramChannel)
{
var user = await GetUser();
var updatedUser = await _userService.UpdateTelegramChannel(user, telegramChannel);
// Send welcome message to the newly configured telegram channel
if (!string.IsNullOrEmpty(telegramChannel))
{
try
{
var welcomeMessage = $"🎉 **Trading Bot - Welcome!**\n\n" +
$"🎯 **Agent:** {user.Name}\n" +
$"📡 **Channel ID:** {telegramChannel}\n" +
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"🔔 **Notification Types:**\n" +
$"• 📈 Position Opens & Closes\n" +
$"• 🤖 Bot configuration changes\n\n" +
$"🚀 **Welcome aboard!** Your trading notifications are now live.";
$"🎯 **Agent:** {user.Name}\n" +
$"📡 **Channel ID:** {telegramChannel}\n" +
$"⏰ **Setup Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
$"🔔 **Notification Types:**\n" +
$"• 📈 Position Opens & Closes\n" +
$"• 🤖 Bot configuration changes\n\n" +
$"🚀 **Welcome aboard!** Your trading notifications are now live.";
await _webhookService.SendMessage(welcomeMessage, telegramChannel);
}
catch (Exception ex)
{
// Log the error but don't fail the update operation
Console.WriteLine($"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
Console.WriteLine(
$"Failed to send welcome message to telegram channel {telegramChannel}: {ex.Message}");
}
}
return Ok(updatedUser);
}
@@ -132,11 +139,12 @@ public class UserController : BaseController
/// Tests the Telegram channel configuration by sending a test message.
/// </summary>
/// <returns>A message indicating the test result.</returns>
[Authorize]
[HttpPost("telegram-channel/test")]
public async Task<ActionResult<string>> TestTelegramChannel()
{
var user = await GetUser();
if (string.IsNullOrEmpty(user.TelegramChannel))
{
return BadRequest("No Telegram channel configured for this user. Please set a Telegram channel first.");
@@ -144,7 +152,7 @@ public class UserController : BaseController
try
{
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
var testMessage = $"🚀 **Trading Bot - Channel Test**\n\n" +
$"🎯 **Agent:** {user.Name}\n" +
$"📡 **Channel ID:** {user.TelegramChannel}\n" +
$"⏰ **Test Time:** {DateTime.UtcNow:MMM dd, yyyy • HH:mm:ss} UTC\n\n" +
@@ -154,13 +162,13 @@ public class UserController : BaseController
$"🎉 **Ready to trade!** Your notifications are now active.";
await _webhookService.SendMessage(testMessage, user.TelegramChannel);
return Ok($"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
return Ok(
$"Test message sent successfully to Telegram channel {user.TelegramChannel}. Please check your Telegram to verify delivery.");
}
catch (Exception ex)
{
return StatusCode(500, $"Failed to send test message: {ex.Message}");
}
}
}
}