diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailMonitorDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailMonitorDto.kt index b279d1d..56ec5c5 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailMonitorDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/CryptoTailMonitorDto.kt @@ -57,7 +57,11 @@ data class CryptoTailMonitorInitResponse( /** 当前时间 (毫秒时间戳) */ val currentTimestamp: Long = System.currentTimeMillis(), /** 是否启用 */ - val enabled: Boolean = true + val enabled: Boolean = true, + /** 投入金额模式: FIXED or RATIO */ + val amountMode: String? = null, + /** 投入金额数值 */ + val amountValue: String? = null ) /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailMonitorService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailMonitorService.kt index e534f76..5efa49b 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailMonitorService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/cryptotail/CryptoTailMonitorService.kt @@ -208,7 +208,9 @@ class CryptoTailMonitorService( tokenIdUp = tokenIds.getOrNull(0), tokenIdDown = tokenIds.getOrNull(1), currentTimestamp = System.currentTimeMillis(), - enabled = strategy.enabled + enabled = strategy.enabled, + amountMode = strategy.amountMode, + amountValue = strategy.amountValue.toPlainString() ) Result.success(response) diff --git a/frontend/src/locales/en/common.json b/frontend/src/locales/en/common.json index 0d007d6..bddd445 100644 --- a/frontend/src/locales/en/common.json +++ b/frontend/src/locales/en/common.json @@ -1676,7 +1676,16 @@ "failed": "Manual order failed: {{reason}}", "errorSigning": "Signing failed: {reason}", "errorNetwork": "Network error, please try again later", - "priceNotLoaded": "Price data not loaded" + "priceNotLoaded": "Price data not loaded", + "insufficientBalance": "Insufficient balance, available amount less than 1 USDC", + "fetchBalanceFailed": "Failed to fetch account balance", + "availableBalance": "Available Balance", + "fetchLatestPrice": "Latest Price", + "priceUpdated": "Price updated", + "maxSize": "Max", + "invalidPriceOrBalance": "Invalid price or balance", + "insufficientBalanceForMax": "Insufficient balance for max size", + "maxSizeUpdated": "Updated to max size" } } } \ 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 fd764cf..ca63a4e 100644 --- a/frontend/src/locales/zh-CN/common.json +++ b/frontend/src/locales/zh-CN/common.json @@ -1676,7 +1676,16 @@ "failed": "手动下单失败:{{reason}}", "errorSigning": "签名失败:{{reason}}", "errorNetwork": "网络错误,请稍后重试", - "priceNotLoaded": "价格数据未加载" + "priceNotLoaded": "价格数据未加载", + "insufficientBalance": "余额不足,可用金额少于 1 USDC", + "fetchBalanceFailed": "获取账户余额失败", + "availableBalance": "可用余额", + "fetchLatestPrice": "获取最新价", + "priceUpdated": "价格已更新", + "maxSize": "最大", + "invalidPriceOrBalance": "价格或余额无效", + "insufficientBalanceForMax": "余额不足以购买最大数量", + "maxSizeUpdated": "已更新为最大数量" } } } \ 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 1d3650b..aa5020a 100644 --- a/frontend/src/locales/zh-TW/common.json +++ b/frontend/src/locales/zh-TW/common.json @@ -1676,7 +1676,16 @@ "failed": "手動下單失敗:{{reason}}", "errorSigning": "簽名失敗:{reason}", "errorNetwork": "網絡錯誤,請稍後重試", - "priceNotLoaded": "價格數據未加載" + "priceNotLoaded": "價格數據未加載", + "insufficientBalance": "餘額不足,可用金額少於 1 USDC", + "fetchBalanceFailed": "獲取賬戶餘額失敗", + "availableBalance": "可用餘額", + "fetchLatestPrice": "獲取最新價", + "priceUpdated": "價格已更新", + "maxSize": "最大", + "invalidPriceOrBalance": "價格或餘額無效", + "insufficientBalanceForMax": "餘額不足以購買最大數量", + "maxSizeUpdated": "已更新為最大數量" } } } \ No newline at end of file diff --git a/frontend/src/pages/CryptoTailMonitor.tsx b/frontend/src/pages/CryptoTailMonitor.tsx index 797fc18..9dd031f 100644 --- a/frontend/src/pages/CryptoTailMonitor.tsx +++ b/frontend/src/pages/CryptoTailMonitor.tsx @@ -99,13 +99,15 @@ const CryptoTailMonitor: React.FC = () => { size: string totalAmount: string bestBid: string + availableBalance: string }>({ visible: false, direction: 'UP', price: '', size: '', totalAmount: '', - bestBid: '' + bestBid: '', + availableBalance: '' }) const [ordering, setOrdering] = useState(false) @@ -756,7 +758,7 @@ const CryptoTailMonitor: React.FC = () => { parseFloat(currentSpread) < Math.min(...minSpreadLineNum) // 手动下单:打开弹窗 - const handleOpenManualOrderModal = (direction: 'UP' | 'DOWN') => { + const handleOpenManualOrderModal = async (direction: 'UP' | 'DOWN') => { if (!pushData) { message.warning(t('cryptoTailMonitor.manualOrder.priceNotLoaded')) return @@ -769,18 +771,81 @@ const CryptoTailMonitor: React.FC = () => { // 计算默认价格:最优 bid × 1.1,限制在 0~1 之间 const rawPrice = parseFloat(bestBid) * 1.1 const defaultPrice = Math.min(1, Math.max(0, rawPrice)) - const defaultAmountUsdc = 10 - const defaultSize = Math.ceil(defaultAmountUsdc / defaultPrice) + + // 获取账户余额 + let availableBalance = '0' + if (initData?.accountId) { + try { + const balanceRes = await apiService.accounts.balance({ accountId: initData.accountId }) + if (balanceRes.data.code === 0 && balanceRes.data.data?.availableBalance) { + availableBalance = balanceRes.data.data.availableBalance + } + } catch (e) { + console.error('获取账户余额失败:', e) + } + } + + // 使用策略配置的金额 + let defaultAmountUsdc = 10 + if (initData?.amountMode === 'FIXED' && initData?.amountValue) { + defaultAmountUsdc = parseFloat(initData.amountValue) + } else if (initData?.amountMode === 'RATIO' && initData?.amountValue) { + // RATIO 模式:按比例计算 + const balanceNum = parseFloat(availableBalance) + const ratio = parseFloat(initData.amountValue || '10') + defaultAmountUsdc = Math.floor(balanceNum * ratio / 100) + // 至少保留 1 USDC + if (defaultAmountUsdc < 1) { + message.warning(t('cryptoTailMonitor.manualOrder.insufficientBalance')) + return + } + } + + // 计算默认数量(保留2位小数,用于手动下单) + let defaultSize = (defaultAmountUsdc / defaultPrice).toFixed(2) + // 确保至少 1 张 + if (parseFloat(defaultSize) < 1) { + defaultSize = '1.00' + } + + // 重新计算总金额(基于实际数量) + const defaultTotalAmount = (defaultPrice * parseFloat(defaultSize)).toFixed(2) + setManualOrderModal({ visible: true, direction, price: defaultPrice.toFixed(4), - size: defaultSize.toFixed(2), - totalAmount: (defaultPrice * defaultSize).toFixed(2), - bestBid + size: defaultSize, + totalAmount: defaultTotalAmount, + bestBid, + availableBalance }) } + // 获取最新价 + const handleFetchLatestPrice = async () => { + if (!pushData) { + message.warning(t('cryptoTailMonitor.manualOrder.priceNotLoaded')) + return + } + const latestPrice = manualOrderModal.direction === 'UP' + ? pushData.currentPriceUp + : pushData.currentPriceDown + if (!latestPrice) { + message.warning(t('cryptoTailMonitor.manualOrder.priceNotLoaded')) + return + } + const price = parseFloat(latestPrice) + const size = parseFloat(manualOrderModal.size) + const totalAmount = (price * size).toFixed(2) + setManualOrderModal({ + ...manualOrderModal, + price: price.toFixed(4), + totalAmount + }) + message.success(t('cryptoTailMonitor.manualOrder.priceUpdated')) + } + const handleCloseManualOrderModal = () => { setManualOrderModal({ visible: false, @@ -788,7 +853,8 @@ const CryptoTailMonitor: React.FC = () => { price: '', size: '', totalAmount: '', - bestBid: '' + bestBid: '', + availableBalance: '' }) } @@ -810,6 +876,33 @@ const CryptoTailMonitor: React.FC = () => { setManualOrderModal({ ...manualOrderModal, size, totalAmount, price: price.toFixed(4) }) } + // 计算最大数量(截位处理) + const handleMaxSize = () => { + const price = parseFloat(manualOrderModal.price) + const balance = parseFloat(manualOrderModal.availableBalance) + + if (price <= 0 || balance <= 0) { + message.warning(t('cryptoTailMonitor.manualOrder.invalidPriceOrBalance')) + return + } + + // 最大数量 = Math.floor(可用余额 / 价格) + const maxSize = Math.floor(balance / price) + + if (maxSize < 1) { + message.warning(t('cryptoTailMonitor.manualOrder.insufficientBalanceForMax')) + return + } + + const totalAmount = (price * maxSize).toFixed(2) + setManualOrderModal({ + ...manualOrderModal, + size: maxSize.toFixed(2), + totalAmount + }) + message.success(t('cryptoTailMonitor.manualOrder.maxSizeUpdated')) + } + const handleManualOrder = async () => { if (!initData || !pushData) return try { @@ -1167,33 +1260,51 @@ const CryptoTailMonitor: React.FC = () => { : t('cryptoTailMonitor.manualOrder.directionDown')} + + + {t('cryptoTailMonitor.manualOrder.availableBalance')} + + + {manualOrderModal.availableBalance ? formatNumber(manualOrderModal.availableBalance, 2) : '-'} {t('cryptoTailMonitor.manualOrder.orderUnit')} + + {t('cryptoTailMonitor.manualOrder.orderPrice')} ({t('cryptoTailMonitor.manualOrder.orderUnit')}) - + + + + {t('cryptoTailMonitor.manualOrder.orderSize')} ({t('cryptoTailMonitor.manualOrder.sizeUnit')}) - + + + +