feat: 优化系统配置和统计功能

- 系统配置支持显示完整的 Builder API Key(不再部分显示)
- Secret 和 Passphrase 使用密码输入框,支持眼睛图标切换显示/隐藏
- 移除已配置提示,简化用户界面
- 修复总卖出金额统计:使用 SellMatchDetail 计算,确保准确性
- 修复 matchSellOrder 中价格不一致问题:matchDetails 使用实际卖出价格
- 移动端 Leader 列表移除网站显示行
- 优化订单创建前订单簿匹配检查,避免 FAK 订单失败
This commit is contained in:
WrBug
2025-12-11 03:17:41 +08:00
parent 0625c62e36
commit 71d0a10c21
7 changed files with 77 additions and 54 deletions
@@ -36,6 +36,9 @@ data class SystemConfigDto(
val builderApiKeyConfigured: Boolean, // Builder API Key 是否已配置
val builderSecretConfigured: Boolean, // Builder Secret 是否已配置
val builderPassphraseConfigured: Boolean, // Builder Passphrase 是否已配置
val builderApiKeyDisplay: String? = null, // Builder API Key 显示值(部分显示,用于前端展示)
val builderSecretDisplay: String? = null, // Builder Secret 显示值(部分显示,用于前端展示)
val builderPassphraseDisplay: String? = null, // Builder Passphrase 显示值(部分显示,用于前端展示)
val autoRedeemEnabled: Boolean = true // 自动赎回(系统级别配置,默认开启)
)
@@ -754,7 +754,28 @@ open class CopyOrderTrackingService(
return
}
// 4. 按FIFO顺序匹配,计算实际可以卖出的数量
// 4. 获取tokenId(直接使用outcomeIndex,支持多元市场)
val tokenIdResult = blockchainService.getTokenId(leaderSellTrade.market, leaderSellTrade.outcomeIndex)
if (tokenIdResult.isFailure) {
logger.error("获取tokenId失败: market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
return
}
val tokenId = tokenIdResult.getOrNull() ?: return
// 5. 计算卖出价格(优先使用订单簿 bestBid,失败则使用 Leader 价格,固定按90%计算)
// 注意:需要先计算卖出价格,因为后续创建 matchDetails 需要使用实际卖出价格
val leaderPrice = leaderSellTrade.price.toSafeBigDecimal()
val sellPrice = runCatching {
clobService.getOrderbookByTokenId(tokenId)
.getOrNull()
?.let { calculateMarketSellPrice(it) }
}
.onFailure { e -> logger.warn("获取订单簿或计算 bestBid 失败,使用 Leader 价格: tokenId=$tokenId, error=${e.message}") }
.getOrNull()
?: calculateFallbackSellPrice(leaderPrice)
// 6. 按FIFO顺序匹配,计算实际可以卖出的数量
// 使用计算出的实际卖出价格(而不是 Leader 价格)来创建匹配明细
var totalMatched = BigDecimal.ZERO
var remaining = needMatch
val matchDetails = mutableListOf<SellMatchDetail>()
@@ -769,19 +790,18 @@ open class CopyOrderTrackingService(
if (matchQty.lte(BigDecimal.ZERO)) continue
// 计算盈亏
// 计算盈亏(使用实际卖出价格)
val buyPrice = order.price.toSafeBigDecimal()
val sellPrice = leaderSellTrade.price.toSafeBigDecimal()
val realizedPnl = sellPrice.subtract(buyPrice).multi(matchQty)
// 创建匹配明细(稍后保存
// 创建匹配明细(使用实际卖出价格
val detail = SellMatchDetail(
matchRecordId = 0, // 稍后设置
trackingId = order.id!!,
buyOrderId = order.buyOrderId,
matchedQuantity = matchQty,
buyPrice = buyPrice,
sellPrice = sellPrice,
sellPrice = sellPrice, // 使用实际卖出价格,与 SellMatchRecord 保持一致
realizedPnl = realizedPnl
)
matchDetails.add(detail)
@@ -794,25 +814,6 @@ open class CopyOrderTrackingService(
return
}
// 5. 获取tokenId(直接使用outcomeIndex,支持多元市场)
val tokenIdResult = blockchainService.getTokenId(leaderSellTrade.market, leaderSellTrade.outcomeIndex)
if (tokenIdResult.isFailure) {
logger.error("获取tokenId失败: market=${leaderSellTrade.market}, outcomeIndex=${leaderSellTrade.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
return
}
val tokenId = tokenIdResult.getOrNull() ?: return
// 6. 计算卖出价格(优先使用订单簿 bestBid,失败则使用 Leader 价格,固定按90%计算)
val leaderPrice = leaderSellTrade.price.toSafeBigDecimal()
val sellPrice = runCatching {
clobService.getOrderbookByTokenId(tokenId)
.getOrNull()
?.let { calculateMarketSellPrice(it) }
}
.onFailure { e -> logger.warn("获取订单簿或计算 bestBid 失败,使用 Leader 价格: tokenId=$tokenId, error=${e.message}") }
.getOrNull()
?: calculateFallbackSellPrice(leaderPrice)
// 7. 解密私钥(在方法开始时解密一次,后续复用)
val decryptedPrivateKey = decryptPrivateKey(account)
// 8. 创建并签名卖出订单
@@ -298,8 +298,10 @@ class CopyTradingStatisticsService(
}
// 卖出统计
// 使用 SellMatchDetail 计算总卖出金额,确保准确性
// 因为每个明细都记录了准确的匹配数量和卖出价格
val totalSellQuantity = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal() }
val totalSellAmount = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal().multi(it.sellPrice) }
val totalSellAmount = matchDetails.sumOf { it.matchedQuantity.toSafeBigDecimal().multi(it.sellPrice) }
val totalSellOrders = sellRecords.size.toLong()
// 持仓统计
@@ -36,10 +36,38 @@ class SystemConfigService(
val builderPassphrase = getConfigValue(CONFIG_KEY_BUILDER_PASSPHRASE)
val autoRedeem = isAutoRedeemEnabled()
// 获取完整的 API Key(用于前端展示)
val builderApiKeyDisplay = builderApiKey?.let {
try {
cryptoUtils.decrypt(it)
} catch (e: Exception) {
null
}
}
val builderSecretDisplay = builderSecret?.let {
try {
cryptoUtils.decrypt(it)
} catch (e: Exception) {
null
}
}
val builderPassphraseDisplay = builderPassphrase?.let {
try {
cryptoUtils.decrypt(it)
} catch (e: Exception) {
null
}
}
return SystemConfigDto(
builderApiKeyConfigured = builderApiKey != null,
builderSecretConfigured = builderSecret != null,
builderPassphraseConfigured = builderPassphrase != null,
builderApiKeyDisplay = builderApiKeyDisplay,
builderSecretDisplay = builderSecretDisplay,
builderPassphraseDisplay = builderPassphraseDisplay,
autoRedeemEnabled = autoRedeem
)
}
+1 -18
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider, Typography } from 'antd'
import { PlusOutlined, EditOutlined, DeleteOutlined, LinkOutlined, GlobalOutlined } from '@ant-design/icons'
import { PlusOutlined, EditOutlined, DeleteOutlined, GlobalOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { apiService } from '../services/api'
import type { Leader } from '../types'
@@ -225,23 +225,6 @@ const LeaderList: React.FC = () => {
</div>
)}
{/* 网站 */}
{leader.website && (
<div style={{ marginBottom: '12px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
{t('leaderList.website') || '网站'}
</Text>
<a
href={leader.website}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: '12px', marginLeft: '4px' }}
>
<LinkOutlined /> {leader.website}
</a>
</div>
)}
<Divider style={{ margin: '12px 0' }} />
{/* 跟单关系数 */}
+14 -11
View File
@@ -348,10 +348,11 @@ const SystemSettings: React.FC = () => {
if (response.data.code === 0 && response.data.data) {
const config = response.data.data
setSystemConfig(config)
// 将已配置的值填充到输入框中
relayerForm.setFieldsValue({
builderApiKey: '',
builderSecret: '',
builderPassphrase: '',
builderApiKey: config.builderApiKeyDisplay || '',
builderSecret: config.builderSecretDisplay || '',
builderPassphrase: config.builderPassphraseDisplay || '',
})
autoRedeemForm.setFieldsValue({
autoRedeemEnabled: config.autoRedeemEnabled
@@ -685,30 +686,32 @@ const SystemSettings: React.FC = () => {
<Form.Item
label={t('builderApiKey.apiKey')}
name="builderApiKey"
help={systemConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
>
<Input
placeholder={systemConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
placeholder={t('builderApiKey.apiKeyPlaceholder')}
style={{ fontFamily: 'monospace' }}
/>
</Form.Item>
<Form.Item
label={t('builderApiKey.secret')}
name="builderSecret"
help={systemConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
>
<Input
placeholder={systemConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
<Input.Password
placeholder={t('builderApiKey.secretPlaceholder')}
style={{ fontFamily: 'monospace' }}
iconRender={(visible) => (visible ? <span>👁</span> : <span>👁🗨</span>)}
/>
</Form.Item>
<Form.Item
label={t('builderApiKey.passphrase')}
name="builderPassphrase"
help={systemConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
>
<Input
placeholder={systemConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
<Input.Password
placeholder={t('builderApiKey.passphrasePlaceholder')}
style={{ fontFamily: 'monospace' }}
iconRender={(visible) => (visible ? <span>👁</span> : <span>👁🗨</span>)}
/>
</Form.Item>
+3
View File
@@ -770,6 +770,9 @@ export interface SystemConfig {
builderApiKeyConfigured: boolean
builderSecretConfigured: boolean
builderPassphraseConfigured: boolean
builderApiKeyDisplay?: string // Builder API Key 显示值(部分显示)
builderSecretDisplay?: string // Builder Secret 显示值(部分显示)
builderPassphraseDisplay?: string // Builder Passphrase 显示值(部分显示)
autoRedeemEnabled: boolean // 自动赎回(系统级别配置,默认开启)
}