Add daily loss force-flatten at configurable equity limit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-03 12:42:13 +08:00
parent b6c3266a9e
commit 2081bf2da9
17 changed files with 850 additions and 97 deletions
+32
View File
@@ -240,6 +240,25 @@ def _on_ctp_connected(mode: str) -> None:
get_bridge().request_position_snapshot(force=True)
get_bridge().calibrate_trading_state()
_persist_snapshot(mode)
conn = connect_db(DB_PATH)
try:
_init_worker_tables(conn)
capital = _capital(conn)
if capital <= 0:
acc = ctp_get_account(mode) or {}
capital = float(acc.get("balance") or 0)
if capital > 0:
from modules.risk.daily_loss_guard import check_daily_loss_and_flatten
check_daily_loss_and_flatten(
conn,
mode,
equity=capital,
notify_fn=_send_wechat_msg,
get_setting=get_setting,
)
finally:
conn.close()
except Exception as exc:
logger.debug("worker ctp connected callback: %s", exc)
@@ -287,6 +306,16 @@ def _start_background_workers() -> None:
get_be_tick_buffer_fn=lambda: get_trailing_be_tick_buffer(get_setting),
notify_fn=_send_wechat_msg,
)
from modules.risk.daily_loss_guard import start_daily_loss_guard_worker
start_daily_loss_guard_worker(
db_path=DB_PATH,
get_mode_fn=_mode,
init_tables_fn=_init_worker_tables,
get_capital_fn=_capital,
get_setting_fn=get_setting,
notify_fn=_send_wechat_msg,
)
def _snapshot_loop() -> None:
time.sleep(3)
@@ -432,6 +461,9 @@ def api_order():
price=float(data.get("price") or 0),
settings=data.get("settings") or {},
order_type=data.get("order_type") or "limit",
urgency=data.get("urgency") or "normal",
equity=data.get("equity"),
slippage_buffer_pct=data.get("slippage_buffer_pct"),
)
_persist_snapshot(mode)
return _json_ok(**result)
+113 -10
View File
@@ -1189,6 +1189,9 @@ class CtpBridge:
price: float,
tick: float,
use_market: bool,
urgency: str = "normal",
equity: Optional[float] = None,
slippage_buffer_pct: Optional[float] = None,
) -> str:
"""平仓:VeighNa OffsetConverter 自动拆分平今/平昨(与 CTA 引擎一致)。"""
from vnpy.trader.constant import Offset
@@ -1210,7 +1213,11 @@ class CtpBridge:
lp = float(price)
if use_market:
lp = self._aggressive_limit_price(ths_code, sym, ex_name, direction, tick, lp)
lp = self._aggressive_limit_price(
ths_code, sym, ex_name, direction, tick, lp,
urgency=urgency, equity=equity,
slippage_buffer_pct=slippage_buffer_pct, lots=lots,
)
else:
lp = round_to_tick(lp, tick)
if lp <= 0:
@@ -1245,6 +1252,9 @@ class CtpBridge:
if use_market:
sub_price = self._aggressive_limit_price(
ths_code, sym, ex_name, sub.direction, tick, float(price),
urgency=urgency, equity=equity,
slippage_buffer_pct=slippage_buffer_pct,
lots=int(sub.volume or lots),
)
else:
sub_price = round_to_tick(float(sub.price or lp), tick)
@@ -1266,6 +1276,48 @@ class CtpBridge:
logger.debug("offset converter order req: %s", exc)
return last_vt
def _find_tick_obj(self, sym: str, ex_name: str) -> Any:
if not self._engine:
return None
sym_l = sym.lower()
ex_u = ex_name.upper()
try:
for tick in self._engine.get_all_ticks():
ts = (getattr(tick, "symbol", "") or "").lower()
te = getattr(tick, "exchange", None)
te_s = str(te.value if hasattr(te, "value") else te or "").upper()
if ts == sym_l and te_s == ex_u:
return tick
except Exception as exc:
logger.debug("find tick: %s", exc)
return None
def _opponent_price_from_tick(self, tick: Any, direction: Any) -> Optional[float]:
from vnpy.trader.constant import Direction
if not tick:
return None
if direction == Direction.LONG:
attrs = ("ask_price_1", "last_price", "pre_close")
else:
attrs = ("bid_price_1", "last_price", "pre_close")
for attr in attrs:
try:
v = float(getattr(tick, attr, 0) or 0)
except (TypeError, ValueError):
v = 0.0
if v > 0:
return v
return None
def _urgency_slip_ticks(self, urgency: str) -> int:
table = {
"normal": 5,
"stop_loss": 12,
"risk_flatten": 20,
}
return max(1, int(table.get((urgency or "normal").strip().lower(), 5)))
def _aggressive_limit_price(
self,
ths_code: str,
@@ -1274,19 +1326,47 @@ class CtpBridge:
direction: Any,
tick: float,
fallback: float,
*,
urgency: str = "normal",
equity: Optional[float] = None,
slippage_buffer_pct: Optional[float] = None,
lots: int = 1,
) -> float:
from vnpy.trader.constant import Direction
self.subscribe_symbol(ths_code)
lp = fallback
detail = self.get_tick_detail(ths_code, mode=self._connected_mode or "")
if detail.get("price"):
lp = float(detail["price"])
slip = max(tick, tick * 3)
tick_obj = self._find_tick_obj(sym, ex_name)
opp = self._opponent_price_from_tick(tick_obj, direction)
if opp is None or opp <= 0:
detail = self.get_tick_detail(ths_code, mode=self._connected_mode or "")
if detail.get("price"):
opp = float(detail["price"])
else:
opp = float(fallback or 0)
if opp <= 0:
return 0.0
slip_ticks = self._urgency_slip_ticks(urgency)
slip_price = slip_ticks * max(tick, 1e-9)
if (
(urgency or "").strip().lower() == "risk_flatten"
and equity
and float(equity) > 0
and slippage_buffer_pct is not None
and float(slippage_buffer_pct) > 0
and lots > 0
):
spec = get_contract_spec(ths_code)
mult = float(spec.get("mult") or 10)
max_yuan = float(equity) * float(slippage_buffer_pct) / 100.0
denom = mult * max(1, int(lots))
if denom > 0:
slip_price = min(slip_price, max_yuan / denom)
if direction == Direction.LONG:
lp = lp + slip
lp = opp + slip_price
else:
lp = max(tick, lp - slip)
lp = max(tick, opp - slip_price)
return round_to_tick(lp, tick)
def ping(self) -> bool:
@@ -2479,6 +2559,9 @@ class CtpBridge:
lots: int,
price: float,
order_type: str = "limit",
urgency: str = "normal",
equity: Optional[float] = None,
slippage_buffer_pct: Optional[float] = None,
) -> str:
from vnpy.trader.constant import Direction, Offset, OrderType
from vnpy.trader.object import OrderRequest
@@ -2502,7 +2585,11 @@ class CtpBridge:
use_market = (order_type or "limit").lower() == "market"
if use_market:
ot = OrderType.FAK
price = self._aggressive_limit_price(ths_code, sym, ex_name, d, tick, price)
price = self._aggressive_limit_price(
ths_code, sym, ex_name, d, tick, price,
urgency=urgency, equity=equity,
slippage_buffer_pct=slippage_buffer_pct, lots=lots,
)
else:
ot = OrderType.LIMIT
price = round_to_tick(float(price), tick)
@@ -2541,7 +2628,11 @@ class CtpBridge:
ot = OrderType.LIMIT
price = round_to_tick(float(price), tick)
if use_market:
price = self._aggressive_limit_price(ths_code, sym, ex_name, d, tick, price)
price = self._aggressive_limit_price(
ths_code, sym, ex_name, d, tick, price,
urgency=urgency, equity=equity,
slippage_buffer_pct=slippage_buffer_pct, lots=lots,
)
if price <= 0:
raise ValueError("委托价格无效,请检查行情或手动填写价格")
return self._submit_close_orders(
@@ -2556,6 +2647,9 @@ class CtpBridge:
price=price,
tick=tick,
use_market=use_market,
urgency=urgency,
equity=equity,
slippage_buffer_pct=slippage_buffer_pct,
)
raise ValueError(f"未知开平: {offset}")
@@ -3062,6 +3156,9 @@ def execute_order(
price: float,
settings: dict | None = None,
order_type: str = "limit",
urgency: str = "normal",
equity: Optional[float] = None,
slippage_buffer_pct: Optional[float] = None,
) -> dict[str, Any]:
"""统一下单:simulation=SimNowlive=期货公司 CTP。"""
if _use_ctp_worker_client():
@@ -3074,6 +3171,9 @@ def execute_order(
"price": price,
"settings": settings or {},
"order_type": order_type,
"urgency": urgency,
"equity": equity,
"slippage_buffer_pct": slippage_buffer_pct,
})
del conn, settings
if mode not in ("simulation", "live"):
@@ -3092,6 +3192,9 @@ def execute_order(
lots=lots,
price=price,
order_type=order_type,
urgency=urgency,
equity=equity,
slippage_buffer_pct=slippage_buffer_pct,
)
return {
"order_id": order_id,