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:
WrBug
2025-12-13 08:05:08 +08:00
parent 20acc72ed6
commit e9e1111df9
50 changed files with 11274 additions and 7 deletions
+10
View File
@@ -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>} />
+27 -2
View File
@@ -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
}
+4 -1
View File
@@ -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
+231
View File
@@ -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
+282
View File
@@ -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
+113 -1
View File
@@ -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', {})
}
}
+290
View File
@@ -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
}