feat(crypto-tail): 触发记录与结算优化、前端收益与价格展示

后端:
- 结算轮询仅处理下单成功订单(status=success 且 orderId 非空)
- 实体通过 copy() 更新,不再直接改字段;结算时用订单 fill 回写 triggerPrice、amountUsdc
- Trigger 实体结算相关字段改为 val,统一 copy+save 更新

前端:
- 触发记录: 去掉市场列,保留触发价格(轮询后为实际成交价);每条收益列、涨跌色
- 策略列表: 总收益与胜率、涨跌色;价格区间格式 0.5 ~ 1
- 类型与 i18n: totalRealizedPnl/winRate/realizedPnl 等

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-14 05:44:19 +08:00
co-authored by Cursor
parent 0efd4a5fc3
commit f200ba7f7c
8 changed files with 132 additions and 32 deletions
@@ -36,19 +36,19 @@ data class CryptoTailStrategyTrigger(
val orderId: String? = null,
@Column(name = "condition_id", length = 66)
var conditionId: String? = null,
val conditionId: String? = null,
@Column(name = "resolved", nullable = false)
var resolved: Boolean = false,
val resolved: Boolean = false,
@Column(name = "winner_outcome_index")
var winnerOutcomeIndex: Int? = null,
val winnerOutcomeIndex: Int? = null,
@Column(name = "realized_pnl", precision = 20, scale = 8)
var realizedPnl: BigDecimal? = null,
val realizedPnl: BigDecimal? = null,
@Column(name = "settled_at")
var settledAt: Long? = null,
val settledAt: Long? = null,
@Column(name = "status", nullable = false, length = 20)
val status: String = "success",
@@ -15,8 +15,8 @@ interface CryptoTailStrategyTriggerRepository : JpaRepository<CryptoTailStrategy
fun findAllByStrategyIdAndStatusOrderByCreatedAtDesc(strategyId: Long, status: String, pageable: Pageable): Page<CryptoTailStrategyTrigger>
fun countByStrategyIdAndStatus(strategyId: Long, status: String): Long
/** 轮询结算:状态成功且未结算的触发记录(用于定时扫描并回写收益) */
fun findByStatusAndResolvedOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
/** 轮询结算:仅处理下单成功的订单(status=success 且 orderId 非空)、且未结算的触发记录 */
fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
/** 策略已结算订单的总已实现盈亏(用于收益统计) */
@Query("SELECT COALESCE(SUM(t.realizedPnl), 0) FROM CryptoTailStrategyTrigger t WHERE t.strategyId = :strategyId AND t.resolved = true")
@@ -85,7 +85,7 @@ class CryptoTailSettlementService(
}
private suspend fun doPollAndSettle(): Int {
val pending = triggerRepository.findByStatusAndResolvedOrderByCreatedAtAsc("success", false)
val pending = triggerRepository.findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc("success", false)
if (pending.isEmpty()) return 0
var settledCount = 0
for (trigger in pending) {
@@ -103,15 +103,36 @@ class CryptoTailSettlementService(
/**
* 处理单条触发记录:解析 conditionId -> 查链上结算 -> 若已结算则计算 pnl 并更新。
* 通过 copy() 生成新实体再 save,不直接修改原实体;有订单信息时用实际成交价与投入金额更新 triggerPrice、amountUsdc。
* @return true 表示本条已结算并更新
*/
private suspend fun settleOne(trigger: CryptoTailStrategyTrigger): Boolean {
if (trigger.resolved) return false
val strategy = strategyRepository.findById(trigger.strategyId).orElse(null) ?: return false
val conditionId = resolveConditionId(strategy, trigger)
?: return false
val (_, payouts) = blockchainService.getCondition(conditionId).getOrNull() ?: return false
if (payouts.isEmpty()) return false
val fill = fetchOrderFill(trigger, strategy)
val (newTriggerPrice, newAmountUsdc) = if (fill != null && fill.first.gt(BigDecimal.ZERO) && fill.second.gt(BigDecimal.ZERO)) {
val price = fill.first
val cost = price.multi(fill.second).setScale(pnlScale, RoundingMode.HALF_UP)
Pair(price, cost)
} else {
Pair(trigger.triggerPrice, trigger.amountUsdc)
}
val conditionId = resolveConditionId(strategy, trigger) ?: return false
val (_, payouts) = blockchainService.getCondition(conditionId).getOrNull() ?: run {
if (fill != null) {
val updated = trigger.copy(triggerPrice = newTriggerPrice, amountUsdc = newAmountUsdc)
triggerRepository.save(updated)
}
return false
}
if (payouts.isEmpty()) {
if (fill != null) {
val updated = trigger.copy(triggerPrice = newTriggerPrice, amountUsdc = newAmountUsdc)
triggerRepository.save(updated)
}
return false
}
val winnerIndex = payouts.indexOfFirst { it == java.math.BigInteger.ONE }
if (winnerIndex < 0) return false
@@ -119,12 +140,16 @@ class CryptoTailSettlementService(
val pnl = computePnlFromApiOrFallback(trigger, strategy, won)
val now = System.currentTimeMillis()
trigger.conditionId = conditionId
trigger.resolved = true
trigger.winnerOutcomeIndex = winnerIndex
trigger.realizedPnl = pnl
trigger.settledAt = now
triggerRepository.save(trigger)
val updated = trigger.copy(
triggerPrice = newTriggerPrice,
amountUsdc = newAmountUsdc,
conditionId = conditionId,
resolved = true,
winnerOutcomeIndex = winnerIndex,
realizedPnl = pnl,
settledAt = now
)
triggerRepository.save(updated)
logger.debug("尾盘结算已更新: triggerId=${trigger.id}, winnerOutcomeIndex=$winnerIndex, won=$won, pnl=$pnl")
return true
}