Files
eth_hedge_sim/backend/tests/test_oo_selection_sizing.py

193 lines
6.0 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 app.exchange.candles import AmplitudeHL
from app.strategy.oo_selection import (
pick_otm_call_strike,
pick_otm_put_strike,
select_oo_pair,
)
from app.strategy.risk_sizing import compute_oo_sizing
def test_otm_strikes_near_amplitude() -> None:
strikes = [1800.0, 1850.0, 1900.0, 1950.0, 2000.0, 2050.0, 2100.0]
assert pick_otm_call_strike(strikes, spot=1950, high=2040) == 2050.0
assert pick_otm_put_strike(strikes, spot=1950, low=1860) == 1850.0
def test_otm_strikes_reject_beyond_1pct() -> None:
# 高点 2000,最近虚值 Call 仅 2100(偏离 5%)→ 拒绝
strikes = [1900.0, 1950.0, 2100.0]
assert pick_otm_call_strike(strikes, spot=1950, high=2000, max_dev_pct=1.0) is None
# 低点 1900,最近虚值 Put 仅 1800(偏离 ~5.3%)→ 拒绝
assert (
pick_otm_put_strike(
[1800.0, 1950.0, 2000.0], spot=1950, low=1900, max_dev_pct=1.0
)
is None
)
# 高点 2095Call 2100 偏离约 0.24% → 通过
assert (
pick_otm_call_strike(strikes, spot=1950, high=2095, max_dev_pct=1.0) == 2100.0
)
def test_select_oo_pair_same_expiry(tmp_path=None) -> None:
contracts = []
for k in (1900, 2000, 2100):
for side, letter in (("call", "C"), ("put", "P")):
contracts.append(
{
"expiry_ymd": "260810",
"expiry_ms": 1_786_320_000_000,
"strike": float(k),
"side": letter,
"inst_id": f"ETH-{k}-{letter}",
}
)
picked = select_oo_pair(
contracts,
spot=2000.0,
high=2105.0,
low=1890.0,
min_hours=1.0,
max_dev_pct=1.0,
)
assert picked is not None
ymd, _ems, ck, pk, call_i, put_i = picked
assert ymd == "260810"
assert ck == 2100.0
assert pk == 1900.0
assert "C" in call_i and "P" in put_i
def test_compute_oo_sizing_1_1_and_reward() -> None:
r = compute_oo_sizing(
budget=100.0,
call_ask=5.0,
put_ask=10.0, # Put 更贵 → 数量更少
fee_rate=0.0,
index_px=2000.0,
cushion=1.0, # 测纯平分,不预留
reward_ratio=2.0,
)
assert r.ok
# 各 50UCall 50/5=10Put 50/10=5
assert r.call_qty_eth == 10.0
assert r.put_qty_eth == 5.0
assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9
assert abs(float(r.call_premium or 0) - 50.0) < 1e-6
assert abs(float(r.put_premium or 0) - 50.0) < 1e-6
def test_compute_oo_sizing_equal_asks() -> None:
r = compute_oo_sizing(
budget=100.0,
call_ask=5.0,
put_ask=5.0,
fee_rate=0.0,
index_px=2000.0,
cushion=0.92,
reward_ratio=2.0,
)
assert r.ok
# 各腿 46U / 5 = 9.2
assert r.call_qty_eth == 9.2
assert r.put_qty_eth == 9.2
assert abs(float(r.net_profit_target or 0) - 200.0) < 1e-9
assert float(r.call_premium or 0) + float(r.put_premium or 0) <= 92.0 + 1e-6
def test_amplitude_range_pct() -> None:
a = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
assert abs(a.range_pct - 3.0) < 1e-9
def test_preview_oo_exit_budget_times_ratio(tmp_path, monkeypatch) -> None:
monkeypatch.setenv("MODE", "SIM")
from app.models.db import Database
from app.sim.ledger import Ledger
from app.strategy.risk_sizing import _preview_oo_sizing
db = Database(tmp_path / "oo_prev.db")
db.set_setting("hedge_mode", "option_option")
db.set_setting("sizing_mode", "risk_based")
db.set_setting("risk_loss_mode", "percent")
db.set_setting("risk_loss_pct", "1")
db.set_setting("risk_capital_source", "manual")
db.set_setting("risk_manual_capital_usdt", "10000")
db.set_setting("oo_reward_ratio", "2")
monkeypatch.setattr("app.strategy.risk_sizing.get_db", lambda: db)
# 无 session 盘口时仍应给出 预算×比
out = _preview_oo_sizing(db, Ledger(db), {"hedge_mode": "option_option"})
assert out.get("ok") is True
assert abs(float(out.get("budget") or 0) - 100.0) < 1e-6
assert abs(float(out.get("net_profit_target") or 0) - 200.0) < 1e-6
db.close()
def test_amplitude_max_gate() -> None:
from app.strategy.oo_selection import build_oo_pick_core
contracts = []
for k in (1900, 1975, 2000, 2025, 2100):
for side, letter in (("call", "C"), ("put", "P")):
contracts.append(
{
"expiry_ymd": "260810",
"expiry_ms": 1_786_320_000_000,
"strike": float(k),
"side": letter,
"inst_id": f"ETH-{k}-{letter}",
}
)
amp = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12)
# 3% > 上限 1.5% → 拒
assert (
build_oo_pick_core(
contracts=contracts,
spot=2000,
call_ask=5,
put_ask=5,
min_hours=1,
min_leverage=1,
amplitude_hours=12,
amplitude_pct=1.5,
amplitude=amp,
)
is None
)
# 3% ≤ 上限 3.5%,且 2025/1975 贴高低 ≤1% → 通过
ok = build_oo_pick_core(
contracts=contracts,
spot=2000,
call_ask=5,
put_ask=5,
min_hours=1,
min_leverage=1,
amplitude_hours=12,
amplitude_pct=3.5,
amplitude=amp,
max_dev_pct=1.0,
)
assert ok is not None
assert ok.call.strike == 2025.0
assert ok.put.strike == 1975.0
# 过滤关闭:振幅 3% 超过 1.5% 上限也可过
ok_off = build_oo_pick_core(
contracts=contracts,
spot=2000,
call_ask=5,
put_ask=5,
min_hours=1,
min_leverage=1,
amplitude_hours=12,
amplitude_pct=1.5,
amplitude=amp,
max_dev_pct=1.0,
amplitude_filter_enabled=False,
)
assert ok_off is not None