feat: 实现跟单筛选条件记录和展示功能
- 新增 FilteredOrder 实体和数据库表,记录被过滤的订单信息 - 在筛选失败时自动记录到数据库并发送 Telegram 通知 - 创建 FilteredOrderService 和 API 接口,支持查询被过滤订单列表 - 前端新增被过滤订单列表页面,支持按过滤类型筛选 - 修复价差计算逻辑,使用数组最大值和最小值而非第一个元素 - 优化编辑页面 UI,钱包和 Leader 显示与创建页面一致(只读) - 添加多语言支持(中文、繁体中文、英文)
This commit is contained in:
+59
-5
@@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.controller
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.CopyTradingService
|
||||
import com.wrbug.polymarketbot.service.FilteredOrderService
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
@@ -15,13 +16,17 @@ import org.springframework.web.bind.annotation.*
|
||||
@RequestMapping("/api/copy-trading")
|
||||
class CopyTradingController(
|
||||
private val copyTradingService: CopyTradingService,
|
||||
private val filteredOrderService: FilteredOrderService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(CopyTradingController::class.java)
|
||||
|
||||
/**
|
||||
* 创建跟单
|
||||
* 创建跟单配置
|
||||
* 支持两种方式:
|
||||
* 1. 提供 templateId:从模板填充配置,可以覆盖部分字段
|
||||
* 2. 不提供 templateId:手动输入所有配置参数
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
fun createCopyTrading(@RequestBody request: CopyTradingCreateRequest): ResponseEntity<ApiResponse<CopyTradingDto>> {
|
||||
@@ -29,12 +34,13 @@ class CopyTradingController(
|
||||
if (request.accountId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
if (request.templateId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
if (request.leaderId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
// templateId 现在是可选的,如果提供则必须 > 0
|
||||
if (request.templateId != null && request.templateId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = copyTradingService.createCopyTrading(request)
|
||||
result.fold(
|
||||
@@ -78,7 +84,37 @@ class CopyTradingController(
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新跟单状态
|
||||
* 更新跟单配置
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
fun updateCopyTrading(@RequestBody request: CopyTradingUpdateRequest): ResponseEntity<ApiResponse<CopyTradingDto>> {
|
||||
return try {
|
||||
if (request.copyTradingId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = copyTradingService.updateCopyTrading(request)
|
||||
result.fold(
|
||||
onSuccess = { copyTrading ->
|
||||
ResponseEntity.ok(ApiResponse.success(copyTrading))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("更新跟单配置失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
|
||||
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message, messageSource))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_UPDATE_FAILED, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新跟单配置异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_UPDATE_FAILED, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新跟单状态(兼容旧接口)
|
||||
*/
|
||||
@PostMapping("/update-status")
|
||||
fun updateCopyTradingStatus(@RequestBody request: CopyTradingUpdateStatusRequest): ResponseEntity<ApiResponse<CopyTradingDto>> {
|
||||
@@ -164,5 +200,23 @@ class CopyTradingController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_TEMPLATES_FETCH_FAILED, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询被过滤订单列表
|
||||
*/
|
||||
@PostMapping("/filtered-orders")
|
||||
fun getFilteredOrders(@RequestBody request: FilteredOrderListRequest): ResponseEntity<ApiResponse<FilteredOrderListResponse>> {
|
||||
return try {
|
||||
if (request.copyTradingId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
|
||||
val response = filteredOrderService.getFilteredOrders(request)
|
||||
ResponseEntity.ok(ApiResponse.success(response))
|
||||
} catch (e: Exception) {
|
||||
logger.error("查询被过滤订单列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,65 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 跟单创建请求
|
||||
* 支持两种方式:
|
||||
* 1. 提供 templateId:从模板填充配置,可以覆盖部分字段
|
||||
* 2. 不提供 templateId:手动输入所有配置参数
|
||||
*/
|
||||
data class CopyTradingCreateRequest(
|
||||
val accountId: Long,
|
||||
val templateId: Long,
|
||||
val leaderId: Long,
|
||||
val enabled: Boolean = true
|
||||
val enabled: Boolean = true,
|
||||
// 可选:如果提供 templateId,则从模板填充配置(可以覆盖)
|
||||
val templateId: Long? = null,
|
||||
// 跟单配置参数(如果提供 templateId,这些字段可选,用于覆盖模板值)
|
||||
val copyMode: String? = null, // "RATIO" 或 "FIXED"
|
||||
val copyRatio: String? = null, // 仅在 copyMode="RATIO" 时生效
|
||||
val fixedAmount: String? = null, // 仅在 copyMode="FIXED" 时生效
|
||||
val maxOrderSize: String? = null,
|
||||
val minOrderSize: String? = null,
|
||||
val maxDailyLoss: String? = null,
|
||||
val maxDailyOrders: Int? = null,
|
||||
val priceTolerance: String? = null, // 百分比
|
||||
val delaySeconds: Int? = null,
|
||||
val pollIntervalSeconds: Int? = null,
|
||||
val useWebSocket: Boolean? = null,
|
||||
val websocketReconnectInterval: Int? = null,
|
||||
val websocketMaxRetries: Int? = null,
|
||||
val supportSell: Boolean? = null,
|
||||
// 过滤条件
|
||||
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
|
||||
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
|
||||
val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用
|
||||
)
|
||||
|
||||
/**
|
||||
* 跟单更新请求
|
||||
*/
|
||||
data class CopyTradingUpdateRequest(
|
||||
val copyTradingId: Long,
|
||||
val enabled: Boolean? = null,
|
||||
// 跟单配置参数(可选,只更新提供的字段)
|
||||
val copyMode: String? = null,
|
||||
val copyRatio: String? = null,
|
||||
val fixedAmount: String? = null,
|
||||
val maxOrderSize: String? = null,
|
||||
val minOrderSize: String? = null,
|
||||
val maxDailyLoss: String? = null,
|
||||
val maxDailyOrders: Int? = null,
|
||||
val priceTolerance: String? = null,
|
||||
val delaySeconds: Int? = null,
|
||||
val pollIntervalSeconds: Int? = null,
|
||||
val useWebSocket: Boolean? = null,
|
||||
val websocketReconnectInterval: Int? = null,
|
||||
val websocketMaxRetries: Int? = null,
|
||||
val supportSell: Boolean? = null,
|
||||
// 过滤条件
|
||||
val minOrderDepth: String? = null,
|
||||
val maxSpread: String? = null,
|
||||
val minOrderbookDepth: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -15,7 +67,6 @@ data class CopyTradingCreateRequest(
|
||||
*/
|
||||
data class CopyTradingListRequest(
|
||||
val accountId: Long? = null,
|
||||
val templateId: Long? = null,
|
||||
val leaderId: Long? = null,
|
||||
val enabled: Boolean? = null
|
||||
)
|
||||
@@ -50,12 +101,29 @@ data class CopyTradingDto(
|
||||
val accountId: Long,
|
||||
val accountName: String?,
|
||||
val walletAddress: String,
|
||||
val templateId: Long,
|
||||
val templateName: String,
|
||||
val leaderId: Long,
|
||||
val leaderName: String?,
|
||||
val leaderAddress: String,
|
||||
val enabled: Boolean,
|
||||
// 跟单配置参数
|
||||
val copyMode: String,
|
||||
val copyRatio: String,
|
||||
val fixedAmount: String?,
|
||||
val maxOrderSize: String,
|
||||
val minOrderSize: String,
|
||||
val maxDailyLoss: String,
|
||||
val maxDailyOrders: Int,
|
||||
val priceTolerance: String,
|
||||
val delaySeconds: Int,
|
||||
val pollIntervalSeconds: Int,
|
||||
val useWebSocket: Boolean,
|
||||
val websocketReconnectInterval: Int,
|
||||
val websocketMaxRetries: Int,
|
||||
val supportSell: Boolean,
|
||||
// 过滤条件
|
||||
val minOrderDepth: String?,
|
||||
val maxSpread: String?,
|
||||
val minOrderbookDepth: String?,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long
|
||||
)
|
||||
@@ -69,11 +137,11 @@ data class CopyTradingListResponse(
|
||||
)
|
||||
|
||||
/**
|
||||
* 钱包绑定的模板信息
|
||||
* 钱包绑定的跟单配置信息(已废弃,保留用于兼容)
|
||||
*/
|
||||
data class AccountTemplateDto(
|
||||
val templateId: Long,
|
||||
val templateName: String,
|
||||
val templateId: Long? = null, // 已废弃
|
||||
val templateName: String? = null, // 已废弃
|
||||
val copyTradingId: Long,
|
||||
val leaderId: Long,
|
||||
val leaderName: String?,
|
||||
|
||||
@@ -9,8 +9,6 @@ data class CopyTradingStatisticsResponse(
|
||||
val accountName: String?,
|
||||
val leaderId: Long,
|
||||
val leaderName: String?,
|
||||
val templateId: Long,
|
||||
val templateName: String?,
|
||||
val enabled: Boolean,
|
||||
|
||||
// 买入统计
|
||||
|
||||
@@ -18,7 +18,11 @@ data class TemplateCreateRequest(
|
||||
val useWebSocket: Boolean? = null,
|
||||
val websocketReconnectInterval: Int? = null,
|
||||
val websocketMaxRetries: Int? = null,
|
||||
val supportSell: Boolean? = null
|
||||
val supportSell: Boolean? = null,
|
||||
// 过滤条件
|
||||
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
|
||||
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
|
||||
val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -40,7 +44,11 @@ data class TemplateUpdateRequest(
|
||||
val useWebSocket: Boolean? = null,
|
||||
val websocketReconnectInterval: Int? = null,
|
||||
val websocketMaxRetries: Int? = null,
|
||||
val supportSell: Boolean? = null
|
||||
val supportSell: Boolean? = null,
|
||||
// 过滤条件
|
||||
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
|
||||
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
|
||||
val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -69,7 +77,11 @@ data class TemplateCopyRequest(
|
||||
val useWebSocket: Boolean? = null,
|
||||
val websocketReconnectInterval: Int? = null,
|
||||
val websocketMaxRetries: Int? = null,
|
||||
val supportSell: Boolean? = null
|
||||
val supportSell: Boolean? = null,
|
||||
// 过滤条件
|
||||
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
|
||||
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
|
||||
val minOrderbookDepth: String? = null // 最小订单簿深度(USDC金额),NULL表示不启用
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -99,7 +111,10 @@ data class TemplateDto(
|
||||
val websocketReconnectInterval: Int,
|
||||
val websocketMaxRetries: Int,
|
||||
val supportSell: Boolean,
|
||||
val useCount: Long = 0, // 使用该模板的跟单数量
|
||||
// 过滤条件
|
||||
val minOrderDepth: String?,
|
||||
val maxSpread: String?,
|
||||
val minOrderbookDepth: String?,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long
|
||||
)
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
/**
|
||||
* 被过滤订单列表请求
|
||||
*/
|
||||
data class FilteredOrderListRequest(
|
||||
val copyTradingId: Long,
|
||||
val filterType: String? = null, // 过滤类型(可选)
|
||||
val page: Int? = 1,
|
||||
val limit: Int? = 20,
|
||||
val startTime: Long? = null, // 开始时间(毫秒时间戳,可选)
|
||||
val endTime: Long? = null // 结束时间(毫秒时间戳,可选)
|
||||
)
|
||||
|
||||
/**
|
||||
* 被过滤订单信息响应
|
||||
*/
|
||||
data class FilteredOrderDto(
|
||||
val id: Long,
|
||||
val copyTradingId: Long,
|
||||
val accountId: Long,
|
||||
val accountName: String?,
|
||||
val leaderId: Long,
|
||||
val leaderName: String?,
|
||||
val leaderTradeId: String,
|
||||
val marketId: String,
|
||||
val marketTitle: String?,
|
||||
val marketSlug: String?,
|
||||
val side: String, // BUY 或 SELL
|
||||
val outcomeIndex: Int?,
|
||||
val outcome: String?,
|
||||
val price: String,
|
||||
val size: String,
|
||||
val calculatedQuantity: String?,
|
||||
val filterReason: String,
|
||||
val filterType: String,
|
||||
val createdAt: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* 被过滤订单列表响应
|
||||
*/
|
||||
data class FilteredOrderListResponse(
|
||||
val list: List<FilteredOrderDto>,
|
||||
val total: Long,
|
||||
val page: Int,
|
||||
val limit: Int
|
||||
)
|
||||
|
||||
@@ -23,9 +23,6 @@ data class CopyOrderTracking(
|
||||
@Column(name = "leader_id", nullable = false)
|
||||
val leaderId: Long,
|
||||
|
||||
@Column(name = "template_id", nullable = false)
|
||||
val templateId: Long,
|
||||
|
||||
@Column(name = "market_id", nullable = false, length = 100)
|
||||
val marketId: String,
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.math.BigDecimal
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
|
||||
/**
|
||||
* 跟单关系实体(钱包-模板关联,多对多关系)
|
||||
* 跟单配置实体(独立配置,不再绑定模板)
|
||||
*/
|
||||
@Entity
|
||||
@Table(
|
||||
name = "copy_trading",
|
||||
uniqueConstraints = [
|
||||
UniqueConstraint(columnNames = ["account_id", "template_id", "leader_id"])
|
||||
UniqueConstraint(columnNames = ["account_id", "leader_id"])
|
||||
]
|
||||
)
|
||||
data class CopyTrading(
|
||||
@@ -20,15 +22,65 @@ data class CopyTrading(
|
||||
@Column(name = "account_id", nullable = false)
|
||||
val accountId: Long, // 钱包账户ID
|
||||
|
||||
@Column(name = "template_id", nullable = false)
|
||||
val templateId: Long, // 模板ID
|
||||
|
||||
@Column(name = "leader_id", nullable = false)
|
||||
val leaderId: Long, // Leader ID
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
val enabled: Boolean = true, // 是否启用
|
||||
|
||||
// 跟单配置参数
|
||||
@Column(name = "copy_mode", nullable = false, length = 10)
|
||||
val copyMode: String = "RATIO", // "RATIO" 或 "FIXED"
|
||||
|
||||
@Column(name = "copy_ratio", nullable = false, precision = 10, scale = 2)
|
||||
val copyRatio: BigDecimal = BigDecimal.ONE, // 仅在 copyMode="RATIO" 时生效
|
||||
|
||||
@Column(name = "fixed_amount", precision = 20, scale = 8)
|
||||
val fixedAmount: BigDecimal? = null, // 仅在 copyMode="FIXED" 时生效
|
||||
|
||||
@Column(name = "max_order_size", nullable = false, precision = 20, scale = 8)
|
||||
val maxOrderSize: BigDecimal = "1000".toSafeBigDecimal(),
|
||||
|
||||
@Column(name = "min_order_size", nullable = false, precision = 20, scale = 8)
|
||||
val minOrderSize: BigDecimal = "1".toSafeBigDecimal(),
|
||||
|
||||
@Column(name = "max_daily_loss", nullable = false, precision = 20, scale = 8)
|
||||
val maxDailyLoss: BigDecimal = "10000".toSafeBigDecimal(),
|
||||
|
||||
@Column(name = "max_daily_orders", nullable = false)
|
||||
val maxDailyOrders: Int = 100,
|
||||
|
||||
@Column(name = "price_tolerance", nullable = false, precision = 5, scale = 2)
|
||||
val priceTolerance: BigDecimal = "5".toSafeBigDecimal(), // 百分比
|
||||
|
||||
@Column(name = "delay_seconds", nullable = false)
|
||||
val delaySeconds: Int = 0,
|
||||
|
||||
@Column(name = "poll_interval_seconds", nullable = false)
|
||||
val pollIntervalSeconds: Int = 5, // 轮询间隔(仅在 WebSocket 不可用时使用)
|
||||
|
||||
@Column(name = "use_websocket", nullable = false)
|
||||
val useWebSocket: Boolean = true, // 是否优先使用 WebSocket 推送
|
||||
|
||||
@Column(name = "websocket_reconnect_interval", nullable = false)
|
||||
val websocketReconnectInterval: Int = 5000, // WebSocket 重连间隔(毫秒)
|
||||
|
||||
@Column(name = "websocket_max_retries", nullable = false)
|
||||
val websocketMaxRetries: Int = 10, // WebSocket 最大重试次数
|
||||
|
||||
@Column(name = "support_sell", nullable = false)
|
||||
val supportSell: Boolean = true, // 是否支持跟单卖出
|
||||
|
||||
// 过滤条件字段
|
||||
@Column(name = "min_order_depth", precision = 20, scale = 8)
|
||||
val minOrderDepth: BigDecimal? = null, // 最小订单深度(USDC金额),NULL表示不启用
|
||||
|
||||
@Column(name = "max_spread", precision = 20, scale = 8)
|
||||
val maxSpread: BigDecimal? = null, // 最大价差(绝对价格),NULL表示不启用
|
||||
|
||||
@Column(name = "min_orderbook_depth", precision = 20, scale = 8)
|
||||
val minOrderbookDepth: BigDecimal? = null, // 最小订单簿深度(USDC金额),NULL表示不启用
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
|
||||
@@ -59,6 +59,16 @@ data class CopyTradingTemplate(
|
||||
@Column(name = "support_sell", nullable = false)
|
||||
val supportSell: Boolean = true, // 是否支持跟单卖出
|
||||
|
||||
// 过滤条件字段
|
||||
@Column(name = "min_order_depth", precision = 20, scale = 8)
|
||||
val minOrderDepth: BigDecimal? = null, // 最小订单深度(USDC金额),NULL表示不启用
|
||||
|
||||
@Column(name = "max_spread", precision = 20, scale = 8)
|
||||
val maxSpread: BigDecimal? = null, // 最大价差(绝对价格),NULL表示不启用
|
||||
|
||||
@Column(name = "min_orderbook_depth", precision = 20, scale = 8)
|
||||
val minOrderbookDepth: BigDecimal? = null, // 最小订单簿深度(USDC金额),NULL表示不启用
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 被过滤订单实体
|
||||
* 记录因筛选条件不满足而被过滤的订单信息
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "filtered_order")
|
||||
data class FilteredOrder(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "copy_trading_id", nullable = false)
|
||||
val copyTradingId: Long,
|
||||
|
||||
@Column(name = "account_id", nullable = false)
|
||||
val accountId: Long,
|
||||
|
||||
@Column(name = "leader_id", nullable = false)
|
||||
val leaderId: Long,
|
||||
|
||||
@Column(name = "leader_trade_id", nullable = false, length = 100)
|
||||
val leaderTradeId: String, // Leader 的交易ID
|
||||
|
||||
@Column(name = "market_id", nullable = false, length = 100)
|
||||
val marketId: String,
|
||||
|
||||
@Column(name = "market_title", length = 500)
|
||||
val marketTitle: String? = null, // 市场标题(从 API 获取)
|
||||
|
||||
@Column(name = "market_slug", length = 200)
|
||||
val marketSlug: String? = null, // 市场 slug(用于生成链接)
|
||||
|
||||
@Column(name = "side", nullable = false, length = 10)
|
||||
val side: String, // BUY 或 SELL
|
||||
|
||||
@Column(name = "outcome_index", nullable = true)
|
||||
val outcomeIndex: Int? = null, // 结果索引(0, 1, 2, ...),支持多元市场
|
||||
|
||||
@Column(name = "outcome", length = 50)
|
||||
val outcome: String? = null, // 市场方向(如 YES, NO 等)
|
||||
|
||||
@Column(name = "price", nullable = false, precision = 20, scale = 8)
|
||||
val price: BigDecimal, // Leader 交易价格
|
||||
|
||||
@Column(name = "size", nullable = false, precision = 20, scale = 8)
|
||||
val size: BigDecimal, // Leader 交易数量
|
||||
|
||||
@Column(name = "calculated_quantity", precision = 20, scale = 8)
|
||||
val calculatedQuantity: BigDecimal? = null, // 计算出的跟单数量(如果已计算)
|
||||
|
||||
@Column(name = "filter_reason", nullable = false, columnDefinition = "TEXT")
|
||||
val filterReason: String, // 过滤原因(详细说明)
|
||||
|
||||
@Column(name = "filter_type", nullable = false, length = 50)
|
||||
val filterType: String, // 过滤类型(如 ORDER_DEPTH, SPREAD, ORDERBOOK_DEPTH 等)
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
+2
-18
@@ -15,27 +15,16 @@ interface CopyTradingRepository : JpaRepository<CopyTrading, Long> {
|
||||
*/
|
||||
fun findByAccountId(accountId: Long): List<CopyTrading>
|
||||
|
||||
/**
|
||||
* 根据模板ID查找跟单列表
|
||||
*/
|
||||
fun findByTemplateId(templateId: Long): List<CopyTrading>
|
||||
|
||||
/**
|
||||
* 根据 Leader ID 查找跟单列表
|
||||
*/
|
||||
fun findByLeaderId(leaderId: Long): List<CopyTrading>
|
||||
|
||||
/**
|
||||
* 根据账户ID和模板ID查找跟单列表
|
||||
* 根据账户ID和Leader ID查找跟单
|
||||
*/
|
||||
fun findByAccountIdAndTemplateId(accountId: Long, templateId: Long): List<CopyTrading>
|
||||
|
||||
/**
|
||||
* 根据账户ID、模板ID和Leader ID查找跟单
|
||||
*/
|
||||
fun findByAccountIdAndTemplateIdAndLeaderId(
|
||||
fun findByAccountIdAndLeaderId(
|
||||
accountId: Long,
|
||||
templateId: Long,
|
||||
leaderId: Long
|
||||
): CopyTrading?
|
||||
|
||||
@@ -54,11 +43,6 @@ interface CopyTradingRepository : JpaRepository<CopyTrading, Long> {
|
||||
*/
|
||||
fun findByLeaderIdAndEnabledTrue(leaderId: Long): List<CopyTrading>
|
||||
|
||||
/**
|
||||
* 统计使用指定模板的跟单数量
|
||||
*/
|
||||
fun countByTemplateId(templateId: Long): Long
|
||||
|
||||
/**
|
||||
* 统计指定 Leader 的跟单数量
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.FilteredOrder
|
||||
import org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface FilteredOrderRepository : JpaRepository<FilteredOrder, Long> {
|
||||
|
||||
/**
|
||||
* 根据跟单配置ID查询被过滤的订单(分页)
|
||||
*/
|
||||
fun findByCopyTradingIdOrderByCreatedAtDesc(
|
||||
copyTradingId: Long,
|
||||
pageable: Pageable
|
||||
): Page<FilteredOrder>
|
||||
|
||||
/**
|
||||
* 根据跟单配置ID和过滤类型查询被过滤的订单(分页)
|
||||
*/
|
||||
fun findByCopyTradingIdAndFilterTypeOrderByCreatedAtDesc(
|
||||
copyTradingId: Long,
|
||||
filterType: String,
|
||||
pageable: Pageable
|
||||
): Page<FilteredOrder>
|
||||
|
||||
/**
|
||||
* 根据跟单配置ID和时间范围查询被过滤的订单(分页)
|
||||
*/
|
||||
@Query("SELECT f FROM FilteredOrder f WHERE f.copyTradingId = :copyTradingId AND f.createdAt >= :startTime AND f.createdAt <= :endTime ORDER BY f.createdAt DESC")
|
||||
fun findByCopyTradingIdAndTimeRange(
|
||||
@Param("copyTradingId") copyTradingId: Long,
|
||||
@Param("startTime") startTime: Long,
|
||||
@Param("endTime") endTime: Long,
|
||||
pageable: Pageable
|
||||
): Page<FilteredOrder>
|
||||
|
||||
/**
|
||||
* 统计某个跟单配置的被过滤订单数量
|
||||
*/
|
||||
fun countByCopyTradingId(copyTradingId: Long): Long
|
||||
|
||||
/**
|
||||
* 统计某个跟单配置的某个过滤类型的被过滤订单数量
|
||||
*/
|
||||
fun countByCopyTradingIdAndFilterType(copyTradingId: Long, filterType: String): Long
|
||||
}
|
||||
|
||||
+330
-194
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,176 @@
|
||||
package com.wrbug.polymarketbot.service
|
||||
|
||||
import com.wrbug.polymarketbot.api.OrderbookResponse
|
||||
import com.wrbug.polymarketbot.entity.CopyTrading
|
||||
import com.wrbug.polymarketbot.util.gt
|
||||
import com.wrbug.polymarketbot.util.lt
|
||||
import com.wrbug.polymarketbot.util.multi
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 跟单过滤条件检查服务
|
||||
*/
|
||||
@Service
|
||||
class CopyTradingFilterService(
|
||||
private val clobService: PolymarketClobService
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(CopyTradingFilterService::class.java)
|
||||
|
||||
/**
|
||||
* 检查过滤条件
|
||||
* @param copyTrading 跟单配置
|
||||
* @param tokenId token ID(用于获取订单簿)
|
||||
* @param isBuyOrder 是否为买入订单(true=买入,false=卖出)
|
||||
* @return Pair<是否通过, 失败原因>
|
||||
*/
|
||||
suspend fun checkFilters(
|
||||
copyTrading: CopyTrading,
|
||||
tokenId: String,
|
||||
isBuyOrder: Boolean
|
||||
): Pair<Boolean, String> {
|
||||
// 1. 价格合理性检查(基础检查,无需配置)
|
||||
// 这个检查在获取订单簿时进行,如果价格不在 0.01-0.99 范围内,订单簿获取会失败
|
||||
|
||||
// 2. 获取订单簿
|
||||
val orderbookResult = clobService.getOrderbookByTokenId(tokenId)
|
||||
if (!orderbookResult.isSuccess) {
|
||||
val error = orderbookResult.exceptionOrNull()
|
||||
return Pair(false, "获取订单簿失败: ${error?.message ?: "未知错误"}")
|
||||
}
|
||||
|
||||
val orderbook = orderbookResult.getOrNull()
|
||||
if (orderbook == null) {
|
||||
return Pair(false, "订单簿为空")
|
||||
}
|
||||
|
||||
// 3. 买一卖一价差过滤
|
||||
val spreadCheck = checkSpread(copyTrading, orderbook)
|
||||
if (!spreadCheck.first) {
|
||||
return spreadCheck
|
||||
}
|
||||
|
||||
// 4. 订单深度过滤
|
||||
val depthCheck = checkOrderDepth(copyTrading, orderbook, isBuyOrder)
|
||||
if (!depthCheck.first) {
|
||||
return depthCheck
|
||||
}
|
||||
|
||||
// 5. 最小订单簿深度过滤(可选)
|
||||
val orderbookDepthCheck = checkOrderbookDepth(copyTrading, orderbook, isBuyOrder)
|
||||
if (!orderbookDepthCheck.first) {
|
||||
return orderbookDepthCheck
|
||||
}
|
||||
|
||||
return Pair(true, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查买一卖一价差
|
||||
* bestBid: 买盘中的最高价格(最大值)
|
||||
* bestAsk: 卖盘中的最低价格(最小值)
|
||||
*/
|
||||
private fun checkSpread(
|
||||
copyTrading: CopyTrading,
|
||||
orderbook: OrderbookResponse
|
||||
): Pair<Boolean, String> {
|
||||
// 如果未启用价差过滤,直接通过
|
||||
if (copyTrading.maxSpread == null) {
|
||||
return Pair(true, "")
|
||||
}
|
||||
|
||||
// 获取买盘中的最高价格(bestBid = bids 中的最大值)
|
||||
val bestBid = orderbook.bids
|
||||
.mapNotNull { it.price.toSafeBigDecimal() }
|
||||
.maxOrNull()
|
||||
|
||||
// 获取卖盘中的最低价格(bestAsk = asks 中的最小值)
|
||||
val bestAsk = orderbook.asks
|
||||
.mapNotNull { it.price.toSafeBigDecimal() }
|
||||
.minOrNull()
|
||||
|
||||
if (bestBid == null || bestAsk == null) {
|
||||
return Pair(false, "订单簿缺少买一或卖一价格")
|
||||
}
|
||||
|
||||
// 计算价差(绝对价格)
|
||||
val spread = bestAsk.subtract(bestBid)
|
||||
|
||||
if (spread.gt(copyTrading.maxSpread)) {
|
||||
return Pair(false, "价差过大: $spread > ${copyTrading.maxSpread}")
|
||||
}
|
||||
|
||||
return Pair(true, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查订单深度
|
||||
*/
|
||||
private fun checkOrderDepth(
|
||||
copyTrading: CopyTrading,
|
||||
orderbook: OrderbookResponse,
|
||||
isBuyOrder: Boolean
|
||||
): Pair<Boolean, String> {
|
||||
// 如果未启用订单深度过滤,直接通过
|
||||
if (copyTrading.minOrderDepth == null) {
|
||||
return Pair(true, "")
|
||||
}
|
||||
|
||||
// 对于买入订单,检查卖盘(asks)深度
|
||||
// 对于卖出订单,检查买盘(bids)深度
|
||||
val orders = if (isBuyOrder) orderbook.asks else orderbook.bids
|
||||
|
||||
// 计算总深度(累计订单金额)
|
||||
var totalDepth = BigDecimal.ZERO
|
||||
for (order in orders) {
|
||||
val price = order.price.toSafeBigDecimal()
|
||||
val size = order.size.toSafeBigDecimal()
|
||||
val orderAmount = price.multi(size)
|
||||
totalDepth = totalDepth.add(orderAmount)
|
||||
}
|
||||
|
||||
if (totalDepth.lt(copyTrading.minOrderDepth)) {
|
||||
return Pair(false, "订单深度不足: $totalDepth < ${copyTrading.minOrderDepth}")
|
||||
}
|
||||
|
||||
return Pair(true, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查最小订单簿深度(前 N 档深度)
|
||||
*/
|
||||
private fun checkOrderbookDepth(
|
||||
copyTrading: CopyTrading,
|
||||
orderbook: OrderbookResponse,
|
||||
isBuyOrder: Boolean
|
||||
): Pair<Boolean, String> {
|
||||
// 如果未启用最小订单簿深度过滤,直接通过
|
||||
if (copyTrading.minOrderbookDepth == null) {
|
||||
return Pair(true, "")
|
||||
}
|
||||
|
||||
// 对于买入订单,检查卖盘(asks)前 3 档深度
|
||||
// 对于卖出订单,检查买盘(bids)前 3 档深度
|
||||
val orders = if (isBuyOrder) orderbook.asks else orderbook.bids
|
||||
val topNOrders = orders.take(3) // 前 3 档
|
||||
|
||||
// 计算前 N 档总深度
|
||||
var totalDepth = BigDecimal.ZERO
|
||||
for (order in topNOrders) {
|
||||
val price = order.price.toSafeBigDecimal()
|
||||
val size = order.size.toSafeBigDecimal()
|
||||
val orderAmount = price.multi(size)
|
||||
totalDepth = totalDepth.add(orderAmount)
|
||||
}
|
||||
|
||||
if (totalDepth.lt(copyTrading.minOrderbookDepth)) {
|
||||
return Pair(false, "订单簿深度不足: $totalDepth < ${copyTrading.minOrderbookDepth}")
|
||||
}
|
||||
|
||||
return Pair(true, "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.repository.CopyTradingRepository
|
||||
import com.wrbug.polymarketbot.repository.CopyTradingTemplateRepository
|
||||
import com.wrbug.polymarketbot.repository.LeaderRepository
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 跟单配置管理服务(钱包-模板关联)
|
||||
* 跟单配置管理服务(独立配置,不再绑定模板)
|
||||
*/
|
||||
@Service
|
||||
class CopyTradingService(
|
||||
@@ -25,7 +27,10 @@ class CopyTradingService(
|
||||
private val logger = LoggerFactory.getLogger(CopyTradingService::class.java)
|
||||
|
||||
/**
|
||||
* 创建跟单
|
||||
* 创建跟单配置
|
||||
* 支持两种方式:
|
||||
* 1. 提供 templateId:从模板填充配置,可以覆盖部分字段
|
||||
* 2. 不提供 templateId:手动输入所有配置参数
|
||||
*/
|
||||
@Transactional
|
||||
fun createCopyTrading(request: CopyTradingCreateRequest): Result<CopyTradingDto> {
|
||||
@@ -34,30 +39,94 @@ class CopyTradingService(
|
||||
val account = accountRepository.findById(request.accountId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
|
||||
// 2. 验证模板是否存在
|
||||
val template = templateRepository.findById(request.templateId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("模板不存在"))
|
||||
|
||||
// 3. 验证 Leader 是否存在
|
||||
// 2. 验证 Leader 是否存在
|
||||
val leader = leaderRepository.findById(request.leaderId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("Leader 不存在"))
|
||||
|
||||
// 4. 检查是否已存在相同的跟单关系
|
||||
val existing = copyTradingRepository.findByAccountIdAndTemplateIdAndLeaderId(
|
||||
// 3. 检查是否已存在相同的跟单关系(accountId + leaderId)
|
||||
val existing = copyTradingRepository.findByAccountIdAndLeaderId(
|
||||
request.accountId,
|
||||
request.templateId,
|
||||
request.leaderId
|
||||
)
|
||||
if (existing != null) {
|
||||
return Result.failure(IllegalArgumentException("该跟单关系已存在"))
|
||||
}
|
||||
|
||||
// 5. 创建跟单关系
|
||||
// 4. 获取配置参数(从模板填充或手动输入)
|
||||
val config = if (request.templateId != null) {
|
||||
// 从模板填充
|
||||
val template = templateRepository.findById(request.templateId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("模板不存在"))
|
||||
|
||||
// 使用模板值,但允许请求中的字段覆盖
|
||||
CopyTradingConfig(
|
||||
copyMode = request.copyMode ?: template.copyMode,
|
||||
copyRatio = request.copyRatio?.toSafeBigDecimal() ?: template.copyRatio,
|
||||
fixedAmount = request.fixedAmount?.toSafeBigDecimal() ?: template.fixedAmount,
|
||||
maxOrderSize = request.maxOrderSize?.toSafeBigDecimal() ?: template.maxOrderSize,
|
||||
minOrderSize = request.minOrderSize?.toSafeBigDecimal() ?: template.minOrderSize,
|
||||
maxDailyLoss = request.maxDailyLoss?.toSafeBigDecimal() ?: template.maxDailyLoss,
|
||||
maxDailyOrders = request.maxDailyOrders ?: template.maxDailyOrders,
|
||||
priceTolerance = request.priceTolerance?.toSafeBigDecimal() ?: template.priceTolerance,
|
||||
delaySeconds = request.delaySeconds ?: template.delaySeconds,
|
||||
pollIntervalSeconds = request.pollIntervalSeconds ?: template.pollIntervalSeconds,
|
||||
useWebSocket = request.useWebSocket ?: template.useWebSocket,
|
||||
websocketReconnectInterval = request.websocketReconnectInterval ?: template.websocketReconnectInterval,
|
||||
websocketMaxRetries = request.websocketMaxRetries ?: template.websocketMaxRetries,
|
||||
supportSell = request.supportSell ?: template.supportSell,
|
||||
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: template.minOrderDepth,
|
||||
maxSpread = request.maxSpread?.toSafeBigDecimal() ?: template.maxSpread,
|
||||
minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: template.minOrderbookDepth
|
||||
)
|
||||
} else {
|
||||
// 手动输入(所有字段必须提供)
|
||||
if (request.copyMode == null) {
|
||||
return Result.failure(IllegalArgumentException("copyMode 不能为空"))
|
||||
}
|
||||
|
||||
CopyTradingConfig(
|
||||
copyMode = request.copyMode,
|
||||
copyRatio = request.copyRatio?.toSafeBigDecimal() ?: BigDecimal.ONE,
|
||||
fixedAmount = request.fixedAmount?.toSafeBigDecimal(),
|
||||
maxOrderSize = request.maxOrderSize?.toSafeBigDecimal() ?: "1000".toSafeBigDecimal(),
|
||||
minOrderSize = request.minOrderSize?.toSafeBigDecimal() ?: "1".toSafeBigDecimal(),
|
||||
maxDailyLoss = request.maxDailyLoss?.toSafeBigDecimal() ?: "10000".toSafeBigDecimal(),
|
||||
maxDailyOrders = request.maxDailyOrders ?: 100,
|
||||
priceTolerance = request.priceTolerance?.toSafeBigDecimal() ?: "5".toSafeBigDecimal(),
|
||||
delaySeconds = request.delaySeconds ?: 0,
|
||||
pollIntervalSeconds = request.pollIntervalSeconds ?: 5,
|
||||
useWebSocket = request.useWebSocket ?: true,
|
||||
websocketReconnectInterval = request.websocketReconnectInterval ?: 5000,
|
||||
websocketMaxRetries = request.websocketMaxRetries ?: 10,
|
||||
supportSell = request.supportSell ?: true,
|
||||
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal(),
|
||||
maxSpread = request.maxSpread?.toSafeBigDecimal(),
|
||||
minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal()
|
||||
)
|
||||
}
|
||||
|
||||
// 5. 创建跟单配置
|
||||
val copyTrading = CopyTrading(
|
||||
accountId = request.accountId,
|
||||
templateId = request.templateId,
|
||||
leaderId = request.leaderId,
|
||||
enabled = request.enabled
|
||||
enabled = request.enabled,
|
||||
copyMode = config.copyMode,
|
||||
copyRatio = config.copyRatio,
|
||||
fixedAmount = config.fixedAmount,
|
||||
maxOrderSize = config.maxOrderSize,
|
||||
minOrderSize = config.minOrderSize,
|
||||
maxDailyLoss = config.maxDailyLoss,
|
||||
maxDailyOrders = config.maxDailyOrders,
|
||||
priceTolerance = config.priceTolerance,
|
||||
delaySeconds = config.delaySeconds,
|
||||
pollIntervalSeconds = config.pollIntervalSeconds,
|
||||
useWebSocket = config.useWebSocket,
|
||||
websocketReconnectInterval = config.websocketReconnectInterval,
|
||||
websocketMaxRetries = config.websocketMaxRetries,
|
||||
supportSell = config.supportSell,
|
||||
minOrderDepth = config.minOrderDepth,
|
||||
maxSpread = config.maxSpread,
|
||||
minOrderbookDepth = config.minOrderbookDepth
|
||||
)
|
||||
|
||||
val saved = copyTradingRepository.save(copyTrading)
|
||||
@@ -73,36 +142,99 @@ class CopyTradingService(
|
||||
}
|
||||
}
|
||||
|
||||
Result.success(toDto(saved, account, template, leader))
|
||||
Result.success(toDto(saved, account, leader))
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建跟单失败", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新跟单配置
|
||||
*/
|
||||
@Transactional
|
||||
fun updateCopyTrading(request: CopyTradingUpdateRequest): Result<CopyTradingDto> {
|
||||
return try {
|
||||
val copyTrading = copyTradingRepository.findById(request.copyTradingId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("跟单配置不存在"))
|
||||
|
||||
// 更新字段(只更新提供的字段)
|
||||
val updated = copyTrading.copy(
|
||||
enabled = request.enabled ?: copyTrading.enabled,
|
||||
copyMode = request.copyMode ?: copyTrading.copyMode,
|
||||
copyRatio = request.copyRatio?.toSafeBigDecimal() ?: copyTrading.copyRatio,
|
||||
fixedAmount = request.fixedAmount?.toSafeBigDecimal() ?: copyTrading.fixedAmount,
|
||||
maxOrderSize = request.maxOrderSize?.toSafeBigDecimal() ?: copyTrading.maxOrderSize,
|
||||
minOrderSize = request.minOrderSize?.toSafeBigDecimal() ?: copyTrading.minOrderSize,
|
||||
maxDailyLoss = request.maxDailyLoss?.toSafeBigDecimal() ?: copyTrading.maxDailyLoss,
|
||||
maxDailyOrders = request.maxDailyOrders ?: copyTrading.maxDailyOrders,
|
||||
priceTolerance = request.priceTolerance?.toSafeBigDecimal() ?: copyTrading.priceTolerance,
|
||||
delaySeconds = request.delaySeconds ?: copyTrading.delaySeconds,
|
||||
pollIntervalSeconds = request.pollIntervalSeconds ?: copyTrading.pollIntervalSeconds,
|
||||
useWebSocket = request.useWebSocket ?: copyTrading.useWebSocket,
|
||||
websocketReconnectInterval = request.websocketReconnectInterval ?: copyTrading.websocketReconnectInterval,
|
||||
websocketMaxRetries = request.websocketMaxRetries ?: copyTrading.websocketMaxRetries,
|
||||
supportSell = request.supportSell ?: copyTrading.supportSell,
|
||||
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: copyTrading.minOrderDepth,
|
||||
maxSpread = request.maxSpread?.toSafeBigDecimal() ?: copyTrading.maxSpread,
|
||||
minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: copyTrading.minOrderbookDepth,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val saved = copyTradingRepository.save(updated)
|
||||
|
||||
// 重新启动监听(确保状态完全同步)
|
||||
kotlinx.coroutines.runBlocking {
|
||||
try {
|
||||
monitorService.restartMonitoring()
|
||||
} catch (e: Exception) {
|
||||
logger.error("重新启动跟单监听失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
val account = accountRepository.findById(saved.accountId).orElse(null)
|
||||
val leader = leaderRepository.findById(saved.leaderId).orElse(null)
|
||||
|
||||
if (account == null || leader == null) {
|
||||
return Result.failure(IllegalStateException("跟单配置数据不完整"))
|
||||
}
|
||||
|
||||
Result.success(toDto(saved, account, leader))
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新跟单配置失败", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新跟单状态(兼容旧接口)
|
||||
*/
|
||||
@Transactional
|
||||
fun updateCopyTradingStatus(request: CopyTradingUpdateStatusRequest): Result<CopyTradingDto> {
|
||||
return updateCopyTrading(
|
||||
CopyTradingUpdateRequest(
|
||||
copyTradingId = request.copyTradingId,
|
||||
enabled = request.enabled
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询跟单列表
|
||||
*/
|
||||
fun getCopyTradingList(request: CopyTradingListRequest): Result<CopyTradingListResponse> {
|
||||
return try {
|
||||
val copyTradings = when {
|
||||
request.accountId != null && request.templateId != null && request.leaderId != null -> {
|
||||
val found = copyTradingRepository.findByAccountIdAndTemplateIdAndLeaderId(
|
||||
request.accountId != null && request.leaderId != null -> {
|
||||
val found = copyTradingRepository.findByAccountIdAndLeaderId(
|
||||
request.accountId,
|
||||
request.templateId,
|
||||
request.leaderId
|
||||
)
|
||||
if (found != null) listOf(found) else emptyList()
|
||||
}
|
||||
request.accountId != null && request.templateId != null -> {
|
||||
copyTradingRepository.findByAccountIdAndTemplateId(request.accountId, request.templateId)
|
||||
}
|
||||
request.accountId != null -> {
|
||||
copyTradingRepository.findByAccountId(request.accountId)
|
||||
}
|
||||
request.templateId != null -> {
|
||||
copyTradingRepository.findByTemplateId(request.templateId)
|
||||
}
|
||||
request.leaderId != null -> {
|
||||
copyTradingRepository.findByLeaderId(request.leaderId)
|
||||
}
|
||||
@@ -121,18 +253,17 @@ class CopyTradingService(
|
||||
copyTradings
|
||||
}
|
||||
|
||||
val dtos = filtered.map { copyTrading ->
|
||||
val dtos = filtered.mapNotNull { copyTrading ->
|
||||
val account = accountRepository.findById(copyTrading.accountId).orElse(null)
|
||||
val template = templateRepository.findById(copyTrading.templateId).orElse(null)
|
||||
val leader = leaderRepository.findById(copyTrading.leaderId).orElse(null)
|
||||
|
||||
if (account == null || template == null || leader == null) {
|
||||
logger.warn("跟单关系数据不完整: ${copyTrading.id}")
|
||||
if (account == null || leader == null) {
|
||||
logger.warn("跟单配置数据不完整: ${copyTrading.id}")
|
||||
null
|
||||
} else {
|
||||
toDto(copyTrading, account, template, leader)
|
||||
toDto(copyTrading, account, leader)
|
||||
}
|
||||
}.filterNotNull()
|
||||
}
|
||||
|
||||
Result.success(
|
||||
CopyTradingListResponse(
|
||||
@@ -146,46 +277,6 @@ class CopyTradingService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新跟单状态
|
||||
*/
|
||||
@Transactional
|
||||
fun updateCopyTradingStatus(request: CopyTradingUpdateStatusRequest): Result<CopyTradingDto> {
|
||||
return try {
|
||||
val copyTrading = copyTradingRepository.findById(request.copyTradingId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("跟单关系不存在"))
|
||||
|
||||
val updated = copyTrading.copy(
|
||||
enabled = request.enabled,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val saved = copyTradingRepository.save(updated)
|
||||
|
||||
// 重新启动监听(确保状态完全同步)
|
||||
kotlinx.coroutines.runBlocking {
|
||||
try {
|
||||
monitorService.restartMonitoring()
|
||||
} catch (e: Exception) {
|
||||
logger.error("重新启动跟单监听失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
val account = accountRepository.findById(saved.accountId).orElse(null)
|
||||
val template = templateRepository.findById(saved.templateId).orElse(null)
|
||||
val leader = leaderRepository.findById(saved.leaderId).orElse(null)
|
||||
|
||||
if (account == null || template == null || leader == null) {
|
||||
return Result.failure(IllegalStateException("跟单关系数据不完整"))
|
||||
}
|
||||
|
||||
Result.success(toDto(saved, account, template, leader))
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新跟单状态失败", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除跟单
|
||||
*/
|
||||
@@ -193,7 +284,7 @@ class CopyTradingService(
|
||||
fun deleteCopyTrading(copyTradingId: Long): Result<Unit> {
|
||||
return try {
|
||||
val copyTrading = copyTradingRepository.findById(copyTradingId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("跟单关系不存在"))
|
||||
?: return Result.failure(IllegalArgumentException("跟单配置不存在"))
|
||||
|
||||
copyTradingRepository.delete(copyTrading)
|
||||
|
||||
@@ -214,7 +305,7 @@ class CopyTradingService(
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询钱包绑定的模板
|
||||
* 查询钱包绑定的跟单配置(兼容旧接口)
|
||||
*/
|
||||
fun getAccountTemplates(accountId: Long): Result<AccountTemplatesResponse> {
|
||||
return try {
|
||||
@@ -225,16 +316,15 @@ class CopyTradingService(
|
||||
val copyTradings = copyTradingRepository.findByAccountId(accountId)
|
||||
|
||||
val dtos = copyTradings.mapNotNull { copyTrading ->
|
||||
val template = templateRepository.findById(copyTrading.templateId).orElse(null)
|
||||
val leader = leaderRepository.findById(copyTrading.leaderId).orElse(null)
|
||||
|
||||
if (template == null || leader == null) {
|
||||
logger.warn("跟单关系数据不完整: ${copyTrading.id}")
|
||||
if (leader == null) {
|
||||
logger.warn("跟单配置数据不完整: ${copyTrading.id}")
|
||||
null
|
||||
} else {
|
||||
AccountTemplateDto(
|
||||
templateId = template.id!!,
|
||||
templateName = template.templateName,
|
||||
templateId = null, // 已废弃
|
||||
templateName = null, // 已废弃
|
||||
copyTradingId = copyTrading.id!!,
|
||||
leaderId = leader.id!!,
|
||||
leaderName = leader.leaderName,
|
||||
@@ -251,7 +341,7 @@ class CopyTradingService(
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("查询钱包绑定的模板失败", e)
|
||||
logger.error("查询钱包绑定的跟单配置失败", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
@@ -262,7 +352,6 @@ class CopyTradingService(
|
||||
private fun toDto(
|
||||
copyTrading: CopyTrading,
|
||||
account: com.wrbug.polymarketbot.entity.Account,
|
||||
template: com.wrbug.polymarketbot.entity.CopyTradingTemplate,
|
||||
leader: com.wrbug.polymarketbot.entity.Leader
|
||||
): CopyTradingDto {
|
||||
return CopyTradingDto(
|
||||
@@ -270,15 +359,52 @@ class CopyTradingService(
|
||||
accountId = account.id!!,
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
templateId = template.id!!,
|
||||
templateName = template.templateName,
|
||||
leaderId = leader.id!!,
|
||||
leaderName = leader.leaderName,
|
||||
leaderAddress = leader.leaderAddress,
|
||||
enabled = copyTrading.enabled,
|
||||
copyMode = copyTrading.copyMode,
|
||||
copyRatio = copyTrading.copyRatio.toPlainString(),
|
||||
fixedAmount = copyTrading.fixedAmount?.toPlainString(),
|
||||
maxOrderSize = copyTrading.maxOrderSize.toPlainString(),
|
||||
minOrderSize = copyTrading.minOrderSize.toPlainString(),
|
||||
maxDailyLoss = copyTrading.maxDailyLoss.toPlainString(),
|
||||
maxDailyOrders = copyTrading.maxDailyOrders,
|
||||
priceTolerance = copyTrading.priceTolerance.toPlainString(),
|
||||
delaySeconds = copyTrading.delaySeconds,
|
||||
pollIntervalSeconds = copyTrading.pollIntervalSeconds,
|
||||
useWebSocket = copyTrading.useWebSocket,
|
||||
websocketReconnectInterval = copyTrading.websocketReconnectInterval,
|
||||
websocketMaxRetries = copyTrading.websocketMaxRetries,
|
||||
supportSell = copyTrading.supportSell,
|
||||
minOrderDepth = copyTrading.minOrderDepth?.toPlainString(),
|
||||
maxSpread = copyTrading.maxSpread?.toPlainString(),
|
||||
minOrderbookDepth = copyTrading.minOrderbookDepth?.toPlainString(),
|
||||
createdAt = copyTrading.createdAt,
|
||||
updatedAt = copyTrading.updatedAt
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部配置类(用于构建 CopyTrading 实体)
|
||||
*/
|
||||
private data class CopyTradingConfig(
|
||||
val copyMode: String,
|
||||
val copyRatio: BigDecimal,
|
||||
val fixedAmount: BigDecimal?,
|
||||
val maxOrderSize: BigDecimal,
|
||||
val minOrderSize: BigDecimal,
|
||||
val maxDailyLoss: BigDecimal,
|
||||
val maxDailyOrders: Int,
|
||||
val priceTolerance: BigDecimal,
|
||||
val delaySeconds: Int,
|
||||
val pollIntervalSeconds: Int,
|
||||
val useWebSocket: Boolean,
|
||||
val websocketReconnectInterval: Int,
|
||||
val websocketMaxRetries: Int,
|
||||
val supportSell: Boolean,
|
||||
val minOrderDepth: BigDecimal?,
|
||||
val maxSpread: BigDecimal?,
|
||||
val minOrderbookDepth: BigDecimal?
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
-4
@@ -29,7 +29,6 @@ class CopyTradingStatisticsService(
|
||||
private val sellMatchDetailRepository: SellMatchDetailRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val leaderRepository: LeaderRepository,
|
||||
private val templateRepository: CopyTradingTemplateRepository,
|
||||
private val accountService: AccountService
|
||||
) {
|
||||
|
||||
@@ -47,7 +46,6 @@ class CopyTradingStatisticsService(
|
||||
// 2. 获取关联信息
|
||||
val account = accountRepository.findById(copyTrading.accountId).orElse(null)
|
||||
val leader = leaderRepository.findById(copyTrading.leaderId).orElse(null)
|
||||
val template = templateRepository.findById(copyTrading.templateId).orElse(null)
|
||||
|
||||
// 3. 获取买入订单
|
||||
val buyOrders = copyOrderTrackingRepository.findByCopyTradingId(copyTradingId)
|
||||
@@ -74,8 +72,6 @@ class CopyTradingStatisticsService(
|
||||
accountName = account?.accountName,
|
||||
leaderId = copyTrading.leaderId,
|
||||
leaderName = leader?.leaderName,
|
||||
templateId = copyTrading.templateId,
|
||||
templateName = template?.templateName,
|
||||
enabled = copyTrading.enabled,
|
||||
totalBuyQuantity = statistics.totalBuyQuantity,
|
||||
totalBuyOrders = statistics.totalBuyOrders,
|
||||
|
||||
+18
-14
@@ -58,7 +58,10 @@ class CopyTradingTemplateService(
|
||||
useWebSocket = request.useWebSocket ?: true,
|
||||
websocketReconnectInterval = request.websocketReconnectInterval ?: 5000,
|
||||
websocketMaxRetries = request.websocketMaxRetries ?: 10,
|
||||
supportSell = request.supportSell ?: true
|
||||
supportSell = request.supportSell ?: true,
|
||||
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal(),
|
||||
maxSpread = request.maxSpread?.toSafeBigDecimal(),
|
||||
minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal()
|
||||
)
|
||||
|
||||
val saved = templateRepository.save(template)
|
||||
@@ -113,6 +116,9 @@ class CopyTradingTemplateService(
|
||||
websocketReconnectInterval = request.websocketReconnectInterval ?: template.websocketReconnectInterval,
|
||||
websocketMaxRetries = request.websocketMaxRetries ?: template.websocketMaxRetries,
|
||||
supportSell = request.supportSell ?: template.supportSell,
|
||||
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: template.minOrderDepth,
|
||||
maxSpread = request.maxSpread?.toSafeBigDecimal() ?: template.maxSpread,
|
||||
minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: template.minOrderbookDepth,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -134,12 +140,7 @@ class CopyTradingTemplateService(
|
||||
val template = templateRepository.findById(templateId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("模板不存在"))
|
||||
|
||||
// 检查是否有跟单正在使用该模板
|
||||
val useCount = copyTradingRepository.countByTemplateId(templateId)
|
||||
if (useCount > 0) {
|
||||
return Result.failure(IllegalStateException("该模板还有 $useCount 个跟单关系在使用,请先删除跟单关系"))
|
||||
}
|
||||
|
||||
// 模板不再绑定跟单配置,可以直接删除,无需检查使用情况
|
||||
templateRepository.delete(template)
|
||||
|
||||
Result.success(Unit)
|
||||
@@ -179,7 +180,10 @@ class CopyTradingTemplateService(
|
||||
useWebSocket = request.useWebSocket ?: sourceTemplate.useWebSocket,
|
||||
websocketReconnectInterval = request.websocketReconnectInterval ?: sourceTemplate.websocketReconnectInterval,
|
||||
websocketMaxRetries = request.websocketMaxRetries ?: sourceTemplate.websocketMaxRetries,
|
||||
supportSell = request.supportSell ?: sourceTemplate.supportSell
|
||||
supportSell = request.supportSell ?: sourceTemplate.supportSell,
|
||||
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: sourceTemplate.minOrderDepth,
|
||||
maxSpread = request.maxSpread?.toSafeBigDecimal() ?: sourceTemplate.maxSpread,
|
||||
minOrderbookDepth = request.minOrderbookDepth?.toSafeBigDecimal() ?: sourceTemplate.minOrderbookDepth
|
||||
)
|
||||
|
||||
val saved = templateRepository.save(newTemplate)
|
||||
@@ -198,8 +202,7 @@ class CopyTradingTemplateService(
|
||||
return try {
|
||||
val templates = templateRepository.findAllByOrderByCreatedAtDesc()
|
||||
val templateDtos = templates.map { template ->
|
||||
val useCount = copyTradingRepository.countByTemplateId(template.id!!)
|
||||
toDto(template, useCount)
|
||||
toDto(template)
|
||||
}
|
||||
|
||||
Result.success(
|
||||
@@ -222,8 +225,7 @@ class CopyTradingTemplateService(
|
||||
val template = templateRepository.findById(templateId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("模板不存在"))
|
||||
|
||||
val useCount = copyTradingRepository.countByTemplateId(templateId)
|
||||
Result.success(toDto(template, useCount))
|
||||
Result.success(toDto(template))
|
||||
} catch (e: Exception) {
|
||||
logger.error("查询模板详情失败", e)
|
||||
Result.failure(e)
|
||||
@@ -233,7 +235,7 @@ class CopyTradingTemplateService(
|
||||
/**
|
||||
* 转换为 DTO
|
||||
*/
|
||||
private fun toDto(template: CopyTradingTemplate, useCount: Long = 0): TemplateDto {
|
||||
private fun toDto(template: CopyTradingTemplate): TemplateDto {
|
||||
return TemplateDto(
|
||||
id = template.id!!,
|
||||
templateName = template.templateName,
|
||||
@@ -251,7 +253,9 @@ class CopyTradingTemplateService(
|
||||
websocketReconnectInterval = template.websocketReconnectInterval,
|
||||
websocketMaxRetries = template.websocketMaxRetries,
|
||||
supportSell = template.supportSell,
|
||||
useCount = useCount,
|
||||
minOrderDepth = template.minOrderDepth?.toPlainString(),
|
||||
maxSpread = template.maxSpread?.toPlainString(),
|
||||
minOrderbookDepth = template.minOrderbookDepth?.toPlainString(),
|
||||
createdAt = template.createdAt,
|
||||
updatedAt = template.updatedAt
|
||||
)
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.wrbug.polymarketbot.service
|
||||
|
||||
import com.wrbug.polymarketbot.dto.FilteredOrderDto
|
||||
import com.wrbug.polymarketbot.dto.FilteredOrderListRequest
|
||||
import com.wrbug.polymarketbot.dto.FilteredOrderListResponse
|
||||
import com.wrbug.polymarketbot.entity.FilteredOrder
|
||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.repository.FilteredOrderRepository
|
||||
import com.wrbug.polymarketbot.repository.LeaderRepository
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.stereotype.Service
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 被过滤订单服务
|
||||
*/
|
||||
@Service
|
||||
class FilteredOrderService(
|
||||
private val filteredOrderRepository: FilteredOrderRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val leaderRepository: LeaderRepository
|
||||
) {
|
||||
|
||||
/**
|
||||
* 查询被过滤订单列表
|
||||
*/
|
||||
fun getFilteredOrders(request: FilteredOrderListRequest): FilteredOrderListResponse {
|
||||
val page = (request.page ?: 1).coerceAtLeast(1)
|
||||
val limit = (request.limit ?: 20).coerceAtMost(100).coerceAtLeast(1)
|
||||
val pageable: Pageable = PageRequest.of(page - 1, limit, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
|
||||
val pageResult = if (request.startTime != null && request.endTime != null) {
|
||||
// 按时间范围查询
|
||||
filteredOrderRepository.findByCopyTradingIdAndTimeRange(
|
||||
copyTradingId = request.copyTradingId,
|
||||
startTime = request.startTime,
|
||||
endTime = request.endTime,
|
||||
pageable = pageable
|
||||
)
|
||||
} else if (request.filterType != null) {
|
||||
// 按过滤类型查询
|
||||
filteredOrderRepository.findByCopyTradingIdAndFilterTypeOrderByCreatedAtDesc(
|
||||
copyTradingId = request.copyTradingId,
|
||||
filterType = request.filterType,
|
||||
pageable = pageable
|
||||
)
|
||||
} else {
|
||||
// 查询所有
|
||||
filteredOrderRepository.findByCopyTradingIdOrderByCreatedAtDesc(
|
||||
copyTradingId = request.copyTradingId,
|
||||
pageable = pageable
|
||||
)
|
||||
}
|
||||
|
||||
val dtos = pageResult.content.map { entity ->
|
||||
convertToDto(entity)
|
||||
}
|
||||
|
||||
return FilteredOrderListResponse(
|
||||
list = dtos,
|
||||
total = pageResult.totalElements,
|
||||
page = page,
|
||||
limit = limit
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 DTO
|
||||
*/
|
||||
private fun convertToDto(entity: FilteredOrder): FilteredOrderDto {
|
||||
val account = accountRepository.findById(entity.accountId).orElse(null)
|
||||
val leader = leaderRepository.findById(entity.leaderId).orElse(null)
|
||||
|
||||
return FilteredOrderDto(
|
||||
id = entity.id!!,
|
||||
copyTradingId = entity.copyTradingId,
|
||||
accountId = entity.accountId,
|
||||
accountName = account?.accountName,
|
||||
leaderId = entity.leaderId,
|
||||
leaderName = leader?.leaderName,
|
||||
leaderTradeId = entity.leaderTradeId,
|
||||
marketId = entity.marketId,
|
||||
marketTitle = entity.marketTitle,
|
||||
marketSlug = entity.marketSlug,
|
||||
side = entity.side,
|
||||
outcomeIndex = entity.outcomeIndex,
|
||||
outcome = entity.outcome,
|
||||
price = entity.price.toString(),
|
||||
size = entity.size.toString(),
|
||||
calculatedQuantity = entity.calculatedQuantity?.toString(),
|
||||
filterReason = entity.filterReason,
|
||||
filterType = entity.filterType,
|
||||
createdAt = entity.createdAt
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计被过滤订单数量
|
||||
*/
|
||||
fun countFilteredOrders(copyTradingId: Long, filterType: String? = null): Long {
|
||||
return if (filterType != null) {
|
||||
filteredOrderRepository.countByCopyTradingIdAndFilterType(copyTradingId, filterType)
|
||||
} else {
|
||||
filteredOrderRepository.countByCopyTradingId(copyTradingId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+187
@@ -184,6 +184,193 @@ class TelegramNotificationService(
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送订单被过滤通知
|
||||
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
|
||||
*/
|
||||
suspend fun sendOrderFilteredNotification(
|
||||
marketTitle: String,
|
||||
marketId: String? = null, // 市场ID(conditionId),用于生成链接
|
||||
marketSlug: String? = null, // 市场slug,用于生成链接
|
||||
side: String,
|
||||
outcome: String? = null, // 市场方向(outcome,如 "YES", "NO" 等)
|
||||
price: String,
|
||||
size: String,
|
||||
filterReason: String, // 过滤原因
|
||||
filterType: String, // 过滤类型
|
||||
accountName: String? = null,
|
||||
walletAddress: String? = null,
|
||||
locale: java.util.Locale? = null
|
||||
) {
|
||||
// 获取语言设置(优先使用传入的 locale,否则从 LocaleContextHolder 获取)
|
||||
val currentLocale = locale ?: try {
|
||||
LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
// 计算订单金额 = price × size(USDC)
|
||||
val amount = try {
|
||||
val priceDecimal = price.toSafeBigDecimal()
|
||||
val sizeDecimal = size.toSafeBigDecimal()
|
||||
priceDecimal.multiply(sizeDecimal).toString()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("计算订单金额失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
|
||||
val message = buildOrderFilteredMessage(
|
||||
marketTitle = marketTitle,
|
||||
marketId = marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = side,
|
||||
outcome = outcome,
|
||||
price = price,
|
||||
size = size,
|
||||
amount = amount,
|
||||
filterReason = filterReason,
|
||||
filterType = filterType,
|
||||
accountName = accountName,
|
||||
walletAddress = walletAddress,
|
||||
locale = currentLocale
|
||||
)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建订单被过滤消息
|
||||
*/
|
||||
private fun buildOrderFilteredMessage(
|
||||
marketTitle: String,
|
||||
marketId: String?,
|
||||
marketSlug: String?,
|
||||
side: String,
|
||||
outcome: String?,
|
||||
price: String,
|
||||
size: String,
|
||||
amount: String?,
|
||||
filterReason: String,
|
||||
filterType: String,
|
||||
accountName: String?,
|
||||
walletAddress: String?,
|
||||
locale: java.util.Locale
|
||||
): String {
|
||||
|
||||
// 获取多语言文本
|
||||
val orderFiltered = messageSource.getMessage("notification.order.filtered", null, "订单被过滤", locale)
|
||||
val orderInfo = messageSource.getMessage("notification.order.info", null, "订单信息", locale)
|
||||
val marketLabel = messageSource.getMessage("notification.order.market", null, "市场", locale)
|
||||
val sideLabel = messageSource.getMessage("notification.order.side", null, "方向", locale)
|
||||
val outcomeLabel = messageSource.getMessage("notification.order.outcome", null, "市场方向", locale)
|
||||
val priceLabel = messageSource.getMessage("notification.order.price", null, "价格", locale)
|
||||
val quantityLabel = messageSource.getMessage("notification.order.quantity", null, "数量", locale)
|
||||
val amountLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale)
|
||||
val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale)
|
||||
val filterReasonLabel = messageSource.getMessage("notification.order.filter_reason", null, "过滤原因", locale)
|
||||
val filterTypeLabel = messageSource.getMessage("notification.order.filter_type", null, "过滤类型", locale)
|
||||
val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale)
|
||||
val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
|
||||
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", locale)
|
||||
|
||||
// 获取方向的多语言文本
|
||||
val sideDisplay = when (side.uppercase()) {
|
||||
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale)
|
||||
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale)
|
||||
else -> side
|
||||
}
|
||||
|
||||
// 获取过滤类型的多语言文本
|
||||
val filterTypeDisplay = when (filterType.uppercase()) {
|
||||
"ORDER_DEPTH" -> messageSource.getMessage("notification.filter.type.order_depth", null, "订单深度不足", locale)
|
||||
"SPREAD" -> messageSource.getMessage("notification.filter.type.spread", null, "价差过大", locale)
|
||||
"ORDERBOOK_DEPTH" -> messageSource.getMessage("notification.filter.type.orderbook_depth", null, "订单簿深度不足", locale)
|
||||
"PRICE_VALIDITY" -> messageSource.getMessage("notification.filter.type.price_validity", null, "价格不合理", locale)
|
||||
"MARKET_STATUS" -> messageSource.getMessage("notification.filter.type.market_status", null, "市场状态不可交易", locale)
|
||||
else -> filterType
|
||||
}
|
||||
|
||||
// 优先使用账户名称,如果没有账户名称才显示钱包地址
|
||||
val accountInfo: String = when {
|
||||
!accountName.isNullOrBlank() -> {
|
||||
accountName!!
|
||||
}
|
||||
!walletAddress.isNullOrBlank() -> {
|
||||
maskAddress(walletAddress!!)
|
||||
}
|
||||
else -> {
|
||||
unknownAccount
|
||||
}
|
||||
}
|
||||
|
||||
val time = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date())
|
||||
|
||||
// 转义 HTML 特殊字符
|
||||
val escapedMarketTitle = marketTitle.replace("<", "<").replace(">", ">")
|
||||
val escapedAccountInfo = accountInfo.replace("<", "<").replace(">", ">")
|
||||
val escapedFilterReason = filterReason.replace("<", "<").replace(">", ">")
|
||||
|
||||
// 格式化金额显示
|
||||
val amountDisplay = if (amount != null) {
|
||||
try {
|
||||
// 保留最多4位小数,去除尾随零
|
||||
val amountDecimal = amount.toSafeBigDecimal()
|
||||
val formatted = if (amountDecimal.scale() > 4) {
|
||||
amountDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros()
|
||||
} else {
|
||||
amountDecimal.stripTrailingZeros()
|
||||
}
|
||||
formatted.toPlainString()
|
||||
} catch (e: Exception) {
|
||||
amount
|
||||
}
|
||||
} else {
|
||||
calculateFailed
|
||||
}
|
||||
|
||||
// 生成市场链接
|
||||
val marketLink = when {
|
||||
!marketSlug.isNullOrBlank() -> {
|
||||
"https://polymarket.com/event/$marketSlug"
|
||||
}
|
||||
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> {
|
||||
"https://polymarket.com/condition/$marketId"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
val marketDisplay = if (marketLink != null) {
|
||||
"<a href=\"$marketLink\">$escapedMarketTitle</a>"
|
||||
} else {
|
||||
escapedMarketTitle
|
||||
}
|
||||
|
||||
// 显示市场方向(outcome)
|
||||
val outcomeDisplay = if (!outcome.isNullOrBlank()) {
|
||||
val escapedOutcome = outcome.replace("<", "<").replace(">", ">")
|
||||
"\n• $outcomeLabel: <b>$escapedOutcome</b>"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return """🚫 <b>$orderFiltered</b>
|
||||
|
||||
📊 <b>$orderInfo:</b>
|
||||
• $marketLabel: $marketDisplay$outcomeDisplay
|
||||
• $sideLabel: <b>$sideDisplay</b>
|
||||
• $priceLabel: <code>$price</code>
|
||||
• $quantityLabel: <code>$size</code> shares
|
||||
• $amountLabel: <code>$amountDisplay</code> USDC
|
||||
• $accountLabel: $escapedAccountInfo
|
||||
|
||||
⚠️ <b>$filterTypeLabel:</b> <code>$filterTypeDisplay</code>
|
||||
|
||||
📝 <b>$filterReasonLabel:</b>
|
||||
<code>$escapedFilterReason</code>
|
||||
|
||||
⏰ $timeLabel: <code>$time</code>"""
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试消息
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
-- ============================================
|
||||
-- V4: 重构跟单系统
|
||||
-- 1. 删除现有 copy_trading 记录
|
||||
-- 2. 移除 template_id 字段,添加所有配置参数字段和过滤条件字段
|
||||
-- 3. 在 copy_trading_templates 表中添加过滤条件字段
|
||||
-- 4. 修改 copy_order_tracking 表,移除 template_id 字段
|
||||
-- ============================================
|
||||
|
||||
-- 1. 删除现有 copy_trading 记录(根据需求直接删除)
|
||||
DELETE FROM copy_trading;
|
||||
|
||||
-- 2. 删除外键约束(先查询外键名称,如果存在则删除)
|
||||
SET @fk_name = (SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'copy_trading'
|
||||
AND REFERENCED_TABLE_NAME = 'copy_trading_templates'
|
||||
LIMIT 1);
|
||||
SET @sql = IF(@fk_name IS NOT NULL,
|
||||
CONCAT('ALTER TABLE copy_trading DROP FOREIGN KEY ', @fk_name),
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 3. 删除 template_id 相关的索引(先查询索引是否存在)
|
||||
SET @idx_name = (SELECT INDEX_NAME FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'copy_trading'
|
||||
AND INDEX_NAME = 'idx_template_id'
|
||||
LIMIT 1);
|
||||
SET @sql = IF(@idx_name IS NOT NULL,
|
||||
CONCAT('DROP INDEX ', @idx_name, ' ON copy_trading'),
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 删除唯一约束 uk_account_template_leader
|
||||
SET @uk_name = (SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'copy_trading'
|
||||
AND CONSTRAINT_TYPE = 'UNIQUE'
|
||||
AND CONSTRAINT_NAME = 'uk_account_template_leader'
|
||||
LIMIT 1);
|
||||
SET @sql = IF(@uk_name IS NOT NULL,
|
||||
CONCAT('ALTER TABLE copy_trading DROP INDEX ', @uk_name),
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 4. 删除 template_id 字段(如果存在)
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'copy_trading'
|
||||
AND COLUMN_NAME = 'template_id');
|
||||
SET @sql = IF(@col_exists > 0,
|
||||
'ALTER TABLE copy_trading DROP COLUMN template_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 5. 添加所有配置参数字段到 copy_trading 表
|
||||
ALTER TABLE copy_trading
|
||||
ADD COLUMN copy_mode VARCHAR(10) NOT NULL DEFAULT 'RATIO' COMMENT '跟单金额模式(RATIO/FIXED)' AFTER leader_id,
|
||||
ADD COLUMN copy_ratio DECIMAL(10, 2) NOT NULL DEFAULT 1.00 COMMENT '跟单比例(仅在copyMode=RATIO时生效)' AFTER copy_mode,
|
||||
ADD COLUMN fixed_amount DECIMAL(20, 8) NULL COMMENT '固定跟单金额(仅在copyMode=FIXED时生效)' AFTER copy_ratio,
|
||||
ADD COLUMN max_order_size DECIMAL(20, 8) NOT NULL DEFAULT 1000.00000000 COMMENT '单笔订单最大金额(USDC)' AFTER fixed_amount,
|
||||
ADD COLUMN min_order_size DECIMAL(20, 8) NOT NULL DEFAULT 1.00000000 COMMENT '单笔订单最小金额(USDC)' AFTER max_order_size,
|
||||
ADD COLUMN max_daily_loss DECIMAL(20, 8) NOT NULL DEFAULT 10000.00000000 COMMENT '每日最大亏损限制(USDC)' AFTER min_order_size,
|
||||
ADD COLUMN max_daily_orders INT NOT NULL DEFAULT 100 COMMENT '每日最大跟单订单数' AFTER max_daily_loss,
|
||||
ADD COLUMN price_tolerance DECIMAL(5, 2) NOT NULL DEFAULT 5.00 COMMENT '价格容忍度(百分比,0-100)' AFTER max_daily_orders,
|
||||
ADD COLUMN delay_seconds INT NOT NULL DEFAULT 0 COMMENT '跟单延迟(秒,默认0立即跟单)' AFTER price_tolerance,
|
||||
ADD COLUMN poll_interval_seconds INT NOT NULL DEFAULT 5 COMMENT '轮询间隔(秒,仅在WebSocket不可用时使用)' AFTER delay_seconds,
|
||||
ADD COLUMN use_websocket BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否优先使用WebSocket推送' AFTER poll_interval_seconds,
|
||||
ADD COLUMN websocket_reconnect_interval INT NOT NULL DEFAULT 5000 COMMENT 'WebSocket重连间隔(毫秒)' AFTER use_websocket,
|
||||
ADD COLUMN websocket_max_retries INT NOT NULL DEFAULT 10 COMMENT 'WebSocket最大重试次数' AFTER websocket_reconnect_interval,
|
||||
ADD COLUMN support_sell BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否支持跟单卖出' AFTER websocket_max_retries,
|
||||
-- 过滤条件字段
|
||||
ADD COLUMN min_order_depth DECIMAL(20, 8) NULL COMMENT '最小订单深度(USDC金额),NULL表示不启用此过滤' AFTER support_sell,
|
||||
ADD COLUMN max_spread DECIMAL(20, 8) NULL COMMENT '最大价差(绝对价格),NULL表示不启用此过滤' AFTER min_order_depth,
|
||||
ADD COLUMN min_orderbook_depth DECIMAL(20, 8) NULL COMMENT '最小订单簿深度(USDC金额),NULL表示不启用此过滤' AFTER max_spread;
|
||||
|
||||
-- 6. 添加新的唯一约束(account_id + leader_id,不再包含 template_id)
|
||||
ALTER TABLE copy_trading
|
||||
ADD UNIQUE KEY uk_account_leader (account_id, leader_id);
|
||||
|
||||
-- 7. 在 copy_trading_templates 表中添加过滤条件字段
|
||||
ALTER TABLE copy_trading_templates
|
||||
ADD COLUMN min_order_depth DECIMAL(20, 8) NULL COMMENT '最小订单深度(USDC金额),NULL表示不启用此过滤' AFTER support_sell,
|
||||
ADD COLUMN max_spread DECIMAL(20, 8) NULL COMMENT '最大价差(绝对价格),NULL表示不启用此过滤' AFTER min_order_depth,
|
||||
ADD COLUMN min_orderbook_depth DECIMAL(20, 8) NULL COMMENT '最小订单簿深度(USDC金额),NULL表示不启用此过滤' AFTER max_spread;
|
||||
|
||||
-- 8. 修改 copy_order_tracking 表,移除 template_id 字段(如果存在)
|
||||
SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'copy_order_tracking'
|
||||
AND COLUMN_NAME = 'template_id');
|
||||
SET @sql = IF(@col_exists > 0,
|
||||
'ALTER TABLE copy_order_tracking DROP COLUMN template_id',
|
||||
'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 9. 更新表注释
|
||||
ALTER TABLE copy_trading COMMENT='跟单配置表(独立配置,不再绑定模板)';
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ============================================
|
||||
-- V5: 添加被过滤订单表
|
||||
-- 用于记录因筛选条件不满足而被过滤的订单信息
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS filtered_order (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
copy_trading_id BIGINT NOT NULL COMMENT '跟单关系ID',
|
||||
account_id BIGINT NOT NULL COMMENT '账户ID',
|
||||
leader_id BIGINT NOT NULL COMMENT 'Leader ID',
|
||||
leader_trade_id VARCHAR(100) NOT NULL COMMENT 'Leader 的交易ID',
|
||||
market_id VARCHAR(100) NOT NULL COMMENT '市场地址',
|
||||
market_title VARCHAR(500) NULL COMMENT '市场标题(从 API 获取)',
|
||||
market_slug VARCHAR(200) NULL COMMENT '市场 slug(用于生成链接)',
|
||||
side VARCHAR(10) NOT NULL COMMENT '订单方向:BUY 或 SELL',
|
||||
outcome_index INT NULL COMMENT '结果索引(0, 1, 2, ...),支持多元市场',
|
||||
outcome VARCHAR(50) NULL COMMENT '市场方向(如 YES, NO 等)',
|
||||
price DECIMAL(20, 8) NOT NULL COMMENT 'Leader 交易价格',
|
||||
size DECIMAL(20, 8) NOT NULL COMMENT 'Leader 交易数量',
|
||||
calculated_quantity DECIMAL(20, 8) NULL COMMENT '计算出的跟单数量(如果已计算)',
|
||||
filter_reason TEXT NOT NULL COMMENT '过滤原因(详细说明)',
|
||||
filter_type VARCHAR(50) NOT NULL COMMENT '过滤类型(如 ORDER_DEPTH, SPREAD, ORDERBOOK_DEPTH 等)',
|
||||
created_at BIGINT NOT NULL COMMENT '创建时间(毫秒时间戳)',
|
||||
INDEX idx_copy_trading (copy_trading_id),
|
||||
INDEX idx_leader_trade (leader_id, leader_trade_id),
|
||||
INDEX idx_market (market_id),
|
||||
INDEX idx_created_at (created_at),
|
||||
INDEX idx_filter_type (filter_type),
|
||||
FOREIGN KEY (copy_trading_id) REFERENCES copy_trading(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (leader_id) REFERENCES copy_trading_leaders(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='被过滤订单表';
|
||||
|
||||
@@ -24,10 +24,12 @@ import TemplateAdd from './pages/TemplateAdd'
|
||||
import TemplateEdit from './pages/TemplateEdit'
|
||||
import CopyTradingList from './pages/CopyTradingList'
|
||||
import CopyTradingAdd from './pages/CopyTradingAdd'
|
||||
import CopyTradingEdit from './pages/CopyTradingEdit'
|
||||
import CopyTradingStatistics from './pages/CopyTradingStatistics'
|
||||
import CopyTradingBuyOrders from './pages/CopyTradingBuyOrders'
|
||||
import CopyTradingSellOrders from './pages/CopyTradingSellOrders'
|
||||
import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders'
|
||||
import FilteredOrdersList from './pages/FilteredOrdersList'
|
||||
import SystemSettings from './pages/SystemSettings'
|
||||
import LanguageSettings from './pages/LanguageSettings'
|
||||
import ApiHealthStatus from './pages/ApiHealthStatus'
|
||||
@@ -236,10 +238,12 @@ function App() {
|
||||
<Route path="/templates/edit/:id" element={<ProtectedRoute><TemplateEdit /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading" element={<ProtectedRoute><CopyTradingList /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/add" element={<ProtectedRoute><CopyTradingAdd /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/edit/:id" element={<ProtectedRoute><CopyTradingEdit /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/statistics/:copyTradingId" element={<ProtectedRoute><CopyTradingStatistics /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/orders/buy/:copyTradingId" element={<ProtectedRoute><CopyTradingBuyOrders /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/orders/sell/:copyTradingId" element={<ProtectedRoute><CopyTradingSellOrders /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/orders/matched/:copyTradingId" element={<ProtectedRoute><CopyTradingMatchedOrders /></ProtectedRoute>} />
|
||||
<Route path="/copy-trading/filtered-orders/:id" element={<ProtectedRoute><FilteredOrdersList /></ProtectedRoute>} />
|
||||
<Route path="/config" element={<ProtectedRoute><ConfigPage /></ProtectedRoute>} />
|
||||
<Route path="/positions" element={<ProtectedRoute><PositionList /></ProtectedRoute>} />
|
||||
<Route path="/statistics" element={<ProtectedRoute><Statistics /></ProtectedRoute>} />
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
"reset": "Reset",
|
||||
"close": "Close",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
"no": "No",
|
||||
"actions": "Actions",
|
||||
"all": "All",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At",
|
||||
"status": "Status",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"noData": "No Data",
|
||||
"saveConfig": "Save Config",
|
||||
"refreshConfig": "Refresh Config"
|
||||
},
|
||||
"account": {
|
||||
"title": "Account Management",
|
||||
@@ -54,6 +64,10 @@
|
||||
"accountIdRequired": "Account ID cannot be empty"
|
||||
},
|
||||
"message": {
|
||||
"success": "Operation successful",
|
||||
"error": "Operation failed",
|
||||
"loading": "Loading...",
|
||||
"noData": "No Data",
|
||||
"loginSuccess": "Login successful",
|
||||
"loginFailed": "Login failed",
|
||||
"createUserSuccess": "User created successfully",
|
||||
@@ -65,11 +79,13 @@
|
||||
"title": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"usernamePlaceholder": "Username",
|
||||
"passwordPlaceholder": "Password",
|
||||
"usernameRequired": "Please enter username",
|
||||
"passwordRequired": "Please enter password",
|
||||
"forgotPassword": "Forgot password? Reset password"
|
||||
"usernamePlaceholder": "Please enter username",
|
||||
"passwordPlaceholder": "Please enter password",
|
||||
"forgotPassword": "Forgot password?",
|
||||
"loginFailed": "Login failed",
|
||||
"loginSuccess": "Login successful"
|
||||
},
|
||||
"order": {
|
||||
"create": "Order Created",
|
||||
@@ -85,6 +101,8 @@
|
||||
},
|
||||
"accountList": {
|
||||
"title": "Account Management",
|
||||
"addAccount": "Add Account",
|
||||
"actions": "Actions",
|
||||
"importAccount": "Import Account",
|
||||
"accountName": "Account Name",
|
||||
"walletAddress": "Wallet Address",
|
||||
@@ -138,7 +156,8 @@
|
||||
"updateSuccess": "Account updated successfully",
|
||||
"updateFailed": "Failed to update account",
|
||||
"getDetailFailedForEdit": "Failed to get account detail",
|
||||
"loading": "Loading..."
|
||||
"loading": "Loading...",
|
||||
"fetchFailed": "Failed to get account list"
|
||||
},
|
||||
"accountImport": {
|
||||
"title": "Import Account",
|
||||
@@ -163,7 +182,13 @@
|
||||
"mnemonicInvalid": "Invalid mnemonic format (should be 12 or 24 words, space-separated)",
|
||||
"walletAddressMismatchMnemonic": "Wallet address does not match mnemonic",
|
||||
"accountName": "Account Name",
|
||||
"accountNameRequired": "Please enter account name",
|
||||
"accountNamePlaceholder": "Optional, for identifying account",
|
||||
"accountNameHelp": "Used to identify account for management",
|
||||
"privateKeyHelp": "Private key will be encrypted and stored, only used for signing transactions",
|
||||
"submit": "Import",
|
||||
"invalidPrivateKey": "Invalid private key",
|
||||
"duplicateAccount": "Account already exists",
|
||||
"importAccount": "Import Account",
|
||||
"importSuccess": "Account imported successfully",
|
||||
"importFailed": "Failed to import account",
|
||||
@@ -174,6 +199,7 @@
|
||||
"leader": {
|
||||
"title": "Leader Management",
|
||||
"leaderName": "Leader Name",
|
||||
"leaderAddress": "Wallet Address",
|
||||
"walletAddress": "Wallet Address",
|
||||
"category": "Category",
|
||||
"all": "All",
|
||||
@@ -181,8 +207,11 @@
|
||||
"createdAt": "Created At",
|
||||
"action": "Actions",
|
||||
"add": "Add",
|
||||
"addLeader": "Add Leader",
|
||||
"edit": "Edit",
|
||||
"editLeader": "Edit Leader",
|
||||
"delete": "Delete",
|
||||
"deleteLeader": "Delete Leader",
|
||||
"listFailed": "Failed to get Leader list",
|
||||
"deleteSuccess": "Leader deleted successfully",
|
||||
"deleteFailed": "Failed to delete Leader",
|
||||
@@ -220,10 +249,16 @@
|
||||
},
|
||||
"apiHealthStatus": {
|
||||
"title": "API Health Status",
|
||||
"checkFailed": "Check Failed",
|
||||
"status": "Status",
|
||||
"responseTime": "Response Time",
|
||||
"lastCheck": "Last Check",
|
||||
"healthy": "Healthy",
|
||||
"unhealthy": "Unhealthy",
|
||||
"unknown": "Unknown",
|
||||
"normal": "Normal",
|
||||
"notConfigured": "Not Configured",
|
||||
"abnormal": "Abnormal",
|
||||
"responseTime": "Response Time"
|
||||
"abnormal": "Abnormal"
|
||||
},
|
||||
"proxySettings": {
|
||||
"title": "Proxy Settings",
|
||||
@@ -403,6 +438,248 @@
|
||||
"invalidNumber": "Please enter a valid number",
|
||||
"fixedAmountError": "Fixed amount must be >= 1, please re-enter"
|
||||
},
|
||||
"templateAdd": {
|
||||
"title": "Create Copy Trading Template",
|
||||
"back": "Back",
|
||||
"templateName": "Template Name",
|
||||
"templateNamePlaceholder": "Please enter template name",
|
||||
"templateNameRequired": "Please enter template name",
|
||||
"templateNameTooltip": "Unique identifier name for the template, used to distinguish different copy trading configuration templates. Template name must be unique and cannot duplicate other templates.",
|
||||
"copyMode": "Copy Amount Mode",
|
||||
"copyModeTooltip": "Select the calculation method for copy amount. Ratio mode: copy amount changes proportionally with Leader order size; Fixed amount mode: copy amount remains fixed regardless of Leader order size.",
|
||||
"ratioMode": "Ratio Mode",
|
||||
"fixedAmountMode": "Fixed Amount Mode",
|
||||
"copyRatio": "Copy Ratio",
|
||||
"copyRatioTooltip": "Copy ratio represents the percentage of copy amount relative to Leader order amount. For example: 100% means 1:1 copy, 50% means half position copy, 200% means double copy",
|
||||
"copyRatioPlaceholder": "For example: 100 means 100% (1:1 copy), default 100%",
|
||||
"fixedAmount": "Fixed Copy Amount (USDC)",
|
||||
"fixedAmountPlaceholder": "Fixed amount, does not change with Leader order size, must be >= 1",
|
||||
"fixedAmountRequired": "Please enter fixed copy amount",
|
||||
"fixedAmountError": "Fixed amount must be >= 1, please re-enter",
|
||||
"maxOrderSize": "Max Order Size (USDC)",
|
||||
"maxOrderSizeTooltip": "In ratio mode, limits the maximum amount cap for single copy order, used to prevent excessive copy amount and control risk. For example: set to 1000, even if calculated copy amount exceeds 1000, it will be limited to 1000 USDC.",
|
||||
"maxOrderSizePlaceholder": "Only effective in ratio mode (optional)",
|
||||
"minOrderSize": "Min Order Size (USDC)",
|
||||
"minOrderSizeTooltip": "In ratio mode, limits the minimum amount floor for single copy order, used to filter out orders with too small amounts, avoiding frequent small trades. If filled, must be >= 1 USDC. For example: set to 10, if calculated copy amount is less than 10, skip this order.",
|
||||
"minOrderSizePlaceholder": "Only effective in ratio mode, must be >= 1 (optional)",
|
||||
"minOrderSizeError": "Minimum amount must be >= 1",
|
||||
"maxDailyOrders": "Max Daily Copy Orders",
|
||||
"maxDailyOrdersTooltip": "Limits the maximum number of copy orders per day, used for risk control and preventing overtrading. For example: set to 50, when daily copy orders reach 50, stop copying, reset next day.",
|
||||
"maxDailyOrdersPlaceholder": "Default 100 (optional)",
|
||||
"priceTolerance": "Price Tolerance (%)",
|
||||
"priceToleranceTooltip": "Allowed adjustment range for copy price based on Leader price, used to adjust price within Leader price ± tolerance range to improve fill rate. For example: set to 5%, Leader price is 0.5, then copy price can be in 0.475-0.525 range.",
|
||||
"priceTolerancePlaceholder": "Default 5% (optional)",
|
||||
"minOrderDepth": "Min Order Depth (USDC)",
|
||||
"minOrderDepthTooltip": "Minimum order depth (USDC amount), NULL means this filter is not enabled. Ensures market has sufficient liquidity",
|
||||
"minOrderDepthPlaceholder": "For example: 100 (optional, leave empty to disable)",
|
||||
"maxSpread": "Max Spread (Absolute Price)",
|
||||
"maxSpreadTooltip": "Maximum spread (absolute price), NULL means this filter is not enabled. Avoid copying in markets with excessive spreads",
|
||||
"maxSpreadPlaceholder": "For example: 0.05 (5 cents, optional, leave empty to disable)",
|
||||
"minOrderbookDepth": "Min Orderbook Depth (USDC)",
|
||||
"minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled. Check depth of first N levels",
|
||||
"minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)",
|
||||
"supportSell": "Support Sell",
|
||||
"supportSellTooltip": "Whether to copy Leader's sell orders. Enabled: copy both Leader's buy and sell orders; Disabled: only copy Leader's buy orders, ignore sell orders.",
|
||||
"create": "Create Template",
|
||||
"createSuccess": "Template created successfully",
|
||||
"createFailed": "Failed to create template",
|
||||
"invalidNumber": "Please enter a valid number"
|
||||
},
|
||||
"templateEdit": {
|
||||
"title": "Edit Copy Trading Template",
|
||||
"back": "Back",
|
||||
"save": "Save Changes",
|
||||
"saveSuccess": "Template updated successfully",
|
||||
"saveFailed": "Failed to update template",
|
||||
"fetchFailed": "Failed to get template details",
|
||||
"templateName": "Template Name",
|
||||
"templateNamePlaceholder": "Please enter template name",
|
||||
"templateNameRequired": "Please enter template name",
|
||||
"templateNameTooltip": "Unique identifier name for the template, used to distinguish different copy trading configuration templates. Template name must be unique and cannot duplicate other templates.",
|
||||
"copyMode": "Copy Amount Mode",
|
||||
"copyModeTooltip": "Select the calculation method for copy amount. Ratio mode: copy amount changes proportionally with Leader order size; Fixed amount mode: copy amount remains fixed regardless of Leader order size.",
|
||||
"ratioMode": "Ratio Mode",
|
||||
"fixedAmountMode": "Fixed Amount Mode",
|
||||
"copyRatio": "Copy Ratio",
|
||||
"copyRatioTooltip": "Copy ratio represents the percentage of copy amount relative to Leader order amount. For example: 100% means 1:1 copy, 50% means half position copy, 200% means double copy",
|
||||
"copyRatioPlaceholder": "For example: 100 means 100% (1:1 copy), default 100%",
|
||||
"fixedAmount": "Fixed Copy Amount (USDC)",
|
||||
"fixedAmountPlaceholder": "Fixed amount, does not change with Leader order size, must be >= 1",
|
||||
"fixedAmountRequired": "Please enter fixed copy amount",
|
||||
"fixedAmountError": "Fixed amount must be >= 1, please re-enter",
|
||||
"fixedAmountTooltip": "In fixed amount mode, the fixed amount for each copy, does not change with Leader order size. Must be >= 1 USDC. For example: set to 10, then regardless of how much Leader buys, copy amount is always 10 USDC.",
|
||||
"maxOrderSize": "Max Order Size (USDC)",
|
||||
"maxOrderSizeTooltip": "In ratio mode, limits the maximum amount cap for single copy order, used to prevent excessive copy amount and control risk. For example: set to 1000, even if calculated copy amount exceeds 1000, it will be limited to 1000 USDC.",
|
||||
"maxOrderSizePlaceholder": "Only effective in ratio mode (optional)",
|
||||
"minOrderSize": "Min Order Size (USDC)",
|
||||
"minOrderSizeTooltip": "In ratio mode, limits the minimum amount floor for single copy order, used to filter out orders with too small amounts, avoiding frequent small trades. If filled, must be >= 1 USDC. For example: set to 10, if calculated copy amount is less than 10, skip this order.",
|
||||
"minOrderSizePlaceholder": "Only effective in ratio mode, must be >= 1 (optional)",
|
||||
"minOrderSizeError": "Minimum amount must be >= 1",
|
||||
"maxDailyOrders": "Max Daily Copy Orders",
|
||||
"maxDailyOrdersTooltip": "Limits the maximum number of copy orders per day, used for risk control and preventing overtrading. For example: set to 50, when daily copy orders reach 50, stop copying, reset next day.",
|
||||
"maxDailyOrdersPlaceholder": "Default 100 (optional)",
|
||||
"priceTolerance": "Price Tolerance (%)",
|
||||
"priceToleranceTooltip": "Allowed adjustment range for copy price based on Leader price, used to adjust price within Leader price ± tolerance range to improve fill rate. For example: set to 5%, Leader price is 0.5, then copy price can be in 0.475-0.525 range.",
|
||||
"priceTolerancePlaceholder": "Default 5% (optional)",
|
||||
"minOrderDepth": "Min Order Depth (USDC)",
|
||||
"minOrderDepthTooltip": "Minimum order depth (USDC amount), NULL means this filter is not enabled. Ensures market has sufficient liquidity",
|
||||
"minOrderDepthPlaceholder": "For example: 100 (optional, leave empty to disable)",
|
||||
"maxSpread": "Max Spread (Absolute Price)",
|
||||
"maxSpreadTooltip": "Maximum spread (absolute price), NULL means this filter is not enabled. Avoid copying in markets with excessive spreads",
|
||||
"maxSpreadPlaceholder": "For example: 0.05 (5 cents, optional, leave empty to disable)",
|
||||
"minOrderbookDepth": "Min Orderbook Depth (USDC)",
|
||||
"minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled. Check depth of first N levels",
|
||||
"minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)",
|
||||
"supportSell": "Support Sell",
|
||||
"supportSellTooltip": "Whether to copy Leader's sell orders. Enabled: copy both Leader's buy and sell orders; Disabled: only copy Leader's buy orders, ignore sell orders.",
|
||||
"invalidNumber": "Please enter a valid number"
|
||||
},
|
||||
"copyTradingAdd": {
|
||||
"title": "Add Copy Trading Config",
|
||||
"back": "Back",
|
||||
"selectWallet": "Select Wallet",
|
||||
"selectWalletPlaceholder": "Please select wallet",
|
||||
"walletRequired": "Please select wallet",
|
||||
"selectLeader": "Select Leader",
|
||||
"selectLeaderPlaceholder": "Please select Leader",
|
||||
"leaderRequired": "Please select Leader",
|
||||
"enabled": "Enabled Status",
|
||||
"selectTemplate": "Select Template",
|
||||
"selectTemplateFromModal": "Fill Config from Template",
|
||||
"templateFilled": "Template content filled, you can modify",
|
||||
"basicConfig": "Basic Config",
|
||||
"copyMode": "Copy Amount Mode",
|
||||
"copyModeTooltip": "Select the calculation method for copy amount. Ratio mode: copy amount changes proportionally with Leader order size; Fixed amount mode: copy amount remains fixed regardless of Leader order size.",
|
||||
"ratioMode": "Ratio Mode",
|
||||
"fixedAmountMode": "Fixed Amount Mode",
|
||||
"ratio": "Ratio",
|
||||
"fixed": "Fixed",
|
||||
"copyRatio": "Copy Ratio",
|
||||
"copyRatioTooltip": "Copy ratio represents the percentage of copy amount relative to Leader order amount. For example: 100% means 1:1 copy, 50% means half position copy, 200% means double copy",
|
||||
"copyRatioPlaceholder": "For example: 100 means 100% (1:1 copy), default 100%",
|
||||
"fixedAmount": "Fixed Copy Amount (USDC)",
|
||||
"fixedAmountPlaceholder": "Fixed amount, does not change with Leader order size, must be >= 1",
|
||||
"fixedAmountRequired": "Please enter fixed copy amount",
|
||||
"fixedAmountMin": "Fixed amount must be >= 1",
|
||||
"maxOrderSize": "Max Order Size (USDC)",
|
||||
"maxOrderSizeTooltip": "In ratio mode, limits the maximum amount cap for single copy order",
|
||||
"maxOrderSizePlaceholder": "Only effective in ratio mode (optional)",
|
||||
"minOrderSize": "Min Order Size (USDC)",
|
||||
"minOrderSizeTooltip": "In ratio mode, limits the minimum amount floor for single copy order, must be >= 1",
|
||||
"minOrderSizePlaceholder": "Only effective in ratio mode, must be >= 1 (optional)",
|
||||
"minOrderSizeMin": "Minimum amount must be >= 1",
|
||||
"maxDailyLoss": "Max Daily Loss Limit (USDC)",
|
||||
"maxDailyLossTooltip": "Limits the maximum daily loss amount, used for risk control",
|
||||
"maxDailyLossPlaceholder": "Default 10000 USDC (optional)",
|
||||
"maxDailyOrders": "Max Daily Copy Orders",
|
||||
"maxDailyOrdersTooltip": "Limits the maximum number of copy orders per day",
|
||||
"maxDailyOrdersPlaceholder": "Default 100 (optional)",
|
||||
"priceTolerance": "Price Tolerance (%)",
|
||||
"priceToleranceTooltip": "Allowed adjustment range for copy price based on Leader price",
|
||||
"priceTolerancePlaceholder": "Default 5% (optional)",
|
||||
"delaySeconds": "Copy Delay (seconds)",
|
||||
"delaySecondsTooltip": "Copy delay time, 0 means copy immediately",
|
||||
"delaySecondsPlaceholder": "Default 0 (copy immediately)",
|
||||
"filterConditions": "Filter Conditions (Optional)",
|
||||
"minOrderDepth": "Min Order Depth (USDC)",
|
||||
"minOrderDepthTooltip": "Minimum order depth (USDC amount), NULL means this filter is not enabled. Ensures market has sufficient liquidity",
|
||||
"minOrderDepthPlaceholder": "For example: 100 (optional, leave empty to disable)",
|
||||
"maxSpread": "Max Spread (Absolute Price)",
|
||||
"maxSpreadTooltip": "Maximum spread (absolute price), NULL means this filter is not enabled. Avoid copying in markets with excessive spreads",
|
||||
"maxSpreadPlaceholder": "For example: 0.05 (5 cents, optional, leave empty to disable)",
|
||||
"minOrderbookDepth": "Min Orderbook Depth (USDC)",
|
||||
"minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled. Check depth of first N levels",
|
||||
"minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)",
|
||||
"supportSell": "Support Sell",
|
||||
"supportSellTooltip": "Whether to copy Leader's sell orders",
|
||||
"create": "Create Copy Trading Config",
|
||||
"createSuccess": "Copy trading config created successfully",
|
||||
"createFailed": "Failed to create copy trading config",
|
||||
"invalidNumber": "Please enter a valid number",
|
||||
"fetchLeaderFailed": "Failed to get Leader list",
|
||||
"fetchTemplateFailed": "Failed to get template list",
|
||||
"templateName": "Template Name"
|
||||
},
|
||||
"copyTradingEdit": {
|
||||
"title": "Edit Copy Trading Config",
|
||||
"back": "Back",
|
||||
"wallet": "Wallet",
|
||||
"leader": "Leader",
|
||||
"selectWallet": "Wallet",
|
||||
"selectLeader": "Leader",
|
||||
"basicConfig": "Basic Config",
|
||||
"copyMode": "Copy Amount Mode",
|
||||
"copyModeTooltip": "Select the calculation method for copy amount",
|
||||
"ratioMode": "Ratio Mode",
|
||||
"fixedAmountMode": "Fixed Amount Mode",
|
||||
"copyRatio": "Copy Ratio",
|
||||
"copyRatioTooltip": "Copy ratio represents the percentage of copy amount relative to Leader order amount",
|
||||
"copyRatioPlaceholder": "For example: 100 means 100% (1:1 copy)",
|
||||
"fixedAmount": "Fixed Copy Amount (USDC)",
|
||||
"fixedAmountPlaceholder": "Fixed amount, does not change with Leader order size, must be >= 1",
|
||||
"fixedAmountRequired": "Please enter fixed copy amount",
|
||||
"fixedAmountMin": "Fixed amount must be >= 1",
|
||||
"maxOrderSize": "Max Order Size (USDC)",
|
||||
"maxOrderSizeTooltip": "In ratio mode, limits the maximum amount cap for single copy order",
|
||||
"maxOrderSizePlaceholder": "Only effective in ratio mode (optional)",
|
||||
"minOrderSize": "Min Order Size (USDC)",
|
||||
"minOrderSizeTooltip": "In ratio mode, limits the minimum amount floor for single copy order, must be >= 1",
|
||||
"minOrderSizePlaceholder": "Only effective in ratio mode, must be >= 1 (optional)",
|
||||
"minOrderSizeMin": "Minimum amount must be >= 1",
|
||||
"maxDailyLoss": "Max Daily Loss Limit (USDC)",
|
||||
"maxDailyLossTooltip": "Limits the maximum daily loss amount, used for risk control",
|
||||
"maxDailyLossPlaceholder": "Default 10000 USDC (optional)",
|
||||
"maxDailyOrders": "Max Daily Copy Orders",
|
||||
"maxDailyOrdersTooltip": "Limits the maximum number of copy orders per day",
|
||||
"maxDailyOrdersPlaceholder": "Default 100 (optional)",
|
||||
"priceTolerance": "Price Tolerance (%)",
|
||||
"priceToleranceTooltip": "Allowed adjustment range for copy price based on Leader price",
|
||||
"priceTolerancePlaceholder": "Default 5% (optional)",
|
||||
"delaySeconds": "Copy Delay (seconds)",
|
||||
"delaySecondsTooltip": "Copy delay time, 0 means copy immediately",
|
||||
"delaySecondsPlaceholder": "Default 0 (copy immediately)",
|
||||
"filterConditions": "Filter Conditions (Optional)",
|
||||
"minOrderDepth": "Min Order Depth (USDC)",
|
||||
"minOrderDepthTooltip": "Minimum order depth (USDC amount), NULL means this filter is not enabled",
|
||||
"minOrderDepthPlaceholder": "For example: 100 (optional, leave empty to disable)",
|
||||
"maxSpread": "Max Spread (Absolute Price)",
|
||||
"maxSpreadTooltip": "Maximum spread (absolute price), NULL means this filter is not enabled",
|
||||
"maxSpreadPlaceholder": "For example: 0.05 (5 cents, optional, leave empty to disable)",
|
||||
"minOrderbookDepth": "Min Orderbook Depth (USDC)",
|
||||
"minOrderbookDepthTooltip": "Minimum orderbook depth (USDC amount), NULL means this filter is not enabled",
|
||||
"minOrderbookDepthPlaceholder": "For example: 50 (optional, leave empty to disable)",
|
||||
"supportSell": "Support Sell",
|
||||
"supportSellTooltip": "Whether to copy Leader's sell orders",
|
||||
"save": "Save",
|
||||
"saveSuccess": "Copy trading config updated successfully",
|
||||
"saveFailed": "Failed to update copy trading config",
|
||||
"fetchFailed": "Failed to get copy trading config",
|
||||
"invalidNumber": "Please enter a valid number"
|
||||
},
|
||||
"filteredOrdersList": {
|
||||
"title": "Filtered Orders List",
|
||||
"fetchFailed": "Failed to fetch filtered orders list",
|
||||
"market": "Market",
|
||||
"outcome": "Outcome",
|
||||
"price": "Price",
|
||||
"size": "Size",
|
||||
"side": "Order Side",
|
||||
"calculatedQuantity": "Calculated Quantity",
|
||||
"filterType": "Filter Type",
|
||||
"filterReason": "Filter Reason",
|
||||
"createdAt": "Time",
|
||||
"filterByType": "Filter by Type",
|
||||
"filterTypes": {
|
||||
"orderDepth": "Insufficient Order Depth",
|
||||
"spread": "Spread Too Large",
|
||||
"orderbookDepth": "Insufficient Orderbook Depth",
|
||||
"priceValidity": "Invalid Price",
|
||||
"marketStatus": "Market Not Tradable",
|
||||
"orderbookError": "Orderbook Fetch Failed",
|
||||
"orderbookEmpty": "Orderbook Empty",
|
||||
"unknown": "Unknown Reason"
|
||||
}
|
||||
},
|
||||
"copyTradingList": {
|
||||
"title": "Copy Trading Config Management",
|
||||
"addCopyTrading": "Add Copy Trading",
|
||||
@@ -410,6 +687,9 @@
|
||||
"account": "Account",
|
||||
"template": "Template",
|
||||
"leader": "Leader",
|
||||
"copyMode": "Copy Mode",
|
||||
"ratioMode": "Ratio",
|
||||
"fixedAmountMode": "Fixed",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"totalPnl": "Total P&L",
|
||||
@@ -419,6 +699,7 @@
|
||||
"buyOrders": "Buy Orders",
|
||||
"sellOrders": "Sell Orders",
|
||||
"matchedOrders": "Matched Orders",
|
||||
"filteredOrders": "Filtered Orders",
|
||||
"filterWallet": "Filter Wallet",
|
||||
"filterTemplate": "Filter Template",
|
||||
"filterLeader": "Filter Leader",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"common": {
|
||||
"back": "返回",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"confirm": "确定",
|
||||
@@ -349,6 +350,248 @@
|
||||
"invalidNumber": "请输入有效的数字",
|
||||
"fixedAmountError": "固定金额必须 >= 1,请重新输入"
|
||||
},
|
||||
"templateAdd": {
|
||||
"title": "创建跟单模板",
|
||||
"back": "返回",
|
||||
"templateName": "模板名称",
|
||||
"templateNamePlaceholder": "请输入模板名称",
|
||||
"templateNameRequired": "请输入模板名称",
|
||||
"templateNameTooltip": "模板的唯一标识名称,用于区分不同的跟单配置模板。模板名称必须唯一,不能与其他模板重名。",
|
||||
"copyMode": "跟单金额模式",
|
||||
"copyModeTooltip": "选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金额模式",
|
||||
"copyRatio": "跟单比例",
|
||||
"copyRatioTooltip": "跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟单),默认 100%",
|
||||
"fixedAmount": "固定跟单金额 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金额,不随 Leader 订单大小变化,必须 >= 1",
|
||||
"fixedAmountRequired": "请输入固定跟单金额",
|
||||
"fixedAmountError": "固定金额必须 >= 1,请重新输入",
|
||||
"maxOrderSize": "单笔订单最大金额 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。",
|
||||
"maxOrderSizePlaceholder": "仅在比例模式下生效(可选)",
|
||||
"minOrderSize": "单笔订单最小金额 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。",
|
||||
"minOrderSizePlaceholder": "仅在比例模式下生效,必须 >= 1(可选)",
|
||||
"minOrderSizeError": "最小金额必须 >= 1",
|
||||
"maxDailyOrders": "每日最大跟单订单数",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟单的订单数量,用于风险控制,防止过度交易。例如:设置为 50,当日跟单订单数达到 50 后,停止跟单,次日重置。",
|
||||
"maxDailyOrdersPlaceholder": "默认 100(可选)",
|
||||
"priceTolerance": "价格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允许跟单价格在 Leader 价格基础上的调整范围,用于在 Leader 价格 ± 容忍度范围内调整价格,提高成交率。例如:设置为 5%,Leader 价格为 0.5,则跟单价格可在 0.475-0.525 范围内。",
|
||||
"priceTolerancePlaceholder": "默认 5%(可选)",
|
||||
"minOrderDepth": "最小订单深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性",
|
||||
"minOrderDepthPlaceholder": "例如:100(可选,不填写表示不启用)",
|
||||
"maxSpread": "最大价差(绝对价格)",
|
||||
"maxSpreadTooltip": "最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可选,不填写表示不启用)",
|
||||
"minOrderbookDepth": "最小订单簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)",
|
||||
"supportSell": "跟单卖出",
|
||||
"supportSellTooltip": "是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。",
|
||||
"create": "创建模板",
|
||||
"createSuccess": "创建模板成功",
|
||||
"createFailed": "创建模板失败",
|
||||
"invalidNumber": "请输入有效的数字"
|
||||
},
|
||||
"templateEdit": {
|
||||
"title": "编辑跟单模板",
|
||||
"back": "返回",
|
||||
"save": "保存修改",
|
||||
"saveSuccess": "更新模板成功",
|
||||
"saveFailed": "更新模板失败",
|
||||
"fetchFailed": "获取模板详情失败",
|
||||
"templateName": "模板名称",
|
||||
"templateNamePlaceholder": "请输入模板名称",
|
||||
"templateNameRequired": "请输入模板名称",
|
||||
"templateNameTooltip": "模板的唯一标识名称,用于区分不同的跟单配置模板。模板名称必须唯一,不能与其他模板重名。",
|
||||
"copyMode": "跟单金额模式",
|
||||
"copyModeTooltip": "选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金额模式",
|
||||
"copyRatio": "跟单比例",
|
||||
"copyRatioTooltip": "跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟单),默认 100%",
|
||||
"fixedAmount": "固定跟单金额 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金额,不随 Leader 订单大小变化,必须 >= 1",
|
||||
"fixedAmountRequired": "请输入固定跟单金额",
|
||||
"fixedAmountError": "固定金额必须 >= 1,请重新输入",
|
||||
"fixedAmountTooltip": "固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= 1 USDC。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 10 USDC。",
|
||||
"maxOrderSize": "单笔订单最大金额 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。",
|
||||
"maxOrderSizePlaceholder": "仅在比例模式下生效(可选)",
|
||||
"minOrderSize": "单笔订单最小金额 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。",
|
||||
"minOrderSizePlaceholder": "仅在比例模式下生效,必须 >= 1(可选)",
|
||||
"minOrderSizeError": "最小金额必须 >= 1",
|
||||
"maxDailyOrders": "每日最大跟单订单数",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟单的订单数量,用于风险控制,防止过度交易。例如:设置为 50,当日跟单订单数达到 50 后,停止跟单,次日重置。",
|
||||
"maxDailyOrdersPlaceholder": "默认 100(可选)",
|
||||
"priceTolerance": "价格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允许跟单价格在 Leader 价格基础上的调整范围,用于在 Leader 价格 ± 容忍度范围内调整价格,提高成交率。例如:设置为 5%,Leader 价格为 0.5,则跟单价格可在 0.475-0.525 范围内。",
|
||||
"priceTolerancePlaceholder": "默认 5%(可选)",
|
||||
"minOrderDepth": "最小订单深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性",
|
||||
"minOrderDepthPlaceholder": "例如:100(可选,不填写表示不启用)",
|
||||
"maxSpread": "最大价差(绝对价格)",
|
||||
"maxSpreadTooltip": "最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可选,不填写表示不启用)",
|
||||
"minOrderbookDepth": "最小订单簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)",
|
||||
"supportSell": "跟单卖出",
|
||||
"supportSellTooltip": "是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。",
|
||||
"invalidNumber": "请输入有效的数字"
|
||||
},
|
||||
"copyTradingAdd": {
|
||||
"title": "新增跟单配置",
|
||||
"back": "返回",
|
||||
"selectWallet": "选择钱包",
|
||||
"selectWalletPlaceholder": "请选择钱包",
|
||||
"walletRequired": "请选择钱包",
|
||||
"selectLeader": "选择 Leader",
|
||||
"selectLeaderPlaceholder": "请选择 Leader",
|
||||
"leaderRequired": "请选择 Leader",
|
||||
"enabled": "启用状态",
|
||||
"selectTemplate": "选择模板",
|
||||
"selectTemplateFromModal": "从模板填充配置",
|
||||
"templateFilled": "模板内容已填充,您可以修改",
|
||||
"basicConfig": "基础配置",
|
||||
"copyMode": "跟单金额模式",
|
||||
"copyModeTooltip": "选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金额模式",
|
||||
"ratio": "比例",
|
||||
"fixed": "固定",
|
||||
"copyRatio": "跟单比例",
|
||||
"copyRatioTooltip": "跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟单),默认 100%",
|
||||
"fixedAmount": "固定跟单金额 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金额,不随 Leader 订单大小变化,必须 >= 1",
|
||||
"fixedAmountRequired": "请输入固定跟单金额",
|
||||
"fixedAmountMin": "固定金额必须 >= 1",
|
||||
"maxOrderSize": "单笔订单最大金额 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最大金额上限",
|
||||
"maxOrderSizePlaceholder": "仅在比例模式下生效(可选)",
|
||||
"minOrderSize": "单笔订单最小金额 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1",
|
||||
"minOrderSizePlaceholder": "仅在比例模式下生效,必须 >= 1(可选)",
|
||||
"minOrderSizeMin": "最小金额必须 >= 1",
|
||||
"maxDailyLoss": "每日最大亏损限制 (USDC)",
|
||||
"maxDailyLossTooltip": "限制每日最大亏损金额,用于风险控制",
|
||||
"maxDailyLossPlaceholder": "默认 10000 USDC(可选)",
|
||||
"maxDailyOrders": "每日最大跟单订单数",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟单的订单数量",
|
||||
"maxDailyOrdersPlaceholder": "默认 100(可选)",
|
||||
"priceTolerance": "价格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允许跟单价格在 Leader 价格基础上的调整范围",
|
||||
"priceTolerancePlaceholder": "默认 5%(可选)",
|
||||
"delaySeconds": "跟单延迟 (秒)",
|
||||
"delaySecondsTooltip": "跟单延迟时间,0 表示立即跟单",
|
||||
"delaySecondsPlaceholder": "默认 0(立即跟单)",
|
||||
"filterConditions": "过滤条件(可选)",
|
||||
"minOrderDepth": "最小订单深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性",
|
||||
"minOrderDepthPlaceholder": "例如:100(可选,不填写表示不启用)",
|
||||
"maxSpread": "最大价差(绝对价格)",
|
||||
"maxSpreadTooltip": "最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可选,不填写表示不启用)",
|
||||
"minOrderbookDepth": "最小订单簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)",
|
||||
"supportSell": "跟单卖出",
|
||||
"supportSellTooltip": "是否跟单 Leader 的卖出订单",
|
||||
"create": "创建跟单配置",
|
||||
"createSuccess": "创建跟单配置成功",
|
||||
"createFailed": "创建跟单配置失败",
|
||||
"invalidNumber": "请输入有效的数字",
|
||||
"fetchLeaderFailed": "获取 Leader 列表失败",
|
||||
"fetchTemplateFailed": "获取模板列表失败",
|
||||
"templateName": "模板名称"
|
||||
},
|
||||
"copyTradingEdit": {
|
||||
"title": "编辑跟单配置",
|
||||
"back": "返回",
|
||||
"wallet": "钱包",
|
||||
"leader": "Leader",
|
||||
"selectWallet": "钱包",
|
||||
"selectLeader": "Leader",
|
||||
"basicConfig": "基础配置",
|
||||
"copyMode": "跟单金额模式",
|
||||
"copyModeTooltip": "选择跟单金额的计算方式",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金额模式",
|
||||
"copyRatio": "跟单比例",
|
||||
"copyRatioTooltip": "跟单比例表示跟单金额相对于 Leader 订单金额的百分比",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟单)",
|
||||
"fixedAmount": "固定跟单金额 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金额,不随 Leader 订单大小变化,必须 >= 1",
|
||||
"fixedAmountRequired": "请输入固定跟单金额",
|
||||
"fixedAmountMin": "固定金额必须 >= 1",
|
||||
"maxOrderSize": "单笔订单最大金额 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最大金额上限",
|
||||
"maxOrderSizePlaceholder": "仅在比例模式下生效(可选)",
|
||||
"minOrderSize": "单笔订单最小金额 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1",
|
||||
"minOrderSizePlaceholder": "仅在比例模式下生效,必须 >= 1(可选)",
|
||||
"minOrderSizeMin": "最小金额必须 >= 1",
|
||||
"maxDailyLoss": "每日最大亏损限制 (USDC)",
|
||||
"maxDailyLossTooltip": "限制每日最大亏损金额,用于风险控制",
|
||||
"maxDailyLossPlaceholder": "默认 10000 USDC(可选)",
|
||||
"maxDailyOrders": "每日最大跟单订单数",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟单的订单数量",
|
||||
"maxDailyOrdersPlaceholder": "默认 100(可选)",
|
||||
"priceTolerance": "价格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允许跟单价格在 Leader 价格基础上的调整范围",
|
||||
"priceTolerancePlaceholder": "默认 5%(可选)",
|
||||
"delaySeconds": "跟单延迟 (秒)",
|
||||
"delaySecondsTooltip": "跟单延迟时间,0 表示立即跟单",
|
||||
"delaySecondsPlaceholder": "默认 0(立即跟单)",
|
||||
"filterConditions": "过滤条件(可选)",
|
||||
"minOrderDepth": "最小订单深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小订单深度(USDC金额),NULL表示不启用此过滤",
|
||||
"minOrderDepthPlaceholder": "例如:100(可选,不填写表示不启用)",
|
||||
"maxSpread": "最大价差(绝对价格)",
|
||||
"maxSpreadTooltip": "最大价差(绝对价格),NULL表示不启用此过滤",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可选,不填写表示不启用)",
|
||||
"minOrderbookDepth": "最小订单簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小订单簿深度(USDC金额),NULL表示不启用此过滤",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可选,不填写表示不启用)",
|
||||
"supportSell": "跟单卖出",
|
||||
"supportSellTooltip": "是否跟单 Leader 的卖出订单",
|
||||
"save": "保存",
|
||||
"saveSuccess": "更新跟单配置成功",
|
||||
"saveFailed": "更新跟单配置失败",
|
||||
"fetchFailed": "获取跟单配置失败",
|
||||
"invalidNumber": "请输入有效的数字"
|
||||
},
|
||||
"filteredOrdersList": {
|
||||
"title": "被过滤订单列表",
|
||||
"fetchFailed": "获取被过滤订单列表失败",
|
||||
"market": "市场",
|
||||
"outcome": "方向",
|
||||
"price": "价格",
|
||||
"size": "数量",
|
||||
"side": "订单方向",
|
||||
"calculatedQuantity": "计算数量",
|
||||
"filterType": "过滤类型",
|
||||
"filterReason": "过滤原因",
|
||||
"createdAt": "时间",
|
||||
"filterByType": "按类型筛选",
|
||||
"filterTypes": {
|
||||
"orderDepth": "订单深度不足",
|
||||
"spread": "价差过大",
|
||||
"orderbookDepth": "订单簿深度不足",
|
||||
"priceValidity": "价格不合理",
|
||||
"marketStatus": "市场状态不可交易",
|
||||
"orderbookError": "订单簿获取失败",
|
||||
"orderbookEmpty": "订单簿为空",
|
||||
"unknown": "未知原因"
|
||||
}
|
||||
},
|
||||
"copyTradingList": {
|
||||
"title": "跟单配置管理",
|
||||
"addCopyTrading": "新增跟单",
|
||||
@@ -356,6 +599,9 @@
|
||||
"account": "账户",
|
||||
"template": "模板",
|
||||
"leader": "Leader",
|
||||
"copyMode": "跟单模式",
|
||||
"ratioMode": "比例",
|
||||
"fixedAmountMode": "固定",
|
||||
"enabled": "开启",
|
||||
"disabled": "停止",
|
||||
"totalPnl": "总盈亏",
|
||||
@@ -365,6 +611,7 @@
|
||||
"buyOrders": "买入订单",
|
||||
"sellOrders": "卖出订单",
|
||||
"matchedOrders": "匹配关系",
|
||||
"filteredOrders": "被过滤订单",
|
||||
"filterWallet": "筛选钱包",
|
||||
"filterTemplate": "筛选模板",
|
||||
"filterLeader": "筛选 Leader",
|
||||
|
||||
@@ -16,7 +16,17 @@
|
||||
"reset": "重置",
|
||||
"close": "關閉",
|
||||
"yes": "是",
|
||||
"no": "否"
|
||||
"no": "否",
|
||||
"actions": "操作",
|
||||
"all": "全部",
|
||||
"createdAt": "創建時間",
|
||||
"updatedAt": "更新時間",
|
||||
"status": "狀態",
|
||||
"enabled": "啟用",
|
||||
"disabled": "禁用",
|
||||
"noData": "暫無數據",
|
||||
"saveConfig": "保存配置",
|
||||
"refreshConfig": "刷新配置"
|
||||
},
|
||||
"account": {
|
||||
"title": "賬戶管理",
|
||||
@@ -54,6 +64,10 @@
|
||||
"accountIdRequired": "賬戶ID不能為空"
|
||||
},
|
||||
"message": {
|
||||
"success": "操作成功",
|
||||
"error": "操作失敗",
|
||||
"loading": "加載中...",
|
||||
"noData": "暫無數據",
|
||||
"loginSuccess": "登錄成功",
|
||||
"loginFailed": "登錄失敗",
|
||||
"createUserSuccess": "創建用戶成功",
|
||||
@@ -65,11 +79,13 @@
|
||||
"title": "登錄",
|
||||
"username": "用戶名",
|
||||
"password": "密碼",
|
||||
"usernamePlaceholder": "用戶名",
|
||||
"passwordPlaceholder": "密碼",
|
||||
"usernameRequired": "請輸入用戶名",
|
||||
"passwordRequired": "請輸入密碼",
|
||||
"forgotPassword": "忘記密碼?重置密碼"
|
||||
"usernamePlaceholder": "請輸入用戶名",
|
||||
"passwordPlaceholder": "請輸入密碼",
|
||||
"forgotPassword": "忘記密碼?",
|
||||
"loginFailed": "登錄失敗",
|
||||
"loginSuccess": "登錄成功"
|
||||
},
|
||||
"order": {
|
||||
"create": "訂單創建",
|
||||
@@ -85,6 +101,8 @@
|
||||
},
|
||||
"accountList": {
|
||||
"title": "賬戶管理",
|
||||
"addAccount": "添加賬戶",
|
||||
"actions": "操作",
|
||||
"importAccount": "導入賬戶",
|
||||
"accountName": "賬戶名稱",
|
||||
"walletAddress": "錢包地址",
|
||||
@@ -138,7 +156,8 @@
|
||||
"updateSuccess": "更新賬戶成功",
|
||||
"updateFailed": "更新賬戶失敗",
|
||||
"getDetailFailedForEdit": "獲取賬戶詳情失敗",
|
||||
"loading": "加載中..."
|
||||
"loading": "加載中...",
|
||||
"fetchFailed": "獲取賬戶列表失敗"
|
||||
},
|
||||
"accountImport": {
|
||||
"title": "導入賬戶",
|
||||
@@ -163,7 +182,13 @@
|
||||
"mnemonicInvalid": "助記詞格式不正確(應為12或24個單詞,用空格分隔)",
|
||||
"walletAddressMismatchMnemonic": "錢包地址與助記詞不匹配",
|
||||
"accountName": "賬戶名稱",
|
||||
"accountNameRequired": "請輸入賬戶名稱",
|
||||
"accountNamePlaceholder": "可選,用於標識賬戶",
|
||||
"accountNameHelp": "用於標識賬戶,便於管理",
|
||||
"privateKeyHelp": "私鑰將加密存儲,僅用於簽名交易",
|
||||
"submit": "導入",
|
||||
"invalidPrivateKey": "無效的私鑰",
|
||||
"duplicateAccount": "賬戶已存在",
|
||||
"importAccount": "導入賬戶",
|
||||
"importSuccess": "導入賬戶成功",
|
||||
"importFailed": "導入賬戶失敗",
|
||||
@@ -174,6 +199,7 @@
|
||||
"leader": {
|
||||
"title": "Leader 管理",
|
||||
"leaderName": "Leader 名稱",
|
||||
"leaderAddress": "錢包地址",
|
||||
"walletAddress": "錢包地址",
|
||||
"category": "分類",
|
||||
"all": "全部",
|
||||
@@ -181,8 +207,11 @@
|
||||
"createdAt": "創建時間",
|
||||
"action": "操作",
|
||||
"add": "添加",
|
||||
"addLeader": "添加 Leader",
|
||||
"edit": "編輯",
|
||||
"editLeader": "編輯 Leader",
|
||||
"delete": "刪除",
|
||||
"deleteLeader": "刪除 Leader",
|
||||
"listFailed": "獲取 Leader 列表失敗",
|
||||
"deleteSuccess": "刪除 Leader 成功",
|
||||
"deleteFailed": "刪除 Leader 失敗",
|
||||
@@ -220,10 +249,16 @@
|
||||
},
|
||||
"apiHealthStatus": {
|
||||
"title": "API 健康狀態",
|
||||
"checkFailed": "檢查失敗",
|
||||
"status": "狀態",
|
||||
"responseTime": "響應時間",
|
||||
"lastCheck": "最後檢查",
|
||||
"healthy": "健康",
|
||||
"unhealthy": "不健康",
|
||||
"unknown": "未知",
|
||||
"normal": "正常",
|
||||
"notConfigured": "未配置",
|
||||
"abnormal": "異常",
|
||||
"responseTime": "響應時間"
|
||||
"abnormal": "異常"
|
||||
},
|
||||
"proxySettings": {
|
||||
"title": "代理設置",
|
||||
@@ -403,6 +438,248 @@
|
||||
"invalidNumber": "請輸入有效的數字",
|
||||
"fixedAmountError": "固定金額必須 >= 1,請重新輸入"
|
||||
},
|
||||
"templateAdd": {
|
||||
"title": "創建跟單模板",
|
||||
"back": "返回",
|
||||
"templateName": "模板名稱",
|
||||
"templateNamePlaceholder": "請輸入模板名稱",
|
||||
"templateNameRequired": "請輸入模板名稱",
|
||||
"templateNameTooltip": "模板的唯一標識名稱,用於區分不同的跟單配置模板。模板名稱必須唯一,不能與其他模板重名。",
|
||||
"copyMode": "跟單金額模式",
|
||||
"copyModeTooltip": "選擇跟單金額的計算方式。比例模式:跟單金額隨 Leader 訂單大小按比例變化;固定金額模式:無論 Leader 訂單大小如何,跟單金額都固定不變。",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金額模式",
|
||||
"copyRatio": "跟單比例",
|
||||
"copyRatioTooltip": "跟單比例表示跟單金額相對於 Leader 訂單金額的百分比。例如:100% 表示 1:1 跟單,50% 表示半倉跟單,200% 表示雙倍跟單",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟單),默認 100%",
|
||||
"fixedAmount": "固定跟單金額 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金額,不隨 Leader 訂單大小變化,必須 >= 1",
|
||||
"fixedAmountRequired": "請輸入固定跟單金額",
|
||||
"fixedAmountError": "固定金額必須 >= 1,請重新輸入",
|
||||
"maxOrderSize": "單筆訂單最大金額 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最大金額上限,用於防止跟單金額過大,控制風險。例如:設置為 1000,即使計算出的跟單金額超過 1000,也會限制為 1000 USDC。",
|
||||
"maxOrderSizePlaceholder": "僅在比例模式下生效(可選)",
|
||||
"minOrderSize": "單筆訂單最小金額 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最小金額下限,用於過濾掉金額過小的訂單,避免頻繁小額交易。如果填寫,必須 >= 1 USDC。例如:設置為 10,如果計算出的跟單金額小於 10,則跳過該訂單。",
|
||||
"minOrderSizePlaceholder": "僅在比例模式下生效,必須 >= 1(可選)",
|
||||
"minOrderSizeError": "最小金額必須 >= 1",
|
||||
"maxDailyOrders": "每日最大跟單訂單數",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟單的訂單數量,用於風險控制,防止過度交易。例如:設置為 50,當日跟單訂單數達到 50 後,停止跟單,次日重置。",
|
||||
"maxDailyOrdersPlaceholder": "默認 100(可選)",
|
||||
"priceTolerance": "價格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允許跟單價格在 Leader 價格基礎上的調整範圍,用於在 Leader 價格 ± 容忍度範圍內調整價格,提高成交率。例如:設置為 5%,Leader 價格為 0.5,則跟單價格可在 0.475-0.525 範圍內。",
|
||||
"priceTolerancePlaceholder": "默認 5%(可選)",
|
||||
"minOrderDepth": "最小訂單深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小訂單深度(USDC金額),NULL表示不啟用此過濾。確保市場有足夠的流動性",
|
||||
"minOrderDepthPlaceholder": "例如:100(可選,不填寫表示不啟用)",
|
||||
"maxSpread": "最大價差(絕對價格)",
|
||||
"maxSpreadTooltip": "最大價差(絕對價格),NULL表示不啟用此過濾。避免在價差過大的市場跟單",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可選,不填寫表示不啟用)",
|
||||
"minOrderbookDepth": "最小訂單簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾。檢查前 N 檔的深度",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)",
|
||||
"supportSell": "跟單賣出",
|
||||
"supportSellTooltip": "是否跟單 Leader 的賣出訂單。開啟:跟單 Leader 的買入和賣出訂單;關閉:只跟單 Leader 的買入訂單,忽略賣出訂單。",
|
||||
"create": "創建模板",
|
||||
"createSuccess": "創建模板成功",
|
||||
"createFailed": "創建模板失敗",
|
||||
"invalidNumber": "請輸入有效的數字"
|
||||
},
|
||||
"templateEdit": {
|
||||
"title": "編輯跟單模板",
|
||||
"back": "返回",
|
||||
"save": "保存修改",
|
||||
"saveSuccess": "更新模板成功",
|
||||
"saveFailed": "更新模板失敗",
|
||||
"fetchFailed": "獲取模板詳情失敗",
|
||||
"templateName": "模板名稱",
|
||||
"templateNamePlaceholder": "請輸入模板名稱",
|
||||
"templateNameRequired": "請輸入模板名稱",
|
||||
"templateNameTooltip": "模板的唯一標識名稱,用於區分不同的跟單配置模板。模板名稱必須唯一,不能與其他模板重名。",
|
||||
"copyMode": "跟單金額模式",
|
||||
"copyModeTooltip": "選擇跟單金額的計算方式。比例模式:跟單金額隨 Leader 訂單大小按比例變化;固定金額模式:無論 Leader 訂單大小如何,跟單金額都固定不變。",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金額模式",
|
||||
"copyRatio": "跟單比例",
|
||||
"copyRatioTooltip": "跟單比例表示跟單金額相對於 Leader 訂單金額的百分比。例如:100% 表示 1:1 跟單,50% 表示半倉跟單,200% 表示雙倍跟單",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟單),默認 100%",
|
||||
"fixedAmount": "固定跟單金額 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金額,不隨 Leader 訂單大小變化,必須 >= 1",
|
||||
"fixedAmountRequired": "請輸入固定跟單金額",
|
||||
"fixedAmountError": "固定金額必須 >= 1,請重新輸入",
|
||||
"fixedAmountTooltip": "固定金額模式下,每次跟單的固定金額,不隨 Leader 訂單大小變化。必須 >= 1 USDC。例如:設置為 10,則無論 Leader 買入多少,跟單金額始終為 10 USDC。",
|
||||
"maxOrderSize": "單筆訂單最大金額 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最大金額上限,用於防止跟單金額過大,控制風險。例如:設置為 1000,即使計算出的跟單金額超過 1000,也會限制為 1000 USDC。",
|
||||
"maxOrderSizePlaceholder": "僅在比例模式下生效(可選)",
|
||||
"minOrderSize": "單筆訂單最小金額 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最小金額下限,用於過濾掉金額過小的訂單,避免頻繁小額交易。如果填寫,必須 >= 1 USDC。例如:設置為 10,如果計算出的跟單金額小於 10,則跳過該訂單。",
|
||||
"minOrderSizePlaceholder": "僅在比例模式下生效,必須 >= 1(可選)",
|
||||
"minOrderSizeError": "最小金額必須 >= 1",
|
||||
"maxDailyOrders": "每日最大跟單訂單數",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟單的訂單數量,用於風險控制,防止過度交易。例如:設置為 50,當日跟單訂單數達到 50 後,停止跟單,次日重置。",
|
||||
"maxDailyOrdersPlaceholder": "默認 100(可選)",
|
||||
"priceTolerance": "價格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允許跟單價格在 Leader 價格基礎上的調整範圍,用於在 Leader 價格 ± 容忍度範圍內調整價格,提高成交率。例如:設置為 5%,Leader 價格為 0.5,則跟單價格可在 0.475-0.525 範圍內。",
|
||||
"priceTolerancePlaceholder": "默認 5%(可選)",
|
||||
"minOrderDepth": "最小訂單深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小訂單深度(USDC金額),NULL表示不啟用此過濾。確保市場有足夠的流動性",
|
||||
"minOrderDepthPlaceholder": "例如:100(可選,不填寫表示不啟用)",
|
||||
"maxSpread": "最大價差(絕對價格)",
|
||||
"maxSpreadTooltip": "最大價差(絕對價格),NULL表示不啟用此過濾。避免在價差過大的市場跟單",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可選,不填寫表示不啟用)",
|
||||
"minOrderbookDepth": "最小訂單簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾。檢查前 N 檔的深度",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)",
|
||||
"supportSell": "跟單賣出",
|
||||
"supportSellTooltip": "是否跟單 Leader 的賣出訂單。開啟:跟單 Leader 的買入和賣出訂單;關閉:只跟單 Leader 的買入訂單,忽略賣出訂單。",
|
||||
"invalidNumber": "請輸入有效的數字"
|
||||
},
|
||||
"copyTradingAdd": {
|
||||
"title": "新增跟單配置",
|
||||
"back": "返回",
|
||||
"selectWallet": "選擇錢包",
|
||||
"selectWalletPlaceholder": "請選擇錢包",
|
||||
"walletRequired": "請選擇錢包",
|
||||
"selectLeader": "選擇 Leader",
|
||||
"selectLeaderPlaceholder": "請選擇 Leader",
|
||||
"leaderRequired": "請選擇 Leader",
|
||||
"enabled": "啟用狀態",
|
||||
"selectTemplate": "選擇模板",
|
||||
"selectTemplateFromModal": "從模板填充配置",
|
||||
"templateFilled": "模板內容已填充,您可以修改",
|
||||
"basicConfig": "基礎配置",
|
||||
"copyMode": "跟單金額模式",
|
||||
"copyModeTooltip": "選擇跟單金額的計算方式。比例模式:跟單金額隨 Leader 訂單大小按比例變化;固定金額模式:無論 Leader 訂單大小如何,跟單金額都固定不變。",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金額模式",
|
||||
"ratio": "比例",
|
||||
"fixed": "固定",
|
||||
"copyRatio": "跟單比例",
|
||||
"copyRatioTooltip": "跟單比例表示跟單金額相對於 Leader 訂單金額的百分比。例如:100% 表示 1:1 跟單,50% 表示半倉跟單,200% 表示雙倍跟單",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟單),默認 100%",
|
||||
"fixedAmount": "固定跟單金額 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金額,不隨 Leader 訂單大小變化,必須 >= 1",
|
||||
"fixedAmountRequired": "請輸入固定跟單金額",
|
||||
"fixedAmountMin": "固定金額必須 >= 1",
|
||||
"maxOrderSize": "單筆訂單最大金額 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最大金額上限",
|
||||
"maxOrderSizePlaceholder": "僅在比例模式下生效(可選)",
|
||||
"minOrderSize": "單筆訂單最小金額 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最小金額下限,必須 >= 1",
|
||||
"minOrderSizePlaceholder": "僅在比例模式下生效,必須 >= 1(可選)",
|
||||
"minOrderSizeMin": "最小金額必須 >= 1",
|
||||
"maxDailyLoss": "每日最大虧損限制 (USDC)",
|
||||
"maxDailyLossTooltip": "限制每日最大虧損金額,用於風險控制",
|
||||
"maxDailyLossPlaceholder": "默認 10000 USDC(可選)",
|
||||
"maxDailyOrders": "每日最大跟單訂單數",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟單的訂單數量",
|
||||
"maxDailyOrdersPlaceholder": "默認 100(可選)",
|
||||
"priceTolerance": "價格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允許跟單價格在 Leader 價格基礎上的調整範圍",
|
||||
"priceTolerancePlaceholder": "默認 5%(可選)",
|
||||
"delaySeconds": "跟單延遲 (秒)",
|
||||
"delaySecondsTooltip": "跟單延遲時間,0 表示立即跟單",
|
||||
"delaySecondsPlaceholder": "默認 0(立即跟單)",
|
||||
"filterConditions": "過濾條件(可選)",
|
||||
"minOrderDepth": "最小訂單深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小訂單深度(USDC金額),NULL表示不啟用此過濾。確保市場有足夠的流動性",
|
||||
"minOrderDepthPlaceholder": "例如:100(可選,不填寫表示不啟用)",
|
||||
"maxSpread": "最大價差(絕對價格)",
|
||||
"maxSpreadTooltip": "最大價差(絕對價格),NULL表示不啟用此過濾。避免在價差過大的市場跟單",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可選,不填寫表示不啟用)",
|
||||
"minOrderbookDepth": "最小訂單簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾。檢查前 N 檔的深度",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)",
|
||||
"supportSell": "跟單賣出",
|
||||
"supportSellTooltip": "是否跟單 Leader 的賣出訂單",
|
||||
"create": "創建跟單配置",
|
||||
"createSuccess": "創建跟單配置成功",
|
||||
"createFailed": "創建跟單配置失敗",
|
||||
"invalidNumber": "請輸入有效的數字",
|
||||
"fetchLeaderFailed": "獲取 Leader 列表失敗",
|
||||
"fetchTemplateFailed": "獲取模板列表失敗",
|
||||
"templateName": "模板名稱"
|
||||
},
|
||||
"copyTradingEdit": {
|
||||
"title": "編輯跟單配置",
|
||||
"back": "返回",
|
||||
"wallet": "錢包",
|
||||
"leader": "Leader",
|
||||
"selectWallet": "錢包",
|
||||
"selectLeader": "Leader",
|
||||
"basicConfig": "基礎配置",
|
||||
"copyMode": "跟單金額模式",
|
||||
"copyModeTooltip": "選擇跟單金額的計算方式",
|
||||
"ratioMode": "比例模式",
|
||||
"fixedAmountMode": "固定金額模式",
|
||||
"copyRatio": "跟單比例",
|
||||
"copyRatioTooltip": "跟單比例表示跟單金額相對於 Leader 訂單金額的百分比",
|
||||
"copyRatioPlaceholder": "例如:100 表示 100%(1:1 跟單)",
|
||||
"fixedAmount": "固定跟單金額 (USDC)",
|
||||
"fixedAmountPlaceholder": "固定金額,不隨 Leader 訂單大小變化,必須 >= 1",
|
||||
"fixedAmountRequired": "請輸入固定跟單金額",
|
||||
"fixedAmountMin": "固定金額必須 >= 1",
|
||||
"maxOrderSize": "單筆訂單最大金額 (USDC)",
|
||||
"maxOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最大金額上限",
|
||||
"maxOrderSizePlaceholder": "僅在比例模式下生效(可選)",
|
||||
"minOrderSize": "單筆訂單最小金額 (USDC)",
|
||||
"minOrderSizeTooltip": "比例模式下,限制單筆跟單訂單的最小金額下限,必須 >= 1",
|
||||
"minOrderSizePlaceholder": "僅在比例模式下生效,必須 >= 1(可選)",
|
||||
"minOrderSizeMin": "最小金額必須 >= 1",
|
||||
"maxDailyLoss": "每日最大虧損限制 (USDC)",
|
||||
"maxDailyLossTooltip": "限制每日最大虧損金額,用於風險控制",
|
||||
"maxDailyLossPlaceholder": "默認 10000 USDC(可選)",
|
||||
"maxDailyOrders": "每日最大跟單訂單數",
|
||||
"maxDailyOrdersTooltip": "限制每日最多跟單的訂單數量",
|
||||
"maxDailyOrdersPlaceholder": "默認 100(可選)",
|
||||
"priceTolerance": "價格容忍度 (%)",
|
||||
"priceToleranceTooltip": "允許跟單價格在 Leader 價格基礎上的調整範圍",
|
||||
"priceTolerancePlaceholder": "默認 5%(可選)",
|
||||
"delaySeconds": "跟單延遲 (秒)",
|
||||
"delaySecondsTooltip": "跟單延遲時間,0 表示立即跟單",
|
||||
"delaySecondsPlaceholder": "默認 0(立即跟單)",
|
||||
"filterConditions": "過濾條件(可選)",
|
||||
"minOrderDepth": "最小訂單深度 (USDC)",
|
||||
"minOrderDepthTooltip": "最小訂單深度(USDC金額),NULL表示不啟用此過濾",
|
||||
"minOrderDepthPlaceholder": "例如:100(可選,不填寫表示不啟用)",
|
||||
"maxSpread": "最大價差(絕對價格)",
|
||||
"maxSpreadTooltip": "最大價差(絕對價格),NULL表示不啟用此過濾",
|
||||
"maxSpreadPlaceholder": "例如:0.05(5美分,可選,不填寫表示不啟用)",
|
||||
"minOrderbookDepth": "最小訂單簿深度 (USDC)",
|
||||
"minOrderbookDepthTooltip": "最小訂單簿深度(USDC金額),NULL表示不啟用此過濾",
|
||||
"minOrderbookDepthPlaceholder": "例如:50(可選,不填寫表示不啟用)",
|
||||
"supportSell": "跟單賣出",
|
||||
"supportSellTooltip": "是否跟單 Leader 的賣出訂單",
|
||||
"save": "保存",
|
||||
"saveSuccess": "更新跟單配置成功",
|
||||
"saveFailed": "更新跟單配置失敗",
|
||||
"fetchFailed": "獲取跟單配置失敗",
|
||||
"invalidNumber": "請輸入有效的數字"
|
||||
},
|
||||
"filteredOrdersList": {
|
||||
"title": "被過濾訂單列表",
|
||||
"fetchFailed": "獲取被過濾訂單列表失敗",
|
||||
"market": "市場",
|
||||
"outcome": "方向",
|
||||
"price": "價格",
|
||||
"size": "數量",
|
||||
"side": "訂單方向",
|
||||
"calculatedQuantity": "計算數量",
|
||||
"filterType": "過濾類型",
|
||||
"filterReason": "過濾原因",
|
||||
"createdAt": "時間",
|
||||
"filterByType": "按類型篩選",
|
||||
"filterTypes": {
|
||||
"orderDepth": "訂單深度不足",
|
||||
"spread": "價差過大",
|
||||
"orderbookDepth": "訂單簿深度不足",
|
||||
"priceValidity": "價格不合理",
|
||||
"marketStatus": "市場狀態不可交易",
|
||||
"orderbookError": "訂單簿獲取失敗",
|
||||
"orderbookEmpty": "訂單簿為空",
|
||||
"unknown": "未知原因"
|
||||
}
|
||||
},
|
||||
"copyTradingList": {
|
||||
"title": "跟單配置管理",
|
||||
"addCopyTrading": "新增跟單",
|
||||
@@ -410,6 +687,9 @@
|
||||
"account": "賬戶",
|
||||
"template": "模板",
|
||||
"leader": "Leader",
|
||||
"copyMode": "跟單模式",
|
||||
"ratioMode": "比例",
|
||||
"fixedAmountMode": "固定",
|
||||
"enabled": "開啟",
|
||||
"disabled": "停止",
|
||||
"totalPnl": "總盈虧",
|
||||
@@ -419,6 +699,7 @@
|
||||
"buyOrders": "買入訂單",
|
||||
"sellOrders": "賣出訂單",
|
||||
"matchedOrders": "匹配關係",
|
||||
"filteredOrders": "被過濾訂單",
|
||||
"filterWallet": "篩選錢包",
|
||||
"filterTemplate": "篩選模板",
|
||||
"filterLeader": "篩選 Leader",
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Button, Select, Switch, message, Typography, Space } from 'antd'
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Modal, Table, Select, Divider } from 'antd'
|
||||
import { ArrowLeftOutlined, SaveOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { Leader, CopyTradingTemplate } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import type { Leader, CopyTradingTemplate, CopyTradingCreateRequest } from '../types'
|
||||
import { formatUSDC } from '../utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title } = Typography
|
||||
const { Option } = Select
|
||||
|
||||
const CopyTradingAdd: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
useMediaQuery({ maxWidth: 768 }) // 用于响应式布局,但当前页面未使用
|
||||
const { accounts, fetchAccounts } = useAccountStore()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [leaders, setLeaders] = useState<Leader[]>([])
|
||||
const [templates, setTemplates] = useState<CopyTradingTemplate[]>([])
|
||||
const [templateModalVisible, setTemplateModalVisible] = useState(false)
|
||||
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
@@ -33,7 +35,7 @@ const CopyTradingAdd: React.FC = () => {
|
||||
setLeaders(response.data.data.list || [])
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取 Leader 列表失败')
|
||||
message.error(error.message || t('copyTradingAdd.fetchLeaderFailed') || '获取 Leader 列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,28 +46,83 @@ const CopyTradingAdd: React.FC = () => {
|
||||
setTemplates(response.data.data.list || [])
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取模板列表失败')
|
||||
message.error(error.message || t('copyTradingAdd.fetchTemplateFailed') || '获取模板列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectTemplate = (template: CopyTradingTemplate) => {
|
||||
// 填充模板数据到表单(只填充模板中存在的字段)
|
||||
form.setFieldsValue({
|
||||
copyMode: template.copyMode,
|
||||
copyRatio: template.copyRatio ? parseFloat(template.copyRatio) * 100 : 100, // 转换为百分比显示
|
||||
fixedAmount: template.fixedAmount ? parseFloat(template.fixedAmount) : undefined,
|
||||
maxOrderSize: template.maxOrderSize ? parseFloat(template.maxOrderSize) : undefined,
|
||||
minOrderSize: template.minOrderSize ? parseFloat(template.minOrderSize) : undefined,
|
||||
maxDailyOrders: template.maxDailyOrders,
|
||||
priceTolerance: template.priceTolerance ? parseFloat(template.priceTolerance) : undefined,
|
||||
supportSell: template.supportSell,
|
||||
minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined,
|
||||
maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined,
|
||||
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined
|
||||
})
|
||||
setCopyMode(template.copyMode)
|
||||
setTemplateModalVisible(false)
|
||||
message.success(t('copyTradingAdd.templateFilled') || '模板内容已填充,您可以修改')
|
||||
}
|
||||
|
||||
const handleCopyModeChange = (mode: 'RATIO' | 'FIXED') => {
|
||||
setCopyMode(mode)
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
// 前端校验
|
||||
if (values.copyMode === 'FIXED') {
|
||||
if (!values.fixedAmount || Number(values.fixedAmount) < 1) {
|
||||
message.error(t('copyTradingAdd.fixedAmountMin') || '固定金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && Number(values.minOrderSize) < 1) {
|
||||
message.error(t('copyTradingAdd.minOrderSizeMin') || '最小金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.copyTrading.create({
|
||||
const request: CopyTradingCreateRequest = {
|
||||
accountId: values.accountId,
|
||||
templateId: values.templateId,
|
||||
leaderId: values.leaderId,
|
||||
enabled: values.enabled !== false
|
||||
})
|
||||
enabled: true, // 默认启用
|
||||
copyMode: values.copyMode || 'RATIO',
|
||||
copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined,
|
||||
fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount?.toString() : undefined,
|
||||
maxOrderSize: values.maxOrderSize?.toString(),
|
||||
minOrderSize: values.minOrderSize?.toString(),
|
||||
maxDailyLoss: values.maxDailyLoss?.toString(),
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
delaySeconds: values.delaySeconds,
|
||||
pollIntervalSeconds: values.pollIntervalSeconds,
|
||||
useWebSocket: values.useWebSocket,
|
||||
websocketReconnectInterval: values.websocketReconnectInterval,
|
||||
websocketMaxRetries: values.websocketMaxRetries,
|
||||
supportSell: values.supportSell !== false,
|
||||
minOrderDepth: values.minOrderDepth?.toString(),
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minOrderbookDepth: values.minOrderbookDepth?.toString()
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.create(request)
|
||||
|
||||
if (response.data.code === 0) {
|
||||
message.success('创建跟单成功')
|
||||
message.success(t('copyTradingAdd.createSuccess') || '创建跟单配置成功')
|
||||
navigate('/copy-trading')
|
||||
} else {
|
||||
message.error(response.data.msg || '创建跟单失败')
|
||||
message.error(response.data.msg || t('copyTradingAdd.createFailed') || '创建跟单配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '创建跟单失败')
|
||||
message.error(error.message || t('copyTradingAdd.createFailed') || '创建跟单配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -78,27 +135,40 @@ const CopyTradingAdd: React.FC = () => {
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/copy-trading')}
|
||||
>
|
||||
返回
|
||||
{t('common.back') || '返回'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Title level={4}>新增跟单</Title>
|
||||
<Title level={4}>{t('copyTradingAdd.title') || '新增跟单配置'}</Title>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{
|
||||
enabled: true
|
||||
copyMode: 'RATIO',
|
||||
copyRatio: 100,
|
||||
maxOrderSize: 1000,
|
||||
minOrderSize: 1,
|
||||
maxDailyLoss: 10000,
|
||||
maxDailyOrders: 100,
|
||||
priceTolerance: 5,
|
||||
delaySeconds: 0,
|
||||
pollIntervalSeconds: 5,
|
||||
useWebSocket: true,
|
||||
websocketReconnectInterval: 5000,
|
||||
websocketMaxRetries: 10,
|
||||
supportSell: true
|
||||
}}
|
||||
>
|
||||
{/* 基础信息 */}
|
||||
<Form.Item
|
||||
label="选择钱包"
|
||||
label={t('copyTradingAdd.selectWallet') || '选择钱包'}
|
||||
name="accountId"
|
||||
rules={[{ required: true, message: '请选择钱包' }]}
|
||||
rules={[{ required: true, message: t('copyTradingAdd.walletRequired') || '请选择钱包' }]}
|
||||
>
|
||||
<Select placeholder="请选择钱包">
|
||||
<Select placeholder={t('copyTradingAdd.selectWalletPlaceholder') || '请选择钱包'}>
|
||||
{accounts.map(account => (
|
||||
<Option key={account.id} value={account.id}>
|
||||
{account.accountName || `账户 ${account.id}`} ({account.walletAddress.slice(0, 6)}...{account.walletAddress.slice(-4)})
|
||||
@@ -108,25 +178,11 @@ const CopyTradingAdd: React.FC = () => {
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择模板"
|
||||
name="templateId"
|
||||
rules={[{ required: true, message: '请选择模板' }]}
|
||||
>
|
||||
<Select placeholder="请选择模板">
|
||||
{templates.map(template => (
|
||||
<Option key={template.id} value={template.id}>
|
||||
{template.templateName} ({template.copyMode === 'RATIO' ? `比例 ${template.copyRatio}x` : `固定 ${template.fixedAmount ? formatUSDC(template.fixedAmount) : '0.0000'} USDC`})
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="选择 Leader"
|
||||
label={t('copyTradingAdd.selectLeader') || '选择 Leader'}
|
||||
name="leaderId"
|
||||
rules={[{ required: true, message: '请选择 Leader' }]}
|
||||
rules={[{ required: true, message: t('copyTradingAdd.leaderRequired') || '请选择 Leader' }]}
|
||||
>
|
||||
<Select placeholder="请选择 Leader">
|
||||
<Select placeholder={t('copyTradingAdd.selectLeaderPlaceholder') || '请选择 Leader'}>
|
||||
{leaders.map(leader => (
|
||||
<Option key={leader.id} value={leader.id}>
|
||||
{leader.leaderName || `Leader ${leader.id}`} ({leader.leaderAddress.slice(0, 6)}...{leader.leaderAddress.slice(-4)})
|
||||
@@ -135,12 +191,231 @@ const CopyTradingAdd: React.FC = () => {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* 模板填充按钮 */}
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => setTemplateModalVisible(true)}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{t('copyTradingAdd.selectTemplateFromModal') || '从模板填充配置'}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单金额模式 */}
|
||||
<Form.Item
|
||||
label="启用状态"
|
||||
name="enabled"
|
||||
label={t('copyTradingAdd.copyMode') || '跟单金额模式'}
|
||||
name="copyMode"
|
||||
tooltip={t('copyTradingAdd.copyModeTooltip') || '选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => handleCopyModeChange(e.target.value)}>
|
||||
<Radio value="RATIO">{t('copyTradingAdd.ratioMode') || '比例模式'}</Radio>
|
||||
<Radio value="FIXED">{t('copyTradingAdd.fixedAmountMode') || '固定金额模式'}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.copyRatio') || '跟单比例'}
|
||||
name="copyRatio"
|
||||
tooltip={t('copyTradingAdd.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={10}
|
||||
max={1000}
|
||||
step={1}
|
||||
precision={0}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="%"
|
||||
placeholder={t('copyTradingAdd.copyRatioPlaceholder') || '例如:100 表示 100%(1:1 跟单),默认 100%'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{copyMode === 'FIXED' && (
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.fixedAmount') || '固定跟单金额 (USDC)'}
|
||||
name="fixedAmount"
|
||||
rules={[
|
||||
{ required: true, message: t('copyTradingAdd.fixedAmountRequired') || '请输入固定跟单金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
const amount = Number(value)
|
||||
if (isNaN(amount)) {
|
||||
return Promise.reject(new Error(t('copyTradingAdd.invalidNumber') || '请输入有效的数字'))
|
||||
}
|
||||
if (amount < 1) {
|
||||
return Promise.reject(new Error(t('copyTradingAdd.fixedAmountMin') || '固定金额必须 >= 1'))
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.maxOrderSize') || '单笔订单最大金额 (USDC)'}
|
||||
name="maxOrderSize"
|
||||
tooltip={t('copyTradingAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.0001}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.minOrderSize') || '单笔订单最小金额 (USDC)'}
|
||||
name="minOrderSize"
|
||||
tooltip={t('copyTradingAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (typeof value === 'number' && value < 1) {
|
||||
return Promise.reject(new Error(t('copyTradingAdd.minOrderSizeMin') || '最小金额必须 >= 1'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
|
||||
name="maxDailyLoss"
|
||||
tooltip={t('copyTradingAdd.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.maxDailyOrders') || '每日最大跟单订单数'}
|
||||
name="maxDailyOrders"
|
||||
tooltip={t('copyTradingAdd.maxDailyOrdersTooltip') || '限制每日最多跟单的订单数量'}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.maxDailyOrdersPlaceholder') || '默认 100(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.priceTolerance') || '价格容忍度 (%)'}
|
||||
name="priceTolerance"
|
||||
tooltip={t('copyTradingAdd.priceToleranceTooltip') || '允许跟单价格在 Leader 价格基础上的调整范围'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.1}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.priceTolerancePlaceholder') || '默认 5%(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.delaySeconds') || '跟单延迟 (秒)'}
|
||||
name="delaySeconds"
|
||||
tooltip={t('copyTradingAdd.delaySecondsTooltip') || '跟单延迟时间,0 表示立即跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.delaySecondsPlaceholder') || '默认 0(立即跟单)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.minOrderDepth') || '最小订单深度 (USDC)'}
|
||||
name="minOrderDepth"
|
||||
tooltip={t('copyTradingAdd.minOrderDepthTooltip') || '最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.maxSpread') || '最大价差(绝对价格)'}
|
||||
name="maxSpread"
|
||||
tooltip={t('copyTradingAdd.maxSpreadTooltip') || '最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.minOrderbookDepth') || '最小订单簿深度 (USDC)'}
|
||||
name="minOrderbookDepth"
|
||||
tooltip={t('copyTradingAdd.minOrderbookDepthTooltip') || '最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingAdd.minOrderbookDepthPlaceholder') || '例如:50(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单卖出 - 表单最底部 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
tooltip={t('copyTradingAdd.supportSellTooltip') || '是否跟单 Leader 的卖出订单'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch checkedChildren="开启" unCheckedChildren="停止" />
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
@@ -151,18 +426,61 @@ const CopyTradingAdd: React.FC = () => {
|
||||
icon={<SaveOutlined />}
|
||||
loading={loading}
|
||||
>
|
||||
创建跟单
|
||||
{t('copyTradingAdd.create') || '创建跟单配置'}
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/copy-trading')}>
|
||||
取消
|
||||
{t('common.cancel') || '取消'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 模板选择 Modal */}
|
||||
<Modal
|
||||
title={t('copyTradingAdd.selectTemplate') || '选择模板'}
|
||||
open={templateModalVisible}
|
||||
onCancel={() => setTemplateModalVisible(false)}
|
||||
footer={null}
|
||||
width={800}
|
||||
>
|
||||
<Table
|
||||
dataSource={templates}
|
||||
rowKey="id"
|
||||
pagination={{ pageSize: 10 }}
|
||||
onRow={(record) => ({
|
||||
onClick: () => handleSelectTemplate(record),
|
||||
style: { cursor: 'pointer' }
|
||||
})}
|
||||
columns={[
|
||||
{
|
||||
title: t('copyTradingAdd.templateName') || '模板名称',
|
||||
dataIndex: 'templateName',
|
||||
key: 'templateName'
|
||||
},
|
||||
{
|
||||
title: t('copyTradingAdd.copyMode') || '跟单模式',
|
||||
key: 'copyMode',
|
||||
render: (_: any, record: CopyTradingTemplate) => (
|
||||
<span>
|
||||
{record.copyMode === 'RATIO'
|
||||
? `${t('copyTradingAdd.ratioMode') || '比例'} ${record.copyRatio}x`
|
||||
: `${t('copyTradingAdd.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
|
||||
}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingAdd.supportSell') || '跟单卖出',
|
||||
dataIndex: 'supportSell',
|
||||
key: 'supportSell',
|
||||
render: (supportSell: boolean) => supportSell ? (t('common.yes') || '是') : (t('common.no') || '否')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingAdd
|
||||
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Card, Form, Button, Switch, message, Typography, Space, Radio, InputNumber, Divider, Spin, Select } from 'antd'
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import type { CopyTrading, CopyTradingUpdateRequest } from '../types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title } = Typography
|
||||
const { Option } = Select
|
||||
|
||||
const CopyTradingEdit: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fetching, setFetching] = useState(true)
|
||||
const [copyTrading, setCopyTrading] = useState<CopyTrading | null>(null)
|
||||
const [copyMode, setCopyMode] = useState<'RATIO' | 'FIXED'>('RATIO')
|
||||
const [originalEnabled, setOriginalEnabled] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchCopyTrading(parseInt(id))
|
||||
}
|
||||
}, [id])
|
||||
|
||||
const fetchCopyTrading = async (copyTradingId: number) => {
|
||||
setFetching(true)
|
||||
try {
|
||||
const response = await apiService.copyTrading.list({})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const found = response.data.data.list.find((ct: CopyTrading) => ct.id === copyTradingId)
|
||||
if (found) {
|
||||
setCopyTrading(found)
|
||||
setCopyMode(found.copyMode)
|
||||
setOriginalEnabled(found.enabled) // 保存原始的enabled状态
|
||||
// 填充表单数据
|
||||
form.setFieldsValue({
|
||||
accountId: found.accountId,
|
||||
leaderId: found.leaderId,
|
||||
copyMode: found.copyMode,
|
||||
copyRatio: found.copyRatio ? parseFloat(found.copyRatio) * 100 : 100, // 转换为百分比显示
|
||||
fixedAmount: found.fixedAmount ? parseFloat(found.fixedAmount) : undefined,
|
||||
maxOrderSize: found.maxOrderSize ? parseFloat(found.maxOrderSize) : undefined,
|
||||
minOrderSize: found.minOrderSize ? parseFloat(found.minOrderSize) : undefined,
|
||||
maxDailyLoss: found.maxDailyLoss ? parseFloat(found.maxDailyLoss) : undefined,
|
||||
maxDailyOrders: found.maxDailyOrders,
|
||||
priceTolerance: found.priceTolerance ? parseFloat(found.priceTolerance) : undefined,
|
||||
delaySeconds: found.delaySeconds,
|
||||
pollIntervalSeconds: found.pollIntervalSeconds,
|
||||
useWebSocket: found.useWebSocket,
|
||||
websocketReconnectInterval: found.websocketReconnectInterval,
|
||||
websocketMaxRetries: found.websocketMaxRetries,
|
||||
supportSell: found.supportSell,
|
||||
minOrderDepth: found.minOrderDepth ? parseFloat(found.minOrderDepth) : undefined,
|
||||
maxSpread: found.maxSpread ? parseFloat(found.maxSpread) : undefined,
|
||||
minOrderbookDepth: found.minOrderbookDepth ? parseFloat(found.minOrderbookDepth) : undefined
|
||||
})
|
||||
} else {
|
||||
message.error(t('copyTradingEdit.fetchFailed') || '跟单配置不存在')
|
||||
navigate('/copy-trading')
|
||||
}
|
||||
} else {
|
||||
message.error(response.data.msg || t('copyTradingEdit.fetchFailed') || '获取跟单配置失败')
|
||||
navigate('/copy-trading')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('copyTradingEdit.fetchFailed') || '获取跟单配置失败')
|
||||
navigate('/copy-trading')
|
||||
} finally {
|
||||
setFetching(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyModeChange = (mode: 'RATIO' | 'FIXED') => {
|
||||
setCopyMode(mode)
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
// 前端校验
|
||||
if (values.copyMode === 'FIXED') {
|
||||
if (!values.fixedAmount || Number(values.fixedAmount) < 1) {
|
||||
message.error('固定金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && Number(values.minOrderSize) < 1) {
|
||||
message.error('最小金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
message.error('配置ID不存在')
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const request: CopyTradingUpdateRequest = {
|
||||
copyTradingId: parseInt(id),
|
||||
enabled: originalEnabled, // 保持原有的enabled状态,不修改
|
||||
copyMode: values.copyMode,
|
||||
copyRatio: values.copyMode === 'RATIO' && values.copyRatio ? (values.copyRatio / 100).toString() : undefined,
|
||||
fixedAmount: values.copyMode === 'FIXED' ? values.fixedAmount?.toString() : undefined,
|
||||
maxOrderSize: values.maxOrderSize?.toString(),
|
||||
minOrderSize: values.minOrderSize?.toString(),
|
||||
maxDailyLoss: values.maxDailyLoss?.toString(),
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
delaySeconds: values.delaySeconds,
|
||||
pollIntervalSeconds: values.pollIntervalSeconds,
|
||||
useWebSocket: values.useWebSocket,
|
||||
websocketReconnectInterval: values.websocketReconnectInterval,
|
||||
websocketMaxRetries: values.websocketMaxRetries,
|
||||
supportSell: values.supportSell,
|
||||
minOrderDepth: values.minOrderDepth?.toString(),
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minOrderbookDepth: values.minOrderbookDepth?.toString()
|
||||
}
|
||||
|
||||
const response = await apiService.copyTrading.update(request)
|
||||
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('copyTradingEdit.saveSuccess') || '更新跟单配置成功')
|
||||
navigate('/copy-trading')
|
||||
} else {
|
||||
message.error(response.data.msg || t('copyTradingEdit.saveFailed') || '更新跟单配置失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('copyTradingEdit.saveFailed') || '更新跟单配置失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (fetching) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!copyTrading) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/copy-trading')}
|
||||
>
|
||||
{t('common.back') || '返回'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Title level={4}>{t('copyTradingEdit.title') || '编辑跟单配置'}</Title>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
{/* 基础信息(只读) */}
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.selectWallet') || t('copyTradingEdit.selectWallet') || '钱包'}
|
||||
name="accountId"
|
||||
>
|
||||
<Select disabled>
|
||||
<Option value={copyTrading.accountId}>
|
||||
{copyTrading.accountName || `账户 ${copyTrading.accountId}`} ({copyTrading.walletAddress.slice(0, 6)}...{copyTrading.walletAddress.slice(-4)})
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingAdd.selectLeader') || t('copyTradingEdit.selectLeader') || 'Leader'}
|
||||
name="leaderId"
|
||||
>
|
||||
<Select disabled>
|
||||
<Option value={copyTrading.leaderId}>
|
||||
{copyTrading.leaderName || `Leader ${copyTrading.leaderId}`} ({copyTrading.leaderAddress.slice(0, 6)}...{copyTrading.leaderAddress.slice(-4)})
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Divider>{t('copyTradingEdit.basicConfig') || '基础配置'}</Divider>
|
||||
|
||||
{/* 跟单金额模式 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.copyMode') || '跟单金额模式'}
|
||||
name="copyMode"
|
||||
tooltip={t('copyTradingEdit.copyModeTooltip') || '选择跟单金额的计算方式'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => handleCopyModeChange(e.target.value)}>
|
||||
<Radio value="RATIO">{t('copyTradingEdit.ratioMode') || '比例模式'}</Radio>
|
||||
<Radio value="FIXED">{t('copyTradingEdit.fixedAmountMode') || '固定金额模式'}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.copyRatio') || '跟单比例'}
|
||||
name="copyRatio"
|
||||
tooltip={t('copyTradingEdit.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比'}
|
||||
>
|
||||
<InputNumber
|
||||
min={10}
|
||||
max={1000}
|
||||
step={1}
|
||||
precision={0}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="%"
|
||||
placeholder={t('copyTradingEdit.copyRatioPlaceholder') || '例如:100 表示 100%(1:1 跟单)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{copyMode === 'FIXED' && (
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.fixedAmount') || '固定跟单金额 (USDC)'}
|
||||
name="fixedAmount"
|
||||
rules={[
|
||||
{ required: true, message: t('copyTradingEdit.fixedAmountRequired') || '请输入固定跟单金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
const amount = Number(value)
|
||||
if (isNaN(amount)) {
|
||||
return Promise.reject(new Error(t('copyTradingEdit.invalidNumber') || '请输入有效的数字'))
|
||||
}
|
||||
if (amount < 1) {
|
||||
return Promise.reject(new Error(t('copyTradingEdit.fixedAmountMin') || '固定金额必须 >= 1'))
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
|
||||
name="maxOrderSize"
|
||||
tooltip={t('copyTradingEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.0001}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
|
||||
name="minOrderSize"
|
||||
tooltip={t('copyTradingEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (typeof value === 'number' && value < 1) {
|
||||
return Promise.reject(new Error(t('copyTradingEdit.minOrderSizeMin') || '最小金额必须 >= 1'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
|
||||
name="maxDailyLoss"
|
||||
tooltip={t('copyTradingEdit.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxDailyOrders') || '每日最大跟单订单数'}
|
||||
name="maxDailyOrders"
|
||||
tooltip={t('copyTradingEdit.maxDailyOrdersTooltip') || '限制每日最多跟单的订单数量'}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxDailyOrdersPlaceholder') || '默认 100(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.priceTolerance') || '价格容忍度 (%)'}
|
||||
name="priceTolerance"
|
||||
tooltip={t('copyTradingEdit.priceToleranceTooltip') || '允许跟单价格在 Leader 价格基础上的调整范围'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={100}
|
||||
step={0.1}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.priceTolerancePlaceholder') || '默认 5%(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.delaySeconds') || '跟单延迟 (秒)'}
|
||||
name="delaySeconds"
|
||||
tooltip={t('copyTradingEdit.delaySecondsTooltip') || '跟单延迟时间,0 表示立即跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.delaySecondsPlaceholder') || '默认 0(立即跟单)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.minOrderDepth') || '最小订单深度 (USDC)'}
|
||||
name="minOrderDepth"
|
||||
tooltip={t('copyTradingEdit.minOrderDepthTooltip') || '最小订单深度(USDC金额),NULL表示不启用此过滤'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.maxSpread') || '最大价差(绝对价格)'}
|
||||
name="maxSpread"
|
||||
tooltip={t('copyTradingEdit.maxSpreadTooltip') || '最大价差(绝对价格),NULL表示不启用此过滤'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.minOrderbookDepth') || '最小订单簿深度 (USDC)'}
|
||||
name="minOrderbookDepth"
|
||||
tooltip={t('copyTradingEdit.minOrderbookDepthTooltip') || '最小订单簿深度(USDC金额),NULL表示不启用此过滤'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('copyTradingEdit.minOrderbookDepthPlaceholder') || '例如:50(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单卖出 - 表单最底部 */}
|
||||
<Form.Item
|
||||
label={t('copyTradingEdit.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
tooltip={t('copyTradingEdit.supportSellTooltip') || '是否跟单 Leader 的卖出订单'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<SaveOutlined />}
|
||||
loading={loading}
|
||||
>
|
||||
{t('copyTradingEdit.save') || '保存'}
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/copy-trading')}>
|
||||
{t('common.cancel') || '取消'}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyTradingEdit
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, Switch, message, Select, Dropdown, Divider, Spin } from 'antd'
|
||||
import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||
import { PlusOutlined, DeleteOutlined, BarChartOutlined, UnorderedListOutlined, ArrowUpOutlined, ArrowDownOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { apiService } from '../services/api'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { CopyTrading, Leader, CopyTradingTemplate, CopyTradingStatistics } from '../types'
|
||||
import type { CopyTrading, Leader, CopyTradingStatistics } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
|
||||
@@ -19,13 +19,11 @@ const CopyTradingList: React.FC = () => {
|
||||
const { accounts, fetchAccounts } = useAccountStore()
|
||||
const [copyTradings, setCopyTradings] = useState<CopyTrading[]>([])
|
||||
const [leaders, setLeaders] = useState<Leader[]>([])
|
||||
const [templates, setTemplates] = useState<CopyTradingTemplate[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [statisticsMap, setStatisticsMap] = useState<Record<number, CopyTradingStatistics>>({})
|
||||
const [loadingStatistics, setLoadingStatistics] = useState<Set<number>>(new Set())
|
||||
const [filters, setFilters] = useState<{
|
||||
accountId?: number
|
||||
templateId?: number
|
||||
leaderId?: number
|
||||
enabled?: boolean
|
||||
}>({})
|
||||
@@ -33,7 +31,6 @@ const CopyTradingList: React.FC = () => {
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchLeaders()
|
||||
fetchTemplates()
|
||||
fetchCopyTradings()
|
||||
}, [])
|
||||
|
||||
@@ -52,17 +49,6 @@ const CopyTradingList: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
try {
|
||||
const response = await apiService.templates.list()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setTemplates(response.data.data.list || [])
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取模板列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCopyTradings = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -179,12 +165,16 @@ const CopyTradingList: React.FC = () => {
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('copyTradingList.template') || '模板',
|
||||
dataIndex: 'templateName',
|
||||
key: 'templateName',
|
||||
title: t('copyTradingList.copyMode') || '跟单模式',
|
||||
key: 'copyMode',
|
||||
width: isMobile ? 100 : 120,
|
||||
render: (text: string) => (
|
||||
<strong style={{ fontSize: isMobile ? 13 : 14 }}>{text}</strong>
|
||||
render: (_: any, record: CopyTrading) => (
|
||||
<Tag color={record.copyMode === 'RATIO' ? 'blue' : 'green'}>
|
||||
{record.copyMode === 'RATIO'
|
||||
? `${t('copyTradingList.ratioMode') || '比例'} ${record.copyRatio}x`
|
||||
: `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}`
|
||||
}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -265,6 +255,15 @@ const CopyTradingList: React.FC = () => {
|
||||
fixed: 'right' as const,
|
||||
render: (_: any, record: CopyTrading) => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('common.edit') || '编辑',
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/edit/${record.id}`)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
key: 'statistics',
|
||||
label: t('copyTradingList.viewStatistics') || '查看统计',
|
||||
@@ -289,6 +288,12 @@ const CopyTradingList: React.FC = () => {
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/orders/matched/${record.id}`)
|
||||
},
|
||||
{
|
||||
key: 'filteredOrders',
|
||||
label: t('copyTradingList.filteredOrders') || '被过滤订单',
|
||||
icon: <UnorderedListOutlined />,
|
||||
onClick: () => navigate(`/copy-trading/filtered-orders/${record.id}`)
|
||||
},
|
||||
{
|
||||
type: 'divider'
|
||||
},
|
||||
@@ -312,14 +317,24 @@ const CopyTradingList: React.FC = () => {
|
||||
return (
|
||||
<Space size={isMobile ? 'small' : 'middle'} wrap>
|
||||
{!isMobile && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
>
|
||||
{t('copyTradingList.statistics') || '统计'}
|
||||
</Button>
|
||||
<>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/edit/${record.id}`)}
|
||||
>
|
||||
{t('common.edit') || '编辑'}
|
||||
</Button>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<BarChartOutlined />}
|
||||
onClick={() => navigate(`/copy-trading/statistics/${record.id}`)}
|
||||
>
|
||||
{t('copyTradingList.statistics') || '统计'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['click']}>
|
||||
<Button
|
||||
@@ -382,20 +397,6 @@ const CopyTradingList: React.FC = () => {
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder={t('copyTradingList.filterTemplate') || '筛选模板'}
|
||||
allowClear
|
||||
style={{ width: isMobile ? '100%' : 200 }}
|
||||
value={filters.templateId}
|
||||
onChange={(value) => setFilters({ ...filters, templateId: value || undefined })}
|
||||
>
|
||||
{templates.map(template => (
|
||||
<Option key={template.id} value={template.id}>
|
||||
{template.templateName}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
placeholder={t('copyTradingList.filterLeader') || '筛选 Leader'}
|
||||
allowClear
|
||||
@@ -464,7 +465,10 @@ const CopyTradingList: React.FC = () => {
|
||||
marginBottom: '8px',
|
||||
color: '#1890ff'
|
||||
}}>
|
||||
{record.templateName}
|
||||
{record.copyMode === 'RATIO'
|
||||
? `${t('copyTradingList.ratioMode') || '比例'} ${record.copyRatio}x`
|
||||
: `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')}`
|
||||
}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Tag color={record.enabled ? 'green' : 'red'}>
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { Card, Table, Button, Tag, Select, Space, message } from 'antd'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { FilteredOrder, FilteredOrderListResponse } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { formatUSDC } from '../utils'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
const FilteredOrdersList: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [filteredOrders, setFilteredOrders] = useState<FilteredOrder[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [limit] = useState(20)
|
||||
const [filterType, setFilterType] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchFilteredOrders()
|
||||
}
|
||||
}, [id, page, filterType])
|
||||
|
||||
const fetchFilteredOrders = async () => {
|
||||
if (!id) return
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.copyTrading.getFilteredOrders({
|
||||
copyTradingId: parseInt(id),
|
||||
filterType: filterType,
|
||||
page: page,
|
||||
limit: limit
|
||||
})
|
||||
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
const data: FilteredOrderListResponse = response.data.data
|
||||
setFilteredOrders(data.list || [])
|
||||
setTotal(data.total || 0)
|
||||
} else {
|
||||
message.error(response.data.msg || t('filteredOrdersList.fetchFailed') || '获取被过滤订单列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取被过滤订单列表失败:', error)
|
||||
message.error(error.message || t('filteredOrdersList.fetchFailed') || '获取被过滤订单列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getFilterTypeTag = (type: string) => {
|
||||
const typeMap: Record<string, { color: string; label: string }> = {
|
||||
'ORDER_DEPTH': { color: 'orange', label: t('filteredOrdersList.filterTypes.orderDepth') || '订单深度不足' },
|
||||
'SPREAD': { color: 'red', label: t('filteredOrdersList.filterTypes.spread') || '价差过大' },
|
||||
'ORDERBOOK_DEPTH': { color: 'volcano', label: t('filteredOrdersList.filterTypes.orderbookDepth') || '订单簿深度不足' },
|
||||
'PRICE_VALIDITY': { color: 'purple', label: t('filteredOrdersList.filterTypes.priceValidity') || '价格不合理' },
|
||||
'MARKET_STATUS': { color: 'blue', label: t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易' },
|
||||
'ORDERBOOK_ERROR': { color: 'default', label: t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败' },
|
||||
'ORDERBOOK_EMPTY': { color: 'default', label: t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空' },
|
||||
'UNKNOWN': { color: 'default', label: t('filteredOrdersList.filterTypes.unknown') || '未知原因' }
|
||||
}
|
||||
const config = typeMap[type] || typeMap['UNKNOWN']
|
||||
return <Tag color={config.color}>{config.label}</Tag>
|
||||
}
|
||||
|
||||
const getMarketLink = (order: FilteredOrder) => {
|
||||
if (order.marketSlug) {
|
||||
return `https://polymarket.com/event/${order.marketSlug}`
|
||||
}
|
||||
if (order.marketId && order.marketId.startsWith('0x')) {
|
||||
return `https://polymarket.com/condition/${order.marketId}`
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('filteredOrdersList.market') || '市场',
|
||||
key: 'market',
|
||||
width: isMobile ? 150 : 200,
|
||||
render: (_: any, record: FilteredOrder) => {
|
||||
const link = getMarketLink(record)
|
||||
const marketTitle = record.marketTitle || record.marketId.slice(0, 10) + '...'
|
||||
return link ? (
|
||||
<a href={link} target="_blank" rel="noopener noreferrer" style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{marketTitle}
|
||||
</a>
|
||||
) : (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{marketTitle}</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.side') || '订单方向',
|
||||
key: 'side',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (_: any, record: FilteredOrder) => (
|
||||
<Tag color={record.side === 'BUY' ? 'green' : 'red'} style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{record.side === 'BUY' ? (t('order.buy') || '买入') : (t('order.sell') || '卖出')}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.outcome') || '市场方向',
|
||||
key: 'outcome',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (_: any, record: FilteredOrder) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{record.outcome || (record.outcomeIndex !== undefined ? `Index ${record.outcomeIndex}` : '-')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.price') || '价格',
|
||||
key: 'price',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (_: any, record: FilteredOrder) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{record.price}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.size') || 'Leader数量',
|
||||
key: 'size',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (_: any, record: FilteredOrder) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>{formatUSDC(record.size)}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.calculatedQuantity') || '计算数量',
|
||||
key: 'calculatedQuantity',
|
||||
width: isMobile ? 80 : 100,
|
||||
render: (_: any, record: FilteredOrder) => (
|
||||
<span style={{ fontSize: isMobile ? 12 : 14 }}>
|
||||
{record.calculatedQuantity ? formatUSDC(record.calculatedQuantity) : '-'}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.filterType') || '过滤类型',
|
||||
key: 'filterType',
|
||||
width: isMobile ? 120 : 150,
|
||||
render: (_: any, record: FilteredOrder) => getFilterTypeTag(record.filterType)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.filterReason') || '过滤原因',
|
||||
key: 'filterReason',
|
||||
width: isMobile ? 150 : 250,
|
||||
ellipsis: true,
|
||||
render: (_: any, record: FilteredOrder) => (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }} title={record.filterReason}>
|
||||
{record.filterReason}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('filteredOrdersList.createdAt') || '时间',
|
||||
key: 'createdAt',
|
||||
width: isMobile ? 120 : 160,
|
||||
render: (_: any, record: FilteredOrder) => {
|
||||
const date = new Date(record.createdAt)
|
||||
const format = isMobile
|
||||
? `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}:${String(date.getSeconds()).padStart(2, '0')}`
|
||||
return (
|
||||
<span style={{ fontSize: isMobile ? 11 : 12 }}>
|
||||
{format}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/copy-trading')}
|
||||
>
|
||||
{t('common.back') || '返回'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8 }}>
|
||||
<h3 style={{ margin: 0 }}>{t('filteredOrdersList.title') || '被过滤订单列表'}</h3>
|
||||
<Space>
|
||||
<Select
|
||||
placeholder={t('filteredOrdersList.filterByType') || '按类型筛选'}
|
||||
value={filterType}
|
||||
onChange={(value) => {
|
||||
setFilterType(value)
|
||||
setPage(1)
|
||||
}}
|
||||
allowClear
|
||||
style={{ width: isMobile ? 120 : 150 }}
|
||||
>
|
||||
<Option value="ORDER_DEPTH">{t('filteredOrdersList.filterTypes.orderDepth') || '订单深度不足'}</Option>
|
||||
<Option value="SPREAD">{t('filteredOrdersList.filterTypes.spread') || '价差过大'}</Option>
|
||||
<Option value="ORDERBOOK_DEPTH">{t('filteredOrdersList.filterTypes.orderbookDepth') || '订单簿深度不足'}</Option>
|
||||
<Option value="PRICE_VALIDITY">{t('filteredOrdersList.filterTypes.priceValidity') || '价格不合理'}</Option>
|
||||
<Option value="MARKET_STATUS">{t('filteredOrdersList.filterTypes.marketStatus') || '市场状态不可交易'}</Option>
|
||||
<Option value="ORDERBOOK_ERROR">{t('filteredOrdersList.filterTypes.orderbookError') || '订单簿获取失败'}</Option>
|
||||
<Option value="ORDERBOOK_EMPTY">{t('filteredOrdersList.filterTypes.orderbookEmpty') || '订单簿为空'}</Option>
|
||||
</Select>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={filteredOrders}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize: limit,
|
||||
total: total,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => t('common.total') + `: ${total}`,
|
||||
onChange: (page) => setPage(page)
|
||||
}}
|
||||
scroll={{ x: isMobile ? 800 : 'auto' }}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilteredOrdersList
|
||||
|
||||
@@ -3,10 +3,12 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typography, Space } from 'antd'
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const TemplateAdd: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -15,7 +17,7 @@ const TemplateAdd: React.FC = () => {
|
||||
const handleSubmit = async (values: any) => {
|
||||
// 前端校验:如果填写了 minOrderSize,必须 >= 1
|
||||
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && values.minOrderSize !== '' && Number(values.minOrderSize) < 1) {
|
||||
message.error('最小金额必须 >= 1')
|
||||
message.error(t('templateAdd.minOrderSizeError') || '最小金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,16 +25,16 @@ const TemplateAdd: React.FC = () => {
|
||||
if (values.copyMode === 'FIXED') {
|
||||
const fixedAmount = values.fixedAmount
|
||||
if (fixedAmount === undefined || fixedAmount === null || fixedAmount === '') {
|
||||
message.error('请输入固定跟单金额')
|
||||
message.error(t('templateAdd.fixedAmountRequired') || '请输入固定跟单金额')
|
||||
return
|
||||
}
|
||||
const amount = Number(fixedAmount)
|
||||
if (isNaN(amount)) {
|
||||
message.error('请输入有效的数字')
|
||||
message.error(t('templateAdd.invalidNumber') || '请输入有效的数字')
|
||||
return
|
||||
}
|
||||
if (amount < 1) {
|
||||
message.error('固定金额必须 >= 1,请重新输入')
|
||||
message.error(t('templateAdd.fixedAmountError') || '固定金额必须 >= 1,请重新输入')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -49,17 +51,20 @@ const TemplateAdd: React.FC = () => {
|
||||
minOrderSize: values.copyMode === 'RATIO' ? values.minOrderSize?.toString() : undefined,
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
supportSell: values.supportSell !== false
|
||||
supportSell: values.supportSell !== false,
|
||||
minOrderDepth: values.minOrderDepth?.toString(),
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minOrderbookDepth: values.minOrderbookDepth?.toString()
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
message.success('创建模板成功')
|
||||
message.success(t('templateAdd.createSuccess') || '创建模板成功')
|
||||
navigate('/templates')
|
||||
} else {
|
||||
message.error(response.data.msg || '创建模板失败')
|
||||
message.error(response.data.msg || t('templateAdd.createFailed') || '创建模板失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '创建模板失败')
|
||||
message.error(error.message || t('templateAdd.createFailed') || '创建模板失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -72,12 +77,12 @@ const TemplateAdd: React.FC = () => {
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/templates')}
|
||||
>
|
||||
返回
|
||||
{t('templateAdd.back') || t('common.back') || '返回'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Title level={4}>创建跟单模板</Title>
|
||||
<Title level={4}>{t('templateAdd.title') || '创建跟单模板'}</Title>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
@@ -94,31 +99,31 @@ const TemplateAdd: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
label={t('templateAdd.templateName') || '模板名称'}
|
||||
name="templateName"
|
||||
tooltip="模板的唯一标识名称,用于区分不同的跟单配置模板。模板名称必须唯一,不能与其他模板重名。"
|
||||
rules={[{ required: true, message: '请输入模板名称' }]}
|
||||
tooltip={t('templateAdd.templateNameTooltip') || '模板的唯一标识名称,用于区分不同的跟单配置模板。模板名称必须唯一,不能与其他模板重名。'}
|
||||
rules={[{ required: true, message: t('templateAdd.templateNameRequired') || '请输入模板名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入模板名称" />
|
||||
<Input placeholder={t('templateAdd.templateNamePlaceholder') || '请输入模板名称'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="跟单金额模式"
|
||||
label={t('templateAdd.copyMode') || '跟单金额模式'}
|
||||
name="copyMode"
|
||||
tooltip="选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。"
|
||||
tooltip={t('templateAdd.copyModeTooltip') || '选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => setCopyMode(e.target.value)}>
|
||||
<Radio value="RATIO">比例模式</Radio>
|
||||
<Radio value="FIXED">固定金额模式</Radio>
|
||||
<Radio value="RATIO">{t('templateAdd.ratioMode') || '比例模式'}</Radio>
|
||||
<Radio value="FIXED">{t('templateAdd.fixedAmountMode') || '固定金额模式'}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<Form.Item
|
||||
label="跟单比例"
|
||||
label={t('templateAdd.copyRatio') || '跟单比例'}
|
||||
name="copyRatio"
|
||||
tooltip="跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单"
|
||||
tooltip={t('templateAdd.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={10}
|
||||
@@ -127,7 +132,7 @@ const TemplateAdd: React.FC = () => {
|
||||
precision={0}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="%"
|
||||
placeholder="例如:100 表示 100%(1:1 跟单),默认 100%"
|
||||
placeholder={t('templateAdd.copyRatioPlaceholder') || '例如:100 表示 100%(1:1 跟单),默认 100%'}
|
||||
parser={(value) => {
|
||||
const parsed = parseFloat(value || '0')
|
||||
if (parsed > 1000) return 1000
|
||||
@@ -145,20 +150,20 @@ const TemplateAdd: React.FC = () => {
|
||||
|
||||
{copyMode === 'FIXED' && (
|
||||
<Form.Item
|
||||
label="固定跟单金额 (USDC)"
|
||||
label={t('templateAdd.fixedAmount') || '固定跟单金额 (USDC)'}
|
||||
name="fixedAmount"
|
||||
rules={[
|
||||
{ required: true, message: '请输入固定跟单金额' },
|
||||
{ required: true, message: t('templateAdd.fixedAmountRequired') || '请输入固定跟单金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
// required 已经处理了空值情况,这里只处理非空值的校验
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
const amount = Number(value)
|
||||
if (isNaN(amount)) {
|
||||
return Promise.reject(new Error('请输入有效的数字'))
|
||||
return Promise.reject(new Error(t('templateAdd.invalidNumber') || '请输入有效的数字'))
|
||||
}
|
||||
if (amount < 1) {
|
||||
return Promise.reject(new Error('固定金额必须 >= 1,请重新输入'))
|
||||
return Promise.reject(new Error(t('templateAdd.fixedAmountError') || '固定金额必须 >= 1,请重新输入'))
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
@@ -170,7 +175,7 @@ const TemplateAdd: React.FC = () => {
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="固定金额,不随 Leader 订单大小变化,必须 >= 1"
|
||||
placeholder={t('templateAdd.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -178,23 +183,23 @@ const TemplateAdd: React.FC = () => {
|
||||
{copyMode === 'RATIO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="单笔订单最大金额 (USDC)"
|
||||
label={t('templateAdd.maxOrderSize') || '单笔订单最大金额 (USDC)'}
|
||||
name="maxOrderSize"
|
||||
tooltip="比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。"
|
||||
tooltip={t('templateAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.0001}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="仅在比例模式下生效(可选)"
|
||||
placeholder={t('templateAdd.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="单笔订单最小金额 (USDC)"
|
||||
label={t('templateAdd.minOrderSize') || '单笔订单最小金额 (USDC)'}
|
||||
name="minOrderSize"
|
||||
tooltip="比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。"
|
||||
tooltip={t('templateAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
@@ -202,7 +207,7 @@ const TemplateAdd: React.FC = () => {
|
||||
return Promise.resolve() // 可选字段,允许为空
|
||||
}
|
||||
if (typeof value === 'number' && value < 1) {
|
||||
return Promise.reject(new Error('最小金额必须 >= 1'))
|
||||
return Promise.reject(new Error(t('templateAdd.minOrderSizeError') || '最小金额必须 >= 1'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -214,29 +219,29 @@ const TemplateAdd: React.FC = () => {
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="仅在比例模式下生效,必须 >= 1(可选)"
|
||||
placeholder={t('templateAdd.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label="每日最大跟单订单数"
|
||||
label={t('templateAdd.maxDailyOrders') || '每日最大跟单订单数'}
|
||||
name="maxDailyOrders"
|
||||
tooltip="限制每日最多跟单的订单数量,用于风险控制,防止过度交易。例如:设置为 50,当日跟单订单数达到 50 后,停止跟单,次日重置。"
|
||||
tooltip={t('templateAdd.maxDailyOrdersTooltip') || '限制每日最多跟单的订单数量,用于风险控制,防止过度交易。例如:设置为 50,当日跟单订单数达到 50 后,停止跟单,次日重置。'}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="默认 100(可选)"
|
||||
placeholder={t('templateAdd.maxDailyOrdersPlaceholder') || '默认 100(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="价格容忍度 (%)"
|
||||
label={t('templateAdd.priceTolerance') || '价格容忍度 (%)'}
|
||||
name="priceTolerance"
|
||||
tooltip="允许跟单价格在 Leader 价格基础上的调整范围,用于在 Leader 价格 ± 容忍度范围内调整价格,提高成交率。例如:设置为 5%,Leader 价格为 0.5,则跟单价格可在 0.475-0.525 范围内。"
|
||||
tooltip={t('templateAdd.priceToleranceTooltip') || '允许跟单价格在 Leader 价格基础上的调整范围,用于在 Leader 价格 ± 容忍度范围内调整价格,提高成交率。例如:设置为 5%,Leader 价格为 0.5,则跟单价格可在 0.475-0.525 范围内。'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
@@ -244,14 +249,57 @@ const TemplateAdd: React.FC = () => {
|
||||
step={0.1}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="默认 5%(可选)"
|
||||
placeholder={t('templateAdd.priceTolerancePlaceholder') || '默认 5%(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="跟单卖出"
|
||||
label={t('templateAdd.minOrderDepth') || '最小订单深度 (USDC)'}
|
||||
name="minOrderDepth"
|
||||
tooltip={t('templateAdd.minOrderDepthTooltip') || '最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('templateAdd.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('templateAdd.maxSpread') || '最大价差(绝对价格)'}
|
||||
name="maxSpread"
|
||||
tooltip={t('templateAdd.maxSpreadTooltip') || '最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('templateAdd.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('templateAdd.minOrderbookDepth') || '最小订单簿深度 (USDC)'}
|
||||
name="minOrderbookDepth"
|
||||
tooltip={t('templateAdd.minOrderbookDepthTooltip') || '最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('templateAdd.minOrderbookDepthPlaceholder') || '例如:50(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单卖出 - 表单最底部 */}
|
||||
<Form.Item
|
||||
label={t('templateAdd.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
tooltip="是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。"
|
||||
tooltip={t('templateAdd.supportSellTooltip') || '是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
@@ -270,10 +318,10 @@ const TemplateAdd: React.FC = () => {
|
||||
loading={loading}
|
||||
disabled={hasErrors}
|
||||
>
|
||||
创建模板
|
||||
{t('templateAdd.create') || '创建模板'}
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/templates')}>
|
||||
取消
|
||||
{t('common.cancel') || '取消'}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
|
||||
@@ -4,14 +4,14 @@ import { Card, Form, Input, Button, Radio, InputNumber, Switch, message, Typogra
|
||||
import { ArrowLeftOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import type { CopyTradingTemplate } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const TemplateEdit: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { id } = useParams<{ id: string }>()
|
||||
useMediaQuery({ maxWidth: 768 }) // 用于响应式布局,但当前页面未使用
|
||||
const [form] = Form.useForm()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fetching, setFetching] = useState(false)
|
||||
@@ -37,14 +37,17 @@ const TemplateEdit: React.FC = () => {
|
||||
fixedAmount: template.fixedAmount ? parseFloat(template.fixedAmount) : undefined,
|
||||
maxOrderSize: template.maxOrderSize ? parseFloat(template.maxOrderSize) : undefined,
|
||||
minOrderSize: template.minOrderSize ? parseFloat(template.minOrderSize) : undefined,
|
||||
priceTolerance: parseFloat(template.priceTolerance)
|
||||
priceTolerance: parseFloat(template.priceTolerance),
|
||||
minOrderDepth: template.minOrderDepth ? parseFloat(template.minOrderDepth) : undefined,
|
||||
maxSpread: template.maxSpread ? parseFloat(template.maxSpread) : undefined,
|
||||
minOrderbookDepth: template.minOrderbookDepth ? parseFloat(template.minOrderbookDepth) : undefined
|
||||
})
|
||||
} else {
|
||||
message.error(response.data.msg || '获取模板详情失败')
|
||||
message.error(response.data.msg || t('templateEdit.fetchFailed') || '获取模板详情失败')
|
||||
navigate('/templates')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取模板详情失败')
|
||||
message.error(error.message || t('templateEdit.fetchFailed') || '获取模板详情失败')
|
||||
navigate('/templates')
|
||||
} finally {
|
||||
setFetching(false)
|
||||
@@ -56,7 +59,7 @@ const TemplateEdit: React.FC = () => {
|
||||
|
||||
// 前端校验:如果填写了 minOrderSize,必须 >= 1
|
||||
if (values.copyMode === 'RATIO' && values.minOrderSize !== undefined && values.minOrderSize !== null && values.minOrderSize !== '' && Number(values.minOrderSize) < 1) {
|
||||
message.error('最小金额必须 >= 1')
|
||||
message.error(t('templateEdit.minOrderSizeError') || '最小金额必须 >= 1')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -64,16 +67,16 @@ const TemplateEdit: React.FC = () => {
|
||||
if (values.copyMode === 'FIXED') {
|
||||
const fixedAmount = values.fixedAmount
|
||||
if (fixedAmount === undefined || fixedAmount === null || fixedAmount === '') {
|
||||
message.error('请输入固定跟单金额')
|
||||
message.error(t('templateEdit.fixedAmountRequired') || '请输入固定跟单金额')
|
||||
return
|
||||
}
|
||||
const amount = Number(fixedAmount)
|
||||
if (isNaN(amount)) {
|
||||
message.error('请输入有效的数字')
|
||||
message.error(t('templateEdit.invalidNumber') || '请输入有效的数字')
|
||||
return
|
||||
}
|
||||
if (amount < 1) {
|
||||
message.error('固定金额必须 >= 1,请重新输入')
|
||||
message.error(t('templateEdit.fixedAmountError') || '固定金额必须 >= 1,请重新输入')
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -91,17 +94,20 @@ const TemplateEdit: React.FC = () => {
|
||||
minOrderSize: values.copyMode === 'RATIO' ? values.minOrderSize?.toString() : undefined,
|
||||
maxDailyOrders: values.maxDailyOrders,
|
||||
priceTolerance: values.priceTolerance?.toString(),
|
||||
supportSell: values.supportSell
|
||||
supportSell: values.supportSell,
|
||||
minOrderDepth: values.minOrderDepth?.toString(),
|
||||
maxSpread: values.maxSpread?.toString(),
|
||||
minOrderbookDepth: values.minOrderbookDepth?.toString()
|
||||
})
|
||||
|
||||
if (response.data.code === 0) {
|
||||
message.success('更新模板成功')
|
||||
message.success(t('templateEdit.saveSuccess') || '更新模板成功')
|
||||
navigate('/templates')
|
||||
} else {
|
||||
message.error(response.data.msg || '更新模板失败')
|
||||
message.error(response.data.msg || t('templateEdit.saveFailed') || '更新模板失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '更新模板失败')
|
||||
message.error(error.message || t('templateEdit.saveFailed') || '更新模板失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -114,12 +120,12 @@ const TemplateEdit: React.FC = () => {
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/templates')}
|
||||
>
|
||||
返回
|
||||
{t('templateEdit.back') || t('common.back') || '返回'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card loading={fetching}>
|
||||
<Title level={4}>编辑跟单模板</Title>
|
||||
<Title level={4}>{t('templateEdit.title') || '编辑跟单模板'}</Title>
|
||||
|
||||
<Form
|
||||
form={form}
|
||||
@@ -127,31 +133,31 @@ const TemplateEdit: React.FC = () => {
|
||||
onFinish={handleSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="模板名称"
|
||||
label={t('templateEdit.templateName') || '模板名称'}
|
||||
name="templateName"
|
||||
tooltip="模板的唯一标识名称,用于区分不同的跟单配置模板。模板名称必须唯一,不能与其他模板重名。"
|
||||
rules={[{ required: true, message: '请输入模板名称' }]}
|
||||
tooltip={t('templateEdit.templateNameTooltip') || '模板的唯一标识名称,用于区分不同的跟单配置模板。模板名称必须唯一,不能与其他模板重名。'}
|
||||
rules={[{ required: true, message: t('templateEdit.templateNameRequired') || '请输入模板名称' }]}
|
||||
>
|
||||
<Input placeholder="请输入模板名称" />
|
||||
<Input placeholder={t('templateEdit.templateNamePlaceholder') || '请输入模板名称'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="跟单金额模式"
|
||||
label={t('templateEdit.copyMode') || '跟单金额模式'}
|
||||
name="copyMode"
|
||||
tooltip="选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。"
|
||||
tooltip={t('templateEdit.copyModeTooltip') || '选择跟单金额的计算方式。比例模式:跟单金额随 Leader 订单大小按比例变化;固定金额模式:无论 Leader 订单大小如何,跟单金额都固定不变。'}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Radio.Group onChange={(e) => setCopyMode(e.target.value)}>
|
||||
<Radio value="RATIO">比例模式</Radio>
|
||||
<Radio value="FIXED">固定金额模式</Radio>
|
||||
<Radio value="RATIO">{t('templateEdit.ratioMode') || '比例模式'}</Radio>
|
||||
<Radio value="FIXED">{t('templateEdit.fixedAmountMode') || '固定金额模式'}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{copyMode === 'RATIO' && (
|
||||
<Form.Item
|
||||
label="跟单比例"
|
||||
label={t('templateEdit.copyRatio') || '跟单比例'}
|
||||
name="copyRatio"
|
||||
tooltip="跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单"
|
||||
tooltip={t('templateEdit.copyRatioTooltip') || '跟单比例表示跟单金额相对于 Leader 订单金额的百分比。例如:100% 表示 1:1 跟单,50% 表示半仓跟单,200% 表示双倍跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={10}
|
||||
@@ -160,7 +166,7 @@ const TemplateEdit: React.FC = () => {
|
||||
precision={0}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="%"
|
||||
placeholder="例如:100 表示 100%(1:1 跟单),默认 100%"
|
||||
placeholder={t('templateEdit.copyRatioPlaceholder') || '例如:100 表示 100%(1:1 跟单),默认 100%'}
|
||||
parser={(value) => {
|
||||
const parsed = parseFloat(value || '0')
|
||||
if (parsed > 1000) return 1000
|
||||
@@ -178,21 +184,21 @@ const TemplateEdit: React.FC = () => {
|
||||
|
||||
{copyMode === 'FIXED' && (
|
||||
<Form.Item
|
||||
label="固定跟单金额 (USDC)"
|
||||
label={t('templateEdit.fixedAmount') || '固定跟单金额 (USDC)'}
|
||||
name="fixedAmount"
|
||||
tooltip="固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= 1 USDC。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 10 USDC。"
|
||||
tooltip={t('templateEdit.fixedAmountTooltip') || '固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= 1 USDC。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 10 USDC。'}
|
||||
rules={[
|
||||
{ required: true, message: '请输入固定跟单金额' },
|
||||
{ required: true, message: t('templateEdit.fixedAmountRequired') || '请输入固定跟单金额' },
|
||||
{
|
||||
validator: (_, value) => {
|
||||
// required 已经处理了空值情况,这里只处理非空值的校验
|
||||
if (value !== undefined && value !== null && value !== '') {
|
||||
const amount = Number(value)
|
||||
if (isNaN(amount)) {
|
||||
return Promise.reject(new Error('请输入有效的数字'))
|
||||
return Promise.reject(new Error(t('templateEdit.invalidNumber') || '请输入有效的数字'))
|
||||
}
|
||||
if (amount < 1) {
|
||||
return Promise.reject(new Error('固定金额必须 >= 1,请重新输入'))
|
||||
return Promise.reject(new Error(t('templateEdit.fixedAmountError') || '固定金额必须 >= 1,请重新输入'))
|
||||
}
|
||||
}
|
||||
return Promise.resolve()
|
||||
@@ -204,7 +210,7 @@ const TemplateEdit: React.FC = () => {
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="固定金额,不随 Leader 订单大小变化,必须 >= 1"
|
||||
placeholder={t('templateEdit.fixedAmountPlaceholder') || '固定金额,不随 Leader 订单大小变化,必须 >= 1'}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
@@ -212,23 +218,23 @@ const TemplateEdit: React.FC = () => {
|
||||
{copyMode === 'RATIO' && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="单笔订单最大金额 (USDC)"
|
||||
label={t('templateEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
|
||||
name="maxOrderSize"
|
||||
tooltip="比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。"
|
||||
tooltip={t('templateEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.0001}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="仅在比例模式下生效(可选)"
|
||||
placeholder={t('templateEdit.maxOrderSizePlaceholder') || '仅在比例模式下生效(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="单笔订单最小金额 (USDC)"
|
||||
label={t('templateEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
|
||||
name="minOrderSize"
|
||||
tooltip="比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。"
|
||||
tooltip={t('templateEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
|
||||
rules={[
|
||||
{
|
||||
validator: (_, value) => {
|
||||
@@ -236,7 +242,7 @@ const TemplateEdit: React.FC = () => {
|
||||
return Promise.resolve() // 可选字段,允许为空
|
||||
}
|
||||
if (typeof value === 'number' && value < 1) {
|
||||
return Promise.reject(new Error('最小金额必须 >= 1'))
|
||||
return Promise.reject(new Error(t('templateEdit.minOrderSizeError') || '最小金额必须 >= 1'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
@@ -248,29 +254,29 @@ const TemplateEdit: React.FC = () => {
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="仅在比例模式下生效,必须 >= 1(可选)"
|
||||
placeholder={t('templateEdit.minOrderSizePlaceholder') || '仅在比例模式下生效,必须 >= 1(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label="每日最大跟单订单数"
|
||||
label={t('templateEdit.maxDailyOrders') || '每日最大跟单订单数'}
|
||||
name="maxDailyOrders"
|
||||
tooltip="限制每日最多跟单的订单数量,用于风险控制,防止过度交易。例如:设置为 50,当日跟单订单数达到 50 后,停止跟单,次日重置。"
|
||||
tooltip={t('templateEdit.maxDailyOrdersTooltip') || '限制每日最多跟单的订单数量,用于风险控制,防止过度交易。例如:设置为 50,当日跟单订单数达到 50 后,停止跟单,次日重置。'}
|
||||
>
|
||||
<InputNumber
|
||||
min={1}
|
||||
step={1}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="默认 100(可选)"
|
||||
placeholder={t('templateEdit.maxDailyOrdersPlaceholder') || '默认 100(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="价格容忍度 (%)"
|
||||
label={t('templateEdit.priceTolerance') || '价格容忍度 (%)'}
|
||||
name="priceTolerance"
|
||||
tooltip="允许跟单价格在 Leader 价格基础上的调整范围,用于在 Leader 价格 ± 容忍度范围内调整价格,提高成交率。例如:设置为 5%,Leader 价格为 0.5,则跟单价格可在 0.475-0.525 范围内。"
|
||||
tooltip={t('templateEdit.priceToleranceTooltip') || '允许跟单价格在 Leader 价格基础上的调整范围,用于在 Leader 价格 ± 容忍度范围内调整价格,提高成交率。例如:设置为 5%,Leader 价格为 0.5,则跟单价格可在 0.475-0.525 范围内。'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
@@ -278,14 +284,57 @@ const TemplateEdit: React.FC = () => {
|
||||
step={0.1}
|
||||
precision={2}
|
||||
style={{ width: '100%' }}
|
||||
placeholder="默认 5%(可选)"
|
||||
placeholder={t('templateEdit.priceTolerancePlaceholder') || '默认 5%(可选)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="跟单卖出"
|
||||
label={t('templateEdit.minOrderDepth') || '最小订单深度 (USDC)'}
|
||||
name="minOrderDepth"
|
||||
tooltip={t('templateEdit.minOrderDepthTooltip') || '最小订单深度(USDC金额),NULL表示不启用此过滤。确保市场有足够的流动性'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('templateEdit.minOrderDepthPlaceholder') || '例如:100(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('templateEdit.maxSpread') || '最大价差(绝对价格)'}
|
||||
name="maxSpread"
|
||||
tooltip={t('templateEdit.maxSpreadTooltip') || '最大价差(绝对价格),NULL表示不启用此过滤。避免在价差过大的市场跟单'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('templateEdit.maxSpreadPlaceholder') || '例如:0.05(5美分,可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={t('templateEdit.minOrderbookDepth') || '最小订单簿深度 (USDC)'}
|
||||
name="minOrderbookDepth"
|
||||
tooltip={t('templateEdit.minOrderbookDepthTooltip') || '最小订单簿深度(USDC金额),NULL表示不启用此过滤。检查前 N 档的深度'}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('templateEdit.minOrderbookDepthPlaceholder') || '例如:50(可选,不填写表示不启用)'}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 跟单卖出 - 表单最底部 */}
|
||||
<Form.Item
|
||||
label={t('templateEdit.supportSell') || '跟单卖出'}
|
||||
name="supportSell"
|
||||
tooltip="是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。"
|
||||
tooltip={t('templateEdit.supportSellTooltip') || '是否跟单 Leader 的卖出订单。开启:跟单 Leader 的买入和卖出订单;关闭:只跟单 Leader 的买入订单,忽略卖出订单。'}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
@@ -304,10 +353,10 @@ const TemplateEdit: React.FC = () => {
|
||||
loading={loading}
|
||||
disabled={hasErrors}
|
||||
>
|
||||
保存修改
|
||||
{t('templateEdit.save') || '保存修改'}
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/templates')}>
|
||||
取消
|
||||
{t('common.cancel') || '取消'}
|
||||
</Button>
|
||||
</Space>
|
||||
)
|
||||
|
||||
@@ -181,12 +181,6 @@ const TemplateList: React.FC = () => {
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('templateList.useCount') || '使用次数',
|
||||
dataIndex: 'useCount',
|
||||
key: 'useCount',
|
||||
render: (count: number) => <Tag>{count}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('common.createdAt') || '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
@@ -321,7 +315,6 @@ const TemplateList: React.FC = () => {
|
||||
<Tag color={template.supportSell ? 'green' : 'red'}>
|
||||
{template.supportSell ? (t('templateList.supportSell') || '跟单卖出') : (t('templateList.notSupportSell') || '不跟单卖出')}
|
||||
</Tag>
|
||||
<Tag>{template.useCount} {t('templateList.timesUsed') || '次使用'}</Tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -359,19 +359,28 @@ export const apiService = {
|
||||
*/
|
||||
copyTrading: {
|
||||
/**
|
||||
* 创建跟单
|
||||
* 创建跟单配置
|
||||
* 支持两种方式:
|
||||
* 1. 提供 templateId:从模板填充配置,可以覆盖部分字段
|
||||
* 2. 不提供 templateId:手动输入所有配置参数
|
||||
*/
|
||||
create: (data: { accountId: number; templateId: number; leaderId: number; enabled?: boolean }) =>
|
||||
create: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/create', data),
|
||||
|
||||
/**
|
||||
* 更新跟单配置
|
||||
*/
|
||||
update: (data: any) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/update', data),
|
||||
|
||||
/**
|
||||
* 查询跟单列表
|
||||
*/
|
||||
list: (data: { accountId?: number; templateId?: number; leaderId?: number; enabled?: boolean } = {}) =>
|
||||
list: (data: { accountId?: number; leaderId?: number; enabled?: boolean } = {}) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/list', data),
|
||||
|
||||
/**
|
||||
* 更新跟单状态
|
||||
* 更新跟单状态(兼容旧接口)
|
||||
*/
|
||||
updateStatus: (data: { copyTradingId: number; enabled: boolean }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/update-status', data),
|
||||
@@ -383,10 +392,23 @@ export const apiService = {
|
||||
apiClient.post<ApiResponse<void>>('/copy-trading/delete', data),
|
||||
|
||||
/**
|
||||
* 查询钱包绑定的模板
|
||||
* 查询钱包绑定的跟单配置(兼容旧接口)
|
||||
*/
|
||||
getAccountTemplates: (data: { accountId: number }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/account-templates', data)
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/account-templates', data),
|
||||
|
||||
/**
|
||||
* 查询被过滤订单列表
|
||||
*/
|
||||
getFilteredOrders: (data: {
|
||||
copyTradingId: number
|
||||
filterType?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
}) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/filtered-orders', data)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+118
-5
@@ -107,7 +107,10 @@ export interface CopyTradingTemplate {
|
||||
maxDailyOrders: number
|
||||
priceTolerance: string
|
||||
supportSell: boolean
|
||||
useCount: number
|
||||
// 过滤条件
|
||||
minOrderDepth?: string
|
||||
maxSpread?: string
|
||||
minOrderbookDepth?: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
@@ -168,19 +171,36 @@ export interface TemplateCopyRequest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟单关系(钱包-模板关联)
|
||||
* 跟单配置(独立配置,不再绑定模板)
|
||||
*/
|
||||
export interface CopyTrading {
|
||||
id: number
|
||||
accountId: number
|
||||
accountName?: string
|
||||
walletAddress: string
|
||||
templateId: number
|
||||
templateName: string
|
||||
leaderId: number
|
||||
leaderName?: string
|
||||
leaderAddress: string
|
||||
enabled: boolean
|
||||
// 跟单配置参数
|
||||
copyMode: 'RATIO' | 'FIXED'
|
||||
copyRatio: string
|
||||
fixedAmount?: string
|
||||
maxOrderSize: string
|
||||
minOrderSize: string
|
||||
maxDailyLoss: string
|
||||
maxDailyOrders: number
|
||||
priceTolerance: string
|
||||
delaySeconds: number
|
||||
pollIntervalSeconds: number
|
||||
useWebSocket: boolean
|
||||
websocketReconnectInterval: number
|
||||
websocketMaxRetries: number
|
||||
supportSell: boolean
|
||||
// 过滤条件
|
||||
minOrderDepth?: string
|
||||
maxSpread?: string
|
||||
minOrderbookDepth?: string
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
@@ -195,12 +215,58 @@ export interface CopyTradingListResponse {
|
||||
|
||||
/**
|
||||
* 跟单创建请求
|
||||
* 所有配置参数都需要手动输入,模板仅用于前端快速填充表单
|
||||
*/
|
||||
export interface CopyTradingCreateRequest {
|
||||
accountId: number
|
||||
templateId: number
|
||||
leaderId: number
|
||||
enabled?: boolean
|
||||
// 跟单配置参数
|
||||
copyMode?: 'RATIO' | 'FIXED'
|
||||
copyRatio?: string
|
||||
fixedAmount?: string
|
||||
maxOrderSize?: string
|
||||
minOrderSize?: string
|
||||
maxDailyLoss?: string
|
||||
maxDailyOrders?: number
|
||||
priceTolerance?: string
|
||||
delaySeconds?: number
|
||||
pollIntervalSeconds?: number
|
||||
useWebSocket?: boolean
|
||||
websocketReconnectInterval?: number
|
||||
websocketMaxRetries?: number
|
||||
supportSell?: boolean
|
||||
// 过滤条件
|
||||
minOrderDepth?: string
|
||||
maxSpread?: string
|
||||
minOrderbookDepth?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 跟单更新请求
|
||||
*/
|
||||
export interface CopyTradingUpdateRequest {
|
||||
copyTradingId: number
|
||||
enabled?: boolean
|
||||
// 跟单配置参数(可选,只更新提供的字段)
|
||||
copyMode?: 'RATIO' | 'FIXED'
|
||||
copyRatio?: string
|
||||
fixedAmount?: string
|
||||
maxOrderSize?: string
|
||||
minOrderSize?: string
|
||||
maxDailyLoss?: string
|
||||
maxDailyOrders?: number
|
||||
priceTolerance?: string
|
||||
delaySeconds?: number
|
||||
pollIntervalSeconds?: number
|
||||
useWebSocket?: boolean
|
||||
websocketReconnectInterval?: number
|
||||
websocketMaxRetries?: number
|
||||
supportSell?: boolean
|
||||
// 过滤条件
|
||||
minOrderDepth?: string
|
||||
maxSpread?: string
|
||||
minOrderbookDepth?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -600,6 +666,53 @@ export interface OrderTrackingRequest {
|
||||
buyOrderId?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 被过滤订单信息
|
||||
*/
|
||||
export interface FilteredOrder {
|
||||
id: number
|
||||
copyTradingId: number
|
||||
accountId: number
|
||||
accountName?: string
|
||||
leaderId: number
|
||||
leaderName?: string
|
||||
leaderTradeId: string
|
||||
marketId: string
|
||||
marketTitle?: string
|
||||
marketSlug?: string
|
||||
side: 'BUY' | 'SELL'
|
||||
outcomeIndex?: number
|
||||
outcome?: string
|
||||
price: string
|
||||
size: string
|
||||
calculatedQuantity?: string
|
||||
filterReason: string
|
||||
filterType: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 被过滤订单列表请求
|
||||
*/
|
||||
export interface FilteredOrderListRequest {
|
||||
copyTradingId: number
|
||||
filterType?: string
|
||||
page?: number
|
||||
limit?: number
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 被过滤订单列表响应
|
||||
*/
|
||||
export interface FilteredOrderListResponse {
|
||||
list: FilteredOrder[]
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息推送配置
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user