Polish risk sizing: 2dp money preview, editable ratio/exit, plan badge.

Exit unit and perp/option units scale with k; plan page shows 以损定仓.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-29 23:48:34 +08:00
parent 3a4c8d639c
commit a47a9b6377
8 changed files with 349 additions and 38 deletions
+3
View File
@@ -329,6 +329,9 @@ export type StrategySettings = {
risk_loss_usdt?: number;
risk_capital_source?: "trading_account" | "manual";
risk_manual_capital_usdt?: number;
risk_perp_unit?: number;
risk_option_unit?: number;
risk_exit_unit?: number;
risk_sizing_preview?: Record<string, unknown>;
exchange?: string;
};
+17 -3
View File
@@ -151,7 +151,9 @@ export default function PlanPage() {
const exitRuleLabel =
exitMode === "premium_multiple"
? `权利金×${fmt(plan?.premium_exit_multiple ?? 1, 2)}`
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
: plan?.risk_based || plan?.sizing_mode === "risk_based"
? `以损定仓 · 固定 ${fmt(exitTarget)} U(基数${fmt(plan?.risk_exit_unit ?? 15)}`
: `固定 ${fmt(plan?.net_profit_target ?? 15)} U`;
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
const atmRule = plan?.fixed_direction_enabled
? plan?.fixed_perp_side === "short"
@@ -343,8 +345,20 @@ export default function PlanPage() {
<div className="kv">
<span></span>
<span className="mono">
{fmt(plan?.perp_qty_eth ?? 1, 0)}ETH/
{fmt(plan?.option_qty_eth ?? 2, 0)}ETH ·
{plan?.risk_based || plan?.sizing_mode === "risk_based" ? (
<>
<span className="status-running"></span>
{" · "}
{fmt(plan?.risk_perp_unit ?? 1, 2)}:
{fmt(plan?.risk_option_unit ?? 2, 2)}
{plan?.risk_last_k
? ` · k=${fmt(plan.risk_last_k, 1)}`
: ""}
{" · "}
</>
) : null}
{fmt(plan?.perp_qty_eth ?? 1, 2)}ETH/
{fmt(plan?.option_qty_eth ?? 2, 2)}ETH ·
{fmt(plan?.min_option_hours, 0)}h ·
{fmt(plan?.min_option_leverage, 0)}x
{plan?.fixed_direction_enabled
+178 -14
View File
@@ -94,6 +94,9 @@ export default function SettingsPage() {
"trading_account" | "manual"
>("trading_account");
const [riskManualCapital, setRiskManualCapital] = useState(10000);
const [riskPerpUnit, setRiskPerpUnit] = useState(1);
const [riskOptUnit, setRiskOptUnit] = useState(2);
const [riskExitUnit, setRiskExitUnit] = useState(15);
const [riskPreview, setRiskPreview] = useState<Record<string, unknown> | null>(
null,
);
@@ -194,6 +197,9 @@ export default function SettingsPage() {
s.risk_capital_source === "manual" ? "manual" : "trading_account",
);
setRiskManualCapital(s.risk_manual_capital_usdt ?? 10000);
setRiskPerpUnit(s.risk_perp_unit ?? 1);
setRiskOptUnit(s.risk_option_unit ?? 2);
setRiskExitUnit(s.risk_exit_unit ?? 15);
setRiskPreview(
s.risk_sizing_preview && typeof s.risk_sizing_preview === "object"
? s.risk_sizing_preview
@@ -349,6 +355,9 @@ export default function SettingsPage() {
risk_loss_usdt: riskLossUsdt,
risk_capital_source: riskCapitalSource,
risk_manual_capital_usdt: riskManualCapital,
risk_perp_unit: riskPerpUnit,
risk_option_unit: riskOptUnit,
risk_exit_unit: riskExitUnit,
exchange,
};
// 以损定仓不提交手填名义/出场,避免禁用输入框脏值导致 422
@@ -739,17 +748,138 @@ export default function SettingsPage() {
<div className="field" style={{ gridColumn: "1 / -1" }}>
<label></label>
<div className="mono" style={{ fontSize: 12, opacity: 0.9 }}>
{riskPreview == null
? "—"
: riskPreview.ok === false
? String(riskPreview.detail || "预览不可用")
: `k=${String(riskPreview.k ?? "—")} · 预算=${String(riskPreview.budget ?? "—")}U · 估亏=${String(riskPreview.max_loss ?? "—")}U · 永续=${String(riskPreview.perp_qty_eth ?? "—")} · 期权=${String(riskPreview.option_qty_eth ?? "—")} · 出场=${String(riskPreview.net_profit_target ?? "—")}`}
{(() => {
if (riskPreview == null) return "—";
if (riskPreview.ok === false) {
return String(riskPreview.detail || "预览不可用");
}
const k = Number(riskPreview.k);
const bud = Number(riskPreview.budget);
const mx = Number(riskPreview.max_loss);
const pk = Number.isFinite(k) ? k : null;
const perp =
pk != null
? Number((riskPerpUnit * pk).toFixed(4))
: riskPreview.perp_qty_eth;
const opt =
pk != null
? Number((riskOptUnit * pk).toFixed(4))
: riskPreview.option_qty_eth;
const exit =
pk != null
? Number((riskExitUnit * pk).toFixed(2))
: riskPreview.net_profit_target;
const budS = Number.isFinite(bud)
? bud.toFixed(2)
: "—";
const mxS = Number.isFinite(mx) ? mx.toFixed(2) : "—";
return `k=${pk ?? "—"} · 预算=${budS}U · 估亏=${mxS}U · 永续=${perp ?? "—"} · 期权=${opt ?? "—"} · 出场=${exit ?? "—"}`;
})()}
</div>
</div>
<div className="field">
<label htmlFor="riskPerpU">k=1</label>
<input
id="riskPerpU"
className="mono"
type="number"
step="0.01"
min="0.01"
value={riskPerpUnit}
onChange={(e) => {
const v = Number(e.target.value);
setRiskPerpUnit(v);
const k = Number(riskPreview?.k);
if (Number.isFinite(k) && k > 0) {
setPerpQty(Number((v * k).toFixed(4)));
}
}}
/>
</div>
<div className="field">
<label htmlFor="riskOptU">k=1</label>
<input
id="riskOptU"
className="mono"
type="number"
step="0.01"
min="0.01"
value={riskOptUnit}
onChange={(e) => {
const v = Number(e.target.value);
setRiskOptUnit(v);
const k = Number(riskPreview?.k);
if (Number.isFinite(k) && k > 0) {
setOptQty(Number((v * k).toFixed(4)));
}
}}
/>
</div>
<div className="field">
<label htmlFor="riskExitU">
k=1
</label>
<input
id="riskExitU"
className="mono"
type="number"
step="0.1"
min="0.1"
value={riskExitUnit}
onChange={(e) => {
const v = Number(e.target.value);
setRiskExitUnit(v);
const k = Number(riskPreview?.k);
if (Number.isFinite(k) && k > 0) {
setNetTarget(Number((v * k).toFixed(2)));
setPerpQty(Number((riskPerpUnit * k).toFixed(4)));
setOptQty(Number((riskOptUnit * k).toFixed(4)));
}
}}
/>
</div>
<div className="field">
<label htmlFor="riskExitAbs">
= ×k
</label>
<input
id="riskExitAbs"
className="mono"
type="number"
step="0.01"
min="0.1"
value={
(() => {
const k = Number(riskPreview?.k);
if (Number.isFinite(k) && k > 0) {
return Number((riskExitUnit * k).toFixed(2));
}
return Number(riskExitUnit.toFixed(2));
})()
}
onChange={(e) => {
const abs = Number(e.target.value);
const k = Number(riskPreview?.k);
if (Number.isFinite(k) && k > 0) {
const unit = Number((abs / k).toFixed(4));
setRiskExitUnit(unit);
setNetTarget(Number(abs.toFixed(2)));
setPerpQty(Number((riskPerpUnit * k).toFixed(4)));
setOptQty(Number((riskOptUnit * k).toFixed(4)));
} else {
setRiskExitUnit(abs);
setNetTarget(abs);
}
}}
/>
</div>
</>
) : null}
<div className="field">
<label htmlFor="perp"> ETH </label>
<label htmlFor="perp">
ETH
{sizingMode === "risk_based" ? "(随比例×k" : ""}
</label>
<input
id="perp"
className="mono"
@@ -757,12 +887,22 @@ export default function SettingsPage() {
step="0.01"
min="0.01"
disabled={sizingMode === "risk_based"}
value={perpQty}
value={
sizingMode === "risk_based" &&
Number(riskPreview?.k) > 0
? Number(
(riskPerpUnit * Number(riskPreview?.k)).toFixed(4),
)
: perpQty
}
onChange={(e) => setPerpQty(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="opt"> ETH </label>
<label htmlFor="opt">
ETH
{sizingMode === "risk_based" ? "(随比例×k" : ""}
</label>
<input
id="opt"
className="mono"
@@ -770,7 +910,14 @@ export default function SettingsPage() {
step="0.01"
min="0.01"
disabled={sizingMode === "risk_based"}
value={optQty}
value={
sizingMode === "risk_based" &&
Number(riskPreview?.k) > 0
? Number(
(riskOptUnit * Number(riskPreview?.k)).toFixed(4),
)
: optQty
}
onChange={(e) => setOptQty(Number(e.target.value))}
/>
</div>
@@ -895,8 +1042,9 @@ export default function SettingsPage() {
{sizingMode === "risk_based" || exitMode === "fixed_usdt" ? (
<div className="field">
<label htmlFor="netTarget">
USDT
{sizingMode === "risk_based" ? " · 以损定仓=15×k" : ""}
{sizingMode === "risk_based"
? "出场资金(k=1 基数)"
: "净盈利出场目标(USDT"}
</label>
<input
id="netTarget"
@@ -904,9 +1052,25 @@ export default function SettingsPage() {
type="number"
step="0.1"
min="0.1"
disabled={sizingMode === "risk_based"}
value={netTarget}
onChange={(e) => setNetTarget(Number(e.target.value))}
value={
sizingMode === "risk_based" ? riskExitUnit : netTarget
}
onChange={(e) => {
const v = Number(e.target.value);
if (sizingMode === "risk_based") {
setRiskExitUnit(v);
const k = Number(riskPreview?.k);
if (Number.isFinite(k) && k > 0) {
setNetTarget(Number((v * k).toFixed(2)));
setPerpQty(
Number((riskPerpUnit * k).toFixed(4)),
);
setOptQty(Number((riskOptUnit * k).toFixed(4)));
}
} else {
setNetTarget(v);
}
}}
/>
</div>
) : (