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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
9
src/Managing.Api/Models/Responses/TickerInfos.cs
Normal file
9
src/Managing.Api/Models/Responses/TickerInfos.cs
Normal 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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -24,5 +24,6 @@
|
||||
"ElasticConfiguration": {
|
||||
"Uri": "http://elasticsearch:9200"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"EnableBotManager": true
|
||||
}
|
||||
Reference in New Issue
Block a user