From 06f9b23f44d67091cc967a1a9996a01c3f026cb1 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 30 Sep 2026 21:54:00 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BF=BB=E5=80=8D=E5=87=BA?= =?UTF-8?q?=E5=9C=BA=E8=BE=BE=E6=A0=87=E5=8D=B4=E4=B8=8D=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=B9=B3=EF=BC=9A=E5=AF=B9=E5=86=B2=E5=BC=82=E5=B8=B8=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=95=B4=E6=AE=B5=E8=B7=B3=E8=BF=87=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E6=8C=89=E4=B9=B0=E4=B8=80=E4=BB=B7=C3=97=E5=85=A8=E4=BB=93?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原先对冲表异常会直接 return 0 导致翻倍出场永不执行;可回收改为买一价×全部张数,买一深度不足时仍会触发分批平仓。 Co-authored-by: Cursor --- lib/options/options_profit_exit_lib.py | 24 ++++++++++++- tests/test_options_profit_exit_recycle.py | 41 +++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/test_options_profit_exit_recycle.py diff --git a/lib/options/options_profit_exit_lib.py b/lib/options/options_profit_exit_lib.py index 109ed67..7d6b27c 100644 --- a/lib/options/options_profit_exit_lib.py +++ b/lib/options/options_profit_exit_lib.py @@ -211,13 +211,34 @@ def _estimate_recycle( pos: dict[str, Any], premium_paid: float | None, ) -> float | None: + """翻倍出场用可回收估算. + + 用买一价 × 全部持仓张数(不吃深度上限):深度不够时仍应触发分批平仓; + 残档/无效买一则返回 None(禁止按假买盘自动平). + """ from lib.options.options_positions_lib import attach_close_preview + from lib.options.options_pricing_lib import total_premium row = dict(pos) attach_close_preview(cfg, ex, row, premium_paid=premium_paid) preview = row.get("close_preview") if isinstance(row.get("close_preview"), dict) else {} if preview.get("bid_invalid"): return None + levels = preview.get("levels") or [] + ct_mult = float(row.get("ct_mult") or 0.01) + sheets = int( + abs( + _safe_float(row.get("avail_pos") if row.get("avail_pos") is not None else row.get("pos")) + or 0 + ) + ) + if levels and sheets > 0 and ct_mult > 0: + try: + px = float(levels[0].get("px") or 0) + except (TypeError, ValueError, AttributeError): + px = 0.0 + if px > 0: + return round(total_premium(px, sheets * ct_mult), 8) return _safe_float(preview.get("total_received")) @@ -296,7 +317,8 @@ def run_options_profit_exits( init_hedge_plan_tables(conn) hedge_managed = active_hedge_option_inst_ids(conn) except Exception: - return 0 + # 对冲表异常时不得整段跳过翻倍出场 + hedge_managed = set() rules = profit_exit_by_inst(conn) triggered = 0 diff --git a/tests/test_options_profit_exit_recycle.py b/tests/test_options_profit_exit_recycle.py new file mode 100644 index 0000000..16056ad --- /dev/null +++ b/tests/test_options_profit_exit_recycle.py @@ -0,0 +1,41 @@ +"""翻倍出场:买一价×全仓张数判定达标(不受买一深度截断).""" +from __future__ import annotations + +import unittest +from unittest.mock import patch + +from lib.options.options_profit_exit_lib import _estimate_recycle, profit_exit_hit + + +class ProfitExitHitTests(unittest.TestCase): + def test_hit_at_0_3_mult(self): + # 0.3倍: recycle >= paid * 1.3 + self.assertTrue(profit_exit_hit(premium_paid=1.0, recycle_usdc=1.3, mult=0.3)) + self.assertFalse(profit_exit_hit(premium_paid=1.0, recycle_usdc=1.29, mult=0.3)) + + def test_estimate_uses_full_sheets_not_depth(self): + pos = {"inst_id": "ETH-USD-260927-2700-P", "pos": 100, "ct_mult": 0.01} + preview = { + "bid_invalid": False, + "levels": [{"px": 0.2, "sheets": 10}], # 深度只够 10 张 + "total_received": 0.02, # 仅深度内 + } + + def fake_attach(cfg, ex, row, premium_paid=None): + row["close_preview"] = preview + row["ct_mult"] = 0.01 + row["pos"] = 100 + return row + + with patch( + "lib.options.options_positions_lib.attach_close_preview", + side_effect=fake_attach, + ): + recv = _estimate_recycle({}, object(), pos, 1.0) + # 应按 100 张 × 0.01 × 0.2 = 0.2,而非深度截断的 0.02 + self.assertAlmostEqual(float(recv), 0.2, places=6) + self.assertTrue(profit_exit_hit(premium_paid=0.15, recycle_usdc=float(recv), mult=0.3)) + + +if __name__ == "__main__": + unittest.main()