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);
|
: 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.
|
||||||
|
|||||||
Reference in New Issue
Block a user