feat: 实现跟单筛选条件记录和展示功能

- 新增 FilteredOrder 实体和数据库表,记录被过滤的订单信息
- 在筛选失败时自动记录到数据库并发送 Telegram 通知
- 创建 FilteredOrderService 和 API 接口,支持查询被过滤订单列表
- 前端新增被过滤订单列表页面,支持按过滤类型筛选
- 修复价差计算逻辑,使用数组最大值和最小值而非第一个元素
- 优化编辑页面 UI,钱包和 Leader 显示与创建页面一致(只读)
- 添加多语言支持(中文、繁体中文、英文)
This commit is contained in:
WrBug
2025-12-05 05:18:20 +08:00
parent e68309ada0
commit 9b8150cf92
33 changed files with 3801 additions and 549 deletions
@@ -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()
)
@@ -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
}
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?
)
}
@@ -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,
@@ -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)
}
}
}
@@ -184,6 +184,193 @@ class TelegramNotificationService(
sendMessage(message)
}
/**
* 发送订单被过滤通知
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
*/
suspend fun sendOrderFilteredNotification(
marketTitle: String,
marketId: String? = null, // 市场IDconditionId),用于生成链接
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 × sizeUSDC
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("<", "&lt;").replace(">", "&gt;")
val escapedAccountInfo = accountInfo.replace("<", "&lt;").replace(">", "&gt;")
val escapedFilterReason = filterReason.replace("<", "&lt;").replace(">", "&gt;")
// 格式化金额显示
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("<", "&lt;").replace(">", "&gt;")
"\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='被过滤订单表';
+4
View File
@@ -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>} />
+288 -7
View File
@@ -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",
+247
View File
@@ -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",
+288 -7
View File
@@ -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",
+361 -43
View File
@@ -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
+432
View File
@@ -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
+48 -44
View File
@@ -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'}>
+239
View File
@@ -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
+91 -43
View File
@@ -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>
)
+98 -49
View File
@@ -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>
)
-7
View File
@@ -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>
+28 -6
View File
@@ -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
View File
@@ -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
}
/**
* 消息推送配置
*/