fix: CTP 连接捕获 SimNow 真实报错并增加诊断脚本

显式设置柜台环境=实盘;连接失败时解析 4097/登录拒单;scripts/test_simnow.py 供服务器排查。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 11:20:30 +08:00
parent 8726760b12
commit 28d6ae52b2
5 changed files with 353 additions and 20 deletions
+3 -2
View File
@@ -19,7 +19,7 @@ TRADING_MODE=simulation
POSITION_SIZING_MODE=risk
RISK_PERCENT=1
# —— SimNow 模拟盘(在 simnow.com 注册后填写)——
# —— SimNow 模拟盘(注册见 docs/SIMNOW.md)——
SIMNOW_USER=
SIMNOW_PASSWORD=
SIMNOW_BROKER_ID=9999
@@ -28,7 +28,8 @@ SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
SIMNOW_APP_ID=simnow_client_test
SIMNOW_AUTH_CODE=0000000000000000
SIMNOW_PRODUCT_INFO=simnow_client_test
# SimNow 看穿式前置固定用「实盘」;仅穿透式测评才用「测试」
SIMNOW_ENV=实盘
# —— 期货公司实盘(后期接入)——
CTP_LIVE_USER=
+4 -3
View File
@@ -127,7 +127,7 @@ ADMIN_SYNC_FROM_ENV=false
WECHAT_WEBHOOK=
QUOTE_SOURCE=sina
# —— SimNow 模拟盘(在 simnow.com.cn 注册)——
# —— SimNow 模拟盘(注册步骤见 docs/SIMNOW.md)——
SIMNOW_USER=你的SimNow账号
SIMNOW_PASSWORD=你的密码
SIMNOW_BROKER_ID=9999
@@ -135,7 +135,7 @@ SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
SIMNOW_APP_ID=simnow_client_test
SIMNOW_AUTH_CODE=0000000000000000
SIMNOW_PRODUCT_INFO=simnow_client_test
SIMNOW_ENV=实盘
TRADING_MODE=simulation
```
@@ -334,7 +334,7 @@ pm2 restart qihuo
**2. 配置 SimNow`.env`**
填写 `SIMNOW_USER``SIMNOW_PASSWORD`,前置地址以 SimNow 官网为准。
注册与查投资者代码见 [SIMNOW.md](./SIMNOW.md)。填写 `SIMNOW_USER`(投资者代码)`SIMNOW_PASSWORD`,前置地址以 SimNow 官网为准。
**3. 连接**
@@ -389,5 +389,6 @@ pm2 restart qihuo
## 相关文档
- [功能说明文档](./FEATURES.md)
- [SimNow 注册与接入说明](./SIMNOW.md)
- [交易与 SimNow 配置](./TRADING.md)
- [README](../README.md)
+220
View File
@@ -0,0 +1,220 @@
# SimNow 仿真账号注册与接入
SimNow 是上海期货交易所全资子公司 **上海期货信息技术有限公司(上期技术)** 提供的期货、期权 **CTP 仿真交易平台**。本系统模拟盘通过 **vnpy_ctp** 连接 SimNow 前置,下单、持仓、权益均来自 SimNow 柜台(非本地假资金)。
- **官网**https://www.simnow.com.cn/
- **客服电话**400-920-6816
- **客服邮箱**helpdesk_a@sfit.shfe.com.cn
---
## 一、SimNow 是什么
| 对比项 | SimNow | 期货公司自有模拟 |
|--------|--------|------------------|
| 接口 | 标准 **CTP**(与实盘一致) | 各公司自建,接口不统一 |
| 维护方 | 上期技术(官方) | 各期货公司 |
| 适用场景 | 程序下单、策略联调、熟悉规则 | 人工在该公司客户端练习 |
本项目的 **模拟盘 · SimNow** 模式,即把 `.env` 中的账号连到 SimNow 的 CTP 前置,与后期接入期货公司实盘 CTP 使用同一套代码路径。
---
## 二、注册前须知
1. **访问时段**:官网多数时间在 **交易日白天** 可正常访问(约 9:0011:30、13:00–15:00,及夜盘相关时段)。非交易时段可能打不开或功能受限,属平台策略,请换交易时段再试。
2. **手机号**:一个手机号只能注册 **一个** 仿真账户。
3. **登录账号不是手机号**:CTP / 快期 / 本系统里填的是 **投资者代码(InvestorID**,注册成功后需在官网查询,不要填手机号。
4. **密码**:注册后首次用客户端(如快期)登录时,可能要求 **修改交易密码**;修改后的密码才用于 CTP 连接。
5. **长期不用会被冻结**:长期未登录或未改密码的账号可能被冻结(持仓与资金清零)。需在官网 **业务 → 激活账号**,再通过 **重置资金** 恢复(通常次日生效)。
---
## 三、注册步骤
### 1. 打开官网
浏览器访问:https://www.simnow.com.cn/
若打不开,请在工作日 **日盘或夜盘交易时段** 重试。
### 2. 点击「注册账号」
首页右上角 **注册账号**(或类似入口)。
### 3. 验证手机号
- 输入 **手机号**
- 输入 **图片验证码**
- 点击 **立即注册**
### 4. 填写账户信息
- 设置 **登录密码**(即后续 CTP 交易密码,请牢记)
- 选择接口类型时选 **标准 CTP**(若页面有该选项)
- 输入 **短信验证码**
- 提交完成注册
### 5. 查询投资者代码(重要)
注册完成后:
1. 在官网 **投资者登录**
2. 进入 **业务导航****查询投资者代码**(页面文案可能略有变化)
3. 记下 **投资者代码**(一般为数字,例如 `123456`
> **本系统 `.env` 里的 `SIMNOW_USER` 填这个投资者代码,不要填手机号。**
### 6. (建议)重置模拟资金
登录官网后:
- **业务导航 → 重置资金** 或 **入金**
SimNow 默认会给一定模拟资金;若账号被冻结后激活,资金可能为 0,需在此重置。
### 7. (建议)用快期验证账号
1. 官网 **终端下载** → 下载 **快期 V2 / V3** 等客户端
2. 安装后用 **投资者代码 + 交易密码** 登录
3. 若提示 **首次登录须修改密码**,按提示改密后再登录
4. 能看到资金与行情,说明账号可用
验证通过后,再将同一 **投资者代码****密码** 写入本系统 `.env`
---
## 四、在本系统中配置
### 1. 编辑 `.env`
在服务器或本地项目目录:
```bash
cp .env.example .env
nano .env # 或用其他编辑器
```
填写 SimNow 相关项(示例):
```env
TRADING_MODE=simulation
SIMNOW_USER=123456 # 投资者代码,不是手机号
SIMNOW_PASSWORD=你的交易密码
SIMNOW_BROKER_ID=9999
SIMNOW_TD_ADDRESS=tcp://180.168.146.187:10201
SIMNOW_MD_ADDRESS=tcp://180.168.146.187:10211
SIMNOW_APP_ID=simnow_client_test
SIMNOW_AUTH_CODE=0000000000000000
SIMNOW_ENV=实盘
```
| 变量 | 说明 |
|------|------|
| `SIMNOW_USER` | 投资者代码(InvestorID |
| `SIMNOW_PASSWORD` | 交易密码(快期能登录的同一密码) |
| `SIMNOW_BROKER_ID` | 固定 **9999**SimNow 默认) |
| `SIMNOW_TD_ADDRESS` | 交易前置,以官网最新为准 |
| `SIMNOW_MD_ADDRESS` | 行情前置,以官网最新为准 |
| `SIMNOW_APP_ID` / `SIMNOW_AUTH_CODE` | SimNow 仿真默认测试值,一般无需改 |
| `SIMNOW_ENV` | **实盘**(SimNow 看穿式前置必须);仅穿透式测评才用「测试」 |
### 2. 前置地址(7×24 与交易时段)
SimNow 提供多种仿真环境,**IP 与端口会随官网公告调整**,部署前务必登录官网核对:
- **7×24 环境**:适合非交易时段联调程序(本仓库 `.env.example` 默认示例多为该环境)
- **交易时段环境**:与实盘时段、规则更接近
在官网 **产品与服务****API 下载 / 接入说明** 中查看当前 **交易前置(TD**、**行情前置(MD)** 地址,格式为:
```text
tcp://IP:端口
```
修改 `.env` 后需重启应用:
```bash
pm2 restart qihuo
```
### 3. 网页端连接 CTP
1. 登录本系统
2. **系统设置** → 确认 **模拟盘 · SimNow**
3. 打开 **持仓监控**`/positions`
4. 点击 **连接 CTP**
5. 顶栏显示 **CTP 已连接**,权益变为 SimNow 账户资金即成功
连接成功后:下单、持仓、浮盈均来自 SimNow 柜台;**系统设置里的「参考资金」不再用于交易**,仅 CTP 未连接时用于品种推荐与以损定仓估算。
---
## 五、常见问题
### 官网打不开
-**交易日 9:0015:00** 或夜盘时段访问
- 平台维护期间会不可用,留意官网 **通知公告**
### 连接 CTP 超时 / 失败
在服务器运行诊断脚本(会测端口并尝试登录,输出具体 CTP 报错):
```bash
cd /opt/qihuo
source venv/bin/activate
python scripts/test_simnow.py
```
| 现象 | 处理 |
|------|------|
| 端口探测失败 | 服务器出网或防火墙问题,`nc -zv 180.168.146.187 10201` |
| 报错 **4097** / 握手失败 | `pip install -U vnpy vnpy_ctp``.env``SIMNOW_ENV=实盘` |
| **不合法的登录** | 投资者代码/密码错,或未在快期改过一次密码 |
| 快期能登、脚本不能 | 多为网络或前置地址,换 SimNow 官网其他组前置试 |
### 提示「未安装 vnpy / vnpy_ctp」
Python 环境未成功安装 CTP 网关,与 SimNow 账号无关。在服务器执行:
```bash
cd /opt/qihuo
apt install -y build-essential python3-dev pkg-config
source venv/bin/activate
pip install -r requirements.txt
python -c "from vnpy_ctp import CtpGateway; print('OK')"
pm2 restart qihuo
```
### 连接成功但下单拒单
- 检查合约代码、价格精度、涨跌停
- 确认 SimNow 账户 **有足够保证金**(可在官网重置资金)
- 部分合约在仿真环境可能受限,换主力合约试
### 忘记密码
在 SimNow 官网使用 **重置密码**(需登录或按官网流程操作)。
---
## 六、与本项目其他文档的关系
| 文档 | 内容 |
|------|------|
| [TRADING.md](./TRADING.md) | 模拟盘 / 实盘通道、API、页面说明 |
| [DEPLOY.md](./DEPLOY.md) | 服务器部署、vnpy 编译、PM2、环境变量总表 |
---
## 七、快速检查清单
- [ ] 已在 https://www.simnow.com.cn/ 注册并完成短信验证
- [ ] 已查询并保存 **投资者代码**(非手机号)
- [ ] 已用快期客户端成功登录(必要时已修改交易密码)
- [ ] `.env``SIMNOW_USER``SIMNOW_PASSWORD` 已填写
- [ ] 前置地址与官网 **7×24 或交易时段** 说明一致
- [ ] `pip install -r requirements.txt``vnpy_ctp` 导入成功
- [ ] 系统 **持仓监控****连接 CTP** 成功
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""SimNow CTP 连接诊断(在服务器 venv 中运行)。"""
from __future__ import annotations
import os
import socket
import sys
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE)
from dotenv import load_dotenv
load_dotenv(os.path.join(BASE, ".env"))
def _probe(host_port: str) -> str:
s = host_port.replace("tcp://", "").strip()
if ":" not in s:
return "invalid"
host, port_s = s.rsplit(":", 1)
try:
port = int(port_s)
sock = socket.create_connection((host, port), timeout=5)
sock.close()
return "ok"
except OSError as exc:
return str(exc)
def main() -> int:
user = os.getenv("SIMNOW_USER", "")
td = os.getenv("SIMNOW_TD_ADDRESS", "tcp://180.168.146.187:10201")
md = os.getenv("SIMNOW_MD_ADDRESS", "tcp://180.168.146.187:10211")
env = os.getenv("SIMNOW_ENV", "实盘")
print("=== SimNow 配置 ===")
print(f"SIMNOW_USER = {user or '(未设置)'}")
print(f"SIMNOW_PASSWORD = {'*' * 8 if os.getenv('SIMNOW_PASSWORD') else '(未设置)'}")
print(f"SIMNOW_TD = {td}")
print(f"SIMNOW_MD = {md}")
print(f"SIMNOW_ENV = {env}")
print()
print("=== 端口探测 ===")
print(f"TD {td} -> {_probe(td)}")
print(f"MD {md} -> {_probe(md)}")
print()
if not user or not os.getenv("SIMNOW_PASSWORD"):
print("错误:请在 .env 填写 SIMNOW_USER / SIMNOW_PASSWORD")
return 1
print("=== CTP 登录测试 ===")
try:
from vnpy_bridge import ctp_connect
st = ctp_connect("simulation", force=True)
acc = st.get("broker_id")
print("连接成功")
print(st)
return 0
except Exception as exc:
print(f"连接失败: {exc}")
return 2
if __name__ == "__main__":
raise SystemExit(main())
+58 -15
View File
@@ -22,7 +22,7 @@ def _env(key: str, default: str = "") -> str:
def _simnow_setting() -> dict[str, str]:
"""SimNow 7×24 仿真默认前置(可在 .env 覆盖)。"""
"""SimNow 仿真前置(可在 .env 覆盖)。看穿式前置需「柜台环境=实盘」。"""
return {
"用户名": _env("SIMNOW_USER"),
"密码": _env("SIMNOW_PASSWORD"),
@@ -31,7 +31,7 @@ def _simnow_setting() -> dict[str, str]:
"行情服务器": _env("SIMNOW_MD_ADDRESS", "tcp://180.168.146.187:10211"),
"产品名称": _env("SIMNOW_APP_ID", "simnow_client_test"),
"授权编码": _env("SIMNOW_AUTH_CODE", "0000000000000000"),
"产品信息": _env("SIMNOW_PRODUCT_INFO", "simnow_client_test"),
"柜台环境": _env("SIMNOW_ENV", "实盘"),
}
@@ -44,7 +44,7 @@ def _live_setting() -> dict[str, str]:
"行情服务器": _env("CTP_LIVE_MD_ADDRESS"),
"产品名称": _env("CTP_LIVE_APP_ID"),
"授权编码": _env("CTP_LIVE_AUTH_CODE"),
"产品信息": _env("CTP_LIVE_PRODUCT_INFO"),
"柜台环境": _env("CTP_LIVE_ENV", "实盘"),
}
@@ -56,6 +56,25 @@ def _mode_label(mode: str) -> str:
return "SimNow 模拟" if mode == "simulation" else "期货公司实盘"
def _format_ctp_failure(ctp_logs: list[str]) -> str:
"""根据 CTP 网关日志拼出可读错误。"""
text = "\n".join(ctp_logs)
if "4097" in text or "Decrypt handshake" in text or "shake hand" in text.lower():
return (
"CTP 握手失败(4097)vnpy_ctp 与 SimNow 前置加密不匹配。"
"请执行 pip install -U vnpy vnpy_ctp 后重启,并确认 .env 中 SIMNOW_ENV=实盘"
)
if "不合法的登录" in text or "密码" in text or "账号" in text:
tail = ctp_logs[-1] if ctp_logs else ""
return f"CTP 登录被拒:{tail or '请检查投资者代码与密码(快期能否登录)'}"
if "连接断开" in text or "disconnect" in text.lower():
tail = ctp_logs[-1] if ctp_logs else ""
return f"CTP 连接断开:{tail or '请检查前置地址与网络'}"
if ctp_logs:
return f"CTP 连接失败:{ctp_logs[-1]}"
return "CTP 连接超时:未收到柜台回报。请检查 SimNow 账号、前置地址、网络(nc 测端口),并用快期验证账号"
class CtpBridge:
def __init__(self) -> None:
self._engine = None
@@ -127,18 +146,42 @@ class CtpBridge:
self._connected_mode = None
time.sleep(1)
self._engine.connect(setting, GATEWAY_NAME)
# 等待登录与结算信息
for _ in range(30):
accounts = self._engine.get_all_accounts()
if accounts:
self._connected_mode = mode
self._last_error = ""
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
return
time.sleep(0.5)
self._last_error = "CTP 连接超时,请检查 SimNow 账号、前置地址与交易时段"
raise RuntimeError(self._last_error)
ctp_logs: list[str] = []
from vnpy.trader.event import EVENT_LOG
def _on_log(event) -> None:
msg = getattr(event.data, "msg", "") or str(event.data)
if msg:
ctp_logs.append(str(msg))
if len(ctp_logs) > 20:
ctp_logs.pop(0)
logger.info("CTP | %s", msg)
self._ee.register(EVENT_LOG, _on_log)
try:
logger.info(
"CTP 连接 [%s] user=%s td=%s env=%s",
mode,
setting.get("用户名"),
setting.get("交易服务器"),
setting.get("柜台环境", "实盘"),
)
self._engine.connect(setting, GATEWAY_NAME)
# 等待登录与结算信息(最多约 30 秒)
for _ in range(60):
accounts = self._engine.get_all_accounts()
if accounts:
self._connected_mode = mode
self._last_error = ""
logger.info("CTP 已连接 [%s] account=%s", mode, len(accounts))
return
time.sleep(0.5)
finally:
self._ee.unregister(EVENT_LOG, _on_log)
hint = _format_ctp_failure(ctp_logs)
self._last_error = hint
raise RuntimeError(hint)
def ensure_connected(self, mode: str) -> None:
if self._connected_mode != mode: