Close residual options when premium recovers above configurable threshold.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
"""残留期权:权利金回升达标后中途平。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from app.models.db import Database
|
||||
from app.sim.matcher import Matcher
|
||||
|
||||
|
||||
def _seed_residual(
|
||||
db: Database,
|
||||
*,
|
||||
group_id: str = "G-res",
|
||||
initial_premium: float = 100.0,
|
||||
qty: float = 2.0,
|
||||
entry_px: float = 50.0,
|
||||
) -> None:
|
||||
now = 1_700_000_000_000
|
||||
with db._lock:
|
||||
db._conn.execute(
|
||||
"""INSERT INTO groups(
|
||||
group_id, status, bias, option_side, perp_side, option_inst_id,
|
||||
strike, expiry_ymd, initial_premium, open_at_ms, close_at_ms,
|
||||
close_reason, realized_pnl, fees, slip_cost
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"option_residual",
|
||||
"test",
|
||||
"call",
|
||||
"short",
|
||||
"ETH-USD_UM-260801-2000-C",
|
||||
2000.0,
|
||||
"260801",
|
||||
initial_premium,
|
||||
now - 10_000,
|
||||
now - 5_000,
|
||||
"target_perp_only",
|
||||
10.0,
|
||||
1.0,
|
||||
0.0,
|
||||
),
|
||||
)
|
||||
db._conn.execute(
|
||||
"""INSERT INTO residual_options(
|
||||
group_id, option_inst_id, option_side, option_qty_eth, option_qty_contracts,
|
||||
option_entry_px, strike, expiry_ymd, expiry_ms, entry_index_px,
|
||||
initial_premium, status, created_at_ms, note
|
||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||||
(
|
||||
group_id,
|
||||
"ETH-USD_UM-260801-2000-C",
|
||||
"call",
|
||||
qty,
|
||||
200.0,
|
||||
entry_px,
|
||||
2000.0,
|
||||
"260801",
|
||||
now + 86_400_000,
|
||||
1900.0,
|
||||
initial_premium,
|
||||
"pending",
|
||||
now - 5_000,
|
||||
"test residual",
|
||||
),
|
||||
)
|
||||
db._conn.commit()
|
||||
|
||||
|
||||
def test_residual_premium_below_threshold_skips(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("MODE", "SIM")
|
||||
db = Database(tmp_path / "below.db")
|
||||
db.set_setting("residual_min_premium_pct", "20")
|
||||
_seed_residual(db, initial_premium=100.0, qty=2.0)
|
||||
m = Matcher(db)
|
||||
|
||||
# bid=5 → premium=10 < 20
|
||||
oq = SimpleNamespace(bid=5.0, ask=5.5, bid_sz=10_000.0, mark_px=5.0)
|
||||
monkeypatch.setattr(m, "_quote_held_option", lambda _id: oq)
|
||||
monkeypatch.setattr(m, "_close_spot_px", lambda _snap: 1900.0)
|
||||
|
||||
assert m.try_close_one_residual(m.list_residual_options()[0]) is None
|
||||
row = db.fetchone(
|
||||
"SELECT status FROM residual_options WHERE group_id=?", ("G-res",)
|
||||
)
|
||||
assert row is not None and row["status"] == "pending"
|
||||
db.close()
|
||||
|
||||
|
||||
def test_residual_premium_above_threshold_closes(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("MODE", "SIM")
|
||||
db = Database(tmp_path / "above.db")
|
||||
db.set_setting("residual_min_premium_pct", "20")
|
||||
_seed_residual(db, initial_premium=100.0, qty=2.0, entry_px=50.0)
|
||||
m = Matcher(db)
|
||||
|
||||
# bid=15 → premium=30 >= 20
|
||||
oq = SimpleNamespace(bid=15.0, ask=15.5, bid_sz=10_000.0, mark_px=15.0)
|
||||
monkeypatch.setattr(m, "_quote_held_option", lambda _id: oq)
|
||||
monkeypatch.setattr(m, "_close_spot_px", lambda _snap: 1900.0)
|
||||
monkeypatch.setattr(m, "_ct_mult", lambda _id: 0.01)
|
||||
|
||||
out = m.try_close_one_residual(m.list_residual_options()[0])
|
||||
assert out is not None
|
||||
assert out["reason"] == "residual_premium_close"
|
||||
row = db.fetchone(
|
||||
"SELECT status, settle_px FROM residual_options WHERE group_id=?", ("G-res",)
|
||||
)
|
||||
assert row is not None and row["status"] == "settled"
|
||||
g = db.fetchone("SELECT status FROM groups WHERE group_id=?", ("G-res",))
|
||||
assert g is not None and g["status"] == "closed"
|
||||
db.close()
|
||||
|
||||
|
||||
def test_residual_liquidity_fail_skips(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("MODE", "SIM")
|
||||
db = Database(tmp_path / "liq.db")
|
||||
db.set_setting("residual_min_premium_pct", "20")
|
||||
_seed_residual(db, initial_premium=100.0, qty=2.0)
|
||||
m = Matcher(db)
|
||||
|
||||
# premium ok but depth tiny
|
||||
oq = SimpleNamespace(bid=15.0, ask=15.5, bid_sz=1.0, mark_px=15.0)
|
||||
monkeypatch.setattr(m, "_quote_held_option", lambda _id: oq)
|
||||
monkeypatch.setattr(m, "_close_spot_px", lambda _snap: 1900.0)
|
||||
monkeypatch.setattr(m, "_ct_mult", lambda _id: 0.01)
|
||||
|
||||
assert m.try_close_one_residual(m.list_residual_options()[0]) is None
|
||||
row = db.fetchone(
|
||||
"SELECT status FROM residual_options WHERE group_id=?", ("G-res",)
|
||||
)
|
||||
assert row is not None and row["status"] == "pending"
|
||||
db.close()
|
||||
|
||||
|
||||
def test_settings_exposes_residual_min_premium_pct(tmp_path, monkeypatch) -> None:
|
||||
monkeypatch.setenv("MODE", "SIM")
|
||||
from app.api import settings as settings_api
|
||||
from app.models.db import set_db
|
||||
|
||||
d = Database(tmp_path / "set.db")
|
||||
set_db(d)
|
||||
try:
|
||||
d.set_setting("residual_min_premium_pct", "35")
|
||||
payload = settings_api._read_settings()
|
||||
assert float(payload["residual_min_premium_pct"]) == 35.0
|
||||
finally:
|
||||
set_db(None)
|
||||
d.close()
|
||||
Reference in New Issue
Block a user