feat(尾盘策略): 触发记录分页与时间筛选、策略列表与表单优化
触发记录: - 成功/失败分 Tab,失败展示失败原因 - 分页与按天时间范围筛选(日历,结束日不超过今天) - 后端 startDate/endDate、Repository 时间范围查询 - 弹窗美化:时间范围卡片、空状态、分页总数;移除订单 ID 列 - 查看触发记录按钮文案改为「订单」 策略列表: - 状态列恢复为 Switch,PC 端放在第一列 - 新增账户列;移除最近触发列 - 文案换行(wordBreak/whiteSpace);时间区间 PC 换行、移动端单行 - 时间区间列宽缩小;移动端 Switch 在底部操作区 - 编辑时账户不可修改;策略名称 placeholder 改为「选填,留空将自动生成」 - 多语言:account、viewTriggers、strategyNamePlaceholder 等 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -93,12 +93,16 @@ data class CryptoTailStrategyDeleteRequest(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发记录列表请求
|
* 触发记录列表请求
|
||||||
|
* @param startDate 开始日期(当天 00:00:00.000 的时间戳毫秒),为 null 表示不限制
|
||||||
|
* @param endDate 结束日期(当天 23:59:59.999 的时间戳毫秒),为 null 表示不限制
|
||||||
*/
|
*/
|
||||||
data class CryptoTailStrategyTriggerListRequest(
|
data class CryptoTailStrategyTriggerListRequest(
|
||||||
val strategyId: Long = 0L,
|
val strategyId: Long = 0L,
|
||||||
val page: Int = 1,
|
val page: Int = 1,
|
||||||
val pageSize: Int = 20,
|
val pageSize: Int = 20,
|
||||||
val status: String? = null
|
val status: String? = null,
|
||||||
|
val startDate: Long? = null,
|
||||||
|
val endDate: Long? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+5
@@ -15,6 +15,11 @@ interface CryptoTailStrategyTriggerRepository : JpaRepository<CryptoTailStrategy
|
|||||||
fun findAllByStrategyIdAndStatusOrderByCreatedAtDesc(strategyId: Long, status: String, pageable: Pageable): Page<CryptoTailStrategyTrigger>
|
fun findAllByStrategyIdAndStatusOrderByCreatedAtDesc(strategyId: Long, status: String, pageable: Pageable): Page<CryptoTailStrategyTrigger>
|
||||||
fun countByStrategyIdAndStatus(strategyId: Long, status: String): Long
|
fun countByStrategyIdAndStatus(strategyId: Long, status: String): Long
|
||||||
|
|
||||||
|
fun findAllByStrategyIdAndCreatedAtBetweenOrderByCreatedAtDesc(strategyId: Long, startInclusive: Long, endInclusive: Long, pageable: Pageable): Page<CryptoTailStrategyTrigger>
|
||||||
|
fun findAllByStrategyIdAndStatusAndCreatedAtBetweenOrderByCreatedAtDesc(strategyId: Long, status: String, startInclusive: Long, endInclusive: Long, pageable: Pageable): Page<CryptoTailStrategyTrigger>
|
||||||
|
fun countByStrategyIdAndCreatedAtBetween(strategyId: Long, startInclusive: Long, endInclusive: Long): Long
|
||||||
|
fun countByStrategyIdAndStatusAndCreatedAtBetween(strategyId: Long, status: String, startInclusive: Long, endInclusive: Long): Long
|
||||||
|
|
||||||
/** 轮询结算:仅处理下单成功的订单(status=success 且 orderId 非空)、且未结算的触发记录 */
|
/** 轮询结算:仅处理下单成功的订单(status=success 且 orderId 非空)、且未结算的触发记录 */
|
||||||
fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
|
fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
|
||||||
|
|
||||||
|
|||||||
+25
-8
@@ -199,16 +199,33 @@ class CryptoTailStrategyService(
|
|||||||
fun getTriggerRecords(request: CryptoTailStrategyTriggerListRequest): Result<CryptoTailStrategyTriggerListResponse> {
|
fun getTriggerRecords(request: CryptoTailStrategyTriggerListRequest): Result<CryptoTailStrategyTriggerListResponse> {
|
||||||
return try {
|
return try {
|
||||||
val page = PageRequest.of((request.page - 1).coerceAtLeast(0), request.pageSize.coerceIn(1, 100))
|
val page = PageRequest.of((request.page - 1).coerceAtLeast(0), request.pageSize.coerceIn(1, 100))
|
||||||
val pageResult = if (request.status != null && request.status.isNotBlank()) {
|
val startTs = request.startDate ?: 0L
|
||||||
triggerRepository.findAllByStrategyIdAndStatusOrderByCreatedAtDesc(request.strategyId, request.status, page)
|
val endTs = request.endDate ?: Long.MAX_VALUE
|
||||||
} else {
|
val useTimeRange = request.startDate != null || request.endDate != null
|
||||||
triggerRepository.findAllByStrategyIdOrderByCreatedAtDesc(request.strategyId, page)
|
val pageResult = when {
|
||||||
|
useTimeRange && request.status != null && request.status.isNotBlank() ->
|
||||||
|
triggerRepository.findAllByStrategyIdAndStatusAndCreatedAtBetweenOrderByCreatedAtDesc(
|
||||||
|
request.strategyId, request.status, startTs, endTs, page
|
||||||
|
)
|
||||||
|
useTimeRange ->
|
||||||
|
triggerRepository.findAllByStrategyIdAndCreatedAtBetweenOrderByCreatedAtDesc(
|
||||||
|
request.strategyId, startTs, endTs, page
|
||||||
|
)
|
||||||
|
request.status != null && request.status.isNotBlank() ->
|
||||||
|
triggerRepository.findAllByStrategyIdAndStatusOrderByCreatedAtDesc(request.strategyId, request.status, page)
|
||||||
|
else ->
|
||||||
|
triggerRepository.findAllByStrategyIdOrderByCreatedAtDesc(request.strategyId, page)
|
||||||
}
|
}
|
||||||
val list = pageResult.content.map { triggerToDto(it) }
|
val list = pageResult.content.map { triggerToDto(it) }
|
||||||
val total = if (request.status != null && request.status.isNotBlank()) {
|
val total = when {
|
||||||
triggerRepository.countByStrategyIdAndStatus(request.strategyId, request.status)
|
useTimeRange && request.status != null && request.status.isNotBlank() ->
|
||||||
} else {
|
triggerRepository.countByStrategyIdAndStatusAndCreatedAtBetween(request.strategyId, request.status, startTs, endTs)
|
||||||
pageResult.totalElements
|
useTimeRange ->
|
||||||
|
triggerRepository.countByStrategyIdAndCreatedAtBetween(request.strategyId, startTs, endTs)
|
||||||
|
request.status != null && request.status.isNotBlank() ->
|
||||||
|
triggerRepository.countByStrategyIdAndStatus(request.strategyId, request.status)
|
||||||
|
else ->
|
||||||
|
pageResult.totalElements
|
||||||
}
|
}
|
||||||
Result.success(CryptoTailStrategyTriggerListResponse(list = list, total = total))
|
Result.success(CryptoTailStrategyTriggerListResponse(list = list, total = total))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -1412,13 +1412,13 @@
|
|||||||
"walletTip": "Use a dedicated wallet for tail strategy. Do not use it for manual trading or copy trading to avoid balance/position issues.",
|
"walletTip": "Use a dedicated wallet for tail strategy. Do not use it for manual trading or copy trading to avoid balance/position issues.",
|
||||||
"addStrategy": "Add Strategy",
|
"addStrategy": "Add Strategy",
|
||||||
"strategyName": "Strategy Name",
|
"strategyName": "Strategy Name",
|
||||||
|
"account": "Account",
|
||||||
"market": "Market",
|
"market": "Market",
|
||||||
"timeWindow": "Time Window",
|
"timeWindow": "Time Window",
|
||||||
"priceRange": "Price Range",
|
"priceRange": "Price Range",
|
||||||
"amountMode": "Amount Mode",
|
"amountMode": "Amount Mode",
|
||||||
"ratio": "Ratio",
|
"ratio": "Ratio",
|
||||||
"fixed": "Fixed",
|
"fixed": "Fixed",
|
||||||
"recentTrigger": "Last Trigger",
|
|
||||||
"totalRealizedPnl": "Total PnL",
|
"totalRealizedPnl": "Total PnL",
|
||||||
"winRate": "Win Rate",
|
"winRate": "Win Rate",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
@@ -1426,14 +1426,14 @@
|
|||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"disable": "Disable",
|
"disable": "Disable",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"viewTriggers": "Trigger Records",
|
"viewTriggers": "Orders",
|
||||||
"deleteConfirm": "Delete this strategy?",
|
"deleteConfirm": "Delete this strategy?",
|
||||||
"fetchFailed": "Failed to fetch list"
|
"fetchFailed": "Failed to fetch list"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"walletTip": "Use a dedicated wallet to avoid balance or order issues.",
|
"walletTip": "Use a dedicated wallet to avoid balance or order issues.",
|
||||||
"strategyName": "Strategy Name",
|
"strategyName": "Strategy Name",
|
||||||
"strategyNamePlaceholder": "Auto",
|
"strategyNamePlaceholder": "Optional, leave blank to auto-generate",
|
||||||
"selectAccount": "Select Account",
|
"selectAccount": "Select Account",
|
||||||
"selectMarket": "Select Market",
|
"selectMarket": "Select Market",
|
||||||
"timeWindowStart": "Window Start",
|
"timeWindowStart": "Window Start",
|
||||||
@@ -1467,6 +1467,11 @@
|
|||||||
},
|
},
|
||||||
"triggerRecords": {
|
"triggerRecords": {
|
||||||
"title": "Trigger Records",
|
"title": "Trigger Records",
|
||||||
|
"timeRange": "Time Range",
|
||||||
|
"startDate": "Start Date",
|
||||||
|
"endDate": "End Date",
|
||||||
|
"successTab": "Success",
|
||||||
|
"failTab": "Failed",
|
||||||
"triggerTime": "Time",
|
"triggerTime": "Time",
|
||||||
"market": "Market",
|
"market": "Market",
|
||||||
"direction": "Direction",
|
"direction": "Direction",
|
||||||
@@ -1478,8 +1483,12 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"fail": "Fail",
|
"fail": "Fail",
|
||||||
|
"failReason": "Failure Reason",
|
||||||
"realizedPnl": "PnL",
|
"realizedPnl": "PnL",
|
||||||
"resolved": "Settled"
|
"resolved": "Settled",
|
||||||
|
"emptySuccess": "No success records",
|
||||||
|
"emptyFail": "No failed records",
|
||||||
|
"totalCount": "{count} record(s)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1411,13 +1411,13 @@
|
|||||||
"walletTip": "请使用单独的钱包运行尾盘策略,避免该钱包用于手动交易、跟单等其他操作,否则可能导致余额或仓位变化,造成策略执行异常。",
|
"walletTip": "请使用单独的钱包运行尾盘策略,避免该钱包用于手动交易、跟单等其他操作,否则可能导致余额或仓位变化,造成策略执行异常。",
|
||||||
"addStrategy": "新增策略",
|
"addStrategy": "新增策略",
|
||||||
"strategyName": "策略名称",
|
"strategyName": "策略名称",
|
||||||
|
"account": "账户",
|
||||||
"market": "关联市场",
|
"market": "关联市场",
|
||||||
"timeWindow": "时间区间",
|
"timeWindow": "时间区间",
|
||||||
"priceRange": "价格区间",
|
"priceRange": "价格区间",
|
||||||
"amountMode": "投入方式",
|
"amountMode": "投入方式",
|
||||||
"ratio": "比例",
|
"ratio": "比例",
|
||||||
"fixed": "固定金额",
|
"fixed": "固定金额",
|
||||||
"recentTrigger": "最近触发",
|
|
||||||
"totalRealizedPnl": "总收益",
|
"totalRealizedPnl": "总收益",
|
||||||
"winRate": "胜率",
|
"winRate": "胜率",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
@@ -1425,14 +1425,14 @@
|
|||||||
"enable": "启用",
|
"enable": "启用",
|
||||||
"disable": "停用",
|
"disable": "停用",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"viewTriggers": "查看触发记录",
|
"viewTriggers": "订单",
|
||||||
"deleteConfirm": "确定删除该策略?",
|
"deleteConfirm": "确定删除该策略?",
|
||||||
"fetchFailed": "获取列表失败"
|
"fetchFailed": "获取列表失败"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"walletTip": "建议使用专用钱包,避免手动操作等导致余额或下单异常。",
|
"walletTip": "建议使用专用钱包,避免手动操作等导致余额或下单异常。",
|
||||||
"strategyName": "策略名称",
|
"strategyName": "策略名称",
|
||||||
"strategyNamePlaceholder": "自动生成",
|
"strategyNamePlaceholder": "选填,留空将自动生成",
|
||||||
"selectAccount": "选择账户",
|
"selectAccount": "选择账户",
|
||||||
"selectMarket": "选择市场",
|
"selectMarket": "选择市场",
|
||||||
"timeWindowStart": "区间开始",
|
"timeWindowStart": "区间开始",
|
||||||
@@ -1466,6 +1466,11 @@
|
|||||||
},
|
},
|
||||||
"triggerRecords": {
|
"triggerRecords": {
|
||||||
"title": "触发记录",
|
"title": "触发记录",
|
||||||
|
"timeRange": "时间范围",
|
||||||
|
"startDate": "开始日期",
|
||||||
|
"endDate": "结束日期",
|
||||||
|
"successTab": "成功",
|
||||||
|
"failTab": "失败",
|
||||||
"triggerTime": "触发时间",
|
"triggerTime": "触发时间",
|
||||||
"market": "市场",
|
"market": "市场",
|
||||||
"direction": "方向",
|
"direction": "方向",
|
||||||
@@ -1477,8 +1482,12 @@
|
|||||||
"status": "状态",
|
"status": "状态",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"fail": "失败",
|
"fail": "失败",
|
||||||
|
"failReason": "失败原因",
|
||||||
"realizedPnl": "收益",
|
"realizedPnl": "收益",
|
||||||
"resolved": "已结算"
|
"resolved": "已结算",
|
||||||
|
"emptySuccess": "暂无成功记录",
|
||||||
|
"emptyFail": "暂无失败记录",
|
||||||
|
"totalCount": "共 {count} 条"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1412,13 +1412,13 @@
|
|||||||
"walletTip": "請使用單獨的錢包運行尾盤策略,避免該錢包用於手動交易、跟單等其他操作,否則可能導致餘額或倉位變化,造成策略執行異常。",
|
"walletTip": "請使用單獨的錢包運行尾盤策略,避免該錢包用於手動交易、跟單等其他操作,否則可能導致餘額或倉位變化,造成策略執行異常。",
|
||||||
"addStrategy": "新增策略",
|
"addStrategy": "新增策略",
|
||||||
"strategyName": "策略名稱",
|
"strategyName": "策略名稱",
|
||||||
|
"account": "賬戶",
|
||||||
"market": "關聯市場",
|
"market": "關聯市場",
|
||||||
"timeWindow": "時間區間",
|
"timeWindow": "時間區間",
|
||||||
"priceRange": "價格區間",
|
"priceRange": "價格區間",
|
||||||
"amountMode": "投入方式",
|
"amountMode": "投入方式",
|
||||||
"ratio": "比例",
|
"ratio": "比例",
|
||||||
"fixed": "固定金額",
|
"fixed": "固定金額",
|
||||||
"recentTrigger": "最近觸發",
|
|
||||||
"totalRealizedPnl": "總收益",
|
"totalRealizedPnl": "總收益",
|
||||||
"winRate": "勝率",
|
"winRate": "勝率",
|
||||||
"actions": "操作",
|
"actions": "操作",
|
||||||
@@ -1426,14 +1426,14 @@
|
|||||||
"enable": "啟用",
|
"enable": "啟用",
|
||||||
"disable": "停用",
|
"disable": "停用",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"viewTriggers": "查看觸發記錄",
|
"viewTriggers": "訂單",
|
||||||
"deleteConfirm": "確定刪除該策略?",
|
"deleteConfirm": "確定刪除該策略?",
|
||||||
"fetchFailed": "獲取列表失敗"
|
"fetchFailed": "獲取列表失敗"
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"walletTip": "建議使用專用錢包,避免手動操作等導致餘額或下單異常。",
|
"walletTip": "建議使用專用錢包,避免手動操作等導致餘額或下單異常。",
|
||||||
"strategyName": "策略名稱",
|
"strategyName": "策略名稱",
|
||||||
"strategyNamePlaceholder": "自動生成",
|
"strategyNamePlaceholder": "選填,留空將自動生成",
|
||||||
"selectAccount": "選擇賬戶",
|
"selectAccount": "選擇賬戶",
|
||||||
"selectMarket": "選擇市場",
|
"selectMarket": "選擇市場",
|
||||||
"timeWindowStart": "區間開始",
|
"timeWindowStart": "區間開始",
|
||||||
@@ -1467,6 +1467,11 @@
|
|||||||
},
|
},
|
||||||
"triggerRecords": {
|
"triggerRecords": {
|
||||||
"title": "觸發記錄",
|
"title": "觸發記錄",
|
||||||
|
"timeRange": "時間範圍",
|
||||||
|
"startDate": "開始日期",
|
||||||
|
"endDate": "結束日期",
|
||||||
|
"successTab": "成功",
|
||||||
|
"failTab": "失敗",
|
||||||
"triggerTime": "觸發時間",
|
"triggerTime": "觸發時間",
|
||||||
"market": "市場",
|
"market": "市場",
|
||||||
"direction": "方向",
|
"direction": "方向",
|
||||||
@@ -1478,8 +1483,12 @@
|
|||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"fail": "失敗",
|
"fail": "失敗",
|
||||||
|
"failReason": "失敗原因",
|
||||||
"realizedPnl": "收益",
|
"realizedPnl": "收益",
|
||||||
"resolved": "已結算"
|
"resolved": "已結算",
|
||||||
|
"emptySuccess": "暫無成功記錄",
|
||||||
|
"emptyFail": "暫無失敗記錄",
|
||||||
|
"totalCount": "共 {count} 條"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,9 +17,16 @@ import {
|
|||||||
InputNumber,
|
InputNumber,
|
||||||
Radio,
|
Radio,
|
||||||
Spin,
|
Spin,
|
||||||
Tooltip
|
Tooltip,
|
||||||
|
Tabs,
|
||||||
|
DatePicker,
|
||||||
|
Empty,
|
||||||
|
Typography,
|
||||||
|
Divider
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import { PlusOutlined, EditOutlined, UnorderedListOutlined, InfoCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
import type { Dayjs } from 'dayjs'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { PlusOutlined, EditOutlined, UnorderedListOutlined, InfoCircleOutlined, WarningOutlined, CalendarOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useMediaQuery } from 'react-responsive'
|
import { useMediaQuery } from 'react-responsive'
|
||||||
import { apiService } from '../services/api'
|
import { apiService } from '../services/api'
|
||||||
@@ -41,9 +48,13 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
const [editingId, setEditingId] = useState<number | null>(null)
|
const [editingId, setEditingId] = useState<number | null>(null)
|
||||||
const [marketOptions, setMarketOptions] = useState<CryptoTailMarketOptionDto[]>([])
|
const [marketOptions, setMarketOptions] = useState<CryptoTailMarketOptionDto[]>([])
|
||||||
const [triggersModalOpen, setTriggersModalOpen] = useState(false)
|
const [triggersModalOpen, setTriggersModalOpen] = useState(false)
|
||||||
const [, setTriggersStrategyId] = useState<number | null>(null)
|
const [triggersStrategyId, setTriggersStrategyId] = useState<number | null>(null)
|
||||||
|
const [triggerTab, setTriggerTab] = useState<'success' | 'fail'>('success')
|
||||||
|
const [triggerDateRange, setTriggerDateRange] = useState<[Dayjs | null, Dayjs | null]>([null, null])
|
||||||
|
const [triggerPage, setTriggerPage] = useState(1)
|
||||||
|
const [triggerPageSize, setTriggerPageSize] = useState(20)
|
||||||
const [triggers, setTriggers] = useState<CryptoTailStrategyTriggerDto[]>([])
|
const [triggers, setTriggers] = useState<CryptoTailStrategyTriggerDto[]>([])
|
||||||
const [, setTriggersTotal] = useState(0)
|
const [triggersTotal, setTriggersTotal] = useState(0)
|
||||||
const [triggersLoading, setTriggersLoading] = useState(false)
|
const [triggersLoading, setTriggersLoading] = useState(false)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
@@ -269,12 +280,30 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openTriggers = async (strategyId: number) => {
|
type TriggerLoadOpts = { page?: number; pageSize?: number; dateRange?: [Dayjs | null, Dayjs | null] }
|
||||||
setTriggersStrategyId(strategyId)
|
|
||||||
setTriggersModalOpen(true)
|
const loadTriggerRecords = async (
|
||||||
|
strategyId: number,
|
||||||
|
status: 'success' | 'fail',
|
||||||
|
opts?: TriggerLoadOpts
|
||||||
|
) => {
|
||||||
|
const page = opts?.page ?? triggerPage
|
||||||
|
const pageSize = opts?.pageSize ?? triggerPageSize
|
||||||
|
const range = opts?.dateRange ?? triggerDateRange
|
||||||
|
const startDate =
|
||||||
|
range[0] != null ? range[0].startOf('day').valueOf() : undefined
|
||||||
|
const endDate =
|
||||||
|
range[1] != null ? range[1].endOf('day').valueOf() : undefined
|
||||||
setTriggersLoading(true)
|
setTriggersLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await apiService.cryptoTailStrategy.triggers({ strategyId, page: 1, pageSize: 50 })
|
const res = await apiService.cryptoTailStrategy.triggers({
|
||||||
|
strategyId,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
status,
|
||||||
|
startDate,
|
||||||
|
endDate
|
||||||
|
})
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
setTriggers(res.data.data.list ?? [])
|
setTriggers(res.data.data.list ?? [])
|
||||||
setTriggersTotal(res.data.data.total ?? 0)
|
setTriggersTotal(res.data.data.total ?? 0)
|
||||||
@@ -284,18 +313,52 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatTimeWindow = (startSec: number, endSec: number): string => {
|
const openTriggers = async (strategyId: number) => {
|
||||||
|
setTriggersStrategyId(strategyId)
|
||||||
|
setTriggerTab('success')
|
||||||
|
setTriggerDateRange([null, null])
|
||||||
|
setTriggerPage(1)
|
||||||
|
setTriggerPageSize(20)
|
||||||
|
setTriggersModalOpen(true)
|
||||||
|
await loadTriggerRecords(strategyId, 'success', { page: 1, pageSize: 20, dateRange: [null, null] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerTabChange = (key: string) => {
|
||||||
|
const next = key === 'success' ? 'success' : 'fail'
|
||||||
|
setTriggerTab(next)
|
||||||
|
setTriggers([])
|
||||||
|
setTriggerPage(1)
|
||||||
|
if (triggersStrategyId != null) {
|
||||||
|
loadTriggerRecords(triggersStrategyId, next, { page: 1 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerDateRangeChange = (dates: [Dayjs | null, Dayjs | null] | null) => {
|
||||||
|
const next = dates ?? [null, null]
|
||||||
|
setTriggerDateRange(next)
|
||||||
|
setTriggerPage(1)
|
||||||
|
if (triggersStrategyId != null) {
|
||||||
|
loadTriggerRecords(triggersStrategyId, triggerTab, { page: 1, dateRange: next })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onTriggerPageChange = (page: number, pageSize: number) => {
|
||||||
|
setTriggerPage(page)
|
||||||
|
setTriggerPageSize(pageSize)
|
||||||
|
if (triggersStrategyId != null) {
|
||||||
|
loadTriggerRecords(triggersStrategyId, triggerTab, { page, pageSize })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabledTriggerEndDate = (current: Dayjs) => current && current.isAfter(dayjs(), 'day')
|
||||||
|
|
||||||
|
const formatTimeWindow = (startSec: number, endSec: number, wrap = true): string => {
|
||||||
const sm = Math.floor(startSec / 60)
|
const sm = Math.floor(startSec / 60)
|
||||||
const ss = startSec % 60
|
const ss = startSec % 60
|
||||||
const em = Math.floor(endSec / 60)
|
const em = Math.floor(endSec / 60)
|
||||||
const es = endSec % 60
|
const es = endSec % 60
|
||||||
return `${sm} ${t('cryptoTailStrategy.form.minute')} ${ss} ${t('cryptoTailStrategy.form.second')} ~ ${em} ${t('cryptoTailStrategy.form.minute')} ${es} ${t('cryptoTailStrategy.form.second')}`
|
const sep = wrap ? '\n~ ' : ' ~ '
|
||||||
}
|
return `${sm} ${t('cryptoTailStrategy.form.minute')} ${ss} ${t('cryptoTailStrategy.form.second')}${sep}${em} ${t('cryptoTailStrategy.form.minute')} ${es} ${t('cryptoTailStrategy.form.second')}`
|
||||||
|
|
||||||
const formatLastTrigger = (ts?: number) => {
|
|
||||||
if (ts == null) return '-'
|
|
||||||
const d = new Date(ts)
|
|
||||||
return d.toLocaleString()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatPriceRange = (minPrice: string, maxPrice: string): string => {
|
const formatPriceRange = (minPrice: string, maxPrice: string): string => {
|
||||||
@@ -314,79 +377,110 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAccountLabel = (accountId: number) => accounts.find((a) => a.id === accountId)?.accountName || `#${accountId}`
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('common.status'),
|
||||||
|
dataIndex: 'enabled',
|
||||||
|
key: 'enabled',
|
||||||
|
width: 80,
|
||||||
|
render: (enabled: boolean, record: CryptoTailStrategyDto) => (
|
||||||
|
<Switch
|
||||||
|
checked={enabled}
|
||||||
|
onChange={() => handleToggle(record)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.strategyName'),
|
title: t('cryptoTailStrategy.list.strategyName'),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: isMobile ? 100 : 140,
|
width: isMobile ? 100 : 160,
|
||||||
render: (name: string | undefined, r: CryptoTailStrategyDto) => name || (r.marketTitle ?? r.marketSlugPrefix) || '-'
|
render: (name: string | undefined, r: CryptoTailStrategyDto) => (
|
||||||
|
<Typography.Text strong style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{name || (r.marketTitle ?? r.marketSlugPrefix) || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.list.account'),
|
||||||
|
dataIndex: 'accountId',
|
||||||
|
key: 'accountId',
|
||||||
|
width: isMobile ? 90 : 120,
|
||||||
|
render: (_: unknown, r: CryptoTailStrategyDto) => (
|
||||||
|
<Typography.Text type="secondary" style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{getAccountLabel(r.accountId)}
|
||||||
|
</Typography.Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.market'),
|
title: t('cryptoTailStrategy.list.market'),
|
||||||
key: 'market',
|
key: 'market',
|
||||||
width: isMobile ? 120 : 200,
|
width: isMobile ? 120 : 200,
|
||||||
render: (_: unknown, r: CryptoTailStrategyDto) =>
|
render: (_: unknown, r: CryptoTailStrategyDto) => (
|
||||||
marketOptions.find((m) => m.slug === r.marketSlugPrefix)?.title ?? r.marketTitle ?? r.marketSlugPrefix ?? '-'
|
<Typography.Text style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{marketOptions.find((m) => m.slug === r.marketSlugPrefix)?.title ?? r.marketTitle ?? r.marketSlugPrefix ?? '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.timeWindow'),
|
title: t('cryptoTailStrategy.list.timeWindow'),
|
||||||
key: 'timeWindow',
|
key: 'timeWindow',
|
||||||
width: isMobile ? 140 : 180,
|
width: isMobile ? 100 : 120,
|
||||||
render: (_: unknown, r: CryptoTailStrategyDto) => formatTimeWindow(r.windowStartSeconds, r.windowEndSeconds)
|
render: (_: unknown, r: CryptoTailStrategyDto) => (
|
||||||
|
<Typography.Text type="secondary" style={{ wordBreak: 'break-word', whiteSpace: 'pre-line' }}>
|
||||||
|
{formatTimeWindow(r.windowStartSeconds, r.windowEndSeconds)}
|
||||||
|
</Typography.Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.priceRange'),
|
title: t('cryptoTailStrategy.list.priceRange'),
|
||||||
key: 'priceRange',
|
key: 'priceRange',
|
||||||
width: isMobile ? 90 : 120,
|
width: isMobile ? 90 : 120,
|
||||||
render: (_: unknown, r: CryptoTailStrategyDto) => formatPriceRange(r.minPrice, r.maxPrice)
|
render: (_: unknown, r: CryptoTailStrategyDto) => (
|
||||||
|
<Typography.Text type="secondary" style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{formatPriceRange(r.minPrice, r.maxPrice)}
|
||||||
|
</Typography.Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.amountMode'),
|
title: t('cryptoTailStrategy.list.amountMode'),
|
||||||
key: 'amountMode',
|
key: 'amountMode',
|
||||||
width: isMobile ? 90 : 120,
|
width: isMobile ? 90 : 120,
|
||||||
render: (_: unknown, r: CryptoTailStrategyDto) =>
|
render: (_: unknown, r: CryptoTailStrategyDto) => (
|
||||||
(r.amountMode?.toUpperCase() ?? '') === 'RATIO'
|
<Typography.Text type="secondary" style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(r.amountValue, 2) || '0'}%`
|
{(r.amountMode?.toUpperCase() ?? '') === 'RATIO'
|
||||||
: `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(r.amountValue)} USDC`
|
? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(r.amountValue, 2) || '0'}%`
|
||||||
},
|
: `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(r.amountValue)} USDC`}
|
||||||
{
|
</Typography.Text>
|
||||||
title: t('common.status'),
|
|
||||||
dataIndex: 'enabled',
|
|
||||||
key: 'enabled',
|
|
||||||
width: 90,
|
|
||||||
render: (enabled: boolean, record: CryptoTailStrategyDto) => (
|
|
||||||
<Switch
|
|
||||||
checked={enabled}
|
|
||||||
onChange={() => handleToggle(record)}
|
|
||||||
checkedChildren={t('cryptoTailStrategy.list.enable')}
|
|
||||||
unCheckedChildren={t('cryptoTailStrategy.list.disable')}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('cryptoTailStrategy.list.recentTrigger'),
|
|
||||||
dataIndex: 'lastTriggerAt',
|
|
||||||
key: 'lastTriggerAt',
|
|
||||||
width: isMobile ? 100 : 160,
|
|
||||||
render: (ts: number | undefined) => formatLastTrigger(ts)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.totalRealizedPnl'),
|
title: t('cryptoTailStrategy.list.totalRealizedPnl'),
|
||||||
key: 'totalRealizedPnl',
|
key: 'totalRealizedPnl',
|
||||||
width: isMobile ? 90 : 110,
|
width: isMobile ? 90 : 120,
|
||||||
render: (_: unknown, r: CryptoTailStrategyDto) => {
|
render: (_: unknown, r: CryptoTailStrategyDto) => {
|
||||||
const text = r.totalRealizedPnl != null ? `${formatUSDC(r.totalRealizedPnl)} USDC` : '-'
|
const text = r.totalRealizedPnl != null ? `${formatUSDC(r.totalRealizedPnl)} USDC` : '-'
|
||||||
const color = pnlColor(r.totalRealizedPnl)
|
const color = pnlColor(r.totalRealizedPnl)
|
||||||
return color ? <span style={{ color }}>{text}</span> : text
|
return color ? (
|
||||||
|
<Typography.Text style={{ color, fontWeight: 500 }}>{text}</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">{text}</Typography.Text>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.winRate'),
|
title: t('cryptoTailStrategy.list.winRate'),
|
||||||
key: 'winRate',
|
key: 'winRate',
|
||||||
width: isMobile ? 70 : 80,
|
width: isMobile ? 70 : 90,
|
||||||
render: (_: unknown, r: CryptoTailStrategyDto) =>
|
render: (_: unknown, r: CryptoTailStrategyDto) =>
|
||||||
r.winRate != null ? `${(Number(r.winRate) * 100).toFixed(1)}%` : '-'
|
r.winRate != null ? (
|
||||||
|
<Tag color="blue">{(Number(r.winRate) * 100).toFixed(1)}%</Tag>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type="secondary">-</Typography.Text>
|
||||||
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('cryptoTailStrategy.list.actions'),
|
title: t('cryptoTailStrategy.list.actions'),
|
||||||
@@ -504,45 +598,71 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
{list.map((item) => (
|
{list.map((item) => (
|
||||||
<Card key={item.id} size="small">
|
<Card
|
||||||
<div style={{ marginBottom: 8 }}>
|
key={item.id}
|
||||||
<strong>{item.name || item.marketSlugPrefix || '-'}</strong>
|
size="small"
|
||||||
|
styles={{ body: { padding: 16 } }}
|
||||||
|
style={{ borderLeft: `3px solid ${item.enabled ? 'var(--ant-colorSuccess)' : 'var(--ant-colorBorder)'}` }}
|
||||||
|
>
|
||||||
|
<Typography.Text strong style={{ fontSize: 15, wordBreak: 'break-word', whiteSpace: 'normal', display: 'block', marginBottom: 8 }}>
|
||||||
|
{item.name || (item.marketTitle ?? item.marketSlugPrefix) || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 4, fontSize: 13, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{t('cryptoTailStrategy.list.account')}: {getAccountLabel(item.accountId)}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 8, fontSize: 13, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{marketOptions.find((m) => m.slug === item.marketSlugPrefix)?.title ?? item.marketSlugPrefix ?? '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '6px 16px', marginBottom: 12 }}>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{t('cryptoTailStrategy.list.timeWindow')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text style={{ fontSize: 12 }}>{formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds, false)}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{t('cryptoTailStrategy.list.priceRange')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>{formatPriceRange(item.minPrice, item.maxPrice)}</Typography.Text>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{t('cryptoTailStrategy.list.amountMode')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Typography.Text style={{ fontSize: 12, wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||||
|
{(item.amountMode?.toUpperCase() ?? '') === 'RATIO'
|
||||||
|
? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(item.amountValue, 2) || '0'}%`
|
||||||
|
: `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(item.amountValue)} USDC`}
|
||||||
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
{t('cryptoTailStrategy.list.timeWindow')}: {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds)}
|
<Space wrap style={{ marginBottom: 12 }}>
|
||||||
</div>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
{t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '}
|
||||||
{t('cryptoTailStrategy.list.priceRange')}: {formatPriceRange(item.minPrice, item.maxPrice)}
|
</Typography.Text>
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
|
||||||
{t('cryptoTailStrategy.list.amountMode')}:{' '}
|
|
||||||
{(item.amountMode?.toUpperCase() ?? '') === 'RATIO'
|
|
||||||
? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(item.amountValue, 2) || '0'}%`
|
|
||||||
: `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(item.amountValue)} USDC`}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>
|
|
||||||
{t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '}
|
|
||||||
{item.totalRealizedPnl != null ? (
|
{item.totalRealizedPnl != null ? (
|
||||||
<span style={{ color: pnlColor(item.totalRealizedPnl) ?? '#666' }}>
|
<Typography.Text style={{ color: pnlColor(item.totalRealizedPnl) ?? undefined, fontWeight: 500, fontSize: 12 }}>
|
||||||
{formatUSDC(item.totalRealizedPnl)} USDC
|
{formatUSDC(item.totalRealizedPnl)} USDC
|
||||||
</span>
|
</Typography.Text>
|
||||||
) : (
|
) : (
|
||||||
'-'
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>-</Typography.Text>
|
||||||
)}
|
)}
|
||||||
{item.winRate != null ? ` · ${t('cryptoTailStrategy.list.winRate')}: ${(Number(item.winRate) * 100).toFixed(1)}%` : ''}
|
{item.winRate != null && (
|
||||||
</div>
|
<>
|
||||||
<Space>
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>·</Typography.Text>
|
||||||
|
<Tag color="blue" style={{ margin: 0 }}>{t('cryptoTailStrategy.list.winRate')} {(Number(item.winRate) * 100).toFixed(1)}%</Tag>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
<Divider style={{ margin: '10px 0' }} />
|
||||||
|
<Space wrap size="small">
|
||||||
<Switch
|
<Switch
|
||||||
checked={item.enabled}
|
checked={item.enabled}
|
||||||
onChange={() => handleToggle(item)}
|
onChange={() => handleToggle(item)}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<Button type="link" size="small" onClick={() => openEditModal(item)}>
|
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEditModal(item)}>
|
||||||
{t('cryptoTailStrategy.list.edit')}
|
{t('cryptoTailStrategy.list.edit')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="link" size="small" onClick={() => openTriggers(item.id)}>
|
<Button type="link" size="small" icon={<UnorderedListOutlined />} onClick={() => openTriggers(item.id)}>
|
||||||
{t('cryptoTailStrategy.list.viewTriggers')}
|
{t('cryptoTailStrategy.list.viewTriggers')}
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
@@ -608,6 +728,7 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
<Select
|
<Select
|
||||||
placeholder={t('cryptoTailStrategy.form.selectAccount')}
|
placeholder={t('cryptoTailStrategy.form.selectAccount')}
|
||||||
options={accounts.map((a) => ({ label: a.accountName || `#${a.id}`, value: a.id }))}
|
options={accounts.map((a) => ({ label: a.accountName || `#${a.id}`, value: a.id }))}
|
||||||
|
disabled={!!editingId}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="name" label={t('cryptoTailStrategy.form.strategyName')}>
|
<Form.Item name="name" label={t('cryptoTailStrategy.form.strategyName')}>
|
||||||
@@ -741,76 +862,193 @@ const CryptoTailStrategyList: React.FC = () => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={t('cryptoTailStrategy.triggerRecords.title')}
|
title={
|
||||||
|
<Space>
|
||||||
|
<UnorderedListOutlined style={{ fontSize: 18, color: 'var(--ant-colorPrimary)' }} />
|
||||||
|
<span>{t('cryptoTailStrategy.triggerRecords.title')}</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
open={triggersModalOpen}
|
open={triggersModalOpen}
|
||||||
onCancel={() => setTriggersModalOpen(false)}
|
onCancel={() => setTriggersModalOpen(false)}
|
||||||
footer={null}
|
footer={null}
|
||||||
width={Math.min(800, window.innerWidth - 48)}
|
width={Math.min(900, window.innerWidth - 48)}
|
||||||
|
styles={{ body: { paddingTop: 16 } }}
|
||||||
>
|
>
|
||||||
<Spin spinning={triggersLoading}>
|
<Card size="small" style={{ marginBottom: 16, background: 'var(--ant-colorFillQuaternary)' }}>
|
||||||
<Table
|
<Space wrap align="center" size="middle">
|
||||||
rowKey="id"
|
<Space align="center">
|
||||||
size="small"
|
<CalendarOutlined style={{ color: 'var(--ant-colorTextSecondary)' }} />
|
||||||
dataSource={triggers}
|
<Typography.Text type="secondary">{t('cryptoTailStrategy.triggerRecords.timeRange')}</Typography.Text>
|
||||||
columns={[
|
</Space>
|
||||||
{
|
<DatePicker.RangePicker
|
||||||
title: t('cryptoTailStrategy.triggerRecords.triggerTime'),
|
value={triggerDateRange}
|
||||||
dataIndex: 'createdAt',
|
onChange={onTriggerDateRangeChange}
|
||||||
key: 'createdAt',
|
format="YYYY-MM-DD"
|
||||||
render: (ts: number) => new Date(ts).toLocaleString()
|
placeholder={[t('cryptoTailStrategy.triggerRecords.startDate'), t('cryptoTailStrategy.triggerRecords.endDate')]}
|
||||||
},
|
allowClear
|
||||||
{
|
disabledDate={disabledTriggerEndDate}
|
||||||
title: t('cryptoTailStrategy.triggerRecords.direction'),
|
style={{ minWidth: 240 }}
|
||||||
dataIndex: 'outcomeIndex',
|
/>
|
||||||
key: 'outcomeIndex',
|
</Space>
|
||||||
render: (i: number) => (i === 0 ? t('cryptoTailStrategy.triggerRecords.up') : t('cryptoTailStrategy.triggerRecords.down'))
|
</Card>
|
||||||
},
|
<Tabs
|
||||||
{
|
activeKey={triggerTab}
|
||||||
title: t('cryptoTailStrategy.triggerRecords.triggerPrice'),
|
onChange={onTriggerTabChange}
|
||||||
dataIndex: 'triggerPrice',
|
items={[
|
||||||
key: 'triggerPrice',
|
{
|
||||||
render: (v: string) => (formatNumber(v, 2) || '-')
|
key: 'success',
|
||||||
},
|
label: t('cryptoTailStrategy.triggerRecords.successTab'),
|
||||||
{
|
children: (
|
||||||
title: t('cryptoTailStrategy.triggerRecords.amount'),
|
<Spin spinning={triggersLoading}>
|
||||||
dataIndex: 'amountUsdc',
|
<Table
|
||||||
key: 'amountUsdc',
|
rowKey="id"
|
||||||
render: (v: string) => `${formatUSDC(v)} USDC`
|
size="small"
|
||||||
},
|
dataSource={triggerTab === 'success' ? triggers : []}
|
||||||
{
|
locale={{
|
||||||
title: t('cryptoTailStrategy.triggerRecords.orderId'),
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('cryptoTailStrategy.triggerRecords.emptySuccess')} />
|
||||||
dataIndex: 'orderId',
|
}}
|
||||||
key: 'orderId',
|
columns={[
|
||||||
ellipsis: true
|
{
|
||||||
},
|
title: t('cryptoTailStrategy.triggerRecords.triggerTime'),
|
||||||
{
|
dataIndex: 'createdAt',
|
||||||
title: t('cryptoTailStrategy.triggerRecords.status'),
|
key: 'createdAt',
|
||||||
dataIndex: 'status',
|
width: 172,
|
||||||
key: 'status',
|
render: (ts: number) => <Typography.Text>{new Date(ts).toLocaleString()}</Typography.Text>
|
||||||
render: (s: string) => (
|
},
|
||||||
<Tag color={s === 'success' ? 'green' : 'red'}>
|
{
|
||||||
{s === 'success' ? t('cryptoTailStrategy.triggerRecords.success') : t('cryptoTailStrategy.triggerRecords.fail')}
|
title: t('cryptoTailStrategy.triggerRecords.direction'),
|
||||||
</Tag>
|
dataIndex: 'outcomeIndex',
|
||||||
)
|
key: 'outcomeIndex',
|
||||||
},
|
width: 80,
|
||||||
{
|
align: 'center',
|
||||||
title: t('cryptoTailStrategy.triggerRecords.realizedPnl'),
|
render: (i: number) =>
|
||||||
dataIndex: 'realizedPnl',
|
i === 0 ? (
|
||||||
key: 'realizedPnl',
|
<Tag icon={<ArrowUpOutlined />} color="green">{t('cryptoTailStrategy.triggerRecords.up')}</Tag>
|
||||||
render: (v: string | undefined, r: CryptoTailStrategyTriggerDto) => {
|
) : (
|
||||||
if (v == null || v === '') return r.resolved ? formatUSDC('0') : '-'
|
<Tag icon={<ArrowDownOutlined />} color="volcano">{t('cryptoTailStrategy.triggerRecords.down')}</Tag>
|
||||||
const num = Number(v)
|
)
|
||||||
const formatted = formatUSDC(String(Math.abs(num)))
|
},
|
||||||
const text = num >= 0 ? `+${formatted}` : `-${formatted}`
|
{
|
||||||
const color = pnlColor(v)
|
title: t('cryptoTailStrategy.triggerRecords.triggerPrice'),
|
||||||
return color ? <span style={{ color }}>{text}</span> : text
|
dataIndex: 'triggerPrice',
|
||||||
}
|
key: 'triggerPrice',
|
||||||
}
|
width: 100,
|
||||||
]}
|
render: (v: string) => (formatNumber(v, 2) || '-')
|
||||||
pagination={false}
|
},
|
||||||
scroll={{ x: 600 }}
|
{
|
||||||
/>
|
title: t('cryptoTailStrategy.triggerRecords.amount'),
|
||||||
</Spin>
|
dataIndex: 'amountUsdc',
|
||||||
|
key: 'amountUsdc',
|
||||||
|
width: 110,
|
||||||
|
render: (v: string) => `${formatUSDC(v)} USDC`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.triggerRecords.realizedPnl'),
|
||||||
|
dataIndex: 'realizedPnl',
|
||||||
|
key: 'realizedPnl',
|
||||||
|
width: 100,
|
||||||
|
render: (v: string | undefined, r: CryptoTailStrategyTriggerDto) => {
|
||||||
|
if (v == null || v === '') return r.resolved ? formatUSDC('0') : '-'
|
||||||
|
const num = Number(v)
|
||||||
|
const formatted = formatUSDC(String(Math.abs(num)))
|
||||||
|
const text = num >= 0 ? `+${formatted}` : `-${formatted}`
|
||||||
|
const color = pnlColor(v)
|
||||||
|
return color ? <Typography.Text style={{ color, fontWeight: 500 }}>{text}</Typography.Text> : text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
pagination={{
|
||||||
|
current: triggerPage,
|
||||||
|
pageSize: triggerPageSize,
|
||||||
|
total: triggersTotal,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => t('cryptoTailStrategy.triggerRecords.totalCount').replace('{count}', String(total)),
|
||||||
|
onChange: onTriggerPageChange
|
||||||
|
}}
|
||||||
|
scroll={{ x: 540 }}
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fail',
|
||||||
|
label: t('cryptoTailStrategy.triggerRecords.failTab'),
|
||||||
|
children: (
|
||||||
|
<Spin spinning={triggersLoading}>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
size="small"
|
||||||
|
dataSource={triggerTab === 'fail' ? triggers : []}
|
||||||
|
locale={{
|
||||||
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('cryptoTailStrategy.triggerRecords.emptyFail')} />
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.triggerRecords.triggerTime'),
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
key: 'createdAt',
|
||||||
|
width: 172,
|
||||||
|
render: (ts: number) => <Typography.Text>{new Date(ts).toLocaleString()}</Typography.Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.triggerRecords.direction'),
|
||||||
|
dataIndex: 'outcomeIndex',
|
||||||
|
key: 'outcomeIndex',
|
||||||
|
width: 80,
|
||||||
|
align: 'center',
|
||||||
|
render: (i: number) =>
|
||||||
|
i === 0 ? (
|
||||||
|
<Tag icon={<ArrowUpOutlined />} color="green">{t('cryptoTailStrategy.triggerRecords.up')}</Tag>
|
||||||
|
) : (
|
||||||
|
<Tag icon={<ArrowDownOutlined />} color="volcano">{t('cryptoTailStrategy.triggerRecords.down')}</Tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.triggerRecords.triggerPrice'),
|
||||||
|
dataIndex: 'triggerPrice',
|
||||||
|
key: 'triggerPrice',
|
||||||
|
width: 100,
|
||||||
|
render: (v: string) => (formatNumber(v, 2) || '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.triggerRecords.amount'),
|
||||||
|
dataIndex: 'amountUsdc',
|
||||||
|
key: 'amountUsdc',
|
||||||
|
width: 110,
|
||||||
|
render: (v: string) => `${formatUSDC(v)} USDC`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('cryptoTailStrategy.triggerRecords.failReason'),
|
||||||
|
dataIndex: 'failReason',
|
||||||
|
key: 'failReason',
|
||||||
|
ellipsis: true,
|
||||||
|
render: (v: string | null | undefined) => {
|
||||||
|
const text = v ?? '-'
|
||||||
|
return text.length > 40 ? (
|
||||||
|
<Tooltip title={text}>
|
||||||
|
<Typography.Text ellipsis style={{ maxWidth: 280 }}>{text}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Typography.Text type={text === '-' ? 'secondary' : undefined}>{text}</Typography.Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
pagination={{
|
||||||
|
current: triggerPage,
|
||||||
|
pageSize: triggerPageSize,
|
||||||
|
total: triggersTotal,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => t('cryptoTailStrategy.triggerRecords.totalCount').replace('{count}', String(total)),
|
||||||
|
onChange: onTriggerPageChange
|
||||||
|
}}
|
||||||
|
scroll={{ x: 540 }}
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -468,7 +468,14 @@ export const apiService = {
|
|||||||
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/update', data),
|
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/update', data),
|
||||||
delete: (data: { strategyId: number }) =>
|
delete: (data: { strategyId: number }) =>
|
||||||
apiClient.post<ApiResponse<void>>('/crypto-tail-strategy/delete', data),
|
apiClient.post<ApiResponse<void>>('/crypto-tail-strategy/delete', data),
|
||||||
triggers: (data: { strategyId: number; page?: number; pageSize?: number; status?: string }) =>
|
triggers: (data: {
|
||||||
|
strategyId: number
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: string
|
||||||
|
startDate?: number
|
||||||
|
endDate?: number
|
||||||
|
}) =>
|
||||||
apiClient.post<ApiResponse<{ list: import('../types').CryptoTailStrategyTriggerDto[]; total: number }>>('/crypto-tail-strategy/triggers', data),
|
apiClient.post<ApiResponse<{ list: import('../types').CryptoTailStrategyTriggerDto[]; total: number }>>('/crypto-tail-strategy/triggers', data),
|
||||||
marketOptions: () =>
|
marketOptions: () =>
|
||||||
apiClient.post<ApiResponse<import('../types').CryptoTailMarketOptionDto[]>>('/crypto-tail-strategy/market-options', {}),
|
apiClient.post<ApiResponse<import('../types').CryptoTailMarketOptionDto[]>>('/crypto-tail-strategy/market-options', {}),
|
||||||
|
|||||||
Reference in New Issue
Block a user