feat: add copy trading safety and leader pool

This commit is contained in:
codyhhchen
2026-05-04 09:53:07 +08:00
parent 09f6e5346d
commit a7bfe660fc
64 changed files with 7006 additions and 29 deletions
+2 -1
View File
@@ -14,6 +14,7 @@ import AccountImport from './pages/AccountImport'
import AccountDetail from './pages/AccountDetail'
import AccountEdit from './pages/AccountEdit'
import LeaderList from './pages/LeaderList'
import LeaderPool from './pages/LeaderPool'
import LeaderAdd from './pages/LeaderAdd'
import LeaderEdit from './pages/LeaderEdit'
import ConfigPage from './pages/ConfigPage'
@@ -265,6 +266,7 @@ function App() {
<Route path="/accounts/detail" element={<ProtectedRoute><AccountDetail /></ProtectedRoute>} />
<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="/leaders/add" element={<ProtectedRoute><LeaderAdd /></ProtectedRoute>} />
<Route path="/leaders/edit" element={<ProtectedRoute><LeaderEdit /></ProtectedRoute>} />
<Route path="/templates" element={<ProtectedRoute><TemplateList /></ProtectedRoute>} />
@@ -300,4 +302,3 @@ function App() {
}
export default App
@@ -0,0 +1,220 @@
import { Alert, Button, Card, Col, Modal, Row, Space, Statistic, Table, Tag, message } from 'antd'
import { useState } from 'react'
import { apiService } from '../services/api'
import type { CopyTradingStatistics } from '../types'
import { formatUSDC } from '../utils'
interface Props {
statistics: CopyTradingStatistics
onApplied?: () => void
compact?: boolean
}
const fieldLabels: Record<string, string> = {
maxDailyOrders: '每日最大订单数',
maxDailyLoss: '每日最大亏损',
minPrice: '最低价格',
maxPrice: '最高价格',
maxPositionValue: '单市场最大仓位',
minOrderDepth: '最小深度',
maxSpread: '最大价差',
priceTolerance: '价格容忍度'
}
const statusText: Record<string, string> = {
AVAILABLE: '报价可用',
NO_MATCH: '有持仓未匹配到报价',
UNAVAILABLE: '报价不可用'
}
const statusColor: Record<string, string> = {
AVAILABLE: 'green',
NO_MATCH: 'orange',
UNAVAILABLE: 'red'
}
type ApplyConservativeConfigPayload = Parameters<typeof apiService.safetyConfig.applyConservative>[0]
const CopyTradingRiskSeatbeltPanel: React.FC<Props> = ({ statistics, onApplied, compact = false }) => {
const diagnosis = statistics.riskDiagnosis
const [applying, setApplying] = useState(false)
if (!diagnosis) {
return (
<Card title="风险安全带" style={{ marginTop: 16 }}>
<Alert type="info" showIcon message="暂无诊断数据" description="旧接口仍可展示基础统计,诊断数据生成后这里会显示亏损归因和风控建议。" />
</Card>
)
}
const riskWarnings = diagnosis.riskWarnings || []
const dangerousWarnings = riskWarnings.filter(item => item.severity === 'HIGH' || item.severity === 'MEDIUM')
const buildApplyPayload = (): ApplyConservativeConfigPayload => {
const payload: ApplyConservativeConfigPayload = {
copyTradingId: statistics.copyTradingId,
confirm: true
}
riskWarnings.forEach(item => {
if (item.suggestedValue === undefined || item.suggestedValue === null) {
return
}
switch (item.field) {
case 'maxDailyOrders':
payload.maxDailyOrders = Number(item.suggestedValue)
break
case 'maxDailyLoss':
payload.maxDailyLoss = item.suggestedValue
break
case 'minPrice':
payload.minPrice = item.suggestedValue
break
case 'maxPrice':
payload.maxPrice = item.suggestedValue
break
case 'maxPositionValue':
payload.maxPositionValue = item.suggestedValue
break
case 'minOrderDepth':
payload.minOrderDepth = item.suggestedValue
break
case 'maxSpread':
payload.maxSpread = item.suggestedValue
break
case 'priceTolerance':
payload.priceTolerance = item.suggestedValue
break
}
})
return payload
}
const applyConservativeConfig = () => {
if (dangerousWarnings.length === 0) {
message.info('当前配置已经比较保守,无需应用安全带建议')
return
}
Modal.confirm({
title: '确认应用保守配置?',
content: (
<div>
<p>/ leader</p>
<Table
size="small"
pagination={false}
rowKey="field"
dataSource={dangerousWarnings}
columns={[
{ title: '字段', dataIndex: 'field', render: (field: string) => fieldLabels[field] || field },
{ title: '当前值', dataIndex: 'currentValue', render: (value: string | null) => value ?? '未设置' },
{ title: '建议值', dataIndex: 'suggestedValue' }
]}
/>
</div>
),
okText: '确认应用',
cancelText: '取消',
onOk: async () => {
setApplying(true)
try {
const response = await apiService.safetyConfig.applyConservative(buildApplyPayload())
if (response.data.code === 0) {
message.success('已应用保守配置')
onApplied?.()
} else {
message.error(response.data.msg || '应用保守配置失败')
}
} finally {
setApplying(false)
}
}
})
}
return (
<Card title="亏损归因与风险安全带" style={{ marginTop: compact ? 12 : 16 }}>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{diagnosis.dataIncomplete && (
<Alert
type="warning"
showIcon
message="诊断数据不完整"
description={`缺失来源:${diagnosis.missingSources.join('、') || '未知'}。系统不会把未知估值当作已确认归零。`}
/>
)}
{diagnosis.lowConfidence && (
<Alert type="info" showIcon message="低置信度" description={diagnosis.confidenceReason} />
)}
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} md={6}>
<Statistic title="归零/未知持仓成本" value={formatUSDC(diagnosis.zeroValuePositionCost)} prefix="$" />
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic title="已确认归零成本" value={formatUSDC(diagnosis.confirmedZeroValuePositionCost)} prefix="$" />
</Col>
<Col xs={24} sm={12} md={6}>
<Statistic title="卖出归零亏损" value={formatUSDC(diagnosis.zeroSellLoss)} prefix="$" />
</Col>
<Col xs={24} sm={12} md={6}>
<div style={{ color: '#999', fontSize: 14, marginBottom: 4 }}></div>
<Tag color={statusColor[diagnosis.quoteOverallStatus] || 'default'}>
{statusText[diagnosis.quoteOverallStatus] || diagnosis.quoteOverallStatus}
</Tag>
<div style={{ color: '#999', marginTop: 8, fontSize: 12 }}>
{diagnosis.quoteAvailableCount} {diagnosis.quoteNoMatchCount} {diagnosis.quoteUnavailableCount}
</div>
</Col>
</Row>
<div>
<h4 style={{ marginBottom: 8 }}></h4>
{diagnosis.topLosingMarkets.length === 0 ? (
<Alert type="success" showIcon message="暂无已实现亏损市场" />
) : (
<Table
size="small"
pagination={false}
rowKey="marketId"
dataSource={diagnosis.topLosingMarkets}
columns={[
{ title: '市场', dataIndex: 'marketId' },
{ title: '已实现 PnL', dataIndex: 'realizedPnl', render: (value: string) => `$${formatUSDC(value)}` },
{ title: '匹配数', dataIndex: 'matchedOrders' }
]}
/>
)}
</div>
<div>
<h4 style={{ marginBottom: 8 }}></h4>
{riskWarnings.length === 0 ? (
<Alert type="success" showIcon message="当前配置已经比较保守" />
) : (
<Table
size="small"
pagination={false}
rowKey="field"
dataSource={riskWarnings}
columns={[
{ title: '字段', dataIndex: 'field', render: (field: string) => fieldLabels[field] || field },
{ title: '当前值', dataIndex: 'currentValue', render: (value: string | null) => value ?? '未设置' },
{ title: '建议值', dataIndex: 'suggestedValue' },
{ title: '级别', dataIndex: 'severity', render: (severity: string) => <Tag color={severity === 'HIGH' ? 'red' : 'orange'}>{severity}</Tag> },
{ title: '原因', dataIndex: 'reason' }
]}
/>
)}
</div>
<Button type="primary" danger disabled={dangerousWarnings.length === 0} loading={applying} onClick={applyConservativeConfig}>
</Button>
</Space>
</Card>
)
}
export default CopyTradingRiskSeatbeltPanel
+7 -3
View File
@@ -74,7 +74,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
const getInitialOpenKeys = (): string[] => {
const path = location.pathname
const keys: string[] = []
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
if (path.startsWith('/leaders') || path.startsWith('/leader-pool') || 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 +92,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
useEffect(() => {
const path = location.pathname
const keys: string[] = []
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading') || path.startsWith('/backtest')) {
if (path.startsWith('/leaders') || path.startsWith('/leader-pool') || 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')) {
@@ -148,6 +148,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
icon: <LinkOutlined />,
label: t('menu.copyTradingConfig')
},
{
key: '/leader-pool',
icon: <TeamOutlined />,
label: t('menu.leaderPool')
},
{
key: '/leaders',
icon: <UserOutlined />,
@@ -504,4 +509,3 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
}
export default Layout
+64 -2
View File
@@ -314,6 +314,7 @@
"leaders": "Leader Management",
"templates": "Templates",
"copyTradingConfig": "Copy Trading Config",
"leaderPool": "Leader Pool",
"cryptoSpreadStrategy": "Crypto Spread Strategy",
"cryptoTailStrategy": "Strategy Config",
"cryptoTailMonitor": "Real-time Monitor",
@@ -552,7 +553,68 @@
"deleteConfirmDesc": "This Leader has {{count}} copy trading relations, please delete them first",
"openDetailFailed": "Failed to open detail",
"noCopyTradings": "No copy trading configs",
"noBacktests": "No backtests"
"noBacktests": "No backtests",
"addToPool": "Add to Leader Pool",
"addToPoolSuccess": "Added to Leader Pool",
"addToPoolExists": "This Leader is already in the pool",
"addToPoolFailed": "Failed to add to Leader Pool",
"goLeaderPool": "Go to Leader Pool"
},
"leaderPool": {
"title": "Leader Pool",
"subtitle": "Separate candidate, watch, trial, cooldown, and retired decisions from real copy trading configs.",
"safetyHint": "Leader Pool will not auto-scale positions or auto-enable large copy trading.",
"safetyDesc": "Trial configs created from the pool use conservative defaults and are disabled by default.",
"fetchFailed": "Failed to fetch Leader Pool",
"selectLeaderPlaceholder": "Select an existing Leader to add",
"selectLeaderFirst": "Please select a Leader first",
"addExistingLeader": "Add to Pool",
"addSuccess": "Added to Leader Pool",
"addFailed": "Failed to add to Leader Pool",
"goLeaders": "Go to Leader Management",
"noLeaders": "No Leaders yet",
"totalCount": "Pool Size",
"trialCount": "In Trial",
"estimatedWorstExposure": "Worst Exposure",
"pendingRiskCount": "Pending Risks",
"filterStatus": "Filter by status",
"emptyDesc": "The pool is empty. Add leaders from Leader Management or select an existing Leader here.",
"leader": "Leader",
"source": "Source",
"suggestedConfig": "Suggested Config",
"fixedAmount": "Fixed Amount",
"maxDailyOrders": "Max Daily Orders",
"maxDailyLoss": "Max Daily Loss",
"minPrice": "Min Price",
"maxPrice": "Max Price",
"maxPositionValue": "Max Position",
"priceRange": "Price Range",
"copyTradingState": "Copy Trading State",
"configs": "configs",
"hasEnabled": "Has enabled config",
"noEnabled": "No enabled config",
"lastReviewedAt": "Last Reviewed",
"updateStatus": "Update Status",
"statusUpdated": "Status updated",
"statusUpdateFailed": "Failed to update status",
"cooldownUntil": "Cooldown Until",
"locked": "Locked",
"editPlan": "Edit Plan",
"planUpdated": "Plan updated",
"planUpdateFailed": "Failed to update plan",
"notes": "Notes",
"createTrial": "Create Trial Config",
"trialConfirmTitle": "This creates a disabled FIXED small trial config",
"trialConfirmDesc": "Confirm fixed amount, max daily orders, max daily loss, price range, and max position before submitting.",
"account": "Account",
"selectAccount": "Please select an account",
"noAccounts": "No accounts yet",
"trialCreated": "Trial config created",
"trialCreateFailed": "Failed to create trial config",
"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"
},
"leaderAdd": {
"title": "Add Leader",
@@ -1811,4 +1873,4 @@
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "Got it"
}
}
}
+64 -2
View File
@@ -314,6 +314,7 @@
"leaders": "Leader 管理",
"templates": "跟单模板",
"copyTradingConfig": "跟单配置",
"leaderPool": "Leader 池",
"cryptoSpreadStrategy": "加密价差策略",
"cryptoTailStrategy": "策略配置",
"cryptoTailMonitor": "实时监控",
@@ -552,7 +553,68 @@
"deleteConfirmDesc": "该 Leader 还有 {{count}} 个跟单关系,请先删除跟单关系",
"openDetailFailed": "打开详情失败",
"noCopyTradings": "暂无跟单配置",
"noBacktests": "暂无回测"
"noBacktests": "暂无回测",
"addToPool": "加入 Leader 池",
"addToPoolSuccess": "已加入 Leader 池",
"addToPoolExists": "该 Leader 已在池子中",
"addToPoolFailed": "加入 Leader 池失败",
"goLeaderPool": "前往 Leader 池"
},
"leaderPool": {
"title": "Leader 池",
"subtitle": "把候选、观察、小额试跟、冷却和淘汰从真实跟单配置里拆出来,先决策,再下真钱。",
"safetyHint": "Leader 池不会自动加仓,也不会自动启用大额跟单。",
"safetyDesc": "从池子创建试跟配置时会使用保守默认值,并默认禁用。你可以之后在跟单配置页手动启用。",
"fetchFailed": "获取 Leader 池失败",
"selectLeaderPlaceholder": "选择已有 Leader 加入池子",
"selectLeaderFirst": "请先选择一个 Leader",
"addExistingLeader": "加入池子",
"addSuccess": "加入 Leader 池成功",
"addFailed": "加入 Leader 池失败",
"goLeaders": "去 Leader 管理",
"noLeaders": "暂无 Leader,请先添加",
"totalCount": "池子人数",
"trialCount": "试跟中人数",
"estimatedWorstExposure": "估算最坏暴露",
"pendingRiskCount": "待处理风险",
"filterStatus": "按状态筛选",
"emptyDesc": "池子还是空的,可以从 Leader 管理加入 leader,或在本页选择已有 leader 加入。",
"leader": "Leader",
"source": "来源",
"suggestedConfig": "建议配置",
"fixedAmount": "固定金额",
"maxDailyOrders": "每日最大单数",
"maxDailyLoss": "每日最大亏损",
"minPrice": "最低价格",
"maxPrice": "最高价格",
"maxPositionValue": "最大持仓",
"priceRange": "价格区间",
"copyTradingState": "跟单配置状态",
"configs": "个配置",
"hasEnabled": "存在启用配置",
"noEnabled": "无启用配置",
"lastReviewedAt": "最后复核",
"updateStatus": "更新状态",
"statusUpdated": "状态已更新",
"statusUpdateFailed": "状态更新失败",
"cooldownUntil": "冷却截止时间",
"locked": "锁定",
"editPlan": "编辑建议",
"planUpdated": "建议配置已更新",
"planUpdateFailed": "建议配置更新失败",
"notes": "备注",
"createTrial": "创建试跟配置",
"trialConfirmTitle": "将创建默认禁用的小额 FIXED 跟单配置",
"trialConfirmDesc": "提交前请确认固定金额、每日最大单数、每日最大亏损、价格区间和最大持仓。",
"account": "账户",
"selectAccount": "请选择账户",
"noAccounts": "暂无账户,请先添加账户",
"trialCreated": "试跟配置已创建",
"trialCreateFailed": "创建试跟配置失败",
"goCopyTrading": "查看跟单配置",
"removeConfirm": "只移除池子项,不会删除 Leader 地址或已有跟单配置。确定继续?",
"removeSuccess": "池子项已移除",
"removeFailed": "移除池子项失败"
},
"leaderAdd": {
"title": "添加 Leader",
@@ -1811,4 +1873,4 @@
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "知道了"
}
}
}
+64 -2
View File
@@ -314,6 +314,7 @@
"leaders": "Leader 管理",
"templates": "跟單模板",
"copyTradingConfig": "跟單配置",
"leaderPool": "Leader 池",
"cryptoSpreadStrategy": "加密價差策略",
"cryptoTailStrategy": "策略配置",
"cryptoTailMonitor": "即時監控",
@@ -552,7 +553,68 @@
"deleteConfirmDesc": "該 Leader 還有 {{count}} 個跟單關係,請先刪除跟單關係",
"openDetailFailed": "打開詳情失敗",
"noCopyTradings": "暫無跟單配置",
"noBacktests": "暫無回測"
"noBacktests": "暫無回測",
"addToPool": "加入 Leader 池",
"addToPoolSuccess": "已加入 Leader 池",
"addToPoolExists": "該 Leader 已在池子中",
"addToPoolFailed": "加入 Leader 池失敗",
"goLeaderPool": "前往 Leader 池"
},
"leaderPool": {
"title": "Leader 池",
"subtitle": "把候選、觀察、小額試跟、冷卻和淘汰從真實跟單配置中拆出來,先決策,再下真錢。",
"safetyHint": "Leader 池不會自動加倉,也不會自動啟用大額跟單。",
"safetyDesc": "從池子創建試跟配置時會使用保守默認值,並默認禁用。你可以之後在跟單配置頁手動啟用。",
"fetchFailed": "獲取 Leader 池失敗",
"selectLeaderPlaceholder": "選擇已有 Leader 加入池子",
"selectLeaderFirst": "請先選擇一個 Leader",
"addExistingLeader": "加入池子",
"addSuccess": "加入 Leader 池成功",
"addFailed": "加入 Leader 池失敗",
"goLeaders": "去 Leader 管理",
"noLeaders": "暫無 Leader,請先添加",
"totalCount": "池子人數",
"trialCount": "試跟中人數",
"estimatedWorstExposure": "估算最壞暴露",
"pendingRiskCount": "待處理風險",
"filterStatus": "按狀態篩選",
"emptyDesc": "池子還是空的,可以從 Leader 管理加入 leader,或在本頁選擇已有 leader 加入。",
"leader": "Leader",
"source": "來源",
"suggestedConfig": "建議配置",
"fixedAmount": "固定金額",
"maxDailyOrders": "每日最大單數",
"maxDailyLoss": "每日最大虧損",
"minPrice": "最低價格",
"maxPrice": "最高價格",
"maxPositionValue": "最大持倉",
"priceRange": "價格區間",
"copyTradingState": "跟單配置狀態",
"configs": "個配置",
"hasEnabled": "存在啟用配置",
"noEnabled": "無啟用配置",
"lastReviewedAt": "最後複核",
"updateStatus": "更新狀態",
"statusUpdated": "狀態已更新",
"statusUpdateFailed": "狀態更新失敗",
"cooldownUntil": "冷卻截止時間",
"locked": "鎖定",
"editPlan": "編輯建議",
"planUpdated": "建議配置已更新",
"planUpdateFailed": "建議配置更新失敗",
"notes": "備註",
"createTrial": "創建試跟配置",
"trialConfirmTitle": "將創建默認禁用的小額 FIXED 跟單配置",
"trialConfirmDesc": "提交前請確認固定金額、每日最大單數、每日最大虧損、價格區間和最大持倉。",
"account": "賬戶",
"selectAccount": "請選擇賬戶",
"noAccounts": "暫無賬戶,請先添加賬戶",
"trialCreated": "試跟配置已創建",
"trialCreateFailed": "創建試跟配置失敗",
"goCopyTrading": "查看跟單配置",
"removeConfirm": "只移除池子項,不會刪除 Leader 地址或已有跟單配置。確定繼續?",
"removeSuccess": "池子項已移除",
"removeFailed": "移除池子項失敗"
},
"leaderAdd": {
"title": "添加 Leader",
@@ -1811,4 +1873,4 @@
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "知道了"
}
}
}
@@ -6,6 +6,7 @@ import { formatUSDC } from '../../utils'
import { useTranslation } from 'react-i18next'
import { useMediaQuery } from 'react-responsive'
import type { CopyTradingStatistics } from '../../types'
import CopyTradingRiskSeatbeltPanel from '../../components/CopyTradingRiskSeatbeltPanel'
interface StatisticsModalProps {
open: boolean
@@ -159,6 +160,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalUnrealizedPnl)}</span>
</div>
</div>
<CopyTradingRiskSeatbeltPanel statistics={statistics} onApplied={fetchStatistics} compact />
</div>
) : (
<div>
@@ -230,6 +232,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
/>
</Col>
</Row>
<CopyTradingRiskSeatbeltPanel statistics={statistics} onApplied={fetchStatistics} compact />
</div>
)}
</Modal>
@@ -237,4 +240,3 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
}
export default StatisticsModal
+3 -1
View File
@@ -6,6 +6,7 @@ import { apiService } from '../services/api'
import { formatUSDC, formatNumber } from '../utils'
import { useMediaQuery } from 'react-responsive'
import type { CopyTradingStatistics } from '../types'
import CopyTradingRiskSeatbeltPanel from '../components/CopyTradingRiskSeatbeltPanel'
const CopyTradingStatisticsPage: React.FC = () => {
const { copyTradingId } = useParams<{ copyTradingId: string }>()
@@ -263,9 +264,10 @@ const CopyTradingStatisticsPage: React.FC = () => {
</Col>
</Row>
</Card>
<CopyTradingRiskSeatbeltPanel statistics={statistics} onApplied={fetchStatistics} />
</div>
)
}
export default CopyTradingStatisticsPage
+47 -1
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider, Typography, Modal, Descriptions, Statistic, Row, Col, Tooltip, Badge } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined, GlobalOutlined, EyeOutlined, ReloadOutlined, WalletOutlined, CopyOutlined, LineChartOutlined } from '@ant-design/icons'
import { PlusOutlined, EditOutlined, DeleteOutlined, GlobalOutlined, EyeOutlined, ReloadOutlined, WalletOutlined, CopyOutlined, LineChartOutlined, TeamOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import type { Leader, LeaderBalanceResponse } from '../types'
@@ -18,6 +18,7 @@ const LeaderList: React.FC = () => {
const [loading, setLoading] = useState(false)
const [balanceMap, setBalanceMap] = useState<Record<number, { total: string; available: string; position: string }>>({})
const [balanceLoading, setBalanceLoading] = useState<Record<number, boolean>>({})
const [addingToPool, setAddingToPool] = useState<Record<number, boolean>>({})
// 详情 Modal
const [detailModalVisible, setDetailModalVisible] = useState(false)
@@ -95,6 +96,31 @@ const LeaderList: React.FC = () => {
}
}
const handleAddToPool = async (leader: Leader) => {
setAddingToPool(prev => ({ ...prev, [leader.id]: true }))
try {
const response = await apiService.leaderPool.add({ leaderId: leader.id })
if (response.data.code === 0) {
message.success({
content: (
<Space>
<span>{t('leaderList.addToPoolSuccess')}</span>
<Button type="link" size="small" onClick={() => navigate('/leader-pool')}>
{t('leaderList.goLeaderPool')}
</Button>
</Space>
)
})
} else {
message.warning(response.data.msg || t('leaderList.addToPoolExists'))
}
} catch (error: any) {
message.error(error.message || t('leaderList.addToPoolFailed'))
} finally {
setAddingToPool(prev => ({ ...prev, [leader.id]: false }))
}
}
const handleShowDetail = async (leader: Leader) => {
try {
setDetailModalVisible(true)
@@ -386,6 +412,26 @@ const LeaderList: React.FC = () => {
</div>
</Tooltip>
<Tooltip title={t('leaderList.addToPool')}>
<div
onClick={() => !addingToPool[record.id] && handleAddToPool(record)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: addingToPool[record.id] ? 'wait' : 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#f0f0f0'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<TeamOutlined style={{ fontSize: '16px', color: '#52c41a' }} />
</div>
</Tooltip>
<Popconfirm
title={t('leaderList.deleteConfirm')}
description={record.copyTradingCount > 0 ? t('leaderList.deleteConfirmDesc', { count: record.copyTradingCount }) : undefined}
+559
View File
@@ -0,0 +1,559 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Alert,
Button,
Card,
DatePicker,
Descriptions,
Empty,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Row,
Col,
Select,
Space,
Statistic,
Table,
Tag,
Typography,
message
} from 'antd'
import { EyeOutlined, LinkOutlined, PlusOutlined, ReloadOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
import dayjs from 'dayjs'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import type { Account, Leader, LeaderPoolItem, LeaderPoolListResponse, LeaderPoolStatus } from '../types'
const { Text, Title, Paragraph } = Typography
const VISIBLE_STATUSES: Array<{ value: LeaderPoolStatus; color: string }> = [
{ value: 'CANDIDATE', color: 'default' },
{ value: 'WATCH', color: 'blue' },
{ value: 'TRIAL', color: 'green' },
{ value: 'COOLDOWN', color: 'orange' },
{ value: 'RETIRED', color: 'red' }
]
type LeaderPoolFilterValue = LeaderPoolStatus | 'ALL'
const statusLabels: Record<LeaderPoolStatus, string> = {
CANDIDATE: '候选',
WATCH: '观察',
PAPER: '模拟',
TRIAL: '小额试跟',
ACTIVE: '活跃',
COOLDOWN: '冷却',
RETIRED: '淘汰'
}
const formatDate = (timestamp?: number) => {
if (!timestamp) return '-'
return dayjs(timestamp).format('YYYY-MM-DD HH:mm')
}
const LeaderPool: React.FC = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const [poolData, setPoolData] = useState<LeaderPoolListResponse>({
summary: {
totalCount: 0,
trialCount: 0,
estimatedWorstExposure: '0',
pendingRiskCount: 0,
defaultExperimentBudget: '50'
},
list: [],
total: 0
})
const [leaders, setLeaders] = useState<Leader[]>([])
const [accounts, setAccounts] = useState<Account[]>([])
const [statusFilter, setStatusFilter] = useState<LeaderPoolStatus | undefined>()
const [loading, setLoading] = useState(false)
const [adding, setAdding] = useState(false)
const [creatingMap, setCreatingMap] = useState<Record<number, boolean>>({})
const [selectedLeaderId, setSelectedLeaderId] = useState<number | undefined>()
const [statusModalItem, setStatusModalItem] = useState<LeaderPoolItem | null>(null)
const [planModalItem, setPlanModalItem] = useState<LeaderPoolItem | null>(null)
const [trialModalItem, setTrialModalItem] = useState<LeaderPoolItem | null>(null)
const [statusForm] = Form.useForm()
const [planForm] = Form.useForm()
const [trialForm] = Form.useForm()
const fetchPool = async (status = statusFilter) => {
setLoading(true)
try {
const response = await apiService.leaderPool.list(status ? { status } : {})
if (response.data.code === 0 && response.data.data) {
setPoolData(response.data.data)
} else {
message.error(response.data.msg || t('leaderPool.fetchFailed'))
}
} catch (error: any) {
message.error(error.message || t('leaderPool.fetchFailed'))
} finally {
setLoading(false)
}
}
const fetchLeaders = async () => {
try {
const response = await apiService.leaders.list()
if (response.data.code === 0 && response.data.data) {
setLeaders(response.data.data.list || [])
}
} catch (error) {
console.error('加载 Leader 列表失败:', error)
}
}
const fetchAccounts = async () => {
try {
const response = await apiService.accounts.list()
if (response.data.code === 0 && response.data.data) {
setAccounts(response.data.data.list || [])
}
} catch (error) {
console.error('加载账户列表失败:', error)
}
}
useEffect(() => {
fetchPool()
fetchLeaders()
fetchAccounts()
}, [])
useEffect(() => {
fetchPool(statusFilter)
}, [statusFilter])
const handleAddLeader = async () => {
if (!selectedLeaderId) {
message.warning(t('leaderPool.selectLeaderFirst'))
return
}
setAdding(true)
try {
const response = await apiService.leaderPool.add({ leaderId: selectedLeaderId })
if (response.data.code === 0) {
message.success(t('leaderPool.addSuccess'))
setSelectedLeaderId(undefined)
fetchPool()
} else {
message.warning(response.data.msg || t('leaderPool.addFailed'))
}
} catch (error: any) {
message.error(error.message || t('leaderPool.addFailed'))
} finally {
setAdding(false)
}
}
const openStatusModal = (item: LeaderPoolItem) => {
setStatusModalItem(item)
statusForm.setFieldsValue({
status: item.status,
cooldownUntil: item.cooldownUntil ? dayjs(item.cooldownUntil) : undefined,
locked: item.locked
})
}
const handleUpdateStatus = async () => {
if (!statusModalItem) return
const values = await statusForm.validateFields()
const response = await apiService.leaderPool.updateStatus({
poolId: statusModalItem.id,
status: values.status,
cooldownUntil: values.cooldownUntil ? values.cooldownUntil.valueOf() : undefined,
locked: values.locked
})
if (response.data.code === 0) {
message.success(t('leaderPool.statusUpdated'))
setStatusModalItem(null)
fetchPool()
} else {
message.error(response.data.msg || t('leaderPool.statusUpdateFailed'))
}
}
const openPlanModal = (item: LeaderPoolItem) => {
setPlanModalItem(item)
planForm.setFieldsValue({
suggestedFixedAmount: item.suggestedFixedAmount,
suggestedMaxDailyOrders: item.suggestedMaxDailyOrders,
suggestedMaxDailyLoss: item.suggestedMaxDailyLoss,
suggestedMinPrice: item.suggestedMinPrice,
suggestedMaxPrice: item.suggestedMaxPrice,
suggestedMaxPositionValue: item.suggestedMaxPositionValue,
notes: item.notes
})
}
const handleUpdatePlan = async () => {
if (!planModalItem) return
const values = await planForm.validateFields()
const response = await apiService.leaderPool.updatePlan({
poolId: planModalItem.id,
suggestedFixedAmount: values.suggestedFixedAmount?.toString(),
suggestedMaxDailyOrders: values.suggestedMaxDailyOrders,
suggestedMaxDailyLoss: values.suggestedMaxDailyLoss?.toString(),
suggestedMinPrice: values.suggestedMinPrice?.toString(),
suggestedMaxPrice: values.suggestedMaxPrice?.toString(),
suggestedMaxPositionValue: values.suggestedMaxPositionValue?.toString(),
notes: values.notes
})
if (response.data.code === 0) {
message.success(t('leaderPool.planUpdated'))
setPlanModalItem(null)
fetchPool()
} else {
message.error(response.data.msg || t('leaderPool.planUpdateFailed'))
}
}
const openTrialModal = (item: LeaderPoolItem) => {
setTrialModalItem(item)
trialForm.setFieldsValue({ accountId: accounts[0]?.id })
}
const handleCreateTrialConfig = async () => {
if (!trialModalItem) return
if (creatingMap[trialModalItem.id]) return
const values = await trialForm.validateFields()
setCreatingMap(prev => ({ ...prev, [trialModalItem.id]: true }))
try {
const response = await apiService.leaderPool.createTrialConfig({
poolId: trialModalItem.id,
accountId: values.accountId,
enableImmediately: false,
confirm: false
})
if (response.data.code === 0) {
message.success({
content: (
<Space>
<span>{t('leaderPool.trialCreated')}</span>
<Button type="link" size="small" onClick={() => navigate('/copy-trading')}>
{t('leaderPool.goCopyTrading')}
</Button>
</Space>
)
})
setTrialModalItem(null)
fetchPool()
} else {
message.error(response.data.msg || t('leaderPool.trialCreateFailed'))
}
} catch (error: any) {
message.error(error.message || t('leaderPool.trialCreateFailed'))
} finally {
setCreatingMap(prev => ({ ...prev, [trialModalItem.id]: false }))
}
}
const handleRemove = async (item: LeaderPoolItem) => {
const response = await apiService.leaderPool.remove({ poolId: item.id })
if (response.data.code === 0) {
message.success(t('leaderPool.removeSuccess'))
fetchPool()
} else {
message.error(response.data.msg || t('leaderPool.removeFailed'))
}
}
const columns = [
{
title: t('leaderPool.leader'),
key: 'leader',
width: 260,
render: (_: unknown, item: LeaderPoolItem) => (
<Space direction="vertical" size={0}>
<Text strong>{item.leaderName || `Leader ${item.leaderId}`}</Text>
<Text copyable style={{ fontSize: 12, fontFamily: 'monospace' }} type="secondary">
{item.leaderAddress}
</Text>
</Space>
)
},
{
title: t('common.status'),
dataIndex: 'status',
width: 120,
render: (status: LeaderPoolStatus) => {
const meta = VISIBLE_STATUSES.find(item => item.value === status)
return <Tag color={meta?.color || 'default'}>{statusLabels[status] || status}</Tag>
}
},
{
title: t('leaderPool.source'),
dataIndex: 'source',
width: 120
},
{
title: t('leaderPool.suggestedConfig'),
key: 'plan',
width: 220,
render: (_: unknown, item: LeaderPoolItem) => (
<Space direction="vertical" size={0}>
<Text>{t('leaderPool.fixedAmount')}: {item.suggestedFixedAmount}</Text>
<Text type="secondary">{t('leaderPool.maxDailyOrders')}: {item.suggestedMaxDailyOrders}</Text>
<Text type="secondary">{t('leaderPool.maxDailyLoss')}: {item.suggestedMaxDailyLoss}</Text>
</Space>
)
},
{
title: t('leaderPool.copyTradingState'),
key: 'copyTradingState',
width: 160,
render: (_: unknown, item: LeaderPoolItem) => (
<Space direction="vertical" size={0}>
<Text>{item.copyTradingCount} {t('leaderPool.configs')}</Text>
<Tag color={item.hasEnabledCopyTrading ? 'green' : 'default'}>
{item.hasEnabledCopyTrading ? t('leaderPool.hasEnabled') : t('leaderPool.noEnabled')}
</Tag>
</Space>
)
},
{
title: t('leaderPool.lastReviewedAt'),
dataIndex: 'lastReviewedAt',
width: 150,
render: (value?: number) => formatDate(value)
},
{
title: t('common.actions'),
key: 'actions',
fixed: 'right' as const,
width: 320,
render: (_: unknown, item: LeaderPoolItem) => (
<Space wrap size={4}>
<Button size="small" icon={<EyeOutlined />} onClick={() => window.open(item.profileUrl, '_blank', 'noopener,noreferrer')}>
Profile
</Button>
<Button size="small" onClick={() => openStatusModal(item)}>
{t('leaderPool.updateStatus')}
</Button>
<Button size="small" onClick={() => openPlanModal(item)}>
{t('leaderPool.editPlan')}
</Button>
<Button
size="small"
type="primary"
loading={creatingMap[item.id]}
disabled={creatingMap[item.id]}
onClick={() => openTrialModal(item)}
>
{t('leaderPool.createTrial')}
</Button>
<Popconfirm
title={t('leaderPool.removeConfirm')}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={() => handleRemove(item)}
>
<Button size="small" danger>{t('common.delete')}</Button>
</Popconfirm>
</Space>
)
}
]
return (
<div>
<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('leaderPool.title')}</Title>
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
{t('leaderPool.subtitle')}
</Paragraph>
</div>
<Button icon={<ReloadOutlined />} onClick={() => fetchPool()}>{t('common.refresh')}</Button>
</Space>
<Alert
type="info"
showIcon
message={t('leaderPool.safetyHint')}
description={t('leaderPool.safetyDesc')}
/>
<Space wrap>
<Select
style={{ minWidth: 260 }}
allowClear
showSearch
placeholder={t('leaderPool.selectLeaderPlaceholder')}
optionFilterProp="label"
value={selectedLeaderId}
onChange={setSelectedLeaderId}
options={leaders.map(leader => ({
label: `${leader.leaderName || `Leader ${leader.id}`} - ${leader.leaderAddress.slice(0, 8)}...`,
value: leader.id
}))}
notFoundContent={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('leaderPool.noLeaders')} />}
/>
<Button type="primary" icon={<PlusOutlined />} loading={adding} onClick={handleAddLeader}>
{t('leaderPool.addExistingLeader')}
</Button>
<Button icon={<LinkOutlined />} onClick={() => navigate('/leaders')}>
{t('leaderPool.goLeaders')}
</Button>
</Space>
</Space>
</Card>
<Row gutter={[16, 16]}>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title={t('leaderPool.totalCount')} value={poolData.summary.totalCount} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title={t('leaderPool.trialCount')} value={poolData.summary.trialCount} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic prefix="$" title={t('leaderPool.estimatedWorstExposure')} value={poolData.summary.estimatedWorstExposure} /></Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card><Statistic title={t('leaderPool.pendingRiskCount')} value={poolData.summary.pendingRiskCount} /></Card>
</Col>
</Row>
<Card>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Select
style={{ width: 220 }}
placeholder={t('leaderPool.filterStatus')}
value={statusFilter ?? 'ALL'}
onChange={(value: LeaderPoolFilterValue) => setStatusFilter(value === 'ALL' ? undefined : value)}
options={[
{ value: 'ALL', label: t('common.all') },
...VISIBLE_STATUSES.map(item => ({ value: item.value, label: statusLabels[item.value] }))
]}
/>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={poolData.list}
scroll={{ x: 1280 }}
locale={{
emptyText: (
<Empty
description={t('leaderPool.emptyDesc')}
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)
}}
/>
</Space>
</Card>
</Space>
<Modal
title={t('leaderPool.updateStatus')}
open={!!statusModalItem}
onCancel={() => setStatusModalItem(null)}
onOk={handleUpdateStatus}
>
<Form form={statusForm} layout="vertical">
<Form.Item name="status" label={t('common.status')} rules={[{ required: true }]}>
<Select options={VISIBLE_STATUSES.map(item => ({ value: item.value, label: statusLabels[item.value] }))} />
</Form.Item>
<Form.Item shouldUpdate noStyle>
{({ getFieldValue }) => getFieldValue('status') === 'COOLDOWN' ? (
<Form.Item name="cooldownUntil" label={t('leaderPool.cooldownUntil')}>
<DatePicker showTime style={{ width: '100%' }} />
</Form.Item>
) : null}
</Form.Item>
<Form.Item name="locked" label={t('leaderPool.locked')}>
<Select
options={[
{ value: false, label: t('common.no') },
{ value: true, label: t('common.yes') }
]}
/>
</Form.Item>
</Form>
</Modal>
<Modal
title={t('leaderPool.editPlan')}
open={!!planModalItem}
onCancel={() => setPlanModalItem(null)}
onOk={handleUpdatePlan}
>
<Form form={planForm} layout="vertical">
<Form.Item name="suggestedFixedAmount" label={t('leaderPool.fixedAmount')} rules={[{ required: true }]}>
<InputNumber min={0.01} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="suggestedMaxDailyOrders" label={t('leaderPool.maxDailyOrders')} rules={[{ required: true }]}>
<InputNumber min={1} max={100} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="suggestedMaxDailyLoss" label={t('leaderPool.maxDailyLoss')} rules={[{ required: true }]}>
<InputNumber min={0.01} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="suggestedMinPrice" label={t('leaderPool.minPrice')}>
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="suggestedMaxPrice" label={t('leaderPool.maxPrice')}>
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="suggestedMaxPositionValue" label={t('leaderPool.maxPositionValue')}>
<InputNumber min={0.01} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="notes" label={t('leaderPool.notes')}>
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
<Modal
title={t('leaderPool.createTrial')}
open={!!trialModalItem}
onCancel={() => setTrialModalItem(null)}
onOk={handleCreateTrialConfig}
confirmLoading={trialModalItem ? creatingMap[trialModalItem.id] : false}
>
{trialModalItem && (
<Space direction="vertical" style={{ width: '100%' }}>
<Alert
type="warning"
showIcon
icon={<SafetyCertificateOutlined />}
message={t('leaderPool.trialConfirmTitle')}
description={t('leaderPool.trialConfirmDesc')}
/>
<Form form={trialForm} layout="vertical">
<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)}...)`
}))}
notFoundContent={<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('leaderPool.noAccounts')} />}
/>
</Form.Item>
</Form>
<Descriptions bordered size="small" column={1}>
<Descriptions.Item label={t('leaderPool.fixedAmount')}>{trialModalItem.suggestedFixedAmount}</Descriptions.Item>
<Descriptions.Item label={t('leaderPool.maxDailyOrders')}>{trialModalItem.suggestedMaxDailyOrders}</Descriptions.Item>
<Descriptions.Item label={t('leaderPool.maxDailyLoss')}>{trialModalItem.suggestedMaxDailyLoss}</Descriptions.Item>
<Descriptions.Item label={t('leaderPool.priceRange')}>
{trialModalItem.suggestedMinPrice || '-'} - {trialModalItem.suggestedMaxPrice || '-'}
</Descriptions.Item>
<Descriptions.Item label={t('leaderPool.maxPositionValue')}>{trialModalItem.suggestedMaxPositionValue || '5'}</Descriptions.Item>
<Descriptions.Item label={t('common.status')}>{t('common.disabled')}</Descriptions.Item>
</Descriptions>
</Space>
)}
</Modal>
</div>
)
}
export default LeaderPool
+54 -2
View File
@@ -1,5 +1,20 @@
import axios, { AxiosInstance, AxiosError } from 'axios'
import type { ApiResponse, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest, NotificationTemplate, TemplateTypeInfo, TemplateVariablesResponse } from '../types'
import type {
ApiResponse,
LeaderPoolAddRequest,
LeaderPoolCreateTrialConfigRequest,
LeaderPoolItem,
LeaderPoolListRequest,
LeaderPoolListResponse,
LeaderPoolUpdatePlanRequest,
LeaderPoolUpdateStatusRequest,
NotificationConfig,
NotificationConfigRequest,
NotificationConfigUpdateRequest,
NotificationTemplate,
TemplateTypeInfo,
TemplateVariablesResponse
} from '../types'
import { getToken, setToken, removeToken } from '../utils'
import { wsManager } from './websocket'
import i18n from '../i18n/config'
@@ -358,6 +373,29 @@ export const apiService = {
balance: (data: { leaderId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leaders/balance', data)
},
/**
* Leader 池 API
*/
leaderPool: {
list: (data: LeaderPoolListRequest = {}) =>
apiClient.post<ApiResponse<LeaderPoolListResponse>>('/copy-trading/leader-pool/list', data),
add: (data: LeaderPoolAddRequest) =>
apiClient.post<ApiResponse<LeaderPoolItem>>('/copy-trading/leader-pool/add', data),
updateStatus: (data: LeaderPoolUpdateStatusRequest) =>
apiClient.post<ApiResponse<LeaderPoolItem>>('/copy-trading/leader-pool/update-status', data),
updatePlan: (data: LeaderPoolUpdatePlanRequest) =>
apiClient.post<ApiResponse<LeaderPoolItem>>('/copy-trading/leader-pool/update-plan', data),
createTrialConfig: (data: LeaderPoolCreateTrialConfigRequest) =>
apiClient.post<ApiResponse<any>>('/copy-trading/leader-pool/create-trial-config', data),
remove: (data: { poolId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/leader-pool/remove', data)
},
/**
* 跟单模板管理 API(子菜单:跟单模板)
@@ -571,6 +609,21 @@ export const apiService = {
detail: (data: { copyTradingId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/statistics/detail', data)
},
safetyConfig: {
applyConservative: (data: {
copyTradingId: number
confirm: boolean
maxDailyOrders?: number
maxDailyLoss?: string
minPrice?: string
maxPrice?: string
maxPositionValue?: string
minOrderDepth?: string
maxSpread?: string
priceTolerance?: string
}) => apiClient.post<ApiResponse<any>>('/copy-trading/configs/apply-conservative-config', data)
},
/**
* 订单跟踪 API
@@ -950,4 +1003,3 @@ export const backtestService = {
*/
rerun: (data: { id: number; taskName?: string }) => apiClient.post('/backtest/tasks/rerun', data)
}
+130
View File
@@ -308,6 +308,89 @@ export interface CopyTradingListResponse {
total: number
}
export type LeaderPoolStatus = 'CANDIDATE' | 'WATCH' | 'PAPER' | 'TRIAL' | 'ACTIVE' | 'COOLDOWN' | 'RETIRED'
export interface LeaderPoolSummary {
totalCount: number
trialCount: number
estimatedWorstExposure: string
pendingRiskCount: number
defaultExperimentBudget: string
}
export interface LeaderPoolItem {
id: number
leaderId: number
leaderName?: string
leaderAddress: string
category?: string
profileUrl: string
status: LeaderPoolStatus
source: string
sourceRank?: number
score?: string
reason?: string
notes?: string
suggestedFixedAmount: string
suggestedMaxDailyOrders: number
suggestedMaxDailyLoss: string
suggestedMinPrice?: string
suggestedMaxPrice?: string
suggestedMaxPositionValue?: string
copyTradingCount: number
hasEnabledCopyTrading: boolean
estimatedWorstExposure: string
lastReviewedAt?: number
lastPromotedAt?: number
cooldownUntil?: number
locked: boolean
createdAt: number
updatedAt: number
}
export interface LeaderPoolListResponse {
summary: LeaderPoolSummary
list: LeaderPoolItem[]
total: number
}
export interface LeaderPoolListRequest {
status?: LeaderPoolStatus
}
export interface LeaderPoolAddRequest {
leaderId: number
source?: string
reason?: string
notes?: string
}
export interface LeaderPoolUpdateStatusRequest {
poolId: number
status: LeaderPoolStatus
cooldownUntil?: number
locked?: boolean
}
export interface LeaderPoolUpdatePlanRequest {
poolId: number
suggestedFixedAmount?: string
suggestedMaxDailyOrders?: number
suggestedMaxDailyLoss?: string
suggestedMinPrice?: string
suggestedMaxPrice?: string
suggestedMaxPositionValue?: string
reason?: string
notes?: string
}
export interface LeaderPoolCreateTrialConfigRequest {
poolId: number
accountId: number
enableImmediately?: boolean
confirm?: boolean
}
/**
* 跟单创建请求
* 所有配置参数都需要手动输入,模板仅用于前端快速填充表单
@@ -707,6 +790,14 @@ export interface CopyTradingStatistics {
currentPositionQuantity: string
currentPositionCost: string
currentPositionValue: string // 按当前价格估算的持仓市值
zeroValuePositionCost?: string
confirmedZeroValuePositionCost?: string
quoteOverallStatus?: 'AVAILABLE' | 'NO_MATCH' | 'UNAVAILABLE'
quoteAvailableCount?: number
quoteNoMatchCount?: number
quoteUnavailableCount?: number
quoteIncomplete?: boolean
riskDiagnosis?: CopyTradingRiskDiagnosis | null
// 盈亏统计
totalRealizedPnl: string
@@ -715,6 +806,45 @@ export interface CopyTradingStatistics {
totalPnlPercent: string
}
export interface CopyTradingRiskDiagnosis {
copyTradingId: number
totalRealizedPnl: string
totalUnrealizedPnl: string
totalPnl: string
currentPositionCost: string
currentPositionValue: string
zeroValuePositionCost: string
confirmedZeroValuePositionCost: string
zeroSellLoss: string
openPositionQuantity: string
totalBuyOrders: number
totalSellRecords: number
totalMatchDetails: number
filteredOrderCount: number
sampleSize: number
lowConfidence: boolean
confidenceReason: string
quoteOverallStatus: 'AVAILABLE' | 'NO_MATCH' | 'UNAVAILABLE'
quoteAvailableCount: number
quoteNoMatchCount: number
quoteUnavailableCount: number
dataIncomplete: boolean
missingSources: string[]
topLosingMarkets: Array<{
marketId: string
realizedPnl: string
matchedOrders: number
}>
riskWarnings: Array<{
field: string
currentValue: string | null
suggestedValue: string
severity: 'LOW' | 'MEDIUM' | 'HIGH'
reason: string
}>
generatedAt: number
}
/**
* 买入订单信息
*/