Files
dekun 0ee1d8be5d Add fixed-direction switch for perp/option open side.
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>
2026-07-29 19:18:12 +08:00

125 lines
4.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
# CallK≤S,现价 3510 → 3500(平值侧最近)
assert pick_itm_or_atm_strike(strikes, 3510, option_side="call") == 3500
# PutK≥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"},
]
# 标的 3502Put 实/平 → 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