fix: CTP报单校验连接与tick价,市价改限价,郑商所平今平昨

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 13:53:10 +08:00
parent 049aaffdcf
commit 3aa3e1ad30
3 changed files with 123 additions and 10 deletions
+112 -7
View File
@@ -13,6 +13,7 @@ from locale_fix import ensure_process_locale
ensure_process_locale()
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
from contract_specs import get_contract_spec
logger = logging.getLogger(__name__)
@@ -80,6 +81,18 @@ def _format_ctp_failure(ctp_logs: list[str]) -> str:
return "CTP 连接超时:未收到柜台回报。请检查 SimNow 账号、前置地址、网络(nc 测端口),并用快期验证账号"
def round_to_tick(price: float, tick: float) -> float:
if tick <= 0:
return float(price)
steps = round(float(price) / tick)
return round(steps * tick, 10)
def _is_long_direction(direction_obj: Any) -> bool:
s = str(direction_obj or "")
return "LONG" in s.upper() or "" in s
class CtpBridge:
def __init__(self) -> None:
self._engine = None
@@ -242,6 +255,84 @@ class CtpBridge:
return
self.connect(mode)
def require_connected(self, mode: str) -> None:
"""报单前检查:须已连接,不在此发起阻塞式 connect。"""
if self._connect_in_progress:
raise RuntimeError("CTP 连接中,请稍候再下单")
if self._connected_mode != mode or not self.ping():
raise RuntimeError("请先连接 CTP(持仓监控页点击「连接 CTP」)")
if not self._td_logged_in():
raise RuntimeError("CTP 交易通道未登录,请重连 CTP 后再下单")
def _td_logged_in(self) -> bool:
try:
gw = self._engine.get_gateway(GATEWAY_NAME)
td = gw.td_api
return bool(getattr(td, "login_status", False))
except Exception:
return False
def _find_position(self, sym: str, ex_name: str, hold_direction: str) -> Any:
if not self._engine:
return None
sym_l = sym.lower()
ex_u = ex_name.upper()
want_long = hold_direction == "long"
try:
for pos in self._engine.get_all_positions():
ps = (getattr(pos, "symbol", "") or "").lower()
pe = getattr(pos, "exchange", None)
pe_s = str(pe.value if hasattr(pe, "value") else pe or "").upper()
if ps != sym_l or pe_s != ex_u:
continue
vol = int(getattr(pos, "volume", 0) or 0)
if vol <= 0:
continue
is_long = _is_long_direction(getattr(pos, "direction", None))
if is_long == want_long:
return pos
except Exception as exc:
logger.debug("find position: %s", exc)
return None
def _resolve_close_offset(self, sym: str, ex_name: str, hold_direction: str, lots: int) -> Any:
from vnpy.trader.constant import Offset
if ex_name not in ("CZCE", "CFFEX"):
return Offset.CLOSE
pos = self._find_position(sym, ex_name, hold_direction)
if not pos:
return Offset.CLOSE
vol = int(getattr(pos, "volume", 0) or 0)
yd = int(getattr(pos, "yd_volume", 0) or 0)
today = max(0, vol - yd)
if today >= lots:
return Offset.CLOSETODAY
return Offset.CLOSEYESTERDAY
def _aggressive_limit_price(
self,
ths_code: str,
sym: str,
ex_name: str,
direction: Any,
tick: float,
fallback: float,
) -> 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)
if direction == Direction.LONG:
lp = lp + slip
else:
lp = max(tick, lp - slip)
return round_to_tick(lp, tick)
def ping(self) -> bool:
"""检测连接是否仍有效;无效则清除 connected 状态。"""
if not self._engine or not self._connected_mode:
@@ -684,10 +775,13 @@ class CtpBridge:
if not self._engine:
raise RuntimeError("CTP 未初始化")
if not self._td_logged_in():
raise RuntimeError("CTP 交易通道未登录,请重连后再下单")
sym, ex_name = ths_to_vnpy_symbol(ths_code)
exchange = to_vnpy_exchange(ex_name)
lots = max(1, int(lots))
price = float(price)
tick = float(get_contract_spec(ths_code).get("tick_size") or 1.0)
offset = (offset or "open").lower()
direction = (direction or "long").lower()
@@ -696,16 +790,23 @@ class CtpBridge:
d = Direction.LONG if direction == "long" or offset == "open_long" else Direction.SHORT
off = Offset.OPEN
elif offset in ("close", "close_long", "close_short"):
# 平多 = 卖;平空 = 买
if direction == "long" or offset == "close_long":
hold = "long" if direction == "long" or offset == "close_long" else "short"
if hold == "long":
d = Direction.SHORT
else:
d = Direction.LONG
off = Offset.CLOSE
off = self._resolve_close_offset(sym, ex_name, hold, lots)
else:
raise ValueError(f"未知开平: {offset}")
ot = OrderType.MARKET if (order_type or "limit").lower() == "market" else OrderType.LIMIT
use_market = (order_type or "limit").lower() == "market"
ot = OrderType.LIMIT
if use_market:
price = self._aggressive_limit_price(ths_code, sym, ex_name, d, tick, price)
else:
price = round_to_tick(float(price), tick)
if price <= 0:
raise ValueError("委托价格无效,请检查行情或手动填写价格")
req = OrderRequest(
symbol=sym,
@@ -716,9 +817,13 @@ class CtpBridge:
price=price,
offset=off,
)
logger.info(
"CTP 报单 %s %s %s %s手 @%s offset=%s type=%s",
sym, ex_name, d, lots, price, off, ot,
)
vt_orderid = self._engine.send_order(req, GATEWAY_NAME)
if not vt_orderid:
raise RuntimeError("CTP 拒单或未返回委托号")
raise RuntimeError("CTP 拒单或未返回委托号(请检查合约代码、价格是否为最小变动价位整数倍)")
return str(vt_orderid)
@@ -849,7 +954,7 @@ def execute_order(
f"模拟盘需配置 .env 中 SIMNOW_USER / SIMNOW_PASSWORD 等"
)
b = get_bridge()
b.ensure_connected(mode)
b.require_connected(mode)
order_id = b.send_order(
ths_code=symbol,
offset=offset,