Add option-option hedge mode with SIM/LIVE parity.

Mutual hedge_mode, amplitude OTM selection, 1:1 risk sizing, win-leg/full close, dual audits and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 16:01:16 +08:00
parent 15fe2f72dc
commit ec244c63c6
22 changed files with 2640 additions and 83 deletions
+14
View File
@@ -304,6 +304,12 @@ export type PlanState = {
move_pct?: number;
initial_premium?: number;
premium_gap?: number;
hedge_mode?: string;
option2_inst_id?: string;
option2_upl?: number;
option2_entry_px?: number;
option2_qty_eth?: number;
strike2?: number | null;
};
residuals?: {
group_id: string;
@@ -341,6 +347,7 @@ export type PlanState = {
martingale_loss_days?: number;
risk_effective_loss_pct?: number;
risk_loss_pct?: number;
hedge_mode?: "perp_option" | "option_option";
sizing_mode?: "manual" | "risk_based";
risk_based?: boolean;
ledger: { equity: number; available: number; reserved: number };
@@ -388,6 +395,13 @@ export type StrategySettings = {
martingale_enabled?: boolean;
martingale_start_after_loss_days?: number;
martingale_max_doubles?: number;
hedge_mode?: "perp_option" | "option_option";
oo_amplitude_pct?: number;
oo_amplitude_hours?: number;
oo_min_option_hours?: number;
oo_min_leverage?: number;
oo_reward_ratio?: number;
oo_budget_cushion?: number;
exchange?: string;
};
+70 -4
View File
@@ -213,7 +213,7 @@ export default function PlanPage() {
const riskRatioLabel = `比例${Number(plan?.risk_perp_unit ?? 1)}:${Number(plan?.risk_option_unit ?? 2)}`;
const sizingModeLabel = riskBased
? [
"以损定仓",
plan?.hedge_mode === "option_option" ? "期期对冲" : "以损定仓",
riskRatioLabel,
plan?.risk_last_k != null ? `k=${fmt(plan.risk_last_k, 1)}` : null,
riskLocked
@@ -236,7 +236,9 @@ export default function PlanPage() {
]
.filter(Boolean)
.join(" ")
: "手动仓位";
: plan?.hedge_mode === "option_option"
? "期期对冲"
: "手动仓位";
const phaseLabel = PHASE_ZH[plan?.phase || ""] || plan?.phase || "—";
const atmRule = plan?.fixed_direction_enabled
? plan?.fixed_perp_side === "short"
@@ -581,6 +583,57 @@ export default function PlanPage() {
<div className="plan-positions">
{open ? (
<>
{plan?.hedge_mode === "option_option" ||
pos?.hedge_mode === "option_option" ||
pos?.option2_inst_id ? (
<div className="pos-card">
<div className="pos-card-head">
<div className="pos-card-symbol">
<strong>
{pos?.option_inst_id || "Call"}
</strong>
<span className="pos-side-badge pos-side-long">
Call
</span>
</div>
</div>
<div className="pos-meta">
<span className="pos-meta-item"> · Call</span>
<span className="pos-meta-item"> {pos?.group_id}</span>
<span className="pos-meta-item mono">
{fmt(pos?.option_qty_eth, 1)} ETH
</span>
</div>
<div className="pos-grid">
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">
{fmt(pos?.option_entry_px)}
</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span
className={`pos-value mono ${pnlClass(pos?.option_upl)}`}
>
{fmt(pos?.option_upl)}
</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">
{fmt(pos?.strike, 0)}
</span>
</div>
<div className="pos-cell">
<span className="pos-label"></span>
<span className="pos-value mono">
{holdDurationLabel}
</span>
</div>
</div>
</div>
) : (
<div className="pos-card">
<div className="pos-card-head">
<div className="pos-card-symbol">
@@ -649,19 +702,32 @@ export default function PlanPage() {
</div>
</div>
</div>
)}
<div className="pos-card">
<div className="pos-card-head">
<div className="pos-card-symbol">
<strong>{pos?.option_inst_id || "期权"}</strong>
<strong>
{plan?.hedge_mode === "option_option" ||
pos?.option2_inst_id
? pos?.option2_inst_id || "Put"
: pos?.option_inst_id || "期权"}
</strong>
<span
className={
plan?.hedge_mode === "option_option" ||
pos?.option2_inst_id ||
pos?.option_side === "call"
? "pos-side-badge pos-side-long"
: "pos-side-badge pos-side-short"
}
>
{pos?.option_side === "call" ? "看涨 Call" : "看跌 Put"}
{plan?.hedge_mode === "option_option" ||
pos?.option2_inst_id
? "看跌 Put"
: pos?.option_side === "call"
? "看涨 Call"
: "看跌 Put"}
</span>
</div>
<div
+123
View File
@@ -108,6 +108,14 @@ export default function SettingsPage() {
const [martingaleOn, setMartingaleOn] = useState(false);
const [martingaleStartAfter, setMartingaleStartAfter] = useState(2);
const [martingaleMaxDoubles, setMartingaleMaxDoubles] = useState(3);
const [hedgeMode, setHedgeMode] = useState<"perp_option" | "option_option">(
"perp_option",
);
const [ooAmpPct, setOoAmpPct] = useState(1.5);
const [ooAmpHours, setOoAmpHours] = useState(12);
const [ooMinHours, setOoMinHours] = useState(24);
const [ooMinLev, setOoMinLev] = useState(200);
const [ooRewardRatio, setOoRewardRatio] = useState(2);
const [riskPreview, setRiskPreview] = useState<Record<string, unknown> | null>(
null,
);
@@ -221,6 +229,14 @@ export default function SettingsPage() {
setMartingaleOn(s.martingale_enabled === true);
setMartingaleStartAfter(s.martingale_start_after_loss_days ?? 2);
setMartingaleMaxDoubles(s.martingale_max_doubles ?? 3);
setHedgeMode(
s.hedge_mode === "option_option" ? "option_option" : "perp_option",
);
setOoAmpPct(s.oo_amplitude_pct ?? 1.5);
setOoAmpHours(s.oo_amplitude_hours ?? 12);
setOoMinHours(s.oo_min_option_hours ?? 24);
setOoMinLev(s.oo_min_leverage ?? 200);
setOoRewardRatio(s.oo_reward_ratio ?? 2);
setRiskPreview(
s.risk_sizing_preview && typeof s.risk_sizing_preview === "object"
? s.risk_sizing_preview
@@ -389,6 +405,12 @@ export default function SettingsPage() {
martingaleOn,
martingale_start_after_loss_days: martingaleStartAfter,
martingale_max_doubles: martingaleMaxDoubles,
hedge_mode: hedgeMode,
oo_amplitude_pct: ooAmpPct,
oo_amplitude_hours: ooAmpHours,
oo_min_option_hours: ooMinHours,
oo_min_leverage: ooMinLev,
oo_reward_ratio: ooRewardRatio,
exchange,
};
// 以损定仓不提交手填名义/出场,避免禁用输入框脏值导致 422
@@ -682,12 +704,113 @@ export default function SettingsPage() {
<option value="isolated"></option>
</select>
</div>
<div className="field">
<label htmlFor="hedgeMode"></label>
<select
id="hedgeMode"
className="mono"
value={hedgeMode}
onChange={(e) => {
const v =
e.target.value === "option_option"
? "option_option"
: "perp_option";
setHedgeMode(v);
if (v === "option_option") {
setSizingMode("risk_based");
setRiskLossMode("percent");
setFixedDirOn(false);
}
}}
>
<option value="perp_option">+</option>
<option value="option_option"></option>
</select>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
N Call/Put
</p>
</div>
{hedgeMode === "option_option" ? (
<>
<div className="field">
<label htmlFor="ooAmp">%</label>
<input
id="ooAmp"
className="mono"
type="number"
step="0.1"
min="0.1"
value={ooAmpPct}
onChange={(e) => setOoAmpPct(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="ooAmpH"></label>
<input
id="ooAmpH"
className="mono"
type="number"
step="1"
min="1"
value={ooAmpHours}
onChange={(e) =>
setOoAmpHours(Number(e.target.value))
}
/>
</div>
<div className="field">
<label htmlFor="ooMinH"></label>
<input
id="ooMinH"
className="mono"
type="number"
step="1"
min="1"
value={ooMinHours}
onChange={(e) =>
setOoMinHours(Number(e.target.value))
}
/>
</div>
<div className="field">
<label htmlFor="ooLev"></label>
<input
id="ooLev"
className="mono"
type="number"
step="1"
min="1"
value={ooMinLev}
onChange={(e) => setOoMinLev(Number(e.target.value))}
/>
</div>
<div className="field">
<label htmlFor="ooRR"></label>
<input
id="ooRR"
className="mono"
type="number"
step="0.1"
min="0.5"
value={ooRewardRatio}
onChange={(e) =>
setOoRewardRatio(Number(e.target.value))
}
/>
<p className="hint" style={{ margin: "0.35rem 0 0" }}>
= × 100U 2
200U 1:1
</p>
</div>
</>
) : null}
<div className="field">
<label htmlFor="sizingMode"></label>
<select
id="sizingMode"
className="mono"
value={sizingMode}
disabled={hedgeMode === "option_option"}
onChange={(e) =>
setSizingMode(
e.target.value === "risk_based"