Auto-fill 期期 sheets from trading balance with same-sheets default.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-19 00:56:35 +08:00
parent f21e4d1166
commit b3548240cc
7 changed files with 374 additions and 5 deletions
+5 -1
View File
@@ -93,7 +93,11 @@
- **T 型报价链**(复用期权页 T 型样式/数据结构).
- 用户选 **腿 A + 腿 B**(通常 Call + Put,或主方向 + 尾部).
- 预算`OKX_OPTIONS_TRADE_BUDGET_USDC` 等既有约束;可拆预算到两腿.
- 预算:`B = min(交易户 USDC × OKX_OPTIONS_BUDGET_BUFFER, OKX_OPTIONS_TRADE_BUDGET_USDC)`(默认 buffer=0.95).
- 自动张数(选齐两腿后写入,可手改):
- **同张数**(默认):最大 `n` 使 `n×(cost_A+cost_B) ≤ B`,两腿均填 `n`
- **均分预算**:各用 `B/2` 反推张数(两腿可不同)
- 另受各自卖一深度上限约束
### 4.2 目标价
+150 -1
View File
@@ -28,6 +28,10 @@
legA: null,
legB: null,
market: null,
ooSheetsMode: "same_sheets",
tradingUsdc: null,
tradeBudgetUsdc: null,
budgetBuffer: 0.95,
};
function $(id) {
@@ -176,6 +180,16 @@
const label = (chain && chain.account_label) || acct.label || "期权账户";
const tag = $("hp-opt-acct-tag");
if (tag) tag.textContent = label;
if (acct.trading_usdc != null && acct.trading_usdc !== "") {
state.tradingUsdc = Number(acct.trading_usdc);
}
if (chain && chain.trade_budget_usdc != null && chain.trade_budget_usdc !== "") {
state.tradeBudgetUsdc = Number(chain.trade_budget_usdc);
}
if (chain && chain.budget_buffer != null && chain.budget_buffer !== "") {
const buf = Number(chain.budget_buffer);
if (!Number.isNaN(buf) && buf > 0) state.budgetBuffer = buf;
}
const line =
label +
" · 交易 USDC " +
@@ -186,6 +200,132 @@
if (el) el.textContent = line;
const oo = $("hp-oo-bal-line");
if (oo) oo.textContent = line;
autoFillOoSheets();
}
function resolveOoBudget() {
const buf = state.budgetBuffer > 0 ? state.budgetBuffer : 0.95;
const trading = state.tradingUsdc;
const cap = state.tradeBudgetUsdc;
let tradingCap = null;
let tradeCap = null;
if (trading != null && !Number.isNaN(Number(trading))) {
tradingCap = Math.max(0, Number(trading) * buf);
}
if (cap != null && !Number.isNaN(Number(cap))) {
tradeCap = Math.max(0, Number(cap));
}
if (tradingCap == null && tradeCap == null) {
return { ok: false, budget: 0, tradingCap: null, tradeCap: null, buf: buf, msg: "缺少交易户余额与单笔预算" };
}
let budget = 0;
if (tradingCap == null) budget = tradeCap;
else if (tradeCap == null) budget = tradingCap;
else budget = Math.min(tradingCap, tradeCap);
budget = Math.floor(budget * 1e6 + 1e-12) / 1e6;
return {
ok: budget > 0,
budget: budget,
tradingCap: tradingCap,
tradeCap: tradeCap,
buf: buf,
msg: budget > 0 ? "" : "可用预算为 0",
};
}
function unitCost(c) {
if (!c) return 0;
const ask = Number(c.ask || 0);
if (!(ask > 0)) return 0;
return ask * Number(c.ct_mult || 0.01);
}
function capByDepth(n, askSz) {
let out = Math.max(0, Math.floor(Number(n) || 0));
if (askSz == null || askSz === "") return out;
const d = Number(askSz);
if (Number.isNaN(d)) return out;
if (d <= 0) return 0;
return Math.min(out, Math.floor(d + 1e-12));
}
function suggestOoSheetsLocal(budget, mode) {
const costA = unitCost(state.legA);
const costB = unitCost(state.legB);
if (!(budget > 0)) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "可用预算为 0" };
}
if (!(costA > 0) || !(costB > 0)) {
return { sheetsA: 0, sheetsB: 0, premium: 0, ok: false, msg: "缺少有效卖一价,无法建议张数" };
}
let nA = 0;
let nB = 0;
if (mode === "split_budget") {
const half = budget / 2;
nA = Math.floor(half / costA + 1e-12);
nB = Math.floor(half / costB + 1e-12);
} 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;
}
const premium = costA * nA + costB * nB;
const ok = nA >= 1 && nB >= 1;
return {
sheetsA: nA,
sheetsB: nB,
premium: premium,
ok: ok,
msg: ok ? "" : "预算不够开 1+1(或卖一深度不足)",
};
}
function syncOoSizeModeUI() {
document.querySelectorAll(".hp-oo-size-mode").forEach(function (b) {
const on = b.getAttribute("data-oo-size") === state.ooSheetsMode;
b.classList.toggle("active", on);
});
}
function updateOoBudgetLine(extra) {
const line = $("hp-oo-budget-line");
if (!line) return;
const b = resolveOoBudget();
const modeLabel = state.ooSheetsMode === "split_budget" ? "均分预算" : "同张数";
const parts = [
"预算 min(交易×" + fmt(b.buf, 2) + ", 单笔) ≈ " + fmt(b.budget, 2) + " USDC",
"模式:" + modeLabel,
];
if (b.tradingCap != null) parts.push("交易×缓冲 " + fmt(b.tradingCap, 2));
if (b.tradeCap != null) parts.push("单笔上限 " + fmt(b.tradeCap, 2));
if (extra && extra.msg) parts.push(extra.msg);
else if (b.msg) parts.push(b.msg);
line.textContent = parts.join(" · ");
line.classList.toggle("hp-oo-budget-warn", !!(extra && extra.msg) || !b.ok);
}
function autoFillOoSheets() {
syncOoSizeModeUI();
if (!state.legA || !state.legB) {
updateOoBudgetLine(null);
return;
}
const b = resolveOoBudget();
const sug = suggestOoSheetsLocal(b.budget, state.ooSheetsMode);
const a = $("hp-oo-sheets-a");
const bb = $("hp-oo-sheets-b");
if (a && !a.disabled) a.value = String(sug.sheetsA);
if (bb && !bb.disabled) bb.value = String(sug.sheetsB);
updateOoBudgetLine(sug.ok ? null : sug);
updateOoPremiumLine();
}
async function loadGates() {
@@ -562,7 +702,7 @@
}
fill("腿A", state.legA, "hp-oo-leg-a-info", "hp-oo-sheets-a");
fill("腿B", state.legB, "hp-oo-leg-b-info", "hp-oo-sheets-b");
updateOoPremiumLine();
autoFillOoSheets();
}
function ooSheets(id) {
@@ -806,6 +946,13 @@
const el = $(id);
if (el) el.addEventListener("input", updateOoPremiumLine);
});
document.querySelectorAll(".hp-oo-size-mode").forEach(function (b) {
b.addEventListener("click", function () {
state.ooSheetsMode = b.getAttribute("data-oo-size") || "same_sheets";
syncOoSizeModeUI();
autoFillOoSheets();
});
});
if ($("hp-preview-btn"))
$("hp-preview-btn").addEventListener("click", function () {
state.mode = "perp_options";
@@ -1262,6 +1409,8 @@
syncTabUI();
syncUnderlyingUI();
syncMoneyUI();
syncOoSizeModeUI();
updateOoBudgetLine(null);
bind();
void refreshAll();
})();
+15
View File
@@ -3258,6 +3258,21 @@ html[data-theme="light"] .opt-be-dist-down {
flex-direction: column;
gap: 8px;
}
.hedge-plan-page-wrap .hp-oo-size-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 6px 0 2px;
}
.hedge-plan-page-wrap .hp-oo-size-mode.active {
border-color: var(--accent, #00d4ff);
color: var(--text, #fff);
background: rgba(0, 212, 255, 0.12);
}
.hedge-plan-page-wrap #hp-oo-budget-line.hp-oo-budget-warn {
color: #ff8a8a;
}
.hedge-plan-page-wrap .hp-oo-leg-row {
display: flex;
flex-wrap: wrap;
+140
View File
@@ -86,6 +86,146 @@ def floor_contracts_to_precision(contracts: float, decimals: int) -> float:
return math.floor(raw * scale + 1e-12) / scale
def option_unit_cost_usdc(*, ask: float, ct_mult: float) -> float:
"""单张权利金(USDC) = 卖一价 × ct_mult."""
a = _f(ask)
if a is None or a <= 0:
return 0.0
return float(a) * float(ct_mult or 0.01)
def resolve_oo_budget_usdc(
*,
trading_usdc: Any,
trade_budget_usdc: Any,
buffer_ratio: Any = 0.95,
) -> dict[str, Any]:
"""期期可用预算 = min(交易户×buffer, 单笔预算)."""
import math
trading = _f(trading_usdc)
cap = _f(trade_budget_usdc)
buf = _f(buffer_ratio)
if buf is None or buf <= 0:
buf = 0.95
if buf > 1:
buf = 1.0
trading_cap = None if trading is None else max(0.0, float(trading) * float(buf))
trade_cap = None if cap is None else max(0.0, float(cap))
if trading_cap is None and trade_cap is None:
return {
"ok": False,
"budget_usdc": 0.0,
"trading_cap": None,
"trade_budget_cap": None,
"buffer_ratio": float(buf),
"msg": "缺少交易户余额与单笔预算",
}
if trading_cap is None:
budget = float(trade_cap or 0.0)
elif trade_cap is None:
budget = float(trading_cap)
else:
budget = min(float(trading_cap), float(trade_cap))
budget = float(math.floor(budget * 1e6 + 1e-12) / 1e6)
return {
"ok": budget > 0,
"budget_usdc": budget,
"trading_cap": None if trading_cap is None else round(float(trading_cap), 6),
"trade_budget_cap": None if trade_cap is None else round(float(trade_cap), 6),
"buffer_ratio": float(buf),
"msg": "" if budget > 0 else "可用预算为 0",
}
def _cap_sheets_by_ask_depth(sheets: int, ask_sz: Any) -> int:
import math
n = max(0, int(sheets))
depth = _f(ask_sz)
if depth is None:
return n
if depth <= 0:
return 0
return min(n, int(math.floor(float(depth) + 1e-12)))
def suggest_oo_sheets(
*,
mode: str,
budget_usdc: float,
ask_a: float,
ct_mult_a: float = 0.01,
ask_sz_a: Any = None,
ask_b: float,
ct_mult_b: float = 0.01,
ask_sz_b: Any = None,
) -> dict[str, Any]:
"""期期建议张数:same_sheets(同张数,默认) / 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"
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)
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",
}
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":
half = budget / 2.0
n_a = int(math.floor(half / cost_a + 1e-12))
n_b = int(math.floor(half / cost_b + 1e-12))
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
prem = cost_a * n_a + cost_b * n_b
ok = n_a >= 1 and n_b >= 1
msg = ""
if not ok:
msg = "预算不够开 1+1(或卖一深度不足)"
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(prem, 6),
"ok": ok,
"msg": msg,
}
def build_perp_options_preview(
*,
direction: str,
+4
View File
@@ -97,6 +97,8 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
"chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
"perp_account_label": (os.getenv("OKX_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"),
"budget_buffer": float(os.getenv("OKX_OPTIONS_BUDGET_BUFFER") or "0.95"),
"live_trading": _env_bool("LIVE_TRADING_ENABLED", False),
"send_wechat": getattr(app_module, "send_wechat_msg", None),
}
@@ -405,6 +407,8 @@ def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
"account_label": cfg.get("options_account_label") or "期权账户",
"account_note": "期权腿使用期权账户(交易 USDC)",
"options_account": opt_acct,
"trade_budget_usdc": cfg.get("trade_budget_usdc"),
"budget_buffer": cfg.get("budget_buffer"),
}
)
@@ -138,17 +138,23 @@
<div id="hp-oo-index" class="muted hp-quote-line"></div>
<div id="hp-oo-bal-line" class="muted hp-quote-line"></div>
<p class="muted hp-unit-hint">震荡突破:设上下两个目标价(USD);触达任一侧重平盈利腿。张数=<strong>期权张</strong> · 权利金=USDC</p>
<div class="form-row hp-oo-size-row">
<span class="muted">自动张数</span>
<button type="button" class="btn-secondary hp-oo-size-mode active" data-oo-size="same_sheets">同张数</button>
<button type="button" class="btn-secondary hp-oo-size-mode" data-oo-size="split_budget">均分预算</button>
</div>
<p class="muted" id="hp-oo-budget-line"></p>
<div id="hp-oo-legs" class="hp-oo-legs">
<div class="hp-oo-leg-row" data-leg="a">
<div class="muted" id="hp-oo-leg-a-info">腿A: 尚未选用</div>
<label>张数 <span class="hp-unit">期权张</span>
<input type="number" step="1" min="1" id="hp-oo-sheets-a" value="1" disabled />
<input type="number" step="1" min="0" id="hp-oo-sheets-a" value="1" disabled />
</label>
</div>
<div class="hp-oo-leg-row" data-leg="b">
<div class="muted" id="hp-oo-leg-b-info">腿B: 尚未选用</div>
<label>张数 <span class="hp-unit">期权张</span>
<input type="number" step="1" min="1" id="hp-oo-sheets-b" value="1" disabled />
<input type="number" step="1" min="0" id="hp-oo-sheets-b" value="1" disabled />
</label>
</div>
<p class="muted" id="hp-oo-prem-line"></p>
@@ -264,4 +270,4 @@
</div>
</div>
</div>
<script src="/static/hedge_plan.js?v=14"></script>
<script src="/static/hedge_plan.js?v=15"></script>
+51
View File
@@ -9,6 +9,8 @@ from lib.hedge_plan.hedge_plan_calc_lib import (
option_expiry_pnl,
option_premium_total,
perp_pnl,
resolve_oo_budget_usdc,
suggest_oo_sheets,
)
@@ -129,6 +131,55 @@ class TestHedgePlanCalc(unittest.TestCase):
self.assertEqual(floor_contracts_to_precision(4.569713, 0), 4.0)
self.assertEqual(floor_contracts_to_precision(0, 4), 0.0)
def test_oo_budget_min_trading_and_cap(self):
b = resolve_oo_budget_usdc(trading_usdc=11.07, trade_budget_usdc=10, buffer_ratio=0.95)
self.assertTrue(b["ok"])
self.assertAlmostEqual(b["trading_cap"], 11.07 * 0.95, places=4)
self.assertEqual(b["budget_usdc"], 10.0)
def test_suggest_oo_same_sheets_default(self):
# cost_a=1, cost_b=1 → pair=2; budget=10 → n=5
s = suggest_oo_sheets(
mode="same_sheets",
budget_usdc=10,
ask_a=100,
ct_mult_a=0.01,
ask_b=100,
ct_mult_b=0.01,
)
self.assertEqual(s["mode"], "same_sheets")
self.assertEqual(s["sheets_a"], 5)
self.assertEqual(s["sheets_b"], 5)
self.assertTrue(s["ok"])
def test_suggest_oo_split_budget(self):
# cost_a=1, cost_b=2; half=5 → nA=5, nB=2
s = suggest_oo_sheets(
mode="split_budget",
budget_usdc=10,
ask_a=100,
ct_mult_a=0.01,
ask_b=200,
ct_mult_b=0.01,
)
self.assertEqual(s["mode"], "split_budget")
self.assertEqual(s["sheets_a"], 5)
self.assertEqual(s["sheets_b"], 2)
def test_suggest_oo_depth_cap(self):
s = suggest_oo_sheets(
mode="same_sheets",
budget_usdc=100,
ask_a=100,
ct_mult_a=0.01,
ask_sz_a=2,
ask_b=100,
ct_mult_b=0.01,
ask_sz_b=50,
)
self.assertEqual(s["sheets_a"], 2)
self.assertEqual(s["sheets_b"], 2)
if __name__ == "__main__":
unittest.main()