fix: CTP报单校验连接与tick价,市价改限价,郑商所平今平昨
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+9
-2
@@ -53,6 +53,7 @@ from vnpy_bridge import (
|
||||
ctp_list_positions,
|
||||
ctp_status,
|
||||
execute_order,
|
||||
get_bridge,
|
||||
)
|
||||
|
||||
|
||||
@@ -527,6 +528,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": err}), 403
|
||||
mode = get_trading_mode(get_setting)
|
||||
ctp_st = ctp_status(mode)
|
||||
if not ctp_st.get("connected"):
|
||||
conn.close()
|
||||
if get_bridge().connect_in_progress():
|
||||
return jsonify({"ok": False, "error": "CTP 连接中,请稍候再下单"}), 400
|
||||
return jsonify({"ok": False, "error": "请先连接 CTP"}), 400
|
||||
sizing = get_sizing_mode(get_setting)
|
||||
if offset.startswith("open") and sizing == MODE_RISK:
|
||||
sl = float(d.get("stop_loss") or 0)
|
||||
@@ -575,8 +582,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
||||
conn.commit()
|
||||
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
||||
conn.close()
|
||||
return jsonify({"ok": True, "result": result, "lots": lots})
|
||||
except ValueError as exc:
|
||||
return jsonify({"ok": True, "result": result, "lots": lots, "message": "委托已提交柜台,限价单需成交后才会显示持仓"})
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
conn.close()
|
||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||
except Exception as exc:
|
||||
|
||||
+2
-1
@@ -299,7 +299,8 @@
|
||||
showOrderMsg(data.error || '下单失败', false);
|
||||
return;
|
||||
}
|
||||
showOrderMsg('开仓成功 · ' + (data.lots || lots) + ' 手', true);
|
||||
var msg = data.message || ('开仓成功 · ' + (data.lots || lots) + ' 手');
|
||||
showOrderMsg(msg, true);
|
||||
pollPositions();
|
||||
refreshQuote();
|
||||
setTimeout(function () { showOrderMsg(''); }, 4000);
|
||||
|
||||
+112
-7
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user