diff --git a/src/Managing.WebApp/src/components/mollecules/TradesModal/TradesModal.tsx b/src/Managing.WebApp/src/components/mollecules/TradesModal/TradesModal.tsx new file mode 100644 index 0000000..2f9b51e --- /dev/null +++ b/src/Managing.WebApp/src/components/mollecules/TradesModal/TradesModal.tsx @@ -0,0 +1,111 @@ +import React, {useEffect, useState} from 'react' +import type {Position, UserStrategyDetailsViewModel} from '../../../generated/ManagingApi' +import {DataClient} from '../../../generated/ManagingApi' +import useApiUrlStore from '../../../app/store/apiStore' +import Modal from '../Modal/Modal' + +interface TradesModalProps { + showModal: boolean + botName: string | null + onClose: () => void +} + +const TradesModal: React.FC = ({ + showModal, + botName, + onClose, +}) => { + const { apiUrl } = useApiUrlStore() + const [strategyData, setStrategyData] = useState(null) + const [loading, setLoading] = useState(false) + + useEffect(() => { + if (showModal && botName) { + fetchStrategyData() + } + }, [showModal, botName]) + + const fetchStrategyData = async () => { + if (!botName) return + + setLoading(true) + const client = new DataClient({}, apiUrl) + + try { + const data = await client.data_GetUserStrategy("Oda", botName) + setStrategyData(data) + } catch (error) { + console.error('Error fetching strategy data:', error) + } finally { + setLoading(false) + } + } + + return ( + +
+ {loading ? ( +
+
+
+ ) : strategyData ? ( +
+
+

Strategy: {strategyData.strategyName}

+

Win Rate: {strategyData.winRate?.toFixed(2)}%

+

PnL: {strategyData.pnL?.toFixed(2)} $

+

Wins: {strategyData.wins} / Losses: {strategyData.losses}

+

Total Volume Traded: {strategyData.totalVolumeTraded?.toFixed(2)} $

+

Volume Last 24H: {strategyData.volumeLast24H?.toFixed(2)} $

+
+ +
+ + + + + + + + + + + + + {strategyData.positions && strategyData.positions.length > 0 ? ( + strategyData.positions.map((position: Position) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
DateDirectionStatusEntry PriceQuantityPnL
{new Date(position.date).toLocaleString()} + {position.originDirection} + {position.status}{position.open.price.toFixed(2)}{position.open.quantity.toFixed(4)} 0 ? 'text-success' : 'text-error'}> + {position.profitAndLoss?.realized?.toFixed(2) || '0.00'} $ +
No trades found
+
+
+ ) : ( +

No data available

+ )} +
+
+ ) +} + +export default TradesModal \ 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 ce30214..c2dbd7d 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -941,6 +941,189 @@ export class DataClient extends AuthorizedApiBase { } return Promise.resolve(null as any); } + + data_GetStrategiesStatistics(): Promise { + let url_ = this.baseUrl + "/Data/GetStrategiesStatistics"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processData_GetStrategiesStatistics(_response); + }); + } + + protected processData_GetStrategiesStatistics(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 StrategiesStatisticsViewModel; + 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); + } + + data_GetTopStrategies(): Promise { + let url_ = this.baseUrl + "/Data/GetTopStrategies"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processData_GetTopStrategies(_response); + }); + } + + protected processData_GetTopStrategies(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 TopStrategiesViewModel; + 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); + } + + data_GetUserStrategies(agentName: string | null | undefined): Promise { + let url_ = this.baseUrl + "/Data/GetUserStrategies?"; + if (agentName !== undefined && agentName !== null) + url_ += "agentName=" + encodeURIComponent("" + agentName) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processData_GetUserStrategies(_response); + }); + } + + protected processData_GetUserStrategies(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 UserStrategyDetailsViewModel[]; + 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); + } + + data_GetUserStrategy(agentName: string | null | undefined, strategyName: string | null | undefined): Promise { + let url_ = this.baseUrl + "/Data/GetUserStrategy?"; + if (agentName !== undefined && agentName !== null) + url_ += "agentName=" + encodeURIComponent("" + agentName) + "&"; + if (strategyName !== undefined && strategyName !== null) + url_ += "strategyName=" + encodeURIComponent("" + strategyName) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processData_GetUserStrategy(_response); + }); + } + + protected processData_GetUserStrategy(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 UserStrategyDetailsViewModel; + 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); + } + + data_GetPlatformSummary(timeFilter: string | null | undefined): Promise { + let url_ = this.baseUrl + "/Data/GetPlatformSummary?"; + if (timeFilter !== undefined && timeFilter !== null) + url_ += "timeFilter=" + encodeURIComponent("" + timeFilter) + "&"; + url_ = url_.replace(/[?&]$/, ""); + + let options_: RequestInit = { + method: "GET", + headers: { + "Accept": "application/json" + } + }; + + return this.transformOptions(options_).then(transformedOptions_ => { + return this.http.fetch(url_, transformedOptions_); + }).then((_response: Response) => { + return this.processData_GetPlatformSummary(_response); + }); + } + + protected processData_GetPlatformSummary(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 PlatformSummaryViewModel; + 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 MoneyManagementClient extends AuthorizedApiBase { @@ -2338,7 +2521,7 @@ export interface Position { signalIdentifier?: string | null; identifier: string; initiator: PositionInitiator; - user?: User | null; + user: User; } export enum TradeDirection { @@ -2642,8 +2825,6 @@ export interface TradingBot { export interface OpenPositionManuallyRequest { botName?: string | null; direction?: TradeDirection; - stopLossPrice?: number; - takeProfitPrice?: number; } export interface SpotlightOverview { @@ -2690,6 +2871,60 @@ export interface TickerSignal { oneDay: Signal[]; } +export interface StrategiesStatisticsViewModel { + totalStrategiesRunning?: number; + changeInLast24Hours?: number; +} + +export interface TopStrategiesViewModel { + topStrategies?: StrategyPerformance[] | null; +} + +export interface StrategyPerformance { + strategyName?: string | null; + pnL?: number; +} + +export interface UserStrategyDetailsViewModel { + name?: string | null; + strategyName?: string | null; + state?: string | null; + pnL?: number; + roiPercentage?: number; + roiLast24H?: number; + runtime?: Date; + winRate?: number; + totalVolumeTraded?: number; + volumeLast24H?: number; + wins?: number; + losses?: number; + positions?: Position[] | null; +} + +export interface PlatformSummaryViewModel { + totalAgents?: number; + totalActiveStrategies?: number; + totalPlatformPnL?: number; + totalPlatformVolume?: number; + totalPlatformVolumeLast24h?: number; + agentSummaries?: AgentSummaryViewModel[] | null; + timeFilter?: string | null; +} + +export interface AgentSummaryViewModel { + username?: string | null; + totalPnL?: number; + pnLLast24h?: number; + totalROI?: number; + roiLast24h?: number; + wins?: number; + losses?: number; + averageWinRate?: number; + activeStrategiesCount?: number; + totalVolume?: number; + volumeLast24h?: number; +} + export enum RiskLevel { Low = "Low", Medium = "Medium", diff --git a/src/Managing.WebApp/src/pages/botsPage/botList.tsx b/src/Managing.WebApp/src/pages/botsPage/botList.tsx index 68cfe44..ccd012f 100644 --- a/src/Managing.WebApp/src/pages/botsPage/botList.tsx +++ b/src/Managing.WebApp/src/pages/botsPage/botList.tsx @@ -1,22 +1,14 @@ -import { EyeIcon, PlayIcon, StopIcon, TrashIcon, PlusCircleIcon } from '@heroicons/react/solid' -import React, { useState } from 'react' +import {ChartBarIcon, EyeIcon, PlayIcon, PlusCircleIcon, StopIcon, TrashIcon} from '@heroicons/react/solid' +import React, {useState} from 'react' import useApiUrlStore from '../../app/store/apiStore' -import { - CardPosition, - CardSignal, - CardText, - Toast, -} from '../../components/mollecules' +import {CardPosition, CardSignal, CardText, Toast,} from '../../components/mollecules' import ManualPositionModal from '../../components/mollecules/ManualPositionModal' -import { TradeChart } from '../../components/organism' -import { BotClient } from '../../generated/ManagingApi' -import type { - BotType, - MoneyManagement, - TradingBot, -} from '../../generated/ManagingApi' -import type { IBotList } from '../../global/type' +import TradesModal from '../../components/mollecules/TradesModal/TradesModal' +import {TradeChart} from '../../components/organism' +import type {BotType, MoneyManagement, TradingBot,} from '../../generated/ManagingApi' +import {BotClient} from '../../generated/ManagingApi' +import type {IBotList} from '../../global/type' import MoneyManagementModal from '../settingsPage/moneymanagement/moneyManagementModal' function baseBadgeClass(isOutlined = false) { @@ -44,6 +36,8 @@ const BotList: React.FC = ({ list }) => { useState() const [showManualPositionModal, setShowManualPositionModal] = useState(false) const [selectedBotForManualPosition, setSelectedBotForManualPosition] = useState(null) + const [showTradesModal, setShowTradesModal] = useState(false) + const [selectedBotForTrades, setSelectedBotForTrades] = useState(null) function getIsForWatchingBadge(isForWatchingOnly: boolean, name: string) { const classes = @@ -135,11 +129,27 @@ const BotList: React.FC = ({ list }) => { ) } + function getTradesBadge(botName: string) { + const classes = baseBadgeClass() + ' bg-secondary' + return ( + + ) + } + function openManualPositionModal(botName: string) { setSelectedBotForManualPosition(botName) setShowManualPositionModal(true) } + function openTradesModal(botName: string) { + setSelectedBotForTrades(botName) + setShowTradesModal(true) + } + function toggleBotStatus(status: string, name: string, botType: BotType) { const isUp = status == 'Up' const t = new Toast(isUp ? 'Stoping bot' : 'Restarting bot') @@ -246,6 +256,7 @@ const BotList: React.FC = ({ list }) => {
PNL {bot.profitAndLoss.toFixed(2).toString()} $
+ {getTradesBadge(bot.name)} @@ -267,6 +278,14 @@ const BotList: React.FC = ({ list }) => { setSelectedBotForManualPosition(null) }} /> + { + setShowTradesModal(false) + setSelectedBotForTrades(null) + }} + /> ) }