From 921e96321fbd256385ca9325859dc9e2926e685c Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 2 Aug 2026 12:34:42 +0800 Subject: [PATCH] Align strategy UI prices with exchange tick sizes. Co-authored-by: Cursor --- frontend/src/format.ts | 43 ++++++++++++++++++++++++ frontend/src/pages/Plan.tsx | 63 +++++++++++++++++++---------------- frontend/src/pages/Trades.tsx | 12 +++++-- 3 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 frontend/src/format.ts diff --git a/frontend/src/format.ts b/frontend/src/format.ts new file mode 100644 index 0000000..1561793 --- /dev/null +++ b/frontend/src/format.ts @@ -0,0 +1,43 @@ +/** 价格按交易所常见精度:永续/指数 0.01;期权权利金 0.1 */ + +export type PxKind = "perp" | "option" | "index"; + +export function fmtExPx( + kind: PxKind, + px: number | null | undefined, +): string { + if (px == null || Number.isNaN(Number(px))) return "—"; + const n = Number(px); + if (kind === "option") { + return (Math.round(n * 10) / 10).toFixed(1); + } + return (Math.round(n * 100) / 100).toFixed(2); +} + +export function fmtTopEx( + kind: PxKind, + px: number | null | undefined, + sz: number | null | undefined, +): string { + if (px == null || Number.isNaN(Number(px))) return "—"; + const p = fmtExPx(kind, px); + if (sz == null || Number.isNaN(Number(sz))) return p; + const n = Number(sz); + const s = + n >= 100 ? n.toFixed(0) : n >= 10 ? n.toFixed(1) : n.toFixed(2); + return `${p}(${s})`; +} + +export function fmtBidLiqEx( + kind: PxKind, + px: number | null | undefined, + sz: number | null | undefined, +): string { + if (px == null || Number.isNaN(Number(px))) return "—"; + const p = fmtExPx(kind, px); + if (sz == null || Number.isNaN(Number(sz))) return p; + const n = Number(sz); + const s = + n >= 100 ? n.toFixed(0) : n >= 10 ? n.toFixed(1) : n.toFixed(2); + return `${p}/${s}`; +} diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index d03720b..1a097f5 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -1,27 +1,12 @@ import { useEffect, useState } from "react"; import { apiFetch, MarketSnapshot, PlanState } from "../api/client"; +import { fmtBidLiqEx, fmtExPx, fmtTopEx } from "../format"; function fmt(n: number | null | undefined, d = 2) { if (n == null || Number.isNaN(n)) return "—"; return n.toFixed(d); } -/** 卖一/买一 + 对应档量:8.80(12.5) / 7.20(8.0) */ -function fmtTop(px: number | null | undefined, sz: number | null | undefined) { - if (px == null || Number.isNaN(px)) return "—"; - if (sz == null || Number.isNaN(sz)) return fmt(px); - const s = sz >= 100 ? sz.toFixed(0) : sz >= 10 ? sz.toFixed(1) : sz.toFixed(2); - return `${fmt(px)}(${s})`; -} - -/** 持仓买一:价格/流动性张数,如 17.00/4000 */ -function fmtBidLiq(px: number | null | undefined, sz: number | null | undefined) { - if (px == null || Number.isNaN(px)) return "—"; - if (sz == null || Number.isNaN(sz)) return fmt(px); - const s = sz >= 100 ? sz.toFixed(0) : sz >= 10 ? sz.toFixed(1) : sz.toFixed(2); - return `${fmt(px)}/${s}`; -} - /** 期权杠杆 = 标的 ÷ 权利金 */ function optionLevLabel( underlying: number | null | undefined, @@ -607,11 +592,15 @@ export default function PlanPage() {
开仓价 - {fmt(pos?.perp_entry_px)} + + {fmtExPx("perp", pos?.perp_entry_px)} +
可平价 - {fmt(pos?.perp_mark_px)} + + {fmtExPx("perp", pos?.perp_mark_px)} +
浮盈亏 @@ -666,7 +655,12 @@ export default function PlanPage() {
期权持仓 到期 {pos?.expiry_ymd || "—"} - 行权 {fmt(pos?.strike, 0)} + + 行权{" "} + {pos?.strike != null + ? Math.round(Number(pos.strike)) + : "—"} + {fmt(pos?.option_qty_eth, 2)} ETH · {fmt(pos?.option_qty_contracts, 0)} 张 @@ -677,12 +671,18 @@ export default function PlanPage() {
开仓均价 - {fmt(pos?.option_entry_px)} + + {fmtExPx("option", pos?.option_entry_px)} +
买一 - {fmtBidLiq(pos?.option_mark_px, pos?.option_bid_sz)} + {fmtBidLiqEx( + "option", + pos?.option_mark_px, + pos?.option_bid_sz, + )}
@@ -722,16 +722,17 @@ export default function PlanPage() {

永续行情

买一 - {fmt(snap?.perp?.bid)} + {fmtExPx("perp", snap?.perp?.bid)}
卖一 - {fmt(snap?.perp?.ask)} + {fmtExPx("perp", snap?.perp?.ask)}
市价 - {fmt( + {fmtExPx( + "perp", snap?.perp?.mark_px ?? (snap?.perp?.bid != null && snap?.perp?.ask != null ? (snap.perp.bid + snap.perp.ask) / 2 @@ -741,13 +742,19 @@ export default function PlanPage() {
指数 - {fmt(snap?.index_px)} + {fmtExPx("index", snap?.index_px)}

{open ? "持仓期权" : "期权 ATM"} - {snap?.pair ? ` @ ${snap.pair.strike}` : ""} + {snap?.pair + ? ` @ ${ + snap.pair.strike != null + ? Math.round(Number(snap.pair.strike)) + : "—" + }` + : ""}

{(() => { const under = @@ -776,14 +783,14 @@ export default function PlanPage() {
Call {sideLabel} - {fmtTop(callPx, callSz)} + {fmtTopEx("option", callPx, callSz)} {!open ? ` · 杠杆 ${callLev}` : ""}
Put {sideLabel} - {fmtTop(putPx, putSz)} + {fmtTopEx("option", putPx, putSz)} {!open ? ` · 杠杆 ${putLev}` : ""}
diff --git a/frontend/src/pages/Trades.tsx b/frontend/src/pages/Trades.tsx index 8078e9b..97626c6 100644 --- a/frontend/src/pages/Trades.tsx +++ b/frontend/src/pages/Trades.tsx @@ -1,5 +1,6 @@ import { useEffect, useId, useMemo, useState } from "react"; import { apiFetch } from "../api/client"; +import { fmtExPx } from "../format"; import { closeReasonZh, fillDescZh, @@ -495,7 +496,7 @@ export default function TradesPage() { {fmtMovePoints(selectedGroup.move_points)} {selectedGroup.entry_index_px != null && selectedGroup.close_index_px != null - ? `(${fmt(selectedGroup.entry_index_px, 1)}→${fmt(selectedGroup.close_index_px, 1)})` + ? `(${fmtExPx("index", selectedGroup.entry_index_px)}→${fmtExPx("index", selectedGroup.close_index_px)})` : ""}
@@ -512,7 +513,10 @@ export default function TradesPage() {
结算指数 - {fmt(selectedGroup.expiry_settle.settle_index_px)} + {fmtExPx( + "index", + selectedGroup.expiry_settle.settle_index_px, + )}
@@ -554,7 +558,9 @@ export default function TradesPage() { ) : null} - {pxLabel} {f.fill_px.toFixed(4)} · 数量 {f.qty_eth} + {pxLabel}{" "} + {fmtExPx(isOpt ? "option" : "perp", f.fill_px)} · 数量{" "} + {f.qty_eth} {notional != null && Number.isFinite(notional) ? ` · 合计 ${notional.toFixed(2)}` : ""}{" "}