Open position for bots

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

View File

@@ -16,6 +16,7 @@ using System;
using System.Collections.Generic; using System.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}");
}
}
} }

View File

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

View File

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

View File

@@ -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

View File

@@ -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");
} }

View File

@@ -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,
} }

View File

@@ -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 {

View File

@@ -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

View File

@@ -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;

View File

@@ -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>
) )
} }