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 builderApiKeyConfigured: Boolean, // Builder API Key 是否已配置
val builderSecretConfigured: Boolean, // Builder Secret 是否已配置 val builderSecretConfigured: Boolean, // Builder Secret 是否已配置
val builderPassphraseConfigured: Boolean, // Builder Passphrase 是否已配置 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 // 自动赎回(系统级别配置,默认开启) val autoRedeemEnabled: Boolean = true // 自动赎回(系统级别配置,默认开启)
) )
@@ -754,7 +754,28 @@ open class CopyOrderTrackingService(
return 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 totalMatched = BigDecimal.ZERO
var remaining = needMatch var remaining = needMatch
val matchDetails = mutableListOf<SellMatchDetail>() val matchDetails = mutableListOf<SellMatchDetail>()
@@ -769,19 +790,18 @@ open class CopyOrderTrackingService(
if (matchQty.lte(BigDecimal.ZERO)) continue if (matchQty.lte(BigDecimal.ZERO)) continue
// 计算盈亏 // 计算盈亏(使用实际卖出价格)
val buyPrice = order.price.toSafeBigDecimal() val buyPrice = order.price.toSafeBigDecimal()
val sellPrice = leaderSellTrade.price.toSafeBigDecimal()
val realizedPnl = sellPrice.subtract(buyPrice).multi(matchQty) val realizedPnl = sellPrice.subtract(buyPrice).multi(matchQty)
// 创建匹配明细(稍后保存 // 创建匹配明细(使用实际卖出价格
val detail = SellMatchDetail( val detail = SellMatchDetail(
matchRecordId = 0, // 稍后设置 matchRecordId = 0, // 稍后设置
trackingId = order.id!!, trackingId = order.id!!,
buyOrderId = order.buyOrderId, buyOrderId = order.buyOrderId,
matchedQuantity = matchQty, matchedQuantity = matchQty,
buyPrice = buyPrice, buyPrice = buyPrice,
sellPrice = sellPrice, sellPrice = sellPrice, // 使用实际卖出价格,与 SellMatchRecord 保持一致
realizedPnl = realizedPnl realizedPnl = realizedPnl
) )
matchDetails.add(detail) matchDetails.add(detail)
@@ -794,25 +814,6 @@ open class CopyOrderTrackingService(
return 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. 解密私钥(在方法开始时解密一次,后续复用) // 7. 解密私钥(在方法开始时解密一次,后续复用)
val decryptedPrivateKey = decryptPrivateKey(account) val decryptedPrivateKey = decryptPrivateKey(account)
// 8. 创建并签名卖出订单 // 8. 创建并签名卖出订单
@@ -298,8 +298,10 @@ class CopyTradingStatisticsService(
} }
// 卖出统计 // 卖出统计
// 使用 SellMatchDetail 计算总卖出金额,确保准确性
// 因为每个明细都记录了准确的匹配数量和卖出价格
val totalSellQuantity = sellRecords.sumOf { it.totalMatchedQuantity.toSafeBigDecimal() } 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() val totalSellOrders = sellRecords.size.toLong()
// 持仓统计 // 持仓统计
@@ -36,10 +36,38 @@ class SystemConfigService(
val builderPassphrase = getConfigValue(CONFIG_KEY_BUILDER_PASSPHRASE) val builderPassphrase = getConfigValue(CONFIG_KEY_BUILDER_PASSPHRASE)
val autoRedeem = isAutoRedeemEnabled() 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( return SystemConfigDto(
builderApiKeyConfigured = builderApiKey != null, builderApiKeyConfigured = builderApiKey != null,
builderSecretConfigured = builderSecret != null, builderSecretConfigured = builderSecret != null,
builderPassphraseConfigured = builderPassphrase != null, builderPassphraseConfigured = builderPassphrase != null,
builderApiKeyDisplay = builderApiKeyDisplay,
builderSecretDisplay = builderSecretDisplay,
builderPassphraseDisplay = builderPassphraseDisplay,
autoRedeemEnabled = autoRedeem autoRedeemEnabled = autoRedeem
) )
} }
+1 -18
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import { Card, Table, Button, Space, Tag, Popconfirm, message, List, Empty, Spin, Divider, Typography } from 'antd' 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 { useTranslation } from 'react-i18next'
import { apiService } from '../services/api' import { apiService } from '../services/api'
import type { Leader } from '../types' import type { Leader } from '../types'
@@ -225,23 +225,6 @@ const LeaderList: React.FC = () => {
</div> </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' }} /> <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) { if (response.data.code === 0 && response.data.data) {
const config = response.data.data const config = response.data.data
setSystemConfig(config) setSystemConfig(config)
// 将已配置的值填充到输入框中
relayerForm.setFieldsValue({ relayerForm.setFieldsValue({
builderApiKey: '', builderApiKey: config.builderApiKeyDisplay || '',
builderSecret: '', builderSecret: config.builderSecretDisplay || '',
builderPassphrase: '', builderPassphrase: config.builderPassphraseDisplay || '',
}) })
autoRedeemForm.setFieldsValue({ autoRedeemForm.setFieldsValue({
autoRedeemEnabled: config.autoRedeemEnabled autoRedeemEnabled: config.autoRedeemEnabled
@@ -685,30 +686,32 @@ const SystemSettings: React.FC = () => {
<Form.Item <Form.Item
label={t('builderApiKey.apiKey')} label={t('builderApiKey.apiKey')}
name="builderApiKey" name="builderApiKey"
help={systemConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')}
> >
<Input <Input
placeholder={systemConfig?.builderApiKeyConfigured ? t('builderApiKey.apiKeyHelp') : t('builderApiKey.apiKeyPlaceholder')} placeholder={t('builderApiKey.apiKeyPlaceholder')}
style={{ fontFamily: 'monospace' }}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('builderApiKey.secret')} label={t('builderApiKey.secret')}
name="builderSecret" name="builderSecret"
help={systemConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')}
> >
<Input <Input.Password
placeholder={systemConfig?.builderSecretConfigured ? t('builderApiKey.secretHelp') : t('builderApiKey.secretPlaceholder')} placeholder={t('builderApiKey.secretPlaceholder')}
style={{ fontFamily: 'monospace' }}
iconRender={(visible) => (visible ? <span>👁</span> : <span>👁🗨</span>)}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={t('builderApiKey.passphrase')} label={t('builderApiKey.passphrase')}
name="builderPassphrase" name="builderPassphrase"
help={systemConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')}
> >
<Input <Input.Password
placeholder={systemConfig?.builderPassphraseConfigured ? t('builderApiKey.passphraseHelp') : t('builderApiKey.passphrasePlaceholder')} placeholder={t('builderApiKey.passphrasePlaceholder')}
style={{ fontFamily: 'monospace' }}
iconRender={(visible) => (visible ? <span>👁</span> : <span>👁🗨</span>)}
/> />
</Form.Item> </Form.Item>
+3
View File
@@ -770,6 +770,9 @@ export interface SystemConfig {
builderApiKeyConfigured: boolean builderApiKeyConfigured: boolean
builderSecretConfigured: boolean builderSecretConfigured: boolean
builderPassphraseConfigured: boolean builderPassphraseConfigured: boolean
builderApiKeyDisplay?: string // Builder API Key 显示值(部分显示)
builderSecretDisplay?: string // Builder Secret 显示值(部分显示)
builderPassphraseDisplay?: string // Builder Passphrase 显示值(部分显示)
autoRedeemEnabled: boolean // 自动赎回(系统级别配置,默认开启) autoRedeemEnabled: boolean // 自动赎回(系统级别配置,默认开启)
} }