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

BIN
.DS_Store vendored

Binary file not shown.

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>
@@ -317,9 +409,9 @@ public class DataController : ControllerBase
return new UserStrategyDetailsViewModel
{
Name = strategy.Name,
StrategyName = strategy.ScenarioName,
StrategyName = strategy.Config.ScenarioName,
State = strategy.GetStatus() == BotStatus.Up.ToString() ? "RUNNING" :
strategy.GetStatus() == BotStatus.Down.ToString() ? "STOPPED" : "UNUSED",
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
};
}
@@ -411,7 +504,7 @@ public class DataController : ControllerBase
// Add to agent summaries
var agentSummary = new AgentSummaryViewModel
{
Username = user.Name,
AgentName = user.AgentName,
TotalPnL = totalPnL,
PnLLast24h = pnlLast24h,
TotalROI = totalROI,

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;
}
@@ -50,4 +50,29 @@ 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
}

View File

@@ -4,6 +4,7 @@ namespace Managing.Application.Abstractions.Repositories;
public interface IUserRepository
{
Task<User> GetUserByAgentNameAsync(string agentName);
Task<User> GetUserByNameAsync(string name);
Task InsertUserAsync(User user);
Task UpdateUser(User user);

View File

@@ -22,7 +22,9 @@ namespace Managing.Application.Abstractions.Services
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null);
List<Candle>? initialCandles = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0);
Task<Backtest> RunFlippingBotBacktest(
Account account,
@@ -36,16 +38,34 @@ namespace Managing.Application.Abstractions.Services
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null);
List<Candle>? initialCandles = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0);
bool DeleteBacktest(string id);
bool DeleteBacktests();
Task<Backtest> RunScalpingBotBacktest(Account account, MoneyManagement moneyManagement, Scenario scenario,
Timeframe timeframe, List<Candle> candles, decimal balance, User user = null);
Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
List<Candle> candles,
decimal balance,
User user = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0);
Task<Backtest> RunFlippingBotBacktest(Account account, MoneyManagement moneyManagement, Scenario scenario,
Timeframe timeframe, List<Candle> candles, decimal balance, User user = null);
Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
List<Candle> candles,
decimal balance,
User user = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0);
// User-specific operations
Task<IEnumerable<Backtest>> GetBacktestsByUser(User user);

View File

@@ -6,4 +6,6 @@ public interface IUserService
{
Task<User> Authenticate(string name, string address, string message, string signature);
Task<User> GetUserByAddressAsync(string address);
Task<User> UpdateAgentName(User user, string agentName);
User GetUser(string name);
}

View File

@@ -50,7 +50,7 @@ public abstract class BaseWorker<T> : BackgroundService where T : class
{
worker = await _workerService.GetWorker(_workerType);
if (worker.IsActive)
if (worker.IsActive || worker.WorkerType.Equals(WorkerType.BotManager))
{
await Run(cancellationToken);
_executionCount++;

View File

@@ -1,16 +1,15 @@
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Application.Bots;
using Managing.Domain.Bots;
using Managing.Domain.Workflows;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions
{
public interface IBotFactory
{
ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance);
ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance);
ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance);
ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance);
IBot CreateSimpleBot(string botName, Workflow workflow);
ITradingBot CreateScalpingBot(TradingBotConfig config);
ITradingBot CreateBacktestScalpingBot(TradingBotConfig config);
ITradingBot CreateFlippingBot(TradingBotConfig config);
ITradingBot CreateBacktestFlippingBot(TradingBotConfig config);
}
}

View File

@@ -1,38 +1,30 @@
using Managing.Common;
using Managing.Application.Bots;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using static Managing.Common.Enums;
namespace Managing.Application.Abstractions;
public interface IBotService
{
void SaveOrUpdateBotBackup(BotBackup botBackup);
void SaveOrUpdateBotBackup(User user, string identifier, Enums.BotType botType, string data);
void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data);
void AddSimpleBotToCache(IBot bot);
void AddTradingBotToCache(ITradingBot bot);
List<ITradingBot> GetActiveBots();
IEnumerable<BotBackup> GetSavedBots();
void StartBotFromBackup(BotBackup backupBot);
BotBackup GetBotBackup(string name);
BotBackup GetBotBackup(string identifier);
ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateScalpingBot(TradingBotConfig config);
ITradingBot CreateBacktestScalpingBot(TradingBotConfig config);
ITradingBot CreateFlippingBot(TradingBotConfig config);
ITradingBot CreateBacktestFlippingBot(TradingBotConfig config);
IBot CreateSimpleBot(string botName, Workflow workflow);
Task<string> StopBot(string requestName);
Task<bool> DeleteBot(string requestName);
Task<string> RestartBot(string requestName);
Task<string> StopBot(string botName);
Task<bool> DeleteBot(string botName);
Task<string> RestartBot(string botName);
void DeleteBotBackup(string backupBotName);
void ToggleIsForWatchingOnly(string botName);
}

View File

@@ -1,33 +1,32 @@
using Managing.Core.FixedSizedQueue;
using Managing.Application.Bots;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
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
{
public interface ITradingBot : IBot
{
HashSet<Signal> Signals { get; set; }
List<Position> Positions { get; set; }
TradingBotConfig Config { get; set; }
Account Account { get; set; }
HashSet<IStrategy> Strategies { get; set; }
FixedSizeQueue<Candle> OptimizedCandles { get; set; }
HashSet<Candle> Candles { get; set; }
Timeframe Timeframe { get; set; }
HashSet<IStrategy> Strategies { get; set; }
Ticker Ticker { get; }
string ScenarioName { get; }
string AccountName { get; }
bool IsForWatchingOnly { get; set; }
MoneyManagement MoneyManagement { get; set; }
BotType BotType { get; set; }
HashSet<Signal> Signals { get; set; }
List<Position> Positions { get; set; }
Dictionary<DateTime, decimal> WalletBalances { get; set; }
Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
User User { get; set; }
string Identifier { get; set; }
DateTime StartupTime { get; set; }
DateTime PreloadSince { get; set; }
int PreloadedCandlesCount { get; set; }
decimal Fee { get; set; }
Scenario Scenario { get; set; }
Task Run();
Task ToggleIsForWatchOnly();
@@ -38,5 +37,6 @@ namespace Managing.Application.Abstractions
void LoadScenario(string scenarioName);
void UpdateStrategiesValues();
Task LoadAccount();
Task<Position> OpenPositionManually(TradeDirection direction);
}
}

View File

@@ -1,6 +1,7 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Core;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
@@ -64,10 +65,26 @@ namespace Managing.Application.Backtesting
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle> initialCandles = null)
List<Candle>? initialCandles = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0)
{
var scalpingBot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, isForWatchingOnly, balance);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = balance,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak
};
var scalpingBot = _botFactory.CreateBacktestScalpingBot(config);
scalpingBot.LoadScenario(scenario.Name);
await scalpingBot.LoadAccount();
var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate);
@@ -91,21 +108,6 @@ namespace Managing.Application.Backtesting
return result;
}
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{
List<Candle> candles;
// Use specific date range
candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker,
startDate, timeframe, endDate).Result;
if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {account.Exchange}");
return candles;
}
public async Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
@@ -118,10 +120,26 @@ namespace Managing.Application.Backtesting
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle> initialCandles = null)
List<Candle>? initialCandles = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0)
{
var flippingBot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, false, balance);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = balance,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak
};
var flippingBot = _botFactory.CreateBacktestFlippingBot(config);
flippingBot.LoadScenario(scenario.Name);
await flippingBot.LoadAccount();
@@ -146,13 +164,34 @@ namespace Managing.Application.Backtesting
return result;
}
public async Task<Backtest> RunScalpingBotBacktest(Account account, MoneyManagement moneyManagement,
public async Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe, List<Candle> candles, decimal balance, User user = null)
Timeframe timeframe,
List<Candle> candles,
decimal balance,
User user = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var bot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, false, balance);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak
};
var bot = _botFactory.CreateBacktestScalpingBot(config);
bot.LoadScenario(scenario.Name);
await bot.LoadAccount();
@@ -167,13 +206,34 @@ namespace Managing.Application.Backtesting
return result;
}
public async Task<Backtest> RunFlippingBotBacktest(Account account, MoneyManagement moneyManagement,
public async Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe, List<Candle> candles, decimal balance, User user = null)
Timeframe timeframe,
List<Candle> candles,
decimal balance,
User user = null,
decimal cooldownPeriod = 1,
int maxLossStreak = 0)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var bot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, false, balance);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak
};
var bot = _botFactory.CreateBacktestFlippingBot(config);
bot.LoadScenario(scenario.Name);
await bot.LoadAccount();
@@ -188,6 +248,21 @@ namespace Managing.Application.Backtesting
return result;
}
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{
List<Candle> candles;
// Use specific date range
candles = _exchangeService.GetCandlesInflux(account.Exchange, ticker,
startDate, timeframe, endDate).Result;
if (candles == null || candles.Count == 0)
throw new Exception($"No candles for {ticker} on {account.Exchange}");
return candles;
}
private Backtest GetBacktestingResult(
Ticker ticker,
Scenario scenario,
@@ -239,7 +314,7 @@ namespace Managing.Application.Backtesting
var score = BacktestScorer.CalculateTotalScore(scoringParams);
var result = new Backtest(ticker, scenario.Name, bot.Positions, bot.Signals.ToList(), timeframe, candles,
bot.BotType, account.Name)
bot.Config.BotType, account.Name)
{
FinalPnl = finalPnl,
WinRate = winRate,

View File

@@ -1,7 +1,6 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -38,82 +37,58 @@ namespace Managing.Application.Bots.Base
return new SimpleBot(botName, _tradingBotLogger, workflow, _botService);
}
ITradingBot IBotFactory.CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
ITradingBot IBotFactory.CreateScalpingBot(TradingBotConfig config)
{
config.BotType = BotType.ScalpingBot;
return new ScalpingBot(
accountName,
moneyManagement,
name,
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
_botService,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly);
config);
}
ITradingBot IBotFactory.CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
ITradingBot IBotFactory.CreateBacktestScalpingBot(TradingBotConfig config)
{
config.BotType = BotType.ScalpingBot;
config.IsForBacktest = true;
return new ScalpingBot(
accountName,
moneyManagement,
"BacktestBot",
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
_botService,
initialTradingBalance,
isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
config);
}
public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
public ITradingBot CreateFlippingBot(TradingBotConfig config)
{
config.BotType = BotType.FlippingBot;
return new FlippingBot(
accountName,
moneyManagement,
name,
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
_botService,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly);
config);
}
public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
public ITradingBot CreateBacktestFlippingBot(TradingBotConfig config)
{
config.BotType = BotType.FlippingBot;
config.IsForBacktest = true;
return new FlippingBot(
accountName,
moneyManagement,
"BacktestBot",
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
_botService,
initialTradingBalance,
isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
config);
}
}
}

View File

@@ -1,6 +1,5 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.MoneyManagements;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -8,39 +7,18 @@ namespace Managing.Application.Bots
{
public class FlippingBot : TradingBot
{
public FlippingBot(string accountName,
MoneyManagement moneyManagement,
string name,
string scenarioName,
public FlippingBot(
IExchangeService exchangeService,
Ticker ticker,
ITradingService tradingService,
ILogger<TradingBot> logger,
Timeframe timeframe,
ITradingService tradingService,
IAccountService accountService,
IMessengerService messengerService,
IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false,
bool isForWatchingOnly = false)
: base(accountName,
moneyManagement,
name,
ticker,
scenarioName,
exchangeService,
logger,
tradingService,
timeframe,
accountService,
messengerService,
botService,
initialTradingBalance,
isForBacktest,
isForWatchingOnly,
flipPosition: true)
TradingBotConfig config)
: base(exchangeService, logger, tradingService, accountService, messengerService, botService, config)
{
BotType = BotType.FlippingBot;
Config.BotType = BotType.FlippingBot;
Config.FlipPosition = true;
}
public sealed override void Start()

View File

@@ -1,6 +1,5 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services;
using Managing.Domain.MoneyManagements;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
@@ -8,38 +7,17 @@ namespace Managing.Application.Bots
{
public class ScalpingBot : TradingBot
{
public ScalpingBot(string accountName,
MoneyManagement moneyManagement,
string name,
string scenarioName,
public ScalpingBot(
IExchangeService exchangeService,
Ticker ticker,
ITradingService tradingService,
ILogger<TradingBot> logger,
Timeframe timeframe,
ITradingService tradingService,
IAccountService accountService,
IMessengerService messengerService,
IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false,
bool isForWatchingOnly = false)
: base(accountName,
moneyManagement,
name,
ticker,
scenarioName,
exchangeService,
logger,
tradingService,
timeframe,
accountService,
messengerService,
botService,
initialTradingBalance,
isForBacktest,
isForWatchingOnly)
TradingBotConfig config)
: base(exchangeService, logger, tradingService, accountService, messengerService, botService, config)
{
BotType = BotType.ScalpingBot;
Config.BotType = BotType.ScalpingBot;
}
public sealed override void Start()

View File

@@ -45,7 +45,7 @@ namespace Managing.Application.Bots
public override void SaveBackup()
{
var data = JsonConvert.SerializeObject(_workflow);
_botService.SaveOrUpdateBotBackup(User, Identifier, BotType.SimpleBot, data);
_botService.SaveOrUpdateBotBackup(User, Identifier, BotType.SimpleBot, Status, data);
}
public override void LoadBackup(BotBackup backup)

View File

@@ -28,76 +28,47 @@ public class TradingBot : Bot, ITradingBot
private readonly ITradingService TradingService;
private readonly IBotService BotService;
public TradingBotConfig Config { get; set; }
public Account Account { get; set; }
public HashSet<IStrategy> Strategies { get; set; }
public FixedSizeQueue<Candle> OptimizedCandles { get; set; }
public HashSet<Candle> Candles { get; set; }
public HashSet<Signal> Signals { get; set; }
public List<Position> Positions { get; set; }
public Ticker Ticker { get; set; }
public string ScenarioName { get; set; }
public string AccountName { get; set; }
public MoneyManagement MoneyManagement { get; set; }
public Timeframe Timeframe { get; set; }
public bool IsForBacktest { get; set; }
public DateTime PreloadSince { get; set; }
public bool IsForWatchingOnly { get; set; }
public bool FlipPosition { get; set; }
public int PreloadedCandlesCount { get; set; }
public BotType BotType { get; set; }
public decimal Fee { get; set; }
public Scenario Scenario { get; set; }
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
public DateTime StartupTime { get; set; }
/// <summary>
/// The dedicated trading balance for this bot in USD
/// </summary>
public decimal BotTradingBalance { get; set; }
public DateTime PreloadSince { get; set; }
public int PreloadedCandlesCount { get; set; }
public decimal Fee { get; set; }
public Scenario Scenario { get; set; }
public TradingBot(
string accountName,
MoneyManagement moneyManagement,
string name,
Ticker ticker,
string scenarioName,
IExchangeService exchangeService,
ILogger<TradingBot> logger,
ITradingService tradingService,
Timeframe timeframe,
IAccountService accountService,
IMessengerService messengerService,
IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false,
bool isForWatchingOnly = false,
bool flipPosition = false)
: base(name)
TradingBotConfig config
)
: base(config.AccountName)
{
ExchangeService = exchangeService;
AccountService = accountService;
MessengerService = messengerService;
TradingService = tradingService;
BotService = botService;
Logger = logger;
if (initialTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
if (config.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
throw new ArgumentException(
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}",
nameof(initialTradingBalance));
nameof(config.BotTradingBalance));
}
IsForWatchingOnly = isForWatchingOnly;
FlipPosition = flipPosition;
AccountName = accountName;
MoneyManagement = moneyManagement;
Ticker = ticker;
ScenarioName = scenarioName;
Timeframe = timeframe;
IsForBacktest = isForBacktest;
Logger = logger;
BotTradingBalance = initialTradingBalance;
Config = config;
Strategies = new HashSet<IStrategy>();
Signals = new HashSet<Signal>();
@@ -107,10 +78,10 @@ public class TradingBot : Bot, ITradingBot
WalletBalances = new Dictionary<DateTime, decimal>();
StrategiesValues = new Dictionary<StrategyType, StrategiesResultBase>();
if (!isForBacktest)
if (!Config.IsForBacktest)
{
Interval = CandleExtensions.GetIntervalFromTimeframe(timeframe);
PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(timeframe);
Interval = CandleExtensions.GetIntervalFromTimeframe(Config.Timeframe);
PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(Config.Timeframe);
}
}
@@ -120,9 +91,9 @@ public class TradingBot : Bot, ITradingBot
// Load account synchronously
await LoadAccount();
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
LoadScenario(ScenarioName);
LoadScenario(Config.ScenarioName);
await PreloadCandles();
await CancelAllOrders();
@@ -146,10 +117,10 @@ public class TradingBot : Bot, ITradingBot
public async Task LoadAccount()
{
var account = await AccountService.GetAccount(AccountName, false, false);
var account = await AccountService.GetAccount(Config.AccountName, false, false);
if (account == null)
{
Logger.LogWarning($"No account found for this {AccountName}");
Logger.LogWarning($"No account found for this {Config.AccountName}");
Stop();
}
else
@@ -185,7 +156,7 @@ public class TradingBot : Bot, ITradingBot
public async Task Run()
{
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
// Check broker balance before running
var balance = await ExchangeService.GetBalance(Account, false);
@@ -200,25 +171,25 @@ public class TradingBot : Bot, ITradingBot
Logger.LogInformation($"____________________{Name}____________________");
Logger.LogInformation(
$"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Last candle : {OptimizedCandles.Last().Date} - Bot : {Name} - Type {BotType} - Ticker : {Ticker}");
$"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Last candle : {OptimizedCandles.Last().Date} - Bot : {Name} - Type {Config.BotType} - Ticker : {Config.Ticker}");
}
var previousLastCandle = OptimizedCandles.LastOrDefault();
if (!IsForBacktest)
if (!Config.IsForBacktest)
await UpdateCandles();
var currentLastCandle = OptimizedCandles.LastOrDefault();
if (currentLastCandle != previousLastCandle || IsForBacktest)
if (currentLastCandle != previousLastCandle || Config.IsForBacktest)
await UpdateSignals(OptimizedCandles);
else
Logger.LogInformation($"No need to update signals for {Ticker}");
Logger.LogInformation($"No need to update signals for {Config.Ticker}");
if (!IsForWatchingOnly)
if (!Config.IsForWatchingOnly)
await ManagePositions();
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
SaveBackup();
UpdateStrategiesValues();
@@ -248,7 +219,8 @@ public class TradingBot : Bot, ITradingBot
if (OptimizedCandles.Any())
return;
var candles = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, PreloadSince, Timeframe);
var candles =
await ExchangeService.GetCandlesInflux(Account.Exchange, Config.Ticker, PreloadSince, Config.Timeframe);
foreach (var candle in candles.Where(c => c.Date < DateTime.Now.ToUniversalTime()))
{
@@ -272,25 +244,22 @@ public class TradingBot : Bot, ITradingBot
await AddSignal(signal);
}
private async Task AddSignal(Signal signal)
{
// if (!IsForBacktest)
// TradingService.InsertSignal(signal);
if (IsForWatchingOnly || (ExecutionCount < 1 && !IsForBacktest))
if (Config.IsForWatchingOnly || (ExecutionCount < 1 && !Config.IsForBacktest))
signal.Status = SignalStatus.Expired;
Signals.Add(signal);
var signalText = $"{ScenarioName} trigger a signal. Signal told you " +
$"to {signal.Direction} {Ticker} on {Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}";
var signalText = $"{Config.ScenarioName} trigger a signal. Signal told you " +
$"to {signal.Direction} {Config.Ticker} on {Config.Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}";
Logger.LogInformation(signalText);
if (IsForWatchingOnly && !IsForBacktest && ExecutionCount > 0)
if (Config.IsForWatchingOnly && !Config.IsForBacktest && ExecutionCount > 0)
{
await MessengerService.SendSignal(signalText, Account.Exchange, Ticker, signal.Direction, Timeframe);
await MessengerService.SendSignal(signalText, Account.Exchange, Config.Ticker, signal.Direction,
Config.Timeframe);
}
}
@@ -300,7 +269,8 @@ public class TradingBot : Bot, ITradingBot
return;
var lastCandle = OptimizedCandles.Last();
var newCandle = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, lastCandle.Date, Timeframe);
var newCandle =
await ExchangeService.GetCandlesInflux(Account.Exchange, Config.Ticker, lastCandle.Date, Config.Timeframe);
foreach (var candle in newCandle.Where(c => c.Date < DateTime.Now.ToUniversalTime()))
{
@@ -342,7 +312,7 @@ public class TradingBot : Bot, ITradingBot
if (WalletBalances.Count == 0)
{
// WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest);
WalletBalances[date] = BotTradingBalance;
WalletBalances[date] = Config.BotTradingBalance;
return;
}
@@ -353,24 +323,23 @@ public class TradingBot : Bot, ITradingBot
}
}
private async Task UpdatePosition(Signal signal, Position positionForSignal)
{
try
{
Logger.LogInformation($"Updating position {positionForSignal.SignalIdentifier}");
var position = IsForBacktest
var position = Config.IsForBacktest
? positionForSignal
: TradingService.GetPositionByIdentifier(positionForSignal.Identifier);
var positionsExchange = IsForBacktest
var positionsExchange = Config.IsForBacktest
? new List<Position> { position }
: await TradingService.GetBrokerPositions(Account);
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
var brokerPosition = positionsExchange.FirstOrDefault(p => p.Ticker == Ticker);
var brokerPosition = positionsExchange.FirstOrDefault(p => p.Ticker == Config.Ticker);
if (brokerPosition != null)
{
UpdatePositionPnl(positionForSignal.Identifier, brokerPosition.ProfitAndLoss.Realized);
@@ -395,7 +364,7 @@ public class TradingBot : Bot, ITradingBot
if (position.Status == PositionStatus.New)
{
var orders = await ExchangeService.GetOpenOrders(Account, Ticker);
var orders = await ExchangeService.GetOpenOrders(Account, Config.Ticker);
if (orders.Any())
{
await LogInformation(
@@ -420,9 +389,9 @@ public class TradingBot : Bot, ITradingBot
// Position might be partially filled, meaning that TPSL havent been sended yet
// But the position might already been closed by the exchange so we have to check should be closed
var lastCandle = IsForBacktest
var lastCandle = Config.IsForBacktest
? OptimizedCandles.Last()
: ExchangeService.GetCandle(Account, Ticker, DateTime.UtcNow);
: ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
if (positionForSignal.OriginDirection == TradeDirection.Long)
{
@@ -512,7 +481,6 @@ public class TradingBot : Bot, ITradingBot
}
}
private async Task OpenPosition(Signal signal)
{
// Check if a position is already open
@@ -521,9 +489,9 @@ public class TradingBot : Bot, ITradingBot
var openedPosition = Positions.FirstOrDefault(p => p.Status == PositionStatus.Filled
&& p.SignalIdentifier != signal.Identifier);
var lastPrice = IsForBacktest
var lastPrice = Config.IsForBacktest
? OptimizedCandles.Last().Close
: ExchangeService.GetPrice(Account, Ticker, DateTime.UtcNow);
: ExchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow);
// If position open
if (openedPosition != null)
@@ -542,7 +510,7 @@ public class TradingBot : Bot, ITradingBot
{
// An operation is already open for the opposite direction
// ==> Flip the position
if (FlipPosition)
if (Config.FlipPosition)
{
await LogInformation("Try to flip the position because of an opposite direction signal");
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
@@ -561,10 +529,8 @@ public class TradingBot : Bot, ITradingBot
}
else
{
if (!CanOpenPosition(signal))
if (!(await CanOpenPosition(signal)))
{
await LogInformation(
"Tried to open position but last position was a loss. Wait for an opposition direction side or wait x candles to open a new position");
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
return;
}
@@ -575,15 +541,15 @@ public class TradingBot : Bot, ITradingBot
try
{
var command = new OpenPositionRequest(
AccountName,
MoneyManagement,
Config.AccountName,
Config.MoneyManagement,
signal.Direction,
Ticker,
Config.Ticker,
PositionInitiator.Bot,
signal.Date,
User,
BotTradingBalance,
IsForBacktest,
Config.BotTradingBalance,
Config.IsForBacktest,
lastPrice,
signalIdentifier: signal.Identifier);
@@ -598,7 +564,7 @@ public class TradingBot : Bot, ITradingBot
{
SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen);
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
await MessengerService.SendPosition(position);
}
@@ -622,25 +588,142 @@ public class TradingBot : Bot, ITradingBot
}
}
private bool CanOpenPosition(Signal signal)
private async Task<bool> CanOpenPosition(Signal signal)
{
if (!IsForBacktest && ExecutionCount < 1)
// Early return if we're in backtest mode and haven't executed yet
if (!Config.IsForBacktest && ExecutionCount < 1)
{
await LogInformation("Cannot open position: Bot hasn't executed yet");
return false;
}
if (Positions.Count == 0)
// Check if we're in backtest mode
if (Config.IsForBacktest)
{
return await CheckCooldownPeriod(signal) && await CheckLossStreak(signal);
}
// Check broker positions for live trading
var canOpenPosition = await CheckBrokerPositions();
if (!canOpenPosition)
{
return false;
}
// Check cooldown period and loss streak
return await CheckCooldownPeriod(signal) && await CheckLossStreak(signal);
}
private async Task<bool> CheckLossStreak(Signal signal)
{
// If MaxLossStreak is 0, there's no limit
if (Config.MaxLossStreak <= 0)
{
return true;
}
// Get the last N finished positions regardless of direction
var recentPositions = Positions
.Where(p => p.IsFinished())
.OrderByDescending(p => p.Open.Date)
.Take(Config.MaxLossStreak)
.ToList();
// If we don't have enough positions to form a streak, we can open
if (recentPositions.Count < Config.MaxLossStreak)
{
return true;
}
// Check if all recent positions were losses
var allLosses = recentPositions.All(p => p.ProfitAndLoss?.Realized < 0);
if (!allLosses)
{
return true;
}
// If we have a loss streak, check if the last position was in the same direction as the signal
var lastPosition = recentPositions.First();
if (lastPosition.OriginDirection == signal.Direction)
{
await LogWarning($"Cannot open position: Max loss streak ({Config.MaxLossStreak}) reached. " +
$"Last {recentPositions.Count} trades were losses. " +
$"Last position was {lastPosition.OriginDirection}, waiting for a signal in the opposite direction.");
return false;
}
return true;
}
private async Task<bool> CheckBrokerPositions()
{
try
{
var positions = await ExchangeService.GetBrokerPositions(Account);
if (!positions.Any(p => p.Ticker == Config.Ticker))
{
return true;
}
// Handle existing position on broker
var previousPosition = Positions.LastOrDefault();
var orders = await ExchangeService.GetOpenOrders(Account, Config.Ticker);
var reason = $"Cannot open position. There is already a position open for {Config.Ticker} on the broker.";
if (previousPosition != null && orders.Count >= 2)
{
await SetPositionStatus(previousPosition.SignalIdentifier, PositionStatus.Filled);
}
else
{
reason +=
" Position open on broker but not enough orders or no previous position internally saved by the bot";
}
await LogWarning(reason);
return false;
}
catch (Exception ex)
{
await LogWarning($"Error checking broker positions: {ex.Message}");
return false;
}
}
private async Task<bool> CheckCooldownPeriod(Signal signal)
{
var lastPosition = Positions.LastOrDefault(p => p.IsFinished()
&& p.SignalIdentifier != signal.Identifier
&& p.OriginDirection == signal.Direction);
if (lastPosition == null)
{
return true;
}
var cooldownCandle = OptimizedCandles.TakeLast((int)Config.CooldownPeriod).FirstOrDefault();
if (cooldownCandle == null)
{
await LogWarning("Cannot check cooldown period: Not enough candles available");
return false;
}
var tenCandleAgo = OptimizedCandles.TakeLast(10).First();
var positionSignal = Signals.FirstOrDefault(s => s.Identifier == lastPosition.SignalIdentifier);
if (positionSignal == null)
{
await LogWarning($"Cannot find signal for last position {lastPosition.Identifier}");
return false;
}
return positionSignal.Date < tenCandleAgo.Date;
var canOpenPosition = positionSignal.Date < cooldownCandle.Date;
if (!canOpenPosition)
{
await LogInformation(
$"Position cannot be opened: Cooldown period not elapsed. Last position date: {positionSignal.Date}, Cooldown candle date: {cooldownCandle.Date}");
}
return canOpenPosition;
}
public async Task CloseTrade(Signal signal, Position position, Trade tradeToClose, decimal lastPrice,
@@ -654,18 +737,18 @@ public class TradingBot : Bot, ITradingBot
}
await LogInformation(
$"Trying to close trade {Ticker} at {lastPrice} - Type : {tradeToClose.TradeType} - Quantity : {tradeToClose.Quantity} " +
$"Trying to close trade {Config.Ticker} at {lastPrice} - Type : {tradeToClose.TradeType} - Quantity : {tradeToClose.Quantity} " +
$"- Closing Position : {tradeClosingPosition}");
// Get status of position before closing it. The position might be already close by the exchange
if (!IsForBacktest && await ExchangeService.GetQuantityInPosition(Account, Ticker) == 0)
if (!Config.IsForBacktest && await ExchangeService.GetQuantityInPosition(Account, Config.Ticker) == 0)
{
Logger.LogInformation($"Trade already close on exchange");
await HandleClosedPosition(position);
}
else
{
var command = new ClosePositionCommand(position, lastPrice, isForBacktest: IsForBacktest);
var command = new ClosePositionCommand(position, lastPrice, isForBacktest: Config.IsForBacktest);
try
{
var closedPosition = (new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService)
@@ -710,12 +793,12 @@ public class TradingBot : Bot, ITradingBot
if (position.ProfitAndLoss != null)
{
// Add PnL (could be positive or negative)
BotTradingBalance += position.ProfitAndLoss.Realized;
Config.BotTradingBalance += position.ProfitAndLoss.Realized;
// Subtract fees
BotTradingBalance -= GetPositionFees(position);
Config.BotTradingBalance -= GetPositionFees(position);
Logger.LogInformation($"Updated bot trading balance to: {BotTradingBalance}");
Logger.LogInformation($"Updated bot trading balance to: {Config.BotTradingBalance}");
}
}
else
@@ -723,7 +806,7 @@ public class TradingBot : Bot, ITradingBot
await LogWarning("Weird things happen - Trying to update position status, but no position found");
}
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
await MessengerService.SendClosingPosition(position);
}
@@ -733,15 +816,15 @@ public class TradingBot : Bot, ITradingBot
private async Task CancelAllOrders()
{
if (!IsForBacktest && !IsForWatchingOnly)
if (!Config.IsForBacktest && !Config.IsForWatchingOnly)
{
try
{
var openOrders = await ExchangeService.GetOpenOrders(Account, Ticker);
var openOrders = await ExchangeService.GetOpenOrders(Account, Config.Ticker);
if (openOrders.Any())
{
var openPositions = (await ExchangeService.GetBrokerPositions(Account))
.Where(p => p.Ticker == Ticker);
.Where(p => p.Ticker == Config.Ticker);
var cancelClose = openPositions.Any();
if (cancelClose)
@@ -750,15 +833,15 @@ public class TradingBot : Bot, ITradingBot
}
else
{
Logger.LogInformation($"Canceling all orders for {Ticker}");
await ExchangeService.CancelOrder(Account, Ticker);
var closePendingOrderStatus = await ExchangeService.CancelOrder(Account, Ticker);
Logger.LogInformation($"Closing all {Ticker} orders status : {closePendingOrderStatus}");
Logger.LogInformation($"Canceling all orders for {Config.Ticker}");
await ExchangeService.CancelOrder(Account, Config.Ticker);
var closePendingOrderStatus = await ExchangeService.CancelOrder(Account, Config.Ticker);
Logger.LogInformation($"Closing all {Config.Ticker} orders status : {closePendingOrderStatus}");
}
}
else
{
Logger.LogInformation($"No need to cancel orders for {Ticker}");
Logger.LogInformation($"No need to cancel orders for {Config.Ticker}");
}
}
catch (Exception ex)
@@ -771,10 +854,11 @@ public class TradingBot : Bot, ITradingBot
private async Task SetPositionStatus(string signalIdentifier, PositionStatus positionStatus)
{
if (!Positions.First(p => p.SignalIdentifier == signalIdentifier).Status.Equals(positionStatus))
var position = Positions.First(p => p.SignalIdentifier == signalIdentifier);
if (!position.Status.Equals(positionStatus))
{
await LogInformation($"Position {signalIdentifier} is now {positionStatus}");
Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
await LogInformation($"Position {signalIdentifier} new status {position.Status} => {positionStatus}");
}
SetSignalStatus(signalIdentifier,
@@ -864,8 +948,8 @@ public class TradingBot : Bot, ITradingBot
public async Task ToggleIsForWatchOnly()
{
IsForWatchingOnly = (!IsForWatchingOnly);
await LogInformation($"Watch only toggle for bot : {Name} - Watch only : {IsForWatchingOnly}");
Config.IsForWatchingOnly = !Config.IsForWatchingOnly;
await LogInformation($"Watch only toggle for bot : {Name} - Watch only : {Config.IsForWatchingOnly}");
}
private async Task LogInformation(string message)
@@ -883,7 +967,7 @@ public class TradingBot : Bot, ITradingBot
private async Task SendTradeMessage(string message, bool isBadBehavior = false)
{
if (!IsForBacktest)
if (!Config.IsForBacktest)
{
await MessengerService.SendTradeMessage(message, isBadBehavior);
}
@@ -894,43 +978,47 @@ public class TradingBot : Bot, ITradingBot
var data = new TradingBotBackup
{
Name = Name,
BotType = BotType,
BotType = Config.BotType,
Signals = Signals,
Positions = Positions,
Timeframe = Timeframe,
Ticker = Ticker,
ScenarioName = ScenarioName,
AccountName = AccountName,
IsForWatchingOnly = IsForWatchingOnly,
Timeframe = Config.Timeframe,
Ticker = Config.Ticker,
ScenarioName = Config.ScenarioName,
AccountName = Config.AccountName,
IsForWatchingOnly = Config.IsForWatchingOnly,
WalletBalances = WalletBalances,
MoneyManagement = MoneyManagement,
BotTradingBalance = BotTradingBalance,
MoneyManagement = Config.MoneyManagement,
BotTradingBalance = Config.BotTradingBalance,
StartupTime = StartupTime,
CooldownPeriod = Config.CooldownPeriod,
};
BotService.SaveOrUpdateBotBackup(User, Identifier, BotType, JsonConvert.SerializeObject(data));
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
}
public override void LoadBackup(BotBackup backup)
{
var data = JsonConvert.DeserializeObject<TradingBotBackup>(backup.Data);
Config = new TradingBotConfig
{
AccountName = data.AccountName,
MoneyManagement = data.MoneyManagement,
Ticker = data.Ticker,
ScenarioName = data.ScenarioName,
Timeframe = data.Timeframe,
IsForBacktest = false, // Always false when loading from backup
IsForWatchingOnly = data.IsForWatchingOnly,
BotTradingBalance = data.BotTradingBalance,
BotType = data.BotType,
CooldownPeriod = data.CooldownPeriod,
};
Signals = data.Signals;
Positions = data.Positions;
WalletBalances = data.WalletBalances;
// MoneyManagement = data.MoneyManagement; => loaded from database
Timeframe = data.Timeframe;
Ticker = data.Ticker;
ScenarioName = data.ScenarioName;
AccountName = data.AccountName;
IsForWatchingOnly = data.IsForWatchingOnly;
BotTradingBalance = data.BotTradingBalance;
PreloadSince = data.StartupTime;
Identifier = backup.Identifier;
User = backup.User;
// Restore the startup time if it was previously saved
if (data.StartupTime != DateTime.MinValue)
{
StartupTime = data.StartupTime;
}
Status = backup.LastStatus;
}
/// <summary>
@@ -949,7 +1037,8 @@ public class TradingBot : Bot, ITradingBot
}
// Create a fake signal for manual position opening
var signal = new Signal(Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date, TradingExchanges.GmxV2,
var signal = new Signal(Config.Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date,
TradingExchanges.GmxV2,
StrategyType.Stc, SignalType.Signal);
signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct
signal.User = Account.User; // Assign user
@@ -989,4 +1078,5 @@ public class TradingBotBackup
public MoneyManagement MoneyManagement { get; set; }
public DateTime StartupTime { get; set; }
public decimal BotTradingBalance { get; set; }
public decimal CooldownPeriod { get; set; }
}

View File

@@ -0,0 +1,20 @@
using Managing.Domain.MoneyManagements;
using static Managing.Common.Enums;
namespace Managing.Application.Bots;
public class TradingBotConfig
{
public string AccountName { get; set; }
public MoneyManagement MoneyManagement { get; set; }
public Ticker Ticker { get; set; }
public string ScenarioName { get; set; }
public Timeframe Timeframe { get; set; }
public bool IsForBacktest { get; set; }
public bool IsForWatchingOnly { get; set; }
public bool FlipPosition { get; set; }
public BotType BotType { get; set; }
public decimal BotTradingBalance { get; set; }
public decimal CooldownPeriod { get; set; } = 1;
public int MaxLossStreak { get; set; } = 0; // 0 means no limit
}

View File

@@ -3,13 +3,13 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Application.Bots;
using Managing.Common;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot
{
@@ -22,13 +22,14 @@ namespace Managing.Application.ManageBot
private readonly ILogger<TradingBot> _tradingBotLogger;
private readonly ITradingService _tradingService;
private readonly IMoneyManagementService _moneyManagementService;
private readonly IUserService _userService;
private ConcurrentDictionary<string, BotTaskWrapper> _botTasks =
new ConcurrentDictionary<string, BotTaskWrapper>();
public BotService(IBotRepository botRepository, IExchangeService exchangeService,
IMessengerService messengerService, IAccountService accountService, ILogger<TradingBot> tradingBotLogger,
ITradingService tradingService, IMoneyManagementService moneyManagementService)
ITradingService tradingService, IMoneyManagementService moneyManagementService, IUserService userService)
{
_botRepository = botRepository;
_exchangeService = exchangeService;
@@ -37,11 +38,7 @@ namespace Managing.Application.ManageBot
_tradingBotLogger = tradingBotLogger;
_tradingService = tradingService;
_moneyManagementService = moneyManagementService;
}
public async void SaveOrUpdateBotBackup(BotBackup botBackup)
{
await _botRepository.InsertBotAsync(botBackup);
_userService = userService;
}
public BotBackup GetBotBackup(string identifier)
@@ -49,12 +46,13 @@ namespace Managing.Application.ManageBot
return _botRepository.GetBots().FirstOrDefault(b => b.Identifier == identifier);
}
public void SaveOrUpdateBotBackup(User user, string identifier, Enums.BotType botType, string data)
public void SaveOrUpdateBotBackup(User user, string identifier, BotType botType, BotStatus status, string data)
{
var backup = GetBotBackup(identifier);
if (backup != null)
{
backup.LastStatus = status;
backup.Data = data;
_botRepository.UpdateBackupBot(backup);
}
@@ -62,6 +60,7 @@ namespace Managing.Application.ManageBot
{
var botBackup = new BotBackup
{
LastStatus = status,
User = user,
Identifier = identifier,
BotType = botType,
@@ -127,7 +126,7 @@ namespace Managing.Application.ManageBot
// null); // Assuming null is an acceptable parameter for workflow
// botTask = Task.Run(() => ((IBot)bot).Start());
// break;
case Enums.BotType.ScalpingBot:
case BotType.ScalpingBot:
var scalpingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
var scalpingMoneyManagement =
_moneyManagementService.GetMoneyMangement(scalpingBotData.MoneyManagement.Name).Result;
@@ -142,7 +141,7 @@ namespace Managing.Application.ManageBot
scalpingBotData.BotTradingBalance);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
break;
case Enums.BotType.FlippingBot:
case BotType.FlippingBot:
var flippingBotData = JsonConvert.DeserializeObject<TradingBotBackup>(backupBot.Data);
var flippingMoneyManagement =
_moneyManagementService.GetMoneyMangement(flippingBotData.MoneyManagement.Name).Result;
@@ -166,9 +165,12 @@ namespace Managing.Application.ManageBot
}
}
private static Action InitBot(ITradingBot bot, BotBackup backupBot)
private Action InitBot(ITradingBot bot, BotBackup backupBot)
{
bot.Start();
var user = _userService.GetUser(backupBot.User.Name);
backupBot.User = user;
bot.LoadBackup(backupBot);
return () => { };
}
@@ -178,9 +180,9 @@ namespace Managing.Application.ManageBot
return new SimpleBot(botName, _tradingBotLogger, workflow, this);
}
public async Task<string> StopBot(string botName)
public async Task<string> StopBot(string identifier)
{
if (_botTasks.TryGetValue(botName, out var botWrapper))
if (_botTasks.TryGetValue(identifier, out var botWrapper))
{
if (botWrapper.BotInstance is IBot bot)
{
@@ -190,12 +192,12 @@ namespace Managing.Application.ManageBot
}
}
return Enums.BotStatus.Down.ToString();
return BotStatus.Down.ToString();
}
public async Task<bool> DeleteBot(string botName)
public async Task<bool> DeleteBot(string identifier)
{
if (_botTasks.TryRemove(botName, out var botWrapper))
if (_botTasks.TryRemove(identifier, out var botWrapper))
{
try
{
@@ -205,7 +207,7 @@ namespace Managing.Application.ManageBot
bot.Stop()); // Assuming Stop is an asynchronous process wrapped in Task.Run for synchronous methods
}
await _botRepository.DeleteBotBackup(botName);
await _botRepository.DeleteBotBackup(identifier);
return true;
}
catch (Exception e)
@@ -218,9 +220,9 @@ namespace Managing.Application.ManageBot
return false;
}
public Task<string> RestartBot(string botName)
public Task<string> RestartBot(string identifier)
{
if (_botTasks.TryGetValue(botName, out var botWrapper))
if (_botTasks.TryGetValue(identifier, out var botWrapper))
{
if (botWrapper.BotInstance is IBot bot)
{
@@ -229,17 +231,17 @@ namespace Managing.Application.ManageBot
}
}
return Task.FromResult(Enums.BotStatus.Down.ToString());
return Task.FromResult(BotStatus.Down.ToString());
}
public void DeleteBotBackup(string backupBotName)
public void DeleteBotBackup(string identifier)
{
_botRepository.DeleteBotBackup(backupBotName);
_botRepository.DeleteBotBackup(identifier);
}
public void ToggleIsForWatchingOnly(string botName)
public void ToggleIsForWatchingOnly(string identifier)
{
if (_botTasks.TryGetValue(botName, out var botTaskWrapper) &&
if (_botTasks.TryGetValue(identifier, out var botTaskWrapper) &&
botTaskWrapper.BotInstance is ITradingBot tradingBot)
{
tradingBot.ToggleIsForWatchOnly().Wait();
@@ -247,89 +249,159 @@ namespace Managing.Application.ManageBot
}
public ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{
var config = new TradingBotConfig
{
AccountName = accountName,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario,
Timeframe = interval,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = initialTradingBalance,
BotType = BotType.ScalpingBot
};
return new ScalpingBot(
accountName,
moneyManagement,
name,
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
this,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly);
config);
}
public ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{
var config = new TradingBotConfig
{
AccountName = accountName,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario,
Timeframe = interval,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = initialTradingBalance,
BotType = BotType.ScalpingBot,
IsForBacktest = true
};
return new ScalpingBot(
accountName,
moneyManagement,
"BacktestBot",
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
this,
initialTradingBalance,
isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
config);
}
public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{
var config = new TradingBotConfig
{
AccountName = accountName,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario,
Timeframe = interval,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = initialTradingBalance,
BotType = BotType.FlippingBot
};
return new FlippingBot(
accountName,
moneyManagement,
name,
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
this,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly);
config);
}
public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{
var config = new TradingBotConfig
{
AccountName = accountName,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario,
Timeframe = interval,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = initialTradingBalance,
BotType = BotType.FlippingBot,
IsForBacktest = true
};
return new FlippingBot(
accountName,
moneyManagement,
"BacktestBot",
scenario,
_exchangeService,
ticker,
_tradingService,
_tradingBotLogger,
interval,
_tradingService,
_accountService,
_messengerService,
this,
initialTradingBalance,
isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
config);
}
public ITradingBot CreateScalpingBot(TradingBotConfig config)
{
return new ScalpingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
this,
config);
}
public ITradingBot CreateBacktestScalpingBot(TradingBotConfig config)
{
config.IsForBacktest = true;
return new ScalpingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
this,
config);
}
public ITradingBot CreateFlippingBot(TradingBotConfig config)
{
return new FlippingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
this,
config);
}
public ITradingBot CreateBacktestFlippingBot(TradingBotConfig config)
{
config.IsForBacktest = true;
return new FlippingBot(
_exchangeService,
_tradingBotLogger,
_tradingService,
_accountService,
_messengerService,
this,
config);
}
}
}

View File

@@ -1,44 +1,20 @@
using Managing.Domain.Users;
using Managing.Application.Bots;
using Managing.Domain.Users;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands
{
public class StartBotCommand : IRequest<string>
{
public string Name { get; }
public BotType BotType { get; }
public Ticker Ticker { get; internal set; }
public Timeframe Timeframe { get; internal set; }
public bool IsForWatchingOnly { get; internal set; }
public string Scenario { get; internal set; }
public string AccountName { get; internal set; }
public string MoneyManagementName { get; internal set; }
public User User { get; internal set; }
public decimal InitialTradingBalance { get; internal set; }
public TradingBotConfig Config { get; }
public User User { get; }
public StartBotCommand(BotType botType,
string name,
Ticker ticker,
string scenario,
Timeframe timeframe,
string accountName,
string moneyManagementName,
User user,
bool isForWatchingOnly = false,
decimal initialTradingBalance = 0)
public StartBotCommand(TradingBotConfig config, string name, User user)
{
BotType = botType;
Config = config;
Name = name;
Scenario = scenario;
Ticker = ticker;
Timeframe = timeframe;
IsForWatchingOnly = isForWatchingOnly;
AccountName = accountName;
MoneyManagementName = moneyManagementName;
User = user;
InitialTradingBalance = initialTradingBalance > 0 ? initialTradingBalance :
throw new ArgumentException("Initial trading balance must be greater than zero", nameof(initialTradingBalance));
}
}
}

View File

@@ -17,7 +17,7 @@ namespace Managing.Application.ManageBot
{
var allActiveBots = _botService.GetActiveBots();
var userBots = allActiveBots
.Where(bot => bot.User != null && bot.User.Name == request.UserName)
.Where(bot => bot.User != null && bot.User.AgentName == request.UserName)
.ToList();
return Task.FromResult(userBots);

View File

@@ -23,9 +23,8 @@ namespace Managing.Application.ManageBot
// 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);
bot.User.AgentName == request.AgentName &&
bot.Identifier == request.StrategyName);
return Task.FromResult(strategy);
}

View File

@@ -31,6 +31,13 @@ public class LoadBackupBotCommandHandler : IRequestHandler<LoadBackupBotCommand,
{
try
{
if (backupBot.LastStatus == BotStatus.Down)
{
_logger.LogInformation("Skipping backup bot {Identifier} as it is marked as Down.",
backupBot.Identifier);
continue;
}
var activeBot = _botService.GetActiveBots().FirstOrDefault(b => b.Identifier == backupBot.Identifier);
if (activeBot == null)

View File

@@ -29,38 +29,38 @@ namespace Managing.Application.ManageBot
{
BotStatus botStatus = BotStatus.Down;
var account = await _accountService.GetAccount(request.AccountName, true, true);
var account = await _accountService.GetAccount(request.Config.AccountName, true, true);
if (account == null)
{
throw new Exception($"Account {request.AccountName} not found");
throw new Exception($"Account {request.Config.AccountName} not found");
}
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
if (usdcBalance == null || usdcBalance.Value < request.InitialTradingBalance)
if (usdcBalance == null || usdcBalance.Value < request.Config.BotTradingBalance)
{
throw new Exception($"Account {request.AccountName} has no USDC balance or not enough balance");
throw new Exception($"Account {request.Config.AccountName} has no USDC balance or not enough balance");
}
var moneyManagement =
await _moneyManagementService.GetMoneyMangement(request.User, request.MoneyManagementName);
switch (request.BotType)
// Ensure cooldown period is set
if (request.Config.CooldownPeriod <= 0)
{
request.Config.CooldownPeriod = 1; // Default to 1 minute if not set
}
switch (request.Config.BotType)
{
case BotType.SimpleBot:
var bot = _botFactory.CreateSimpleBot(request.Name, null);
_botService.AddSimpleBotToCache(bot);
return bot.GetStatus();
case BotType.ScalpingBot:
var sBot = _botFactory.CreateScalpingBot(request.AccountName, moneyManagement, request.Name,
request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly,
request.InitialTradingBalance);
var sBot = _botFactory.CreateScalpingBot(request.Config);
_botService.AddTradingBotToCache(sBot);
return sBot.GetStatus();
case BotType.FlippingBot:
var fBot = _botFactory.CreateFlippingBot(request.AccountName, moneyManagement, request.Name,
request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly,
request.InitialTradingBalance);
var fBot = _botFactory.CreateFlippingBot(request.Config);
_botService.AddTradingBotToCache(fBot);
return fBot.GetStatus();
}

View File

@@ -17,7 +17,7 @@ namespace Managing.Application.ManageBot
{
_botService.ToggleIsForWatchingOnly(request.Name);
var bot = _botService.GetActiveBots().FirstOrDefault(b => b.Name == request.Name);
return Task.FromResult(bot?.IsForWatchingOnly.ToString());
return Task.FromResult(bot?.Config.IsForWatchingOnly.ToString());
}
}
}

View File

@@ -24,7 +24,7 @@ public class UserService : IUserService
"0x23AA99254cfaA2c374bE2bA5B55C68018cCdFCb3", // Local optiflex
"0x932167388dD9aad41149b3cA23eBD489E2E2DD78", // Embedded wallet
"0x66CB57Fe3f53cE57376421106dFDa2D39186cBd0", // Embedded wallet optiflex
"0x7baBf95621f22bEf2DB67E500D022Ca110722FaD", // DevCowchain
"0x7baBf95621f22bEf2DB67E500D022Ca110722FaD", // DevCowchain
"0xc8bC497534d0A43bAb2BBA9BA94d46D9Ddfaea6B" // DevCowchain2
];
@@ -47,7 +47,8 @@ public class UserService : IUserService
if (!message.Equals("KaigenTeamXCowchain"))
{
_logger.LogWarning($"Message {message} not starting with KaigenTeamXCowchain");
throw new Exception($"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}");
throw new Exception(
$"Message not good : {message} - Address : {address} - User : {name} - Signature : {signature}");
}
// if (!authorizedAddresses.Contains(recoveredAddress))
@@ -106,11 +107,19 @@ public class UserService : IUserService
{
account
};
// Update user with the new account
await _userRepository.UpdateUser(user);
}
return user;
}
public User GetUser(string name)
{
return _userRepository.GetUserByNameAsync(name).Result;
}
public async Task<User> GetUserByAddressAsync(string address)
{
var account = await _accountService.GetAccountByKey(address, true, false);
@@ -118,4 +127,18 @@ public class UserService : IUserService
user.Accounts = _accountService.GetAccountsByUser(user).ToList();
return user;
}
public async Task<User> UpdateAgentName(User user, string agentName)
{
// Check if agent name is already used
var existingUser = await _userRepository.GetUserByAgentNameAsync(agentName);
if (existingUser != null)
{
throw new Exception("Agent name already used");
}
user.AgentName = agentName;
await _userRepository.UpdateUser(user);
return user;
}
}

View File

@@ -115,6 +115,7 @@ public static class ApiBootstrap
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IWorkerRepository, WorkerRepository>();
// Cache
services.AddDistributedMemoryCache();
services.AddTransient<ICacheService, CacheService>();

View File

@@ -14,6 +14,7 @@ using Managing.Application.Scenarios;
using Managing.Application.Shared;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Application.Users;
using Managing.Application.Workers;
using Managing.Application.Workers.Abstractions;
using Managing.Domain.Trades;
@@ -52,6 +53,7 @@ public static class WorkersBootstrap
private static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddSingleton<IUserService, UserService>();
services.AddSingleton<ITradingService, TradingService>();
services.AddSingleton<IBotFactory, BotFactory>();
services.AddSingleton<IScenarioService, ScenarioService>();
@@ -102,6 +104,7 @@ public static class WorkersBootstrap
services.AddTransient<ITradingRepository, TradingRepository>();
services.AddTransient<IBacktestRepository, BacktestRepository>();
services.AddTransient<IBotRepository, BotRepository>();
services.AddTransient<IUserRepository, UserRepository>();
// Cache
services.AddDistributedMemoryCache();

View File

@@ -9,4 +9,5 @@ public class BotBackup
public string Identifier { get; set; }
public User User { get; set; }
public string Data { get; set; }
public BotStatus LastStatus { get; set; }
}

View File

@@ -1,7 +1,10 @@
namespace Managing.Domain.Bots
using Managing.Domain.Users;
namespace Managing.Domain.Bots
{
public interface IBot
{
User User { get; set; }
string Name { get; set; }
void Start();
void Stop();

View File

@@ -6,4 +6,5 @@ public class User
{
public string Name { get; set; }
public List<Account> Accounts { get; set; }
public string AgentName { get; set; }
}

View File

@@ -12,4 +12,5 @@ public class BotDto : Document
public BotType BotType { get; set; }
public string Identifier { get; set; }
public UserDto User { get; set; }
public BotStatus LastStatus { get; set; }
}

View File

@@ -7,4 +7,5 @@ namespace Managing.Infrastructure.Databases.MongoDb.Collections;
public class UserDto : Document
{
public string Name { get; set; }
public string AgentName { get; set; }
}

View File

@@ -516,6 +516,7 @@ public static class MongoMappers
return new User
{
Name = user.Name,
AgentName = user.AgentName
};
}
@@ -523,7 +524,8 @@ public static class MongoMappers
{
return new UserDto
{
Name = user.Name
Name = user.Name,
AgentName = user.AgentName
};
}
@@ -724,6 +726,7 @@ public static class MongoMappers
Identifier = bot.Identifier,
BotType = bot.BotType,
Data = bot.Data,
LastStatus = bot.LastStatus
};
}
@@ -736,7 +739,8 @@ public static class MongoMappers
User = Map(b.User),
Identifier = b.Identifier,
BotType = b.BotType,
Data = b.Data
Data = b.Data,
LastStatus = b.LastStatus
};
}

View File

@@ -15,6 +15,12 @@ public class UserRepository : IUserRepository
_userRepository = userRepository;
}
public async Task<User> GetUserByAgentNameAsync(string agentName)
{
var user = await _userRepository.FindOneAsync(u => u.AgentName == agentName);
return MongoMappers.Map(user);
}
public async Task<User> GetUserByNameAsync(string name)
{
var user = await _userRepository.FindOneAsync(u => u.Name == name);
@@ -31,7 +37,7 @@ public class UserRepository : IUserRepository
try
{
var dto = await _userRepository.FindOneAsync(u => u.Name == user.Name);
dto.Name = user.Name;
dto.AgentName = user.AgentName;
_userRepository.Update(dto);
}
catch (Exception e)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,703 @@
//----------------------
// <auto-generated>
// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
// </auto-generated>
//----------------------
/* tslint:disable */
/* eslint-disable */
// ReSharper disable InconsistentNaming
export interface Account {
name: string;
exchange: TradingExchanges;
type: AccountType;
key?: string | null;
secret?: string | null;
user?: User | null;
balances?: Balance[] | null;
isPrivyWallet?: boolean;
}
export enum TradingExchanges {
Binance = "Binance",
Kraken = "Kraken",
Ftx = "Ftx",
Evm = "Evm",
GmxV2 = "GmxV2",
}
export enum AccountType {
Cex = "Cex",
Trader = "Trader",
Watch = "Watch",
Auth = "Auth",
Privy = "Privy",
}
export interface User {
name?: string | null;
accounts?: Account[] | null;
agentName?: string | null;
}
export interface Balance {
tokenImage?: string | null;
tokenName?: string | null;
amount?: number;
price?: number;
value?: number;
tokenAdress?: string | null;
chain?: Chain | null;
}
export interface Chain {
id?: string | null;
rpcUrl?: string | null;
name?: string | null;
chainId?: number;
}
export interface Backtest {
id: string;
finalPnl: number;
winRate: number;
growthPercentage: number;
hodlPercentage: number;
ticker: Ticker;
scenario: string;
positions: Position[];
signals: Signal[];
timeframe: Timeframe;
botType: BotType;
accountName: string;
candles: Candle[];
startDate: Date;
endDate: Date;
statistics: PerformanceMetrics;
fees: number;
walletBalances: KeyValuePairOfDateTimeAndDecimal[];
optimizedMoneyManagement: MoneyManagement;
moneyManagement: MoneyManagement;
user: User;
strategiesValues: { [key in keyof typeof StrategyType]?: StrategiesResultBase; };
score: number;
}
export enum Ticker {
AAVE = "AAVE",
ADA = "ADA",
APE = "APE",
ALGO = "ALGO",
ARB = "ARB",
ATOM = "ATOM",
AVAX = "AVAX",
BNB = "BNB",
BTC = "BTC",
BAL = "BAL",
CHZ = "CHZ",
COMP = "COMP",
CRO = "CRO",
CRV = "CRV",
DOGE = "DOGE",
DOT = "DOT",
DYDX = "DYDX",
ENS = "ENS",
ETC = "ETC",
ETH = "ETH",
FIL = "FIL",
FLM = "FLM",
FTM = "FTM",
GALA = "GALA",
GMX = "GMX",
GRT = "GRT",
IMX = "IMX",
JASMY = "JASMY",
KSM = "KSM",
LDO = "LDO",
LINK = "LINK",
LRC = "LRC",
LTC = "LTC",
MANA = "MANA",
MATIC = "MATIC",
MKR = "MKR",
NEAR = "NEAR",
OP = "OP",
PEPE = "PEPE",
QTUM = "QTUM",
REN = "REN",
ROSE = "ROSE",
RSR = "RSR",
RUNE = "RUNE",
SAND = "SAND",
SOL = "SOL",
SRM = "SRM",
SUSHI = "SUSHI",
THETA = "THETA",
UNI = "UNI",
USDC = "USDC",
USDT = "USDT",
WIF = "WIF",
XMR = "XMR",
XRP = "XRP",
XTZ = "XTZ",
SHIB = "SHIB",
STX = "STX",
ORDI = "ORDI",
APT = "APT",
BOME = "BOME",
MEME = "MEME",
FLOKI = "FLOKI",
MEW = "MEW",
TAO = "TAO",
BONK = "BONK",
WLD = "WLD",
TBTC = "tBTC",
WBTC_b = "WBTC_b",
EIGEN = "EIGEN",
SUI = "SUI",
SEI = "SEI",
USDC_e = "USDC_e",
DAI = "DAI",
TIA = "TIA",
TRX = "TRX",
TON = "TON",
PENDLE = "PENDLE",
WstETH = "wstETH",
USDe = "USDe",
SATS = "SATS",
POL = "POL",
XLM = "XLM",
BCH = "BCH",
ICP = "ICP",
RENDER = "RENDER",
INJ = "INJ",
TRUMP = "TRUMP",
MELANIA = "MELANIA",
ENA = "ENA",
FARTCOIN = "FARTCOIN",
AI16Z = "AI16Z",
ANIME = "ANIME",
BERA = "BERA",
VIRTUAL = "VIRTUAL",
PENGU = "PENGU",
ONDO = "ONDO",
FET = "FET",
AIXBT = "AIXBT",
CAKE = "CAKE",
S = "S",
JUP = "JUP",
HYPE = "HYPE",
OM = "OM",
DOLO = "DOLO",
Unknown = "Unknown",
}
export interface Position {
accountName: string;
date: Date;
originDirection: TradeDirection;
ticker: Ticker;
moneyManagement: MoneyManagement;
open: Trade;
stopLoss: Trade;
takeProfit1: Trade;
takeProfit2?: Trade | null;
profitAndLoss?: ProfitAndLoss | null;
status: PositionStatus;
signalIdentifier?: string | null;
identifier: string;
initiator: PositionInitiator;
user: User;
}
export enum TradeDirection {
None = "None",
Short = "Short",
Long = "Long",
}
export interface MoneyManagement {
name: string;
timeframe: Timeframe;
stopLoss: number;
takeProfit: number;
leverage: number;
user?: User | null;
}
export enum Timeframe {
FiveMinutes = "FiveMinutes",
FifteenMinutes = "FifteenMinutes",
ThirtyMinutes = "ThirtyMinutes",
OneHour = "OneHour",
FourHour = "FourHour",
OneDay = "OneDay",
OneMinute = "OneMinute",
}
export interface Trade {
fee?: number;
date: Date;
direction: TradeDirection;
status: TradeStatus;
tradeType: TradeType;
ticker: Ticker;
quantity: number;
price: number;
leverage?: number;
exchangeOrderId: string;
message?: string | null;
}
export enum TradeStatus {
PendingOpen = "PendingOpen",
Requested = "Requested",
Cancelled = "Cancelled",
Filled = "Filled",
}
export enum TradeType {
Limit = "Limit",
Market = "Market",
StopMarket = "StopMarket",
StopLimit = "StopLimit",
StopLoss = "StopLoss",
TakeProfit = "TakeProfit",
StopLossProfit = "StopLossProfit",
StopLossProfitLimit = "StopLossProfitLimit",
StopLossLimit = "StopLossLimit",
TakeProfitLimit = "TakeProfitLimit",
TrailingStop = "TrailingStop",
TrailingStopLimit = "TrailingStopLimit",
StopLossAndLimit = "StopLossAndLimit",
SettlePosition = "SettlePosition",
}
export interface ProfitAndLoss {
realized?: number;
net?: number;
averageOpenPrice?: number;
}
export enum PositionStatus {
New = "New",
Canceled = "Canceled",
Rejected = "Rejected",
Updating = "Updating",
PartiallyFilled = "PartiallyFilled",
Filled = "Filled",
Flipped = "Flipped",
Finished = "Finished",
}
export enum PositionInitiator {
PaperTrading = "PaperTrading",
Bot = "Bot",
User = "User",
CopyTrading = "CopyTrading",
}
export interface ValueObject {
}
export interface Signal extends ValueObject {
status: SignalStatus;
direction: TradeDirection;
confidence: Confidence;
timeframe: Timeframe;
date: Date;
candle: Candle;
identifier: string;
ticker: Ticker;
exchange: TradingExchanges;
strategyType: StrategyType;
signalType: SignalType;
user?: User | null;
}
export enum SignalStatus {
WaitingForPosition = "WaitingForPosition",
PositionOpen = "PositionOpen",
Expired = "Expired",
}
export enum Confidence {
Low = "Low",
Medium = "Medium",
High = "High",
None = "None",
}
export interface Candle {
exchange: TradingExchanges;
ticker: string;
openTime: Date;
date: Date;
open: number;
close: number;
volume?: number;
high: number;
low: number;
baseVolume?: number;
quoteVolume?: number;
tradeCount?: number;
takerBuyBaseVolume?: number;
takerBuyQuoteVolume?: number;
timeframe: Timeframe;
}
export enum StrategyType {
RsiDivergence = "RsiDivergence",
RsiDivergenceConfirm = "RsiDivergenceConfirm",
MacdCross = "MacdCross",
EmaCross = "EmaCross",
ThreeWhiteSoldiers = "ThreeWhiteSoldiers",
SuperTrend = "SuperTrend",
ChandelierExit = "ChandelierExit",
EmaTrend = "EmaTrend",
Composite = "Composite",
StochRsiTrend = "StochRsiTrend",
Stc = "Stc",
StDev = "StDev",
LaggingStc = "LaggingStc",
SuperTrendCrossEma = "SuperTrendCrossEma",
}
export enum SignalType {
Signal = "Signal",
Trend = "Trend",
Context = "Context",
}
export enum BotType {
SimpleBot = "SimpleBot",
ScalpingBot = "ScalpingBot",
FlippingBot = "FlippingBot",
}
export interface PerformanceMetrics {
count?: number;
sharpeRatio?: number;
maxDrawdown?: number;
maxDrawdownPc?: number;
maxDrawdownRecoveryTime?: string;
winningTrades?: number;
loosingTrades?: number;
totalPnL?: number;
}
export interface KeyValuePairOfDateTimeAndDecimal {
key?: Date;
value?: number;
}
export interface StrategiesResultBase {
ema?: EmaResult[] | null;
macd?: MacdResult[] | null;
rsi?: RsiResult[] | null;
stoch?: StochResult[] | null;
stochRsi?: StochRsiResult[] | null;
bollingerBands?: BollingerBandsResult[] | null;
chandelierShort?: ChandelierResult[] | null;
stc?: StcResult[] | null;
stdDev?: StdDevResult[] | null;
superTrend?: SuperTrendResult[] | null;
chandelierLong?: ChandelierResult[] | null;
}
export interface ResultBase {
date?: Date;
}
export interface EmaResult extends ResultBase {
ema?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface MacdResult extends ResultBase {
macd?: number | null;
signal?: number | null;
histogram?: number | null;
fastEma?: number | null;
slowEma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface RsiResult extends ResultBase {
rsi?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
/** Stochastic indicator results includes aliases for those who prefer the simpler K,D,J outputs. See documentation for more information. */
export interface StochResult extends ResultBase {
oscillator?: number | null;
signal?: number | null;
percentJ?: number | null;
k?: number | null;
d?: number | null;
j?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StochRsiResult extends ResultBase {
stochRsi?: number | null;
signal?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface BollingerBandsResult extends ResultBase {
sma?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
percentB?: number | null;
zScore?: number | null;
width?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface ChandelierResult extends ResultBase {
chandelierExit?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StcResult extends ResultBase {
stc?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface StdDevResult extends ResultBase {
stdDev?: number | null;
mean?: number | null;
zScore?: number | null;
stdDevSma?: number | null;
"skender.Stock.Indicators.IReusableResult.Value"?: number | null;
}
export interface SuperTrendResult extends ResultBase {
superTrend?: number | null;
upperBand?: number | null;
lowerBand?: number | null;
}
export interface StartBotRequest {
botType?: BotType;
botName?: string | null;
ticker?: Ticker;
scenario?: string | null;
timeframe?: Timeframe;
accountName?: string | null;
moneyManagementName?: string | null;
isForWatchOnly?: boolean;
initialTradingBalance?: number;
}
export interface TradingBot {
name: string;
status: string;
signals: Signal[];
positions: Position[];
candles: Candle[];
winRate: number;
profitAndLoss: number;
timeframe: Timeframe;
ticker: Ticker;
scenario: string;
isForWatchingOnly: boolean;
botType: BotType;
accountName: string;
moneyManagement: MoneyManagement;
}
export interface OpenPositionManuallyRequest {
botName?: string | null;
direction?: TradeDirection;
}
export interface ClosePositionRequest {
botName?: string | null;
positionId?: string | null;
}
export interface SpotlightOverview {
spotlights: Spotlight[];
dateTime: Date;
identifier?: string;
scenarioCount?: number;
}
export interface Spotlight {
scenario: Scenario;
tickerSignals: TickerSignal[];
}
export interface Scenario {
name?: string | null;
strategies?: Strategy[] | null;
loopbackPeriod?: number | null;
user?: User | null;
}
export interface Strategy {
name?: string | null;
type?: StrategyType;
signalType?: SignalType;
minimumHistory?: number;
period?: number | null;
fastPeriods?: number | null;
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
user?: User | null;
}
export interface TickerSignal {
ticker: Ticker;
fiveMinutes: Signal[];
fifteenMinutes: Signal[];
oneHour: Signal[];
fourHour: Signal[];
oneDay: Signal[];
}
export interface StrategiesStatisticsViewModel {
totalStrategiesRunning?: number;
changeInLast24Hours?: number;
}
export interface TopStrategiesViewModel {
topStrategies?: StrategyPerformance[] | null;
}
export interface StrategyPerformance {
strategyName?: string | null;
pnL?: number;
}
export interface UserStrategyDetailsViewModel {
name?: string | null;
strategyName?: string | null;
state?: string | null;
pnL?: number;
roiPercentage?: number;
roiLast24H?: number;
runtime?: Date;
winRate?: number;
totalVolumeTraded?: number;
volumeLast24H?: number;
wins?: number;
losses?: number;
positions?: Position[] | null;
}
export interface PlatformSummaryViewModel {
totalAgents?: number;
totalActiveStrategies?: number;
totalPlatformPnL?: number;
totalPlatformVolume?: number;
totalPlatformVolumeLast24h?: number;
agentSummaries?: AgentSummaryViewModel[] | null;
timeFilter?: string | null;
}
export interface AgentSummaryViewModel {
username?: string | null;
totalPnL?: number;
pnLLast24h?: number;
totalROI?: number;
roiLast24h?: number;
wins?: number;
losses?: number;
averageWinRate?: number;
activeStrategiesCount?: number;
totalVolume?: number;
volumeLast24h?: number;
}
export enum RiskLevel {
Low = "Low",
Medium = "Medium",
High = "High",
Adaptive = "Adaptive",
}
export interface LoginRequest {
name: string;
address: string;
signature: string;
message: string;
}
export interface Workflow {
name: string;
usage: WorkflowUsage;
flows: IFlow[];
description: string;
}
export enum WorkflowUsage {
Trading = "Trading",
Task = "Task",
}
export interface IFlow {
id: string;
name: string;
type: FlowType;
description: string;
acceptedInputs: FlowOutput[];
children?: IFlow[] | null;
parameters: FlowParameter[];
parentId?: string;
output?: string | null;
outputTypes: FlowOutput[];
}
export enum FlowType {
RsiDivergence = "RsiDivergence",
FeedTicker = "FeedTicker",
OpenPosition = "OpenPosition",
}
export enum FlowOutput {
Signal = "Signal",
Candles = "Candles",
Position = "Position",
MoneyManagement = "MoneyManagement",
}
export interface FlowParameter {
value?: any | null;
name?: string | null;
}
export interface SyntheticWorkflow {
name: string;
usage: WorkflowUsage;
description: string;
flows: SyntheticFlow[];
}
export interface SyntheticFlow {
id: string;
parentId?: string | null;
type: FlowType;
parameters: SyntheticFlowParameter[];
}
export interface SyntheticFlowParameter {
value: string;
name: string;
}
export interface FileResponse {
data: Blob;
status: number;
fileName?: string;
headers?: { [name: string]: any };
}

View File

@@ -7,13 +7,15 @@ import Toast from '../Toast/Toast'
interface TradesModalProps {
showModal: boolean
botName: string | null
agentName: string | null
strategyName: string | null
onClose: () => void
}
const TradesModal: React.FC<TradesModalProps> = ({
showModal,
botName,
strategyName,
agentName,
onClose,
}) => {
const { apiUrl } = useApiUrlStore()
@@ -22,19 +24,19 @@ const TradesModal: React.FC<TradesModalProps> = ({
const [closingPosition, setClosingPosition] = useState<string | null>(null)
useEffect(() => {
if (showModal && botName) {
if (showModal && strategyName && agentName) {
fetchStrategyData()
}
}, [showModal, botName])
}, [showModal, strategyName, agentName])
const fetchStrategyData = async () => {
if (!botName) return
if (!strategyName || !agentName) return
setLoading(true)
const client = new DataClient({}, apiUrl)
try {
const data = await client.data_GetUserStrategy("Oda", botName)
const data = await client.data_GetUserStrategy(agentName, strategyName)
setStrategyData(data)
} catch (error) {
console.error('Error fetching strategy data:', error)
@@ -46,7 +48,7 @@ const TradesModal: React.FC<TradesModalProps> = ({
}
const closePosition = async (position: Position) => {
if (!botName) return
if (!agentName) return
try {
setClosingPosition(position.identifier)
@@ -56,7 +58,7 @@ const TradesModal: React.FC<TradesModalProps> = ({
// Use BotClient instead of fetch
const botClient = new BotClient({}, apiUrl)
const request: ClosePositionRequest = {
botName: botName,
identifier: agentName,
positionId: position.identifier
}
@@ -78,7 +80,7 @@ const TradesModal: React.FC<TradesModalProps> = ({
<Modal
showModal={showModal}
onClose={onClose}
titleHeader={`Trades for ${botName}`}
titleHeader={`Trades for ${strategyName}`}
>
<div className="mt-4">
{loading ? (

View File

@@ -42,7 +42,9 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
const { register, handleSubmit, setValue } = useForm<IBacktestsFormInput>({
defaultValues: {
startDate: defaultStartDateString,
endDate: defaultEndDateString
endDate: defaultEndDateString,
cooldownPeriod: 1, // Default cooldown period of 1 minute
maxLossStreak: 0 // Default max loss streak of 0 (no limit)
}
});
const [selectedAccount, setSelectedAccount] = useState<string>('')
@@ -110,6 +112,8 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
new Date(form.startDate), // startDate
new Date(form.endDate), // endDate
form.save,
form.cooldownPeriod, // Use the cooldown period from the form
form.maxLossStreak, // Add the max loss streak parameter
customMoneyManagement
)
.then((backtest: Backtest) => {
@@ -362,6 +366,28 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
/>
</FormInput>
<FormInput label="Cooldown Period (candles)" htmlFor="cooldownPeriod">
<input
type="number"
className="input input-bordered w-full"
min="1"
step="1"
{...register('cooldownPeriod', { valueAsNumber: true })}
/>
</FormInput>
</div>
<div className="grid grid-cols-2 gap-4">
<FormInput label="Max Loss Streak" htmlFor="maxLossStreak">
<input
type="number"
className="input input-bordered w-full"
min="0"
step="1"
{...register('maxLossStreak', { valueAsNumber: true })}
/>
</FormInput>
<FormInput label="Save" htmlFor="save">
<input
type="checkbox"

View File

@@ -337,7 +337,7 @@ export class BacktestClient extends AuthorizedApiBase {
return Promise.resolve<Backtest>(null as any);
}
backtest_Run(accountName: string | null | undefined, botType: BotType | undefined, ticker: Ticker | undefined, scenarioName: string | null | undefined, timeframe: Timeframe | undefined, watchOnly: boolean | undefined, balance: number | undefined, moneyManagementName: string | null | undefined, startDate: Date | undefined, endDate: Date | undefined, save: boolean | undefined, moneyManagement: MoneyManagement | undefined): Promise<Backtest> {
backtest_Run(accountName: string | null | undefined, botType: BotType | undefined, ticker: Ticker | undefined, scenarioName: string | null | undefined, timeframe: Timeframe | undefined, watchOnly: boolean | undefined, balance: number | undefined, moneyManagementName: string | null | undefined, startDate: Date | undefined, endDate: Date | undefined, save: boolean | undefined, cooldownPeriod: number | undefined, maxLossStreak: number | undefined, moneyManagement: MoneyManagement | undefined): Promise<Backtest> {
let url_ = this.baseUrl + "/Backtest/Run?";
if (accountName !== undefined && accountName !== null)
url_ += "accountName=" + encodeURIComponent("" + accountName) + "&";
@@ -377,6 +377,14 @@ export class BacktestClient extends AuthorizedApiBase {
throw new Error("The parameter 'save' cannot be null.");
else if (save !== undefined)
url_ += "save=" + encodeURIComponent("" + save) + "&";
if (cooldownPeriod === null)
throw new Error("The parameter 'cooldownPeriod' cannot be null.");
else if (cooldownPeriod !== undefined)
url_ += "cooldownPeriod=" + encodeURIComponent("" + cooldownPeriod) + "&";
if (maxLossStreak === null)
throw new Error("The parameter 'maxLossStreak' cannot be null.");
else if (maxLossStreak !== undefined)
url_ += "maxLossStreak=" + encodeURIComponent("" + maxLossStreak) + "&";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(moneyManagement);
@@ -465,14 +473,14 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_Stop(botType: BotType | undefined, botName: string | null | undefined): Promise<string> {
bot_Stop(botType: BotType | undefined, identifier: string | null | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/Stop?";
if (botType === null)
throw new Error("The parameter 'botType' cannot be null.");
else if (botType !== undefined)
url_ += "botType=" + encodeURIComponent("" + botType) + "&";
if (botName !== undefined && botName !== null)
url_ += "botName=" + encodeURIComponent("" + botName) + "&";
if (identifier !== undefined && identifier !== null)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@@ -506,10 +514,10 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_Delete(botName: string | null | undefined): Promise<boolean> {
bot_Delete(identifier: string | null | undefined): Promise<boolean> {
let url_ = this.baseUrl + "/Bot/Delete?";
if (botName !== undefined && botName !== null)
url_ += "botName=" + encodeURIComponent("" + botName) + "&";
if (identifier !== undefined && identifier !== null)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@@ -578,14 +586,14 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_Restart(botType: BotType | undefined, botName: string | null | undefined): Promise<string> {
bot_Restart(botType: BotType | undefined, identifier: string | null | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/Restart?";
if (botType === null)
throw new Error("The parameter 'botType' cannot be null.");
else if (botType !== undefined)
url_ += "botType=" + encodeURIComponent("" + botType) + "&";
if (botName !== undefined && botName !== null)
url_ += "botName=" + encodeURIComponent("" + botName) + "&";
if (identifier !== undefined && identifier !== null)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@@ -654,10 +662,10 @@ export class BotClient extends AuthorizedApiBase {
return Promise.resolve<string>(null as any);
}
bot_ToggleIsForWatching(botName: string | null | undefined): Promise<string> {
bot_ToggleIsForWatching(identifier: string | null | undefined): Promise<string> {
let url_ = this.baseUrl + "/Bot/ToggleIsForWatching?";
if (botName !== undefined && botName !== null)
url_ += "botName=" + encodeURIComponent("" + botName) + "&";
if (identifier !== undefined && identifier !== null)
url_ += "identifier=" + encodeURIComponent("" + identifier) + "&";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
@@ -816,7 +824,7 @@ export class DataClient extends AuthorizedApiBase {
this.baseUrl = baseUrl ?? "http://localhost:5000";
}
data_GetTickers(timeframe: Timeframe | undefined): Promise<Ticker[]> {
data_GetTickers(timeframe: Timeframe | undefined): Promise<TickerInfos[]> {
let url_ = this.baseUrl + "/Data/GetTickers?";
if (timeframe === null)
throw new Error("The parameter 'timeframe' cannot be null.");
@@ -838,13 +846,13 @@ export class DataClient extends AuthorizedApiBase {
});
}
protected processData_GetTickers(response: Response): Promise<Ticker[]> {
protected processData_GetTickers(response: Response): Promise<TickerInfos[]> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Ticker[];
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as TickerInfos[];
return result200;
});
} else if (status !== 200 && status !== 204) {
@@ -852,7 +860,7 @@ export class DataClient extends AuthorizedApiBase {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<Ticker[]>(null as any);
return Promise.resolve<TickerInfos[]>(null as any);
}
data_GetSpotlight(): Promise<SpotlightOverview> {
@@ -2202,6 +2210,80 @@ export class UserClient extends AuthorizedApiBase {
}
return Promise.resolve<string>(null as any);
}
user_GetCurrentUser(): Promise<User> {
let url_ = this.baseUrl + "/User";
url_ = url_.replace(/[?&]$/, "");
let options_: RequestInit = {
method: "GET",
headers: {
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processUser_GetCurrentUser(_response);
});
}
protected processUser_GetCurrentUser(response: Response): Promise<User> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as User;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<User>(null as any);
}
user_UpdateAgentName(agentName: string): Promise<User> {
let url_ = this.baseUrl + "/User/agent-name";
url_ = url_.replace(/[?&]$/, "");
const content_ = JSON.stringify(agentName);
let options_: RequestInit = {
body: content_,
method: "PUT",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
}
};
return this.transformOptions(options_).then(transformedOptions_ => {
return this.http.fetch(url_, transformedOptions_);
}).then((_response: Response) => {
return this.processUser_UpdateAgentName(_response);
});
}
protected processUser_UpdateAgentName(response: Response): Promise<User> {
const status = response.status;
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
if (status === 200) {
return response.text().then((_responseText) => {
let result200: any = null;
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as User;
return result200;
});
} else if (status !== 200 && status !== 204) {
return response.text().then((_responseText) => {
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
});
}
return Promise.resolve<User>(null as any);
}
}
export class WorkflowClient extends AuthorizedApiBase {
@@ -2397,6 +2479,7 @@ export enum AccountType {
export interface User {
name?: string | null;
accounts?: Account[] | null;
agentName?: string | null;
}
export interface Balance {
@@ -2839,15 +2922,15 @@ export interface SuperTrendResult extends ResultBase {
}
export interface StartBotRequest {
botType: BotType;
botName: string;
ticker: Ticker;
timeframe: Timeframe;
isForWatchOnly: boolean;
scenario: string;
accountName: string;
moneyManagementName: string;
initialTradingBalance: number;
botType?: BotType;
identifier?: string | null;
ticker?: Ticker;
scenario?: string | null;
timeframe?: Timeframe;
accountName?: string | null;
moneyManagementName?: string | null;
isForWatchOnly?: boolean;
initialTradingBalance?: number;
}
export interface TradingBot {
@@ -2865,18 +2948,25 @@ export interface TradingBot {
botType: BotType;
accountName: string;
moneyManagement: MoneyManagement;
identifier: string;
agentName: string;
}
export interface OpenPositionManuallyRequest {
botName?: string | null;
identifier?: string | null;
direction?: TradeDirection;
}
export interface ClosePositionRequest {
botName?: string | null;
identifier?: string | null;
positionId?: string | null;
}
export interface TickerInfos {
ticker?: Ticker;
imageUrl?: string | null;
}
export interface SpotlightOverview {
spotlights: Spotlight[];
dateTime: Date;
@@ -2962,7 +3052,7 @@ export interface PlatformSummaryViewModel {
}
export interface AgentSummaryViewModel {
username?: string | null;
agentName?: string | null;
totalPnL?: number;
pnLLast24h?: number;
totalROI?: number;

View File

@@ -111,6 +111,8 @@ export type IBacktestsFormInput = {
loop: number
startDate: string
endDate: string
cooldownPeriod: number
maxLossStreak: number
}
export type IBacktestCards = {

View File

@@ -1,8 +1,8 @@
import React, { useState } from 'react'
import React, {useState} from 'react'
import 'react-toastify/dist/ReactToastify.css'
import { Tabs } from '../../components/mollecules'
import type { TabsType } from '../../global/type'
import {Tabs} from '../../components/mollecules'
import type {TabsType} from '../../global/type'
import BacktestLoop from './backtestLoop'
import BacktestPlayground from './backtestPlayground'
@@ -12,14 +12,14 @@ import BacktestUpload from './backtestUpload'
// Tabs Array
const tabs: TabsType = [
{
Component: BacktestScanner,
Component: BacktestPlayground,
index: 1,
label: 'Scanner',
label: 'Playground',
},
{
Component: BacktestPlayground,
Component: BacktestScanner,
index: 2,
label: 'Playground',
label: 'Scanner',
},
{
Component: BacktestLoop,

View File

@@ -37,13 +37,13 @@ const BotList: React.FC<IBotList> = ({ list }) => {
const [showManualPositionModal, setShowManualPositionModal] = useState(false)
const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null)
const [showTradesModal, setShowTradesModal] = useState(false)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<string | null>(null)
const [selectedBotForTrades, setSelectedBotForTrades] = useState<{ identifier: string; agentName: string } | null>(null)
function getIsForWatchingBadge(isForWatchingOnly: boolean, name: string) {
function getIsForWatchingBadge(isForWatchingOnly: boolean, identifier: string) {
const classes =
baseBadgeClass() + (isForWatchingOnly ? ' bg-accent' : ' bg-primary')
return (
<button className={classes} onClick={() => toggleIsForWatchingOnly(name)}>
<button className={classes} onClick={() => toggleIsForWatchingOnly(identifier)}>
{isForWatchingOnly ? (
<p className="text-accent-content flex">
<EyeIcon width={12}></EyeIcon>
@@ -59,16 +59,16 @@ const BotList: React.FC<IBotList> = ({ list }) => {
)
}
function toggleIsForWatchingOnly(name: string) {
function toggleIsForWatchingOnly(identifier: string) {
const t = new Toast('Switch watch only')
client.bot_ToggleIsForWatching(name).then(() => {
client.bot_ToggleIsForWatching(identifier).then(() => {
t.update('success', 'Bot updated')
})
}
function getDeleteBadge(name: string) {
function getDeleteBadge(identifier: string) {
const classes = baseBadgeClass() + 'bg-error'
return (
<button className={classes} onClick={() => deleteBot(name)}>
<button className={classes} onClick={() => deleteBot(identifier)}>
<p className="text-primary-content flex">
<TrashIcon width={15}></TrashIcon>
</p>
@@ -77,7 +77,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
}
function getToggleBotStatusBadge(
status: string,
name: string,
identifier: string,
botType: BotType
) {
const classes =
@@ -85,7 +85,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
return (
<button
className={classes}
onClick={() => toggleBotStatus(status, name, botType)}
onClick={() => toggleBotStatus(status, identifier, botType)}
>
{status == 'Up' ? (
<p className="text-accent-content flex">
@@ -118,10 +118,10 @@ const BotList: React.FC<IBotList> = ({ list }) => {
)
}
function getManualPositionBadge(botName: string) {
function getManualPositionBadge(botIdentifier: string) {
const classes = baseBadgeClass() + ' bg-info'
return (
<button className={classes} onClick={() => openManualPositionModal(botName)}>
<button className={classes} onClick={() => openManualPositionModal(botIdentifier)}>
<p className="text-primary-content flex">
<PlusCircleIcon width={15}></PlusCircleIcon>
</p>
@@ -129,10 +129,10 @@ const BotList: React.FC<IBotList> = ({ list }) => {
)
}
function getTradesBadge(botName: string) {
function getTradesBadge(botIdentifier: string, agentName: string) {
const classes = baseBadgeClass() + ' bg-secondary'
return (
<button className={classes} onClick={() => openTradesModal(botName)}>
<button className={classes} onClick={() => openTradesModal(botIdentifier, agentName)}>
<p className="text-primary-content flex">
<ChartBarIcon width={15}></ChartBarIcon>
</p>
@@ -140,23 +140,23 @@ const BotList: React.FC<IBotList> = ({ list }) => {
)
}
function openManualPositionModal(botName: string) {
setSelectedBotForManualPosition(botName)
function openManualPositionModal(botIdentifier: string) {
setSelectedBotForManualPosition(botIdentifier)
setShowManualPositionModal(true)
}
function openTradesModal(botName: string) {
setSelectedBotForTrades(botName)
function openTradesModal(botIdentifier: string, agentName: string) {
setSelectedBotForTrades({ identifier: botIdentifier, agentName })
setShowTradesModal(true)
}
function toggleBotStatus(status: string, name: string, botType: BotType) {
function toggleBotStatus(status: string, identifier: string, botType: BotType) {
const isUp = status == 'Up'
const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot')
if (status == 'Up') {
client
.bot_Stop(botType, name)
.bot_Stop(botType, identifier)
.then(() => {
t.update('success', 'Bot stopped')
})
@@ -165,7 +165,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
})
} else if (status == 'Down') {
client
.bot_Restart(botType, name)
.bot_Restart(botType, identifier)
.then(() => {
t.update('success', 'Bot up')
})
@@ -175,11 +175,11 @@ const BotList: React.FC<IBotList> = ({ list }) => {
}
}
function deleteBot(name: string) {
function deleteBot(identifier: string) {
const t = new Toast('Deleting bot')
client
.bot_Delete(name)
.bot_Delete(identifier)
.then(() => {
t.update('success', 'Bot deleted')
})
@@ -211,10 +211,10 @@ const BotList: React.FC<IBotList> = ({ list }) => {
<h2 className="card-title text-sm">
{bot.ticker}
{getMoneyManagementBadge(bot.moneyManagement)}
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.name)}
{getToggleBotStatusBadge(bot.status, bot.name, bot.botType)}
{getManualPositionBadge(bot.name)}
{getDeleteBadge(bot.name)}
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.identifier)}
{getToggleBotStatusBadge(bot.status, bot.identifier, bot.botType)}
{getManualPositionBadge(bot.identifier)}
{getDeleteBadge(bot.identifier)}
</h2>
@@ -256,7 +256,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
<div className={baseBadgeClass(true)}>
PNL {bot.profitAndLoss.toFixed(2).toString()} $
</div>
{getTradesBadge(bot.name)}
{getTradesBadge(bot.identifier, bot.agentName)}
</div>
</div>
</div>
@@ -280,7 +280,8 @@ const BotList: React.FC<IBotList> = ({ list }) => {
/>
<TradesModal
showModal={showTradesModal}
botName={selectedBotForTrades}
strategyName={selectedBotForTrades?.identifier ?? null}
agentName={selectedBotForTrades?.agentName ?? null}
onClose={() => {
setShowTradesModal(false)
setSelectedBotForTrades(null)

View File

@@ -0,0 +1,101 @@
import {useState} from 'react'
import {useForm} from 'react-hook-form'
import {useQuery, useQueryClient} from '@tanstack/react-query'
import {UserClient} from '../../generated/ManagingApi'
import Modal from '../../components/mollecules/Modal/Modal'
import useApiUrlStore from '../../app/store/apiStore'
import {Toast} from '../../components/mollecules'
type UpdateAgentNameForm = {
agentName: string
}
function UserInfoSettings() {
const [showUpdateModal, setShowUpdateModal] = useState(false)
const queryClient = useQueryClient()
const { apiUrl } = useApiUrlStore()
const api = new UserClient({}, apiUrl)
const { data: user } = useQuery({
queryKey: ['user'],
queryFn: () => api.user_GetCurrentUser(),
})
const {
register,
handleSubmit,
formState: { errors },
} = useForm<UpdateAgentNameForm>()
const onSubmit = async (data: UpdateAgentNameForm) => {
const toast = new Toast('Updating agent name')
try {
await api.user_UpdateAgentName(data.agentName)
queryClient.invalidateQueries({ queryKey: ['user'] })
setShowUpdateModal(false)
toast.update('success', 'Agent name updated successfully')
} catch (error) {
console.error('Error updating agent name:', error)
toast.update('error', 'Failed to update agent name')
}
}
return (
<div className="container mx-auto p-4">
<div className="bg-base-200 rounded-lg p-6 shadow-lg">
<h2 className="text-2xl font-bold mb-4">User Information</h2>
<div className="space-y-4">
<div>
<label className="font-semibold">Name:</label>
<p>{user?.name}</p>
</div>
<div>
<label className="font-semibold">Agent Name:</label>
<p>{user?.agentName || 'Not set'}</p>
<button
className="btn btn-primary mt-2"
onClick={() => setShowUpdateModal(true)}
>
Update Agent Name
</button>
</div>
</div>
</div>
<Modal
showModal={showUpdateModal}
onClose={() => setShowUpdateModal(false)}
onSubmit={handleSubmit(onSubmit)}
titleHeader="Update Agent Name"
>
<div className="form-control w-full">
<label className="label">
<span className="label-text">Agent Name</span>
</label>
<input
type="text"
className="input input-bordered w-full"
{...register('agentName', { required: 'Agent name is required' })}
defaultValue={user?.agentName || ''}
/>
{errors.agentName && (
<label className="label">
<span className="label-text-alt text-error">
{errors.agentName.message}
</span>
</label>
)}
</div>
<div className="modal-action">
<button type="submit" className="btn">
Update
</button>
</div>
</Modal>
</div>
)
}
export default UserInfoSettings

View File

@@ -1,12 +1,13 @@
import { useState } from 'react'
import {useState} from 'react'
import { Tabs } from '../../components/mollecules'
import {Tabs} from '../../components/mollecules'
import AccountSettings from './account/accountSettings'
import HealthChecks from './healthchecks/healthChecks'
import MoneyManagementSettings from './moneymanagement/moneyManagement'
import Theme from './theme'
import DefaultConfig from './defaultConfig/defaultConfig'
import UserInfoSettings from './UserInfoSettings'
type TabsType = {
label: string
@@ -17,28 +18,33 @@ type TabsType = {
// Tabs Array
const tabs: TabsType = [
{
Component: MoneyManagementSettings,
Component: UserInfoSettings,
index: 1,
label: 'User Info',
},
{
Component: MoneyManagementSettings,
index: 2,
label: 'Money Management',
},
{
Component: AccountSettings,
index: 2,
index: 3,
label: 'Account Settings',
},
{
Component: Theme,
index: 3,
index: 4,
label: 'Theme',
},
{
Component: DefaultConfig,
index: 4,
index: 5,
label: 'Quick Start Config',
},
{
Component: HealthChecks,
index: 5,
index: 6,
label: 'Health Checks',
},
]