Show open-capacity labels, enlarge funds, move rules; WeCom on short funds.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,6 +17,7 @@ from ..env_store import live_ready
|
|||||||
from .clock import can_open_new, window_key
|
from .clock import can_open_new, window_key
|
||||||
from .exits import check_expiry_close, check_exits, resolve_exit_target
|
from .exits import check_expiry_close, check_exits, resolve_exit_target
|
||||||
from .group import next_group_id
|
from .group import next_group_id
|
||||||
|
from .open_capacity import assess_open_capacity, maybe_notify_funds_short
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -101,6 +102,23 @@ class StrategyEngine:
|
|||||||
self._set_state(last_error=None)
|
self._set_state(last_error=None)
|
||||||
last_error = None
|
last_error = None
|
||||||
allow_open = can_open_new(skip_weekends=skip_weekends)
|
allow_open = can_open_new(skip_weekends=skip_weekends)
|
||||||
|
try:
|
||||||
|
open_cap = assess_open_capacity(self.db)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("assess_open_capacity failed")
|
||||||
|
open_cap = {
|
||||||
|
"perp_can_open": None,
|
||||||
|
"option_can_open": None,
|
||||||
|
"perp_label": f"永续{int(round(leverage))}x —",
|
||||||
|
"option_label": "期权 —",
|
||||||
|
"funds_ok": False,
|
||||||
|
"leverage": leverage,
|
||||||
|
}
|
||||||
|
if open_cap.get("perp_can_open") is False or open_cap.get("option_can_open") is False:
|
||||||
|
try:
|
||||||
|
maybe_notify_funds_short(open_cap)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
return {
|
return {
|
||||||
"running": bool(row["running"]),
|
"running": bool(row["running"]),
|
||||||
"phase": row["phase"],
|
"phase": row["phase"],
|
||||||
@@ -120,6 +138,7 @@ class StrategyEngine:
|
|||||||
"atm_open_offset_enabled": atm_off_on,
|
"atm_open_offset_enabled": atm_off_on,
|
||||||
"max_atm_open_offset": max_atm_off,
|
"max_atm_open_offset": max_atm_off,
|
||||||
"can_open": allow_open,
|
"can_open": allow_open,
|
||||||
|
"open_capacity": open_cap,
|
||||||
"last_error": last_error,
|
"last_error": last_error,
|
||||||
"position": upl,
|
"position": upl,
|
||||||
"residuals": self.matcher.list_residual_options(pending_only=True),
|
"residuals": self.matcher.list_residual_options(pending_only=True),
|
||||||
@@ -593,6 +612,22 @@ class StrategyEngine:
|
|||||||
self._set_state(phase="open", last_error="有未平仓,禁止开下一组")
|
self._set_state(phase="open", last_error="有未平仓,禁止开下一组")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
cap = assess_open_capacity(self.db)
|
||||||
|
if cap.get("perp_can_open") is False or cap.get("option_can_open") is False:
|
||||||
|
detail = (
|
||||||
|
f"{cap.get('perp_label')} · {cap.get('option_label')};"
|
||||||
|
f"永续需≈{cap.get('perp_need_usdt')}U/有{cap.get('perp_have_usdt')}U,"
|
||||||
|
f"期权需≈{cap.get('option_need_usdc')}U/有{cap.get('option_have_usdc')}U"
|
||||||
|
)
|
||||||
|
self._set_state(phase="wait_funds", last_error=f"资金不足,暂不可开新仓:{detail}")
|
||||||
|
maybe_notify_funds_short(cap)
|
||||||
|
return
|
||||||
|
if st["phase"] == "wait_funds":
|
||||||
|
self._set_state(phase="idle", last_error=None)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("open capacity gate failed")
|
||||||
|
|
||||||
self._set_state(phase="wait_signal")
|
self._set_state(phase="wait_signal")
|
||||||
pick = await get_session().pick_for_open_async()
|
pick = await get_session().pick_for_open_async()
|
||||||
if pick is None:
|
if pick is None:
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""开仓资金可开判定:永续保证金 + 期权权利金。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from ..config import get_settings
|
||||||
|
from ..models.db import Database, get_db
|
||||||
|
from ..sim.funds_wallets import SimFundsWallets
|
||||||
|
from ..sim.ledger import Ledger
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_live_bal_cache: dict[str, Any] = {"ts": 0.0, "data": None}
|
||||||
|
_LIVE_BAL_TTL_SEC = 8.0
|
||||||
|
|
||||||
|
_last_notify_key: str | None = None
|
||||||
|
_last_notify_ms: float = 0.0
|
||||||
|
_NOTIFY_DEDUP_SEC = 600.0 # 同状态 10 分钟内不重复推
|
||||||
|
|
||||||
|
|
||||||
|
def _f(v: Any) -> float | None:
|
||||||
|
try:
|
||||||
|
if v is None or v == "":
|
||||||
|
return None
|
||||||
|
return float(v)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _index_and_option_ask() -> tuple[float | None, float | None]:
|
||||||
|
"""指数价 + 期权卖一粗估(取 Call/Put 卖一较大者,偏保守)。"""
|
||||||
|
try:
|
||||||
|
from .session import get_session
|
||||||
|
|
||||||
|
snap = get_session().snapshot()
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
idx = _f(getattr(snap, "index_px", None))
|
||||||
|
if idx is None and snap.perp:
|
||||||
|
idx = _f(snap.perp.mark_px) or _f(snap.perp.ask) or _f(snap.perp.bid)
|
||||||
|
asks: list[float] = []
|
||||||
|
for leg in (snap.call, snap.put):
|
||||||
|
if leg is None:
|
||||||
|
continue
|
||||||
|
a = _f(leg.ask)
|
||||||
|
if a is not None and a > 0:
|
||||||
|
asks.append(a)
|
||||||
|
ask = max(asks) if asks else None
|
||||||
|
return idx, ask
|
||||||
|
|
||||||
|
|
||||||
|
def _live_balances() -> dict[str, float | None]:
|
||||||
|
now = time.time()
|
||||||
|
if _live_bal_cache["data"] is not None and now - float(_live_bal_cache["ts"]) < _LIVE_BAL_TTL_SEC:
|
||||||
|
return dict(_live_bal_cache["data"])
|
||||||
|
out: dict[str, float | None] = {
|
||||||
|
"funding_usdt": None,
|
||||||
|
"trading_usdt": None,
|
||||||
|
"options_funding_usdc": None,
|
||||||
|
"options_trading_usdc": None,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
from ..live.okx_funds import OkxFundsClient
|
||||||
|
|
||||||
|
client = OkxFundsClient()
|
||||||
|
try:
|
||||||
|
bal = client.fetch_balances()
|
||||||
|
for k in out:
|
||||||
|
out[k] = _f(bal.get(k))
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("open_capacity live balance failed: %s", e)
|
||||||
|
_live_bal_cache["ts"] = now
|
||||||
|
_live_bal_cache["data"] = dict(out)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _sim_balances(db: Database) -> dict[str, float]:
|
||||||
|
w = SimFundsWallets(db).snapshot()
|
||||||
|
led = Ledger(db).snapshot()
|
||||||
|
avail = float(led.get("available") or 0)
|
||||||
|
funding = float(w.get("funding_usdt") or 0)
|
||||||
|
trading = float(w.get("trading_usdt") or 0)
|
||||||
|
opt_f = float(w.get("options_funding_usdc") or 0)
|
||||||
|
opt_t = float(w.get("options_trading_usdc") or 0)
|
||||||
|
# 钱包未播种时回退账本可用
|
||||||
|
usdt = funding + trading
|
||||||
|
if usdt < 1e-9:
|
||||||
|
usdt = avail
|
||||||
|
usdc = opt_f + opt_t
|
||||||
|
if usdc < 1e-9:
|
||||||
|
# SIM 早期权利金从账本扣;无 USDC 钱包时用可用资金估期权可开
|
||||||
|
usdc = avail
|
||||||
|
return {
|
||||||
|
"perp_usdt": usdt,
|
||||||
|
"option_usdc": usdc,
|
||||||
|
"ledger_available": avail,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def assess_open_capacity(db: Database | None = None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
返回永续/期权是否有足够资金开新仓。
|
||||||
|
- 永续:需 USDT >= 名义/杠杆
|
||||||
|
- 期权:需 USDC(或 SIM 回退可用) >= 卖一×名义×(1+费率)
|
||||||
|
"""
|
||||||
|
db = db or get_db()
|
||||||
|
s = get_settings()
|
||||||
|
ledger = Ledger(db)
|
||||||
|
lev = float(ledger.get_setting_float("leverage", s.leverage) or 3)
|
||||||
|
if lev <= 0:
|
||||||
|
lev = 3.0
|
||||||
|
perp_qty = float(ledger.get_setting_float("perp_qty_eth", s.perp_qty_eth) or 1)
|
||||||
|
opt_qty = float(ledger.get_setting_float("option_qty_eth", s.option_qty_eth) or 2)
|
||||||
|
fee_rate = float(ledger.get_setting_float("fee_rate", s.fee_rate) or 0.0005)
|
||||||
|
|
||||||
|
idx, ask = _index_and_option_ask()
|
||||||
|
margin_need = (float(idx) * perp_qty / lev) if idx and idx > 0 else None
|
||||||
|
premium_need = (
|
||||||
|
float(ask) * opt_qty * (1.0 + fee_rate) if ask is not None and ask > 0 else None
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.is_sim:
|
||||||
|
bal = _sim_balances(db)
|
||||||
|
have_perp = float(bal["perp_usdt"])
|
||||||
|
have_opt = float(bal["option_usdc"])
|
||||||
|
else:
|
||||||
|
live = _live_balances()
|
||||||
|
# 永续用交易账户;若为空则合并资金账户(提示需划转但仍可显示)
|
||||||
|
t = live.get("trading_usdt")
|
||||||
|
f = live.get("funding_usdt")
|
||||||
|
if t is not None and t > 1e-9:
|
||||||
|
have_perp = float(t)
|
||||||
|
elif t is not None or f is not None:
|
||||||
|
have_perp = float(t or 0) + float(f or 0)
|
||||||
|
else:
|
||||||
|
have_perp = None
|
||||||
|
of_ = live.get("options_funding_usdc")
|
||||||
|
ot_ = live.get("options_trading_usdc")
|
||||||
|
if of_ is None and ot_ is None:
|
||||||
|
have_opt = None
|
||||||
|
else:
|
||||||
|
have_opt = float(of_ or 0) + float(ot_ or 0)
|
||||||
|
|
||||||
|
perp_ok: bool | None
|
||||||
|
if margin_need is None or have_perp is None:
|
||||||
|
perp_ok = None
|
||||||
|
else:
|
||||||
|
perp_ok = have_perp + 1e-9 >= margin_need
|
||||||
|
|
||||||
|
opt_ok: bool | None
|
||||||
|
if premium_need is None or have_opt is None:
|
||||||
|
opt_ok = None
|
||||||
|
else:
|
||||||
|
opt_ok = have_opt + 1e-9 >= premium_need
|
||||||
|
|
||||||
|
lev_i = int(round(lev)) if abs(lev - round(lev)) < 1e-9 else lev
|
||||||
|
if perp_ok is True:
|
||||||
|
perp_label = f"永续{lev_i}x 可开"
|
||||||
|
elif perp_ok is False:
|
||||||
|
perp_label = f"永续{lev_i}x 不可开"
|
||||||
|
else:
|
||||||
|
perp_label = f"永续{lev_i}x —"
|
||||||
|
|
||||||
|
if opt_ok is True:
|
||||||
|
opt_label = "期权可开"
|
||||||
|
elif opt_ok is False:
|
||||||
|
opt_label = "期权不可开"
|
||||||
|
else:
|
||||||
|
opt_label = "期权 —"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"leverage": lev,
|
||||||
|
"perp_qty_eth": perp_qty,
|
||||||
|
"option_qty_eth": opt_qty,
|
||||||
|
"index_px": idx,
|
||||||
|
"option_ask": ask,
|
||||||
|
"perp_need_usdt": round(margin_need, 2) if margin_need is not None else None,
|
||||||
|
"option_need_usdc": round(premium_need, 2) if premium_need is not None else None,
|
||||||
|
"perp_have_usdt": round(have_perp, 2) if have_perp is not None else None,
|
||||||
|
"option_have_usdc": round(have_opt, 2) if have_opt is not None else None,
|
||||||
|
"perp_can_open": perp_ok,
|
||||||
|
"option_can_open": opt_ok,
|
||||||
|
"perp_label": perp_label,
|
||||||
|
"option_label": opt_label,
|
||||||
|
"funds_ok": (perp_ok is True and opt_ok is True),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_notify_funds_short(cap: dict[str, Any] | None = None) -> None:
|
||||||
|
"""资金不足时企业微信推送(去重)。"""
|
||||||
|
global _last_notify_key, _last_notify_ms
|
||||||
|
cap = cap or assess_open_capacity()
|
||||||
|
parts: list[str] = []
|
||||||
|
if cap.get("perp_can_open") is False:
|
||||||
|
parts.append(
|
||||||
|
f"永续不足:需约 {cap.get('perp_need_usdt')}U,现有 {cap.get('perp_have_usdt')}U"
|
||||||
|
)
|
||||||
|
if cap.get("option_can_open") is False:
|
||||||
|
parts.append(
|
||||||
|
f"期权不足:需约 {cap.get('option_need_usdc')}U,现有 {cap.get('option_have_usdc')}U"
|
||||||
|
)
|
||||||
|
if not parts:
|
||||||
|
return
|
||||||
|
key = "|".join(parts)
|
||||||
|
now = time.time()
|
||||||
|
if key == _last_notify_key and now - _last_notify_ms < _NOTIFY_DEDUP_SEC:
|
||||||
|
return
|
||||||
|
_last_notify_key = key
|
||||||
|
_last_notify_ms = now
|
||||||
|
try:
|
||||||
|
from ..notify import wecom
|
||||||
|
|
||||||
|
wecom.notify_async(
|
||||||
|
wecom.build_markdown(
|
||||||
|
tag=wecom.TAG_FAULT,
|
||||||
|
title="资金不足 · 无法开新仓",
|
||||||
|
lines=[
|
||||||
|
f"**永续**: {cap.get('perp_label')}",
|
||||||
|
f"**期权**: {cap.get('option_label')}",
|
||||||
|
*[f"**详情**: {p}" for p in parts],
|
||||||
|
"请划转/兑换后重试。",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("wecom funds notify failed")
|
||||||
@@ -5,6 +5,16 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-07-29 — 权益杠杆可开判定 + 资金不足微信推送
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
1. 计划页「权益 / 杠杆」改为 **永续Nx 可开 / 期权可开**(绿)或 **不可开**(红);按保证金与权利金估算对比可用资金。
|
||||||
|
2. 资金不足时企业微信推送(10 分钟去重);策略开仓前拦截并进入 `wait_funds`。
|
||||||
|
3. 「规则说明」移到启动策略按钮上方;顶栏资金数字加大显示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 2026-07-29 — 顶栏对齐 crypto_monitor:标题在上、资金卡同宽
|
## 2026-07-29 — 顶栏对齐 crypto_monitor:标题在上、资金卡同宽
|
||||||
|
|
||||||
### 变更
|
### 变更
|
||||||
|
|||||||
@@ -233,6 +233,18 @@ export type PlanState = {
|
|||||||
atm_open_offset_enabled?: boolean;
|
atm_open_offset_enabled?: boolean;
|
||||||
max_atm_open_offset?: number;
|
max_atm_open_offset?: number;
|
||||||
can_open: boolean;
|
can_open: boolean;
|
||||||
|
open_capacity?: {
|
||||||
|
leverage?: number;
|
||||||
|
perp_can_open?: boolean | null;
|
||||||
|
option_can_open?: boolean | null;
|
||||||
|
perp_label?: string;
|
||||||
|
option_label?: string;
|
||||||
|
funds_ok?: boolean;
|
||||||
|
perp_need_usdt?: number | null;
|
||||||
|
option_need_usdc?: number | null;
|
||||||
|
perp_have_usdt?: number | null;
|
||||||
|
option_have_usdc?: number | null;
|
||||||
|
};
|
||||||
last_error: string | null;
|
last_error: string | null;
|
||||||
show_manual_trade_buttons?: boolean;
|
show_manual_trade_buttons?: boolean;
|
||||||
position: {
|
position: {
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ function pnlClass(n: number | null | undefined) {
|
|||||||
|
|
||||||
export default function FundsBar() {
|
export default function FundsBar() {
|
||||||
const [s, setS] = useState<FundsSummary | null>(null);
|
const [s, setS] = useState<FundsSummary | null>(null);
|
||||||
const [rulesOpen, setRulesOpen] = useState(false);
|
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
apiFetch<FundsSummary>("/api/funds/summary")
|
apiFetch<FundsSummary>("/api/funds/summary")
|
||||||
@@ -49,10 +48,6 @@ export default function FundsBar() {
|
|||||||
|
|
||||||
if (!s?.ok) return null;
|
if (!s?.ok) return null;
|
||||||
|
|
||||||
const rulesText = `行情 ${s.exchange || "OKX"} · ${
|
|
||||||
s.perp_inst_id || "ETH-USDT-SWAP"
|
|
||||||
} · 目标平仓(双腿/远虚只平永续) · 未达标则到期结算`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="funds-bar-shell">
|
<div className="funds-bar-shell">
|
||||||
<div className="funds-bar-inner">
|
<div className="funds-bar-inner">
|
||||||
@@ -83,50 +78,33 @@ export default function FundsBar() {
|
|||||||
: "—"}
|
: "—"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">总资金</div>
|
<div className="funds-label">总资金</div>
|
||||||
<div className="funds-value mono">{fmtU(s.total_funds)}</div>
|
<div className="funds-value mono">{fmtU(s.total_funds)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">资金账户</div>
|
<div className="funds-label">资金账户</div>
|
||||||
<div className="funds-value mono">{fmtU(s.funding_usdt)}</div>
|
<div className="funds-value mono">{fmtU(s.funding_usdt)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">交易账户</div>
|
<div className="funds-label">交易账户</div>
|
||||||
<div className="funds-value mono">{fmtU(s.trading_usdt)}</div>
|
<div className="funds-value mono">{fmtU(s.trading_usdt)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">期权资金账户</div>
|
<div className="funds-label">期权资金账户</div>
|
||||||
<div className="funds-value mono">{s.options_funding_label}</div>
|
<div className="funds-value mono">{s.options_funding_label}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item">
|
<div className="funds-item funds-item--money">
|
||||||
<div className="funds-label">期权交易账户</div>
|
<div className="funds-label">期权交易账户</div>
|
||||||
<div className="funds-value mono">{s.options_trading_label}</div>
|
<div className="funds-value mono">{s.options_trading_label}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="funds-item funds-item--pnl">
|
<div className="funds-item funds-item--pnl funds-item--money">
|
||||||
<div className="funds-label">实时盈亏</div>
|
<div className="funds-label">实时盈亏</div>
|
||||||
<div className={`funds-value mono ${pnlClass(s.realtime_pnl)}`}>
|
<div className={`funds-value mono ${pnlClass(s.realtime_pnl)}`}>
|
||||||
{s.realtime_pnl == null ? "—" : fmtU(s.realtime_pnl)}
|
{s.realtime_pnl == null ? "—" : fmtU(s.realtime_pnl)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="funds-rules">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="funds-rules-toggle"
|
|
||||||
aria-expanded={rulesOpen}
|
|
||||||
onClick={() => setRulesOpen((v) => !v)}
|
|
||||||
>
|
|
||||||
<span>规则说明</span>
|
|
||||||
<span className="funds-rules-chevron" aria-hidden>
|
|
||||||
{rulesOpen ? "▾" : "▸"}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{rulesOpen ? (
|
|
||||||
<p className="funds-rules-body meta">{rulesText}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const PHASE_ZH: Record<string, string> = {
|
|||||||
outside_window: "窗外",
|
outside_window: "窗外",
|
||||||
liquidity_wait: "流动性等待",
|
liquidity_wait: "流动性等待",
|
||||||
weekend_skip: "周末跳过",
|
weekend_skip: "周末跳过",
|
||||||
|
wait_funds: "资金不足",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PlanPage() {
|
export default function PlanPage() {
|
||||||
@@ -213,6 +214,16 @@ export default function PlanPage() {
|
|||||||
|
|
||||||
{err ? <div className="err">{err}</div> : null}
|
{err ? <div className="err">{err}</div> : null}
|
||||||
|
|
||||||
|
<div className="plan-rules-fold">
|
||||||
|
<details>
|
||||||
|
<summary>规则说明</summary>
|
||||||
|
<p className="meta plan-rules-body">
|
||||||
|
行情 {String(plan?.exchange || "okx").toUpperCase()} · ETH-USDT-SWAP ·
|
||||||
|
目标平仓(双腿/远虚只平永续) · 未达标则到期结算
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="plan-actions">
|
<div className="plan-actions">
|
||||||
<button
|
<button
|
||||||
className={plan?.running ? "btn btn-running" : "btn"}
|
className={plan?.running ? "btn btn-running" : "btn"}
|
||||||
@@ -318,9 +329,41 @@ export default function PlanPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
<span>权益 / 杠杆</span>
|
<span>权益 / 杠杆</span>
|
||||||
<span className="mono">
|
<span className="open-cap-row">
|
||||||
{fmt(plan?.ledger?.equity)} / 可用 {fmt(plan?.ledger?.available)} ·{" "}
|
<span
|
||||||
{fmt(plan?.leverage, 0)}x
|
className={
|
||||||
|
plan?.open_capacity?.perp_can_open === true
|
||||||
|
? "open-cap-ok"
|
||||||
|
: plan?.open_capacity?.perp_can_open === false
|
||||||
|
? "open-cap-bad"
|
||||||
|
: "mono"
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
plan?.open_capacity?.perp_need_usdt != null
|
||||||
|
? `需≈${plan.open_capacity.perp_need_usdt}U / 有${plan.open_capacity.perp_have_usdt ?? "—"}U`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{plan?.open_capacity?.perp_label ||
|
||||||
|
`永续${fmt(plan?.leverage, 0)}x —`}
|
||||||
|
</span>
|
||||||
|
<span className="open-cap-sep">·</span>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
plan?.open_capacity?.option_can_open === true
|
||||||
|
? "open-cap-ok"
|
||||||
|
: plan?.open_capacity?.option_can_open === false
|
||||||
|
? "open-cap-bad"
|
||||||
|
: "mono"
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
plan?.open_capacity?.option_need_usdc != null
|
||||||
|
? `需≈${plan.open_capacity.option_need_usdc}U / 有${plan.open_capacity.option_have_usdc ?? "—"}U`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{plan?.open_capacity?.option_label || "期权 —"}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="kv">
|
<div className="kv">
|
||||||
|
|||||||
@@ -232,6 +232,16 @@ input {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.funds-item--money .funds-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.funds-item--money .funds-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.funds-item--pnl .funds-value {
|
.funds-item--pnl .funds-value {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
@@ -271,6 +281,55 @@ input {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plan-rules-fold {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-rules-fold details {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-rules-fold summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-rules-fold summary:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-rules-body {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-cap-row {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-cap-sep {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-cap-ok {
|
||||||
|
color: #0ecb81;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-cap-bad {
|
||||||
|
color: #f6465d;
|
||||||
|
}
|
||||||
|
|
||||||
.topnav {
|
.topnav {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user