修改:卖出支持小数

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 */ /** 价格(整数,已乘 10000 */
price: number price: number
side: number side: number
/** 数量(份额) */ /** 数量(micro units1_000_000 = 1 share支持 6 位小数份额) */
size: number size: number
taker: boolean taker: boolean
tokenID: string tokenID: string

View File

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