Open position for bots
This commit is contained in:
@@ -16,6 +16,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Managing.Domain.Trades;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using static Managing.Common.Enums;
|
using static Managing.Common.Enums;
|
||||||
using ApplicationTradingBot = Managing.Application.Bots.TradingBot;
|
using ApplicationTradingBot = Managing.Application.Bots.TradingBot;
|
||||||
@@ -419,4 +420,48 @@ public class BotController : BaseController
|
|||||||
var botsList = await GetBotList();
|
var botsList = await GetBotList();
|
||||||
await _hubContext.Clients.All.SendAsync("BotsSubscription", botsList);
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -744,6 +744,7 @@ public class TradingBot : Bot, ITradingBot
|
|||||||
private async Task LogWarning(string message)
|
private async Task LogWarning(string message)
|
||||||
{
|
{
|
||||||
Logger.LogWarning(message);
|
Logger.LogWarning(message);
|
||||||
|
SentrySdk.CaptureException(new Exception(message));
|
||||||
await SendTradeMessage(message, true);
|
await SendTradeMessage(message, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,6 +788,50 @@ public class TradingBot : Bot, ITradingBot
|
|||||||
AccountName = data.AccountName;
|
AccountName = data.AccountName;
|
||||||
IsForWatchingOnly = data.IsForWatchingOnly;
|
IsForWatchingOnly = data.IsForWatchingOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manually opens a position using the bot's settings and a generated signal.
|
||||||
|
/// Relies on the bot's MoneyManagement for Stop Loss and Take Profit placement.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="direction">The direction of the trade (Long/Short).</param>
|
||||||
|
/// <returns>The created Position object.</returns>
|
||||||
|
/// <exception cref="Exception">Throws if no candles are available or position opening fails.</exception>
|
||||||
|
public async Task<Position> OpenPositionManually(TradeDirection direction)
|
||||||
|
{
|
||||||
|
var lastCandle = OptimizedCandles.LastOrDefault();
|
||||||
|
if (lastCandle == null)
|
||||||
|
{
|
||||||
|
throw new Exception("No candles available to open position");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a fake signal for manual position opening
|
||||||
|
var signal = new Signal(Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date, TradingExchanges.GmxV2,
|
||||||
|
StrategyType.Stc, SignalType.Signal);
|
||||||
|
signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct
|
||||||
|
signal.User = Account.User; // Assign user
|
||||||
|
|
||||||
|
// Add the signal to our collection
|
||||||
|
await AddSignal(signal);
|
||||||
|
|
||||||
|
// Open the position using the generated signal (SL/TP handled by MoneyManagement)
|
||||||
|
await OpenPosition(signal);
|
||||||
|
|
||||||
|
// Get the opened position
|
||||||
|
var position = Positions.FirstOrDefault(p => p.SignalIdentifier == signal.Identifier);
|
||||||
|
if (position == null)
|
||||||
|
{
|
||||||
|
// Clean up the signal if position creation failed
|
||||||
|
SetSignalStatus(signal.Identifier, SignalStatus.Expired);
|
||||||
|
throw new Exception("Failed to open position");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removed manual setting of SL/TP, as MoneyManagement should handle it
|
||||||
|
// position.StopLoss.Price = stopLossPrice;
|
||||||
|
// position.TakeProfit1.Price = takeProfitPrice;
|
||||||
|
|
||||||
|
Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}");
|
||||||
|
return position;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TradingBotBackup
|
public class TradingBotBackup
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace Managing.Application.Trading
|
|||||||
: exchangeService.GetBalance(account, request.IsForPaperTrading).Result;
|
: exchangeService.GetBalance(account, request.IsForPaperTrading).Result;
|
||||||
var balanceAtRisk = RiskHelpers.GetBalanceAtRisk(balance, request.MoneyManagement);
|
var balanceAtRisk = RiskHelpers.GetBalanceAtRisk(balance, request.MoneyManagement);
|
||||||
|
|
||||||
if (balanceAtRisk < 13)
|
if (balanceAtRisk < 7)
|
||||||
{
|
{
|
||||||
throw new Exception($"Try to risk {balanceAtRisk} $ but inferior to minimum to trade");
|
throw new Exception($"Try to risk {balanceAtRisk} $ but inferior to minimum to trade");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { getSwapAmountsByFromValue, getSwapAmountsByToValue } from "../../utils/
|
|||||||
import { EntryField, SidecarSlTpOrderEntryValid } from "../../types/sidecarOrders.js";
|
import { EntryField, SidecarSlTpOrderEntryValid } from "../../types/sidecarOrders.js";
|
||||||
import { bigMath } from "../../utils/bigmath.js";
|
import { bigMath } from "../../utils/bigmath.js";
|
||||||
|
|
||||||
|
const ALLOWED_SLIPPAGE_BPS = BigInt(100);
|
||||||
/** Base Optional params for helpers, allows to avoid calling markets, tokens and uiFeeFactor methods if they are already passed */
|
/** Base Optional params for helpers, allows to avoid calling markets, tokens and uiFeeFactor methods if they are already passed */
|
||||||
interface BaseOptionalParams {
|
interface BaseOptionalParams {
|
||||||
marketsInfoData?: MarketsInfoData;
|
marketsInfoData?: MarketsInfoData;
|
||||||
@@ -174,7 +175,7 @@ export async function increaseOrderHelper(
|
|||||||
indexPrice: 0n,
|
indexPrice: 0n,
|
||||||
collateralPrice: 0n,
|
collateralPrice: 0n,
|
||||||
acceptablePrice: params.stopLossPrice,
|
acceptablePrice: params.stopLossPrice,
|
||||||
acceptablePriceDeltaBps: 0n,
|
acceptablePriceDeltaBps: ALLOWED_SLIPPAGE_BPS,
|
||||||
recommendedAcceptablePriceDeltaBps: 0n,
|
recommendedAcceptablePriceDeltaBps: 0n,
|
||||||
estimatedPnl: 0n,
|
estimatedPnl: 0n,
|
||||||
estimatedPnlPercentage: 0n,
|
estimatedPnlPercentage: 0n,
|
||||||
@@ -194,8 +195,8 @@ export async function increaseOrderHelper(
|
|||||||
payedRemainingCollateralUsd: 0n,
|
payedRemainingCollateralUsd: 0n,
|
||||||
receiveTokenAmount: 0n,
|
receiveTokenAmount: 0n,
|
||||||
receiveUsd: 0n,
|
receiveUsd: 0n,
|
||||||
decreaseSwapType: DecreasePositionSwapType.NoSwap,
|
decreaseSwapType: DecreasePositionSwapType.SwapPnlTokenToCollateralToken,
|
||||||
triggerOrderType: OrderType.LimitDecrease,
|
triggerOrderType: OrderType.StopLossDecrease,
|
||||||
triggerPrice: params.stopLossPrice,
|
triggerPrice: params.stopLossPrice,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +240,7 @@ export async function increaseOrderHelper(
|
|||||||
indexPrice: 0n,
|
indexPrice: 0n,
|
||||||
collateralPrice: 0n,
|
collateralPrice: 0n,
|
||||||
acceptablePrice: params.takeProfitPrice,
|
acceptablePrice: params.takeProfitPrice,
|
||||||
acceptablePriceDeltaBps: 0n,
|
acceptablePriceDeltaBps: ALLOWED_SLIPPAGE_BPS,
|
||||||
recommendedAcceptablePriceDeltaBps: 0n,
|
recommendedAcceptablePriceDeltaBps: 0n,
|
||||||
estimatedPnl: 0n,
|
estimatedPnl: 0n,
|
||||||
estimatedPnlPercentage: 0n,
|
estimatedPnlPercentage: 0n,
|
||||||
@@ -259,7 +260,7 @@ export async function increaseOrderHelper(
|
|||||||
payedRemainingCollateralUsd: 0n,
|
payedRemainingCollateralUsd: 0n,
|
||||||
receiveTokenAmount: 0n,
|
receiveTokenAmount: 0n,
|
||||||
receiveUsd: 0n,
|
receiveUsd: 0n,
|
||||||
decreaseSwapType: DecreasePositionSwapType.NoSwap,
|
decreaseSwapType: DecreasePositionSwapType.SwapPnlTokenToCollateralToken,
|
||||||
triggerOrderType: OrderType.LimitDecrease,
|
triggerOrderType: OrderType.LimitDecrease,
|
||||||
triggerPrice: params.takeProfitPrice,
|
triggerPrice: params.takeProfitPrice,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,8 +126,8 @@ export const openGmxPositionImpl = async (
|
|||||||
quantity: number,
|
quantity: number,
|
||||||
leverage: number,
|
leverage: number,
|
||||||
price?: number,
|
price?: number,
|
||||||
takeProfitPrice?: number,
|
stopLossPrice?: number,
|
||||||
stopLossPrice?: number
|
takeProfitPrice?: number
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
// Get markets and tokens data from GMX SDK
|
// Get markets and tokens data from GMX SDK
|
||||||
@@ -138,6 +138,8 @@ export const openGmxPositionImpl = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('price', price)
|
console.log('price', price)
|
||||||
|
console.log('stopLossPrice', stopLossPrice)
|
||||||
|
console.log('takeProfitPrice', takeProfitPrice)
|
||||||
|
|
||||||
const marketInfo = getMarketInfoFromTicker(ticker, marketsInfoData);
|
const marketInfo = getMarketInfoFromTicker(ticker, marketsInfoData);
|
||||||
const collateralToken = getTokenDataFromTicker("USDC", tokensData); // Using USDC as collateral
|
const collateralToken = getTokenDataFromTicker("USDC", tokensData); // Using USDC as collateral
|
||||||
@@ -167,7 +169,6 @@ export const openGmxPositionImpl = async (
|
|||||||
|
|
||||||
console.log('params', params)
|
console.log('params', params)
|
||||||
|
|
||||||
// Execute the main position order
|
|
||||||
if (direction === TradeDirection.Long) {
|
if (direction === TradeDirection.Long) {
|
||||||
await sdk.orders.long(params);
|
await sdk.orders.long(params);
|
||||||
} else {
|
} else {
|
||||||
@@ -208,8 +209,8 @@ export async function openGmxPosition(
|
|||||||
price?: number,
|
price?: number,
|
||||||
quantity?: number,
|
quantity?: number,
|
||||||
leverage?: number,
|
leverage?: number,
|
||||||
takeProfitPrice?: number,
|
stopLossPrice?: number,
|
||||||
stopLossPrice?: number
|
takeProfitPrice?: number
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
// Validate the request parameters
|
// Validate the request parameters
|
||||||
@@ -221,8 +222,8 @@ export async function openGmxPosition(
|
|||||||
price,
|
price,
|
||||||
quantity,
|
quantity,
|
||||||
leverage,
|
leverage,
|
||||||
takeProfitPrice,
|
stopLossPrice,
|
||||||
stopLossPrice
|
takeProfitPrice
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get client for the address
|
// Get client for the address
|
||||||
@@ -236,8 +237,8 @@ export async function openGmxPosition(
|
|||||||
quantity,
|
quantity,
|
||||||
leverage,
|
leverage,
|
||||||
price,
|
price,
|
||||||
takeProfitPrice,
|
stopLossPrice,
|
||||||
stopLossPrice
|
takeProfitPrice
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import { BotClient, TradeDirection } from '../../generated/ManagingApi'
|
||||||
|
import useApiUrlStore from '../../app/store/apiStore'
|
||||||
|
import Toast from './Toast/Toast'
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
direction: z.nativeEnum(TradeDirection),
|
||||||
|
})
|
||||||
|
|
||||||
|
type ManualPositionFormValues = z.infer<typeof schema>
|
||||||
|
|
||||||
|
interface ManualPositionModalProps {
|
||||||
|
showModal: boolean
|
||||||
|
botName: string | null
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function ManualPositionModal({ showModal, botName, onClose }: ManualPositionModalProps) {
|
||||||
|
const { apiUrl } = useApiUrlStore()
|
||||||
|
const client = new BotClient({}, apiUrl)
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<ManualPositionFormValues>()
|
||||||
|
|
||||||
|
const onSubmit = async (data: ManualPositionFormValues) => {
|
||||||
|
if (!botName) return
|
||||||
|
|
||||||
|
const t = new Toast('Opening position...')
|
||||||
|
try {
|
||||||
|
await client.bot_OpenPositionManually({
|
||||||
|
botName: botName,
|
||||||
|
direction: data.direction,
|
||||||
|
})
|
||||||
|
t.update('success', 'Position opened successfully')
|
||||||
|
reset()
|
||||||
|
onClose()
|
||||||
|
} catch (error: any) {
|
||||||
|
t.update('error', `Failed to open position: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showModal || !botName) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dialog open={showModal} className="modal modal-bottom sm:modal-middle modal-open">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">Open Position Manually for {botName}</h3>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="form-control w-full max-w-xs py-2">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text">Direction</span>
|
||||||
|
</label>
|
||||||
|
<select {...register('direction')} className="select select-bordered w-full max-w-xs">
|
||||||
|
<option value={TradeDirection.Long}>Long</option>
|
||||||
|
<option value={TradeDirection.Short}>Short</option>
|
||||||
|
</select>
|
||||||
|
{errors.direction && <span className="text-error text-xs">{errors.direction.message}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-action">
|
||||||
|
<button type="submit" className="btn btn-primary">Open Position</button>
|
||||||
|
<button type="button" className="btn" onClick={() => { reset(); onClose(); }}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/* Click outside closes modal */}
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button type="button" onClick={() => { reset(); onClose(); }}>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ManualPositionModal
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//----------------------
|
//----------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// Generated using the NSwag toolchain v14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
|
// Generated using the NSwag toolchain v14.3.0.0 (NJsonSchema v11.2.0.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org)
|
||||||
// </auto-generated>
|
// </auto-generated>
|
||||||
//----------------------
|
//----------------------
|
||||||
|
|
||||||
@@ -765,6 +765,45 @@ export class BotClient extends AuthorizedApiBase {
|
|||||||
}
|
}
|
||||||
return Promise.resolve<TradingBot[]>(null as any);
|
return Promise.resolve<TradingBot[]>(null as any);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise<Position> {
|
||||||
|
let url_ = this.baseUrl + "/Bot/OpenPosition";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
const content_ = JSON.stringify(request);
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
body: content_,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processBot_OpenPositionManually(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processBot_OpenPositionManually(response: Response): Promise<Position> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
let result200: any = null;
|
||||||
|
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as Position;
|
||||||
|
return result200;
|
||||||
|
});
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<Position>(null as any);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataClient extends AuthorizedApiBase {
|
export class DataClient extends AuthorizedApiBase {
|
||||||
@@ -1445,6 +1484,138 @@ export class ScenarioClient extends AuthorizedApiBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SentryTestClient extends AuthorizedApiBase {
|
||||||
|
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||||
|
private baseUrl: string;
|
||||||
|
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||||
|
|
||||||
|
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||||
|
super(configuration);
|
||||||
|
this.http = http ? http : window as any;
|
||||||
|
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
||||||
|
}
|
||||||
|
|
||||||
|
sentryTest_TestException(): Promise<FileResponse> {
|
||||||
|
let url_ = this.baseUrl + "/api/SentryTest/test-exception";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/octet-stream"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processSentryTest_TestException(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processSentryTest_TestException(response: Response): Promise<FileResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200 || status === 206) {
|
||||||
|
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||||
|
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||||
|
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||||
|
if (fileName) {
|
||||||
|
fileName = decodeURIComponent(fileName);
|
||||||
|
} else {
|
||||||
|
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||||
|
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||||
|
}
|
||||||
|
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<FileResponse>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
sentryTest_ThrowException(): Promise<FileResponse> {
|
||||||
|
let url_ = this.baseUrl + "/api/SentryTest/throw-exception";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/octet-stream"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processSentryTest_ThrowException(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processSentryTest_ThrowException(response: Response): Promise<FileResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200 || status === 206) {
|
||||||
|
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||||
|
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||||
|
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||||
|
if (fileName) {
|
||||||
|
fileName = decodeURIComponent(fileName);
|
||||||
|
} else {
|
||||||
|
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||||
|
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||||
|
}
|
||||||
|
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<FileResponse>(null as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
sentryTest_TestMessage(): Promise<FileResponse> {
|
||||||
|
let url_ = this.baseUrl + "/api/SentryTest/test-message";
|
||||||
|
url_ = url_.replace(/[?&]$/, "");
|
||||||
|
|
||||||
|
let options_: RequestInit = {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/octet-stream"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||||
|
return this.http.fetch(url_, transformedOptions_);
|
||||||
|
}).then((_response: Response) => {
|
||||||
|
return this.processSentryTest_TestMessage(_response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processSentryTest_TestMessage(response: Response): Promise<FileResponse> {
|
||||||
|
const status = response.status;
|
||||||
|
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||||
|
if (status === 200 || status === 206) {
|
||||||
|
const contentDisposition = response.headers ? response.headers.get("content-disposition") : undefined;
|
||||||
|
let fileNameMatch = contentDisposition ? /filename\*=(?:(\\?['"])(.*?)\1|(?:[^\s]+'.*?')?([^;\n]*))/g.exec(contentDisposition) : undefined;
|
||||||
|
let fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[3] || fileNameMatch[2] : undefined;
|
||||||
|
if (fileName) {
|
||||||
|
fileName = decodeURIComponent(fileName);
|
||||||
|
} else {
|
||||||
|
fileNameMatch = contentDisposition ? /filename="?([^"]*?)"?(;|$)/g.exec(contentDisposition) : undefined;
|
||||||
|
fileName = fileNameMatch && fileNameMatch.length > 1 ? fileNameMatch[1] : undefined;
|
||||||
|
}
|
||||||
|
return response.blob().then(blob => { return { fileName: fileName, data: blob, status: status, headers: _headers }; });
|
||||||
|
} else if (status !== 200 && status !== 204) {
|
||||||
|
return response.text().then((_responseText) => {
|
||||||
|
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve<FileResponse>(null as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SettingsClient extends AuthorizedApiBase {
|
export class SettingsClient extends AuthorizedApiBase {
|
||||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
@@ -2022,6 +2193,7 @@ export interface Account {
|
|||||||
secret?: string | null;
|
secret?: string | null;
|
||||||
user?: User | null;
|
user?: User | null;
|
||||||
balances?: Balance[] | null;
|
balances?: Balance[] | null;
|
||||||
|
isPrivyWallet?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TradingExchanges {
|
export enum TradingExchanges {
|
||||||
@@ -2192,6 +2364,7 @@ export enum Timeframe {
|
|||||||
OneHour = "OneHour",
|
OneHour = "OneHour",
|
||||||
FourHour = "FourHour",
|
FourHour = "FourHour",
|
||||||
OneDay = "OneDay",
|
OneDay = "OneDay",
|
||||||
|
OneMinute = "OneMinute",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Trade {
|
export interface Trade {
|
||||||
@@ -2466,6 +2639,13 @@ export interface TradingBot {
|
|||||||
moneyManagement: MoneyManagement;
|
moneyManagement: MoneyManagement;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OpenPositionManuallyRequest {
|
||||||
|
botName?: string | null;
|
||||||
|
direction?: TradeDirection;
|
||||||
|
stopLossPrice?: number;
|
||||||
|
takeProfitPrice?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SpotlightOverview {
|
export interface SpotlightOverview {
|
||||||
spotlights: Spotlight[];
|
spotlights: Spotlight[];
|
||||||
dateTime: Date;
|
dateTime: Date;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EyeIcon, PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/solid'
|
import { EyeIcon, PlayIcon, StopIcon, TrashIcon, PlusCircleIcon } from '@heroicons/react/solid'
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
import useApiUrlStore from '../../app/store/apiStore'
|
import useApiUrlStore from '../../app/store/apiStore'
|
||||||
import {
|
import {
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CardText,
|
CardText,
|
||||||
Toast,
|
Toast,
|
||||||
} from '../../components/mollecules'
|
} from '../../components/mollecules'
|
||||||
|
import ManualPositionModal from '../../components/mollecules/ManualPositionModal'
|
||||||
import { TradeChart } from '../../components/organism'
|
import { TradeChart } from '../../components/organism'
|
||||||
import { BotClient } from '../../generated/ManagingApi'
|
import { BotClient } from '../../generated/ManagingApi'
|
||||||
import type {
|
import type {
|
||||||
@@ -38,9 +39,11 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
const { apiUrl } = useApiUrlStore()
|
const { apiUrl } = useApiUrlStore()
|
||||||
const client = new BotClient({}, apiUrl)
|
const client = new BotClient({}, apiUrl)
|
||||||
const [showMoneyManagementModal, setShowMoneyManagementModal] =
|
const [showMoneyManagementModal, setShowMoneyManagementModal] =
|
||||||
React.useState(false)
|
useState(false)
|
||||||
const [selectedMoneyManagement, setSelectedMoneyManagement] =
|
const [selectedMoneyManagement, setSelectedMoneyManagement] =
|
||||||
React.useState<MoneyManagement>()
|
useState<MoneyManagement>()
|
||||||
|
const [showManualPositionModal, setShowManualPositionModal] = useState(false)
|
||||||
|
const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState<string | null>(null)
|
||||||
|
|
||||||
function getIsForWatchingBadge(isForWatchingOnly: boolean, name: string) {
|
function getIsForWatchingBadge(isForWatchingOnly: boolean, name: string) {
|
||||||
const classes =
|
const classes =
|
||||||
@@ -121,6 +124,22 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getManualPositionBadge(botName: string) {
|
||||||
|
const classes = baseBadgeClass() + ' bg-info'
|
||||||
|
return (
|
||||||
|
<button className={classes} onClick={() => openManualPositionModal(botName)}>
|
||||||
|
<p className="text-primary-content flex">
|
||||||
|
<PlusCircleIcon width={15}></PlusCircleIcon>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openManualPositionModal(botName: string) {
|
||||||
|
setSelectedBotForManualPosition(botName)
|
||||||
|
setShowManualPositionModal(true)
|
||||||
|
}
|
||||||
|
|
||||||
function toggleBotStatus(status: string, name: string, botType: BotType) {
|
function toggleBotStatus(status: string, name: string, botType: BotType) {
|
||||||
const isUp = status == 'Up'
|
const isUp = status == 'Up'
|
||||||
const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot')
|
const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot')
|
||||||
@@ -184,6 +203,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
{getMoneyManagementBadge(bot.moneyManagement)}
|
{getMoneyManagementBadge(bot.moneyManagement)}
|
||||||
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.name)}
|
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.name)}
|
||||||
{getToggleBotStatusBadge(bot.status, bot.name, bot.botType)}
|
{getToggleBotStatusBadge(bot.status, bot.name, bot.botType)}
|
||||||
|
{getManualPositionBadge(bot.name)}
|
||||||
{getDeleteBadge(bot.name)}
|
{getDeleteBadge(bot.name)}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
@@ -239,6 +259,14 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
|||||||
onClose={() => setShowMoneyManagementModal(false)}
|
onClose={() => setShowMoneyManagementModal(false)}
|
||||||
disableInputs={true}
|
disableInputs={true}
|
||||||
/>
|
/>
|
||||||
|
<ManualPositionModal
|
||||||
|
showModal={showManualPositionModal}
|
||||||
|
botName={selectedBotForManualPosition}
|
||||||
|
onClose={() => {
|
||||||
|
setShowManualPositionModal(false)
|
||||||
|
setSelectedBotForManualPosition(null)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user