Fix healthcheck and tradingbot positions

This commit is contained in:
2025-04-25 15:50:12 +07:00
parent 5844d89175
commit ea1a25e699
4 changed files with 291 additions and 316 deletions

View File

@@ -411,6 +411,8 @@ public class TradingBot : Bot, ITradingBot
{ {
Logger.LogInformation( Logger.LogInformation(
$"Position {signal.Identifier} don't need to be update. Position still opened"); $"Position {signal.Identifier} don't need to be update. Position still opened");
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
} }
} }
@@ -445,6 +447,10 @@ public class TradingBot : Bot, ITradingBot
{ {
Logger.LogInformation( Logger.LogInformation(
$"Position {signal.Identifier} don't need to be update. Position still opened"); $"Position {signal.Identifier} don't need to be update. Position still opened");
position.Status = PositionStatus.Filled;
await SetPositionStatus(signal.Identifier, PositionStatus.Filled);
} }
} }
} }

View File

@@ -1,14 +1,8 @@
import { ArrowDownIcon, ArrowUpIcon } from '@heroicons/react/solid' import {ArrowDownIcon, ArrowUpIcon} from '@heroicons/react/solid'
import React from 'react' import React from 'react'
import { import {useExpanded, useFilters, usePagination, useSortBy, useTable,} from 'react-table'
useTable,
usePagination,
useSortBy,
useFilters,
useExpanded,
} from 'react-table'
import type { TableInstanceWithHooks } from '../../../global/type' import type {TableInstanceWithHooks} from '../../../global/type'
// Define a default UI for filtering // Define a default UI for filtering
function DefaultColumnFilter({ function DefaultColumnFilter({
@@ -31,7 +25,7 @@ export default function Table({
columns, columns,
data, data,
renderRowSubCompontent, renderRowSubCompontent,
showPagination, showPagination = true,
hiddenColumns, hiddenColumns,
showTotal = false, showTotal = false,
}: any) { }: any) {
@@ -49,10 +43,7 @@ export default function Table({
headerGroups, headerGroups,
prepareRow, prepareRow,
visibleColumns, visibleColumns,
page, // Instead of using 'rows', we'll use page, page,
// which has only the rows for the active page
// The rest of these things are super handy, too ;)
canPreviousPage, canPreviousPage,
canNextPage, canNextPage,
pageOptions, pageOptions,

View File

@@ -1,25 +1,14 @@
import { import {ArrowDownIcon, ArrowUpIcon, ChevronDownIcon, ChevronRightIcon, PlayIcon,} from '@heroicons/react/solid'
ArrowDownIcon, import React, {useEffect, useState} from 'react'
ArrowUpIcon,
ChevronDownIcon,
ChevronRightIcon,
PlayIcon,
} from '@heroicons/react/solid'
import React, { useEffect, useState } from 'react'
import { Hub } from '../../../app/providers/Hubs' import {Hub} from '../../../app/providers/Hubs'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import type { Account, TradingBot } from '../../../generated/ManagingApi' import type {Account, TradingBot} from '../../../generated/ManagingApi'
import { import {AccountClient, BotClient, TradeDirection, TradeStatus,} from '../../../generated/ManagingApi'
AccountClient, import {SelectColumnFilter, Table} from '../../mollecules'
BotClient,
TradeDirection,
TradeStatus,
} from '../../../generated/ManagingApi'
import { SelectColumnFilter, Table } from '../../mollecules'
import BacktestRowDetails from '../Backtest/backtestRowDetails'
import StatusBadge from '../StatusBadge/StatusBadge' import StatusBadge from '../StatusBadge/StatusBadge'
import Summary from '../Trading/Summary' import Summary from '../Trading/Summary'
import BotRowDetails from './botRowDetails'
export default function ActiveBots() { export default function ActiveBots() {
const [bots, setBots] = useState<TradingBot[]>([]) const [bots, setBots] = useState<TradingBot[]>([])
@@ -63,10 +52,14 @@ export default function ActiveBots() {
Header: 'Status', Header: 'Status',
accessor: 'status', accessor: 'status',
disableFilters: true, disableFilters: true,
sortType: 'basic', disableSortBy: true,
search: false,
}, },
{ {
accessor: 'isForWatchingOnly', accessor: 'isForWatchingOnly',
disableFilters: true,
disableSortBy: true,
search: false,
}, },
{ {
Filter: SelectColumnFilter, Filter: SelectColumnFilter,
@@ -185,10 +178,9 @@ export default function ActiveBots() {
const renderRowSubComponent = React.useCallback( const renderRowSubComponent = React.useCallback(
({ row }: any) => ( ({ row }: any) => (
<> <>
<BacktestRowDetails <BotRowDetails
candles={row.original.candles} bot={row.original}
positions={row.original.positions} ></BotRowDetails>
></BacktestRowDetails>
</> </>
), ),
[] []
@@ -204,6 +196,7 @@ export default function ActiveBots() {
columns={columns} columns={columns}
data={bots} data={bots}
renderRowSubCompontent={renderRowSubComponent} renderRowSubCompontent={renderRowSubComponent}
hiddenColumns={['isForWatchingOnly']}
/> />
</div> </div>
</> </>

View File

@@ -1,315 +1,300 @@
import React, {useEffect, useState} from 'react' import React from 'react'
import {useQuery} from '@tanstack/react-query'
import useApiUrlStore from '../../../app/store/apiStore' import useApiUrlStore from '../../../app/store/apiStore'
import {Table} from '../../../components/mollecules' import {Table} from '../../../components/mollecules'
// Define health check response interface based on the provided example // Define health check response interface based on the provided example
interface HealthCheckEntry { interface HealthCheckEntry {
data: Record<string, any> data: Record<string, any>
duration: string duration: string
status: string status: string
tags: string[] tags: string[]
description?: string description?: string
} }
interface HealthCheckResponse { interface HealthCheckResponse {
status: string status: string
totalDuration: string totalDuration: string
entries: Record<string, HealthCheckEntry> entries: Record<string, HealthCheckEntry>
} }
// Interface for candle timeframe check data // Interface for candle timeframe check data
interface CandleTimeframeCheck { interface CandleTimeframeCheck {
CheckedTicker: string CheckedTicker: string
CheckedTimeframe: string CheckedTimeframe: string
StartDate: string StartDate: string
LatestCandleDate?: string LatestCandleDate?: string
TimeDifference?: string TimeDifference?: string
Status: string Status: string
Message: string Message: string
} }
interface Web3ProxyHealthDetail { interface Web3ProxyHealthDetail {
status: string status: string
message: string message: string
data?: Record<string, any> data?: Record<string, any>
} }
const HealthChecks: React.FC = () => { const HealthChecks: React.FC = () => {
const { apiUrl, workerUrl } = useApiUrlStore() const {apiUrl, workerUrl} = useApiUrlStore()
const [apiHealth, setApiHealth] = useState<HealthCheckResponse | null>(null)
const [workerHealth, setWorkerHealth] = useState<HealthCheckResponse | null>(null)
const [web3ProxyHealth, setWeb3ProxyHealth] = useState<HealthCheckResponse | null>(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => { // Use TanStack Query for API health check
const fetchHealthChecks = async () => { const {data: apiHealth, isLoading: isLoadingApi} = useQuery({
setIsLoading(true) queryKey: ['health', 'api'],
try { queryFn: async () => {
// Fetch API health check const response = await fetch(`${apiUrl}/health`)
const apiResponse = await fetch(`${apiUrl}/health`) if (!response.ok) throw new Error('Failed to fetch API health')
if (apiResponse.ok) { return response.json() as Promise<HealthCheckResponse>
const data = await apiResponse.json() }
setApiHealth(data) })
// Use TanStack Query for Worker health check
const {data: workerHealth, isLoading: isLoadingWorker} = useQuery({
queryKey: ['health', 'worker'],
queryFn: async () => {
const response = await fetch(`${workerUrl}/health`)
if (!response.ok) throw new Error('Failed to fetch Worker health')
return response.json() as Promise<HealthCheckResponse>
}
})
// Determine overall loading state
const isLoading = isLoadingApi || isLoadingWorker
// Helper function to prepare table data from health response
const prepareHealthData = (
service: string,
health: HealthCheckResponse | null
) => {
if (!health) {
return [
{
service,
component: 'N/A',
status: 'Unreachable',
duration: 'N/A',
tags: 'N/A',
description: 'Service unreachable',
details: null,
},
]
} }
// Fetch Worker health check // Convert entries to rows for the table
const workerResponse = await fetch(`${workerUrl}/health`) const results: any[] = [];
if (workerResponse.ok) {
const data = await workerResponse.json()
setWorkerHealth(data)
}
// Fetch Web3Proxy health check - use the dedicated endpoint we created Object.entries(health.entries).forEach(([key, entry]) => {
const web3Response = await fetch(`${apiUrl}/health/web3proxy`) // Basic health check entry
if (web3Response.ok) { const baseEntry = {
const data = await web3Response.json() service,
setWeb3ProxyHealth(data) component: key,
} status: entry.status,
} catch (error) { duration: entry.duration,
console.error('Error fetching health checks:', error) tags: entry.tags.join(', '),
} finally { description: entry.description || '',
setIsLoading(false) details: null,
} };
}
fetchHealthChecks() // Add the base entry
}, [apiUrl, workerUrl]) results.push(baseEntry);
// Helper function to prepare table data from health response // Special handling for candle-data to expand timeframe checks
const prepareHealthData = ( if (key === 'candle-data' && entry.data) {
service: string, // Extract timeframe checks
health: HealthCheckResponse | null Object.entries(entry.data)
) => { .filter(([dataKey]) => dataKey.startsWith('TimeframeCheck_'))
if (!health) { .forEach(([dataKey, timeframeData]) => {
return [ const tfData = timeframeData as CandleTimeframeCheck;
{ results.push({
service, service,
component: 'N/A', component: `${key} - ${tfData.CheckedTimeframe}`,
status: 'Unreachable', status: tfData.Status,
duration: 'N/A', duration: '',
tags: 'N/A', tags: 'candles',
description: 'Service unreachable', description: tfData.Message,
details: null, details: {
}, Ticker: tfData.CheckedTicker,
] LatestCandle: tfData.LatestCandleDate,
} TimeDifference: tfData.TimeDifference,
},
// Convert entries to rows for the table });
const results: any[] = []; });
Object.entries(health.entries).forEach(([key, entry]) => {
// Basic health check entry
const baseEntry = {
service,
component: key,
status: entry.status,
duration: entry.duration,
tags: entry.tags.join(', '),
description: entry.description || '',
details: null,
};
// Add the base entry
results.push(baseEntry);
// Special handling for candle-data to expand timeframe checks
if (key === 'candle-data' && entry.data) {
// Extract timeframe checks
Object.entries(entry.data)
.filter(([dataKey]) => dataKey.startsWith('TimeframeCheck_'))
.forEach(([dataKey, timeframeData]) => {
const tfData = timeframeData as CandleTimeframeCheck;
results.push({
service,
component: `${key} - ${tfData.CheckedTimeframe}`,
status: tfData.Status,
duration: '',
tags: 'candles',
description: tfData.Message,
details: {
Ticker: tfData.CheckedTicker,
LatestCandle: tfData.LatestCandleDate,
TimeDifference: tfData.TimeDifference,
},
});
});
}
// Special handling for Web3Proxy components
if (key === 'web3proxy' && entry.data) {
// Handle Privy check if present
if (entry.data.privy) {
const privyData = entry.data.privy as Web3ProxyHealthDetail;
results.push({
service,
component: `${key} - Privy`,
status: privyData.status,
duration: '',
tags: 'privy, external',
description: privyData.message || '',
details: null,
});
}
// Handle GMX check if present
if (entry.data.gmx) {
const gmxData = entry.data.gmx as Web3ProxyHealthDetail;
const marketDetails: Record<string, any> = {};
// Add market count and response time if available
if (gmxData.data) {
if (gmxData.data.marketCount) {
marketDetails['Market Count'] = gmxData.data.marketCount;
} }
if (gmxData.data.responseTimeMs) {
marketDetails['Response Time'] = `${gmxData.data.responseTimeMs}ms`; // Special handling for Web3Proxy components
} if (key === 'web3proxy' && entry.data) {
// Handle Privy check if present
// Add sample markets info (just count for details section) if (entry.data.privy) {
if (gmxData.data.sampleMarkets && Array.isArray(gmxData.data.sampleMarkets)) { const privyData = entry.data.privy as Web3ProxyHealthDetail;
marketDetails['Sample Markets'] = gmxData.data.sampleMarkets.length; results.push({
service,
// If there are sample markets, add the first one's details component: `${key} - Privy`,
if (gmxData.data.sampleMarkets.length > 0) { status: privyData.status,
const firstMarket = gmxData.data.sampleMarkets[0]; duration: '',
if (firstMarket.indexToken) { tags: 'privy, external',
marketDetails['Example Market'] = firstMarket.indexToken; description: privyData.message || '',
details: null,
});
}
// Handle GMX check if present
if (entry.data.gmx) {
const gmxData = entry.data.gmx as Web3ProxyHealthDetail;
const marketDetails: Record<string, any> = {};
// Add market count and response time if available
if (gmxData.data) {
if (gmxData.data.marketCount) {
marketDetails['Market Count'] = gmxData.data.marketCount;
}
if (gmxData.data.responseTimeMs) {
marketDetails['Response Time'] = `${gmxData.data.responseTimeMs}ms`;
}
// Add sample markets info (just count for details section)
if (gmxData.data.sampleMarkets && Array.isArray(gmxData.data.sampleMarkets)) {
marketDetails['Sample Markets'] = gmxData.data.sampleMarkets.length;
// If there are sample markets, add the first one's details
if (gmxData.data.sampleMarkets.length > 0) {
const firstMarket = gmxData.data.sampleMarkets[0];
if (firstMarket.indexToken) {
marketDetails['Example Market'] = firstMarket.indexToken;
}
}
}
}
results.push({
service,
component: `${key} - GMX`,
status: gmxData.status,
duration: '',
tags: 'gmx, external',
description: gmxData.message || '',
details: Object.keys(marketDetails).length > 0 ? marketDetails : null,
});
}
// Add version info if available
if (entry.data.version) {
const versionDetails: Record<string, any> = {
Version: entry.data.version
};
if (entry.data.timestamp) {
versionDetails['Last Updated'] = entry.data.timestamp;
}
results.push({
service,
component: `${key} - Version`,
status: entry.status,
duration: '',
tags: 'version',
description: `Web3Proxy Version: ${entry.data.version}`,
details: versionDetails,
});
} }
}
} }
} });
results.push({
service,
component: `${key} - GMX`,
status: gmxData.status,
duration: '',
tags: 'gmx, external',
description: gmxData.message || '',
details: Object.keys(marketDetails).length > 0 ? marketDetails : null,
});
}
// Add version info if available
if (entry.data.version) {
const versionDetails: Record<string, any> = {
Version: entry.data.version
};
if (entry.data.timestamp) {
versionDetails['Last Updated'] = entry.data.timestamp;
}
results.push({
service,
component: `${key} - Version`,
status: entry.status,
duration: '',
tags: 'version',
description: `Web3Proxy Version: ${entry.data.version}`,
details: versionDetails,
});
}
}
});
return results;
}
// Combine all health check data for display return results;
const healthData = [ }
...prepareHealthData('Managing API', apiHealth),
...prepareHealthData('Managing Worker', workerHealth),
...prepareHealthData('Web3 Proxy', web3ProxyHealth),
]
// Define columns for the table // Combine all health check data for display
const columns = React.useMemo( const healthData = [
() => [ ...prepareHealthData('Managing API', apiHealth || null),
{ ...prepareHealthData('Managing Worker', workerHealth || null),
Header: 'Service', ]
accessor: 'service',
disableSortBy: true, // Define columns for the table
disableFilters: true, const columns = React.useMemo(
}, () => [
{ {
Header: 'Component', Header: 'Service',
accessor: 'component', accessor: 'service',
disableSortBy: true, disableSortBy: true,
disableFilters: true, disableFilters: true,
}, },
{ {
Header: 'Status', Header: 'Component',
accessor: 'status', accessor: 'component',
Cell: ({ value }: { value: string }) => ( disableSortBy: true,
<span disableFilters: true,
className={`badge ${ },
value === 'Healthy' || value === 'healthy' {
? 'badge-success' Header: 'Status',
: value === 'Unreachable' || value === 'unhealthy' accessor: 'status',
? 'badge-error' Cell: ({value}: { value: string }) => (
: 'badge-warning' <span
}`} className={`badge ${
> value === 'Healthy' || value === 'healthy'
? 'badge-success'
: value === 'Unreachable' || value === 'unhealthy'
? 'badge-error'
: 'badge-warning'
}`}
>
{value.charAt(0).toUpperCase() + value.slice(1)} {value.charAt(0).toUpperCase() + value.slice(1)}
</span> </span>
), ),
disableSortBy: true, disableSortBy: true,
disableFilters: true, disableFilters: true,
}, },
{ {
Header: 'Description', Header: 'Description',
accessor: 'description', accessor: 'description',
disableSortBy: true, disableSortBy: true,
disableFilters: true, disableFilters: true,
Cell: ({ value, row }: any) => ( Cell: ({value, row}: any) => (
<div> <div>
<div>{value}</div> <div>{value}</div>
{row.original.details && ( {row.original.details && (
<div className="text-xs mt-1 opacity-80"> <div className="text-xs mt-1 opacity-80">
{Object.entries(row.original.details).filter(([_, val]) => val !== undefined).map( {Object.entries(row.original.details).filter(([_, val]) => val !== undefined).map(
([key, val]) => ( ([key, val]) => (
<div key={key}> <div key={key}>
<span className="font-semibold">{key}:</span> {String(val)} <span className="font-semibold">{key}:</span> {String(val)}
</div>
)
)}
</div>
)}
</div> </div>
) ),
)} },
</div> {
)} Header: 'Duration',
</div> accessor: 'duration',
), disableSortBy: true,
}, disableFilters: true,
{ },
Header: 'Duration', {
accessor: 'duration', Header: 'Tags',
disableSortBy: true, accessor: 'tags',
disableFilters: true, disableSortBy: true,
}, disableFilters: true,
{ },
Header: 'Tags', ],
accessor: 'tags', []
disableSortBy: true, )
disableFilters: true,
},
],
[]
)
return ( return (
<div className="container mx-auto"> <div className="container mx-auto">
<h2 className="text-xl font-bold mb-4">System Health Status</h2> <h2 className="text-xl font-bold mb-4">System Health Status</h2>
{isLoading ? ( {isLoading ? (
<progress className="progress progress-primary w-56"></progress> <progress className="progress progress-primary w-56"></progress>
) : ( ) : (
<Table <Table
columns={columns} columns={columns}
data={healthData} data={healthData}
showPagination={false} showPagination={true}
/> />
)} )}
</div> </div>
) )
} }
export default HealthChecks export default HealthChecks