Balance for bot (#20)

* Add bot balance

* Update amount to trade

* fix initial trading balance

* Update MM modal

* fix backtest

* stop bot if no more balance

* Add constant for minimum trading

* Add constant
This commit is contained in:
Oda
2025-04-28 16:52:42 +02:00
committed by GitHub
parent 967e8410dc
commit 204bd87e6a
39 changed files with 600 additions and 546 deletions

View File

@@ -93,17 +93,6 @@ public class BacktestController : BaseController
return Ok(_backtester.DeleteBacktestByUser(user, id)); return Ok(_backtester.DeleteBacktestByUser(user, id));
} }
/// <summary>
/// Deletes all backtests.
/// </summary>
/// <returns>An ActionResult indicating the outcome of the operation.</returns>
[HttpDelete]
[Route("deleteAll")]
public ActionResult DeleteBacktests()
{
return Ok(_backtester.DeleteBacktests());
}
/// <summary> /// <summary>
/// Runs a backtest with the specified parameters. /// Runs a backtest with the specified parameters.
/// </summary> /// </summary>

View File

@@ -3,6 +3,7 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Hubs; using Managing.Application.Hubs;
using Managing.Application.ManageBot.Commands; using Managing.Application.ManageBot.Commands;
using Managing.Common;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@@ -123,9 +124,16 @@ public class BotController : BaseController
return BadRequest("Money management not found"); return BadRequest("Money management not found");
} }
// Validate initialTradingBalance
if (request.InitialTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
return BadRequest(
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
var result = await _mediator.Send(new StartBotCommand(request.BotType, request.BotName, request.Ticker, var result = await _mediator.Send(new StartBotCommand(request.BotType, request.BotName, request.Ticker,
request.Scenario, request.Timeframe, request.AccountName, request.MoneyManagementName, user, request.Scenario, request.Timeframe, request.AccountName, request.MoneyManagementName, user,
request.IsForWatchOnly)); request.IsForWatchOnly, request.InitialTradingBalance));
await NotifyBotSubscriberAsync(); await NotifyBotSubscriberAsync();
return Ok(result); return Ok(result);

View File

@@ -150,6 +150,7 @@ public class TradingController : BaseController
PositionInitiator.User, PositionInitiator.User,
DateTime.UtcNow, DateTime.UtcNow,
user, user,
100m, // Default trading balance for user-initiated trades
isForPaperTrading: isForPaperTrading, isForPaperTrading: isForPaperTrading,
price: openPrice); price: openPrice);
var result = await _openTradeCommandHandler.Handle(command); var result = await _openTradeCommandHandler.Handle(command);

View File

@@ -5,21 +5,20 @@ namespace Managing.Api.Models.Requests
{ {
public class StartBotRequest public class StartBotRequest
{ {
[Required] public BotType BotType { get; set; }
[Required] public string BotName { get; set; }
[Required] public Ticker Ticker { get; set; }
[Required] public Timeframe Timeframe { get; set; }
[Required] public bool IsForWatchOnly { get; set; }
[Required] public string Scenario { get; set; }
[Required] public string AccountName { get; set; }
[Required] public string MoneyManagementName { get; set; }
/// <summary>
/// Initial trading balance in USD for the bot
/// </summary>
[Required] [Required]
public BotType BotType { get; set; } [Range(10.00, double.MaxValue, ErrorMessage = "Initial trading balance must be greater than ten")]
[Required] public decimal InitialTradingBalance { get; set; }
public string BotName { get; set; }
[Required]
public Ticker Ticker { get; set; }
[Required]
public Timeframe Timeframe { get; set; }
[Required]
public bool IsForWatchOnly { get; set; }
[Required]
public string Scenario { get; set; }
[Required]
public string AccountName { get; set; }
[Required]
public string MoneyManagementName { get; set; }
} }
} }

View File

@@ -2,6 +2,7 @@
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Users;
using Managing.Infrastructure.Tests; using Managing.Infrastructure.Tests;
using Moq; using Moq;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -24,7 +25,6 @@ public class BaseTests
_moneyManagementService = new Mock<IMoneyManagementService>(); _moneyManagementService = new Mock<IMoneyManagementService>();
MoneyManagement = new MoneyManagement() MoneyManagement = new MoneyManagement()
{ {
BalanceAtRisk = 1m, // 30%
Leverage = 2, // x2 Leverage = 2, // x2
Timeframe = Timeframe.FifteenMinutes, Timeframe = Timeframe.FifteenMinutes,
StopLoss = 0.008m, // 0.8% StopLoss = 0.008m, // 0.8%
@@ -32,7 +32,7 @@ public class BaseTests
Name = "Default MM" Name = "Default MM"
}; };
_ = _moneyManagementService.Setup(m => m.GetMoneyMangement(It.IsAny<Domain.Users.User>(), It.IsAny<string>())) _ = _moneyManagementService.Setup(m => m.GetMoneyMangement(It.IsAny<User>(), It.IsAny<string>()))
.Returns(Task.FromResult(MoneyManagement)); .Returns(Task.FromResult(MoneyManagement));
_accountService.Setup(a => a.GetAccount(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>())) _accountService.Setup(a => a.GetAccount(It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))

View File

@@ -9,7 +9,6 @@ using Managing.Core;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using MongoDB.Driver.Linq;
using Moq; using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Xunit; using Xunit;
@@ -115,7 +114,6 @@ namespace Managing.Application.Tests
var moneyManagement = new MoneyManagement() var moneyManagement = new MoneyManagement()
{ {
BalanceAtRisk = 0.05m,
Leverage = 1, Leverage = 1,
Timeframe = timeframe, Timeframe = timeframe,
StopLoss = 0.01m, StopLoss = 0.01m,
@@ -179,7 +177,6 @@ namespace Managing.Application.Tests
{ {
var moneyManagement = new MoneyManagement() var moneyManagement = new MoneyManagement()
{ {
BalanceAtRisk = 0.05m,
Leverage = 1, Leverage = 1,
Timeframe = timeframe, Timeframe = timeframe,
StopLoss = s, StopLoss = s,
@@ -293,7 +290,6 @@ namespace Managing.Application.Tests
{ {
var moneyManagement = new MoneyManagement() var moneyManagement = new MoneyManagement()
{ {
BalanceAtRisk = 0.05m,
Leverage = 1, Leverage = 1,
Timeframe = timeframe, Timeframe = timeframe,
StopLoss = s, StopLoss = s,

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,6 @@ namespace Managing.Application.Tests
{ {
_moneyManagement = new MoneyManagement() _moneyManagement = new MoneyManagement()
{ {
BalanceAtRisk = 0.05m,
Leverage = 1, Leverage = 1,
Timeframe = Timeframe.OneDay, Timeframe = Timeframe.OneDay,
StopLoss = 0.008m, StopLoss = 0.008m,

View File

@@ -2,7 +2,6 @@
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Workers.Abstractions; using Managing.Application.Workers.Abstractions;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios; using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
@@ -243,7 +242,6 @@ public class StatisticService : IStatisticService
{ {
var moneyManagement = new MoneyManagement() var moneyManagement = new MoneyManagement()
{ {
BalanceAtRisk = 0.05m,
Leverage = 1, Leverage = 1,
Timeframe = timeframe, Timeframe = timeframe,
StopLoss = 0.008m, StopLoss = 0.008m,

View File

@@ -7,10 +7,10 @@ namespace Managing.Application.Abstractions
{ {
public interface IBotFactory public interface IBotFactory
{ {
ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly); 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); 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); 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); ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance);
IBot CreateSimpleBot(string botName, Workflow workflow); IBot CreateSimpleBot(string botName, Workflow workflow);
} }
} }

View File

@@ -1,5 +1,4 @@
using Managing.Common; using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
@@ -18,16 +17,16 @@ public interface IBotService
BotBackup GetBotBackup(string name); BotBackup GetBotBackup(string name);
ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Enums.Ticker ticker, ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly); string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Enums.Ticker ticker, ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly); string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Enums.Ticker ticker, ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly); string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Enums.Ticker ticker, ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Enums.Ticker ticker,
string scenario, Enums.Timeframe interval, bool isForWatchingOnly); string scenario, Enums.Timeframe interval, bool isForWatchingOnly, decimal initialTradingAmount);
IBot CreateSimpleBot(string botName, Workflow workflow); IBot CreateSimpleBot(string botName, Workflow workflow);
Task<string> StopBot(string requestName); Task<string> StopBot(string requestName);

View File

@@ -11,10 +11,10 @@ using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies; using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base; using Managing.Domain.Strategies.Base;
using Managing.Domain.Users;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums; using static Managing.Common.Enums;
using Managing.Domain.Users;
namespace Managing.Application.Backtesting namespace Managing.Application.Backtesting
{ {
@@ -67,7 +67,7 @@ namespace Managing.Application.Backtesting
List<Candle> initialCandles = null) List<Candle> initialCandles = null)
{ {
var scalpingBot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario", var scalpingBot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, isForWatchingOnly); timeframe, isForWatchingOnly, balance);
scalpingBot.LoadScenario(scenario.Name); scalpingBot.LoadScenario(scenario.Name);
await scalpingBot.LoadAccount(); await scalpingBot.LoadAccount();
var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate); var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate);
@@ -121,7 +121,7 @@ namespace Managing.Application.Backtesting
List<Candle> initialCandles = null) List<Candle> initialCandles = null)
{ {
var flippingBot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario", var flippingBot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, false); timeframe, false, balance);
flippingBot.LoadScenario(scenario.Name); flippingBot.LoadScenario(scenario.Name);
await flippingBot.LoadAccount(); await flippingBot.LoadAccount();
@@ -152,7 +152,7 @@ namespace Managing.Application.Backtesting
{ {
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker); var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var bot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario", var bot = _botFactory.CreateBacktestScalpingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, false); timeframe, false, balance);
bot.LoadScenario(scenario.Name); bot.LoadScenario(scenario.Name);
await bot.LoadAccount(); await bot.LoadAccount();
@@ -173,7 +173,7 @@ namespace Managing.Application.Backtesting
{ {
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker); var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var bot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario", var bot = _botFactory.CreateBacktestFlippingBot(account.Name, moneyManagement, ticker, "scenario",
timeframe, false); timeframe, false, balance);
bot.LoadScenario(scenario.Name); bot.LoadScenario(scenario.Name);
await bot.LoadAccount(); await bot.LoadAccount();

View File

@@ -1,10 +1,10 @@
using Microsoft.Extensions.Logging; using Managing.Application.Abstractions;
using static Managing.Common.Enums;
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Domain.Bots;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Workflows; using Managing.Domain.Workflows;
using Managing.Domain.Bots; using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Application.Bots.Base namespace Managing.Application.Bots.Base
{ {
@@ -38,7 +38,7 @@ namespace Managing.Application.Bots.Base
return new SimpleBot(botName, _tradingBotLogger, workflow, _botService); return new SimpleBot(botName, _tradingBotLogger, workflow, _botService);
} }
ITradingBot IBotFactory.CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly) ITradingBot IBotFactory.CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
{ {
return new ScalpingBot( return new ScalpingBot(
accountName, accountName,
@@ -53,10 +53,11 @@ namespace Managing.Application.Bots.Base
_accountService, _accountService,
_messengerService, _messengerService,
_botService, _botService,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly); isForWatchingOnly: isForWatchingOnly);
} }
ITradingBot IBotFactory.CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly) ITradingBot IBotFactory.CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
{ {
return new ScalpingBot( return new ScalpingBot(
accountName, accountName,
@@ -71,11 +72,12 @@ namespace Managing.Application.Bots.Base
_accountService, _accountService,
_messengerService, _messengerService,
_botService, _botService,
true, initialTradingBalance,
isForWatchingOnly); isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
} }
public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly) public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
{ {
return new FlippingBot( return new FlippingBot(
accountName, accountName,
@@ -90,10 +92,11 @@ namespace Managing.Application.Bots.Base
_accountService, _accountService,
_messengerService, _messengerService,
_botService, _botService,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly); isForWatchingOnly: isForWatchingOnly);
} }
public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly) public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, Ticker ticker, string scenario, Timeframe interval, bool isForWatchingOnly, decimal initialTradingBalance)
{ {
return new FlippingBot( return new FlippingBot(
accountName, accountName,
@@ -108,8 +111,9 @@ namespace Managing.Application.Bots.Base
_accountService, _accountService,
_messengerService, _messengerService,
_botService, _botService,
true, initialTradingBalance,
isForWatchingOnly); isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
} }
} }
} }

View File

@@ -20,6 +20,7 @@ namespace Managing.Application.Bots
IAccountService accountService, IAccountService accountService,
IMessengerService messengerService, IMessengerService messengerService,
IBotService botService, IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false, bool isForBacktest = false,
bool isForWatchingOnly = false) bool isForWatchingOnly = false)
: base(accountName, : base(accountName,
@@ -34,6 +35,7 @@ namespace Managing.Application.Bots
accountService, accountService,
messengerService, messengerService,
botService, botService,
initialTradingBalance,
isForBacktest, isForBacktest,
isForWatchingOnly, isForWatchingOnly,
flipPosition: true) flipPosition: true)

View File

@@ -20,6 +20,7 @@ namespace Managing.Application.Bots
IAccountService accountService, IAccountService accountService,
IMessengerService messengerService, IMessengerService messengerService,
IBotService botService, IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false, bool isForBacktest = false,
bool isForWatchingOnly = false) bool isForWatchingOnly = false)
: base(accountName, : base(accountName,
@@ -34,6 +35,7 @@ namespace Managing.Application.Bots
accountService, accountService,
messengerService, messengerService,
botService, botService,
initialTradingBalance,
isForBacktest, isForBacktest,
isForWatchingOnly) isForWatchingOnly)
{ {

View File

@@ -2,6 +2,7 @@
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Trading; using Managing.Application.Trading;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Commands;
using Managing.Common;
using Managing.Core.FixedSizedQueue; using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Bots; using Managing.Domain.Bots;
@@ -50,6 +51,11 @@ public class TradingBot : Bot, ITradingBot
public Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; } public Dictionary<StrategyType, StrategiesResultBase> StrategiesValues { get; set; }
public DateTime StartupTime { get; set; } public DateTime StartupTime { get; set; }
/// <summary>
/// The dedicated trading balance for this bot in USD
/// </summary>
public decimal BotTradingBalance { get; set; }
public TradingBot( public TradingBot(
string accountName, string accountName,
MoneyManagement moneyManagement, MoneyManagement moneyManagement,
@@ -63,6 +69,7 @@ public class TradingBot : Bot, ITradingBot
IAccountService accountService, IAccountService accountService,
IMessengerService messengerService, IMessengerService messengerService,
IBotService botService, IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false, bool isForBacktest = false,
bool isForWatchingOnly = false, bool isForWatchingOnly = false,
bool flipPosition = false) bool flipPosition = false)
@@ -74,6 +81,13 @@ public class TradingBot : Bot, ITradingBot
TradingService = tradingService; TradingService = tradingService;
BotService = botService; BotService = botService;
if (initialTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
throw new ArgumentException(
$"Initial trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}",
nameof(initialTradingBalance));
}
IsForWatchingOnly = isForWatchingOnly; IsForWatchingOnly = isForWatchingOnly;
FlipPosition = flipPosition; FlipPosition = flipPosition;
AccountName = accountName; AccountName = accountName;
@@ -83,6 +97,7 @@ public class TradingBot : Bot, ITradingBot
Timeframe = timeframe; Timeframe = timeframe;
IsForBacktest = isForBacktest; IsForBacktest = isForBacktest;
Logger = logger; Logger = logger;
BotTradingBalance = initialTradingBalance;
Strategies = new HashSet<IStrategy>(); Strategies = new HashSet<IStrategy>();
Signals = new HashSet<Signal>(); Signals = new HashSet<Signal>();
@@ -172,6 +187,17 @@ public class TradingBot : Bot, ITradingBot
{ {
if (!IsForBacktest) if (!IsForBacktest)
{ {
// Check broker balance before running
var balance = await ExchangeService.GetBalance(Account, false);
if (balance < Constants.GMX.Config.MinimumPositionAmount)
{
await LogWarning(
$"Balance on broker is below {Constants.GMX.Config.MinimumPositionAmount} USD (actual: {balance}). Stopping bot and saving backup.");
SaveBackup();
Stop();
return;
}
Logger.LogInformation($"____________________{Name}____________________"); Logger.LogInformation($"____________________{Name}____________________");
Logger.LogInformation( 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 {BotType} - Ticker : {Ticker}");
@@ -315,7 +341,8 @@ public class TradingBot : Bot, ITradingBot
if (WalletBalances.Count == 0) if (WalletBalances.Count == 0)
{ {
WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest); // WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest);
WalletBalances[date] = BotTradingBalance;
return; return;
} }
@@ -555,10 +582,9 @@ public class TradingBot : Bot, ITradingBot
PositionInitiator.Bot, PositionInitiator.Bot,
signal.Date, signal.Date,
User, User,
BotTradingBalance,
IsForBacktest, IsForBacktest,
lastPrice, lastPrice,
balance: WalletBalances.LastOrDefault().Value,
fee: Fee,
signalIdentifier: signal.Identifier); signalIdentifier: signal.Identifier);
var position = await new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService) var position = await new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService)
@@ -591,7 +617,6 @@ public class TradingBot : Bot, ITradingBot
// Keep signal open for debug purpose // Keep signal open for debug purpose
//SetSignalStatus(signal.Identifier, SignalStatus.Expired); //SetSignalStatus(signal.Identifier, SignalStatus.Expired);
SetSignalStatus(signal.Identifier, SignalStatus.Expired); SetSignalStatus(signal.Identifier, SignalStatus.Expired);
await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}"); await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}");
} }
} }
@@ -599,8 +624,9 @@ public class TradingBot : Bot, ITradingBot
private bool CanOpenPosition(Signal signal) private bool CanOpenPosition(Signal signal)
{ {
if (ExecutionCount < 1) if (!IsForBacktest && ExecutionCount < 1)
return false; return false;
if (Positions.Count == 0) if (Positions.Count == 0)
return true; return true;
@@ -639,7 +665,6 @@ public class TradingBot : Bot, ITradingBot
} }
else else
{ {
position.Initiator = PositionInitiator.Bot;
var command = new ClosePositionCommand(position, lastPrice); var command = new ClosePositionCommand(position, lastPrice);
try try
{ {
@@ -681,6 +706,18 @@ public class TradingBot : Bot, ITradingBot
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished); await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
Logger.LogInformation( Logger.LogInformation(
$"Position {position.SignalIdentifier} type correctly close. Pnl on position : {position.ProfitAndLoss?.Realized}"); $"Position {position.SignalIdentifier} type correctly close. Pnl on position : {position.ProfitAndLoss?.Realized}");
// Update the bot's trading balance after position is closed
if (position.ProfitAndLoss != null)
{
// Add PnL (could be positive or negative)
BotTradingBalance += position.ProfitAndLoss.Realized;
// Subtract fees
BotTradingBalance -= GetPositionFees(position);
Logger.LogInformation($"Updated bot trading balance to: {BotTradingBalance}");
}
} }
else else
{ {
@@ -734,9 +771,13 @@ public class TradingBot : Bot, ITradingBot
} }
private async Task SetPositionStatus(string signalIdentifier, PositionStatus positionStatus) private async Task SetPositionStatus(string signalIdentifier, PositionStatus positionStatus)
{
if (!Positions.First(p => p.SignalIdentifier == signalIdentifier).Status.Equals(positionStatus))
{ {
await LogInformation($"Position {signalIdentifier} is now {positionStatus}"); await LogInformation($"Position {signalIdentifier} is now {positionStatus}");
Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus; Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
}
SetSignalStatus(signalIdentifier, SetSignalStatus(signalIdentifier,
positionStatus == PositionStatus.Filled ? SignalStatus.PositionOpen : SignalStatus.Expired); positionStatus == PositionStatus.Filled ? SignalStatus.PositionOpen : SignalStatus.Expired);
} }
@@ -799,6 +840,29 @@ public class TradingBot : Bot, ITradingBot
return fees; return fees;
} }
/// <summary>
/// Calculates the total fees for a specific position
/// </summary>
/// <param name="position">The position to calculate fees for</param>
/// <returns>The total fees for the position</returns>
private decimal GetPositionFees(Position position)
{
decimal fees = 0;
fees += position.Open.Fee;
fees += position.StopLoss.Status == TradeStatus.Filled ? position.StopLoss.Fee : 0;
fees += position.TakeProfit1.Status == TradeStatus.Filled ? position.TakeProfit1.Fee : 0;
if (position.IsFinished() &&
position.StopLoss.Status != TradeStatus.Filled && position.TakeProfit1.Status != TradeStatus.Filled)
fees += position.Open.Fee;
if (position.TakeProfit2 != null)
fees += position.TakeProfit2.Status == TradeStatus.Filled ? position.TakeProfit2.Fee : 0;
return fees;
}
public async Task ToggleIsForWatchOnly() public async Task ToggleIsForWatchOnly()
{ {
IsForWatchingOnly = (!IsForWatchingOnly); IsForWatchingOnly = (!IsForWatchingOnly);
@@ -813,6 +877,7 @@ public class TradingBot : Bot, ITradingBot
private async Task LogWarning(string message) private async Task LogWarning(string message)
{ {
message = $"[{Name}][{Identifier}] {message}";
Logger.LogWarning(message); Logger.LogWarning(message);
SentrySdk.CaptureException(new Exception(message)); SentrySdk.CaptureException(new Exception(message));
await SendTradeMessage(message, true); await SendTradeMessage(message, true);
@@ -841,6 +906,7 @@ public class TradingBot : Bot, ITradingBot
IsForWatchingOnly = IsForWatchingOnly, IsForWatchingOnly = IsForWatchingOnly,
WalletBalances = WalletBalances, WalletBalances = WalletBalances,
MoneyManagement = MoneyManagement, MoneyManagement = MoneyManagement,
BotTradingBalance = BotTradingBalance,
StartupTime = StartupTime, StartupTime = StartupTime,
}; };
BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data)); BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data));
@@ -858,6 +924,7 @@ public class TradingBot : Bot, ITradingBot
ScenarioName = data.ScenarioName; ScenarioName = data.ScenarioName;
AccountName = data.AccountName; AccountName = data.AccountName;
IsForWatchingOnly = data.IsForWatchingOnly; IsForWatchingOnly = data.IsForWatchingOnly;
BotTradingBalance = data.BotTradingBalance;
// Restore the startup time if it was previously saved // Restore the startup time if it was previously saved
if (data.StartupTime != DateTime.MinValue) if (data.StartupTime != DateTime.MinValue)
@@ -902,10 +969,6 @@ public class TradingBot : Bot, ITradingBot
throw new Exception("Failed to open position"); throw new Exception("Failed to open position");
} }
// Removed manual setting of SL/TP, as MoneyManagement should handle it
// position.StopLoss.Price = stopLossPrice;
// position.TakeProfit1.Price = takeProfitPrice;
Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}"); Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}");
return position; return position;
} }
@@ -925,4 +988,5 @@ public class TradingBotBackup
public Dictionary<DateTime, decimal> WalletBalances { get; set; } public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public MoneyManagement MoneyManagement { get; set; } public MoneyManagement MoneyManagement { get; set; }
public DateTime StartupTime { get; set; } public DateTime StartupTime { get; set; }
public decimal BotTradingBalance { get; set; }
} }

View File

@@ -136,7 +136,8 @@ namespace Managing.Application.ManageBot
scalpingBotData.Ticker, scalpingBotData.Ticker,
scalpingBotData.ScenarioName, scalpingBotData.ScenarioName,
scalpingBotData.Timeframe, scalpingBotData.Timeframe,
scalpingBotData.IsForWatchingOnly); scalpingBotData.IsForWatchingOnly,
scalpingBotData.BotTradingBalance);
botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot)); botTask = Task.Run(() => InitBot((ITradingBot)bot, backupBot));
break; break;
case Enums.BotType.FlippingBot: case Enums.BotType.FlippingBot:
@@ -150,7 +151,8 @@ namespace Managing.Application.ManageBot
flippingBotData.Ticker, flippingBotData.Ticker,
flippingBotData.ScenarioName, flippingBotData.ScenarioName,
flippingBotData.Timeframe, flippingBotData.Timeframe,
flippingBotData.IsForWatchingOnly); flippingBotData.IsForWatchingOnly,
flippingBotData.BotTradingBalance);
botTask = Task.Run(InitBot((ITradingBot)bot, backupBot)); botTask = Task.Run(InitBot((ITradingBot)bot, backupBot));
break; break;
} }
@@ -243,7 +245,8 @@ namespace Managing.Application.ManageBot
} }
public ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name, public ITradingBot CreateScalpingBot(string accountName, MoneyManagement moneyManagement, string name,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly) Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{ {
return new ScalpingBot( return new ScalpingBot(
accountName, accountName,
@@ -258,11 +261,13 @@ namespace Managing.Application.ManageBot
_accountService, _accountService,
_messengerService, _messengerService,
this, this,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly); isForWatchingOnly: isForWatchingOnly);
} }
public ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement, public ITradingBot CreateBacktestScalpingBot(string accountName, MoneyManagement moneyManagement,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly) Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{ {
return new ScalpingBot( return new ScalpingBot(
accountName, accountName,
@@ -277,12 +282,14 @@ namespace Managing.Application.ManageBot
_accountService, _accountService,
_messengerService, _messengerService,
this, this,
true, initialTradingBalance,
isForWatchingOnly); isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
} }
public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name, public ITradingBot CreateFlippingBot(string accountName, MoneyManagement moneyManagement, string name,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly) Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{ {
return new FlippingBot( return new FlippingBot(
accountName, accountName,
@@ -297,11 +304,13 @@ namespace Managing.Application.ManageBot
_accountService, _accountService,
_messengerService, _messengerService,
this, this,
initialTradingBalance,
isForWatchingOnly: isForWatchingOnly); isForWatchingOnly: isForWatchingOnly);
} }
public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement, public ITradingBot CreateBacktestFlippingBot(string accountName, MoneyManagement moneyManagement,
Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly) Enums.Ticker ticker, string scenario, Enums.Timeframe interval, bool isForWatchingOnly,
decimal initialTradingBalance)
{ {
return new FlippingBot( return new FlippingBot(
accountName, accountName,
@@ -316,8 +325,9 @@ namespace Managing.Application.ManageBot
_accountService, _accountService,
_messengerService, _messengerService,
this, this,
true, initialTradingBalance,
isForWatchingOnly); isForBacktest: true,
isForWatchingOnly: isForWatchingOnly);
} }
} }
} }

View File

@@ -1,5 +1,5 @@
using MediatR; using Managing.Domain.Users;
using Managing.Domain.Users; using MediatR;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.ManageBot.Commands namespace Managing.Application.ManageBot.Commands
@@ -15,6 +15,7 @@ namespace Managing.Application.ManageBot.Commands
public string AccountName { get; internal set; } public string AccountName { get; internal set; }
public string MoneyManagementName { get; internal set; } public string MoneyManagementName { get; internal set; }
public User User { get; internal set; } public User User { get; internal set; }
public decimal InitialTradingBalance { get; internal set; }
public StartBotCommand(BotType botType, public StartBotCommand(BotType botType,
string name, string name,
@@ -24,7 +25,8 @@ namespace Managing.Application.ManageBot.Commands
string accountName, string accountName,
string moneyManagementName, string moneyManagementName,
User user, User user,
bool isForWatchingOnly = false) bool isForWatchingOnly = false,
decimal initialTradingBalance = 0)
{ {
BotType = botType; BotType = botType;
Name = name; Name = name;
@@ -35,6 +37,8 @@ namespace Managing.Application.ManageBot.Commands
AccountName = accountName; AccountName = accountName;
MoneyManagementName = moneyManagementName; MoneyManagementName = moneyManagementName;
User = user; User = user;
InitialTradingBalance = initialTradingBalance > 0 ? initialTradingBalance :
throw new ArgumentException("Initial trading balance must be greater than zero", nameof(initialTradingBalance));
} }
} }
} }

View File

@@ -1,7 +1,8 @@
using MediatR; using Managing.Application.Abstractions;
using static Managing.Common.Enums; using Managing.Application.Abstractions.Services;
using Managing.Application.Abstractions;
using Managing.Application.ManageBot.Commands; using Managing.Application.ManageBot.Commands;
using MediatR;
using static Managing.Common.Enums;
namespace Managing.Application.ManageBot namespace Managing.Application.ManageBot
{ {
@@ -10,38 +11,61 @@ namespace Managing.Application.ManageBot
private readonly IBotFactory _botFactory; private readonly IBotFactory _botFactory;
private readonly IBotService _botService; private readonly IBotService _botService;
private readonly IMoneyManagementService _moneyManagementService; private readonly IMoneyManagementService _moneyManagementService;
private readonly IExchangeService _exchangeService;
private readonly IAccountService _accountService;
public StartBotCommandHandler(IBotFactory botFactory, IBotService botService, public StartBotCommandHandler(IBotFactory botFactory, IBotService botService,
IMoneyManagementService moneyManagementService) IMoneyManagementService moneyManagementService, IExchangeService exchangeService,
IAccountService accountService)
{ {
_botFactory = botFactory; _botFactory = botFactory;
_botService = botService; _botService = botService;
_moneyManagementService = moneyManagementService; _moneyManagementService = moneyManagementService;
_exchangeService = exchangeService;
_accountService = accountService;
} }
public Task<string> Handle(StartBotCommand request, CancellationToken cancellationToken) public async Task<string> Handle(StartBotCommand request, CancellationToken cancellationToken)
{ {
BotStatus botStatus = BotStatus.Down; BotStatus botStatus = BotStatus.Down;
var moneyManagement = _moneyManagementService.GetMoneyMangement(request.User, request.MoneyManagementName).Result;
var account = await _accountService.GetAccount(request.AccountName, true, true);
if (account == null)
{
throw new Exception($"Account {request.AccountName} not found");
}
var usdcBalance = account.Balances.FirstOrDefault(b => b.TokenName == Ticker.USDC.ToString());
if (usdcBalance == null || usdcBalance.Value < request.InitialTradingBalance)
{
throw new Exception($"Account {request.AccountName} has no USDC balance or not enough balance");
}
var moneyManagement =
await _moneyManagementService.GetMoneyMangement(request.User, request.MoneyManagementName);
switch (request.BotType) switch (request.BotType)
{ {
case BotType.SimpleBot: case BotType.SimpleBot:
var bot = _botFactory.CreateSimpleBot(request.Name, null); var bot = _botFactory.CreateSimpleBot(request.Name, null);
_botService.AddSimpleBotToCache(bot); _botService.AddSimpleBotToCache(bot);
return Task.FromResult(bot.GetStatus()); return bot.GetStatus();
case BotType.ScalpingBot: case BotType.ScalpingBot:
var sBot = _botFactory.CreateScalpingBot(request.AccountName, moneyManagement, request.Name, var sBot = _botFactory.CreateScalpingBot(request.AccountName, moneyManagement, request.Name,
request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly); request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly,
request.InitialTradingBalance);
_botService.AddTradingBotToCache(sBot); _botService.AddTradingBotToCache(sBot);
return Task.FromResult(sBot.GetStatus()); return sBot.GetStatus();
case BotType.FlippingBot: case BotType.FlippingBot:
var fBot = _botFactory.CreateFlippingBot(request.AccountName, moneyManagement, request.Name, var fBot = _botFactory.CreateFlippingBot(request.AccountName, moneyManagement, request.Name,
request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly); request.Ticker, request.Scenario, request.Timeframe, request.IsForWatchingOnly,
request.InitialTradingBalance);
_botService.AddTradingBotToCache(fBot); _botService.AddTradingBotToCache(fBot);
return Task.FromResult(fBot.GetStatus()); return fBot.GetStatus();
} }
return Task.FromResult(botStatus.ToString()); return botStatus.ToString();
} }
} }
} }

View File

@@ -1,8 +1,8 @@
using Managing.Domain.MoneyManagements; using Managing.Application.Abstractions;
using Managing.Application.Abstractions;
using Microsoft.Extensions.Logging;
using Managing.Application.Abstractions.Repositories; using Managing.Application.Abstractions.Repositories;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Users; using Managing.Domain.Users;
using Microsoft.Extensions.Logging;
namespace Managing.Application.MoneyManagements; namespace Managing.Application.MoneyManagements;
@@ -47,12 +47,12 @@ public class MoneyManagementService : IMoneyManagementService
// Additional check to ensure user's ownership // Additional check to ensure user's ownership
if (moneyManagement.User?.Name != user.Name) if (moneyManagement.User?.Name != user.Name)
{ {
throw new UnauthorizedAccessException("You do not have permission to update this money management strategy."); throw new UnauthorizedAccessException(
"You do not have permission to update this money management strategy.");
} }
moneyManagement.StopLoss = request.StopLoss; moneyManagement.StopLoss = request.StopLoss;
moneyManagement.TakeProfit = request.TakeProfit; moneyManagement.TakeProfit = request.TakeProfit;
moneyManagement.BalanceAtRisk = request.BalanceAtRisk;
moneyManagement.Leverage = request.Leverage; moneyManagement.Leverage = request.Leverage;
moneyManagement.Timeframe = request.Timeframe; moneyManagement.Timeframe = request.Timeframe;
moneyManagement.User = user; moneyManagement.User = user;
@@ -122,7 +122,8 @@ public class MoneyManagementService : IMoneyManagementService
if (moneyManagement != null && moneyManagement.User?.Name != user.Name) if (moneyManagement != null && moneyManagement.User?.Name != user.Name)
{ {
throw new UnauthorizedAccessException("You do not have permission to delete this money management strategy."); throw new UnauthorizedAccessException(
"You do not have permission to delete this money management strategy.");
} }
_settingsRepository.DeleteMoneyManagement(name); _settingsRepository.DeleteMoneyManagement(name);
@@ -150,7 +151,8 @@ public class MoneyManagementService : IMoneyManagementService
{ {
// This fallback is not ideal as it would delete all money managements regardless of user // This fallback is not ideal as it would delete all money managements regardless of user
// In a real implementation, we would need a filtered repository method // In a real implementation, we would need a filtered repository method
_logger.LogWarning("DeleteMoneyManagementsByUser not implemented, cannot delete user-specific money managements"); _logger.LogWarning(
"DeleteMoneyManagementsByUser not implemented, cannot delete user-specific money managements");
} }
return true; return true;

View File

@@ -200,7 +200,6 @@ public class SettingsService : ISettingsService
{ {
Name = "Personal-Hourly", Name = "Personal-Hourly",
Timeframe = Timeframe.OneHour, Timeframe = Timeframe.OneHour,
BalanceAtRisk = 25, // 25%
StopLoss = 2, // 2% StopLoss = 2, // 2%
TakeProfit = 4, // 4% TakeProfit = 4, // 4%
Leverage = 1, Leverage = 1,

View File

@@ -16,25 +16,28 @@ namespace Managing.Application.Trading.Commands
PositionInitiator initiator, PositionInitiator initiator,
DateTime date, DateTime date,
User user, User user,
decimal amountToTrade,
bool isForPaperTrading = false, bool isForPaperTrading = false,
decimal? price = null, decimal? price = null,
decimal? balance = 1000,
decimal? fee = null,
bool? ignoreSLTP = false,
string signalIdentifier = null) string signalIdentifier = null)
{ {
AccountName = accountName; AccountName = accountName;
MoneyManagement = moneyManagement; MoneyManagement = moneyManagement;
Direction = direction; Direction = direction;
Ticker = ticker; Ticker = ticker;
Initiator = initiator;
Date = date;
User = user;
if (amountToTrade <= 10)
{
throw new ArgumentException("Bot trading balance must be greater than zero", nameof(amountToTrade));
}
AmountToTrade = amountToTrade;
IsForPaperTrading = isForPaperTrading; IsForPaperTrading = isForPaperTrading;
Price = price; Price = price;
Date = date;
Balance = balance;
Initiator = initiator;
Fee = fee;
IgnoreSLTP = ignoreSLTP;
User = user;
SignalIdentifier = signalIdentifier; SignalIdentifier = signalIdentifier;
} }
@@ -45,11 +48,9 @@ namespace Managing.Application.Trading.Commands
public Ticker Ticker { get; } public Ticker Ticker { get; }
public bool IsForPaperTrading { get; } public bool IsForPaperTrading { get; }
public decimal? Price { get; } public decimal? Price { get; }
public decimal? Fee { get; } public decimal AmountToTrade { get; }
public bool? IgnoreSLTP { get; } public DateTime Date { get; }
public decimal? Balance { get; } public PositionInitiator Initiator { get; }
public DateTime Date { get; set; } public User User { get; }
public PositionInitiator Initiator { get; internal set; }
public User User { get; internal set; }
} }
} }

View File

@@ -1,6 +1,7 @@
using Managing.Application.Abstractions; using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Commands;
using Managing.Common;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -36,22 +37,20 @@ namespace Managing.Application.Trading
position.SignalIdentifier = request.SignalIdentifier; position.SignalIdentifier = request.SignalIdentifier;
} }
var balance = request.IsForPaperTrading // Always use BotTradingBalance directly as the balance to risk
? request.Balance.GetValueOrDefault() decimal balanceToRisk = request.AmountToTrade;
: exchangeService.GetBalance(account, request.IsForPaperTrading).Result;
var balanceAtRisk = RiskHelpers.GetBalanceAtRisk(balance, request.MoneyManagement);
if (balanceAtRisk < 7) // Minimum check
if (balanceToRisk < Constants.GMX.Config.MinimumPositionAmount)
{ {
throw new Exception($"Try to risk {balanceAtRisk} $ but inferior to minimum to trade"); throw new Exception(
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
} }
var price = request.IsForPaperTrading && request.Price.HasValue var price = request.IsForPaperTrading && request.Price.HasValue
? request.Price.Value ? request.Price.Value
: exchangeService.GetPrice(account, request.Ticker, DateTime.Now); : exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
var quantity = balanceAtRisk / price; var quantity = balanceToRisk / price;
// var expectedStatus = GetExpectedStatus(request);
// position.Open = TradingPolicies.OpenPosition(expectedStatus).Execute(async () => { });
var openPrice = request.IsForPaperTrading || request.Price.HasValue var openPrice = request.IsForPaperTrading || request.Price.HasValue
? request.Price.Value ? request.Price.Value
@@ -71,20 +70,19 @@ namespace Managing.Application.Trading
TradeType.Limit, TradeType.Limit,
isForPaperTrading: request.IsForPaperTrading, isForPaperTrading: request.IsForPaperTrading,
currentDate: request.Date, currentDate: request.Date,
stopLossPrice: stopLossPrice, // Pass determined SL price stopLossPrice: stopLossPrice,
takeProfitPrice: takeProfitPrice); // Pass determined TP price takeProfitPrice: takeProfitPrice);
//trade.Fee = TradingHelpers.GetFeeAmount(fee, openPrice * quantity, account.Exchange);
position.Open = trade; position.Open = trade;
var closeDirection = request.Direction == TradeDirection.Long var closeDirection = request.Direction == TradeDirection.Long
? TradeDirection.Short ? TradeDirection.Short
: TradeDirection.Long; : TradeDirection.Long;
// Stop loss - Use the determined price // Stop loss
position.StopLoss = exchangeService.BuildEmptyTrade( position.StopLoss = exchangeService.BuildEmptyTrade(
request.Ticker, request.Ticker,
stopLossPrice, // Use determined SL price stopLossPrice,
position.Open.Quantity, position.Open.Quantity,
closeDirection, closeDirection,
request.MoneyManagement.Leverage, request.MoneyManagement.Leverage,
@@ -92,13 +90,10 @@ namespace Managing.Application.Trading
request.Date, request.Date,
TradeStatus.Requested); TradeStatus.Requested);
// position.StopLoss.Fee = TradingHelpers.GetFeeAmount(fee, // Take profit
// position.StopLoss.Price * position.StopLoss.Quantity, account.Exchange);
// Take profit - Use the determined price
position.TakeProfit1 = exchangeService.BuildEmptyTrade( position.TakeProfit1 = exchangeService.BuildEmptyTrade(
request.Ticker, request.Ticker,
takeProfitPrice, // Use determined TP price takeProfitPrice,
quantity, quantity,
closeDirection, closeDirection,
request.MoneyManagement.Leverage, request.MoneyManagement.Leverage,

View File

@@ -147,10 +147,9 @@ public class OpenPosition : FlowBase
PositionInitiator.Bot, PositionInitiator.Bot,
signal.Date, signal.Date,
account.User, account.User,
100m, // Default bot trading balance
OpenPositionParameters.IsForBacktest, OpenPositionParameters.IsForBacktest,
lastPrice, lastPrice);
balance: WalletBalances.LastOrDefault().Value,
fee: Fee);
var position = await new OpenPositionCommandHandler(_exchangeService, _accountService, _tradingService) var position = await new OpenPositionCommandHandler(_exchangeService, _accountService, _tradingService)
.Handle(command); .Handle(command);

View File

@@ -84,6 +84,8 @@
{ {
public const int USD = 30; public const int USD = 30;
} }
public const decimal MinimumPositionAmount = 10m;
} }
public class TokenAddress public class TokenAddress

View File

@@ -11,8 +11,6 @@ namespace Managing.Domain.MoneyManagements
[Required] [Required]
public Timeframe Timeframe { get; set; } public Timeframe Timeframe { get; set; }
[Required] [Required]
public decimal BalanceAtRisk { get; set; }
[Required]
public decimal StopLoss { get; set; } public decimal StopLoss { get; set; }
[Required] [Required]
public decimal TakeProfit { get; set; } public decimal TakeProfit { get; set; }
@@ -25,7 +23,6 @@ namespace Managing.Domain.MoneyManagements
{ {
StopLoss /= 100; StopLoss /= 100;
TakeProfit /= 100; TakeProfit /= 100;
BalanceAtRisk /= 100;
} }
} }
} }

View File

@@ -12,18 +12,6 @@ namespace Managing.Domain.Shared.Helpers
price += price * moneyManagement.StopLoss; price += price * moneyManagement.StopLoss;
} }
public static decimal GetBalanceAtRisk(decimal balance, MoneyManagement moneyManagement)
{
decimal amountToRisk = balance * moneyManagement.BalanceAtRisk;
if (amountToRisk <= 1)
{
throw new Exception("Cannot open trade, not enough balance");
}
return amountToRisk;
}
public static decimal GetTakeProfitPrice(TradeDirection direction, decimal price, MoneyManagement moneyManagement, int count = 1) public static decimal GetTakeProfitPrice(TradeDirection direction, decimal price, MoneyManagement moneyManagement, int count = 1)
{ {
decimal percentage = moneyManagement.TakeProfit * count; decimal percentage = moneyManagement.TakeProfit * count;

View File

@@ -134,7 +134,6 @@ public static class TradingBox
moneyManagement.StopLoss = stoplossPercentage.Average(); moneyManagement.StopLoss = stoplossPercentage.Average();
moneyManagement.TakeProfit = takeProfitsPercentage.Average(); moneyManagement.TakeProfit = takeProfitsPercentage.Average();
moneyManagement.BalanceAtRisk = originMoneyManagement.BalanceAtRisk * 100;
moneyManagement.Timeframe = originMoneyManagement.Timeframe; moneyManagement.Timeframe = originMoneyManagement.Timeframe;
moneyManagement.Leverage = originMoneyManagement.Leverage; moneyManagement.Leverage = originMoneyManagement.Leverage;
moneyManagement.Name = "Optimized"; moneyManagement.Name = "Optimized";
@@ -310,7 +309,8 @@ public static class TradingBox
(position.Open.Date >= cutoff || (position.Open.Date >= cutoff ||
(position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) || (position.StopLoss.Status == TradeStatus.Filled && position.StopLoss.Date >= cutoff) ||
(position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) || (position.TakeProfit1.Status == TradeStatus.Filled && position.TakeProfit1.Date >= cutoff) ||
(position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled && position.TakeProfit2.Date >= cutoff))) (position.TakeProfit2 != null && position.TakeProfit2.Status == TradeStatus.Filled &&
position.TakeProfit2.Date >= cutoff)))
{ {
profitLast24h += position.ProfitAndLoss != null ? position.ProfitAndLoss.Realized : 0; profitLast24h += position.ProfitAndLoss != null ? position.ProfitAndLoss.Realized : 0;
investmentLast24h += position.Open.Quantity * position.Open.Price; investmentLast24h += position.Open.Quantity * position.Open.Price;
@@ -368,7 +368,8 @@ public static class TradingBox
(p.Date >= cutoffDate || (p.Date >= cutoffDate ||
(p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) ||
(p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) ||
(p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))) (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled &&
p.TakeProfit2.Date >= cutoffDate)))
.Sum(p => p.ProfitAndLoss.Realized); .Sum(p => p.ProfitAndLoss.Realized);
} }
@@ -418,7 +419,8 @@ public static class TradingBox
(p.Date >= cutoffDate || (p.Date >= cutoffDate ||
(p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) ||
(p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) ||
(p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))); (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled &&
p.TakeProfit2.Date >= cutoffDate)));
// Calculate investment and profit // Calculate investment and profit
decimal totalInvestment = filteredPositions.Sum(p => p.Open.Quantity * p.Open.Price); decimal totalInvestment = filteredPositions.Sum(p => p.Open.Quantity * p.Open.Price);
@@ -473,7 +475,8 @@ public static class TradingBox
(p.Date >= cutoffDate || (p.Date >= cutoffDate ||
(p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) || (p.StopLoss.Status == TradeStatus.Filled && p.StopLoss.Date >= cutoffDate) ||
(p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) || (p.TakeProfit1.Status == TradeStatus.Filled && p.TakeProfit1.Date >= cutoffDate) ||
(p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled && p.TakeProfit2.Date >= cutoffDate))); (p.TakeProfit2 != null && p.TakeProfit2.Status == TradeStatus.Filled &&
p.TakeProfit2.Date >= cutoffDate)));
int wins = 0; int wins = 0;
int losses = 0; int losses = 0;

View File

@@ -1,5 +1,4 @@
using Managing.Core; using Managing.Domain.Accounts;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests; using Managing.Domain.Backtests;
using Managing.Domain.Bots; using Managing.Domain.Bots;
using Managing.Domain.Candles; using Managing.Domain.Candles;
@@ -12,9 +11,8 @@ using Managing.Domain.Users;
using Managing.Domain.Workers; using Managing.Domain.Workers;
using Managing.Domain.Workflows.Synthetics; using Managing.Domain.Workflows.Synthetics;
using Managing.Infrastructure.Databases.MongoDb.Collections; using Managing.Infrastructure.Databases.MongoDb.Collections;
using static Managing.Common.Enums;
using MongoDB.Bson; using MongoDB.Bson;
using Managing.Domain.Shared.Helpers; using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.MongoDb; namespace Managing.Infrastructure.Databases.MongoDb;
@@ -486,7 +484,6 @@ public static class MongoMappers
return new MoneyManagementDto return new MoneyManagementDto
{ {
Timeframe = request.Timeframe, Timeframe = request.Timeframe,
BalanceAtRisk = request.BalanceAtRisk,
StopLoss = request.StopLoss, StopLoss = request.StopLoss,
TakeProfit = request.TakeProfit, TakeProfit = request.TakeProfit,
Leverage = request.Leverage, Leverage = request.Leverage,
@@ -503,7 +500,6 @@ public static class MongoMappers
return new MoneyManagement return new MoneyManagement
{ {
Timeframe = request.Timeframe, Timeframe = request.Timeframe,
BalanceAtRisk = request.BalanceAtRisk,
StopLoss = request.StopLoss, StopLoss = request.StopLoss,
TakeProfit = request.TakeProfit, TakeProfit = request.TakeProfit,
Leverage = request.Leverage, Leverage = request.Leverage,

View File

@@ -69,12 +69,11 @@ namespace Managing.Infrastructure.Messengers.Discord
var moneymanagement = new MoneyManagement var moneymanagement = new MoneyManagement
{ {
BalanceAtRisk = 5,
StopLoss = stopLoss.GetValueOrDefault(), StopLoss = stopLoss.GetValueOrDefault(),
TakeProfit = takeProfit.GetValueOrDefault(), TakeProfit = takeProfit.GetValueOrDefault(),
}; };
var tradeCommand = new OpenPositionRequest(accountName, moneymanagement, direction, ticker, var tradeCommand = new OpenPositionRequest(accountName, moneymanagement, direction, ticker,
PositionInitiator.User, DateTime.UtcNow, new User()); PositionInitiator.User, DateTime.UtcNow, new User(), 100m);
var result = await _openTradeCommandHandler.Handle(tradeCommand); var result = await _openTradeCommandHandler.Handle(tradeCommand);
var builder = new ComponentBuilder().WithButton("Close Position", var builder = new ComponentBuilder().WithButton("Close Position",
$"{Constants.DiscordButtonAction.ClosePosition}|{result.Open.ExchangeOrderId}"); $"{Constants.DiscordButtonAction.ClosePosition}|{result.Open.ExchangeOrderId}");

View File

@@ -12,6 +12,7 @@ using Managing.Core;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Managing.Domain.Statistics; using Managing.Domain.Statistics;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Domain.Users;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
@@ -264,7 +265,7 @@ namespace Managing.Infrastructure.Messengers.Discord
var tradingService = (ITradingService)_services.GetService(typeof(ITradingService)); var tradingService = (ITradingService)_services.GetService(typeof(ITradingService));
// Create default user for Discord bot operations // Create default user for Discord bot operations
var defaultUser = new Domain.Users.User { Name = "DiscordBot" }; var defaultUser = new User { Name = "DiscordBot" };
var tradeCommand = new OpenPositionRequest( var tradeCommand = new OpenPositionRequest(
accountName, accountName,
@@ -274,7 +275,7 @@ namespace Managing.Infrastructure.Messengers.Discord
initiator, initiator,
DateTime.UtcNow, DateTime.UtcNow,
defaultUser, defaultUser,
ignoreSLTP: ignoreSLTP); 100m);
var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) var position = await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
.Handle(tradeCommand); .Handle(tradeCommand);
@@ -416,7 +417,6 @@ namespace Managing.Infrastructure.Messengers.Discord
new MoneyManagement new MoneyManagement
{ {
Name = "MediumRisk", Name = "MediumRisk",
BalanceAtRisk = 0.05m,
Leverage = 1, Leverage = 1,
StopLoss = 0.021m, StopLoss = 0.021m,
TakeProfit = 0.042m, TakeProfit = 0.042m,

View File

@@ -1,14 +1,14 @@
import React, { useState, useEffect } from 'react' import React, {useEffect, useState} from 'react'
import type { Backtest, MoneyManagement } from '../../../generated/ManagingApi' import type {Backtest, MoneyManagement} from '../../../generated/ManagingApi'
import type { IModalProps } from '../../../global/type' import type {IModalProps} from '../../../global/type'
import Modal from './Modal' import Modal from './Modal'
interface IBotNameModalProps extends IModalProps { interface IBotNameModalProps extends IModalProps {
backtest: Backtest backtest: Backtest
isForWatchOnly: boolean isForWatchOnly: boolean
onSubmitBotName: (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string) => void onSubmitBotName: (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => void
moneyManagements: MoneyManagement[] moneyManagements: MoneyManagement[]
selectedMoneyManagement: string selectedMoneyManagement: string
setSelectedMoneyManagement: (name: string) => void setSelectedMoneyManagement: (name: string) => void
@@ -25,6 +25,7 @@ const BotNameModal: React.FC<IBotNameModalProps> = ({
setSelectedMoneyManagement, setSelectedMoneyManagement,
}) => { }) => {
const [botName, setBotName] = useState<string>('') const [botName, setBotName] = useState<string>('')
const [initialTradingBalance, setInitialTradingBalance] = useState<number>(1000)
// Initialize botName when backtest changes // Initialize botName when backtest changes
useEffect(() => { useEffect(() => {
@@ -35,8 +36,8 @@ const BotNameModal: React.FC<IBotNameModalProps> = ({
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (botName.trim()) { if (botName.trim() && initialTradingBalance > 0) {
onSubmitBotName(botName, backtest, isForWatchOnly, selectedMoneyManagement) onSubmitBotName(botName, backtest, isForWatchOnly, selectedMoneyManagement, initialTradingBalance)
} }
} }
@@ -85,6 +86,22 @@ const BotNameModal: React.FC<IBotNameModalProps> = ({
</select> </select>
</div> </div>
<div className="form-control w-full mb-4">
<label className="label">
<span className="label-text">Initial Trading Balance ($)</span>
</label>
<input
type="number"
placeholder="Enter initial trading balance"
className="input input-bordered w-full"
value={initialTradingBalance}
onChange={(e) => setInitialTradingBalance(Number(e.target.value))}
min="10"
step="0.01"
required
/>
</div>
<p className="text-xs text-gray-500 mt-2 mb-4"> <p className="text-xs text-gray-500 mt-2 mb-4">
{isForWatchOnly {isForWatchOnly
? 'The bot will run in watch-only mode and will not execute trades.' ? 'The bot will run in watch-only mode and will not execute trades.'

View File

@@ -1,22 +1,13 @@
import { DotsVerticalIcon, TrashIcon } from '@heroicons/react/solid' import {DotsVerticalIcon, TrashIcon} from '@heroicons/react/solid'
import moment from 'moment' import moment from 'moment'
import React from 'react' import React from 'react'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import type { import type {Backtest, MoneyManagement, StartBotRequest, Ticker,} from '../../../generated/ManagingApi'
Backtest, import {BacktestClient, BotClient, BotType,} from '../../../generated/ManagingApi'
MoneyManagement, import type {IBacktestCards} from '../../../global/type'
StartBotRequest,
Ticker,
} from '../../../generated/ManagingApi'
import {
BacktestClient,
BotClient,
BotType,
} from '../../../generated/ManagingApi'
import type { IBacktestCards } from '../../../global/type'
import MoneyManagementModal from '../../../pages/settingsPage/moneymanagement/moneyManagementModal' import MoneyManagementModal from '../../../pages/settingsPage/moneymanagement/moneyManagementModal'
import { CardPosition, CardText, Toast } from '../../mollecules' import {CardPosition, CardText, Toast} from '../../mollecules'
import CardPositionItem from '../Trading/CardPositionItem' import CardPositionItem from '../Trading/CardPositionItem'
import TradeChart from '../Trading/TradeChart/TradeChart' import TradeChart from '../Trading/TradeChart/TradeChart'
@@ -73,6 +64,7 @@ const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
scenario: backtest.scenario, scenario: backtest.scenario,
ticker: backtest.ticker as Ticker, ticker: backtest.ticker as Ticker,
timeframe: backtest.timeframe, timeframe: backtest.timeframe,
initialTradingBalance: 1000,
} }
await client await client
@@ -89,6 +81,10 @@ const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
const t = new Toast('Optimized backtest is running') const t = new Toast('Optimized backtest is running')
const client = new BacktestClient({}, apiUrl) const client = new BacktestClient({}, apiUrl)
// Calculate dates for the API call
const startDate = backtest.candles[0].date
const endDate = backtest.candles[backtest.candles.length - 1].date
await client await client
.backtest_Run( .backtest_Run(
backtest.accountName, backtest.accountName,
@@ -96,12 +92,13 @@ const BacktestCards: React.FC<IBacktestCards> = ({ list, setBacktests }) => {
backtest.ticker as Ticker, backtest.ticker as Ticker,
backtest.scenario, backtest.scenario,
backtest.timeframe, backtest.timeframe,
false, false, // watchOnly
daysBetween(backtest.candles[0].date), backtest.walletBalances[0].value, // balance
backtest.walletBalances[0].value, '', // moneyManagementName (empty since we're passing the optimized moneyManagement object)
'', startDate, // startDate
false, endDate, // endDate
backtest.optimizedMoneyManagement false, // save
backtest.optimizedMoneyManagement // moneyManagement object
) )
.then((backtest: Backtest) => { .then((backtest: Backtest) => {
t.update('success', `${backtest.ticker} Backtest Succeeded`) t.update('success', `${backtest.ticker} Backtest Succeeded`)

View File

@@ -1,28 +1,23 @@
import { useQuery } from '@tanstack/react-query' import {useQuery} from '@tanstack/react-query'
import React, { useEffect, useState } from 'react' import React, {useEffect, useState} from 'react'
import { useForm, type SubmitHandler } from 'react-hook-form' import {type SubmitHandler, useForm} from 'react-hook-form'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import {
Backtest,
MoneyManagement,
Ticker,
} from '../../../generated/ManagingApi'
import { import {
AccountClient, AccountClient,
Backtest,
BacktestClient, BacktestClient,
BotType, BotType,
DataClient, DataClient,
MoneyManagement,
MoneyManagementClient, MoneyManagementClient,
ScenarioClient, ScenarioClient,
Ticker,
Timeframe, Timeframe,
} from '../../../generated/ManagingApi' } from '../../../generated/ManagingApi'
import type { import type {BacktestModalProps, IBacktestsFormInput,} from '../../../global/type'
BacktestModalProps, import {Loader, Slider} from '../../atoms'
IBacktestsFormInput, import {Modal, Toast} from '../../mollecules'
} from '../../../global/type'
import { Loader, Slider } from '../../atoms'
import { Modal, Toast } from '../../mollecules'
import FormInput from '../../mollecules/FormInput/FormInput' import FormInput from '../../mollecules/FormInput/FormInput'
import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement' import CustomMoneyManagement from '../CustomMoneyManagement/CustomMoneyManagement'
@@ -100,6 +95,8 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
// Use the name of the money management strategy if custom is not provided // Use the name of the money management strategy if custom is not provided
const moneyManagementName = customMoneyManagement ? undefined : selectedMoneyManagement const moneyManagementName = customMoneyManagement ? undefined : selectedMoneyManagement
console.log(customMoneyManagement)
backtestClient backtestClient
.backtest_Run( .backtest_Run(
form.accountName, form.accountName,
@@ -122,7 +119,6 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
if (showLoopSlider && selectedLoopQuantity > loopCount) { if (showLoopSlider && selectedLoopQuantity > loopCount) {
const nextCount = loopCount + 1 const nextCount = loopCount + 1
const mm: MoneyManagement = { const mm: MoneyManagement = {
balanceAtRisk: backtest.optimizedMoneyManagement.balanceAtRisk,
leverage: backtest.optimizedMoneyManagement.leverage, leverage: backtest.optimizedMoneyManagement.leverage,
name: backtest.optimizedMoneyManagement.name + nextCount, name: backtest.optimizedMoneyManagement.name + nextCount,
stopLoss: backtest.optimizedMoneyManagement.stopLoss, stopLoss: backtest.optimizedMoneyManagement.stopLoss,
@@ -148,6 +144,7 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
function onMoneyManagementChange(e: any) { function onMoneyManagementChange(e: any) {
if (e.target.value === 'custom') { if (e.target.value === 'custom') {
setShowCustomMoneyManagement(true) setShowCustomMoneyManagement(true)
setCustomMoneyManagement(e.target.value)
} else { } else {
setShowCustomMoneyManagement(false) setShowCustomMoneyManagement(false)
setCustomMoneyManagement(undefined) setCustomMoneyManagement(undefined)
@@ -280,6 +277,32 @@ const BacktestModal: React.FC<BacktestModalProps> = ({
</select> </select>
</FormInput> </FormInput>
{/* Losing streak info */}
{(() => {
// Find the selected money management object
let mm: MoneyManagement | undefined = undefined;
if (showCustomMoneyManagement && customMoneyManagement) {
mm = { ...customMoneyManagement, stopLoss: customMoneyManagement.stopLoss / 100 };
} else if (selectedMoneyManagement) {
mm = moneyManagements.find((m) => m.name === selectedMoneyManagement);
}
// Use actual initial balance and a minimum threshold
const initialBalance = balance;
const minBalance = 10; // You can make this configurable if needed
if (mm && mm.leverage && mm.stopLoss && initialBalance > minBalance) {
const perLoss = mm.leverage * mm.stopLoss;
if (perLoss > 0 && perLoss < 1) {
const streak = Math.floor(Math.log(minBalance / initialBalance) / Math.log(1 - perLoss));
return (
<div className="text-sm text-info mt-1">
Max losing streak before balance drops below {minBalance}: <b>{streak}</b> trades (with leverage x{mm.leverage} and stop loss {Math.round(mm.stopLoss * 10000) / 100}% per trade, starting from {initialBalance})
</div>
);
}
}
return null;
})()}
{showCustomMoneyManagement && ( {showCustomMoneyManagement && (
<div className="mt-6"> <div className="mt-6">
<CustomMoneyManagement <CustomMoneyManagement

View File

@@ -1,23 +1,12 @@
import { import {ChevronDownIcon, ChevronRightIcon, EyeIcon, PlayIcon, TrashIcon} from '@heroicons/react/solid'
ChevronDownIcon, import React, {useEffect, useState} from 'react'
ChevronRightIcon, import {useQuery} from '@tanstack/react-query'
PlayIcon,
TrashIcon,
EyeIcon
} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import type { import type {Backtest, StartBotRequest, Ticker,} from '../../../generated/ManagingApi'
Backtest, import {BacktestClient, BotClient, MoneyManagementClient} from '../../../generated/ManagingApi'
MoneyManagement, import type {IBacktestCards} from '../../../global/type'
StartBotRequest, import {CardText, SelectColumnFilter, Table, Toast} from '../../mollecules'
Ticker,
} from '../../../generated/ManagingApi'
import { BacktestClient, BotClient, MoneyManagementClient } from '../../../generated/ManagingApi'
import type { IBacktestCards } from '../../../global/type'
import { Toast, SelectColumnFilter, Table, CardText } from '../../mollecules'
import BotNameModal from '../../mollecules/Modal/BotNameModal' import BotNameModal from '../../mollecules/Modal/BotNameModal'
import BacktestRowDetails from './backtestRowDetails' import BacktestRowDetails from './backtestRowDetails'
@@ -50,7 +39,7 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
} }
}, [moneyManagements]) }, [moneyManagements])
async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string) { async function runBot(botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) {
const t = new Toast('Bot is starting') const t = new Toast('Bot is starting')
const client = new BotClient({}, apiUrl) const client = new BotClient({}, apiUrl)
@@ -70,6 +59,7 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
scenario: backtest.scenario, scenario: backtest.scenario,
ticker: backtest.ticker as Ticker, ticker: backtest.ticker as Ticker,
timeframe: backtest.timeframe, timeframe: backtest.timeframe,
initialTradingBalance: initialTradingBalance,
} }
await client await client
@@ -92,8 +82,8 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
setShowBotNameModal(false) setShowBotNameModal(false)
} }
const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string) => { const handleSubmitBotName = (botName: string, backtest: Backtest, isForWatchOnly: boolean, moneyManagementName: string, initialTradingBalance: number) => {
runBot(botName, backtest, isForWatchOnly, moneyManagementName) runBot(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
setShowBotNameModal(false) setShowBotNameModal(false)
} }
@@ -364,8 +354,8 @@ const BacktestTable: React.FC<IBacktestCards> = ({ list, isFetching, setBacktest
onClose={handleCloseBotNameModal} onClose={handleCloseBotNameModal}
backtest={currentBacktest} backtest={currentBacktest}
isForWatchOnly={isForWatchOnly} isForWatchOnly={isForWatchOnly}
onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName) => onSubmitBotName={(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance) =>
handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName) handleSubmitBotName(botName, backtest, isForWatchOnly, moneyManagementName, initialTradingBalance)
} }
moneyManagements={moneyManagements} moneyManagements={moneyManagements}
selectedMoneyManagement={selectedMoneyManagement} selectedMoneyManagement={selectedMoneyManagement}

View File

@@ -1,9 +1,8 @@
import React, { useEffect, useState } from 'react' import React, {useEffect, useState} from 'react'
import type { MoneyManagement, Timeframe } from '../../../generated/ManagingApi' import type {MoneyManagement, Timeframe} from '../../../generated/ManagingApi'
import { Slider } from '../../atoms'
import FormInput from '../../mollecules/FormInput/FormInput' import FormInput from '../../mollecules/FormInput/FormInput'
import { useCustomMoneyManagement } from '../../../app/store/customMoneyManagement' import {useCustomMoneyManagement} from '../../../app/store/customMoneyManagement'
type ICustomMoneyManagement = { type ICustomMoneyManagement = {
onCreateMoneyManagement: (moneyManagement: MoneyManagement) => void onCreateMoneyManagement: (moneyManagement: MoneyManagement) => void
@@ -18,14 +17,12 @@ const CustomMoneyManagement: React.FC<ICustomMoneyManagement> = ({
}) => { }) => {
const { moneyManagement, setCustomMoneyManagement } = useCustomMoneyManagement() const { moneyManagement, setCustomMoneyManagement } = useCustomMoneyManagement()
const [balanceAtRisk, setBalanceAtRisk] = useState<number>(moneyManagement?.balanceAtRisk || 5)
const [leverage, setLeverage] = useState<number>(moneyManagement?.leverage || 1) const [leverage, setLeverage] = useState<number>(moneyManagement?.leverage || 1)
const [takeProfit, setTakeProfit] = useState<number>(moneyManagement?.takeProfit || 2) const [takeProfit, setTakeProfit] = useState<number>(moneyManagement?.takeProfit || 2)
const [stopLoss, setStopLoss] = useState<number>(moneyManagement?.stopLoss || 1) const [stopLoss, setStopLoss] = useState<number>(moneyManagement?.stopLoss || 1)
const handleCreateMoneyManagement = () => { const handleCreateMoneyManagement = () => {
const moneyManagement: MoneyManagement = { const moneyManagement: MoneyManagement = {
balanceAtRisk,
leverage, leverage,
name: 'custom', name: 'custom',
stopLoss, stopLoss,
@@ -38,7 +35,7 @@ const CustomMoneyManagement: React.FC<ICustomMoneyManagement> = ({
useEffect(() => { useEffect(() => {
handleCreateMoneyManagement() handleCreateMoneyManagement()
}, [balanceAtRisk, leverage, takeProfit, stopLoss]) }, [leverage, takeProfit, stopLoss])
return ( return (
<> <>
@@ -49,23 +46,6 @@ const CustomMoneyManagement: React.FC<ICustomMoneyManagement> = ({
Custom MoneyManagement Custom MoneyManagement
</div> </div>
<div className="collapse-content"> <div className="collapse-content">
<FormInput
label="Balance at risk"
htmlFor="balanceAtRisk"
inline={false}
>
<input
id="balanceAtRisk"
value={balanceAtRisk}
onChange={(e) => setBalanceAtRisk(parseInt(e.target.value))}
min="1"
max="100"
step="1"
type='number'
className='input input-bordered'
></input>
</FormInput>
<FormInput label="Leverage" htmlFor="leverage" inline={false}> <FormInput label="Leverage" htmlFor="leverage" inline={false}>
<input <input
id="leverage" id="leverage"

View File

@@ -2572,7 +2572,6 @@ export enum TradeDirection {
export interface MoneyManagement { export interface MoneyManagement {
name: string; name: string;
timeframe: Timeframe; timeframe: Timeframe;
balanceAtRisk: number;
stopLoss: number; stopLoss: number;
takeProfit: number; takeProfit: number;
leverage: number; leverage: number;
@@ -2842,6 +2841,7 @@ export interface StartBotRequest {
scenario: string; scenario: string;
accountName: string; accountName: string;
moneyManagementName: string; moneyManagementName: string;
initialTradingBalance: number;
} }
export interface TradingBot { export interface TradingBot {

View File

@@ -1,27 +1,20 @@
import { useEffect, useState } from 'react' import {useEffect, useState} from 'react'
import type { SubmitHandler } from 'react-hook-form' import type {SubmitHandler} from 'react-hook-form'
import { useForm } from 'react-hook-form' import {useForm} from 'react-hook-form'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import { Slider } from '../../../components/atoms' import {Slider} from '../../../components/atoms'
import { FormInput, Modal, Toast } from '../../../components/mollecules' import {FormInput, Modal, Toast} from '../../../components/mollecules'
import type { MoneyManagement } from '../../../generated/ManagingApi' import type {MoneyManagement} from '../../../generated/ManagingApi'
import { import {MoneyManagementClient, Timeframe,} from '../../../generated/ManagingApi'
MoneyManagementClient, import type {IMoneyManagementFormInput, IMoneyManagementModalProps,} from '../../../global/type'
Timeframe,
} from '../../../generated/ManagingApi'
import type {
IMoneyManagementModalProps,
IMoneyManagementFormInput,
} from '../../../global/type'
const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({ const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({
showModal, showModal,
onClose, onClose,
moneyManagement, moneyManagement,
disableInputs = false, disableInputs = false,
}) => { }) => {
const [balanceAtRisk, setBalanceAtRisk] = useState<number>(5)
const [takeProfit, setTakeProfit] = useState<number>(20) const [takeProfit, setTakeProfit] = useState<number>(20)
const [name, setName] = useState<string>('') const [name, setName] = useState<string>('')
const [stopLoss, setStopLoss] = useState<number>(10) const [stopLoss, setStopLoss] = useState<number>(10)
@@ -29,14 +22,13 @@ const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({
const [timeframe, setTimeframe] = useState<Timeframe>( const [timeframe, setTimeframe] = useState<Timeframe>(
Timeframe.FifteenMinutes Timeframe.FifteenMinutes
) )
const { reset, register, handleSubmit } = useForm<IMoneyManagementFormInput>() const {reset, register, handleSubmit} = useForm<IMoneyManagementFormInput>()
const { apiUrl } = useApiUrlStore() const {apiUrl} = useApiUrlStore()
async function createMoneyManagement(form: IMoneyManagementFormInput) { async function createMoneyManagement(form: IMoneyManagementFormInput) {
const t = new Toast('Creating settings') const t = new Toast('Creating settings')
const client = new MoneyManagementClient({}, apiUrl) const client = new MoneyManagementClient({}, apiUrl)
const mm: MoneyManagement = { const mm: MoneyManagement = {
balanceAtRisk: balanceAtRisk / 100,
leverage: leverage, leverage: leverage,
name: name, name: name,
stopLoss: stopLoss / 100, stopLoss: stopLoss / 100,
@@ -53,6 +45,7 @@ const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({
t.update('error', 'Error :' + err) t.update('error', 'Error :' + err)
}) })
} }
const onSubmit: SubmitHandler<IMoneyManagementFormInput> = async (form) => { const onSubmit: SubmitHandler<IMoneyManagementFormInput> = async (form) => {
// @ts-ignore // @ts-ignore
await createMoneyManagement(form) await createMoneyManagement(form)
@@ -75,14 +68,10 @@ const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({
setLeverage(e.target.value) setLeverage(e.target.value)
} }
function onBalanceAtRiskChange(e: any) {
setBalanceAtRisk(e.target.value)
}
useEffect(() => { useEffect(() => {
if (moneyManagement) { if (moneyManagement) {
console.log(moneyManagement) console.log(moneyManagement)
setBalanceAtRisk(moneyManagement.balanceAtRisk * 100)
setTakeProfit(moneyManagement.takeProfit * 100) setTakeProfit(moneyManagement.takeProfit * 100)
setStopLoss(moneyManagement.stopLoss * 100) setStopLoss(moneyManagement.stopLoss * 100)
setLeverage(moneyManagement.leverage) setLeverage(moneyManagement.leverage)
@@ -90,14 +79,13 @@ const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({
setName(moneyManagement.name) setName(moneyManagement.name)
const defaultValues: MoneyManagement = { const defaultValues: MoneyManagement = {
balanceAtRisk: moneyManagement.balanceAtRisk,
leverage: moneyManagement.leverage, leverage: moneyManagement.leverage,
name: moneyManagement.name || '', name: moneyManagement.name || '',
stopLoss: moneyManagement.stopLoss, stopLoss: moneyManagement.stopLoss,
takeProfit: moneyManagement.takeProfit, takeProfit: moneyManagement.takeProfit,
timeframe: moneyManagement.timeframe, timeframe: moneyManagement.timeframe,
} }
reset({ ...defaultValues }) reset({...defaultValues})
} }
}, [showModal, moneyManagement]) }, [showModal, moneyManagement])
@@ -136,18 +124,6 @@ const MoneyManagementModal: React.FC<IMoneyManagementModalProps> = ({
</select> </select>
</FormInput> </FormInput>
<FormInput label="Balance at risk" htmlFor="balanceAtRisk">
<Slider
id="balanceAtRisk"
value={balanceAtRisk}
onChange={onBalanceAtRiskChange}
step="1"
max="100"
suffixValue="%"
disabled={disableInputs}
></Slider>
</FormInput>
<FormInput label="TP%" htmlFor="takeProfit"> <FormInput label="TP%" htmlFor="takeProfit">
<Slider <Slider
id="takeProfit" id="takeProfit"

View File

@@ -1,15 +1,11 @@
import { PencilAltIcon, TrashIcon } from '@heroicons/react/solid' import {PencilAltIcon, TrashIcon} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react' import React, {useEffect, useState} from 'react'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import { import {SelectColumnFilter, Table, Toast,} from '../../../components/mollecules'
Toast, import type {MoneyManagement} from '../../../generated/ManagingApi'
SelectColumnFilter, import {MoneyManagementClient} from '../../../generated/ManagingApi'
Table, import type {IMoneyManagementList} from '../../../global/type'
} from '../../../components/mollecules'
import type { MoneyManagement } from '../../../generated/ManagingApi'
import { MoneyManagementClient } from '../../../generated/ManagingApi'
import type { IMoneyManagementList } from '../../../global/type'
import MoneyManagementModal from './moneyManagementModal' import MoneyManagementModal from './moneyManagementModal'
@@ -63,13 +59,7 @@ const MoneyManagementTable: React.FC<IMoneyManagementList> = ({ list }) => {
disableSortBy: true, disableSortBy: true,
}, },
{ {
Cell: ({ cell }: any) => <>{cell.row.values.balanceAtRisk * 100} %</>, Cell: ({ cell }: any) => <>{cell.row.values.stopLoss * 100} %</>,
Header: 'Balance used',
accessor: 'balanceAtRisk',
disableFilters: true,
},
{
Cell: ({ cell }) => <>{cell.row.values.stopLoss * 100} %</>,
Header: 'SL', Header: 'SL',
accessor: 'stopLoss', accessor: 'stopLoss',
disableFilters: true, disableFilters: true,