Clean code, remove warning for future and spot
This commit is contained in:
@@ -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;
|
||||||
@@ -84,12 +83,12 @@ public class MoneyManagementController : BaseController
|
|||||||
{
|
{
|
||||||
var user = await GetUser();
|
var user = await GetUser();
|
||||||
var result = await _moneyManagementService.GetMoneyMangement(user, name);
|
var result = await _moneyManagementService.GetMoneyMangement(user, name);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
{
|
{
|
||||||
return NotFound($"Money management strategy '{name}' not found");
|
return NotFound($"Money management strategy '{name}' not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -110,12 +109,12 @@ public class MoneyManagementController : BaseController
|
|||||||
{
|
{
|
||||||
var user = await GetUser();
|
var user = await GetUser();
|
||||||
var result = await _moneyManagementService.DeleteMoneyManagement(user, name);
|
var result = await _moneyManagementService.DeleteMoneyManagement(user, name);
|
||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
{
|
{
|
||||||
return NotFound($"Money management strategy '{name}' not found or could not be deleted");
|
return NotFound($"Money management strategy '{name}' not found or could not be deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new { success = true, message = $"Money management strategy '{name}' deleted successfully" });
|
return Ok(new { success = true, message = $"Money management strategy '{name}' deleted successfully" });
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Sentry;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Managing.Api.Controllers
|
namespace Managing.Api.Controllers
|
||||||
{
|
{
|
||||||
@@ -26,22 +24,22 @@ namespace Managing.Api.Controllers
|
|||||||
{
|
{
|
||||||
// Add breadcrumbs for context
|
// Add breadcrumbs for context
|
||||||
SentrySdk.AddBreadcrumb("About to capture test exception", "test");
|
SentrySdk.AddBreadcrumb("About to capture test exception", "test");
|
||||||
|
|
||||||
// Add context to the error
|
// Add context to the error
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
scope.SetTag("test_type", "manual_exception");
|
scope.SetTag("test_type", "manual_exception");
|
||||||
scope.SetExtra("timestamp", DateTime.Now);
|
scope.SetExtra("timestamp", DateTime.Now);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log to both Serilog and Sentry
|
// Log to both Serilog and Sentry
|
||||||
_logger.LogError(ex, "Test exception captured in SentryTestController");
|
_logger.LogError(ex, "Test exception captured in SentryTestController");
|
||||||
|
|
||||||
// Explicitly capture exception
|
// Explicitly capture exception
|
||||||
SentrySdk.CaptureException(ex);
|
SentrySdk.CaptureException(ex);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
message = "Exception manually captured and sent to Sentry",
|
message = "Exception manually captured and sent to Sentry",
|
||||||
exceptionMessage = ex.Message,
|
exceptionMessage = ex.Message,
|
||||||
timestamp = DateTime.Now
|
timestamp = DateTime.Now
|
||||||
@@ -53,7 +51,7 @@ namespace Managing.Api.Controllers
|
|||||||
public IActionResult ThrowException()
|
public IActionResult ThrowException()
|
||||||
{
|
{
|
||||||
_logger.LogInformation("About to throw an uncaught exception");
|
_logger.LogInformation("About to throw an uncaught exception");
|
||||||
|
|
||||||
// This should be automatically captured by Sentry middleware
|
// This should be automatically captured by Sentry middleware
|
||||||
throw new InvalidOperationException($"Uncaught exception from ThrowException endpoint - {DateTime.Now}");
|
throw new InvalidOperationException($"Uncaught exception from ThrowException endpoint - {DateTime.Now}");
|
||||||
}
|
}
|
||||||
@@ -63,12 +61,12 @@ namespace Managing.Api.Controllers
|
|||||||
{
|
{
|
||||||
// Send a simple message to Sentry
|
// Send a simple message to Sentry
|
||||||
SentrySdk.CaptureMessage("Test message from Managing API", SentryLevel.Info);
|
SentrySdk.CaptureMessage("Test message from Managing API", SentryLevel.Info);
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
message = "Test message sent to Sentry",
|
message = "Test message sent to Sentry",
|
||||||
timestamp = DateTime.Now
|
timestamp = DateTime.Now
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using Sentry;
|
|
||||||
|
|
||||||
namespace Managing.Api.Exceptions;
|
namespace Managing.Api.Exceptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -14,14 +12,15 @@ 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 =>
|
||||||
{
|
{
|
||||||
// Add context information
|
// Add context information
|
||||||
scope.SetTag("context", contextName);
|
scope.SetTag("context", contextName);
|
||||||
scope.SetTag("error_type", exception.GetType().Name);
|
scope.SetTag("error_type", exception.GetType().Name);
|
||||||
|
|
||||||
// Add any extra data provided
|
// Add any extra data provided
|
||||||
if (extraData != null)
|
if (extraData != null)
|
||||||
{
|
{
|
||||||
@@ -30,7 +29,7 @@ public static class SentryErrorCapture
|
|||||||
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add extra info from the exception's Data dictionary if available
|
// Add extra info from the exception's Data dictionary if available
|
||||||
foreach (var key in exception.Data.Keys)
|
foreach (var key in exception.Data.Keys)
|
||||||
{
|
{
|
||||||
@@ -39,7 +38,7 @@ public static class SentryErrorCapture
|
|||||||
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
|
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a breadcrumb for context
|
// Add a breadcrumb for context
|
||||||
scope.AddBreadcrumb(
|
scope.AddBreadcrumb(
|
||||||
message: $"Exception in {contextName}",
|
message: $"Exception in {contextName}",
|
||||||
@@ -48,7 +47,7 @@ public static class SentryErrorCapture
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enriches an exception with additional context data before throwing
|
/// Enriches an exception with additional context data before throwing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -64,10 +63,10 @@ public static class SentryErrorCapture
|
|||||||
exception.Data[item.Key] = item.Value;
|
exception.Data[item.Key] = item.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return exception;
|
return exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Captures a message in Sentry with additional context
|
/// Captures a message in Sentry with additional context
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,17 +75,18 @@ 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);
|
||||||
|
|
||||||
// Then add context via a scope
|
// Then add context via a scope
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
// Add context information
|
// Add context information
|
||||||
scope.SetTag("context", contextName);
|
scope.SetTag("context", contextName);
|
||||||
|
|
||||||
// Add any extra data provided
|
// Add any extra data provided
|
||||||
if (extraData != null)
|
if (extraData != null)
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ public static class SentryErrorCapture
|
|||||||
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a breadcrumb for context
|
// Add a breadcrumb for context
|
||||||
scope.AddBreadcrumb(
|
scope.AddBreadcrumb(
|
||||||
message: $"Message from {contextName}",
|
message: $"Message from {contextName}",
|
||||||
@@ -103,7 +103,7 @@ public static class SentryErrorCapture
|
|||||||
level: BreadcrumbLevel.Info
|
level: BreadcrumbLevel.Info
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using Sentry;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Managing.Api.Middleware
|
namespace Managing.Api.Middleware
|
||||||
@@ -37,17 +36,20 @@ 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,10 +71,11 @@ 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");
|
||||||
|
|
||||||
// Return the diagnostic information
|
// Return the diagnostic information
|
||||||
context.Response.ContentType = "text/plain";
|
context.Response.ContentType = "text/plain";
|
||||||
await context.Response.WriteAsync(response.ToString());
|
await context.Response.WriteAsync(response.ToString());
|
||||||
@@ -87,4 +90,4 @@ namespace Managing.Api.Middleware
|
|||||||
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
|
return builder.UseMiddleware<SentryDiagnosticsMiddleware>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
@@ -7,4 +6,4 @@ public class OpenPositionManuallyRequest
|
|||||||
{
|
{
|
||||||
public string BotName { get; set; }
|
public string BotName { get; set; }
|
||||||
public TradeDirection Direction { get; set; }
|
public TradeDirection Direction { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
using Managing.Domain.MoneyManagements;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
|
|
||||||
@@ -14,8 +13,8 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test1",
|
Name = "Test1",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 10, // 10%
|
StopLoss = 10, // 10%
|
||||||
TakeProfit = 20, // 20%
|
TakeProfit = 20, // 20%
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test2",
|
Name = "Test2",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 0.1m, // Already 0.1 (10%)
|
StopLoss = 0.1m, // Already 0.1 (10%)
|
||||||
TakeProfit = 0.2m, // Already 0.2 (20%)
|
TakeProfit = 0.2m, // Already 0.2 (20%)
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
@@ -56,7 +55,7 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test3",
|
Name = "Test3",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 15, // 15% (should be formatted)
|
StopLoss = 15, // 15% (should be formatted)
|
||||||
TakeProfit = 0.25m, // Already 0.25 (25%) (should NOT be formatted)
|
TakeProfit = 0.25m, // Already 0.25 (25%) (should NOT be formatted)
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
@@ -77,7 +76,7 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test4a",
|
Name = "Test4a",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 0.01m, // 1% as decimal
|
StopLoss = 0.01m, // 1% as decimal
|
||||||
TakeProfit = 0.02m, // 2% as decimal
|
TakeProfit = 0.02m, // 2% as decimal
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
@@ -98,8 +97,8 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test4b",
|
Name = "Test4b",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 1m, // 1% as percentage
|
StopLoss = 1m, // 1% as percentage
|
||||||
TakeProfit = 2m, // 2% as percentage
|
TakeProfit = 2m, // 2% as percentage
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,8 +118,8 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test5",
|
Name = "Test5",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 1.0m, // Exactly 1.0 (boundary)
|
StopLoss = 1.0m, // Exactly 1.0 (boundary)
|
||||||
TakeProfit = 0.5m, // 0.5% (should not be formatted)
|
TakeProfit = 0.5m, // 0.5% (should not be formatted)
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,7 +133,7 @@ namespace Managing.Application.Tests
|
|||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(0.01, 0.01)] // 1% as decimal - should not change
|
[InlineData(0.01, 0.01)] // 1% as decimal - should not change
|
||||||
[InlineData(0.5, 0.5)] // 0.5% as decimal - should not change
|
[InlineData(0.5, 0.5)] // 0.5% as decimal - should not change
|
||||||
[InlineData(0.25, 0.25)] // 0.25% as decimal - should not change
|
[InlineData(0.25, 0.25)] // 0.25% as decimal - should not change
|
||||||
public void FormatPercentage_WithDecimalValuesLessThanOne_ShouldNotFormat(decimal input, decimal expected)
|
public void FormatPercentage_WithDecimalValuesLessThanOne_ShouldNotFormat(decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
@@ -157,11 +156,12 @@ namespace Managing.Application.Tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(1, 0.01)] // 1% as percentage - should format to 0.01
|
[InlineData(1, 0.01)] // 1% as percentage - should format to 0.01
|
||||||
[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
|
||||||
@@ -189,7 +189,7 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test8a",
|
Name = "Test8a",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 0.01m, // 1% as decimal
|
StopLoss = 0.01m, // 1% as decimal
|
||||||
TakeProfit = 0.0075m, // 0.75% as decimal
|
TakeProfit = 0.0075m, // 0.75% as decimal
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
@@ -211,8 +211,8 @@ namespace Managing.Application.Tests
|
|||||||
{
|
{
|
||||||
Name = "Test8b",
|
Name = "Test8b",
|
||||||
Timeframe = Timeframe.FifteenMinutes,
|
Timeframe = Timeframe.FifteenMinutes,
|
||||||
StopLoss = 1m, // 1% as percentage
|
StopLoss = 1m, // 1% as percentage
|
||||||
TakeProfit = 0.75m, // 0.75% as percentage
|
TakeProfit = 0.75m, // 0.75% as percentage
|
||||||
Leverage = 1
|
Leverage = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -224,4 +224,4 @@ namespace Managing.Application.Tests
|
|||||||
Assert.Equal(0.75m, moneyManagement.TakeProfit); // Not formatted because < 1
|
Assert.Equal(0.75m, moneyManagement.TakeProfit); // Not formatted because < 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,17 +171,16 @@ 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" +
|
$"Internal position exists but no token balance found\n" +
|
||||||
$"Internal position exists but no token balance found\n" +
|
$"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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,183 +2,152 @@ 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(
|
||||||
|
IExchangeService exchangeService,
|
||||||
|
IAccountService accountService,
|
||||||
|
ITradingService tradingService)
|
||||||
|
: ICommandHandler<OpenSpotPositionRequest, Position>
|
||||||
{
|
{
|
||||||
public class OpenSpotPositionCommandHandler(
|
public async Task<Position> Handle(OpenSpotPositionRequest request)
|
||||||
IExchangeService exchangeService,
|
|
||||||
IAccountService accountService,
|
|
||||||
ITradingService tradingService)
|
|
||||||
: ICommandHandler<OpenSpotPositionRequest, Position>
|
|
||||||
{
|
{
|
||||||
public async Task<Position> Handle(OpenSpotPositionRequest request)
|
var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false);
|
||||||
|
|
||||||
|
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
|
||||||
|
var position = new Position(Guid.NewGuid(), account.Id, request.Direction,
|
||||||
|
request.Ticker,
|
||||||
|
request.MoneyManagement,
|
||||||
|
initiator, request.Date, request.User);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.SignalIdentifier))
|
||||||
{
|
{
|
||||||
var account = await accountService.GetAccount(request.AccountName, hideSecrets: false, getBalance: false);
|
position.SignalIdentifier = request.SignalIdentifier;
|
||||||
|
|
||||||
var initiator = request.IsForPaperTrading ? PositionInitiator.PaperTrading : request.Initiator;
|
|
||||||
var position = new Position(Guid.NewGuid(), account.Id, request.Direction,
|
|
||||||
request.Ticker,
|
|
||||||
request.MoneyManagement,
|
|
||||||
initiator, request.Date, request.User);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.SignalIdentifier))
|
|
||||||
{
|
|
||||||
position.SignalIdentifier = request.SignalIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
position.InitiatorIdentifier = request.InitiatorIdentifier;
|
|
||||||
position.TradingType = request.TradingType;
|
|
||||||
|
|
||||||
// Always use BotTradingBalance directly as the balance to risk
|
|
||||||
// Round to 2 decimal places to prevent precision errors
|
|
||||||
decimal balanceToRisk = Math.Round(request.AmountToTrade, 0, MidpointRounding.ToZero);
|
|
||||||
|
|
||||||
// Minimum check
|
|
||||||
if (balanceToRisk < Constants.GMX.Config.MinimumPositionAmount)
|
|
||||||
{
|
|
||||||
throw new Exception(
|
|
||||||
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
|
|
||||||
}
|
|
||||||
|
|
||||||
var price = request.IsForPaperTrading && request.Price.HasValue
|
|
||||||
? request.Price.Value
|
|
||||||
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
|
|
||||||
var quantity = balanceToRisk / price;
|
|
||||||
|
|
||||||
var openPrice = request.IsForPaperTrading || request.Price.HasValue
|
|
||||||
? request.Price.Value
|
|
||||||
: 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;
|
|
||||||
if (request.IsForPaperTrading)
|
|
||||||
{
|
|
||||||
// Simulate successful swap for backtest
|
|
||||||
swapResult = new SwapInfos
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Hash = Guid.NewGuid().ToString(),
|
|
||||||
Message = "Backtest spot position opened successfully"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// For live trading, call SwapGmxTokensAsync
|
|
||||||
swapResult = await tradingService.SwapGmxTokensAsync(
|
|
||||||
request.User,
|
|
||||||
request.AccountName,
|
|
||||||
fromTicker,
|
|
||||||
toTicker,
|
|
||||||
swapAmount,
|
|
||||||
"market",
|
|
||||||
null,
|
|
||||||
0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!swapResult.Success)
|
|
||||||
{
|
|
||||||
position.Status = PositionStatus.Rejected;
|
|
||||||
throw new InvalidOperationException($"Failed to open spot position: {swapResult.Error ?? swapResult.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the opening trade
|
|
||||||
var trade = exchangeService.BuildEmptyTrade(
|
|
||||||
request.Ticker,
|
|
||||||
openPrice,
|
|
||||||
quantity,
|
|
||||||
request.Direction,
|
|
||||||
1, // Spot trading has no leverage
|
|
||||||
TradeType.Market,
|
|
||||||
request.Date,
|
|
||||||
TradeStatus.Filled);
|
|
||||||
|
|
||||||
position.Open = trade;
|
|
||||||
|
|
||||||
// Calculate and set fees for the position
|
|
||||||
position.GasFees = TradingBox.CalculateOpeningGasFees();
|
|
||||||
|
|
||||||
// Set UI fees for opening
|
|
||||||
var positionSizeUsd = TradingBox.GetVolumeForPosition(position);
|
|
||||||
position.UiFees = TradingBox.CalculateOpeningUiFees(positionSizeUsd);
|
|
||||||
|
|
||||||
var closeDirection = request.Direction == TradeDirection.Long
|
|
||||||
? TradeDirection.Short
|
|
||||||
: TradeDirection.Long;
|
|
||||||
|
|
||||||
// Determine SL/TP Prices
|
|
||||||
var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement);
|
|
||||||
var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement);
|
|
||||||
|
|
||||||
// Stop loss
|
|
||||||
position.StopLoss = exchangeService.BuildEmptyTrade(
|
|
||||||
request.Ticker,
|
|
||||||
stopLossPrice,
|
|
||||||
position.Open.Quantity,
|
|
||||||
closeDirection,
|
|
||||||
1, // Spot trading has no leverage
|
|
||||||
TradeType.StopLoss,
|
|
||||||
request.Date,
|
|
||||||
TradeStatus.Requested);
|
|
||||||
|
|
||||||
// Take profit
|
|
||||||
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
|
|
||||||
request.Ticker,
|
|
||||||
takeProfitPrice,
|
|
||||||
quantity,
|
|
||||||
closeDirection,
|
|
||||||
1, // Spot trading has no leverage
|
|
||||||
TradeType.TakeProfit,
|
|
||||||
request.Date,
|
|
||||||
TradeStatus.Requested);
|
|
||||||
|
|
||||||
position.Status = IsOpenTradeHandled(position.Open.Status)
|
|
||||||
? position.Status
|
|
||||||
: PositionStatus.Rejected;
|
|
||||||
|
|
||||||
if (position.Status == PositionStatus.Rejected)
|
|
||||||
{
|
|
||||||
SentrySdk.CaptureException(
|
|
||||||
new Exception($"Position {position.Identifier} for {request.SignalIdentifier} rejected"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!request.IsForPaperTrading)
|
|
||||||
{
|
|
||||||
await tradingService.InsertPositionAsync(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsOpenTradeHandled(TradeStatus tradeStatus)
|
position.InitiatorIdentifier = request.InitiatorIdentifier;
|
||||||
|
position.TradingType = request.TradingType;
|
||||||
|
|
||||||
|
// Always use BotTradingBalance directly as the balance to risk
|
||||||
|
// Round to 2 decimal places to prevent precision errors
|
||||||
|
decimal balanceToRisk = Math.Round(request.AmountToTrade, 0, MidpointRounding.ToZero);
|
||||||
|
|
||||||
|
// Minimum check
|
||||||
|
if (balanceToRisk < Constants.GMX.Config.MinimumPositionAmount)
|
||||||
{
|
{
|
||||||
return tradeStatus == TradeStatus.Filled
|
throw new Exception(
|
||||||
|| tradeStatus == TradeStatus.Requested;
|
$"Bot trading balance of {balanceToRisk} USD is less than the minimum {Constants.GMX.Config.MinimumPositionAmount} USD required to trade");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var price = request.IsForPaperTrading && request.Price.HasValue
|
||||||
|
? request.Price.Value
|
||||||
|
: await exchangeService.GetPrice(account, request.Ticker, DateTime.Now);
|
||||||
|
var quantity = balanceToRisk / price;
|
||||||
|
|
||||||
|
var openPrice = request.IsForPaperTrading
|
||||||
|
? request.Price ?? price
|
||||||
|
: price;
|
||||||
|
|
||||||
|
SwapInfos swapResult;
|
||||||
|
if (request.IsForPaperTrading)
|
||||||
|
{
|
||||||
|
// Simulate successful swap for backtest
|
||||||
|
swapResult = new SwapInfos
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Hash = Guid.NewGuid().ToString(),
|
||||||
|
Message = "Backtest spot position opened successfully"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// For live trading, call SwapGmxTokensAsync
|
||||||
|
swapResult = await tradingService.SwapGmxTokensAsync(
|
||||||
|
request.User,
|
||||||
|
request.AccountName,
|
||||||
|
Ticker.USDC,
|
||||||
|
request.Ticker,
|
||||||
|
(double)balanceToRisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!swapResult.Success)
|
||||||
|
{
|
||||||
|
position.Status = PositionStatus.Rejected;
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to open spot position: {swapResult.Error ?? swapResult.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the opening trade
|
||||||
|
var trade = exchangeService.BuildEmptyTrade(
|
||||||
|
request.Ticker,
|
||||||
|
openPrice,
|
||||||
|
quantity,
|
||||||
|
request.Direction,
|
||||||
|
1, // Spot trading has no leverage
|
||||||
|
TradeType.Market,
|
||||||
|
request.Date,
|
||||||
|
TradeStatus.Filled);
|
||||||
|
|
||||||
|
position.Open = trade;
|
||||||
|
|
||||||
|
// Calculate and set fees for the position
|
||||||
|
position.GasFees = TradingBox.CalculateOpeningGasFees();
|
||||||
|
|
||||||
|
// Set UI fees for opening
|
||||||
|
var positionSizeUsd = position.Open.Quantity * position.Open.Price;
|
||||||
|
position.UiFees = TradingBox.CalculateOpeningUiFees(positionSizeUsd);
|
||||||
|
|
||||||
|
// Determine SL/TP Prices
|
||||||
|
var stopLossPrice = RiskHelpers.GetStopLossPrice(request.Direction, openPrice, request.MoneyManagement);
|
||||||
|
var takeProfitPrice = RiskHelpers.GetTakeProfitPrice(request.Direction, openPrice, request.MoneyManagement);
|
||||||
|
|
||||||
|
// Stop loss
|
||||||
|
position.StopLoss = exchangeService.BuildEmptyTrade(
|
||||||
|
request.Ticker,
|
||||||
|
stopLossPrice,
|
||||||
|
position.Open.Quantity,
|
||||||
|
TradeDirection.Short,
|
||||||
|
1, // Spot trading has no leverage
|
||||||
|
TradeType.StopLoss,
|
||||||
|
request.Date,
|
||||||
|
TradeStatus.Requested);
|
||||||
|
|
||||||
|
// Take profit
|
||||||
|
position.TakeProfit1 = exchangeService.BuildEmptyTrade(
|
||||||
|
request.Ticker,
|
||||||
|
takeProfitPrice,
|
||||||
|
quantity,
|
||||||
|
TradeDirection.Short,
|
||||||
|
1, // Spot trading has no leverage
|
||||||
|
TradeType.TakeProfit,
|
||||||
|
request.Date,
|
||||||
|
TradeStatus.Requested);
|
||||||
|
|
||||||
|
position.Status = IsOpenTradeHandled(position.Open.Status)
|
||||||
|
? position.Status
|
||||||
|
: PositionStatus.Rejected;
|
||||||
|
|
||||||
|
if (position.Status == PositionStatus.Rejected)
|
||||||
|
{
|
||||||
|
SentrySdk.CaptureException(
|
||||||
|
new Exception($"Position {position.Identifier} for {request.SignalIdentifier} rejected"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.IsForPaperTrading)
|
||||||
|
{
|
||||||
|
await tradingService.InsertPositionAsync(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private static bool IsOpenTradeHandled(TradeStatus tradeStatus)
|
||||||
|
{
|
||||||
|
return tradeStatus is TradeStatus.Filled or TradeStatus.Requested;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -111,4 +110,4 @@ public static class Extensions
|
|||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
using Sentry;
|
|
||||||
|
|
||||||
namespace Managing.Core.Exceptions;
|
namespace Managing.Core.Exceptions;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -14,14 +12,15 @@ 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 =>
|
||||||
{
|
{
|
||||||
// Add context information
|
// Add context information
|
||||||
scope.SetTag("context", contextName);
|
scope.SetTag("context", contextName);
|
||||||
scope.SetTag("error_type", exception.GetType().Name);
|
scope.SetTag("error_type", exception.GetType().Name);
|
||||||
|
|
||||||
// Add any extra data provided
|
// Add any extra data provided
|
||||||
if (extraData != null)
|
if (extraData != null)
|
||||||
{
|
{
|
||||||
@@ -30,7 +29,7 @@ public static class SentryErrorCapture
|
|||||||
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add extra info from the exception's Data dictionary if available
|
// Add extra info from the exception's Data dictionary if available
|
||||||
foreach (var key in exception.Data.Keys)
|
foreach (var key in exception.Data.Keys)
|
||||||
{
|
{
|
||||||
@@ -39,7 +38,7 @@ public static class SentryErrorCapture
|
|||||||
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
|
scope.SetExtra($"exception_data_{keyStr}", exception.Data[key].ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a breadcrumb for context
|
// Add a breadcrumb for context
|
||||||
scope.AddBreadcrumb(
|
scope.AddBreadcrumb(
|
||||||
message: $"Exception in {contextName}",
|
message: $"Exception in {contextName}",
|
||||||
@@ -48,7 +47,7 @@ public static class SentryErrorCapture
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enriches an exception with additional context data before throwing
|
/// Enriches an exception with additional context data before throwing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -64,10 +63,10 @@ public static class SentryErrorCapture
|
|||||||
exception.Data[item.Key] = item.Value;
|
exception.Data[item.Key] = item.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return exception;
|
return exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Captures a message in Sentry with additional context
|
/// Captures a message in Sentry with additional context
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,17 +75,18 @@ 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);
|
||||||
|
|
||||||
// Then add context via a scope
|
// Then add context via a scope
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
// Add context information
|
// Add context information
|
||||||
scope.SetTag("context", contextName);
|
scope.SetTag("context", contextName);
|
||||||
|
|
||||||
// Add any extra data provided
|
// Add any extra data provided
|
||||||
if (extraData != null)
|
if (extraData != null)
|
||||||
{
|
{
|
||||||
@@ -95,7 +95,7 @@ public static class SentryErrorCapture
|
|||||||
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
scope.SetExtra(kvp.Key, kvp.Value?.ToString() ?? "null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a breadcrumb for context
|
// Add a breadcrumb for context
|
||||||
scope.AddBreadcrumb(
|
scope.AddBreadcrumb(
|
||||||
message: $"Message from {contextName}",
|
message: $"Message from {contextName}",
|
||||||
@@ -103,7 +103,7 @@ public static class SentryErrorCapture
|
|||||||
level: BreadcrumbLevel.Info
|
level: BreadcrumbLevel.Info
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
@@ -11,13 +10,13 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly ILogger<GlobalErrorHandlingMiddleware> _logger;
|
private readonly ILogger<GlobalErrorHandlingMiddleware> _logger;
|
||||||
|
|
||||||
public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger<GlobalErrorHandlingMiddleware> logger)
|
public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger<GlobalErrorHandlingMiddleware> logger)
|
||||||
{
|
{
|
||||||
_next = next;
|
_next = next;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -29,12 +28,12 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
await HandleExceptionAsync(context, ex);
|
await HandleExceptionAsync(context, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task HandleExceptionAsync(HttpContext context, Exception exception)
|
private Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||||
{
|
{
|
||||||
HttpStatusCode status;
|
HttpStatusCode status;
|
||||||
string errorMessage;
|
string errorMessage;
|
||||||
|
|
||||||
// Determine the appropriate status code based on exception type
|
// Determine the appropriate status code based on exception type
|
||||||
status = exception switch
|
status = exception switch
|
||||||
{
|
{
|
||||||
@@ -43,41 +42,41 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
ValidationException => HttpStatusCode.BadRequest,
|
ValidationException => HttpStatusCode.BadRequest,
|
||||||
FormatException => HttpStatusCode.BadRequest,
|
FormatException => HttpStatusCode.BadRequest,
|
||||||
InvalidOperationException => HttpStatusCode.BadRequest,
|
InvalidOperationException => HttpStatusCode.BadRequest,
|
||||||
|
|
||||||
// 401 Unauthorized
|
// 401 Unauthorized
|
||||||
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
|
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
|
||||||
|
|
||||||
// 403 Forbidden
|
// 403 Forbidden
|
||||||
ForbiddenException => HttpStatusCode.Forbidden,
|
ForbiddenException => HttpStatusCode.Forbidden,
|
||||||
|
|
||||||
// 404 Not Found
|
// 404 Not Found
|
||||||
KeyNotFoundException => HttpStatusCode.NotFound,
|
KeyNotFoundException => HttpStatusCode.NotFound,
|
||||||
FileNotFoundException => HttpStatusCode.NotFound,
|
FileNotFoundException => HttpStatusCode.NotFound,
|
||||||
DirectoryNotFoundException => HttpStatusCode.NotFound,
|
DirectoryNotFoundException => HttpStatusCode.NotFound,
|
||||||
NotFoundException => HttpStatusCode.NotFound,
|
NotFoundException => HttpStatusCode.NotFound,
|
||||||
|
|
||||||
// 408 Request Timeout
|
// 408 Request Timeout
|
||||||
TimeoutException => HttpStatusCode.RequestTimeout,
|
TimeoutException => HttpStatusCode.RequestTimeout,
|
||||||
|
|
||||||
// 409 Conflict
|
// 409 Conflict
|
||||||
ConflictException => HttpStatusCode.Conflict,
|
ConflictException => HttpStatusCode.Conflict,
|
||||||
|
|
||||||
// 429 Too Many Requests
|
// 429 Too Many Requests
|
||||||
RateLimitExceededException => HttpStatusCode.TooManyRequests,
|
RateLimitExceededException => HttpStatusCode.TooManyRequests,
|
||||||
|
|
||||||
// 501 Not Implemented
|
// 501 Not Implemented
|
||||||
NotImplementedException => HttpStatusCode.NotImplemented,
|
NotImplementedException => HttpStatusCode.NotImplemented,
|
||||||
|
|
||||||
// 503 Service Unavailable
|
// 503 Service Unavailable
|
||||||
ServiceUnavailableException => HttpStatusCode.ServiceUnavailable,
|
ServiceUnavailableException => HttpStatusCode.ServiceUnavailable,
|
||||||
|
|
||||||
// 500 Internal Server Error (default)
|
// 500 Internal Server Error (default)
|
||||||
_ => HttpStatusCode.InternalServerError
|
_ => HttpStatusCode.InternalServerError
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log the error with appropriate severity based on status code
|
// Log the error with appropriate severity based on status code
|
||||||
var isServerError = (int)status >= 500;
|
var isServerError = (int)status >= 500;
|
||||||
|
|
||||||
if (isServerError)
|
if (isServerError)
|
||||||
{
|
{
|
||||||
_logger.LogError(exception, "Server Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
|
_logger.LogError(exception, "Server Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
|
||||||
@@ -86,29 +85,29 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
{
|
{
|
||||||
_logger.LogWarning(exception, "Client Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
|
_logger.LogWarning(exception, "Client Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture exception in Sentry with request context
|
// Capture exception in Sentry with request context
|
||||||
var sentryId = SentrySdk.CaptureException(exception, scope =>
|
var sentryId = SentrySdk.CaptureException(exception, scope =>
|
||||||
{
|
{
|
||||||
// Add HTTP request information
|
// Add HTTP request information
|
||||||
scope.SetTag("http.method", context.Request.Method);
|
scope.SetTag("http.method", context.Request.Method);
|
||||||
scope.SetTag("http.url", context.Request.Path);
|
scope.SetTag("http.url", context.Request.Path);
|
||||||
|
|
||||||
// Add request details
|
// Add request details
|
||||||
scope.SetExtra("query_string", context.Request.QueryString.ToString());
|
scope.SetExtra("query_string", context.Request.QueryString.ToString());
|
||||||
|
|
||||||
// Add custom tags to help with filtering
|
// Add custom tags to help with filtering
|
||||||
scope.SetTag("error_type", exception.GetType().Name);
|
scope.SetTag("error_type", exception.GetType().Name);
|
||||||
scope.SetTag("status_code", ((int)status).ToString());
|
scope.SetTag("status_code", ((int)status).ToString());
|
||||||
scope.SetTag("host", context.Request.Host.ToString());
|
scope.SetTag("host", context.Request.Host.ToString());
|
||||||
scope.SetTag("path", context.Request.Path.ToString());
|
scope.SetTag("path", context.Request.Path.ToString());
|
||||||
|
|
||||||
// Add any correlation IDs if available
|
// Add any correlation IDs if available
|
||||||
if (context.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
|
if (context.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
|
||||||
{
|
{
|
||||||
scope.SetTag("correlation_id", correlationId.ToString());
|
scope.SetTag("correlation_id", correlationId.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional context based on exception type
|
// Additional context based on exception type
|
||||||
if (exception is ValidationException)
|
if (exception is ValidationException)
|
||||||
{
|
{
|
||||||
@@ -118,7 +117,7 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
{
|
{
|
||||||
scope.SetTag("error_category", "not_found");
|
scope.SetTag("error_category", "not_found");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add additional context from exception data if available
|
// Add additional context from exception data if available
|
||||||
foreach (var key in exception.Data.Keys)
|
foreach (var key in exception.Data.Keys)
|
||||||
{
|
{
|
||||||
@@ -127,7 +126,7 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
scope.SetExtra(keyStr, exception.Data[key].ToString());
|
scope.SetExtra(keyStr, exception.Data[key].ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add breadcrumb for the request
|
// Add breadcrumb for the request
|
||||||
scope.AddBreadcrumb(
|
scope.AddBreadcrumb(
|
||||||
message: $"Request to {context.Request.Path}",
|
message: $"Request to {context.Request.Path}",
|
||||||
@@ -135,7 +134,7 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
level: BreadcrumbLevel.Info
|
level: BreadcrumbLevel.Info
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use a more user-friendly error message in production
|
// Use a more user-friendly error message in production
|
||||||
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
|
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
|
||||||
{
|
{
|
||||||
@@ -154,7 +153,7 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
{
|
{
|
||||||
errorMessage = exception.Message;
|
errorMessage = exception.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the error response
|
// Create the error response
|
||||||
var errorResponse = new ErrorResponse
|
var errorResponse = new ErrorResponse
|
||||||
{
|
{
|
||||||
@@ -162,23 +161,23 @@ public class GlobalErrorHandlingMiddleware
|
|||||||
Message = errorMessage,
|
Message = errorMessage,
|
||||||
TraceId = sentryId.ToString()
|
TraceId = sentryId.ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include stack trace in development environment
|
// Only include stack trace in development environment
|
||||||
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
|
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
|
||||||
{
|
{
|
||||||
errorResponse.StackTrace = exception.StackTrace;
|
errorResponse.StackTrace = exception.StackTrace;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
|
var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
});
|
});
|
||||||
|
|
||||||
context.Response.ContentType = "application/json";
|
context.Response.ContentType = "application/json";
|
||||||
context.Response.StatusCode = (int)status;
|
context.Response.StatusCode = (int)status;
|
||||||
return context.Response.WriteAsync(result);
|
return context.Response.WriteAsync(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom error response class
|
// Custom error response class
|
||||||
private class ErrorResponse
|
private class ErrorResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -97,4 +97,4 @@ public class SimpleTradingBoxTests
|
|||||||
// Assert
|
// Assert
|
||||||
result.Should().Be(fee);
|
result.Should().Be(fee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
@@ -106,4 +97,4 @@ public class SimpleTradingBoxTests
|
|||||||
// Assert
|
// Assert
|
||||||
result.Should().Be(fee);
|
result.Should().Be(fee);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
@@ -53,4 +51,4 @@ public class GmxTrade
|
|||||||
[JsonProperty("exchangeOrderId")]
|
[JsonProperty("exchangeOrderId")]
|
||||||
[JsonPropertyName("exchangeOrderId")]
|
[JsonPropertyName("exchangeOrderId")]
|
||||||
public string ExchangeOrderId { get; set; }
|
public string ExchangeOrderId { get; set; }
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user