Files
eth_hedge_sim/backend/tests/test_p1_p2_rules.py
T
dekun f1e3d5527a Fix semi open stalls: one-leg asks, OTM realign, clearer pick errors.
Monitor no longer shows ATM Put bias while semi-auto is authorized for Call.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 14:43:00 +08:00

286 lines
9.2 KiB
Python
Raw 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 datetime import datetime
from zoneinfo import ZoneInfo
from app.sim.liquidity import bid_mark_ok
from app.sim.pricing import option_fill, perp_fill
from app.strategy.clock import (
can_open_new,
expiry_blocked_by_one_per_day,
used_expiry_ymds_for_day,
window_key,
)
from app.strategy.exits import check_expiry_close, check_exits
from app.strategy.signal import decide
_SH = ZoneInfo("Asia/Shanghai")
def test_signal_buy_call_short_perp() -> None:
s = decide(20.0, 15.0)
assert s is not None
assert s.option_side == "call"
assert s.perp_side == "short"
def test_signal_buy_put_long_perp() -> None:
s = decide(10.0, 16.0)
assert s is not None
assert s.option_side == "put"
assert s.perp_side == "long"
def test_signal_equal() -> None:
assert decide(10.0, 10.0) is None
def test_decide_fixed_long_put() -> None:
from app.strategy.signal import decide_fixed
s = decide_fixed(20.0, 15.0, perp_side="long")
assert s is not None
assert s.option_side == "put"
assert s.perp_side == "long"
assert s.bias == "fixed_long_put"
def test_decide_fixed_short_call() -> None:
from app.strategy.signal import decide_fixed
s = decide_fixed(20.0, 15.0, perp_side="short")
assert s is not None
assert s.option_side == "call"
assert s.perp_side == "short"
assert s.bias == "fixed_short_call"
def test_decide_fixed_needs_only_own_leg() -> None:
from app.strategy.signal import decide_fixed
# 半自动多/空:对侧卖一缺失仍可定方向
sc = decide_fixed(8.2, None, perp_side="short")
assert sc is not None and sc.option_side == "call"
sp = decide_fixed(None, 11.4, perp_side="long")
assert sp is not None and sp.option_side == "put"
assert decide_fixed(None, None, perp_side="short") is None
def test_signal_strike_below_spot_call_short() -> None:
# 现价 1859、ATM 1850:即使 Put 卖一更高,也走 Call+空
s = decide(10.0, 20.0, strike=1850, mark_px=1859)
assert s is not None
assert s.option_side == "call"
assert s.perp_side == "short"
assert s.bias == "strike_below_spot"
def test_signal_strike_above_spot_put_long() -> None:
# 现价 1859、ATM 1875:即使 Call 卖一更高,也走 Put+多
s = decide(20.0, 10.0, strike=1875, mark_px=1859)
assert s is not None
assert s.option_side == "put"
assert s.perp_side == "long"
assert s.bias == "strike_above_spot"
def test_signal_strike_flat_falls_back_to_ask() -> None:
s = decide(20.0, 15.0, strike=1860, mark_px=1860)
assert s is not None
assert s.bias == "call_ask_gt_put"
assert s.option_side == "call"
def test_exit_fixed_and_premium_multiple() -> None:
fixed = check_exits(
net_pnl=15.0,
exit_mode="fixed_usdt",
net_profit_target=15,
premium_exit_multiple=1,
initial_premium=40,
)
assert fixed.reason == "fixed_usdt"
assert fixed.target == 15
assert (
check_exits(
net_pnl=14.9,
exit_mode="fixed_usdt",
net_profit_target=15,
premium_exit_multiple=1,
initial_premium=40,
).should_close
is False
)
prem = check_exits(
net_pnl=40.0,
exit_mode="premium_multiple",
net_profit_target=15,
premium_exit_multiple=1,
initial_premium=40,
)
assert prem.reason == "premium_multiple"
assert prem.target == 40
half = check_exits(
net_pnl=20.0,
exit_mode="premium_multiple",
net_profit_target=15,
premium_exit_multiple=0.5,
initial_premium=40,
)
assert half.should_close is True
assert half.target == 20
def test_perp_pricing() -> None:
r = perp_fill(side="long", action="open", bid=100, ask=101, qty_eth=1, fee_rate=0.001)
assert abs(r.fill_px - 101 * 1.001) < 1e-9
def test_option_open_close_pricing() -> None:
o = option_fill(action="open", bid=10, ask=12, qty_eth=2, fee_rate=0.001)
assert o.fill_px > 12
c = option_fill(action="close", bid=10, ask=12, qty_eth=2, fee_rate=0.001)
assert c.fill_px < 10
def test_window_weekend_skip() -> None:
# 2026-07-24 周五可开;25/26 周六日不可开
fri = datetime(2026, 7, 24, 17, 0, tzinfo=_SH)
sat = datetime(2026, 7, 25, 12, 0, tzinfo=_SH)
sun = datetime(2026, 7, 26, 10, 0, tzinfo=_SH)
mon = datetime(2026, 7, 27, 9, 0, tzinfo=_SH)
assert can_open_new(fri, skip_weekends=True) is True
assert can_open_new(sat, skip_weekends=True) is False
assert can_open_new(sun, skip_weekends=True) is False
assert can_open_new(mon, skip_weekends=True) is True
assert can_open_new(sat, skip_weekends=False) is True
assert window_key(fri) == "20260724"
def test_one_expiry_per_day(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.models.db import Database
db = Database(tmp_path / "one_exp.db")
# 日历已跨到 8.3,但 8.2 开过的 260803 仍须占用
day = datetime(2026, 8, 3, 0, 0, 2, tzinfo=_SH)
with db._lock:
db._conn.execute(
"""INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
VALUES ('G-20260802-01','closed','260803',1)"""
)
db._conn.execute(
"""INSERT INTO groups(group_id, status, expiry_ymd, open_at_ms)
VALUES ('G-20260801-01','closed','260802',1)"""
)
db._conn.commit()
used = used_expiry_ymds_for_day(db, day)
assert used == {"260803", "260802"}
assert expiry_blocked_by_one_per_day("260803", used, enabled=True) is True
assert expiry_blocked_by_one_per_day("260804", used, enabled=True) is False
assert expiry_blocked_by_one_per_day("260803", used, enabled=False) is False
db.close()
def test_bid_mark_deviation_30pct() -> None:
# |7-10|/10 = 30% → 允许(≤30%
ok, _ = bid_mark_ok(bid=7.0, mark=10.0, max_dev_pct=30)
assert ok is True
ok2, _ = bid_mark_ok(bid=6.9, mark=10.0, max_dev_pct=30)
assert ok2 is False
ok3, why = bid_mark_ok(bid=None, mark=10.0, max_dev_pct=30)
assert ok3 is False
assert "买一" in why
def test_expiry_close() -> None:
assert check_expiry_close(expiry_ms=None).should_close is False
d = check_expiry_close(expiry_ms=1_000, now_ms=999)
assert d.should_close is False
d2 = check_expiry_close(expiry_ms=1_000, now_ms=1_000)
assert d2.should_close is True
assert d2.reason == "expiry"
d3 = check_expiry_close(expiry_ms=1_000, now_ms=1_001)
assert d3.should_close is True
def test_deep_otm_and_expiry_settle() -> None:
from app.sim.pricing import is_deep_otm, option_expiry_settle, option_intrinsic
assert is_deep_otm(option_side="call", strike=1860, spot=1840) is True
assert is_deep_otm(option_side="call", strike=1860, spot=1882) is False
assert is_deep_otm(option_side="put", strike=1860, spot=1882) is True
assert option_intrinsic(option_side="call", strike=1860, spot=1840) == 0.0
settled = option_expiry_settle(intrinsic=0.0, qty_eth=2.0, fee_rate=0.0005)
assert settled.fill_px == 0.0
assert settled.notional == 0.0
def test_pair_from_held_option_inst() -> None:
from app.exchange.option_ids import flip_option_side, pair_from_option_inst
put = "ETH-USD_UM-260727-1880-P"
pair = pair_from_option_inst(put)
assert pair is not None
assert pair.strike == 1880
assert pair.expiry_ymd == "260727"
assert pair.put_inst_id == put
assert pair.call_inst_id == "ETH-USD_UM-260727-1880-C"
assert flip_option_side(put) == pair.call_inst_id
bn = "ETH-260727-1890-C"
bp = pair_from_option_inst(bn)
assert bp is not None
assert bp.strike == 1890
assert bp.call_inst_id == bn
assert bp.put_inst_id == "ETH-260727-1890-P"
def test_option_intrinsic_and_close_bid_floor() -> None:
from app.sim.pricing import (
option_expiry_settle,
option_intrinsic,
resolve_option_close_bid,
)
assert option_intrinsic(option_side="call", strike=1860, spot=1882) == 22.0
assert option_intrinsic(option_side="put", strike=1860, spot=1882) == 0.0
assert option_intrinsic(option_side="put", strike=1860, spot=1840) == 20.0
# 到期:严格按内在价值,无滑点
settled = option_expiry_settle(intrinsic=22.0, qty_eth=2.0, fee_rate=0.0005)
assert settled.fill_px == 22.0
assert settled.slip == 0.0
assert settled.notional == 44.0
assert abs(settled.fee - 44.0 * 0.0005) < 1e-12
# 紧急:对齐 OKX 市价卖,只按买一(垃圾买一不抬到内在价值/标记)
assert (
resolve_option_close_bid(
bid=0.2, mark=22.0, intrinsic=22.0, bypass_liquidity=True
)
== 0.2
)
# 常规:内在价值地板
assert (
resolve_option_close_bid(
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=False
)
== 22.0
)
# 买一高于内在价值,保留买一
assert (
resolve_option_close_bid(
bid=25.0, mark=24.0, intrinsic=22.0, bypass_liquidity=True
)
== 25.0
)
# 紧急无买一 → 无法定价(不能用标记)
assert (
resolve_option_close_bid(
bid=None, mark=3.0, intrinsic=22.0, bypass_liquidity=True
)
is None
)