24943adf13
Plan no longer shows perp exit_unit*k as the OO target. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.2 KiB
Python
136 lines
4.2 KiB
Python
"""期期对冲:选约 / 定仓纯函数测试。"""
|
|
|
|
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_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,
|
|
)
|
|
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=5.0,
|
|
fee_rate=0.0,
|
|
index_px=2000.0,
|
|
cushion=0.92,
|
|
reward_ratio=2.0,
|
|
)
|
|
assert r.ok
|
|
assert r.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, 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}",
|
|
}
|
|
)
|
|
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% → 可过振幅门(杠杆/卖一足够)
|
|
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,
|
|
)
|
|
assert ok is not None
|