Document Git-only deploy workflow and reduce positions page IPC blocking.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-01 14:05:37 +08:00
parent 7748a88219
commit b354d6c701
6 changed files with 247 additions and 65 deletions
+5 -1
View File
@@ -33,6 +33,8 @@
## 快速开始 ## 快速开始
> **发布铁律**:本地改代码 → 提交并 `git push` → 服务器 **仅** `git pull` / `git reset --hard origin/main` 更新。**禁止 SCP 复制代码到服务器。** 详见 [部署文档 · 代码发布铁律](docs/DEPLOY.md#代码发布铁律强制不容置疑)。
**服务器(Ubuntu** **服务器(Ubuntu**
```bash ```bash
@@ -40,7 +42,7 @@ cd /opt/qihuo && bash deploy.sh
# 访问 http://<IP>:6600 # 访问 http://<IP>:6600
``` ```
**更新** **更新**(须先在本机 `git push`
```bash ```bash
cd /opt/qihuo cd /opt/qihuo
@@ -50,6 +52,8 @@ python scripts/run_schema_migrate.py
pm2 restart ecosystem.config.cjs --update-env pm2 restart ecosystem.config.cjs --update-env
``` ```
**禁止** 使用 `scp` / 手工复制更新服务器代码。
生产环境须同时维护 **`qihuo`**Web)与 **`qihuo-ctp`**CTP Worker)两个 PM2 进程。 生产环境须同时维护 **`qihuo`**Web)与 **`qihuo-ctp`**CTP Worker)两个 PM2 进程。
详见 [部署文档](docs/DEPLOY.md)。 详见 [部署文档](docs/DEPLOY.md)。
+100 -1
View File
@@ -4,6 +4,52 @@
--- ---
## 代码发布铁律(强制,不容置疑)
**所有代码变更必须且只能按以下三步执行,不得跳过、不得变通:**
| 步骤 | 在哪里 | 做什么 |
|------|--------|--------|
| **1. 本地修改** | 开发机 / 本仓库工作区 | 改代码、自测 |
| **2. 提交仓库** | `git.bz121.com` | `git add``git commit``git push origin main`(或约定分支) |
| **3. 更新服务器** | `/opt/qihuo` | **仅** `git fetch` + `git reset --hard origin/main`(或 `git pull`)→ 依赖/迁移 → `pm2 restart` |
### 严禁事项
- **禁止** 用 `scp``rsync`、SFTP、手工复制等方式把 `.py` / `.js` / `.html` / 模板 / 静态资源 **直接覆盖** 到服务器。
- **禁止** 在服务器上 `vim` 改业务代码后长期不提交仓库(`.env`、日志、上传文件除外)。
- **禁止** 「服务器上先改一版、本地以后再补提交」——服务器代码必须与远端 Git **完全一致**
违反上述规则会导致:`git pull` 冲突、Web 与 Worker 版本不一致、问题无法复现、回滚困难。**一律视为部署事故。**
### 服务器唯一合法更新命令
代码已推送到远端后,在服务器执行:
```bash
cd /opt/qihuo
git fetch origin
git reset --hard origin/main
source venv/bin/activate
pip install -r requirements.txt
python scripts/run_schema_migrate.py
pm2 restart ecosystem.config.cjs --update-env
pm2 save
```
或使用 `bash deploy.sh`(内部同样通过 Git 拉取,见下文)。
### 数据与配置(不受 Git 管理)
以下文件 **不**`git pull` 更新,卸载/重装时须 **单独备份与恢复**
- `/opt/qihuo/.env`
- `/opt/qihuo/futures.db`SQLite)或 PostgreSQL 数据
- `/opt/qihuo/uploads/`
- `/opt/qihuo/backups/`(若有)
---
## 部署概要 ## 部署概要
| 项目 | 默认值 | | 项目 | 默认值 |
@@ -87,6 +133,56 @@ MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
--- ---
## 服务器卸载与全新部署(Git 唯一来源)
当服务器代码被 SCP 弄乱、版本不可信、或需要与仓库 **完全对齐** 时,按本节 **卸载后重装**。全程 **只** 通过 Git 获取代码,**不得** SCP 复制业务文件。
### 1. 备份(必做)
```bash
# 在服务器上
cp /opt/qihuo/.env /root/qihuo.env.bak
# SQLite
cp /opt/qihuo/futures.db /root/futures.db.bak 2>/dev/null || true
# PostgreSQL 见 POSTGRES.md 备份命令
tar czf /root/qihuo_uploads.bak.tar.gz -C /opt/qihuo uploads 2>/dev/null || true
```
### 2. 卸载 PM2 与代码目录
```bash
pm2 stop qihuo qihuo-ctp 2>/dev/null || true
pm2 delete qihuo qihuo-ctp 2>/dev/null || true
pm2 save
rm -rf /opt/qihuo
```
> **不删除** `/root/qihuo.env.bak`、`/root/futures.db.bak` 等备份。
### 3. 从 Git 全新克隆并部署
```bash
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
cd /opt/qihuo
cp /root/qihuo.env.bak .env
# SQLite 恢复(若使用)
cp /root/futures.db.bak futures.db 2>/dev/null || true
bash deploy.sh
```
### 4. 验收
```bash
cd /opt/qihuo && git log -1 --oneline # 须与远端 main 最新提交一致
pm2 status # qihuo、qihuo-ctp 均为 online
```
浏览器访问 `http://<服务器IP>:6600` 登录验证。
此后所有更新 **只** 走上文「代码发布铁律」三步,**禁止** 再使用 SCP 更新代码。
---
## 手动部署 ## 手动部署
### 1. 安装系统依赖 ### 1. 安装系统依赖
@@ -206,6 +302,9 @@ mkdir -p /opt/qihuo/logs /opt/qihuo/uploads
## 更新部署 ## 更新部署
> **强制流程**:本地修改 → `git push` → 服务器 `git fetch && git reset --hard origin/main` → 迁移 → `pm2 restart`。
> **禁止 SCP 复制代码。** 详见上文 [代码发布铁律](#代码发布铁律强制不容置疑)。
代码已推送后,在服务器执行: 代码已推送后,在服务器执行:
```bash ```bash
@@ -383,7 +482,7 @@ ufw allow 6600/tcp
| **下单监控无持仓** | 未连接 CTP 或确实无仓 | 先点「连接 CTP」 | | **下单监控无持仓** | 未连接 CTP 或确实无仓 | 先点「连接 CTP」 |
| **`Could not resolve host`** | 服务器 DNS 故障 | 配置 systemd-resolved 公共 DNS,见下方 | | **`Could not resolve host`** | 服务器 DNS 故障 | 配置 systemd-resolved 公共 DNS,见下方 |
| `database is locked` | SQLite 并发 | **推荐改 PostgreSQL**`MIGRATE_SQLITE=1 bash scripts/deploy_postgres.sh`,见 [POSTGRES.md](./POSTGRES.md) | | `database is locked` | SQLite 并发 | **推荐改 PostgreSQL**`MIGRATE_SQLITE=1 bash scripts/deploy_postgres.sh`,见 [POSTGRES.md](./POSTGRES.md) |
| `git pull` 冲突 | 本地有修改 / SCP 部署 | `git fetch && git reset --hard origin/main` | | `git pull` 冲突 | 曾用 SCP 覆盖文件(**禁止**) | 按 [服务器卸载与全新部署](#服务器卸载与全新部署git-唯一来源) 或 `git reset --hard origin/main` 与远端对齐 |
查看应用是否在监听: 查看应用是否在监听:
+1 -1
View File
@@ -41,7 +41,7 @@
| [TRADING.md](./TRADING.md) | 可开仓品种、计仓、SimNow/实盘 | | [TRADING.md](./TRADING.md) | 可开仓品种、计仓、SimNow/实盘 |
| [SIMNOW.md](./SIMNOW.md) | SimNow 仿真注册与接入 | | [SIMNOW.md](./SIMNOW.md) | SimNow 仿真注册与接入 |
| [CTP_LIVE.md](./CTP_LIVE.md) | **期货公司实盘 CTP** 与开平仓对比 | | [CTP_LIVE.md](./CTP_LIVE.md) | **期货公司实盘 CTP** 与开平仓对比 |
| [DEPLOY.md](./DEPLOY.md) | 部署说明 | | [DEPLOY.md](./DEPLOY.md) | 部署说明(含 **代码发布铁律**:仅 Git 三步,禁止 SCP |
| [POSTGRES.md](./POSTGRES.md) | **PostgreSQL 生产库**(一键部署、迁移、备份恢复) | | [POSTGRES.md](./POSTGRES.md) | **PostgreSQL 生产库**(一键部署、迁移、备份恢复) |
| [BACKUP.md](./BACKUP.md) | 数据备份与恢复 | | [BACKUP.md](./BACKUP.md) | 数据备份与恢复 |
+76 -50
View File
@@ -154,6 +154,65 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"""注册交易相关路由。""" """注册交易相关路由。"""
_nav = require_nav _nav = require_nav
_live_refresh_lock = threading.Lock() _live_refresh_lock = threading.Lock()
_ctp_status_cache: dict = {"mode": "", "status": {}, "ts": 0.0}
_ctp_status_cache_lock = threading.Lock()
_ctp_status_refresh_flag = {"busy": False}
def _remember_ctp_status(mode: str, st: dict) -> None:
if not isinstance(st, dict) or not st:
return
with _ctp_status_cache_lock:
_ctp_status_cache["mode"] = mode
_ctp_status_cache["status"] = dict(st)
_ctp_status_cache["ts"] = time.time()
def _schedule_ctp_status_refresh(mode: str) -> None:
with _ctp_status_cache_lock:
if _ctp_status_refresh_flag["busy"]:
return
_ctp_status_refresh_flag["busy"] = True
def _run() -> None:
try:
st = dict(ctp_status(mode) or {})
_remember_ctp_status(mode, st)
snap = position_hub.get_snapshot()
if snap:
merged = dict(snap)
merged["ctp_status"] = st
position_hub.set_snapshot(merged)
except Exception as exc:
logger.debug("ctp status refresh: %s", exc)
finally:
with _ctp_status_cache_lock:
_ctp_status_refresh_flag["busy"] = False
threading.Thread(
target=_run,
daemon=True,
name="ctp-status-refresh",
).start()
def _cached_ctp_status(mode: str) -> dict:
"""页面/SSE 优先读快照与内存缓存,避免同步 worker IPC 阻塞 HTTP 线程。"""
try:
snap = position_hub.get_snapshot() or {}
st = snap.get("ctp_status")
if isinstance(st, dict) and st:
_remember_ctp_status(mode, st)
return dict(st)
except Exception:
pass
with _ctp_status_cache_lock:
if _ctp_status_cache["mode"] == mode and _ctp_status_cache["status"]:
return dict(_ctp_status_cache["status"])
_schedule_ctp_status_refresh(mode)
return {
"connected": False,
"connecting": True,
"last_error": "",
"mode_label": trading_mode_label(get_setting),
}
def _sizing_mode_label(mode: str) -> str: def _sizing_mode_label(mode: str) -> str:
m = normalize_sizing_mode(mode) m = normalize_sizing_mode(mode)
@@ -217,7 +276,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
get_fixed_lots_fn=lambda: get_fixed_lots(get_setting), get_fixed_lots_fn=lambda: get_fixed_lots(get_setting),
) )
def _recommend_payload(conn) -> dict: def _recommend_payload(conn, *, use_ctp_margin: bool = True) -> dict:
mode = get_trading_mode(get_setting) mode = get_trading_mode(get_setting)
return recommend_payload( return recommend_payload(
conn, conn,
@@ -226,6 +285,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
trading_mode=mode, trading_mode=mode,
sizing_mode=get_sizing_mode(get_setting), sizing_mode=get_sizing_mode(get_setting),
fixed_lots=get_fixed_lots(get_setting), fixed_lots=get_fixed_lots(get_setting),
use_ctp_margin=use_ctp_margin,
) )
def _recommend_capital(conn) -> float: def _recommend_capital(conn) -> float:
@@ -2011,6 +2071,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M") now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
mode = get_trading_mode(get_setting) mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode) ctp_st = ctp_status(mode)
_remember_ctp_status(mode, ctp_st)
capital = _capital(conn) capital = _capital(conn)
if ctp_st.get("connected") and not fast: if ctp_st.get("connected") and not fast:
_reconcile_pending(conn, mode, capital=capital) _reconcile_pending(conn, mode, capital=capital)
@@ -2061,45 +2122,19 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
} }
def _minimal_live_payload(conn) -> dict: def _minimal_live_payload(conn) -> dict:
"""CTP 直出兜底:不跑对账/写库,避免与后台 worker 争锁""" """零 IPC 兜底:仅读库 + 缓存 CTP 状态,持仓由后台 worker 补全"""
from zoneinfo import ZoneInfo
tz = ZoneInfo("Asia/Shanghai")
now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M")
mode = get_trading_mode(get_setting) mode = get_trading_mode(get_setting)
ctp_st = ctp_status(mode) ctp_st = _cached_ctp_status(mode)
capital = _capital(conn) capital = _capital(conn)
rows: list[dict] = []
if ctp_st.get("connected"):
for p in _ctp_positions(mode, refresh_if_empty=False):
lots = int(p.get("lots") or 0)
if lots <= 0:
continue
ths = _ctp_pos_to_ths_code(p) or (p.get("symbol") or "")
direction = p.get("direction") or "long"
mon = {"symbol": ths, "direction": direction}
try:
row = _compose_position_row(
conn,
mon=mon,
ctp=p,
mode=mode,
capital=capital,
now_iso=now_iso,
fast=True,
)
if row:
rows.append(row)
except Exception as exc:
logger.warning("minimal live row failed: %s", exc)
risk = get_risk_status( risk = get_risk_status(
conn, conn,
active_count=_effective_active_position_count(conn, mode), active_count=count_active_trade_monitors(conn),
equity=capital, equity=capital,
) )
syncing = bool(ctp_st.get("connected") or ctp_st.get("connecting"))
return { return {
"ok": True, "ok": True,
"rows": rows, "rows": [],
"active_orders": [], "active_orders": [],
"pending_orders": [], "pending_orders": [],
"capital": capital, "capital": capital,
@@ -2110,8 +2145,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
"night_session": is_night_trading_session(), "night_session": is_night_trading_session(),
"session_clock": trading_session_clock(), "session_clock": trading_session_clock(),
"pending_order_timeout_min": get_pending_order_timeout_min(get_setting), "pending_order_timeout_min": get_pending_order_timeout_min(get_setting),
"sync_state": trading_state.sync_state, "sync_state": "syncing" if syncing else trading_state.sync_state,
"sync_label": trading_state.sync_label(), "sync_label": "加载中…" if syncing else trading_state.sync_label(),
} }
def _normalize_live_payload(payload: dict) -> dict: def _normalize_live_payload(payload: dict) -> dict:
@@ -2445,10 +2480,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
conn.commit() conn.commit()
sizing = get_sizing_mode(get_setting) sizing = get_sizing_mode(get_setting)
max_pct = get_max_margin_pct(get_setting) max_pct = get_max_margin_pct(get_setting)
rec_cache = _recommend_payload(conn) rec_cache = _recommend_payload(conn, use_ctp_margin=False)
if rec_cache.get("needs_refresh"): if rec_cache.get("needs_refresh"):
_schedule_recommend_refresh() _schedule_recommend_refresh()
ctp_connected = is_ctp_connected(get_setting) ctp_connected = connected
margin_rec = small_account_margin_recommendations() margin_rec = small_account_margin_recommendations()
if not bootstrap_live: if not bootstrap_live:
bootstrap_live = { bootstrap_live = {
@@ -2529,13 +2564,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
def api_trading_stream(): def api_trading_stream():
from queue import Empty from queue import Empty
@stream_with_context
def generate(): def generate():
yield ": stream\n\n"
q = position_hub.subscribe() q = position_hub.subscribe()
try: try:
snap = position_hub.get_snapshot() snap = position_hub.get_snapshot()
if snap: if not snap:
yield sse_format("positions", snap)
else:
conn = get_db() conn = get_db()
try: try:
init_strategy_tables(conn) init_strategy_tables(conn)
@@ -2545,6 +2580,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
position_hub.set_snapshot(payload) position_hub.set_snapshot(payload)
yield sse_format("positions", payload) yield sse_format("positions", payload)
_push_position_snapshot_async(fast=True) _push_position_snapshot_async(fast=True)
else:
yield sse_format("positions", snap)
while True: while True:
try: try:
msg = q.get(timeout=25) msg = q.get(timeout=25)
@@ -2866,17 +2903,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
def _roll_ui_modes(): def _roll_ui_modes():
return frozenset({ADD_MODE_MARKET, ADD_MODE_BREAKOUT}) return frozenset({ADD_MODE_MARKET, ADD_MODE_BREAKOUT})
def _cached_ctp_status(mode: str) -> dict:
"""页面渲染优先读持仓快照里的 CTP 状态,避免每次打 worker IPC。"""
try:
snap = position_hub.get_snapshot() or {}
st = snap.get("ctp_status")
if isinstance(st, dict) and st:
return dict(st)
except Exception:
pass
return dict(ctp_status(mode) or {})
def _roll_filled_lots_map(conn, group_ids: list[int]) -> dict[int, int]: def _roll_filled_lots_map(conn, group_ids: list[int]) -> dict[int, int]:
if not group_ids: if not group_ids:
return {} return {}
+41 -6
View File
@@ -110,15 +110,29 @@ def recommend_cache_needs_refresh(
def _ctp_connected_for_mode(trading_mode: str) -> bool: def _ctp_connected_for_mode(trading_mode: str) -> bool:
try: try:
from vnpy_bridge import ctp_status from position_stream import position_hub
return bool(ctp_status(trading_mode).get("connected")) snap = position_hub.get_snapshot() or {}
st = snap.get("ctp_status")
if isinstance(st, dict) and st:
return bool(st.get("connected"))
except Exception: except Exception:
return False pass
del trading_mode
return False
def recommend_margin_used(trading_mode: str) -> float: def recommend_margin_used(trading_mode: str) -> float:
"""当前持仓已占用保证金(各持仓 CTP 回报之和,与柜台持仓保证金一致)。""" """当前持仓已占用保证金(各持仓 CTP 回报之和,与柜台持仓保证金一致)。"""
try:
from position_stream import position_hub
snap = position_hub.get_snapshot() or {}
raw = snap.get("margin_used")
if raw is not None:
return max(0.0, float(raw or 0))
except Exception:
pass
if not _ctp_connected_for_mode(trading_mode): if not _ctp_connected_for_mode(trading_mode):
return 0.0 return 0.0
try: try:
@@ -162,6 +176,7 @@ def enrich_recommend_rows(
max_margin_pct: float = 30.0, max_margin_pct: float = 30.0,
trading_mode: str = "simulation", trading_mode: str = "simulation",
margin_used: float = 0.0, margin_used: float = 0.0,
use_ctp_margin: bool = True,
) -> list[dict]: ) -> list[dict]:
"""用当前权益与保证金比例补算最大可开手数(兼容旧缓存)。""" """用当前权益与保证金比例补算最大可开手数(兼容旧缓存)。"""
cap = float(capital or 0) cap = float(capital or 0)
@@ -193,7 +208,7 @@ def enrich_recommend_rows(
code_for_margin, code_for_margin,
price, price,
direction="max", direction="max",
trading_mode=trading_mode if ctp_connected else None, trading_mode=trading_mode if (ctp_connected and use_ctp_margin) else None,
) )
if spec_used.get("mult"): if spec_used.get("mult"):
row["mult"] = spec_used["mult"] row["mult"] = spec_used["mult"]
@@ -340,19 +355,39 @@ def recommend_payload(
trading_mode: str = "simulation", trading_mode: str = "simulation",
sizing_mode: str = "fixed", sizing_mode: str = "fixed",
fixed_lots: int = 1, fixed_lots: int = 1,
use_ctp_margin: bool = True,
) -> dict: ) -> dict:
"""读取缓存并附带当前权益(展示用,可能与缓存计算时不同)。""" """读取缓存并附带当前权益(展示用,可能与缓存计算时不同)。"""
payload = load_recommend_cache(conn) payload = load_recommend_cache(conn)
cap = float(live_capital or 0) cap = float(live_capital or 0)
pct = max(1.0, min(100.0, float(max_margin_pct or 30.0))) pct = max(1.0, min(100.0, float(max_margin_pct or 30.0)))
used = recommend_margin_used(trading_mode) if use_ctp_margin:
used = recommend_margin_used(trading_mode)
else:
used = 0.0
try:
from position_stream import position_hub
snap = position_hub.get_snapshot() or {}
raw = snap.get("margin_used")
if raw is not None:
used = max(0.0, float(raw or 0))
except Exception:
pass
if used <= 0:
used = float(payload.get("margin_used") or 0)
budget_info = margin_budget_info(cap, pct, used) budget_info = margin_budget_info(cap, pct, used)
payload["capital"] = cap payload["capital"] = cap
payload["max_margin_pct"] = pct payload["max_margin_pct"] = pct
payload.update(budget_info) payload.update(budget_info)
rows = payload.get("rows") or [] rows = payload.get("rows") or []
rows = enrich_recommend_rows( rows = enrich_recommend_rows(
rows, cap, max_margin_pct=pct, trading_mode=trading_mode, margin_used=used, rows,
cap,
max_margin_pct=pct,
trading_mode=trading_mode,
margin_used=used,
use_ctp_margin=use_ctp_margin,
) )
rows = filter_rows_for_account_scope( rows = filter_rows_for_account_scope(
rows, cap, ctp_connected=_ctp_connected_for_mode(trading_mode), rows, cap, ctp_connected=_ctp_connected_for_mode(trading_mode),
+24 -6
View File
@@ -118,10 +118,29 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
return {} return {}
def _ctp_status_from_snapshot(mode: str) -> Optional[dict]:
"""读持仓快照中的 CTP 状态,避免页面渲染同步 IPC。"""
try:
from position_stream import position_hub
snap = position_hub.get_snapshot() or {}
st = snap.get("ctp_status")
if isinstance(st, dict) and st:
return st
except Exception:
pass
del mode
return None
def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float: def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
"""优先 SimNow/期货公司 CTP 权益;未连接时用最近快照或设置中的参考资金""" """优先读持仓/Worker 快照权益;无快照时才同步问 CTP"""
del conn del conn
mode = get_trading_mode(get_setting) mode = get_trading_mode(get_setting)
cached = _cached_ctp_account(mode)
balance = float(cached.get("balance") or 0)
if balance > 0:
return balance
try: try:
from vnpy_bridge import ctp_status, get_ctp_balance from vnpy_bridge import ctp_status, get_ctp_balance
@@ -132,10 +151,6 @@ def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
return float(bal) return float(bal)
except Exception: except Exception:
pass pass
cached = _cached_ctp_account(mode)
balance = float(cached.get("balance") or 0)
if balance > 0:
return balance
try: try:
return float(get_setting("live_capital", "0") or 0) return float(get_setting("live_capital", "0") or 0)
except (TypeError, ValueError): except (TypeError, ValueError):
@@ -153,10 +168,13 @@ def get_recommend_capital(conn, get_setting: Callable[[str, str], str]) -> float
def is_ctp_connected(get_setting: Callable[[str, str], str]) -> bool: def is_ctp_connected(get_setting: Callable[[str, str], str]) -> bool:
"""当前交易模式(SimNow / 实盘)是否已连接 CTP。""" """当前交易模式(SimNow / 实盘)是否已连接 CTP。"""
mode = get_trading_mode(get_setting)
st = _ctp_status_from_snapshot(mode)
if st is not None:
return bool(st.get("connected"))
try: try:
from vnpy_bridge import ctp_status from vnpy_bridge import ctp_status
mode = get_trading_mode(get_setting)
return bool(ctp_status(mode).get("connected")) return bool(ctp_status(mode).get("connected"))
except Exception: except Exception:
return False return False