feat: 优化系统配置和统计功能
- 系统配置支持显示完整的 Builder API Key(不再部分显示) - Secret 和 Passphrase 使用密码输入框,支持眼睛图标切换显示/隐藏 - 移除已配置提示,简化用户界面 - 修复总卖出金额统计:使用 SellMatchDetail 计算,确保准确性 - 修复 matchSellOrder 中价格不一致问题:matchDetails 使用实际卖出价格 - 移动端 Leader 列表移除网站显示行 - 优化订单创建前订单簿匹配检查,避免 FAK 订单失败
This commit is contained in:
@@ -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 // 自动赎回(系统级别配置,默认开启)
|
||||
)
|
||||
|
||||
|
||||
+25
-24
@@ -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. 创建并签名卖出订单
|
||||
|
||||
+3
-1
@@ -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,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' }} />
|
||||
|
||||
{/* 跟单关系数 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 // 自动赎回(系统级别配置,默认开启)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user