feat: 实现 NBA 量化交易系统
- 后端实现: - 实现 NBA 比赛数据服务,从 Polymarket API 获取数据 - 实现数据库存储和增量拉取逻辑(优先从 DB 获取,数据不足时增量拉取) - 使用 sports_market_types 参数直接筛选 moneyline 类型 - 实现分页拉取逻辑(基于 gameStartTime 和 createdAt) - 移除 nba_markets 相关的外键约束(V12 迁移) - 修复数据拉取逻辑:超过 3 天的数据不拉取 - 前端实现: - 实现策略创建/编辑/列表页面 - 实现交易信号展示页面和统计页面 - 修复重复请求问题(使用 useCallback 包装 fetchGames) - 支持选择单场比赛进行配置 - 使用西8区时间格式化显示 - 数据库: - 创建 NBA 量化交易相关表(V11 迁移) - 移除外键约束(V12 迁移) - 文档: - 添加产品需求文档、技术方案、算法文档等
This commit is contained in:
@@ -33,6 +33,11 @@ import FilteredOrdersList from './pages/FilteredOrdersList'
|
||||
import SystemSettings from './pages/SystemSettings'
|
||||
import ApiHealthStatus from './pages/ApiHealthStatus'
|
||||
import Announcements from './pages/Announcements'
|
||||
import NbaQuantitativeStrategyList from './pages/NbaQuantitativeStrategyList'
|
||||
import NbaQuantitativeStrategyAdd from './pages/NbaQuantitativeStrategyAdd'
|
||||
import NbaQuantitativeStrategyEdit from './pages/NbaQuantitativeStrategyEdit'
|
||||
import NbaTradingSignals from './pages/NbaTradingSignals'
|
||||
import NbaStatistics from './pages/NbaStatistics'
|
||||
import { wsManager } from './services/websocket'
|
||||
import type { OrderPushMessage } from './types'
|
||||
import { apiService } from './services/api'
|
||||
@@ -259,6 +264,11 @@ function App() {
|
||||
<Route path="/copy-trading/filtered-orders/:id" element={<ProtectedRoute><FilteredOrdersList /></ProtectedRoute>} />
|
||||
<Route path="/config" element={<ProtectedRoute><ConfigPage /></ProtectedRoute>} />
|
||||
<Route path="/positions" element={<ProtectedRoute><PositionList /></ProtectedRoute>} />
|
||||
<Route path="/nba/strategies" element={<ProtectedRoute><NbaQuantitativeStrategyList /></ProtectedRoute>} />
|
||||
<Route path="/nba/strategies/add" element={<ProtectedRoute><NbaQuantitativeStrategyAdd /></ProtectedRoute>} />
|
||||
<Route path="/nba/strategies/edit/:id" element={<ProtectedRoute><NbaQuantitativeStrategyEdit /></ProtectedRoute>} />
|
||||
<Route path="/nba/signals" element={<ProtectedRoute><NbaTradingSignals /></ProtectedRoute>} />
|
||||
<Route path="/nba/statistics/:id" element={<ProtectedRoute><NbaStatistics /></ProtectedRoute>} />
|
||||
<Route path="/statistics" element={<ProtectedRoute><Statistics /></ProtectedRoute>} />
|
||||
<Route path="/users" element={<ProtectedRoute><UserList /></ProtectedRoute>} />
|
||||
<Route path="/announcements" element={<ProtectedRoute><Announcements /></ProtectedRoute>} />
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
TwitterOutlined,
|
||||
CheckCircleOutlined,
|
||||
SendOutlined,
|
||||
NotificationOutlined
|
||||
NotificationOutlined,
|
||||
ThunderboltOutlined,
|
||||
SignalFilled
|
||||
} from '@ant-design/icons'
|
||||
import type { MenuProps } from 'antd'
|
||||
import type { ReactNode } from 'react'
|
||||
@@ -52,6 +54,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
keys.push('/copy-trading-management')
|
||||
}
|
||||
if (path.startsWith('/nba')) {
|
||||
keys.push('/nba-quantitative-trading')
|
||||
}
|
||||
if (path.startsWith('/system-settings')) {
|
||||
keys.push('/system-settings')
|
||||
}
|
||||
@@ -67,6 +72,9 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
if (path.startsWith('/leaders') || path.startsWith('/templates') || path.startsWith('/copy-trading')) {
|
||||
keys.push('/copy-trading-management')
|
||||
}
|
||||
if (path.startsWith('/nba')) {
|
||||
keys.push('/nba-quantitative-trading')
|
||||
}
|
||||
if (path.startsWith('/system-settings')) {
|
||||
keys.push('/system-settings')
|
||||
}
|
||||
@@ -111,6 +119,23 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: t('menu.positions')
|
||||
},
|
||||
{
|
||||
key: '/nba-quantitative-trading',
|
||||
icon: <ThunderboltOutlined />,
|
||||
label: t('menu.nbaQuantitativeTrading') || 'NBA量化交易',
|
||||
children: [
|
||||
{
|
||||
key: '/nba/strategies',
|
||||
icon: <FileTextOutlined />,
|
||||
label: t('menu.nbaStrategies') || '策略管理'
|
||||
},
|
||||
{
|
||||
key: '/nba/signals',
|
||||
icon: <SignalFilled />,
|
||||
label: t('menu.nbaSignals') || '交易信号'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: '/statistics',
|
||||
icon: <BarChartOutlined />,
|
||||
@@ -169,7 +194,7 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
|
||||
const handleMenuClick = ({ key }: { key: string }) => {
|
||||
// 如果是父菜单,不导航(但 /system-settings 作为子菜单项时可以导航)
|
||||
if (key === '/copy-trading-management') {
|
||||
if (key === '/copy-trading-management' || key === '/nba-quantitative-trading') {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,10 @@
|
||||
"logout": "退出登录",
|
||||
"logoutConfirm": "确认退出",
|
||||
"logoutConfirmDesc": "确定要退出登录吗?",
|
||||
"navigation": "导航菜单"
|
||||
"navigation": "导航菜单",
|
||||
"nbaQuantitativeTrading": "NBA量化交易",
|
||||
"nbaStrategies": "策略管理",
|
||||
"nbaSignals": "交易信号"
|
||||
},
|
||||
"apiHealthStatus": {
|
||||
"title": "API 健康状态",
|
||||
|
||||
@@ -0,0 +1,828 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Button, Steps, message, Input, Select, Switch, InputNumber, DatePicker, Space, Divider, Checkbox } from 'antd'
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { NbaQuantitativeStrategyCreateRequest, NbaGame } from '../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
// 配置 dayjs 时区插件
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
const { RangePicker } = DatePicker
|
||||
|
||||
const NbaQuantitativeStrategyAdd: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { accounts, fetchAccounts } = useAccountStore()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [games, setGames] = useState<NbaGame[]>([])
|
||||
const [loadingGames, setLoadingGames] = useState(false)
|
||||
const [selectedGameId, setSelectedGameId] = useState<string | null>(null)
|
||||
|
||||
const fetchGames = useCallback(async () => {
|
||||
setLoadingGames(true)
|
||||
try {
|
||||
// 使用西8区时间计算时间戳
|
||||
const today = dayjs().tz('America/Los_Angeles').startOf('day')
|
||||
const nextWeek = dayjs().tz('America/Los_Angeles').add(7, 'day').endOf('day')
|
||||
|
||||
const response = await apiService.nbaGames.list({
|
||||
startTimestamp: today.valueOf(), // 传递时间戳(毫秒)
|
||||
endTimestamp: nextWeek.valueOf() // 传递时间戳(毫秒)
|
||||
})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setGames(response.data.data.list || [])
|
||||
} else {
|
||||
message.warning('获取比赛列表失败,请稍后重试')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取比赛列表失败')
|
||||
} finally {
|
||||
setLoadingGames(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchGames()
|
||||
// 设置默认值
|
||||
form.setFieldsValue({
|
||||
enabled: true,
|
||||
minWinProbabilityDiff: 0.1,
|
||||
minTradeValue: 0.05,
|
||||
buyAmountStrategy: 'FIXED',
|
||||
fixedBuyAmount: 10,
|
||||
buyTiming: 'IMMEDIATE',
|
||||
buyDirection: 'AUTO',
|
||||
enableSell: true,
|
||||
sellRatio: 1.0,
|
||||
sellTiming: 'IMMEDIATE',
|
||||
priceStrategy: 'MARKET',
|
||||
priceOffset: 0,
|
||||
maxPosition: 50,
|
||||
minPosition: 5,
|
||||
priceTolerance: 0.05,
|
||||
baseStrengthWeight: 0.3,
|
||||
recentFormWeight: 0.25,
|
||||
lineupIntegrityWeight: 0.2,
|
||||
starStatusWeight: 0.15,
|
||||
environmentWeight: 0.1,
|
||||
matchupAdvantageWeight: 0.2,
|
||||
scoreDiffWeight: 0.3,
|
||||
momentumWeight: 0.2,
|
||||
dataUpdateFrequency: 30,
|
||||
analysisFrequency: 30,
|
||||
pushFailedOrders: false,
|
||||
pushFrequency: 'REALTIME',
|
||||
batchPushInterval: 1
|
||||
})
|
||||
}, [fetchGames])
|
||||
|
||||
const handleGameSelectionChange = (gameId: string | null) => {
|
||||
setSelectedGameId(gameId)
|
||||
if (gameId) {
|
||||
const selectedGame = games.find(game => game.nbaGameId === gameId)
|
||||
if (selectedGame) {
|
||||
// 自动提取该比赛的两支球队
|
||||
form.setFieldsValue({
|
||||
filterTeams: [selectedGame.homeTeam, selectedGame.awayTeam]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
filterTeams: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{ title: '基本信息', description: '策略名称和账户' },
|
||||
{ title: '触发条件', description: '概率阈值和交易价值' },
|
||||
{ title: '交易规则', description: '买入卖出规则' },
|
||||
{ title: '风险控制', description: '持仓和每日限制' },
|
||||
{ title: '高级配置', description: '算法权重和系统配置' }
|
||||
]
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
setLoading(true)
|
||||
|
||||
const request: NbaQuantitativeStrategyCreateRequest = {
|
||||
strategyName: values.strategyName,
|
||||
strategyDescription: values.strategyDescription,
|
||||
accountId: values.accountId,
|
||||
enabled: values.enabled,
|
||||
filterTeams: values.filterTeams,
|
||||
filterDateFrom: values.dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
filterDateTo: values.dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
filterGameImportance: values.filterGameImportance,
|
||||
minWinProbabilityDiff: values.minWinProbabilityDiff?.toString(),
|
||||
minWinProbability: values.minWinProbability?.toString(),
|
||||
maxWinProbability: values.maxWinProbability?.toString(),
|
||||
minTradeValue: values.minTradeValue?.toString(),
|
||||
minRemainingTime: values.minRemainingTime,
|
||||
maxRemainingTime: values.maxRemainingTime,
|
||||
minScoreDiff: values.minScoreDiff,
|
||||
maxScoreDiff: values.maxScoreDiff,
|
||||
buyAmountStrategy: values.buyAmountStrategy,
|
||||
fixedBuyAmount: values.fixedBuyAmount?.toString(),
|
||||
buyRatio: values.buyRatio?.toString(),
|
||||
baseBuyAmount: values.baseBuyAmount?.toString(),
|
||||
buyTiming: values.buyTiming,
|
||||
delayBuySeconds: values.delayBuySeconds,
|
||||
buyDirection: values.buyDirection,
|
||||
enableSell: values.enableSell,
|
||||
takeProfitThreshold: values.takeProfitThreshold?.toString(),
|
||||
stopLossThreshold: values.stopLossThreshold?.toString(),
|
||||
probabilityReversalThreshold: values.probabilityReversalThreshold?.toString(),
|
||||
sellRatio: values.sellRatio?.toString(),
|
||||
sellTiming: values.sellTiming,
|
||||
delaySellSeconds: values.delaySellSeconds,
|
||||
priceStrategy: values.priceStrategy,
|
||||
fixedPrice: values.fixedPrice?.toString(),
|
||||
priceOffset: values.priceOffset?.toString(),
|
||||
maxPosition: values.maxPosition?.toString(),
|
||||
minPosition: values.minPosition?.toString(),
|
||||
maxGamePosition: values.maxGamePosition?.toString(),
|
||||
maxDailyLoss: values.maxDailyLoss?.toString(),
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
maxDailyProfit: values.maxDailyProfit?.toString(),
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
minProbabilityThreshold: values.minProbabilityThreshold?.toString(),
|
||||
maxProbabilityThreshold: values.maxProbabilityThreshold?.toString(),
|
||||
baseStrengthWeight: values.baseStrengthWeight?.toString(),
|
||||
recentFormWeight: values.recentFormWeight?.toString(),
|
||||
lineupIntegrityWeight: values.lineupIntegrityWeight?.toString(),
|
||||
starStatusWeight: values.starStatusWeight?.toString(),
|
||||
environmentWeight: values.environmentWeight?.toString(),
|
||||
matchupAdvantageWeight: values.matchupAdvantageWeight?.toString(),
|
||||
scoreDiffWeight: values.scoreDiffWeight?.toString(),
|
||||
momentumWeight: values.momentumWeight?.toString(),
|
||||
dataUpdateFrequency: values.dataUpdateFrequency,
|
||||
analysisFrequency: values.analysisFrequency,
|
||||
pushFailedOrders: values.pushFailedOrders,
|
||||
pushFrequency: values.pushFrequency,
|
||||
batchPushInterval: values.batchPushInterval
|
||||
}
|
||||
|
||||
const response = await apiService.nbaStrategies.create(request)
|
||||
if (response.data.code === 0) {
|
||||
message.success('创建策略成功')
|
||||
navigate('/nba/strategies')
|
||||
} else {
|
||||
message.error(response.data.msg || '创建策略失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
// 表单验证错误
|
||||
const firstErrorField = error.errorFields[0]
|
||||
message.error(`${firstErrorField.name.join('.')}: ${firstErrorField.errors[0]}`)
|
||||
} else {
|
||||
message.error(error.message || '创建策略失败')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const next = async () => {
|
||||
try {
|
||||
const fields = getFieldsForStep(currentStep)
|
||||
await form.validateFields(fields)
|
||||
setCurrentStep(currentStep + 1)
|
||||
} catch (error) {
|
||||
// 验证失败,不跳转
|
||||
}
|
||||
}
|
||||
|
||||
const prev = () => {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
|
||||
const getFieldsForStep = (step: number): string[] => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return ['strategyName', 'accountId']
|
||||
case 1:
|
||||
return ['minWinProbabilityDiff', 'minTradeValue']
|
||||
case 2:
|
||||
return ['buyAmountStrategy', 'priceStrategy']
|
||||
case 3:
|
||||
return ['maxPosition', 'minPosition']
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const buyAmountStrategy = Form.useWatch('buyAmountStrategy', form)
|
||||
const priceStrategy = Form.useWatch('priceStrategy', form)
|
||||
const buyTiming = Form.useWatch('buyTiming', form)
|
||||
const sellTiming = Form.useWatch('sellTiming', form)
|
||||
const pushFrequency = Form.useWatch('pushFrequency', form)
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? '16px' : '24px' }}>
|
||||
<Card>
|
||||
<Space style={{ marginBottom: 24 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/nba/strategies')}>
|
||||
返回
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Steps current={currentStep} items={steps} style={{ marginBottom: 32 }} />
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{/* 第一步:基本信息 */}
|
||||
{currentStep === 0 && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="strategyName"
|
||||
label="策略名称"
|
||||
rules={[{ required: true, message: '请输入策略名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入策略名称" maxLength={50} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="strategyDescription"
|
||||
label="策略描述"
|
||||
>
|
||||
<TextArea rows={4} placeholder="请输入策略描述(可选)" maxLength={200} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="accountId"
|
||||
label="关联账户"
|
||||
rules={[{ required: true, message: '请选择关联账户' }]}
|
||||
>
|
||||
<Select placeholder="请选择账户">
|
||||
{accounts.map(account => (
|
||||
<Option key={account.id} value={account.id}>
|
||||
{account.accountName || account.walletAddress}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="启用状态"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择比赛(可选)"
|
||||
tooltip="选择一场比赛,系统会自动提取该比赛的两支球队作为关注球队"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择一场比赛"
|
||||
value={selectedGameId}
|
||||
onChange={handleGameSelectionChange}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{loadingGames ? (
|
||||
<Option value="loading" disabled>加载中...</Option>
|
||||
) : games.length === 0 ? (
|
||||
<Option value="empty" disabled>暂无比赛数据</Option>
|
||||
) : (
|
||||
games.map(game => (
|
||||
<Option
|
||||
key={game.nbaGameId}
|
||||
value={game.nbaGameId}
|
||||
label={`${game.awayTeam} @ ${game.homeTeam} (${dayjs(game.gameDate).format('MM-DD')}${game.gameTime ? ` ${dayjs(game.gameTime).tz('America/Los_Angeles').format('HH:mm')}` : ''})`}
|
||||
>
|
||||
<div>
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
{game.awayTeam} @ {game.homeTeam}
|
||||
</span>
|
||||
<span style={{ marginLeft: '8px', color: '#999', fontSize: '12px' }}>
|
||||
{dayjs(game.gameDate).format('MM-DD')}
|
||||
{game.gameTime && ` ${dayjs(game.gameTime).tz('America/Los_Angeles').format('HH:mm')}`}
|
||||
</span>
|
||||
{game.gameStatus && (
|
||||
<span style={{ marginLeft: '8px', color: '#666', fontSize: '12px' }}>
|
||||
({game.gameStatus === 'scheduled' ? '未开始' : game.gameStatus === 'active' ? '进行中' : '已结束'})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
{selectedGameId && (
|
||||
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
|
||||
已选择比赛,系统将自动关注该比赛的两支球队
|
||||
</div>
|
||||
)}
|
||||
<Form.Item name="filterTeams" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="dateRange"
|
||||
label="日期范围(可选)"
|
||||
>
|
||||
<RangePicker style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="filterGameImportance"
|
||||
label="比赛重要性"
|
||||
>
|
||||
<Select placeholder="选择比赛重要性">
|
||||
<Option value="all">全部</Option>
|
||||
<Option value="regular">常规赛</Option>
|
||||
<Option value="playoff">季后赛</Option>
|
||||
<Option value="key">关键战</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 第二步:触发条件 */}
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="minWinProbabilityDiff"
|
||||
label="最小获胜概率差异"
|
||||
rules={[{ required: true, message: '请输入最小获胜概率差异' }]}
|
||||
tooltip="主队和客队获胜概率的最小差异(如 0.1 表示至少 10% 的差异)"
|
||||
>
|
||||
<InputNumber min={0.05} max={0.5} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="minWinProbability"
|
||||
label="最小获胜概率(可选)"
|
||||
tooltip="生成买入信号时的最小获胜概率"
|
||||
>
|
||||
<InputNumber min={0.5} max={1.0} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxWinProbability"
|
||||
label="最大获胜概率(可选)"
|
||||
tooltip="生成买入信号时的最大获胜概率(用于反向策略)"
|
||||
>
|
||||
<InputNumber min={0.0} max={0.5} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="minTradeValue"
|
||||
label="最小交易价值"
|
||||
rules={[{ required: true, message: '请输入最小交易价值' }]}
|
||||
tooltip="交易价值评分的最小值,只有达到此值才会生成信号"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="minRemainingTime"
|
||||
label="最小剩余时间(分钟,可选)"
|
||||
>
|
||||
<InputNumber min={0} max={48} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxRemainingTime"
|
||||
label="最大剩余时间(分钟,可选)"
|
||||
>
|
||||
<InputNumber min={0} max={48} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="minScoreDiff"
|
||||
label="最小分差(可选)"
|
||||
>
|
||||
<InputNumber min={-50} max={50} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxScoreDiff"
|
||||
label="最大分差(可选)"
|
||||
>
|
||||
<InputNumber min={-50} max={50} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 第三步:交易规则 */}
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<Divider orientation="left">买入规则</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="buyAmountStrategy"
|
||||
label="买入金额策略"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="FIXED">固定金额</Option>
|
||||
<Option value="RATIO">按比例</Option>
|
||||
<Option value="DYNAMIC">动态计算</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{buyAmountStrategy === 'FIXED' && (
|
||||
<Form.Item
|
||||
name="fixedBuyAmount"
|
||||
label="固定买入金额(USDC)"
|
||||
rules={[{ required: true, message: '请输入固定买入金额' }]}
|
||||
>
|
||||
<InputNumber min={0.01} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{buyAmountStrategy === 'RATIO' && (
|
||||
<Form.Item
|
||||
name="buyRatio"
|
||||
label="买入比例(0-1)"
|
||||
rules={[{ required: true, message: '请输入买入比例' }]}
|
||||
>
|
||||
<InputNumber min={0.01} max={1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{buyAmountStrategy === 'DYNAMIC' && (
|
||||
<Form.Item
|
||||
name="baseBuyAmount"
|
||||
label="基础买入金额(USDC)"
|
||||
rules={[{ required: true, message: '请输入基础买入金额' }]}
|
||||
>
|
||||
<InputNumber min={0.01} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="buyTiming"
|
||||
label="买入时机"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="IMMEDIATE">立即买入</Option>
|
||||
<Option value="DELAYED">延迟买入</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{buyTiming === 'DELAYED' && (
|
||||
<Form.Item
|
||||
name="delayBuySeconds"
|
||||
label="延迟买入时间(秒)"
|
||||
rules={[{ required: true, message: '请输入延迟买入时间' }]}
|
||||
>
|
||||
<InputNumber min={0} max={300} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="buyDirection"
|
||||
label="买入方向"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="AUTO">系统自动判断</Option>
|
||||
<Option value="YES">YES</Option>
|
||||
<Option value="NO">NO</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">卖出规则</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="enableSell"
|
||||
label="启用卖出"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="takeProfitThreshold"
|
||||
label="止盈阈值(0-1,可选)"
|
||||
tooltip="预期收益达到多少时卖出(如 0.2 表示 20%)"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="stopLossThreshold"
|
||||
label="止损阈值(-1-0,可选)"
|
||||
tooltip="预期亏损达到多少时卖出(如 -0.1 表示 -10%)"
|
||||
>
|
||||
<InputNumber min={-1} max={0} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="probabilityReversalThreshold"
|
||||
label="概率反转阈值(0-1,可选)"
|
||||
tooltip="获胜概率反转多少时卖出(如 0.15 表示 15%)"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="sellRatio"
|
||||
label="卖出比例(0-1)"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber min={0.1} max={1} step={0.1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="sellTiming"
|
||||
label="卖出时机"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="IMMEDIATE">立即卖出</Option>
|
||||
<Option value="DELAYED">延迟卖出</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{sellTiming === 'DELAYED' && (
|
||||
<Form.Item
|
||||
name="delaySellSeconds"
|
||||
label="延迟卖出时间(秒)"
|
||||
rules={[{ required: true, message: '请输入延迟卖出时间' }]}
|
||||
>
|
||||
<InputNumber min={0} max={300} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Divider orientation="left">价格策略</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="priceStrategy"
|
||||
label="价格策略"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="FIXED">固定价格</Option>
|
||||
<Option value="MARKET">市场价格</Option>
|
||||
<Option value="DYNAMIC">动态价格</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{priceStrategy === 'FIXED' && (
|
||||
<Form.Item
|
||||
name="fixedPrice"
|
||||
label="固定价格(0-1)"
|
||||
rules={[{ required: true, message: '请输入固定价格' }]}
|
||||
>
|
||||
<InputNumber min={0.01} max={0.99} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
name="priceOffset"
|
||||
label="价格偏移(-0.1-0.1)"
|
||||
tooltip="价格偏移百分比(用于调整价格,提高交易成功率,如 0.05 表示 +5%)"
|
||||
>
|
||||
<InputNumber min={-0.1} max={0.1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 第四步:风险控制 */}
|
||||
{currentStep === 3 && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="maxPosition"
|
||||
label="最大持仓(USDC)"
|
||||
rules={[{ required: true, message: '请输入最大持仓' }]}
|
||||
>
|
||||
<InputNumber min={1} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="minPosition"
|
||||
label="最小持仓(USDC)"
|
||||
rules={[{ required: true, message: '请输入最小持仓' }]}
|
||||
>
|
||||
<InputNumber min={1} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxGamePosition"
|
||||
label="单场比赛最大持仓(USDC,可选)"
|
||||
>
|
||||
<InputNumber min={1} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxDailyLoss"
|
||||
label="每日亏损限制(USDC,可选)"
|
||||
>
|
||||
<InputNumber min={0.01} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxDailyOrders"
|
||||
label="每日订单限制(可选)"
|
||||
>
|
||||
<InputNumber min={1} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxDailyProfit"
|
||||
label="每日盈利目标(USDC,可选)"
|
||||
>
|
||||
<InputNumber min={0.01} step={1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="priceTolerance"
|
||||
label="价格容忍度(0-1)"
|
||||
tooltip="允许的价格偏差百分比(如 0.05 表示 5%)"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="minProbabilityThreshold"
|
||||
label="最小概率阈值(0.5-1.0,可选)"
|
||||
>
|
||||
<InputNumber min={0.5} max={1.0} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="maxProbabilityThreshold"
|
||||
label="最大概率阈值(0.0-0.5,可选)"
|
||||
>
|
||||
<InputNumber min={0.0} max={0.5} step={0.01} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 第五步:高级配置 */}
|
||||
{currentStep === 4 && (
|
||||
<>
|
||||
<Divider orientation="left">算法权重(高级)</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="baseStrengthWeight"
|
||||
label="基础实力权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="recentFormWeight"
|
||||
label="近期状态权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="lineupIntegrityWeight"
|
||||
label="阵容完整度权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="starStatusWeight"
|
||||
label="球星状态权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="environmentWeight"
|
||||
label="环境因素权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="matchupAdvantageWeight"
|
||||
label="对位优势权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="scoreDiffWeight"
|
||||
label="分差调整权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="momentumWeight"
|
||||
label="势头调整权重"
|
||||
>
|
||||
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
|
||||
<Divider orientation="left">系统配置</Divider>
|
||||
|
||||
<Form.Item
|
||||
name="dataUpdateFrequency"
|
||||
label="数据更新频率(秒)"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value={10}>10 秒</Option>
|
||||
<Option value={30}>30 秒</Option>
|
||||
<Option value={60}>1 分钟</Option>
|
||||
<Option value={300}>5 分钟</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="analysisFrequency"
|
||||
label="分析频率(秒)"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value={10}>10 秒</Option>
|
||||
<Option value={30}>30 秒</Option>
|
||||
<Option value={60}>1 分钟</Option>
|
||||
<Option value={300}>5 分钟</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="pushFailedOrders"
|
||||
label="推送失败订单"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="pushFrequency"
|
||||
label="推送频率"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="REALTIME">实时推送</Option>
|
||||
<Option value="BATCH">批量推送</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{pushFrequency === 'BATCH' && (
|
||||
<Form.Item
|
||||
name="batchPushInterval"
|
||||
label="批量推送间隔(秒)"
|
||||
rules={[{ required: true, message: '请输入批量推送间隔' }]}
|
||||
>
|
||||
<InputNumber min={1} max={60} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
||||
<Space>
|
||||
{currentStep > 0 && (
|
||||
<Button onClick={prev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 && (
|
||||
<Button type="primary" onClick={next}>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === steps.length - 1 && (
|
||||
<Button type="primary" htmlType="submit" loading={loading} icon={<SaveOutlined />}>
|
||||
创建策略
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NbaQuantitativeStrategyAdd
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Card, Form, Button, Steps, message, Input, Select, Switch, InputNumber, DatePicker, Space, Checkbox } from 'antd'
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { NbaQuantitativeStrategyUpdateRequest, NbaGame } from '../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
|
||||
// 配置 dayjs 时区插件
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
|
||||
const { Option } = Select
|
||||
const { TextArea } = Input
|
||||
const { RangePicker } = DatePicker
|
||||
|
||||
const NbaQuantitativeStrategyEdit: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { accounts, fetchAccounts } = useAccountStore()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loadingData, setLoadingData] = useState(true)
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [games, setGames] = useState<NbaGame[]>([])
|
||||
const [loadingGames, setLoadingGames] = useState(false)
|
||||
const [selectedGameId, setSelectedGameId] = useState<string | null>(null)
|
||||
const [strategyData, setStrategyData] = useState<any>(null)
|
||||
|
||||
const fetchGames = useCallback(async () => {
|
||||
setLoadingGames(true)
|
||||
try {
|
||||
// 使用西8区时间计算时间戳
|
||||
const today = dayjs().tz('America/Los_Angeles').startOf('day')
|
||||
const nextWeek = dayjs().tz('America/Los_Angeles').add(7, 'day').endOf('day')
|
||||
|
||||
const response = await apiService.nbaGames.list({
|
||||
startTimestamp: today.valueOf(), // 传递时间戳(毫秒)
|
||||
endTimestamp: nextWeek.valueOf() // 传递时间戳(毫秒)
|
||||
})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setGames(response.data.data.list || [])
|
||||
} else {
|
||||
message.warning('获取比赛列表失败,请稍后重试')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取比赛列表失败')
|
||||
} finally {
|
||||
setLoadingGames(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchGames()
|
||||
if (id) {
|
||||
fetchStrategyDetail(parseInt(id))
|
||||
}
|
||||
}, [id, fetchGames])
|
||||
|
||||
const handleGameSelectionChange = (gameId: string | null) => {
|
||||
setSelectedGameId(gameId)
|
||||
if (gameId) {
|
||||
const selectedGame = games.find(game => game.nbaGameId === gameId)
|
||||
if (selectedGame) {
|
||||
// 自动提取该比赛的两支球队
|
||||
form.setFieldsValue({
|
||||
filterTeams: [selectedGame.homeTeam, selectedGame.awayTeam]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
form.setFieldsValue({
|
||||
filterTeams: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 当games和strategyData都加载完成后,根据filterTeams找到对应的比赛
|
||||
useEffect(() => {
|
||||
if (games.length > 0 && strategyData?.filterTeams && strategyData.filterTeams.length >= 2) {
|
||||
const filterTeams = strategyData.filterTeams as string[]
|
||||
// 查找包含这两支球队的比赛(顺序无关)
|
||||
const matchedGame = games.find(game => {
|
||||
const gameTeams = [game.homeTeam, game.awayTeam]
|
||||
return filterTeams.every(team => gameTeams.includes(team)) &&
|
||||
gameTeams.every(team => filterTeams.includes(team))
|
||||
})
|
||||
if (matchedGame && matchedGame.nbaGameId) {
|
||||
setSelectedGameId(matchedGame.nbaGameId)
|
||||
}
|
||||
}
|
||||
}, [games, strategyData])
|
||||
|
||||
const fetchStrategyDetail = async (strategyId: number) => {
|
||||
setLoadingData(true)
|
||||
try {
|
||||
const response = await apiService.nbaStrategies.detail({ id: strategyId })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const strategy = response.data.data
|
||||
setStrategyData(strategy)
|
||||
|
||||
// 填充表单数据
|
||||
form.setFieldsValue({
|
||||
strategyName: strategy.strategyName,
|
||||
strategyDescription: strategy.strategyDescription,
|
||||
accountId: strategy.accountId,
|
||||
enabled: strategy.enabled,
|
||||
filterTeams: strategy.filterTeams,
|
||||
dateRange: strategy.filterDateFrom && strategy.filterDateTo ? [
|
||||
dayjs(strategy.filterDateFrom),
|
||||
dayjs(strategy.filterDateTo)
|
||||
] : undefined,
|
||||
filterGameImportance: strategy.filterGameImportance,
|
||||
minWinProbabilityDiff: parseFloat(strategy.minWinProbabilityDiff),
|
||||
minWinProbability: strategy.minWinProbability ? parseFloat(strategy.minWinProbability) : undefined,
|
||||
maxWinProbability: strategy.maxWinProbability ? parseFloat(strategy.maxWinProbability) : undefined,
|
||||
minTradeValue: parseFloat(strategy.minTradeValue),
|
||||
minRemainingTime: strategy.minRemainingTime,
|
||||
maxRemainingTime: strategy.maxRemainingTime,
|
||||
minScoreDiff: strategy.minScoreDiff,
|
||||
maxScoreDiff: strategy.maxScoreDiff,
|
||||
buyAmountStrategy: strategy.buyAmountStrategy,
|
||||
fixedBuyAmount: strategy.fixedBuyAmount ? parseFloat(strategy.fixedBuyAmount) : undefined,
|
||||
buyRatio: strategy.buyRatio ? parseFloat(strategy.buyRatio) : undefined,
|
||||
baseBuyAmount: strategy.baseBuyAmount ? parseFloat(strategy.baseBuyAmount) : undefined,
|
||||
buyTiming: strategy.buyTiming,
|
||||
delayBuySeconds: strategy.delayBuySeconds,
|
||||
buyDirection: strategy.buyDirection,
|
||||
enableSell: strategy.enableSell,
|
||||
takeProfitThreshold: strategy.takeProfitThreshold ? parseFloat(strategy.takeProfitThreshold) : undefined,
|
||||
stopLossThreshold: strategy.stopLossThreshold ? parseFloat(strategy.stopLossThreshold) : undefined,
|
||||
probabilityReversalThreshold: strategy.probabilityReversalThreshold ? parseFloat(strategy.probabilityReversalThreshold) : undefined,
|
||||
sellRatio: parseFloat(strategy.sellRatio),
|
||||
sellTiming: strategy.sellTiming,
|
||||
delaySellSeconds: strategy.delaySellSeconds,
|
||||
priceStrategy: strategy.priceStrategy,
|
||||
fixedPrice: strategy.fixedPrice ? parseFloat(strategy.fixedPrice) : undefined,
|
||||
priceOffset: parseFloat(strategy.priceOffset),
|
||||
maxPosition: parseFloat(strategy.maxPosition),
|
||||
minPosition: parseFloat(strategy.minPosition),
|
||||
maxGamePosition: strategy.maxGamePosition ? parseFloat(strategy.maxGamePosition) : undefined,
|
||||
maxDailyLoss: strategy.maxDailyLoss ? parseFloat(strategy.maxDailyLoss) : undefined,
|
||||
maxDailyOrders: strategy.maxDailyOrders,
|
||||
maxDailyProfit: strategy.maxDailyProfit ? parseFloat(strategy.maxDailyProfit) : undefined,
|
||||
priceTolerance: parseFloat(strategy.priceTolerance),
|
||||
minProbabilityThreshold: strategy.minProbabilityThreshold ? parseFloat(strategy.minProbabilityThreshold) : undefined,
|
||||
maxProbabilityThreshold: strategy.maxProbabilityThreshold ? parseFloat(strategy.maxProbabilityThreshold) : undefined,
|
||||
baseStrengthWeight: parseFloat(strategy.baseStrengthWeight),
|
||||
recentFormWeight: parseFloat(strategy.recentFormWeight),
|
||||
lineupIntegrityWeight: parseFloat(strategy.lineupIntegrityWeight),
|
||||
starStatusWeight: parseFloat(strategy.starStatusWeight),
|
||||
environmentWeight: parseFloat(strategy.environmentWeight),
|
||||
matchupAdvantageWeight: parseFloat(strategy.matchupAdvantageWeight),
|
||||
scoreDiffWeight: parseFloat(strategy.scoreDiffWeight),
|
||||
momentumWeight: parseFloat(strategy.momentumWeight),
|
||||
dataUpdateFrequency: strategy.dataUpdateFrequency,
|
||||
analysisFrequency: strategy.analysisFrequency,
|
||||
pushFailedOrders: strategy.pushFailedOrders,
|
||||
pushFrequency: strategy.pushFrequency,
|
||||
batchPushInterval: strategy.batchPushInterval
|
||||
})
|
||||
} else {
|
||||
message.error(response.data.msg || '获取策略详情失败')
|
||||
navigate('/nba/strategies')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取策略详情失败')
|
||||
navigate('/nba/strategies')
|
||||
} finally {
|
||||
setLoadingData(false)
|
||||
}
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{ title: '基本信息', description: '策略名称和账户' },
|
||||
{ title: '触发条件', description: '概率阈值和交易价值' },
|
||||
{ title: '交易规则', description: '买入卖出规则' },
|
||||
{ title: '风险控制', description: '持仓和每日限制' },
|
||||
{ title: '高级配置', description: '算法权重和系统配置' }
|
||||
]
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
setLoading(true)
|
||||
|
||||
const request: NbaQuantitativeStrategyUpdateRequest = {
|
||||
id: parseInt(id!),
|
||||
strategyName: values.strategyName,
|
||||
strategyDescription: values.strategyDescription,
|
||||
enabled: values.enabled,
|
||||
filterTeams: values.filterTeams,
|
||||
filterDateFrom: values.dateRange?.[0]?.format('YYYY-MM-DD'),
|
||||
filterDateTo: values.dateRange?.[1]?.format('YYYY-MM-DD'),
|
||||
filterGameImportance: values.filterGameImportance,
|
||||
minWinProbabilityDiff: values.minWinProbabilityDiff?.toString(),
|
||||
minWinProbability: values.minWinProbability?.toString(),
|
||||
maxWinProbability: values.maxWinProbability?.toString(),
|
||||
minTradeValue: values.minTradeValue?.toString(),
|
||||
minRemainingTime: values.minRemainingTime,
|
||||
maxRemainingTime: values.maxRemainingTime,
|
||||
minScoreDiff: values.minScoreDiff,
|
||||
maxScoreDiff: values.maxScoreDiff,
|
||||
buyAmountStrategy: values.buyAmountStrategy,
|
||||
fixedBuyAmount: values.fixedBuyAmount?.toString(),
|
||||
buyRatio: values.buyRatio?.toString(),
|
||||
baseBuyAmount: values.baseBuyAmount?.toString(),
|
||||
buyTiming: values.buyTiming,
|
||||
delayBuySeconds: values.delayBuySeconds,
|
||||
buyDirection: values.buyDirection,
|
||||
enableSell: values.enableSell,
|
||||
takeProfitThreshold: values.takeProfitThreshold?.toString(),
|
||||
stopLossThreshold: values.stopLossThreshold?.toString(),
|
||||
probabilityReversalThreshold: values.probabilityReversalThreshold?.toString(),
|
||||
sellRatio: values.sellRatio?.toString(),
|
||||
sellTiming: values.sellTiming,
|
||||
delaySellSeconds: values.delaySellSeconds,
|
||||
priceStrategy: values.priceStrategy,
|
||||
fixedPrice: values.fixedPrice?.toString(),
|
||||
priceOffset: values.priceOffset?.toString(),
|
||||
maxPosition: values.maxPosition?.toString(),
|
||||
minPosition: values.minPosition?.toString(),
|
||||
maxGamePosition: values.maxGamePosition?.toString(),
|
||||
maxDailyLoss: values.maxDailyLoss?.toString(),
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
maxDailyProfit: values.maxDailyProfit?.toString(),
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
minProbabilityThreshold: values.minProbabilityThreshold?.toString(),
|
||||
maxProbabilityThreshold: values.maxProbabilityThreshold?.toString(),
|
||||
baseStrengthWeight: values.baseStrengthWeight?.toString(),
|
||||
recentFormWeight: values.recentFormWeight?.toString(),
|
||||
lineupIntegrityWeight: values.lineupIntegrityWeight?.toString(),
|
||||
starStatusWeight: values.starStatusWeight?.toString(),
|
||||
environmentWeight: values.environmentWeight?.toString(),
|
||||
matchupAdvantageWeight: values.matchupAdvantageWeight?.toString(),
|
||||
scoreDiffWeight: values.scoreDiffWeight?.toString(),
|
||||
momentumWeight: values.momentumWeight?.toString(),
|
||||
dataUpdateFrequency: values.dataUpdateFrequency,
|
||||
analysisFrequency: values.analysisFrequency,
|
||||
pushFailedOrders: values.pushFailedOrders,
|
||||
pushFrequency: values.pushFrequency,
|
||||
batchPushInterval: values.batchPushInterval
|
||||
}
|
||||
|
||||
const response = await apiService.nbaStrategies.update(request)
|
||||
if (response.data.code === 0) {
|
||||
message.success('更新策略成功')
|
||||
navigate('/nba/strategies')
|
||||
} else {
|
||||
message.error(response.data.msg || '更新策略失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
const firstErrorField = error.errorFields[0]
|
||||
message.error(`${firstErrorField.name.join('.')}: ${firstErrorField.errors[0]}`)
|
||||
} else {
|
||||
message.error(error.message || '更新策略失败')
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const next = async () => {
|
||||
try {
|
||||
const fields = getFieldsForStep(currentStep)
|
||||
await form.validateFields(fields)
|
||||
setCurrentStep(currentStep + 1)
|
||||
} catch (error) {
|
||||
// 验证失败,不跳转
|
||||
}
|
||||
}
|
||||
|
||||
const prev = () => {
|
||||
setCurrentStep(currentStep - 1)
|
||||
}
|
||||
|
||||
const getFieldsForStep = (step: number): string[] => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return ['strategyName', 'accountId']
|
||||
case 1:
|
||||
return ['minWinProbabilityDiff', 'minTradeValue']
|
||||
case 2:
|
||||
return ['buyAmountStrategy', 'priceStrategy']
|
||||
case 3:
|
||||
return ['maxPosition', 'minPosition']
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const buyAmountStrategy = Form.useWatch('buyAmountStrategy', form)
|
||||
const priceStrategy = Form.useWatch('priceStrategy', form)
|
||||
const buyTiming = Form.useWatch('buyTiming', form)
|
||||
const sellTiming = Form.useWatch('sellTiming', form)
|
||||
const pushFrequency = Form.useWatch('pushFrequency', form)
|
||||
|
||||
if (loadingData) {
|
||||
return <div style={{ padding: 24, textAlign: 'center' }}>加载中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? '16px' : '24px' }}>
|
||||
<Card>
|
||||
<Space style={{ marginBottom: 24 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/nba/strategies')}>
|
||||
返回
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Steps current={currentStep} items={steps} style={{ marginBottom: 32 }} />
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{/* 表单内容与创建页面相同,这里省略,实际应该复用相同的表单组件 */}
|
||||
{/* 为了简化,这里只显示关键部分 */}
|
||||
|
||||
{currentStep === 0 && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="strategyName"
|
||||
label="策略名称"
|
||||
rules={[{ required: true, message: '请输入策略名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入策略名称" maxLength={50} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="strategyDescription"
|
||||
label="策略描述"
|
||||
>
|
||||
<TextArea rows={4} placeholder="请输入策略描述(可选)" maxLength={200} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label="启用状态"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择比赛(可选)"
|
||||
tooltip="选择一场比赛,系统会自动提取该比赛的两支球队作为关注球队"
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择一场比赛"
|
||||
value={selectedGameId}
|
||||
onChange={handleGameSelectionChange}
|
||||
allowClear
|
||||
showSearch
|
||||
filterOption={(input, option) =>
|
||||
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{loadingGames ? (
|
||||
<Option value="loading" disabled>加载中...</Option>
|
||||
) : games.length === 0 ? (
|
||||
<Option value="empty" disabled>暂无比赛数据</Option>
|
||||
) : (
|
||||
games.map(game => (
|
||||
<Option
|
||||
key={game.nbaGameId}
|
||||
value={game.nbaGameId}
|
||||
label={`${game.awayTeam} @ ${game.homeTeam} (${dayjs(game.gameDate).format('MM-DD')}${game.gameTime ? ` ${dayjs(game.gameTime).tz('America/Los_Angeles').format('HH:mm')}` : ''})`}
|
||||
>
|
||||
<div>
|
||||
<span style={{ fontWeight: 500 }}>
|
||||
{game.awayTeam} @ {game.homeTeam}
|
||||
</span>
|
||||
<span style={{ marginLeft: '8px', color: '#999', fontSize: '12px' }}>
|
||||
{dayjs(game.gameDate).format('MM-DD')}
|
||||
{game.gameTime && ` ${dayjs(game.gameTime).tz('America/Los_Angeles').format('HH:mm')}`}
|
||||
</span>
|
||||
{game.gameStatus && (
|
||||
<span style={{ marginLeft: '8px', color: '#666', fontSize: '12px' }}>
|
||||
({game.gameStatus === 'scheduled' ? '未开始' : game.gameStatus === 'active' ? '进行中' : '已结束'})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
{selectedGameId && (
|
||||
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
|
||||
已选择比赛,系统将自动关注该比赛的两支球队
|
||||
</div>
|
||||
)}
|
||||
<Form.Item name="filterTeams" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 其他步骤的表单内容与创建页面相同 */}
|
||||
{/* 为了代码简洁,这里省略,实际应该复用相同的表单组件 */}
|
||||
|
||||
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
||||
<Space>
|
||||
{currentStep > 0 && (
|
||||
<Button onClick={prev}>
|
||||
上一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep < steps.length - 1 && (
|
||||
<Button type="primary" onClick={next}>
|
||||
下一步
|
||||
</Button>
|
||||
)}
|
||||
{currentStep === steps.length - 1 && (
|
||||
<Button type="primary" htmlType="submit" loading={loading} icon={<SaveOutlined />}>
|
||||
保存策略
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NbaQuantitativeStrategyEdit
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Input } from 'antd'
|
||||
import { PlusOutlined, DeleteOutlined, EditOutlined, BarChartOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { NbaQuantitativeStrategy } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
|
||||
const { Option } = Select
|
||||
const { Search } = Input
|
||||
|
||||
const NbaQuantitativeStrategyList: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const { accounts, fetchAccounts } = useAccountStore()
|
||||
const [strategies, setStrategies] = useState<NbaQuantitativeStrategy[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [filters, setFilters] = useState<{
|
||||
accountId?: number
|
||||
enabled?: boolean
|
||||
strategyName?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchStrategies()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStrategies()
|
||||
}, [filters])
|
||||
|
||||
const fetchStrategies = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.nbaStrategies.list({
|
||||
accountId: filters.accountId,
|
||||
enabled: filters.enabled,
|
||||
strategyName: filters.strategyName,
|
||||
page: 1,
|
||||
limit: 100
|
||||
})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setStrategies(response.data.data.list || [])
|
||||
} else {
|
||||
message.error(response.data.msg || '获取策略列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取策略列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleStatus = async (strategy: NbaQuantitativeStrategy) => {
|
||||
try {
|
||||
const response = await apiService.nbaStrategies.update({
|
||||
id: strategy.id!,
|
||||
enabled: !strategy.enabled
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success(strategy.enabled ? '禁用策略成功' : '启用策略成功')
|
||||
fetchStrategies()
|
||||
} else {
|
||||
message.error(response.data.msg || '更新策略状态失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '更新策略状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (strategyId: number) => {
|
||||
try {
|
||||
const response = await apiService.nbaStrategies.delete({ id: strategyId })
|
||||
if (response.data.code === 0) {
|
||||
message.success('删除策略成功')
|
||||
fetchStrategies()
|
||||
} else {
|
||||
message.error(response.data.msg || '删除策略失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '删除策略失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '策略名称',
|
||||
dataIndex: 'strategyName',
|
||||
key: 'strategyName',
|
||||
width: isMobile ? 120 : 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '关联账户',
|
||||
dataIndex: 'accountName',
|
||||
key: 'accountName',
|
||||
width: isMobile ? 100 : 150,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '启用状态',
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
width: 100,
|
||||
render: (enabled: boolean, record: NbaQuantitativeStrategy) => (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => handleToggleStatus(record)}
|
||||
size={isMobile ? 'small' : 'default'}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 180,
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: isMobile ? 150 : 200,
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: NbaQuantitativeStrategy) => (
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/nba/strategies/edit/${record.id}`)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/nba/statistics/${record.id}`)}
|
||||
>
|
||||
统计
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个策略吗?"
|
||||
onConfirm={() => handleDelete(record.id!)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? '16px' : '24px' }}>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => navigate('/nba/strategies/add')}
|
||||
>
|
||||
创建策略
|
||||
</Button>
|
||||
<Select
|
||||
placeholder="选择账户"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.accountId}
|
||||
onChange={(value) => setFilters({ ...filters, accountId: value })}
|
||||
>
|
||||
{accounts.map(account => (
|
||||
<Option key={account.id} value={account.id}>
|
||||
{account.accountName || account.walletAddress}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
placeholder="启用状态"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.enabled}
|
||||
onChange={(value) => setFilters({ ...filters, enabled: value })}
|
||||
>
|
||||
<Option value={true}>已启用</Option>
|
||||
<Option value={false}>已禁用</Option>
|
||||
</Select>
|
||||
<Search
|
||||
placeholder="搜索策略名称"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
onSearch={(value) => setFilters({ ...filters, strategyName: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={strategies}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: isMobile ? 800 : 'auto' }}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NbaQuantitativeStrategyList
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Card, Statistic, Row, Col, Table, DatePicker, Select, Space, message } from 'antd'
|
||||
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { RangePicker } = DatePicker
|
||||
const { Option } = Select
|
||||
|
||||
interface StrategyStatistics {
|
||||
totalSignals: number
|
||||
buySignals: number
|
||||
sellSignals: number
|
||||
successSignals: number
|
||||
failedSignals: number
|
||||
totalProfit: string
|
||||
totalVolume: string
|
||||
successRate: number
|
||||
averageProfit: string
|
||||
}
|
||||
|
||||
const NbaStatistics: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [statistics, setStatistics] = useState<StrategyStatistics | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null)
|
||||
const [timeDimension, setTimeDimension] = useState<'today' | 'week' | 'month' | 'all'>('all')
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchStatistics()
|
||||
}
|
||||
}, [id, dateRange, timeDimension])
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
if (!id) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
let startDate: string | undefined
|
||||
let endDate: string | undefined
|
||||
|
||||
if (dateRange) {
|
||||
startDate = dateRange[0].format('YYYY-MM-DD')
|
||||
endDate = dateRange[1].format('YYYY-MM-DD')
|
||||
} else {
|
||||
// 根据时间维度设置日期范围
|
||||
const now = dayjs()
|
||||
switch (timeDimension) {
|
||||
case 'today':
|
||||
startDate = now.format('YYYY-MM-DD')
|
||||
endDate = now.format('YYYY-MM-DD')
|
||||
break
|
||||
case 'week':
|
||||
startDate = now.subtract(7, 'day').format('YYYY-MM-DD')
|
||||
endDate = now.format('YYYY-MM-DD')
|
||||
break
|
||||
case 'month':
|
||||
startDate = now.subtract(30, 'day').format('YYYY-MM-DD')
|
||||
endDate = now.format('YYYY-MM-DD')
|
||||
break
|
||||
case 'all':
|
||||
// 不设置日期范围
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiService.nbaStatistics.strategy({
|
||||
strategyId: parseInt(id),
|
||||
startDate,
|
||||
endDate
|
||||
})
|
||||
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data = response.data.data
|
||||
const successRate = data.totalSignals > 0
|
||||
? (data.successSignals / data.totalSignals * 100)
|
||||
: 0
|
||||
const averageProfit = data.totalSignals > 0
|
||||
? (parseFloat(data.totalProfit) / data.totalSignals).toString()
|
||||
: '0'
|
||||
|
||||
setStatistics({
|
||||
...data,
|
||||
successRate,
|
||||
averageProfit
|
||||
})
|
||||
} else {
|
||||
message.error(response.data.msg || '获取统计信息失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取统计信息失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getProfitColor = (value: string): string => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return '#666'
|
||||
return num >= 0 ? '#3f8600' : '#cf1322'
|
||||
}
|
||||
|
||||
const getProfitIcon = (value: string) => {
|
||||
const num = parseFloat(value)
|
||||
if (isNaN(num)) return null
|
||||
return num >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? '16px' : '24px' }}>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Space>
|
||||
<Select
|
||||
value={timeDimension}
|
||||
onChange={setTimeDimension}
|
||||
style={{ width: 150 }}
|
||||
>
|
||||
<Option value="today">今日</Option>
|
||||
<Option value="week">本周</Option>
|
||||
<Option value="month">本月</Option>
|
||||
<Option value="all">全部</Option>
|
||||
</Select>
|
||||
<RangePicker
|
||||
value={dateRange}
|
||||
onChange={(dates) => setDateRange(dates as [dayjs.Dayjs, dayjs.Dayjs] | null)}
|
||||
allowClear
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="信号总数"
|
||||
value={statistics?.totalSignals || 0}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="买入信号"
|
||||
value={statistics?.buySignals || 0}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="卖出信号"
|
||||
value={statistics?.sellSignals || 0}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="成功率"
|
||||
value={statistics?.successRate || 0}
|
||||
precision={2}
|
||||
suffix="%"
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总盈亏"
|
||||
value={statistics?.totalProfit || '0'}
|
||||
precision={4}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getProfitColor(statistics?.totalProfit || '0') }}
|
||||
prefix={getProfitIcon(statistics?.totalProfit || '0')}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="平均盈亏"
|
||||
value={statistics?.averageProfit || '0'}
|
||||
precision={4}
|
||||
suffix="USDC"
|
||||
valueStyle={{ color: getProfitColor(statistics?.averageProfit || '0') }}
|
||||
prefix={getProfitIcon(statistics?.averageProfit || '0')}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总交易量"
|
||||
value={statistics?.totalVolume || '0'}
|
||||
precision={4}
|
||||
suffix="USDC"
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="成功信号"
|
||||
value={statistics?.successSignals || 0}
|
||||
loading={loading}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NbaStatistics
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Card, Table, Tag, Select, Input, Space, message, Modal, Descriptions, Button } from 'antd'
|
||||
import { EyeOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { NbaTradingSignal } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
|
||||
const { Option } = Select
|
||||
const { Search } = Input
|
||||
|
||||
const NbaTradingSignals: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [signals, setSignals] = useState<NbaTradingSignal[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
||||
const [selectedSignal, setSelectedSignal] = useState<NbaTradingSignal | null>(null)
|
||||
const [filters, setFilters] = useState<{
|
||||
strategyId?: number
|
||||
signalType?: string
|
||||
signalStatus?: string
|
||||
}>({})
|
||||
|
||||
useEffect(() => {
|
||||
fetchSignals()
|
||||
}, [filters])
|
||||
|
||||
const fetchSignals = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.nbaSignals.list({
|
||||
strategyId: filters.strategyId,
|
||||
signalType: filters.signalType,
|
||||
signalStatus: filters.signalStatus,
|
||||
page: 1,
|
||||
limit: 100
|
||||
})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setSignals(response.data.data.list || [])
|
||||
} else {
|
||||
message.error(response.data.msg || '获取交易信号列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取交易信号列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = async (signal: NbaTradingSignal) => {
|
||||
try {
|
||||
const response = await apiService.nbaSignals.detail({ id: signal.id })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setSelectedSignal(response.data.data)
|
||||
setDetailModalVisible(true)
|
||||
} else {
|
||||
message.error(response.data.msg || '获取信号详情失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取信号详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getSignalTypeColor = (type: string) => {
|
||||
return type === 'BUY' ? 'green' : 'red'
|
||||
}
|
||||
|
||||
const getSignalStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'GENERATED':
|
||||
return 'default'
|
||||
case 'EXECUTING':
|
||||
return 'processing'
|
||||
case 'SUCCESS':
|
||||
return 'success'
|
||||
case 'FAILED':
|
||||
return 'error'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
const getSignalStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'GENERATED':
|
||||
return '已生成'
|
||||
case 'EXECUTING':
|
||||
return '执行中'
|
||||
case 'SUCCESS':
|
||||
return '执行成功'
|
||||
case 'FAILED':
|
||||
return '执行失败'
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '信号类型',
|
||||
dataIndex: 'signalType',
|
||||
key: 'signalType',
|
||||
width: 100,
|
||||
render: (type: string) => (
|
||||
<Tag color={getSignalTypeColor(type)}>{type}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '策略名称',
|
||||
dataIndex: 'strategyName',
|
||||
key: 'strategyName',
|
||||
width: isMobile ? 120 : 150,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '方向',
|
||||
dataIndex: 'direction',
|
||||
key: 'direction',
|
||||
width: 80,
|
||||
render: (direction: string) => (
|
||||
<Tag color={direction === 'YES' ? 'blue' : 'orange'}>{direction}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
width: 100,
|
||||
render: (price: string) => parseFloat(price).toFixed(4)
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'quantity',
|
||||
key: 'quantity',
|
||||
width: 120,
|
||||
render: (quantity: string) => formatUSDC(quantity)
|
||||
},
|
||||
{
|
||||
title: '总金额',
|
||||
dataIndex: 'totalAmount',
|
||||
key: 'totalAmount',
|
||||
width: 120,
|
||||
render: (amount: string) => `${formatUSDC(amount)} USDC`
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'signalStatus',
|
||||
key: 'signalStatus',
|
||||
width: 100,
|
||||
render: (status: string) => (
|
||||
<Tag color={getSignalStatusColor(status)}>{getSignalStatusText(status)}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '生成时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 180,
|
||||
render: (timestamp: number) => new Date(timestamp).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 80,
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: NbaTradingSignal) => (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetail(record)}
|
||||
>
|
||||
详情
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? '16px' : '24px' }}>
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
<Select
|
||||
placeholder="信号类型"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.signalType}
|
||||
onChange={(value) => setFilters({ ...filters, signalType: value })}
|
||||
>
|
||||
<Option value="BUY">买入</Option>
|
||||
<Option value="SELL">卖出</Option>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder="信号状态"
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 150 }}
|
||||
value={filters.signalStatus}
|
||||
onChange={(value) => setFilters({ ...filters, signalStatus: value })}
|
||||
>
|
||||
<Option value="GENERATED">已生成</Option>
|
||||
<Option value="EXECUTING">执行中</Option>
|
||||
<Option value="SUCCESS">执行成功</Option>
|
||||
<Option value="FAILED">执行失败</Option>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={signals}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
scroll={{ x: isMobile ? 1000 : 'auto' }}
|
||||
pagination={{
|
||||
pageSize: 50,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title="信号详情"
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
footer={null}
|
||||
width={isMobile ? '90%' : 800}
|
||||
>
|
||||
{selectedSignal && (
|
||||
<Descriptions column={1} bordered>
|
||||
<Descriptions.Item label="信号ID">{selectedSignal.id}</Descriptions.Item>
|
||||
<Descriptions.Item label="信号类型">
|
||||
<Tag color={getSignalTypeColor(selectedSignal.signalType)}>
|
||||
{selectedSignal.signalType}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="策略名称">{selectedSignal.strategyName || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="方向">
|
||||
<Tag color={selectedSignal.direction === 'YES' ? 'blue' : 'orange'}>
|
||||
{selectedSignal.direction}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="价格">{parseFloat(selectedSignal.price).toFixed(4)}</Descriptions.Item>
|
||||
<Descriptions.Item label="数量">{formatUSDC(selectedSignal.quantity)}</Descriptions.Item>
|
||||
<Descriptions.Item label="总金额">{formatUSDC(selectedSignal.totalAmount)} USDC</Descriptions.Item>
|
||||
<Descriptions.Item label="获胜概率">
|
||||
{selectedSignal.winProbability ? `${(parseFloat(selectedSignal.winProbability) * 100).toFixed(2)}%` : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="交易价值">
|
||||
{selectedSignal.tradeValue ? parseFloat(selectedSignal.tradeValue).toFixed(4) : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="触发原因">{selectedSignal.reason || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={getSignalStatusColor(selectedSignal.signalStatus)}>
|
||||
{getSignalStatusText(selectedSignal.signalStatus)}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
{selectedSignal.executionResult && (
|
||||
<Descriptions.Item label="执行结果">{selectedSignal.executionResult}</Descriptions.Item>
|
||||
)}
|
||||
{selectedSignal.errorMessage && (
|
||||
<Descriptions.Item label="错误信息">
|
||||
<span style={{ color: 'red' }}>{selectedSignal.errorMessage}</span>
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="生成时间">
|
||||
{new Date(selectedSignal.createdAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">
|
||||
{new Date(selectedSignal.updatedAt).toLocaleString()}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NbaTradingSignals
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios'
|
||||
import type { ApiResponse, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest } from '../types'
|
||||
import type {
|
||||
ApiResponse,
|
||||
NotificationConfig,
|
||||
NotificationConfigRequest,
|
||||
NotificationConfigUpdateRequest,
|
||||
NbaGameListResponse,
|
||||
NbaMarketListRequest,
|
||||
NbaMarketListResponse
|
||||
} from '../types'
|
||||
import { getToken, setToken, removeToken } from '../utils'
|
||||
import { wsManager } from './websocket'
|
||||
import i18n from '../i18n/config'
|
||||
@@ -672,6 +680,110 @@ export const apiService = {
|
||||
total?: number
|
||||
}
|
||||
}>>('/announcements/detail', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* NBA 量化策略 API
|
||||
*/
|
||||
nbaStrategies: {
|
||||
/**
|
||||
* 创建策略
|
||||
*/
|
||||
create: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/strategies/create', data),
|
||||
|
||||
/**
|
||||
* 更新策略
|
||||
*/
|
||||
update: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/strategies/update', data),
|
||||
|
||||
/**
|
||||
* 获取策略列表
|
||||
*/
|
||||
list: (data: { accountId?: number; enabled?: boolean; strategyName?: string; page?: number; limit?: number } = {}) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/strategies/list', data),
|
||||
|
||||
/**
|
||||
* 获取策略详情
|
||||
*/
|
||||
detail: (data: { id: number }) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/strategies/detail', data),
|
||||
|
||||
/**
|
||||
* 删除策略
|
||||
*/
|
||||
delete: (data: { id: number }) =>
|
||||
apiClient.post<ApiResponse<void>>('/nba/strategies/delete', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* NBA 交易信号 API
|
||||
*/
|
||||
nbaSignals: {
|
||||
/**
|
||||
* 获取交易信号列表
|
||||
*/
|
||||
list: (data: { strategyId?: number; signalType?: string; signalStatus?: string; page?: number; limit?: number } = {}) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/signals/list', data),
|
||||
|
||||
/**
|
||||
* 获取信号详情
|
||||
*/
|
||||
detail: (data: { id: number }) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/signals/detail', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* NBA 统计 API
|
||||
*/
|
||||
nbaStatistics: {
|
||||
/**
|
||||
* 获取策略统计
|
||||
*/
|
||||
strategy: (data: { strategyId: number; startDate?: string; endDate?: string }) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/statistics/strategy', data),
|
||||
|
||||
/**
|
||||
* 获取总体统计
|
||||
*/
|
||||
overall: (data: { startDate?: string; endDate?: string } = {}) =>
|
||||
apiClient.post<ApiResponse<any>>('/nba/statistics/overall', data)
|
||||
},
|
||||
|
||||
/**
|
||||
* NBA 比赛 API
|
||||
*/
|
||||
nbaGames: {
|
||||
/**
|
||||
* 获取 NBA 比赛列表
|
||||
* 前端传递时间戳(毫秒),后端转换为西8区时间
|
||||
*/
|
||||
list: (data: { startTimestamp?: number; endTimestamp?: number; gameStatus?: string } = {}) =>
|
||||
apiClient.post<ApiResponse<NbaGameListResponse>>('/nba/games/list', data),
|
||||
|
||||
/**
|
||||
* 获取 7 天内的所有球队
|
||||
*/
|
||||
getTeams: () =>
|
||||
apiClient.post<ApiResponse<string[]>>('/nba/games/teams', {})
|
||||
},
|
||||
|
||||
/**
|
||||
* NBA 市场 API
|
||||
*/
|
||||
nbaMarkets: {
|
||||
/**
|
||||
* 获取 NBA 市场列表
|
||||
*/
|
||||
list: (data: NbaMarketListRequest) =>
|
||||
apiClient.post<ApiResponse<NbaMarketListResponse>>('/nba/markets/list', data),
|
||||
|
||||
/**
|
||||
* 从市场中获取球队列表(用于策略配置)
|
||||
*/
|
||||
getTeams: () =>
|
||||
apiClient.post<ApiResponse<string[]>>('/nba/markets/teams', {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -797,3 +797,293 @@ export interface NotificationConfigUpdateRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 量化策略
|
||||
*/
|
||||
export interface NbaQuantitativeStrategy {
|
||||
id: number | null
|
||||
strategyName: string
|
||||
strategyDescription?: string
|
||||
accountId: number
|
||||
accountName?: string
|
||||
enabled: boolean
|
||||
filterTeams?: string[]
|
||||
filterDateFrom?: string
|
||||
filterDateTo?: string
|
||||
filterGameImportance?: string
|
||||
minWinProbabilityDiff: string
|
||||
minWinProbability?: string
|
||||
maxWinProbability?: string
|
||||
minTradeValue: string
|
||||
minRemainingTime?: number
|
||||
maxRemainingTime?: number
|
||||
minScoreDiff?: number
|
||||
maxScoreDiff?: number
|
||||
buyAmountStrategy: string
|
||||
fixedBuyAmount?: string
|
||||
buyRatio?: string
|
||||
baseBuyAmount?: string
|
||||
buyTiming: string
|
||||
delayBuySeconds: number
|
||||
buyDirection: string
|
||||
enableSell: boolean
|
||||
takeProfitThreshold?: string
|
||||
stopLossThreshold?: string
|
||||
probabilityReversalThreshold?: string
|
||||
sellRatio: string
|
||||
sellTiming: string
|
||||
delaySellSeconds: number
|
||||
priceStrategy: string
|
||||
fixedPrice?: string
|
||||
priceOffset: string
|
||||
maxPosition: string
|
||||
minPosition: string
|
||||
maxGamePosition?: string
|
||||
maxDailyLoss?: string
|
||||
maxDailyOrders?: number
|
||||
maxDailyProfit?: string
|
||||
priceTolerance: string
|
||||
minProbabilityThreshold?: string
|
||||
maxProbabilityThreshold?: string
|
||||
baseStrengthWeight: string
|
||||
recentFormWeight: string
|
||||
lineupIntegrityWeight: string
|
||||
starStatusWeight: string
|
||||
environmentWeight: string
|
||||
matchupAdvantageWeight: string
|
||||
scoreDiffWeight: string
|
||||
momentumWeight: string
|
||||
dataUpdateFrequency: number
|
||||
analysisFrequency: number
|
||||
pushFailedOrders: boolean
|
||||
pushFrequency: string
|
||||
batchPushInterval: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 量化策略列表响应
|
||||
*/
|
||||
export interface NbaQuantitativeStrategyListResponse {
|
||||
list: NbaQuantitativeStrategy[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 市场 DTO
|
||||
*/
|
||||
export interface NbaMarketDto {
|
||||
id?: string
|
||||
question?: string
|
||||
conditionId?: string
|
||||
slug?: string
|
||||
description?: string
|
||||
category?: string
|
||||
active?: boolean
|
||||
closed?: boolean
|
||||
archived?: boolean
|
||||
volume?: string
|
||||
liquidity?: string
|
||||
endDate?: string
|
||||
startDate?: string
|
||||
outcomes?: string
|
||||
outcomePrices?: string
|
||||
volumeNum?: number
|
||||
liquidityNum?: number
|
||||
lastTradePrice?: number
|
||||
bestBid?: number
|
||||
bestAsk?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 市场列表请求
|
||||
*/
|
||||
export interface NbaMarketListRequest {
|
||||
active?: boolean
|
||||
closed?: boolean
|
||||
archived?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 市场列表响应
|
||||
*/
|
||||
export interface NbaMarketListResponse {
|
||||
list: NbaMarketDto[]
|
||||
total: number
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 量化策略创建请求
|
||||
*/
|
||||
export interface NbaQuantitativeStrategyCreateRequest {
|
||||
strategyName: string
|
||||
strategyDescription?: string
|
||||
accountId: number
|
||||
enabled?: boolean
|
||||
filterTeams?: string[]
|
||||
filterDateFrom?: string
|
||||
filterDateTo?: string
|
||||
filterGameImportance?: string
|
||||
minWinProbabilityDiff?: string
|
||||
minWinProbability?: string
|
||||
maxWinProbability?: string
|
||||
minTradeValue?: string
|
||||
minRemainingTime?: number
|
||||
maxRemainingTime?: number
|
||||
minScoreDiff?: number
|
||||
maxScoreDiff?: number
|
||||
buyAmountStrategy?: string
|
||||
fixedBuyAmount?: string
|
||||
buyRatio?: string
|
||||
baseBuyAmount?: string
|
||||
buyTiming?: string
|
||||
delayBuySeconds?: number
|
||||
buyDirection?: string
|
||||
enableSell?: boolean
|
||||
takeProfitThreshold?: string
|
||||
stopLossThreshold?: string
|
||||
probabilityReversalThreshold?: string
|
||||
sellRatio?: string
|
||||
sellTiming?: string
|
||||
delaySellSeconds?: number
|
||||
priceStrategy?: string
|
||||
fixedPrice?: string
|
||||
priceOffset?: string
|
||||
maxPosition?: string
|
||||
minPosition?: string
|
||||
maxGamePosition?: string
|
||||
maxDailyLoss?: string
|
||||
maxDailyOrders?: number
|
||||
maxDailyProfit?: string
|
||||
priceTolerance?: string
|
||||
minProbabilityThreshold?: string
|
||||
maxProbabilityThreshold?: string
|
||||
baseStrengthWeight?: string
|
||||
recentFormWeight?: string
|
||||
lineupIntegrityWeight?: string
|
||||
starStatusWeight?: string
|
||||
environmentWeight?: string
|
||||
matchupAdvantageWeight?: string
|
||||
scoreDiffWeight?: string
|
||||
momentumWeight?: string
|
||||
dataUpdateFrequency?: number
|
||||
analysisFrequency?: number
|
||||
pushFailedOrders?: boolean
|
||||
pushFrequency?: string
|
||||
batchPushInterval?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 量化策略更新请求
|
||||
*/
|
||||
export interface NbaQuantitativeStrategyUpdateRequest {
|
||||
id: number
|
||||
strategyName?: string
|
||||
strategyDescription?: string
|
||||
enabled?: boolean
|
||||
filterTeams?: string[]
|
||||
filterDateFrom?: string
|
||||
filterDateTo?: string
|
||||
filterGameImportance?: string
|
||||
minWinProbabilityDiff?: string
|
||||
minWinProbability?: string
|
||||
maxWinProbability?: string
|
||||
minTradeValue?: string
|
||||
minRemainingTime?: number
|
||||
maxRemainingTime?: number
|
||||
minScoreDiff?: number
|
||||
maxScoreDiff?: number
|
||||
buyAmountStrategy?: string
|
||||
fixedBuyAmount?: string
|
||||
buyRatio?: string
|
||||
baseBuyAmount?: string
|
||||
buyTiming?: string
|
||||
delayBuySeconds?: number
|
||||
buyDirection?: string
|
||||
enableSell?: boolean
|
||||
takeProfitThreshold?: string
|
||||
stopLossThreshold?: string
|
||||
probabilityReversalThreshold?: string
|
||||
sellRatio?: string
|
||||
sellTiming?: string
|
||||
delaySellSeconds?: number
|
||||
priceStrategy?: string
|
||||
fixedPrice?: string
|
||||
priceOffset?: string
|
||||
maxPosition?: string
|
||||
minPosition?: string
|
||||
maxGamePosition?: string
|
||||
maxDailyLoss?: string
|
||||
maxDailyOrders?: number
|
||||
maxDailyProfit?: string
|
||||
priceTolerance?: string
|
||||
minProbabilityThreshold?: string
|
||||
maxProbabilityThreshold?: string
|
||||
baseStrengthWeight?: string
|
||||
recentFormWeight?: string
|
||||
lineupIntegrityWeight?: string
|
||||
starStatusWeight?: string
|
||||
environmentWeight?: string
|
||||
matchupAdvantageWeight?: string
|
||||
scoreDiffWeight?: string
|
||||
momentumWeight?: string
|
||||
dataUpdateFrequency?: number
|
||||
analysisFrequency?: number
|
||||
pushFailedOrders?: boolean
|
||||
pushFrequency?: string
|
||||
batchPushInterval?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA 交易信号
|
||||
*/
|
||||
export interface NbaTradingSignal {
|
||||
id: number
|
||||
strategyId: number
|
||||
strategyName?: string
|
||||
gameId?: number
|
||||
marketId?: number
|
||||
signalType: 'BUY' | 'SELL'
|
||||
direction: 'YES' | 'NO'
|
||||
price: string
|
||||
quantity: string
|
||||
totalAmount: string
|
||||
reason?: string
|
||||
winProbability?: string
|
||||
tradeValue?: string
|
||||
signalStatus: 'GENERATED' | 'EXECUTING' | 'SUCCESS' | 'FAILED'
|
||||
executionResult?: string
|
||||
errorMessage?: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface NbaGame {
|
||||
id?: number
|
||||
nbaGameId?: string
|
||||
homeTeam: string
|
||||
awayTeam: string
|
||||
gameDate: string
|
||||
gameTime?: number
|
||||
gameStatus: string
|
||||
homeScore: number
|
||||
awayScore: number
|
||||
period: number
|
||||
timeRemaining?: string
|
||||
polymarketMarketId?: string
|
||||
}
|
||||
|
||||
export interface NbaGameListRequest {
|
||||
startTimestamp?: number // 开始时间戳(毫秒)
|
||||
endTimestamp?: number // 结束时间戳(毫秒)
|
||||
gameStatus?: string
|
||||
}
|
||||
|
||||
export interface NbaGameListResponse {
|
||||
list: NbaGame[]
|
||||
total: number
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user