fix close early in profit

This commit is contained in:
2025-06-07 16:35:14 +07:00
parent aac28adebe
commit 1e50703da3

View File

@@ -467,148 +467,98 @@ public class TradingBot : Bot, ITradingBot
: ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow); : ExchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow; var currentTime = Config.IsForBacktest ? lastCandle.Date : DateTime.UtcNow;
var currentPnl = positionForSignal.ProfitAndLoss?.Realized ?? 0;
var pnlPercentage = positionForSignal.Open.Price * positionForSignal.Open.Quantity != 0
? Math.Round((currentPnl / (positionForSignal.Open.Price * positionForSignal.Open.Quantity)) * 100,
2)
: 0;
// Check time-based position management (only if MaxPositionTimeHours is set) // Check if position is in profit by comparing entry price with current market price
if (Config.MaxPositionTimeHours.HasValue) var isPositionInProfit = positionForSignal.OriginDirection == TradeDirection.Long
? lastCandle.Close > positionForSignal.Open.Price
: lastCandle.Close < positionForSignal.Open.Price;
var hasExceededTimeLimit = Config.MaxPositionTimeHours.HasValue &&
HasPositionExceededTimeLimit(positionForSignal, currentTime);
// 2. Time-based closure (if time limit exceeded)
if (hasExceededTimeLimit)
{ {
var hasExceededTimeLimit = HasPositionExceededTimeLimit(positionForSignal, currentTime); // If CloseEarlyWhenProfitable is enabled, only close if profitable
// If CloseEarlyWhenProfitable is disabled, close regardless of profit status
var shouldCloseOnTimeLimit = !Config.CloseEarlyWhenProfitable || isPositionInProfit;
// Calculate current unrealized PNL for logging if (shouldCloseOnTimeLimit)
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)
{ {
var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close); var profitStatus = isPositionInProfit ? "in profit" : "at a loss";
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 ({Config.MaxPositionTimeHours} hours exceeded) - " +
await LogInformation( $"Position is {profitStatus} (entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Closing position early due to profitability - Position opened at {positionForSignal.Open.Date}, " + $"Realized PNL: ${currentPnl:F2} ({pnlPercentage:F2}%)");
$"current time {currentTime}. Position is {(isPositionInProfit ? "in profit" : "at breakeven")} " + await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " + return;
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is enabled.");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
// Time limit exceeded logic when CloseEarlyWhenProfitable is enabled
if (hasExceededTimeLimit && (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}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is enabled.");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
else if (hasExceededTimeLimit)
{
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 enabled - waiting for profit or breakeven before closing.");
}
} }
else else
{ {
// Time limit exceeded logic when CloseEarlyWhenProfitable is disabled await LogInformation(
if (hasExceededTimeLimit) $"Time limit exceeded but position is at a loss " +
{ $"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close); $"Realized PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
var profitStatus = isPositionInProfit ? "in profit" : $"Waiting for profit before closing (CloseEarlyWhenProfitable enabled).");
Math.Abs(lastCandle.Close - positionForSignal.Open.Price) < 0.01m ? "at breakeven" : "at a loss";
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 {profitStatus} " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Current PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"CloseEarlyWhenProfitable is disabled - closing regardless of profit status.");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
} }
} }
// 3. Normal stop loss and take profit checks
if (positionForSignal.OriginDirection == TradeDirection.Long) if (positionForSignal.OriginDirection == TradeDirection.Long)
{ {
if (positionForSignal.StopLoss.Price >= lastCandle.Low) if (positionForSignal.StopLoss.Price >= lastCandle.Low)
{ {
await LogInformation( await LogInformation($"Closing position - SL hit at {positionForSignal.StopLoss.Price}");
$"Closing position - SL {positionForSignal.StopLoss.Price} >= Price {lastCandle.Low}");
await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss,
positionForSignal.StopLoss.Price, true); positionForSignal.StopLoss.Price, true);
positionForSignal.StopLoss.SetStatus(TradeStatus.Filled); positionForSignal.StopLoss.SetStatus(TradeStatus.Filled);
} }
else if (positionForSignal.TakeProfit1.Price <= lastCandle.High else if (positionForSignal.TakeProfit1.Price <= lastCandle.High &&
&& positionForSignal.TakeProfit1.Status != TradeStatus.Filled) positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
{ {
await LogInformation( await LogInformation($"Closing position - TP1 hit at {positionForSignal.TakeProfit1.Price}");
$"Closing position - TP1 {positionForSignal.TakeProfit1.Price} <= Price {lastCandle.High}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1,
positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null); positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled);
} }
else if (positionForSignal.TakeProfit2?.Price <= lastCandle.High) else if (positionForSignal.TakeProfit2?.Price <= lastCandle.High)
{ {
await LogInformation( await LogInformation($"Closing position - TP2 hit at {positionForSignal.TakeProfit2.Price}");
$"Closing position - TP2 {positionForSignal.TakeProfit2.Price} <= Price {lastCandle.High}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2,
positionForSignal.TakeProfit2.Price, true); positionForSignal.TakeProfit2.Price, true);
positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled); positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled);
} }
else
{
Logger.LogInformation(
$"Position {signal.Identifier} don't need to be update. Position still opened");
}
} }
else if (positionForSignal.OriginDirection == TradeDirection.Short)
if (positionForSignal.OriginDirection == TradeDirection.Short)
{ {
if (positionForSignal.StopLoss.Price <= lastCandle.High) if (positionForSignal.StopLoss.Price <= lastCandle.High)
{ {
await LogInformation( await LogInformation($"Closing position - SL hit at {positionForSignal.StopLoss.Price}");
$"Closing position - SL {positionForSignal.StopLoss.Price} <= Price {lastCandle.High}");
await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss,
positionForSignal.StopLoss.Price, true); positionForSignal.StopLoss.Price, true);
positionForSignal.StopLoss.SetStatus(TradeStatus.Filled); positionForSignal.StopLoss.SetStatus(TradeStatus.Filled);
} }
else if (positionForSignal.TakeProfit1.Price >= lastCandle.Low else if (positionForSignal.TakeProfit1.Price >= lastCandle.Low &&
&& positionForSignal.TakeProfit1.Status != TradeStatus.Filled) positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
{ {
await LogInformation( await LogInformation($"Closing position - TP1 hit at {positionForSignal.TakeProfit1.Price}");
$"Closing position - TP1 {positionForSignal.TakeProfit1.Price} >= Price {lastCandle.Low}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1,
positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null); positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled);
} }
else if (positionForSignal.TakeProfit2?.Price >= lastCandle.Low) else if (positionForSignal.TakeProfit2?.Price >= lastCandle.Low)
{ {
await LogInformation( await LogInformation($"Closing position - TP2 hit at {positionForSignal.TakeProfit2.Price}");
$"Closing position - TP2 {positionForSignal.TakeProfit2.Price} >= Price {lastCandle.Low}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2,
positionForSignal.TakeProfit2.Price, true); positionForSignal.TakeProfit2.Price, true);
positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled); positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled);
} }
else
{
Logger.LogInformation(
$"Position {signal.Identifier} don't need to be update. Position still opened");
}
} }
} }
else if (position.Status == (PositionStatus.Rejected | PositionStatus.Canceled)) else if (position.Status == (PositionStatus.Rejected | PositionStatus.Canceled))
@@ -663,7 +613,7 @@ public class TradingBot : Bot, ITradingBot
if (Config.FlipPosition) if (Config.FlipPosition)
{ {
// Check if current position is in profit before flipping // Check if current position is in profit before flipping
var isPositionInProfit = await IsPositionInProfit(openedPosition, lastPrice); var isPositionInProfit = (openedPosition.ProfitAndLoss?.Realized ?? 0) > 0;
// Determine if we should flip based on configuration // Determine if we should flip based on configuration
var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit; var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
@@ -684,8 +634,9 @@ public class TradingBot : Bot, ITradingBot
} }
else else
{ {
var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0;
await LogInformation( await LogInformation(
$"Position {previousSignal.Identifier} is not in profit (entry: {openedPosition.Open.Price}, current: {lastPrice}). " + $"Position {previousSignal.Identifier} is not in profit (PnL: ${currentPnl:F2}). " +
$"Signal {signal.Identifier} will wait for position to become profitable before flipping."); $"Signal {signal.Identifier} will wait for position to become profitable before flipping.");
SetSignalStatus(signal.Identifier, SignalStatus.Expired); SetSignalStatus(signal.Identifier, SignalStatus.Expired);
@@ -1279,28 +1230,6 @@ public class TradingBot : Bot, ITradingBot
return position; return position;
} }
/// <summary>
/// Checks if a position is currently in profit based on current market price
/// </summary>
/// <param name="position">The position to check</param>
/// <param name="currentPrice">The current market price</param>
/// <returns>True if position is in profit, false otherwise</returns>
private async Task<bool> IsPositionInProfit(Position position, decimal currentPrice)
{
if (position.OriginDirection == TradeDirection.Long)
{
return currentPrice >= position.Open.Price;
}
else if (position.OriginDirection == TradeDirection.Short)
{
return currentPrice <= position.Open.Price;
}
else
{
throw new ArgumentException("Invalid position direction");
}
}
/// <summary> /// <summary>
/// Checks if a position has exceeded the maximum time limit for being open. /// Checks if a position has exceeded the maximum time limit for being open.
/// </summary> /// </summary>
@@ -1320,28 +1249,6 @@ public class TradingBot : Bot, ITradingBot
return timeOpen >= maxTimeAllowed; 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> /// <summary>
/// Updates the trading bot configuration with new settings. /// Updates the trading bot configuration with new settings.
/// This method validates the new configuration and applies it to the running bot. /// This method validates the new configuration and applies it to the running bot.