"""期期对冲:选约 / 定仓纯函数测试。""" 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 ) # 高点 2095,Call 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 # 各 50U:Call 50/5=10,Put 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