Add OKX hedge-plan P0: env group, preview page, and PnL scenario math.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* OKX 对冲计划 P0:行情 + 永期列表 / 期期 T + 情景测算 + 门禁.
|
||||
*/
|
||||
(function () {
|
||||
const root = document.getElementById("hedge-plan-root");
|
||||
if (!root) return;
|
||||
|
||||
const state = {
|
||||
mode: "perp_options",
|
||||
underlying: root.getAttribute("data-default-underly") || "ETH",
|
||||
chain: null,
|
||||
selected: null,
|
||||
legA: null,
|
||||
legB: null,
|
||||
market: null,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
async function apiJson(url, opts) {
|
||||
const res = await fetch(url, Object.assign({ credentials: "same-origin" }, opts || {}));
|
||||
const data = await res.json().catch(function () {
|
||||
return {};
|
||||
});
|
||||
if (!res.ok) throw new Error(data.msg || res.statusText || "请求失败");
|
||||
return data;
|
||||
}
|
||||
|
||||
function fmt(v, d) {
|
||||
if (v === null || v === undefined || Number.isNaN(Number(v))) return "—";
|
||||
return Number(v).toFixed(d == null ? 2 : d);
|
||||
}
|
||||
|
||||
function optTypeForDirection(dir) {
|
||||
return dir === "short" ? "C" : "P";
|
||||
}
|
||||
|
||||
function syncModeUI() {
|
||||
document.querySelectorAll(".hp-mode-btn").forEach(function (b) {
|
||||
b.classList.toggle("active", b.getAttribute("data-mode") === state.mode);
|
||||
});
|
||||
const po = $("hp-po-layout");
|
||||
const oo = $("hp-oo-layout");
|
||||
if (po) po.classList.toggle("hidden", state.mode !== "perp_options");
|
||||
if (oo) oo.classList.toggle("hidden", state.mode !== "options_options");
|
||||
}
|
||||
|
||||
function setGateLine(gates) {
|
||||
const el = $("hp-gate-line");
|
||||
if (!el) return;
|
||||
if (!gates) {
|
||||
el.textContent = "";
|
||||
return;
|
||||
}
|
||||
const parts = [
|
||||
"计仓:" + (gates.is_full_margin ? "全仓" : "非全仓"),
|
||||
"测算:" + (gates.can_preview ? "可" : "否"),
|
||||
"开仓:" + (gates.can_start ? "可" : "否"),
|
||||
];
|
||||
if (gates.reasons && gates.reasons.length) parts.push(gates.reasons.join("; "));
|
||||
el.textContent = parts.join(" · ");
|
||||
const start = $("hp-start-btn");
|
||||
if (start) start.disabled = !gates.can_start;
|
||||
}
|
||||
|
||||
async function loadGates() {
|
||||
try {
|
||||
const d = await apiJson("/api/hedge-plan/gates?plan_type=" + encodeURIComponent(state.mode));
|
||||
setGateLine(d);
|
||||
} catch (e) {
|
||||
setGateLine({ can_preview: false, can_start: false, reasons: [e.message], is_full_margin: false });
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMarket() {
|
||||
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
|
||||
const d = await apiJson(
|
||||
"/api/hedge-plan/market?base=" +
|
||||
encodeURIComponent(state.underlying) +
|
||||
"&direction=" +
|
||||
encodeURIComponent(dir)
|
||||
);
|
||||
state.market = d;
|
||||
setGateLine(d.gates);
|
||||
const q = $("hp-perp-quote");
|
||||
if (q) {
|
||||
q.innerHTML =
|
||||
"标记 <strong>" +
|
||||
fmt(d.mark, 2) +
|
||||
"</strong> · 最新 " +
|
||||
fmt(d.last, 2) +
|
||||
" · 卖一 " +
|
||||
fmt(d.ask, 2) +
|
||||
" · 买一 " +
|
||||
fmt(d.bid, 2) +
|
||||
"<br/>面值 " +
|
||||
fmt(d.contract_size, 4) +
|
||||
" · 可用 " +
|
||||
fmt(d.available_usdt, 2) +
|
||||
" U";
|
||||
}
|
||||
const sz = $("hp-sizing-line");
|
||||
if (sz) {
|
||||
if (d.full_margin_sizing) {
|
||||
const s = d.full_margin_sizing;
|
||||
sz.textContent =
|
||||
"全仓建议:保证金 " +
|
||||
fmt(s.margin_capital, 2) +
|
||||
"U × " +
|
||||
s.leverage +
|
||||
"x → 名义 " +
|
||||
fmt(s.notional_value, 2) +
|
||||
"U · 建议约 " +
|
||||
fmt(d.suggest_contracts, 4) +
|
||||
" 张";
|
||||
} else {
|
||||
sz.textContent = "非全仓或不具备保证金数据时仅手动填张数;永期开仓需全仓.";
|
||||
}
|
||||
}
|
||||
const entry = $("hp-entry");
|
||||
if (entry && d.entry_ref && !entry.value) entry.value = d.entry_ref;
|
||||
const contracts = $("hp-contracts");
|
||||
if (contracts && d.suggest_contracts != null && !contracts.value) {
|
||||
contracts.value = d.suggest_contracts;
|
||||
}
|
||||
const label = $("hp-opt-type-label");
|
||||
if (label) label.textContent = d.suggested_opt_type === "C" ? "Call" : "Put";
|
||||
}
|
||||
|
||||
function fillExpSelect(sel, chain) {
|
||||
if (!sel) return;
|
||||
const prev = sel.value;
|
||||
sel.innerHTML = '<option value="">选择到期日</option>';
|
||||
(chain.expiries || []).forEach(function (e) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(e.exp_time);
|
||||
const dt = new Date(Number(e.exp_time));
|
||||
opt.textContent = dt.toLocaleString();
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
if (prev) sel.value = prev;
|
||||
if (!sel.value && chain.expiries && chain.expiries[0]) {
|
||||
sel.value = String(chain.expiries[0].exp_time);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChain() {
|
||||
const d = await apiJson(
|
||||
"/api/hedge-plan/options-chain?underlying=" + encodeURIComponent(state.underlying)
|
||||
);
|
||||
state.chain = d;
|
||||
const idx = $("hp-index-line");
|
||||
if (idx) idx.textContent = "指数 " + fmt(d.index_px, 2) + " · " + (d.inst_family || "");
|
||||
const ooIdx = $("hp-oo-index");
|
||||
if (ooIdx) ooIdx.textContent = "指数 " + fmt(d.index_px, 2);
|
||||
fillExpSelect($("hp-exp-select"), d);
|
||||
fillExpSelect($("hp-oo-exp-select"), d);
|
||||
renderListStrikes();
|
||||
renderTStrikes();
|
||||
if (d.index_px && $("hp-target") && !$("hp-target").value) {
|
||||
$("hp-target").value = d.index_px;
|
||||
}
|
||||
}
|
||||
|
||||
function currentExp(selectId) {
|
||||
const sel = $(selectId);
|
||||
const expMs = sel && sel.value;
|
||||
if (!expMs || !state.chain) return null;
|
||||
return (state.chain.expiries || []).find(function (e) {
|
||||
return String(e.exp_time) === String(expMs);
|
||||
});
|
||||
}
|
||||
|
||||
function renderListStrikes() {
|
||||
const tbody = $("hp-strike-tbody");
|
||||
if (!tbody) return;
|
||||
const dir = ($("hp-direction") && $("hp-direction").value) || "long";
|
||||
const want = optTypeForDirection(dir);
|
||||
const exp = currentExp("hp-exp-select");
|
||||
tbody.innerHTML = "";
|
||||
if (!exp) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
|
||||
return;
|
||||
}
|
||||
const list = (exp.contracts || []).filter(function (c) {
|
||||
return String(c.opt_type || "").toUpperCase() === want;
|
||||
});
|
||||
if (!list.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="muted">无匹配合约</td></tr>';
|
||||
return;
|
||||
}
|
||||
list.forEach(function (c) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.innerHTML =
|
||||
"<td>" +
|
||||
c.strike +
|
||||
"</td><td>" +
|
||||
c.opt_type +
|
||||
"</td><td>" +
|
||||
fmt(c.ask, 4) +
|
||||
"</td><td>" +
|
||||
fmt(c.bid, 4) +
|
||||
'</td><td><button type="button" class="btn-secondary hp-pick" data-inst="' +
|
||||
c.inst_id +
|
||||
'">选用</button></td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tbody.querySelectorAll(".hp-pick").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const inst = btn.getAttribute("data-inst");
|
||||
const c = list.find(function (x) {
|
||||
return x.inst_id === inst;
|
||||
});
|
||||
if (!c) return;
|
||||
state.selected = c;
|
||||
const el = $("hp-sel-inst");
|
||||
if (el) el.textContent = c.inst_id;
|
||||
updatePremiumLine();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updatePremiumLine() {
|
||||
const line = $("hp-premium-line");
|
||||
if (!line || !state.selected) {
|
||||
if (line) line.textContent = "";
|
||||
return;
|
||||
}
|
||||
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
|
||||
const ct = Number(state.selected.ct_mult || 0.01);
|
||||
const ask = Number(state.selected.ask || 0);
|
||||
const prem = ask * sheets * ct;
|
||||
line.textContent = "预估权利金 ≈ " + fmt(prem, 4) + " USDC";
|
||||
}
|
||||
|
||||
function buildStraddleRows(contracts) {
|
||||
const map = {};
|
||||
(contracts || []).forEach(function (c) {
|
||||
const key = String(c.strike);
|
||||
if (!map[key]) map[key] = { strike: c.strike, call: null, put: null };
|
||||
const o = (c.opt_type || "").toUpperCase();
|
||||
if (o === "C") map[key].call = c;
|
||||
else if (o === "P") map[key].put = c;
|
||||
});
|
||||
return Object.keys(map)
|
||||
.map(function (k) {
|
||||
return map[k];
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return Number(a.strike) - Number(b.strike);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTStrikes() {
|
||||
const tbody = $("hp-oo-tbody");
|
||||
if (!tbody) return;
|
||||
const exp = currentExp("hp-oo-exp-select");
|
||||
tbody.innerHTML = "";
|
||||
if (!exp) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="muted">请选择到期日</td></tr>';
|
||||
return;
|
||||
}
|
||||
const rows = buildStraddleRows(exp.contracts);
|
||||
rows.forEach(function (row) {
|
||||
const tr = document.createElement("tr");
|
||||
const callAsk = row.call ? fmt(row.call.ask, 4) : "—";
|
||||
const putAsk = row.put ? fmt(row.put.ask, 4) : "—";
|
||||
tr.innerHTML =
|
||||
"<td>" +
|
||||
callAsk +
|
||||
'</td><td>' +
|
||||
(row.call
|
||||
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="C" data-inst="' +
|
||||
row.call.inst_id +
|
||||
'">Call</button>'
|
||||
: "—") +
|
||||
'</td><td class="opt-t-strike"><strong>' +
|
||||
row.strike +
|
||||
"</strong></td><td>" +
|
||||
putAsk +
|
||||
'</td><td>' +
|
||||
(row.put
|
||||
? '<button type="button" class="btn-secondary hp-oo-pick" data-side="P" data-inst="' +
|
||||
row.put.inst_id +
|
||||
'">Put</button>'
|
||||
: "—") +
|
||||
"</td>";
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
tbody.querySelectorAll(".hp-oo-pick").forEach(function (btn) {
|
||||
btn.addEventListener("click", function () {
|
||||
const inst = btn.getAttribute("data-inst");
|
||||
const exp2 = currentExp("hp-oo-exp-select");
|
||||
const c = (exp2.contracts || []).find(function (x) {
|
||||
return x.inst_id === inst;
|
||||
});
|
||||
if (!c) return;
|
||||
if (!state.legA) state.legA = c;
|
||||
else if (!state.legB || state.legB.inst_id === state.legA.inst_id) state.legB = c;
|
||||
else {
|
||||
state.legA = c;
|
||||
state.legB = null;
|
||||
}
|
||||
renderOoLegs();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderOoLegs() {
|
||||
const el = $("hp-oo-legs");
|
||||
if (!el) return;
|
||||
function one(tag, c) {
|
||||
if (!c) return tag + ": —";
|
||||
return (
|
||||
tag +
|
||||
": " +
|
||||
c.opt_type +
|
||||
" K" +
|
||||
c.strike +
|
||||
" ask=" +
|
||||
fmt(c.ask, 4) +
|
||||
" (" +
|
||||
c.inst_id +
|
||||
")"
|
||||
);
|
||||
}
|
||||
el.innerHTML = one("腿A", state.legA) + "<br/>" + one("腿B", state.legB);
|
||||
}
|
||||
|
||||
function legPayload(c, sheets) {
|
||||
return {
|
||||
opt_type: c.opt_type,
|
||||
strike: c.strike,
|
||||
sheets: sheets,
|
||||
ct_mult: c.ct_mult || 0.01,
|
||||
ask: c.ask,
|
||||
inst_id: c.inst_id,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPreview() {
|
||||
const tbody = $("hp-result-tbody");
|
||||
const summary = $("hp-summary");
|
||||
try {
|
||||
let body;
|
||||
if (state.mode === "options_options") {
|
||||
if (!state.legA || !state.legB) throw new Error("请选用两条期权腿");
|
||||
const target = Number(($("hp-target") && $("hp-target").value) || 0);
|
||||
if (!target) throw new Error("请填写目标价");
|
||||
body = {
|
||||
plan_type: "options_options",
|
||||
target_price: target,
|
||||
index_px: (state.chain && state.chain.index_px) || target,
|
||||
leg_a: legPayload(state.legA, 1),
|
||||
leg_b: legPayload(state.legB, 1),
|
||||
};
|
||||
} else {
|
||||
if (!state.selected) throw new Error("请选用期权腿");
|
||||
const entry = Number(($("hp-entry") && $("hp-entry").value) || 0);
|
||||
const tp = Number(($("hp-tp") && $("hp-tp").value) || 0);
|
||||
const sl = Number(($("hp-sl") && $("hp-sl").value) || 0);
|
||||
const contracts = Number(($("hp-contracts") && $("hp-contracts").value) || 0);
|
||||
const sheets = Number(($("hp-sheets") && $("hp-sheets").value) || 1);
|
||||
if (!entry || !tp || !sl || !contracts) throw new Error("请完整填写开仓/止盈/止损/张数");
|
||||
body = {
|
||||
plan_type: "perp_options",
|
||||
direction: ($("hp-direction") && $("hp-direction").value) || "long",
|
||||
entry: entry,
|
||||
tp: tp,
|
||||
sl: sl,
|
||||
contracts: contracts,
|
||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||
opt_type: state.selected.opt_type,
|
||||
strike: state.selected.strike,
|
||||
sheets: sheets,
|
||||
ct_mult: state.selected.ct_mult || 0.01,
|
||||
ask: state.selected.ask,
|
||||
index_px: state.chain && state.chain.index_px,
|
||||
};
|
||||
}
|
||||
const d = await apiJson("/api/hedge-plan/preview", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
setGateLine(d.gates);
|
||||
const s = d.summary || {};
|
||||
if (summary) {
|
||||
if (d.plan_type === "perp_options") {
|
||||
summary.innerHTML =
|
||||
"止盈合计 <strong>" +
|
||||
fmt(s.tp_total) +
|
||||
"</strong> · 止损合计 <strong>" +
|
||||
fmt(s.sl_total) +
|
||||
"</strong> · 保费 " +
|
||||
fmt(s.premium_paid) +
|
||||
(s.hedge_ratio_at_sl != null ? " · 止损对冲率 " + fmt(s.hedge_ratio_at_sl) + "%" : "");
|
||||
} else {
|
||||
summary.innerHTML =
|
||||
"目标价合计 <strong>" +
|
||||
fmt(s.at_target_total) +
|
||||
"</strong> · 到期现价 <strong>" +
|
||||
fmt(s.expiry_flat_total) +
|
||||
"</strong> · 保费 " +
|
||||
fmt(s.premium_paid) +
|
||||
(s.expiry_is_loss ? " · 到期无盈利(记总亏损)" : "");
|
||||
}
|
||||
}
|
||||
tbody.innerHTML = "";
|
||||
(d.scenarios || []).forEach(function (sc) {
|
||||
const tr = document.createElement("tr");
|
||||
let mid;
|
||||
if (sc.perp_pnl != null) {
|
||||
mid = "永续 " + fmt(sc.perp_pnl);
|
||||
} else {
|
||||
mid = "A " + fmt(sc.leg_a_pnl) + " / B " + fmt(sc.leg_b_pnl);
|
||||
}
|
||||
const optCol = sc.options_pnl != null ? fmt(sc.options_pnl) : "—";
|
||||
tr.innerHTML =
|
||||
"<td>" +
|
||||
(sc.label || sc.id) +
|
||||
"</td><td>" +
|
||||
fmt(sc.spot) +
|
||||
"</td><td>" +
|
||||
mid +
|
||||
"</td><td>" +
|
||||
optCol +
|
||||
"</td><td><strong>" +
|
||||
fmt(sc.total) +
|
||||
"</strong></td><td class=\"muted\">" +
|
||||
(sc.note || "") +
|
||||
"</td>";
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (e) {
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="err">' + (e.message || e) + "</td></tr>";
|
||||
if (summary) summary.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
function bind() {
|
||||
document.querySelectorAll(".hp-mode-btn").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
state.mode = b.getAttribute("data-mode") || "perp_options";
|
||||
syncModeUI();
|
||||
loadGates();
|
||||
});
|
||||
});
|
||||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (b) {
|
||||
b.addEventListener("click", function () {
|
||||
state.underlying = b.getAttribute("data-uly") || "ETH";
|
||||
document.querySelectorAll(".hp-uly-btn, .hp-uly-btn-oo").forEach(function (x) {
|
||||
x.classList.toggle("active", x.getAttribute("data-uly") === state.underlying);
|
||||
});
|
||||
state.selected = null;
|
||||
state.legA = null;
|
||||
state.legB = null;
|
||||
void refreshAll();
|
||||
});
|
||||
});
|
||||
const dir = $("hp-direction");
|
||||
if (dir) {
|
||||
dir.addEventListener("change", function () {
|
||||
void loadMarket().then(function () {
|
||||
renderListStrikes();
|
||||
});
|
||||
});
|
||||
}
|
||||
if ($("hp-refresh")) $("hp-refresh").addEventListener("click", function () {
|
||||
void refreshAll();
|
||||
});
|
||||
if ($("hp-load-chain")) $("hp-load-chain").addEventListener("click", function () {
|
||||
void loadChain();
|
||||
});
|
||||
if ($("hp-oo-load-chain")) $("hp-oo-load-chain").addEventListener("click", function () {
|
||||
void loadChain();
|
||||
});
|
||||
if ($("hp-exp-select"))
|
||||
$("hp-exp-select").addEventListener("change", renderListStrikes);
|
||||
if ($("hp-oo-exp-select"))
|
||||
$("hp-oo-exp-select").addEventListener("change", renderTStrikes);
|
||||
if ($("hp-sheets")) $("hp-sheets").addEventListener("input", updatePremiumLine);
|
||||
if ($("hp-preview-btn")) $("hp-preview-btn").addEventListener("click", function () {
|
||||
void runPreview();
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await loadGates();
|
||||
try {
|
||||
await loadMarket();
|
||||
} catch (e) {
|
||||
const q = $("hp-perp-quote");
|
||||
if (q) q.textContent = e.message || String(e);
|
||||
}
|
||||
try {
|
||||
await loadChain();
|
||||
} catch (e) {
|
||||
const tbody = $("hp-strike-tbody");
|
||||
if (tbody) tbody.innerHTML = '<tr><td colspan="5" class="err">' + (e.message || e) + "</td></tr>";
|
||||
}
|
||||
}
|
||||
|
||||
syncModeUI();
|
||||
bind();
|
||||
void refreshAll();
|
||||
})();
|
||||
@@ -26,6 +26,8 @@
|
||||
records: "show_nav_records",
|
||||
stats: "show_nav_stats",
|
||||
options: "show_nav_options",
|
||||
"hedge-plan": "show_nav_hedge_plan",
|
||||
hedge_plan: "show_nav_hedge_plan",
|
||||
risk_policy: "show_nav_risk_policy",
|
||||
env_config: "show_nav_env_config",
|
||||
};
|
||||
@@ -48,6 +50,8 @@
|
||||
records: "show_nav_records",
|
||||
stats: "show_nav_stats",
|
||||
options: "show_nav_options",
|
||||
"hedge-plan": "show_nav_hedge_plan",
|
||||
hedge_plan: "show_nav_hedge_plan",
|
||||
risk_policy: "show_nav_risk_policy",
|
||||
env_config: "show_nav_env_config",
|
||||
};
|
||||
|
||||
Vendored
+9
@@ -82,6 +82,15 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"APP_PASSWORD",
|
||||
"APP_AUTH_DISABLED",
|
||||
"WECHAT_WEBHOOK",
|
||||
"HEDGE_PLAN_ENABLED",
|
||||
"HEDGE_PLAN_LIVE_ORDER",
|
||||
"HEDGE_PLAN_OPEN_ORDER",
|
||||
"HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS",
|
||||
"HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS",
|
||||
"HEDGE_PLAN_OO_CLOSE_WINNER_ONLY",
|
||||
"MAX_ACTIVE_HEDGE_PLANS",
|
||||
"HEDGE_PLAN_MONITOR_POLL_SECONDS",
|
||||
"HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION",
|
||||
})
|
||||
|
||||
SENSITIVE_EXACT = frozenset({
|
||||
|
||||
Vendored
+18
@@ -125,6 +125,22 @@ _OPTIONS_SECTION: dict[str, Any] = {
|
||||
],
|
||||
}
|
||||
|
||||
_HEDGE_PLAN_SECTION: dict[str, Any] = {
|
||||
"title": "对冲计划",
|
||||
"exchanges": frozenset({"okx"}),
|
||||
"fields": [
|
||||
("HEDGE_PLAN_ENABLED", "启用对冲计划", "关闭则隐藏导航且不可开仓"),
|
||||
("HEDGE_PLAN_LIVE_ORDER", "允许对冲真实下单", "再与实盘开关与;P0 仅测算"),
|
||||
("HEDGE_PLAN_OPEN_ORDER", "永期开仓顺序", "options_first 或 perp_first"),
|
||||
("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", "永期止损后强制平期权", "保护机制,建议保持 true"),
|
||||
("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", "永期止盈后强制平期权", "默认 false,保险腿不平"),
|
||||
("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", "期期只平盈利腿", "达目标价只平盈利方"),
|
||||
("MAX_ACTIVE_HEDGE_PLANS", "最大同时活跃计划数", "建议 1"),
|
||||
("HEDGE_PLAN_MONITOR_POLL_SECONDS", "对冲监控轮询(秒)", "默认 15"),
|
||||
("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", "半腿失败时自动平期权", ""),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
# 与运行时 os.getenv 默认一致;.env 未写明时展示实际生效值(同风控说明页)
|
||||
_RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
@@ -191,6 +207,8 @@ def ui_sections_for_exchange(exchange_key: str) -> list[dict[str, Any]]:
|
||||
sections.extend(_SHARED_SECTIONS)
|
||||
if ex in _OPTIONS_SECTION.get("exchanges", frozenset()):
|
||||
sections.append(_OPTIONS_SECTION)
|
||||
if ex in _HEDGE_PLAN_SECTION.get("exchanges", frozenset()):
|
||||
sections.append(_HEDGE_PLAN_SECTION)
|
||||
return sections
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# hedge_plan package
|
||||
@@ -0,0 +1,320 @@
|
||||
"""对冲计划:情景测算与全仓建议仓(纯函数,无 IO)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
def _f(v: Any) -> Optional[float]:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def perp_coin_amount(*, contracts: float, contract_size: float) -> float:
|
||||
return float(contracts) * float(contract_size or 1.0)
|
||||
|
||||
|
||||
def perp_pnl(
|
||||
*,
|
||||
direction: str,
|
||||
entry: float,
|
||||
exit_px: float,
|
||||
contracts: float,
|
||||
contract_size: float,
|
||||
) -> float:
|
||||
coins = perp_coin_amount(contracts=contracts, contract_size=contract_size)
|
||||
d = (direction or "long").strip().lower()
|
||||
if d == "short":
|
||||
return (float(entry) - float(exit_px)) * coins
|
||||
return (float(exit_px) - float(entry)) * coins
|
||||
|
||||
|
||||
def option_premium_total(*, ask: float, sheets: float, ct_mult: float) -> float:
|
||||
"""卖一报价为每 1 币;权利金 = ask × 张数 × ct_mult."""
|
||||
return float(ask) * float(sheets) * float(ct_mult or 0.01)
|
||||
|
||||
|
||||
def option_expiry_pnl(
|
||||
*,
|
||||
opt_type: str,
|
||||
strike: float,
|
||||
spot: float,
|
||||
sheets: float,
|
||||
ct_mult: float,
|
||||
premium_paid: float,
|
||||
) -> float:
|
||||
o = (opt_type or "").strip().upper()
|
||||
intrinsic_per_coin = 0.0
|
||||
if o in ("C", "CALL"):
|
||||
intrinsic_per_coin = max(0.0, float(spot) - float(strike))
|
||||
elif o in ("P", "PUT"):
|
||||
intrinsic_per_coin = max(0.0, float(strike) - float(spot))
|
||||
else:
|
||||
return -float(premium_paid)
|
||||
value = intrinsic_per_coin * float(sheets) * float(ct_mult or 0.01)
|
||||
return value - float(premium_paid)
|
||||
|
||||
|
||||
def suggest_contracts_from_notional(
|
||||
*,
|
||||
notional: float,
|
||||
entry: float,
|
||||
contract_size: float,
|
||||
) -> float:
|
||||
if entry <= 0 or contract_size <= 0 or notional <= 0:
|
||||
return 0.0
|
||||
return float(notional) / (float(entry) * float(contract_size))
|
||||
|
||||
|
||||
def build_perp_options_preview(
|
||||
*,
|
||||
direction: str,
|
||||
entry: float,
|
||||
tp: float,
|
||||
sl: float,
|
||||
contracts: float,
|
||||
contract_size: float,
|
||||
opt_type: str,
|
||||
strike: float,
|
||||
sheets: float,
|
||||
ct_mult: float,
|
||||
premium_paid: float,
|
||||
index_px: Optional[float] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
永期情景.
|
||||
止盈账:永续止盈盈利 - 权利金.
|
||||
止损账:期权到期内在(按 SL 价) - 永续止损亏损额.
|
||||
"""
|
||||
d = (direction or "long").strip().lower()
|
||||
pnl_tp_perp = perp_pnl(
|
||||
direction=d, entry=entry, exit_px=tp, contracts=contracts, contract_size=contract_size
|
||||
)
|
||||
pnl_sl_perp = perp_pnl(
|
||||
direction=d, entry=entry, exit_px=sl, contracts=contracts, contract_size=contract_size
|
||||
)
|
||||
# 止盈统计口径
|
||||
tp_total = float(pnl_tp_perp) - float(premium_paid)
|
||||
# 止损:期权按 SL 价结算内在 - |永续亏损|
|
||||
opt_at_sl = option_expiry_pnl(
|
||||
opt_type=opt_type,
|
||||
strike=strike,
|
||||
spot=sl,
|
||||
sheets=sheets,
|
||||
ct_mult=ct_mult,
|
||||
premium_paid=premium_paid,
|
||||
)
|
||||
sl_total = float(opt_at_sl) - abs(float(pnl_sl_perp)) if pnl_sl_perp < 0 else float(opt_at_sl) + float(
|
||||
pnl_sl_perp
|
||||
)
|
||||
# 有符号相加更稳:期权盈亏 + 永续盈亏
|
||||
sl_total_signed = float(opt_at_sl) + float(pnl_sl_perp)
|
||||
|
||||
spot = float(index_px) if index_px is not None else float(entry)
|
||||
opt_flat = option_expiry_pnl(
|
||||
opt_type=opt_type,
|
||||
strike=strike,
|
||||
spot=spot,
|
||||
sheets=sheets,
|
||||
ct_mult=ct_mult,
|
||||
premium_paid=premium_paid,
|
||||
)
|
||||
flat_total = 0.0 + float(opt_flat)
|
||||
|
||||
opt_at_tp = option_expiry_pnl(
|
||||
opt_type=opt_type,
|
||||
strike=strike,
|
||||
spot=tp,
|
||||
sheets=sheets,
|
||||
ct_mult=ct_mult,
|
||||
premium_paid=premium_paid,
|
||||
)
|
||||
|
||||
return {
|
||||
"plan_type": "perp_options",
|
||||
"direction": d,
|
||||
"contracts": contracts,
|
||||
"coin_amount": perp_coin_amount(contracts=contracts, contract_size=contract_size),
|
||||
"premium_paid": round(float(premium_paid), 6),
|
||||
"scenarios": [
|
||||
{
|
||||
"id": "tp",
|
||||
"label": "止盈(计划结束口径)",
|
||||
"spot": tp,
|
||||
"perp_pnl": round(pnl_tp_perp, 4),
|
||||
"options_pnl": round(-float(premium_paid), 4),
|
||||
"total": round(tp_total, 4),
|
||||
"note": "止盈盈利 − 权利金;期权可不强平",
|
||||
},
|
||||
{
|
||||
"id": "sl",
|
||||
"label": "止损(计划结束口径)",
|
||||
"spot": sl,
|
||||
"perp_pnl": round(pnl_sl_perp, 4),
|
||||
"options_pnl": round(opt_at_sl, 4),
|
||||
"total": round(sl_total_signed, 4),
|
||||
"note": "期权盈利 − 永续亏损(有符号相加);期权须强平",
|
||||
},
|
||||
{
|
||||
"id": "flat",
|
||||
"label": "到期·现价附近",
|
||||
"spot": spot,
|
||||
"perp_pnl": 0.0,
|
||||
"options_pnl": round(opt_flat, 4),
|
||||
"total": round(flat_total, 4),
|
||||
"note": "示意:永续未动,期权按到期内在",
|
||||
},
|
||||
{
|
||||
"id": "expiry_tp",
|
||||
"label": "到期·止盈价",
|
||||
"spot": tp,
|
||||
"perp_pnl": round(pnl_tp_perp, 4),
|
||||
"options_pnl": round(opt_at_tp, 4),
|
||||
"total": round(pnl_tp_perp + opt_at_tp, 4),
|
||||
"note": "若期权拿到 TP 价到期(参考)",
|
||||
},
|
||||
{
|
||||
"id": "expiry_sl",
|
||||
"label": "到期·止损价",
|
||||
"spot": sl,
|
||||
"perp_pnl": round(pnl_sl_perp, 4),
|
||||
"options_pnl": round(opt_at_sl, 4),
|
||||
"total": round(pnl_sl_perp + opt_at_sl, 4),
|
||||
"note": "与止损口径相近(期权用内在)",
|
||||
},
|
||||
],
|
||||
"summary": {
|
||||
"tp_total": round(tp_total, 4),
|
||||
"sl_total": round(sl_total_signed, 4),
|
||||
"premium_paid": round(float(premium_paid), 4),
|
||||
"hedge_ratio_at_sl": _hedge_ratio(opt_at_sl, pnl_sl_perp),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]:
|
||||
loss = abs(float(perp_pnl)) if float(perp_pnl) < 0 else 0.0
|
||||
if loss <= 1e-12:
|
||||
return None
|
||||
if float(opt_pnl) <= 0:
|
||||
return 0.0
|
||||
return round(float(opt_pnl) / loss * 100.0, 2)
|
||||
|
||||
|
||||
def build_options_options_preview(
|
||||
*,
|
||||
target_price: float,
|
||||
index_px: float,
|
||||
leg_a: dict[str, Any],
|
||||
leg_b: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""期期情景:目标价 / 到期现价 / 到期两边."""
|
||||
|
||||
def _leg_pnl(leg: dict[str, Any], spot: float) -> float:
|
||||
return option_expiry_pnl(
|
||||
opt_type=str(leg.get("opt_type") or ""),
|
||||
strike=float(leg["strike"]),
|
||||
spot=spot,
|
||||
sheets=float(leg.get("sheets") or 0),
|
||||
ct_mult=float(leg.get("ct_mult") or 0.01),
|
||||
premium_paid=float(leg.get("premium_paid") or 0),
|
||||
)
|
||||
|
||||
prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0)
|
||||
a_t = _leg_pnl(leg_a, target_price)
|
||||
b_t = _leg_pnl(leg_b, target_price)
|
||||
at_target = a_t + b_t
|
||||
win_leg = "a" if a_t >= b_t else "b"
|
||||
a_flat = _leg_pnl(leg_a, index_px)
|
||||
b_flat = _leg_pnl(leg_b, index_px)
|
||||
flat_total = a_flat + b_flat
|
||||
expiry_loss = flat_total if flat_total <= 0 else flat_total
|
||||
|
||||
return {
|
||||
"plan_type": "options_options",
|
||||
"premium_paid": round(prem, 6),
|
||||
"target_price": target_price,
|
||||
"winner_at_target": win_leg,
|
||||
"scenarios": [
|
||||
{
|
||||
"id": "target",
|
||||
"label": "到达目标价",
|
||||
"spot": target_price,
|
||||
"leg_a_pnl": round(a_t, 4),
|
||||
"leg_b_pnl": round(b_t, 4),
|
||||
"total": round(at_target, 4),
|
||||
"note": f"盈利方≈腿{win_leg.upper()}(可平);亏损方默认到期",
|
||||
},
|
||||
{
|
||||
"id": "expiry_flat",
|
||||
"label": "到期·现价(无突破)",
|
||||
"spot": index_px,
|
||||
"leg_a_pnl": round(a_flat, 4),
|
||||
"leg_b_pnl": round(b_flat, 4),
|
||||
"total": round(flat_total, 4),
|
||||
"note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值",
|
||||
},
|
||||
{
|
||||
"id": "max_premium_loss",
|
||||
"label": "最大保费损耗",
|
||||
"spot": None,
|
||||
"leg_a_pnl": round(-float(leg_a.get("premium_paid") or 0), 4),
|
||||
"leg_b_pnl": round(-float(leg_b.get("premium_paid") or 0), 4),
|
||||
"total": round(-prem, 4),
|
||||
"note": "双腿权利金全部损失",
|
||||
},
|
||||
],
|
||||
"summary": {
|
||||
"at_target_total": round(at_target, 4),
|
||||
"expiry_flat_total": round(expiry_loss, 4),
|
||||
"premium_paid": round(prem, 4),
|
||||
"expiry_is_loss": flat_total <= 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def gate_status(
|
||||
*,
|
||||
hedge_enabled: bool,
|
||||
sizing_mode: str,
|
||||
plan_type: str,
|
||||
options_enabled: bool,
|
||||
) -> dict[str, Any]:
|
||||
from lib.trade.position_sizing_lib import is_full_margin_mode
|
||||
|
||||
full = is_full_margin_mode(sizing_mode)
|
||||
pt = (plan_type or "").strip().lower()
|
||||
can_preview = True
|
||||
can_start = False
|
||||
reasons: list[str] = []
|
||||
if not hedge_enabled:
|
||||
can_start = False
|
||||
reasons.append("对冲计划未启用(HEDGE_PLAN_ENABLED)")
|
||||
if not options_enabled:
|
||||
can_preview = False
|
||||
can_start = False
|
||||
reasons.append("期权模块未启用")
|
||||
if pt == "perp_options":
|
||||
if not full:
|
||||
can_start = False
|
||||
reasons.append("永期开仓仅全仓模式可用(当前可测算)")
|
||||
elif hedge_enabled and options_enabled:
|
||||
can_start = False
|
||||
reasons.append("P0 仅测算,真实开仓将在后续版本开放")
|
||||
elif pt == "options_options":
|
||||
if hedge_enabled and options_enabled:
|
||||
can_start = False
|
||||
reasons.append("P0 仅测算,真实开仓将在后续版本开放")
|
||||
return {
|
||||
"hedge_enabled": hedge_enabled,
|
||||
"options_enabled": options_enabled,
|
||||
"sizing_mode": sizing_mode,
|
||||
"is_full_margin": full,
|
||||
"plan_type": pt,
|
||||
"can_preview": can_preview,
|
||||
"can_start": can_start,
|
||||
"reasons": reasons,
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
"""OKX 对冲计划:P0 测算页与 API 注册."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
from jinja2 import ChoiceLoader, FileSystemLoader
|
||||
|
||||
from lib.hedge_plan.hedge_plan_calc_lib import (
|
||||
build_options_options_preview,
|
||||
build_perp_options_preview,
|
||||
gate_status,
|
||||
option_premium_total,
|
||||
suggest_contracts_from_notional,
|
||||
)
|
||||
from lib.trade.position_sizing_lib import (
|
||||
compute_full_margin_sizing,
|
||||
load_position_sizing_mode,
|
||||
)
|
||||
|
||||
|
||||
def _env_bool(key: str, default: bool = False) -> bool:
|
||||
raw = (os.getenv(key) or "").strip().lower()
|
||||
if not raw:
|
||||
return default
|
||||
return raw in ("1", "true", "yes", "on")
|
||||
|
||||
|
||||
def attach_hedge_plan_templates(app: Flask, repo_root: str) -> None:
|
||||
tpl_dir = os.path.join(repo_root, "lib", "hedge_plan", "templates")
|
||||
if not os.path.isdir(tpl_dir):
|
||||
return
|
||||
existing = app.jinja_loader
|
||||
loaders = [FileSystemLoader(tpl_dir)]
|
||||
if existing is not None:
|
||||
if isinstance(existing, ChoiceLoader):
|
||||
loaders = list(existing.loaders) + loaders
|
||||
else:
|
||||
loaders.insert(0, existing)
|
||||
app.jinja_loader = ChoiceLoader(loaders)
|
||||
|
||||
|
||||
def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None:
|
||||
attach_hedge_plan_templates(app, repo_root)
|
||||
cfg = _build_cfg(app_module)
|
||||
app.extensions["hedge_plan_cfg"] = cfg
|
||||
register_hedge_plan_routes(app, cfg)
|
||||
|
||||
|
||||
def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||
from lib.exchange.okx_options_lib import build_option_chain
|
||||
|
||||
return {
|
||||
"get_db": app_module.get_db,
|
||||
"login_required": app_module.login_required,
|
||||
"render_main_page": app_module.render_main_page,
|
||||
"exchange": getattr(app_module, "exchange", None),
|
||||
"exchange_options": getattr(app_module, "exchange_options", None),
|
||||
"get_available_trading_usdt": getattr(app_module, "get_available_trading_usdt", None),
|
||||
"get_contract_size": getattr(app_module, "get_contract_size", None),
|
||||
"normalize_exchange_symbol": getattr(app_module, "normalize_exchange_symbol", None),
|
||||
"ensure_markets_loaded": getattr(app_module, "ensure_markets_loaded", None),
|
||||
"build_option_chain": build_option_chain,
|
||||
"btc_leverage": int(getattr(app_module, "BTC_LEVERAGE", 10) or 10),
|
||||
"alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5),
|
||||
"full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98),
|
||||
"funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2),
|
||||
"options_enabled": _env_bool("OKX_OPTIONS_ENABLED", False),
|
||||
"default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(),
|
||||
"chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"),
|
||||
}
|
||||
|
||||
|
||||
def _hedge_enabled() -> bool:
|
||||
return _env_bool("HEDGE_PLAN_ENABLED", False)
|
||||
|
||||
|
||||
def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
||||
lr = cfg["login_required"]
|
||||
|
||||
@app.route("/hedge-plan")
|
||||
@lr
|
||||
def page_hedge_plan():
|
||||
from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled
|
||||
|
||||
redir = redirect_to_embed_shell_if_enabled("hedge_plan")
|
||||
if redir is not None:
|
||||
return redir
|
||||
return cfg["render_main_page"]("hedge_plan")
|
||||
|
||||
@app.route("/api/hedge-plan/gates")
|
||||
@lr
|
||||
def api_hedge_gates():
|
||||
plan_type = (request.args.get("plan_type") or "perp_options").strip()
|
||||
return jsonify(
|
||||
{
|
||||
"ok": True,
|
||||
**gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
plan_type=plan_type,
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/hedge-plan/market")
|
||||
@lr
|
||||
def api_hedge_market():
|
||||
base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper()
|
||||
if base not in ("BTC", "ETH"):
|
||||
return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400
|
||||
direction = (request.args.get("direction") or "long").strip().lower()
|
||||
if direction not in ("long", "short"):
|
||||
direction = "long"
|
||||
data, err = _fetch_perp_market(cfg, base)
|
||||
if err:
|
||||
return jsonify({"ok": False, "msg": err}), 400
|
||||
sizing_mode = load_position_sizing_mode()
|
||||
gates = gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=sizing_mode,
|
||||
plan_type="perp_options",
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
)
|
||||
out = {
|
||||
"ok": True,
|
||||
"base": base,
|
||||
"direction": direction,
|
||||
"suggested_opt_type": "P" if direction == "long" else "C",
|
||||
**data,
|
||||
"gates": gates,
|
||||
"sizing_mode": sizing_mode,
|
||||
}
|
||||
return jsonify(out)
|
||||
|
||||
@app.route("/api/hedge-plan/options-chain")
|
||||
@lr
|
||||
def api_hedge_options_chain():
|
||||
if not cfg.get("options_enabled"):
|
||||
return jsonify({"ok": False, "msg": "期权模块未启用"}), 400
|
||||
ex = cfg.get("exchange_options")
|
||||
if ex is None:
|
||||
return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400
|
||||
u = (request.args.get("underlying") or cfg.get("default_underly") or "ETH").upper()
|
||||
try:
|
||||
chain = cfg["build_option_chain"](
|
||||
ex,
|
||||
u,
|
||||
max_dte_days=float(cfg.get("chain_max_dte") or 14),
|
||||
itm_only=False,
|
||||
itm_max_dist_usd=float(os.getenv("OKX_OPTIONS_ITM_MAX_DIST_USD") or "30"),
|
||||
)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500
|
||||
return jsonify({"ok": True, **chain, "chain_max_dte_days": cfg.get("chain_max_dte")})
|
||||
|
||||
@app.route("/api/hedge-plan/preview", methods=["POST"])
|
||||
@lr
|
||||
def api_hedge_preview():
|
||||
body = request.get_json(silent=True) or {}
|
||||
plan_type = (body.get("plan_type") or "perp_options").strip().lower()
|
||||
gates = gate_status(
|
||||
hedge_enabled=_hedge_enabled(),
|
||||
sizing_mode=load_position_sizing_mode(),
|
||||
plan_type=plan_type,
|
||||
options_enabled=bool(cfg.get("options_enabled")),
|
||||
)
|
||||
if not gates.get("can_preview"):
|
||||
return jsonify({"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可测算"]), "gates": gates}), 400
|
||||
try:
|
||||
if plan_type == "options_options":
|
||||
data = _preview_oo(body)
|
||||
else:
|
||||
data = _preview_po(body)
|
||||
except ValueError as e:
|
||||
return jsonify({"ok": False, "msg": str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "msg": f"测算失败: {e}"}), 500
|
||||
return jsonify({"ok": True, "gates": gates, **data})
|
||||
|
||||
|
||||
def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
|
||||
direction = str(body.get("direction") or "long").lower()
|
||||
entry = float(body["entry"])
|
||||
tp = float(body["tp"])
|
||||
sl = float(body["sl"])
|
||||
contracts = float(body["contracts"])
|
||||
contract_size = float(body.get("contract_size") or 0.01)
|
||||
opt_type = str(body.get("opt_type") or ("P" if direction == "long" else "C"))
|
||||
strike = float(body["strike"])
|
||||
sheets = float(body.get("sheets") or 1)
|
||||
ct_mult = float(body.get("ct_mult") or 0.01)
|
||||
ask = body.get("ask")
|
||||
premium = body.get("premium_paid")
|
||||
if premium is None:
|
||||
if ask is None:
|
||||
raise ValueError("缺少权利金或卖一价")
|
||||
premium = option_premium_total(ask=float(ask), sheets=sheets, ct_mult=ct_mult)
|
||||
index_px = body.get("index_px")
|
||||
return build_perp_options_preview(
|
||||
direction=direction,
|
||||
entry=entry,
|
||||
tp=tp,
|
||||
sl=sl,
|
||||
contracts=contracts,
|
||||
contract_size=contract_size,
|
||||
opt_type=opt_type,
|
||||
strike=strike,
|
||||
sheets=sheets,
|
||||
ct_mult=ct_mult,
|
||||
premium_paid=float(premium),
|
||||
index_px=float(index_px) if index_px is not None else None,
|
||||
)
|
||||
|
||||
|
||||
def _preview_oo(body: dict[str, Any]) -> dict[str, Any]:
|
||||
target = float(body["target_price"])
|
||||
index_px = float(body.get("index_px") or target)
|
||||
leg_a = body.get("leg_a") or {}
|
||||
leg_b = body.get("leg_b") or {}
|
||||
for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)):
|
||||
if not leg.get("strike"):
|
||||
raise ValueError(f"缺少 {name} 行权价")
|
||||
if leg.get("premium_paid") is None and leg.get("ask") is not None:
|
||||
leg["premium_paid"] = option_premium_total(
|
||||
ask=float(leg["ask"]),
|
||||
sheets=float(leg.get("sheets") or 1),
|
||||
ct_mult=float(leg.get("ct_mult") or 0.01),
|
||||
)
|
||||
if leg.get("premium_paid") is None:
|
||||
raise ValueError(f"缺少 {name} 权利金")
|
||||
return build_options_options_preview(
|
||||
target_price=target,
|
||||
index_px=index_px,
|
||||
leg_a=leg_a,
|
||||
leg_b=leg_b,
|
||||
)
|
||||
|
||||
|
||||
def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], str | None]:
|
||||
ex = cfg.get("exchange")
|
||||
if ex is None:
|
||||
return {}, "永续交易所未初始化"
|
||||
ensure = cfg.get("ensure_markets_loaded")
|
||||
if callable(ensure):
|
||||
try:
|
||||
ensure()
|
||||
except Exception as e:
|
||||
return {}, f"加载市场失败: {e}"
|
||||
norm = cfg.get("normalize_exchange_symbol")
|
||||
sym = f"{base}/USDT:USDT"
|
||||
if callable(norm):
|
||||
try:
|
||||
sym = norm(f"{base}/USDT")
|
||||
except Exception:
|
||||
sym = f"{base}/USDT:USDT"
|
||||
mark = bid = ask = last = None
|
||||
try:
|
||||
t = ex.fetch_ticker(sym)
|
||||
last = _sf(t.get("last"))
|
||||
mark = _sf(t.get("info", {}).get("markPx")) if isinstance(t.get("info"), dict) else None
|
||||
if mark is None:
|
||||
mark = _sf(t.get("mark")) or last
|
||||
bid = _sf(t.get("bid"))
|
||||
ask = _sf(t.get("ask"))
|
||||
except Exception as e:
|
||||
return {}, f"拉永续行情失败: {e}"
|
||||
|
||||
cs = 0.01
|
||||
get_cs = cfg.get("get_contract_size")
|
||||
if callable(get_cs):
|
||||
try:
|
||||
cs = float(get_cs(sym) or 0.01)
|
||||
except Exception:
|
||||
cs = 0.01
|
||||
|
||||
available = None
|
||||
get_av = cfg.get("get_available_trading_usdt")
|
||||
if callable(get_av):
|
||||
try:
|
||||
available = get_av()
|
||||
except Exception:
|
||||
available = None
|
||||
|
||||
entry = float(mark or last or 0)
|
||||
sizing = None
|
||||
suggest_contracts = None
|
||||
if available is not None and entry > 0:
|
||||
sizing, _serr = compute_full_margin_sizing(
|
||||
symbol=sym,
|
||||
available_usdt=float(available),
|
||||
capital_base=float(available),
|
||||
buffer_ratio=float(cfg.get("full_margin_buffer") or 0.98),
|
||||
btc_leverage=int(cfg.get("btc_leverage") or 10),
|
||||
alt_leverage=int(cfg.get("alt_leverage") or 5),
|
||||
funds_decimals=int(cfg.get("funds_decimals") or 2),
|
||||
)
|
||||
if sizing:
|
||||
suggest_contracts = suggest_contracts_from_notional(
|
||||
notional=float(sizing["notional_value"]),
|
||||
entry=entry,
|
||||
contract_size=cs,
|
||||
)
|
||||
|
||||
return {
|
||||
"exchange_symbol": sym,
|
||||
"mark": mark,
|
||||
"last": last,
|
||||
"bid": bid,
|
||||
"ask": ask,
|
||||
"contract_size": cs,
|
||||
"available_usdt": available,
|
||||
"full_margin_sizing": sizing,
|
||||
"suggest_contracts": round(suggest_contracts, 6) if suggest_contracts is not None else None,
|
||||
"entry_ref": entry or None,
|
||||
}, None
|
||||
|
||||
|
||||
def _sf(v: Any) -> float | None:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
@@ -0,0 +1,144 @@
|
||||
<div class="hedge-plan-page-wrap" style="grid-column:1/-1" id="hedge-plan-root"
|
||||
data-default-underly="{{ options_default_underly | default('ETH') }}"
|
||||
data-hedge-enabled="{{ '1' if hedge_plan_enabled else '0' }}"
|
||||
data-options-enabled="{{ '1' if options_enabled else '0' }}"
|
||||
data-sizing-mode="{{ position_sizing_mode | default('risk') }}"
|
||||
data-is-full-margin="{{ '1' if position_sizing_mode == 'full_margin' else '0' }}">
|
||||
{% if not hedge_plan_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px">对冲计划未启用:请在 <code>env配置 → 对冲计划</code> 打开 <code>HEDGE_PLAN_ENABLED</code>(可热更).</div>
|
||||
{% endif %}
|
||||
{% if not options_enabled %}
|
||||
<div class="flash" style="margin-bottom:12px">期权模块未启用,无法拉期权链.请先配置期权账户.</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card" style="margin-bottom:12px">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between">
|
||||
<h2 style="margin:0">对冲计划 <span class="muted" style="font-size:.85rem;font-weight:400">P0 测算</span></h2>
|
||||
<div class="form-row" style="margin:0">
|
||||
<button type="button" class="btn-secondary hp-mode-btn active" data-mode="perp_options">永期对冲</button>
|
||||
<button type="button" class="btn-secondary hp-mode-btn" data-mode="options_options">期期对冲</button>
|
||||
<button type="button" class="btn-secondary" id="hp-refresh">刷新行情</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="muted" id="hp-gate-line" style="margin:8px 0 0"></p>
|
||||
</div>
|
||||
|
||||
<div class="options-dual-grid" id="hp-po-layout">
|
||||
<div class="card">
|
||||
<h2>永续(列表行情)</h2>
|
||||
<div class="form-row">
|
||||
<button type="button" class="btn-secondary hp-uly-btn active" data-uly="ETH">ETH</button>
|
||||
<button type="button" class="btn-secondary hp-uly-btn" data-uly="BTC">BTC</button>
|
||||
<select id="hp-direction">
|
||||
<option value="long">做多</option>
|
||||
<option value="short">做空</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="hp-perp-quote" class="muted" style="margin:8px 0;line-height:1.6">加载中…</div>
|
||||
<div class="form-row" style="flex-wrap:wrap">
|
||||
<label>开仓价 <input type="number" step="any" id="hp-entry" /></label>
|
||||
<label>止盈 <input type="number" step="any" id="hp-tp" /></label>
|
||||
<label>止损 <input type="number" step="any" id="hp-sl" /></label>
|
||||
<label>张数 <input type="number" step="any" id="hp-contracts" /></label>
|
||||
</div>
|
||||
<p class="muted" id="hp-sizing-line"></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>期权(列表) · <span id="hp-opt-type-label">Put</span></h2>
|
||||
<div class="form-row">
|
||||
<select id="hp-exp-select"><option value="">选择到期日</option></select>
|
||||
<button type="button" class="btn-secondary" id="hp-load-chain">刷新链</button>
|
||||
</div>
|
||||
<div id="hp-index-line" class="muted"></div>
|
||||
<div class="options-strike-table-wrap">
|
||||
<table class="options-strike-table" id="hp-strike-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>行权价</th>
|
||||
<th>类型</th>
|
||||
<th>卖一/张</th>
|
||||
<th>买一</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="hp-strike-tbody">
|
||||
<tr><td colspan="5" class="muted">请刷新期权链</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top:8px;flex-wrap:wrap">
|
||||
<label>已选合约 <code id="hp-sel-inst">—</code></label>
|
||||
<label>张数 <input type="number" step="1" min="1" id="hp-sheets" value="1" /></label>
|
||||
<span class="muted" id="hp-premium-line"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-dual-grid hidden" id="hp-oo-layout">
|
||||
<div class="card">
|
||||
<h2>期期参数</h2>
|
||||
<div class="form-row">
|
||||
<button type="button" class="btn-secondary hp-uly-btn-oo active" data-uly="ETH">ETH</button>
|
||||
<button type="button" class="btn-secondary hp-uly-btn-oo" data-uly="BTC">BTC</button>
|
||||
<label>目标价 S* <input type="number" step="any" id="hp-target" /></label>
|
||||
</div>
|
||||
<div id="hp-oo-index" class="muted"></div>
|
||||
<div id="hp-oo-legs" class="muted" style="margin-top:8px;line-height:1.6">尚未选用两腿</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>期权 T 型报价</h2>
|
||||
<div class="form-row">
|
||||
<select id="hp-oo-exp-select"><option value="">选择到期日</option></select>
|
||||
<button type="button" class="btn-secondary" id="hp-oo-load-chain">刷新链</button>
|
||||
</div>
|
||||
<div class="options-strike-table-wrap options-strike-table-wrap--t">
|
||||
<table class="options-strike-table options-strike-table--t" id="hp-oo-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2" class="opt-t-head-call">Call</th>
|
||||
<th class="opt-t-head-mid">行权</th>
|
||||
<th colspan="2" class="opt-t-head-put">Put</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>卖一</th><th>选用</th>
|
||||
<th>K</th>
|
||||
<th>卖一</th><th>选用</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="hp-oo-tbody">
|
||||
<tr><td colspan="5" class="muted">请刷新期权链</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-top:12px">
|
||||
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;justify-content:space-between">
|
||||
<h2 style="margin:0">情景测算</h2>
|
||||
<div class="form-row" style="margin:0">
|
||||
<button type="button" class="primary" id="hp-preview-btn">计算</button>
|
||||
<button type="button" class="btn-secondary" id="hp-start-btn" disabled title="P0 不开仓">启动计划(P0禁用)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hp-summary" class="muted" style="margin:8px 0"></div>
|
||||
<div class="options-strike-table-wrap">
|
||||
<table class="options-strike-table" id="hp-result-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>情景</th>
|
||||
<th>现货价</th>
|
||||
<th>永续/腿盈亏</th>
|
||||
<th>期权盈亏</th>
|
||||
<th>合计≈U</th>
|
||||
<th>说明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="hp-result-tbody">
|
||||
<tr><td colspan="6" class="muted">填写参数后点计算</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/hedge_plan.js?v=1"></script>
|
||||
@@ -15,6 +15,7 @@ DEFAULT_INSTANCE_DISPLAY: dict[str, bool] = {
|
||||
"show_nav_risk_policy": True,
|
||||
"show_nav_env_config": True,
|
||||
"show_nav_options": True,
|
||||
"show_nav_hedge_plan": True,
|
||||
"show_settings_transfer": True,
|
||||
"show_settings_export": True,
|
||||
"show_settings_password": True,
|
||||
@@ -30,6 +31,7 @@ DISPLAY_LABELS: dict[str, str] = {
|
||||
"show_nav_risk_policy": "风控说明",
|
||||
"show_nav_env_config": "env配置",
|
||||
"show_nav_options": "期权",
|
||||
"show_nav_hedge_plan": "对冲计划",
|
||||
"show_settings_transfer": "资金划转",
|
||||
"show_settings_export": "数据导出",
|
||||
"show_settings_password": "账户密码修改",
|
||||
@@ -45,6 +47,7 @@ NAV_TAB_ALLOWED: dict[str, str] = {
|
||||
"risk_policy": "show_nav_risk_policy",
|
||||
"env_config": "show_nav_env_config",
|
||||
"options": "show_nav_options",
|
||||
"hedge_plan": "show_nav_hedge_plan",
|
||||
}
|
||||
|
||||
|
||||
@@ -104,6 +107,7 @@ def display_meta_for_ui() -> list[dict[str, Any]]:
|
||||
"show_nav_risk_policy",
|
||||
"show_nav_env_config",
|
||||
"show_nav_options",
|
||||
"show_nav_hedge_plan",
|
||||
]
|
||||
settings_keys = [
|
||||
"show_settings_transfer",
|
||||
|
||||
@@ -16,6 +16,7 @@ EMBED_TABS: tuple[str, ...] = (
|
||||
"strategy",
|
||||
"strategy_records",
|
||||
"options",
|
||||
"hedge_plan",
|
||||
"records",
|
||||
"stats",
|
||||
"risk_policy",
|
||||
@@ -32,6 +33,7 @@ PATH_TO_EMBED_TAB: dict[str, str] = {
|
||||
"/strategy/roll": "strategy",
|
||||
"/strategy/records": "strategy_records",
|
||||
"/options": "options",
|
||||
"/hedge-plan": "hedge_plan",
|
||||
"/records": "records",
|
||||
"/stats": "stats",
|
||||
"/risk_policy": "risk_policy",
|
||||
|
||||
@@ -293,6 +293,8 @@
|
||||
{% include 'strategy_records_page.html' %}
|
||||
{% elif page == 'options' %}
|
||||
{% include 'options_panel.html' %}
|
||||
{% elif page == 'hedge_plan' %}
|
||||
{% include 'hedge_plan_panel.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
{% if options_nav_visible and display.show_nav_options %}
|
||||
<a href="/options" data-embed-tab="options" class="{% if initial_tab == 'options' %}active{% endif %}">期权</a>
|
||||
{% endif %}
|
||||
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
|
||||
<a href="/hedge-plan" data-embed-tab="hedge_plan" class="{% if initial_tab == 'hedge_plan' %}active{% endif %}">对冲计划</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_risk_policy %}
|
||||
<a href="/risk_policy" data-embed-tab="risk_policy" class="{% if initial_tab == 'risk_policy' %}active{% endif %}">风控说明</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -133,6 +133,9 @@
|
||||
{% if options_nav_visible and display.show_nav_options %}
|
||||
<a href="/options" class="{% if page == 'options' %}active{% endif %}">期权</a>
|
||||
{% endif %}
|
||||
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
|
||||
<a href="/hedge-plan" class="{% if page == 'hedge_plan' %}active{% endif %}">对冲计划</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_risk_policy %}
|
||||
<a href="/risk_policy" class="{% if page == 'risk_policy' %}active{% endif %}">风控说明</a>
|
||||
{% endif %}
|
||||
@@ -144,7 +147,7 @@
|
||||
{% with msg=get_flashed_messages() %}{% if msg %}<div class="flash">{{ msg[0] }}</div>{% endif %}{% endwith %}
|
||||
|
||||
{% include 'instance_header_panel.html' %}
|
||||
{% if page not in ('settings', 'risk_policy', 'env_config', 'options') %}
|
||||
{% if page not in ('settings', 'risk_policy', 'env_config', 'options', 'hedge_plan') %}
|
||||
{% include 'instance_top_bar.html' %}
|
||||
{% endif %}
|
||||
|
||||
@@ -361,6 +364,8 @@
|
||||
{% include 'strategy_records_page.html' %}
|
||||
{% elif page == 'options' %}
|
||||
{% include 'options_panel.html' %}
|
||||
{% elif page == 'hedge_plan' %}
|
||||
{% include 'hedge_plan_panel.html' %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user