Fix empty recommend list and CTP premarket auto-connect.
Correct main_code order in product refresh, refresh on CTP connect, and limit reconnect to trading or premarket windows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
| **[部署文档](docs/DEPLOY.md)** | 一键部署、更新、PM2、故障排查 |
|
| **[部署文档](docs/DEPLOY.md)** | 一键部署、更新、PM2、故障排查 |
|
||||||
| **[备份与恢复](docs/BACKUP.md)** | 自动备份、下载、跨服务器恢复 |
|
| **[备份与恢复](docs/BACKUP.md)** | 自动备份、下载、跨服务器恢复 |
|
||||||
| **[SimNow 接入](docs/SIMNOW.md)** | 仿真账号注册与 CTP 前置 |
|
| **[SimNow 接入](docs/SIMNOW.md)** | 仿真账号注册与 CTP 前置 |
|
||||||
|
| **[期货公司实盘 CTP](docs/CTP_LIVE.md)** | 实盘接入、与 SimNow 开平仓对比 |
|
||||||
| **[交易与策略](docs/TRADING.md)** | 下单、持仓、可开仓品种、策略 API |
|
| **[交易与策略](docs/TRADING.md)** | 下单、持仓、可开仓品种、策略 API |
|
||||||
| **[手续费与导航](docs/FEES.md)** | CTP 费率同步、导航开关 |
|
| **[手续费与导航](docs/FEES.md)** | CTP 费率同步、导航开关 |
|
||||||
| **[软件购买与使用协议](docs/软件购买与使用协议.md)** | 个人版授权模板(含签署栏) |
|
| **[软件购买与使用协议](docs/软件购买与使用协议.md)** | 个人版授权模板(含签署栏) |
|
||||||
|
|||||||
+31
-19
@@ -13,7 +13,7 @@ import time
|
|||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
from ctp_settings import is_ctp_auto_connect_enabled
|
from ctp_settings import is_ctp_auto_connect_enabled
|
||||||
from market_sessions import in_premarket_connect_window
|
from market_sessions import in_premarket_connect_window, is_trading_session
|
||||||
from vnpy_bridge import ctp_start_connect, ctp_status
|
from vnpy_bridge import ctp_start_connect, ctp_status
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -22,6 +22,13 @@ CHECK_INTERVAL_SEC = 60
|
|||||||
DEFAULT_MINUTES_BEFORE = 30
|
DEFAULT_MINUTES_BEFORE = 30
|
||||||
|
|
||||||
|
|
||||||
|
def premarket_minutes_before() -> int:
|
||||||
|
try:
|
||||||
|
return max(5, int(os.getenv("CTP_PREMARKET_MINUTES", str(DEFAULT_MINUTES_BEFORE))))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return DEFAULT_MINUTES_BEFORE
|
||||||
|
|
||||||
|
|
||||||
def _premarket_enabled() -> bool:
|
def _premarket_enabled() -> bool:
|
||||||
return (os.getenv("CTP_PREMARKET_CONNECT", "true") or "true").strip().lower() in (
|
return (os.getenv("CTP_PREMARKET_CONNECT", "true") or "true").strip().lower() in (
|
||||||
"1",
|
"1",
|
||||||
@@ -30,11 +37,14 @@ def _premarket_enabled() -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _minutes_before_open() -> int:
|
def should_auto_connect_now(*, minutes_before: int | None = None) -> bool:
|
||||||
try:
|
"""交易时段内,或距下一段开盘 <= minutes_before 且尚未开盘。"""
|
||||||
return max(5, int(os.getenv("CTP_PREMARKET_MINUTES", str(DEFAULT_MINUTES_BEFORE))))
|
mins = premarket_minutes_before() if minutes_before is None else minutes_before
|
||||||
except (TypeError, ValueError):
|
if is_trading_session():
|
||||||
return DEFAULT_MINUTES_BEFORE
|
return True
|
||||||
|
if not _premarket_enabled():
|
||||||
|
return False
|
||||||
|
return in_premarket_connect_window(minutes_before=mins)
|
||||||
|
|
||||||
|
|
||||||
def start_ctp_premarket_connect_worker(
|
def start_ctp_premarket_connect_worker(
|
||||||
@@ -48,17 +58,12 @@ def start_ctp_premarket_connect_worker(
|
|||||||
def _loop() -> None:
|
def _loop() -> None:
|
||||||
time.sleep(10)
|
time.sleep(10)
|
||||||
while True:
|
while True:
|
||||||
|
sleep_sec = max(30, interval)
|
||||||
try:
|
try:
|
||||||
gs = get_setting_fn
|
gs = get_setting_fn
|
||||||
if gs is None:
|
if gs is None:
|
||||||
from fee_specs import get_setting as gs
|
from fee_specs import get_setting as gs
|
||||||
if (
|
if is_ctp_auto_connect_enabled(gs) and should_auto_connect_now():
|
||||||
is_ctp_auto_connect_enabled(gs)
|
|
||||||
and _premarket_enabled()
|
|
||||||
and in_premarket_connect_window(
|
|
||||||
minutes_before=_minutes_before_open(),
|
|
||||||
)
|
|
||||||
):
|
|
||||||
mode = get_mode_fn()
|
mode = get_mode_fn()
|
||||||
st = ctp_status(mode)
|
st = ctp_status(mode)
|
||||||
if (
|
if (
|
||||||
@@ -68,13 +73,20 @@ def start_ctp_premarket_connect_worker(
|
|||||||
):
|
):
|
||||||
info = ctp_start_connect(mode, force=False)
|
info = ctp_start_connect(mode, force=False)
|
||||||
if info.get("started"):
|
if info.get("started"):
|
||||||
logger.info(
|
if is_trading_session():
|
||||||
"盘前自动连接 CTP [%s](开盘前 %d 分钟)",
|
logger.info("交易时段内自动连接 CTP [%s]", mode)
|
||||||
mode,
|
else:
|
||||||
_minutes_before_open(),
|
logger.info(
|
||||||
)
|
"盘前自动连接 CTP [%s](开盘前 %d 分钟)",
|
||||||
|
mode,
|
||||||
|
premarket_minutes_before(),
|
||||||
|
)
|
||||||
|
if not is_trading_session() and in_premarket_connect_window(
|
||||||
|
minutes_before=premarket_minutes_before(),
|
||||||
|
):
|
||||||
|
sleep_sec = 30
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("CTP premarket connect worker: %s", exc)
|
logger.warning("CTP premarket connect worker: %s", exc)
|
||||||
time.sleep(max(30, interval))
|
time.sleep(sleep_sec)
|
||||||
|
|
||||||
threading.Thread(target=_loop, daemon=True, name="ctp-premarket-connect").start()
|
threading.Thread(target=_loop, daemon=True, name="ctp-premarket-connect").start()
|
||||||
|
|||||||
+7
-4
@@ -12,6 +12,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
|
from ctp_premarket_connect import should_auto_connect_now
|
||||||
from ctp_settings import is_ctp_auto_connect_enabled
|
from ctp_settings import is_ctp_auto_connect_enabled
|
||||||
from vnpy_bridge import ctp_try_auto_reconnect
|
from vnpy_bridge import ctp_try_auto_reconnect
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def start_ctp_reconnect_worker(
|
|||||||
get_setting_fn: Callable[[str, str], str] | None = None,
|
get_setting_fn: Callable[[str, str], str] | None = None,
|
||||||
interval: int = RECONNECT_INTERVAL_SEC,
|
interval: int = RECONNECT_INTERVAL_SEC,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""定时检测 CTP 连接,断线后自动重连。"""
|
"""定时检测 CTP 连接;仅在交易时段或盘前窗口内尝试重连,避免非交易时段反复登录。"""
|
||||||
|
|
||||||
def _loop() -> None:
|
def _loop() -> None:
|
||||||
while True:
|
while True:
|
||||||
@@ -42,9 +43,11 @@ def start_ctp_reconnect_worker(
|
|||||||
gs = get_setting_fn
|
gs = get_setting_fn
|
||||||
if gs is None:
|
if gs is None:
|
||||||
from fee_specs import get_setting as gs
|
from fee_specs import get_setting as gs
|
||||||
if not is_ctp_auto_connect_enabled(gs):
|
if (
|
||||||
pass
|
is_ctp_auto_connect_enabled(gs)
|
||||||
elif _auto_reconnect_enabled():
|
and _auto_reconnect_enabled()
|
||||||
|
and should_auto_connect_now()
|
||||||
|
):
|
||||||
mode = get_mode_fn()
|
mode = get_mode_fn()
|
||||||
if ctp_try_auto_reconnect(mode):
|
if ctp_try_auto_reconnect(mode):
|
||||||
logger.debug("CTP 连接正常 [%s]", mode)
|
logger.debug("CTP 连接正常 [%s]", mode)
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
# 期货公司实盘 CTP
|
||||||
|
|
||||||
|
本文说明如何接入 **期货公司实盘 CTP**,并对比 **SimNow 模拟盘** 与 **实盘** 的开平仓逻辑是否一致。
|
||||||
|
|
||||||
|
相关代码:`vnpy_bridge.py`、`ctp_settings.py`、`trading_context.py`、`install_trading.py`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、SimNow 与实盘的关系
|
||||||
|
|
||||||
|
| 项目 | 模拟盘(SimNow) | 实盘(期货公司 CTP) |
|
||||||
|
|------|------------------|----------------------|
|
||||||
|
| 系统设置 | **模拟盘 · SimNow** | **期货公司实盘** |
|
||||||
|
| 配置项 | `SIMNOW_*` / `simnow_*` | `CTP_LIVE_*` / `ctp_live_*` |
|
||||||
|
| 连接对象 | SimNow 仿真前置 | 开户期货公司提供的 TD/MD 前置 |
|
||||||
|
| 资金与持仓 | SimNow 柜台 | 真实资金与持仓 |
|
||||||
|
| **报单代码路径** | `execute_order()` → `vnpy_bridge.send_order()` | **完全相同** |
|
||||||
|
|
||||||
|
> 本系统 **没有** 本地假撮合。模拟盘与实盘均通过 **vnpy_ctp** 向 CTP 柜台发真实委托;区别仅在于 **连哪组前置、用哪套账号**,以及柜台返回的保证金/费率/拒单规则。
|
||||||
|
|
||||||
|
SimNow 接入步骤见 [SIMNOW.md](./SIMNOW.md)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实盘接入前准备
|
||||||
|
|
||||||
|
### 1. 向期货公司申请
|
||||||
|
|
||||||
|
通常需要:
|
||||||
|
|
||||||
|
1. 期货账户已开户并完成适当性评估
|
||||||
|
2. 申请 **CTP 程序化交易** 或 **API 接入**(各公司流程不同)
|
||||||
|
3. 获取 **看穿式监管** 所需的:
|
||||||
|
- 经纪商代码(BrokerID)
|
||||||
|
- 投资者代码 / 资金账号
|
||||||
|
- 交易密码
|
||||||
|
- **交易前置(TD)**、**行情前置(MD)** 地址(`tcp://IP:端口`)
|
||||||
|
- **AppID(产品名称)**、**AuthCode(授权编码)** — 实盘必须由期货公司分配,**不能** 使用 SimNow 的 `simnow_client_test`
|
||||||
|
|
||||||
|
4. 在期货公司 **仿真/实盘环境** 中确认 AppID 已报备、账号已绑定
|
||||||
|
|
||||||
|
具体以开户营业部或官方 API 文档为准。
|
||||||
|
|
||||||
|
### 2. 配置 `.env`
|
||||||
|
|
||||||
|
```env
|
||||||
|
TRADING_MODE=live
|
||||||
|
|
||||||
|
CTP_LIVE_USER=你的投资者代码
|
||||||
|
CTP_LIVE_PASSWORD=你的交易密码
|
||||||
|
CTP_LIVE_BROKER_ID=期货公司BrokerID
|
||||||
|
CTP_LIVE_TD_ADDRESS=tcp://xxx.xxx.xxx.xxx:端口
|
||||||
|
CTP_LIVE_MD_ADDRESS=tcp://xxx.xxx.xxx.xxx:端口
|
||||||
|
CTP_LIVE_APP_ID=期货公司分配的AppID
|
||||||
|
CTP_LIVE_AUTH_CODE=期货公司分配的AuthCode
|
||||||
|
CTP_LIVE_ENV=实盘
|
||||||
|
```
|
||||||
|
|
||||||
|
也可在 **系统设置 → CTP 连接 → 期货公司实盘** 中填写;**页面保存优先于 `.env`**。
|
||||||
|
|
||||||
|
| 变量 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `CTP_LIVE_USER` | 投资者代码(InvestorID),非手机号 |
|
||||||
|
| `CTP_LIVE_PASSWORD` | 交易密码 |
|
||||||
|
| `CTP_LIVE_BROKER_ID` | 期货公司 BrokerID(每家不同,**不是** SimNow 的 9999) |
|
||||||
|
| `CTP_LIVE_TD_ADDRESS` | 交易服务器 |
|
||||||
|
| `CTP_LIVE_MD_ADDRESS` | 行情服务器 |
|
||||||
|
| `CTP_LIVE_APP_ID` | 看穿式 AppID |
|
||||||
|
| `CTP_LIVE_AUTH_CODE` | 看穿式授权码 |
|
||||||
|
| `CTP_LIVE_ENV` | 一般为 **实盘**;测评环境按期货公司要求 |
|
||||||
|
|
||||||
|
### 3. 切换与连接
|
||||||
|
|
||||||
|
1. **系统设置 → 交易模式** 选 **期货公司实盘**
|
||||||
|
2. 保存 CTP 实盘账号与前置
|
||||||
|
3. **下单监控** → **连接 CTP**
|
||||||
|
4. 顶栏显示 **CTP 已连接**、**期货公司实盘**,权益为柜台资金
|
||||||
|
|
||||||
|
修改模式或前置后建议 **重连 CTP** 或 `pm2 restart qihuo`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、开平仓逻辑(代码层)
|
||||||
|
|
||||||
|
模拟盘与实盘共用 **`vnpy_bridge.send_order()`**,映射如下。
|
||||||
|
|
||||||
|
### 开仓
|
||||||
|
|
||||||
|
| 本系统参数 | CTP / vnpy |
|
||||||
|
|------------|------------|
|
||||||
|
| `offset=open` + `direction=long` | Direction.**LONG** + Offset.**OPEN**(买开) |
|
||||||
|
| `offset=open` + `direction=short` | Direction.**SHORT** + Offset.**OPEN**(卖开) |
|
||||||
|
|
||||||
|
### 平仓
|
||||||
|
|
||||||
|
| 本系统参数 | CTP 方向 | 开平标志 |
|
||||||
|
|------------|----------|----------|
|
||||||
|
| `offset=close_long` + 持多 | Direction.**SHORT**(卖平) | 见下表 |
|
||||||
|
| `offset=close_short` + 持空 | Direction.**LONG**(买平) | 见下表 |
|
||||||
|
|
||||||
|
**开平标志** 由 `_resolve_close_offset()` 按 **交易所规则** 自动选择(与 SimNow/实盘 CTP 规范一致):
|
||||||
|
|
||||||
|
| 交易所 | 规则 |
|
||||||
|
|--------|------|
|
||||||
|
| **大商所(DCE)** 等 | 使用通用 **CLOSE** |
|
||||||
|
| **上期所(SHFE)、能源(INE)、郑商所(CZCE)、中金所(CFFEX)** | 区分 **平今 CLOSETODAY** / **平昨 CLOSEYESTERDAY** |
|
||||||
|
| 选择依据 | 查询持仓的今仓 `td_volume`、昨仓 `yd_volume`,优先平今再平昨 |
|
||||||
|
|
||||||
|
这与国内期货公司 CTP 客户端、快期等 **标准投机平仓逻辑** 一致。
|
||||||
|
|
||||||
|
### 委托类型
|
||||||
|
|
||||||
|
| 页面选项 | vnpy 类型 | 说明 |
|
||||||
|
|----------|-----------|------|
|
||||||
|
| 限价 | `OrderType.LIMIT` | 价格按最小变动价位取整 |
|
||||||
|
| 市价 | `OrderType.FAK` + 对手价偏移 | 非「无价格市价单」,而是 **带滑点的限价 FAK**,以提高 SimNow/各前置成交率 |
|
||||||
|
|
||||||
|
止盈止损触发、手动平仓、策略平仓均走 **`order_type=market`** 的上述 FAK 逻辑。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、SimNow 开平仓是否符合期货公司实盘逻辑?
|
||||||
|
|
||||||
|
### 结论(简要)
|
||||||
|
|
||||||
|
| 层级 | 是否一致 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| **CTP 报单语义**(开/平、买卖方向、平今平昨) | **是** | 同一套 `send_order`,符合国内 CTP 规范 |
|
||||||
|
| **交易所平今平昨规则** | **是** | 按 SHFE/INE/CZCE/CFFEX 与 DCE 分别处理 |
|
||||||
|
| **连接与鉴权** | **否(环境不同)** | BrokerID、前置、AppID/AuthCode 必须换期货公司参数 |
|
||||||
|
| **柜台业务规则** | **部分一致** | SimNow 仿真在保证金、合约、拒单细节上可能与实盘有差异 |
|
||||||
|
| **本程序附加层** | **两模式相同** | 本地 SL/TP、移动保本、风控、微信推送等与柜台无关,SimNow/实盘行为一致 |
|
||||||
|
|
||||||
|
### 一致的部分(可放心联调)
|
||||||
|
|
||||||
|
1. **开仓**:买开 / 卖开 → `Offset.OPEN`
|
||||||
|
2. **平仓**:平多 = 卖 + 平今/平昨/平;平空 = 买 + 平今/平昨/平
|
||||||
|
3. **持仓同步**:来自 CTP 柜台回报,写入监控与 `trade_logs` 同步逻辑相同
|
||||||
|
4. **撤单、挂单超时**:对 CTP 委托号操作,两模式相同
|
||||||
|
5. **策略 / 关键位自动单**:最终都调用 `execute_order()`,无「模拟专用分支」
|
||||||
|
|
||||||
|
在 SimNow 上验证通过的开平仓流程,**报单层面** 可直接用于实盘;实盘主要风险在 **账号权限、资金、AppID 报备、网络到期货公司前置**,而非本系统换一套开平逻辑。
|
||||||
|
|
||||||
|
### 可能差异(接入实盘时需自行核对)
|
||||||
|
|
||||||
|
| 项目 | SimNow | 实盘 |
|
||||||
|
|------|--------|------|
|
||||||
|
| BrokerID | 固定 9999 | 各期货公司不同 |
|
||||||
|
| AppID / AuthCode | 默认测试值 | 必须向期货公司申请 |
|
||||||
|
| 保证金 / 手续费 | 仿真柜台 | 真实费率,以 CTP 查询为准 |
|
||||||
|
| 合约限制 | 部分合约或规则简化 | 以期货公司 + 交易所为准 |
|
||||||
|
| 拒单原因 | 仿真环境 | 资金不足、非交易时间、未报备 AppID、风控拒单等 |
|
||||||
|
| 看穿式 | `SIMNOW_ENV=实盘` | `CTP_LIVE_ENV=实盘`,且 AppID 必须在期货公司报备 |
|
||||||
|
|
||||||
|
### 本系统「非 CTP 标准」但两模式相同的部分
|
||||||
|
|
||||||
|
以下 **不是** 期货公司客户端自带功能,而是 **本程序本地逻辑**;SimNow 与实盘 **行为一致**,但都与「只用手动在快期下单」不同:
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 本地止盈 / 止损 | 程序监控价格,触发后 **再向 CTP 发平仓单** |
|
||||||
|
| 移动保本 | 动态抬止损,触发后市价 FAK 平仓 |
|
||||||
|
| 挂单超时撤单 | 本系统定时检查 pending 监控并撤单 |
|
||||||
|
| 账户冷静期 / 仓位上限 | 本系统风控,在发单前拦截 |
|
||||||
|
| 开单计划 / 关键支阻区 | 仅提醒,不自动报单(关键位自动单除外) |
|
||||||
|
|
||||||
|
实盘使用时需知:CTP 柜台 **不会** 自动执行上述本地 SL/TP;只有程序运行且 CTP 已连接时才会生效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、实盘常见问题
|
||||||
|
|
||||||
|
| 现象 | 处理 |
|
||||||
|
|------|------|
|
||||||
|
| 连接失败 / 4097 | 核对 AppID、AuthCode、BrokerID;升级 `vnpy_ctp`;`CTP_LIVE_ENV=实盘` |
|
||||||
|
| 不合法的登录 | 投资者代码或密码错误;是否在期货公司柜台改过密码 |
|
||||||
|
| 登录成功但拒单 | 资金、交易时段、合约代码、价格 tick、AppID 未报备 |
|
||||||
|
| 平今平昨报错 | 检查是否持有对应今/昨仓;上期所等必须正确平今/平昨 |
|
||||||
|
| 服务器连不上前置 | 期货公司是否要求 **白名单 IP**;云服务器出网是否放行 |
|
||||||
|
| 与 SimNow 行为不一致 | 优先查 **保证金、费率、合约**;报单方向/开平标志可在日志中看 `CTP 报单 … offset=` |
|
||||||
|
|
||||||
|
诊断 SimNow 可用 `python scripts/test_simnow.py`;实盘可将 `.env` 临时改为 live 配置后在同脚本逻辑下连 TD(需自行改脚本或看 PM2/应用日志)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、日志中如何确认报单
|
||||||
|
|
||||||
|
连接成功后,下单会在日志中输出类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CTP 报单 rb2510 SHFE Direction.SHORT 2手 @3205 offset=Offset.CLOSETODAY type=OrderType.FAK
|
||||||
|
```
|
||||||
|
|
||||||
|
- **offset=Offset.OPEN** → 开仓
|
||||||
|
- **offset=Offset.CLOSETODAY / CLOSEYESTERDAY / CLOSE** → 平仓类型
|
||||||
|
- **Direction** 与持仓方向相反 → 平仓方向正确
|
||||||
|
|
||||||
|
SimNow 与实盘日志格式相同,仅 `mode` 与前置地址不同。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、相关文档
|
||||||
|
|
||||||
|
| 文档 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| [SIMNOW.md](./SIMNOW.md) | SimNow 注册与仿真接入 |
|
||||||
|
| [TRADING.md](./TRADING.md) | 下单监控、两种通道概览 |
|
||||||
|
| [ORDER_MONITOR.md](./ORDER_MONITOR.md) | 手动开平仓、SL/TP、移动保本 |
|
||||||
|
| [DEPLOY.md](./DEPLOY.md) | 部署与环境变量总表 |
|
||||||
|
| [RISK.md](./RISK.md) | 账户风控(实盘同样生效) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、实盘接入检查清单
|
||||||
|
|
||||||
|
- [ ] 已向期货公司申请 CTP / 程序化权限
|
||||||
|
- [ ] 已获取 BrokerID、TD/MD 地址、AppID、AuthCode
|
||||||
|
- [ ] `.env` 或系统设置中 `CTP_LIVE_*` 已填写
|
||||||
|
- [ ] 系统设置交易模式为 **期货公司实盘**
|
||||||
|
- [ ] 下单监控 **连接 CTP** 成功,权益为真实资金
|
||||||
|
- [ ] 用小仓位测试:开仓 → 平仓(含上期所品种测平今)
|
||||||
|
- [ ] 确认本地 SL/TP 触发后能在 CTP 看到平仓委托
|
||||||
|
- [ ] 生产环境限制访问(HTTPS、防火墙、强密码)
|
||||||
+3
-1
@@ -36,9 +36,10 @@
|
|||||||
| 文档 | 说明 |
|
| 文档 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [TRADING.md](./TRADING.md) | 可开仓品种、计仓、SimNow/实盘 |
|
| [TRADING.md](./TRADING.md) | 可开仓品种、计仓、SimNow/实盘 |
|
||||||
|
| [SIMNOW.md](./SIMNOW.md) | SimNow 仿真注册与接入 |
|
||||||
|
| [CTP_LIVE.md](./CTP_LIVE.md) | **期货公司实盘 CTP** 与开平仓对比 |
|
||||||
| [DEPLOY.md](./DEPLOY.md) | 部署说明 |
|
| [DEPLOY.md](./DEPLOY.md) | 部署说明 |
|
||||||
| [BACKUP.md](./BACKUP.md) | 数据备份与恢复 |
|
| [BACKUP.md](./BACKUP.md) | 数据备份与恢复 |
|
||||||
| [SIMNOW.md](./SIMNOW.md) | SimNow 模拟盘 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,3 +54,4 @@
|
|||||||
| AI 何时分析、推什么 | [AI.md](./AI.md) |
|
| AI 何时分析、推什么 | [AI.md](./AI.md) |
|
||||||
| 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) |
|
| 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) |
|
||||||
| 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) |
|
| 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) |
|
||||||
|
| 实盘 CTP 怎么接、和 SimNow 开平是否一样 | [CTP_LIVE.md](./CTP_LIVE.md) |
|
||||||
|
|||||||
+2
-1
@@ -16,7 +16,7 @@ SimNow 是上海期货交易所全资子公司 **上海期货信息技术有限
|
|||||||
| 维护方 | 上期技术(官方) | 各期货公司 |
|
| 维护方 | 上期技术(官方) | 各期货公司 |
|
||||||
| 适用场景 | 程序下单、策略联调、熟悉规则 | 人工在该公司客户端练习 |
|
| 适用场景 | 程序下单、策略联调、熟悉规则 | 人工在该公司客户端练习 |
|
||||||
|
|
||||||
本项目的 **模拟盘 · SimNow** 模式,即把 `.env` 中的账号连到 SimNow 的 CTP 前置,与后期接入期货公司实盘 CTP 使用同一套代码路径。
|
本项目的 **模拟盘 · SimNow** 模式,即把 `.env` 中的账号连到 SimNow 的 CTP 前置,与 **期货公司实盘 CTP** 使用同一套报单代码路径(见 [CTP_LIVE.md](./CTP_LIVE.md))。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -216,6 +216,7 @@ pm2 restart qihuo
|
|||||||
| 文档 | 内容 |
|
| 文档 | 内容 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| [TRADING.md](./TRADING.md) | 模拟盘 / 实盘通道、API、页面说明 |
|
| [TRADING.md](./TRADING.md) | 模拟盘 / 实盘通道、API、页面说明 |
|
||||||
|
| [CTP_LIVE.md](./CTP_LIVE.md) | 期货公司实盘 CTP 与 SimNow 开平仓对比 |
|
||||||
| [DEPLOY.md](./DEPLOY.md) | 服务器部署、vnpy 编译、PM2、环境变量总表 |
|
| [DEPLOY.md](./DEPLOY.md) | 服务器部署、vnpy 编译、PM2、环境变量总表 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+2
-1
@@ -20,7 +20,8 @@
|
|||||||
| **模拟盘** | SimNow(vnpy → CTP 仿真前置) | SimNow 账户权益 |
|
| **模拟盘** | SimNow(vnpy → CTP 仿真前置) | SimNow 账户权益 |
|
||||||
| **实盘** | 期货公司 CTP(`.env` 中 `CTP_LIVE_*`) | 柜台权益 |
|
| **实盘** | 期货公司 CTP(`.env` 中 `CTP_LIVE_*`) | 柜台权益 |
|
||||||
|
|
||||||
模拟盘与实盘均走 **vnpy_ctp**,无本地假撮合。
|
模拟盘与实盘均走 **vnpy_ctp**,无本地假撮合。
|
||||||
|
**实盘接入与开平仓对比** → [CTP_LIVE.md](./CTP_LIVE.md) · SimNow → [SIMNOW.md](./SIMNOW.md)
|
||||||
|
|
||||||
## 下单与持仓
|
## 下单与持仓
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ from vnpy_bridge import (
|
|||||||
get_bridge,
|
get_bridge,
|
||||||
set_position_refresh_callback,
|
set_position_refresh_callback,
|
||||||
set_tick_sl_tp_callback,
|
set_tick_sl_tp_callback,
|
||||||
|
set_ctp_connected_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1587,6 +1588,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
lambda: _push_position_snapshot_async(fast=True)
|
lambda: _push_position_snapshot_async(fast=True)
|
||||||
)
|
)
|
||||||
set_tick_sl_tp_callback(_on_tick_sl_tp)
|
set_tick_sl_tp_callback(_on_tick_sl_tp)
|
||||||
|
set_ctp_connected_callback(_on_ctp_connected)
|
||||||
|
|
||||||
def _warm() -> None:
|
def _warm() -> None:
|
||||||
try:
|
try:
|
||||||
@@ -1608,6 +1610,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("bootstrap ctp connect: %s", exc)
|
logger.debug("bootstrap ctp connect: %s", exc)
|
||||||
|
|
||||||
|
def _on_ctp_connected(mode: str) -> None:
|
||||||
|
if mode != get_trading_mode(get_setting):
|
||||||
|
return
|
||||||
|
_schedule_recommend_refresh()
|
||||||
|
|
||||||
@app.route("/trade")
|
@app.route("/trade")
|
||||||
@login_required
|
@login_required
|
||||||
def trade_page():
|
def trade_page():
|
||||||
|
|||||||
@@ -295,6 +295,7 @@ def list_product_recommendations(
|
|||||||
try:
|
try:
|
||||||
quote = quote_fn(ths) or {}
|
quote = quote_fn(ths) or {}
|
||||||
price = quote.get("price")
|
price = quote.get("price")
|
||||||
|
main_code = (quote.get("ths_code") or "").strip()
|
||||||
row = assess_product_for_capital(
|
row = assess_product_for_capital(
|
||||||
product, capital, price,
|
product, capital, price,
|
||||||
max_margin_pct=max_margin_pct,
|
max_margin_pct=max_margin_pct,
|
||||||
@@ -302,7 +303,6 @@ def list_product_recommendations(
|
|||||||
ctp_connected=ctp_connected,
|
ctp_connected=ctp_connected,
|
||||||
main_code=main_code,
|
main_code=main_code,
|
||||||
)
|
)
|
||||||
main_code = (quote.get("ths_code") or "").strip()
|
|
||||||
row["main_code"] = main_code
|
row["main_code"] = main_code
|
||||||
if main_code:
|
if main_code:
|
||||||
row.update(analyze_product_daily(main_code))
|
row.update(analyze_product_daily(main_code))
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ def _load_persisted_last_error() -> str:
|
|||||||
|
|
||||||
_position_refresh_callback: Optional[Callable[[], None]] = None
|
_position_refresh_callback: Optional[Callable[[], None]] = None
|
||||||
_tick_sl_tp_callback: Optional[Callable[[str, str, float], None]] = None
|
_tick_sl_tp_callback: Optional[Callable[[str, str, float], None]] = None
|
||||||
|
_ctp_connected_callback: Optional[Callable[[str], None]] = None
|
||||||
_position_refresh_debounce_lock = threading.Lock()
|
_position_refresh_debounce_lock = threading.Lock()
|
||||||
_position_refresh_debounce_ts: float = 0.0
|
_position_refresh_debounce_ts: float = 0.0
|
||||||
|
|
||||||
@@ -89,6 +90,24 @@ def set_tick_sl_tp_callback(fn: Optional[Callable[[str, str, float], None]]) ->
|
|||||||
_tick_sl_tp_callback = fn
|
_tick_sl_tp_callback = fn
|
||||||
|
|
||||||
|
|
||||||
|
def set_ctp_connected_callback(fn: Optional[Callable[[str], None]]) -> None:
|
||||||
|
"""CTP 交易通道登录成功后回调(mode=simulation|live)。"""
|
||||||
|
global _ctp_connected_callback
|
||||||
|
_ctp_connected_callback = fn
|
||||||
|
|
||||||
|
|
||||||
|
def _fire_ctp_connected_callback(mode: str) -> None:
|
||||||
|
fn = _ctp_connected_callback
|
||||||
|
if not fn:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
threading.Thread(
|
||||||
|
target=fn, args=(mode,), daemon=True, name="ctp-connected-cb",
|
||||||
|
).start()
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("ctp connected callback: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
def _fire_position_refresh_callback() -> None:
|
def _fire_position_refresh_callback() -> None:
|
||||||
fn = _position_refresh_callback
|
fn = _position_refresh_callback
|
||||||
if not fn:
|
if not fn:
|
||||||
@@ -634,6 +653,7 @@ class CtpBridge:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("post-connect calibrate: %s", exc)
|
logger.debug("post-connect calibrate: %s", exc)
|
||||||
_fire_position_refresh_burst()
|
_fire_position_refresh_burst()
|
||||||
|
_fire_ctp_connected_callback(mode)
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
self._ee.unregister(EVENT_LOG, _on_log)
|
self._ee.unregister(EVENT_LOG, _on_log)
|
||||||
|
|||||||
Reference in New Issue
Block a user