Compare commits

..

8 Commits

Author SHA1 Message Date
WrBug 662aa47de6 refactor: 优化匹配关系列表的订单ID显示
- 将'卖出订单ID'和'买入订单ID'两列合并为一列'订单ID'
- 第一行显示买入订单ID,第二行显示卖出订单ID
- 添加'买入'和'卖出'标签区分两种订单ID
- 保持复制按钮功能
- 优化移动端和桌面端的显示效果
- 桌面端列宽从120+180调整为150+200以容纳更多内容
2026-01-20 23:19:05 +08:00
WrBug c3d9d10d5d fix: 修复卖出订单标签页的多语言键值错误
- 修复分组标签使用错误的键值:allFullyMatched -> allFullySold
- 修复筛选器选项使用错误的键值:statusPartiallyMatched -> partiallySold,statusFullyMatched -> allFullySold
- 确保中文环境显示正确的中文文案:'全部成交' -> '全部卖出','部分成交' -> '部分卖出'
- 与买入订单标签页的键值使用保持一致
2026-01-20 23:11:54 +08:00
WrBug 9926533049 fix: 修复多语言配置中的键值重复和缺失问题
- 修复 copyTradingOrders 对象中重复的键值
- 统一键名规范:allFullyMatched -> allFullySold, partiallyMatched -> partiallySold
- 添加缺失的 filterMarketTitle 键值
- 确保中英文和繁体中文的键值配置一致
- 修复所有语言文件的 JSON 格式(添加换行符)
2026-01-20 22:57:17 +08:00
WrBug 4d72017b97 fix: 优化匹配关系列表页的显示和交互
- 将'查询'按钮改为'刷新'按钮,文案更准确
- 在表格中新增'市场'列,显示市场标题和ID
- 为移动端和桌面端的市场标题添加跳转到 Polymarket 的链接
- 导入 getPolymarketUrl 工具函数支持市场跳转
- 优化用户体验,便于快速查看市场详情
2026-01-20 11:48:14 +08:00
WrBug 45734c051e fix: 优化买入订单分组的卖出状态文案显示
- 当完全成交数为0时,显示'未卖出'而不是'部分成交 (0/X)'
- 统一状态文案:'部分成交'改为'部分卖出','全部成交'改为'全部卖出'
- 更新中英文和繁体中文的多语言配置
- 优化筛选选项中的状态文案,与分组显示保持一致
2026-01-20 11:44:33 +08:00
WrBug db8471bb16 fix: 统一卖出订单和买入订单的市场分组排序规则
- 修改卖出订单的市场分组排序逻辑
- 按照该市场最近的卖出订单时间倒序排序(与买入订单保持一致)
- 替换原有的按订单数量倒序排序规则
- 优化分组显示的用户体验,便于快速定位活跃市场
2026-01-20 11:38:41 +08:00
WrBug 0dc6f5894f refactor: 优化最大仓位价值检查逻辑,按市场+方向分别限制
- 修改 maxPositionValue 检查逻辑,从按市场累计改为按市场+方向(outcomeIndex)分别计算
- 新增 Repository 方法 sumCurrentPositionValueByMarketAndOutcomeIndex 支持按方向统计
- 移除未使用的 sumCurrentPositionValueByMarket 和 existsByCopyTradingIdAndMarketIdAndRemainingQuantityGreaterThan 方法
- 更新 checkPositionLimits 方法,增加 outcomeIndex 参数
- 更新 checkFilters 方法,传递 outcomeIndex 参数
- 更新日志和注释,明确说明按市场+方向检查仓位

说明:同一市场的 YES 和 NO 方向现在分别受 maxPositionValue 限制,不再累加计算
2026-01-20 11:15:38 +08:00
WrBug c9769aa17a fix: 修复比例模式计算数量错误
- 移除 copyRatio 多余的除以 100 操作
- copyRatio 字段存储的是倍数值(如 1.3 表示 130%),不需要再转换
- 影响:买入数量计算、固定金额模式卖出数量计算
- 修复示例:配置 130% 后,Leader 买入 6.25 份,跟单数量应为 8.125 而不是 0.08125
2026-01-20 10:45:03 +08:00
11 changed files with 1603 additions and 165 deletions
@@ -75,15 +75,11 @@ interface CopyOrderTrackingRepository : JpaRepository<CopyOrderTracking, Long> {
fun countActivePositions(copyTradingId: Long): Int
/**
* 检查指定市场是否存在活跃仓位
* 计算指定跟单配置、市场和方向下的当前持仓总价值 (成本价计算)
* 按市场+方向(outcomeIndex)分别统计
*/
fun existsByCopyTradingIdAndMarketIdAndRemainingQuantityGreaterThan(copyTradingId: Long, marketId: String, remainingQuantity: BigDecimal): Boolean
/**
* 计算指定跟单配置和市场下的当前持仓总价值 (成本价计算)
*/
@Query("SELECT SUM(t.remainingQuantity * t.price) FROM CopyOrderTracking t WHERE t.copyTradingId = :copyTradingId AND t.marketId = :marketId AND t.remainingQuantity > 0")
fun sumCurrentPositionValueByMarket(copyTradingId: Long, marketId: String): BigDecimal?
@Query("SELECT SUM(t.remainingQuantity * t.price) FROM CopyOrderTracking t WHERE t.copyTradingId = :copyTradingId AND t.marketId = :marketId AND t.outcomeIndex = :outcomeIndex AND t.remainingQuantity > 0")
fun sumCurrentPositionValueByMarketAndOutcomeIndex(copyTradingId: Long, marketId: String, outcomeIndex: Int): BigDecimal?
/**
* 查询指定跟单配置下,创建时间超过指定时间点的未匹配订单(FIFO顺序)
@@ -45,7 +45,8 @@ class CopyTradingFilterService(
copyOrderAmount: BigDecimal? = null, // 跟单金额(USDC),用于仓位检查
marketId: String? = null, // 市场ID,用于仓位检查(按市场过滤仓位)
marketTitle: String? = null, // 市场标题,用于关键字过滤
marketEndDate: Long? = null // 市场截止时间,用于市场截止时间检查
marketEndDate: Long? = null, // 市场截止时间,用于市场截止时间检查
outcomeIndex: Int? = null // 方向索引(0, 1, 2, ...),用于按市场+方向检查仓位
): FilterResult {
// 1. 关键字过滤检查(如果配置了关键字过滤)
if (copyTrading.keywordFilterMode != null && copyTrading.keywordFilterMode != "DISABLED") {
@@ -79,7 +80,7 @@ class CopyTradingFilterService(
if (!needOrderbook) {
// 仓位检查(如果配置了最大仓位限制且提供了跟单金额和市场ID)
if (copyOrderAmount != null && marketId != null) {
val positionCheck = checkPositionLimits(copyTrading, copyOrderAmount, marketId)
val positionCheck = checkPositionLimits(copyTrading, copyOrderAmount, marketId, outcomeIndex)
if (!positionCheck.isPassed) {
return positionCheck
}
@@ -116,7 +117,7 @@ class CopyTradingFilterService(
// 7. 仓位检查(如果配置了最大仓位限制且提供了跟单金额和市场ID)
if (copyOrderAmount != null && marketId != null) {
val positionCheck = checkPositionLimits(copyTrading, copyOrderAmount, marketId)
val positionCheck = checkPositionLimits(copyTrading, copyOrderAmount, marketId, outcomeIndex)
if (!positionCheck.isPassed) {
return positionCheck
}
@@ -291,87 +292,104 @@ class CopyTradingFilterService(
}
/**
* 检查仓位限制(按市场检查)
* 检查仓位限制(按市场+方向检查)
* @param copyTrading 跟单配置
* @param copyOrderAmount 跟单金额(USDC
* @param marketId 市场ID,用于过滤该市场的仓位
* @param outcomeIndex 方向索引(0, 1, 2, ...),用于按市场+方向检查仓位
* @return 过滤结果
*/
private suspend fun checkPositionLimits(
copyTrading: CopyTrading,
copyOrderAmount: BigDecimal,
marketId: String
marketId: String,
outcomeIndex: Int?
): FilterResult {
// 如果未配置仓位限制,直接通过
if (copyTrading.maxPositionValue == null && copyTrading.maxPositionCount == null) {
return FilterResult.passed()
}
try {
// 获取账户的所有仓位信息
val positionsResult = accountService.getAllPositions()
if (positionsResult.isFailure) {
logger.warn("获取仓位信息失败,跳过仓位检查: accountId=${copyTrading.accountId}, marketId=$marketId, error=${positionsResult.exceptionOrNull()?.message}")
logger.warn("获取仓位信息失败,跳过仓位检查: accountId=${copyTrading.accountId}, marketId=$marketId, outcomeIndex=$outcomeIndex, error=${positionsResult.exceptionOrNull()?.message}")
// 如果获取仓位失败,为了安全起见,不通过检查
return FilterResult.maxPositionValueFailed("获取仓位信息失败,无法进行仓位检查")
}
val positions = positionsResult.getOrNull() ?: return FilterResult.maxPositionValueFailed("仓位信息为空")
// 过滤出当前账户且该市场的仓位
val marketPositions = positions.currentPositions.filter {
val marketPositions = positions.currentPositions.filter {
it.accountId == copyTrading.accountId && it.marketId == marketId
}
// 检查最大仓位金额(如果配置了)
if (copyTrading.maxPositionValue != null) {
// 比较数据库成本价(本地订单记录)和外部持仓市值(可能来自其他终端的操作),取最大值
val dbValue = copyOrderTrackingRepository.sumCurrentPositionValueByMarket(copyTrading.id!!, marketId) ?: BigDecimal.ZERO
val extValue = marketPositions.sumOf { it.currentValue.toSafeBigDecimal() }
if (copyTrading.maxPositionValue != null && outcomeIndex != null) {
// 按市场+方向(outcomeIndex)分别计算数据库成本价
val dbValue = copyOrderTrackingRepository.sumCurrentPositionValueByMarketAndOutcomeIndex(
copyTrading.id!!, marketId, outcomeIndex
) ?: BigDecimal.ZERO
// 外部持仓也需要按方向过滤,但由于外部持仓可能没有 outcomeIndex 信息,这里保守处理:
// 如果外部持仓存在,取该市场的所有外部持仓市值(与数据库取最大值)
val extValue = if (marketPositions.isNotEmpty()) {
marketPositions.sumOf { it.currentValue.toSafeBigDecimal() }
} else {
BigDecimal.ZERO
}
// 取数据库值和外部持仓值的最大值
val currentPositionValue = dbValue.max(extValue)
// 检查:该市场的当前仓位 + 跟单金额 <= 最大仓位金额
// 检查:该市场该方向的当前仓位 + 跟单金额 <= 最大仓位金额
val totalValueAfterOrder = currentPositionValue.add(copyOrderAmount)
if (totalValueAfterOrder.gt(copyTrading.maxPositionValue)) {
return FilterResult.maxPositionValueFailed(
"超过最大仓位金额限制: 当前该市场仓位(取最大值)=${currentPositionValue} USDC (DB=${dbValue}, Ext=${extValue}), 跟单金额=${copyOrderAmount} USDC, 总计=${totalValueAfterOrder} USDC > 最大限制=${copyTrading.maxPositionValue} USDC"
"超过最大仓位金额限制: 市场=$marketId, 方向=$outcomeIndex, 当前仓位(取最大值)=${currentPositionValue} USDC (DB=${dbValue}, Ext=${extValue}), 跟单金额=${copyOrderAmount} USDC, 总计=${totalValueAfterOrder} USDC > 最大限制=${copyTrading.maxPositionValue} USDC"
)
}
}
// 检查最大仓位数量(如果配置了)
if (copyTrading.maxPositionCount != null) {
// 使用数据库中的订单记录计算活跃仓位数量(解决延迟问题)
val dbCount = copyOrderTrackingRepository.countActivePositions(copyTrading.id!!)
// 计算外部持仓中的唯一市场数量(防止遗漏非本项目创建的仓位)
val extCount = positions.currentPositions
.filter { it.accountId == copyTrading.accountId }
.map { it.marketId }
.distinct()
.size
val currentPositionCount = maxOf(dbCount, extCount)
// 检查:如果当前没有该市场的活跃仓位,且总仓位数量已达到限制,则不允许开新仓
// 判断当前市场是否已有活跃仓位(数据库或外部持仓
val hasDbPosition = copyOrderTrackingRepository.existsByCopyTradingIdAndMarketIdAndRemainingQuantityGreaterThan(
copyTrading.id, marketId, BigDecimal.ZERO
)
// 检查:如果当前没有该市场该方向的活跃仓位,且总仓位数量已达到限制,则不允许开新仓
// 判断当前市场该方向是否已有活跃仓位(数据库)
val hasDbPosition = if (outcomeIndex != null) {
copyOrderTrackingRepository.findUnmatchedBuyOrdersByOutcomeIndex(
copyTrading.id, marketId, outcomeIndex
).isNotEmpty()
} else {
false
}
val hasExtPosition = marketPositions.isNotEmpty()
val hasCurrentMarketPosition = hasDbPosition || hasExtPosition
if (!hasCurrentMarketPosition && currentPositionCount >= copyTrading.maxPositionCount) {
return FilterResult.maxPositionCountFailed(
"超过最大仓位数量限制: 当前活跃仓位总数(取最大值)=${currentPositionCount} (DB=${dbCount}, Ext=${extCount}) >= 最大限制=${copyTrading.maxPositionCount}"
)
}
}
return FilterResult.passed()
} catch (e: Exception) {
logger.error("仓位检查异常: accountId=${copyTrading.accountId}, marketId=$marketId, error=${e.message}", e)
logger.error("仓位检查异常: accountId=${copyTrading.accountId}, marketId=$marketId, outcomeIndex=$outcomeIndex, error=${e.message}", e)
// 如果检查异常,为了安全起见,不通过检查
return FilterResult.maxPositionValueFailed("仓位检查异常: ${e.message}")
}
@@ -302,7 +302,7 @@ open class CopyOrderTrackingService(
// 过滤条件检查(在计算订单参数之前)
// 传入 Leader 交易价格,用于价格区间检查
// 传入跟单金额和市场ID,用于仓位检查(按市场检查仓位)
// 传入跟单金额和市场ID,用于仓位检查(按市场+方向检查仓位)
// 传入市场标题,用于关键字过滤
// 传入市场截止时间,用于市场截止时间检查
// 订单簿只请求一次,返回给后续逻辑使用
@@ -313,7 +313,8 @@ open class CopyOrderTrackingService(
copyOrderAmount = copyOrderAmount,
marketId = trade.market,
marketTitle = marketTitle,
marketEndDate = marketEndDate
marketEndDate = marketEndDate,
outcomeIndex = trade.outcomeIndex
)
val orderbook = filterResult.orderbook // 获取订单簿(如果需要)
if (!filterResult.isPassed) {
@@ -703,8 +704,8 @@ open class CopyOrderTrackingService(
private fun calculateBuyQuantity(trade: TradeResponse, copyTrading: CopyTrading): BigDecimal {
return when (copyTrading.copyMode) {
"RATIO" -> {
// 比例模式:Leader 数量 × (比例 / 100)
trade.size.toSafeBigDecimal().multi(copyTrading.copyRatio.div(100))
// 比例模式:Leader 数量 × 比例倍数(copyRatio 已经是倍数值,如 1.3 表示 130%)
trade.size.toSafeBigDecimal().multi(copyTrading.copyRatio)
}
"FIXED" -> {
@@ -736,7 +737,7 @@ open class CopyOrderTrackingService(
val leader = leaderRepository.findById(copyTrading.leaderId).orElse(null)
?: run {
logger.warn("Leader 不存在,使用默认比例: leaderId=${copyTrading.leaderId}")
return leaderSellQuantity.multi(copyTrading.copyRatio.div(100))
return leaderSellQuantity.multi(copyTrading.copyRatio)
}
// 创建不需要认证的 CLOB API 客户端(用于查询公开的交易数据)
@@ -807,7 +808,7 @@ open class CopyOrderTrackingService(
// 如果无法计算总比例(查询失败),使用默认比例
if (totalLeaderQuantity.lte(BigDecimal.ZERO)) {
logger.warn("无法计算总比例(Leader 买入数量为 0),使用默认比例: copyTradingId=${copyTrading.id}")
return leaderSellQuantity.multi(copyTrading.copyRatio.div(100))
return leaderSellQuantity.multi(copyTrading.copyRatio)
}
// 计算实际比例:跟单买入数量 / Leader 买入数量
@@ -883,13 +884,13 @@ open class CopyOrderTrackingService(
}
"RATIO" -> {
// 比例模式:直接使用配置的 copyRatio (需要除以100)
leaderSellTrade.size.toSafeBigDecimal().multi(copyTrading.copyRatio.div(100))
// 比例模式:直接使用配置的 copyRatio(已经是倍数值,如 1.3 表示 130%)
leaderSellTrade.size.toSafeBigDecimal().multi(copyTrading.copyRatio)
}
else -> {
logger.warn("不支持的 copyMode: ${copyTrading.copyMode},使用默认比例模式")
leaderSellTrade.size.toSafeBigDecimal().multi(copyTrading.copyRatio.div(100))
leaderSellTrade.size.toSafeBigDecimal().multi(copyTrading.copyRatio)
}
}
@@ -799,7 +799,15 @@ class CopyTradingStatisticsService(
stats = stats,
orders = orderDtos as List<Any>
)
}.sortedByDescending { it.stats.count }
}.sortedByDescending { group ->
// 找出该市场最近的卖出订单时间(与买入订单分组排序规则一致)
group.orders.mapNotNull { order ->
when (order) {
is SellOrderInfo -> order.createdAt
else -> null
}
}.maxOrNull() ?: 0L
}
// 5. 分页
val page = (request.page ?: 1)
+8 -5
View File
@@ -1099,6 +1099,7 @@
"price": "Price",
"amount": "Amount",
"filterMarketId": "Filter Market ID",
"filterMarketTitle": "Filter Market Title",
"filterSide": "Filter Side",
"filterStatus": "Filter Status",
"filterSellOrderId": "Filter Sell Order ID",
@@ -1109,12 +1110,15 @@
"groupByMarket": "Group by Market",
"expandAll": "Expand All",
"collapseAll": "Collapse All",
"allFullyMatched": "All Fully Matched",
"partiallyMatched": "Partially Matched",
"allFullySold": "All Fully Sold",
"notSold": "Not Sold",
"partiallySold": "Partially Sold",
"orderCount": "Order Count",
"totalAmount": "Total Amount",
"totalPnl": "Total PnL",
"statusBreakdown": "Status",
"allFullyMatched": "All Fully Sold",
"partiallyMatched": "Partially Sold",
"totalPnl": "Total PnL",
"markets": "markets",
"fetchBuyOrdersFailed": "Failed to fetch buy orders",
"fetchSellOrdersFailed": "Failed to fetch sell orders",
@@ -1126,7 +1130,6 @@
"totalMatchedOrders": "Total Matched Orders",
"totalBuyAmount": "Total Buy Amount",
"totalSellAmount": "Total Sell Amount",
"totalPnl": "Total PnL",
"totalRealizedPnl": "Total Realized PnL",
"totalUnrealizedPnl": "Total Unrealized PnL",
"winRate": "Win Rate",
@@ -1195,4 +1198,4 @@
"providerChainstack": "Chainstack",
"providerGetBlock": "GetBlock"
}
}
}
+6 -5
View File
@@ -1080,8 +1080,8 @@
"sellStatus": "卖出状态",
"status": "状态",
"statusFilled": "未成交",
"statusPartiallySold": "部分成交",
"statusFullySold": "全部成交",
"statusPartiallySold": "部分卖出",
"statusFullySold": "全部卖出",
"realizedPnl": "已实现盈亏",
"createdAt": "创建时间",
"matchedAt": "匹配时间",
@@ -1110,8 +1110,9 @@
"groupByMarket": "按市场分组",
"expandAll": "展开全部",
"collapseAll": "折叠全部",
"allFullyMatched": "全部成交",
"partiallyMatched": "部分成交",
"allFullySold": "全部卖出",
"notSold": "未卖出",
"partiallySold": "部分卖出",
"orderCount": "订单数",
"totalAmount": "总金额",
"statusBreakdown": "状态",
@@ -1195,4 +1196,4 @@
"providerChainstack": "Chainstack",
"providerGetBlock": "GetBlock"
}
}
}
+8 -7
View File
@@ -1018,7 +1018,7 @@
"telegramConfig": {
"title": "Telegram 配置說明",
"step1": "通過 <strong>@BotFather</strong> 創建 Telegram 機器人,獲取 Bot Token",
"step2": "填寫 Bot Token 後,點擊\"獲取 Chat ID\"按鈕自動獲取(需要先向機器人發送消息)",
"step2": "填寫 Bot Token 後,點擊\"獲取 Chat ID\"按鈕自動獲取(需要先向機器人發送消息)",
"step3": "或通過 <strong>@userinfobot</strong> 手動獲取 Chat ID",
"step4": "支持配置多個 Chat ID(用逗號分隔),所有配置的用戶都會收到通知",
"step5": "訂單成功或失敗時會自動發送 Telegram 消息",
@@ -1080,8 +1080,8 @@
"sellStatus": "賣出狀態",
"status": "狀態",
"statusFilled": "已完成",
"statusPartiallySold": "部分成交",
"statusFullySold": "全部成交",
"statusPartiallySold": "部分賣出",
"statusFullySold": "全部賣出",
"realizedPnl": "已實現盈虧",
"createdAt": "創建時間",
"matchedAt": "匹配時間",
@@ -1099,6 +1099,7 @@
"price": "價格",
"amount": "金額",
"filterMarketId": "篩選市場ID",
"filterMarketTitle": "篩選市場標題",
"filterSide": "篩選方向",
"filterStatus": "篩選狀態",
"filterSellOrderId": "篩選賣出訂單ID",
@@ -1109,8 +1110,9 @@
"groupByMarket": "按市場分組",
"expandAll": "展開全部",
"collapseAll": "折疊全部",
"allFullyMatched": "全部成交",
"partiallyMatched": "部分成交",
"allFullySold": "全部賣出",
"notSold": "未賣出",
"partiallySold": "部分賣出",
"orderCount": "訂單數",
"totalAmount": "總金額",
"totalPnl": "總盈虧",
@@ -1126,7 +1128,6 @@
"totalMatchedOrders": "總匹配訂單數",
"totalBuyAmount": "總買入金額",
"totalSellAmount": "總賣出金額",
"totalPnl": "總盈虧",
"totalRealizedPnl": "總已實現盈虧",
"totalUnrealizedPnl": "總未實現盈虧",
"winRate": "勝率",
@@ -1195,4 +1196,4 @@
"providerChainstack": "Chainstack",
"providerGetBlock": "GetBlock"
}
}
}
@@ -373,10 +373,12 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
</span>
)}
{group.stats.fullyMatched ? (
<Tag color="success">{t('copyTradingOrders.allFullyMatched') || '全部成交'}</Tag>
<Tag color="success">{t('copyTradingOrders.allFullySold') || '全部卖出'}</Tag>
) : group.stats.fullyMatchedCount === 0 ? (
<Tag color="default">{t('copyTradingOrders.notSold') || '未卖出'}</Tag>
) : (
<Tag color="warning">
{t('copyTradingOrders.partiallyMatched') || '部分成交'} ({group.stats.fullyMatchedCount}/{group.stats.count})
{t('copyTradingOrders.partiallySold') || '部分卖出'} ({group.stats.fullyMatchedCount}/{group.stats.count})
</Tag>
)}
</div>
@@ -384,10 +386,10 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
<span>{t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count}</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC</span>
<span>
{t('copyTradingOrders.statusBreakdown') || '状态'}:
{group.stats.fullyMatchedCount > 0 && ` ${t('copyTradingOrders.statusFullySold') || '全部成交'} ${group.stats.fullyMatchedCount}`}
{group.stats.partiallyMatchedCount > 0 && ` ${t('copyTradingOrders.statusPartiallySold') || '部分成交'} ${group.stats.partiallyMatchedCount}`}
{group.stats.filledCount > 0 && ` ${t('copyTradingOrders.statusFilled') || '未成交'} ${group.stats.filledCount}`}
{t('copyTradingOrders.statusBreakdown') || '状态'}:
{group.stats.fullyMatchedCount > 0 && ` ${t('copyTradingOrders.allFullySold') || '全部卖出'} ${group.stats.fullyMatchedCount}`}
{group.stats.partiallyMatchedCount > 0 && ` ${t('copyTradingOrders.partiallySold') || '部分卖出'} ${group.stats.partiallyMatchedCount}`}
{group.stats.filledCount > 0 && ` ${t('copyTradingOrders.notSold') || '未卖出'} ${group.stats.filledCount}`}
</span>
</div>
</div>
@@ -668,9 +670,9 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
value={filters.status}
onChange={(value) => setFilters({ ...filters, status: value || undefined })}
>
<Option value="filled">{t('copyTradingOrders.statusFilled') || '未成交'}</Option>
<Option value="partially_matched">{t('copyTradingOrders.statusPartiallySold') || '部分成交'}</Option>
<Option value="fully_matched">{t('copyTradingOrders.statusFullySold') || '全部成交'}</Option>
<Option value="filled">{t('copyTradingOrders.notSold') || '未卖出'}</Option>
<Option value="partially_matched">{t('copyTradingOrders.partiallySold') || '部分卖出'}</Option>
<Option value="fully_matched">{t('copyTradingOrders.allFullySold') || '全部卖出'}</Option>
</Select>
<Space>
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { Table, Input, Button, Card, Divider, Spin, message } from 'antd'
import { apiService } from '../../services/api'
import { formatUSDC, isAutoGeneratedOrderId, copyToClipboard } from '../../utils'
import { formatUSDC, isAutoGeneratedOrderId, copyToClipboard, getPolymarketUrl } from '../../utils'
import { useMediaQuery } from 'react-responsive'
import { useTranslation } from 'react-i18next'
import type { MatchedOrderInfo, OrderTrackingRequest, OrderTrackingListResponse } from '../../types'
@@ -91,59 +91,101 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
const columns = [
{
title: t('copyTradingOrders.sellOrderId') || '卖出订单ID',
dataIndex: 'sellOrderId',
key: 'sellOrderId',
width: isMobile ? 120 : 180,
render: (text: string) => {
const isAuto = isAutoGeneratedOrderId(text)
title: t('copyTradingOrders.market') || '市场',
dataIndex: 'marketId',
key: 'marketId',
width: isMobile ? 120 : 200,
render: (text: string, record: MatchedOrderInfo) => {
const marketUrl = getPolymarketUrl(record.marketSlug, record.eventSlug, record.marketCategory, record.marketId)
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
{record.marketTitle ? (
marketUrl ? (
<a
href={marketUrl}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: isMobile ? 11 : 12,
fontWeight: 500,
color: '#1890ff',
textDecoration: 'none',
cursor: 'pointer'
}}
>
{record.marketTitle}
</a>
) : (
<span style={{ fontSize: isMobile ? 11 : 12, fontWeight: 500 }}>
{record.marketTitle}
</span>
)
) : null}
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 10 : 11, color: '#999' }}>
{isMobile
? `${text.slice(0, 6)}...${text.slice(-4)}`
: `${text.slice(0, 8)}...${text.slice(-6)}`
}
</span>
{!isAuto && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(text)}
style={{ padding: 0, height: 'auto', fontSize: isMobile ? 11 : 12 }}
title={t('common.copy') || '复制'}
/>
)}
</div>
)
}
},
{
title: t('copyTradingOrders.buyOrderId') || '买入订单ID',
dataIndex: 'buyOrderId',
key: 'buyOrderId',
width: isMobile ? 120 : 180,
render: (text: string) => {
const isAuto = isAutoGeneratedOrderId(text)
title: t('copyTradingOrders.orderId') || '订单ID',
dataIndex: 'orderId',
key: 'orderId',
width: isMobile ? 150 : 200,
render: (_: any, record: MatchedOrderInfo) => {
const buyOrderId = record.buyOrderId
const sellOrderId = record.sellOrderId
const isBuyAuto = isAutoGeneratedOrderId(buyOrderId)
const isSellAuto = isAutoGeneratedOrderId(sellOrderId)
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
{isMobile
? `${text.slice(0, 6)}...${text.slice(-4)}`
: `${text.slice(0, 8)}...${text.slice(-6)}`
}
</span>
{!isAuto && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(text)}
style={{ padding: 0, height: 'auto', fontSize: isMobile ? 11 : 12 }}
title={t('common.copy') || '复制'}
/>
)}
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
<span style={{ fontSize: isMobile ? 11 : 12, color: '#999' }}>
{t('copyTradingOrders.buy') || '买入'}:
</span>
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
{isMobile
? `${buyOrderId.slice(0, 6)}...${buyOrderId.slice(-4)}`
: `${buyOrderId.slice(0, 8)}...${buyOrderId.slice(-6)}`
}
</span>
{!isBuyAuto && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(buyOrderId)}
style={{ padding: 0, height: 'auto', fontSize: isMobile ? 11 : 12 }}
title={t('common.copy') || '复制'}
/>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: isMobile ? 11 : 12, color: '#999' }}>
{t('copyTradingOrders.sell') || '卖出'}:
</span>
<span style={{ fontFamily: 'monospace', fontSize: isMobile ? 11 : 12 }}>
{isMobile
? `${sellOrderId.slice(0, 6)}...${sellOrderId.slice(-4)}`
: `${sellOrderId.slice(0, 8)}...${sellOrderId.slice(-6)}`
}
</span>
{!isSellAuto && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(sellOrderId)}
style={{ padding: 0, height: 'auto', fontSize: isMobile ? 11 : 12 }}
title={t('common.copy') || '复制'}
/>
)}
</div>
</div>
)
}
@@ -233,7 +275,7 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
onChange={(e) => setFilters({ ...filters, buyOrderId: e.target.value || undefined })}
/>
<Button type="primary" onClick={fetchOrders} icon={<ReloadOutlined />}>{t('common.search') || '查询'}</Button>
<Button type="primary" onClick={fetchOrders} icon={<ReloadOutlined />}>{t('common.refresh') || '刷新'}</Button>
</div>
{isMobile ? (
@@ -273,9 +315,31 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
<div style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.market') || '市场'}</div>
{order.marketTitle ? (
<div style={{ fontSize: '13px', fontWeight: '500', marginBottom: '4px' }}>
{order.marketTitle}
</div>
(() => {
const marketUrl = getPolymarketUrl(order.marketSlug, order.eventSlug, order.marketCategory, order.marketId)
return marketUrl ? (
<a
href={marketUrl}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: '13px',
fontWeight: '500',
marginBottom: '4px',
color: '#1890ff',
textDecoration: 'none',
cursor: 'pointer',
display: 'block'
}}
>
{order.marketTitle}
</a>
) : (
<div style={{ fontSize: '13px', fontWeight: '500', marginBottom: '4px' }}>
{order.marketTitle}
</div>
)
})()
) : null}
{order.marketId && (
<div style={{ fontSize: '12px', color: '#999', fontFamily: 'monospace' }}>
@@ -284,50 +348,42 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
)}
</div>
)}
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.sellOrderId') || '卖出订单ID'}</div>
<div style={{
fontSize: '13px',
fontWeight: '500',
fontFamily: 'monospace',
marginBottom: '8px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span>{order.sellOrderId.slice(0, 8)}...{order.sellOrderId.slice(-6)}</span>
{!isAutoGeneratedOrderId(order.sellOrderId) && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(order.sellOrderId)}
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
title={t('common.copy') || '复制'}
/>
)}
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.orderId') || '订单ID'}</div>
<div style={{ marginBottom: '8px' }}>
<div style={{ marginBottom: '4px' }}>
<div style={{ fontSize: '11px', color: '#999', marginBottom: '2px' }}>{t('copyTradingOrders.buy') || '买入'}:</div>
<div style={{ fontSize: '13px', fontWeight: '500', fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>{order.buyOrderId.slice(0, 8)}...{order.buyOrderId.slice(-6)}</span>
{!isAutoGeneratedOrderId(order.buyOrderId) && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(order.buyOrderId)}
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
title={t('common.copy') || '复制'}
/>
)}
</div>
</div>
<div style={{ fontSize: '12px', color: '#666', marginBottom: '4px' }}>{t('copyTradingOrders.buyOrderId') || '买入订单ID'}</div>
<div style={{
fontSize: '13px',
fontWeight: '500',
fontFamily: 'monospace',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span>{order.buyOrderId.slice(0, 8)}...{order.buyOrderId.slice(-6)}</span>
{!isAutoGeneratedOrderId(order.buyOrderId) && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(order.buyOrderId)}
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
title={t('common.copy') || '复制'}
/>
)}
<div>
<div style={{ fontSize: '11px', color: '#999', marginBottom: '2px' }}>{t('copyTradingOrders.sell') || '卖出'}:</div>
<div style={{ fontSize: '13px', fontWeight: '500', fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: '8px' }}>
<span>{order.sellOrderId.slice(0, 8)}...{order.sellOrderId.slice(-6)}</span>
{!isAutoGeneratedOrderId(order.sellOrderId) && (
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={() => handleCopyOrderId(order.sellOrderId)}
style={{ padding: 0, height: 'auto', fontSize: '12px' }}
title={t('common.copy') || '复制'}
/>
)}
</div>
</div>
</div>
</div>
<Divider style={{ margin: '12px 0' }} />
@@ -358,7 +358,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
{marketDisplayName}
</span>
)}
<Tag color="success">{t('copyTradingOrders.allFullyMatched') || '全部成交'}</Tag>
<Tag color="success">{t('copyTradingOrders.allFullySold') || '全部卖出'}</Tag>
</div>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', fontSize: isMobile ? '12px' : '13px', color: '#666' }}>
<span>{t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count}</span>
@@ -648,8 +648,8 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
onChange={(value) => setFilters({ ...filters, status: value || undefined })}
>
<Option value="filled">{t('copyTradingOrders.statusFilled') || '未成交'}</Option>
<Option value="partially_matched">{t('copyTradingOrders.statusPartiallyMatched') || '部分成交'}</Option>
<Option value="fully_matched">{t('copyTradingOrders.statusFullyMatched') || '完全成交'}</Option>
<Option value="partially_matched">{t('copyTradingOrders.partiallySold') || '部分卖出'}</Option>
<Option value="fully_matched">{t('copyTradingOrders.allFullySold') || '全部卖出'}</Option>
</Select>
<Space>
+1352
View File
File diff suppressed because it is too large Load Diff