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

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

View File

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

View File

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

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; }
}