diff --git a/.env.example b/.env.example index 85ac736..6924276 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 1db4c1e..d225b59 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -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) diff --git a/docs/SIMNOW.md b/docs/SIMNOW.md new file mode 100644 index 0000000..e7a19ad --- /dev/null +++ b/docs/SIMNOW.md @@ -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:00–11: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:00–15: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** 成功 diff --git a/scripts/test_simnow.py b/scripts/test_simnow.py new file mode 100644 index 0000000..0f1669e --- /dev/null +++ b/scripts/test_simnow.py @@ -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()) diff --git a/vnpy_bridge.py b/vnpy_bridge.py index c2a5938..6aaea94 100644 --- a/vnpy_bridge.py +++ b/vnpy_bridge.py @@ -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: