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:
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user