Hedge start: re-quote ask and resize OO sheets; add HEDGE_PLAN_BUDGET_BUFFER.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -487,9 +487,9 @@
|
|||||||
else if (b.msg) parts.push(b.msg);
|
else if (b.msg) parts.push(b.msg);
|
||||||
line.textContent = parts.join(" · ");
|
line.textContent = parts.join(" · ");
|
||||||
line.title =
|
line.title =
|
||||||
"预算=min(交易×" +
|
"对冲预算=min(交易×" +
|
||||||
fmt(b.buf, 2) +
|
fmt(b.buf, 2) +
|
||||||
", 单笔)" +
|
", 单笔) · 缓冲 HEDGE_PLAN_BUDGET_BUFFER" +
|
||||||
(b.tradingCap != null ? " · 交易×缓冲 " + fmt(b.tradingCap, 2) : "") +
|
(b.tradingCap != null ? " · 交易×缓冲 " + fmt(b.tradingCap, 2) : "") +
|
||||||
(b.tradeCap != null ? " · 单笔 " + fmt(b.tradeCap, 2) : "");
|
(b.tradeCap != null ? " · 单笔 " + fmt(b.tradeCap, 2) : "");
|
||||||
line.classList.toggle("hp-oo-budget-warn", !!(extra && extra.msg) || !b.ok);
|
line.classList.toggle("hp-oo-budget-warn", !!(extra && extra.msg) || !b.ok);
|
||||||
@@ -1692,6 +1692,7 @@
|
|||||||
target_price_down: down,
|
target_price_down: down,
|
||||||
target_price: up,
|
target_price: up,
|
||||||
oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry",
|
oo_close_mode: state.ooCloseModeEnabled ? state.ooCloseMode : "hold_expiry",
|
||||||
|
oo_sheets_mode: state.ooSheetsMode || "same_sheets",
|
||||||
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
|
leg_a: legPayload(state.legA, ooSheets("hp-oo-sheets-a")),
|
||||||
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
|
leg_b: legPayload(state.legB, ooSheets("hp-oo-sheets-b")),
|
||||||
};
|
};
|
||||||
@@ -1731,19 +1732,28 @@
|
|||||||
});
|
});
|
||||||
setGateLine(d.gates);
|
setGateLine(d.gates);
|
||||||
closePreviewModal();
|
closePreviewModal();
|
||||||
|
const refreshHint =
|
||||||
|
d.refresh && d.refresh.msg ? "\n" + String(d.refresh.msg) : "";
|
||||||
if (d.partial) {
|
if (d.partial) {
|
||||||
alert(
|
alert(
|
||||||
(d.msg || "半腿失败,已挂待补") +
|
(d.msg || "半腿失败,已挂待补") +
|
||||||
(d.plan_id ? "\n计划 #" + d.plan_id : "") +
|
(d.plan_id ? "\n计划 #" + d.plan_id : "") +
|
||||||
|
refreshHint +
|
||||||
"\n请到「进行中的计划」补开缺失腿"
|
"\n请到「进行中的计划」补开缺失腿"
|
||||||
);
|
);
|
||||||
state.tab = "active";
|
state.tab = "active";
|
||||||
syncTabUI();
|
syncTabUI();
|
||||||
void loadActivePlans();
|
void loadActivePlans();
|
||||||
} else {
|
} else {
|
||||||
alert("计划已启动 #" + (d.plan_id || "") + (d.dry_run ? " (dry_run)" : ""));
|
alert(
|
||||||
|
"计划已启动 #" +
|
||||||
|
(d.plan_id || "") +
|
||||||
|
(d.dry_run ? " (dry_run)" : "") +
|
||||||
|
refreshHint
|
||||||
|
);
|
||||||
}
|
}
|
||||||
void loadGates();
|
void loadGates();
|
||||||
|
if (isOo) void loadChain();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.message || String(e));
|
alert(e.message || String(e));
|
||||||
syncPreviewStartBtn();
|
syncPreviewStartBtn();
|
||||||
|
|||||||
Vendored
+1
@@ -94,6 +94,7 @@ HOT_RELOAD_EXACT = frozenset({
|
|||||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
|
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED",
|
||||||
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
|
"HEDGE_PLAN_OO_BIAS_SPLIT_BY",
|
||||||
"HEDGE_PLAN_OO_BIAS_RATIO",
|
"HEDGE_PLAN_OO_BIAS_RATIO",
|
||||||
|
"HEDGE_PLAN_BUDGET_BUFFER",
|
||||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
|
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL",
|
||||||
"MAX_ACTIVE_HEDGE_PLANS",
|
"MAX_ACTIVE_HEDGE_PLANS",
|
||||||
|
|||||||
Vendored
+6
@@ -165,6 +165,11 @@ _HEDGE_PLAN_SECTION: dict[str, Any] = {
|
|||||||
"期期做多做空主腿占比",
|
"期期做多做空主腿占比",
|
||||||
"默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间",
|
"默认 0.7(即 7:3);做多主腿=Call,做空主腿=Put;须在 0~1 之间",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"HEDGE_PLAN_BUDGET_BUFFER",
|
||||||
|
"对冲预算缓冲比例",
|
||||||
|
"默认 0.95;仅对冲计划(期期可用预算=交易户×本比例);与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE",
|
||||||
"对冲与期权互斥门控",
|
"对冲与期权互斥门控",
|
||||||
@@ -199,6 +204,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
|||||||
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
|
"HEDGE_PLAN_OO_CLOSE_MODE_ENABLED": "true",
|
||||||
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget",
|
"HEDGE_PLAN_OO_BIAS_SPLIT_BY": "budget",
|
||||||
"HEDGE_PLAN_OO_BIAS_RATIO": "0.7",
|
"HEDGE_PLAN_OO_BIAS_RATIO": "0.7",
|
||||||
|
"HEDGE_PLAN_BUDGET_BUFFER": "0.95",
|
||||||
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
|
"HEDGE_PLAN_OPTIONS_MUTUAL_EXCLUSIVE": "true",
|
||||||
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
|
"HEDGE_PLAN_MANUAL_COMPLETE_ON_PARTIAL": "true",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,6 +350,201 @@ def _park_partial(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _hedge_budget_buffer(cfg: dict[str, Any] | None = None) -> float:
|
||||||
|
"""对冲专用预算缓冲;默认 0.95.与 OKX_OPTIONS_BUDGET_BUFFER 独立."""
|
||||||
|
raw = None
|
||||||
|
if cfg is not None:
|
||||||
|
raw = cfg.get("budget_buffer")
|
||||||
|
if raw is None or raw == "":
|
||||||
|
raw = os.getenv("HEDGE_PLAN_BUDGET_BUFFER") or "0.95"
|
||||||
|
try:
|
||||||
|
buf = float(raw)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
buf = 0.95
|
||||||
|
if buf <= 0:
|
||||||
|
buf = 0.95
|
||||||
|
if buf > 1:
|
||||||
|
buf = 1.0
|
||||||
|
return float(buf)
|
||||||
|
|
||||||
|
|
||||||
|
def _oo_bias_settings(cfg: dict[str, Any] | None = None) -> tuple[str, float]:
|
||||||
|
from lib.hedge_plan.hedge_plan_calc_lib import _clamp_oo_bias_ratio, _normalize_oo_bias_split_by
|
||||||
|
|
||||||
|
split = None
|
||||||
|
ratio = None
|
||||||
|
if cfg is not None:
|
||||||
|
split = cfg.get("oo_bias_split_by")
|
||||||
|
ratio = cfg.get("oo_bias_ratio")
|
||||||
|
if split in (None, ""):
|
||||||
|
split = os.getenv("HEDGE_PLAN_OO_BIAS_SPLIT_BY") or "budget"
|
||||||
|
if ratio in (None, ""):
|
||||||
|
ratio = os.getenv("HEDGE_PLAN_OO_BIAS_RATIO") or "0.7"
|
||||||
|
return _normalize_oo_bias_split_by(split), _clamp_oo_bias_ratio(ratio)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_oo_sizing_before_start(cfg: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""启动前再拉两腿卖一,按对冲预算缓冲重算张数;就地写回 body.leg_*.
|
||||||
|
|
||||||
|
方案 A:成交价与张数均基于点击启动瞬间的最新卖一/余额.
|
||||||
|
"""
|
||||||
|
from lib.exchange.okx_options_lib import fetch_options_trading_usdc, option_buy_liquidity_ok
|
||||||
|
from lib.hedge_plan.hedge_plan_calc_lib import resolve_oo_budget_usdc, suggest_oo_sheets
|
||||||
|
|
||||||
|
leg_a = dict(body.get("leg_a") or {})
|
||||||
|
leg_b = dict(body.get("leg_b") or {})
|
||||||
|
inst_a = str(leg_a.get("inst_id") or "").strip()
|
||||||
|
inst_b = str(leg_b.get("inst_id") or "").strip()
|
||||||
|
if not inst_a or not inst_b:
|
||||||
|
return {"ok": False, "msg": "缺少期权合约"}
|
||||||
|
quote_fn = cfg.get("quote_option_contract")
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
if not callable(quote_fn) or ex is None:
|
||||||
|
return {"ok": False, "msg": "期权报价能力未就绪"}
|
||||||
|
|
||||||
|
qa = quote_fn(ex, inst_a)
|
||||||
|
if not qa.get("ok"):
|
||||||
|
return {"ok": False, "msg": qa.get("msg") or "腿A报价失败", "quote_a": qa}
|
||||||
|
qb = quote_fn(ex, inst_b)
|
||||||
|
if not qb.get("ok"):
|
||||||
|
return {"ok": False, "msg": qb.get("msg") or "腿B报价失败", "quote_b": qb}
|
||||||
|
|
||||||
|
for tag, q in (("A", qa), ("B", qb)):
|
||||||
|
can_open, block_msg = option_buy_liquidity_ok(q.get("ask"), q.get("ask_sz"))
|
||||||
|
if not can_open:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": f"腿{tag}: {block_msg or '暂无卖一深度,无法买入'}",
|
||||||
|
"quote_a": qa,
|
||||||
|
"quote_b": qb,
|
||||||
|
}
|
||||||
|
|
||||||
|
trading = fetch_options_trading_usdc(ex)
|
||||||
|
buf = _hedge_budget_buffer(cfg)
|
||||||
|
budget_info = resolve_oo_budget_usdc(
|
||||||
|
trading_usdc=trading,
|
||||||
|
trade_budget_usdc=cfg.get("trade_budget_usdc"),
|
||||||
|
buffer_ratio=buf,
|
||||||
|
)
|
||||||
|
if not budget_info.get("ok"):
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": budget_info.get("msg") or "可用预算不足",
|
||||||
|
"budget": budget_info,
|
||||||
|
"quote_a": qa,
|
||||||
|
"quote_b": qb,
|
||||||
|
}
|
||||||
|
|
||||||
|
mode = str(body.get("oo_sheets_mode") or "same_sheets")
|
||||||
|
split_by, bias_ratio = _oo_bias_settings(cfg)
|
||||||
|
opt_a = str(
|
||||||
|
leg_a.get("opt_type")
|
||||||
|
or (qa.get("meta") or {}).get("optType")
|
||||||
|
or qa.get("opt_type")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
opt_b = str(
|
||||||
|
leg_b.get("opt_type")
|
||||||
|
or (qb.get("meta") or {}).get("optType")
|
||||||
|
or qb.get("opt_type")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
sug = suggest_oo_sheets(
|
||||||
|
mode=mode,
|
||||||
|
budget_usdc=float(budget_info["budget_usdc"]),
|
||||||
|
ask_a=float(qa["ask"]),
|
||||||
|
ct_mult_a=float(qa.get("ct_mult") or leg_a.get("ct_mult") or 0.01),
|
||||||
|
ask_sz_a=qa.get("ask_sz"),
|
||||||
|
opt_type_a=opt_a,
|
||||||
|
ask_b=float(qb["ask"]),
|
||||||
|
ct_mult_b=float(qb.get("ct_mult") or leg_b.get("ct_mult") or 0.01),
|
||||||
|
ask_sz_b=qb.get("ask_sz"),
|
||||||
|
opt_type_b=opt_b,
|
||||||
|
bias_split_by=split_by,
|
||||||
|
bias_ratio=bias_ratio,
|
||||||
|
)
|
||||||
|
if not sug.get("ok"):
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": sug.get("msg") or "按最新卖一无法建议张数",
|
||||||
|
"sizing": sug,
|
||||||
|
"budget": budget_info,
|
||||||
|
"quote_a": qa,
|
||||||
|
"quote_b": qb,
|
||||||
|
}
|
||||||
|
|
||||||
|
prev_a = leg_a.get("sheets")
|
||||||
|
prev_b = leg_b.get("sheets")
|
||||||
|
leg_a["sheets"] = int(sug["sheets_a"])
|
||||||
|
leg_a["ask"] = float(qa["ask"])
|
||||||
|
leg_a["ask_sz"] = qa.get("ask_sz")
|
||||||
|
leg_a["ct_mult"] = float(qa.get("ct_mult") or leg_a.get("ct_mult") or 0.01)
|
||||||
|
if opt_a:
|
||||||
|
leg_a["opt_type"] = opt_a
|
||||||
|
leg_b["sheets"] = int(sug["sheets_b"])
|
||||||
|
leg_b["ask"] = float(qb["ask"])
|
||||||
|
leg_b["ask_sz"] = qb.get("ask_sz")
|
||||||
|
leg_b["ct_mult"] = float(qb.get("ct_mult") or leg_b.get("ct_mult") or 0.01)
|
||||||
|
if opt_b:
|
||||||
|
leg_b["opt_type"] = opt_b
|
||||||
|
body["leg_a"] = leg_a
|
||||||
|
body["leg_b"] = leg_b
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"buffer_ratio": buf,
|
||||||
|
"budget": budget_info,
|
||||||
|
"sizing": sug,
|
||||||
|
"quote_a": qa,
|
||||||
|
"quote_b": qb,
|
||||||
|
"prev_sheets_a": prev_a,
|
||||||
|
"prev_sheets_b": prev_b,
|
||||||
|
"sheets_a": int(sug["sheets_a"]),
|
||||||
|
"sheets_b": int(sug["sheets_b"]),
|
||||||
|
"ask_a": float(qa["ask"]),
|
||||||
|
"ask_b": float(qb["ask"]),
|
||||||
|
"premium_est": sug.get("premium_est"),
|
||||||
|
"msg": (
|
||||||
|
f"已按最新卖一重算: A {sug['sheets_a']}张@{qa['ask']} + "
|
||||||
|
f"B {sug['sheets_b']}张@{qb['ask']} · 预估 {sug.get('premium_est')}U"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_po_option_quote_before_start(cfg: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""永期启动前再拉保险腿卖一(张数沿用页面值,不按预算重算)."""
|
||||||
|
from lib.exchange.okx_options_lib import option_buy_liquidity_ok
|
||||||
|
|
||||||
|
inst = str(body.get("opt_inst_id") or "").strip()
|
||||||
|
if not inst:
|
||||||
|
return {"ok": False, "msg": "缺少期权合约"}
|
||||||
|
quote_fn = cfg.get("quote_option_contract")
|
||||||
|
ex = cfg.get("exchange_options")
|
||||||
|
if not callable(quote_fn) or ex is None:
|
||||||
|
return {"ok": False, "msg": "期权报价能力未就绪"}
|
||||||
|
q = quote_fn(ex, inst)
|
||||||
|
if not q.get("ok"):
|
||||||
|
return {"ok": False, "msg": q.get("msg") or "期权报价失败", "quote": q}
|
||||||
|
can_open, block_msg = option_buy_liquidity_ok(q.get("ask"), q.get("ask_sz"))
|
||||||
|
if not can_open:
|
||||||
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": block_msg or "暂无卖一深度,无法买入",
|
||||||
|
"quote": q,
|
||||||
|
}
|
||||||
|
body["ask"] = float(q["ask"])
|
||||||
|
body["ask_sz"] = q.get("ask_sz")
|
||||||
|
if q.get("ct_mult") is not None:
|
||||||
|
body["ct_mult"] = float(q.get("ct_mult") or 0.01)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"ask": float(q["ask"]),
|
||||||
|
"ask_sz": q.get("ask_sz"),
|
||||||
|
"sheets": body.get("sheets"),
|
||||||
|
"quote": q,
|
||||||
|
"msg": f"已按最新卖一: {body.get('sheets')}张@{q['ask']}",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def execute_perp_options_start(
|
def execute_perp_options_start(
|
||||||
cfg: dict[str, Any],
|
cfg: dict[str, Any],
|
||||||
body: dict[str, Any],
|
body: dict[str, Any],
|
||||||
@@ -357,6 +552,9 @@ def execute_perp_options_start(
|
|||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
persist: Optional[Callable[..., Any]] = None,
|
persist: Optional[Callable[..., Any]] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
refresh = refresh_po_option_quote_before_start(cfg, body)
|
||||||
|
if not refresh.get("ok"):
|
||||||
|
return {"ok": False, "msg": refresh.get("msg") or "启动前刷新卖一失败", "refresh": refresh}
|
||||||
path = build_po_path_plan(body)
|
path = build_po_path_plan(body)
|
||||||
results: list[dict[str, Any]] = []
|
results: list[dict[str, Any]] = []
|
||||||
opt_res: Optional[dict[str, Any]] = None
|
opt_res: Optional[dict[str, Any]] = None
|
||||||
@@ -461,6 +659,7 @@ def execute_perp_options_start(
|
|||||||
"results": results,
|
"results": results,
|
||||||
"option": opt_res,
|
"option": opt_res,
|
||||||
"perp": perp_res,
|
"perp": perp_res,
|
||||||
|
"refresh": refresh,
|
||||||
"opened_at": _now(),
|
"opened_at": _now(),
|
||||||
}
|
}
|
||||||
if persist and not dry_run:
|
if persist and not dry_run:
|
||||||
@@ -475,6 +674,9 @@ def execute_options_options_start(
|
|||||||
dry_run: bool = False,
|
dry_run: bool = False,
|
||||||
persist: Optional[Callable[..., Any]] = None,
|
persist: Optional[Callable[..., Any]] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
refresh = refresh_oo_sizing_before_start(cfg, body)
|
||||||
|
if not refresh.get("ok"):
|
||||||
|
return {"ok": False, "msg": refresh.get("msg") or "启动前刷新卖一/张数失败", "refresh": refresh}
|
||||||
path = build_oo_path_plan(body)
|
path = build_oo_path_plan(body)
|
||||||
results: list[dict[str, Any]] = []
|
results: list[dict[str, Any]] = []
|
||||||
leg_a = body.get("leg_a") or {}
|
leg_a = body.get("leg_a") or {}
|
||||||
@@ -482,7 +684,13 @@ def execute_options_options_start(
|
|||||||
a_res = _buy_option(cfg, inst_id=str(leg_a.get("inst_id") or ""), sheets=float(leg_a.get("sheets") or 1), dry_run=dry_run)
|
a_res = _buy_option(cfg, inst_id=str(leg_a.get("inst_id") or ""), sheets=float(leg_a.get("sheets") or 1), dry_run=dry_run)
|
||||||
results.append({"step": "options_buy_limit", "leg": "a", **a_res})
|
results.append({"step": "options_buy_limit", "leg": "a", **a_res})
|
||||||
if not a_res.get("ok"):
|
if not a_res.get("ok"):
|
||||||
return {"ok": False, "msg": a_res.get("msg") or "腿A开仓失败", "path": path, "results": results}
|
return {
|
||||||
|
"ok": False,
|
||||||
|
"msg": a_res.get("msg") or "腿A开仓失败",
|
||||||
|
"path": path,
|
||||||
|
"results": results,
|
||||||
|
"refresh": refresh,
|
||||||
|
}
|
||||||
b_res = _buy_option(cfg, inst_id=str(leg_b.get("inst_id") or ""), sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run)
|
b_res = _buy_option(cfg, inst_id=str(leg_b.get("inst_id") or ""), sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run)
|
||||||
results.append({"step": "options_buy_limit", "leg": "b", **b_res})
|
results.append({"step": "options_buy_limit", "leg": "b", **b_res})
|
||||||
if not b_res.get("ok"):
|
if not b_res.get("ok"):
|
||||||
@@ -497,9 +705,10 @@ def execute_options_options_start(
|
|||||||
"path": path,
|
"path": path,
|
||||||
"results": results,
|
"results": results,
|
||||||
"partial": True,
|
"partial": True,
|
||||||
|
"refresh": refresh,
|
||||||
}
|
}
|
||||||
if not dry_run and manual_complete_on_partial() and persist:
|
if not dry_run and manual_complete_on_partial() and persist:
|
||||||
return _park_partial(
|
out_p = _park_partial(
|
||||||
cfg,
|
cfg,
|
||||||
plan_type="options_options",
|
plan_type="options_options",
|
||||||
body=body,
|
body=body,
|
||||||
@@ -512,6 +721,8 @@ def execute_options_options_start(
|
|||||||
leg_a=a_res,
|
leg_a=a_res,
|
||||||
leg_b=None,
|
leg_b=None,
|
||||||
)
|
)
|
||||||
|
out_p["refresh"] = refresh
|
||||||
|
return out_p
|
||||||
msg = b_res.get("msg") or "腿B开仓失败"
|
msg = b_res.get("msg") or "腿B开仓失败"
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
_notify_partial(cfg, "options_options", msg, results)
|
_notify_partial(cfg, "options_options", msg, results)
|
||||||
@@ -521,6 +732,7 @@ def execute_options_options_start(
|
|||||||
"path": path,
|
"path": path,
|
||||||
"results": results,
|
"results": results,
|
||||||
"partial": True,
|
"partial": True,
|
||||||
|
"refresh": refresh,
|
||||||
}
|
}
|
||||||
out = {
|
out = {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
@@ -530,6 +742,7 @@ def execute_options_options_start(
|
|||||||
"results": results,
|
"results": results,
|
||||||
"leg_a": a_res,
|
"leg_a": a_res,
|
||||||
"leg_b": b_res,
|
"leg_b": b_res,
|
||||||
|
"refresh": refresh,
|
||||||
"opened_at": _now(),
|
"opened_at": _now(),
|
||||||
}
|
}
|
||||||
if persist and not dry_run:
|
if persist and not dry_run:
|
||||||
|
|||||||
@@ -98,7 +98,10 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
|||||||
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
|
"perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(),
|
||||||
"options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(),
|
"options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(),
|
||||||
"trade_budget_usdc": float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC") or "10"),
|
"trade_budget_usdc": float(os.getenv("OKX_OPTIONS_TRADE_BUDGET_USDC") or "10"),
|
||||||
"budget_buffer": float(os.getenv("OKX_OPTIONS_BUDGET_BUFFER") or "0.95"),
|
# 对冲专用缓冲;与期权页 OKX_OPTIONS_BUDGET_BUFFER 独立
|
||||||
|
"budget_buffer": float(os.getenv("HEDGE_PLAN_BUDGET_BUFFER") or "0.95"),
|
||||||
|
"oo_bias_split_by": _oo_bias_split_by(),
|
||||||
|
"oo_bias_ratio": _oo_bias_ratio(),
|
||||||
"live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
|
"live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
|
||||||
"send_wechat": getattr(app_module, "send_wechat_msg", None),
|
"send_wechat": getattr(app_module, "send_wechat_msg", None),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,4 +301,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=28"></script>
|
<script src="/static/hedge_plan.js?v=29"></script>
|
||||||
|
|||||||
@@ -133,15 +133,15 @@ class TestHedgePlanOrderPath(unittest.TestCase):
|
|||||||
|
|
||||||
def test_dry_run_oo(self):
|
def test_dry_run_oo(self):
|
||||||
quote = MagicMock(
|
quote = MagicMock(
|
||||||
return_value={
|
side_effect=lambda _ex, inst_id: {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"ask": 10,
|
"ask": 10,
|
||||||
"ask_sz": 5,
|
"ask_sz": 50,
|
||||||
"can_open": True,
|
"can_open": True,
|
||||||
"ct_mult": 0.01,
|
"ct_mult": 0.01,
|
||||||
"tick_sz": "0.1",
|
"tick_sz": "0.1",
|
||||||
"strike": 1800,
|
"strike": 1800,
|
||||||
"meta": {"optType": "C"},
|
"meta": {"optType": "C" if inst_id == "A" else "P"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
cfg = {
|
cfg = {
|
||||||
@@ -149,17 +149,27 @@ class TestHedgePlanOrderPath(unittest.TestCase):
|
|||||||
"quote_option_contract": quote,
|
"quote_option_contract": quote,
|
||||||
"place_option_limit_order": MagicMock(),
|
"place_option_limit_order": MagicMock(),
|
||||||
"td_mode_for_option_buy": lambda x: "isolated",
|
"td_mode_for_option_buy": lambda x: "isolated",
|
||||||
|
"trade_budget_usdc": 10,
|
||||||
|
"budget_buffer": 0.95,
|
||||||
}
|
}
|
||||||
body = {
|
body = {
|
||||||
"target_price": 1900,
|
"target_price": 1900,
|
||||||
"target_price_up": 1950,
|
"target_price_up": 1950,
|
||||||
"target_price_down": 1750,
|
"target_price_down": 1750,
|
||||||
"leg_a": {"inst_id": "A", "sheets": 1},
|
"oo_sheets_mode": "same_sheets",
|
||||||
"leg_b": {"inst_id": "B", "sheets": 1},
|
"leg_a": {"inst_id": "A", "sheets": 1, "opt_type": "C"},
|
||||||
|
"leg_b": {"inst_id": "B", "sheets": 1, "opt_type": "P"},
|
||||||
}
|
}
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"lib.exchange.okx_options_lib.fetch_options_trading_usdc",
|
||||||
|
return_value=100.0,
|
||||||
|
):
|
||||||
out = execute_options_options_start(cfg, body, dry_run=True)
|
out = execute_options_options_start(cfg, body, dry_run=True)
|
||||||
self.assertTrue(out["ok"])
|
self.assertTrue(out["ok"], out)
|
||||||
self.assertEqual(len(out["results"]), 2)
|
self.assertEqual(len(out["results"]), 2)
|
||||||
|
self.assertTrue(out.get("refresh", {}).get("ok"))
|
||||||
|
|
||||||
def test_buy_rejects_without_ask_depth(self):
|
def test_buy_rejects_without_ask_depth(self):
|
||||||
from lib.hedge_plan.hedge_plan_orders_lib import _buy_option
|
from lib.hedge_plan.hedge_plan_orders_lib import _buy_option
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
"""对冲启动:再拉卖一 + 对冲专用预算缓冲重算张数."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from lib.hedge_plan.hedge_plan_orders_lib import (
|
||||||
|
_hedge_budget_buffer,
|
||||||
|
execute_options_options_start,
|
||||||
|
refresh_oo_sizing_before_start,
|
||||||
|
refresh_po_option_quote_before_start,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestHedgeStartRefresh(unittest.TestCase):
|
||||||
|
def test_hedge_budget_buffer_independent(self):
|
||||||
|
with mock.patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"HEDGE_PLAN_BUDGET_BUFFER": "0.9",
|
||||||
|
"OKX_OPTIONS_BUDGET_BUFFER": "0.5",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
self.assertAlmostEqual(_hedge_budget_buffer(None), 0.9)
|
||||||
|
self.assertAlmostEqual(_hedge_budget_buffer({"budget_buffer": 0.88}), 0.88)
|
||||||
|
|
||||||
|
def test_refresh_oo_resizes_from_fresh_ask(self):
|
||||||
|
def quote(_ex, inst_id):
|
||||||
|
if inst_id == "A":
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"ask": 20,
|
||||||
|
"ask_sz": 100,
|
||||||
|
"ct_mult": 0.01,
|
||||||
|
"meta": {"optType": "C"},
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"ask": 20,
|
||||||
|
"ask_sz": 100,
|
||||||
|
"ct_mult": 0.01,
|
||||||
|
"meta": {"optType": "P"},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg = {
|
||||||
|
"exchange_options": object(),
|
||||||
|
"quote_option_contract": quote,
|
||||||
|
"trade_budget_usdc": 10,
|
||||||
|
"budget_buffer": 0.95,
|
||||||
|
}
|
||||||
|
body = {
|
||||||
|
"oo_sheets_mode": "same_sheets",
|
||||||
|
"leg_a": {"inst_id": "A", "sheets": 1, "opt_type": "C"},
|
||||||
|
"leg_b": {"inst_id": "B", "sheets": 1, "opt_type": "P"},
|
||||||
|
}
|
||||||
|
# unit cost = 20*0.01=0.2 each → pair 0.4; budget min(100*0.95,10)=10 → n=25
|
||||||
|
with mock.patch(
|
||||||
|
"lib.exchange.okx_options_lib.fetch_options_trading_usdc",
|
||||||
|
return_value=100.0,
|
||||||
|
):
|
||||||
|
out = refresh_oo_sizing_before_start(cfg, body)
|
||||||
|
self.assertTrue(out.get("ok"), out)
|
||||||
|
self.assertEqual(body["leg_a"]["sheets"], 25)
|
||||||
|
self.assertEqual(body["leg_b"]["sheets"], 25)
|
||||||
|
self.assertEqual(body["leg_a"]["ask"], 20)
|
||||||
|
self.assertEqual(out["sheets_a"], 25)
|
||||||
|
|
||||||
|
def test_execute_oo_start_uses_refreshed_sheets(self):
|
||||||
|
quote = MagicMock(
|
||||||
|
side_effect=lambda _ex, inst_id: {
|
||||||
|
"ok": True,
|
||||||
|
"ask": 10,
|
||||||
|
"ask_sz": 50,
|
||||||
|
"ct_mult": 0.01,
|
||||||
|
"tick_sz": "0.1",
|
||||||
|
"meta": {"optType": "C" if inst_id == "A" else "P"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cfg = {
|
||||||
|
"exchange_options": object(),
|
||||||
|
"quote_option_contract": quote,
|
||||||
|
"place_option_limit_order": MagicMock(),
|
||||||
|
"td_mode_for_option_buy": lambda x: "isolated",
|
||||||
|
"trade_budget_usdc": 4,
|
||||||
|
"budget_buffer": 0.95,
|
||||||
|
}
|
||||||
|
body = {
|
||||||
|
"oo_sheets_mode": "same_sheets",
|
||||||
|
"leg_a": {"inst_id": "A", "sheets": 1, "opt_type": "C"},
|
||||||
|
"leg_b": {"inst_id": "B", "sheets": 1, "opt_type": "P"},
|
||||||
|
}
|
||||||
|
# cost 0.1+0.1=0.2; budget 4 → 20 sheets each
|
||||||
|
with mock.patch(
|
||||||
|
"lib.exchange.okx_options_lib.fetch_options_trading_usdc",
|
||||||
|
return_value=100.0,
|
||||||
|
):
|
||||||
|
out = execute_options_options_start(cfg, body, dry_run=True)
|
||||||
|
self.assertTrue(out["ok"], out)
|
||||||
|
self.assertEqual(body["leg_a"]["sheets"], 20)
|
||||||
|
self.assertEqual(out["results"][0]["sheets"], 20)
|
||||||
|
self.assertIn("refresh", out)
|
||||||
|
self.assertTrue(out["refresh"]["ok"])
|
||||||
|
|
||||||
|
def test_refresh_po_keeps_sheets(self):
|
||||||
|
quote = MagicMock(
|
||||||
|
return_value={
|
||||||
|
"ok": True,
|
||||||
|
"ask": 12.5,
|
||||||
|
"ask_sz": 8,
|
||||||
|
"ct_mult": 0.01,
|
||||||
|
"meta": {"optType": "P"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
cfg = {"exchange_options": object(), "quote_option_contract": quote}
|
||||||
|
body = {"opt_inst_id": "X", "sheets": 3}
|
||||||
|
out = refresh_po_option_quote_before_start(cfg, body)
|
||||||
|
self.assertTrue(out["ok"])
|
||||||
|
self.assertEqual(body["ask"], 12.5)
|
||||||
|
self.assertEqual(body["sheets"], 3)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user