Fixes for bots running (#22)

* Fixes for bots running

* Up botmanager

* Add cooldown

* Refact can open position

* Add cooldown Period and MaxLossStreak

* Add agentName

* Add env variable for botManager

* Always enable Botmanager

* Fix bot handle

* Fix get positions

* Add Ticker url

* Dont start stopped bot

* fix
This commit is contained in:
Oda
2025-05-09 17:40:31 +02:00
committed by GitHub
parent a8eb0aaf02
commit 7c38c27b4a
54 changed files with 5164 additions and 641 deletions

View File

@@ -108,6 +108,8 @@ public class BacktestController : BaseController
/// <param name="moneyManagementName">The name of the money management strategy to use.</param>
/// <param name="moneyManagement">The money management strategy details, if not using a named strategy.</param>
/// <param name="save">Whether to save the backtest results.</param>
/// <param name="cooldownPeriod">The cooldown period for the backtest.</param>
/// <param name="maxLossStreak">The maximum loss streak for the backtest.</param>
/// <returns>The result of the backtest.</returns>
[HttpPost]
[Route("Run")]
@@ -122,7 +124,9 @@ public class BacktestController : BaseController
DateTime startDate,
DateTime endDate,
MoneyManagement? moneyManagement = null,
bool save = false)
bool save = false,
decimal cooldownPeriod = 1,
int maxLossStreak = 0)
{
if (string.IsNullOrEmpty(accountName))
{
@@ -174,7 +178,9 @@ public class BacktestController : BaseController
endDate,
user,
watchOnly,
save);
save,
cooldownPeriod: cooldownPeriod,
maxLossStreak: maxLossStreak);
break;
case BotType.FlippingBot:
backtestResult = await _backtester.RunFlippingBotBacktest(
@@ -188,7 +194,9 @@ public class BacktestController : BaseController
endDate,
user,
watchOnly,
save);
save,
cooldownPeriod: cooldownPeriod,
maxLossStreak: maxLossStreak);
break;
}

View File

@@ -1,7 +1,7 @@
using Managing.Application.Abstractions.Services;
using System.Security.Claims;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace Managing.Api.Controllers;
@@ -10,7 +10,7 @@ namespace Managing.Api.Controllers;
[Produces("application/json")]
public abstract class BaseController : ControllerBase
{
private readonly IUserService _userService;
public readonly IUserService _userService;
public BaseController(IUserService userService)
{

View File

@@ -1,6 +1,6 @@
using Managing.Api.Models.Requests;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands;
using Managing.Common;
@@ -56,10 +56,10 @@ public class BotController : BaseController
/// <summary>
/// Checks if the current authenticated user owns the account associated with the specified bot or account name
/// </summary>
/// <param name="botName">The name of the bot to check</param>
/// <param name="identifier">The identifier of the bot to check</param>
/// <param name="accountName">Optional account name to check when creating a new bot</param>
/// <returns>True if the user owns the account, False otherwise</returns>
private async Task<bool> UserOwnsBotAccount(string botName, string accountName = null)
private async Task<bool> UserOwnsBotAccount(string identifier, string accountName = null)
{
try
{
@@ -78,12 +78,12 @@ public class BotController : BaseController
// For existing bots, check if the user owns the bot's account
var activeBots = _botService.GetActiveBots();
var bot = activeBots.FirstOrDefault(b => b.Name == botName);
var bot = activeBots.FirstOrDefault(b => b.Identifier == identifier);
if (bot == null)
return true; // Bot doesn't exist yet, so no ownership conflict
var botAccountService = HttpContext.RequestServices.GetRequiredService<IAccountService>();
var botAccount = await botAccountService.GetAccount(bot.AccountName, true, false);
var botAccount = await botAccountService.GetAccount(bot.Config.AccountName, true, false);
// Compare the user names
return botAccount != null && botAccount.User != null && botAccount.User.Name == user.Name;
}
@@ -106,7 +106,7 @@ public class BotController : BaseController
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(request.BotName, request.AccountName))
if (!await UserOwnsBotAccount(request.Identifier, request.AccountName))
{
return Forbid("You don't have permission to start this bot");
}
@@ -131,9 +131,19 @@ public class BotController : BaseController
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
var result = await _mediator.Send(new StartBotCommand(request.BotType, request.BotName, request.Ticker,
request.Scenario, request.Timeframe, request.AccountName, request.MoneyManagementName, user,
request.IsForWatchOnly, request.InitialTradingBalance));
var config = new TradingBotConfig
{
AccountName = request.AccountName,
MoneyManagement = moneyManagement,
Ticker = request.Ticker,
ScenarioName = request.Scenario,
Timeframe = request.Timeframe,
IsForWatchingOnly = request.IsForWatchOnly,
BotTradingBalance = request.InitialTradingBalance,
BotType = request.BotType
};
var result = await _mediator.Send(new StartBotCommand(config, request.Identifier, user));
await NotifyBotSubscriberAsync();
return Ok(result);
@@ -149,22 +159,22 @@ public class BotController : BaseController
/// Stops a bot specified by type and name.
/// </summary>
/// <param name="botType">The type of the bot to stop.</param>
/// <param name="botName">The name of the bot to stop.</param>
/// <param name="identifier">The identifier of the bot to stop.</param>
/// <returns>A string indicating the result of the stop operation.</returns>
[HttpGet]
[Route("Stop")]
public async Task<ActionResult<string>> Stop(BotType botType, string botName)
public async Task<ActionResult<string>> Stop(BotType botType, string identifier)
{
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(botName))
if (!await UserOwnsBotAccount(identifier))
{
return Forbid("You don't have permission to stop this bot");
}
var result = await _mediator.Send(new StopBotCommand(botType, botName));
_logger.LogInformation($"{botType} type called {botName} is now {result}");
var result = await _mediator.Send(new StopBotCommand(botType, identifier));
_logger.LogInformation($"{botType} type with identifier {identifier} is now {result}");
await NotifyBotSubscriberAsync();
@@ -180,21 +190,21 @@ public class BotController : BaseController
/// <summary>
/// Deletes a bot specified by name.
/// </summary>
/// <param name="botName">The name of the bot to delete.</param>
/// <param name="identifier">The identifier of the bot to delete.</param>
/// <returns>A boolean indicating the result of the delete operation.</returns>
[HttpDelete]
[Route("Delete")]
public async Task<ActionResult<bool>> Delete(string botName)
public async Task<ActionResult<bool>> Delete(string identifier)
{
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(botName))
if (!await UserOwnsBotAccount(identifier))
{
return Forbid("You don't have permission to delete this bot");
}
var result = await _botService.DeleteBot(botName);
var result = await _botService.DeleteBot(identifier);
await NotifyBotSubscriberAsync();
return Ok(result);
}
@@ -235,9 +245,9 @@ public class BotController : BaseController
foreach (var bot in userBots)
{
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Name));
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier));
await _hubContext.Clients.All.SendAsync("SendNotification",
$"Bot {bot.Name} paused by {user.Name}.", "Info");
$"Bot {bot.Identifier} paused by {user.Name}.", "Info");
}
await NotifyBotSubscriberAsync();
@@ -254,22 +264,22 @@ public class BotController : BaseController
/// Restarts a bot specified by type and name.
/// </summary>
/// <param name="botType">The type of the bot to restart.</param>
/// <param name="botName">The name of the bot to restart.</param>
/// <param name="identifier">The identifier of the bot to restart.</param>
/// <returns>A string indicating the result of the restart operation.</returns>
[HttpGet]
[Route("Restart")]
public async Task<ActionResult<string>> Restart(BotType botType, string botName)
public async Task<ActionResult<string>> Restart(BotType botType, string identifier)
{
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(botName))
if (!await UserOwnsBotAccount(identifier))
{
return Forbid("You don't have permission to restart this bot");
}
var result = await _mediator.Send(new RestartBotCommand(botType, botName));
_logger.LogInformation($"{botType} type called {botName} is now {result}");
var result = await _mediator.Send(new RestartBotCommand(botType, identifier));
_logger.LogInformation($"{botType} type with identifier {identifier} is now {result}");
await NotifyBotSubscriberAsync();
@@ -314,15 +324,15 @@ public class BotController : BaseController
{
// We can't directly restart a bot with just BotType and Name
// Instead, stop the bot and then retrieve the backup to start it again
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Name));
await _mediator.Send(new StopBotCommand(bot.BotType, bot.Identifier));
// Get the saved bot backup
var backup = _botService.GetBotBackup(bot.Name);
var backup = _botService.GetBotBackup(bot.Identifier);
if (backup != null)
{
_botService.StartBotFromBackup(backup);
await _hubContext.Clients.All.SendAsync("SendNotification",
$"Bot {bot.Name} restarted by {user.Name}.", "Info");
$"Bot {bot.Identifier} restarted by {user.Name}.", "Info");
}
}
@@ -339,22 +349,22 @@ public class BotController : BaseController
/// <summary>
/// Toggles the watching status of a bot specified by name.
/// </summary>
/// <param name="botName">The name of the bot to toggle watching status.</param>
/// <param name="identifier">The identifier of the bot to toggle watching status.</param>
/// <returns>A string indicating the new watching status of the bot.</returns>
[HttpGet]
[Route("ToggleIsForWatching")]
public async Task<ActionResult<string>> ToggleIsForWatching(string botName)
public async Task<ActionResult<string>> ToggleIsForWatching(string identifier)
{
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(botName))
if (!await UserOwnsBotAccount(identifier))
{
return Forbid("You don't have permission to modify this bot");
}
var result = await _mediator.Send(new ToggleIsForWatchingCommand(botName));
_logger.LogInformation($"{botName} bot is now {result}");
var result = await _mediator.Send(new ToggleIsForWatchingCommand(identifier));
_logger.LogInformation($"Bot with identifier {identifier} is now {result}");
await NotifyBotSubscriberAsync();
@@ -397,13 +407,15 @@ public class BotController : BaseController
Candles = item.Candles.DistinctBy(c => c.Date).ToList(),
WinRate = item.GetWinRate(),
ProfitAndLoss = item.GetProfitAndLoss(),
Timeframe = item.Timeframe,
Ticker = item.Ticker,
Scenario = item.ScenarioName,
IsForWatchingOnly = item.IsForWatchingOnly,
BotType = item.BotType,
AccountName = item.AccountName,
MoneyManagement = item.MoneyManagement
Timeframe = item.Config.Timeframe,
Ticker = item.Config.Ticker,
Scenario = item.Config.ScenarioName,
IsForWatchingOnly = item.Config.IsForWatchingOnly,
BotType = item.Config.BotType,
AccountName = item.Config.AccountName,
MoneyManagement = item.Config.MoneyManagement,
Identifier = item.Identifier,
AgentName = item.User.AgentName
});
}
@@ -431,22 +443,22 @@ public class BotController : BaseController
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(request.BotName))
if (!await UserOwnsBotAccount(request.Identifier))
{
return Forbid("You don't have permission to open positions for this bot");
}
var activeBots = _botService.GetActiveBots();
var bot = activeBots.FirstOrDefault(b => b.Name == request.BotName) as ApplicationTradingBot;
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier) as ApplicationTradingBot;
if (bot == null)
{
return NotFound($"Bot {request.BotName} not found or is not a trading bot");
return NotFound($"Bot with identifier {request.Identifier} not found or is not a trading bot");
}
if (bot.GetStatus() != BotStatus.Up.ToString())
{
return BadRequest($"Bot {request.BotName} is not running");
return BadRequest($"Bot with identifier {request.Identifier} is not running");
}
var position = await bot.OpenPositionManually(
@@ -475,29 +487,30 @@ public class BotController : BaseController
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(request.BotName))
if (!await UserOwnsBotAccount(request.Identifier))
{
return Forbid("You don't have permission to close positions for this bot");
}
var activeBots = _botService.GetActiveBots();
var bot = activeBots.FirstOrDefault(b => b.Name == request.BotName) as ApplicationTradingBot;
var bot = activeBots.FirstOrDefault(b => b.Identifier == request.Identifier) as ApplicationTradingBot;
if (bot == null)
{
return NotFound($"Bot {request.BotName} not found or is not a trading bot");
return NotFound($"Bot with identifier {request.Identifier} not found or is not a trading bot");
}
if (bot.GetStatus() != BotStatus.Up.ToString())
{
return BadRequest($"Bot {request.BotName} is not running");
return BadRequest($"Bot with identifier {request.Identifier} is not running");
}
// Find the position to close
var position = bot.Positions.FirstOrDefault(p => p.Identifier == request.PositionId);
if (position == null)
{
return NotFound($"Position with ID {request.PositionId} not found for bot {request.BotName}");
return NotFound(
$"Position with ID {request.PositionId} not found for bot with identifier {request.Identifier}");
}
// Find the signal associated with this position
@@ -528,18 +541,85 @@ public class BotController : BaseController
}
}
/// <summary>
/// Request model for opening a position manually
/// </summary>
public class OpenPositionManuallyRequest
{
/// <summary>
/// The identifier of the bot
/// </summary>
public string Identifier { get; set; }
/// <summary>
/// The direction of the position
/// </summary>
public TradeDirection Direction { get; set; }
}
/// <summary>
/// Request model for closing a position
/// </summary>
public class ClosePositionRequest
{
/// <summary>
/// The name of the bot
/// The identifier of the bot
/// </summary>
public string BotName { get; set; }
public string Identifier { get; set; }
/// <summary>
/// The ID of the position to close
/// </summary>
public string PositionId { get; set; }
}
/// <summary>
/// Request model for starting a bot
/// </summary>
public class StartBotRequest
{
/// <summary>
/// The type of bot to start
/// </summary>
public BotType BotType { get; set; }
/// <summary>
/// The identifier of the bot
/// </summary>
public string Identifier { get; set; }
/// <summary>
/// The ticker to trade
/// </summary>
public Ticker Ticker { get; set; }
/// <summary>
/// The scenario to use
/// </summary>
public string Scenario { get; set; }
/// <summary>
/// The timeframe to use
/// </summary>
public Timeframe Timeframe { get; set; }
/// <summary>
/// The account name to use
/// </summary>
public string AccountName { get; set; }
/// <summary>
/// The money management name to use
/// </summary>
public string MoneyManagementName { get; set; }
/// <summary>
/// Whether the bot is for watching only
/// </summary>
public bool IsForWatchOnly { get; set; }
/// <summary>
/// The initial trading balance
/// </summary>
public decimal InitialTradingBalance { get; set; }
}

View File

@@ -1,20 +1,18 @@
using Managing.Application.Abstractions;
using Managing.Api.Models.Responses;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
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 Managing.Domain.Trades;
using Managing.Domain.Users;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System.Linq;
using static Managing.Common.Enums;
namespace Managing.Api.Controllers;
@@ -65,20 +63,114 @@ public class DataController : ControllerBase
/// <param name="timeframe">The timeframe for which to retrieve tickers.</param>
/// <returns>An array of tickers.</returns>
[HttpPost("GetTickers")]
public async Task<ActionResult<Ticker[]>> GetTickers(Timeframe timeframe)
public async Task<ActionResult<List<TickerInfos>>> GetTickers(Timeframe timeframe)
{
var cacheKey = string.Concat(timeframe.ToString());
var tickers = _cacheService.GetValue<List<Ticker>>(cacheKey);
var tickers = _cacheService.GetValue<List<TickerInfos>>(cacheKey);
if (tickers == null || tickers.Count == 0)
{
tickers = await _exchangeService.GetTickers(timeframe);
var availableTicker = await _exchangeService.GetTickers(timeframe);
tickers = MapTickerToTickerInfos(availableTicker);
_cacheService.SaveValue(cacheKey, tickers, TimeSpan.FromHours(2));
}
return Ok(tickers);
}
private List<TickerInfos> MapTickerToTickerInfos(List<Ticker> availableTicker)
{
var tickerInfos = new List<TickerInfos>();
var tokens = new Dictionary<string, string>
{
{ "AAVE", "https://assets.coingecko.com/coins/images/12645/standard/AAVE.png?1696512452" },
{ "ADA", "https://assets.coingecko.com/coins/images/975/standard/cardano.png?1696502090" },
{ "APE", "https://assets.coingecko.com/coins/images/24383/standard/apecoin.jpg?1696523566" },
{ "ARB", "https://assets.coingecko.com/coins/images/16547/small/photo_2023-03-29_21.47.00.jpeg?1680097630" },
{ "ATOM", "https://assets.coingecko.com/coins/images/1481/standard/cosmos_hub.png?1696502525" },
{ "AVAX", "https://assets.coingecko.com/coins/images/12559/small/coin-round-red.png?1604021818" },
{ "BNB", "https://assets.coingecko.com/coins/images/825/standard/bnb-icon2_2x.png?1696501970" },
{ "BTC", "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579" },
{ "DOGE", "https://assets.coingecko.com/coins/images/5/small/dogecoin.png?1547792256" },
{ "DOT", "https://static.coingecko.com/s/polkadot-73b0c058cae10a2f076a82dcade5cbe38601fad05d5e6211188f09eb96fa4617.gif" },
{ "ETH", "https://assets.coingecko.com/coins/images/279/small/ethereum.png?1595348880" },
{ "FIL", "https://assets.coingecko.com/coins/images/12817/standard/filecoin.png?1696512609" },
{ "GMX", "https://assets.coingecko.com/coins/images/18323/small/arbit.png?1631532468" },
{ "LINK", "https://assets.coingecko.com/coins/images/877/thumb/chainlink-new-logo.png?1547034700" },
{ "LTC", "https://assets.coingecko.com/coins/images/2/small/litecoin.png?1547033580" },
{ "MATIC", "https://assets.coingecko.com/coins/images/32440/standard/polygon.png?1698233684" },
{ "NEAR", "https://assets.coingecko.com/coins/images/10365/standard/near.jpg?1696510367" },
{ "OP", "https://assets.coingecko.com/coins/images/25244/standard/Optimism.png?1696524385" },
{ "PEPE", "https://assets.coingecko.com/coins/images/29850/standard/pepe-token.jpeg?1696528776" },
{ "SOL", "https://assets.coingecko.com/coins/images/4128/small/solana.png?1640133422" },
{ "UNI", "https://assets.coingecko.com/coins/images/12504/thumb/uniswap-uni.png?1600306604" },
{ "USDC", "https://assets.coingecko.com/coins/images/6319/thumb/USD_Coin_icon.png?1547042389" },
{ "USDT", "https://assets.coingecko.com/coins/images/325/thumb/Tether-logo.png?1598003707" },
{ "WIF", "https://assets.coingecko.com/coins/images/33566/standard/dogwifhat.jpg?1702499428" },
{ "XRP", "https://assets.coingecko.com/coins/images/44/small/xrp-symbol-white-128.png?1605778731" },
{ "SHIB", "https://assets.coingecko.com/coins/images/11939/standard/shiba.png?1696511800" },
{ "STX", "https://assets.coingecko.com/coins/images/2069/standard/Stacks_Logo_png.png?1709979332" },
{ "ORDI", "https://assets.coingecko.com/coins/images/30162/standard/ordi.png?1696529082" },
{ "APT", "https://assets.coingecko.com/coins/images/26455/standard/aptos_round.png?1696525528" },
{ "BOME", "https://assets.coingecko.com/coins/images/36071/standard/bome.png?1710407255" },
{ "MEME", "https://assets.coingecko.com/coins/images/32528/standard/memecoin_%282%29.png?1698912168" },
{ "FLOKI", "https://assets.coingecko.com/coins/images/16746/standard/PNG_image.png?1696516318" },
{ "MEW", "https://assets.coingecko.com/coins/images/36440/standard/MEW.png?1711442286" },
{ "TAO", "https://assets.coingecko.com/coins/images/28452/standard/ARUsPeNQ_400x400.jpeg?1696527447" },
{ "BONK", "https://assets.coingecko.com/coins/images/28600/standard/bonk.jpg?1696527587" },
{ "WLD", "https://assets.coingecko.com/coins/images/31069/standard/worldcoin.jpeg?1696529903" },
{ "tBTC", "https://assets.coingecko.com/coins/images/11224/standard/0x18084fba666a33d37592fa2633fd49a74dd93a88.png?1696511155" },
{ "EIGEN", "https://assets.coingecko.com/coins/images/37441/standard/eigen.jpg?1728023974" },
{ "SUI", "https://assets.coingecko.com/coins/images/26375/standard/sui-ocean-square.png?1727791290" },
{ "SEI", "https://assets.coingecko.com/coins/images/28205/standard/Sei_Logo_-_Transparent.png?1696527207" },
{ "DAI", "https://assets.coingecko.com/coins/images/9956/thumb/4943.png?1636636734" },
{ "TIA", "https://assets.coingecko.com/coins/images/31967/standard/tia.jpg?1696530772" },
{ "TRX", "https://assets.coingecko.com/coins/images/1094/standard/tron-logo.png?1696502193" },
{ "TON", "https://assets.coingecko.com/coins/images/17980/standard/photo_2024-09-10_17.09.00.jpeg?1725963446" },
{ "PENDLE", "https://assets.coingecko.com/coins/images/15069/standard/Pendle_Logo_Normal-03.png?1696514728" },
{ "wstETH", "https://assets.coingecko.com/coins/images/18834/standard/wstETH.png?1696518295" },
{ "USDe", "https://assets.coingecko.com/coins/images/33613/standard/USDE.png?1716355685" },
{ "SATS", "https://assets.coingecko.com/coins/images/30666/standard/_dD8qr3M_400x400.png?1702913020" },
{ "POL", "https://assets.coingecko.com/coins/images/32440/standard/polygon.png?1698233684" },
{ "XLM", "https://assets.coingecko.com/coins/images/100/standard/Stellar_symbol_black_RGB.png?1696501482" },
{ "BCH", "https://assets.coingecko.com/coins/images/780/standard/bitcoin-cash-circle.png?1696501932" },
{ "ICP", "https://assets.coingecko.com/coins/images/14495/standard/Internet_Computer_logo.png?1696514180" },
{ "RENDER", "https://assets.coingecko.com/coins/images/11636/standard/rndr.png?1696511529" },
{ "INJ", "https://assets.coingecko.com/coins/images/12882/standard/Secondary_Symbol.png?1696512670" },
{ "TRUMP", "https://assets.coingecko.com/coins/images/53746/standard/trump.png?1737171561" },
{ "MELANIA", "https://assets.coingecko.com/coins/images/53775/standard/melania-meme.png?1737329885" },
{ "ENA", "https://assets.coingecko.com/coins/images/36530/standard/ethena.png?1711701436" },
{ "FARTCOIN", "https://assets.coingecko.com/coins/images/50891/standard/fart.jpg?1729503972" },
{ "AI16Z", "https://assets.coingecko.com/coins/images/51090/standard/AI16Z.jpg?1730027175" },
{ "ANIME", "https://assets.coingecko.com/coins/images/53575/standard/anime.jpg?1736748703" },
{ "BERA", "https://assets.coingecko.com/coins/images/25235/standard/BERA.png?1738822008" },
{ "VIRTUAL", "https://assets.coingecko.com/coins/images/34057/standard/LOGOMARK.png?1708356054" },
{ "PENGU", "https://assets.coingecko.com/coins/images/52622/standard/PUDGY_PENGUINS_PENGU_PFP.png?1733809110" },
{ "FET", "https://assets.coingecko.com/coins/images/5681/standard/ASI.png?1719827289" },
{ "ONDO", "https://assets.coingecko.com/coins/images/26580/standard/ONDO.png?1696525656" },
{ "AIXBT", "https://assets.coingecko.com/coins/images/51784/standard/3.png?1731981138" },
{ "CAKE", "https://assets.coingecko.com/coins/images/12632/standard/pancakeswap-cake-logo_%281%29.png?1696512440" },
{ "S", "https://assets.coingecko.com/coins/images/38108/standard/200x200_Sonic_Logo.png?1734679256" },
{ "JUP", "https://assets.coingecko.com/coins/images/34188/standard/jup.png?1704266489" },
{ "HYPE", "https://assets.coingecko.com/coins/images/50882/standard/hyperliquid.jpg?1729431300" },
{ "OM", "https://assets.coingecko.com/coins/images/12151/standard/OM_Token.png?1696511991" }
};
foreach (var ticker in availableTicker)
{
var tickerInfo = new TickerInfos
{
Ticker = ticker,
ImageUrl = tokens.GetValueOrDefault(ticker.ToString(), "https://assets.coingecko.com/coins/images/1/small/bitcoin.png?1547033579") // Default to BTC image if not found
};
tickerInfos.Add(tickerInfo);
}
return tickerInfos;
}
/// <summary>
/// Retrieves the latest spotlight overview, using caching to enhance response times.
/// </summary>
@@ -222,24 +314,24 @@ public class DataController : ControllerBase
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);
}
@@ -253,32 +345,32 @@ public class DataController : ControllerBase
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>
@@ -288,7 +380,7 @@ public class DataController : ControllerBase
{
// 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;
@@ -296,30 +388,30 @@ public class DataController : ControllerBase
{
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",
StrategyName = strategy.Config.ScenarioName,
State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
PnL = pnl,
ROIPercentage = roi,
ROILast24H = roiLast24h,
@@ -329,7 +421,8 @@ public class DataController : ControllerBase
VolumeLast24H = volumeLast24h,
Wins = wins,
Losses = losses,
Positions = strategy.Positions.OrderByDescending(p => p.Date).ToList() // Include sorted positions with most recent first
Positions = strategy.Positions.OrderByDescending(p => p.Date)
.ToList() // Include sorted positions with most recent first
};
}
@@ -347,20 +440,20 @@ public class DataController : ControllerBase
{
timeFilter = "Total"; // Default to Total if invalid
}
string cacheKey = $"PlatformSummary_{timeFilter}";
// Check if the platform summary is already cached
var cachedSummary = _cacheService.GetValue<PlatformSummaryViewModel>(cacheKey);
if (cachedSummary != null)
{
return Ok(cachedSummary);
}
// Get all agents and their strategies
var agentsWithStrategies = await _mediator.Send(new GetAllAgentsCommand(timeFilter));
// Create the platform summary
var summary = new PlatformSummaryViewModel
{
@@ -368,50 +461,50 @@ public class DataController : ControllerBase
TotalActiveStrategies = agentsWithStrategies.Values.Sum(list => list.Count),
TimeFilter = timeFilter
};
// Calculate total platform metrics
decimal totalPlatformPnL = 0;
decimal totalPlatformVolume = 0;
decimal totalPlatformVolumeLast24h = 0;
// 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
{
Username = user.Name,
AgentName = user.AgentName,
TotalPnL = totalPnL,
PnLLast24h = pnlLast24h,
TotalROI = totalROI,
@@ -423,26 +516,26 @@ public class DataController : ControllerBase
TotalVolume = totalVolume,
VolumeLast24h = volumeLast24h
};
summary.AgentSummaries.Add(agentSummary);
// Add to platform totals
totalPlatformPnL += totalPnL;
totalPlatformVolume += totalVolume;
totalPlatformVolumeLast24h += volumeLast24h;
}
// Set the platform totals
summary.TotalPlatformPnL = totalPlatformPnL;
summary.TotalPlatformVolume = totalPlatformVolume;
summary.TotalPlatformVolumeLast24h = totalPlatformVolumeLast24h;
// Sort agent summaries by total PnL (highest first)
summary.AgentSummaries = summary.AgentSummaries.OrderByDescending(a => a.TotalPnL).ToList();
// Cache the results for 5 minutes
_cacheService.SaveValue(cacheKey, summary, TimeSpan.FromMinutes(5));
return Ok(summary);
}
}

View File

@@ -1,6 +1,7 @@
using Managing.Api.Authorization;
using Managing.Api.Models.Requests;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -12,10 +13,9 @@ namespace Managing.Api.Controllers;
[ApiController]
[Route("[controller]")]
[Produces("application/json")]
public class UserController : ControllerBase
public class UserController : BaseController
{
private IConfiguration _config;
private readonly IUserService _userService;
private readonly IJwtUtils _jwtUtils;
/// <summary>
@@ -25,9 +25,9 @@ public class UserController : ControllerBase
/// <param name="userService">Service for user-related operations.</param>
/// <param name="jwtUtils">Utility for JWT token operations.</param>
public UserController(IConfiguration config, IUserService userService, IJwtUtils jwtUtils)
: base(userService)
{
_config = config;
_userService = userService;
_jwtUtils = jwtUtils;
}
@@ -49,5 +49,30 @@ public class UserController : ControllerBase
}
return Unauthorized();
}
/// <summary>
/// Gets the current user's information.
/// </summary>
/// <returns>The current user's information.</returns>
[HttpGet]
public async Task<ActionResult<User>> GetCurrentUser()
{
var user = await base.GetUser();
return Ok(user);
}
}
/// <summary>
/// Updates the agent name for the current user.
/// </summary>
/// <param name="agentName">The new agent name to set.</param>
/// <returns>The updated user with the new agent name.</returns>
[HttpPut("agent-name")]
public async Task<ActionResult<User>> UpdateAgentName([FromBody] string agentName)
{
var user = await GetUser();
var updatedUser = await _userService.UpdateAgentName(user, agentName);
return Ok(updatedUser);
}
}

View File

@@ -20,5 +20,12 @@ namespace Managing.Api.Models.Requests
[Required]
[Range(10.00, double.MaxValue, ErrorMessage = "Initial trading balance must be greater than ten")]
public decimal InitialTradingBalance { get; set; }
/// <summary>
/// Cooldown period in minutes between trades
/// </summary>
[Required]
[Range(1, 1440, ErrorMessage = "Cooldown period must be between 1 and 1440 minutes (24 hours)")]
public decimal CooldownPeriod { get; set; } = 1; // Default to 1 minute if not specified
}
}

View File

@@ -6,9 +6,9 @@ namespace Managing.Api.Models.Responses
public class AgentSummaryViewModel
{
/// <summary>
/// Username of the agent
/// AgentName of the agent
/// </summary>
public string Username { get; set; }
public string AgentName { get; set; }
/// <summary>
/// Total profit and loss in USD

View File

@@ -0,0 +1,9 @@
using Managing.Common;
namespace Managing.Api.Models.Responses;
public class TickerInfos
{
public Enums.Ticker Ticker { get; set; }
public string ImageUrl { get; set; }
}

View File

@@ -1,8 +1,8 @@
using Managing.Domain.Candles;
using System.ComponentModel.DataAnnotations;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Strategies;
using Managing.Domain.Trades;
using System.ComponentModel.DataAnnotations;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Responses
@@ -23,5 +23,7 @@ namespace Managing.Api.Models.Responses
[Required] public BotType BotType { get; internal set; }
[Required] public string AccountName { get; internal set; }
[Required] public MoneyManagement MoneyManagement { get; internal set; }
[Required] public string Identifier { get; set; }
[Required] public string AgentName { get; set; }
}
}

View File

@@ -4,6 +4,7 @@ using HealthChecks.UI.Client;
using Managing.Api.Authorization;
using Managing.Api.Filters;
using Managing.Api.HealthChecks;
using Managing.Api.Workers;
using Managing.Application.Hubs;
using Managing.Bootstrap;
using Managing.Common;
@@ -195,7 +196,10 @@ builder.Services.AddSwaggerGen(options =>
});
builder.WebHost.SetupDiscordBot();
// builder.Services.AddHostedService<BotManagerWorker>();
if (builder.Configuration.GetValue<bool>("EnableBotManager", false))
{
builder.Services.AddHostedService<BotManagerWorker>();
}
// App
var app = builder.Build();

View File

@@ -24,5 +24,6 @@
"ElasticConfiguration": {
"Uri": "http://elasticsearch:9200"
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"EnableBotManager": true
}