Auto-fill 期期 sheets from trading balance with same-sheets default.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-1
@@ -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 目标价
|
||||
|
||||
|
||||
@@ -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();
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user