Update bot config on front and back

This commit is contained in:
2025-06-04 15:42:21 +07:00
parent f41af96406
commit 756cd5fb11
14 changed files with 422 additions and 369 deletions

View File

@@ -1,13 +1,11 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Repositories;
using Managing.Application.Abstractions.Services;
using Managing.Core;
using Managing.Core.FixedSizedQueue;
using Managing.Domain.Accounts;
using Managing.Domain.Backtests;
using Managing.Domain.Bots;
using Managing.Domain.Candles;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers;
using Managing.Domain.Strategies;
@@ -26,18 +24,22 @@ namespace Managing.Application.Backtesting
private readonly IExchangeService _exchangeService;
private readonly IBotFactory _botFactory;
private readonly IScenarioService _scenarioService;
private readonly IAccountService _accountService;
public Backtester(
IExchangeService exchangeService,
IBotFactory botFactory,
IBacktestRepository backtestRepository,
ILogger<Backtester> logger, IScenarioService scenarioService)
ILogger<Backtester> logger,
IScenarioService scenarioService,
IAccountService accountService)
{
_exchangeService = exchangeService;
_botFactory = botFactory;
_backtestRepository = backtestRepository;
_logger = logger;
_scenarioService = scenarioService;
_accountService = accountService;
}
public Backtest RunSimpleBotBacktest(Workflow workflow, bool save = false)
@@ -54,45 +56,19 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
decimal balance,
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
List<Candle>? initialCandles = null)
{
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = balance,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var scalpingBot = _botFactory.CreateBacktestScalpingBot(config);
scalpingBot.LoadScenario(scenario.Name);
scalpingBot.LoadScenario(config.ScenarioName);
scalpingBot.User = user;
await scalpingBot.LoadAccount();
var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate);
var candles = initialCandles ?? GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate);
var result = GetBacktestingResult(config, scalpingBot, candles);
if (user != null)
@@ -113,46 +89,20 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Ticker ticker,
Scenario scenario,
Timeframe timeframe,
decimal balance,
TradingBotConfig config,
DateTime startDate,
DateTime endDate,
User user = null,
bool isForWatchingOnly = false,
bool save = false,
List<Candle>? initialCandles = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
List<Candle>? initialCandles = null)
{
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = isForWatchingOnly,
BotTradingBalance = balance,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var flippingBot = _botFactory.CreateBacktestFlippingBot(config);
flippingBot.LoadScenario(scenario.Name);
flippingBot.LoadScenario(config.ScenarioName);
flippingBot.User = user;
await flippingBot.LoadAccount();
var candles = initialCandles ?? GetCandles(account, ticker, timeframe, startDate, endDate);
var candles = initialCandles ?? GetCandles(account, config.Ticker, config.Timeframe, startDate, endDate);
var result = GetBacktestingResult(config, flippingBot, candles);
if (user != null)
@@ -173,38 +123,13 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunScalpingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
TradingBotConfig config,
List<Candle> candles,
decimal balance,
User user = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
User user = null)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
BotType = BotType.ScalpingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var bot = _botFactory.CreateBacktestScalpingBot(config);
bot.LoadScenario(scenario.Name);
bot.LoadScenario(config.ScenarioName);
bot.User = user;
await bot.LoadAccount();
@@ -219,38 +144,13 @@ namespace Managing.Application.Backtesting
}
public async Task<Backtest> RunFlippingBotBacktest(
Account account,
MoneyManagement moneyManagement,
Scenario scenario,
Timeframe timeframe,
TradingBotConfig config,
List<Candle> candles,
decimal balance,
User user = null,
int cooldownPeriod = 1,
int maxLossStreak = 0,
decimal? maxPositionTimeHours = null,
bool flipOnlyWhenInProfit = true)
User user = null)
{
var ticker = MiscExtensions.ParseEnum<Ticker>(candles.FirstOrDefault().Ticker);
var config = new TradingBotConfig
{
AccountName = account.Name,
MoneyManagement = moneyManagement,
Ticker = ticker,
ScenarioName = scenario.Name,
Timeframe = timeframe,
IsForWatchingOnly = false,
BotTradingBalance = balance,
BotType = BotType.FlippingBot,
IsForBacktest = true,
CooldownPeriod = cooldownPeriod,
MaxLossStreak = maxLossStreak,
MaxPositionTimeHours = maxPositionTimeHours,
FlipOnlyWhenInProfit = flipOnlyWhenInProfit
};
var account = await GetAccountFromConfig(config);
var bot = _botFactory.CreateBacktestFlippingBot(config);
bot.LoadScenario(scenario.Name);
bot.LoadScenario(config.ScenarioName);
bot.User = user;
await bot.LoadAccount();
@@ -264,6 +164,23 @@ namespace Managing.Application.Backtesting
return result;
}
private async Task<Account> GetAccountFromConfig(TradingBotConfig config)
{
// Use the account service to get the actual account
var account = await _accountService.GetAccount(config.AccountName, false, false);
if (account != null)
{
return account;
}
// Fallback: create a basic account structure if not found
return new Account
{
Name = config.AccountName,
Exchange = TradingExchanges.GmxV2 // Default exchange, should be configurable
};
}
private List<Candle> GetCandles(Account account, Ticker ticker, Timeframe timeframe,
DateTime startDate, DateTime endDate)
{

View File

@@ -468,28 +468,55 @@ public class TradingBot : Bot, ITradingBot
var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow;
// Check if position has exceeded maximum time limit (only if MaxPositionTimeHours is set)
if (Config.MaxPositionTimeHours.HasValue && HasPositionExceededTimeLimit(positionForSignal, currentTime))
// Check time-based position management (only if MaxPositionTimeHours is set)
if (Config.MaxPositionTimeHours.HasValue)
{
// Check if position is in profit or at breakeven before closing
var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close);
var isAtBreakeven = Math.Abs(lastCandle.Close - positionForSignal.Open.Price) < 0.01m; // Small tolerance for breakeven
var hasExceededTimeLimit = HasPositionExceededTimeLimit(positionForSignal, currentTime);
if (isPositionInProfit || isAtBreakeven)
// Calculate current unrealized PNL for logging
var currentPnl = CalculateUnrealizedPnl(positionForSignal, lastCandle.Close);
var pnlPercentage = Math.Round((currentPnl / (positionForSignal.Open.Price * positionForSignal.Open.Quantity)) * 100, 2);
// Early closure logic when CloseEarlyWhenProfitable is enabled
if (Config.CloseEarlyWhenProfitable && (isPositionInProfit || isAtBreakeven))
{
await LogInformation(
$"Closing position due to time limit - Position opened at {positionForSignal.Open.Date}, " +
$"current time {currentTime}, max time limit {Config.MaxPositionTimeHours} hours. " +
$"Position is {(isPositionInProfit ? "in profit" : "at breakeven")} (entry: {positionForSignal.Open.Price}, current: {lastCandle.Close})");
$"Closing position early due to profitability - Position opened at {positionForSignal.Open.Date}, " +
$"current time {currentTime}. Position is {(isPositionInProfit ? "in profit" : "at breakeven")} " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is enabled.");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
else
// Time limit exceeded logic
if (hasExceededTimeLimit)
{
await LogInformation(
$"Position has exceeded time limit ({Config.MaxPositionTimeHours} hours) but is at a loss " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Waiting for profit or breakeven before closing.");
if (Config.CloseEarlyWhenProfitable || isPositionInProfit || isAtBreakeven)
{
// Close when time limit is reached if:
// 1. CloseEarlyWhenProfitable is enabled (safety net), OR
// 2. Position is in profit/breakeven (when CloseEarlyWhenProfitable is disabled)
await LogInformation(
$"Closing position due to time limit - Position opened at {positionForSignal.Open.Date}, " +
$"current time {currentTime}, max time limit {Config.MaxPositionTimeHours} hours. " +
$"Position is {(isPositionInProfit ? "in profit" : isAtBreakeven ? "at breakeven" : "at a loss")} " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%)");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
else
{
await LogInformation(
$"Position has exceeded time limit ({Config.MaxPositionTimeHours} hours) but is at a loss " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is disabled - waiting for profit or breakeven before closing.");
}
}
}
@@ -1269,6 +1296,28 @@ public class TradingBot : Bot, ITradingBot
return timeOpen >= maxTimeAllowed;
}
/// <summary>
/// Calculates the current unrealized PNL for a position
/// </summary>
/// <param name="position">The position to calculate PNL for</param>
/// <param name="currentPrice">The current market price</param>
/// <returns>The current unrealized PNL</returns>
private decimal CalculateUnrealizedPnl(Position position, decimal currentPrice)
{
if (position.OriginDirection == TradeDirection.Long)
{
return currentPrice - position.Open.Price;
}
else if (position.OriginDirection == TradeDirection.Short)
{
return position.Open.Price - currentPrice;
}
else
{
throw new ArgumentException("Invalid position direction");
}
}
/// <summary>
/// Updates the trading bot configuration with new settings.
/// This method validates the new configuration and applies it to the running bot.