Add stats for kaigen

This commit is contained in:
2025-04-24 22:40:10 +07:00
parent 86692b60fa
commit 1d14d31af2
13 changed files with 483 additions and 8 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -4,7 +4,9 @@ using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
using Managing.Application.Workers.Abstractions;
using Managing.Api.Models.Responses;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using MediatR;
using Microsoft.AspNetCore.Authorization;
@@ -171,25 +173,25 @@ public class DataController : ControllerBase
public async Task<ActionResult<TopStrategiesViewModel>> GetTopStrategies()
{
const string cacheKey = "TopStrategies";
// Check if the top strategies are already cached
var cachedStrategies = _cacheService.GetValue<TopStrategiesViewModel>(cacheKey);
if (cachedStrategies != null)
{
return Ok(cachedStrategies);
}
// Get active bots
var activeBots = await _mediator.Send(new GetActiveBotsCommand());
// Calculate PnL for each bot once and store in a list of tuples
var botsWithPnL = activeBots
.Select(bot => new { Bot = bot, PnL = bot.GetProfitAndLoss() })
.OrderByDescending(item => item.PnL)
.Take(3)
.ToList();
// Map to view model
var topStrategies = new TopStrategiesViewModel
{
@@ -201,10 +203,130 @@ public class DataController : ControllerBase
})
.ToList()
};
// Cache the result for 10 minutes
_cacheService.SaveValue(cacheKey, topStrategies, TimeSpan.FromMinutes(10));
return Ok(topStrategies);
}
/// <summary>
/// Retrieves list of the active strategies for a user with detailed information
/// </summary>
/// <param name="agentName">The agentName to retrieve strategies for</param>
/// <returns>A list of detailed strategy information</returns>
[HttpGet("GetUserStrategies")]
public async Task<ActionResult<List<UserStrategyDetailsViewModel>>> GetUserStrategies(string agentName)
{
string cacheKey = $"UserStrategies_{agentName}";
// Check if the user strategy details are already cached
var cachedDetails = _cacheService.GetValue<List<UserStrategyDetailsViewModel>>(cacheKey);
if (cachedDetails != null && cachedDetails.Count > 0)
{
return Ok(cachedDetails);
}
// Get all strategies for the specified user
var userStrategies = await _mediator.Send(new GetUserStrategiesCommand(agentName));
// Convert to detailed view model with additional information
var result = userStrategies.Select(strategy => MapStrategyToViewModel(strategy)).ToList();
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
return Ok(result);
}
/// <summary>
/// Retrieves a specific strategy for a user by strategy name
/// </summary>
/// <param name="agentName">The agent/user name to retrieve the strategy for</param>
/// <param name="strategyName">The name of the strategy to retrieve</param>
/// <returns>Detailed information about the requested strategy</returns>
[HttpGet("GetUserStrategy")]
public async Task<ActionResult<UserStrategyDetailsViewModel>> GetUserStrategy(string agentName, string strategyName)
{
string cacheKey = $"UserStrategy_{agentName}_{strategyName}";
// Check if the user strategy details are already cached
var cachedDetails = _cacheService.GetValue<UserStrategyDetailsViewModel>(cacheKey);
if (cachedDetails != null)
{
return Ok(cachedDetails);
}
// Get the specific strategy for the user
var strategy = await _mediator.Send(new GetUserStrategyCommand(agentName, strategyName));
if (strategy == null)
{
return NotFound($"Strategy '{strategyName}' not found for user '{agentName}'");
}
// Map the strategy to a view model using the shared method
var result = MapStrategyToViewModel(strategy);
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, result, TimeSpan.FromMinutes(5));
return Ok(result);
}
/// <summary>
/// Maps a trading bot to a strategy view model with detailed statistics
/// </summary>
/// <param name="strategy">The trading bot to map</param>
/// <returns>A view model with detailed strategy information</returns>
private UserStrategyDetailsViewModel MapStrategyToViewModel(ITradingBot 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();
// 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);
// Calculate win/loss statistics
(int wins, int losses) = TradingBox.GetWinLossCount(strategy.Positions);
// Calculate ROI for last 24h
decimal roiLast24h = TradingBox.GetLast24HROI(strategy.Positions);
return new UserStrategyDetailsViewModel
{
Name = strategy.Name,
StrategyName = strategy.ScenarioName,
State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
PnL = pnl,
ROIPercentage = roi,
ROILast24H = roiLast24h,
Runtime = startupTime,
WinRate = strategy.GetWinRate(),
TotalVolumeTraded = totalVolume,
VolumeLast24H = volumeLast24h,
Wins = wins,
Losses = losses,
Positions = strategy.Positions.OrderByDescending(p => p.Date).ToList() // Include sorted positions with most recent first
};
}
}

View File

@@ -0,0 +1,76 @@
using Managing.Domain.Trades;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses
{
/// <summary>
/// Detailed information about a user's deployed strategy
/// </summary>
public class UserStrategyDetailsViewModel
{
/// <summary>
/// Name of the deployed strategy
/// </summary>
public string Name { get; set; }
/// <summary>
/// Strategy identifier
/// </summary>
public string StrategyName { get; set; }
/// <summary>
/// Current state of the strategy (RUNNING, STOPPED, UNUSED)
/// </summary>
public string State { get; set; }
/// <summary>
/// Total profit or loss generated by the strategy in USD
/// </summary>
public decimal PnL { get; set; }
/// <summary>
/// Return on investment percentage
/// </summary>
public decimal ROIPercentage { get; set; }
/// <summary>
/// Return on investment percentage in the last 24 hours
/// </summary>
public decimal ROILast24H { get; set; }
/// <summary>
/// Date and time when the strategy was started
/// </summary>
public DateTime Runtime { get; set; }
/// <summary>
/// Average percentage of successful trades
/// </summary>
public int WinRate { get; set; }
/// <summary>
/// Total trading volume for all trades
/// </summary>
public decimal TotalVolumeTraded { get; set; }
/// <summary>
/// Trading volume in the last 24 hours
/// </summary>
public decimal VolumeLast24H { get; set; }
/// <summary>
/// Number of winning trades
/// </summary>
public int Wins { get; set; }
/// <summary>
/// Number of losing trades
/// </summary>
public int Losses { get; set; }
/// <summary>
/// List of all positions executed by this strategy
/// </summary>
public List<Position> Positions { get; set; } = new List<Position>();
}
}

View File

@@ -6,6 +6,7 @@ using Managing.Domain.MoneyManagements;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Trades;
using Managing.Domain.Users;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
@@ -26,6 +27,7 @@ namespace Managing.Application.Abstractions
BotType BotType { get; set; }
Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
User User { get; set; }
Task Run();
Task ToggleIsForWatchOnly();

View File

@@ -48,6 +48,7 @@ public class TradingBot : Bot, ITradingBot
public Scenario Scenario { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
public DateTime StartupTime { get; set; }
public TradingBot(
string accountName,
@@ -139,6 +140,8 @@ public class TradingBot : Bot, ITradingBot
else
{
Account = account;
// Set the User property from the account
User = account.User;
}
}
@@ -806,7 +809,8 @@ public class TradingBot : Bot, ITradingBot
AccountName = AccountName,
IsForWatchingOnly = IsForWatchingOnly,
WalletBalances = WalletBalances,
MoneyManagement = MoneyManagement
MoneyManagement = MoneyManagement,
StartupTime = DateTime.Now
};
BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data));
}
@@ -823,6 +827,12 @@ public class TradingBot : Bot, ITradingBot
ScenarioName = data.ScenarioName;
AccountName = data.AccountName;
IsForWatchingOnly = data.IsForWatchingOnly;
// Restore the startup time if it was previously saved
if (data.StartupTime != DateTime.MinValue)
{
StartupTime = data.StartupTime;
}
}
/// <summary>
@@ -883,4 +893,5 @@ public class TradingBotBackup
public bool IsForWatchingOnly { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public MoneyManagement MoneyManagement { get; set; }
public DateTime StartupTime { get; set; }
}

View File

@@ -0,0 +1,18 @@
using Managing.Application.Abstractions;
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
/// <summary>
/// Command to retrieve all strategies owned by a specific user
/// </summary>
public class GetUserStrategiesCommand : IRequest<List<ITradingBot>>
{
public string UserName { get; }
public GetUserStrategiesCommand(string userName)
{
UserName = userName;
}
}
}

View File

@@ -0,0 +1,27 @@
using Managing.Application.Abstractions;
using MediatR;
namespace Managing.Application.ManageBot.Commands
{
/// <summary>
/// Command to retrieve a specific strategy owned by a user
/// </summary>
public class GetUserStrategyCommand : IRequest<ITradingBot>
{
/// <summary>
/// The username of the agent/user that owns the strategy
/// </summary>
public string AgentName { get; }
/// <summary>
/// The name of the strategy/bot to retrieve
/// </summary>
public string StrategyName { get; }
public GetUserStrategyCommand(string agentName, string strategyName)
{
AgentName = agentName;
StrategyName = strategyName;
}
}
}

View File

@@ -0,0 +1,26 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
namespace Managing.Application.ManageBot
{
public class GetUserStrategiesCommandHandler : IRequestHandler<GetUserStrategiesCommand, List<ITradingBot>>
{
private readonly IBotService _botService;
public GetUserStrategiesCommandHandler(IBotService botService)
{
_botService = botService;
}
public Task<List<ITradingBot>> Handle(GetUserStrategiesCommand request, CancellationToken cancellationToken)
{
var allActiveBots = _botService.GetActiveBots();
var userBots = allActiveBots
.Where(bot => bot.User != null && bot.User.Name == request.UserName)
.ToList();
return Task.FromResult(userBots);
}
}
}

View File

@@ -0,0 +1,33 @@
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands;
using MediatR;
namespace Managing.Application.ManageBot
{
/// <summary>
/// Handler for retrieving a specific strategy owned by a user
/// </summary>
public class GetUserStrategyCommandHandler : IRequestHandler<GetUserStrategyCommand, ITradingBot>
{
private readonly IBotService _botService;
public GetUserStrategyCommandHandler(IBotService botService)
{
_botService = botService;
}
public Task<ITradingBot> Handle(GetUserStrategyCommand request, CancellationToken cancellationToken)
{
var allActiveBots = _botService.GetActiveBots();
// Find the specific strategy that matches both user and strategy name
var strategy = allActiveBots
.FirstOrDefault(bot =>
bot.User != null &&
bot.User.Name == request.AgentName &&
bot.Name == request.StrategyName);
return Task.FromResult(strategy);
}
}
}

View File

@@ -16,6 +16,10 @@ namespace Managing.Domain.Bots
public int Interval { get; set; }
public BotStatus Status { get; set; }
public User User { get; set; }
/// <summary>
/// The time when the bot was started
/// </summary>
public DateTime StartupTime { get; private set; }
private CancellationTokenSource CancellationToken { get; set; }
public Bot(string name)
@@ -26,11 +30,13 @@ namespace Managing.Domain.Bots
CancellationToken = new CancellationTokenSource();
ExecutionCount = 0;
Interval = 3000;
StartupTime = DateTime.MinValue; // Initialize with minimum value to indicate it hasn't been started yet
}
public virtual void Start()
{
Status = BotStatus.Up;
StartupTime = DateTime.UtcNow; // Record the startup time when the bot is started
}
public async Task InitWorker(Func<Task> action)
@@ -76,6 +82,7 @@ namespace Managing.Domain.Bots
public void Restart()
{
Status = BotStatus.Up;
StartupTime = DateTime.UtcNow; // Update the startup time when the bot is restarted
}
public string GetStatus()
@@ -87,6 +94,18 @@ namespace Managing.Domain.Bots
{
return Name;
}
/// <summary>
/// Gets the total runtime of the bot since it was started
/// </summary>
/// <returns>TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running</returns>
public TimeSpan GetRuntime()
{
if (Status != BotStatus.Up || StartupTime == DateTime.MinValue)
return TimeSpan.Zero;
return DateTime.UtcNow - StartupTime;
}
public abstract void SaveBackup();
public abstract void LoadBackup(BotBackup backup);

View File

@@ -8,6 +8,11 @@
void Restart();
string GetStatus();
string GetName();
/// <summary>
/// Gets the total runtime of the bot since it was started
/// </summary>
/// <returns>TimeSpan representing the runtime, or TimeSpan.Zero if the bot is not running</returns>
TimeSpan GetRuntime();
void SaveBackup();
void LoadBackup(BotBackup backup);
}

View File

@@ -187,4 +187,140 @@ public static class TradingBox
pnl.Realized = pnl.Realized * leverage;
return pnl;
}
/// <summary>
/// Calculates the total volume traded across all positions
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <returns>The total volume traded in decimal</returns>
public static decimal GetTotalVolumeTraded(List<Position> positions)
{
decimal totalVolume = 0;
foreach (var position in positions)
{
// Add entry volume
totalVolume += position.Open.Quantity * position.Open.Price;
// Add exit volumes from stop loss or take profits if they were executed
if (position.StopLoss.Status == TradeStatus.Filled)
{
totalVolume += position.StopLoss.Quantity * position.StopLoss.Price;
}
if (position.TakeProfit1.Status == TradeStatus.Filled)
{
totalVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price;
}
if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled)
{
totalVolume += position.TakeProfit2.Quantity * position.TakeProfit2.Price;
}
}
return totalVolume;
}
/// <summary>
/// Calculates the volume traded in the last 24 hours
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <returns>The volume traded in the last 24 hours in decimal</returns>
public static decimal GetLast24HVolumeTraded(List<Position> positions)
{
decimal last24hVolume = 0;
DateTime cutoff = DateTime.UtcNow.AddHours(-24);
foreach (var position in positions)
{
// Check if any part of this position was traded in the last 24 hours
// Add entry volume if it was within the last 24 hours
if (position.Open.Date >= cutoff)
{
last24hVolume += position.Open.Quantity * position.Open.Price;
}
// Add exit volumes if they were executed within the last 24 hours
if (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff)
{
last24hVolume += position.StopLoss.Quantity * position.StopLoss.Price;
}
if (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff)
{
last24hVolume += position.TakeProfit1.Quantity * position.TakeProfit1.Price;
}
if (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled &&
position.TakeProfit2.Date >= cutoff)
{
last24hVolume += position.TakeProfit2.Quantity * position.TakeProfit2.Price;
}
}
return last24hVolume;
}
/// <summary>
/// Gets the win/loss counts from positions
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <returns>A tuple containing (wins, losses)</returns>
public static (int Wins, int Losses) GetWinLossCount(List<Position> positions)
{
int wins = 0;
int losses = 0;
foreach (var position in positions)
{
// Only count finished positions
if (position.IsFinished())
{
if (position.ProfitAndLoss != null && position.ProfitAndLoss.Realized > 0)
{
wins++;
}
else
{
losses++;
}
}
}
return (wins, losses);
}
/// <summary>
/// Calculates the ROI for the last 24 hours
/// </summary>
/// <param name="positions">List of positions to analyze</param>
/// <returns>The ROI for the last 24 hours as a percentage</returns>
public static decimal GetLast24HROI(List<Position> positions)
{
decimal profitLast24h = 0;
decimal investmentLast24h = 0;
DateTime cutoff = DateTime.UtcNow.AddHours(-24);
foreach (var position in positions)
{
// Only count positions that were opened or closed within the last 24 hours
if (position.IsFinished() &&
(position.Open.Date >= cutoff ||
(position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) ||
(position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) ||
(position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date >= cutoff)))
{
profitLast24h += position.ProfitAndLoss != null ? position.ProfitAndLoss.Realized : 0;
investmentLast24h += position.Open.Quantity * position.Open.Price;
}
}
// Avoid division by zero
if (investmentLast24h == 0)
return 0;
return (profitLast24h / investmentLast24h) * 100;
}
}

Binary file not shown.