Fix strategy-logic P0s from audit: monitor false-flat, fill-confirmed open/close, mode gates.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -50,12 +50,28 @@ class TestHedgePlanOrderPath(unittest.TestCase):
|
||||
"tp": 1900,
|
||||
"sl": 1700,
|
||||
"contracts": 1,
|
||||
"opt_inst_id": "X",
|
||||
"opt_inst_id": "ETH-USD-260731-1800-P",
|
||||
"opt_type": "P",
|
||||
"sheets": 1,
|
||||
"exchange_symbol": "ETH/USDT:USDT",
|
||||
},
|
||||
)
|
||||
self.assertIsNone(ok)
|
||||
bad_type = validate_start_body(
|
||||
"perp_options",
|
||||
{
|
||||
"direction": "long",
|
||||
"entry": 1800,
|
||||
"tp": 1900,
|
||||
"sl": 1700,
|
||||
"contracts": 1,
|
||||
"opt_inst_id": "ETH-USD-260731-1800-C",
|
||||
"opt_type": "C",
|
||||
"sheets": 1,
|
||||
"exchange_symbol": "ETH/USDT:USDT",
|
||||
},
|
||||
)
|
||||
self.assertIsNotNone(bad_type)
|
||||
|
||||
def test_gate_can_start_when_live(self):
|
||||
g = gate_status(
|
||||
@@ -196,7 +212,7 @@ class TestHedgePlanOrderPath(unittest.TestCase):
|
||||
self.assertIn("卖一", out["msg"])
|
||||
cfg["place_option_limit_order"].assert_not_called()
|
||||
|
||||
def test_buy_caps_sheets_to_ask_depth(self):
|
||||
def test_buy_rejects_when_ask_depth_below_request(self):
|
||||
from lib.hedge_plan.hedge_plan_orders_lib import _buy_option
|
||||
|
||||
quote = MagicMock(
|
||||
@@ -217,8 +233,9 @@ class TestHedgePlanOrderPath(unittest.TestCase):
|
||||
"td_mode_for_option_buy": lambda x: "isolated",
|
||||
}
|
||||
out = _buy_option(cfg, inst_id="X", sheets=9, dry_run=True)
|
||||
self.assertTrue(out["ok"])
|
||||
self.assertEqual(out["sheets"], 2)
|
||||
self.assertFalse(out["ok"])
|
||||
self.assertIn("不足请求", out["msg"])
|
||||
cfg["place_option_limit_order"].assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""永期监控:假平仓防护与 TP/SL 分类."""
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from lib.hedge_plan.hedge_plan_monitor_lib import (
|
||||
_classify_po_flat_reason,
|
||||
_tick_po,
|
||||
_within_open_grace,
|
||||
)
|
||||
|
||||
|
||||
class TestPoMonitorSafety(unittest.TestCase):
|
||||
def test_classify_sl_before_tp(self):
|
||||
self.assertEqual(
|
||||
_classify_po_flat_reason(
|
||||
direction="long", entry=1800, mark=1690, tp=1900, sl=1700
|
||||
),
|
||||
"perp_sl",
|
||||
)
|
||||
self.assertEqual(
|
||||
_classify_po_flat_reason(
|
||||
direction="long", entry=1800, mark=1910, tp=1900, sl=1700
|
||||
),
|
||||
"perp_tp",
|
||||
)
|
||||
|
||||
def test_classify_ambiguous_prefers_sl(self):
|
||||
# mark 在中间:偏 SL,避免误判 TP 跳过强平期权
|
||||
self.assertEqual(
|
||||
_classify_po_flat_reason(
|
||||
direction="long", entry=1800, mark=1800, tp=1900, sl=1700
|
||||
),
|
||||
"perp_sl",
|
||||
)
|
||||
|
||||
def test_classify_unknown_without_mark(self):
|
||||
self.assertEqual(
|
||||
_classify_po_flat_reason(
|
||||
direction="long", entry=1800, mark=None, tp=1900, sl=1700
|
||||
),
|
||||
"perp_flat_unknown",
|
||||
)
|
||||
|
||||
def test_tick_po_skips_when_live_none(self):
|
||||
cfg = {"get_live_position_contracts": lambda *_a, **_k: None}
|
||||
plan = {"id": 1, "direction": "long", "entry_mark": 1800, "tp": 1900, "sl": 1700}
|
||||
legs = [{"leg_role": "perp", "status": "open", "symbol": "ETH/USDT:USDT", "size": 1}]
|
||||
self.assertIsNone(_tick_po(cfg, MagicMock(), plan, legs))
|
||||
|
||||
def test_tick_po_skips_when_live_positive(self):
|
||||
cfg = {"get_live_position_contracts": lambda *_a, **_k: 2.0}
|
||||
plan = {"id": 1, "direction": "long", "entry_mark": 1800, "tp": 1900, "sl": 1700}
|
||||
legs = [{"leg_role": "perp", "status": "open", "symbol": "ETH/USDT:USDT", "size": 1}]
|
||||
self.assertIsNone(_tick_po(cfg, MagicMock(), plan, legs))
|
||||
|
||||
def test_within_grace_true_without_opened_at(self):
|
||||
self.assertTrue(_within_open_grace({}))
|
||||
|
||||
def test_pending_sl_sticky(self):
|
||||
"""perp_sl_pending_opt 粘滞,不因 mark 反弹改判 TP."""
|
||||
cfg = {
|
||||
"get_live_position_contracts": lambda *_a, **_k: 0.0,
|
||||
"get_contract_size": lambda *_a, **_k: 0.01,
|
||||
"exchange": None,
|
||||
}
|
||||
plan = {
|
||||
"id": 9,
|
||||
"direction": "long",
|
||||
"entry_mark": 1800,
|
||||
"tp": 1900,
|
||||
"sl": 1700,
|
||||
"premium_total": 5,
|
||||
"opened_at": "2000-01-01 00:00:00",
|
||||
"close_reason": "perp_sl_pending_opt",
|
||||
}
|
||||
legs = [
|
||||
{"id": 1, "leg_role": "perp", "status": "open", "symbol": "ETH/USDT:USDT", "size": 1, "avg_open": 1800},
|
||||
{
|
||||
"id": 2,
|
||||
"leg_role": "option_hedge",
|
||||
"status": "open",
|
||||
"inst_id": "ETH-USD-260731-1700-P",
|
||||
"size": 1,
|
||||
"avg_open": 10,
|
||||
},
|
||||
]
|
||||
with patch("lib.hedge_plan.hedge_plan_monitor_lib._sell_option") as sell:
|
||||
sell.return_value = {"ok": False, "msg": "thin book", "fully_closed": False}
|
||||
with patch("lib.hedge_plan.hedge_plan_monitor_lib.notify_hedge"):
|
||||
with patch("lib.hedge_plan.hedge_plan_monitor_lib.update_plan") as up:
|
||||
conn = MagicMock()
|
||||
out = _tick_po(cfg, conn, plan, legs)
|
||||
self.assertTrue(out and out.get("retry"))
|
||||
up.assert_called()
|
||||
self.assertEqual(up.call_args.kwargs.get("close_reason"), "perp_sl_pending_opt")
|
||||
self.assertNotEqual(up.call_args.kwargs.get("status"), "closed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user