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

@@ -2,7 +2,6 @@
namespace Managing.Api.Authorization;
public class JwtMiddleware
{
private readonly RequestDelegate _next;
@@ -14,7 +13,21 @@ public class JwtMiddleware
public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils)
{
if (context.Request.Path.StartsWithSegments("/User/create-token") ||
context.Request.Path.StartsWithSegments("/swagger") ||
context.Request.Path.StartsWithSegments("/health"))
{
await _next(context);
return;
}
var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
if (string.IsNullOrEmpty(token))
{
throw new UnauthorizedAccessException("Authorization token is missing");
}
var userId = jwtUtils.ValidateJwtToken(token);
if (userId != null)
{

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}");
}
}
}
}

View File

@@ -0,0 +1,55 @@
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
/// <summary>
/// Request model for getting paginated bots with filtering and sorting
/// </summary>
public class GetBotsPaginatedRequest
{
/// <summary>
/// Page number (1-based). Default is 1.
/// </summary>
public int PageNumber { get; set; } = 1;
/// <summary>
/// Number of items per page. Default is 10, maximum is 100.
/// </summary>
public int PageSize { get; set; } = 10;
/// <summary>
/// Filter by bot status. If null, returns bots of all statuses.
/// </summary>
public BotStatus? Status { get; set; }
/// <summary>
/// Filter by user ID. If null, returns bots for all users.
/// </summary>
public int? UserId { get; set; }
/// <summary>
/// Filter by bot name (partial match, case-insensitive). If null, no name filtering is applied.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Filter by ticker (partial match, case-insensitive). If null, no ticker filtering is applied.
/// </summary>
public string? Ticker { get; set; }
/// <summary>
/// Filter by agent name (partial match, case-insensitive). If null, no agent name filtering is applied.
/// </summary>
public string? AgentName { get; set; }
/// <summary>
/// Sort field. Valid values: "Name", "Ticker", "Status", "CreateDate", "StartupTime", "Pnl", "WinRate", "AgentName".
/// Default is "CreateDate".
/// </summary>
public string SortBy { get; set; } = "CreateDate";
/// <summary>
/// Sort direction. Default is "Desc" (descending).
/// </summary>
public string SortDirection { get; set; } = "Desc";
}

View File

@@ -13,7 +13,7 @@ public class UpdateBotConfigRequest
/// The unique identifier of the bot to update
/// </summary>
[Required]
public string Identifier { get; set; }
public Guid Identifier { get; set; }
/// <summary>
/// The new trading bot configuration request

View File

@@ -15,21 +15,11 @@ namespace Managing.Api.Models.Responses
/// </summary>
public decimal TotalPnL { get; set; }
/// <summary>
/// Profit and loss in the last 24 hours in USD
/// </summary>
public decimal PnLLast24h { get; set; }
/// <summary>
/// Total return on investment as a percentage
/// </summary>
public decimal TotalROI { get; set; }
/// <summary>
/// Return on investment in the last 24 hours as a percentage
/// </summary>
public decimal ROILast24h { get; set; }
/// <summary>
/// Number of winning trades
/// </summary>
@@ -40,10 +30,6 @@ namespace Managing.Api.Models.Responses
/// </summary>
public int Losses { get; set; }
/// <summary>
/// Average win rate as a percentage
/// </summary>
public int AverageWinRate { get; set; }
/// <summary>
/// Number of active strategies for this agent
@@ -54,11 +40,6 @@ namespace Managing.Api.Models.Responses
/// Total volume traded by this agent in USD
/// </summary>
public decimal TotalVolume { get; set; }
/// <summary>
/// Volume traded in the last 24 hours in USD
/// </summary>
public decimal VolumeLast24h { get; set; }
}
/// <summary>

View File

@@ -12,10 +12,11 @@ public class CandlesWithIndicatorsResponse
/// <summary>
/// The list of candles.
/// </summary>
public List<Candle> Candles { get; set; } = new List<Candle>();
public HashSet<Candle> Candles { get; set; } = new HashSet<Candle>();
/// <summary>
/// The calculated indicators values.
/// </summary>
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } = new Dictionary<IndicatorType, IndicatorsResultBase>();
}
public Dictionary<IndicatorType, IndicatorsResultBase> IndicatorsValues { get; set; } =
new Dictionary<IndicatorType, IndicatorsResultBase>();
}

View File

@@ -1,3 +1,5 @@
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses;
/// <summary>
@@ -48,7 +50,7 @@ public class PaginatedAgentIndexResponse
/// <summary>
/// Field used for sorting
/// </summary>
public string SortBy { get; set; } = "TotalPnL";
public SortableFields SortBy { get; set; } = SortableFields.TotalPnL;
/// <summary>
/// Sort order (asc or desc)

View File

@@ -0,0 +1,43 @@
namespace Managing.Api.Models.Responses;
/// <summary>
/// Generic pagination response model
/// </summary>
/// <typeparam name="T">The type of items in the response</typeparam>
public class PaginatedResponse<T>
{
/// <summary>
/// The items for the current page
/// </summary>
public List<T> Items { get; set; } = new();
/// <summary>
/// Total number of items across all pages
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// Current page number (1-based)
/// </summary>
public int PageNumber { get; set; }
/// <summary>
/// Number of items per page
/// </summary>
public int PageSize { get; set; }
/// <summary>
/// Total number of pages
/// </summary>
public int TotalPages { get; set; }
/// <summary>
/// Whether there is a previous page
/// </summary>
public bool HasPreviousPage { get; set; }
/// <summary>
/// Whether there is a next page
/// </summary>
public bool HasNextPage { get; set; }
}

View File

@@ -1,7 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses
{
@@ -14,16 +15,16 @@ namespace Managing.Api.Models.Responses
public string Status { get; internal set; }
/// <summary>
/// List of signals generated by the bot
/// Dictionary of signals generated by the bot, keyed by signal identifier
/// </summary>
[Required]
public List<LightSignal> Signals { get; internal set; }
public Dictionary<string, LightSignal> Signals { get; internal set; }
/// <summary>
/// List of positions opened by the bot
/// Dictionary of positions opened by the bot, keyed by position identifier
/// </summary>
[Required]
public List<Position> Positions { get; internal set; }
public Dictionary<Guid, Position> Positions { get; internal set; }
/// <summary>
/// Candles used by the bot for analysis
@@ -55,12 +56,6 @@ namespace Managing.Api.Models.Responses
[Required]
public string AgentName { get; set; }
/// <summary>
/// The full trading bot configuration
/// </summary>
[Required]
public TradingBotConfig Config { get; internal set; }
/// <summary>
/// The time when the bot was created
/// </summary>
@@ -72,5 +67,13 @@ namespace Managing.Api.Models.Responses
/// </summary>
[Required]
public DateTime StartupTime { get; internal set; }
[Required] public string Name { get; set; }
/// <summary>
/// The ticker/symbol being traded by this bot
/// </summary>
[Required]
public Ticker Ticker { get; set; }
}
}

View File

@@ -63,16 +63,12 @@ namespace Managing.Api.Models.Responses
public int Losses { get; set; }
/// <summary>
/// List of all positions executed by this strategy
/// Dictionary of all positions executed by this strategy, keyed by position identifier
/// </summary>
public List<Position> Positions { get; set; } = new List<Position>();
public Dictionary<Guid, Position> Positions { get; set; } = new Dictionary<Guid, Position>();
public string Identifier { get; set; }
/// <summary>
/// Name of the scenario used by this strategy
/// </summary>
public string ScenarioName { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; } = new Dictionary<DateTime, decimal>();
}
}

View File

@@ -5,7 +5,6 @@ using Managing.Api.Authorization;
using Managing.Api.Filters;
using Managing.Api.HealthChecks;
using Managing.Application.Hubs;
using Managing.Application.Workers;
using Managing.Bootstrap;
using Managing.Common;
using Managing.Core.Middleawares;
@@ -170,7 +169,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJw
ValidateIssuerSigningKey = true
};
});
builder.Services.AddAuthorization();
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", builder =>
{
builder
@@ -233,12 +232,6 @@ builder.Services.AddSwaggerGen(options =>
});
builder.WebHost.SetupDiscordBot();
if (builder.Configuration.GetValue<bool>("EnableBotManager", false))
{
builder.Services.AddHostedService<BotManagerWorker>();
}
// Workers are now registered in ApiBootstrap.cs
// App
var app = builder.Build();
@@ -288,5 +281,4 @@ app.UseEndpoints(endpoints =>
});
app.Run();

View File

@@ -5,7 +5,8 @@
"Token": "Fw2FPL2OwTzDHzSbR2Sd5xs0EKQYy00Q-hYKYAhr9cC1_q5YySONpxuf_Ck0PTjyUiF13xXmi__bu_pXH-H9zA=="
},
"PostgreSql": {
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres"
"ConnectionString": "Host=localhost;Port=5432;Database=managing;Username=postgres;Password=postgres",
"Orleans": "Host=localhost;Port=5432;Database=orleans;Username=postgres;Password=postgres"
},
"Privy": {
"AppId": "cm6f47n1l003jx7mjwaembhup",

View File

@@ -1,6 +1,7 @@
{
"PostgreSql": {
"ConnectionString": "Host=apps.prod.live;Port=5432;Database=managing;Username=postgres;Password=postgres"
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"Url": "https://influx-db.apps.managing.live",

View File

@@ -1,6 +1,7 @@
{
"PostgreSql": {
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37"
"ConnectionString": "Host=managing-postgre.apps.managing.live;Port=5432;Database=managing;Username=postgres;Password=29032b13a5bc4d37",
"Orleans": "Host=managing-postgre.apps.managing.live;Port=5432;Database=orleans;Username=postgres;Password=29032b13a5bc4d37"
},
"InfluxDb": {
"Url": "http://srv-captain--influx-db:8086/",