fix close early in profit
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user