Fixes for bots running (#22)

* Fixes for bots running

* Up botmanager

* Add cooldown

* Refact can open position

* Add cooldown Period and MaxLossStreak

* Add agentName

* Add env variable for botManager

* Always enable Botmanager

* Fix bot handle

* Fix get positions

* Add Ticker url

* Dont start stopped bot

* fix
This commit is contained in:
Oda
2025-05-09 17:40:31 +02:00
committed by GitHub
parent a8eb0aaf02
commit 7c38c27b4a
54 changed files with 5164 additions and 641 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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