Files
crypto_monitor/tests/test_hedge_plan_option_primary.py
T
dekun eca6d091e9 feat(hedge): option-primary watch entry with leverage gate
Start strategy arms a watching plan instead of opening immediately; list filters by leverage; type is a dropdown defaulting to OTM.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-09 09:27:42 +08:00

196 lines
6.9 KiB
Python

"""永期「以期权为主」定仓/方向/目标位/校验."""
import unittest
from lib.hedge_plan.hedge_plan_option_primary_lib import (
PREMIUM_EXEC_FACTOR,
build_option_primary_preview,
estimate_combo_net_pnl,
floor2,
opt_type_for_view,
option_bid_liquidity_ok,
perp_direction_for_view,
pick_option_primary_candidate,
size_from_premium,
target_hit,
validate_option_primary_start,
validate_option_primary_watch,
)
from lib.hedge_plan.hedge_plan_orders_lib import build_po_path_plan, validate_start_body
class TestOptionPrimary(unittest.TestCase):
def test_direction_mapping(self):
self.assertEqual(opt_type_for_view("long"), "C")
self.assertEqual(opt_type_for_view("short"), "P")
self.assertEqual(perp_direction_for_view("long"), "short")
self.assertEqual(perp_direction_for_view("short"), "long")
def test_size_from_premium_095_and_eth_2dp(self):
# ask=10 → 1 ETH 成本 10U; 预算 100 → usable 95 → eth=9.5 → sheets=950 (ct=0.01)
sized = size_from_premium(
premium_budget=100,
ask=10,
ct_mult=0.01,
ratio=2,
contract_size=0.01,
)
self.assertTrue(sized["ok"])
self.assertAlmostEqual(sized["usable_premium"], 95.0)
self.assertEqual(sized["eth_qty"], 9.5)
self.assertEqual(sized["sheets"], 950.0)
# perp_eth = 9.5/2=4.75; contracts=4.75/0.01=475
self.assertAlmostEqual(sized["contracts"], 475.0)
self.assertEqual(PREMIUM_EXEC_FACTOR, 0.95)
def test_floor2(self):
self.assertEqual(floor2(1.239), 1.23)
self.assertEqual(floor2(0.009), 0.0)
def test_target_hit(self):
self.assertTrue(target_hit(view_side="long", index_px=1950, strike=1900, points=50))
self.assertFalse(target_hit(view_side="long", index_px=1949, strike=1900, points=50))
self.assertTrue(target_hit(view_side="short", index_px=1850, strike=1900, points=50))
self.assertFalse(target_hit(view_side="short", index_px=1851, strike=1900, points=50))
self.assertFalse(target_hit(view_side="long", index_px=1900, strike=1900, points=0))
def test_bid_liquidity(self):
ok, _ = option_bid_liquidity_ok(1.2, 10, need_sheets=5)
self.assertTrue(ok)
ok2, msg = option_bid_liquidity_ok(None, 10, need_sheets=1)
self.assertFalse(ok2)
self.assertIn("买一", msg)
def test_net_pnl_uses_buy_fee_for_sell(self):
net = estimate_combo_net_pnl(
view_side="long",
strike=1900,
index_px=1950,
ask_open=20,
bid=30,
sheets=2,
ct_mult=0.01,
perp_direction="short",
perp_entry=1900,
perp_mark=1950,
contracts=10,
contract_size=0.01,
fee=0.001,
)
# opt: proceeds=30*2*0.01=0.6; premium=0.4; fees=0.0004+0.0006; opt_net=0.6-0.4-0.001=0.199
self.assertIn("net", net)
self.assertEqual(net["fee_rate"], 0.001)
def test_path_option_primary_no_tpsl_options_first(self):
path = build_po_path_plan(
{
"option_primary": True,
"direction": "long",
"opt_inst_id": "ETH-USD-260831-1900-C",
"sheets": 2,
"exchange_symbol": "ETH/USDT:USDT",
"contracts": 1,
}
)
self.assertEqual(path[0]["step"], "options_buy_limit")
self.assertEqual(path[1]["direction"], "short")
self.assertFalse(path[1]["attach_tpsl"])
def test_validate_option_primary_watch_and_start(self):
watch_body = {
"option_primary": True,
"watch_entry": 1,
"direction": "long",
"exchange_symbol": "ETH/USDT:USDT",
"premium_budget": 100,
"option_target_points": 50,
"perp_target_points": 30,
"option_perp_ratio": 4,
"option_leverage": 200,
"moneyness": "otm",
}
self.assertIsNone(validate_option_primary_watch(watch_body))
self.assertIsNone(validate_start_body("perp_options", watch_body))
body = {
"option_primary": True,
"watch_entry": 0,
"direction": "long",
"contracts": 1,
"opt_inst_id": "ETH-USD-260831-1900-C",
"opt_type": "C",
"sheets": 2,
"exchange_symbol": "ETH/USDT:USDT",
"premium_budget": 100,
"option_target_points": 50,
"perp_target_points": 30,
"option_perp_ratio": 2,
"strike": 1900,
"index_px": 1905,
"ask": 10,
"moneyness": "atm",
"strike_interval": 15,
"min_option_hours": 36,
"hours_to_expiry": 40,
"option_leverage": 100,
}
self.assertIsNone(validate_option_primary_start(body))
self.assertIsNone(validate_start_body("perp_options", body))
bad = dict(body, opt_type="P")
self.assertIsNotNone(validate_start_body("perp_options", bad))
def test_pick_candidate_respects_leverage(self):
chain = {
"index_px": 1900,
"expiries": [
{
"exp_time": 9_999_999_999_999,
"contracts": [
{"inst_id": "LOW", "opt_type": "C", "strike": 1920, "ask": 20, "ct_mult": 0.01},
{"inst_id": "OK", "opt_type": "C", "strike": 1925, "ask": 8, "ct_mult": 0.01},
],
}
],
}
# 1900/20=95 < 200; 1900/8=237.5 ≥ 200
picked = pick_option_primary_candidate(
chain,
direction="long",
moneyness="otm",
strike_interval=50,
min_hours=1,
min_opt_leverage=200,
)
self.assertIsNotNone(picked)
self.assertEqual(picked["inst_id"], "OK")
def test_preview_builds_scenarios(self):
body = {
"direction": "long",
"strike": 1900,
"option_target_points": 50,
"perp_target_points": 30,
"ask": 20,
"sheets": 10,
"ct_mult": 0.01,
"contracts": 5,
"contract_size": 0.01,
"entry": 1900,
"index_px": 1900,
"premium_budget": 100,
}
out = build_option_primary_preview(body)
self.assertTrue(out["option_primary"])
self.assertEqual(len(out["scenarios"]), 2)
def test_hours_to_expiry_from_ms(self):
from lib.hedge_plan.hedge_plan_option_primary_lib import hours_to_expiry_from_ms
now = 1_700_000_000_000.0
# +40h in ms
h = hours_to_expiry_from_ms(now + 40 * 3600 * 1000, now_ms=now)
self.assertAlmostEqual(h, 40.0, places=3)
if __name__ == "__main__":
unittest.main()