Fix btc_eth watching entry, preview sizing, and coin-to-U settlement.
Use dual indices for watch/preview; perp entry uses ETH mark; expiry/close convert coin PnL via option index; align legacy long→Call; omit combo U when index missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2722,7 +2722,8 @@
|
|||||||
option_primary: true,
|
option_primary: true,
|
||||||
market_order: 1,
|
market_order: 1,
|
||||||
direction: getDirection(),
|
direction: getDirection(),
|
||||||
entry: indexPx() || Number((state.market && state.market.mark) || 0),
|
// entry=永续参考价;跨币用 ETH mark,勿写 BTC 指数
|
||||||
|
entry: Number(perpIndexPx() || (state.market && state.market.mark) || 0),
|
||||||
contracts: sized.contracts,
|
contracts: sized.contracts,
|
||||||
sheets: sized.sheets,
|
sheets: sized.sheets,
|
||||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||||
@@ -2730,7 +2731,7 @@
|
|||||||
strike: state.selected.strike,
|
strike: state.selected.strike,
|
||||||
ct_mult: state.selected.ct_mult || 0.01,
|
ct_mult: state.selected.ct_mult || 0.01,
|
||||||
ask: state.selected.ask,
|
ask: state.selected.ask,
|
||||||
index_px: indexPx() || 0,
|
index_px: Number(optionIndexPx() || 0),
|
||||||
premium_budget: premBudget > 0 ? premBudget : Math.max(premEst / 0.95, premEst),
|
premium_budget: premBudget > 0 ? premBudget : Math.max(premEst / 0.95, premEst),
|
||||||
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
||||||
option_target_points: optPts,
|
option_target_points: optPts,
|
||||||
@@ -3825,12 +3826,15 @@
|
|||||||
plan_type: "perp_options",
|
plan_type: "perp_options",
|
||||||
option_primary: true,
|
option_primary: true,
|
||||||
watch_entry: 1,
|
watch_entry: 1,
|
||||||
underlying: state.underlying,
|
po_mode: state.poMode,
|
||||||
|
underlying: state.optionUnderly || state.underlying,
|
||||||
|
option_underlying: state.optionUnderly || state.underlying,
|
||||||
|
perp_underlying: state.perpUnderly || state.underlying,
|
||||||
direction: getDirection(),
|
direction: getDirection(),
|
||||||
exchange_symbol: state.market.exchange_symbol,
|
exchange_symbol: state.market.exchange_symbol,
|
||||||
contract_size: state.market.contract_size || 0.01,
|
contract_size: state.market.contract_size || 0.01,
|
||||||
index_px: indexPx() || Number(state.market.mark || 0),
|
index_px: Number(optionIndexPx() || 0),
|
||||||
entry: indexPx() || Number(state.market.mark || 0),
|
entry: Number(perpIndexPx() || (state.market && state.market.mark) || 0),
|
||||||
premium_budget: prem,
|
premium_budget: prem,
|
||||||
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
option_perp_ratio: numInput("hp-opt-perp-ratio", 2),
|
||||||
option_target_points: optPts,
|
option_target_points: optPts,
|
||||||
@@ -3909,7 +3913,10 @@
|
|||||||
throw new Error("门禁未满足,不可开仓: " + gateTxt);
|
throw new Error("门禁未满足,不可开仓: " + gateTxt);
|
||||||
}
|
}
|
||||||
const exp = currentExp("hp-exp-select");
|
const exp = currentExp("hp-exp-select");
|
||||||
const entry = indexPx() || Number((state.market && state.market.mark) || 0);
|
const optIdx = Number(optionIndexPx() || 0);
|
||||||
|
const perpIdx = Number(
|
||||||
|
perpIndexPx() || (state.market && state.market.mark) || 0
|
||||||
|
);
|
||||||
const premEst =
|
const premEst =
|
||||||
Number(state.selected.ask || 0) *
|
Number(state.selected.ask || 0) *
|
||||||
sized.sheets *
|
sized.sheets *
|
||||||
@@ -3925,7 +3932,7 @@
|
|||||||
option_underlying: state.optionUnderly || state.underlying,
|
option_underlying: state.optionUnderly || state.underlying,
|
||||||
perp_underlying: state.perpUnderly || state.underlying,
|
perp_underlying: state.perpUnderly || state.underlying,
|
||||||
direction: getDirection(),
|
direction: getDirection(),
|
||||||
entry: Number((state.market && state.market.mark) || entry),
|
entry: perpIdx,
|
||||||
contracts: sized.contracts,
|
contracts: sized.contracts,
|
||||||
sheets: sized.sheets,
|
sheets: sized.sheets,
|
||||||
contract_size: (state.market && state.market.contract_size) || 0.01,
|
contract_size: (state.market && state.market.contract_size) || 0.01,
|
||||||
@@ -3934,7 +3941,7 @@
|
|||||||
opt_type: state.selected.opt_type,
|
opt_type: state.selected.opt_type,
|
||||||
strike: state.selected.strike,
|
strike: state.selected.strike,
|
||||||
ask: state.selected.ask,
|
ask: state.selected.ask,
|
||||||
index_px: entry,
|
index_px: optIdx,
|
||||||
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
|
exchange_symbol: (state.market && state.market.exchange_symbol) || "",
|
||||||
leverage: defaultPerpLeverage(state.perpUnderly || state.underlying),
|
leverage: defaultPerpLeverage(state.perpUnderly || state.underlying),
|
||||||
option_leverage: 0,
|
option_leverage: 0,
|
||||||
|
|||||||
@@ -313,9 +313,8 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
|
|||||||
round(opt_net_coin * float(idx), 4) if idx is not None and idx > 0 else None
|
round(opt_net_coin * float(idx), 4) if idx is not None and idx > 0 else None
|
||||||
)
|
)
|
||||||
perp_net = float(net.get("perp_net") or 0)
|
perp_net = float(net.get("perp_net") or 0)
|
||||||
|
# 指数缺失时禁止用永续 U 冒充组合净利(尤其跨币会丢掉期权腿)
|
||||||
combo_usdt = combo_net_usdt(net, option_index=float(idx or 0))
|
combo_usdt = combo_net_usdt(net, option_index=float(idx or 0))
|
||||||
if combo_usdt is None:
|
|
||||||
combo_usdt = round(perp_net, 4)
|
|
||||||
prem_coin = float(net.get("premium") or (float(avg_open or 0) * float(sheets or 0) * float(ct or 0.01)))
|
prem_coin = float(net.get("premium") or (float(avg_open or 0) * float(sheets or 0) * float(ct or 0.01)))
|
||||||
prem_usdt = round(prem_coin * float(idx), 2) if idx is not None and idx > 0 else None
|
prem_usdt = round(prem_coin * float(idx), 2) if idx is not None and idx > 0 else None
|
||||||
|
|
||||||
@@ -393,7 +392,11 @@ def build_live_hedge_cards(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any
|
|||||||
"net_usdt": combo_usdt,
|
"net_usdt": combo_usdt,
|
||||||
"opt_net_usdt": opt_net_usdt,
|
"opt_net_usdt": opt_net_usdt,
|
||||||
"perp_net_usdt": perp_net,
|
"perp_net_usdt": perp_net,
|
||||||
"note": "扣开平双边手续费;永续按盘口均价估",
|
"note": (
|
||||||
|
"期权指数缺失,组合净利暂不可算"
|
||||||
|
if combo_usdt is None
|
||||||
|
else "扣开平双边手续费;永续按盘口均价估"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,19 +46,29 @@ def _perp_live_contracts(cfg: dict[str, Any], symbol: str, direction: str) -> Op
|
|||||||
|
|
||||||
|
|
||||||
def _index_px(cfg: dict[str, Any], underlying: str) -> Optional[float]:
|
def _index_px(cfg: dict[str, Any], underlying: str) -> Optional[float]:
|
||||||
ex = cfg.get("exchange_options")
|
"""标的指数:公共 HTTP 优先,再 cfg.fetch_index_price(ex, uly);勿与异币永续价混用."""
|
||||||
fn = cfg.get("fetch_index_price")
|
from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px
|
||||||
if callable(fn) and ex is not None:
|
|
||||||
|
uly = str(underlying or "ETH").upper()
|
||||||
|
ex_opt = cfg.get("exchange_options")
|
||||||
|
if callable(ex_opt):
|
||||||
try:
|
try:
|
||||||
return fn(ex, underlying)
|
ex_opt = ex_opt()
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
ex_opt = None
|
||||||
# 无期权账户时回退永续 ticker
|
px = resolve_option_index_px(cfg, uly, ex_opt=ex_opt, cross_asset=False, perp_fallback=None)
|
||||||
|
if px is not None and px > 0:
|
||||||
|
return px
|
||||||
|
# 无指数时回退同标的永续 ticker(同币兜底;调用方跨币应分别取期权/永续指数)
|
||||||
ex_perp = cfg.get("exchange")
|
ex_perp = cfg.get("exchange")
|
||||||
|
if callable(ex_perp):
|
||||||
|
try:
|
||||||
|
ex_perp = ex_perp()
|
||||||
|
except Exception:
|
||||||
|
ex_perp = None
|
||||||
if ex_perp is not None:
|
if ex_perp is not None:
|
||||||
try:
|
try:
|
||||||
base = (underlying or "ETH").upper()
|
sym = f"{uly}/USDT:USDT"
|
||||||
sym = f"{base}/USDT:USDT"
|
|
||||||
t = ex_perp.fetch_ticker(sym)
|
t = ex_perp.fetch_ticker(sym)
|
||||||
return _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
|
return _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last"))
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -66,6 +76,28 @@ def _index_px(cfg: dict[str, Any], underlying: str) -> Optional[float]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _option_pnl_to_usdt(
|
||||||
|
opt_pnl: float,
|
||||||
|
option_index: Optional[float],
|
||||||
|
*,
|
||||||
|
inst_id: str = "",
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""期权盈亏统一到 U:币本位 × 指数;USDC 原样.指数缺失且为币本位则 None."""
|
||||||
|
coin = False
|
||||||
|
try:
|
||||||
|
from lib.options.options_pricing_lib import _quote_in_coin_from_context
|
||||||
|
|
||||||
|
coin = bool(_quote_in_coin_from_context(inst_id=inst_id))
|
||||||
|
except Exception:
|
||||||
|
coin = "-USD-" in str(inst_id).upper() and "USDC" not in str(inst_id).upper()
|
||||||
|
if not coin:
|
||||||
|
return round(float(opt_pnl), 4)
|
||||||
|
idx = _sf(option_index)
|
||||||
|
if idx is None or idx <= 0:
|
||||||
|
return None
|
||||||
|
return round(float(opt_pnl) * float(idx), 4)
|
||||||
|
|
||||||
|
|
||||||
def _plan_open_grace_sec() -> float:
|
def _plan_open_grace_sec() -> float:
|
||||||
try:
|
try:
|
||||||
return max(0.0, float(os.getenv("HEDGE_PLAN_OPEN_GRACE_SEC") or "90"))
|
return max(0.0, float(os.getenv("HEDGE_PLAN_OPEN_GRACE_SEC") or "90"))
|
||||||
@@ -378,6 +410,13 @@ def _tick_po_option_primary_watching(
|
|||||||
)
|
)
|
||||||
from lib.hedge_plan.hedge_plan_orders_lib import execute_perp_options_start
|
from lib.hedge_plan.hedge_plan_orders_lib import execute_perp_options_start
|
||||||
from lib.hedge_plan.hedge_plan_register import _activate_watching_po
|
from lib.hedge_plan.hedge_plan_register import _activate_watching_po
|
||||||
|
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||||
|
apply_mode_to_start_body,
|
||||||
|
get_hedge_po_mode,
|
||||||
|
is_cross_asset,
|
||||||
|
option_underlying,
|
||||||
|
perp_underlying,
|
||||||
|
)
|
||||||
|
|
||||||
build_chain = cfg.get("build_option_chain")
|
build_chain = cfg.get("build_option_chain")
|
||||||
ex = cfg.get("exchange_options")
|
ex = cfg.get("exchange_options")
|
||||||
@@ -390,7 +429,15 @@ def _tick_po_option_primary_watching(
|
|||||||
body0 = dict(blob.get("start_body") or blob or {})
|
body0 = dict(blob.get("start_body") or blob or {})
|
||||||
except Exception:
|
except Exception:
|
||||||
body0 = {}
|
body0 = {}
|
||||||
uly = str(plan.get("underlying") or body0.get("underlying") or "ETH").upper()
|
mode = str(plan.get("po_mode") or body0.get("po_mode") or get_hedge_po_mode())
|
||||||
|
uly = str(
|
||||||
|
plan.get("option_underlying")
|
||||||
|
or plan.get("underlying")
|
||||||
|
or body0.get("underlying")
|
||||||
|
or option_underlying(mode)
|
||||||
|
or "ETH"
|
||||||
|
).upper()
|
||||||
|
perp_u = str(plan.get("perp_underlying") or body0.get("perp_underlying") or perp_underlying(mode)).upper()
|
||||||
direction = str(plan.get("direction") or body0.get("direction") or "long").lower()
|
direction = str(plan.get("direction") or body0.get("direction") or "long").lower()
|
||||||
money = str(plan.get("option_moneyness") or body0.get("moneyness") or "otm").lower()
|
money = str(plan.get("option_moneyness") or body0.get("moneyness") or "otm").lower()
|
||||||
interval = plan.get("strike_interval")
|
interval = plan.get("strike_interval")
|
||||||
@@ -425,24 +472,34 @@ def _tick_po_option_primary_watching(
|
|||||||
return None
|
return None
|
||||||
ask = float(cand.get("ask") or 0)
|
ask = float(cand.get("ask") or 0)
|
||||||
ct = float(cand.get("ct_mult") or body0.get("ct_mult") or 0.01)
|
ct = float(cand.get("ct_mult") or body0.get("ct_mult") or 0.01)
|
||||||
|
# 双指数定仓:跨币禁止用 BTC 指数当 ETH 永续价
|
||||||
|
opt_idx = _index_px(cfg, uly) or _sf(cand.get("index_px") or chain.get("index_px"))
|
||||||
|
perp_idx = _index_px(cfg, perp_u) if is_cross_asset(mode) else opt_idx
|
||||||
|
if is_cross_asset(mode) and (not opt_idx or not perp_idx):
|
||||||
|
update_plan(conn, int(plan["id"]), note="盯盘定仓失败:跨币缺期权/永续指数"[:500])
|
||||||
|
return None
|
||||||
sized = size_from_premium(
|
sized = size_from_premium(
|
||||||
premium_budget=float(plan.get("premium_budget") or body0.get("premium_budget") or 0),
|
premium_budget=float(plan.get("premium_budget") or body0.get("premium_budget") or 0),
|
||||||
ask=ask,
|
ask=ask,
|
||||||
ct_mult=ct,
|
ct_mult=ct,
|
||||||
ratio=float(plan.get("option_perp_ratio") or body0.get("option_perp_ratio") or 2),
|
ratio=float(plan.get("option_perp_ratio") or body0.get("option_perp_ratio") or 2),
|
||||||
contract_size=float(body0.get("contract_size") or 0.01),
|
contract_size=float(body0.get("contract_size") or 0.01),
|
||||||
|
option_index=opt_idx,
|
||||||
|
perp_index=perp_idx or opt_idx,
|
||||||
)
|
)
|
||||||
if not sized.get("ok"):
|
if not sized.get("ok"):
|
||||||
update_plan(conn, int(plan["id"]), note=f"盯盘定仓失败: {sized.get('msg')}"[:500])
|
update_plan(conn, int(plan["id"]), note=f"盯盘定仓失败: {sized.get('msg')}"[:500])
|
||||||
return None
|
return None
|
||||||
idx = float(cand.get("index_px") or chain.get("index_px") or 0)
|
|
||||||
body = dict(body0)
|
body = dict(body0)
|
||||||
body.update(
|
body.update(
|
||||||
{
|
{
|
||||||
"plan_type": "perp_options",
|
"plan_type": "perp_options",
|
||||||
"option_primary": True,
|
"option_primary": True,
|
||||||
"watch_entry": 0,
|
"watch_entry": 0,
|
||||||
|
"po_mode": mode,
|
||||||
"underlying": uly,
|
"underlying": uly,
|
||||||
|
"option_underlying": uly,
|
||||||
|
"perp_underlying": perp_u,
|
||||||
"direction": direction,
|
"direction": direction,
|
||||||
"moneyness": money,
|
"moneyness": money,
|
||||||
"opt_inst_id": cand.get("inst_id"),
|
"opt_inst_id": cand.get("inst_id"),
|
||||||
@@ -453,8 +510,9 @@ def _tick_po_option_primary_watching(
|
|||||||
"sheets": sized["sheets"],
|
"sheets": sized["sheets"],
|
||||||
"contracts": sized["contracts"],
|
"contracts": sized["contracts"],
|
||||||
"eth_qty": sized.get("eth_qty"),
|
"eth_qty": sized.get("eth_qty"),
|
||||||
"index_px": idx,
|
# index_px=期权指数;entry=永续参考价(跨币=ETH),禁止写成 BTC
|
||||||
"entry": idx,
|
"index_px": float(opt_idx or 0),
|
||||||
|
"entry": float(perp_idx or opt_idx or 0),
|
||||||
"hours_to_expiry": cand.get("hours_to_expiry"),
|
"hours_to_expiry": cand.get("hours_to_expiry"),
|
||||||
"strike_interval": interval,
|
"strike_interval": interval,
|
||||||
"min_option_hours": min_h,
|
"min_option_hours": min_h,
|
||||||
@@ -463,11 +521,13 @@ def _tick_po_option_primary_watching(
|
|||||||
"option_target_points": plan.get("option_target_points") or body0.get("option_target_points"),
|
"option_target_points": plan.get("option_target_points") or body0.get("option_target_points"),
|
||||||
"perp_target_points": plan.get("perp_target_points") or body0.get("perp_target_points"),
|
"perp_target_points": plan.get("perp_target_points") or body0.get("perp_target_points"),
|
||||||
"premium_budget": plan.get("premium_budget") or body0.get("premium_budget"),
|
"premium_budget": plan.get("premium_budget") or body0.get("premium_budget"),
|
||||||
|
"profit_target_usdt": plan.get("profit_target_usdt") or body0.get("profit_target_usdt"),
|
||||||
"leverage": plan.get("leverage") or body0.get("leverage") or 100,
|
"leverage": plan.get("leverage") or body0.get("leverage") or 100,
|
||||||
"exchange_symbol": body0.get("exchange_symbol") or f"{uly}-USDT-SWAP",
|
"exchange_symbol": body0.get("exchange_symbol") or f"{perp_u}/USDT:USDT",
|
||||||
"contract_size": body0.get("contract_size") or 0.01,
|
"contract_size": body0.get("contract_size") or 0.01,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
body.update(apply_mode_to_start_body(body, mode=mode))
|
||||||
dry = str(os.getenv("HEDGE_PLAN_DRY_RUN") or "").strip().lower() in ("1", "true", "yes", "on")
|
dry = str(os.getenv("HEDGE_PLAN_DRY_RUN") or "").strip().lower() in ("1", "true", "yes", "on")
|
||||||
out = execute_perp_options_start(cfg, body, dry_run=dry, persist=None)
|
out = execute_perp_options_start(cfg, body, dry_run=dry, persist=None)
|
||||||
if not out.get("ok"):
|
if not out.get("ok"):
|
||||||
@@ -571,8 +631,10 @@ def _tick_po_option_primary_both_expired(
|
|||||||
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
|
cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]
|
||||||
) -> Optional[dict[str, Any]]:
|
) -> Optional[dict[str, Any]]:
|
||||||
"""两腿仍 open 但期权已到期:结算期权并市价平永续,避免裸奔."""
|
"""两腿仍 open 但期权已到期:结算期权并市价平永续,避免裸奔."""
|
||||||
|
from lib.hedge_plan.hedge_plan_book_lib import fetch_perp_mark_px
|
||||||
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
|
from lib.hedge_plan.hedge_plan_option_primary_lib import perp_direction_for_view
|
||||||
from lib.hedge_plan.hedge_plan_orders_lib import _close_perp
|
from lib.hedge_plan.hedge_plan_orders_lib import _close_perp
|
||||||
|
from lib.hedge_plan.hedge_po_mode_lib import get_hedge_po_mode, option_underlying, perp_underlying
|
||||||
|
|
||||||
perp = next((x for x in legs if x.get("leg_role") == "perp"), None)
|
perp = next((x for x in legs if x.get("leg_role") == "perp"), None)
|
||||||
opt = next((x for x in legs if x.get("leg_role") == "option_hedge"), None)
|
opt = next((x for x in legs if x.get("leg_role") == "option_hedge"), None)
|
||||||
@@ -582,21 +644,40 @@ def _tick_po_option_primary_both_expired(
|
|||||||
return None
|
return None
|
||||||
if not leg_is_expired(opt):
|
if not leg_is_expired(opt):
|
||||||
return None
|
return None
|
||||||
spot = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
mode = str(plan.get("po_mode") or get_hedge_po_mode())
|
||||||
if spot is None:
|
opt_u = str(plan.get("underlying") or plan.get("option_underlying") or option_underlying(mode) or "ETH").upper()
|
||||||
|
perp_u = str(plan.get("perp_underlying") or perp_underlying(mode) or opt_u).upper()
|
||||||
|
opt_spot = _index_px(cfg, opt_u)
|
||||||
|
if opt_spot is None:
|
||||||
|
return None
|
||||||
|
est = settle_option_leg_at_spot(opt, float(opt_spot))
|
||||||
|
opt_pnl_raw = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
||||||
|
opt_pnl_u = _option_pnl_to_usdt(
|
||||||
|
opt_pnl_raw, opt_spot, inst_id=str(opt.get("inst_id") or "")
|
||||||
|
)
|
||||||
|
if opt_pnl_u is None:
|
||||||
return None
|
return None
|
||||||
est = settle_option_leg_at_spot(opt, float(spot))
|
|
||||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||||
("closed", "expiry", _now(), round(opt_pnl, 4), opt["id"]),
|
("closed", "expiry", _now(), round(opt_pnl_u, 4), opt["id"]),
|
||||||
)
|
)
|
||||||
view = str(plan.get("direction") or "long").lower()
|
view = str(plan.get("direction") or "long").lower()
|
||||||
perp_dir = str(plan.get("perp_direction") or perp.get("side") or perp_direction_for_view(view)).lower()
|
perp_dir = str(plan.get("perp_direction") or perp.get("side") or perp_direction_for_view(view)).lower()
|
||||||
symbol = str(perp.get("symbol") or "")
|
symbol = str(perp.get("symbol") or f"{perp_u}/USDT:USDT")
|
||||||
contracts = float(perp.get("size") or plan.get("perp_size") or 0)
|
contracts = float(perp.get("size") or plan.get("perp_size") or 0)
|
||||||
perp_close = _close_perp(cfg, symbol=symbol, direction=perp_dir, contracts=contracts, dry_run=False)
|
perp_close = _close_perp(cfg, symbol=symbol, direction=perp_dir, contracts=contracts, dry_run=False)
|
||||||
entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or float(spot)
|
ex_perp = cfg.get("exchange")
|
||||||
|
if callable(ex_perp):
|
||||||
|
try:
|
||||||
|
ex_perp = ex_perp()
|
||||||
|
except Exception:
|
||||||
|
ex_perp = None
|
||||||
|
perp_mark = fetch_perp_mark_px(ex_perp, symbol) if ex_perp is not None else None
|
||||||
|
if perp_mark is None or float(perp_mark) <= 0:
|
||||||
|
perp_mark = _index_px(cfg, perp_u)
|
||||||
|
entry = _sf(perp.get("avg_open")) or _sf(plan.get("entry_mark")) or _sf(perp_mark)
|
||||||
|
if entry is None or perp_mark is None:
|
||||||
|
return None
|
||||||
cs = float(cfg.get("default_contract_size") or 0.01)
|
cs = float(cfg.get("default_contract_size") or 0.01)
|
||||||
get_cs = cfg.get("get_contract_size")
|
get_cs = cfg.get("get_contract_size")
|
||||||
if callable(get_cs) and symbol:
|
if callable(get_cs) and symbol:
|
||||||
@@ -606,9 +687,9 @@ def _tick_po_option_primary_both_expired(
|
|||||||
pass
|
pass
|
||||||
coins = contracts * cs
|
coins = contracts * cs
|
||||||
if perp_dir == "short":
|
if perp_dir == "short":
|
||||||
perp_pnl = (float(entry) - float(spot)) * coins
|
perp_pnl = (float(entry) - float(perp_mark)) * coins
|
||||||
else:
|
else:
|
||||||
perp_pnl = (float(spot) - float(entry)) * coins
|
perp_pnl = (float(perp_mark) - float(entry)) * coins
|
||||||
if not perp_close.get("ok"):
|
if not perp_close.get("ok"):
|
||||||
notify_hedge(
|
notify_hedge(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -622,7 +703,7 @@ def _tick_po_option_primary_both_expired(
|
|||||||
conn,
|
conn,
|
||||||
int(plan["id"]),
|
int(plan["id"]),
|
||||||
close_reason="opt_target_perp_pending",
|
close_reason="opt_target_perp_pending",
|
||||||
realized_pnl_options=round(opt_pnl, 4),
|
realized_pnl_options=round(opt_pnl_u, 4),
|
||||||
note="期权已到期结算,永续待平",
|
note="期权已到期结算,永续待平",
|
||||||
)
|
)
|
||||||
return {"plan_id": plan["id"], "retry": True, "perp_close": perp_close}
|
return {"plan_id": plan["id"], "retry": True, "perp_close": perp_close}
|
||||||
@@ -630,14 +711,14 @@ def _tick_po_option_primary_both_expired(
|
|||||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||||
("closed", "option_expired", _now(), round(perp_pnl, 4), perp["id"]),
|
("closed", "option_expired", _now(), round(perp_pnl, 4), perp["id"]),
|
||||||
)
|
)
|
||||||
total = opt_pnl + perp_pnl
|
total = float(opt_pnl_u) + float(perp_pnl)
|
||||||
update_plan(
|
update_plan(
|
||||||
conn,
|
conn,
|
||||||
int(plan["id"]),
|
int(plan["id"]),
|
||||||
status="closed",
|
status="closed",
|
||||||
close_reason="option_expired",
|
close_reason="option_expired",
|
||||||
realized_pnl_perp=round(perp_pnl, 4),
|
realized_pnl_perp=round(perp_pnl, 4),
|
||||||
realized_pnl_options=round(opt_pnl, 4),
|
realized_pnl_options=round(opt_pnl_u, 4),
|
||||||
realized_pnl_total=round(total, 4),
|
realized_pnl_total=round(total, 4),
|
||||||
stats_bucket="opt_primary",
|
stats_bucket="opt_primary",
|
||||||
closed_at=_now(),
|
closed_at=_now(),
|
||||||
@@ -658,24 +739,33 @@ def _tick_po_option_primary_expiry(
|
|||||||
return None
|
return None
|
||||||
if not leg_is_expired(opt):
|
if not leg_is_expired(opt):
|
||||||
return None
|
return None
|
||||||
spot = _index_px(cfg, str(plan.get("underlying") or "ETH"))
|
from lib.hedge_plan.hedge_po_mode_lib import get_hedge_po_mode, option_underlying
|
||||||
|
|
||||||
|
mode = str(plan.get("po_mode") or get_hedge_po_mode())
|
||||||
|
opt_u = str(plan.get("underlying") or plan.get("option_underlying") or option_underlying(mode) or "ETH").upper()
|
||||||
|
spot = _index_px(cfg, opt_u)
|
||||||
if spot is None:
|
if spot is None:
|
||||||
return None
|
return None
|
||||||
est = settle_option_leg_at_spot(opt, float(spot))
|
est = settle_option_leg_at_spot(opt, float(spot))
|
||||||
opt_pnl = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
opt_pnl_raw = _option_leg_pnl_after_close(cfg, opt, fallback=est)
|
||||||
|
opt_pnl_u = _option_pnl_to_usdt(
|
||||||
|
opt_pnl_raw, spot, inst_id=str(opt.get("inst_id") or "")
|
||||||
|
)
|
||||||
|
if opt_pnl_u is None:
|
||||||
|
return None
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
"UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?",
|
||||||
("closed", "expiry", _now(), round(opt_pnl, 4), opt["id"]),
|
("closed", "expiry", _now(), round(opt_pnl_u, 4), opt["id"]),
|
||||||
)
|
)
|
||||||
perp_pnl = float(perp.get("realized_pnl") or 0) if perp else float(plan.get("realized_pnl_perp") or 0)
|
perp_pnl = float(perp.get("realized_pnl") or 0) if perp else float(plan.get("realized_pnl_perp") or 0)
|
||||||
total = perp_pnl + opt_pnl
|
total = float(perp_pnl) + float(opt_pnl_u)
|
||||||
update_plan(
|
update_plan(
|
||||||
conn,
|
conn,
|
||||||
int(plan["id"]),
|
int(plan["id"]),
|
||||||
status="closed",
|
status="closed",
|
||||||
close_reason="perp_target_points_expiry",
|
close_reason="perp_target_points_expiry",
|
||||||
realized_pnl_perp=round(perp_pnl, 4),
|
realized_pnl_perp=round(perp_pnl, 4),
|
||||||
realized_pnl_options=round(opt_pnl, 4),
|
realized_pnl_options=round(opt_pnl_u, 4),
|
||||||
realized_pnl_total=round(total, 4),
|
realized_pnl_total=round(total, 4),
|
||||||
stats_bucket="opt_primary",
|
stats_bucket="opt_primary",
|
||||||
closed_at=_now(),
|
closed_at=_now(),
|
||||||
|
|||||||
@@ -1282,10 +1282,15 @@ def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]:
|
|||||||
opt_type = "C"
|
opt_type = "C"
|
||||||
if opt_type not in ("P", "C"):
|
if opt_type not in ("P", "C"):
|
||||||
return "缺少期权类型(Put/Call)"
|
return "缺少期权类型(Put/Call)"
|
||||||
if direction == "long" and opt_type != "P":
|
# 与 opt_type_for_view 一致:做多→Call+空永续,做空→Put+多永续
|
||||||
return "做多永期对冲须用 Put"
|
from lib.hedge_plan.hedge_plan_option_primary_lib import opt_type_for_view
|
||||||
if direction == "short" and opt_type != "C":
|
|
||||||
return "做空永期对冲须用 Call"
|
want = opt_type_for_view(direction)
|
||||||
|
if opt_type != want:
|
||||||
|
return (
|
||||||
|
f"做{'多' if direction == 'long' else '空'}永期对冲须用 "
|
||||||
|
f"{'Call' if want == 'C' else 'Put'}"
|
||||||
|
)
|
||||||
if direction == "long" and not (sl < entry < tp):
|
if direction == "long" and not (sl < entry < tp):
|
||||||
return "做多须满足 止损 < 入场 < 止盈"
|
return "做多须满足 止损 < 入场 < 止盈"
|
||||||
if direction == "short" and not (tp < entry < sl):
|
if direction == "short" and not (tp < entry < sl):
|
||||||
@@ -1775,13 +1780,19 @@ def execute_manual_close_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> d
|
|||||||
notes.append("卖币失败: " + str(sell_r.get("msg") or ""))
|
notes.append("卖币失败: " + str(sell_r.get("msg") or ""))
|
||||||
|
|
||||||
total = float(opt_pnl) + float(perp_pnl)
|
total = float(opt_pnl) + float(perp_pnl)
|
||||||
# 期权币本位盈亏换算到 U:用期权标的指数(跨币=BTC,禁止×ETH)
|
# 币本位期权盈亏一律 × 期权指数折 U(跨币=BTC);不再用 |pnl|<10 启发式
|
||||||
try:
|
try:
|
||||||
from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px
|
from lib.hedge_plan.hedge_plan_live_positions_lib import resolve_option_index_px
|
||||||
from lib.hedge_plan.hedge_po_mode_lib import get_hedge_po_mode, is_cross_asset, option_underlying
|
from lib.hedge_plan.hedge_po_mode_lib import get_hedge_po_mode, is_cross_asset, option_underlying
|
||||||
|
from lib.options.options_pricing_lib import _quote_in_coin_from_context
|
||||||
|
|
||||||
mode = str(plan.get("po_mode") or get_hedge_po_mode())
|
mode = str(plan.get("po_mode") or get_hedge_po_mode())
|
||||||
opt_u = str(plan.get("option_underlying") or plan.get("underlying") or option_underlying(mode) or uly).upper()
|
opt_u = str(plan.get("option_underlying") or plan.get("underlying") or option_underlying(mode) or uly).upper()
|
||||||
|
inst = ""
|
||||||
|
if opt:
|
||||||
|
inst = str(opt.get("inst_id") or "")
|
||||||
|
coin = bool(_quote_in_coin_from_context(inst_id=inst)) if inst else True
|
||||||
|
if coin:
|
||||||
ex_idx = cfg.get("exchange_options") or cfg.get("exchange")
|
ex_idx = cfg.get("exchange_options") or cfg.get("exchange")
|
||||||
if callable(ex_idx):
|
if callable(ex_idx):
|
||||||
try:
|
try:
|
||||||
@@ -1798,11 +1809,9 @@ def execute_manual_close_plan(cfg: dict[str, Any], conn: Any, plan_id: int) -> d
|
|||||||
)
|
)
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
# opt_pnl 可能已是币;若为币且绝对值很小则 × 指数
|
opt_pnl_u = float(opt_pnl) * idx if idx > 0 else float(opt_pnl)
|
||||||
if abs(opt_pnl) < 10 and idx > 0 and abs(opt_pnl * idx) > abs(opt_pnl):
|
|
||||||
opt_pnl_u = opt_pnl * idx
|
|
||||||
else:
|
else:
|
||||||
opt_pnl_u = opt_pnl
|
opt_pnl_u = float(opt_pnl)
|
||||||
except Exception:
|
except Exception:
|
||||||
opt_pnl_u = opt_pnl
|
opt_pnl_u = opt_pnl
|
||||||
|
|
||||||
|
|||||||
@@ -509,11 +509,24 @@ def _activate_watching_po(
|
|||||||
str((perp or {}).get("direction") or "")
|
str((perp or {}).get("direction") or "")
|
||||||
or perp_direction_for_view(view)
|
or perp_direction_for_view(view)
|
||||||
)
|
)
|
||||||
|
# 永续开仓价:优先成交回写,否则 body.entry(应为永续标的价,跨币=ETH)
|
||||||
|
perp_avg = None
|
||||||
|
try:
|
||||||
|
order = (perp or {}).get("order") or {}
|
||||||
|
perp_avg = float(
|
||||||
|
order.get("average")
|
||||||
|
or order.get("avgPx")
|
||||||
|
or (order.get("info") or {}).get("avgPx")
|
||||||
|
or body.get("entry")
|
||||||
|
or 0
|
||||||
|
) or None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
perp_avg = float(body.get("entry") or 0) or None
|
||||||
update_plan(
|
update_plan(
|
||||||
conn,
|
conn,
|
||||||
int(plan_id),
|
int(plan_id),
|
||||||
status="partial" if is_partial else "active",
|
status="partial" if is_partial else "active",
|
||||||
entry_mark=float(body.get("entry") or body.get("index_px") or 0) or None,
|
entry_mark=perp_avg,
|
||||||
perp_size=float((perp or {}).get("contracts") or body.get("contracts") or 0),
|
perp_size=float((perp or {}).get("contracts") or body.get("contracts") or 0),
|
||||||
leverage=float(body.get("leverage") or 100),
|
leverage=float(body.get("leverage") or 100),
|
||||||
premium_total=premium,
|
premium_total=premium,
|
||||||
@@ -531,7 +544,7 @@ def _activate_watching_po(
|
|||||||
"symbol": str(body.get("exchange_symbol") or ""),
|
"symbol": str(body.get("exchange_symbol") or ""),
|
||||||
"side": perp_dir,
|
"side": perp_dir,
|
||||||
"size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
|
"size": float((perp or {}).get("contracts") or body.get("contracts") or 0),
|
||||||
"avg_open": float(body.get("entry") or 0) if perp_ok else None,
|
"avg_open": perp_avg if perp_ok else None,
|
||||||
"status": "open" if perp_ok else "pending",
|
"status": "open" if perp_ok else "pending",
|
||||||
"exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
|
"exchange_ord_id": str((perp or {}).get("exchange_ord_id") or ""),
|
||||||
"opened_at": result.get("opened_at") if perp_ok else None,
|
"opened_at": result.get("opened_at") if perp_ok else None,
|
||||||
@@ -1455,12 +1468,43 @@ def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
err = validate_option_primary_start(body)
|
err = validate_option_primary_start(body)
|
||||||
if err:
|
if err:
|
||||||
raise ValueError(err)
|
raise ValueError(err)
|
||||||
|
from lib.hedge_plan.hedge_po_mode_lib import (
|
||||||
|
apply_mode_to_start_body,
|
||||||
|
get_hedge_po_mode,
|
||||||
|
is_cross_asset,
|
||||||
|
option_underlying,
|
||||||
|
perp_underlying,
|
||||||
|
)
|
||||||
|
|
||||||
|
body = apply_mode_to_start_body(body)
|
||||||
|
mode = str(body.get("po_mode") or get_hedge_po_mode())
|
||||||
|
opt_idx = None
|
||||||
|
perp_idx = None
|
||||||
|
try:
|
||||||
|
from lib.exchange.okx_public_http_lib import fetch_index_px
|
||||||
|
|
||||||
|
opt_idx = float(fetch_index_px(option_underlying(mode)) or 0) or None
|
||||||
|
if is_cross_asset(mode):
|
||||||
|
perp_idx = float(fetch_index_px(perp_underlying(mode)) or 0) or None
|
||||||
|
else:
|
||||||
|
perp_idx = opt_idx
|
||||||
|
except Exception:
|
||||||
|
opt_idx = perp_idx = None
|
||||||
|
if opt_idx is None:
|
||||||
|
try:
|
||||||
|
opt_idx = float(body.get("index_px") or body.get("entry") or 0) or None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
opt_idx = None
|
||||||
|
if perp_idx is None:
|
||||||
|
perp_idx = opt_idx
|
||||||
sized = size_from_premium(
|
sized = size_from_premium(
|
||||||
premium_budget=float(body.get("premium_budget") or 0),
|
premium_budget=float(body.get("premium_budget") or 0),
|
||||||
ask=float(body.get("ask") or 0),
|
ask=float(body.get("ask") or 0),
|
||||||
ct_mult=float(body.get("ct_mult") or 0.01),
|
ct_mult=float(body.get("ct_mult") or 0.01),
|
||||||
ratio=float(body.get("option_perp_ratio") or 2),
|
ratio=float(body.get("option_perp_ratio") or 2),
|
||||||
contract_size=float(body.get("contract_size") or 0.01),
|
contract_size=float(body.get("contract_size") or 0.01),
|
||||||
|
option_index=opt_idx,
|
||||||
|
perp_index=perp_idx,
|
||||||
)
|
)
|
||||||
if not sized.get("ok"):
|
if not sized.get("ok"):
|
||||||
raise ValueError(sized.get("msg") or "定仓失败")
|
raise ValueError(sized.get("msg") or "定仓失败")
|
||||||
@@ -1468,19 +1512,25 @@ def _preview_po(body: dict[str, Any]) -> dict[str, Any]:
|
|||||||
body["sheets"] = sized["sheets"]
|
body["sheets"] = sized["sheets"]
|
||||||
body["contracts"] = sized["contracts"]
|
body["contracts"] = sized["contracts"]
|
||||||
body["eth_qty"] = sized["eth_qty"]
|
body["eth_qty"] = sized["eth_qty"]
|
||||||
|
if not body.get("index_px") and opt_idx:
|
||||||
|
body["index_px"] = opt_idx
|
||||||
|
# entry=永续参考价(跨币用 ETH 指数);勿把 BTC 指数写入永续开仓价
|
||||||
if not body.get("entry"):
|
if not body.get("entry"):
|
||||||
body["entry"] = body.get("index_px") or 0
|
body["entry"] = perp_idx or opt_idx or body.get("index_px") or 0
|
||||||
out = build_option_primary_preview(body)
|
out = build_option_primary_preview(body)
|
||||||
out["sizing"] = sized
|
out["sizing"] = sized
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
from lib.hedge_plan.hedge_plan_option_primary_lib import opt_type_for_view
|
||||||
|
|
||||||
direction = str(body.get("direction") or "long").lower()
|
direction = str(body.get("direction") or "long").lower()
|
||||||
entry = float(body["entry"])
|
entry = float(body["entry"])
|
||||||
tp = float(body["tp"])
|
tp = float(body["tp"])
|
||||||
sl = float(body["sl"])
|
sl = float(body["sl"])
|
||||||
contracts = float(body["contracts"])
|
contracts = float(body["contracts"])
|
||||||
contract_size = float(body.get("contract_size") or 0.01)
|
contract_size = float(body.get("contract_size") or 0.01)
|
||||||
opt_type = str(body.get("opt_type") or ("P" if direction == "long" else "C"))
|
# 与以期权为主同口径:long→Call, short→Put(旧保险 long→Put 已废弃)
|
||||||
|
opt_type = str(body.get("opt_type") or opt_type_for_view(direction))
|
||||||
strike = float(body["strike"])
|
strike = float(body["strike"])
|
||||||
sheets = float(body.get("sheets") or 1)
|
sheets = float(body.get("sheets") or 1)
|
||||||
ct_mult = float(body.get("ct_mult") or 0.01)
|
ct_mult = float(body.get("ct_mult") or 0.01)
|
||||||
|
|||||||
@@ -35,14 +35,40 @@ def leg_is_expired(leg: dict[str, Any], *, now_ms: Optional[int] = None) -> bool
|
|||||||
|
|
||||||
|
|
||||||
def settle_option_leg_at_spot(leg: dict[str, Any], spot: float) -> float:
|
def settle_option_leg_at_spot(leg: dict[str, Any], spot: float) -> float:
|
||||||
"""按到期结算口径估算腿盈亏(USDC)."""
|
"""按到期结算估算腿盈亏(与权利金同单位).
|
||||||
|
|
||||||
|
- 币本位(BTC-USD/ETH-USD):返回 **币** 盈亏(内在价值/币 − 权利金币);调用方须 × 期权指数折 U.
|
||||||
|
- USDC:返回 U 盈亏.
|
||||||
|
"""
|
||||||
premium = float(leg.get("premium") or 0)
|
premium = float(leg.get("premium") or 0)
|
||||||
strike = _sf(leg.get("strike"))
|
strike = _sf(leg.get("strike"))
|
||||||
if strike is None:
|
if strike is None:
|
||||||
return -premium
|
return -premium
|
||||||
sheets = float(leg.get("size") or 1)
|
sheets = float(leg.get("size") or 1)
|
||||||
# ct_mult 未入库时默认 0.01
|
|
||||||
ct = float(leg.get("ct_mult") or 0.01)
|
ct = float(leg.get("ct_mult") or 0.01)
|
||||||
|
inst_id = str(leg.get("inst_id") or "")
|
||||||
|
try:
|
||||||
|
from lib.options.options_pricing_lib import _quote_in_coin_from_context, intrinsic_px_per_unit
|
||||||
|
|
||||||
|
coin = _quote_in_coin_from_context(inst_id=inst_id)
|
||||||
|
except Exception:
|
||||||
|
coin = "-USD-" in inst_id.upper() and "USDC" not in inst_id.upper()
|
||||||
|
intrinsic_px_per_unit = None # type: ignore
|
||||||
|
if coin:
|
||||||
|
unit = None
|
||||||
|
if intrinsic_px_per_unit is not None:
|
||||||
|
try:
|
||||||
|
unit = intrinsic_px_per_unit(
|
||||||
|
str(leg.get("opt_type") or "P"),
|
||||||
|
float(strike),
|
||||||
|
float(spot),
|
||||||
|
inst_id=inst_id,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
unit = None
|
||||||
|
if unit is None:
|
||||||
|
unit = 0.0
|
||||||
|
return float(unit) * sheets * ct - premium
|
||||||
return float(
|
return float(
|
||||||
option_expiry_pnl(
|
option_expiry_pnl(
|
||||||
opt_type=str(leg.get("opt_type") or "P"),
|
opt_type=str(leg.get("opt_type") or "P"),
|
||||||
|
|||||||
@@ -508,4 +508,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/hedge_plan.js?v=67"></script>
|
<script src="/static/hedge_plan.js?v=68"></script>
|
||||||
|
|||||||
@@ -214,6 +214,45 @@ class TestHedgePoMode(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertAlmostEqual(px, 2476.0)
|
self.assertAlmostEqual(px, 2476.0)
|
||||||
|
|
||||||
|
def test_settle_coin_option_returns_coin_units(self):
|
||||||
|
from lib.hedge_plan.hedge_plan_settle_lib import settle_option_leg_at_spot
|
||||||
|
|
||||||
|
# BTC put ITM: K=90000 S=80000 → 币内在 (90000-80000)/80000=0.125; 1张×0.01=0.00125币
|
||||||
|
# 权利金 0.01 币 → 净利 0.00125-0.01 = -0.00875 币(非 ×8万的美元差)
|
||||||
|
pnl = settle_option_leg_at_spot(
|
||||||
|
{
|
||||||
|
"inst_id": "BTC-USD-261018-90000-P",
|
||||||
|
"opt_type": "P",
|
||||||
|
"strike": 90000,
|
||||||
|
"size": 1,
|
||||||
|
"ct_mult": 0.01,
|
||||||
|
"premium": 0.01,
|
||||||
|
},
|
||||||
|
80000.0,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(pnl, 0.00125 - 0.01, places=6)
|
||||||
|
|
||||||
|
def test_size_from_premium_preview_needs_dual_index(self):
|
||||||
|
# 无双指数 → 同币路径;有双指数 → 跨币张数
|
||||||
|
same = size_from_premium(
|
||||||
|
premium_budget=1000,
|
||||||
|
ask=0.01,
|
||||||
|
ct_mult=0.01,
|
||||||
|
ratio=2,
|
||||||
|
contract_size=0.01,
|
||||||
|
)
|
||||||
|
cross = size_from_premium(
|
||||||
|
premium_budget=1000,
|
||||||
|
ask=0.01,
|
||||||
|
ct_mult=0.01,
|
||||||
|
ratio=2,
|
||||||
|
contract_size=0.01,
|
||||||
|
option_index=100_000,
|
||||||
|
perp_index=2_500,
|
||||||
|
)
|
||||||
|
self.assertTrue(same["ok"] and cross["ok"])
|
||||||
|
self.assertGreater(float(cross["contracts"]), float(same["contracts"]))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user