fix: OKX options buy/close orders with tick alignment and reduceOnly
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2390,4 +2390,15 @@ html[data-theme="light"] .settings-export-link {
|
|||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
#opt-order-msg.opt-error,
|
||||||
|
.opt-error {
|
||||||
|
color: #ff6b6b;
|
||||||
|
}
|
||||||
|
.opt-row-actions {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.opt-row-actions .btn-primary,
|
||||||
|
.opt-row-actions .btn-secondary {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,13 @@
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scrollToOrderPanel() {
|
||||||
|
const panel = document.getElementById("opt-order-panel");
|
||||||
|
if (panel && panel.style.display !== "none") {
|
||||||
|
panel.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshBalances() {
|
async function refreshBalances() {
|
||||||
const d = await apiJson("/api/options/balances");
|
const d = await apiJson("/api/options/balances");
|
||||||
if (!d.ok) return;
|
if (!d.ok) return;
|
||||||
@@ -81,7 +88,10 @@
|
|||||||
"<td><code>" + c.inst_id + "</code></td>" +
|
"<td><code>" + c.inst_id + "</code></td>" +
|
||||||
"<td>" + fmt(c.ask, 4) + "</td>" +
|
"<td>" + fmt(c.ask, 4) + "</td>" +
|
||||||
"<td>" + fmt(c.bid, 4) + "</td>" +
|
"<td>" + fmt(c.bid, 4) + "</td>" +
|
||||||
'<td><button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button></td>';
|
'<td class="opt-row-actions">' +
|
||||||
|
'<button type="button" class="btn-secondary opt-pick-btn" data-inst="' + c.inst_id + '">选择</button> ' +
|
||||||
|
'<button type="button" class="btn-primary opt-buy-btn" data-inst="' + c.inst_id + '">买入</button>' +
|
||||||
|
"</td>";
|
||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
|
tbody.querySelectorAll(".opt-pick-btn").forEach(function (btn) {
|
||||||
@@ -89,6 +99,12 @@
|
|||||||
selectContract(btn.getAttribute("data-inst"));
|
selectContract(btn.getAttribute("data-inst"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
tbody.querySelectorAll(".opt-buy-btn").forEach(function (btn) {
|
||||||
|
btn.addEventListener("click", async function () {
|
||||||
|
await selectContract(btn.getAttribute("data-inst"));
|
||||||
|
await openPosition();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectContract(instId) {
|
async function selectContract(instId) {
|
||||||
@@ -108,7 +124,19 @@
|
|||||||
document.getElementById("opt-order-sheets").textContent = sz.sheets != null ? sz.sheets : "—";
|
document.getElementById("opt-order-sheets").textContent = sz.sheets != null ? sz.sheets : "—";
|
||||||
document.getElementById("opt-order-eth").textContent = sz.eth_amount != null ? sz.eth_amount : "—";
|
document.getElementById("opt-order-eth").textContent = sz.eth_amount != null ? sz.eth_amount : "—";
|
||||||
document.getElementById("opt-order-premium").textContent = sz.total_premium != null ? fmt(sz.total_premium, 4) + " USDC" : "—";
|
document.getElementById("opt-order-premium").textContent = sz.total_premium != null ? fmt(sz.total_premium, 4) + " USDC" : "—";
|
||||||
document.getElementById("opt-order-msg").textContent = sz.ok === false ? (sz.msg || "") : "";
|
const msgEl = document.getElementById("opt-order-msg");
|
||||||
|
if (!d.ok) {
|
||||||
|
msgEl.textContent = d.msg || "报价失败";
|
||||||
|
msgEl.classList.add("opt-error");
|
||||||
|
} else if (sz.ok === false) {
|
||||||
|
msgEl.textContent = sz.msg || "";
|
||||||
|
msgEl.classList.add("opt-error");
|
||||||
|
} else {
|
||||||
|
msgEl.textContent = "";
|
||||||
|
msgEl.classList.remove("opt-error");
|
||||||
|
}
|
||||||
|
scrollToOrderPanel();
|
||||||
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChain() {
|
async function loadChain() {
|
||||||
@@ -123,25 +151,69 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openPosition() {
|
async function openPosition() {
|
||||||
if (!state.selectedInst) return;
|
if (!state.selectedInst) {
|
||||||
const mode = document.querySelector('input[name="opt-size-mode"]:checked').value;
|
alert("请先选择合约");
|
||||||
const body = {
|
return;
|
||||||
inst_id: state.selectedInst,
|
|
||||||
mode: mode,
|
|
||||||
signal_note: document.getElementById("opt-signal-note").value || "",
|
|
||||||
};
|
|
||||||
if (mode === "eth_amount") {
|
|
||||||
body.eth_amount = parseFloat(document.getElementById("opt-eth-amount").value);
|
|
||||||
}
|
}
|
||||||
const d = await apiJson("/api/options/open", {
|
const btn = document.getElementById("opt-open-btn");
|
||||||
method: "POST",
|
btn.disabled = true;
|
||||||
headers: { "Content-Type": "application/json" },
|
try {
|
||||||
body: JSON.stringify(body),
|
const mode = document.querySelector('input[name="opt-size-mode"]:checked').value;
|
||||||
});
|
const body = {
|
||||||
document.getElementById("opt-order-msg").textContent = d.ok ? "下单已提交" : (d.msg || "失败");
|
inst_id: state.selectedInst,
|
||||||
if (d.ok) {
|
mode: mode,
|
||||||
refreshBalances();
|
signal_note: document.getElementById("opt-signal-note").value || "",
|
||||||
|
};
|
||||||
|
if (mode === "eth_amount") {
|
||||||
|
body.eth_amount = parseFloat(document.getElementById("opt-eth-amount").value);
|
||||||
|
}
|
||||||
|
const d = await apiJson("/api/options/open", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
const msgEl = document.getElementById("opt-order-msg");
|
||||||
|
msgEl.textContent = d.ok ? "下单已提交,可在 OKX 委托中查看" : (d.msg || "失败");
|
||||||
|
msgEl.classList.toggle("opt-error", !d.ok);
|
||||||
|
if (d.ok) {
|
||||||
|
refreshBalances();
|
||||||
|
refreshPositions();
|
||||||
|
} else {
|
||||||
|
alert(d.msg || "下单失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closePosition(inst, btn) {
|
||||||
|
const q = await apiJson("/api/options/quote?inst_id=" + encodeURIComponent(inst) + "&mode=budget_full");
|
||||||
|
if (!q.ok) {
|
||||||
|
alert(q.msg || "获取买一价失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bid = q.bid;
|
||||||
|
if (bid == null || bid <= 0) {
|
||||||
|
alert("暂无买一价,请稍后在 OKX App 平仓或等盘口恢复");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!confirm("限价卖出 @ 买一 " + fmt(bid, 4) + "(每 1 ETH/BTC)?\n合约:" + inst)) return;
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const r = await apiJson("/api/options/close", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ inst_id: inst }),
|
||||||
|
});
|
||||||
|
if (r.ok) {
|
||||||
|
alert("平仓单已提交" + (r.bid != null ? " @ " + fmt(r.bid, 4) : ""));
|
||||||
|
} else {
|
||||||
|
alert(r.msg || "平仓失败");
|
||||||
|
}
|
||||||
refreshPositions();
|
refreshPositions();
|
||||||
|
refreshBalances();
|
||||||
|
} finally {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,17 +240,8 @@
|
|||||||
tbody.appendChild(tr);
|
tbody.appendChild(tr);
|
||||||
});
|
});
|
||||||
tbody.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
tbody.querySelectorAll(".opt-close-btn").forEach(function (btn) {
|
||||||
btn.addEventListener("click", async function () {
|
btn.addEventListener("click", function () {
|
||||||
const inst = btn.getAttribute("data-inst");
|
closePosition(btn.getAttribute("data-inst"), btn);
|
||||||
if (!confirm("确认限价卖出 @ 买一?")) return;
|
|
||||||
const r = await apiJson("/api/options/close", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ inst_id: inst }),
|
|
||||||
});
|
|
||||||
alert(r.ok ? "平仓单已提交" : (r.msg || "失败"));
|
|
||||||
refreshPositions();
|
|
||||||
refreshBalances();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+125
-13
@@ -1,6 +1,7 @@
|
|||||||
"""OKX USDⓈ 期权 API 封装(主账户 exchange_options 专用)。"""
|
"""OKX USDⓈ 期权 API 封装(主账户 exchange_options 专用)。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
@@ -38,6 +39,57 @@ def _safe_float(v: Any) -> float | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def round_option_px(px: float, tick_sz: Any, side: str) -> float:
|
||||||
|
"""按 OKX tickSz 对齐:买入向上取整,卖出向下取整。"""
|
||||||
|
tick = _safe_float(tick_sz)
|
||||||
|
if tick is None or tick <= 0 or px <= 0:
|
||||||
|
return px
|
||||||
|
steps = px / tick
|
||||||
|
side_l = (side or "").lower()
|
||||||
|
if side_l == "buy":
|
||||||
|
return math.ceil(steps - 1e-12) * tick
|
||||||
|
return math.floor(steps + 1e-12) * tick
|
||||||
|
|
||||||
|
|
||||||
|
def format_option_px(px: float, tick_sz: Any) -> str:
|
||||||
|
tick = _safe_float(tick_sz)
|
||||||
|
if tick is None or tick <= 0:
|
||||||
|
return str(px)
|
||||||
|
decimals = max(0, -int(round(math.log10(tick)))) if tick < 1 else 0
|
||||||
|
if tick >= 1:
|
||||||
|
decimals = len(str(tick).split(".")[-1]) if "." in str(tick) else 0
|
||||||
|
return f"{px:.{decimals}f}".rstrip("0").rstrip(".") or "0"
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_book_bid_ask(ex: ccxt.okx, inst_id: str) -> tuple[float | None, float | None]:
|
||||||
|
try:
|
||||||
|
rows = ex.public_get_market_books({"instId": inst_id, "sz": "1"}).get("data") or []
|
||||||
|
if not rows:
|
||||||
|
return None, None
|
||||||
|
row = rows[0]
|
||||||
|
asks = row.get("asks") or []
|
||||||
|
bids = row.get("bids") or []
|
||||||
|
ask = _safe_float(asks[0][0]) if asks else None
|
||||||
|
bid = _safe_float(bids[0][0]) if bids else None
|
||||||
|
return bid, ask
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _pos_side_from_position(pos: dict[str, Any] | None) -> str | None:
|
||||||
|
if not pos:
|
||||||
|
return None
|
||||||
|
ps = str(pos.get("posSide") or "").strip().lower()
|
||||||
|
if ps in ("long", "short", "net"):
|
||||||
|
return ps
|
||||||
|
sheets = _safe_float(pos.get("pos")) or 0.0
|
||||||
|
if sheets > 0:
|
||||||
|
return "long"
|
||||||
|
if sheets < 0:
|
||||||
|
return "short"
|
||||||
|
return "net"
|
||||||
|
|
||||||
|
|
||||||
def _extract_ccy_balance(balance: dict[str, Any], ccy: str) -> float | None:
|
def _extract_ccy_balance(balance: dict[str, Any], ccy: str) -> float | None:
|
||||||
ccy = (ccy or "").upper()
|
ccy = (ccy or "").upper()
|
||||||
if not isinstance(balance, dict):
|
if not isinstance(balance, dict):
|
||||||
@@ -200,15 +252,29 @@ def quote_option_contract(ex: ccxt.okx, inst_id: str) -> dict[str, Any]:
|
|||||||
meta = meta_rows[0]
|
meta = meta_rows[0]
|
||||||
t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
|
t_rows = ex.public_get_market_ticker({"instId": inst_id}).get("data") or []
|
||||||
t = t_rows[0] if t_rows else {}
|
t = t_rows[0] if t_rows else {}
|
||||||
|
ask = _safe_float(t.get("askPx"))
|
||||||
|
bid = _safe_float(t.get("bidPx"))
|
||||||
|
if ask is None or bid is None:
|
||||||
|
book_bid, book_ask = _fetch_book_bid_ask(ex, inst_id)
|
||||||
|
if ask is None:
|
||||||
|
ask = book_ask
|
||||||
|
if bid is None:
|
||||||
|
bid = book_bid
|
||||||
|
mark = _safe_float(t.get("markPx"))
|
||||||
|
tick_sz = meta.get("tickSz")
|
||||||
|
if ask is None and mark is not None:
|
||||||
|
ask = round_option_px(mark, tick_sz, "buy")
|
||||||
|
if bid is None and mark is not None:
|
||||||
|
bid = round_option_px(mark, tick_sz, "sell")
|
||||||
uly = str(meta.get("uly") or "")
|
uly = str(meta.get("uly") or "")
|
||||||
idx = fetch_index_price(ex, uly)
|
idx = fetch_index_price(ex, uly)
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"inst_id": inst_id,
|
"inst_id": inst_id,
|
||||||
"meta": meta,
|
"meta": meta,
|
||||||
"ask": _safe_float(t.get("askPx")),
|
"ask": ask,
|
||||||
"bid": _safe_float(t.get("bidPx")),
|
"bid": bid,
|
||||||
"mark": _safe_float(t.get("markPx")),
|
"mark": mark,
|
||||||
"index_px": idx,
|
"index_px": idx,
|
||||||
"ct_mult": _safe_float(meta.get("ctMult")) or 0.01,
|
"ct_mult": _safe_float(meta.get("ctMult")) or 0.01,
|
||||||
"min_sz": int(_safe_float(meta.get("minSz")) or 1),
|
"min_sz": int(_safe_float(meta.get("minSz")) or 1),
|
||||||
@@ -227,23 +293,69 @@ def place_option_limit_order(
|
|||||||
sheets: int,
|
sheets: int,
|
||||||
price: float,
|
price: float,
|
||||||
td_mode: str = "cross",
|
td_mode: str = "cross",
|
||||||
|
tick_sz: Any = None,
|
||||||
|
reduce_only: bool = False,
|
||||||
|
pos_side: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
side_l = (side or "").lower()
|
side_l = (side or "").lower()
|
||||||
if side_l not in ("buy", "sell"):
|
if side_l not in ("buy", "sell"):
|
||||||
return {"ok": False, "msg": "side 必须为 buy 或 sell"}
|
return {"ok": False, "msg": "side 必须为 buy 或 sell"}
|
||||||
if sheets < 1:
|
if sheets < 1:
|
||||||
return {"ok": False, "msg": "张数至少为 1"}
|
return {"ok": False, "msg": "张数至少为 1"}
|
||||||
|
px = round_option_px(float(price), tick_sz, side_l)
|
||||||
|
if px <= 0:
|
||||||
|
return {"ok": False, "msg": "价格无效"}
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"instId": inst_id,
|
||||||
|
"tdMode": td_mode,
|
||||||
|
"side": side_l,
|
||||||
|
"ordType": "limit",
|
||||||
|
"px": format_option_px(px, tick_sz),
|
||||||
|
"sz": str(int(sheets)),
|
||||||
|
}
|
||||||
|
if pos_side:
|
||||||
|
body["posSide"] = pos_side
|
||||||
|
if reduce_only:
|
||||||
|
body["reduceOnly"] = True
|
||||||
try:
|
try:
|
||||||
resp = ex.private_post_trade_order(
|
resp = ex.private_post_trade_order(body)
|
||||||
{
|
data = (resp or {}).get("data") or []
|
||||||
"instId": inst_id,
|
if data and str(data[0].get("sCode")) == "0":
|
||||||
"tdMode": td_mode,
|
return {"ok": True, "data": data[0], "raw": resp, "px": px}
|
||||||
"side": side_l,
|
msg = data[0].get("sMsg") if data else str(resp)
|
||||||
"ordType": "limit",
|
return {"ok": False, "msg": msg or "下单失败", "raw": resp, "px": px}
|
||||||
"px": str(price),
|
except Exception as e:
|
||||||
"sz": str(int(sheets)),
|
return {"ok": False, "msg": str(e), "px": px}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
def place_option_market_order(
|
||||||
|
ex: ccxt.okx,
|
||||||
|
*,
|
||||||
|
inst_id: str,
|
||||||
|
side: str,
|
||||||
|
sheets: int,
|
||||||
|
td_mode: str = "cross",
|
||||||
|
reduce_only: bool = False,
|
||||||
|
pos_side: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
side_l = (side or "").lower()
|
||||||
|
if side_l not in ("buy", "sell"):
|
||||||
|
return {"ok": False, "msg": "side 必须为 buy 或 sell"}
|
||||||
|
if sheets < 1:
|
||||||
|
return {"ok": False, "msg": "张数至少为 1"}
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"instId": inst_id,
|
||||||
|
"tdMode": td_mode,
|
||||||
|
"side": side_l,
|
||||||
|
"ordType": "market",
|
||||||
|
"sz": str(int(sheets)),
|
||||||
|
}
|
||||||
|
if pos_side:
|
||||||
|
body["posSide"] = pos_side
|
||||||
|
if reduce_only:
|
||||||
|
body["reduceOnly"] = True
|
||||||
|
try:
|
||||||
|
resp = ex.private_post_trade_order(body)
|
||||||
data = (resp or {}).get("data") or []
|
data = (resp or {}).get("data") or []
|
||||||
if data and str(data[0].get("sCode")) == "0":
|
if data and str(data[0].get("sCode")) == "0":
|
||||||
return {"ok": True, "data": data[0], "raw": resp}
|
return {"ok": True, "data": data[0], "raw": resp}
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None
|
|||||||
|
|
||||||
def _build_cfg(app_module: Any) -> dict[str, Any]:
|
def _build_cfg(app_module: Any) -> dict[str, Any]:
|
||||||
from lib.exchange.okx_options_lib import (
|
from lib.exchange.okx_options_lib import (
|
||||||
|
_pos_side_from_position,
|
||||||
|
_safe_float,
|
||||||
build_option_chain,
|
build_option_chain,
|
||||||
estimate_usdt_to_usdc,
|
estimate_usdt_to_usdc,
|
||||||
execute_convert,
|
execute_convert,
|
||||||
@@ -67,6 +69,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
|||||||
format_position_row,
|
format_position_row,
|
||||||
options_api_ready,
|
options_api_ready,
|
||||||
place_option_limit_order,
|
place_option_limit_order,
|
||||||
|
place_option_market_order,
|
||||||
quote_option_contract,
|
quote_option_contract,
|
||||||
transfer_ccy,
|
transfer_ccy,
|
||||||
)
|
)
|
||||||
@@ -91,6 +94,7 @@ def _build_cfg(app_module: Any) -> dict[str, Any]:
|
|||||||
"build_option_chain": build_option_chain,
|
"build_option_chain": build_option_chain,
|
||||||
"quote_option_contract": quote_option_contract,
|
"quote_option_contract": quote_option_contract,
|
||||||
"place_option_limit_order": place_option_limit_order,
|
"place_option_limit_order": place_option_limit_order,
|
||||||
|
"place_option_market_order": place_option_market_order,
|
||||||
"fetch_option_positions": fetch_option_positions,
|
"fetch_option_positions": fetch_option_positions,
|
||||||
"fetch_options_balances": fetch_options_balances,
|
"fetch_options_balances": fetch_options_balances,
|
||||||
"format_position_row": format_position_row,
|
"format_position_row": format_position_row,
|
||||||
@@ -220,6 +224,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
if not sizing.get("ok"):
|
if not sizing.get("ok"):
|
||||||
return jsonify({"ok": False, "msg": sizing.get("msg") or "张数计算失败", "sizing": sizing})
|
return jsonify({"ok": False, "msg": sizing.get("msg") or "张数计算失败", "sizing": sizing})
|
||||||
sheets = int(sizing["sheets"])
|
sheets = int(sizing["sheets"])
|
||||||
|
tick_sz = q.get("tick_sz")
|
||||||
order = cfg["place_option_limit_order"](
|
order = cfg["place_option_limit_order"](
|
||||||
ex,
|
ex,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
@@ -227,6 +232,7 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
sheets=sheets,
|
sheets=sheets,
|
||||||
price=float(ask),
|
price=float(ask),
|
||||||
td_mode=cfg["td_mode"],
|
td_mode=cfg["td_mode"],
|
||||||
|
tick_sz=tick_sz,
|
||||||
)
|
)
|
||||||
if not order.get("ok"):
|
if not order.get("ok"):
|
||||||
return jsonify(order)
|
return jsonify(order)
|
||||||
@@ -291,38 +297,43 @@ def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None:
|
|||||||
pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None)
|
pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None)
|
||||||
if not pos:
|
if not pos:
|
||||||
return jsonify({"ok": False, "msg": "未找到持仓"})
|
return jsonify({"ok": False, "msg": "未找到持仓"})
|
||||||
avail = float(pos.get("availPos") or pos.get("pos") or 0)
|
avail = _safe_float(pos.get("availPos"))
|
||||||
close_sheets = int(sheets) if sheets else int(abs(avail))
|
if avail is None or avail <= 0:
|
||||||
|
avail = abs(_safe_float(pos.get("pos")) or 0)
|
||||||
|
close_sheets = int(sheets) if sheets else int(avail)
|
||||||
if close_sheets < 1:
|
if close_sheets < 1:
|
||||||
return jsonify({"ok": False, "msg": "可平张数不足"})
|
return jsonify({"ok": False, "msg": "可平张数不足"})
|
||||||
|
td_mode = str(pos.get("mgnMode") or cfg["td_mode"])
|
||||||
|
pos_side = _pos_side_from_position(pos) or "net"
|
||||||
|
tick_sz = q.get("tick_sz")
|
||||||
if use_market:
|
if use_market:
|
||||||
try:
|
order = cfg["place_option_market_order"](
|
||||||
resp = ex.private_post_trade_order(
|
ex,
|
||||||
{
|
inst_id=inst_id,
|
||||||
"instId": inst_id,
|
side="sell",
|
||||||
"tdMode": cfg["td_mode"],
|
sheets=close_sheets,
|
||||||
"side": "sell",
|
td_mode=td_mode,
|
||||||
"ordType": "market",
|
reduce_only=True,
|
||||||
"sz": str(close_sheets),
|
pos_side=pos_side,
|
||||||
}
|
)
|
||||||
)
|
if not order.get("ok"):
|
||||||
data_rows = (resp or {}).get("data") or []
|
return jsonify(order)
|
||||||
if not data_rows or str(data_rows[0].get("sCode")) != "0":
|
|
||||||
return jsonify({"ok": False, "msg": data_rows[0].get("sMsg") if data_rows else "市价平仓失败"})
|
|
||||||
order = {"ok": True, "data": data_rows[0]}
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"ok": False, "msg": str(e)})
|
|
||||||
else:
|
else:
|
||||||
|
close_px = float(bid)
|
||||||
order = cfg["place_option_limit_order"](
|
order = cfg["place_option_limit_order"](
|
||||||
ex,
|
ex,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
side="sell",
|
side="sell",
|
||||||
sheets=close_sheets,
|
sheets=close_sheets,
|
||||||
price=float(bid),
|
price=close_px,
|
||||||
td_mode=cfg["td_mode"],
|
td_mode=td_mode,
|
||||||
|
tick_sz=tick_sz,
|
||||||
|
reduce_only=True,
|
||||||
|
pos_side=pos_side,
|
||||||
)
|
)
|
||||||
if not order.get("ok"):
|
if not order.get("ok"):
|
||||||
return jsonify(order)
|
return jsonify(order)
|
||||||
|
bid = order.get("px", close_px)
|
||||||
prem_recv = total_premium(float(bid or 0), close_sheets * float(q.get("ct_mult") or 0.01))
|
prem_recv = total_premium(float(bid or 0), close_sheets * float(q.get("ct_mult") or 0.01))
|
||||||
conn = cfg["get_db"]()
|
conn = cfg["get_db"]()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{% if not options_enabled %}
|
{% if not options_enabled %}
|
||||||
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code> 及主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code>。</div>
|
<div class="flash" style="margin-bottom:12px">期权 API 未启用:请在 <code>crypto_monitor_okx/.env</code> 设置 <code>OKX_OPTIONS_ENABLED=true</code> 及主账户 <code>OKX_OPTIONS_API_*</code>,然后 <code>pm2 restart crypto_okx --update-env</code>。</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="muted options-hint">资金账户兑换 USDT→USDC 后,划转到交易账户即可买入。报价单位为每 1 ETH/BTC;1 张 = 0.01 ETH/BTC。</p>
|
<p class="muted options-hint">资金账户兑换 USDT→USDC 后,划转到交易账户即可买入。报价单位为每 1 ETH/BTC;1 张 = 0.01 ETH/BTC。表格中点「买入」直接下单;或点「选择」后在下方确认张数再买入。</p>
|
||||||
|
|
||||||
<div class="options-funds-grid">
|
<div class="options-funds-grid">
|
||||||
<div class="options-funds-col">
|
<div class="options-funds-col">
|
||||||
@@ -128,4 +128,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/options_panel.js?v=1"></script>
|
<script src="/static/options_panel.js?v=2"></script>
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ from lib.options.options_pricing_lib import (
|
|||||||
sheets_from_eth_amount,
|
sheets_from_eth_amount,
|
||||||
total_premium,
|
total_premium,
|
||||||
)
|
)
|
||||||
|
from lib.exchange.okx_options_lib import format_option_px, round_option_px
|
||||||
|
|
||||||
|
|
||||||
|
def test_round_option_px():
|
||||||
|
assert round_option_px(14.9184, "0.2", "sell") == 14.8
|
||||||
|
assert round_option_px(14.81, "0.2", "buy") == 15.0
|
||||||
|
assert format_option_px(14.8, "0.2") == "14.8"
|
||||||
|
|
||||||
|
|
||||||
def test_premium_per_sheet():
|
def test_premium_per_sheet():
|
||||||
|
|||||||
Reference in New Issue
Block a user