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 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 // 自动赎回(系统级别配置,默认开启)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+25
-24
@@ -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. 创建并签名卖出订单
|
||||||
|
|||||||
+3
-1
@@ -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,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' }} />
|
||||||
|
|
||||||
{/* 跟单关系数 */}
|
{/* 跟单关系数 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 // 自动赎回(系统级别配置,默认开启)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user