fix: adaptive price precision for meme coins

- Add adaptivePriceRound() in store/position.go for database storage
- Update position_builder.go to use adaptive precision for entry/exit prices
- Add Gate to OrderSync skip list in auto_trader.go
- Add debug logging in gate/order_sync.go for price parsing issues
- Create web/src/utils/format.ts with formatPrice() for frontend display
- Update TraderDashboardPage.tsx and PositionHistory.tsx to use adaptive formatting

Fixes issue where meme coin prices (e.g. 0.000000166) displayed as 0.0000
This commit is contained in:
tinkle-community
2026-02-06 00:46:48 +08:00
parent 22f6ddc045
commit 0b4f43d72b
7 changed files with 240 additions and 38 deletions

View File

@@ -3,6 +3,7 @@ import { api } from '../lib/api'
import { useLanguage } from '../contexts/LanguageContext'
import { t } from '../i18n/translations'
import { MetricTooltip } from './MetricTooltip'
import { formatPrice, formatQuantity } from '../utils/format'
import type {
HistoricalPosition,
TraderStats,
@@ -14,7 +15,7 @@ interface PositionHistoryProps {
traderId: string
}
// Format number with proper decimals
// Format number with proper decimals (for large numbers)
function formatNumber(value: number, decimals: number = 2): string {
if (Math.abs(value) >= 1000000) {
return (value / 1000000).toFixed(2) + 'M'
@@ -25,14 +26,6 @@ function formatNumber(value: number, decimals: number = 2): string {
return value.toFixed(decimals)
}
// Format price with proper decimals
function formatPrice(price: number): string {
if (!price || price === 0) return '-'
if (price >= 1000) return price.toFixed(2)
if (price >= 1) return price.toFixed(4)
return price.toFixed(6)
}
// Format duration from minutes
function formatDuration(minutes: number): string {
if (!minutes || minutes <= 0) return '-'
@@ -300,7 +293,7 @@ function PositionRow({ position }: { position: HistoricalPosition }) {
{/* Quantity */}
<td className="py-3 px-4 text-right font-mono" style={{ color: '#848E9C' }}>
{displayQty.toFixed(4)}
{formatQuantity(displayQty)}
</td>
{/* Position Value (Entry Price * Quantity) */}

View File

@@ -6,6 +6,7 @@ import { DecisionCard } from '../components/DecisionCard'
import { PositionHistory } from '../components/PositionHistory'
import { PunkAvatar, getTraderAvatar } from '../components/PunkAvatar'
import { confirmToast, notify } from '../lib/notify'
import { formatPrice, formatQuantity } from '../utils/format'
import { t, type Language } from '../i18n/translations'
import { LogOut, Loader2, Eye, EyeOff, Copy, Check } from 'lucide-react'
import { DeepVoidBackground } from '../components/DeepVoidBackground'
@@ -653,9 +654,9 @@ export function TraderDashboardPage({
{language === 'zh' ? '平仓' : 'Close'}
</button>
</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{pos.entry_price.toFixed(4)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{pos.mark_price.toFixed(4)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{pos.quantity.toFixed(4)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.entry_price)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{formatPrice(pos.mark_price)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-main">{formatQuantity(pos.quantity)}</td>
<td className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right text-nofx-text-main hidden md:table-cell">{(pos.quantity * pos.mark_price).toFixed(2)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-center text-nofx-gold hidden md:table-cell">{pos.leverage}x</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
@@ -667,7 +668,7 @@ export function TraderDashboardPage({
{pos.unrealized_pnl.toFixed(2)}
</span>
</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-muted hidden md:table-cell">{pos.liquidation_price.toFixed(4)}</td>
<td className="px-1 py-3 font-mono whitespace-nowrap text-right text-nofx-text-muted hidden md:table-cell">{formatPrice(pos.liquidation_price)}</td>
</tr>
))}
</tbody>

135
web/src/utils/format.ts Normal file
View File

@@ -0,0 +1,135 @@
/**
* 数字格式化工具
*
* formatPrice: 根据数值大小自适应显示精度,避免极小数显示为 0.0000
*/
/**
* 格式化价格,根据数值大小自适应精度
* 对于极小的数字(如 meme 币价格 0.000000166),会保留足够的有效数字
*
* @param price 价格数值
* @param minDecimals 最少小数位数(默认 2
* @returns 格式化后的字符串
*/
export function formatPrice(price: number | undefined | null, minDecimals = 2): string {
if (price === undefined || price === null || isNaN(price)) {
return '0'
}
if (price === 0) {
return '0'
}
const absPrice = Math.abs(price)
// 根据价格大小决定显示精度
let decimals: number
if (absPrice < 0.000001) {
// 极小价格 (如 CHEEMS, SHIB 等 meme 币)
decimals = 15
} else if (absPrice < 0.0001) {
// 很小价格 (如 PEPE, FLOKI, BONK)
decimals = 12
} else if (absPrice < 0.01) {
// 小价格
decimals = 10
} else if (absPrice < 1) {
// 中等价格
decimals = 8
} else if (absPrice < 1000) {
// 正常价格
decimals = 4
} else {
// 大价格 (如 BTC)
decimals = 2
}
// 确保至少有 minDecimals 位小数
decimals = Math.max(decimals, minDecimals)
// 格式化并去除尾部多余的零
let formatted = price.toFixed(decimals)
// 去除尾部零(保留小数点后至少 minDecimals 位)
if (formatted.includes('.')) {
// 先去掉所有尾部零
formatted = formatted.replace(/\.?0+$/, '')
// 如果小数位不足 minDecimals补零
const dotIndex = formatted.indexOf('.')
if (dotIndex === -1) {
formatted += '.' + '0'.repeat(minDecimals)
} else {
const currentDecimals = formatted.length - dotIndex - 1
if (currentDecimals < minDecimals) {
formatted += '0'.repeat(minDecimals - currentDecimals)
}
}
}
return formatted
}
/**
* 格式化数量,根据数值大小自适应精度
*
* @param quantity 数量
* @param minDecimals 最少小数位数(默认 2
* @returns 格式化后的字符串
*/
export function formatQuantity(quantity: number | undefined | null, minDecimals = 2): string {
if (quantity === undefined || quantity === null || isNaN(quantity)) {
return '0'
}
if (quantity === 0) {
return '0'
}
const absQty = Math.abs(quantity)
let decimals: number
if (absQty >= 1000000) {
decimals = 0
} else if (absQty >= 1000) {
decimals = 2
} else if (absQty >= 1) {
decimals = 4
} else {
decimals = 8
}
decimals = Math.max(decimals, minDecimals)
let formatted = quantity.toFixed(decimals)
if (formatted.includes('.')) {
formatted = formatted.replace(/\.?0+$/, '')
const dotIndex = formatted.indexOf('.')
if (dotIndex === -1) {
formatted += '.' + '0'.repeat(minDecimals)
} else {
const currentDecimals = formatted.length - dotIndex - 1
if (currentDecimals < minDecimals) {
formatted += '0'.repeat(minDecimals - currentDecimals)
}
}
}
return formatted
}
/**
* 格式化百分比
*
* @param value 百分比值
* @param decimals 小数位数(默认 2
* @returns 格式化后的字符串
*/
export function formatPercent(value: number | undefined | null, decimals = 2): string {
if (value === undefined || value === null || isNaN(value)) {
return '0.00'
}
return value.toFixed(decimals)
}
export default { formatPrice, formatQuantity, formatPercent }