0ee1d8be5d
When enabled, lock perp long→buy Put or short→buy Call with ITM/ATM only; off keeps ATM/ask rules. Co-authored-by: Cursor <cursoragent@cursor.com>
125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
from zoneinfo import ZoneInfo
|
||
|
||
from app.market.instruments import (
|
||
hours_until_expiry,
|
||
list_eligible_expiry_ymds,
|
||
next_session_expiry_ymd,
|
||
option_leverage,
|
||
parse_option_inst_id,
|
||
pick_atm_strike,
|
||
select_option_pair,
|
||
)
|
||
|
||
_SH = ZoneInfo("Asia/Shanghai")
|
||
|
||
|
||
def test_parse_option_inst_id() -> None:
|
||
y, s, o = parse_option_inst_id("ETH-USD_UM-260725-3500-C")
|
||
assert y == "260725"
|
||
assert s == 3500.0
|
||
assert o == "C"
|
||
|
||
|
||
def test_pick_atm_strike() -> None:
|
||
assert pick_atm_strike([3400, 3500, 3600], 3510) == 3500
|
||
|
||
|
||
def test_pick_itm_or_atm_strike() -> None:
|
||
from app.strategy.selection import is_itm_or_atm, pick_itm_or_atm_strike
|
||
|
||
strikes = [3400, 3500, 3600]
|
||
# Call:K≤S,现价 3510 → 3500(平值侧最近)
|
||
assert pick_itm_or_atm_strike(strikes, 3510, option_side="call") == 3500
|
||
# Put:K≥S,现价 3510 → 3600(实值最近;无 3510 档)
|
||
assert pick_itm_or_atm_strike(strikes, 3510, option_side="put") == 3600
|
||
# Put 现价正好 3500 → 平值 3500
|
||
assert pick_itm_or_atm_strike(strikes, 3500, option_side="put") == 3500
|
||
assert is_itm_or_atm(option_side="call", strike=3500, mark_px=3510)
|
||
assert not is_itm_or_atm(option_side="call", strike=3600, mark_px=3510)
|
||
assert is_itm_or_atm(option_side="put", strike=3600, mark_px=3510)
|
||
assert not is_itm_or_atm(option_side="put", strike=3400, mark_px=3510)
|
||
|
||
|
||
def test_select_option_pair_itm_put() -> None:
|
||
rows = [
|
||
{"instId": "ETH-USD_UM-260725-3490-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3490-P", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3500-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3500-P", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3510-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3510-P", "state": "live"},
|
||
]
|
||
# 标的 3502:Put 实/平 → 3510(≥3502 最近)
|
||
pair = select_option_pair(rows, mark_px=3502, expiry_ymd="260725", option_side="put")
|
||
assert pair is not None
|
||
assert pair.strike == 3510
|
||
# Call 实/平 → 3500(≤3502 最近)
|
||
pair_c = select_option_pair(
|
||
rows, mark_px=3502, expiry_ymd="260725", option_side="call"
|
||
)
|
||
assert pair_c is not None
|
||
assert pair_c.strike == 3500
|
||
|
||
|
||
def test_next_session_expiry_before_open() -> None:
|
||
now = datetime(2026, 7, 24, 15, 0, tzinfo=_SH)
|
||
assert next_session_expiry_ymd(now) == "260724"
|
||
|
||
|
||
def test_next_session_expiry_after_open() -> None:
|
||
now = datetime(2026, 7, 24, 16, 0, tzinfo=_SH)
|
||
assert next_session_expiry_ymd(now) == "260725"
|
||
|
||
|
||
def test_select_option_pair_atm() -> None:
|
||
rows = [
|
||
{"instId": "ETH-USD_UM-260725-3490-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3490-P", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3500-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-3500-P", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260726-3500-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260726-3500-P", "state": "live"},
|
||
]
|
||
pair = select_option_pair(rows, mark_px=3502, expiry_ymd="260725")
|
||
assert pair is not None
|
||
assert pair.strike == 3500
|
||
assert pair.call_inst_id.endswith("-3500-C")
|
||
assert pair.put_inst_id.endswith("-3500-P")
|
||
|
||
|
||
def test_eligible_skips_short_ttm() -> None:
|
||
# 2026-07-25 08:30 SH:当日 16:00 到期仅约 7.5h,应跳过 260725,选 260726
|
||
now = datetime(2026, 7, 25, 8, 30, tzinfo=_SH)
|
||
rows = [
|
||
{"instId": "ETH-USD_UM-260725-1850-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260725-1850-P", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260726-1850-C", "state": "live"},
|
||
{"instId": "ETH-USD_UM-260726-1850-P", "state": "live"},
|
||
]
|
||
assert hours_until_expiry("260725", now) < 12
|
||
assert hours_until_expiry("260726", now) >= 12
|
||
eligible = list_eligible_expiry_ymds(rows, min_hours=12, now=now)
|
||
assert eligible == ["260726"]
|
||
pair = select_option_pair(rows, mark_px=1853, min_hours=12, now=now)
|
||
assert pair is not None
|
||
assert pair.expiry_ymd == "260726"
|
||
|
||
|
||
def test_option_leverage() -> None:
|
||
assert abs((option_leverage(1850, 18.5) or 0) - 100) < 1e-9
|
||
assert option_leverage(1850, 0) is None
|
||
|
||
|
||
def test_atm_allows_open_offset() -> None:
|
||
from app.strategy.selection import atm_allows_open, atm_open_offset
|
||
|
||
assert atm_open_offset(1850, 1852.5) == 2.5
|
||
# 默认关闭:不限制
|
||
assert atm_allows_open(1850, 1860, max_offset=3, enabled=False) is True
|
||
assert atm_allows_open(1850, 1852.5, max_offset=3, enabled=True) is True
|
||
assert atm_allows_open(1850, 1854, max_offset=3, enabled=True) is False
|
||
assert atm_allows_open(1850, 1860, max_offset=3, enabled=True) is False
|