refactor: 尾盘策略统一更名为加密价差策略

- 前端:菜单父级「加密价差策略」,子项「策略配置」「实时监控」;i18n 与类型注释全部替换
- 后端:i18n、ErrorCode、DTO/实体/服务注释与日志「尾盘」→「加密价差策略」;TG 通知文案
- 文档:README、中英文用户指南、UI 规格、任务、流程、价差流程、市场数据、动态系数等全部更新
- 不修改 API 路径、路由、表名与类名,保持兼容

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WrBug
2026-02-25 21:30:50 +08:00
co-authored by Cursor
parent 4b277eaeab
commit c5d59dfdf9
39 changed files with 197 additions and 197 deletions
@@ -6,7 +6,7 @@ import jakarta.annotation.PostConstruct
import org.springframework.context.annotation.Configuration
/**
* 尾盘监控服务配置
* 加密价差策略监控服务配置
* 处理 WebSocketSubscriptionService 和 CryptoTailMonitorService 之间的循环依赖
*/
@Configuration
@@ -43,12 +43,12 @@ class CryptoTailStrategyController(
result.fold(
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
onFailure = { e ->
logger.error("查询尾盘策略列表失败: ${e.message}", e)
logger.error("查询加密价差策略列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_CRYPTO_TAIL_STRATEGY_LIST_FETCH_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询尾盘策略列表异常: ${e.message}", e)
logger.error("查询加密价差策略列表异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_CRYPTO_TAIL_STRATEGY_LIST_FETCH_FAILED, e.message, messageSource))
}
}
@@ -60,7 +60,7 @@ class CryptoTailStrategyController(
result.fold(
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
onFailure = { e ->
logger.error("创建尾盘策略失败: ${e.message}", e)
logger.error("创建加密价差策略失败: ${e.message}", e)
val code = when (e.message) {
ErrorCode.CRYPTO_TAIL_STRATEGY_WINDOW_INVALID.messageKey -> ErrorCode.CRYPTO_TAIL_STRATEGY_WINDOW_INVALID
ErrorCode.CRYPTO_TAIL_STRATEGY_WINDOW_EXCEED.messageKey -> ErrorCode.CRYPTO_TAIL_STRATEGY_WINDOW_EXCEED
@@ -72,7 +72,7 @@ class CryptoTailStrategyController(
}
)
} catch (e: Exception) {
logger.error("创建尾盘策略异常: ${e.message}", e)
logger.error("创建加密价差策略异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_CRYPTO_TAIL_STRATEGY_CREATE_FAILED, e.message, messageSource))
}
}
@@ -87,7 +87,7 @@ class CryptoTailStrategyController(
result.fold(
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
onFailure = { e ->
logger.error("更新尾盘策略失败: ${e.message}", e)
logger.error("更新加密价差策略失败: ${e.message}", e)
val code = when (e.message) {
ErrorCode.CRYPTO_TAIL_STRATEGY_NOT_FOUND.messageKey -> ErrorCode.CRYPTO_TAIL_STRATEGY_NOT_FOUND
ErrorCode.CRYPTO_TAIL_STRATEGY_WINDOW_INVALID.messageKey -> ErrorCode.CRYPTO_TAIL_STRATEGY_WINDOW_INVALID
@@ -99,7 +99,7 @@ class CryptoTailStrategyController(
}
)
} catch (e: Exception) {
logger.error("更新尾盘策略异常: ${e.message}", e)
logger.error("更新加密价差策略异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_CRYPTO_TAIL_STRATEGY_UPDATE_FAILED, e.message, messageSource))
}
}
@@ -115,12 +115,12 @@ class CryptoTailStrategyController(
result.fold(
onSuccess = { ResponseEntity.ok(ApiResponse.success(Unit)) },
onFailure = { e ->
logger.error("删除尾盘策略失败: ${e.message}", e)
logger.error("删除加密价差策略失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_CRYPTO_TAIL_STRATEGY_DELETE_FAILED, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("删除尾盘策略异常: ${e.message}", e)
logger.error("删除加密价差策略异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_CRYPTO_TAIL_STRATEGY_DELETE_FAILED, e.message, messageSource))
}
}
@@ -194,7 +194,7 @@ class CryptoTailStrategyController(
}
/**
* 初始化尾盘策略监控
* 初始化加密价差策略监控
* 返回策略信息、开盘价、tokenIds等初始化数据
*/
@PostMapping("/monitor/init")
@@ -207,12 +207,12 @@ class CryptoTailStrategyController(
result.fold(
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
onFailure = { e ->
logger.error("初始化尾盘监控失败: ${e.message}", e)
logger.error("初始化加密价差策略监控失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("初始化尾盘监控异常: ${e.message}", e)
logger.error("初始化加密价差策略监控异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
}
}
@@ -1,7 +1,7 @@
package com.wrbug.polymarketbot.dto
/**
* 尾盘策略监控初始化请求
* 加密价差策略监控初始化请求
*/
data class CryptoTailMonitorInitRequest(
/** 策略ID */
@@ -9,7 +9,7 @@ data class CryptoTailMonitorInitRequest(
)
/**
* 尾盘策略监控初始化响应
* 加密价差策略监控初始化响应
*/
data class CryptoTailMonitorInitResponse(
/** 策略ID */
@@ -59,7 +59,7 @@ data class CryptoTailMonitorInitResponse(
)
/**
* 尾盘策略监控实时推送数据
* 加密价差策略监控实时推送数据
*/
data class CryptoTailMonitorPushData(
/** 策略ID */
@@ -1,7 +1,7 @@
package com.wrbug.polymarketbot.dto
/**
* 尾盘策略创建请求
* 加密价差策略创建请求
* 金额与价格使用 String,后端转为 BigDecimal
*/
data class CryptoTailStrategyCreateRequest(
@@ -25,7 +25,7 @@ data class CryptoTailStrategyCreateRequest(
)
/**
* 尾盘策略更新请求
* 加密价差策略更新请求
*/
data class CryptoTailStrategyUpdateRequest(
val strategyId: Long = 0L,
@@ -46,7 +46,7 @@ data class CryptoTailStrategyUpdateRequest(
)
/**
* 尾盘策略列表请求
* 加密价差策略列表请求
*/
data class CryptoTailStrategyListRequest(
val accountId: Long? = null,
@@ -54,7 +54,7 @@ data class CryptoTailStrategyListRequest(
)
/**
* 尾盘策略 DTO(列表与详情)
* 加密价差策略 DTO(列表与详情)
*/
data class CryptoTailStrategyDto(
val id: Long = 0L,
@@ -90,14 +90,14 @@ data class CryptoTailStrategyDto(
)
/**
* 尾盘策略列表响应
* 加密价差策略列表响应
*/
data class CryptoTailStrategyListResponse(
val list: List<CryptoTailStrategyDto> = emptyList()
)
/**
* 尾盘策略删除请求
* 加密价差策略删除请求
*/
data class CryptoTailStrategyDeleteRequest(
val strategyId: Long = 0L
@@ -8,7 +8,7 @@ import jakarta.persistence.*
import java.math.BigDecimal
/**
* 加密市场尾盘策略实体
* 加密价差策略实体
* 5/15 分钟 Up or Down 市场,在周期内时间窗口、价格进入区间时市价买入
*/
@Entity
@@ -5,7 +5,7 @@ import java.math.BigDecimal
import com.wrbug.polymarketbot.util.toSafeBigDecimal
/**
* 尾盘策略触发记录
* 加密价差策略触发记录
*/
@Entity
@Table(name = "crypto_tail_strategy_trigger")
@@ -158,8 +158,8 @@ enum class ErrorCode(
ACCOUNT_BALANCE_FETCH_FAILED(4707, "查询账户余额失败", "error.account_balance_fetch_failed"),
ACCOUNT_POSITIONS_FETCH_FAILED(4708, "查询仓位列表失败", "error.account_positions_fetch_failed"),
// 尾盘策略 (4710-4729)
CRYPTO_TAIL_STRATEGY_NOT_FOUND(4710, "尾盘策略不存在", "error.crypto_tail_strategy_not_found"),
// 加密价差策略 (4710-4729)
CRYPTO_TAIL_STRATEGY_NOT_FOUND(4710, "加密价差策略不存在", "error.crypto_tail_strategy_not_found"),
CRYPTO_TAIL_STRATEGY_WINDOW_INVALID(4711, "时间区间开始不能大于结束", "error.crypto_tail_strategy_window_invalid"),
CRYPTO_TAIL_STRATEGY_WINDOW_EXCEED(4712, "时间区间不能超过周期长度", "error.crypto_tail_strategy_window_exceed"),
CRYPTO_TAIL_STRATEGY_INTERVAL_INVALID(4713, "周期仅支持 300 或 900 秒", "error.crypto_tail_strategy_interval_invalid"),
@@ -259,11 +259,11 @@ enum class ErrorCode(
SERVER_BACKTEST_RETRY_FAILED(5612, "重试回测任务失败", "error.server.backtest_retry_failed"),
SERVER_BACKTEST_RERUN_FAILED(5613, "按配置重新测试失败", "error.server.backtest_rerun_failed"),
// 尾盘策略服务 (5620-5629)
SERVER_CRYPTO_TAIL_STRATEGY_CREATE_FAILED(5620, "创建尾盘策略失败", "error.server.crypto_tail_strategy_create_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_UPDATE_FAILED(5621, "更新尾盘策略失败", "error.server.crypto_tail_strategy_update_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_DELETE_FAILED(5622, "删除尾盘策略失败", "error.server.crypto_tail_strategy_delete_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_LIST_FETCH_FAILED(5623, "查询尾盘策略列表失败", "error.server.crypto_tail_strategy_list_fetch_failed"),
// 加密价差策略服务 (5620-5629)
SERVER_CRYPTO_TAIL_STRATEGY_CREATE_FAILED(5620, "创建加密价差策略失败", "error.server.crypto_tail_strategy_create_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_UPDATE_FAILED(5621, "更新加密价差策略失败", "error.server.crypto_tail_strategy_update_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_DELETE_FAILED(5622, "删除加密价差策略失败", "error.server.crypto_tail_strategy_delete_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_LIST_FETCH_FAILED(5623, "查询加密价差策略列表失败", "error.server.crypto_tail_strategy_list_fetch_failed"),
SERVER_CRYPTO_TAIL_STRATEGY_TRIGGERS_FETCH_FAILED(5624, "查询触发记录失败", "error.server.crypto_tail_strategy_triggers_fetch_failed");
companion object {
@@ -3,6 +3,6 @@ package com.wrbug.polymarketbot.event
import org.springframework.context.ApplicationEvent
/**
* 尾盘策略创建/更新/启用状态变更后发布,用于立即触发一轮执行检查。
* 加密价差策略创建/更新/启用状态变更后发布,用于立即触发一轮执行检查。
*/
class CryptoTailStrategyChangedEvent(source: Any) : ApplicationEvent(source)
@@ -23,7 +23,7 @@ interface CryptoTailStrategyTriggerRepository : JpaRepository<CryptoTailStrategy
/** 轮询结算:仅处理下单成功的订单(status=success 且 orderId 非空)、且未结算的触发记录 */
fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
/** 根据订单 ID 查询尾盘触发记录 */
/** 根据订单 ID 查询加密价差策略触发记录 */
fun findByOrderId(orderId: String): CryptoTailStrategyTrigger?
/** 轮询发 TGstatus=success、orderId 非空、未发过通知,按创建时间正序 */
@@ -396,7 +396,7 @@ class PositionCheckService(
val positionsByAccount = redeemablePositions.groupBy { it.accountId }
for ((accountId, positions) in positionsByAccount) {
// 查找该账户下所有启用的跟单配置(仅用于赎回成功后更新跟单订单状态;无跟单配置的账户如尾盘策略账户也会执行赎回)
// 查找该账户下所有启用的跟单配置(仅用于赎回成功后更新跟单订单状态;无跟单配置的账户如加密价差策略账户也会执行赎回)
val copyTradings = copyTradingRepository.findByAccountId(accountId)
.filter { it.enabled }
@@ -94,7 +94,7 @@ class BinanceKlineAutoSpreadService(
val baseDown = averageAfterIqr(spreadsDown).setScale(8, RoundingMode.HALF_UP)
cache[cacheKey(marketSlugPrefix, intervalSeconds, periodStartUnix)] = baseUp to baseDown
logger.info(
"尾盘自动价差已计算并缓存(100%基准): market=$marketSlugPrefix symbol=$symbol interval=${intervalSeconds}s periodStartUnix=$periodStartUnix | " +
"加密价差策略自动价差已计算并缓存(100%基准): market=$marketSlugPrefix symbol=$symbol interval=${intervalSeconds}s periodStartUnix=$periodStartUnix | " +
"Up方向: 样本数=${spreadsUp.size}, baseSpreadUp=${baseUp.toPlainString()} | " +
"Down方向: 样本数=${spreadsDown.size}, baseSpreadDown=${baseDown.toPlainString()}"
)
@@ -19,7 +19,7 @@ import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicReference
/**
* 币安 K 线 WebSocket:按需订阅尾盘策略使用的币种 5m/15m,维护当前周期 (open, close),供价差校验使用。
* 币安 K 线 WebSocket:按需订阅加密价差策略使用的币种 5m/15m,维护当前周期 (open, close),供价差校验使用。
* 仅当存在启用策略且策略使用到某市场时才订阅对应币种,无策略时不建立连接。
*/
@Service
@@ -46,7 +46,7 @@ class BinanceKlineService {
/** 已连接的 WebSocket: wsKey (symbol-interval) -> WebSocket */
private val connectedWebSockets = ConcurrentHashMap<String, WebSocket>()
/** 当前需要订阅的完整市场集合(如 btc-updown-5m、btc-updown-15m),由尾盘策略刷新时更新 */
/** 当前需要订阅的完整市场集合(如 btc-updown-5m、btc-updown-15m),由加密价差策略刷新时更新 */
private val requiredMarketPrefixes = AtomicReference<Set<String>>(emptySet())
private val subscriptionLock = Any()
private var reconnectJob: Job? = null
@@ -40,14 +40,14 @@ class WebSocketSubscriptionService(
// 存储 order 频道的订阅回调:sessionId -> callback(用于取消订阅)
private val orderChannelCallbacks = ConcurrentHashMap<String, (OrderPushMessage) -> Unit>()
// 存储尾盘监控频道的订阅回调:sessionId -> (strategyId -> callback)
// 存储加密价差策略监控频道的订阅回调:sessionId -> (strategyId -> callback)
private val monitorChannelCallbacks = ConcurrentHashMap<String, MutableMap<Long, (CryptoTailMonitorPushData) -> Unit>>()
// 尾盘监控服务(延迟注入,避免循环依赖)
// 加密价差策略监控服务(延迟注入,避免循环依赖)
private var cryptoTailMonitorService: CryptoTailMonitorService? = null
/**
* 设置尾盘监控服务(由 Spring 在初始化后调用)
* 设置加密价差策略监控服务(由 Spring 在初始化后调用)
*/
fun setCryptoTailMonitorService(service: CryptoTailMonitorService) {
cryptoTailMonitorService = service
@@ -75,7 +75,7 @@ class WebSocketSubscriptionService(
// 清理 order 频道的回调
orderChannelCallbacks.remove(sessionId)
// 清理尾盘监控频道的回调
// 清理加密价差策略监控频道的回调
val monitorCallbacks = monitorChannelCallbacks.remove(sessionId)
monitorCallbacks?.keys?.forEach { strategyId ->
cryptoTailMonitorService?.unsubscribe(sessionId, strategyId)
@@ -127,7 +127,7 @@ class WebSocketSubscriptionService(
orderPushService.subscribeAllEnabled(callback)
}
channel.startsWith("crypto_tail_monitor_") -> {
// 尾盘策略监控频道
// 加密价差策略监控频道
val strategyId = channel.removePrefix("crypto_tail_monitor_").toLongOrNull()
if (strategyId != null && cryptoTailMonitorService != null) {
val callback: (CryptoTailMonitorPushData) -> Unit = { message ->
@@ -136,7 +136,7 @@ class WebSocketSubscriptionService(
monitorChannelCallbacks.getOrPut(sessionId) { mutableMapOf() }[strategyId] = callback
cryptoTailMonitorService!!.subscribe(sessionId, strategyId, callback)
} else {
logger.warn("无效的尾盘监控频道或服务未初始化: $channel")
logger.warn("无效的加密价差策略监控频道或服务未初始化: $channel")
sendSubscribeAck(sessionId, channel, false, "无效的策略ID")
}
}
@@ -167,7 +167,7 @@ class WebSocketSubscriptionService(
}
}
channel.startsWith("crypto_tail_monitor_") -> {
// 取消尾盘监控订阅
// 取消加密价差策略监控订阅
val strategyId = channel.removePrefix("crypto_tail_monitor_").toLongOrNull()
if (strategyId != null) {
monitorChannelCallbacks[sessionId]?.remove(strategyId)
@@ -178,21 +178,21 @@ class WebSocketSubscriptionService(
}
/**
* 注册尾盘监控回调(由 CryptoTailMonitorService 调用)
* 注册加密价差策略监控回调(由 CryptoTailMonitorService 调用)
*/
fun registerMonitorCallback(sessionId: String, strategyId: Long, callback: (CryptoTailMonitorPushData) -> Unit) {
monitorChannelCallbacks.getOrPut(sessionId) { mutableMapOf() }[strategyId] = callback
}
/**
* 注销尾盘监控回调(由 CryptoTailMonitorService 调用)
* 注销加密价差策略监控回调(由 CryptoTailMonitorService 调用)
*/
fun unregisterMonitorCallback(sessionId: String, strategyId: Long) {
monitorChannelCallbacks[sessionId]?.remove(strategyId)
}
/**
* 推送尾盘监控数据(由 CryptoTailMonitorService 调用)
* 推送加密价差策略监控数据(由 CryptoTailMonitorService 调用)
*/
fun pushMonitorData(strategyId: Long, data: CryptoTailMonitorPushData) {
val channel = "crypto_tail_monitor_$strategyId"
@@ -41,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/**
* 尾盘策略监控服务
* 加密价差策略监控服务
* 负责实时推送监控数据到前端
*/
@Service
@@ -526,7 +526,7 @@ class CryptoTailMonitorService(
val msg = """{"type":"MARKET","assets_ids":${tokenIds.toJson()}}"""
try {
webSocket.send(msg)
logger.info("尾盘监控 WebSocket(当前周期)已连接并订阅: ${tokenIds.size} 个 token")
logger.info("加密价差策略监控 WebSocket(当前周期)已连接并订阅: ${tokenIds.size} 个 token")
} catch (e: Exception) {
logger.warn("发送当前周期订阅失败: ${e.message}")
}
@@ -563,9 +563,9 @@ class CryptoTailMonitorService(
try {
webSocket.send(msg)
if (tokenIds.isEmpty()) {
logger.info("尾盘监控 WebSocket(下一周期)已连接,暂无 token 订阅,等待周期切换后更新")
logger.info("加密价差策略监控 WebSocket(下一周期)已连接,暂无 token 订阅,等待周期切换后更新")
} else {
logger.info("尾盘监控 WebSocket(下一周期)已连接并订阅: ${tokenIds.size} 个 token")
logger.info("加密价差策略监控 WebSocket(下一周期)已连接并订阅: ${tokenIds.size} 个 token")
}
} catch (e: Exception) {
logger.warn("发送下一周期订阅失败: ${e.message}")
@@ -593,7 +593,7 @@ class CryptoTailMonitorService(
private fun closeCurrentPeriodWebSocket() {
currentPeriodWebSocket?.close(1000, "period_ended")
currentPeriodWebSocket = null
logger.info("尾盘监控 WebSocket(当前周期)已关闭")
logger.info("加密价差策略监控 WebSocket(当前周期)已关闭")
}
private fun closeAllWebSockets() {
@@ -604,7 +604,7 @@ class CryptoTailMonitorService(
currentPeriodWebSocket = null
nextPeriodWebSocket?.close(1000, "no_subscribers")
nextPeriodWebSocket = null
logger.info("尾盘监控 WebSocket 已全部关闭(无订阅者)")
logger.info("加密价差策略监控 WebSocket 已全部关闭(无订阅者)")
}
private fun handleMessage(webSocket: WebSocket, text: String) {
@@ -802,7 +802,7 @@ class CryptoTailMonitorService(
delay(reconnectDelayMs)
reconnectJob = null
if (strategySubscribers.isNotEmpty()) {
logger.info("尾盘监控 WebSocket 尝试重连")
logger.info("加密价差策略监控 WebSocket 尝试重连")
refreshSubscription()
}
}
@@ -22,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional
import jakarta.annotation.PreDestroy
/**
* 尾盘策略订单 TG 通知轮询服务(与跟单一致)
* 加密价差策略订单 TG 通知轮询服务(与跟单一致)
* 定时查询「下单成功且未发 TG」的触发记录,通过 CLOB getOrder 获取订单详情后发送 TG 并标记已发。
*/
@Service
@@ -57,14 +57,14 @@ class CryptoTailOrderNotificationPollingService(
@Scheduled(fixedDelay = 5000)
fun scheduledSendPendingNotifications() {
if (notificationJob != null && notificationJob!!.isActive) {
logger.debug("上一轮尾盘 TG 通知任务仍在执行,跳过本次")
logger.debug("上一轮加密价差策略 TG 通知任务仍在执行,跳过本次")
return
}
notificationJob = scope.launch {
try {
getSelf().sendPendingNotifications()
} catch (e: Exception) {
logger.error("尾盘 TG 通知轮询异常: ${e.message}", e)
logger.error("加密价差策略 TG 通知轮询异常: ${e.message}", e)
} finally {
notificationJob = null
}
@@ -88,7 +88,7 @@ class CryptoTailOrderNotificationPollingService(
triggerRepository.save(trigger)
}
} catch (e: Exception) {
logger.warn("尾盘 TG 通知单条失败: triggerId=${trigger.id}, orderId=${trigger.orderId}, ${e.message}", e)
logger.warn("加密价差策略 TG 通知单条失败: triggerId=${trigger.id}, orderId=${trigger.orderId}, ${e.message}", e)
}
}
}
@@ -142,7 +142,7 @@ class CryptoTailOrderNotificationPollingService(
walletAddress = account.walletAddress,
orderTime = orderTimeMs
)
logger.info("尾盘订单 TG 通知已发送: orderId=$orderId, strategyId=${strategy.id}, triggerId=${trigger.id}")
logger.info("加密价差策略订单 TG 通知已发送: orderId=$orderId, strategyId=${strategy.id}, triggerId=${trigger.id}")
return true
}
@@ -35,7 +35,7 @@ import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
/**
* 尾盘策略订单簿 WebSocket 监听:订阅 CLOB Market 频道,收到订单簿/价格变更时若满足条件立即触发下单。
* 加密价差策略订单簿 WebSocket 监听:订阅 CLOB Market 频道,收到订单簿/价格变更时若满足条件立即触发下单。
*/
@Service
class CryptoTailOrderbookWsService(
@@ -103,7 +103,7 @@ class CryptoTailOrderbookWsService(
try {
webSocket?.close(1000, "shutdown")
} catch (e: Exception) {
logger.debug("关闭尾盘策略 WebSocket 时异常: ${e.message}")
logger.debug("关闭加密价差策略 WebSocket 时异常: ${e.message}")
}
webSocket = null
scopeJob.cancel()
@@ -116,7 +116,7 @@ class CryptoTailOrderbookWsService(
val request = Request.Builder().url(wsUrl).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: okhttp3.Response) {
logger.info("尾盘策略订单簿 WebSocket 已连接")
logger.info("加密价差策略订单簿 WebSocket 已连接")
refreshAndSubscribe(fromConnect = true)
}
@@ -130,13 +130,13 @@ class CryptoTailOrderbookWsService(
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: okhttp3.Response?) {
logger.warn("尾盘策略订单簿 WebSocket 异常: ${t.message}")
logger.warn("加密价差策略订单簿 WebSocket 异常: ${t.message}")
this@CryptoTailOrderbookWsService.webSocket = null
scheduleReconnect()
}
})
} catch (e: Exception) {
logger.error("尾盘策略订单簿 WebSocket 连接失败: ${e.message}", e)
logger.error("加密价差策略订单簿 WebSocket 连接失败: ${e.message}", e)
scheduleReconnect()
}
}
@@ -150,7 +150,7 @@ class CryptoTailOrderbookWsService(
delay(reconnectDelayMs)
reconnectJob = null
if (strategyRepository.findAllByEnabledTrue().isEmpty()) return@launch
logger.info("尾盘策略订单簿 WebSocket 尝试重连")
logger.info("加密价差策略订单簿 WebSocket 尝试重连")
connect()
}
}
@@ -244,7 +244,7 @@ class CryptoTailOrderbookWsService(
synchronized(refreshLock) {
// 如果正在刷新,直接返回,避免重复调用
if (isRefreshing.get()) {
logger.debug("尾盘策略订阅刷新已在进行中,跳过本次调用")
logger.debug("加密价差策略订阅刷新已在进行中,跳过本次调用")
return
}
isRefreshing.set(true)
@@ -281,7 +281,7 @@ class CryptoTailOrderbookWsService(
val msg = """{"type":"MARKET","assets_ids":${tokenIds.toJson()}}"""
try {
webSocket?.send(msg)
logger.info("尾盘策略订单簿订阅: ${tokenIds.size} 个 token, 市场: $marketSlugs")
logger.info("加密价差策略订单簿订阅: ${tokenIds.size} 个 token, 市场: $marketSlugs")
} catch (e: Exception) {
logger.warn("发送订阅失败: ${e.message}")
return
@@ -303,9 +303,9 @@ class CryptoTailOrderbookWsService(
try {
ws.close(1000, "subscription_change")
} catch (e: Exception) {
logger.debug("关闭尾盘策略 WebSocket 时异常: ${e.message}")
logger.debug("关闭加密价差策略 WebSocket 时异常: ${e.message}")
}
logger.info("尾盘策略订单簿 WebSocket 已关闭(订阅更新,将重连)")
logger.info("加密价差策略订单簿 WebSocket 已关闭(订阅更新,将重连)")
}
}
@@ -357,9 +357,9 @@ class CryptoTailOrderbookWsService(
try {
ws.close(1000, "no_enabled_strategies")
} catch (e: Exception) {
logger.debug("关闭尾盘策略 WebSocket 时异常: ${e.message}")
logger.debug("关闭加密价差策略 WebSocket 时异常: ${e.message}")
}
logger.info("尾盘策略订单簿 WebSocket 已关闭(无启用策略)")
logger.info("加密价差策略订单簿 WebSocket 已关闭(无启用策略)")
}
}
@@ -377,7 +377,7 @@ class CryptoTailOrderbookWsService(
periodEndCountdownJob = null
refreshAndSubscribe()
}
logger.debug("尾盘策略订单簿订阅倒计时: ${delayMs / 1000}s 后刷新")
logger.debug("加密价差策略订单簿订阅倒计时: ${delayMs / 1000}s 后刷新")
}
private fun buildSubscriptionMap(): Pair<List<String>, Map<String, List<WsBookEntry>>> {
@@ -391,23 +391,23 @@ class CryptoTailOrderbookWsService(
val periodStartUnix = (nowSeconds / interval) * interval
val windowEnd = periodStartUnix + strategy.windowEndSeconds
if (nowSeconds >= windowEnd) {
logger.debug("尾盘策略跳过(已过时间窗口): strategyId=${strategy.id}, slug=${strategy.marketSlugPrefix}, windowEnd=$windowEnd")
logger.debug("加密价差策略跳过(已过时间窗口): strategyId=${strategy.id}, slug=${strategy.marketSlugPrefix}, windowEnd=$windowEnd")
continue
}
val slug = "${strategy.marketSlugPrefix}-$periodStartUnix"
val event = runBlocking { fetchEventBySlugWithRetry(slug).getOrNull() }
if (event == null) {
logger.warn("尾盘策略跳过(拉取事件失败): strategyId=${strategy.id}, slug=$slug,请确认 Gamma 是否存在该 slug 或稍后重试")
logger.warn("加密价差策略跳过(拉取事件失败): strategyId=${strategy.id}, slug=$slug,请确认 Gamma 是否存在该 slug 或稍后重试")
continue
}
val market = event.markets?.firstOrNull()
if (market == null) {
logger.warn("尾盘策略跳过(事件无市场): strategyId=${strategy.id}, slug=$slug")
logger.warn("加密价差策略跳过(事件无市场): strategyId=${strategy.id}, slug=$slug")
continue
}
val tokenIds = parseClobTokenIds(market.clobTokenIds)
if (tokenIds.size < 2) {
logger.warn("尾盘策略跳过(token 数量不足): strategyId=${strategy.id}, slug=$slug, tokenCount=${tokenIds.size}")
logger.warn("加密价差策略跳过(token 数量不足): strategyId=${strategy.id}, slug=$slug, tokenCount=${tokenIds.size}")
continue
}
tokenIdSet.addAll(tokenIds)
@@ -27,7 +27,7 @@ import java.math.BigDecimal
import java.math.RoundingMode
/**
* 尾盘策略结算轮询服务
* 加密价差策略结算轮询服务
* 定时扫描「状态成功但未结算」的触发记录,通过 Gamma 获取 conditionId、链上查询结算结果,计算收益并回写。
* 实际成交价与成交量使用 Data API 的 activity 接口获取(getUserActivity),比 CLOB getOrder 更准确;失败时回退为触发时的 amountUsdc + 固定价 0.99。
*/
@@ -60,14 +60,14 @@ class CryptoTailSettlementService(
fun scheduledPollAndSettle() {
val previousJob = settlementJob
if (previousJob != null && previousJob.isActive) {
logger.debug("上一轮尾盘结算任务仍在执行,跳过本次调度")
logger.debug("上一轮加密价差策略结算任务仍在执行,跳过本次调度")
return
}
settlementJob = settlementScope.launch {
try {
doPollAndSettle()
} catch (e: Exception) {
logger.error("尾盘策略结算定时任务异常: ${e.message}", e)
logger.error("加密价差策略结算定时任务异常: ${e.message}", e)
} finally {
settlementJob = null
}
@@ -91,11 +91,11 @@ class CryptoTailSettlementService(
try {
if (settleOne(trigger)) settledCount++
} catch (e: Exception) {
logger.warn("尾盘结算单条失败: triggerId=${trigger.id}, ${e.message}", e)
logger.warn("加密价差策略结算单条失败: triggerId=${trigger.id}, ${e.message}", e)
}
}
if (settledCount > 0) {
logger.info("尾盘策略结算轮询完成: 处理=${pending.size}, 新结算=$settledCount")
logger.info("加密价差策略结算轮询完成: 处理=${pending.size}, 新结算=$settledCount")
}
return settledCount
}
@@ -154,7 +154,7 @@ class CryptoTailSettlementService(
settledAt = now
)
triggerRepository.save(updated)
logger.debug("尾盘结算已更新: triggerId=${trigger.id}, winnerOutcomeIndex=$winnerIndex, won=$won, pnl=$pnl")
logger.debug("加密价差策略结算已更新: triggerId=${trigger.id}, winnerOutcomeIndex=$winnerIndex, won=$won, pnl=$pnl")
return true
}
@@ -201,7 +201,7 @@ class CryptoTailSettlementService(
conditionId: String
): ActivityFill? {
val account = accountRepository.findById(strategy.accountId).orElse(null) ?: run {
logger.warn("尾盘结算未拉取 activity: 账户不存在, triggerId=${trigger.id}, accountId=${strategy.accountId}")
logger.warn("加密价差策略结算未拉取 activity: 账户不存在, triggerId=${trigger.id}, accountId=${strategy.accountId}")
return null
}
val user = account.proxyAddress
@@ -220,7 +220,7 @@ class CryptoTailSettlementService(
sortDirection = "DESC"
)
if (!response.isSuccessful || response.body() == null) {
logger.warn("尾盘结算拉取 activity 失败: triggerId=${trigger.id}, code=${response.code()}")
logger.warn("加密价差策略结算拉取 activity 失败: triggerId=${trigger.id}, code=${response.code()}")
return null
}
val activities = response.body()!!
@@ -234,7 +234,7 @@ class CryptoTailSettlementService(
a.price != null && a.price > 0 &&
a.size != null && a.size > 0
} ?: run {
logger.debug("尾盘结算 activity 无匹配成交: triggerId=${trigger.id}, conditionId=$conditionId, outcomeIndex=${trigger.outcomeIndex}, 条数=${activities.size}")
logger.debug("加密价差策略结算 activity 无匹配成交: triggerId=${trigger.id}, conditionId=$conditionId, outcomeIndex=${trigger.outcomeIndex}, 条数=${activities.size}")
return null
}
val price = match.price!!.toSafeBigDecimal()
@@ -243,11 +243,11 @@ class CryptoTailSettlementService(
if (price.gt(BigDecimal.ZERO) && size.gt(BigDecimal.ZERO)) {
ActivityFill(price = price, size = size, usdcSize = usdcSize)
} else {
logger.debug("尾盘结算 activity 成交数据无效: triggerId=${trigger.id}, price=$price, size=$size")
logger.debug("加密价差策略结算 activity 成交数据无效: triggerId=${trigger.id}, price=$price, size=$size")
null
}
} catch (e: Exception) {
logger.warn("尾盘结算拉取 activity 异常,触发价/投入金额不会更新: triggerId=${trigger.id}, error=${e.message}")
logger.warn("加密价差策略结算拉取 activity 异常,触发价/投入金额不会更新: triggerId=${trigger.id}, error=${e.message}")
null
}
}
@@ -34,7 +34,7 @@ import java.math.RoundingMode
import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
/** 尾盘策略固定下单价格(最高价 0.99),不再在触发时拉取最优价 */
/** 加密价差策略固定下单价格(最高价 0.99),不再在触发时拉取最优价 */
private const val TRIGGER_FIXED_PRICE = "0.99"
/** 最大价差模式(MAX)时,买入价格调整系数(加在触发价格上) */
@@ -62,7 +62,7 @@ private data class PeriodContext(
)
/**
* 尾盘策略执行服务:按周期与时间窗口检查价格并下单,每周期最多触发一次。
* 加密价差策略执行服务:按周期与时间窗口检查价格并下单,每周期最多触发一次。
* 周期开始预置账户、解密、费率、签名类型、CLOB 客户端;触发时按 outcomeIndex 计算 size 并签名提交。
*/
@Service
@@ -131,7 +131,7 @@ class CryptoTailStrategyExecutionService(
val decryptedKey = try {
cryptoUtils.decrypt(account.privateKey) ?: return null
} catch (e: Exception) {
logger.warn("尾盘策略周期上下文解密私钥失败: accountId=${account.id}", e)
logger.warn("加密价差策略周期上下文解密私钥失败: accountId=${account.id}", e)
return null
}
val apiSecret = try {
@@ -230,11 +230,11 @@ class CryptoTailStrategyExecutionService(
)
val openPrice = oc?.first?.toPlainString() ?: "-"
val closePrice = oc?.second?.toPlainString() ?: "-"
val strategyName = strategy.name?.takeIf { it.isNotBlank() } ?: "尾盘策略-${strategy.marketSlugPrefix}"
val strategyName = strategy.name?.takeIf { it.isNotBlank() } ?: "加密价差策略-${strategy.marketSlugPrefix}"
val direction = if (outcomeIndex == 0) "Up" else "Down"
val modeStr = if (strategy.spreadDirection == SpreadDirection.MAX) "最大价差" else "最小价差"
logger.info(
"尾盘策略首次满足条件: strategyName=$strategyName, strategyId=${strategy.id}, " +
"加密价差策略首次满足条件: strategyName=$strategyName, strategyId=${strategy.id}, " +
"openPrice=$openPrice, closePrice=$closePrice, marketPrice=${bestBid.toPlainString()}, " +
"direction=$direction, outcomeIndex=$outcomeIndex, spreadMode=$modeStr"
)
@@ -446,7 +446,7 @@ class CryptoTailStrategyExecutionService(
"success",
null
)
logger.info("尾盘策略下单成功: strategyId=${strategy.id}, periodStartUnix=$periodStartUnix, outcomeIndex=$outcomeIndex, orderId=${body.orderId}")
logger.info("加密价差策略下单成功: strategyId=${strategy.id}, periodStartUnix=$periodStartUnix, outcomeIndex=$outcomeIndex, orderId=${body.orderId}")
return
}
failReason = body.errorMsg ?: "unknown"
@@ -456,7 +456,7 @@ class CryptoTailStrategyExecutionService(
}
} catch (e: Exception) {
failReason = e.message ?: e.toString()
logger.error("尾盘策略下单异常: strategyId=${strategy.id}, periodStartUnix=$periodStartUnix", e)
logger.error("加密价差策略下单异常: strategyId=${strategy.id}, periodStartUnix=$periodStartUnix", e)
}
saveTriggerRecord(
strategy,
@@ -469,7 +469,7 @@ class CryptoTailStrategyExecutionService(
"fail",
failReason
)
logger.error("尾盘策略下单失败: strategyId=${strategy.id}, periodStartUnix=$periodStartUnix, reason=$failReason")
logger.error("加密价差策略下单失败: strategyId=${strategy.id}, periodStartUnix=$periodStartUnix, reason=$failReason")
}
/** 无预置上下文时的完整流程:固定价格 0.99,账户/解密/费率/签名在触发时执行 */
@@ -672,6 +672,6 @@ class CryptoTailStrategyExecutionService(
periodContextCache.clear()
// 清理所有锁,避免内存泄漏
triggerMutexMap.clear()
logger.debug("尾盘策略执行服务已清理缓存和锁")
logger.debug("加密价差策略执行服务已清理缓存和锁")
}
}
@@ -104,7 +104,7 @@ class CryptoTailStrategyService(
} catch (e: IllegalArgumentException) {
Result.failure(e)
} catch (e: Exception) {
logger.error("创建尾盘策略失败: ${e.message}", e)
logger.error("创建加密价差策略失败: ${e.message}", e)
Result.failure(e)
}
}
@@ -179,7 +179,7 @@ class CryptoTailStrategyService(
} catch (e: IllegalArgumentException) {
Result.failure(e)
} catch (e: Exception) {
logger.error("更新尾盘策略失败: ${e.message}", e)
logger.error("更新加密价差策略失败: ${e.message}", e)
Result.failure(e)
}
}
@@ -194,7 +194,7 @@ class CryptoTailStrategyService(
eventPublisher.publishEvent(CryptoTailStrategyChangedEvent(this))
Result.success(Unit)
} catch (e: Exception) {
logger.error("删除尾盘策略失败: ${e.message}", e)
logger.error("删除加密价差策略失败: ${e.message}", e)
Result.failure(e)
}
}
@@ -215,7 +215,7 @@ class CryptoTailStrategyService(
val dtos = list.map { entityToDto(it, lastTriggerMap[it.id]) }
Result.success(CryptoTailStrategyListResponse(list = dtos))
} catch (e: Exception) {
logger.error("查询尾盘策略列表失败: ${e.message}", e)
logger.error("查询加密价差策略列表失败: ${e.message}", e)
Result.failure(e)
}
}
@@ -263,7 +263,7 @@ class CryptoTailStrategyService(
private fun generateStrategyName(marketSlugPrefix: String): String {
val suffix = Instant.now().atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
return "尾盘策略-${marketSlugPrefix}-$suffix"
return "加密价差策略-${marketSlugPrefix}-$suffix"
}
private fun entityToDto(e: CryptoTailStrategy, lastTriggerAt: Long?): CryptoTailStrategyDto {
@@ -250,7 +250,7 @@ class ApiHealthCheckService(
name = "币安 WebSocket",
url = binanceWsUrl,
status = "success",
message = "尾盘策略,未订阅"
message = "加密价差策略,未订阅"
)
} else if (connected > 0) {
val which = statuses.filter { it.value }.keys.joinToString("")
@@ -304,7 +304,7 @@ class TelegramNotificationService(
}
/**
* 发送尾盘策略下单成功通知与跟单一致在收到 WS 订单推送时匹配尾盘订单后调用
* 发送加密价差策略下单成功通知与跟单一致在收到 WS 订单推送时匹配价差策略订单后调用
*/
suspend fun sendCryptoTailOrderSuccessNotification(
orderId: String?,
@@ -324,7 +324,7 @@ class TelegramNotificationService(
if (orderId != null) {
val lastSentTime = sentOrderIds[orderId]
if (lastSentTime != null && System.currentTimeMillis() - lastSentTime < 5 * 60 * 1000) {
logger.info("尾盘订单通知已发送过(5分钟内),跳过: orderId=$orderId")
logger.info("加密价差策略订单通知已发送过(5分钟内),跳过: orderId=$orderId")
return
}
sentOrderIds[orderId] = System.currentTimeMillis()
@@ -894,7 +894,7 @@ class TelegramNotificationService(
}
/**
* 构建尾盘策略下单成功消息与订单成功格式一致增加尾盘策略标题与策略名
* 构建加密价差策略下单成功消息与订单成功格式一致增加加密价差策略标题与策略名
*/
private fun buildCryptoTailOrderSuccessMessage(
orderId: String?,
@@ -912,7 +912,7 @@ class TelegramNotificationService(
locale: java.util.Locale,
orderTime: Long?
): String {
val tailOrderSuccess = messageSource.getMessage("notification.tail.order.success", null, "尾盘策略下单成功", locale)
val tailOrderSuccess = messageSource.getMessage("notification.tail.order.success", null, "加密价差策略下单成功", locale)
val strategyLabel = messageSource.getMessage("notification.tail.strategy", null, "策略", locale)
val orderInfo = messageSource.getMessage("notification.order.info", null, "订单信息", locale)
val orderIdLabel = messageSource.getMessage("notification.order.id", null, "订单ID", locale)
@@ -16,7 +16,7 @@ notification.order.time=Time
notification.order.error_info=Error Information
notification.order.unknown_account=Unknown Account
notification.order.calculate_failed=Calculation Failed
notification.tail.order.success=Tail Session Order Success
notification.tail.order.success=Crypto spread strategy order success
notification.tail.strategy=Strategy
notification.redeem.success=Position Redeemed Successfully
notification.redeem.info=Redeem Information
@@ -277,16 +277,16 @@ error.server.backtest_stop_failed=Failed to stop backtest task
error.server.backtest_retry_failed=Failed to retry backtest task
error.server.backtest_rerun_failed=Failed to re-run backtest with same config
# Crypto tail strategy
error.crypto_tail_strategy_not_found=Crypto tail strategy not found
# Crypto spread strategy
error.crypto_tail_strategy_not_found=Crypto spread strategy not found
error.crypto_tail_strategy_window_invalid=Window start must not be greater than window end
error.crypto_tail_strategy_window_exceed=Time window must not exceed period length
error.crypto_tail_strategy_interval_invalid=Interval must be 300 or 900 seconds
error.crypto_tail_strategy_amount_mode_invalid=Amount mode must be RATIO or FIXED
error.server.crypto_tail_strategy_create_failed=Failed to create crypto tail strategy
error.server.crypto_tail_strategy_update_failed=Failed to update crypto tail strategy
error.server.crypto_tail_strategy_delete_failed=Failed to delete crypto tail strategy
error.server.crypto_tail_strategy_list_fetch_failed=Failed to fetch crypto tail strategy list
error.server.crypto_tail_strategy_create_failed=Failed to create crypto spread strategy
error.server.crypto_tail_strategy_update_failed=Failed to update crypto spread strategy
error.server.crypto_tail_strategy_delete_failed=Failed to delete crypto spread strategy
error.server.crypto_tail_strategy_list_fetch_failed=Failed to fetch crypto spread strategy list
error.server.crypto_tail_strategy_triggers_fetch_failed=Failed to fetch trigger records
# Backtest Management
backtest.title=Backtest Management
@@ -16,7 +16,7 @@ notification.order.time=时间
notification.order.error_info=错误信息
notification.order.unknown_account=未知账户
notification.order.calculate_failed=计算失败
notification.tail.order.success=尾盘策略下单成功
notification.tail.order.success=加密价差策略下单成功
notification.tail.strategy=策略
notification.redeem.success=仓位赎回成功
notification.redeem.info=赎回信息
@@ -277,16 +277,16 @@ error.server.backtest_stop_failed=停止回测任务失败
error.server.backtest_retry_failed=重试回测任务失败
error.server.backtest_rerun_failed=按配置重新测试失败
# 尾盘策略
error.crypto_tail_strategy_not_found=尾盘策略不存在
# 加密价差策略
error.crypto_tail_strategy_not_found=加密价差策略不存在
error.crypto_tail_strategy_window_invalid=时间区间开始不能大于结束
error.crypto_tail_strategy_window_exceed=时间区间不能超过周期长度
error.crypto_tail_strategy_interval_invalid=周期仅支持 300 或 900 秒
error.crypto_tail_strategy_amount_mode_invalid=投入方式仅支持 RATIO 或 FIXED
error.server.crypto_tail_strategy_create_failed=创建尾盘策略失败
error.server.crypto_tail_strategy_update_failed=更新尾盘策略失败
error.server.crypto_tail_strategy_delete_failed=删除尾盘策略失败
error.server.crypto_tail_strategy_list_fetch_failed=查询尾盘策略列表失败
error.server.crypto_tail_strategy_create_failed=创建加密价差策略失败
error.server.crypto_tail_strategy_update_failed=更新加密价差策略失败
error.server.crypto_tail_strategy_delete_failed=删除加密价差策略失败
error.server.crypto_tail_strategy_list_fetch_failed=查询加密价差策略列表失败
error.server.crypto_tail_strategy_triggers_fetch_failed=查询触发记录失败
# 回测管理
backtest.title=回测管理
@@ -16,7 +16,7 @@ notification.order.time=時間
notification.order.error_info=錯誤信息
notification.order.unknown_account=未知賬戶
notification.order.calculate_failed=計算失敗
notification.tail.order.success=尾盤策略下單成功
notification.tail.order.success=加密價差策略下單成功
notification.tail.strategy=策略
notification.redeem.success=倉位贖回成功
notification.redeem.info=贖回信息
@@ -277,16 +277,16 @@ error.server.backtest_stop_failed=停止回測任務失敗
error.server.backtest_retry_failed=重試回測任務失敗
error.server.backtest_rerun_failed=依配置重新測試失敗
# 尾盤策略
error.crypto_tail_strategy_not_found=尾盤策略不存在
# 加密價差策略
error.crypto_tail_strategy_not_found=加密價差策略不存在
error.crypto_tail_strategy_window_invalid=時間區間開始不能大於結束
error.crypto_tail_strategy_window_exceed=時間區間不能超過週期長度
error.crypto_tail_strategy_interval_invalid=週期僅支援 300 或 900 秒
error.crypto_tail_strategy_amount_mode_invalid=投入方式僅支援 RATIO 或 FIXED
error.server.crypto_tail_strategy_create_failed=創建尾盤策略失敗
error.server.crypto_tail_strategy_update_failed=更新尾盤策略失敗
error.server.crypto_tail_strategy_delete_failed=刪除尾盤策略失敗
error.server.crypto_tail_strategy_list_fetch_failed=查詢尾盤策略列表失敗
error.server.crypto_tail_strategy_create_failed=創建加密價差策略失敗
error.server.crypto_tail_strategy_update_failed=更新加密價差策略失敗
error.server.crypto_tail_strategy_delete_failed=刪除加密價差策略失敗
error.server.crypto_tail_strategy_list_fetch_failed=查詢加密價差策略列表失敗
error.server.crypto_tail_strategy_triggers_fetch_failed=查詢觸發記錄失敗
# 回測管理
backtest.title=回測管理