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_list_positions,
|
||||||
ctp_status,
|
ctp_status,
|
||||||
execute_order,
|
execute_order,
|
||||||
|
get_bridge,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -527,6 +528,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": err}), 403
|
return jsonify({"ok": False, "error": err}), 403
|
||||||
mode = get_trading_mode(get_setting)
|
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)
|
sizing = get_sizing_mode(get_setting)
|
||||||
if offset.startswith("open") and sizing == MODE_RISK:
|
if offset.startswith("open") and sizing == MODE_RISK:
|
||||||
sl = float(d.get("stop_loss") or 0)
|
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()
|
conn.commit()
|
||||||
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
send_wechat_msg(f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}")
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": True, "result": result, "lots": lots})
|
return jsonify({"ok": True, "result": result, "lots": lots, "message": "委托已提交柜台,限价单需成交后才会显示持仓"})
|
||||||
except ValueError as exc:
|
except (ValueError, RuntimeError) as exc:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
+2
-1
@@ -299,7 +299,8 @@
|
|||||||
showOrderMsg(data.error || '下单失败', false);
|
showOrderMsg(data.error || '下单失败', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showOrderMsg('开仓成功 · ' + (data.lots || lots) + ' 手', true);
|
var msg = data.message || ('开仓成功 · ' + (data.lots || lots) + ' 手');
|
||||||
|
showOrderMsg(msg, true);
|
||||||
pollPositions();
|
pollPositions();
|
||||||
refreshQuote();
|
refreshQuote();
|
||||||
setTimeout(function () { showOrderMsg(''); }, 4000);
|
setTimeout(function () { showOrderMsg(''); }, 4000);
|
||||||
|
|||||||
+112
-7
@@ -13,6 +13,7 @@ from locale_fix import ensure_process_locale
|
|||||||
ensure_process_locale()
|
ensure_process_locale()
|
||||||
|
|
||||||
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
|
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
|
||||||
|
from contract_specs import get_contract_spec
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -80,6 +81,18 @@ def _format_ctp_failure(ctp_logs: list[str]) -> str:
|
|||||||
return "CTP 连接超时:未收到柜台回报。请检查 SimNow 账号、前置地址、网络(nc 测端口),并用快期验证账号"
|
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:
|
class CtpBridge:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._engine = None
|
self._engine = None
|
||||||
@@ -242,6 +255,84 @@ class CtpBridge:
|
|||||||
return
|
return
|
||||||
self.connect(mode)
|
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:
|
def ping(self) -> bool:
|
||||||
"""检测连接是否仍有效;无效则清除 connected 状态。"""
|
"""检测连接是否仍有效;无效则清除 connected 状态。"""
|
||||||
if not self._engine or not self._connected_mode:
|
if not self._engine or not self._connected_mode:
|
||||||
@@ -684,10 +775,13 @@ class CtpBridge:
|
|||||||
|
|
||||||
if not self._engine:
|
if not self._engine:
|
||||||
raise RuntimeError("CTP 未初始化")
|
raise RuntimeError("CTP 未初始化")
|
||||||
|
if not self._td_logged_in():
|
||||||
|
raise RuntimeError("CTP 交易通道未登录,请重连后再下单")
|
||||||
|
|
||||||
sym, ex_name = ths_to_vnpy_symbol(ths_code)
|
sym, ex_name = ths_to_vnpy_symbol(ths_code)
|
||||||
exchange = to_vnpy_exchange(ex_name)
|
exchange = to_vnpy_exchange(ex_name)
|
||||||
lots = max(1, int(lots))
|
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()
|
offset = (offset or "open").lower()
|
||||||
direction = (direction or "long").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
|
d = Direction.LONG if direction == "long" or offset == "open_long" else Direction.SHORT
|
||||||
off = Offset.OPEN
|
off = Offset.OPEN
|
||||||
elif offset in ("close", "close_long", "close_short"):
|
elif offset in ("close", "close_long", "close_short"):
|
||||||
# 平多 = 卖;平空 = 买
|
hold = "long" if direction == "long" or offset == "close_long" else "short"
|
||||||
if direction == "long" or offset == "close_long":
|
if hold == "long":
|
||||||
d = Direction.SHORT
|
d = Direction.SHORT
|
||||||
else:
|
else:
|
||||||
d = Direction.LONG
|
d = Direction.LONG
|
||||||
off = Offset.CLOSE
|
off = self._resolve_close_offset(sym, ex_name, hold, lots)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"未知开平: {offset}")
|
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(
|
req = OrderRequest(
|
||||||
symbol=sym,
|
symbol=sym,
|
||||||
@@ -716,9 +817,13 @@ class CtpBridge:
|
|||||||
price=price,
|
price=price,
|
||||||
offset=off,
|
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)
|
vt_orderid = self._engine.send_order(req, GATEWAY_NAME)
|
||||||
if not vt_orderid:
|
if not vt_orderid:
|
||||||
raise RuntimeError("CTP 拒单或未返回委托号")
|
raise RuntimeError("CTP 拒单或未返回委托号(请检查合约代码、价格是否为最小变动价位整数倍)")
|
||||||
return str(vt_orderid)
|
return str(vt_orderid)
|
||||||
|
|
||||||
|
|
||||||
@@ -849,7 +954,7 @@ def execute_order(
|
|||||||
f"模拟盘需配置 .env 中 SIMNOW_USER / SIMNOW_PASSWORD 等"
|
f"模拟盘需配置 .env 中 SIMNOW_USER / SIMNOW_PASSWORD 等"
|
||||||
)
|
)
|
||||||
b = get_bridge()
|
b = get_bridge()
|
||||||
b.ensure_connected(mode)
|
b.require_connected(mode)
|
||||||
order_id = b.send_order(
|
order_id = b.send_order(
|
||||||
ths_code=symbol,
|
ths_code=symbol,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
|
|||||||
Reference in New Issue
Block a user