Revise strategy: TTM+ATM+leverage option pick, % exit, perp leverage/margin.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""策略状态机:选向开仓 / 盯盘平仓 / 休息 / 限轮。"""
|
||||
"""策略状态机:选向开仓 / 盯盘平仓 / 休息(无开仓窗、无轮次上限)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,10 +12,9 @@ from ..market import get_gateway
|
||||
from ..models.db import get_db
|
||||
from ..sim.ledger import Ledger
|
||||
from ..sim.matcher import Matcher
|
||||
from .clock import can_open_new, window_key
|
||||
from .clock import window_key
|
||||
from .exits import check_exits
|
||||
from .group import next_group_id
|
||||
from .signal import decide
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,15 +32,18 @@ class StrategyEngine:
|
||||
assert row is not None
|
||||
upl = self.matcher.unrealized()
|
||||
s = get_settings()
|
||||
exit_pts = self.ledger.get_setting_float("exit_move_points", s.exit_move_points)
|
||||
exit_pct = self.ledger.get_setting_float("exit_move_pct", s.exit_move_pct)
|
||||
rest_sec = self.ledger.get_setting_int("rest_seconds", s.rest_seconds)
|
||||
max_rounds = self.ledger.get_setting_int("max_rounds", s.max_rounds)
|
||||
leverage = self.ledger.get_setting_float("leverage", s.leverage)
|
||||
min_hours = self.ledger.get_setting_float("min_option_hours", s.min_option_hours)
|
||||
min_opt_lev = self.ledger.get_setting_float(
|
||||
"min_option_leverage", s.min_option_leverage
|
||||
)
|
||||
rest_until = row["rest_until_ms"]
|
||||
rest_left = 0
|
||||
if rest_until:
|
||||
rest_left = max(0, int((int(rest_until) - time.time() * 1000) / 1000))
|
||||
last_error = row["last_error"]
|
||||
# 清掉已修复的旧序列化错误残留
|
||||
if last_error and "PriceResult" in str(last_error) and "__dict__" in str(last_error):
|
||||
self._set_state(last_error=None)
|
||||
last_error = None
|
||||
@@ -49,13 +51,15 @@ class StrategyEngine:
|
||||
"running": bool(row["running"]),
|
||||
"phase": row["phase"],
|
||||
"rounds_done": int(row["rounds_done"] or 0),
|
||||
"max_rounds": max_rounds,
|
||||
"window_key": row["window_key"],
|
||||
"rest_until_ms": rest_until,
|
||||
"rest_left_sec": rest_left,
|
||||
"rest_seconds": rest_sec,
|
||||
"exit_move_points": exit_pts,
|
||||
"can_open": can_open_new(open_hhmm=s.open_hhmm, stop_hhmm=s.stop_open_hhmm),
|
||||
"exit_move_pct": exit_pct,
|
||||
"leverage": leverage,
|
||||
"min_option_hours": min_hours,
|
||||
"min_option_leverage": min_opt_lev,
|
||||
"can_open": True,
|
||||
"last_error": last_error,
|
||||
"position": upl,
|
||||
"ledger": self.ledger.snapshot(),
|
||||
@@ -103,23 +107,14 @@ class StrategyEngine:
|
||||
assert row is not None
|
||||
rounds = int(row["rounds_done"] or 0) + 1
|
||||
rest_sec = self.ledger.get_setting_int("rest_seconds", s.rest_seconds)
|
||||
max_rounds = self.ledger.get_setting_int("max_rounds", s.max_rounds)
|
||||
rest_until = int(time.time() * 1000) + rest_sec * 1000
|
||||
if rounds >= max_rounds:
|
||||
self._set_state(
|
||||
rounds_done=rounds,
|
||||
phase="stopped",
|
||||
rest_until_ms=None,
|
||||
)
|
||||
else:
|
||||
self._set_state(
|
||||
rounds_done=rounds,
|
||||
phase="resting",
|
||||
rest_until_ms=rest_until,
|
||||
)
|
||||
self._set_state(
|
||||
rounds_done=rounds,
|
||||
phase="resting",
|
||||
rest_until_ms=rest_until,
|
||||
)
|
||||
|
||||
def _count_groups_for_window(self, wkey: str) -> int:
|
||||
# group_id like G-20260724-01 ; window_key is YYYYMMDD
|
||||
def _count_groups_for_day(self, wkey: str) -> int:
|
||||
rows = self.db.fetchall(
|
||||
"SELECT group_id FROM groups WHERE group_id LIKE ?",
|
||||
(f"G-{wkey}-%",),
|
||||
@@ -135,12 +130,11 @@ class StrategyEngine:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
async with self._lock:
|
||||
# 空仓且 ATM 偏离现价时先重选,再跑开仓逻辑
|
||||
try:
|
||||
await get_gateway().ensure_atm_async(force=False)
|
||||
except Exception as e:
|
||||
logger.warning("ATM ensure before tick failed: %s", e)
|
||||
await asyncio.to_thread(self._tick)
|
||||
await self._tick_async()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -148,34 +142,33 @@ class StrategyEngine:
|
||||
self._set_state(last_error=str(e))
|
||||
await asyncio.sleep(1)
|
||||
|
||||
def _tick(self) -> None:
|
||||
async def _tick_async(self) -> None:
|
||||
s = get_settings()
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
wkey = window_key()
|
||||
if st["window_key"] != wkey:
|
||||
# 新业务窗重置轮次
|
||||
self._set_state(window_key=wkey, rounds_done=0, phase="idle", rest_until_ms=None)
|
||||
self._set_state(window_key=wkey, phase="idle")
|
||||
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
max_rounds = self.ledger.get_setting_int("max_rounds", s.max_rounds)
|
||||
exit_pts = self.ledger.get_setting_float("exit_move_points", s.exit_move_points)
|
||||
exit_pct = self.ledger.get_setting_float("exit_move_pct", s.exit_move_pct)
|
||||
pos = self.matcher.current_position()
|
||||
|
||||
# 有仓:盯平仓
|
||||
if pos.get("status") == "open":
|
||||
self._set_state(phase="open", last_error=None)
|
||||
upl = self.matcher.unrealized()
|
||||
decision = check_exits(
|
||||
perp_upl=float(upl["perp_upl"]),
|
||||
initial_premium=float(upl["initial_premium"] or 0),
|
||||
move_points=float(upl["move_points"] or 0),
|
||||
exit_move_points=exit_pts,
|
||||
move_pct=float(upl.get("move_pct") or 0),
|
||||
exit_move_pct=exit_pct,
|
||||
)
|
||||
if decision.should_close:
|
||||
self._set_state(phase="closing")
|
||||
r = self.matcher.close_group(reason=decision.reason)
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.close_group, reason=decision.reason
|
||||
)
|
||||
if r.ok:
|
||||
self._after_close()
|
||||
elif r.liquidity_wait:
|
||||
@@ -184,7 +177,6 @@ class StrategyEngine:
|
||||
self._set_state(last_error=r.detail)
|
||||
return
|
||||
|
||||
# 休息中
|
||||
if st["phase"] == "resting" and st["rest_until_ms"]:
|
||||
if int(time.time() * 1000) < int(st["rest_until_ms"]):
|
||||
return
|
||||
@@ -192,46 +184,38 @@ class StrategyEngine:
|
||||
|
||||
st = self.db.fetchone("SELECT * FROM strategy_state WHERE id=1")
|
||||
assert st is not None
|
||||
if int(st["rounds_done"] or 0) >= max_rounds:
|
||||
self._set_state(phase="stopped")
|
||||
if st["phase"] in ("paused",):
|
||||
return
|
||||
# 旧「轮次停开」状态:自动恢复为空闲以便继续
|
||||
if st["phase"] in ("stopped", "outside_window"):
|
||||
self._set_state(phase="idle")
|
||||
|
||||
if not can_open_new(open_hhmm=s.open_hhmm, stop_hhmm=s.stop_open_hhmm):
|
||||
self._set_state(phase="outside_window")
|
||||
return
|
||||
|
||||
if st["phase"] in ("stopped", "paused"):
|
||||
return
|
||||
|
||||
# 尝试开仓
|
||||
self._set_state(phase="wait_signal")
|
||||
gw = get_gateway()
|
||||
snap = gw.snapshot()
|
||||
if not snap.pair or not snap.call or not snap.put:
|
||||
return
|
||||
sig = decide(snap.call.ask, snap.put.ask)
|
||||
if sig is None:
|
||||
pick = await gw.pick_for_open_async()
|
||||
if pick is None:
|
||||
self._set_state(
|
||||
last_error="无合格期权:需剩余时长与杠杆倍数同时满足"
|
||||
)
|
||||
return
|
||||
|
||||
self._set_state(phase="opening")
|
||||
count = self._count_groups_for_window(wkey)
|
||||
self._set_state(phase="opening", last_error=None)
|
||||
count = self._count_groups_for_day(wkey)
|
||||
gid = next_group_id(count)
|
||||
option_inst = (
|
||||
snap.pair.call_inst_id if sig.option_side == "call" else snap.pair.put_inst_id
|
||||
pick.pair.call_inst_id if pick.option_side == "call" else pick.pair.put_inst_id
|
||||
)
|
||||
entry_idx = snap.index_px or (snap.perp.mark_px if snap.perp else None)
|
||||
if entry_idx is None:
|
||||
self._set_state(last_error="no index/mark for entry")
|
||||
return
|
||||
r = self.matcher.open_group(
|
||||
entry_idx = pick.underlying_px
|
||||
r = await asyncio.to_thread(
|
||||
self.matcher.open_group,
|
||||
group_id=gid,
|
||||
bias=sig.bias,
|
||||
option_side=sig.option_side,
|
||||
perp_side=sig.perp_side,
|
||||
bias=pick.bias,
|
||||
option_side=pick.option_side,
|
||||
perp_side=pick.perp_side,
|
||||
option_inst_id=option_inst,
|
||||
entry_index_px=float(entry_idx),
|
||||
strike=snap.pair.strike,
|
||||
expiry_ymd=snap.pair.expiry_ymd,
|
||||
strike=pick.pair.strike,
|
||||
expiry_ymd=pick.pair.expiry_ymd,
|
||||
)
|
||||
if r.ok:
|
||||
self._set_state(phase="open", last_error=None)
|
||||
|
||||
Reference in New Issue
Block a user