feat(frontend): 手动下单功能增强与金额模式支持
- 后端返回 amountMode 和 amountValue 字段,支持前端获取策略配置 - 手动下单确认弹窗显示用户可用余额(绿色高亮) - 价格输入框右侧添加"获取最新价"按钮,支持实时更新价格 - 数量输入框右侧添加"最大"按钮,根据余额自动计算最大购买数量 - 修正数量计算逻辑:FIXED 和 RATIO 模式均按实际金额计算数量,保留2位小数 - FIXED 模式:使用配置的固定金额 - RATIO 模式:按比例计算可用余额 - 数量计算使用保留小数方式(而非向上取整),确保精确匹配投入金额 - 示例:固定1U,价格0.4,自动填充2.5张 - 示例:比例30%,余额20U,价格0.4,自动填充15张 Made-with: Cursor
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1676,7 +1676,16 @@
|
||||
"failed": "手动下单失败:{{reason}}",
|
||||
"errorSigning": "签名失败:{{reason}}",
|
||||
"errorNetwork": "网络错误,请稍后重试",
|
||||
"priceNotLoaded": "价格数据未加载"
|
||||
"priceNotLoaded": "价格数据未加载",
|
||||
"insufficientBalance": "余额不足,可用金额少于 1 USDC",
|
||||
"fetchBalanceFailed": "获取账户余额失败",
|
||||
"availableBalance": "可用余额",
|
||||
"fetchLatestPrice": "获取最新价",
|
||||
"priceUpdated": "价格已更新",
|
||||
"maxSize": "最大",
|
||||
"invalidPriceOrBalance": "价格或余额无效",
|
||||
"insufficientBalanceForMax": "余额不足以购买最大数量",
|
||||
"maxSizeUpdated": "已更新为最大数量"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1676,7 +1676,16 @@
|
||||
"failed": "手動下單失敗:{{reason}}",
|
||||
"errorSigning": "簽名失敗:{reason}",
|
||||
"errorNetwork": "網絡錯誤,請稍後重試",
|
||||
"priceNotLoaded": "價格數據未加載"
|
||||
"priceNotLoaded": "價格數據未加載",
|
||||
"insufficientBalance": "餘額不足,可用金額少於 1 USDC",
|
||||
"fetchBalanceFailed": "獲取賬戶餘額失敗",
|
||||
"availableBalance": "可用餘額",
|
||||
"fetchLatestPrice": "獲取最新價",
|
||||
"priceUpdated": "價格已更新",
|
||||
"maxSize": "最大",
|
||||
"invalidPriceOrBalance": "價格或餘額無效",
|
||||
"insufficientBalanceForMax": "餘額不足以購買最大數量",
|
||||
"maxSizeUpdated": "已更新為最大數量"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')}
|
||||
</Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>
|
||||
{t('cryptoTailMonitor.manualOrder.availableBalance')}
|
||||
</Text>
|
||||
<Text strong style={{ fontSize: 16, color: '#52c41a' }}>
|
||||
{manualOrderModal.availableBalance ? formatNumber(manualOrderModal.availableBalance, 2) : '-'} {t('cryptoTailMonitor.manualOrder.orderUnit')}
|
||||
</Text>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>
|
||||
{t('cryptoTailMonitor.manualOrder.orderPrice')} ({t('cryptoTailMonitor.manualOrder.orderUnit')})
|
||||
</Text>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={manualOrderModal.price ? parseFloat(manualOrderModal.price) : undefined}
|
||||
onChange={handlePriceChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
placeholder="0.0000"
|
||||
/>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={manualOrderModal.price ? parseFloat(manualOrderModal.price) : undefined}
|
||||
onChange={handlePriceChange}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.0001}
|
||||
precision={4}
|
||||
placeholder="0.0000"
|
||||
/>
|
||||
<Button onClick={handleFetchLatestPrice} icon={<SyncOutlined />}>
|
||||
{t('cryptoTailMonitor.manualOrder.fetchLatestPrice')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>
|
||||
{t('cryptoTailMonitor.manualOrder.orderSize')} ({t('cryptoTailMonitor.manualOrder.sizeUnit')})
|
||||
</Text>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={manualOrderModal.size ? parseFloat(manualOrderModal.size) : undefined}
|
||||
onChange={handleSizeChange}
|
||||
min={1}
|
||||
precision={2}
|
||||
placeholder="1"
|
||||
/>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={manualOrderModal.size ? parseFloat(manualOrderModal.size) : undefined}
|
||||
onChange={handleSizeChange}
|
||||
min={1}
|
||||
precision={2}
|
||||
placeholder="1"
|
||||
/>
|
||||
<Button onClick={handleMaxSize} type="primary" ghost>
|
||||
{t('cryptoTailMonitor.manualOrder.maxSize')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 4 }}>
|
||||
|
||||
Reference in New Issue
Block a user