Open position for bots
This commit is contained in:
@@ -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