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(
|
||||
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
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
+5
@@ -15,6 +15,11 @@ interface CryptoTailStrategyTriggerRepository : JpaRepository<CryptoTailStrategy
|
||||
fun findAllByStrategyIdAndStatusOrderByCreatedAtDesc(strategyId: Long, status: String, pageable: Pageable): Page<CryptoTailStrategyTrigger>
|
||||
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 非空)、且未结算的触发记录 */
|
||||
fun findByStatusAndResolvedAndOrderIdIsNotNullOrderByCreatedAtAsc(status: String, resolved: Boolean): List<CryptoTailStrategyTrigger>
|
||||
|
||||
|
||||
+25
-8
@@ -199,16 +199,33 @@ class CryptoTailStrategyService(
|
||||
fun getTriggerRecords(request: CryptoTailStrategyTriggerListRequest): Result<CryptoTailStrategyTriggerListResponse> {
|
||||
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) {
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} 条"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} 條"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<number | null>(null)
|
||||
const [marketOptions, setMarketOptions] = useState<CryptoTailMarketOptionDto[]>([])
|
||||
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 [, 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) => (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => 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) => (
|
||||
<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'),
|
||||
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) => (
|
||||
<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'),
|
||||
key: 'timeWindow',
|
||||
width: isMobile ? 140 : 180,
|
||||
render: (_: unknown, r: CryptoTailStrategyDto) => formatTimeWindow(r.windowStartSeconds, r.windowEndSeconds)
|
||||
width: isMobile ? 100 : 120,
|
||||
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'),
|
||||
key: 'priceRange',
|
||||
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'),
|
||||
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) => (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => handleToggle(record)}
|
||||
checkedChildren={t('cryptoTailStrategy.list.enable')}
|
||||
unCheckedChildren={t('cryptoTailStrategy.list.disable')}
|
||||
/>
|
||||
render: (_: unknown, r: CryptoTailStrategyDto) => (
|
||||
<Typography.Text type="secondary" style={{ wordBreak: 'break-word', whiteSpace: 'normal' }}>
|
||||
{(r.amountMode?.toUpperCase() ?? '') === 'RATIO'
|
||||
? `${t('cryptoTailStrategy.list.ratio')} ${formatNumber(r.amountValue, 2) || '0'}%`
|
||||
: `${t('cryptoTailStrategy.list.fixed')} ${formatUSDC(r.amountValue)} USDC`}
|
||||
</Typography.Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
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 ? <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'),
|
||||
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 ? (
|
||||
<Tag color="blue">{(Number(r.winRate) * 100).toFixed(1)}%</Tag>
|
||||
) : (
|
||||
<Typography.Text type="secondary">-</Typography.Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.list.actions'),
|
||||
@@ -504,45 +598,71 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
</div>
|
||||
<Spin spinning={loading}>
|
||||
{isMobile ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{list.map((item) => (
|
||||
<Card key={item.id} size="small">
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<strong>{item.name || item.marketSlugPrefix || '-'}</strong>
|
||||
<Card
|
||||
key={item.id}
|
||||
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 style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
||||
{t('cryptoTailStrategy.list.timeWindow')}: {formatTimeWindow(item.windowStartSeconds, item.windowEndSeconds)}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
|
||||
{t('cryptoTailStrategy.list.priceRange')}: {formatPriceRange(item.minPrice, item.maxPrice)}
|
||||
</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')}:{' '}
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Space wrap style={{ marginBottom: 12 }}>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{t('cryptoTailStrategy.list.totalRealizedPnl')}:{' '}
|
||||
</Typography.Text>
|
||||
{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
|
||||
</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)}%` : ''}
|
||||
</div>
|
||||
<Space>
|
||||
{item.winRate != null && (
|
||||
<>
|
||||
<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
|
||||
checked={item.enabled}
|
||||
onChange={() => handleToggle(item)}
|
||||
size="small"
|
||||
/>
|
||||
<Button type="link" size="small" onClick={() => openEditModal(item)}>
|
||||
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEditModal(item)}>
|
||||
{t('cryptoTailStrategy.list.edit')}
|
||||
</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')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
@@ -608,6 +728,7 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
<Select
|
||||
placeholder={t('cryptoTailStrategy.form.selectAccount')}
|
||||
options={accounts.map((a) => ({ label: a.accountName || `#${a.id}`, value: a.id }))}
|
||||
disabled={!!editingId}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label={t('cryptoTailStrategy.form.strategyName')}>
|
||||
@@ -741,76 +862,193 @@ const CryptoTailStrategyList: React.FC = () => {
|
||||
</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}
|
||||
onCancel={() => setTriggersModalOpen(false)}
|
||||
footer={null}
|
||||
width={Math.min(800, window.innerWidth - 48)}
|
||||
width={Math.min(900, window.innerWidth - 48)}
|
||||
styles={{ body: { paddingTop: 16 } }}
|
||||
>
|
||||
<Spin spinning={triggersLoading}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={triggers}
|
||||
columns={[
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.triggerTime'),
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (ts: number) => new Date(ts).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.direction'),
|
||||
dataIndex: 'outcomeIndex',
|
||||
key: 'outcomeIndex',
|
||||
render: (i: number) => (i === 0 ? t('cryptoTailStrategy.triggerRecords.up') : t('cryptoTailStrategy.triggerRecords.down'))
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.triggerPrice'),
|
||||
dataIndex: 'triggerPrice',
|
||||
key: 'triggerPrice',
|
||||
render: (v: string) => (formatNumber(v, 2) || '-')
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.amount'),
|
||||
dataIndex: 'amountUsdc',
|
||||
key: 'amountUsdc',
|
||||
render: (v: string) => `${formatUSDC(v)} USDC`
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.orderId'),
|
||||
dataIndex: 'orderId',
|
||||
key: 'orderId',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.status'),
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (s: string) => (
|
||||
<Tag color={s === 'success' ? 'green' : 'red'}>
|
||||
{s === 'success' ? t('cryptoTailStrategy.triggerRecords.success') : t('cryptoTailStrategy.triggerRecords.fail')}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('cryptoTailStrategy.triggerRecords.realizedPnl'),
|
||||
dataIndex: 'realizedPnl',
|
||||
key: 'realizedPnl',
|
||||
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 ? <span style={{ color }}>{text}</span> : text
|
||||
}
|
||||
}
|
||||
]}
|
||||
pagination={false}
|
||||
scroll={{ x: 600 }}
|
||||
/>
|
||||
</Spin>
|
||||
<Card size="small" style={{ marginBottom: 16, background: 'var(--ant-colorFillQuaternary)' }}>
|
||||
<Space wrap align="center" size="middle">
|
||||
<Space align="center">
|
||||
<CalendarOutlined style={{ color: 'var(--ant-colorTextSecondary)' }} />
|
||||
<Typography.Text type="secondary">{t('cryptoTailStrategy.triggerRecords.timeRange')}</Typography.Text>
|
||||
</Space>
|
||||
<DatePicker.RangePicker
|
||||
value={triggerDateRange}
|
||||
onChange={onTriggerDateRangeChange}
|
||||
format="YYYY-MM-DD"
|
||||
placeholder={[t('cryptoTailStrategy.triggerRecords.startDate'), t('cryptoTailStrategy.triggerRecords.endDate')]}
|
||||
allowClear
|
||||
disabledDate={disabledTriggerEndDate}
|
||||
style={{ minWidth: 240 }}
|
||||
/>
|
||||
</Space>
|
||||
</Card>
|
||||
<Tabs
|
||||
activeKey={triggerTab}
|
||||
onChange={onTriggerTabChange}
|
||||
items={[
|
||||
{
|
||||
key: 'success',
|
||||
label: t('cryptoTailStrategy.triggerRecords.successTab'),
|
||||
children: (
|
||||
<Spin spinning={triggersLoading}>
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="small"
|
||||
dataSource={triggerTab === 'success' ? triggers : []}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('cryptoTailStrategy.triggerRecords.emptySuccess')} />
|
||||
}}
|
||||
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.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>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -468,7 +468,14 @@ export const apiService = {
|
||||
apiClient.post<ApiResponse<import('../types').CryptoTailStrategyDto>>('/crypto-tail-strategy/update', data),
|
||||
delete: (data: { strategyId: number }) =>
|
||||
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),
|
||||
marketOptions: () =>
|
||||
apiClient.post<ApiResponse<import('../types').CryptoTailMarketOptionDto[]>>('/crypto-tail-strategy/market-options', {}),
|
||||
|
||||
Reference in New Issue
Block a user