修改:卖出支持小数

This commit is contained in:
马丁 2026-07-04 22:09:29 +08:00
parent 537f3030e7
commit 604623bd37
2 changed files with 108 additions and 38 deletions

View File

@ -15,7 +15,7 @@ export interface ClobSubmitOrderRequest {
/** 价格(整数,已乘 10000 */
price: number
side: number
/** 数量(份额) */
/** 数量(micro units1_000_000 = 1 share支持 6 位小数份额) */
size: number
taker: boolean
tokenID: string

View File

@ -97,14 +97,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
variant="outlined"
@ -246,14 +247,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
variant="outlined"
class="shares-input-field"
hide-details
@ -357,14 +359,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span v-if="activeTab === 'sell'" class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
variant="outlined"
class="shares-input-field"
hide-details
@ -389,7 +392,7 @@
<v-btn class="share-btn" @click="setMaxShares">{{ t('trade.max') }}</v-btn>
</div>
<p v-if="sellSharesExceedsMax" class="shares-exceeds-max-hint">
{{ t('trade.sharesExceedsMax', { max: maxAvailableShares }) }}
{{ t('trade.sharesExceedsMax', { max: maxAvailableSharesDisplay }) }}
</p>
<div v-if="activeTab === 'buy'" class="matching-info">
<v-icon size="14">mdi-information</v-icon>
@ -548,14 +551,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
density="compact"
@ -685,14 +689,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
density="compact"
@ -786,14 +791,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span v-if="activeTab === 'sell'" class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
density="compact"
@ -990,14 +996,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
density="compact"
@ -1127,14 +1134,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
variant="outlined"
@ -1228,14 +1236,15 @@
<div class="shares-header sell-shares-header">
<span class="label">{{ t('trade.shares') }}</span>
<span v-if="activeTab === 'sell'" class="max-shares-inline"
>{{ t('trade.maxShares') }}: {{ maxAvailableShares }}</span
>{{ t('trade.maxShares') }}: {{ maxAvailableSharesDisplay }}</span
>
</div>
<div class="shares-input-wrapper">
<v-text-field
:model-value="shares"
type="number"
min="1"
:min="sharesInputMin"
:step="sharesInputStep"
class="shares-input-field"
hide-details
density="compact"
@ -1712,7 +1721,9 @@ const limitType = ref<'Market' | 'Limit'>(prefs.limitType)
const expirationEnabled = ref(false)
const selectedOption = ref<'yes' | 'no'>(props.initialOption ?? 'no')
const limitPrice = ref(0.82) // 01
const shares = ref(20) //
const shares = ref(20) // Buy Sell 6
const ORDER_SIZE_SCALE = 1_000_000
const ORDER_SIZE_MIN = 1 / ORDER_SIZE_SCALE
const expirationTime = ref('5m') //
const EXPIRATION_VALUES = [
'5m',
@ -1836,21 +1847,44 @@ const currentOptionPositionShares = computed(() => {
return totalShares
})
/** 最大可卖出份额 */
/** 最大可卖出份额(保留 6 位小数) */
const maxAvailableShares = computed(() => {
return Math.floor(currentOptionPositionShares.value)
return clampSellShares(currentOptionPositionShares.value)
})
/** 将 shares 限制为正整数(>= 1 */
/** 展示用最大可卖份额 */
const maxAvailableSharesDisplay = computed(() =>
trimTrailingZeros(maxAvailableShares.value.toFixed(6)),
)
const sharesInputMin = computed(() => (activeTab.value === 'sell' ? ORDER_SIZE_MIN : 1))
const sharesInputStep = computed(() => (activeTab.value === 'sell' ? ORDER_SIZE_MIN : 1))
/** 将 shares 限制为正整数(>= 1Buy 模式使用 */
function clampShares(v: number): number {
const n = Math.floor(Number.isFinite(v) ? v : 1)
return Math.max(1, n)
}
// Sell
/** Sell 模式:保留 6 位小数 */
function clampSellShares(v: number): number {
if (!Number.isFinite(v) || v <= 0) return 0
const scaled = Math.round(v * ORDER_SIZE_SCALE) / ORDER_SIZE_SCALE
return scaled < ORDER_SIZE_MIN ? 0 : scaled
}
function sharesToMicro(v: number): number {
return Math.round(v * ORDER_SIZE_SCALE)
}
//
const setMaxShares = () => {
const maxShares = currentOptionPositionShares.value
shares.value = maxShares > 0 ? clampShares(maxShares) : 0
if (activeTab.value === 'sell') {
shares.value = maxShares > 0 ? clampSellShares(maxShares) : 0
} else {
shares.value = maxShares > 0 ? clampShares(maxShares) : 0
}
}
function applyInitialOption(option: 'yes' | 'no') {
@ -1995,15 +2029,23 @@ const increasePrice = () => {
limitPrice.value = ALLOWED_LIMIT_PRICES[nextIdx] ?? limitPrice.value
}
/** 仅在值为正整数时更新 shares */
/** 更新 sharesBuy 仅正整数Sell 支持 6 位小数 */
function onSharesInput(v: unknown) {
const num = v == null ? NaN : Number(v)
if (!Number.isFinite(num)) return
if (activeTab.value === 'sell') {
if (num <= 0) return
const scaled = clampSellShares(num)
if (scaled <= 0) return
shares.value = scaled
return
}
const n = Math.floor(num)
if (!Number.isFinite(num) || n < 1 || num !== n) return
if (n < 1 || num !== n) return
shares.value = n
}
/** 只允许数字输入Shares 为正整数) */
/** 键盘输入Sell 允许小数点 */
function onSharesKeydown(e: KeyboardEvent) {
const key = e.key
const allowed = ['Backspace', 'Delete', 'Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End']
@ -2012,12 +2054,24 @@ function onSharesKeydown(e: KeyboardEvent) {
if (['a', 'c', 'v', 'x'].includes(key.toLowerCase())) return
}
if (key >= '0' && key <= '9') return
if (
activeTab.value === 'sell' &&
key === '.' &&
!String((e.target as HTMLInputElement)?.value ?? '').includes('.')
) {
return
}
e.preventDefault()
}
/** 粘贴时只接受正整数 */
/** 粘贴Sell 接受小数Buy 仅正整数 */
function onSharesPaste(e: ClipboardEvent) {
const text = e.clipboardData?.getData('text') ?? ''
if (activeTab.value === 'sell') {
const num = parseFloat(text)
if (!Number.isFinite(num) || num <= 0) e.preventDefault()
return
}
const num = parseInt(text, 10)
if (!Number.isFinite(num) || num < 1) e.preventDefault()
}
@ -2031,14 +2085,15 @@ const adjustShares = (amount: number) => {
const sellSharesExceedsMax = computed(
() =>
activeTab.value === 'sell' &&
maxAvailableShares.value >= 0 &&
shares.value > maxAvailableShares.value,
maxAvailableShares.value > 0 &&
sharesToMicro(shares.value) > sharesToMicro(maxAvailableShares.value),
)
// Sell使
const setSharesPercentage = (percentage: number) => {
const maxShares = currentOptionPositionShares.value || 100
shares.value = clampShares(Math.round((maxShares * percentage) / 100))
const maxShares = currentOptionPositionShares.value
if (!Number.isFinite(maxShares) || maxShares <= 0) return
shares.value = clampSellShares((maxShares * percentage) / 100)
}
// Market mode methods
@ -2160,8 +2215,8 @@ async function submitOrder() {
isNoAvailableSharesError.value = true
return
}
if (shares.value > maxShares) {
orderError.value = t('trade.sharesExceedsMax', { max: maxShares })
if (sharesToMicro(shares.value) > sharesToMicro(maxShares)) {
orderError.value = t('trade.sharesExceedsMax', { max: maxAvailableSharesDisplay.value })
isNoAvailableSharesError.value = false
return
}
@ -2179,8 +2234,23 @@ async function submitOrder() {
? parseExpirationTimestamp(expirationTime.value)
: 0
const rawSize = isMarket && activeTab.value === 'buy' ? amount.value : clampShares(shares.value)
const sizeValue = Math.round(rawSize * 1_000_000)
const rawSize =
isMarket && activeTab.value === 'buy'
? amount.value
: activeTab.value === 'sell'
? clampSellShares(shares.value)
: clampShares(shares.value)
if (activeTab.value === 'sell' && rawSize <= 0) {
orderError.value = t('trade.orderFailed')
isNoAvailableSharesError.value = false
return
}
const sizeValue = sharesToMicro(rawSize)
if (sizeValue <= 0) {
orderError.value = t('trade.orderFailed')
isNoAvailableSharesError.value = false
return
}
orderLoading.value = true
orderError.value = ''