refactor: 重构市场数据接口和移除敏感信息
- 创建 MarketController,将市场相关接口从 AccountController 移出 - 添加 /api/copy-trading/markets/latest-price 接口供前端获取最新价 - 在 PolymarketClobService 中封装获取订单表和最优价的逻辑 - 支持多元市场(二元、三元及以上)的最优价获取 - 市价单价格添加调整系数(买单+0.01,卖单-0.02) - 移除所有测试文件中的真实私钥和API凭证 - 更新前端API服务,添加markets相关接口
This commit is contained in:
@@ -252,7 +252,7 @@ const PositionList: React.FC = () => {
|
||||
|
||||
// 加载市场价格
|
||||
try {
|
||||
const response = await apiService.accounts.getMarketPrice({ marketId: position.marketId })
|
||||
const response = await apiService.markets.getMarketPrice({ marketId: position.marketId })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setMarketPrice(response.data.data)
|
||||
// 默认使用最优买价作为限价
|
||||
@@ -324,7 +324,8 @@ const PositionList: React.FC = () => {
|
||||
const request: PositionSellRequest = {
|
||||
accountId: selectedPosition.accountId,
|
||||
marketId: selectedPosition.marketId,
|
||||
side: selectedPosition.side as 'YES' | 'NO',
|
||||
side: selectedPosition.side,
|
||||
outcomeIndex: selectedPosition.outcomeIndex, // 传递 outcomeIndex
|
||||
orderType: orderType,
|
||||
quantity: sellQuantity,
|
||||
price: orderType === 'LIMIT' ? limitPrice : undefined
|
||||
@@ -791,70 +792,70 @@ const PositionList: React.FC = () => {
|
||||
|
||||
// 只有当前仓位才显示盈亏和已实现盈亏列
|
||||
if (positionFilter === 'current') {
|
||||
baseColumns.push(
|
||||
{
|
||||
title: '盈亏',
|
||||
dataIndex: 'pnl',
|
||||
key: 'pnl',
|
||||
render: (pnl: string, record: AccountPosition) => {
|
||||
const pnlNum = parseFloat(pnl || '0')
|
||||
const percentPnl = parseFloat(record.percentPnl || '0')
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{pnlNum >= 0 ? '+' : ''}{formatNumber(pnl, 2)} USDC
|
||||
</div>
|
||||
baseColumns.push(
|
||||
{
|
||||
title: '盈亏',
|
||||
dataIndex: 'pnl',
|
||||
key: 'pnl',
|
||||
render: (pnl: string, record: AccountPosition) => {
|
||||
const pnlNum = parseFloat(pnl || '0')
|
||||
const percentPnl = parseFloat(record.percentPnl || '0')
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{pnlNum >= 0 ? '+' : ''}{formatNumber(pnl, 2)} USDC
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
|
||||
}}>
|
||||
{formatPercent(record.percentPnl)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
align: 'right' as const,
|
||||
width: 150,
|
||||
sorter: (a: AccountPosition, b: AccountPosition) => {
|
||||
const pnlA = parseFloat(a.pnl || '0')
|
||||
const pnlB = parseFloat(b.pnl || '0')
|
||||
return pnlA - pnlB
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '已实现盈亏',
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
render: (realizedPnl: string | undefined, record: AccountPosition) => {
|
||||
if (!realizedPnl) return '-'
|
||||
const pnlNum = parseFloat(realizedPnl)
|
||||
const percentPnl = parseFloat(record.percentRealizedPnl || '0')
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{pnlNum >= 0 ? '+' : ''}{formatNumber(realizedPnl, 2)} USDC
|
||||
</div>
|
||||
{record.percentRealizedPnl && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
|
||||
}}>
|
||||
{formatPercent(record.percentPnl)}
|
||||
{formatPercent(record.percentRealizedPnl)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
align: 'right' as const,
|
||||
width: 150,
|
||||
sorter: (a: AccountPosition, b: AccountPosition) => {
|
||||
const pnlA = parseFloat(a.pnl || '0')
|
||||
const pnlB = parseFloat(b.pnl || '0')
|
||||
return pnlA - pnlB
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '已实现盈亏',
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
render: (realizedPnl: string | undefined, record: AccountPosition) => {
|
||||
if (!realizedPnl) return '-'
|
||||
const pnlNum = parseFloat(realizedPnl)
|
||||
const percentPnl = parseFloat(record.percentRealizedPnl || '0')
|
||||
return (
|
||||
<div>
|
||||
<div style={{
|
||||
color: pnlNum >= 0 ? '#3f8600' : '#cf1322',
|
||||
fontWeight: 'bold'
|
||||
}}>
|
||||
{pnlNum >= 0 ? '+' : ''}{formatNumber(realizedPnl, 2)} USDC
|
||||
</div>
|
||||
{record.percentRealizedPnl && (
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: percentPnl >= 0 ? '#3f8600' : '#cf1322'
|
||||
}}>
|
||||
{formatPercent(record.percentRealizedPnl)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
align: 'right' as const,
|
||||
width: 150
|
||||
}
|
||||
)
|
||||
align: 'right' as const,
|
||||
width: 150
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 只有当前仓位才显示操作列
|
||||
@@ -910,7 +911,7 @@ const PositionList: React.FC = () => {
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px', marginBottom: '12px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<h2 style={{ margin: 0 }}>仓位管理</h2>
|
||||
<h2 style={{ margin: 0 }}>仓位管理</h2>
|
||||
{/* WebSocket 连接状态指示器 */}
|
||||
<Tag
|
||||
color={wsConnected ? 'green' : 'orange'}
|
||||
@@ -976,9 +977,9 @@ const PositionList: React.FC = () => {
|
||||
return nameA.localeCompare(nameB, 'zh-CN')
|
||||
})
|
||||
.map(account => ({
|
||||
value: account.id,
|
||||
label: account.accountName || `账户 ${account.id}`
|
||||
}))
|
||||
value: account.id,
|
||||
label: account.accountName || `账户 ${account.id}`
|
||||
}))
|
||||
]}
|
||||
/>
|
||||
<div style={{
|
||||
@@ -988,10 +989,10 @@ const PositionList: React.FC = () => {
|
||||
display: 'inline-flex',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<Radio.Group
|
||||
value={positionFilter}
|
||||
onChange={(e) => setPositionFilter(e.target.value)}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
<Radio.Group
|
||||
value={positionFilter}
|
||||
onChange={(e) => setPositionFilter(e.target.value)}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
style={{ display: 'flex', gap: '4px' }}
|
||||
>
|
||||
<Radio.Button
|
||||
@@ -1027,7 +1028,7 @@ const PositionList: React.FC = () => {
|
||||
{currentCount}
|
||||
</Tag>
|
||||
</span>
|
||||
</Radio.Button>
|
||||
</Radio.Button>
|
||||
<Radio.Button
|
||||
value="historical"
|
||||
style={{
|
||||
@@ -1061,8 +1062,8 @@ const PositionList: React.FC = () => {
|
||||
{historicalCount}
|
||||
</Tag>
|
||||
</span>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1082,20 +1083,20 @@ const PositionList: React.FC = () => {
|
||||
)}
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Table
|
||||
dataSource={filteredPositions}
|
||||
columns={columns}
|
||||
rowKey={(record, index) => `${record.accountId}-${record.marketId}-${index}`}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => `共 ${total} 个仓位${searchKeyword ? `(已过滤)` : ''}`
|
||||
}}
|
||||
scroll={isMobile ? { x: 1500 } : undefined}
|
||||
/>
|
||||
</Card>
|
||||
<Card>
|
||||
<Table
|
||||
dataSource={filteredPositions}
|
||||
columns={columns}
|
||||
rowKey={(record, index) => `${record.accountId}-${record.marketId}-${index}`}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 20,
|
||||
showSizeChanger: !isMobile,
|
||||
showTotal: (total) => `共 ${total} 个仓位${searchKeyword ? `(已过滤)` : ''}`
|
||||
}}
|
||||
scroll={isMobile ? { x: 1500 } : undefined}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 出售模态框 */}
|
||||
|
||||
@@ -105,11 +105,23 @@ export const apiService = {
|
||||
sellPosition: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/positions/sell', data),
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 市场数据 API
|
||||
*/
|
||||
markets: {
|
||||
/**
|
||||
* 获取市场价格
|
||||
* 获取市场价格(通过 Gamma API)
|
||||
*/
|
||||
getMarketPrice: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/markets/price', data)
|
||||
getMarketPrice: (data: { marketId: string }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/markets/price', data),
|
||||
|
||||
/**
|
||||
* 获取最新价(从订单表获取,供前端下单时显示)
|
||||
*/
|
||||
getLatestPrice: (data: { tokenId: string }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/markets/latest-price', data)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -163,7 +163,8 @@ export interface AccountPosition {
|
||||
marketTitle?: string
|
||||
marketSlug?: string
|
||||
marketIcon?: string // 市场图标 URL
|
||||
side: string // YES 或 NO
|
||||
side: string // 结果名称(如 "YES", "NO", "Pakistan" 等)
|
||||
outcomeIndex?: number // 结果索引(0, 1, 2...),用于计算 tokenId
|
||||
quantity: string
|
||||
avgPrice: string
|
||||
currentPrice: string
|
||||
@@ -193,7 +194,8 @@ export interface PositionListResponse {
|
||||
export interface PositionSellRequest {
|
||||
accountId: number
|
||||
marketId: string
|
||||
side: 'YES' | 'NO'
|
||||
side: string // 结果名称(如 "YES", "NO", "Pakistan" 等)
|
||||
outcomeIndex?: number // 结果索引(0, 1, 2...),用于计算 tokenId(推荐提供)
|
||||
orderType: 'MARKET' | 'LIMIT'
|
||||
quantity: string
|
||||
price?: string // 限价订单必需
|
||||
|
||||
Reference in New Issue
Block a user