c9c8388250
Co-authored-by: Cursor <cursoragent@cursor.com>
328 lines
9.3 KiB
Python
328 lines
9.3 KiB
Python
"""期权定价单测."""
|
|
from lib.options.options_pricing_lib import (
|
|
calc_order_size,
|
|
premium_per_sheet,
|
|
sheets_from_eth_amount,
|
|
total_premium,
|
|
)
|
|
from lib.exchange.okx_options_lib import format_option_px, inst_family_from_inst_id, round_option_px
|
|
|
|
|
|
def test_inst_family_from_inst_id():
|
|
assert inst_family_from_inst_id("ETH-USD_UM-260707-1790-C") == "ETH-USD_UM"
|
|
assert inst_family_from_inst_id("BTC-USD-260925-60000-C") == "BTC-USD"
|
|
|
|
|
|
def test_round_option_px():
|
|
assert round_option_px(14.9184, "0.2", "sell") == 14.8
|
|
assert round_option_px(14.81, "0.2", "buy") == 15.0
|
|
assert format_option_px(14.8, "0.2") == "14.8"
|
|
|
|
|
|
def test_premium_per_sheet():
|
|
assert abs(premium_per_sheet(15.6, 0.01) - 0.156) < 1e-9
|
|
|
|
|
|
def test_total_premium_half_eth():
|
|
assert abs(total_premium(15.6, 0.5) - 7.8) < 1e-9
|
|
|
|
|
|
def test_sheets_from_eth():
|
|
assert sheets_from_eth_amount(0.5, 0.01) == 50
|
|
|
|
|
|
def test_calc_order_size_budget():
|
|
r = calc_order_size(
|
|
quote_per_unit=15.6,
|
|
ct_mult=0.01,
|
|
min_sz=1,
|
|
budget_usdc=10,
|
|
budget_buffer=0.95,
|
|
budget_cap=10,
|
|
)
|
|
assert r["ok"] is True
|
|
assert r["sheets"] >= 1
|
|
assert r["total_premium"] <= 10
|
|
|
|
|
|
def test_calc_order_size_sheets():
|
|
r = calc_order_size(
|
|
quote_per_unit=15.6,
|
|
ct_mult=0.01,
|
|
min_sz=1,
|
|
sheets=3,
|
|
budget_cap=10,
|
|
)
|
|
assert r["ok"] is True
|
|
assert r["sheets"] == 3
|
|
assert abs(r["total_premium"] - 0.468) < 1e-9
|
|
|
|
|
|
def test_option_moneyness():
|
|
from lib.options.options_pricing_lib import option_moneyness, option_moneyness_label
|
|
|
|
assert option_moneyness(opt_type="C", strike=1700, index_px=1800) == "itm"
|
|
assert option_moneyness(opt_type="C", strike=1900, index_px=1800) == "otm"
|
|
assert option_moneyness_label("itm") == "实值"
|
|
assert option_moneyness_label("otm") == "虚值"
|
|
|
|
|
|
def test_equivalent_contract_leverage():
|
|
from lib.options.options_pricing_lib import equivalent_contract_leverage
|
|
|
|
# index 1768, 0.2 ETH, premium 2.44 -> ~144.9x
|
|
lev = equivalent_contract_leverage(index_px=1768, eth_amount=0.2, total_premium=2.44)
|
|
assert lev == 144.9
|
|
|
|
|
|
def test_estimate_expiry_value_and_profit_at_index():
|
|
from lib.options.options_pricing_lib import (
|
|
estimate_expiry_profit_at_index,
|
|
estimate_expiry_value_at_index,
|
|
)
|
|
|
|
value = estimate_expiry_value_at_index(
|
|
opt_type="C", strike=1800, target_idx=2000, eth_amount=1.0
|
|
)
|
|
assert value == 200.0
|
|
|
|
profit = estimate_expiry_profit_at_index(
|
|
opt_type="C",
|
|
strike=1800,
|
|
target_idx=2000,
|
|
entry_px=0.148,
|
|
eth_amount=1.0,
|
|
total_premium=14.8,
|
|
)
|
|
assert profit == 185.2
|
|
|
|
# Call 1780, ask 12.2, 0.01 ETH, target 1793 -> value 0.13, profit 0.01
|
|
v = estimate_expiry_value_at_index(
|
|
opt_type="C", strike=1780, target_idx=1793, eth_amount=0.01
|
|
)
|
|
assert v == 0.13
|
|
p = estimate_expiry_profit_at_index(
|
|
opt_type="C",
|
|
strike=1780,
|
|
target_idx=1793,
|
|
entry_px=12.2,
|
|
eth_amount=0.01,
|
|
total_premium=0.122,
|
|
)
|
|
assert p == 0.01
|
|
# OTM call loses premium
|
|
p2 = estimate_expiry_profit_at_index(
|
|
opt_type="C",
|
|
strike=1780,
|
|
target_idx=1770,
|
|
entry_px=12.2,
|
|
eth_amount=0.01,
|
|
total_premium=0.122,
|
|
)
|
|
assert p2 == -0.12
|
|
|
|
|
|
def test_resolve_chain_quote_otm_no_quote():
|
|
from lib.exchange.okx_options_lib import _resolve_chain_quote
|
|
|
|
q = _resolve_chain_quote(
|
|
ticker={},
|
|
meta={"tickSz": "0.2"},
|
|
opt_type="C",
|
|
strike=1800,
|
|
index_px=1776,
|
|
)
|
|
assert q["ask"] is None
|
|
assert q["bid"] is None
|
|
assert q["ask_estimated"] is False
|
|
|
|
|
|
def test_resolve_chain_quote_estimated_ask():
|
|
from lib.exchange.okx_options_lib import _resolve_chain_quote
|
|
|
|
q = _resolve_chain_quote(
|
|
ticker={"bidPx": "0.2", "bidSz": "3500"},
|
|
meta={"tickSz": "0.2"},
|
|
opt_type="C",
|
|
strike=1650,
|
|
index_px=1776,
|
|
)
|
|
assert q["ask_estimated"] is True
|
|
assert q["ask"] is not None
|
|
assert q["ask"] >= 120
|
|
|
|
|
|
def test_format_quote_liquidity():
|
|
from lib.options.options_pricing_lib import format_quote_liquidity
|
|
|
|
assert format_quote_liquidity(17.2, 150) == "17.2/150"
|
|
assert format_quote_liquidity(817.6, 11) == "817.6/11"
|
|
assert format_quote_liquidity(15.6, None) == "15.6"
|
|
assert format_quote_liquidity(None, 10) is None
|
|
|
|
|
|
def test_estimate_close_by_bids_full_depth():
|
|
from lib.options.options_pricing_lib import estimate_close_by_bids
|
|
|
|
out = estimate_close_by_bids(
|
|
[{"px": 12.3, "sz": 2}, {"px": 12.1, "sz": 3}],
|
|
4,
|
|
ct_mult=0.01,
|
|
premium_paid=0.4,
|
|
)
|
|
assert out["covered_sheets"] == 4
|
|
assert out["uncovered_sheets"] == 0
|
|
assert out["total_received"] == 0.488
|
|
assert out["avg_px"] == 12.2
|
|
assert out["estimated_pnl"] == 0.088
|
|
assert [x["sheets"] for x in out["levels"]] == [2, 2]
|
|
|
|
|
|
def test_estimate_close_by_bids_partial_depth():
|
|
from lib.options.options_pricing_lib import estimate_close_by_bids
|
|
|
|
out = estimate_close_by_bids([{"px": 10, "sz": 1}], 3, ct_mult=0.01, premium_paid=0.6)
|
|
assert out["covered_sheets"] == 1
|
|
assert out["uncovered_sheets"] == 2
|
|
assert out["total_received"] == 0.1
|
|
assert out["estimated_pnl"] == -0.1
|
|
|
|
|
|
def test_estimate_close_by_bids_empty():
|
|
from lib.options.options_pricing_lib import estimate_close_by_bids
|
|
|
|
out = estimate_close_by_bids([], 2)
|
|
assert out["covered_sheets"] == 0
|
|
assert out["uncovered_sheets"] == 2
|
|
assert out["avg_px"] is None
|
|
|
|
|
|
def test_expiry_breakeven_from_ask():
|
|
from lib.options.options_pricing_lib import expiry_breakeven_from_ask
|
|
|
|
assert expiry_breakeven_from_ask(opt_type="C", strike=1760, ask_px=15.6) == 1775.6
|
|
assert expiry_breakeven_from_ask(opt_type="P", strike=1760, ask_px=15.6) == 1744.4
|
|
assert expiry_breakeven_from_ask(opt_type="C", strike=1760, ask_px=None, mark_px=14.2) == 1774.2
|
|
|
|
|
|
def test_calc_order_size_too_small():
|
|
r = calc_order_size(
|
|
quote_per_unit=2000.0,
|
|
ct_mult=0.01,
|
|
min_sz=1,
|
|
budget_usdc=10,
|
|
budget_buffer=0.95,
|
|
budget_cap=10,
|
|
)
|
|
assert r["ok"] is False
|
|
|
|
|
|
def test_expiry_breakeven_from_api():
|
|
from lib.options.options_pricing_lib import expiry_breakeven_px
|
|
|
|
assert expiry_breakeven_px(
|
|
opt_type="C", strike=3500, avg_px=15.6, be_px_api=3516.2
|
|
) == 3516.2
|
|
|
|
|
|
def test_expiry_breakeven_call_put():
|
|
from lib.options.options_pricing_lib import expiry_breakeven_px
|
|
|
|
assert expiry_breakeven_px(opt_type="C", strike=3500, avg_px=15.6) == 3515.6
|
|
assert expiry_breakeven_px(opt_type="P", strike=3500, avg_px=15.6) == 3484.4
|
|
|
|
|
|
def test_close_breakeven_at_mark_equals_avg():
|
|
from lib.options.options_pricing_lib import close_breakeven_idx
|
|
|
|
assert close_breakeven_idx(
|
|
opt_type="C", idx_px=3480, mark_px=15.6, avg_px=15.6
|
|
) == 3480.0
|
|
assert close_breakeven_idx(
|
|
opt_type="P", idx_px=3480, mark_px=15.6, avg_px=15.6
|
|
) == 3480.0
|
|
|
|
|
|
def test_close_breakeven_with_delta():
|
|
from lib.options.options_pricing_lib import close_breakeven_idx
|
|
|
|
# mark below avg, delta 0.5 ETH on 0.5 ETH position -> slope 1
|
|
be = close_breakeven_idx(
|
|
opt_type="C",
|
|
idx_px=3480,
|
|
mark_px=14.6,
|
|
avg_px=15.6,
|
|
delta_pa=0.5,
|
|
pos=50,
|
|
ct_mult=0.01,
|
|
)
|
|
assert be == 3481.0
|
|
|
|
|
|
def test_format_options_breakeven_line():
|
|
from lib.options.options_pricing_lib import format_options_breakeven_line
|
|
|
|
s = format_options_breakeven_line(
|
|
expiry_be_px=3515.6, close_be_px=3498.0, idx_px=3480.0
|
|
)
|
|
assert "到期平衡3516" in s
|
|
assert "平掉回本3498" in s
|
|
assert "指数3480" in s
|
|
|
|
|
|
def test_format_position_row_premium_and_inst_parse():
|
|
from lib.exchange.okx_options_lib import format_position_row
|
|
|
|
row = format_position_row(
|
|
{
|
|
"instId": "ETH-USD_UM-260709-1700-P",
|
|
"pos": "20",
|
|
"avgPx": "6.2",
|
|
"markPx": "6.3241",
|
|
"idxPx": "1746",
|
|
"upl": "0.0248",
|
|
"uplRatio": "0.02",
|
|
}
|
|
)
|
|
assert row["opt_type"] == "P"
|
|
assert row["strike"] == 1700.0
|
|
assert row["premium_paid"] == 1.24
|
|
assert row["exp_time_ms"] is not None
|
|
assert row["exp_time_ms"] > 0
|
|
|
|
|
|
def test_expiry_ms_from_inst_id():
|
|
from lib.exchange.okx_options_lib import expiry_ms_from_inst_id, normalize_option_exp_ms
|
|
|
|
ms = expiry_ms_from_inst_id("ETH-USD_UM-260709-1700-P")
|
|
assert ms is not None
|
|
from datetime import datetime, timezone
|
|
|
|
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
|
assert dt.year == 2026 and dt.month == 7 and dt.day == 9 and dt.hour == 8
|
|
assert normalize_option_exp_ms(None, "ETH-USD_UM-260709-1700-P") == ms
|
|
|
|
|
|
def test_format_position_row_breakeven():
|
|
from lib.exchange.okx_options_lib import format_position_row
|
|
|
|
row = format_position_row(
|
|
{
|
|
"instId": "ETH-USD_UM-260703-1800-C",
|
|
"pos": "50",
|
|
"avgPx": "15.6",
|
|
"markPx": "16.2",
|
|
"idxPx": "3480",
|
|
"bePx": "3515.6",
|
|
"optType": "C",
|
|
"stk": "3500",
|
|
"deltaPA": "0.45",
|
|
"upl": "0.3",
|
|
"uplRatio": "0.02",
|
|
}
|
|
)
|
|
assert row["expiry_be_px"] == 3515.6
|
|
assert row["idx_px"] == 3480.0
|
|
assert row["close_be_px"] is not None
|
|
assert row["dist_expiry_be"] == 35.6
|