diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailStrategyDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailStrategyDto.kt index 7b33506..fca8be9 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailStrategyDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailStrategyDto.kt @@ -93,12 +93,16 @@ data class CryptoTailStrategyDeleteRequest( /** * 触发记录列表请求 + * @param startDate 开始日期(当天 00:00:00.000 的时间戳毫秒),为 null 表示不限制 + * @param endDate 结束日期(当天 23:59:59.999 的时间戳毫秒),为 null 表示不限制 */ data class CryptoTailStrategyTriggerListRequest( val strategyId: Long = 0L, val page: Int = 1, val pageSize: Int = 20, - val status: String? = null + val status: String? = null, + val startDate: Long? = null, + val endDate: Long? = null ) /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/CryptoTailStrategyTriggerRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/CryptoTailStrategyTriggerRepository.kt index f8e1fd5..8419eb6 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/CryptoTailStrategyTriggerRepository.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/CryptoTailStrategyTriggerRepository.kt @@ -15,6 +15,11 @@ interface CryptoTailStrategyTriggerRepository : JpaRepository fun countByStrategyIdAndStatus(strategyId: Long, status: String): Long + fun findAllByStrategyIdAndCreatedAtBetweenOrderByCreatedAtDesc(strategyId: Long, startInclusive: Long, endInclusive: Long, pageable: Pageable): Page + fun findAllByStrategyIdAndStatusAndCreatedAtBetweenOrderByCreatedAtDesc(strategyId: Long, status: String, startInclusive: Long, endInclusive: Long, pageable: Pageable): Page + fun countByStrategyIdAndCreatedAtBetween(strategyId: Long, startInclusive: Long, endInclusive: Long): Long + fun countByStrategyIdAndStatusAndCreatedAtBetween(strategyId: Long, status: String, startInclusive: Long, endInclusive: Long): Long + /** 轮询结算:仅处理下单成功的订单(status=success 且 orderId 非空)、且未结算的触发记录 */ fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailStrategyService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailStrategyService.kt index f126ba2..491befd 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailStrategyService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailStrategyService.kt @@ -199,16 +199,33 @@ class CryptoTailStrategyService( fun getTriggerRecords(request: CryptoTailStrategyTriggerListRequest): Result { return try { val page = PageRequest.of((request.page - 1).coerceAtLeast(0), request.pageSize.coerceIn(1, 100)) - val pageResult = if (request.status != null && request.status.isNotBlank()) { - triggerRepository.findAllByStrategyIdAndStatusOrderByCreatedAtDesc(request.strategyId, request.status, page) - } else { - triggerRepository.findAllByStrategyIdOrderByCreatedAtDesc(request.strategyId, page) + val startTs = request.startDate ?: 0L + val endTs = request.endDate ?: Long.MAX_VALUE + val useTimeRange = request.startDate != null || request.endDate != null + 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 total = if (request.status != null && request.status.isNotBlank()) { - triggerRepository.countByStrategyIdAndStatus(request.strategyId, request.status) - } else { - pageResult.totalElements + val total = when { + useTimeRange && request.status != null && request.status.isNotBlank() -> + triggerRepository.countByStrategyIdAndStatusAndCreatedAtBetween(request.strategyId, request.status, startTs, endTs) + 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)) } catch (e: Exception) { diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 6e3a262..4979a60 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -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.", "addStrategy": "Add Strategy", "strategyName": "Strategy Name", + "account": "Account", "market": "Market", "timeWindow": "Time Window", "priceRange": "Price Range", "amountMode": "Amount Mode", "ratio": "Ratio", "fixed": "Fixed", - "recentTrigger": "Last Trigger", "totalRealizedPnl": "Total PnL", "winRate": "Win Rate", "actions": "Actions", @@ -1426,14 +1426,14 @@ "enable": "Enable", "disable": "Disable", "delete": "Delete", - "viewTriggers": "Trigger Records", + "viewTriggers": "Orders", "deleteConfirm": "Delete this strategy?", "fetchFailed": "Failed to fetch list" }, "form": { "walletTip": "Use a dedicated wallet to avoid balance or order issues.", "strategyName": "Strategy Name", - "strategyNamePlaceholder": "Auto", + "strategyNamePlaceholder": "Optional, leave blank to auto-generate", "selectAccount": "Select Account", "selectMarket": "Select Market", "timeWindowStart": "Window Start", @@ -1467,6 +1467,11 @@ }, "triggerRecords": { "title": "Trigger Records", + "timeRange": "Time Range", + "startDate": "Start Date", + "endDate": "End Date", + "successTab": "Success", + "failTab": "Failed", "triggerTime": "Time", "market": "Market", "direction": "Direction", @@ -1478,8 +1483,12 @@ "status": "Status", "success": "Success", "fail": "Fail", + "failReason": "Failure Reason", "realizedPnl": "PnL", - "resolved": "Settled" + "resolved": "Settled", + "emptySuccess": "No success records", + "emptyFail": "No failed records", + "totalCount": "{count} record(s)" } } } \ No newline at end of file diff --git a/frontend/src/locales/zh-CN/common.json b/frontend/src/locales/zh-CN/common.json index e624250..1bc1383 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -1411,13 +1411,13 @@ "walletTip": "请使用单独的钱包运行尾盘策略,避免该钱包用于手动交易、跟单等其他操作,否则可能导致余额或仓位变化,造成策略执行异常。", "addStrategy": "新增策略", "strategyName": "策略名称", + "account": "账户", "market": "关联市场", "timeWindow": "时间区间", "priceRange": "价格区间", "amountMode": "投入方式", "ratio": "比例", "fixed": "固定金额", - "recentTrigger": "最近触发", "totalRealizedPnl": "总收益", "winRate": "胜率", "actions": "操作", @@ -1425,14 +1425,14 @@ "enable": "启用", "disable": "停用", "delete": "删除", - "viewTriggers": "查看触发记录", + "viewTriggers": "订单", "deleteConfirm": "确定删除该策略?", "fetchFailed": "获取列表失败" }, "form": { "walletTip": "建议使用专用钱包,避免手动操作等导致余额或下单异常。", "strategyName": "策略名称", - "strategyNamePlaceholder": "自动生成", + "strategyNamePlaceholder": "选填,留空将自动生成", "selectAccount": "选择账户", "selectMarket": "选择市场", "timeWindowStart": "区间开始", @@ -1466,6 +1466,11 @@ }, "triggerRecords": { "title": "触发记录", + "timeRange": "时间范围", + "startDate": "开始日期", + "endDate": "结束日期", + "successTab": "成功", + "failTab": "失败", "triggerTime": "触发时间", "market": "市场", "direction": "方向", @@ -1477,8 +1482,12 @@ "status": "状态", "success": "成功", "fail": "失败", + "failReason": "失败原因", "realizedPnl": "收益", - "resolved": "已结算" + "resolved": "已结算", + "emptySuccess": "暂无成功记录", + "emptyFail": "暂无失败记录", + "totalCount": "共 {count} 条" } } } \ No newline at end of file diff --git a/frontend/src/locales/zh-TW/common.json b/frontend/src/locales/zh-TW/common.json index 24c257e..440c394 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -1412,13 +1412,13 @@ "walletTip": "請使用單獨的錢包運行尾盤策略,避免該錢包用於手動交易、跟單等其他操作,否則可能導致餘額或倉位變化,造成策略執行異常。", "addStrategy": "新增策略", "strategyName": "策略名稱", + "account": "賬戶", "market": "關聯市場", "timeWindow": "時間區間", "priceRange": "價格區間", "amountMode": "投入方式", "ratio": "比例", "fixed": "固定金額", - "recentTrigger": "最近觸發", "totalRealizedPnl": "總收益", "winRate": "勝率", "actions": "操作", @@ -1426,14 +1426,14 @@ "enable": "啟用", "disable": "停用", "delete": "刪除", - "viewTriggers": "查看觸發記錄", + "viewTriggers": "訂單", "deleteConfirm": "確定刪除該策略?", "fetchFailed": "獲取列表失敗" }, "form": { "walletTip": "建議使用專用錢包,避免手動操作等導致餘額或下單異常。", "strategyName": "策略名稱", - "strategyNamePlaceholder": "自動生成", + "strategyNamePlaceholder": "選填,留空將自動生成", "selectAccount": "選擇賬戶", "selectMarket": "選擇市場", "timeWindowStart": "區間開始", @@ -1467,6 +1467,11 @@ }, "triggerRecords": { "title": "觸發記錄", + "timeRange": "時間範圍", + "startDate": "開始日期", + "endDate": "結束日期", + "successTab": "成功", + "failTab": "失敗", "triggerTime": "觸發時間", "market": "市場", "direction": "方向", @@ -1478,8 +1483,12 @@ "status": "狀態", "success": "成功", "fail": "失敗", + "failReason": "失敗原因", "realizedPnl": "收益", - "resolved": "已結算" + "resolved": "已結算", + "emptySuccess": "暫無成功記錄", + "emptyFail": "暫無失敗記錄", + "totalCount": "共 {count} 條" } } } \ No newline at end of file diff --git a/frontend/src/pages/CryptoTailStrategyList.tsx b/frontend/src/pages/CryptoTailStrategyList.tsx index 3448b8d..1e82c23 100644 --- a/frontend/src/pages/CryptoTailStrategyList.tsx +++ b/frontend/src/pages/CryptoTailStrategyList.tsx @@ -17,9 +17,16 @@ import { InputNumber, Radio, Spin, - Tooltip + Tooltip, + Tabs, + DatePicker, + Empty, + Typography, + Divider } 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 { useMediaQuery } from 'react-responsive' import { apiService } from '../services/api' @@ -41,9 +48,13 @@ const CryptoTailStrategyList: React.FC = () => { const [editingId, setEditingId] = useState(null) const [marketOptions, setMarketOptions] = useState([]) const [triggersModalOpen, setTriggersModalOpen] = useState(false) - const [, setTriggersStrategyId] = useState(null) + const [triggersStrategyId, setTriggersStrategyId] = useState(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([]) - const [, setTriggersTotal] = useState(0) + const [triggersTotal, setTriggersTotal] = useState(0) const [triggersLoading, setTriggersLoading] = useState(false) const [form] = Form.useForm() @@ -269,12 +280,30 @@ const CryptoTailStrategyList: React.FC = () => { } } - const openTriggers = async (strategyId: number) => { - setTriggersStrategyId(strategyId) - setTriggersModalOpen(true) + type TriggerLoadOpts = { page?: number; pageSize?: number; dateRange?: [Dayjs | null, Dayjs | null] } + + 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) 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) { setTriggers(res.data.data.list ?? []) 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 ss = startSec % 60 const em = Math.floor(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 formatLastTrigger = (ts?: number) => { - if (ts == null) return '-' - const d = new Date(ts) - return d.toLocaleString() + 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 formatPriceRange = (minPrice: string, maxPrice: string): string => { @@ -314,79 +377,110 @@ const CryptoTailStrategyList: React.FC = () => { return undefined } + const getAccountLabel = (accountId: number) => accounts.find((a) => a.id === accountId)?.accountName || `#${accountId}` + const columns = [ + { + title: t('common.status'), + dataIndex: 'enabled', + key: 'enabled', + width: 80, + render: (enabled: boolean, record: CryptoTailStrategyDto) => ( + handleToggle(record)} + size="small" + /> + ) + }, { title: t('cryptoTailStrategy.list.strategyName'), dataIndex: 'name', key: 'name', - width: isMobile ? 100 : 140, - render: (name: string | undefined, r: CryptoTailStrategyDto) => name || (r.marketTitle ?? r.marketSlugPrefix) || '-' + width: isMobile ? 100 : 160, + render: (name: string | undefined, r: CryptoTailStrategyDto) => ( + + {name || (r.marketTitle ?? r.marketSlugPrefix) || '-'} + + ) + }, + { + title: t('cryptoTailStrategy.list.account'), + dataIndex: 'accountId', + key: 'accountId', + width: isMobile ? 90 : 120, + render: (_: unknown, r: CryptoTailStrategyDto) => ( + + {getAccountLabel(r.accountId)} + + ) }, { title: t('cryptoTailStrategy.list.market'), key: 'market', width: isMobile ? 120 : 200, - render: (_: unknown, r: CryptoTailStrategyDto) => - marketOptions.find((m) => m.slug === r.marketSlugPrefix)?.title ?? r.marketTitle ?? r.marketSlugPrefix ?? '-' + render: (_: unknown, r: CryptoTailStrategyDto) => ( + + {marketOptions.find((m) => m.slug === r.marketSlugPrefix)?.title ?? r.marketTitle ?? r.marketSlugPrefix ?? '-'} + + ) }, { title: t('cryptoTailStrategy.list.timeWindow'), key: 'timeWindow', - width: isMobile ? 140 : 180, - render: (_: unknown, r: CryptoTailStrategyDto) => formatTimeWindow(r.windowStartSeconds, r.windowEndSeconds) + width: isMobile ? 100 : 120, + render: (_: unknown, r: CryptoTailStrategyDto) => ( + + {formatTimeWindow(r.windowStartSeconds, r.windowEndSeconds)} + + ) }, { title: t('cryptoTailStrategy.list.priceRange'), key: 'priceRange', width: isMobile ? 90 : 120, - render: (_: unknown, r: CryptoTailStrategyDto) => formatPriceRange(r.minPrice, r.maxPrice) + render: (_: unknown, r: CryptoTailStrategyDto) => ( + + {formatPriceRange(r.minPrice, r.maxPrice)} + + ) }, { title: t('cryptoTailStrategy.list.amountMode'), key: 'amountMode', width: isMobile ? 90 : 120, - render: (_: unknown, r: CryptoTailStrategyDto) => - (r.amountMode?.toUpperCase() ?? '') === 'RATIO' - ? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(r.amountValue, 2) || '0'}%` - : `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(r.amountValue)} USDC` - }, - { - title: t('common.status'), - dataIndex: 'enabled', - key: 'enabled', - width: 90, - render: (enabled: boolean, record: CryptoTailStrategyDto) => ( - handleToggle(record)} - checkedChildren={t('cryptoTailStrategy.list.enable')} - unCheckedChildren={t('cryptoTailStrategy.list.disable')} - /> + render: (_: unknown, r: CryptoTailStrategyDto) => ( + + {(r.amountMode?.toUpperCase() ?? '') === 'RATIO' + ? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(r.amountValue, 2) || '0'}%` + : `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(r.amountValue)} USDC`} + ) }, - { - title: t('cryptoTailStrategy.list.recentTrigger'), - dataIndex: 'lastTriggerAt', - key: 'lastTriggerAt', - width: isMobile ? 100 : 160, - render: (ts: number | undefined) => formatLastTrigger(ts) - }, { title: t('cryptoTailStrategy.list.totalRealizedPnl'), key: 'totalRealizedPnl', - width: isMobile ? 90 : 110, + width: isMobile ? 90 : 120, render: (_: unknown, r: CryptoTailStrategyDto) => { const text = r.totalRealizedPnl != null ? `${formatUSDC(r.totalRealizedPnl)} USDC` : '-' const color = pnlColor(r.totalRealizedPnl) - return color ? {text} : text + return color ? ( + {text} + ) : ( + {text} + ) } }, { title: t('cryptoTailStrategy.list.winRate'), key: 'winRate', - width: isMobile ? 70 : 80, + width: isMobile ? 70 : 90, render: (_: unknown, r: CryptoTailStrategyDto) => - r.winRate != null ? `${(Number(r.winRate) * 100).toFixed(1)}%` : '-' + r.winRate != null ? ( + {(Number(r.winRate) * 100).toFixed(1)}% + ) : ( + - + ) }, { title: t('cryptoTailStrategy.list.actions'), @@ -504,45 +598,71 @@ const CryptoTailStrategyList: React.FC = () => { {isMobile ? ( -
+
{list.map((item) => ( - -
- {item.name || item.marketSlugPrefix || '-'} + + + {item.name || (item.marketTitle ?? item.marketSlugPrefix) || '-'} + + + {t('cryptoTailStrategy.list.account')}: {getAccountLabel(item.accountId)} + + + {marketOptions.find((m) => m.slug === item.marketSlugPrefix)?.title ?? item.marketSlugPrefix ?? '-'} + +
+ + {t('cryptoTailStrategy.list.timeWindow')} + + {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds, false)} + + {t('cryptoTailStrategy.list.priceRange')} + + {formatPriceRange(item.minPrice, item.maxPrice)} + + {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`} +
-
- {t('cryptoTailStrategy.list.timeWindow')}: {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds)} -
-
- {t('cryptoTailStrategy.list.priceRange')}: {formatPriceRange(item.minPrice, item.maxPrice)} -
-
- {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`} -
-
- {t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '} + + + + {t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '} + {item.totalRealizedPnl != null ? ( - + {formatUSDC(item.totalRealizedPnl)} USDC - + ) : ( - '-' + - )} - {item.winRate != null ? ` · ${t('cryptoTailStrategy.list.winRate')}: ${(Number(item.winRate) * 100).toFixed(1)}%` : ''} -
- + {item.winRate != null && ( + <> + · + {t('cryptoTailStrategy.list.winRate')} {(Number(item.winRate) * 100).toFixed(1)}% + + )} + + + handleToggle(item)} size="small" /> - - {