Initial standalone crypto_okx with one-click deploy.
Add deploy/manage.sh bootstrap for git.bz121.com/dekun/crypto_okx and point docs at this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,567 @@
|
||||
"""OKX USDⓈ 期权:张数与权利金计算."""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import Any
|
||||
|
||||
|
||||
def ct_mult_from_meta(meta: dict[str, Any] | None) -> float:
|
||||
if not meta:
|
||||
return 0.01
|
||||
try:
|
||||
return float(meta.get("ctMult") or 0.01)
|
||||
except (TypeError, ValueError):
|
||||
return 0.01
|
||||
|
||||
|
||||
def min_sz_from_meta(meta: dict[str, Any] | None) -> int:
|
||||
if not meta:
|
||||
return 1
|
||||
try:
|
||||
return max(1, int(float(meta.get("minSz") or 1)))
|
||||
except (TypeError, ValueError):
|
||||
return 1
|
||||
|
||||
|
||||
def premium_per_sheet(quote_per_unit: float, ct_mult: float = 0.01) -> float:
|
||||
"""报价为每 1 ETH/BTC;每张权利金 = 报价 × ctMult."""
|
||||
return float(quote_per_unit) * float(ct_mult)
|
||||
|
||||
|
||||
def format_quote_liquidity(px: float | None, sz: float | None, *, px_decimals: int = 4) -> str | None:
|
||||
"""盘口展示:价格/张数,如 17.2/150."""
|
||||
if px is None:
|
||||
return None
|
||||
try:
|
||||
price = f"{float(px):.{px_decimals}f}".rstrip("0").rstrip(".")
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if sz is None:
|
||||
return price
|
||||
try:
|
||||
s = float(sz)
|
||||
size = str(int(s)) if abs(s - int(s)) < 1e-9 else str(s).rstrip("0").rstrip(".")
|
||||
except (TypeError, ValueError):
|
||||
return price
|
||||
return f"{price}/{size}"
|
||||
|
||||
|
||||
def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.01) -> float:
|
||||
return float(quote_per_unit) * float(eth_amount)
|
||||
|
||||
|
||||
# 买一相对标记价/内在价值低于该比例 → 视为残档,禁止按买盘自动/多档平仓
|
||||
BID_CLOSE_MIN_RATIO = 0.3
|
||||
|
||||
|
||||
def _safe_px(v: Any) -> float | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
x = float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return x if x > 0 else None
|
||||
|
||||
|
||||
def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px: float | None) -> float | None:
|
||||
o = (opt_type or "").strip().upper()
|
||||
if strike is None or index_px is None:
|
||||
return None
|
||||
try:
|
||||
k = float(strike)
|
||||
idx = float(index_px)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if o == "C" and idx > k:
|
||||
return idx - k
|
||||
if o == "P" and idx < k:
|
||||
return k - idx
|
||||
return None
|
||||
|
||||
|
||||
def is_stub_bid_px(
|
||||
bid_px: float | None,
|
||||
*,
|
||||
mark_px: float | None = None,
|
||||
intrinsic_px: float | None = None,
|
||||
min_ratio: float = BID_CLOSE_MIN_RATIO,
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
判断买一是否为无效残档(如标记 42、买一 0.2).
|
||||
返回 (is_stub, reason).
|
||||
"""
|
||||
bid = _safe_px(bid_px)
|
||||
if bid is None:
|
||||
return True, "无买一"
|
||||
ref = _safe_px(mark_px)
|
||||
ref_name = "标记价"
|
||||
intrinsic = _safe_px(intrinsic_px)
|
||||
if intrinsic is not None and (ref is None or intrinsic > ref):
|
||||
ref = intrinsic
|
||||
ref_name = "内在价值"
|
||||
if ref is None:
|
||||
return False, ""
|
||||
ratio = float(min_ratio) if min_ratio and min_ratio > 0 else BID_CLOSE_MIN_RATIO
|
||||
if bid < ref * ratio:
|
||||
return True, f"买一{bid:g}远低于{ref_name}{ref:g},属无效残档,禁止按买盘自动平仓"
|
||||
return False, ""
|
||||
|
||||
|
||||
def fetch_option_mark_px(ex: Any, inst_id: str) -> float | None:
|
||||
"""优先 mark-price 接口,失败则 None."""
|
||||
inst_id = (inst_id or "").strip()
|
||||
if not inst_id or ex is None:
|
||||
return None
|
||||
try:
|
||||
rows = ex.public_get_public_mark_price({"instType": "OPTION", "instId": inst_id}).get("data") or []
|
||||
if rows:
|
||||
return _safe_px(rows[0].get("markPx"))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def close_ref_prices(
|
||||
*,
|
||||
mark_px: float | None = None,
|
||||
opt_type: str | None = None,
|
||||
strike: float | None = None,
|
||||
index_px: float | None = None,
|
||||
) -> tuple[float | None, float | None]:
|
||||
"""返回 (mark_px, intrinsic_px) 供残档判断."""
|
||||
return _safe_px(mark_px), intrinsic_px_per_unit(opt_type, strike, index_px)
|
||||
|
||||
|
||||
def filter_bids_for_close(
|
||||
bids: list[dict[str, Any]] | None,
|
||||
*,
|
||||
mark_px: float | None = None,
|
||||
intrinsic_px: float | None = None,
|
||||
min_ratio: float = BID_CLOSE_MIN_RATIO,
|
||||
) -> tuple[list[dict[str, Any]], bool, str]:
|
||||
"""过滤不可用于平仓的残档买盘.返回 (usable_bids, had_stub_only, reason)."""
|
||||
raw = list(bids or [])
|
||||
usable: list[dict[str, Any]] = []
|
||||
stub_reason = ""
|
||||
for level in raw:
|
||||
px = _safe_px(level.get("px") if isinstance(level, dict) else None)
|
||||
stub, reason = is_stub_bid_px(px, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_ratio)
|
||||
if stub:
|
||||
if not stub_reason:
|
||||
stub_reason = reason or "买一无效"
|
||||
continue
|
||||
usable.append(level)
|
||||
if raw and not usable:
|
||||
return [], True, stub_reason or "暂无有效买盘"
|
||||
return usable, False, ""
|
||||
|
||||
|
||||
def estimate_close_by_bids(
|
||||
bids: list[dict[str, Any]] | None,
|
||||
sheets: int | float,
|
||||
*,
|
||||
ct_mult: float = 0.01,
|
||||
premium_paid: float | None = None,
|
||||
mark_px: float | None = None,
|
||||
intrinsic_px: float | None = None,
|
||||
min_bid_ratio: float = BID_CLOSE_MIN_RATIO,
|
||||
max_levels: int = 1,
|
||||
) -> dict[str, Any]:
|
||||
"""按买盘估算限价卖出可收回金额;默认只估算买一(与实盘平仓一致);残档不参与."""
|
||||
target = max(0, int(float(sheets or 0)))
|
||||
remaining = target
|
||||
total_received = 0.0
|
||||
levels: list[dict[str, Any]] = []
|
||||
max_lv = max(1, int(max_levels or 1))
|
||||
empty = {
|
||||
"levels": [],
|
||||
"covered_sheets": 0,
|
||||
"uncovered_sheets": target,
|
||||
"total_received": 0.0,
|
||||
"avg_px": None,
|
||||
"estimated_pnl": None,
|
||||
"estimated_pnl_ratio_pct": None,
|
||||
"bid_invalid": False,
|
||||
"bid_invalid_reason": None,
|
||||
"auto_close_blocked": False,
|
||||
"max_levels": max_lv,
|
||||
}
|
||||
if target <= 0 or ct_mult <= 0:
|
||||
return empty
|
||||
usable, stub_only, stub_reason = filter_bids_for_close(
|
||||
bids, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_bid_ratio
|
||||
)
|
||||
if stub_only:
|
||||
out = dict(empty)
|
||||
out["bid_invalid"] = True
|
||||
out["bid_invalid_reason"] = stub_reason
|
||||
out["auto_close_blocked"] = True
|
||||
out["raw_bid_px"] = _safe_px((bids or [{}])[0].get("px")) if bids else None
|
||||
return out
|
||||
for i, level in enumerate(usable[:max_lv], start=1):
|
||||
if remaining <= 0:
|
||||
break
|
||||
try:
|
||||
px = float(level.get("px"))
|
||||
sz = int(float(level.get("sz")))
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
continue
|
||||
if px <= 0 or sz <= 0:
|
||||
continue
|
||||
take = min(remaining, sz)
|
||||
eth_amount = eth_amount_from_sheets(take, ct_mult)
|
||||
received = total_premium(px, eth_amount)
|
||||
levels.append(
|
||||
{
|
||||
"level": i,
|
||||
"px": px,
|
||||
"available_sheets": sz,
|
||||
"sheets": take,
|
||||
"eth_amount": eth_amount,
|
||||
"received": round(received, 4),
|
||||
}
|
||||
)
|
||||
total_received += received
|
||||
remaining -= take
|
||||
covered = target - remaining
|
||||
avg_px = (total_received / eth_amount_from_sheets(covered, ct_mult)) if covered > 0 else None
|
||||
# 净盈亏 = 本轮买盘可回收 − 全部权利金(买一不够时剩余张数计入 uncovered)
|
||||
estimated_pnl = None
|
||||
estimated_pnl_ratio_pct = None
|
||||
if premium_paid is not None and covered > 0:
|
||||
paid = float(premium_paid)
|
||||
estimated_pnl = round(total_received - paid, 4)
|
||||
if paid > 0:
|
||||
estimated_pnl_ratio_pct = round(estimated_pnl / paid * 100.0, 2)
|
||||
return {
|
||||
"levels": levels,
|
||||
"covered_sheets": covered,
|
||||
"uncovered_sheets": remaining,
|
||||
"total_received": round(total_received, 4),
|
||||
"avg_px": round(avg_px, 4) if avg_px is not None else None,
|
||||
"estimated_pnl": estimated_pnl,
|
||||
"estimated_pnl_ratio_pct": estimated_pnl_ratio_pct,
|
||||
"bid_invalid": False,
|
||||
"bid_invalid_reason": None,
|
||||
"auto_close_blocked": False,
|
||||
"max_levels": max_lv,
|
||||
}
|
||||
|
||||
|
||||
def sheets_from_eth_amount(eth_amount: float, ct_mult: float = 0.01) -> int:
|
||||
if eth_amount <= 0 or ct_mult <= 0:
|
||||
return 0
|
||||
return int(math.floor(eth_amount / ct_mult + 1e-12))
|
||||
|
||||
|
||||
def eth_amount_from_sheets(sheets: int, ct_mult: float = 0.01) -> float:
|
||||
return round(int(sheets) * float(ct_mult), 8)
|
||||
|
||||
|
||||
def resolve_budget_full_usdc(trading_usdc: float, trade_budget_usdc: float) -> float:
|
||||
"""按可用余额打满:余额大于预算用预算,否则用余额."""
|
||||
return min(float(trading_usdc), float(trade_budget_usdc))
|
||||
|
||||
|
||||
def resolve_compound_full_usdc(
|
||||
trading_usdc: float,
|
||||
*,
|
||||
cap_enabled: bool = False,
|
||||
cap_usdc: float | None = None,
|
||||
) -> float:
|
||||
"""全仓复利:默认用期权交易户全部可用;上限开关开启时再封顶."""
|
||||
bal = max(0.0, float(trading_usdc or 0))
|
||||
if not cap_enabled:
|
||||
return bal
|
||||
try:
|
||||
cap = float(cap_usdc) if cap_usdc is not None else 0.0
|
||||
except (TypeError, ValueError):
|
||||
cap = 0.0
|
||||
if cap <= 0:
|
||||
return bal
|
||||
return min(bal, cap)
|
||||
|
||||
|
||||
def calc_order_size(
|
||||
*,
|
||||
quote_per_unit: float,
|
||||
ct_mult: float,
|
||||
min_sz: int,
|
||||
budget_usdc: float | None = None,
|
||||
budget_buffer: float = 0.95,
|
||||
eth_amount: float | None = None,
|
||||
sheets: int | None = None,
|
||||
budget_cap: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
返回 sheets, eth_amount, total_premium.
|
||||
mode: budget_full / eth_amount / sheets.
|
||||
"""
|
||||
if quote_per_unit <= 0:
|
||||
return {"ok": False, "msg": "卖一价无效", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
|
||||
if sheets is not None and int(sheets) > 0:
|
||||
sheets = int(sheets)
|
||||
elif eth_amount is not None and eth_amount > 0:
|
||||
sheets = sheets_from_eth_amount(eth_amount, ct_mult)
|
||||
elif budget_usdc is not None and budget_usdc > 0:
|
||||
eff = float(budget_usdc) * float(budget_buffer)
|
||||
per_sheet = premium_per_sheet(quote_per_unit, ct_mult)
|
||||
if per_sheet <= 0:
|
||||
return {"ok": False, "msg": "无法计算单张权利金", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
sheets = int(math.floor(eff / per_sheet))
|
||||
else:
|
||||
return {"ok": False, "msg": "请指定预算,币数量或张数", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0}
|
||||
|
||||
if sheets < min_sz:
|
||||
per = premium_per_sheet(quote_per_unit, ct_mult)
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"预算不足,无法买入 {min_sz} 张(单张约 {per:.4f} USDC)",
|
||||
"sheets": sheets,
|
||||
"eth_amount": eth_amount_from_sheets(sheets, ct_mult),
|
||||
"total_premium": total_premium(quote_per_unit, eth_amount_from_sheets(sheets, ct_mult)),
|
||||
}
|
||||
|
||||
eth = eth_amount_from_sheets(sheets, ct_mult)
|
||||
prem = total_premium(quote_per_unit, eth)
|
||||
if budget_cap is not None and prem > float(budget_cap) + 1e-9:
|
||||
return {
|
||||
"ok": False,
|
||||
"msg": f"权利金 {prem:.4f} 超过单笔上限 {budget_cap} USDC",
|
||||
"sheets": sheets,
|
||||
"eth_amount": eth,
|
||||
"total_premium": prem,
|
||||
}
|
||||
return {"ok": True, "msg": "", "sheets": sheets, "eth_amount": eth, "total_premium": prem}
|
||||
|
||||
|
||||
def is_shallow_itm(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float,
|
||||
index_px: float,
|
||||
max_dist_usd: float,
|
||||
) -> bool:
|
||||
o = (opt_type or "").upper()
|
||||
if o == "C":
|
||||
if strike >= index_px:
|
||||
return False
|
||||
return (index_px - strike) <= max_dist_usd
|
||||
if o == "P":
|
||||
if strike <= index_px:
|
||||
return False
|
||||
return (strike - index_px) <= max_dist_usd
|
||||
return False
|
||||
|
||||
|
||||
def option_moneyness(*, opt_type: str, strike: float, index_px: float) -> str:
|
||||
"""返回 itm / otm / atm."""
|
||||
o = (opt_type or "").upper()
|
||||
if strike is None or index_px is None or index_px <= 0:
|
||||
return "unknown"
|
||||
atm_band = max(index_px * 0.002, 2.0)
|
||||
if abs(strike - index_px) <= atm_band:
|
||||
return "atm"
|
||||
if o == "C":
|
||||
return "itm" if strike < index_px else "otm"
|
||||
if o == "P":
|
||||
return "itm" if strike > index_px else "otm"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def option_moneyness_label(moneyness: str) -> str:
|
||||
return {"itm": "实值", "otm": "虚值", "atm": "平值"}.get((moneyness or "").lower(), "")
|
||||
|
||||
|
||||
def expiry_breakeven_from_ask(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float | None,
|
||||
ask_px: float | None,
|
||||
mark_px: float | None = None,
|
||||
) -> float | None:
|
||||
"""买入前预估到期平衡:权利金按卖一;无卖一时回退标记价."""
|
||||
prem = ask_px if ask_px is not None and ask_px > 0 else mark_px
|
||||
return expiry_breakeven_px(opt_type=opt_type, strike=strike, avg_px=prem)
|
||||
|
||||
|
||||
def expiry_breakeven_px(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float | None,
|
||||
avg_px: float | None,
|
||||
be_px_api: float | None = None,
|
||||
) -> float | None:
|
||||
"""到期平衡点:持有至到期时标的指数盈亏为 0 的价格.优先 OKX bePx."""
|
||||
if be_px_api is not None and be_px_api > 0:
|
||||
return round(float(be_px_api), 2)
|
||||
if strike is None or avg_px is None:
|
||||
return None
|
||||
o = (opt_type or "").upper()
|
||||
if o == "C":
|
||||
return round(strike + avg_px, 2)
|
||||
if o == "P":
|
||||
return round(strike - avg_px, 2)
|
||||
return None
|
||||
|
||||
|
||||
def close_breakeven_idx(
|
||||
*,
|
||||
opt_type: str,
|
||||
idx_px: float | None,
|
||||
mark_px: float | None,
|
||||
avg_px: float | None,
|
||||
delta_pa: float | None = None,
|
||||
pos: float = 0,
|
||||
ct_mult: float = 0.01,
|
||||
) -> float | None:
|
||||
"""
|
||||
平掉回本:标的指数达到该价位时,按标记价平仓近似盈亏为 0.
|
||||
优先用 deltaPA 线性外推,否则用时间价值近似(适合短期轻度实值).
|
||||
"""
|
||||
if idx_px is None or mark_px is None or avg_px is None:
|
||||
return None
|
||||
eth_amt = abs(float(pos)) * float(ct_mult)
|
||||
if eth_amt > 1e-12 and delta_pa is not None and abs(float(delta_pa)) > 1e-12:
|
||||
slope = float(delta_pa) / eth_amt
|
||||
return round(float(idx_px) + (float(avg_px) - float(mark_px)) / slope, 2)
|
||||
o = (opt_type or "").upper()
|
||||
if o == "C":
|
||||
return round(float(idx_px) + float(avg_px) - float(mark_px), 2)
|
||||
if o == "P":
|
||||
return round(float(idx_px) + float(mark_px) - float(avg_px), 2)
|
||||
return None
|
||||
|
||||
|
||||
def idx_distance_to_be(idx_px: float | None, be_px: float | None) -> float | None:
|
||||
"""指数距平衡点(正=指数需上涨才到平衡点)."""
|
||||
if idx_px is None or be_px is None:
|
||||
return None
|
||||
return round(float(be_px) - float(idx_px), 2)
|
||||
|
||||
|
||||
def format_options_breakeven_line(
|
||||
*,
|
||||
expiry_be_px: float | None,
|
||||
close_be_px: float | None,
|
||||
idx_px: float | None = None,
|
||||
) -> str:
|
||||
"""持仓摘要行:到期平衡 / 平掉回本."""
|
||||
parts: list[str] = []
|
||||
if expiry_be_px is not None:
|
||||
parts.append(f"到期平衡{expiry_be_px:.0f}")
|
||||
if close_be_px is not None:
|
||||
parts.append(f"平掉回本{close_be_px:.0f}")
|
||||
if idx_px is not None and parts:
|
||||
return " ".join(parts) + f"(指数{idx_px:.0f})"
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def estimate_expiry_value_at_index(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float | None,
|
||||
target_idx: float | None,
|
||||
eth_amount: float | None,
|
||||
) -> float | None:
|
||||
"""到期测算:目标指数价下期权内在价值总额(不含已付权利金)."""
|
||||
if strike is None or target_idx is None or eth_amount is None:
|
||||
return None
|
||||
if eth_amount <= 0:
|
||||
return None
|
||||
o = (opt_type or "").upper()
|
||||
if o == "C":
|
||||
intrinsic = max(0.0, float(target_idx) - float(strike))
|
||||
elif o == "P":
|
||||
intrinsic = max(0.0, float(strike) - float(target_idx))
|
||||
else:
|
||||
return None
|
||||
return round(intrinsic * float(eth_amount), 2)
|
||||
|
||||
|
||||
def estimate_expiry_profit_at_index(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float | None,
|
||||
target_idx: float | None,
|
||||
entry_px: float | None,
|
||||
eth_amount: float | None,
|
||||
total_premium: float | None = None,
|
||||
) -> float | None:
|
||||
"""到期测算:目标指数价下净盈利 = 预计价值 − 权利金."""
|
||||
value = estimate_expiry_value_at_index(
|
||||
opt_type=opt_type,
|
||||
strike=strike,
|
||||
target_idx=target_idx,
|
||||
eth_amount=eth_amount,
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
prem = total_premium
|
||||
if prem is None and entry_px is not None and eth_amount is not None:
|
||||
prem = float(entry_px) * float(eth_amount)
|
||||
if prem is None:
|
||||
return None
|
||||
return round(float(value) - float(prem), 2)
|
||||
|
||||
|
||||
def equivalent_contract_leverage(
|
||||
*,
|
||||
index_px: float | None,
|
||||
eth_amount: float | None,
|
||||
total_premium: float | None,
|
||||
) -> float | None:
|
||||
"""名义价值 / 权利金,近似相当于永续合约杠杆倍数(测算用)."""
|
||||
if index_px is None or eth_amount is None or total_premium is None:
|
||||
return None
|
||||
if eth_amount <= 0 or total_premium <= 0:
|
||||
return None
|
||||
return round(float(index_px) * float(eth_amount) / float(total_premium), 1)
|
||||
|
||||
|
||||
def straddle_ask_per_unit(
|
||||
call_ask: float | None,
|
||||
put_ask: float | None,
|
||||
) -> float | None:
|
||||
"""跨式双买:每 1 标的币的卖一报价之和."""
|
||||
if call_ask is None or put_ask is None:
|
||||
return None
|
||||
if float(call_ask) <= 0 or float(put_ask) <= 0:
|
||||
return None
|
||||
return round(float(call_ask) + float(put_ask), 4)
|
||||
|
||||
|
||||
def straddle_premium_total(
|
||||
call_ask: float | None,
|
||||
put_ask: float | None,
|
||||
eth_amount: float | None,
|
||||
) -> float | None:
|
||||
"""跨式双买权利金总额(USDC)."""
|
||||
per = straddle_ask_per_unit(call_ask, put_ask)
|
||||
if per is None or eth_amount is None or float(eth_amount) <= 0:
|
||||
return None
|
||||
return round(per * float(eth_amount), 2)
|
||||
|
||||
|
||||
def straddle_breakeven_band(
|
||||
strike: float | None,
|
||||
combined_ask_per_unit: float | None,
|
||||
) -> tuple[float | None, float | None]:
|
||||
"""跨式到期平衡带:下平衡 ~ 上平衡(按双卖一报价和)."""
|
||||
if strike is None or combined_ask_per_unit is None:
|
||||
return None, None
|
||||
k = float(strike)
|
||||
d = float(combined_ask_per_unit)
|
||||
return round(k - d, 2), round(k + d, 2)
|
||||
|
||||
|
||||
def format_straddle_band(
|
||||
strike: float | None,
|
||||
combined_ask_per_unit: float | None,
|
||||
) -> str:
|
||||
lo, hi = straddle_breakeven_band(strike, combined_ask_per_unit)
|
||||
if lo is None or hi is None:
|
||||
return ""
|
||||
return f"{lo:.0f} ~ {hi:.0f}"
|
||||
Reference in New Issue
Block a user