Show risk params box on control cards and modal.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -223,13 +223,19 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di
|
|||||||
"exit_mode": st.get("exit_mode"),
|
"exit_mode": st.get("exit_mode"),
|
||||||
"exit_target_usdt": st.get("exit_target_usdt"),
|
"exit_target_usdt": st.get("exit_target_usdt"),
|
||||||
"net_profit_target": st.get("net_profit_target"),
|
"net_profit_target": st.get("net_profit_target"),
|
||||||
|
"premium_exit_multiple": st.get("premium_exit_multiple"),
|
||||||
"leverage": st.get("leverage"),
|
"leverage": st.get("leverage"),
|
||||||
|
"min_option_leverage": st.get("min_option_leverage"),
|
||||||
"perp_margin_mode": st.get("perp_margin_mode"),
|
"perp_margin_mode": st.get("perp_margin_mode"),
|
||||||
"perp_qty_eth": st.get("perp_qty_eth"),
|
"perp_qty_eth": st.get("perp_qty_eth"),
|
||||||
"option_qty_eth": st.get("option_qty_eth"),
|
"option_qty_eth": st.get("option_qty_eth"),
|
||||||
"sizing_mode": st.get("sizing_mode"),
|
"sizing_mode": st.get("sizing_mode"),
|
||||||
"risk_last_k": st.get("risk_last_k"),
|
"risk_last_k": st.get("risk_last_k"),
|
||||||
"risk_sizing_locked": st.get("risk_sizing_locked"),
|
"risk_sizing_locked": st.get("risk_sizing_locked"),
|
||||||
|
"risk_loss_pct": st.get("risk_loss_pct"),
|
||||||
|
"risk_perp_unit": st.get("risk_perp_unit"),
|
||||||
|
"risk_option_unit": st.get("risk_option_unit"),
|
||||||
|
"risk_exit_unit": st.get("risk_exit_unit"),
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"status": pos.get("status") or ("open" if pos.get("has_position") else "flat"),
|
"status": pos.get("status") or ("open" if pos.get("has_position") else "flat"),
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ class StrategyEngine:
|
|||||||
risk_perp_unit = self.ledger.get_setting_float("risk_perp_unit", 1.0)
|
risk_perp_unit = self.ledger.get_setting_float("risk_perp_unit", 1.0)
|
||||||
risk_option_unit = self.ledger.get_setting_float("risk_option_unit", 2.0)
|
risk_option_unit = self.ledger.get_setting_float("risk_option_unit", 2.0)
|
||||||
risk_exit_unit = self.ledger.get_setting_float("risk_exit_unit", 15.0)
|
risk_exit_unit = self.ledger.get_setting_float("risk_exit_unit", 15.0)
|
||||||
|
risk_loss_pct = self.ledger.get_setting_float("risk_loss_pct", 1.0)
|
||||||
risk_last_k = self.ledger.get_setting_float("risk_last_k", 0.0)
|
risk_last_k = self.ledger.get_setting_float("risk_last_k", 0.0)
|
||||||
risk_preview: dict[str, Any] | None = None
|
risk_preview: dict[str, Any] | None = None
|
||||||
pos_status = str(upl.get("status") or "flat")
|
pos_status = str(upl.get("status") or "flat")
|
||||||
@@ -225,6 +226,7 @@ class StrategyEngine:
|
|||||||
"risk_perp_unit": risk_perp_unit,
|
"risk_perp_unit": risk_perp_unit,
|
||||||
"risk_option_unit": risk_option_unit,
|
"risk_option_unit": risk_option_unit,
|
||||||
"risk_exit_unit": risk_exit_unit,
|
"risk_exit_unit": risk_exit_unit,
|
||||||
|
"risk_loss_pct": risk_loss_pct,
|
||||||
"risk_last_k": risk_last_k if risk_last_k > 0 else None,
|
"risk_last_k": risk_last_k if risk_last_k > 0 else None,
|
||||||
"risk_sizing_preview": risk_preview,
|
"risk_sizing_preview": risk_preview,
|
||||||
"risk_sizing_locked": bool(trade_locked and sizing_mode == "risk_based"),
|
"risk_sizing_locked": bool(trade_locked and sizing_mode == "risk_based"),
|
||||||
|
|||||||
@@ -50,6 +50,95 @@ function hasOpenPosition(position: Record<string, unknown>): boolean {
|
|||||||
return OPEN_STATUSES.has(st);
|
return OPEN_STATUSES.has(st);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unitLabel(v: unknown): string {
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isFinite(n)) return "—";
|
||||||
|
if (Number.isInteger(n)) return String(n);
|
||||||
|
return n.toFixed(2).replace(/\.?0+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function leveragePair(strat: Record<string, unknown>): string {
|
||||||
|
const lev = Number(strat.leverage);
|
||||||
|
const opt = Number(strat.min_option_leverage);
|
||||||
|
const a = Number.isFinite(lev) ? `${Math.round(lev)}x` : "—";
|
||||||
|
const b = Number.isFinite(opt) ? `${Math.round(opt)}x` : "—";
|
||||||
|
return `${a}/${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RiskLines = {
|
||||||
|
riskBased: boolean;
|
||||||
|
sizing: string;
|
||||||
|
lossPct: string | null;
|
||||||
|
exit: string;
|
||||||
|
openRatio: string | null;
|
||||||
|
leverage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function riskLines(strat: Record<string, unknown>): RiskLines {
|
||||||
|
const riskBased =
|
||||||
|
strat.sizing_mode === "risk_based" || strat.risk_based === true;
|
||||||
|
const exitMode = String(strat.exit_mode || "fixed_usdt");
|
||||||
|
let exit: string;
|
||||||
|
if (riskBased) {
|
||||||
|
exit = `基数${unitLabel(strat.risk_exit_unit ?? 15)}`;
|
||||||
|
} else if (exitMode === "premium_multiple") {
|
||||||
|
exit = `权利金×${fmt(strat.premium_exit_multiple ?? 1, 2)}`;
|
||||||
|
} else {
|
||||||
|
exit = `固定 ${fmt(strat.net_profit_target ?? strat.exit_target_usdt, 2)}U`;
|
||||||
|
}
|
||||||
|
const lossN = Number(strat.risk_loss_pct);
|
||||||
|
const lossPct =
|
||||||
|
riskBased && Number.isFinite(lossN)
|
||||||
|
? `${fmt(lossN, lossN % 1 === 0 ? 0 : 2)}%`
|
||||||
|
: null;
|
||||||
|
const openRatio = riskBased
|
||||||
|
? `${unitLabel(strat.risk_perp_unit ?? 1)}:${unitLabel(strat.risk_option_unit ?? 2)}`
|
||||||
|
: null;
|
||||||
|
return {
|
||||||
|
riskBased,
|
||||||
|
sizing: riskBased ? "以损定仓" : "手动仓位",
|
||||||
|
lossPct,
|
||||||
|
exit,
|
||||||
|
openRatio,
|
||||||
|
leverage: leveragePair(strat),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function RiskParamsBox({ strat }: { strat: Record<string, unknown> }) {
|
||||||
|
const r = riskLines(strat);
|
||||||
|
return (
|
||||||
|
<div className="risk-box">
|
||||||
|
<div className="risk-box-title">风控参数</div>
|
||||||
|
<dl className="kv risk-kv">
|
||||||
|
<div>
|
||||||
|
<dt>定仓</dt>
|
||||||
|
<dd>{r.sizing}</dd>
|
||||||
|
</div>
|
||||||
|
{r.lossPct ? (
|
||||||
|
<div>
|
||||||
|
<dt>风险比例</dt>
|
||||||
|
<dd>{r.lossPct}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div>
|
||||||
|
<dt>出场</dt>
|
||||||
|
<dd>{r.exit}</dd>
|
||||||
|
</div>
|
||||||
|
{r.openRatio ? (
|
||||||
|
<div>
|
||||||
|
<dt>开仓比例</dt>
|
||||||
|
<dd>{r.openRatio}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div>
|
||||||
|
<dt>杠杆</dt>
|
||||||
|
<dd>{r.leverage}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** 页面级快照:整页刷新前保留上次卡片,避免空白等待 */
|
/** 页面级快照:整页刷新前保留上次卡片,避免空白等待 */
|
||||||
let cachedNodes: NodeCard[] = [];
|
let cachedNodes: NodeCard[] = [];
|
||||||
let cachedSseSec = 1;
|
let cachedSseSec = 1;
|
||||||
@@ -374,6 +463,7 @@ export default function MonitorPage() {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
<RiskParamsBox strat={s.strat} />
|
||||||
{n.fleet_error ? <div className="err soft">{n.fleet_error}</div> : null}
|
{n.fleet_error ? <div className="err soft">{n.fleet_error}</div> : null}
|
||||||
{n.error ? <div className="err soft">{n.error}</div> : null}
|
{n.error ? <div className="err soft">{n.error}</div> : null}
|
||||||
<div className="node-actions" onClick={(e) => e.stopPropagation()}>
|
<div className="node-actions" onClick={(e) => e.stopPropagation()}>
|
||||||
@@ -447,6 +537,7 @@ export default function MonitorPage() {
|
|||||||
|
|
||||||
<section className="modal-section">
|
<section className="modal-section">
|
||||||
<h4>策略详情</h4>
|
<h4>策略详情</h4>
|
||||||
|
<RiskParamsBox strat={detail.strat} />
|
||||||
<dl className="kv detail-kv">
|
<dl className="kv detail-kv">
|
||||||
<div>
|
<div>
|
||||||
<dt>状态</dt>
|
<dt>状态</dt>
|
||||||
@@ -471,11 +562,8 @@ export default function MonitorPage() {
|
|||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>杠杆 / 保证金</dt>
|
<dt>保证金</dt>
|
||||||
<dd>
|
<dd>{String(detail.strat.perp_margin_mode || "—")}</dd>
|
||||||
{fmt(detail.strat.leverage, 1)}x /{" "}
|
|
||||||
{String(detail.strat.perp_margin_mode || "—")}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>名义 永续/期权</dt>
|
<dt>名义 永续/期权</dt>
|
||||||
@@ -488,15 +576,6 @@ export default function MonitorPage() {
|
|||||||
<dt>出场目标</dt>
|
<dt>出场目标</dt>
|
||||||
<dd>{fmt(detail.strat.exit_target_usdt, 2)} USDT</dd>
|
<dd>{fmt(detail.strat.exit_target_usdt, 2)} USDT</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<dt>定仓</dt>
|
|
||||||
<dd>
|
|
||||||
{String(detail.strat.sizing_mode || "—")}
|
|
||||||
{detail.strat.risk_last_k != null
|
|
||||||
? ` · k=${fmt(detail.strat.risk_last_k, 2)}`
|
|
||||||
: ""}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<dt>指数</dt>
|
<dt>指数</dt>
|
||||||
<dd>{fmt(detail.index_px, 2)}</dd>
|
<dd>{fmt(detail.index_px, 2)}</dd>
|
||||||
|
|||||||
@@ -290,6 +290,30 @@ input {
|
|||||||
font-size: 0.92rem;
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.risk-box {
|
||||||
|
border: 1px solid rgba(46, 229, 154, 0.45);
|
||||||
|
background: rgba(46, 229, 154, 0.08);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-box-title {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--pnl-pos);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-kv {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.risk-kv dd {
|
||||||
|
color: var(--pnl-pos);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.node-actions {
|
.node-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
Reference in New Issue
Block a user