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

@@ -2,6 +2,7 @@
using Managing.Application.Abstractions.Services;
using Managing.Application.Trading;
using Managing.Application.Trading.Commands;
using Managing.Common;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Bots;
@@ -50,6 +51,11 @@ public class TradingBot : Bot, ITradingBot
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 TradingBot(
string accountName,
MoneyManagement moneyManagement,
@@ -63,6 +69,7 @@ public class TradingBot : Bot, ITradingBot
IAccountService accountService,
IMessengerService messengerService,
IBotService botService,
decimal initialTradingBalance,
bool isForBacktest = false,
bool isForWatchingOnly = false,
bool flipPosition = false)
@@ -74,6 +81,13 @@ public class TradingBot : Bot, ITradingBot
TradingService = tradingService;
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;
FlipPosition = flipPosition;
AccountName = accountName;
@@ -83,6 +97,7 @@ public class TradingBot : Bot, ITradingBot
Timeframe = timeframe;
IsForBacktest = isForBacktest;
Logger = logger;
BotTradingBalance = initialTradingBalance;
Strategies = new HashSet<IStrategy>();
Signals = new HashSet<Signal>();
@@ -172,6 +187,17 @@ public class TradingBot : Bot, ITradingBot
{
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(
$"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)
{
WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest);
// WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest);
WalletBalances[date] = BotTradingBalance;
return;
}
@@ -555,10 +582,9 @@ public class TradingBot : Bot, ITradingBot
PositionInitiator.Bot,
signal.Date,
User,
BotTradingBalance,
IsForBacktest,
lastPrice,
balance: WalletBalances.LastOrDefault().Value,
fee: Fee,
signalIdentifier: signal.Identifier);
var position = await new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService)
@@ -591,7 +617,6 @@ public class TradingBot : Bot, ITradingBot
// Keep signal open for debug purpose
//SetSignalStatus(signal.Identifier, SignalStatus.Expired);
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}");
}
}
@@ -599,8 +624,9 @@ public class TradingBot : Bot, ITradingBot
private bool CanOpenPosition(Signal signal)
{
if (ExecutionCount < 1)
if (!IsForBacktest && ExecutionCount < 1)
return false;
if (Positions.Count == 0)
return true;
@@ -639,7 +665,6 @@ public class TradingBot : Bot, ITradingBot
}
else
{
position.Initiator = PositionInitiator.Bot;
var command = new ClosePositionCommand(position, lastPrice);
try
{
@@ -681,6 +706,18 @@ public class TradingBot : Bot, ITradingBot
await SetPositionStatus(position.SignalIdentifier, PositionStatus.Finished);
Logger.LogInformation(
$"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
{
@@ -735,8 +772,12 @@ public class TradingBot : Bot, ITradingBot
private async Task SetPositionStatus(string signalIdentifier, PositionStatus positionStatus)
{
await LogInformation($"Position {signalIdentifier} is now {positionStatus}");
Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
if (!Positions.First(p => p.SignalIdentifier == signalIdentifier).Status.Equals(positionStatus))
{
await LogInformation($"Position {signalIdentifier} is now {positionStatus}");
Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus;
}
SetSignalStatus(signalIdentifier,
positionStatus == PositionStatus.Filled ? SignalStatus.PositionOpen : SignalStatus.Expired);
}
@@ -799,6 +840,29 @@ public class TradingBot : Bot, ITradingBot
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()
{
IsForWatchingOnly = (!IsForWatchingOnly);
@@ -813,6 +877,7 @@ public class TradingBot : Bot, ITradingBot
private async Task LogWarning(string message)
{
message = $"[{Name}][{Identifier}] {message}";
Logger.LogWarning(message);
SentrySdk.CaptureException(new Exception(message));
await SendTradeMessage(message, true);
@@ -841,6 +906,7 @@ public class TradingBot : Bot, ITradingBot
IsForWatchingOnly = IsForWatchingOnly,
WalletBalances = WalletBalances,
MoneyManagement = MoneyManagement,
BotTradingBalance = BotTradingBalance,
StartupTime = StartupTime,
};
BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data));
@@ -858,6 +924,7 @@ public class TradingBot : Bot, ITradingBot
ScenarioName = data.ScenarioName;
AccountName = data.AccountName;
IsForWatchingOnly = data.IsForWatchingOnly;
BotTradingBalance = data.BotTradingBalance;
// Restore the startup time if it was previously saved
if (data.StartupTime != DateTime.MinValue)
@@ -902,10 +969,6 @@ public class TradingBot : Bot, ITradingBot
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}");
return position;
}
@@ -925,4 +988,5 @@ public class TradingBotBackup
public Dictionary<DateTime, decimal> WalletBalances { get; set; }
public MoneyManagement MoneyManagement { get; set; }
public DateTime StartupTime { get; set; }
public decimal BotTradingBalance { get; set; }
}