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:
@@ -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",
|
||||
|
||||
+2
-2
@@ -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")
|
||||
|
||||
+36
-11
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user