Open position for bots
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ export const openGmxPositionImpl = async (
|
||||
quantity: number,
|
||||
leverage: number,
|
||||
price?: number,
|
||||
takeProfitPrice?: number,
|
||||
stopLossPrice?: number
|
||||
stopLossPrice?: number,
|
||||
takeProfitPrice?: number
|
||||
): Promise<string> => {
|
||||
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 {
|
||||
|
||||
@@ -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>
|
||||
// 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>
|
||||
//----------------------
|
||||
|
||||
@@ -765,6 +765,45 @@ export class BotClient extends AuthorizedApiBase {
|
||||
}
|
||||
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 {
|
||||
@@ -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 {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
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;
|
||||
|
||||
@@ -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<IBotList> = ({ list }) => {
|
||||
const { apiUrl } = useApiUrlStore()
|
||||
const client = new BotClient({}, apiUrl)
|
||||
const [showMoneyManagementModal, setShowMoneyManagementModal] =
|
||||
React.useState(false)
|
||||
useState(false)
|
||||
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) {
|
||||
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) {
|
||||
const isUp = status == 'Up'
|
||||
const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot')
|
||||
@@ -184,6 +203,7 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
||||
{getMoneyManagementBadge(bot.moneyManagement)}
|
||||
{getIsForWatchingBadge(bot.isForWatchingOnly, bot.name)}
|
||||
{getToggleBotStatusBadge(bot.status, bot.name, bot.botType)}
|
||||
{getManualPositionBadge(bot.name)}
|
||||
{getDeleteBadge(bot.name)}
|
||||
</h2>
|
||||
|
||||
@@ -239,6 +259,14 @@ const BotList: React.FC<IBotList> = ({ list }) => {
|
||||
onClose={() => setShowMoneyManagementModal(false)}
|
||||
disableInputs={true}
|
||||
/>
|
||||
<ManualPositionModal
|
||||
showModal={showManualPositionModal}
|
||||
botName={selectedBotForManualPosition}
|
||||
onClose={() => {
|
||||
setShowManualPositionModal(false)
|
||||
setSelectedBotForManualPosition(null)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user