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

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

View File

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