Replace 期期 equal-split with long/short bias sizing and env ratio controls.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 15:25:10 +08:00
parent bc797cb1db
commit e3cd2a75de
13 changed files with 330 additions and 67 deletions
+73 -13
View File
@@ -29,6 +29,8 @@
legB: null,
market: null,
ooSheetsMode: "same_sheets",
ooBiasSplitBy: "budget",
ooBiasRatio: 0.7,
ooCloseModeEnabled: root.getAttribute("data-oo-close-mode-enabled") !== "0",
ooCloseMode: "close_all",
direction: "long",
@@ -335,6 +337,20 @@
return Math.min(out, Math.floor(d + 1e-12));
}
function normalizeOptCP(ot) {
const u = String(ot || "").toUpperCase();
if (u.indexOf("C") === 0) return "C";
if (u.indexOf("P") === 0) return "P";
return "";
}
function ooSizeModeLabel(mode) {
if (mode === "long_bias") return "做多";
if (mode === "short_bias") return "做空";
if (mode === "split_budget") return "均分";
return "同张数";
}
function suggestOoSheetsLocal(budget, mode) {
const costA = unitCost(state.legA);
const costB = unitCost(state.legB);
@@ -344,24 +360,61 @@
if (!(costA > 0) || !(costB > 0)) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "缺少有效卖一价,无法建议张数" };
}
let ratio = Number(state.ooBiasRatio);
if (!(ratio > 0) || !(ratio < 1)) ratio = 0.7;
const splitBy = state.ooBiasSplitBy === "sheets" ? "sheets" : "budget";
const pair = costA + costB;
let nSame = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0;
nSame = capByDepth(nSame, state.legA && state.legA.ask_sz);
nSame = capByDepth(nSame, state.legB && state.legB.ask_sz);
let nA = 0;
let nB = 0;
if (mode === "split_budget") {
if (mode === "long_bias" || mode === "short_bias") {
const aCP = normalizeOptCP(state.legA && state.legA.opt_type);
const bCP = normalizeOptCP(state.legB && state.legB.opt_type);
if (!((aCP === "C" && bCP === "P") || (aCP === "P" && bCP === "C"))) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "做多/做空需一腿 Call、一腿 Put" };
}
const callIsA = aCP === "C";
const majorIsCall = mode === "long_bias";
let nCall = 0;
let nPut = 0;
if (splitBy === "sheets") {
if (nSame < 2) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "同张数规模不足 2,无法按比例拆分" };
}
let majorN = Math.round(nSame * ratio);
majorN = Math.max(1, Math.min(majorN, nSame - 1));
const minorN = nSame - majorN;
nCall = majorIsCall ? majorN : minorN;
nPut = majorIsCall ? minorN : majorN;
} else {
const majBudget = budget * ratio;
const minBudget = budget * (1 - ratio);
const costCall = callIsA ? costA : costB;
const costPut = callIsA ? costB : costA;
if (majorIsCall) {
nCall = Math.floor(majBudget / costCall + 1e-12);
nPut = Math.floor(minBudget / costPut + 1e-12);
} else {
nPut = Math.floor(majBudget / costPut + 1e-12);
nCall = Math.floor(minBudget / costCall + 1e-12);
}
}
nA = callIsA ? nCall : nPut;
nB = callIsA ? nPut : nCall;
nA = capByDepth(nA, state.legA && state.legA.ask_sz);
nB = capByDepth(nB, state.legB && state.legB.ask_sz);
} else if (mode === "split_budget") {
const half = budget / 2;
nA = Math.floor(half / costA + 1e-12);
nB = Math.floor(half / costB + 1e-12);
nA = capByDepth(nA, state.legA && state.legA.ask_sz);
nB = capByDepth(nB, state.legB && state.legB.ask_sz);
} else {
const pair = costA + costB;
const n = pair > 0 ? Math.floor(budget / pair + 1e-12) : 0;
nA = n;
nB = n;
}
nA = capByDepth(nA, state.legA && state.legA.ask_sz);
nB = capByDepth(nB, state.legB && state.legB.ask_sz);
if (mode !== "split_budget") {
const n = Math.min(nA, nB);
nA = n;
nB = n;
nA = nSame;
nB = nSame;
}
const premium = costA * nA + costB * nB;
const ok = nA >= 1 && nB >= 1;
@@ -400,7 +453,7 @@
const line = $("hp-oo-budget-line");
if (!line) return;
const b = resolveOoBudget();
const sizeLabel = state.ooSheetsMode === "split_budget" ? "均分" : "同张数";
const sizeLabel = ooSizeModeLabel(state.ooSheetsMode);
const closeLabel = !state.ooCloseModeEnabled
? ""
: state.ooCloseMode === "hold_expiry"
@@ -447,8 +500,15 @@
} else if (d.oo_close_mode_default && !state._ooCloseModeTouched) {
state.ooCloseMode = d.oo_close_mode_default === "hold_expiry" ? "hold_expiry" : "close_all";
}
if (d.oo_bias_split_by != null) {
state.ooBiasSplitBy = d.oo_bias_split_by === "sheets" ? "sheets" : "budget";
}
if (d.oo_bias_ratio != null && Number(d.oo_bias_ratio) > 0 && Number(d.oo_bias_ratio) < 1) {
state.ooBiasRatio = Number(d.oo_bias_ratio);
}
syncOoCloseModeUI();
setGateLine(d);
if (state.mode === "options_options") autoFillOoSheets();
} catch (e) {
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
}
+6
View File
@@ -91,6 +91,8 @@ HOT_RELOAD_EXACT = frozenset({
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
"HEDGE_PLAN_OO_BIAS_RATIO",
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
"MAX_ACTIVE_HEDGE_PLANS",
@@ -121,6 +123,10 @@ SELECT_OPTIONS: dict[str, tuple[tuple[str, str], ...]] = {
("long_only", "仅做多"),
("short_only", "仅做空"),
),
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": (
("budget", "预算金额"),
("sheets", "张数"),
),
}
_SELECT_ALIASES: dict[str, dict[str, str]] = {
+12
View File
@@ -146,6 +146,16 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
"期期平仓模式(方案C)",
"默认 true;开启后页面可选「到期平/全平」(盈利腿平后另一腿);关闭则固定到期平",
),
(
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
"期期做多做空拆分口径",
"默认预算金额;budget=按权利金预算按比例分两腿;sheets=先算同张数 n 再按比例拆张数",
),
(
"HEDGE_PLAN_OO_BIAS_RATIO",
"期期做多做空主腿占比",
"默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间",
),
(
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
"对冲与期权互斥门控",
@@ -177,6 +187,8 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget",
"HEDGE_PLAN_OO_BIAS_RATIO": "0.7",
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
}
+116 -40
View File
@@ -150,6 +150,54 @@ def _cap_sheets_by_ask_depth(sheets: int, ask_sz: Any) -> int:
return min(n, int(math.floor(float(depth) + 1e-12)))
def _normalize_oo_sheets_mode(mode: str) -> str:
m = (mode or "same_sheets").strip().lower()
if m in ("long_bias", "bias_long", "long", "做多"):
return "long_bias"
if m in ("short_bias", "bias_short", "short", "做空"):
return "short_bias"
# 旧「均分」兼容:按预算 50/50(页面已移除)
if m in ("split", "equal_budget", "split_budget", "均分"):
return "split_budget"
return "same_sheets"
def _normalize_oo_bias_split_by(raw: Any) -> str:
v = str(raw or "budget").strip().lower()
if v in ("sheets", "qty", "quantity", "张数"):
return "sheets"
return "budget"
def _clamp_oo_bias_ratio(raw: Any, default: float = 0.7) -> float:
try:
r = float(raw)
except (TypeError, ValueError):
r = float(default)
if r <= 0 or r >= 1:
r = float(default)
return r
def _oo_call_put_leg_index(opt_type_a: str, opt_type_b: str) -> tuple[Optional[str], Optional[str], str]:
"""返回 (call_side, put_side, err);side 为 'a'/'b'."""
a = (opt_type_a or "").strip().upper()
b = (opt_type_b or "").strip().upper()
if a.startswith("C"):
a = "C"
elif a.startswith("P"):
a = "P"
if b.startswith("C"):
b = "C"
elif b.startswith("P"):
b = "P"
if {a, b} != {"C", "P"}:
return None, None, "做多/做空需一腿 Call、一腿 Put"
call_side = "a" if a == "C" else "b"
put_side = "b" if call_side == "a" else "a"
return call_side, put_side, ""
def suggest_oo_sheets(
*,
mode: str,
@@ -157,63 +205,89 @@ def suggest_oo_sheets(
ask_a: float,
ct_mult_a: float = 0.01,
ask_sz_a: Any = None,
opt_type_a: str = "",
ask_b: float,
ct_mult_b: float = 0.01,
ask_sz_b: Any = None,
opt_type_b: str = "",
bias_split_by: str = "budget",
bias_ratio: float = 0.7,
) -> dict[str, Any]:
"""期期建议张数:same_sheets(同张数,默认) / split_budget(均分预算)."""
"""期期建议张数:same_sheets / long_bias / short_bias(及旧 split_budget)."""
import math
m = (mode or "same_sheets").strip().lower()
if m in ("split", "equal_budget", "split_budget", "均分"):
m = "split_budget"
else:
m = "same_sheets"
m = _normalize_oo_sheets_mode(mode)
split_by = _normalize_oo_bias_split_by(bias_split_by)
ratio = _clamp_oo_bias_ratio(bias_ratio)
budget = max(0.0, float(budget_usdc or 0.0))
cost_a = option_unit_cost_usdc(ask=ask_a, ct_mult=ct_mult_a)
cost_b = option_unit_cost_usdc(ask=ask_b, ct_mult=ct_mult_b)
def _fail(msg: str, n_a: int = 0, n_b: int = 0) -> dict[str, Any]:
return {
"mode": m,
"sheets_a": n_a,
"sheets_b": n_b,
"cost_a": round(cost_a, 8),
"cost_b": round(cost_b, 8),
"premium_est": round(cost_a * n_a + cost_b * n_b, 6),
"ok": False,
"msg": msg,
"bias_split_by": split_by,
"bias_ratio": ratio,
}
if budget <= 0:
return {
"mode": m,
"sheets_a": 0,
"sheets_b": 0,
"cost_a": round(cost_a, 8),
"cost_b": round(cost_b, 8),
"premium_est": 0.0,
"ok": False,
"msg": "可用预算为 0",
}
return _fail("可用预算为 0")
if cost_a <= 0 or cost_b <= 0:
return {
"mode": m,
"sheets_a": 0,
"sheets_b": 0,
"cost_a": round(cost_a, 8),
"cost_b": round(cost_b, 8),
"premium_est": 0.0,
"ok": False,
"msg": "缺少有效卖一价,无法建议张数",
}
if m == "split_budget":
return _fail("缺少有效卖一价,无法建议张数")
pair = cost_a + cost_b
n_same = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0
n_same = _cap_sheets_by_ask_depth(n_same, ask_sz_a)
n_same = _cap_sheets_by_ask_depth(n_same, ask_sz_b)
if m == "same_sheets":
n_a = n_same
n_b = n_same
elif m == "split_budget":
half = budget / 2.0
n_a = int(math.floor(half / cost_a + 1e-12))
n_b = int(math.floor(half / cost_b + 1e-12))
n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
else:
pair = cost_a + cost_b
n = int(math.floor(budget / pair + 1e-12)) if pair > 0 else 0
n_a = n
n_b = n
n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
if m == "same_sheets":
n = min(n_a, n_b)
n_a = n
n_b = n
call_side, put_side, err = _oo_call_put_leg_index(opt_type_a, opt_type_b)
if err:
return _fail(err)
major_is_call = m == "long_bias"
if split_by == "sheets":
if n_same < 2:
return _fail("同张数规模不足 2,无法按比例拆分")
major_n = int(round(n_same * ratio))
major_n = max(1, min(major_n, n_same - 1))
minor_n = n_same - major_n
n_call = major_n if major_is_call else minor_n
n_put = minor_n if major_is_call else major_n
else:
maj_budget = budget * ratio
min_budget = budget * (1.0 - ratio)
cost_call = cost_a if call_side == "a" else cost_b
cost_put = cost_b if call_side == "a" else cost_a
if major_is_call:
n_call = int(math.floor(maj_budget / cost_call + 1e-12)) if cost_call > 0 else 0
n_put = int(math.floor(min_budget / cost_put + 1e-12)) if cost_put > 0 else 0
else:
n_put = int(math.floor(maj_budget / cost_put + 1e-12)) if cost_put > 0 else 0
n_call = int(math.floor(min_budget / cost_call + 1e-12)) if cost_call > 0 else 0
n_a = n_call if call_side == "a" else n_put
n_b = n_put if call_side == "a" else n_call
n_a = _cap_sheets_by_ask_depth(n_a, ask_sz_a)
n_b = _cap_sheets_by_ask_depth(n_b, ask_sz_b)
prem = cost_a * n_a + cost_b * n_b
ok = n_a >= 1 and n_b >= 1
msg = ""
if not ok:
msg = "预算不够开 1+1(或卖一深度不足)"
msg = "" if ok else "预算不够开 1+1(或卖一深度不足)"
return {
"mode": m,
"sheets_a": n_a,
@@ -223,6 +297,8 @@ def suggest_oo_sheets(
"premium_est": round(prem, 6),
"ok": ok,
"msg": msg,
"bias_split_by": split_by,
"bias_ratio": ratio,
}
+14
View File
@@ -120,6 +120,18 @@ def _oo_close_mode_enabled() -> bool:
return _env_bool("HEDGE_PLAN_OO_CLOSE_MODE_ENABLED", True)
def _oo_bias_split_by() -> str:
from lib.hedge_plan.hedge_plan_calc_lib import _normalize_oo_bias_split_by
return _normalize_oo_bias_split_by(os.getenv("HEDGE_PLAN_OO_BIAS_SPLIT_BY") or "budget")
def _oo_bias_ratio() -> float:
from lib.hedge_plan.hedge_plan_calc_lib import _clamp_oo_bias_ratio
return _clamp_oo_bias_ratio(os.getenv("HEDGE_PLAN_OO_BIAS_RATIO") or "0.7")
def _normalize_oo_close_mode(raw: Any) -> str:
"""方案C关闭时强制 hold_expiry;开启时默认 close_all."""
if not _oo_close_mode_enabled():
@@ -192,6 +204,8 @@ def _gates_public(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]:
g = _gates_dict(cfg, plan_type)
g["oo_close_mode_enabled"] = _oo_close_mode_enabled()
g["oo_close_mode_default"] = "close_all" if _oo_close_mode_enabled() else "hold_expiry"
g["oo_bias_split_by"] = _oo_bias_split_by()
g["oo_bias_ratio"] = _oo_bias_ratio()
return g
@@ -135,7 +135,8 @@
<span class="hp-oo-ctrl-lab">张数</span>
<div class="hp-oo-seg" role="group" aria-label="自动张数">
<button type="button" class="btn-secondary hp-oo-size-mode is-selected" data-oo-size="same_sheets" title="两腿同张数,总权利金≤预算"><span class="hp-oo-check" aria-hidden="true"></span>同张数</button>
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="split_budget" title="预算对半拆到两腿"><span class="hp-oo-check" aria-hidden="true"></span>均分</button>
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="long_bias" title="偏多:Call 占比更高(比例见 env)"><span class="hp-oo-check" aria-hidden="true"></span>做多</button>
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="short_bias" title="偏空:Put 占比更高(比例见 env)"><span class="hp-oo-check" aria-hidden="true"></span>做空</button>
</div>
</div>
<div class="hp-oo-ctrl" id="hp-oo-close-mode-row">
@@ -300,4 +301,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=22"></script>
<script src="/static/hedge_plan.js?v=23"></script>