Add new parameters

This commit is contained in:
2025-06-03 01:01:17 +07:00
parent 71bcaea76d
commit 8c2e9b59de
23 changed files with 1346 additions and 510 deletions

View File

@@ -43,6 +43,7 @@ public class TradingBot : Bot, ITradingBot
public decimal Fee { get; set; }
public Scenario Scenario { get; set; }
public TradingBot(
IExchangeService exchangeService,
ILogger<TradingBot> logger,
@@ -293,11 +294,12 @@ public class TradingBot : Bot, ITradingBot
{
// Get the candle that corresponds to the position opening time
var positionCandle = OptimizedCandles.FirstOrDefault(c => c.Date <= position.Open.Date)
?? OptimizedCandles.LastOrDefault();
?? OptimizedCandles.LastOrDefault();
if (positionCandle == null)
{
await LogWarning($"Cannot find candle for position {position.Identifier} opened at {position.Open.Date}");
await LogWarning(
$"Cannot find candle for position {position.Identifier} opened at {position.Open.Date}");
return null;
}
@@ -317,14 +319,15 @@ public class TradingBot : Bot, ITradingBot
// to use the new signal identifier, or find another approach
// For now, let's update the position's SignalIdentifier to match the recreated signal
position.SignalIdentifier = recreatedSignal.Identifier;
recreatedSignal.Status = SignalStatus.PositionOpen;
recreatedSignal.User = Account.User;
// Add the recreated signal to our collection
Signals.Add(recreatedSignal);
await LogInformation($"Successfully recreated signal {recreatedSignal.Identifier} for position {position.Identifier}");
await LogInformation(
$"Successfully recreated signal {recreatedSignal.Identifier} for position {position.Identifier}");
return recreatedSignal;
}
catch (Exception ex)
@@ -343,10 +346,10 @@ public class TradingBot : Bot, ITradingBot
if (signalForPosition == null)
{
await LogInformation($"Signal not found for position {position.Identifier}. Recreating signal...");
// Recreate the signal based on position information
signalForPosition = await RecreateSignalFromPosition(position);
if (signalForPosition == null)
{
await LogWarning($"Failed to recreate signal for position {position.Identifier}");
@@ -357,7 +360,8 @@ public class TradingBot : Bot, ITradingBot
// Ensure signal status is correctly set to PositionOpen if position is not finished
if (signalForPosition.Status != SignalStatus.PositionOpen)
{
await LogInformation($"Updating signal {signalForPosition.Identifier} status from {signalForPosition.Status} to PositionOpen");
await LogInformation(
$"Updating signal {signalForPosition.Identifier} status from {signalForPosition.Status} to PositionOpen");
SetSignalStatus(signalForPosition.Identifier, SignalStatus.PositionOpen);
}
@@ -462,6 +466,33 @@ public class TradingBot : Bot, ITradingBot
? OptimizedCandles.Last()
: ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
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 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
if (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})");
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}). " +
$"Waiting for profit or breakeven before closing.");
}
}
if (positionForSignal.OriginDirection == TradeDirection.Long)
{
if (positionForSignal.StopLoss.Price >= lastCandle.Low)
@@ -583,10 +614,18 @@ public class TradingBot : Bot, ITradingBot
{
// Check if current position is in profit before flipping
var isPositionInProfit = await IsPositionInProfit(openedPosition, lastPrice);
if (isPositionInProfit)
// Determine if we should flip based on configuration
var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
if (shouldFlip)
{
await LogInformation("Try to flip the position because of an opposite direction signal and current position is in profit");
var flipReason = Config.FlipOnlyWhenInProfit
? "current position is in profit"
: "FlipOnlyWhenInProfit is disabled";
await LogInformation(
$"Try to flip the position because of an opposite direction signal and {flipReason}");
await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true);
await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped);
await OpenPosition(signal);
@@ -598,7 +637,7 @@ public class TradingBot : Bot, ITradingBot
await LogInformation(
$"Position {previousSignal.Identifier} is not in profit (entry: {openedPosition.Open.Price}, current: {lastPrice}). " +
$"Signal {signal.Identifier} will wait for position to become profitable before flipping.");
// Keep signal in waiting status to check again on next execution
SetSignalStatus(signal.Identifier, SignalStatus.WaitingForPosition);
return;
@@ -871,10 +910,10 @@ public class TradingBot : Bot, ITradingBot
if (Positions.Any(p => p.Identifier == position.Identifier))
{
// Update the close date for the trade that actually closed the position
var currentCandle = Config.IsForBacktest
? OptimizedCandles.LastOrDefault()
var currentCandle = Config.IsForBacktest
? OptimizedCandles.LastOrDefault()
: ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
if (currentCandle != null && position.ProfitAndLoss != null)
{
// Determine which trade closed the position based on realized P&L
@@ -1111,6 +1150,9 @@ public class TradingBot : Bot, ITradingBot
BotTradingBalance = Config.BotTradingBalance,
StartupTime = StartupTime,
CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours ?? 0m,
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
};
BotService.SaveOrUpdateBotBackup(User, Identifier, Config.BotType, Status, JsonConvert.SerializeObject(data));
}
@@ -1131,6 +1173,8 @@ public class TradingBot : Bot, ITradingBot
BotType = data.BotType,
CooldownPeriod = data.CooldownPeriod,
MaxLossStreak = data.MaxLossStreak,
MaxPositionTimeHours = data.MaxPositionTimeHours == 0m ? null : data.MaxPositionTimeHours,
FlipOnlyWhenInProfit = data.FlipOnlyWhenInProfit,
Name = data.Name
};
@@ -1205,6 +1249,140 @@ public class TradingBot : Bot, ITradingBot
throw new ArgumentException("Invalid position direction");
}
}
/// <summary>
/// Checks if a position has exceeded the maximum time limit for being open.
/// </summary>
/// <param name="position">The position to check</param>
/// <param name="currentTime">The current time to compare against</param>
/// <returns>True if the position has exceeded the time limit, false otherwise</returns>
private bool HasPositionExceededTimeLimit(Position position, DateTime currentTime)
{
if (!Config.MaxPositionTimeHours.HasValue)
{
return false; // Time-based closure is disabled
}
var timeOpen = currentTime - position.Open.Date;
var maxTimeAllowed = TimeSpan.FromHours((double)Config.MaxPositionTimeHours.Value);
return timeOpen >= maxTimeAllowed;
}
/// <summary>
/// Updates the trading bot configuration with new settings.
/// This method validates the new configuration and applies it to the running bot.
/// </summary>
/// <param name="newConfig">The new configuration to apply</param>
/// <returns>True if the configuration was successfully updated, false otherwise</returns>
/// <exception cref="ArgumentException">Thrown when the new configuration is invalid</exception>
public async Task<bool> UpdateConfiguration(TradingBotConfig newConfig)
{
try
{
// Validate the new configuration
if (newConfig == null)
{
throw new ArgumentException("Configuration cannot be null");
}
if (newConfig.BotTradingBalance <= Constants.GMX.Config.MinimumPositionAmount)
{
throw new ArgumentException(
$"Bot trading balance must be greater than {Constants.GMX.Config.MinimumPositionAmount}");
}
if (string.IsNullOrEmpty(newConfig.AccountName))
{
throw new ArgumentException("Account name cannot be null or empty");
}
if (string.IsNullOrEmpty(newConfig.ScenarioName))
{
throw new ArgumentException("Scenario name cannot be null or empty");
}
// Protect critical properties that shouldn't change for running bots
var protectedBotType = Config.BotType;
var protectedIsForBacktest = Config.IsForBacktest;
var protectedName = Config.Name;
// Log the configuration update
await LogInformation($"Updating bot configuration. Previous config: " +
$"Balance: {Config.BotTradingBalance}, " +
$"MaxTime: {Config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
$"FlipOnlyProfit: {Config.FlipOnlyWhenInProfit}, " +
$"Cooldown: {Config.CooldownPeriod}, " +
$"MaxLoss: {Config.MaxLossStreak}");
// Update the configuration
Config = newConfig;
// Restore protected properties
Config.BotType = protectedBotType;
Config.IsForBacktest = protectedIsForBacktest;
Config.Name = protectedName;
// If account changed, reload it
if (Config.AccountName != Account?.Name)
{
await LoadAccount();
}
// If scenario changed, reload it
var currentScenario = Scenario?.Name;
if (Config.ScenarioName != currentScenario)
{
LoadScenario(Config.ScenarioName);
}
await LogInformation($"Bot configuration updated successfully. New config: " +
$"Balance: {Config.BotTradingBalance}, " +
$"MaxTime: {Config.MaxPositionTimeHours?.ToString() ?? "Disabled"}, " +
$"FlipOnlyProfit: {Config.FlipOnlyWhenInProfit}, " +
$"Cooldown: {Config.CooldownPeriod}, " +
$"MaxLoss: {Config.MaxLossStreak}");
// Save the updated configuration as backup
if (!Config.IsForBacktest)
{
SaveBackup();
}
return true;
}
catch (Exception ex)
{
await LogWarning($"Failed to update bot configuration: {ex.Message}");
return false;
}
}
/// <summary>
/// Gets the current trading bot configuration.
/// </summary>
/// <returns>A copy of the current configuration</returns>
public TradingBotConfig GetConfiguration()
{
return new TradingBotConfig
{
AccountName = Config.AccountName,
MoneyManagement = Config.MoneyManagement,
Ticker = Config.Ticker,
ScenarioName = Config.ScenarioName,
Timeframe = Config.Timeframe,
IsForWatchingOnly = Config.IsForWatchingOnly,
BotTradingBalance = Config.BotTradingBalance,
BotType = Config.BotType,
IsForBacktest = Config.IsForBacktest,
CooldownPeriod = Config.CooldownPeriod,
MaxLossStreak = Config.MaxLossStreak,
MaxPositionTimeHours = Config.MaxPositionTimeHours,
FlipOnlyWhenInProfit = Config.FlipOnlyWhenInProfit,
FlipPosition = Config.FlipPosition,
Name = Config.Name
};
}
}
public class TradingBotBackup
@@ -1224,4 +1402,6 @@ public class TradingBotBackup
public decimal BotTradingBalance { get; set; }
public int CooldownPeriod { get; set; }
public int MaxLossStreak { get; set; }
public decimal MaxPositionTimeHours { get; set; }
public bool FlipOnlyWhenInProfit { get; set; }
}