Update fetch borkerPosition in bot + better HandleClosePosition + Add debug channel to receive all debug

This commit is contained in:
2025-10-25 18:35:51 +07:00
parent 38e6998ff3
commit f816b8de50
9 changed files with 229 additions and 98 deletions

View File

@@ -67,6 +67,7 @@
"CopyTradingChannelId": 1132022887012909126, "CopyTradingChannelId": 1132022887012909126,
"RequestsChannelId": 1018589494968078356, "RequestsChannelId": 1018589494968078356,
"FundingRateChannelId": 1263566138709774336, "FundingRateChannelId": 1263566138709774336,
"DebugChannelId": 1431289070297813044,
"ButtonExpirationMinutes": 10 "ButtonExpirationMinutes": 10
}, },
"RunOrleansGrains": true, "RunOrleansGrains": true,

View File

@@ -21,4 +21,5 @@ public interface IDiscordService
Task SendDowngradedFundingRate(FundingRate oldRate); Task SendDowngradedFundingRate(FundingRate oldRate);
Task SendNewTopFundingRate(FundingRate newRate); Task SendNewTopFundingRate(FundingRate newRate);
Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate); Task SendFundingRateUpdate(FundingRate oldRate, FundingRate newRate);
Task SendDebugMessage(string message);
} }

View File

@@ -28,4 +28,5 @@ public interface IMessengerService
Task SendBacktestNotification(Backtest backtest); Task SendBacktestNotification(Backtest backtest);
Task SendGeneticAlgorithmNotification(GeneticRequest request, double bestFitness, object? bestChromosome); Task SendGeneticAlgorithmNotification(GeneticRequest request, double bestFitness, object? bestChromosome);
Task SendClosedPosition(Position position, User user); Task SendClosedPosition(Position position, User user);
Task SendDebugMessage(string message);
} }

View File

@@ -371,9 +371,8 @@ public class TradingBotBase : ITradingBot
if (positionForSignal.Status == PositionStatus.Canceled || if (positionForSignal.Status == PositionStatus.Canceled ||
positionForSignal.Status == PositionStatus.Rejected) positionForSignal.Status == PositionStatus.Rejected)
{ {
Logger.LogDebug( await LogDebug(
"Skipping update for position {PositionId} - status is {Status} (never filled)", $"Skipping update for position {positionForSignal.Identifier} - status is {positionForSignal.Status} (never filled)");
positionForSignal.Identifier, positionForSignal.Status);
return; return;
} }
@@ -402,8 +401,12 @@ public class TradingBotBase : ITradingBot
if (!Config.IsForBacktest) if (!Config.IsForBacktest)
{ {
var brokerPosition = brokerPositions.FirstOrDefault(p => // Improved broker position matching with more robust logic
p.Ticker == Config.Ticker && p.OriginDirection == positionForSignal.OriginDirection); var brokerPosition = brokerPositions
.Where(p => p.Ticker == Config.Ticker)
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
.FirstOrDefault(p => p.OriginDirection == positionForSignal.OriginDirection);
if (brokerPosition != null) if (brokerPosition != null)
{ {
var previousPositionStatus = internalPosition.Status; var previousPositionStatus = internalPosition.Status;
@@ -419,8 +422,8 @@ public class TradingBotBase : ITradingBot
internalPosition.Open.SetStatus(TradeStatus.Filled); internalPosition.Open.SetStatus(TradeStatus.Filled);
positionForSignal.Open.SetStatus(TradeStatus.Filled); positionForSignal.Open.SetStatus(TradeStatus.Filled);
internalPosition.Open.SetPrice(brokerPosition.Open.Price, 5); internalPosition.Open.Price = brokerPosition.Open.Price;
positionForSignal.Open.SetPrice(brokerPosition.Open.Price, 5); positionForSignal.Open.Price = brokerPosition.Open.Price;
// Update Open trade ExchangeOrderId if broker position has one // Update Open trade ExchangeOrderId if broker position has one
if (brokerPosition.Open?.ExchangeOrderId != null && internalPosition.Open != null) if (brokerPosition.Open?.ExchangeOrderId != null && internalPosition.Open != null)
@@ -462,12 +465,50 @@ public class TradingBotBase : ITradingBot
} }
else else
{ {
// No position on the broker, the position have been closed by the exchange // Position not found in broker's active positions list
// Need to verify if it was actually closed or just not returned by the API
if (internalPosition.Status.Equals(PositionStatus.Filled)) if (internalPosition.Status.Equals(PositionStatus.Filled))
{ {
internalPosition.Status = PositionStatus.Finished; Logger.LogWarning(
// Call HandleClosedPosition to ensure trade dates are properly updated $"⚠️ Position Sync Issue Detected\n" +
await HandleClosedPosition(internalPosition); $"Internal position {internalPosition.Identifier} shows Filled\n" +
$"But not found in broker positions list (Count: {brokerPositions.Count})\n" +
$"Checking position history before marking as closed...");
// Verify in exchange history before assuming it's closed
bool existsInHistory = await CheckPositionInExchangeHistory(positionForSignal);
if (existsInHistory)
{
// Position was actually filled and closed by the exchange
Logger.LogInformation(
$"✅ Position Confirmed Closed via History\n" +
$"Position {internalPosition.Identifier} found in exchange history\n" +
$"Proceeding with HandleClosedPosition");
internalPosition.Status = PositionStatus.Finished;
await HandleClosedPosition(internalPosition);
return;
}
else
{
// Position not in history either - could be API issue or timing problem
// Don't immediately close, just log warning and retry next cycle
await LogWarning(
$"⚠️ Position Synchronization Warning\n" +
$"Position `{internalPosition.Identifier}` ({internalPosition.OriginDirection} {Config.Ticker})\n" +
$"Not found in broker positions OR exchange history\n" +
$"Status: `{internalPosition.Status}`\n" +
$"This could indicate:\n" +
$"• API returned incomplete data\n" +
$"• Timing issue with broker API\n" +
$"• Position direction mismatch\n" +
$"Will retry verification on next cycle before taking action");
// Don't change the position status yet - wait for next cycle to verify
await LogDebug(
$"Position {internalPosition.Identifier} will be rechecked on next bot cycle for broker synchronization");
}
} }
} }
} }
@@ -852,7 +893,7 @@ public class TradingBotBase : ITradingBot
private async Task<Position> OpenPosition(LightSignal signal) private async Task<Position> OpenPosition(LightSignal signal)
{ {
Logger.LogDebug($"Opening position for {signal.Identifier}"); await LogDebug($"🔓 Opening position for signal: `{signal.Identifier}`");
// Check for any existing open position (not finished) for this ticker // Check for any existing open position (not finished) for this ticker
var openedPosition = var openedPosition =
@@ -963,12 +1004,12 @@ public class TradingBotBase : ITradingBot
if (!Config.IsForBacktest) if (!Config.IsForBacktest)
{ {
await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory,
async messengerService => { await messengerService.SendPosition(position); }); async messengerService => { await messengerService.SendPosition(position); });
} }
Logger.LogDebug($"Position requested"); await LogDebug($"Position requested successfully for signal: `{signal.Identifier}`");
return position; return position;
} }
else else
{ {
@@ -1104,12 +1145,22 @@ public class TradingBotBase : ITradingBot
List<Position> positions = null; List<Position> positions = null;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
async exchangeService => { positions = [.. await exchangeService.GetBrokerPositions(Account)]; }); async exchangeService => { positions = [.. await exchangeService.GetBrokerPositions(Account)]; });
if (!positions.Any(p => p.Ticker == Config.Ticker))
// Check if there's a position for this ticker on the broker
var brokerPositionForTicker = positions.FirstOrDefault(p => p.Ticker == Config.Ticker);
if (brokerPositionForTicker == null)
{ {
// No position on broker for this ticker, safe to open
return true; return true;
} }
// Handle existing position on broker // Handle existing position on broker
await LogDebug(
$"🔍 Broker Position Found\n" +
$"Ticker: {Config.Ticker}\n" +
$"Direction: {brokerPositionForTicker.OriginDirection}\n" +
$"Checking internal positions for synchronization...");
var previousPosition = Positions.Values.LastOrDefault(); var previousPosition = Positions.Values.LastOrDefault();
List<Trade> orders = null; List<Trade> orders = null;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
@@ -1118,31 +1169,61 @@ public class TradingBotBase : ITradingBot
orders = [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]; orders = [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)];
}); });
var reason = $"Cannot open position. There is already a position open for {Config.Ticker} on the broker."; var reason =
$"Cannot open position. There is already a position open for {Config.Ticker} on the broker (Direction: {brokerPositionForTicker.OriginDirection}).";
if (previousPosition != null) if (previousPosition != null)
{ {
if (orders.Count >= 2) // Check if this position matches the broker position
if (previousPosition.OriginDirection == brokerPositionForTicker.OriginDirection)
{ {
await SetPositionStatus(previousPosition.SignalIdentifier, PositionStatus.Filled); // Same direction - this is likely the same position
if (orders.Count >= 2)
{
Logger.LogInformation(
$"✅ Broker Position Matched with Internal Position\n" +
$"Position: {previousPosition.Identifier}\n" +
$"Direction: {previousPosition.OriginDirection}\n" +
$"Orders found: {orders.Count}\n" +
$"Setting status to Filled");
await SetPositionStatus(previousPosition.SignalIdentifier, PositionStatus.Filled);
}
else
{
// Position exists on broker but not enough orders - something is wrong
Logger.LogWarning(
$"⚠️ Incomplete Order Set\n" +
$"Position: {previousPosition.Identifier}\n" +
$"Direction: {previousPosition.OriginDirection}\n" +
$"Expected orders: ≥2, Found: {orders.Count}\n" +
$"This position may need manual intervention");
reason += $" Position exists on broker but only has {orders.Count} orders (expected ≥2).";
}
} }
else else
{ {
// Broker already has an open position, cancel the internally created (DB) position // Different direction - possible flip scenario or orphaned position
await SetPositionStatus(previousPosition.SignalIdentifier, PositionStatus.Canceled); Logger.LogWarning(
if (!Config.IsForBacktest) $"⚠️ Direction Mismatch Detected\n" +
{ $"Internal: {previousPosition.OriginDirection}\n" +
await UpdatePositionDatabase(previousPosition); $"Broker: {brokerPositionForTicker.OriginDirection}\n" +
} $"This could indicate a flipped position or orphaned broker position");
reason += reason +=
" Position open on broker; internal position has been marked as Canceled."; $" Direction mismatch: Internal ({previousPosition.OriginDirection}) vs Broker ({brokerPositionForTicker.OriginDirection}).";
} }
} }
else else
{ {
reason += // Broker has a position but we don't have any internal tracking
" Position open on broker but not enough orders or no previous position internally saved by the bot"; Logger.LogWarning(
$"⚠️ Orphaned Broker Position Detected\n" +
$"Broker has position for {Config.Ticker} ({brokerPositionForTicker.OriginDirection})\n" +
$"But no internal position found in bot tracking\n" +
$"This may require manual cleanup");
reason += " Position open on broker but no internal position tracked by the bot.";
} }
await LogWarning(reason); await LogWarning(reason);
@@ -1158,13 +1239,6 @@ public class TradingBotBase : ITradingBot
public async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice, public async Task CloseTrade(LightSignal signal, Position position, Trade tradeToClose, decimal lastPrice,
bool tradeClosingPosition = false) bool tradeClosingPosition = false)
{ {
if (position.TakeProfit2 != null && position.TakeProfit1.Status == TradeStatus.Filled &&
tradeToClose.TradeType == TradeType.StopMarket)
{
// If trade is the 2nd Take profit
tradeToClose.Quantity = position.TakeProfit2.Quantity;
}
await LogInformation( await LogInformation(
$"🔧 Closing {position.OriginDirection} Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\n📋 Type: `{tradeToClose.TradeType}`\n📊 Quantity: `{tradeToClose.Quantity:F5}`"); $"🔧 Closing {position.OriginDirection} Trade\nTicker: `{Config.Ticker}`\nPrice: `${lastPrice}`\n📋 Type: `{tradeToClose.TradeType}`\n📊 Quantity: `{tradeToClose.Quantity:F5}`");
@@ -1175,6 +1249,7 @@ public class TradingBotBase : ITradingBot
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
async exchangeService => async exchangeService =>
{ {
// TODO should also pass the direction to get quantity in correct position
quantity = await exchangeService.GetQuantityInPosition(Account, Config.Ticker); quantity = await exchangeService.GetQuantityInPosition(Account, Config.Ticker);
}); });
} }
@@ -1182,7 +1257,7 @@ public class TradingBotBase : ITradingBot
// Get status of position before closing it. The position might be already close by the exchange // Get status of position before closing it. The position might be already close by the exchange
if (!Config.IsForBacktest && quantity == 0) if (!Config.IsForBacktest && quantity == 0)
{ {
Logger.LogDebug($"Trade already close on exchange"); await LogDebug($"Trade already closed on exchange for position: `{position.Identifier}`");
await HandleClosedPosition(position); await HandleClosedPosition(position);
} }
else else
@@ -1247,39 +1322,44 @@ public class TradingBotBase : ITradingBot
{ {
try try
{ {
Logger.LogDebug( await LogDebug(
$"🔍 Fetching Position History from GMX\nPosition: `{position.Identifier}`\nTicker: `{Config.Ticker}`"); $"🔍 Fetching Position History from GMX\nPosition: `{position.Identifier}`\nTicker: `{Config.Ticker}`");
var positionHistory = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>( var positionHistory = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>(
_scopeFactory, _scopeFactory,
async exchangeService => async exchangeService =>
{ {
// Get position history from the last 24 hours // Get position history from the last 24 hours for better coverage
var fromDate = DateTime.UtcNow.AddHours(-1); var fromDate = DateTime.UtcNow.AddHours(-24);
var toDate = DateTime.UtcNow; var toDate = DateTime.UtcNow;
return await exchangeService.GetPositionHistory(Account, Config.Ticker, fromDate, toDate); return await exchangeService.GetPositionHistory(Account, Config.Ticker, fromDate, toDate);
}); });
// Find the matching position in history based on the most recent closed position // Find the matching position in history based on the most recent closed position with same direction
if (positionHistory != null && positionHistory.Any()) if (positionHistory != null && positionHistory.Any())
{ {
// Get the most recent closed position from GMX // Get the most recent closed position from GMX that matches the direction
var gmxPosition = positionHistory.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue) var brokerPosition = positionHistory
.Where(p => p.OriginDirection == position.OriginDirection) // Ensure same direction
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
.FirstOrDefault(); .FirstOrDefault();
if (gmxPosition != null && gmxPosition.ProfitAndLoss != null) if (brokerPosition != null && brokerPosition.ProfitAndLoss != null)
{ {
Logger.LogDebug( await LogDebug(
$"✅ GMX Position History Found\n" + $"✅ Broker Position History Found\n" +
$"Position: `{position.Identifier}`\n" + $"Position: `{position.Identifier}`\n" +
$"GMX Realized PnL (after fees): `${gmxPosition.ProfitAndLoss.Realized:F2}`\n" + $"Realized PnL (after fees): `${brokerPosition.ProfitAndLoss.Realized:F2}`\n" +
$"Bot's UI Fees: `${position.UiFees:F2}`\n" + $"Bot's UI Fees: `${position.UiFees:F2}`\n" +
$"Bot's Gas Fees: `${position.GasFees:F2}`"); $"Bot's Gas Fees: `${position.GasFees:F2}`");
// Use the actual GMX PnL data (this is already net of fees from GMX) // Use the actual GMX PnL data (this is already net of fees from GMX)
// We use this for reconciliation with the bot's own calculations // We use this for reconciliation with the bot's own calculations
var totalBotFees = position.GasFees + position.UiFees; var closingVolume = brokerPosition.Open.Price * position.Open.Quantity *
var gmxNetPnl = gmxPosition.ProfitAndLoss.Realized; // This is already after GMX fees position.Open.Leverage;
var totalBotFees = position.GasFees + position.UiFees +
TradingHelpers.CalculateClosingUiFees(closingVolume);
var gmxNetPnl = brokerPosition.ProfitAndLoss.Realized; // This is already after GMX fees
position.ProfitAndLoss = new ProfitAndLoss position.ProfitAndLoss = new ProfitAndLoss
{ {
@@ -1290,19 +1370,19 @@ public class TradingBotBase : ITradingBot
}; };
// Update the closing trade price if available // Update the closing trade price if available
if (gmxPosition.Open != null) if (brokerPosition.Open != null)
{ {
var gmxClosingPrice = gmxPosition.Open.Price; var brokerClosingPrice = brokerPosition.Open.Price;
var isProfitable = position.OriginDirection == TradeDirection.Long
// Determine which trade was the closing trade based on profitability ? position.Open.Price < brokerClosingPrice
bool isProfitable = position.ProfitAndLoss.Realized > 0; : position.Open.Price > brokerClosingPrice;
if (isProfitable) if (isProfitable)
{ {
if (position.TakeProfit1 != null) if (position.TakeProfit1 != null)
{ {
position.TakeProfit1.SetPrice(gmxClosingPrice, 2); position.TakeProfit1.Price = brokerClosingPrice;
position.TakeProfit1.SetDate(gmxPosition.Open.Date); position.TakeProfit1.SetDate(brokerPosition.Open.Date);
position.TakeProfit1.SetStatus(TradeStatus.Filled); position.TakeProfit1.SetStatus(TradeStatus.Filled);
} }
@@ -1316,8 +1396,8 @@ public class TradingBotBase : ITradingBot
{ {
if (position.StopLoss != null) if (position.StopLoss != null)
{ {
position.StopLoss.SetPrice(gmxClosingPrice, 2); position.StopLoss.Price = brokerClosingPrice;
position.StopLoss.SetDate(gmxPosition.Open.Date); position.StopLoss.SetDate(brokerPosition.Open.Date);
position.StopLoss.SetStatus(TradeStatus.Filled); position.StopLoss.SetStatus(TradeStatus.Filled);
} }
@@ -1333,12 +1413,12 @@ public class TradingBotBase : ITradingBot
} }
} }
Logger.LogDebug( await LogDebug(
$"📊 Position Reconciliation Complete\n" + $"📊 Position Reconciliation Complete\n" +
$"Position: `{position.Identifier}`\n" + $"Position: `{position.Identifier}`\n" +
$"Closing Price: `${gmxClosingPrice:F2}`\n" + $"Closing Price: `${brokerClosingPrice:F2}`\n" +
$"Used: `{(isProfitable ? "Take Profit" : "Stop Loss")}`\n" + $"Used: `{(isProfitable ? "Take Profit" : "Stop Loss")}`\n" +
$"PnL from GMX: `${position.ProfitAndLoss.Realized:F2}`"); $"PnL from broker: `${position.ProfitAndLoss.Realized:F2}`");
} }
// Skip the candle-based PnL calculation since we have actual GMX data // Skip the candle-based PnL calculation since we have actual GMX data
@@ -1457,7 +1537,7 @@ public class TradingBotBase : ITradingBot
position.TakeProfit2.SetStatus(TradeStatus.Cancelled); position.TakeProfit2.SetStatus(TradeStatus.Cancelled);
} }
Logger.LogDebug( await LogDebug(
$"🛑 Stop Loss Execution Confirmed\n" + $"🛑 Stop Loss Execution Confirmed\n" +
$"Position: `{position.Identifier}`\n" + $"Position: `{position.Identifier}`\n" +
$"SL Price: `${closingPrice:F2}` was hit (was `${position.StopLoss.Price:F2}`)\n" + $"SL Price: `${closingPrice:F2}` was hit (was `${position.StopLoss.Price:F2}`)\n" +
@@ -1480,7 +1560,7 @@ public class TradingBotBase : ITradingBot
position.StopLoss.SetStatus(TradeStatus.Cancelled); position.StopLoss.SetStatus(TradeStatus.Cancelled);
} }
Logger.LogDebug( await LogDebug(
$"🎯 Take Profit Execution Confirmed\n" + $"🎯 Take Profit Execution Confirmed\n" +
$"Position: `{position.Identifier}`\n" + $"Position: `{position.Identifier}`\n" +
$"TP Price: `${closingPrice:F2}` was hit (was `${position.TakeProfit1.Price:F2}`)\n" + $"TP Price: `${closingPrice:F2}` was hit (was `${position.TakeProfit1.Price:F2}`)\n" +
@@ -1535,7 +1615,7 @@ public class TradingBotBase : ITradingBot
} }
} }
Logger.LogDebug( await LogDebug(
$"✋ Manual/Exchange Close Detected\n" + $"✋ Manual/Exchange Close Detected\n" +
$"Position: `{position.Identifier}`\n" + $"Position: `{position.Identifier}`\n" +
$"SL: `${position.StopLoss.Price:F2}` | TP: `${position.TakeProfit1.Price:F2}`\n" + $"SL: `${position.StopLoss.Price:F2}` | TP: `${position.TakeProfit1.Price:F2}`\n" +
@@ -1604,7 +1684,7 @@ public class TradingBotBase : ITradingBot
position.ProfitAndLoss.Net = netPnl; position.ProfitAndLoss.Net = netPnl;
} }
Logger.LogDebug( await LogDebug(
$"💰 P&L Calculated for Position {position.Identifier}\n" + $"💰 P&L Calculated for Position {position.Identifier}\n" +
$"Entry: `${entryPrice:F2}` | Exit: `${closingPrice:F2}`\n" + $"Entry: `${entryPrice:F2}` | Exit: `${closingPrice:F2}`\n" +
$"Realized P&L: `${pnl:F2}` | Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`\n" + $"Realized P&L: `${pnl:F2}` | Net P&L (after fees): `${position.ProfitAndLoss.Net:F2}`\n" +
@@ -1635,30 +1715,29 @@ public class TradingBotBase : ITradingBot
} }
else else
{ {
Logger.LogDebug( await LogDebug(
"Skipping PositionClosed notification for position {PositionId} - position was never filled (Open trade status: {OpenStatus})", $"Skipping PositionClosed notification for position {position.Identifier} - position was never filled (Open trade status: {position.Open?.Status})");
position.Identifier, position.Open?.Status);
} }
} }
// Only update balance and log success if position was actually filled // Only update balance and log success if position was actually filled
if (position.Open?.Status == TradeStatus.Filled) if (position.Open?.Status == TradeStatus.Filled)
{ {
Logger.LogDebug( await LogDebug(
$"✅ Position Closed Successfully\nPosition: `{position.SignalIdentifier}`\nPnL: `${position.ProfitAndLoss?.Net:F2}`"); $"✅ Position Closed Successfully\nPosition: `{position.SignalIdentifier}`\nPnL: `${position.ProfitAndLoss?.Net:F2}`");
if (position.ProfitAndLoss != null) if (position.ProfitAndLoss != null)
{ {
Config.BotTradingBalance += position.ProfitAndLoss.Net; Config.BotTradingBalance += position.ProfitAndLoss.Net;
Logger.LogDebug( await LogDebug(
string.Format("💰 Balance Updated\nNew bot trading balance: `${0:F2}`", string.Format("💰 Balance Updated\nNew bot trading balance: `${0:F2}`",
Config.BotTradingBalance)); Config.BotTradingBalance));
} }
} }
else else
{ {
Logger.LogDebug( await LogDebug(
$"✅ Position Cleanup\nPosition: `{position.SignalIdentifier}` was never filled - no balance or PnL changes"); $"✅ Position Cleanup\nPosition: `{position.SignalIdentifier}` was never filled - no balance or PnL changes");
} }
} }
@@ -1701,24 +1780,24 @@ public class TradingBotBase : ITradingBot
if (cancelClose) if (cancelClose)
{ {
Logger.LogDebug($"Position still open, cancel close orders"); await LogDebug($"Position still open, cancel close orders");
} }
else else
{ {
Logger.LogDebug($"Canceling all orders for {Config.Ticker}"); await LogDebug($"Canceling all orders for {Config.Ticker}");
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
async exchangeService => async exchangeService =>
{ {
await exchangeService.CancelOrder(Account, Config.Ticker); await exchangeService.CancelOrder(Account, Config.Ticker);
var closePendingOrderStatus = await exchangeService.CancelOrder(Account, Config.Ticker); var closePendingOrderStatus = await exchangeService.CancelOrder(Account, Config.Ticker);
Logger.LogDebug( await LogDebug(
$"Closing all {Config.Ticker} orders status : {closePendingOrderStatus}"); $"Closing all {Config.Ticker} orders status : {closePendingOrderStatus}");
}); });
} }
} }
else else
{ {
Logger.LogDebug($"No need to cancel orders for {Config.Ticker}"); await LogDebug($"No need to cancel orders for {Config.Ticker}");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -1896,6 +1975,27 @@ public class TradingBotBase : ITradingBot
} }
} }
public async Task LogDebug(string message)
{
if (Config.IsForBacktest)
return;
Logger.LogDebug(message);
try
{
await ServiceScopeHelpers.WithScopedService<IMessengerService>(_scopeFactory,
async messengerService =>
{
await messengerService.SendDebugMessage($"🤖 {Account.User.AgentName} - {Config.Name}\n{message}");
});
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
private async Task SendTradeMessage(string message, bool isBadBehavior = false) private async Task SendTradeMessage(string message, bool isBadBehavior = false)
{ {
if (!Config.IsForBacktest) if (!Config.IsForBacktest)
@@ -1977,12 +2077,12 @@ public class TradingBotBase : ITradingBot
signalValidationResult.IsBlocked) signalValidationResult.IsBlocked)
{ {
signal.Status = SignalStatus.Expired; signal.Status = SignalStatus.Expired;
Logger.LogDebug($"Signal {signal.Identifier} blocked by Synth risk assessment"); await LogDebug($"Signal {signal.Identifier} blocked by Synth risk assessment");
} }
else else
{ {
signal.Confidence = signalValidationResult.Confidence; signal.Confidence = signalValidationResult.Confidence;
Logger.LogDebug( await LogDebug(
$"Signal {signal.Identifier} passed Synth risk assessment with confidence {signalValidationResult.Confidence}"); $"Signal {signal.Identifier} passed Synth risk assessment with confidence {signalValidationResult.Confidence}");
} }
}); });
@@ -2001,7 +2101,7 @@ public class TradingBotBase : ITradingBot
}); });
} }
Logger.LogDebug( await LogDebug(
$"Processed signal for {Config.Ticker}: {signal.Direction} with status {signal.Status}"); $"Processed signal for {Config.Ticker}: {signal.Direction} with status {signal.Status}");
} }
catch (Exception ex) catch (Exception ex)
@@ -2376,8 +2476,7 @@ public class TradingBotBase : ITradingBot
if (LastCandle != null) if (LastCandle != null)
{ {
Logger.LogDebug("Successfully refreshed last candle for {Ticker} at {Date}", await LogDebug($"Successfully refreshed last candle for {Config.Ticker} at {LastCandle.Date}");
Config.Ticker, LastCandle.Date);
} }
else else
{ {
@@ -2472,8 +2571,7 @@ public class TradingBotBase : ITradingBot
await agentGrain.OnPositionOpenedAsync(positionOpenEvent); await agentGrain.OnPositionOpenedAsync(positionOpenEvent);
await platformGrain.OnPositionOpenAsync(positionOpenEvent); await platformGrain.OnPositionOpenAsync(positionOpenEvent);
Logger.LogDebug("Sent position opened event to both grains for position {PositionId}", await LogDebug($"Sent position opened event to both grains for position {position.Identifier}");
position.Identifier);
break; break;
case NotificationEventType.PositionClosed: case NotificationEventType.PositionClosed:
@@ -2488,8 +2586,7 @@ public class TradingBotBase : ITradingBot
await agentGrain.OnPositionClosedAsync(positionClosedEvent); await agentGrain.OnPositionClosedAsync(positionClosedEvent);
await platformGrain.OnPositionClosedAsync(positionClosedEvent); await platformGrain.OnPositionClosedAsync(positionClosedEvent);
Logger.LogDebug("Sent position closed event to both grains for position {PositionId}", await LogDebug($"Sent position closed event to both grains for position {position.Identifier}");
position.Identifier);
break; break;
case NotificationEventType.PositionUpdated: case NotificationEventType.PositionUpdated:
@@ -2501,8 +2598,7 @@ public class TradingBotBase : ITradingBot
await agentGrain.OnPositionUpdatedAsync(positionUpdatedEvent); await agentGrain.OnPositionUpdatedAsync(positionUpdatedEvent);
// No need to notify platform grain, it will be notified when position is closed or opened only // No need to notify platform grain, it will be notified when position is closed or opened only
Logger.LogDebug("Sent position updated event to both grains for position {PositionId}", await LogDebug($"Sent position updated event to both grains for position {position.Identifier}");
position.Identifier);
break; break;
} }
}); });
@@ -2530,39 +2626,51 @@ public class TradingBotBase : ITradingBot
try try
{ {
Logger.LogDebug( await LogDebug(
$"🔍 Checking Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`"); $"🔍 Checking Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`");
List<Position> positionHistory = null; List<Position> positionHistory = null;
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory,
async exchangeService => async exchangeService =>
{ {
// Get position history from the last 24 hours // Get position history from the last 24 hours for comprehensive check
var fromDate = DateTime.UtcNow.AddMinutes(-10); var fromDate = DateTime.UtcNow.AddHours(-24);
var toDate = DateTime.UtcNow; var toDate = DateTime.UtcNow;
positionHistory = positionHistory =
await exchangeService.GetPositionHistory(Account, Config.Ticker, fromDate, toDate); await exchangeService.GetPositionHistory(Account, Config.Ticker, fromDate, toDate);
}); });
// Check if there's a recent position with PnL data // Check if there's a recent position with PnL data and matching direction
if (positionHistory != null && positionHistory.Any()) if (positionHistory != null && positionHistory.Any())
{ {
var recentPosition = positionHistory var recentPosition = positionHistory
.Where(p => p.OriginDirection == position.OriginDirection) // Ensure same direction
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue) .OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
.FirstOrDefault(); .FirstOrDefault();
if (recentPosition != null && recentPosition.ProfitAndLoss != null) if (recentPosition != null && recentPosition.ProfitAndLoss != null)
{ {
Logger.LogDebug( await LogDebug(
$"✅ Position Found in Exchange History\n" + $"✅ Position Found in Exchange History\n" +
$"Position: `{position.Identifier}`\n" + $"Position: `{position.Identifier}`\n" +
$"Direction: `{position.OriginDirection}` (Matched: ✅)\n" +
$"Exchange PnL: `${recentPosition.ProfitAndLoss.Realized:F2}`\n" + $"Exchange PnL: `${recentPosition.ProfitAndLoss.Realized:F2}`\n" +
$"Position was actually filled and closed"); $"Position was actually filled and closed");
return true; return true;
} }
else
{
// Found positions in history but none match the direction
var allHistoryDirections = positionHistory.Select(p => p.OriginDirection).Distinct().ToList();
await LogDebug(
$"⚠️ Direction Mismatch in History\n" +
$"Looking for: `{position.OriginDirection}`\n" +
$"Found in history: `{string.Join(", ", allHistoryDirections)}`\n" +
$"No matching position found");
}
} }
Logger.LogDebug( await LogDebug(
$"❌ No Position Found in Exchange History\nPosition: `{position.Identifier}`\nPosition was never filled"); $"❌ No Position Found in Exchange History\nPosition: `{position.Identifier}`\nPosition was never filled");
return false; return false;
} }

View File

@@ -246,6 +246,11 @@ public class MessengerService : IMessengerService
} }
} }
public async Task SendDebugMessage(string message)
{
await _discordService.SendDebugMessage(message);
}
private string BuildBacktestMessage(Backtest backtest) private string BuildBacktestMessage(Backtest backtest)
{ {
var config = backtest.Config; var config = backtest.Config;

View File

@@ -112,9 +112,9 @@ namespace Managing.Common
public static decimal MinimumSwapEthBalanceUsd = 1m; public static decimal MinimumSwapEthBalanceUsd = 1m;
public const decimal MaximumGasFeeUsd = 1.5m; public const decimal MaximumGasFeeUsd = 1.5m;
public const double AutoSwapAmount = 3; public const double AutoSwapAmount = 3;
// Fee Configuration // Fee Configuration
public const decimal UiFeeRate = 0.001m; // 0.1% UI fee rate public const decimal UiFeeRate = 0.00075m; // 0.1% UI fee rate
public const decimal GasFeePerTransaction = 0.15m; // $0.15 gas fee per transaction public const decimal GasFeePerTransaction = 0.15m; // $0.15 gas fee per transaction
} }

View File

@@ -503,6 +503,19 @@ namespace Managing.Infrastructure.Messengers.Discord
} }
} }
public async Task SendDebugMessage(string message)
{
try
{
var channel = _client.GetChannel(_settings.DebugChannelId) as IMessageChannel;
await channel.SendMessageAsync(message);
}
catch (Exception e)
{
SentrySdk.CaptureException(e);
}
}
public async Task SendClosedPosition(string address, Trade oldTrade) public async Task SendClosedPosition(string address, Trade oldTrade)
{ {
var fields = new List<EmbedFieldBuilder>() var fields = new List<EmbedFieldBuilder>()

View File

@@ -18,12 +18,14 @@ namespace Managing.Infrastructure.Messengers.Discord
ButtonExpirationMinutes = config.GetValue<int>("Discord:ButtonExpirationMinutes"); ButtonExpirationMinutes = config.GetValue<int>("Discord:ButtonExpirationMinutes");
HandleUserAction = config.GetValue<bool>("Discord:HandleUserAction"); HandleUserAction = config.GetValue<bool>("Discord:HandleUserAction");
BotActivity = config.GetValue<string>("Discord:BotActivity"); BotActivity = config.GetValue<string>("Discord:BotActivity");
DebugChannelId = config.GetValue<ulong>("Discord:DebugChannelId");
BotEnabled = true; BotEnabled = true;
} }
public int ButtonExpirationMinutes { get; set; } public int ButtonExpirationMinutes { get; set; }
public bool HandleUserAction { get; } public bool HandleUserAction { get; }
public string BotActivity { get; } public string BotActivity { get; }
public ulong DebugChannelId { get; }
public string Token { get; } public string Token { get; }
public ulong SignalChannelId { get; } public ulong SignalChannelId { get; }
public ulong CopyTradingChannelId { get; } public ulong CopyTradingChannelId { get; }

View File

@@ -4,7 +4,7 @@ import {getClientForAddress, getPositionHistoryImpl} from '../../src/plugins/cus
test('GMX get position history - Closed positions with actual PnL', async (t) => { test('GMX get position history - Closed positions with actual PnL', async (t) => {
await t.test('should get closed positions with actual GMX PnL data', async () => { await t.test('should get closed positions with actual GMX PnL data', async () => {
const sdk = await getClientForAddress('0x987b67313ee4827FE55e1FBcd8883D3bb0Bde83b') const sdk = await getClientForAddress('0x8767F195D1a3103789230aaaE9c0E0825a9802c6')
const result = await getPositionHistoryImpl( const result = await getPositionHistoryImpl(
sdk, sdk,
@@ -50,7 +50,7 @@ test('GMX get position history - Closed positions with actual PnL', async (t) =>
}) })
await t.test('should get closed positions with date range', async () => { await t.test('should get closed positions with date range', async () => {
const sdk = await getClientForAddress('0x987b67313ee4827FE55e1FBcd8883D3bb0Bde83b') const sdk = await getClientForAddress('0x8767F195D1a3103789230aaaE9c0E0825a9802c6')
// Get positions from the last 1 hour // Get positions from the last 1 hour
const toDate = new Date() const toDate = new Date()
@@ -89,7 +89,7 @@ test('GMX get position history - Closed positions with actual PnL', async (t) =>
}) })
await t.test('should verify PnL data is suitable for reconciliation', async () => { await t.test('should verify PnL data is suitable for reconciliation', async () => {
const sdk = await getClientForAddress('0x987b67313ee4827FE55e1FBcd8883D3bb0Bde83b') const sdk = await getClientForAddress('0x8767F195D1a3103789230aaaE9c0E0825a9802c6')
const result = await getPositionHistoryImpl(sdk, 0, 5) const result = await getPositionHistoryImpl(sdk, 0, 5)