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);
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)
if (Config.MaxPositionTimeHours.HasValue)
// Check if position is in profit by comparing entry price with current market price
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
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)
if (shouldCloseOnTimeLimit)
{
var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close);
var isAtBreakeven =
Math.Abs(lastCandle.Close - positionForSignal.Open.Price) <
0.01m; // Small tolerance for breakeven
var profitStatus = isPositionInProfit ? "in profit" : "at a loss";
if (isPositionInProfit || isAtBreakeven)
{
await LogInformation(
$"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;
}
// 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.");
}
await LogInformation(
$"Closing position due to time limit ({Config.MaxPositionTimeHours} hours exceeded) - " +
$"Position is {profitStatus} (entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Realized PNL: ${currentPnl:F2} ({pnlPercentage:F2}%)");
await CloseTrade(signal, positionForSignal, positionForSignal.Open, lastCandle.Close, true);
return;
}
else
{
// Time limit exceeded logic when CloseEarlyWhenProfitable is disabled
if (hasExceededTimeLimit)
{
var isPositionInProfit = await IsPositionInProfit(positionForSignal, lastCandle.Close);
var profitStatus = isPositionInProfit ? "in profit" :
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;
}
await LogInformation(
$"Time limit exceeded but position is at a loss " +
$"(entry: {positionForSignal.Open.Price}, current: {lastCandle.Close}). " +
$"Realized PNL: ${currentPnl:F2} ({pnlPercentage:F2}%). " +
$"Waiting for profit before closing (CloseEarlyWhenProfitable enabled).");
}
}
// 3. Normal stop loss and take profit checks
if (positionForSignal.OriginDirection == TradeDirection.Long)
{
if (positionForSignal.StopLoss.Price >= lastCandle.Low)
{
await LogInformation(
$"Closing position - SL {positionForSignal.StopLoss.Price} >= Price {lastCandle.Low}");
await LogInformation($"Closing position - SL hit at {positionForSignal.StopLoss.Price}");
await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss,
positionForSignal.StopLoss.Price, true);
positionForSignal.StopLoss.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit1.Price <= lastCandle.High
&& positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
else if (positionForSignal.TakeProfit1.Price <= lastCandle.High &&
positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
{
await LogInformation(
$"Closing position - TP1 {positionForSignal.TakeProfit1.Price} <= Price {lastCandle.High}");
await LogInformation($"Closing position - TP1 hit at {positionForSignal.TakeProfit1.Price}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1,
positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit2?.Price <= lastCandle.High)
{
await LogInformation(
$"Closing position - TP2 {positionForSignal.TakeProfit2.Price} <= Price {lastCandle.High}");
await LogInformation($"Closing position - TP2 hit at {positionForSignal.TakeProfit2.Price}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2,
positionForSignal.TakeProfit2.Price, true);
positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled);
}
else
{
Logger.LogInformation(
$"Position {signal.Identifier} don't need to be update. Position still opened");
}
}
if (positionForSignal.OriginDirection == TradeDirection.Short)
else if (positionForSignal.OriginDirection == TradeDirection.Short)
{
if (positionForSignal.StopLoss.Price <= lastCandle.High)
{
await LogInformation(
$"Closing position - SL {positionForSignal.StopLoss.Price} <= Price {lastCandle.High}");
await LogInformation($"Closing position - SL hit at {positionForSignal.StopLoss.Price}");
await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss,
positionForSignal.StopLoss.Price, true);
positionForSignal.StopLoss.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit1.Price >= lastCandle.Low
&& positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
else if (positionForSignal.TakeProfit1.Price >= lastCandle.Low &&
positionForSignal.TakeProfit1.Status != TradeStatus.Filled)
{
await LogInformation(
$"Closing position - TP1 {positionForSignal.TakeProfit1.Price} >= Price {lastCandle.Low}");
await LogInformation($"Closing position - TP1 hit at {positionForSignal.TakeProfit1.Price}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1,
positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null);
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled);
}
else if (positionForSignal.TakeProfit2?.Price >= lastCandle.Low)
{
await LogInformation(
$"Closing position - TP2 {positionForSignal.TakeProfit2.Price} >= Price {lastCandle.Low}");
await LogInformation($"Closing position - TP2 hit at {positionForSignal.TakeProfit2.Price}");
await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2,
positionForSignal.TakeProfit2.Price, true);
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))
@@ -663,7 +613,7 @@ public class TradingBot : Bot, ITradingBot
if (Config.FlipPosition)
{
// 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
var shouldFlip = !Config.FlipOnlyWhenInProfit || isPositionInProfit;
@@ -684,8 +634,9 @@ public class TradingBot : Bot, ITradingBot
}
else
{
var currentPnl = openedPosition.ProfitAndLoss?.Realized ?? 0;
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.");
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
@@ -1279,28 +1230,6 @@ public class TradingBot : Bot, ITradingBot
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>
/// Checks if a position has exceeded the maximum time limit for being open.
/// </summary>
@@ -1320,28 +1249,6 @@ 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.