Sell residual options at latest bid via IOC limit, not market.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1150,7 +1150,7 @@ class BinanceLiveExecutor(Matcher):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def try_close_one_residual(self, row: dict) -> dict | None:
|
def try_close_one_residual(self, row: dict) -> dict | None:
|
||||||
"""LIVE-BN:权利金达标后交易所市价卖出归档期权。"""
|
"""LIVE-BN:权利金达标后按最新买一 IOC 限价卖出归档期权(不扫市价)。"""
|
||||||
err = self._guard_live()
|
err = self._guard_live()
|
||||||
if err:
|
if err:
|
||||||
logger.warning("residual premium close blocked: %s", err)
|
logger.warning("residual premium close blocked: %s", err)
|
||||||
@@ -1175,17 +1175,25 @@ class BinanceLiveExecutor(Matcher):
|
|||||||
"residual premium close skip %s: bad contracts", row.get("group_id")
|
"residual premium close skip %s: bad contracts", row.get("group_id")
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
oq2 = self._quote_held_option(option_inst_id)
|
||||||
|
bid_px = float(oq2.bid) if oq2 is not None and oq2.bid is not None else float(close_bid)
|
||||||
|
if bid_px <= 0:
|
||||||
|
logger.debug(
|
||||||
|
"residual premium close skip %s: bid vanished", row.get("group_id")
|
||||||
|
)
|
||||||
|
return None
|
||||||
client = self._client()
|
client = self._client()
|
||||||
try:
|
try:
|
||||||
opt_live = client.place_option_market(
|
opt_live = client.place_option_ioc(
|
||||||
symbol=option_inst_id,
|
symbol=option_inst_id,
|
||||||
side="SELL",
|
side="SELL",
|
||||||
quantity=max(1.0, opt_contracts),
|
quantity=max(1.0, opt_contracts),
|
||||||
|
price=bid_px,
|
||||||
reduce_only=True,
|
reduce_only=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"residual premium close exchange sell failed %s: %s",
|
"residual premium close bid-ioc sell failed %s: %s",
|
||||||
row.get("group_id"),
|
row.get("group_id"),
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
@@ -1208,7 +1216,7 @@ class BinanceLiveExecutor(Matcher):
|
|||||||
notional=of_notional,
|
notional=of_notional,
|
||||||
slip=0.0,
|
slip=0.0,
|
||||||
now_ms=now_ms,
|
now_ms=now_ms,
|
||||||
note="LIVE-BN residual mid-close by premium recovery",
|
note=f"LIVE-BN residual mid-close at bid IOC px={bid_px}",
|
||||||
exec_mode="LIVE",
|
exec_mode="LIVE",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,35 @@ class BinanceTradeClient:
|
|||||||
data = self._signed(self._eapi, "POST", "/eapi/v1/order", params)
|
data = self._signed(self._eapi, "POST", "/eapi/v1/order", params)
|
||||||
return self._fill_from_eapi(symbol, data)
|
return self._fill_from_eapi(symbol, data)
|
||||||
|
|
||||||
|
def place_option_ioc(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
symbol: str,
|
||||||
|
side: str, # BUY|SELL
|
||||||
|
quantity: float,
|
||||||
|
price: float,
|
||||||
|
reduce_only: bool = False,
|
||||||
|
) -> LiveFill:
|
||||||
|
"""期权限价 IOC:按买一/卖一价吃单,未成交部分取消。"""
|
||||||
|
qty = str(int(round(quantity)))
|
||||||
|
if qty == "0":
|
||||||
|
qty = "1"
|
||||||
|
px = f"{float(price):.8f}".rstrip("0").rstrip(".")
|
||||||
|
if not px or px == "0":
|
||||||
|
raise RuntimeError("币安期权 IOC 价格无效")
|
||||||
|
params: dict[str, Any] = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"side": side.upper(),
|
||||||
|
"type": "LIMIT",
|
||||||
|
"timeInForce": "IOC",
|
||||||
|
"quantity": qty,
|
||||||
|
"price": px,
|
||||||
|
}
|
||||||
|
if reduce_only:
|
||||||
|
params["reduceOnly"] = "true"
|
||||||
|
data = self._signed(self._eapi, "POST", "/eapi/v1/order", params)
|
||||||
|
return self._fill_from_eapi(symbol, data)
|
||||||
|
|
||||||
def _fill_from_eapi(self, symbol: str, data: dict[str, Any]) -> LiveFill:
|
def _fill_from_eapi(self, symbol: str, data: dict[str, Any]) -> LiveFill:
|
||||||
ord_id = str(data.get("orderId") or data.get("id") or "")
|
ord_id = str(data.get("orderId") or data.get("id") or "")
|
||||||
avg = safe_float(data.get("avgPrice")) or safe_float(data.get("price"))
|
avg = safe_float(data.get("avgPrice")) or safe_float(data.get("price"))
|
||||||
|
|||||||
@@ -1193,7 +1193,7 @@ class OkxLiveExecutor(Matcher):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def try_close_one_residual(self, row: dict) -> dict | None:
|
def try_close_one_residual(self, row: dict) -> dict | None:
|
||||||
"""LIVE:权利金达标后交易所市价卖出归档期权。"""
|
"""LIVE:权利金达标后按最新买一 IOC 限价卖出归档期权(不扫市价)。"""
|
||||||
err = self._guard_live()
|
err = self._guard_live()
|
||||||
if err:
|
if err:
|
||||||
logger.warning("residual premium close blocked: %s", err)
|
logger.warning("residual premium close blocked: %s", err)
|
||||||
@@ -1218,18 +1218,27 @@ class OkxLiveExecutor(Matcher):
|
|||||||
"residual premium close skip %s: bad contracts", row.get("group_id")
|
"residual premium close skip %s: bad contracts", row.get("group_id")
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
# 下单前再刷一次买一,按最新盘口挂 IOC
|
||||||
|
oq2 = self._quote_held_option(option_inst_id)
|
||||||
|
bid_px = float(oq2.bid) if oq2 is not None and oq2.bid is not None else float(close_bid)
|
||||||
|
if bid_px <= 0:
|
||||||
|
logger.debug(
|
||||||
|
"residual premium close skip %s: bid vanished", row.get("group_id")
|
||||||
|
)
|
||||||
|
return None
|
||||||
client = self._client()
|
client = self._client()
|
||||||
try:
|
try:
|
||||||
opt_live = client.place_market(
|
opt_live = client.place_ioc(
|
||||||
inst_id=option_inst_id,
|
inst_id=option_inst_id,
|
||||||
side="sell",
|
side="sell",
|
||||||
sz=str(max(1, int(round(opt_contracts)))),
|
sz=str(max(1, int(round(opt_contracts)))),
|
||||||
|
px=bid_px,
|
||||||
td_mode="cash",
|
td_mode="cash",
|
||||||
reduce_only=True,
|
reduce_only=True,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"residual premium close exchange sell failed %s: %s",
|
"residual premium close bid-ioc sell failed %s: %s",
|
||||||
row.get("group_id"),
|
row.get("group_id"),
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
@@ -1252,7 +1261,7 @@ class OkxLiveExecutor(Matcher):
|
|||||||
notional=of_notional,
|
notional=of_notional,
|
||||||
slip=0.0,
|
slip=0.0,
|
||||||
now_ms=now_ms,
|
now_ms=now_ms,
|
||||||
note="LIVE residual mid-close by premium recovery",
|
note=f"LIVE residual mid-close at bid IOC px={bid_px}",
|
||||||
exec_mode="LIVE",
|
exec_mode="LIVE",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,36 @@ class OkxTradeClient:
|
|||||||
fill = self._wait_fill(inst_id, ord_id)
|
fill = self._wait_fill(inst_id, ord_id)
|
||||||
return fill
|
return fill
|
||||||
|
|
||||||
|
def place_ioc(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
inst_id: str,
|
||||||
|
side: str, # buy|sell
|
||||||
|
sz: str,
|
||||||
|
px: float | str,
|
||||||
|
td_mode: str,
|
||||||
|
pos_side: str | None = None,
|
||||||
|
reduce_only: bool = False,
|
||||||
|
) -> LiveFill:
|
||||||
|
"""限价 IOC:残留回收等场景按指定买一/卖一吃单,不成交部分立即取消。"""
|
||||||
|
body: dict[str, Any] = {
|
||||||
|
"instId": inst_id,
|
||||||
|
"tdMode": td_mode,
|
||||||
|
"side": side,
|
||||||
|
"ordType": "ioc",
|
||||||
|
"sz": str(sz),
|
||||||
|
"px": str(px),
|
||||||
|
}
|
||||||
|
if pos_side:
|
||||||
|
body["posSide"] = pos_side
|
||||||
|
if reduce_only:
|
||||||
|
body["reduceOnly"] = True
|
||||||
|
rows = self._request("POST", "/api/v5/trade/order", body)
|
||||||
|
if not rows:
|
||||||
|
raise RuntimeError("OKX IOC 下单无返回")
|
||||||
|
ord_id = str(rows[0].get("ordId") or "")
|
||||||
|
return self._wait_fill(inst_id, ord_id)
|
||||||
|
|
||||||
def _fill_from_order_row(self, inst_id: str, ord_id: str, row: dict[str, Any]) -> LiveFill:
|
def _fill_from_order_row(self, inst_id: str, ord_id: str, row: dict[str, Any]) -> LiveFill:
|
||||||
avg = safe_float(row.get("avgPx")) or 0.0
|
avg = safe_float(row.get("avgPx")) or 0.0
|
||||||
sz = safe_float(row.get("accFillSz")) or safe_float(row.get("sz")) or 0.0
|
sz = safe_float(row.get("accFillSz")) or safe_float(row.get("sz")) or 0.0
|
||||||
|
|||||||
@@ -1037,7 +1037,7 @@ class Matcher:
|
|||||||
notional=of.notional,
|
notional=of.notional,
|
||||||
slip=of.slip,
|
slip=of.slip,
|
||||||
now_ms=now_ms,
|
now_ms=now_ms,
|
||||||
note="residual mid-close by premium recovery",
|
note=f"residual mid-close at bid px={close_bid}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def try_close_pending_residuals(self) -> list[dict[str, Any]]:
|
def try_close_pending_residuals(self) -> list[dict[str, Any]]:
|
||||||
|
|||||||
+2
-2
@@ -181,7 +181,7 @@ k = floor(budget / (2A + I×fee_rate×3) × 10) / 10
|
|||||||
- 动作:
|
- 动作:
|
||||||
1. **只市价平掉永续**,兑现净利里永续那一截;
|
1. **只市价平掉永续**,兑现净利里永续那一截;
|
||||||
2. 本张期权归档为「残留」:不占用活跃持仓、**不挡住下一组开仓**;下一组只扫当前活跃组期权;
|
2. 本张期权归档为「残留」:不占用活跃持仓、**不挡住下一组开仓**;下一组只扫当前活跃组期权;
|
||||||
3. **中途回收(可配置)**:默认每 **5 分钟**巡检 pending 残留;当 **买一权利金 ≥ 初始权利金 × 比例**(默认 **20%**,系统设置「残留期权回收」可改)且通过买一流动性闸门时,**市价卖掉**该残留并结清;
|
3. **中途回收(可配置)**:默认每 **5 分钟**巡检 pending 残留;当 **买一权利金 ≥ 初始权利金 × 比例**(默认 **20%**,系统设置「残留期权回收」可改)且通过买一流动性闸门时,按**最新买一 IOC 限价**卖掉该残留并结清(不扫市价簿);
|
||||||
4. 未达比例或闸门不过 → 继续等到下次巡检,或到期按 **内在价值** 结算(多半接近 0);
|
4. 未达比例或闸门不过 → 继续等到下次巡检,或到期按 **内在价值** 结算(多半接近 0);
|
||||||
5. 页面:**活跃持仓区变空**;归档腿出现在「残留期权(待到期)」;下方期权盘口 **切回新 ATM**(见 4.6)。
|
5. 页面:**活跃持仓区变空**;归档腿出现在「残留期权(待到期)」;下方期权盘口 **切回新 ATM**(见 4.6)。
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ k = floor(budget / (2A + I×fee_rate×3) × 10) / 10
|
|||||||
└─ 否 → 持有直到到期 → 内在价值结算(+ 若有永续则平永续)
|
└─ 否 → 持有直到到期 → 内在价值结算(+ 若有永续则平永续)
|
||||||
|
|
||||||
残留期权(已归档)
|
残留期权(已归档)
|
||||||
├─ 周期性:买一权利金 / 初始 ≥ 设置% 且流动性过 → 市价卖出结清
|
├─ 周期性:买一权利金 / 初始 ≥ 设置% 且流动性过 → 最新买一 IOC 卖出结清
|
||||||
└─ 否则到期内在价值结算;不挡下一组开仓
|
└─ 否则到期内在价值结算;不挡下一组开仓
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1323,8 +1323,8 @@ export default function SettingsPage() {
|
|||||||
流动性闸门;LIVE 以交易所能否成交为准。
|
流动性闸门;LIVE 以交易所能否成交为准。
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
残留期权回收比例:只平永续后,当买一权利金回升到初始权利金的该比例及以上时,才尝试中途卖掉归档期权;默认
|
残留期权回收比例:只平永续后,当买一权利金回升到初始权利金的该比例及以上时,按最新买一
|
||||||
20%。未达标则等到期按内在价值结算。
|
IOC 限价卖掉归档期权(不扫市价);默认 20%。未达标或未完全成交则等到下次巡检或到期结算。
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user