diff --git a/src/Managing.Api/Controllers/BotController.cs b/src/Managing.Api/Controllers/BotController.cs index 66bc40b..3270cc7 100644 --- a/src/Managing.Api/Controllers/BotController.cs +++ b/src/Managing.Api/Controllers/BotController.cs @@ -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); } + + /// + /// Manually opens a position for a specified bot with the given parameters. + /// + /// The request containing position parameters. + /// A response indicating the result of the operation. + [HttpPost] + [Route("OpenPosition")] + public async Task> 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}"); + } + } } \ No newline at end of file diff --git a/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs b/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs deleted file mode 100644 index fd832d1..0000000 --- a/src/Managing.Api/Exceptions/GlobalErrorHandlingMiddleware.cs +++ /dev/null @@ -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 _logger; - - public GlobalErrorHandlingMiddleware(RequestDelegate next, ILogger 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; } - } -} diff --git a/src/Managing.Api/Models/Requests/OpenPositionManuallyRequest.cs b/src/Managing.Api/Models/Requests/OpenPositionManuallyRequest.cs new file mode 100644 index 0000000..3b3dae2 --- /dev/null +++ b/src/Managing.Api/Models/Requests/OpenPositionManuallyRequest.cs @@ -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; } +} \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBot.cs b/src/Managing.Application/Bots/TradingBot.cs index 7d9983b..57fb734 100644 --- a/src/Managing.Application/Bots/TradingBot.cs +++ b/src/Managing.Application/Bots/TradingBot.cs @@ -744,6 +744,7 @@ public class TradingBot : Bot, ITradingBot private async Task LogWarning(string message) { Logger.LogWarning(message); + SentrySdk.CaptureException(new Exception(message)); await SendTradeMessage(message, true); } @@ -787,6 +788,50 @@ public class TradingBot : Bot, ITradingBot AccountName = data.AccountName; IsForWatchingOnly = data.IsForWatchingOnly; } + + /// + /// 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. + /// + /// The direction of the trade (Long/Short). + /// The created Position object. + /// Throws if no candles are available or position opening fails. + public async Task 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 diff --git a/src/Managing.Application/Trading/OpenPositionCommandHandler.cs b/src/Managing.Application/Trading/OpenPositionCommandHandler.cs index df3f625..bffcd51 100644 --- a/src/Managing.Application/Trading/OpenPositionCommandHandler.cs +++ b/src/Managing.Application/Trading/OpenPositionCommandHandler.cs @@ -33,7 +33,7 @@ namespace Managing.Application.Trading : exchangeService.GetBalance(account, request.IsForPaperTrading).Result; 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"); } diff --git a/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/helpers.ts b/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/helpers.ts index 1ac163c..2953702 100644 --- a/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/helpers.ts +++ b/src/Managing.Web3Proxy/src/generated/gmxsdk/modules/orders/helpers.ts @@ -13,6 +13,7 @@ import { getSwapAmountsByFromValue, getSwapAmountsByToValue } from "../../utils/ import { EntryField, SidecarSlTpOrderEntryValid } from "../../types/sidecarOrders.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 */ interface BaseOptionalParams { marketsInfoData?: MarketsInfoData; @@ -174,7 +175,7 @@ export async function increaseOrderHelper( indexPrice: 0n, collateralPrice: 0n, acceptablePrice: params.stopLossPrice, - acceptablePriceDeltaBps: 0n, + acceptablePriceDeltaBps: ALLOWED_SLIPPAGE_BPS, recommendedAcceptablePriceDeltaBps: 0n, estimatedPnl: 0n, estimatedPnlPercentage: 0n, @@ -194,8 +195,8 @@ export async function increaseOrderHelper( payedRemainingCollateralUsd: 0n, receiveTokenAmount: 0n, receiveUsd: 0n, - decreaseSwapType: DecreasePositionSwapType.NoSwap, - triggerOrderType: OrderType.LimitDecrease, + decreaseSwapType: DecreasePositionSwapType.SwapPnlTokenToCollateralToken, + triggerOrderType: OrderType.StopLossDecrease, triggerPrice: params.stopLossPrice, } @@ -239,7 +240,7 @@ export async function increaseOrderHelper( indexPrice: 0n, collateralPrice: 0n, acceptablePrice: params.takeProfitPrice, - acceptablePriceDeltaBps: 0n, + acceptablePriceDeltaBps: ALLOWED_SLIPPAGE_BPS, recommendedAcceptablePriceDeltaBps: 0n, estimatedPnl: 0n, estimatedPnlPercentage: 0n, @@ -259,7 +260,7 @@ export async function increaseOrderHelper( payedRemainingCollateralUsd: 0n, receiveTokenAmount: 0n, receiveUsd: 0n, - decreaseSwapType: DecreasePositionSwapType.NoSwap, + decreaseSwapType: DecreasePositionSwapType.SwapPnlTokenToCollateralToken, triggerOrderType: OrderType.LimitDecrease, triggerPrice: params.takeProfitPrice, } diff --git a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts index 46cd80e..6edc76a 100644 --- a/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts +++ b/src/Managing.Web3Proxy/src/plugins/custom/gmx.ts @@ -126,8 +126,8 @@ export const openGmxPositionImpl = async ( quantity: number, leverage: number, price?: number, - takeProfitPrice?: number, - stopLossPrice?: number + stopLossPrice?: number, + takeProfitPrice?: number ): Promise => { try { // Get markets and tokens data from GMX SDK @@ -138,6 +138,8 @@ export const openGmxPositionImpl = async ( } console.log('price', price) + console.log('stopLossPrice', stopLossPrice) + console.log('takeProfitPrice', takeProfitPrice) const marketInfo = getMarketInfoFromTicker(ticker, marketsInfoData); const collateralToken = getTokenDataFromTicker("USDC", tokensData); // Using USDC as collateral @@ -167,7 +169,6 @@ export const openGmxPositionImpl = async ( console.log('params', params) - // Execute the main position order if (direction === TradeDirection.Long) { await sdk.orders.long(params); } else { @@ -208,8 +209,8 @@ export async function openGmxPosition( price?: number, quantity?: number, leverage?: number, - takeProfitPrice?: number, - stopLossPrice?: number + stopLossPrice?: number, + takeProfitPrice?: number ) { try { // Validate the request parameters @@ -221,13 +222,13 @@ export async function openGmxPosition( price, quantity, leverage, - takeProfitPrice, - stopLossPrice + stopLossPrice, + takeProfitPrice }); // Get client for the address const sdk = await this.getClientForAddress(account); - + // Call the implementation function const hash = await openGmxPositionImpl( sdk, @@ -236,8 +237,8 @@ export async function openGmxPosition( quantity, leverage, price, - takeProfitPrice, - stopLossPrice + stopLossPrice, + takeProfitPrice ); return { diff --git a/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx b/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx new file mode 100644 index 0000000..f7363d1 --- /dev/null +++ b/src/Managing.WebApp/src/components/mollecules/ManualPositionModal.tsx @@ -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 + +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() + + 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 ( + + + Open Position Manually for {botName} + + + + Direction + + + Long + Short + + {errors.direction && {errors.direction.message}} + + + + Open Position + { reset(); onClose(); }}>Cancel + + + + {/* Click outside closes modal */} + + { reset(); onClose(); }}>close + + + ) +} + +export default ManualPositionModal \ No newline at end of file diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index d782fb5..ce30214 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -1,6 +1,6 @@ //---------------------- // -// 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) // //---------------------- @@ -765,6 +765,45 @@ export class BotClient extends AuthorizedApiBase { } return Promise.resolve(null as any); } + + bot_OpenPositionManually(request: OpenPositionManuallyRequest): Promise { + 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 { + 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(null as any); + } } 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 }; + private baseUrl: string; + protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; + + constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise }) { + super(configuration); + this.http = http ? http : window as any; + this.baseUrl = baseUrl ?? "http://localhost:5000"; + } + + sentryTest_TestException(): Promise { + 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 { + 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(null as any); + } + + sentryTest_ThrowException(): Promise { + 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 { + 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(null as any); + } + + sentryTest_TestMessage(): Promise { + 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 { + 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(null as any); + } +} + export class SettingsClient extends AuthorizedApiBase { private http: { fetch(url: RequestInfo, init?: RequestInit): Promise }; private baseUrl: string; @@ -2022,6 +2193,7 @@ export interface Account { secret?: string | null; user?: User | null; balances?: Balance[] | null; + isPrivyWallet?: boolean; } export enum TradingExchanges { @@ -2192,6 +2364,7 @@ export enum Timeframe { OneHour = "OneHour", FourHour = "FourHour", OneDay = "OneDay", + OneMinute = "OneMinute", } export interface Trade { @@ -2466,6 +2639,13 @@ export interface TradingBot { moneyManagement: MoneyManagement; } +export interface OpenPositionManuallyRequest { + botName?: string | null; + direction?: TradeDirection; + stopLossPrice?: number; + takeProfitPrice?: number; +} + export interface SpotlightOverview { spotlights: Spotlight[]; dateTime: Date; diff --git a/src/Managing.WebApp/src/pages/botsPage/botList.tsx b/src/Managing.WebApp/src/pages/botsPage/botList.tsx index d758914..68cfe44 100644 --- a/src/Managing.WebApp/src/pages/botsPage/botList.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/botList.tsx @@ -1,5 +1,5 @@ -import { EyeIcon, PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/solid' -import React from 'react' +import { EyeIcon, PlayIcon, StopIcon, TrashIcon, PlusCircleIcon } from '@heroicons/react/solid' +import React, { useState } from 'react' import useApiUrlStore from '../../app/store/apiStore' import { @@ -8,6 +8,7 @@ import { CardText, Toast, } from '../../components/mollecules' +import ManualPositionModal from '../../components/mollecules/ManualPositionModal' import { TradeChart } from '../../components/organism' import { BotClient } from '../../generated/ManagingApi' import type { @@ -38,9 +39,11 @@ const BotList: React.FC = ({ list }) => { const { apiUrl } = useApiUrlStore() const client = new BotClient({}, apiUrl) const [showMoneyManagementModal, setShowMoneyManagementModal] = - React.useState(false) + useState(false) const [selectedMoneyManagement, setSelectedMoneyManagement] = - React.useState() + useState() + const [showManualPositionModal, setShowManualPositionModal] = useState(false) + const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState(null) function getIsForWatchingBadge(isForWatchingOnly: boolean, name: string) { const classes = @@ -121,6 +124,22 @@ const BotList: React.FC = ({ list }) => { ) } + function getManualPositionBadge(botName: string) { + const classes = baseBadgeClass() + ' bg-info' + return ( + openManualPositionModal(botName)}> + + + + + ) + } + + function openManualPositionModal(botName: string) { + setSelectedBotForManualPosition(botName) + setShowManualPositionModal(true) + } + function toggleBotStatus(status: string, name: string, botType: BotType) { const isUp = status == 'Up' const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot') @@ -184,6 +203,7 @@ const BotList: React.FC = ({ list }) => { {getMoneyManagementBadge(bot.moneyManagement)} {getIsForWatchingBadge(bot.isForWatchingOnly, bot.name)} {getToggleBotStatusBadge(bot.status, bot.name, bot.botType)} + {getManualPositionBadge(bot.name)} {getDeleteBadge(bot.name)} @@ -239,6 +259,14 @@ const BotList: React.FC = ({ list }) => { onClose={() => setShowMoneyManagementModal(false)} disableInputs={true} /> + { + setShowManualPositionModal(false) + setSelectedBotForManualPosition(null) + }} + /> ) }
+ +