feat(options): use premium profit RR instead of target index
单独期权与中控改为盈亏比×权利金触发买一平仓,默认2;不达标等到期。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -321,7 +321,7 @@
|
|||||||
[
|
[
|
||||||
"opt-sheets-amount",
|
"opt-sheets-amount",
|
||||||
"opt-eth-amount",
|
"opt-eth-amount",
|
||||||
"opt-target-idx",
|
"opt-profit-rr",
|
||||||
].forEach(function (id) {
|
].forEach(function (id) {
|
||||||
harden(document.getElementById(id));
|
harden(document.getElementById(id));
|
||||||
});
|
});
|
||||||
@@ -894,11 +894,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOrderEstimates() {
|
function updateOrderEstimates() {
|
||||||
const levEl = document.getElementById("opt-order-leverage");
|
|
||||||
const valueEl = document.getElementById("opt-est-value");
|
const valueEl = document.getElementById("opt-est-value");
|
||||||
const profitEl = document.getElementById("opt-est-profit");
|
const profitEl = document.getElementById("opt-est-profit");
|
||||||
const targetLevEl = document.getElementById("opt-est-leverage");
|
const levEl = document.getElementById("opt-order-leverage");
|
||||||
const targetEl = document.getElementById("opt-target-idx");
|
const rrEl = document.getElementById("opt-profit-rr");
|
||||||
const q = state.orderQuote;
|
const q = state.orderQuote;
|
||||||
if (!q || !q.ok || !q.can_open) {
|
if (!q || !q.ok || !q.can_open) {
|
||||||
if (levEl) levEl.textContent = "—";
|
if (levEl) levEl.textContent = "—";
|
||||||
@@ -907,7 +906,6 @@
|
|||||||
profitEl.textContent = "—";
|
profitEl.textContent = "—";
|
||||||
profitEl.className = "v";
|
profitEl.className = "v";
|
||||||
}
|
}
|
||||||
if (targetLevEl) targetLevEl.textContent = "—";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sz = q.sizing || {};
|
const sz = q.sizing || {};
|
||||||
@@ -916,30 +914,19 @@
|
|||||||
const lev = calcContractLeverage(q.index_px, ethAmount, premium);
|
const lev = calcContractLeverage(q.index_px, ethAmount, premium);
|
||||||
if (levEl) levEl.textContent = fmtLeverage(lev);
|
if (levEl) levEl.textContent = fmtLeverage(lev);
|
||||||
|
|
||||||
if (valueEl && profitEl && targetEl) {
|
if (valueEl && profitEl && rrEl) {
|
||||||
const targetRaw = targetEl.value;
|
const rrRaw = rrEl.value;
|
||||||
if (targetRaw === "" || targetRaw == null) {
|
const rr = rrRaw === "" || rrRaw == null ? NaN : Number(rrRaw);
|
||||||
|
if (!Number.isFinite(rr) || rr <= 0 || !(Number(premium) > 0)) {
|
||||||
valueEl.textContent = "—";
|
valueEl.textContent = "—";
|
||||||
profitEl.textContent = "—";
|
profitEl.textContent = "—";
|
||||||
profitEl.className = "v";
|
profitEl.className = "v";
|
||||||
if (targetLevEl) targetLevEl.textContent = "—";
|
|
||||||
} else {
|
} else {
|
||||||
const value = estimateExpiryValue(q.opt_type, q.strike, Number(targetRaw), ethAmount);
|
const targetProfit = Number(premium) * rr;
|
||||||
const profit = estimateExpiryProfit(q.opt_type, q.strike, Number(targetRaw), ethAmount, premium);
|
const needRecycle = Number(premium) + targetProfit;
|
||||||
if (value == null || Number.isNaN(value)) {
|
valueEl.textContent = fmtUsdc(needRecycle) + " USDC";
|
||||||
valueEl.textContent = "—";
|
profitEl.textContent = fmtUsdcSigned(targetProfit);
|
||||||
} else {
|
profitEl.className = "v " + pnlCls(targetProfit);
|
||||||
valueEl.textContent = fmtUsdc(value) + " USDC";
|
|
||||||
}
|
|
||||||
if (profit == null || Number.isNaN(profit)) {
|
|
||||||
profitEl.textContent = "—";
|
|
||||||
profitEl.className = "v";
|
|
||||||
} else {
|
|
||||||
profitEl.textContent = fmtUsdcSigned(profit);
|
|
||||||
profitEl.className = "v " + pnlCls(profit);
|
|
||||||
}
|
|
||||||
const targetLev = calcContractLeverage(Number(targetRaw), ethAmount, premium);
|
|
||||||
if (targetLevEl) targetLevEl.textContent = fmtLeverage(targetLev);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1329,15 +1316,13 @@
|
|||||||
} else if (mode === "sheets") {
|
} else if (mode === "sheets") {
|
||||||
body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10);
|
body.sheets = parseInt(document.getElementById("opt-sheets-amount").value, 10);
|
||||||
}
|
}
|
||||||
const tgtRaw = (document.getElementById("opt-target-idx").value || "").trim();
|
const rrRaw = (document.getElementById("opt-profit-rr").value || "").trim();
|
||||||
if (tgtRaw !== "") {
|
const rr = rrRaw === "" ? 2 : parseFloat(rrRaw);
|
||||||
const tgt = parseFloat(tgtRaw);
|
if (!Number.isFinite(rr) || rr <= 0) {
|
||||||
if (!Number.isFinite(tgt) || tgt <= 0) {
|
alert("盈亏比无效");
|
||||||
alert("目标位无效");
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
body.target_index = tgt;
|
|
||||||
}
|
}
|
||||||
|
body.profit_rr = rr;
|
||||||
const d = await apiJson("/api/options/open", {
|
const d = await apiJson("/api/options/open", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -1428,15 +1413,17 @@
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTargetEstimateHtml(optType, strike, targetIdx, ethAmount, premiumPaid) {
|
function formatRrEstimateHtml(rr, premiumPaid) {
|
||||||
const value = estimateExpiryValue(optType, strike, targetIdx, ethAmount);
|
const r = Number(rr);
|
||||||
const profit = estimateExpiryProfit(optType, strike, targetIdx, ethAmount, premiumPaid);
|
const prem = Number(premiumPaid);
|
||||||
if (value == null && profit == null) return "";
|
if (!Number.isFinite(r) || r <= 0 || !Number.isFinite(prem) || prem <= 0) return "";
|
||||||
|
const profit = Math.round(prem * r * 100) / 100;
|
||||||
|
const need = Math.round((prem + profit) * 100) / 100;
|
||||||
let html = '<span class="opt-target-est">';
|
let html = '<span class="opt-target-est">';
|
||||||
html += '<span class="opt-target-est-item"><span class="k">价值</span><span class="v">' +
|
html += '<span class="opt-target-est-item"><span class="k">目标盈利</span><span class="v ' + pnlCls(profit) + '">' +
|
||||||
(value == null ? "—" : fmtUsdc(value) + " USDC") + "</span></span>";
|
fmtUsdcSigned(profit) + "</span></span>";
|
||||||
html += '<span class="opt-target-est-item"><span class="k">预估盈利</span><span class="v ' + pnlCls(profit) + '">' +
|
html += '<span class="opt-target-est-item"><span class="k">需回收</span><span class="v">' +
|
||||||
(profit == null ? "—" : fmtUsdcSigned(profit)) + "</span></span>";
|
fmtUsdc(need) + " USDC</span></span>";
|
||||||
html += "</span>";
|
html += "</span>";
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
@@ -1444,47 +1431,73 @@
|
|||||||
function renderTargetDelegateRow(p) {
|
function renderTargetDelegateRow(p) {
|
||||||
const inst = p.inst_id || "";
|
const inst = p.inst_id || "";
|
||||||
const hedgeTarget = p.hedge_plan_target || null;
|
const hedgeTarget = p.hedge_plan_target || null;
|
||||||
if (hedgeTarget && Number(hedgeTarget.target_index) > 0) {
|
if (hedgeTarget && hedgeTarget.managed_by === "hedge_plan") {
|
||||||
const side = (p.opt_type || hedgeTarget.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
|
const rr = hedgeTarget.oo_profit_rr != null ? Number(hedgeTarget.oo_profit_rr) : null;
|
||||||
|
const armedTxt =
|
||||||
|
rr != null && Number.isFinite(rr) && rr > 0
|
||||||
|
? "盈亏比 ×" + fmt(rr, 2)
|
||||||
|
: hedgeTarget.target_index != null
|
||||||
|
? "目标 " + fmt(hedgeTarget.target_index, 1)
|
||||||
|
: "托管中";
|
||||||
return (
|
return (
|
||||||
'<div class="opt-target-row opt-target-row--managed">' +
|
'<div class="opt-target-row opt-target-row--managed">' +
|
||||||
'<span class="opt-target-row-label">对冲计划</span>' +
|
'<span class="opt-target-row-label">对冲计划</span>' +
|
||||||
'<span class="opt-target-armed">计划 #' +
|
'<span class="opt-target-armed">计划 #' +
|
||||||
hedgeTarget.plan_id +
|
hedgeTarget.plan_id +
|
||||||
" · " +
|
" · " +
|
||||||
side +
|
armedTxt +
|
||||||
" " +
|
|
||||||
fmt(hedgeTarget.target_index, 1) +
|
|
||||||
"</span>" +
|
"</span>" +
|
||||||
'<span class="muted opt-target-row-hint">进行中 · 由对冲计划监控,到位后仅平盈利腿</span>' +
|
'<span class="muted opt-target-row-hint">进行中 · 由对冲计划监控</span>' +
|
||||||
"</div>"
|
"</div>"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const tgt = p.target_index != null && p.target_index !== "" ? Number(p.target_index) : null;
|
const rrArmed =
|
||||||
const armed = tgt != null && Number.isFinite(tgt) && tgt > 0;
|
p.profit_rr != null && p.profit_rr !== ""
|
||||||
const ethAmt = posEthAmount(p);
|
? Number(p.profit_rr)
|
||||||
|
: p.target_monitor && p.target_monitor.profit_rr != null
|
||||||
|
? Number(p.target_monitor.profit_rr)
|
||||||
|
: null;
|
||||||
|
const armed = rrArmed != null && Number.isFinite(rrArmed) && rrArmed > 0;
|
||||||
const prem = p.premium_paid;
|
const prem = p.premium_paid;
|
||||||
|
const draft =
|
||||||
|
state.targetDraftByInst[inst] != null
|
||||||
|
? String(state.targetDraftByInst[inst])
|
||||||
|
: armed
|
||||||
|
? String(rrArmed)
|
||||||
|
: "2";
|
||||||
const estHtml = armed
|
const estHtml = armed
|
||||||
? formatTargetEstimateHtml(p.opt_type, p.strike, tgt, ethAmt, prem)
|
? formatRrEstimateHtml(rrArmed, prem)
|
||||||
: '<span class="opt-target-est opt-target-est--idle"></span>';
|
: '<span class="opt-target-est opt-target-est--idle"></span>';
|
||||||
return (
|
return (
|
||||||
'<div class="opt-target-row" data-inst="' + inst + '"' +
|
'<div class="opt-target-row" data-inst="' +
|
||||||
' data-opt-type="' + (p.opt_type || "") + '"' +
|
inst +
|
||||||
' data-strike="' + (p.strike != null ? p.strike : "") + '"' +
|
'"' +
|
||||||
' data-eth="' + (ethAmt != null ? ethAmt : "") + '"' +
|
' data-prem="' +
|
||||||
' data-prem="' + (prem != null ? prem : "") + '"' +
|
(prem != null ? prem : "") +
|
||||||
' data-armed-target="' + (armed ? tgt : "") + '">' +
|
'"' +
|
||||||
|
' data-armed-rr="' +
|
||||||
|
(armed ? rrArmed : "") +
|
||||||
|
'">' +
|
||||||
'<span class="opt-target-row-label">委托</span>' +
|
'<span class="opt-target-row-label">委托</span>' +
|
||||||
'<input type="number" class="opt-pos-target-input" data-inst="' + inst + '" step="0.1" min="0" placeholder="监控目标指数" value="' +
|
'<input type="number" class="opt-pos-target-input" data-inst="' +
|
||||||
(state.targetDraftByInst[inst] != null ? String(state.targetDraftByInst[inst]) : "") + '">' +
|
inst +
|
||||||
'<button type="button" class="btn-secondary opt-target-set-btn" data-inst="' + inst + '">设定</button>' +
|
'" step="0.1" min="0.1" placeholder="盈亏比" value="' +
|
||||||
'<button type="button" class="btn-secondary opt-target-cancel-btn" data-inst="' + inst + '"' + (armed ? "" : " disabled") + ">取消</button>" +
|
draft +
|
||||||
(armed
|
'">' +
|
||||||
? '<span class="opt-target-armed">目标 ' + fmt(tgt, 1) + "</span>"
|
'<button type="button" class="btn-secondary opt-target-set-btn" data-inst="' +
|
||||||
: "") +
|
inst +
|
||||||
|
'">设定</button>' +
|
||||||
|
'<button type="button" class="btn-secondary opt-target-cancel-btn" data-inst="' +
|
||||||
|
inst +
|
||||||
|
'"' +
|
||||||
|
(armed ? "" : " disabled") +
|
||||||
|
">取消</button>" +
|
||||||
|
(armed ? '<span class="opt-target-armed">盈亏比 ×' + fmt(rrArmed, 2) + "</span>" : "") +
|
||||||
estHtml +
|
estHtml +
|
||||||
'<span class="muted opt-target-row-hint">' +
|
'<span class="muted opt-target-row-hint">' +
|
||||||
(armed ? "监控中 · 到位按买一限价平" : "输入后设定 · 到位按买一限价平 · 到期即止损") +
|
(armed
|
||||||
|
? "监控中 · 买一浮盈达盈亏比后全平"
|
||||||
|
: "默认2 · 买一浮盈达盈亏比×权利金后全平 · 不达标等到期") +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
"</div>"
|
"</div>"
|
||||||
);
|
);
|
||||||
@@ -1496,20 +1509,14 @@
|
|||||||
if (!est) return;
|
if (!est) return;
|
||||||
const inp = row.querySelector(".opt-pos-target-input");
|
const inp = row.querySelector(".opt-pos-target-input");
|
||||||
const typed = inp ? String(inp.value || "").trim() : "";
|
const typed = inp ? String(inp.value || "").trim() : "";
|
||||||
const armed = row.getAttribute("data-armed-target") || "";
|
const armed = row.getAttribute("data-armed-rr") || "";
|
||||||
const targetRaw = typed !== "" ? typed : armed;
|
const rrRaw = typed !== "" ? typed : armed;
|
||||||
if (targetRaw === "") {
|
if (rrRaw === "") {
|
||||||
est.className = "opt-target-est opt-target-est--idle";
|
est.className = "opt-target-est opt-target-est--idle";
|
||||||
est.innerHTML = "";
|
est.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const html = formatTargetEstimateHtml(
|
const html = formatRrEstimateHtml(rrRaw, row.getAttribute("data-prem"));
|
||||||
row.getAttribute("data-opt-type"),
|
|
||||||
row.getAttribute("data-strike"),
|
|
||||||
targetRaw,
|
|
||||||
row.getAttribute("data-eth"),
|
|
||||||
row.getAttribute("data-prem")
|
|
||||||
);
|
|
||||||
if (!html) {
|
if (!html) {
|
||||||
est.className = "opt-target-est opt-target-est--idle";
|
est.className = "opt-target-est opt-target-est--idle";
|
||||||
est.innerHTML = "";
|
est.innerHTML = "";
|
||||||
@@ -1641,9 +1648,9 @@
|
|||||||
const row = card ? card.querySelector(".opt-target-row") : null;
|
const row = card ? card.querySelector(".opt-target-row") : null;
|
||||||
const inp = card ? card.querySelector(".opt-pos-target-input") : null;
|
const inp = card ? card.querySelector(".opt-pos-target-input") : null;
|
||||||
const raw = inp ? String(inp.value || "").trim() : "";
|
const raw = inp ? String(inp.value || "").trim() : "";
|
||||||
const tgt = parseFloat(raw);
|
const rr = raw === "" ? 2 : parseFloat(raw);
|
||||||
if (!Number.isFinite(tgt) || tgt <= 0) {
|
if (!Number.isFinite(rr) || rr <= 0) {
|
||||||
alert("请输入有效目标指数价");
|
alert("请输入有效盈亏比(相对权利金,默认2)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (btn) btn.disabled = true;
|
if (btn) btn.disabled = true;
|
||||||
@@ -1651,17 +1658,16 @@
|
|||||||
const d = await apiJson("/api/options/target", {
|
const d = await apiJson("/api/options/target", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ inst_id: inst, target_index: tgt }),
|
body: JSON.stringify({ inst_id: inst, profit_rr: rr }),
|
||||||
});
|
});
|
||||||
if (!d.ok) {
|
if (!d.ok) {
|
||||||
alert(d.msg || "设定失败");
|
alert(d.msg || "设定失败");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
delete state.targetDraftByInst[inst];
|
delete state.targetDraftByInst[inst];
|
||||||
if (inp) inp.value = "";
|
if (inp) inp.value = String(rr);
|
||||||
if (row) {
|
if (row) {
|
||||||
row.setAttribute("data-armed-target", String(tgt));
|
row.setAttribute("data-armed-rr", String(rr));
|
||||||
updatePosTargetEstimate(row);
|
|
||||||
}
|
}
|
||||||
await refreshAllPositions();
|
await refreshAllPositions();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1701,12 +1707,22 @@
|
|||||||
}
|
}
|
||||||
box.hidden = false;
|
box.hidden = false;
|
||||||
host.innerHTML = rows.map(function (t) {
|
host.innerHTML = rows.map(function (t) {
|
||||||
const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
|
|
||||||
const managed = t.managed_by === "hedge_plan";
|
const managed = t.managed_by === "hedge_plan";
|
||||||
|
let rule;
|
||||||
|
if (t.profit_rr != null && Number(t.profit_rr) > 0) {
|
||||||
|
rule = "盈亏比 ×" + fmt(t.profit_rr, 2);
|
||||||
|
} else if (t.oo_profit_rr != null && Number(t.oo_profit_rr) > 0) {
|
||||||
|
rule = "盈亏比 ×" + fmt(t.oo_profit_rr, 2);
|
||||||
|
} else if (t.target_index != null && Number(t.target_index) > 0) {
|
||||||
|
const side = (t.opt_type || "").toUpperCase() === "P" ? "Put ≤" : "Call ≥";
|
||||||
|
rule = side + " " + fmt(t.target_index, 1);
|
||||||
|
} else {
|
||||||
|
rule = "委托中";
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
'<div class="opt-target-mon-item' + (managed ? " opt-target-mon-item--managed" : "") + '">' +
|
'<div class="opt-target-mon-item' + (managed ? " opt-target-mon-item--managed" : "") + '">' +
|
||||||
'<code class="opt-target-mon-inst" title="' + (t.inst_id || "") + '">' + (t.inst_id || "") + "</code>" +
|
'<code class="opt-target-mon-inst" title="' + (t.inst_id || "") + '">' + (t.inst_id || "") + "</code>" +
|
||||||
'<span class="opt-target-mon-rule">' + side + " " + fmt(t.target_index, 1) + "</span>" +
|
'<span class="opt-target-mon-rule">' + rule + "</span>" +
|
||||||
(managed
|
(managed
|
||||||
? '<span class="opt-target-mon-managed">对冲计划 #' + (t.plan_id || "") + " · 进行中</span>"
|
? '<span class="opt-target-mon-managed">对冲计划 #' + (t.plan_id || "") + " · 进行中</span>"
|
||||||
: '<button type="button" class="btn-secondary opt-target-mon-cancel" data-inst="' + (t.inst_id || "") + '">取消</button>') +
|
: '<button type="button" class="btn-secondary opt-target-mon-cancel" data-inst="' + (t.inst_id || "") + '">取消</button>') +
|
||||||
@@ -1887,20 +1903,23 @@
|
|||||||
paintPositions(list);
|
paintPositions(list);
|
||||||
const fromPos = list.reduce(function (targets, p) {
|
const fromPos = list.reduce(function (targets, p) {
|
||||||
if (!p) return targets;
|
if (!p) return targets;
|
||||||
if (p.target_index != null) {
|
if (p.profit_rr != null || p.target_index != null) {
|
||||||
targets.push({
|
targets.push({
|
||||||
id: p.target_monitor_id,
|
id: p.target_monitor_id,
|
||||||
inst_id: p.inst_id,
|
inst_id: p.inst_id,
|
||||||
opt_type: p.opt_type,
|
opt_type: p.opt_type,
|
||||||
target_index: p.target_index,
|
target_index: p.target_index,
|
||||||
|
profit_rr: p.profit_rr,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const hedgeTarget = p.hedge_plan_target;
|
const hedgeTarget = p.hedge_plan_target;
|
||||||
if (hedgeTarget && hedgeTarget.target_index != null) {
|
if (hedgeTarget) {
|
||||||
targets.push({
|
targets.push({
|
||||||
inst_id: p.inst_id,
|
inst_id: p.inst_id,
|
||||||
opt_type: p.opt_type || hedgeTarget.opt_type,
|
opt_type: p.opt_type || hedgeTarget.opt_type,
|
||||||
target_index: hedgeTarget.target_index,
|
target_index: hedgeTarget.target_index,
|
||||||
|
oo_profit_rr: hedgeTarget.oo_profit_rr,
|
||||||
|
profit_rr: hedgeTarget.oo_profit_rr,
|
||||||
plan_id: hedgeTarget.plan_id,
|
plan_id: hedgeTarget.plan_id,
|
||||||
managed_by: hedgeTarget.managed_by,
|
managed_by: hedgeTarget.managed_by,
|
||||||
});
|
});
|
||||||
@@ -2286,17 +2305,17 @@
|
|||||||
}
|
}
|
||||||
bindOrderDialogChrome();
|
bindOrderDialogChrome();
|
||||||
|
|
||||||
["opt-sheets-amount", "opt-eth-amount", "opt-target-idx"].forEach(function (id) {
|
["opt-sheets-amount", "opt-eth-amount", "opt-profit-rr"].forEach(function (id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
el.addEventListener("change", function () {
|
el.addEventListener("change", function () {
|
||||||
if (id === "opt-target-idx") {
|
if (id === "opt-profit-rr") {
|
||||||
updateEstimatedProfit();
|
updateEstimatedProfit();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.selectedInst) selectContract(state.selectedInst, null, true);
|
if (state.selectedInst) selectContract(state.selectedInst, null, true);
|
||||||
});
|
});
|
||||||
if (id === "opt-target-idx") {
|
if (id === "opt-profit-rr") {
|
||||||
el.addEventListener("input", updateEstimatedProfit);
|
el.addEventListener("input", updateEstimatedProfit);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -219,38 +219,42 @@
|
|||||||
const hint = closeGateHint(closePreview);
|
const hint = closeGateHint(closePreview);
|
||||||
return hint ? '<div class="muted opt-bid-invalid-hint">' + hint + "</div>" : "";
|
return hint ? '<div class="muted opt-bid-invalid-hint">' + hint + "</div>" : "";
|
||||||
})() +
|
})() +
|
||||||
(p.target_index != null
|
(p.profit_rr != null || p.target_index != null
|
||||||
? (function () {
|
? (function () {
|
||||||
const eth = p.eth_amount != null ? Number(p.eth_amount)
|
const hedgeTarget = p.hedge_plan_target || null;
|
||||||
: (Number(p.pos) > 0 ? Number(p.pos) * Number(p.ct_mult || 0.01) : null);
|
const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan";
|
||||||
const strike = Number(p.strike);
|
const rr =
|
||||||
const tgt = Number(p.target_index);
|
managed && hedgeTarget.oo_profit_rr != null
|
||||||
|
? Number(hedgeTarget.oo_profit_rr)
|
||||||
|
: p.profit_rr != null
|
||||||
|
? Number(p.profit_rr)
|
||||||
|
: null;
|
||||||
const prem = Number(p.premium_paid);
|
const prem = Number(p.premium_paid);
|
||||||
let profit = null;
|
let profit = null;
|
||||||
let value = null;
|
let need = null;
|
||||||
if (Number.isFinite(tgt) && Number.isFinite(strike) && eth > 0) {
|
if (rr != null && Number.isFinite(rr) && rr > 0 && Number.isFinite(prem) && prem > 0) {
|
||||||
const o = String(p.opt_type || "").toUpperCase();
|
profit = Math.round(prem * rr * 100) / 100;
|
||||||
const intrinsic = o === "C" ? Math.max(0, tgt - strike) : o === "P" ? Math.max(0, strike - tgt) : null;
|
need = Math.round((prem + profit) * 100) / 100;
|
||||||
if (intrinsic != null) {
|
|
||||||
value = Math.round(intrinsic * eth * 100) / 100;
|
|
||||||
if (!hidePnl && Number.isFinite(prem)) profit = Math.round((value - prem) * 100) / 100;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC");
|
const profitTxt = profit == null ? "—" : ((profit > 0 ? "+" : "") + fmtUsdc(profit) + " USDC");
|
||||||
const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : "";
|
const profitCls = profit > 0 ? " pnl-pos" : profit < 0 ? " pnl-neg" : "";
|
||||||
const hedgeTarget = p.hedge_plan_target || null;
|
|
||||||
const managed = hedgeTarget && hedgeTarget.managed_by === "hedge_plan";
|
|
||||||
const profitSpan = hidePnl
|
const profitSpan = hidePnl
|
||||||
? ""
|
? ""
|
||||||
: '<span class="pos-value' + profitCls + '">预估盈利 ' + profitTxt + "</span>";
|
: '<span class="pos-value' + profitCls + '">目标盈利 ' + profitTxt + "</span>";
|
||||||
|
const ruleTxt =
|
||||||
|
rr != null && Number.isFinite(rr) && rr > 0
|
||||||
|
? "盈亏比 ×" + fmt(rr, 2)
|
||||||
|
: p.target_index != null
|
||||||
|
? "目标 " + fmt(p.target_index, 1)
|
||||||
|
: "委托中";
|
||||||
return (
|
return (
|
||||||
'<div class="opt-target-row opt-target-row--ro' + (managed ? " opt-target-row--managed" : "") + '">' +
|
'<div class="opt-target-row opt-target-row--ro' + (managed ? " opt-target-row--managed" : "") + '">' +
|
||||||
'<span class="opt-target-row-label">' + (managed ? "对冲计划 #" + hedgeTarget.plan_id : "委托") + "</span>" +
|
'<span class="opt-target-row-label">' + (managed ? "对冲计划 #" + hedgeTarget.plan_id : "委托") + "</span>" +
|
||||||
'<span class="pos-value">目标 ' + fmt(p.target_index, 1) + "</span>" +
|
'<span class="pos-value">' + ruleTxt + "</span>" +
|
||||||
'<span class="pos-value">价值 ' + (value == null ? "—" : fmtUsdc(value) + " USDC") + "</span>" +
|
(need != null ? '<span class="pos-value">需回收 ' + fmtUsdc(need) + " USDC</span>" : "") +
|
||||||
profitSpan +
|
profitSpan +
|
||||||
'<span class="muted opt-target-row-hint">' +
|
'<span class="muted opt-target-row-hint">' +
|
||||||
(managed ? "进行中 · 由对冲计划监控,到位后仅平盈利腿" : "监控中 · 到位按买一限价平") +
|
(managed ? "进行中 · 由对冲计划监控" : "监控中 · 买一浮盈达盈亏比后全平") +
|
||||||
"</span></div>"
|
"</span></div>"
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -123,21 +123,31 @@ def _resolve_options_source(conn, inst_id: str) -> tuple[str, str, int | None]:
|
|||||||
|
|
||||||
def _format_options_target(p: dict[str, Any]) -> str:
|
def _format_options_target(p: dict[str, Any]) -> str:
|
||||||
hedge = p.get("hedge_plan_target") if isinstance(p.get("hedge_plan_target"), dict) else None
|
hedge = p.get("hedge_plan_target") if isinstance(p.get("hedge_plan_target"), dict) else None
|
||||||
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
|
|
||||||
if hedge:
|
if hedge:
|
||||||
ot = str(hedge.get("opt_type") or opt_type).upper()
|
rr = _safe_float(hedge.get("oo_profit_rr") or hedge.get("profit_rr"))
|
||||||
|
pid = hedge.get("plan_id")
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
return f"对冲#{pid} 盈亏比×{rr:g}" if pid is not None else f"盈亏比×{rr:g}"
|
||||||
|
ot = str(hedge.get("opt_type") or p.get("opt_type") or p.get("optType") or "").upper()
|
||||||
side = "Put ≤" if ot == "P" else "Call ≥"
|
side = "Put ≤" if ot == "P" else "Call ≥"
|
||||||
tgt = _safe_float(hedge.get("target_index"))
|
tgt = _safe_float(hedge.get("target_index"))
|
||||||
pid = hedge.get("plan_id")
|
|
||||||
if tgt is not None:
|
if tgt is not None:
|
||||||
return f"对冲#{pid} {side} {tgt:g}" if pid is not None else f"{side} {tgt:g}"
|
return f"对冲#{pid} {side} {tgt:g}" if pid is not None else f"{side} {tgt:g}"
|
||||||
|
mon = p.get("target_monitor") if isinstance(p.get("target_monitor"), dict) else None
|
||||||
|
rr = _safe_float(p.get("profit_rr"))
|
||||||
|
if rr is None and mon:
|
||||||
|
rr = _safe_float(mon.get("profit_rr"))
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
return f"盈亏比×{rr:g}"
|
||||||
tgt = _safe_float(p.get("target_index"))
|
tgt = _safe_float(p.get("target_index"))
|
||||||
|
if tgt is None and mon:
|
||||||
|
tgt = _safe_float(mon.get("target_index"))
|
||||||
if tgt is not None and tgt > 0:
|
if tgt is not None and tgt > 0:
|
||||||
|
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
|
||||||
side = "Put ≤" if opt_type == "P" else "Call ≥"
|
side = "Put ≤" if opt_type == "P" else "Call ≥"
|
||||||
return f"{side} {tgt:g}"
|
return f"{side} {tgt:g}"
|
||||||
return "—"
|
return "—"
|
||||||
|
|
||||||
|
|
||||||
def _format_options_item(p: dict[str, Any], *, conn=None) -> dict[str, Any]:
|
def _format_options_item(p: dict[str, Any], *, conn=None) -> dict[str, Any]:
|
||||||
inst = str(p.get("inst_id") or p.get("instId") or "-").strip() or "-"
|
inst = str(p.get("inst_id") or p.get("instId") or "-").strip() or "-"
|
||||||
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
|
opt_type = str(p.get("opt_type") or p.get("optType") or "").upper()
|
||||||
|
|||||||
@@ -72,11 +72,14 @@ def fetch_light_option_positions_for_dashboard(cfg: dict[str, Any]) -> list[dict
|
|||||||
mon = tgt_map.get(str(row.get("inst_id") or ""))
|
mon = tgt_map.get(str(row.get("inst_id") or ""))
|
||||||
if mon:
|
if mon:
|
||||||
row["target_index"] = mon.get("target_index")
|
row["target_index"] = mon.get("target_index")
|
||||||
|
row["profit_rr"] = mon.get("profit_rr")
|
||||||
row["target_monitor_id"] = mon.get("id")
|
row["target_monitor_id"] = mon.get("id")
|
||||||
row["target_monitor"] = mon
|
row["target_monitor"] = mon
|
||||||
hedge_target = hedge_target_map.get(str(row.get("inst_id") or ""))
|
hedge_target = hedge_target_map.get(str(row.get("inst_id") or ""))
|
||||||
if hedge_target:
|
if hedge_target:
|
||||||
row["hedge_plan_target"] = hedge_target
|
row["hedge_plan_target"] = hedge_target
|
||||||
|
if hedge_target.get("oo_profit_rr") is not None and row.get("profit_rr") is None:
|
||||||
|
row["profit_rr"] = hedge_target.get("oo_profit_rr")
|
||||||
if not mon:
|
if not mon:
|
||||||
row["target_index"] = hedge_target.get("target_index")
|
row["target_index"] = hedge_target.get("target_index")
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|||||||
@@ -38,14 +38,15 @@ def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]:
|
|||||||
mon = tgt_map.get(str(p.get("inst_id") or ""))
|
mon = tgt_map.get(str(p.get("inst_id") or ""))
|
||||||
if mon:
|
if mon:
|
||||||
p["target_index"] = mon.get("target_index")
|
p["target_index"] = mon.get("target_index")
|
||||||
|
p["profit_rr"] = mon.get("profit_rr")
|
||||||
p["target_monitor_id"] = mon.get("id")
|
p["target_monitor_id"] = mon.get("id")
|
||||||
p["target_monitor"] = mon
|
p["target_monitor"] = mon
|
||||||
hedge_target = hedge_target_map.get(str(p.get("inst_id") or ""))
|
hedge_target = hedge_target_map.get(str(p.get("inst_id") or ""))
|
||||||
if hedge_target:
|
if hedge_target:
|
||||||
p["hedge_plan_target"] = hedge_target
|
p["hedge_plan_target"] = hedge_target
|
||||||
if not mon:
|
if not mon:
|
||||||
# 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。
|
|
||||||
p["target_index"] = hedge_target.get("target_index")
|
p["target_index"] = hedge_target.get("target_index")
|
||||||
|
p["profit_rr"] = hedge_target.get("oo_profit_rr")
|
||||||
try:
|
try:
|
||||||
from lib.instance.instance_dashboard_lib import (
|
from lib.instance.instance_dashboard_lib import (
|
||||||
_format_options_target,
|
_format_options_target,
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ def options_monitor_loop(
|
|||||||
conn,
|
conn,
|
||||||
positions,
|
positions,
|
||||||
close_fn=target_close_fn,
|
close_fn=target_close_fn,
|
||||||
|
bid_fn=ticker_bid_fn,
|
||||||
send_wechat=send_wechat,
|
send_wechat=send_wechat,
|
||||||
account_label=account_label,
|
account_label=account_label,
|
||||||
cfg={"send_wechat": send_wechat, "account_label": account_label},
|
cfg={"send_wechat": send_wechat, "account_label": account_label},
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ def build_options_open_message(
|
|||||||
premium_paid: Any = None,
|
premium_paid: Any = None,
|
||||||
open_quote: Any = None,
|
open_quote: Any = None,
|
||||||
target_index: Any = None,
|
target_index: Any = None,
|
||||||
|
profit_rr: Any = None,
|
||||||
signal_note: str = "",
|
signal_note: str = "",
|
||||||
trade_id: Any = None,
|
trade_id: Any = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -73,7 +74,12 @@ def build_options_open_message(
|
|||||||
f"权利金:{_fmt(premium_paid)} USDC",
|
f"权利金:{_fmt(premium_paid)} USDC",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if target_index is not None and str(target_index).strip() != "":
|
if profit_rr is not None and str(profit_rr).strip() != "":
|
||||||
|
try:
|
||||||
|
lines.append(f"盈亏比:×{float(profit_rr):g}(达标全平;不达标等到期)")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
lines.append(f"盈亏比:{profit_rr}")
|
||||||
|
elif target_index is not None and str(target_index).strip() != "":
|
||||||
try:
|
try:
|
||||||
lines.append(f"目标指数:{float(target_index):g}")
|
lines.append(f"目标指数:{float(target_index):g}")
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@@ -96,6 +102,7 @@ def build_options_close_message(
|
|||||||
realized_pnl: Any = None,
|
realized_pnl: Any = None,
|
||||||
close_quote: Any = None,
|
close_quote: Any = None,
|
||||||
target_index: Any = None,
|
target_index: Any = None,
|
||||||
|
profit_rr: Any = None,
|
||||||
trigger_idx: Any = None,
|
trigger_idx: Any = None,
|
||||||
trade_id: Any = None,
|
trade_id: Any = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -116,7 +123,12 @@ def build_options_close_message(
|
|||||||
f"实现盈亏:{_fmt(realized_pnl, 4)} USDC",
|
f"实现盈亏:{_fmt(realized_pnl, 4)} USDC",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if target_index is not None and str(target_index).strip() != "":
|
if profit_rr is not None and str(profit_rr).strip() != "":
|
||||||
|
try:
|
||||||
|
lines.append(f"盈亏比:×{float(profit_rr):g}")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
lines.append(f"盈亏比:{profit_rr}")
|
||||||
|
elif target_index is not None and str(target_index).strip() != "":
|
||||||
try:
|
try:
|
||||||
lines.append(f"目标指数:{float(target_index):g}")
|
lines.append(f"目标指数:{float(target_index):g}")
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
@@ -141,6 +153,7 @@ def notify_options_open(
|
|||||||
premium_paid: Any = None,
|
premium_paid: Any = None,
|
||||||
open_quote: Any = None,
|
open_quote: Any = None,
|
||||||
target_index: Any = None,
|
target_index: Any = None,
|
||||||
|
profit_rr: Any = None,
|
||||||
signal_note: str = "",
|
signal_note: str = "",
|
||||||
) -> bool:
|
) -> bool:
|
||||||
ensure_options_notify_columns(conn) if conn is not None else None
|
ensure_options_notify_columns(conn) if conn is not None else None
|
||||||
@@ -160,6 +173,7 @@ def notify_options_open(
|
|||||||
premium_paid=premium_paid,
|
premium_paid=premium_paid,
|
||||||
open_quote=open_quote,
|
open_quote=open_quote,
|
||||||
target_index=target_index,
|
target_index=target_index,
|
||||||
|
profit_rr=profit_rr,
|
||||||
signal_note=signal_note,
|
signal_note=signal_note,
|
||||||
trade_id=trade_id,
|
trade_id=trade_id,
|
||||||
)
|
)
|
||||||
@@ -196,6 +210,7 @@ def notify_options_close(
|
|||||||
realized_pnl: Any = None,
|
realized_pnl: Any = None,
|
||||||
close_quote: Any = None,
|
close_quote: Any = None,
|
||||||
target_index: Any = None,
|
target_index: Any = None,
|
||||||
|
profit_rr: Any = None,
|
||||||
trigger_idx: Any = None,
|
trigger_idx: Any = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@@ -257,6 +272,7 @@ def notify_options_close(
|
|||||||
realized_pnl=total_pnl,
|
realized_pnl=total_pnl,
|
||||||
close_quote=close_quote if close_quote is not None else head.get("close_quote"),
|
close_quote=close_quote if close_quote is not None else head.get("close_quote"),
|
||||||
target_index=target_index,
|
target_index=target_index,
|
||||||
|
profit_rr=profit_rr,
|
||||||
trigger_idx=trigger_idx,
|
trigger_idx=trigger_idx,
|
||||||
trade_id=head.get("id") if len(rows) == 1 else None,
|
trade_id=head.get("id") if len(rows) == 1 else None,
|
||||||
)
|
)
|
||||||
@@ -286,6 +302,7 @@ def notify_options_close(
|
|||||||
realized_pnl=realized_pnl,
|
realized_pnl=realized_pnl,
|
||||||
close_quote=close_quote,
|
close_quote=close_quote,
|
||||||
target_index=target_index,
|
target_index=target_index,
|
||||||
|
profit_rr=profit_rr,
|
||||||
trigger_idx=trigger_idx,
|
trigger_idx=trigger_idx,
|
||||||
trade_id=trade_id,
|
trade_id=trade_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -646,6 +646,17 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
mode = (data.get("mode") or "budget_full").strip()
|
mode = (data.get("mode") or "budget_full").strip()
|
||||||
signal_note = (data.get("signal_note") or "").strip()
|
signal_note = (data.get("signal_note") or "").strip()
|
||||||
target_index = None
|
target_index = None
|
||||||
|
profit_rr = None
|
||||||
|
raw_rr = data.get("profit_rr")
|
||||||
|
if raw_rr is None or str(raw_rr).strip() == "":
|
||||||
|
raw_rr = data.get("oo_profit_rr")
|
||||||
|
if raw_rr is not None and str(raw_rr).strip() != "":
|
||||||
|
try:
|
||||||
|
profit_rr = float(raw_rr)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({"ok": False, "msg": "盈亏比无效"})
|
||||||
|
if profit_rr <= 0:
|
||||||
|
return jsonify({"ok": False, "msg": "盈亏比须大于 0"})
|
||||||
raw_target = data.get("target_index")
|
raw_target = data.get("target_index")
|
||||||
if raw_target is not None and str(raw_target).strip() != "":
|
if raw_target is not None and str(raw_target).strip() != "":
|
||||||
try:
|
try:
|
||||||
@@ -654,6 +665,9 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
return jsonify({"ok": False, "msg": "目标位无效"})
|
return jsonify({"ok": False, "msg": "目标位无效"})
|
||||||
if target_index <= 0:
|
if target_index <= 0:
|
||||||
return jsonify({"ok": False, "msg": "目标位无效"})
|
return jsonify({"ok": False, "msg": "目标位无效"})
|
||||||
|
# 未显式传目标时默认盈亏比 2
|
||||||
|
if profit_rr is None and target_index is None:
|
||||||
|
profit_rr = 2.0
|
||||||
if not inst_id:
|
if not inst_id:
|
||||||
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
return jsonify({"ok": False, "msg": "缺少 inst_id"})
|
||||||
q = cfg["quote_option_contract"](ex, inst_id)
|
q = cfg["quote_option_contract"](ex, inst_id)
|
||||||
@@ -820,13 +834,14 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
trade_id = int(cur.lastrowid)
|
trade_id = int(cur.lastrowid)
|
||||||
if target_index is not None:
|
if profit_rr is not None or target_index is not None:
|
||||||
from lib.options.options_target_lib import upsert_target_monitor
|
from lib.options.options_target_lib import upsert_target_monitor
|
||||||
|
|
||||||
target_mon = upsert_target_monitor(
|
target_mon = upsert_target_monitor(
|
||||||
conn,
|
conn,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
target_index=target_index,
|
target_index=target_index,
|
||||||
|
profit_rr=profit_rr,
|
||||||
underlying=u,
|
underlying=u,
|
||||||
opt_type=str(opt_type) if opt_type else None,
|
opt_type=str(opt_type) if opt_type else None,
|
||||||
trade_id=trade_id,
|
trade_id=trade_id,
|
||||||
@@ -854,6 +869,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
premium_paid=sizing.get("total_premium"),
|
premium_paid=sizing.get("total_premium"),
|
||||||
open_quote=fill_px,
|
open_quote=fill_px,
|
||||||
target_index=target_index,
|
target_index=target_index,
|
||||||
|
profit_rr=profit_rr,
|
||||||
signal_note=signal_note,
|
signal_note=signal_note,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
@@ -969,11 +985,14 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
mon = tgt_map.get(inst)
|
mon = tgt_map.get(inst)
|
||||||
if mon:
|
if mon:
|
||||||
row["target_index"] = mon.get("target_index")
|
row["target_index"] = mon.get("target_index")
|
||||||
|
row["profit_rr"] = mon.get("profit_rr")
|
||||||
row["target_monitor_id"] = mon.get("id")
|
row["target_monitor_id"] = mon.get("id")
|
||||||
row["target_monitor"] = mon
|
row["target_monitor"] = mon
|
||||||
hedge_target = hedge_target_map.get(inst)
|
hedge_target = hedge_target_map.get(inst)
|
||||||
if hedge_target:
|
if hedge_target:
|
||||||
row["hedge_plan_target"] = hedge_target
|
row["hedge_plan_target"] = hedge_target
|
||||||
|
if hedge_target.get("oo_profit_rr") is not None:
|
||||||
|
row.setdefault("profit_rr", hedge_target.get("oo_profit_rr"))
|
||||||
try:
|
try:
|
||||||
from lib.instance.instance_dashboard_lib import _resolve_options_source
|
from lib.instance.instance_dashboard_lib import _resolve_options_source
|
||||||
|
|
||||||
@@ -1031,12 +1050,28 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
conn_h.close()
|
conn_h.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
return jsonify({"ok": False, "msg": f"对冲托管校验失败: {e}"})
|
||||||
try:
|
profit_rr = None
|
||||||
target_index = float(data.get("target_index"))
|
target_index = None
|
||||||
except (TypeError, ValueError):
|
raw_rr = data.get("profit_rr")
|
||||||
return jsonify({"ok": False, "msg": "目标位无效"})
|
if raw_rr is None or str(raw_rr).strip() == "":
|
||||||
if target_index <= 0:
|
raw_rr = data.get("oo_profit_rr")
|
||||||
return jsonify({"ok": False, "msg": "目标位无效"})
|
if raw_rr is not None and str(raw_rr).strip() != "":
|
||||||
|
try:
|
||||||
|
profit_rr = float(raw_rr)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({"ok": False, "msg": "盈亏比无效"})
|
||||||
|
if profit_rr <= 0:
|
||||||
|
return jsonify({"ok": False, "msg": "盈亏比须大于 0"})
|
||||||
|
raw_tgt = data.get("target_index")
|
||||||
|
if raw_tgt is not None and str(raw_tgt).strip() != "":
|
||||||
|
try:
|
||||||
|
target_index = float(raw_tgt)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return jsonify({"ok": False, "msg": "目标位无效"})
|
||||||
|
if target_index <= 0:
|
||||||
|
return jsonify({"ok": False, "msg": "目标位无效"})
|
||||||
|
if profit_rr is None and target_index is None:
|
||||||
|
profit_rr = 2.0
|
||||||
raw = cfg["fetch_option_positions"](ex)
|
raw = cfg["fetch_option_positions"](ex)
|
||||||
if raw is None:
|
if raw is None:
|
||||||
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
return jsonify({"ok": False, "msg": "获取期权持仓失败"})
|
||||||
@@ -1065,6 +1100,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
conn,
|
conn,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
target_index=target_index,
|
target_index=target_index,
|
||||||
|
profit_rr=profit_rr,
|
||||||
underlying=str(underlying) if underlying else None,
|
underlying=str(underlying) if underlying else None,
|
||||||
opt_type=str(opt_type) if opt_type else None,
|
opt_type=str(opt_type) if opt_type else None,
|
||||||
trade_id=trade_id,
|
trade_id=trade_id,
|
||||||
@@ -1411,7 +1447,24 @@ def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
raw = cfg["fetch_option_positions"](ex)
|
raw = cfg["fetch_option_positions"](ex)
|
||||||
if raw is None:
|
if raw is None:
|
||||||
return []
|
return []
|
||||||
return [cfg["format_position_row"](p) for p in raw]
|
rows = [cfg["format_position_row"](p) for p in raw]
|
||||||
|
try:
|
||||||
|
from lib.options.options_db import sum_open_premium_paid
|
||||||
|
|
||||||
|
conn = cfg["get_db"]()
|
||||||
|
try:
|
||||||
|
for row in rows:
|
||||||
|
inst = str(row.get("inst_id") or "")
|
||||||
|
if not inst:
|
||||||
|
continue
|
||||||
|
paid = sum_open_premium_paid(conn, inst)
|
||||||
|
if paid is not None:
|
||||||
|
row["premium_paid"] = paid
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return rows
|
||||||
|
|
||||||
def _sync(conn):
|
def _sync(conn):
|
||||||
from lib.exchange.okx_options_lib import fetch_option_position_history
|
from lib.exchange.okx_options_lib import fetch_option_position_history
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"""期权目标位委托:指数目标价仅用于监控触发;触发后按买一限价平仓(无止损,到期结算)."""
|
"""期权目标委托:盈亏比×权利金触发后按买一限价平仓(无止损,到期结算).
|
||||||
|
|
||||||
|
兼容旧「目标指数」委托:无 profit_rr 时仍按指数到位触发.
|
||||||
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from lib.options.options_db import init_options_tables
|
from lib.options.options_db import init_options_tables, sum_open_premium_paid
|
||||||
from lib.options.options_pricing_lib import close_ref_prices, fetch_option_mark_px
|
from lib.options.options_pricing_lib import close_ref_prices, fetch_option_mark_px
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +21,18 @@ def _safe_float(v: Any) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None:
|
||||||
|
rows = conn.execute(f"PRAGMA table_info({table})").fetchall()
|
||||||
|
names: set[str] = set()
|
||||||
|
for r in rows:
|
||||||
|
try:
|
||||||
|
names.add(str(r["name"]))
|
||||||
|
except (TypeError, KeyError, IndexError):
|
||||||
|
names.add(str(r[1]))
|
||||||
|
if col not in names:
|
||||||
|
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}")
|
||||||
|
|
||||||
|
|
||||||
def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]:
|
def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]:
|
||||||
from lib.exchange.okx_options_lib import option_fields_from_inst_id
|
from lib.exchange.okx_options_lib import option_fields_from_inst_id
|
||||||
|
|
||||||
@@ -63,21 +78,44 @@ def ensure_target_tables(conn: sqlite3.Connection) -> None:
|
|||||||
ON options_target_monitors(status)
|
ON options_target_monitors(status)
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
# 盈亏比=目标盈利/权利金;如 2=盈利 2 倍权利金.有值时优先生效,target_index 可置 0
|
||||||
|
_ensure_column(conn, "options_target_monitors", "profit_rr", "REAL")
|
||||||
|
|
||||||
|
|
||||||
def target_hit(*, opt_type: str | None, index_px: float, target_index: float) -> bool:
|
def target_hit(*, opt_type: str | None, index_px: float, target_index: float) -> bool:
|
||||||
"""Call:指数涨到/超过目标平仓;Put:指数跌到/低于目标平仓."""
|
"""旧逻辑:Call 指数≥目标;Put 指数≤目标."""
|
||||||
ot = (opt_type or "").strip().upper()
|
ot = (opt_type or "").strip().upper()
|
||||||
if ot == "P":
|
if ot == "P":
|
||||||
return index_px <= target_index
|
return index_px <= target_index
|
||||||
return index_px >= target_index
|
return index_px >= target_index
|
||||||
|
|
||||||
|
|
||||||
|
def profit_rr_hit(
|
||||||
|
*,
|
||||||
|
premium: float,
|
||||||
|
bid: float | None,
|
||||||
|
sheets: float,
|
||||||
|
ct_mult: float,
|
||||||
|
profit_rr: float,
|
||||||
|
) -> bool:
|
||||||
|
"""买一回收 − 权利金 ≥ 盈亏比 × 权利金."""
|
||||||
|
if premium <= 0 or profit_rr <= 0:
|
||||||
|
return False
|
||||||
|
if bid is None or float(bid) <= 0:
|
||||||
|
return False
|
||||||
|
if sheets <= 0 or ct_mult <= 0:
|
||||||
|
return False
|
||||||
|
recycle = float(bid) * float(sheets) * float(ct_mult)
|
||||||
|
pnl = recycle - float(premium)
|
||||||
|
return pnl + 1e-9 >= float(profit_rr) * float(premium)
|
||||||
|
|
||||||
|
|
||||||
def upsert_target_monitor(
|
def upsert_target_monitor(
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
*,
|
*,
|
||||||
inst_id: str,
|
inst_id: str,
|
||||||
target_index: float,
|
target_index: float | None = None,
|
||||||
|
profit_rr: float | None = None,
|
||||||
underlying: str | None = None,
|
underlying: str | None = None,
|
||||||
opt_type: str | None = None,
|
opt_type: str | None = None,
|
||||||
trade_id: int | None = None,
|
trade_id: int | None = None,
|
||||||
@@ -87,9 +125,18 @@ def upsert_target_monitor(
|
|||||||
inst_id = (inst_id or "").strip()
|
inst_id = (inst_id or "").strip()
|
||||||
if not inst_id:
|
if not inst_id:
|
||||||
return {"ok": False, "msg": "缺少 inst_id"}
|
return {"ok": False, "msg": "缺少 inst_id"}
|
||||||
if target_index is None or float(target_index) <= 0:
|
|
||||||
return {"ok": False, "msg": "目标位无效"}
|
rr = _safe_float(profit_rr)
|
||||||
target_index = float(target_index)
|
tgt = _safe_float(target_index)
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
tgt_store = float(tgt) if tgt is not None and tgt > 0 else 0.0
|
||||||
|
rr_store = float(rr)
|
||||||
|
elif tgt is not None and tgt > 0:
|
||||||
|
tgt_store = float(tgt)
|
||||||
|
rr_store = None
|
||||||
|
else:
|
||||||
|
return {"ok": False, "msg": "请填写盈亏比(相对权利金,默认2)"}
|
||||||
|
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT id FROM options_target_monitors
|
SELECT id FROM options_target_monitors
|
||||||
@@ -104,6 +151,7 @@ def upsert_target_monitor(
|
|||||||
"""
|
"""
|
||||||
UPDATE options_target_monitors
|
UPDATE options_target_monitors
|
||||||
SET target_index = ?,
|
SET target_index = ?,
|
||||||
|
profit_rr = ?,
|
||||||
underlying = COALESCE(?, underlying),
|
underlying = COALESCE(?, underlying),
|
||||||
opt_type = COALESCE(?, opt_type),
|
opt_type = COALESCE(?, opt_type),
|
||||||
trade_id = COALESCE(?, trade_id),
|
trade_id = COALESCE(?, trade_id),
|
||||||
@@ -115,14 +163,13 @@ def upsert_target_monitor(
|
|||||||
triggered_at = NULL
|
triggered_at = NULL
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
""",
|
""",
|
||||||
(target_index, underlying, opt_type, trade_id, sheets, int(row["id"])),
|
(tgt_store, rr_store, underlying, opt_type, trade_id, sheets, int(row["id"])),
|
||||||
)
|
)
|
||||||
mon_id = int(row["id"])
|
mon_id = int(row["id"])
|
||||||
# 同一合约其他进行中的委托取消,避免双轨触发重复推送
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE options_target_monitors
|
UPDATE options_target_monitors
|
||||||
SET status = 'cancelled', message = '被新目标位覆盖'
|
SET status = 'cancelled', message = '被新目标委托覆盖'
|
||||||
WHERE inst_id = ? AND id != ? AND status IN ('active', 'closing')
|
WHERE inst_id = ? AND id != ? AND status IN ('active', 'closing')
|
||||||
""",
|
""",
|
||||||
(inst_id, mon_id),
|
(inst_id, mon_id),
|
||||||
@@ -131,13 +178,21 @@ def upsert_target_monitor(
|
|||||||
cur = conn.execute(
|
cur = conn.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO options_target_monitors
|
INSERT INTO options_target_monitors
|
||||||
(inst_id, underlying, opt_type, target_index, trade_id, sheets, status)
|
(inst_id, underlying, opt_type, target_index, profit_rr, trade_id, sheets, status)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
|
||||||
""",
|
""",
|
||||||
(inst_id, underlying, opt_type, target_index, trade_id, sheets),
|
(inst_id, underlying, opt_type, tgt_store, rr_store, trade_id, sheets),
|
||||||
)
|
)
|
||||||
mon_id = int(cur.lastrowid)
|
mon_id = int(cur.lastrowid)
|
||||||
return {"ok": True, "id": mon_id, "inst_id": inst_id, "target_index": target_index}
|
out: dict[str, Any] = {
|
||||||
|
"ok": True,
|
||||||
|
"id": mon_id,
|
||||||
|
"inst_id": inst_id,
|
||||||
|
"target_index": tgt_store if tgt_store > 0 else None,
|
||||||
|
}
|
||||||
|
if rr_store is not None:
|
||||||
|
out["profit_rr"] = rr_store
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = None, monitor_id: int | None = None) -> int:
|
def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = None, monitor_id: int | None = None) -> int:
|
||||||
@@ -166,12 +221,19 @@ def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = Non
|
|||||||
|
|
||||||
|
|
||||||
def _row_to_target(r: sqlite3.Row) -> dict[str, Any]:
|
def _row_to_target(r: sqlite3.Row) -> dict[str, Any]:
|
||||||
|
tgt = _safe_float(r["target_index"])
|
||||||
|
rr = None
|
||||||
|
try:
|
||||||
|
rr = _safe_float(r["profit_rr"])
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
rr = None
|
||||||
return {
|
return {
|
||||||
"id": int(r["id"]),
|
"id": int(r["id"]),
|
||||||
"inst_id": r["inst_id"],
|
"inst_id": r["inst_id"],
|
||||||
"underlying": r["underlying"],
|
"underlying": r["underlying"],
|
||||||
"opt_type": r["opt_type"],
|
"opt_type": r["opt_type"],
|
||||||
"target_index": _safe_float(r["target_index"]),
|
"target_index": tgt if tgt is not None and tgt > 0 else None,
|
||||||
|
"profit_rr": rr if rr is not None and rr > 0 else None,
|
||||||
"trade_id": r["trade_id"],
|
"trade_id": r["trade_id"],
|
||||||
"sheets": r["sheets"],
|
"sheets": r["sheets"],
|
||||||
"status": r["status"],
|
"status": r["status"],
|
||||||
@@ -180,16 +242,16 @@ def _row_to_target(r: sqlite3.Row) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_TARGET_SELECT = (
|
||||||
|
"SELECT id, inst_id, underlying, opt_type, target_index, profit_rr, trade_id, sheets, "
|
||||||
|
"status, message, created_at FROM options_target_monitors"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_active_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
def list_active_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
||||||
ensure_target_tables(conn)
|
ensure_target_tables(conn)
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
f"{_TARGET_SELECT} WHERE status = 'active' ORDER BY id DESC"
|
||||||
SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets,
|
|
||||||
status, message, created_at
|
|
||||||
FROM options_target_monitors
|
|
||||||
WHERE status = 'active'
|
|
||||||
ORDER BY id DESC
|
|
||||||
"""
|
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [_row_to_target(r) for r in rows]
|
return [_row_to_target(r) for r in rows]
|
||||||
|
|
||||||
@@ -198,13 +260,7 @@ def list_closing_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]:
|
|||||||
"""已挂出平仓单、等待成交的目标(不再重复推送微信)."""
|
"""已挂出平仓单、等待成交的目标(不再重复推送微信)."""
|
||||||
ensure_target_tables(conn)
|
ensure_target_tables(conn)
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"""
|
f"{_TARGET_SELECT} WHERE status = 'closing' ORDER BY id DESC"
|
||||||
SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets,
|
|
||||||
status, message, created_at
|
|
||||||
FROM options_target_monitors
|
|
||||||
WHERE status = 'closing'
|
|
||||||
ORDER BY id DESC
|
|
||||||
"""
|
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [_row_to_target(r) for r in rows]
|
return [_row_to_target(r) for r in rows]
|
||||||
|
|
||||||
@@ -286,23 +342,23 @@ def close_option_by_bid_depth(
|
|||||||
inst_id,
|
inst_id,
|
||||||
sheets=sheets,
|
sheets=sheets,
|
||||||
require_recycle_gate=True,
|
require_recycle_gate=True,
|
||||||
signal_note="目标位平仓",
|
signal_note="盈亏比平仓",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _notify_target_close(
|
def _notify_target_close(
|
||||||
cfg: dict[str, Any] | None,
|
cfg: dict[str, Any] | None,
|
||||||
send_wechat: Callable[[str], None] | None,
|
send_wechat: Callable[[str], None] | None,
|
||||||
*,
|
*,
|
||||||
account_label: str,
|
account_label: str,
|
||||||
inst_id: str,
|
inst_id: str,
|
||||||
target: float,
|
target: float | None,
|
||||||
idx: float,
|
profit_rr: float | None,
|
||||||
|
idx: float | None,
|
||||||
result: dict[str, Any],
|
result: dict[str, Any],
|
||||||
conn: Any = None,
|
conn: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""目标位平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
|
"""目标平仓推送:优先走统一平仓必发(幂等);无 cfg 时回退旧文案."""
|
||||||
if result.get("fully_closed") or result.get("already_flat"):
|
if result.get("fully_closed") or result.get("already_flat"):
|
||||||
if cfg is not None:
|
if cfg is not None:
|
||||||
try:
|
try:
|
||||||
@@ -312,12 +368,13 @@ def _notify_target_close(
|
|||||||
cfg,
|
cfg,
|
||||||
conn,
|
conn,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
reason="目标位平仓",
|
reason="盈亏比平仓" if profit_rr else "目标位平仓",
|
||||||
sheets=result.get("submitted_sheets"),
|
sheets=result.get("submitted_sheets"),
|
||||||
premium_received=result.get("premium_received"),
|
premium_received=result.get("premium_received"),
|
||||||
close_quote=result.get("locked_bid_px") or result.get("bid"),
|
close_quote=result.get("locked_bid_px") or result.get("bid"),
|
||||||
target_index=target,
|
target_index=target,
|
||||||
trigger_idx=idx,
|
trigger_idx=idx,
|
||||||
|
profit_rr=profit_rr,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -325,14 +382,20 @@ def _notify_target_close(
|
|||||||
if not send_wechat:
|
if not send_wechat:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
if profit_rr is not None and profit_rr > 0:
|
||||||
|
rule = f"盈亏比×{profit_rr:g}"
|
||||||
|
elif target is not None:
|
||||||
|
rule = f"目标指数:{target:g}"
|
||||||
|
else:
|
||||||
|
rule = "目标委托"
|
||||||
send_wechat(
|
send_wechat(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"【OKX期权·目标位平仓】",
|
"【OKX期权·盈亏比平仓】" if profit_rr else "【OKX期权·目标位平仓】",
|
||||||
f"账户:{account_label}",
|
f"账户:{account_label}",
|
||||||
f"合约:{inst_id}",
|
f"合约:{inst_id}",
|
||||||
f"目标指数:{target:g}",
|
rule,
|
||||||
f"触发指数:{idx:g}",
|
f"触发指数:{idx:g}" if idx is not None else "触发指数:—",
|
||||||
f"提交张数:{result.get('submitted_sheets') or '—'}",
|
f"提交张数:{result.get('submitted_sheets') or '—'}",
|
||||||
f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else '—'} USDC",
|
f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else '—'} USDC",
|
||||||
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
|
f"状态:{'已全平' if (result.get('fully_closed') or result.get('already_flat')) else '挂单中/部分'}",
|
||||||
@@ -354,18 +417,77 @@ def _result_fully_done(result: dict[str, Any]) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _monitor_should_close(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
mon: dict[str, Any],
|
||||||
|
pos: dict[str, Any],
|
||||||
|
*,
|
||||||
|
bid_fn: Callable[[str], float | None] | None,
|
||||||
|
index_fn: Callable[[dict[str, Any]], float | None] | None,
|
||||||
|
) -> tuple[bool, float | None]:
|
||||||
|
"""返回 (是否触发, 当前指数)."""
|
||||||
|
inst_id = str(mon.get("inst_id") or "")
|
||||||
|
rr = _safe_float(mon.get("profit_rr"))
|
||||||
|
if index_fn is not None:
|
||||||
|
idx = index_fn(pos)
|
||||||
|
else:
|
||||||
|
idx = _safe_float(pos.get("idx_px") or pos.get("idxPx"))
|
||||||
|
|
||||||
|
if rr is not None and rr > 0:
|
||||||
|
premium = sum_open_premium_paid(conn, inst_id)
|
||||||
|
if premium is None or premium <= 0:
|
||||||
|
premium = _safe_float(pos.get("premium_paid"))
|
||||||
|
sheets = _safe_float(mon.get("sheets"))
|
||||||
|
if sheets is None or sheets <= 0:
|
||||||
|
sheets = _safe_float(pos.get("pos") or pos.get("avail_pos") or pos.get("availPos"))
|
||||||
|
ct = _safe_float(pos.get("ct_mult") or pos.get("ctMult")) or 0.01
|
||||||
|
bid = None
|
||||||
|
if bid_fn is not None:
|
||||||
|
try:
|
||||||
|
bid = bid_fn(inst_id)
|
||||||
|
except Exception:
|
||||||
|
bid = None
|
||||||
|
if bid is None:
|
||||||
|
bid = _safe_float(pos.get("bid_px") or pos.get("bidPx") or pos.get("bid"))
|
||||||
|
preview = pos.get("close_preview") if isinstance(pos.get("close_preview"), dict) else {}
|
||||||
|
if bid is None:
|
||||||
|
bid = _safe_float(preview.get("bid") or preview.get("best_bid"))
|
||||||
|
if premium is None or sheets is None:
|
||||||
|
return False, idx
|
||||||
|
return (
|
||||||
|
profit_rr_hit(
|
||||||
|
premium=float(premium),
|
||||||
|
bid=bid,
|
||||||
|
sheets=float(sheets),
|
||||||
|
ct_mult=float(ct),
|
||||||
|
profit_rr=float(rr),
|
||||||
|
),
|
||||||
|
idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
target = _safe_float(mon.get("target_index"))
|
||||||
|
if target is None or target <= 0 or idx is None:
|
||||||
|
return False, idx
|
||||||
|
opt_type = mon.get("opt_type") or pos.get("opt_type") or pos.get("optType")
|
||||||
|
return (
|
||||||
|
target_hit(opt_type=str(opt_type) if opt_type else None, index_px=idx, target_index=target),
|
||||||
|
idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_options_target_closes(
|
def run_options_target_closes(
|
||||||
conn: sqlite3.Connection,
|
conn: sqlite3.Connection,
|
||||||
positions: list[dict[str, Any]],
|
positions: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
close_fn: Callable[[str], dict[str, Any]],
|
close_fn: Callable[[str], dict[str, Any]],
|
||||||
index_fn: Callable[[dict[str, Any]], float | None] | None = None,
|
index_fn: Callable[[dict[str, Any]], float | None] | None = None,
|
||||||
|
bid_fn: Callable[[str], float | None] | None = None,
|
||||||
send_wechat: Callable[[str], None] | None = None,
|
send_wechat: Callable[[str], None] | None = None,
|
||||||
account_label: str = "OKX期权",
|
account_label: str = "OKX期权",
|
||||||
cfg: dict[str, Any] | None = None,
|
cfg: dict[str, Any] | None = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"""
|
"""
|
||||||
扫描 active 目标委托;指数到位后限价平仓.
|
扫描 active 目标委托;盈亏比达标(或旧指数到位)后限价平仓.
|
||||||
状态先 commit 再推微信,避免 sync 失败回滚导致同一笔反复推送.
|
状态先 commit 再推微信,避免 sync 失败回滚导致同一笔反复推送.
|
||||||
未完全成交进入 closing,仅重试平仓不再推送.
|
未完全成交进入 closing,仅重试平仓不再推送.
|
||||||
返回本次新触发(并推送)的条数.
|
返回本次新触发(并推送)的条数.
|
||||||
@@ -412,7 +534,7 @@ def run_options_target_closes(
|
|||||||
status="triggered",
|
status="triggered",
|
||||||
trigger_idx=idx,
|
trigger_idx=idx,
|
||||||
close_ord_id=result.get("close_ord_id"),
|
close_ord_id=result.get("close_ord_id"),
|
||||||
message="目标位限价平仓完成",
|
message="盈亏比限价平仓完成",
|
||||||
)
|
)
|
||||||
_commit_monitor(conn)
|
_commit_monitor(conn)
|
||||||
continue
|
continue
|
||||||
@@ -429,8 +551,7 @@ def run_options_target_closes(
|
|||||||
triggered = 0
|
triggered = 0
|
||||||
for mon in list_active_targets(conn):
|
for mon in list_active_targets(conn):
|
||||||
inst_id = str(mon.get("inst_id") or "")
|
inst_id = str(mon.get("inst_id") or "")
|
||||||
target = _safe_float(mon.get("target_index"))
|
if not inst_id:
|
||||||
if not inst_id or target is None:
|
|
||||||
continue
|
continue
|
||||||
if inst_id in hedge_managed:
|
if inst_id in hedge_managed:
|
||||||
mark_monitor(
|
mark_monitor(
|
||||||
@@ -444,17 +565,15 @@ def run_options_target_closes(
|
|||||||
pos = pos_by_inst.get(inst_id)
|
pos = pos_by_inst.get(inst_id)
|
||||||
if not pos:
|
if not pos:
|
||||||
continue
|
continue
|
||||||
if index_fn is not None:
|
should, idx = _monitor_should_close(
|
||||||
idx = index_fn(pos)
|
conn, mon, pos, bid_fn=bid_fn, index_fn=index_fn
|
||||||
else:
|
)
|
||||||
idx = _safe_float(pos.get("idx_px") or pos.get("idxPx"))
|
if not should:
|
||||||
if idx is None:
|
|
||||||
continue
|
|
||||||
opt_type = mon.get("opt_type") or pos.get("opt_type") or pos.get("optType")
|
|
||||||
if not target_hit(opt_type=str(opt_type) if opt_type else None, index_px=idx, target_index=target):
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result = close_fn(inst_id)
|
result = close_fn(inst_id)
|
||||||
|
rr = _safe_float(mon.get("profit_rr"))
|
||||||
|
target = _safe_float(mon.get("target_index"))
|
||||||
if result.get("already_flat"):
|
if result.get("already_flat"):
|
||||||
mark_monitor(conn, int(mon["id"]), status="expired", trigger_idx=idx, message="持仓已平")
|
mark_monitor(conn, int(mon["id"]), status="expired", trigger_idx=idx, message="持仓已平")
|
||||||
_commit_monitor(conn)
|
_commit_monitor(conn)
|
||||||
@@ -472,15 +591,19 @@ def run_options_target_closes(
|
|||||||
|
|
||||||
done = _result_fully_done(result)
|
done = _result_fully_done(result)
|
||||||
status = "triggered" if done else "closing"
|
status = "triggered" if done else "closing"
|
||||||
|
hit_msg = (
|
||||||
|
"盈亏比达标限价平仓"
|
||||||
|
if (rr is not None and rr > 0)
|
||||||
|
else "目标位触发限价平仓"
|
||||||
|
)
|
||||||
mark_monitor(
|
mark_monitor(
|
||||||
conn,
|
conn,
|
||||||
int(mon["id"]),
|
int(mon["id"]),
|
||||||
status=status,
|
status=status,
|
||||||
trigger_idx=idx,
|
trigger_idx=idx,
|
||||||
close_ord_id=result.get("close_ord_id"),
|
close_ord_id=result.get("close_ord_id"),
|
||||||
message="目标位触发限价平仓" if done else "目标位已挂买一限价,等待成交",
|
message=hit_msg if done else "已挂买一限价,等待成交",
|
||||||
)
|
)
|
||||||
# 关键:先落库,再推送——否则后续 sync 异常回滚会让同一笔反复推微信
|
|
||||||
_commit_monitor(conn)
|
_commit_monitor(conn)
|
||||||
triggered += 1
|
triggered += 1
|
||||||
_notify_target_close(
|
_notify_target_close(
|
||||||
@@ -489,6 +612,7 @@ def run_options_target_closes(
|
|||||||
account_label=account_label,
|
account_label=account_label,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
target=target,
|
target=target,
|
||||||
|
profit_rr=rr,
|
||||||
idx=idx,
|
idx=idx,
|
||||||
result=result,
|
result=result,
|
||||||
conn=conn,
|
conn=conn,
|
||||||
|
|||||||
@@ -107,17 +107,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="options-estimate-row">
|
<div class="options-estimate-row">
|
||||||
<div class="opt-est-main">
|
<div class="opt-est-main">
|
||||||
<label class="btn-secondary opt-order-chip" for="opt-target-idx">目标位(指数)</label>
|
<label class="btn-secondary opt-order-chip" for="opt-profit-rr" title="目标盈利=盈亏比×权利金;例2=赚满2倍权利金后全平">盈亏比</label>
|
||||||
<input type="number" id="opt-target-idx" class="opt-target-idx" step="0.1" min="0" placeholder="达价限价平仓"
|
<input type="number" id="opt-profit-rr" class="opt-target-idx" step="0.1" min="0.1" value="2" placeholder="默认2"
|
||||||
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
autocomplete="off" inputmode="decimal" data-lpignore="true" data-1p-ignore="true" data-form-type="other">
|
||||||
<span class="k">预计价值</span>
|
<span class="k">目标盈利</span>
|
||||||
<span id="opt-est-value" class="v">—</span>
|
|
||||||
<span class="k">盈利</span>
|
|
||||||
<span id="opt-est-profit" class="v">—</span>
|
<span id="opt-est-profit" class="v">—</span>
|
||||||
<span class="k">目标杠杆</span>
|
<span class="k">需回收</span>
|
||||||
<span id="opt-est-leverage" class="v" title="目标位名义价值÷权利金">—</span>
|
<span id="opt-est-value" class="v" title="权利金+目标盈利">—</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="muted opt-est-note">目标价=监控指数;到位后按买一限价平仓;无止损,到期即止损</span>
|
<span class="muted opt-est-note">按买一浮盈达盈亏比×权利金后限价全平;不达标等到期;无止损</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row options-order-mode-row">
|
<div class="form-row options-order-mode-row">
|
||||||
<div class="opt-size-mode-bar">
|
<div class="opt-size-mode-bar">
|
||||||
@@ -324,4 +322,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
<script src="/static/options_expiry_countdown.js?v=1"></script>
|
||||||
<script src="/static/options_panel.js?v=54"></script>
|
<script src="/static/options_panel.js?v=55"></script>
|
||||||
|
|||||||
@@ -3930,6 +3930,19 @@
|
|||||||
|
|
||||||
function renderOptionsTargetCell(target) {
|
function renderOptionsTargetCell(target) {
|
||||||
if (!target) return "<td>—</td>";
|
if (!target) return "<td>—</td>";
|
||||||
|
const rr =
|
||||||
|
target.profit_rr != null
|
||||||
|
? Number(target.profit_rr)
|
||||||
|
: target.oo_profit_rr != null
|
||||||
|
? Number(target.oo_profit_rr)
|
||||||
|
: null;
|
||||||
|
if (rr != null && Number.isFinite(rr) && rr > 0) {
|
||||||
|
const txt = `盈亏比×${fmt(rr, 2)}`;
|
||||||
|
if (target.managed_by === "hedge_plan") {
|
||||||
|
return `<td class="hub-opt-target-cell is-on is-hedge" title="由对冲计划监控">对冲#${esc(target.plan_id)} ${esc(txt)}</td>`;
|
||||||
|
}
|
||||||
|
return `<td class="hub-opt-target-cell is-on" title="盈亏比监控">${esc(txt)}</td>`;
|
||||||
|
}
|
||||||
const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
|
const side = String(target.opt_type || "").toUpperCase() === "P" ? "Put≤" : "Call≥";
|
||||||
const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
|
const px = target.target_index != null ? fmt(target.target_index, 1) : "—";
|
||||||
if (target.managed_by === "hedge_plan") {
|
if (target.managed_by === "hedge_plan") {
|
||||||
@@ -3942,7 +3955,7 @@
|
|||||||
if (!pos.length) return '<div class="empty-hint hub-slot-pos">暂无期权持仓</div>';
|
if (!pos.length) return '<div class="empty-hint hub-slot-pos">暂无期权持仓</div>';
|
||||||
const showPnl = showAccountPnlPref();
|
const showPnl = showAccountPnlPref();
|
||||||
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
|
let html = '<div class="table-wrap hub-options-table-wrap"><table class="hub-options-table"><thead><tr>';
|
||||||
html += "<th>合约</th><th>类型</th><th>张数</th><th>到期倒计时</th><th>目标监控</th>";
|
html += "<th>合约</th><th>类型</th><th>张数</th><th>到期倒计时</th><th>盈亏比</th>";
|
||||||
if (showPnl) html += "<th>净盈亏</th><th>收益率</th>";
|
if (showPnl) html += "<th>净盈亏</th><th>收益率</th>";
|
||||||
html += "</tr></thead><tbody>";
|
html += "</tr></thead><tbody>";
|
||||||
pos.forEach((p) => {
|
pos.forEach((p) => {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
<span class="plan-radio-row" id="plan-create-direction"></span>
|
<span class="plan-radio-row" id="plan-create-direction"></span>
|
||||||
</label>
|
</label>
|
||||||
<label class="plan-field">
|
<label class="plan-field">
|
||||||
<span>目标位</span>
|
<span>盈亏比</span>
|
||||||
<input id="plan-create-target" type="text" placeholder="如 68500" autocomplete="off" data-lpignore="true" data-1p-ignore="true" data-form-type="other" />
|
<input id="plan-create-target" type="text" placeholder="如 68500" autocomplete="off" data-lpignore="true" data-1p-ignore="true" data-form-type="other" />
|
||||||
</label>
|
</label>
|
||||||
<label class="plan-field">
|
<label class="plan-field">
|
||||||
@@ -1765,8 +1765,8 @@
|
|||||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||||
<script src="/assets/time_close_ui.js?v=3"></script>
|
<script src="/assets/time_close_ui.js?v=3"></script>
|
||||||
<script src="/assets/options_expiry_countdown.js?v=1"></script>
|
<script src="/assets/options_expiry_countdown.js?v=1"></script>
|
||||||
<script src="/assets/options_position_cards.js?v=4"></script>
|
<script src="/assets/options_position_cards.js?v=5"></script>
|
||||||
<script src="/assets/backup.js?v=1"></script>
|
<script src="/assets/backup.js?v=1"></script>
|
||||||
<script src="/assets/app.js?v=20260807-opt-float"></script>
|
<script src="/assets/app.js?v=20260811-opt-rr"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""期权目标位委托单元测试."""
|
"""期权目标委托单元测试(盈亏比 + 旧指数兼容)."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -8,6 +8,7 @@ from lib.options.options_target_lib import (
|
|||||||
ensure_target_tables,
|
ensure_target_tables,
|
||||||
list_active_targets,
|
list_active_targets,
|
||||||
list_closing_targets,
|
list_closing_targets,
|
||||||
|
profit_rr_hit,
|
||||||
run_options_target_closes,
|
run_options_target_closes,
|
||||||
target_hit,
|
target_hit,
|
||||||
upsert_target_monitor,
|
upsert_target_monitor,
|
||||||
@@ -21,7 +22,67 @@ class OptionsTargetLibTests(unittest.TestCase):
|
|||||||
self.assertTrue(target_hit(opt_type="P", index_px=1800, target_index=1850))
|
self.assertTrue(target_hit(opt_type="P", index_px=1800, target_index=1850))
|
||||||
self.assertFalse(target_hit(opt_type="P", index_px=1900, target_index=1850))
|
self.assertFalse(target_hit(opt_type="P", index_px=1900, target_index=1850))
|
||||||
|
|
||||||
def test_upsert_and_trigger_close(self):
|
def test_profit_rr_hit(self):
|
||||||
|
# premium=10, rr=2 → need pnl≥20 → recycle≥30 → bid*sheets*ct ≥30
|
||||||
|
self.assertTrue(
|
||||||
|
profit_rr_hit(premium=10, bid=30, sheets=1, ct_mult=1, profit_rr=2)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
profit_rr_hit(premium=10, bid=29.9, sheets=1, ct_mult=1, profit_rr=2)
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
profit_rr_hit(premium=10, bid=None, sheets=1, ct_mult=1, profit_rr=2)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_upsert_rr_and_trigger_close(self):
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
ensure_target_tables(conn)
|
||||||
|
out = upsert_target_monitor(
|
||||||
|
conn,
|
||||||
|
inst_id="ETH-USD_UM-260717-1900-C",
|
||||||
|
profit_rr=2,
|
||||||
|
opt_type="C",
|
||||||
|
sheets=1,
|
||||||
|
)
|
||||||
|
self.assertTrue(out["ok"])
|
||||||
|
self.assertEqual(out.get("profit_rr"), 2.0)
|
||||||
|
self.assertEqual(len(list_active_targets(conn)), 1)
|
||||||
|
|
||||||
|
closed = []
|
||||||
|
|
||||||
|
def close_fn(inst_id: str):
|
||||||
|
closed.append(inst_id)
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"submitted_sheets": 1,
|
||||||
|
"premium_received": 30.0,
|
||||||
|
"close_ord_id": "oid1",
|
||||||
|
"fully_closed": True,
|
||||||
|
"remaining_sheets": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
# bid=30, ct=1 → pnl=20 ≥ 2*10; premium 来自持仓字段
|
||||||
|
n = run_options_target_closes(
|
||||||
|
conn,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"inst_id": "ETH-USD_UM-260717-1900-C",
|
||||||
|
"idx_px": 1885,
|
||||||
|
"opt_type": "C",
|
||||||
|
"pos": 1,
|
||||||
|
"ct_mult": 1,
|
||||||
|
"premium_paid": 10,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
close_fn=close_fn,
|
||||||
|
bid_fn=lambda _i: 30.0,
|
||||||
|
)
|
||||||
|
self.assertEqual(n, 1)
|
||||||
|
self.assertEqual(closed, ["ETH-USD_UM-260717-1900-C"])
|
||||||
|
self.assertEqual(len(list_active_targets(conn)), 0)
|
||||||
|
|
||||||
|
def test_upsert_and_trigger_close_legacy_index(self):
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
ensure_target_tables(conn)
|
ensure_target_tables(conn)
|
||||||
@@ -107,7 +168,6 @@ class OptionsTargetLibTests(unittest.TestCase):
|
|||||||
self.assertEqual(len(list_active_targets(conn)), 0)
|
self.assertEqual(len(list_active_targets(conn)), 0)
|
||||||
self.assertEqual(len(list_closing_targets(conn)), 1)
|
self.assertEqual(len(list_closing_targets(conn)), 1)
|
||||||
|
|
||||||
# 模拟后续 sync 异常也不会再推:closing 重试静默
|
|
||||||
n2 = run_options_target_closes(
|
n2 = run_options_target_closes(
|
||||||
conn,
|
conn,
|
||||||
pos,
|
pos,
|
||||||
@@ -120,7 +180,6 @@ class OptionsTargetLibTests(unittest.TestCase):
|
|||||||
self.assertEqual(len(list_closing_targets(conn)), 0)
|
self.assertEqual(len(list_closing_targets(conn)), 0)
|
||||||
|
|
||||||
def test_commit_before_wechat_survives_later_rollback(self):
|
def test_commit_before_wechat_survives_later_rollback(self):
|
||||||
"""状态在推送前已 commit,外层异常回滚不应让委托回到 active."""
|
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
ensure_target_tables(conn)
|
ensure_target_tables(conn)
|
||||||
@@ -149,12 +208,10 @@ class OptionsTargetLibTests(unittest.TestCase):
|
|||||||
close_fn=close_fn,
|
close_fn=close_fn,
|
||||||
send_wechat=notices.append,
|
send_wechat=notices.append,
|
||||||
)
|
)
|
||||||
# 模拟 loop 后续 sync 抛错后 close 未再 commit —— 但 status 已提前 commit
|
|
||||||
conn.rollback()
|
conn.rollback()
|
||||||
self.assertEqual(len(notices), 1)
|
self.assertEqual(len(notices), 1)
|
||||||
self.assertEqual(len(list_active_targets(conn)), 0)
|
self.assertEqual(len(list_active_targets(conn)), 0)
|
||||||
|
|
||||||
# 下一轮不应再次触发推送
|
|
||||||
n2 = run_options_target_closes(
|
n2 = run_options_target_closes(
|
||||||
conn,
|
conn,
|
||||||
[{"inst_id": "ETH-USD_UM-260715-1870-P", "idx_px": 1860, "opt_type": "P"}],
|
[{"inst_id": "ETH-USD_UM-260715-1870-P", "idx_px": 1860, "opt_type": "P"}],
|
||||||
|
|||||||
Reference in New Issue
Block a user