Clean code, remove warning for future and spot

This commit is contained in:
2025-12-11 14:36:35 +07:00
parent df8c199cce
commit 1426f0b560
17 changed files with 314 additions and 388 deletions

View File

@@ -1,5 +1,4 @@
using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services;
using Managing.Application.Abstractions.Services;
using Managing.Domain.MoneyManagements; using Managing.Domain.MoneyManagements;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@@ -1,6 +1,4 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Sentry;
using System;
namespace Managing.Api.Controllers namespace Managing.Api.Controllers
{ {

View File

@@ -1,5 +1,3 @@
using Sentry;
namespace Managing.Api.Exceptions; namespace Managing.Api.Exceptions;
/// <summary> /// <summary>
@@ -14,7 +12,8 @@ public static class SentryErrorCapture
/// <param name="contextName">A descriptive name for where the error occurred</param> /// <param name="contextName">A descriptive name for where the error occurred</param>
/// <param name="extraData">Optional dictionary of additional data to include</param> /// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns> /// <returns>The Sentry event ID</returns>
public static SentryId CaptureException(Exception exception, string contextName, IDictionary<string, object> extraData = null) public static SentryId CaptureException(Exception exception, string contextName,
IDictionary<string, object> extraData = null)
{ {
return SentrySdk.CaptureException(exception, scope => return SentrySdk.CaptureException(exception, scope =>
{ {
@@ -76,7 +75,8 @@ public static class SentryErrorCapture
/// <param name="contextName">A descriptive name for where the message originated</param> /// <param name="contextName">A descriptive name for where the message originated</param>
/// <param name="extraData">Optional dictionary of additional data to include</param> /// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns> /// <returns>The Sentry event ID</returns>
public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary<string, object> extraData = null) public static SentryId CaptureMessage(string message, SentryLevel level, string contextName,
IDictionary<string, object> extraData = null)
{ {
// First capture the message with the specified level // First capture the message with the specified level
var id = SentrySdk.CaptureMessage(message, level); var id = SentrySdk.CaptureMessage(message, level);

View File

@@ -1,4 +1,3 @@
using Sentry;
using System.Text; using System.Text;
namespace Managing.Api.Middleware namespace Managing.Api.Middleware
@@ -37,16 +36,19 @@ namespace Managing.Api.Middleware
// Check if Sentry is initialized // Check if Sentry is initialized
response.AppendLine("## Sentry SDK Status"); response.AppendLine("## Sentry SDK Status");
response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}"); response.AppendLine($"Sentry Enabled: {SentrySdk.IsEnabled}");
response.AppendLine($"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}"); response.AppendLine(
$"Application Environment: {Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}");
response.AppendLine(); response.AppendLine();
// Send a test event // Send a test event
response.AppendLine("## Test Event"); response.AppendLine("## Test Event");
try try
{ {
var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}", SentryLevel.Info); var id = SentrySdk.CaptureMessage($"Diagnostics test from {context.Request.Host} at {DateTime.Now}",
SentryLevel.Info);
response.AppendLine($"Test Event ID: {id}"); response.AppendLine($"Test Event ID: {id}");
response.AppendLine("Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received."); response.AppendLine(
"Test event was sent to Sentry. Check your Sentry dashboard to confirm it was received.");
// Try to send an exception too // Try to send an exception too
try try
@@ -69,7 +71,8 @@ namespace Managing.Api.Middleware
response.AppendLine("## Connectivity Check"); response.AppendLine("## Connectivity Check");
response.AppendLine("If events are not appearing in Sentry, check the following:"); response.AppendLine("If events are not appearing in Sentry, check the following:");
response.AppendLine("1. Verify your DSN is correct in appsettings.json"); response.AppendLine("1. Verify your DSN is correct in appsettings.json");
response.AppendLine("2. Ensure your network allows outbound HTTPS connections to sentry.apps.managing.live"); response.AppendLine(
"2. Ensure your network allows outbound HTTPS connections to sentry.apps.managing.live");
response.AppendLine("3. Check Sentry server logs for any ingestion issues"); response.AppendLine("3. Check Sentry server logs for any ingestion issues");
response.AppendLine("4. Verify your Sentry project is correctly configured to receive events"); response.AppendLine("4. Verify your Sentry project is correctly configured to receive events");

View File

@@ -1,4 +1,3 @@
using Managing.Common;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests; namespace Managing.Api.Models.Requests;

View File

@@ -1,4 +1,3 @@
using Managing.Domain.MoneyManagements;
using Xunit; using Xunit;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -161,7 +160,8 @@ namespace Managing.Application.Tests
[InlineData(5, 0.05)] // 5% as percentage - should format to 0.05 [InlineData(5, 0.05)] // 5% as percentage - should format to 0.05
[InlineData(10, 0.1)] // 10% as percentage - should format to 0.1 [InlineData(10, 0.1)] // 10% as percentage - should format to 0.1
[InlineData(50, 0.5)] // 50% as percentage - should format to 0.5 [InlineData(50, 0.5)] // 50% as percentage - should format to 0.5
public void FormatPercentage_WithPercentageValuesGreaterThanOrEqualToOne_ShouldFormat(decimal input, decimal expected) public void FormatPercentage_WithPercentageValuesGreaterThanOrEqualToOne_ShouldFormat(decimal input,
decimal expected)
{ {
// Arrange // Arrange
var moneyManagement = new LightMoneyManagement var moneyManagement = new LightMoneyManagement

View File

@@ -1,4 +1,3 @@
using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Grains; using Managing.Application.Abstractions.Grains;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Commands;
@@ -19,7 +18,7 @@ using static Managing.Common.Enums;
namespace Managing.Application.Bots; namespace Managing.Application.Bots;
public class FuturesBot : TradingBotBase, ITradingBot public class FuturesBot : TradingBotBase
{ {
public FuturesBot( public FuturesBot(
ILogger<TradingBotBase> logger, ILogger<TradingBotBase> logger,
@@ -72,24 +71,14 @@ public class FuturesBot : TradingBotBase, ITradingBot
// For live trading, get position from database via trading service // For live trading, get position from database via trading service
return await ServiceScopeHelpers.WithScopedService<ITradingService, Position>( return await ServiceScopeHelpers.WithScopedService<ITradingService, Position>(
_scopeFactory, _scopeFactory,
async tradingService => { return await tradingService.GetPositionByIdentifierAsync(position.Identifier); }); async tradingService => await tradingService.GetPositionByIdentifierAsync(position.Identifier));
}
protected override async Task UpdatePositionWithBrokerData(Position position, List<Position> brokerPositions)
{
// Live trading broker position synchronization logic is handled in the base UpdatePosition method
// This override allows for any futures-specific synchronization if needed
await base.UpdatePositionWithBrokerData(position, brokerPositions);
} }
protected override async Task<Candle> GetCurrentCandleForPositionClose(Account account, string ticker) protected override async Task<Candle> GetCurrentCandleForPositionClose(Account account, string ticker)
{ {
// For live trading, get real-time candle from exchange // For live trading, get real-time candle from exchange
return await ServiceScopeHelpers.WithScopedService<IExchangeService, Candle>(_scopeFactory, return await ServiceScopeHelpers.WithScopedService<IExchangeService, Candle>(_scopeFactory,
async exchangeService => async exchangeService => await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow));
{
return await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
});
} }
protected override async Task<bool> CheckBrokerPositions() protected override async Task<bool> CheckBrokerPositions()
@@ -170,7 +159,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
} }
else else
{ {
// Broker has a position but we don't have any internal tracking // Broker has a position, but we don't have any internal tracking
Logger.LogWarning( Logger.LogWarning(
$"⚠️ Orphaned Broker Position Detected\n" + $"⚠️ Orphaned Broker Position Detected\n" +
$"Broker has position for {Config.Ticker} ({brokerPositionForTicker.OriginDirection})\n" + $"Broker has position for {Config.Ticker} ({brokerPositionForTicker.OriginDirection})\n" +
@@ -196,7 +185,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
if (Config.TradingType == TradingType.BacktestFutures) return; if (Config.TradingType == TradingType.BacktestFutures) return;
await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService => await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService =>
{ {
var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false); var account = await accountService.GetAccountByAccountName(Config.AccountName, false);
Account = account; Account = account;
}); });
} }
@@ -297,7 +286,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
$"Cannot verify if position is closed\n" + $"Cannot verify if position is closed\n" +
$"Will retry on next execution cycle"); $"Will retry on next execution cycle");
// Don't change position status, wait for next cycle // Don't change position status, wait for next cycle
return;
} }
else if (existsInHistory) else if (existsInHistory)
{ {
@@ -309,7 +297,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
internalPosition.Status = PositionStatus.Finished; internalPosition.Status = PositionStatus.Finished;
await HandleClosedPosition(internalPosition); await HandleClosedPosition(internalPosition);
return;
} }
else else
{ {
@@ -347,11 +334,11 @@ public class FuturesBot : TradingBotBase, ITradingBot
} }
var orders = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Trade>>(_scopeFactory, var orders = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Trade>>(_scopeFactory,
async exchangeService => { return [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]; }); async exchangeService => [.. await exchangeService.GetOpenOrders(Account, Config.Ticker)]);
if (orders.Any()) if (orders.Count != 0)
{ {
var ordersCount = orders.Count(); var ordersCount = orders.Count;
if (ordersCount >= 3) if (ordersCount >= 3)
{ {
var currentTime = DateTime.UtcNow; var currentTime = DateTime.UtcNow;
@@ -386,7 +373,6 @@ public class FuturesBot : TradingBotBase, ITradingBot
positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Cancelled);
await UpdatePositionDatabase(positionForSignal); await UpdatePositionDatabase(positionForSignal);
return;
} }
else else
{ {
@@ -538,7 +524,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
/// </summary> /// </summary>
/// <param name="position">The position to check</param> /// <param name="position">The position to check</param>
/// <returns>True if position found in exchange history with PnL, false otherwise; hadError indicates Web3/infra issues</returns> /// <returns>True if position found in exchange history with PnL, false otherwise; hadError indicates Web3/infra issues</returns>
protected async Task<(bool found, bool hadError)> CheckPositionInExchangeHistory(Position position) private async Task<(bool found, bool hadError)> CheckPositionInExchangeHistory(Position position)
{ {
try try
{ {
@@ -564,7 +550,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue) .OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
.FirstOrDefault(); .FirstOrDefault();
if (recentPosition != null && recentPosition.ProfitAndLoss != null) if (recentPosition is { ProfitAndLoss: not null })
{ {
await LogDebugAsync( await LogDebugAsync(
$"✅ Position Found in Exchange History\n" + $"✅ Position Found in Exchange History\n" +
@@ -757,7 +743,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
.OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue) .OrderByDescending(p => p.Open?.Date ?? DateTime.MinValue)
.FirstOrDefault(); .FirstOrDefault();
if (brokerPosition != null && brokerPosition.ProfitAndLoss != null) if (brokerPosition is { ProfitAndLoss: not null })
{ {
await LogDebugAsync( await LogDebugAsync(
$"✅ Broker Position History Found\n" + $"✅ Broker Position History Found\n" +
@@ -934,7 +920,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
// Fallback to current candle if available // Fallback to current candle if available
if (currentCandle != null) if (currentCandle != null)
{ {
recentCandles = new List<Candle> { currentCandle }; recentCandles = [currentCandle];
} }
else else
{ {
@@ -950,8 +936,8 @@ public class FuturesBot : TradingBotBase, ITradingBot
var minPriceRecent = recentCandles.Min(c => c.Low); var minPriceRecent = recentCandles.Min(c => c.Low);
var maxPriceRecent = recentCandles.Max(c => c.High); var maxPriceRecent = recentCandles.Max(c => c.High);
bool wasStopLossHit = false; var wasStopLossHit = false;
bool wasTakeProfitHit = false; var wasTakeProfitHit = false;
if (position.OriginDirection == TradeDirection.Long) if (position.OriginDirection == TradeDirection.Long)
{ {
@@ -1193,7 +1179,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
signal.Date, signal.Date,
Account.User, Account.User,
Config.BotTradingBalance, Config.BotTradingBalance,
isForPaperTrading: false, // Futures is live trading isForPaperTrading: false,
lastPrice, lastPrice,
signalIdentifier: signal.Identifier, signalIdentifier: signal.Identifier,
initiatorIdentifier: Identifier, initiatorIdentifier: Identifier,
@@ -1203,10 +1189,8 @@ public class FuturesBot : TradingBotBase, ITradingBot
.WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>( .WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>(
_scopeFactory, _scopeFactory,
async (exchangeService, accountService, tradingService) => async (exchangeService, accountService, tradingService) =>
{ await new OpenPositionCommandHandler(exchangeService, accountService, tradingService)
return await new OpenPositionCommandHandler(exchangeService, accountService, tradingService) .Handle(command));
.Handle(command);
});
return position; return position;
} }
@@ -1230,7 +1214,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
if (quantity == 0) if (quantity == 0)
{ {
await LogDebugAsync($"✅ Trade already closed on exchange for position: `{position.Identifier}`"); await LogDebugAsync($"✅ Trade already closed on exchange for position: `{position.Identifier}`");
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, forceMarketClose); await HandleClosedPosition(position, forceMarketClose ? lastPrice : null, forceMarketClose);
} }
else else
{ {
@@ -1258,7 +1242,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
await SetPositionStatus(signal.Identifier, PositionStatus.Finished); await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
} }
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : null,
forceMarketClose); forceMarketClose);
} }
else else
@@ -1275,7 +1259,7 @@ public class FuturesBot : TradingBotBase, ITradingBot
// Trade close on exchange => Should close trade manually // Trade close on exchange => Should close trade manually
await SetPositionStatus(signal.Identifier, PositionStatus.Finished); await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
// Ensure trade dates are properly updated even for canceled/rejected positions // Ensure trade dates are properly updated even for canceled/rejected positions
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, await HandleClosedPosition(position, forceMarketClose ? lastPrice : null,
forceMarketClose); forceMarketClose);
} }
} }

View File

@@ -94,7 +94,7 @@ public class SpotBot : TradingBotBase
// Try to get current price from exchange // Try to get current price from exchange
currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>( currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(
_scopeFactory, _scopeFactory,
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker));
} }
if (currentPrice == 0) if (currentPrice == 0)
@@ -144,10 +144,7 @@ public class SpotBot : TradingBotBase
{ {
// For live trading, get real-time candle from exchange // For live trading, get real-time candle from exchange
return await ServiceScopeHelpers.WithScopedService<IExchangeService, Candle>(_scopeFactory, return await ServiceScopeHelpers.WithScopedService<IExchangeService, Candle>(_scopeFactory,
async exchangeService => async exchangeService => await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow));
{
return await exchangeService.GetCandle(Account, Config.Ticker, DateTime.UtcNow);
});
} }
protected override async Task<bool> CheckBrokerPositions() protected override async Task<bool> CheckBrokerPositions()
@@ -164,7 +161,7 @@ public class SpotBot : TradingBotBase
if (hasOpenPosition) if (hasOpenPosition)
{ {
// We have an internal position - verify it matches broker balance // We have an internal position - verify it matches broker balance
if (tokenBalance != null && tokenBalance.Amount > 0) if (tokenBalance is { Amount: > 0 })
{ {
await LogDebugAsync( await LogDebugAsync(
$"✅ Spot Position Verified\n" + $"✅ Spot Position Verified\n" +
@@ -174,8 +171,7 @@ public class SpotBot : TradingBotBase
$"Position matches broker balance"); $"Position matches broker balance");
return false; // Position already open, cannot open new one return false; // Position already open, cannot open new one
} }
else
{
await LogWarningAsync( await LogWarningAsync(
$"⚠️ Position Mismatch\n" + $"⚠️ Position Mismatch\n" +
$"Ticker: {Config.Ticker}\n" + $"Ticker: {Config.Ticker}\n" +
@@ -183,8 +179,8 @@ public class SpotBot : TradingBotBase
$"Position may need synchronization"); $"Position may need synchronization");
return false; // Don't allow opening new position until resolved return false; // Don't allow opening new position until resolved
} }
}
else if (tokenBalance != null && tokenBalance.Value > 1m) if (tokenBalance is { Value: > 1m })
{ {
// We have a token balance but no internal position - orphaned position // We have a token balance but no internal position - orphaned position
await LogWarningAsync( await LogWarningAsync(
@@ -212,7 +208,7 @@ public class SpotBot : TradingBotBase
if (Config.TradingType == TradingType.BacktestSpot) return; if (Config.TradingType == TradingType.BacktestSpot) return;
await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService => await ServiceScopeHelpers.WithScopedService<IAccountService>(_scopeFactory, async accountService =>
{ {
var account = await accountService.GetAccountByAccountName(Config.AccountName, false, false); var account = await accountService.GetAccountByAccountName(Config.AccountName, false);
Account = account; Account = account;
}); });
} }
@@ -227,7 +223,7 @@ public class SpotBot : TradingBotBase
_scopeFactory, _scopeFactory,
async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker)); async exchangeService => await exchangeService.GetBalance(Account, Config.Ticker));
if (tokenBalance != null && tokenBalance.Amount > 0) if (tokenBalance is { Amount: > 0 })
{ {
// Verify that the token balance matches the position amount with 0.1% tolerance // Verify that the token balance matches the position amount with 0.1% tolerance
var positionQuantity = internalPosition.Open.Quantity; var positionQuantity = internalPosition.Open.Quantity;
@@ -281,7 +277,7 @@ public class SpotBot : TradingBotBase
// Calculate and update PnL based on current price // Calculate and update PnL based on current price
var currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>( var currentPrice = await ServiceScopeHelpers.WithScopedService<IExchangeService, decimal>(
_scopeFactory, _scopeFactory,
async exchangeService => { return await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); async exchangeService => await exchangeService.GetCurrentPrice(Account, Config.Ticker));
if (currentPrice > 0) if (currentPrice > 0)
{ {
@@ -353,17 +349,16 @@ public class SpotBot : TradingBotBase
await LogDebugAsync( await LogDebugAsync(
$"🔍 Checking Spot Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`"); $"🔍 Checking Spot Position History for Position: `{position.Identifier}`\nTicker: `{Config.Ticker}`");
List<Position> positionHistory = null; var positionHistory = await ServiceScopeHelpers.WithScopedService<IExchangeService, List<Position>>(
await ServiceScopeHelpers.WithScopedService<IExchangeService>(_scopeFactory, _scopeFactory,
async exchangeService => async exchangeService =>
{ {
var fromDate = DateTime.UtcNow.AddHours(-24); var fromDate = DateTime.UtcNow.AddHours(-24);
var toDate = DateTime.UtcNow; var toDate = DateTime.UtcNow;
positionHistory = return await exchangeService.GetSpotPositionHistory(Account, Config.Ticker, fromDate, toDate);
await exchangeService.GetSpotPositionHistory(Account, Config.Ticker, fromDate, toDate);
}); });
if (positionHistory != null && positionHistory.Any()) if (positionHistory != null && positionHistory.Count != 0)
{ {
var recentPosition = positionHistory var recentPosition = positionHistory
.OrderByDescending(p => p.Date) .OrderByDescending(p => p.Date)
@@ -415,28 +410,28 @@ public class SpotBot : TradingBotBase
} }
} }
protected override async Task MonitorSynthRisk(LightSignal signal, Position position) protected override Task MonitorSynthRisk(LightSignal signal, Position position)
{ {
// Spot trading doesn't use Synth risk monitoring (futures-specific feature) // Spot trading doesn't use Synth risk monitoring (futures-specific feature)
return; return Task.CompletedTask;
} }
protected override async Task<bool> RecoverOpenPositionFromBroker(LightSignal signal, Position positionForSignal) protected override Task<bool> RecoverOpenPositionFromBroker(LightSignal signal, Position positionForSignal)
{ {
// Spot trading doesn't have broker positions to recover // Spot trading doesn't have broker positions to recover
// Positions are token balances, not tracked positions // Positions are token balances, not tracked positions
return false; return Task.FromResult(false);
} }
protected override async Task<bool> ReconcileWithBrokerHistory(Position position, Candle currentCandle) protected override Task<bool> ReconcileWithBrokerHistory(Position position, Candle currentCandle)
{ {
// Spot trading doesn't have broker position history like futures // Spot trading doesn't have broker position history like futures
// Return false to continue with candle-based calculation // Return false to continue with candle-based calculation
return false; return Task.FromResult(false);
} }
protected override async Task<(decimal closingPrice, bool pnlCalculated)> CalculatePositionClosingFromCandles( protected override Task<(decimal closingPrice, bool pnlCalculated)> CalculatePositionClosingFromCandles(
Position position, Candle currentCandle, bool forceMarketClose, decimal? forcedClosingPrice) Position position, Candle? currentCandle, bool forceMarketClose, decimal? forcedClosingPrice)
{ {
decimal closingPrice = 0; decimal closingPrice = 0;
bool pnlCalculated = false; bool pnlCalculated = false;
@@ -529,7 +524,7 @@ public class SpotBot : TradingBotBase
? closingPrice > position.Open.Price ? closingPrice > position.Open.Price
: closingPrice < position.Open.Price; : closingPrice < position.Open.Price;
if (isManualCloseProfitable) if (isManualCloseProfitable && position.TakeProfit1 != null)
{ {
position.TakeProfit1.SetPrice(closingPrice, 2); position.TakeProfit1.SetPrice(closingPrice, 2);
position.TakeProfit1.SetDate(currentCandle.Date); position.TakeProfit1.SetDate(currentCandle.Date);
@@ -542,9 +537,9 @@ public class SpotBot : TradingBotBase
} }
else else
{ {
position.StopLoss.SetPrice(closingPrice, 2); position.StopLoss?.SetPrice(closingPrice, 2);
position.StopLoss.SetDate(currentCandle.Date); position.StopLoss?.SetDate(currentCandle.Date);
position.StopLoss.SetStatus(TradeStatus.Filled); position.StopLoss?.SetStatus(TradeStatus.Filled);
if (position.TakeProfit1 != null) if (position.TakeProfit1 != null)
{ {
@@ -561,11 +556,11 @@ public class SpotBot : TradingBotBase
pnlCalculated = true; pnlCalculated = true;
} }
return (closingPrice, pnlCalculated); return Task.FromResult((closingPrice, pnlCalculated));
} }
protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles, protected override async Task UpdateSignalsCore(IReadOnlyList<Candle> candles,
Dictionary<IndicatorType, IndicatorsResultBase> preCalculatedIndicatorValues = null) Dictionary<IndicatorType, IndicatorsResultBase>? preCalculatedIndicatorValues = null)
{ {
// For spot trading, always fetch signals regardless of open positions // For spot trading, always fetch signals regardless of open positions
// Check if we're in cooldown period // Check if we're in cooldown period
@@ -625,7 +620,7 @@ public class SpotBot : TradingBotBase
return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal); return !await IsInCooldownPeriodAsync() && await CheckLossStreak(signal);
} }
protected override async Task<Position> HandleFlipPosition(LightSignal signal, Position openedPosition, protected override async Task<Position?> HandleFlipPosition(LightSignal signal, Position openedPosition,
LightSignal previousSignal, decimal lastPrice) LightSignal previousSignal, decimal lastPrice)
{ {
// For spot trading, SHORT signals should close the open LONG position // For spot trading, SHORT signals should close the open LONG position
@@ -694,10 +689,8 @@ public class SpotBot : TradingBotBase
.WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>( .WithScopedServices<IExchangeService, IAccountService, ITradingService, Position>(
_scopeFactory, _scopeFactory,
async (exchangeService, accountService, tradingService) => async (exchangeService, accountService, tradingService) =>
{ await new OpenSpotPositionCommandHandler(exchangeService, accountService, tradingService)
return await new OpenSpotPositionCommandHandler(exchangeService, accountService, tradingService) .Handle(command));
.Handle(command);
});
return position; return position;
} }
@@ -725,7 +718,7 @@ public class SpotBot : TradingBotBase
await SetPositionStatus(signal.Identifier, PositionStatus.Finished); await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
} }
await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : (decimal?)null, await HandleClosedPosition(closedPosition, forceMarketClose ? lastPrice : null,
forceMarketClose); forceMarketClose);
} }
else else
@@ -742,7 +735,7 @@ public class SpotBot : TradingBotBase
// Trade close on exchange => Should close trade manually // Trade close on exchange => Should close trade manually
await SetPositionStatus(signal.Identifier, PositionStatus.Finished); await SetPositionStatus(signal.Identifier, PositionStatus.Finished);
// Ensure trade dates are properly updated even for canceled/rejected positions // Ensure trade dates are properly updated even for canceled/rejected positions
await HandleClosedPosition(position, forceMarketClose ? lastPrice : (decimal?)null, await HandleClosedPosition(position, forceMarketClose ? lastPrice : null,
forceMarketClose); forceMarketClose);
} }
} }

View File

@@ -2,14 +2,13 @@ using Managing.Application.Abstractions;
using Managing.Application.Abstractions.Services; using Managing.Application.Abstractions.Services;
using Managing.Application.Trading.Commands; using Managing.Application.Trading.Commands;
using Managing.Common; using Managing.Common;
using Managing.Core.Exceptions;
using Managing.Domain.Accounts; using Managing.Domain.Accounts;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using static Managing.Common.Enums; using static Managing.Common.Enums;
namespace Managing.Application.Trading.Handlers namespace Managing.Application.Trading.Handlers;
{
public class OpenSpotPositionCommandHandler( public class OpenSpotPositionCommandHandler(
IExchangeService exchangeService, IExchangeService exchangeService,
IAccountService accountService, IAccountService accountService,
@@ -50,31 +49,10 @@ namespace Managing.Application.Trading.Handlers
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now); : await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
var quantity = balanceToRisk / price; var quantity = balanceToRisk / price;
var openPrice = request.IsForPaperTrading || request.Price.HasValue var openPrice = request.IsForPaperTrading
? request.Price.Value ? request.Price ?? price
: price; : price;
// For spot trading, determine swap direction
// Long: Swap USDC -> Token (buy token with USDC)
// Short: Swap Token -> USDC (sell token for USDC)
Ticker fromTicker;
Ticker toTicker;
double swapAmount;
if (request.Direction == TradeDirection.Long)
{
fromTicker = Ticker.USDC;
toTicker = request.Ticker;
swapAmount = (double)balanceToRisk;
}
else
{
fromTicker = request.Ticker;
toTicker = Ticker.USDC;
swapAmount = (double)quantity;
}
// For backtest/paper trading, simulate the swap without calling the exchange
SwapInfos swapResult; SwapInfos swapResult;
if (request.IsForPaperTrading) if (request.IsForPaperTrading)
{ {
@@ -92,18 +70,16 @@ namespace Managing.Application.Trading.Handlers
swapResult = await tradingService.SwapGmxTokensAsync( swapResult = await tradingService.SwapGmxTokensAsync(
request.User, request.User,
request.AccountName, request.AccountName,
fromTicker, Ticker.USDC,
toTicker, request.Ticker,
swapAmount, (double)balanceToRisk);
"market",
null,
0.5);
} }
if (!swapResult.Success) if (!swapResult.Success)
{ {
position.Status = PositionStatus.Rejected; position.Status = PositionStatus.Rejected;
throw new InvalidOperationException($"Failed to open spot position: {swapResult.Error ?? swapResult.Message}"); throw new InvalidOperationException(
$"Failed to open spot position: {swapResult.Error ?? swapResult.Message}");
} }
// Build the opening trade // Build the opening trade
@@ -123,13 +99,9 @@ namespace Managing.Application.Trading.Handlers
position.GasFees = TradingBox.CalculateOpeningGasFees(); position.GasFees = TradingBox.CalculateOpeningGasFees();
// Set UI fees for opening // Set UI fees for opening
var positionSizeUsd = TradingBox.GetVolumeForPosition(position); var positionSizeUsd = position.Open.Quantity * position.Open.Price;
position.UiFees = TradingBox.CalculateOpeningUiFees(positionSizeUsd); position.UiFees = TradingBox.CalculateOpeningUiFees(positionSizeUsd);
var closeDirection = request.Direction == TradeDirection.Long
? TradeDirection.Short
: TradeDirection.Long;
// Determine SL/TP Prices // Determine SL/TP Prices
var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement); var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement);
var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement); var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement);
@@ -139,7 +111,7 @@ namespace Managing.Application.Trading.Handlers
request.Ticker, request.Ticker,
stopLossPrice, stopLossPrice,
position.Open.Quantity, position.Open.Quantity,
closeDirection, TradeDirection.Short,
1, // Spot trading has no leverage 1, // Spot trading has no leverage
TradeType.StopLoss, TradeType.StopLoss,
request.Date, request.Date,
@@ -150,7 +122,7 @@ namespace Managing.Application.Trading.Handlers
request.Ticker, request.Ticker,
takeProfitPrice, takeProfitPrice,
quantity, quantity,
closeDirection, TradeDirection.Short,
1, // Spot trading has no leverage 1, // Spot trading has no leverage
TradeType.TakeProfit, TradeType.TakeProfit,
request.Date, request.Date,
@@ -176,9 +148,6 @@ namespace Managing.Application.Trading.Handlers
private static bool IsOpenTradeHandled(TradeStatus tradeStatus) private static bool IsOpenTradeHandled(TradeStatus tradeStatus)
{ {
return tradeStatus == TradeStatus.Filled return tradeStatus is TradeStatus.Filled or TradeStatus.Requested;
|| tradeStatus == TradeStatus.Requested;
} }
} }
}

View File

@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry; using OpenTelemetry;
using OpenTelemetry.Metrics; using OpenTelemetry.Metrics;
using OpenTelemetry.Trace; using OpenTelemetry.Trace;

View File

@@ -1,5 +1,3 @@
using Sentry;
namespace Managing.Core.Exceptions; namespace Managing.Core.Exceptions;
/// <summary> /// <summary>
@@ -14,7 +12,8 @@ public static class SentryErrorCapture
/// <param name="contextName">A descriptive name for where the error occurred</param> /// <param name="contextName">A descriptive name for where the error occurred</param>
/// <param name="extraData">Optional dictionary of additional data to include</param> /// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns> /// <returns>The Sentry event ID</returns>
public static SentryId CaptureException(Exception exception, string contextName, IDictionary<string, object> extraData = null) public static SentryId CaptureException(Exception exception, string contextName,
IDictionary<string, object> extraData = null)
{ {
return SentrySdk.CaptureException(exception, scope => return SentrySdk.CaptureException(exception, scope =>
{ {
@@ -76,7 +75,8 @@ public static class SentryErrorCapture
/// <param name="contextName">A descriptive name for where the message originated</param> /// <param name="contextName">A descriptive name for where the message originated</param>
/// <param name="extraData">Optional dictionary of additional data to include</param> /// <param name="extraData">Optional dictionary of additional data to include</param>
/// <returns>The Sentry event ID</returns> /// <returns>The Sentry event ID</returns>
public static SentryId CaptureMessage(string message, SentryLevel level, string contextName, IDictionary<string, object> extraData = null) public static SentryId CaptureMessage(string message, SentryLevel level, string contextName,
IDictionary<string, object> extraData = null)
{ {
// First capture the message with the specified level // First capture the message with the specified level
var id = SentrySdk.CaptureMessage(message, level); var id = SentrySdk.CaptureMessage(message, level);

View File

@@ -1,9 +1,8 @@
using System.Net; using System.Net;
using System.Text.Json; using System.Text.Json;
using Managing.Core.Exceptions;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sentry;
using Managing.Core.Exceptions;
namespace Managing.Core.Middleawares; namespace Managing.Core.Middleawares;

View File

@@ -2,7 +2,6 @@ using System.Text;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sentry;
namespace Managing.Core.Middleawares; namespace Managing.Core.Middleawares;

View File

@@ -1,7 +1,7 @@
using FluentAssertions; using FluentAssertions;
using Managing.Common; using Managing.Common;
using Managing.Domain.Candles;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using static Managing.Common.Enums;
using Xunit; using Xunit;
namespace Managing.Domain.SimpleTests; namespace Managing.Domain.SimpleTests;
@@ -16,12 +16,12 @@ public class SimpleTradingBoxTests
public void GetHodlPercentage_WithPriceIncrease_CalculatesCorrectPercentage() public void GetHodlPercentage_WithPriceIncrease_CalculatesCorrectPercentage()
{ {
// Arrange // Arrange
var candle1 = new Managing.Domain.Candles.Candle var candle1 = new Candle
{ {
Close = 100m, Close = 100m,
Date = DateTime.UtcNow Date = DateTime.UtcNow
}; };
var candle2 = new Managing.Domain.Candles.Candle var candle2 = new Candle
{ {
Close = 110m, Close = 110m,
Date = DateTime.UtcNow.AddHours(1) Date = DateTime.UtcNow.AddHours(1)
@@ -38,12 +38,12 @@ public class SimpleTradingBoxTests
public void GetHodlPercentage_WithPriceDecrease_CalculatesNegativePercentage() public void GetHodlPercentage_WithPriceDecrease_CalculatesNegativePercentage()
{ {
// Arrange // Arrange
var candle1 = new Managing.Domain.Candles.Candle var candle1 = new Candle
{ {
Close = 100m, Close = 100m,
Date = DateTime.UtcNow Date = DateTime.UtcNow
}; };
var candle2 = new Managing.Domain.Candles.Candle var candle2 = new Candle
{ {
Close = 90m, Close = 90m,
Date = DateTime.UtcNow.AddHours(1) Date = DateTime.UtcNow.AddHours(1)

View File

@@ -1,15 +1,6 @@
using FluentAssertions; using FluentAssertions;
using Managing.Common;
using Managing.Domain.Accounts;
using Managing.Domain.Candles; using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.MoneyManagements;
using Managing.Domain.Scenarios;
using Managing.Domain.Shared.Helpers; using Managing.Domain.Shared.Helpers;
using Managing.Domain.Statistics;
using Managing.Domain.Strategies;
using Managing.Domain.Strategies.Base;
using Managing.Domain.Trades;
using Xunit; using Xunit;
using static Managing.Common.Enums; using static Managing.Common.Enums;
@@ -25,12 +16,12 @@ public class SimpleTradingBoxTests
public void GetHodlPercentage_WithPriceIncrease_CalculatesCorrectPercentage() public void GetHodlPercentage_WithPriceIncrease_CalculatesCorrectPercentage()
{ {
// Arrange // Arrange
var candle1 = new Managing.Domain.Candles.Candle var candle1 = new Candle
{ {
Close = 100m, Close = 100m,
Date = DateTime.UtcNow Date = DateTime.UtcNow
}; };
var candle2 = new Managing.Domain.Candles.Candle var candle2 = new Candle
{ {
Close = 110m, Close = 110m,
Date = DateTime.UtcNow.AddHours(1) Date = DateTime.UtcNow.AddHours(1)
@@ -47,12 +38,12 @@ public class SimpleTradingBoxTests
public void GetHodlPercentage_WithPriceDecrease_CalculatesNegativePercentage() public void GetHodlPercentage_WithPriceDecrease_CalculatesNegativePercentage()
{ {
// Arrange // Arrange
var candle1 = new Managing.Domain.Candles.Candle var candle1 = new Candle
{ {
Close = 100m, Close = 100m,
Date = DateTime.UtcNow Date = DateTime.UtcNow
}; };
var candle2 = new Managing.Domain.Candles.Candle var candle2 = new Candle
{ {
Close = 90m, Close = 90m,
Date = DateTime.UtcNow.AddHours(1) Date = DateTime.UtcNow.AddHours(1)

View File

@@ -1,11 +1,6 @@
using Managing.Common; using Managing.Common;
using Managing.Domain.Trades; using Managing.Domain.Trades;
using Managing.Infrastructure.Evm;
using Managing.Infrastructure.Evm.Models.Privy;
using Managing.Infrastructure.Evm.Services;
using Xunit; using Xunit;
using Managing.Infrastructure.Evm.Abstractions;
using Microsoft.Extensions.Options;
namespace Managing.Infrastructure.Tests; namespace Managing.Infrastructure.Tests;

View File

@@ -1,12 +1,10 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Managing.Domain.Trades;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Managing.Infrastructure.Evm.Models.Proxy; namespace Managing.Infrastructure.Evm.Models.Proxy;
public class GetGmxTradesResponse : Web3ProxyBaseResponse public class GetGmxTradesResponse : Web3ProxyBaseResponse
{ {
[JsonProperty("trades")] [JsonProperty("trades")]
[JsonPropertyName("trades")] [JsonPropertyName("trades")]
public List<GmxTrade> Trades { get; set; } public List<GmxTrade> Trades { get; set; }