feat: add leader research agent
This commit is contained in:
@@ -15,6 +15,7 @@ import AccountDetail from './pages/AccountDetail'
|
||||
import AccountEdit from './pages/AccountEdit'
|
||||
import LeaderList from './pages/LeaderList'
|
||||
import LeaderPool from './pages/LeaderPool'
|
||||
import LeaderResearch from './pages/LeaderResearch'
|
||||
import LeaderAdd from './pages/LeaderAdd'
|
||||
import LeaderEdit from './pages/LeaderEdit'
|
||||
import ConfigPage from './pages/ConfigPage'
|
||||
@@ -267,6 +268,7 @@ function App() {
|
||||
<Route path="/accounts/edit" element={<ProtectedRoute><AccountEdit /></ProtectedRoute>} />
|
||||
<Route path="/leaders" element={<ProtectedRoute><LeaderList /></ProtectedRoute>} />
|
||||
<Route path="/leader-pool" element={<ProtectedRoute><LeaderPool /></ProtectedRoute>} />
|
||||
<Route path="/leader-research" element={<ProtectedRoute><LeaderResearch /></ProtectedRoute>} />
|
||||
<Route path="/leaders/add" element={<ProtectedRoute><LeaderAdd /></ProtectedRoute>} />
|
||||
<Route path="/leaders/edit" element={<ProtectedRoute><LeaderEdit /></ProtectedRoute>} />
|
||||
<Route path="/templates" element={<ProtectedRoute><TemplateList /></ProtectedRoute>} />
|
||||
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
NotificationOutlined,
|
||||
LineChartOutlined,
|
||||
RocketOutlined,
|
||||
DashboardOutlined
|
||||
DashboardOutlined,
|
||||
ExperimentOutlined
|
||||
} from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import type { ReactNode } from 'react'
|
||||
@@ -74,7 +75,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
const getInitialOpenKeys = (): string[] => {
|
||||
const path = location.pathname
|
||||
const keys: string[] = []
|
||||
if (path.startsWith('/leaders') || path.startsWith('/leader-pool') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/leader-pool') || path.startsWith('/leader-research') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
|
||||
keys.push('/copy-trading-management')
|
||||
}
|
||||
if (path.startsWith('/crypto-tail-strategy') || path.startsWith('/crypto-tail-monitor')) {
|
||||
@@ -92,7 +93,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
const path = location.pathname
|
||||
const keys: string[] = []
|
||||
if (path.startsWith('/leaders') || path.startsWith('/leader-pool') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/leader-pool') || path.startsWith('/leader-research') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
|
||||
keys.push('/copy-trading-management')
|
||||
}
|
||||
if (path.startsWith('/crypto-tail-strategy') || path.startsWith('/crypto-tail-monitor')) {
|
||||
@@ -153,6 +154,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
icon: <TeamOutlined />,
|
||||
label: t('menu.leaderPool')
|
||||
},
|
||||
{
|
||||
key: '/leader-research',
|
||||
icon: <ExperimentOutlined />,
|
||||
label: t('menu.leaderResearch')
|
||||
},
|
||||
{
|
||||
key: '/leaders',
|
||||
icon: <UserOutlined />,
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"edit": "Edit",
|
||||
"detail": "Detail",
|
||||
"overview": "Overview",
|
||||
"time": "Time",
|
||||
"viewDetail": "View Details",
|
||||
"delete": "Delete",
|
||||
"add": "Add",
|
||||
@@ -315,6 +318,7 @@
|
||||
"templates": "Templates",
|
||||
"copyTradingConfig": "Copy Trading Config",
|
||||
"leaderPool": "Leader Pool",
|
||||
"leaderResearch": "Leader Research",
|
||||
"cryptoSpreadStrategy": "Crypto Spread Strategy",
|
||||
"cryptoTailStrategy": "Strategy Config",
|
||||
"cryptoTailMonitor": "Real-time Monitor",
|
||||
@@ -623,7 +627,69 @@
|
||||
"goCopyTrading": "View Copy Trading",
|
||||
"removeConfirm": "This only removes the pool item. It will not delete the Leader address or existing copy trading configs. Continue?",
|
||||
"removeSuccess": "Pool item removed",
|
||||
"removeFailed": "Failed to remove pool item"
|
||||
"removeFailed": "Failed to remove pool item",
|
||||
"viewResearch": "View Research"
|
||||
},
|
||||
"leaderResearch": {
|
||||
"title": "Leader Research",
|
||||
"subtitle": "Automatically discover, paper trade, and score strong leaders. Real-money copying still requires your manual approval and enablement.",
|
||||
"safetyTitle": "The research agent never auto-enables real-money copy trading",
|
||||
"safetyDesc": "The agent only advances research state, maintains the paper ledger, and recommends trials. Approval creates a disabled config by default.",
|
||||
"fetchFailed": "Failed to fetch Leader Research data",
|
||||
"runNow": "Run Research",
|
||||
"runStarted": "Research run completed",
|
||||
"runFailed": "Research run failed",
|
||||
"sourceLimitations": "Source Limitations",
|
||||
"sourceHealth": "Source Health",
|
||||
"filterState": "Filter by research state",
|
||||
"searchPlaceholder": "Search wallet or reason",
|
||||
"empty": "No research candidates yet. Configure a watchlist or wait for persisted activity events.",
|
||||
"wallet": "Wallet",
|
||||
"score": "Score",
|
||||
"paper": "Paper",
|
||||
"source": "Source",
|
||||
"lastSeen": "Last Seen",
|
||||
"copyablePnl": "Copyable PnL",
|
||||
"trades": "Trades",
|
||||
"filtered": "Filtered",
|
||||
"candidates": "Candidates",
|
||||
"detailTitle": "Research Detail",
|
||||
"reason": "Reason",
|
||||
"riskFlags": "Risk Flags",
|
||||
"sourceEvidence": "Source Evidence",
|
||||
"scoreBreakdown": "Score Breakdown",
|
||||
"paperTrades": "Paper Trades",
|
||||
"paperPositions": "Paper Positions",
|
||||
"events": "Events",
|
||||
"eventType": "Event Type",
|
||||
"createDisabledTrial": "Create Disabled Trial",
|
||||
"approvalSafetyTitle": "This only creates a disabled config",
|
||||
"approvalSafetyDesc": "You must manually enable it on the copy trading config page before any real-money copy trading happens.",
|
||||
"approvalCreated": "Disabled trial config created",
|
||||
"approvalFailed": "Failed to create disabled trial",
|
||||
"trialReadyHint": "Suggested small trial, awaiting your confirmation",
|
||||
"runStatus": "Run Status",
|
||||
"lastRun": "Last Run",
|
||||
"duration": "Duration",
|
||||
"sourceCounts": "Source Counts",
|
||||
"candidateCounts": "Candidate Counts",
|
||||
"noRuns": "No research runs yet",
|
||||
"pendingDecisions": "Pending Decisions",
|
||||
"noPendingDecisions": "No candidates awaiting approval",
|
||||
"approvalPreview": "Conservative config to be created",
|
||||
"fixedAmount": "Fixed Amount",
|
||||
"maxDailyLoss": "Max Daily Loss",
|
||||
"maxDailyOrders": "Max Daily Orders",
|
||||
"priceRange": "Price Range",
|
||||
"maxPositionValue": "Max Position Value",
|
||||
"states": {
|
||||
"DISCOVERED": "Discovered",
|
||||
"CANDIDATE": "Candidate",
|
||||
"PAPER": "Paper",
|
||||
"TRIAL_READY": "Trial Ready",
|
||||
"COOLDOWN": "Cooldown",
|
||||
"RETIRED": "Retired"
|
||||
}
|
||||
},
|
||||
"leaderAdd": {
|
||||
"title": "Add Leader",
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"later": "稍后",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"detail": "详情",
|
||||
"overview": "概览",
|
||||
"time": "时间",
|
||||
"viewDetail": "查看详情",
|
||||
"add": "添加",
|
||||
"refresh": "刷新",
|
||||
@@ -315,6 +318,7 @@
|
||||
"templates": "跟单模板",
|
||||
"copyTradingConfig": "跟单配置",
|
||||
"leaderPool": "Leader 池",
|
||||
"leaderResearch": "Leader 研究",
|
||||
"cryptoSpreadStrategy": "加密价差策略",
|
||||
"cryptoTailStrategy": "策略配置",
|
||||
"cryptoTailMonitor": "实时监控",
|
||||
@@ -623,7 +627,69 @@
|
||||
"goCopyTrading": "查看跟单配置",
|
||||
"removeConfirm": "只移除池子项,不会删除 Leader 地址或已有跟单配置。确定继续?",
|
||||
"removeSuccess": "池子项已移除",
|
||||
"removeFailed": "移除池子项失败"
|
||||
"removeFailed": "移除池子项失败",
|
||||
"viewResearch": "查看研究"
|
||||
},
|
||||
"leaderResearch": {
|
||||
"title": "Leader 研究",
|
||||
"subtitle": "自动发现、纸上跟单和评分优秀 leader;真钱跟单必须由你手动审批和启用。",
|
||||
"safetyTitle": "研究 Agent 不会自动真钱跟单",
|
||||
"safetyDesc": "Agent 只会推进研究状态、维护纸跟账本、给出试跟建议。审批时也只创建默认禁用的跟单配置。",
|
||||
"fetchFailed": "获取 Leader 研究数据失败",
|
||||
"runNow": "立即运行研究",
|
||||
"runStarted": "研究运行完成",
|
||||
"runFailed": "运行研究失败",
|
||||
"sourceLimitations": "来源限制",
|
||||
"sourceHealth": "来源健康",
|
||||
"filterState": "按研究状态筛选",
|
||||
"searchPlaceholder": "搜索钱包或原因",
|
||||
"empty": "暂无研究候选。可以先配置 watchlist 或等待 activity 事件持久化。",
|
||||
"wallet": "钱包",
|
||||
"score": "评分",
|
||||
"paper": "纸跟",
|
||||
"source": "来源",
|
||||
"lastSeen": "最后发现",
|
||||
"copyablePnl": "可归因 PnL",
|
||||
"trades": "成交",
|
||||
"filtered": "过滤",
|
||||
"candidates": "候选",
|
||||
"detailTitle": "研究详情",
|
||||
"reason": "原因",
|
||||
"riskFlags": "风险标记",
|
||||
"sourceEvidence": "来源证据",
|
||||
"scoreBreakdown": "评分拆解",
|
||||
"paperTrades": "纸跟交易",
|
||||
"paperPositions": "纸跟仓位",
|
||||
"events": "事件",
|
||||
"eventType": "事件类型",
|
||||
"createDisabledTrial": "创建禁用试跟",
|
||||
"approvalSafetyTitle": "只会创建禁用配置",
|
||||
"approvalSafetyDesc": "创建后你需要到跟单配置页手动启用,才会发生真钱跟单。",
|
||||
"approvalCreated": "已创建禁用试跟配置",
|
||||
"approvalFailed": "创建禁用试跟失败",
|
||||
"trialReadyHint": "建议小额试跟,待你确认",
|
||||
"runStatus": "运行状态",
|
||||
"lastRun": "最近运行",
|
||||
"duration": "耗时",
|
||||
"sourceCounts": "来源统计",
|
||||
"candidateCounts": "候选统计",
|
||||
"noRuns": "暂无运行记录",
|
||||
"pendingDecisions": "待处理决策",
|
||||
"noPendingDecisions": "暂无待审批候选",
|
||||
"approvalPreview": "将创建的保守配置",
|
||||
"fixedAmount": "固定金额",
|
||||
"maxDailyLoss": "每日最大亏损",
|
||||
"maxDailyOrders": "每日最大订单数",
|
||||
"priceRange": "价格范围",
|
||||
"maxPositionValue": "最大仓位金额",
|
||||
"states": {
|
||||
"DISCOVERED": "已发现",
|
||||
"CANDIDATE": "候选",
|
||||
"PAPER": "纸跟中",
|
||||
"TRIAL_READY": "建议试跟",
|
||||
"COOLDOWN": "冷却",
|
||||
"RETIRED": "淘汰"
|
||||
}
|
||||
},
|
||||
"leaderAdd": {
|
||||
"title": "添加 Leader",
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"edit": "編輯",
|
||||
"detail": "詳情",
|
||||
"overview": "概覽",
|
||||
"time": "時間",
|
||||
"viewDetail": "查看詳情",
|
||||
"delete": "刪除",
|
||||
"add": "添加",
|
||||
@@ -315,6 +318,7 @@
|
||||
"templates": "跟單模板",
|
||||
"copyTradingConfig": "跟單配置",
|
||||
"leaderPool": "Leader 池",
|
||||
"leaderResearch": "Leader 研究",
|
||||
"cryptoSpreadStrategy": "加密價差策略",
|
||||
"cryptoTailStrategy": "策略配置",
|
||||
"cryptoTailMonitor": "即時監控",
|
||||
@@ -623,7 +627,69 @@
|
||||
"goCopyTrading": "查看跟單配置",
|
||||
"removeConfirm": "只移除池子項,不會刪除 Leader 地址或已有跟單配置。確定繼續?",
|
||||
"removeSuccess": "池子項已移除",
|
||||
"removeFailed": "移除池子項失敗"
|
||||
"removeFailed": "移除池子項失敗",
|
||||
"viewResearch": "查看研究"
|
||||
},
|
||||
"leaderResearch": {
|
||||
"title": "Leader 研究",
|
||||
"subtitle": "自動發現、紙上跟單和評分優秀 leader;真錢跟單必須由你手動審批和啟用。",
|
||||
"safetyTitle": "研究 Agent 不會自動真錢跟單",
|
||||
"safetyDesc": "Agent 只會推進研究狀態、維護紙跟帳本、給出試跟建議。審批時也只建立預設停用的跟單配置。",
|
||||
"fetchFailed": "取得 Leader 研究資料失敗",
|
||||
"runNow": "立即運行研究",
|
||||
"runStarted": "研究運行完成",
|
||||
"runFailed": "運行研究失敗",
|
||||
"sourceLimitations": "來源限制",
|
||||
"sourceHealth": "來源健康",
|
||||
"filterState": "按研究狀態篩選",
|
||||
"searchPlaceholder": "搜尋錢包或原因",
|
||||
"empty": "暫無研究候選。可以先配置 watchlist 或等待 activity 事件持久化。",
|
||||
"wallet": "錢包",
|
||||
"score": "評分",
|
||||
"paper": "紙跟",
|
||||
"source": "來源",
|
||||
"lastSeen": "最後發現",
|
||||
"copyablePnl": "可歸因 PnL",
|
||||
"trades": "成交",
|
||||
"filtered": "過濾",
|
||||
"candidates": "候選",
|
||||
"detailTitle": "研究詳情",
|
||||
"reason": "原因",
|
||||
"riskFlags": "風險標記",
|
||||
"sourceEvidence": "來源證據",
|
||||
"scoreBreakdown": "評分拆解",
|
||||
"paperTrades": "紙跟交易",
|
||||
"paperPositions": "紙跟倉位",
|
||||
"events": "事件",
|
||||
"eventType": "事件類型",
|
||||
"createDisabledTrial": "建立停用試跟",
|
||||
"approvalSafetyTitle": "只會建立停用配置",
|
||||
"approvalSafetyDesc": "建立後你需要到跟單配置頁手動啟用,才會發生真錢跟單。",
|
||||
"approvalCreated": "已建立停用試跟配置",
|
||||
"approvalFailed": "建立停用試跟失敗",
|
||||
"trialReadyHint": "建議小額試跟,待你確認",
|
||||
"runStatus": "運行狀態",
|
||||
"lastRun": "最近運行",
|
||||
"duration": "耗時",
|
||||
"sourceCounts": "來源統計",
|
||||
"candidateCounts": "候選統計",
|
||||
"noRuns": "暫無運行記錄",
|
||||
"pendingDecisions": "待處理決策",
|
||||
"noPendingDecisions": "暫無待審批候選",
|
||||
"approvalPreview": "將建立的保守配置",
|
||||
"fixedAmount": "固定金額",
|
||||
"maxDailyLoss": "每日最大虧損",
|
||||
"maxDailyOrders": "每日最大訂單數",
|
||||
"priceRange": "價格範圍",
|
||||
"maxPositionValue": "最大倉位金額",
|
||||
"states": {
|
||||
"DISCOVERED": "已發現",
|
||||
"CANDIDATE": "候選",
|
||||
"PAPER": "紙跟中",
|
||||
"TRIAL_READY": "建議試跟",
|
||||
"COOLDOWN": "冷卻",
|
||||
"RETIRED": "淘汰"
|
||||
}
|
||||
},
|
||||
"leaderAdd": {
|
||||
"title": "添加 Leader",
|
||||
|
||||
@@ -269,6 +269,17 @@ const LeaderPool: React.FC = () => {
|
||||
<Text copyable style={{ fontSize: 12, fontFamily: 'monospace' }} type="secondary">
|
||||
{item.leaderAddress}
|
||||
</Text>
|
||||
{item.researchBadge && (
|
||||
<Space size={4}>
|
||||
<Tag color={item.researchState === 'TRIAL_READY' ? 'green' : 'blue'}>{t(`leaderResearch.states.${item.researchState}`, { defaultValue: item.researchState })}</Tag>
|
||||
{item.researchScore && <Text type="secondary">{t('leaderResearch.score')}: {item.researchScore}</Text>}
|
||||
</Space>
|
||||
)}
|
||||
{item.researchCandidateId && (
|
||||
<Button type="link" size="small" style={{ padding: 0 }} onClick={() => navigate('/leader-research')}>
|
||||
{t('leaderPool.viewResearch')}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
message
|
||||
} from 'antd'
|
||||
import {
|
||||
ExperimentOutlined,
|
||||
PlayCircleOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined
|
||||
} from '@ant-design/icons'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type {
|
||||
Account,
|
||||
LeaderPaperPosition,
|
||||
LeaderPaperTrade,
|
||||
LeaderResearchCandidate,
|
||||
LeaderResearchCandidateDetail,
|
||||
LeaderResearchCandidateListResponse,
|
||||
LeaderResearchSourceState,
|
||||
LeaderResearchState,
|
||||
LeaderResearchSummary
|
||||
} from '../types'
|
||||
|
||||
const { Paragraph, Text, Title } = Typography
|
||||
|
||||
const STATE_COLORS: Record<LeaderResearchState, string> = {
|
||||
DISCOVERED: 'default',
|
||||
CANDIDATE: 'blue',
|
||||
PAPER: 'geekblue',
|
||||
TRIAL_READY: 'green',
|
||||
COOLDOWN: 'orange',
|
||||
RETIRED: 'red'
|
||||
}
|
||||
|
||||
const VALUATION_COLORS: Record<string, string> = {
|
||||
AVAILABLE: 'green',
|
||||
CONFIRMED_ZERO: 'purple',
|
||||
UNKNOWN: 'orange',
|
||||
UNAVAILABLE: 'red',
|
||||
NO_MATCH: 'volcano'
|
||||
}
|
||||
|
||||
const formatDate = (timestamp?: number) => {
|
||||
if (!timestamp) return '-'
|
||||
return dayjs(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const usdc = (value?: string) => value ? `${value} USDC` : '-'
|
||||
|
||||
const approvalPreview = (candidate?: LeaderResearchCandidate | null) => ({
|
||||
fixedAmount: usdc(candidate?.suggestedFixedAmount),
|
||||
maxDailyLoss: usdc(candidate?.suggestedMaxDailyLoss),
|
||||
maxDailyOrders: candidate?.suggestedMaxDailyOrders ?? '-',
|
||||
priceRange: candidate?.suggestedMinPrice || candidate?.suggestedMaxPrice
|
||||
? `${candidate?.suggestedMinPrice ?? '-'} - ${candidate?.suggestedMaxPrice ?? '-'}`
|
||||
: '-',
|
||||
maxPositionValue: usdc(candidate?.suggestedMaxPositionValue)
|
||||
})
|
||||
|
||||
const valuationTag = (status?: string) => {
|
||||
if (!status) return <Tag>-</Tag>
|
||||
return <Tag color={VALUATION_COLORS[status] || 'default'}>{status}</Tag>
|
||||
}
|
||||
|
||||
const LeaderResearch: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [summary, setSummary] = useState<LeaderResearchSummary | null>(null)
|
||||
const [candidates, setCandidates] = useState<LeaderResearchCandidateListResponse>({ list: [], total: 0, summary: summaryFallback })
|
||||
const [sourceHealth, setSourceHealth] = useState<LeaderResearchSourceState[]>([])
|
||||
const [accounts, setAccounts] = useState<Account[]>([])
|
||||
const [stateFilter, setStateFilter] = useState<LeaderResearchState | undefined>()
|
||||
const [query, setQuery] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [running, setRunning] = useState(false)
|
||||
const [detailLoading, setDetailLoading] = useState(false)
|
||||
const [detail, setDetail] = useState<LeaderResearchCandidateDetail | null>(null)
|
||||
const [approvalCandidate, setApprovalCandidate] = useState<LeaderResearchCandidate | null>(null)
|
||||
const [approvalLoading, setApprovalLoading] = useState(false)
|
||||
const [approvalForm] = Form.useForm()
|
||||
|
||||
const loadAll = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const [candidateResp, summaryResp, sourceResp, accountResp] = await Promise.all([
|
||||
apiService.leaderResearch.listCandidates({ page: 0, size: 50, state: stateFilter, query: query || undefined }),
|
||||
apiService.leaderResearch.summary(),
|
||||
apiService.leaderResearch.sourceHealth(),
|
||||
apiService.accounts.list()
|
||||
])
|
||||
if (candidateResp.data.code === 0 && candidateResp.data.data) {
|
||||
setCandidates(candidateResp.data.data)
|
||||
} else {
|
||||
message.error(candidateResp.data.msg || t('leaderResearch.fetchFailed'))
|
||||
}
|
||||
if (summaryResp.data.code === 0 && summaryResp.data.data) {
|
||||
setSummary(summaryResp.data.data)
|
||||
}
|
||||
if (sourceResp.data.code === 0 && sourceResp.data.data) {
|
||||
setSourceHealth(sourceResp.data.data)
|
||||
}
|
||||
if (accountResp.data.code === 0 && accountResp.data.data) {
|
||||
setAccounts(accountResp.data.data.list || [])
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('leaderResearch.fetchFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadAll()
|
||||
}, [stateFilter])
|
||||
|
||||
const runAgent = async () => {
|
||||
setRunning(true)
|
||||
try {
|
||||
const response = await apiService.leaderResearch.run({ dryRun: false, triggerType: 'MANUAL' })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('leaderResearch.runStarted'))
|
||||
await loadAll()
|
||||
} else {
|
||||
message.error(response.data.msg || t('leaderResearch.runFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('leaderResearch.runFailed'))
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openDetail = async (candidate: LeaderResearchCandidate) => {
|
||||
setDetailLoading(true)
|
||||
try {
|
||||
const response = await apiService.leaderResearch.detail({ candidateId: candidate.id })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setDetail(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || t('leaderResearch.fetchFailed'))
|
||||
}
|
||||
} finally {
|
||||
setDetailLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const openApproval = (candidate: LeaderResearchCandidate) => {
|
||||
setApprovalCandidate(candidate)
|
||||
approvalForm.setFieldsValue({ accountId: accounts[0]?.id })
|
||||
}
|
||||
|
||||
const submitApproval = async () => {
|
||||
if (!approvalCandidate) return
|
||||
const values = await approvalForm.validateFields()
|
||||
setApprovalLoading(true)
|
||||
try {
|
||||
const response = await apiService.leaderResearch.createDisabledTrialConfig({
|
||||
candidateId: approvalCandidate.id,
|
||||
accountId: values.accountId,
|
||||
confirm: true
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('leaderResearch.approvalCreated'))
|
||||
setApprovalCandidate(null)
|
||||
await loadAll()
|
||||
} else {
|
||||
message.error(response.data.msg || t('leaderResearch.approvalFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('leaderResearch.approvalFailed'))
|
||||
} finally {
|
||||
setApprovalLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const activeSummary = summary || candidates.summary || summaryFallback
|
||||
const pendingDecisions = candidates.list.filter(candidate => candidate.researchState === 'TRIAL_READY')
|
||||
const lastRun = activeSummary.lastRun
|
||||
const activeApprovalPreview = approvalPreview(approvalCandidate)
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('leaderResearch.wallet'),
|
||||
key: 'wallet',
|
||||
width: 260,
|
||||
render: (_: unknown, item: LeaderResearchCandidate) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{item.leaderName || item.normalizedWallet.slice(0, 10)}</Text>
|
||||
<Text copyable type="secondary" style={{ fontSize: 12, fontFamily: 'monospace' }}>
|
||||
{item.normalizedWallet}
|
||||
</Text>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('common.status'),
|
||||
dataIndex: 'researchState',
|
||||
width: 130,
|
||||
render: (state: LeaderResearchState) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Tag color={STATE_COLORS[state]}>{t(`leaderResearch.states.${state}`, { defaultValue: state })}</Tag>
|
||||
{state === 'TRIAL_READY' && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{t('leaderResearch.trialReadyHint')}</Text>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('leaderResearch.score'),
|
||||
dataIndex: 'score',
|
||||
width: 100,
|
||||
render: (score?: string) => <Text strong>{score || '-'}</Text>
|
||||
},
|
||||
{
|
||||
title: t('leaderResearch.paper'),
|
||||
key: 'paper',
|
||||
width: 220,
|
||||
render: (_: unknown, item: LeaderResearchCandidate) => {
|
||||
const session = item.latestPaperSession
|
||||
if (!session) return <Text type="secondary">-</Text>
|
||||
return (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text>{t('leaderResearch.copyablePnl')}: {session.copyablePnl}</Text>
|
||||
<Text type="secondary">{t('leaderResearch.trades')}: {session.tradeCount} / {t('leaderResearch.filtered')}: {session.filteredCount}</Text>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('leaderResearch.source'),
|
||||
dataIndex: 'source',
|
||||
width: 160
|
||||
},
|
||||
{
|
||||
title: t('leaderResearch.lastSeen'),
|
||||
dataIndex: 'lastSourceSeenAt',
|
||||
width: 160,
|
||||
render: (value?: number) => formatDate(value)
|
||||
},
|
||||
{
|
||||
title: t('common.actions'),
|
||||
key: 'actions',
|
||||
fixed: 'right' as const,
|
||||
width: 230,
|
||||
render: (_: unknown, item: LeaderResearchCandidate) => (
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => openDetail(item)}>
|
||||
{t('common.detail')}
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
icon={<SafetyCertificateOutlined />}
|
||||
disabled={item.researchState !== 'TRIAL_READY'}
|
||||
onClick={() => openApproval(item)}
|
||||
>
|
||||
{t('leaderResearch.createDisabledTrial')}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
||||
<Card>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Space align="start" style={{ justifyContent: 'space-between', width: '100%' }}>
|
||||
<div>
|
||||
<Title level={3} style={{ marginBottom: 4 }}>{t('leaderResearch.title')}</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>{t('leaderResearch.subtitle')}</Paragraph>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={loadAll}>{t('common.refresh')}</Button>
|
||||
<Button type="primary" icon={<PlayCircleOutlined />} loading={running} onClick={runAgent}>
|
||||
{t('leaderResearch.runNow')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
icon={<ExperimentOutlined />}
|
||||
message={t('leaderResearch.safetyTitle')}
|
||||
description={t('leaderResearch.safetyDesc')}
|
||||
/>
|
||||
{activeSummary.sourceLimitations?.length > 0 && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={t('leaderResearch.sourceLimitations')}
|
||||
description={activeSummary.sourceLimitations.join(' | ')}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} lg={4}><Card><Statistic title={t('leaderResearch.states.DISCOVERED')} value={activeSummary.discoveredCount} /></Card></Col>
|
||||
<Col xs={24} sm={12} lg={4}><Card><Statistic title={t('leaderResearch.states.CANDIDATE')} value={activeSummary.candidateCount} /></Card></Col>
|
||||
<Col xs={24} sm={12} lg={4}><Card><Statistic title={t('leaderResearch.states.PAPER')} value={activeSummary.paperCount} /></Card></Col>
|
||||
<Col xs={24} sm={12} lg={4}><Card><Statistic title={t('leaderResearch.states.TRIAL_READY')} value={activeSummary.trialReadyCount} /></Card></Col>
|
||||
<Col xs={24} sm={12} lg={4}><Card><Statistic title={t('leaderResearch.states.COOLDOWN')} value={activeSummary.cooldownCount} /></Card></Col>
|
||||
<Col xs={24} sm={12} lg={4}><Card><Statistic title={t('leaderResearch.states.RETIRED')} value={activeSummary.retiredCount} /></Card></Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title={t('leaderResearch.runStatus')}>
|
||||
{lastRun ? (
|
||||
<Descriptions size="small" column={1}>
|
||||
<Descriptions.Item label={t('common.status')}>
|
||||
<Tag color={lastRun.partialFailure ? 'orange' : lastRun.status === 'SUCCESS' ? 'green' : 'default'}>{lastRun.status}</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.lastRun')}>{formatDate(lastRun.startedAt)}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.duration')}>{lastRun.durationMs ?? '-'} ms</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.sourceCounts')}>{lastRun.sourceCountsJson || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.candidateCounts')}>{lastRun.candidateCountsJson || '-'}</Descriptions.Item>
|
||||
{(lastRun.errorMessage || lastRun.skippedReason) && (
|
||||
<Descriptions.Item label={t('leaderResearch.reason')}>{lastRun.errorMessage || lastRun.skippedReason}</Descriptions.Item>
|
||||
)}
|
||||
</Descriptions>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('leaderResearch.noRuns')} />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} lg={12}>
|
||||
<Card title={t('leaderResearch.pendingDecisions')}>
|
||||
{pendingDecisions.length > 0 ? (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{pendingDecisions.slice(0, 5).map(candidate => (
|
||||
<Card key={candidate.id} size="small">
|
||||
<Space style={{ justifyContent: 'space-between', width: '100%' }} wrap>
|
||||
<Space direction="vertical" size={0}>
|
||||
<Text strong>{candidate.leaderName || candidate.normalizedWallet.slice(0, 10)}</Text>
|
||||
<Text type="secondary">{t('leaderResearch.trialReadyHint')}</Text>
|
||||
</Space>
|
||||
<Button size="small" type="primary" loading={approvalLoading && approvalCandidate?.id === candidate.id} onClick={() => openApproval(candidate)}>
|
||||
{t('leaderResearch.createDisabledTrial')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
) : (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('leaderResearch.noPendingDecisions')} />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Space wrap>
|
||||
<Select
|
||||
allowClear
|
||||
style={{ width: 220 }}
|
||||
placeholder={t('leaderResearch.filterState')}
|
||||
value={stateFilter}
|
||||
onChange={setStateFilter}
|
||||
options={Object.keys(STATE_COLORS).map(state => ({
|
||||
value: state,
|
||||
label: t(`leaderResearch.states.${state}`, { defaultValue: state })
|
||||
}))}
|
||||
/>
|
||||
<Input.Search
|
||||
allowClear
|
||||
style={{ width: 320 }}
|
||||
placeholder={t('leaderResearch.searchPlaceholder')}
|
||||
value={query}
|
||||
onChange={event => setQuery(event.target.value)}
|
||||
onSearch={loadAll}
|
||||
/>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={candidates.list}
|
||||
scroll={{ x: 1300 }}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('leaderResearch.empty')} /> }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title={t('leaderResearch.sourceHealth')}>
|
||||
<Row gutter={[12, 12]}>
|
||||
{sourceHealth.map(source => (
|
||||
<Col xs={24} md={12} lg={6} key={source.sourceType}>
|
||||
<Card size="small">
|
||||
<Space direction="vertical" size={4}>
|
||||
<Badge status={source.status === 'SUCCESS' ? 'success' : source.status === 'DISABLED' ? 'default' : 'warning'} text={source.sourceType} />
|
||||
<Tag>{source.status}</Tag>
|
||||
<Text type="secondary">{t('leaderResearch.candidates')}: {source.lastCandidateCount}</Text>
|
||||
<Text type="secondary">{formatDate(source.lastRunAt)}</Text>
|
||||
{(source.disabledReason || source.errorMessage) && <Text type="secondary">{source.disabledReason || source.errorMessage}</Text>}
|
||||
</Space>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
width={880}
|
||||
open={!!detail}
|
||||
title={t('leaderResearch.detailTitle')}
|
||||
onClose={() => setDetail(null)}
|
||||
loading={detailLoading}
|
||||
>
|
||||
{detail && (
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'overview',
|
||||
label: t('common.overview'),
|
||||
children: (
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Descriptions bordered column={1} size="small">
|
||||
<Descriptions.Item label={t('leaderResearch.wallet')}>{detail.candidate.normalizedWallet}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('common.status')}>{detail.candidate.researchState}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.score')}>{detail.candidate.score || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.reason')}>{detail.candidate.reason || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.riskFlags')}>{detail.candidate.riskFlags.join(', ') || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.sourceEvidence')}>{detail.candidate.sourceEvidence || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{detail.latestScore && (
|
||||
<Descriptions bordered size="small" column={2} title={t('leaderResearch.scoreBreakdown')}>
|
||||
<Descriptions.Item label="profit">{detail.latestScore.profitSignal}</Descriptions.Item>
|
||||
<Descriptions.Item label="repeatability">{detail.latestScore.repeatability}</Descriptions.Item>
|
||||
<Descriptions.Item label="liquidity">{detail.latestScore.liquidityFit}</Descriptions.Item>
|
||||
<Descriptions.Item label="entry">{detail.latestScore.entryPriceFit}</Descriptions.Item>
|
||||
<Descriptions.Item label="slippage">{detail.latestScore.slippageRisk}</Descriptions.Item>
|
||||
<Descriptions.Item label="drawdown">{detail.latestScore.drawdownRisk}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'trades',
|
||||
label: t('leaderResearch.paperTrades'),
|
||||
children: <PaperTradeTable trades={detail.paperTrades} />
|
||||
},
|
||||
{
|
||||
key: 'positions',
|
||||
label: t('leaderResearch.paperPositions'),
|
||||
children: <PaperPositionTable positions={detail.paperPositions} />
|
||||
},
|
||||
{
|
||||
key: 'events',
|
||||
label: t('leaderResearch.events'),
|
||||
children: (
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={detail.events}
|
||||
columns={[
|
||||
{ title: t('common.time'), dataIndex: 'createdAt', render: formatDate },
|
||||
{ title: t('leaderResearch.eventType'), dataIndex: 'eventType' },
|
||||
{ title: t('leaderResearch.reason'), dataIndex: 'reason' }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
open={!!approvalCandidate}
|
||||
title={t('leaderResearch.createDisabledTrial')}
|
||||
onCancel={() => setApprovalCandidate(null)}
|
||||
onOk={submitApproval}
|
||||
confirmLoading={approvalLoading}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={t('leaderResearch.approvalSafetyTitle')}
|
||||
description={t('leaderResearch.approvalSafetyDesc')}
|
||||
/>
|
||||
<Form form={approvalForm} layout="vertical">
|
||||
<Descriptions bordered size="small" column={1} title={t('leaderResearch.approvalPreview')}>
|
||||
<Descriptions.Item label={t('leaderResearch.fixedAmount')}>{activeApprovalPreview.fixedAmount}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.maxDailyLoss')}>{activeApprovalPreview.maxDailyLoss}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.maxDailyOrders')}>{activeApprovalPreview.maxDailyOrders}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.priceRange')}>{activeApprovalPreview.priceRange}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('leaderResearch.maxPositionValue')}>{activeApprovalPreview.maxPositionValue}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Form.Item name="accountId" label={t('leaderPool.account')} rules={[{ required: true, message: t('leaderPool.selectAccount') }]}>
|
||||
<Select
|
||||
options={accounts.map(account => ({
|
||||
value: account.id,
|
||||
label: `${account.accountName || account.walletAddress} (${account.proxyAddress?.slice(0, 8)}...)`
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Modal>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
|
||||
const PaperTradeTable: React.FC<{ trades: LeaderPaperTrade[] }> = ({ trades }) => (
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={trades}
|
||||
columns={[
|
||||
{ title: 'Time', dataIndex: 'eventTime', render: formatDate },
|
||||
{ title: 'Side', dataIndex: 'side' },
|
||||
{ title: 'Market', dataIndex: 'marketTitle', render: (value?: string, item?: LeaderPaperTrade) => value || item?.marketId },
|
||||
{ title: 'Leader Price', dataIndex: 'leaderPrice' },
|
||||
{ title: 'Sim Amount', dataIndex: 'simulatedAmount' },
|
||||
{ title: 'Filter', dataIndex: 'filterResult' },
|
||||
{ title: 'Quote', dataIndex: 'quoteConfidence' },
|
||||
{ title: 'Valuation', dataIndex: 'valuationStatus', render: valuationTag }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
const PaperPositionTable: React.FC<{ positions: LeaderPaperPosition[] }> = ({ positions }) => (
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={positions}
|
||||
columns={[
|
||||
{ title: 'Market', dataIndex: 'marketId' },
|
||||
{ title: 'Outcome', dataIndex: 'outcome' },
|
||||
{ title: 'Qty', dataIndex: 'quantity' },
|
||||
{ title: 'Cost', dataIndex: 'cost' },
|
||||
{ title: 'Value', dataIndex: 'currentValue' },
|
||||
{ title: 'PnL', dataIndex: 'unrealizedPnl' },
|
||||
{ title: 'Quote', dataIndex: 'quoteConfidence' },
|
||||
{ title: 'Valuation', dataIndex: 'valuationStatus', render: valuationTag }
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
const summaryFallback: LeaderResearchSummary = {
|
||||
discoveredCount: 0,
|
||||
candidateCount: 0,
|
||||
paperCount: 0,
|
||||
trialReadyCount: 0,
|
||||
cooldownCount: 0,
|
||||
retiredCount: 0,
|
||||
activePaperSessions: 0,
|
||||
pendingRiskCount: 0,
|
||||
sourceLimitations: []
|
||||
}
|
||||
|
||||
export default LeaderResearch
|
||||
@@ -8,6 +8,16 @@ import type {
|
||||
LeaderPoolListResponse,
|
||||
LeaderPoolUpdatePlanRequest,
|
||||
LeaderPoolUpdateStatusRequest,
|
||||
LeaderResearchApprovalRequest,
|
||||
LeaderResearchApprovalResponse,
|
||||
LeaderResearchCandidateDetail,
|
||||
LeaderResearchCandidateListRequest,
|
||||
LeaderResearchCandidateListResponse,
|
||||
LeaderResearchEvent,
|
||||
LeaderResearchRun,
|
||||
LeaderResearchRunRequest,
|
||||
LeaderResearchSourceState,
|
||||
LeaderResearchSummary,
|
||||
NotificationConfig,
|
||||
NotificationConfigRequest,
|
||||
NotificationConfigUpdateRequest,
|
||||
@@ -396,6 +406,29 @@ export const apiService = {
|
||||
remove: (data: { poolId: number }) =>
|
||||
apiClient.post<ApiResponse<void>>('/copy-trading/leader-pool/remove', data)
|
||||
},
|
||||
|
||||
leaderResearch: {
|
||||
run: (data: LeaderResearchRunRequest = {}) =>
|
||||
apiClient.post<ApiResponse<LeaderResearchRun>>('/copy-trading/leader-research/run', data),
|
||||
|
||||
summary: () =>
|
||||
apiClient.post<ApiResponse<LeaderResearchSummary>>('/copy-trading/leader-research/summary', {}),
|
||||
|
||||
listCandidates: (data: LeaderResearchCandidateListRequest = {}) =>
|
||||
apiClient.post<ApiResponse<LeaderResearchCandidateListResponse>>('/copy-trading/leader-research/candidates/list', data),
|
||||
|
||||
detail: (data: { candidateId: number }) =>
|
||||
apiClient.post<ApiResponse<LeaderResearchCandidateDetail>>('/copy-trading/leader-research/candidates/detail', data),
|
||||
|
||||
sourceHealth: () =>
|
||||
apiClient.post<ApiResponse<LeaderResearchSourceState[]>>('/copy-trading/leader-research/source-health', {}),
|
||||
|
||||
events: (data: { page?: number; size?: number } = {}) =>
|
||||
apiClient.post<ApiResponse<LeaderResearchEvent[]>>('/copy-trading/leader-research/events/list', data),
|
||||
|
||||
createDisabledTrialConfig: (data: LeaderResearchApprovalRequest) =>
|
||||
apiClient.post<ApiResponse<LeaderResearchApprovalResponse>>('/copy-trading/leader-research/approval/create-disabled-trial-config', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* 跟单模板管理 API(子菜单:跟单模板)
|
||||
|
||||
@@ -344,6 +344,12 @@ export interface LeaderPoolItem {
|
||||
lastPromotedAt?: number
|
||||
cooldownUntil?: number
|
||||
locked: boolean
|
||||
researchCandidateId?: number
|
||||
researchState?: LeaderResearchState
|
||||
researchBadge?: string
|
||||
researchSummary?: string
|
||||
researchScore?: string
|
||||
researchUpdatedAt?: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
@@ -391,6 +397,231 @@ export interface LeaderPoolCreateTrialConfigRequest {
|
||||
confirm?: boolean
|
||||
}
|
||||
|
||||
export type LeaderResearchState = 'DISCOVERED' | 'CANDIDATE' | 'PAPER' | 'TRIAL_READY' | 'COOLDOWN' | 'RETIRED'
|
||||
|
||||
export interface LeaderResearchRunRequest {
|
||||
dryRun?: boolean
|
||||
triggerType?: 'MANUAL' | 'SCHEDULED' | 'PREVIEW'
|
||||
}
|
||||
|
||||
export interface LeaderResearchRun {
|
||||
id: number
|
||||
status: string
|
||||
triggerType: string
|
||||
dryRun: boolean
|
||||
startedAt: number
|
||||
finishedAt?: number
|
||||
durationMs?: number
|
||||
sourceCountsJson?: string
|
||||
candidateCountsJson?: string
|
||||
partialFailure: boolean
|
||||
skippedReason?: string
|
||||
errorClass?: string
|
||||
errorMessage?: string
|
||||
}
|
||||
|
||||
export interface LeaderResearchSummary {
|
||||
discoveredCount: number
|
||||
candidateCount: number
|
||||
paperCount: number
|
||||
trialReadyCount: number
|
||||
cooldownCount: number
|
||||
retiredCount: number
|
||||
activePaperSessions: number
|
||||
pendingRiskCount: number
|
||||
lastRun?: LeaderResearchRun
|
||||
sourceLimitations: string[]
|
||||
}
|
||||
|
||||
export interface LeaderResearchCandidateListRequest {
|
||||
page?: number
|
||||
size?: number
|
||||
state?: LeaderResearchState
|
||||
query?: string
|
||||
}
|
||||
|
||||
export interface LeaderResearchCandidateListResponse {
|
||||
list: LeaderResearchCandidate[]
|
||||
total: number
|
||||
summary: LeaderResearchSummary
|
||||
}
|
||||
|
||||
export interface LeaderResearchCandidate {
|
||||
id: number
|
||||
normalizedWallet: string
|
||||
leaderId?: number
|
||||
leaderName?: string
|
||||
poolId?: number
|
||||
poolStatus?: string
|
||||
suggestedFixedAmount?: string
|
||||
suggestedMaxDailyLoss?: string
|
||||
suggestedMaxDailyOrders?: number
|
||||
suggestedMinPrice?: string
|
||||
suggestedMaxPrice?: string
|
||||
suggestedMaxPositionValue?: string
|
||||
researchState: LeaderResearchState
|
||||
source: string
|
||||
sourceRank?: number
|
||||
score?: string
|
||||
scoreVersion?: string
|
||||
reason?: string
|
||||
riskFlags: string[]
|
||||
locked: boolean
|
||||
agentOwned: boolean
|
||||
provenance: string
|
||||
sourceEvidence?: string
|
||||
firstSeenAt: number
|
||||
lastSourceSeenAt?: number
|
||||
lastScoredAt?: number
|
||||
cooldownUntil?: number
|
||||
cooldownCount: number
|
||||
trialReadyAt?: number
|
||||
retiredAt?: number
|
||||
lastPaperSessionId?: number
|
||||
latestPaperSession?: LeaderPaperSession
|
||||
}
|
||||
|
||||
export interface LeaderResearchCandidateDetail {
|
||||
candidate: LeaderResearchCandidate
|
||||
latestScore?: LeaderResearchScore
|
||||
paperSessions: LeaderPaperSession[]
|
||||
paperTrades: LeaderPaperTrade[]
|
||||
paperPositions: LeaderPaperPosition[]
|
||||
events: LeaderResearchEvent[]
|
||||
}
|
||||
|
||||
export interface LeaderResearchScore {
|
||||
id: number
|
||||
candidateId: number
|
||||
runId?: number
|
||||
scoreVersion: string
|
||||
totalScore: string
|
||||
profitSignal: string
|
||||
repeatability: string
|
||||
liquidityFit: string
|
||||
entryPriceFit: string
|
||||
slippageRisk: string
|
||||
holdingPeriodFit: string
|
||||
marketTypeRisk: string
|
||||
drawdownRisk: string
|
||||
exitLiquidityRisk: string
|
||||
dataFreshness: string
|
||||
filterPassRate: string
|
||||
sampleTradeCount: number
|
||||
reason?: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export interface LeaderPaperSession {
|
||||
id: number
|
||||
candidateId: number
|
||||
status: string
|
||||
startedAt: number
|
||||
endedAt?: number
|
||||
tradeCount: number
|
||||
filteredCount: number
|
||||
openExposure: string
|
||||
totalRealizedPnl: string
|
||||
totalUnrealizedPnl: string
|
||||
copyablePnl: string
|
||||
maxDrawdown: string
|
||||
unknownValuationExposure: string
|
||||
confirmedZeroExposure: string
|
||||
filteredRatio: string
|
||||
lastProcessedEventTime?: number
|
||||
scoreSnapshot?: string
|
||||
}
|
||||
|
||||
export interface LeaderPaperTrade {
|
||||
id: number
|
||||
sessionId: number
|
||||
candidateId: number
|
||||
activityEventId?: number
|
||||
leaderTradeId: string
|
||||
marketId: string
|
||||
marketTitle?: string
|
||||
marketSlug?: string
|
||||
side: string
|
||||
outcome?: string
|
||||
outcomeIndex?: number
|
||||
leaderPrice?: string
|
||||
leaderSize?: string
|
||||
simulatedPrice?: string
|
||||
simulatedSize?: string
|
||||
simulatedAmount?: string
|
||||
fillAssumption: string
|
||||
quoteConfidence: string
|
||||
quoteSource?: string
|
||||
quoteTimestamp?: number
|
||||
filterResult: string
|
||||
filterReason?: string
|
||||
valuationStatus: string
|
||||
realizedPnl?: string
|
||||
eventTime: number
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export interface LeaderPaperPosition {
|
||||
id: number
|
||||
sessionId: number
|
||||
candidateId: number
|
||||
marketId: string
|
||||
outcome?: string
|
||||
outcomeIndex?: number
|
||||
quantity: string
|
||||
cost: string
|
||||
avgPrice: string
|
||||
currentPrice?: string
|
||||
currentValue: string
|
||||
realizedPnl: string
|
||||
unrealizedPnl: string
|
||||
valuationStatus: string
|
||||
quoteConfidence: string
|
||||
quoteSource?: string
|
||||
quoteTimestamp?: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface LeaderResearchSourceState {
|
||||
sourceType: string
|
||||
status: string
|
||||
lastSuccessAt?: number
|
||||
lastFailureAt?: number
|
||||
lastRunAt?: number
|
||||
lastCandidateCount: number
|
||||
errorClass?: string
|
||||
errorMessage?: string
|
||||
stale: boolean
|
||||
disabledReason?: string
|
||||
lastCursor?: string
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface LeaderResearchEvent {
|
||||
id: number
|
||||
candidateId?: number
|
||||
runId?: number
|
||||
eventType: string
|
||||
reason?: string
|
||||
payloadSummary?: string
|
||||
notificationStatus: string
|
||||
notificationError?: string
|
||||
dedupeKey?: string
|
||||
createdAt: number
|
||||
notifiedAt?: number
|
||||
}
|
||||
|
||||
export interface LeaderResearchApprovalRequest {
|
||||
candidateId: number
|
||||
accountId: number
|
||||
confirm?: boolean
|
||||
}
|
||||
|
||||
export interface LeaderResearchApprovalResponse {
|
||||
copyTrading: CopyTrading
|
||||
warning: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟单创建请求
|
||||
* 所有配置参数都需要手动输入,模板仅用于前端快速填充表单
|
||||
|
||||
Reference in New Issue
Block a user