Open position for bots

This commit is contained in:
2025-04-23 10:02:04 +02:00
parent 8d191e8d14
commit ad5996ca61
10 changed files with 410 additions and 208 deletions

View File

@@ -16,6 +16,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Managing.Domain.Trades;
using Microsoft.Extensions.DependencyInjection;
using static Managing.Common.Enums;
using ApplicationTradingBot = Managing.Application.Bots.TradingBot;
@@ -419,4 +420,48 @@ public class BotController : BaseController
var botsList = await GetBotList();
await _hubContext.Clients.All.SendAsync("BotsSubscription", botsList);
}
/// <summary>
/// Manually opens a position for a specified bot with the given parameters.
/// </summary>
/// <param name="request">The request containing position parameters.</param>
/// <returns>A response indicating the result of the operation.</returns>
[HttpPost]
[Route("OpenPosition")]
public async Task<ActionResult<Position>> OpenPositionManually([FromBody] OpenPositionManuallyRequest request)
{
try
{
// Check if user owns the account
if (!await UserOwnsBotAccount(request.BotName))
{
return Forbid("You don't have permission to open positions for this bot");
}
var activeBots = _botService.GetActiveBots();
var bot = activeBots.FirstOrDefault(b => b.Name == request.BotName) as ApplicationTradingBot;
if (bot == null)
{
return NotFound($"Bot {request.BotName} not found or is not a trading bot");
}
if (bot.GetStatus() != BotStatus.Up.ToString())
{
return BadRequest($"Bot {request.BotName} is not running");
}
var position = await bot.OpenPositionManually(
request.Direction
);
await NotifyBotSubscriberAsync();
return Ok(position);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error opening position manually");
return StatusCode(500, $"Error opening position: {ex.Message}");
}
}
}

View File

@@ -1,187 +0,0 @@
using System.Net;
using System.Text.Json;
using Sentry;
namespace Managing.Api.Exceptions;
public class GlobalErrorHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalErrorHandlingMiddleware> _logger;
public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger<GlobalErrorHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
HttpStatusCode status;
string errorMessage;
// Determine the appropriate status code based on exception type
status = exception switch
{
// 400 Bad Request
ArgumentException => HttpStatusCode.BadRequest,
ValidationException => HttpStatusCode.BadRequest,
FormatException => HttpStatusCode.BadRequest,
InvalidOperationException => HttpStatusCode.BadRequest,
// 401 Unauthorized
UnauthorizedAccessException => HttpStatusCode.Unauthorized,
// 403 Forbidden
ForbiddenException => HttpStatusCode.Forbidden,
// 404 Not Found
KeyNotFoundException => HttpStatusCode.NotFound,
FileNotFoundException => HttpStatusCode.NotFound,
DirectoryNotFoundException => HttpStatusCode.NotFound,
NotFoundException => HttpStatusCode.NotFound,
// 408 Request Timeout
TimeoutException => HttpStatusCode.RequestTimeout,
// 409 Conflict
ConflictException => HttpStatusCode.Conflict,
// 429 Too Many Requests
RateLimitExceededException => HttpStatusCode.TooManyRequests,
// 501 Not Implemented
NotImplementedException => HttpStatusCode.NotImplemented,
// 503 Service Unavailable
ServiceUnavailableException => HttpStatusCode.ServiceUnavailable,
// 500 Internal Server Error (default)
_ => HttpStatusCode.InternalServerError
};
// Log the error with appropriate severity based on status code
var isServerError = (int)status >= 500;
if (isServerError)
{
_logger.LogError(exception, "Server Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
}
else
{
_logger.LogWarning(exception, "Client Error: {StatusCode} on {Path}", (int)status, context.Request.Path);
}
// Capture exception in Sentry with request context
var sentryId = SentrySdk.CaptureException(exception, scope =>
{
// Add HTTP request information
scope.SetTag("http.method", context.Request.Method);
scope.SetTag("http.url", context.Request.Path);
// Add request details
scope.SetExtra("query_string", context.Request.QueryString.ToString());
// Add custom tags to help with filtering
scope.SetTag("error_type", exception.GetType().Name);
scope.SetTag("status_code", ((int)status).ToString());
scope.SetTag("host", context.Request.Host.ToString());
scope.SetTag("path", context.Request.Path.ToString());
// Add any correlation IDs if available
if (context.Request.Headers.TryGetValue("X-Correlation-ID", out var correlationId))
{
scope.SetTag("correlation_id", correlationId.ToString());
}
// Additional context based on exception type
if (exception is ValidationException)
{
scope.SetTag("error_category", "validation");
}
else if (exception is NotFoundException)
{
scope.SetTag("error_category", "not_found");
}
// Add additional context from exception data if available
foreach (var key in exception.Data.Keys)
{
if (key is string keyStr && exception.Data[key] != null)
{
scope.SetExtra(keyStr, exception.Data[key].ToString());
}
}
// Add breadcrumb for the request
scope.AddBreadcrumb(
message: $"Request to {context.Request.Path}",
category: "request",
level: BreadcrumbLevel.Info
);
});
// Use a more user-friendly error message in production
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
{
// For 5xx errors, use a generic message
if (isServerError)
{
errorMessage = "An unexpected error occurred. Our team has been notified.";
}
else
{
// For 4xx errors, keep the original message since it's likely helpful for the user
errorMessage = exception.Message;
}
}
else
{
errorMessage = exception.Message;
}
// Create the error response
var errorResponse = new ErrorResponse
{
StatusCode = (int)status,
Message = errorMessage,
TraceId = sentryId.ToString()
};
// Only include stack trace in development environment
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Production")
{
errorResponse.StackTrace = exception.StackTrace;
}
var result = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)status;
return context.Response.WriteAsync(result);
}
// Custom error response class
private class ErrorResponse
{
public int StatusCode { get; set; }
public string Message { get; set; }
public string TraceId { get; set; }
public string StackTrace { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
using Managing.Common;
using static Managing.Common.Enums;
namespace Managing.Api.Models.Requests;
public class OpenPositionManuallyRequest
{
public string BotName { get; set; }
public TradeDirection Direction { get; set; }
}