eca6d091e9
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>
196 lines
6.9 KiB
Python
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()
|