Restructure into modules/ with single-process CTP and config/ layout.

Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-01 14:42:16 +08:00
parent b354d6c701
commit e5a586f903
209 changed files with 21962 additions and 20963 deletions
+2 -61
View File
@@ -1,61 +1,2 @@
# 服务配置
HOST=0.0.0.0
PORT=6600
DEBUG=false
SECRET_KEY=change-this-to-a-random-secret-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-on-first-login
ADMIN_SYNC_FROM_ENV=false
WECHAT_WEBHOOK=
QUOTE_SOURCE=sina
THS_REFRESH_TOKEN=
# 交易模式:simulation=SimNowlive=期货公司(系统设置页可改)
TRADING_MODE=simulation
POSITION_SIZING_MODE=risk
RISK_PERCENT=1
# CTP 断线后后台自动重连(true/false)
CTP_AUTO_RECONNECT=true
# —— SimNow 模拟盘(也可在「系统设置 → CTP 连接」配置,优先于本文件)——
SIMNOW_USER=
SIMNOW_PASSWORD=
SIMNOW_BROKER_ID=9999
# 7×24 / 日盘前置(deploy.sh 会自动 nc 探测并写入可用线路)
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 看穿式前置固定用「实盘」;仅穿透式测评才用「测试」
SIMNOW_ENV=实盘
# —— 期货公司实盘(后期接入)——
CTP_LIVE_USER=
CTP_LIVE_PASSWORD=
CTP_LIVE_BROKER_ID=
CTP_LIVE_TD_ADDRESS=
CTP_LIVE_MD_ADDRESS=
CTP_LIVE_APP_ID=
CTP_LIVE_AUTH_CODE=
CTP_LIVE_PRODUCT_INFO=
# 账户冷静期
RISK_CONTROL_ENABLED=true
RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
MAX_ACTIVE_POSITIONS=1
RISK_DAILY_POSITION_LIMIT=5
RISK_DAILY_TRADING_RISK_PCT=2
TRADING_DAY_RESET_HOUR=8
# —— 数据库(生产推荐 PostgreSQL,见 docs/POSTGRES.md)——
# 未配置 DATABASE_URL 时使用本地 SQLite futures.db
# DATABASE_URL=postgresql://qihuo:your_password@127.0.0.1:5432/qihuo
# PG_POOL_MIN=2
# PG_POOL_MAX=20
# 环境变量模板已迁移至 config/.env.example
# 使用: cp config/.env.example config/.env
+1
View File
@@ -1,4 +1,5 @@
.env
config/.env
*.db
__pycache__/
*.py[cod]
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.settings.admin_settings
from modules.settings.admin_settings import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.ai_client
from modules.notify.ai_client import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.ai_messages
from modules.notify.ai_messages import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.ai_worker
from modules.notify.ai_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.contract_profile
from modules.core.contract_profile import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.contract_specs
from modules.core.contract_specs import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_entry_price
from modules.ctp.ctp_entry_price import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_fee_sync
from modules.ctp.ctp_fee_sync import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_fee_worker
from modules.ctp.ctp_fee_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_ipc_client
from modules.ctp.ctp_ipc_client import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_kline
from modules.ctp.ctp_kline import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_premarket_connect
from modules.ctp.ctp_premarket_connect import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_reconnect
from modules.ctp.ctp_reconnect import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_settings
from modules.ctp.ctp_settings import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_symbol
from modules.ctp.ctp_symbol import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_trade_sync
from modules.ctp.ctp_trade_sync import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_trading_state
from modules.ctp.ctp_trading_state import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.ctp_worker
from modules.ctp.ctp_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.stats.dashboard_lib
from modules.stats.dashboard_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.backup.db_backup
from modules.backup.db_backup import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.db_conn
from modules.core.db_conn import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.doc_render
from modules.core.doc_render import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.env_file
from modules.core.env_file import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.fees.fee_specs
from modules.fees.fee_specs import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.fees.fee_sync
from modules.fees.fee_sync import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.keys.key_monitor_lib
from modules.keys.key_monitor_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.kline_chart
from modules.market.kline_chart import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.kline_store
from modules.market.kline_store import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.kline_stream
from modules.market.kline_stream import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.locale_fix
from modules.core.locale_fix import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.market
from modules.market.market import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.market.market_sessions
from modules.market.market_sessions import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.settings.nav_settings
from modules.settings.nav_settings import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.order_pending
from modules.trading.order_pending import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.pending_order_worker
from modules.trading.pending_order_worker import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.position_sizing
from modules.trading.position_sizing import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.position_stream
from modules.trading.position_stream import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.product_recommend
from modules.trading.product_recommend import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.recommend_store
from modules.trading.recommend_store import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.recommend_stream
from modules.trading.recommend_stream import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.recommend_trend
from modules.trading.recommend_trend import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.risk.account_risk_lib
from modules.risk.account_risk_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.sl_tp_guard
from modules.trading.sl_tp_guard import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.stats.stats_engine
from modules.stats.stats_engine import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.fib_lib
from modules.strategy.fib_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_db
from modules.strategy.strategy_db import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_roll_lib
from modules.strategy.strategy_roll_lib import * # noqa: F401,F403
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_roll_monitor_lib
from modules.strategy.strategy_roll_monitor_lib import * # noqa: F401,F403
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_snapshot_lib
from modules.strategy.strategy_snapshot_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.strategy.strategy_trend_lib
from modules.strategy.strategy_trend_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.symbols
from modules.core.symbols import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.trade_log_lib
from modules.trading.trade_log_lib import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.trading.trade_notify
from modules.trading.trade_notify import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.core.trading_context
from modules.core.trading_context import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.ctp.vnpy_bridge
from modules.ctp.vnpy_bridge import * # noqa: F401,F403
+2
View File
@@ -0,0 +1,2 @@
# Compatibility shim — use modules.notify.wechat_notify
from modules.notify.wechat_notify import * # noqa: F401,F403
+51 -1442
View File
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
# 服务配置
HOST=0.0.0.0
PORT=6600
DEBUG=false
SECRET_KEY=change-this-to-a-random-secret-key
ADMIN_USERNAME=admin
ADMIN_PASSWORD=change-me-on-first-login
ADMIN_SYNC_FROM_ENV=false
WECHAT_WEBHOOK=
QUOTE_SOURCE=sina
THS_REFRESH_TOKEN=
# 交易模式:simulation=SimNowlive=期货公司(系统设置页可改)
TRADING_MODE=simulation
POSITION_SIZING_MODE=risk
RISK_PERCENT=1
# CTP 断线后后台自动重连(true/false)
CTP_AUTO_RECONNECT=true
# —— SimNow 模拟盘(也可在「系统设置 → CTP 连接」配置,优先于本文件)——
SIMNOW_USER=
SIMNOW_PASSWORD=
SIMNOW_BROKER_ID=9999
# 7×24 / 日盘前置(deploy.sh 会自动 nc 探测并写入可用线路)
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 看穿式前置固定用「实盘」;仅穿透式测评才用「测试」
SIMNOW_ENV=实盘
# —— 期货公司实盘(后期接入)——
CTP_LIVE_USER=
CTP_LIVE_PASSWORD=
CTP_LIVE_BROKER_ID=
CTP_LIVE_TD_ADDRESS=
CTP_LIVE_MD_ADDRESS=
CTP_LIVE_APP_ID=
CTP_LIVE_AUTH_CODE=
CTP_LIVE_PRODUCT_INFO=
# 账户冷静期
RISK_CONTROL_ENABLED=true
RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
MAX_ACTIVE_POSITIONS=1
RISK_DAILY_POSITION_LIMIT=5
RISK_DAILY_TRADING_RISK_PCT=2
TRADING_DAY_RESET_HOUR=8
# —— 数据库(生产推荐 PostgreSQL,见 docs/POSTGRES.md)——
# 未配置 DATABASE_URL 时使用本地 SQLite futures.db
# DATABASE_URL=postgresql://qihuo:your_password@127.0.0.1:5432/qihuo
# PG_POOL_MIN=2
# PG_POOL_MAX=20
+15 -13
View File
@@ -161,25 +161,27 @@ pip install --upgrade pip -q
pip install -r "$APP_DIR/requirements.txt"
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
echo "==> 配置 .env..."
if [ ! -f "$APP_DIR/.env" ]; then
cp "$APP_DIR/.env.example" "$APP_DIR/.env"
echo "==> 配置 config/.env..."
ENV_FILE="$APP_DIR/config/.env"
mkdir -p "$APP_DIR/config"
if [ ! -f "$ENV_FILE" ]; then
cp "$APP_DIR/config/.env.example" "$ENV_FILE"
RAND_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$APP_DIR/.env"
echo " 已生成 .env,请编辑 SIMNOW_USER / ADMIN_PASSWORD"
sed -i "s/change-this-to-a-random-secret-key/${RAND_KEY}/" "$ENV_FILE"
echo " 已生成 config/.env,请编辑 SIMNOW_USER / ADMIN_PASSWORD"
fi
ensure_env_key "$APP_DIR/.env" "SIMNOW_ENV" "实盘"
ensure_env_key "$APP_DIR/.env" "CTP_AUTO_RECONNECT" "true"
ensure_env_key "$APP_DIR/.env" "SIMNOW_BROKER_ID" "9999"
ensure_env_key "$APP_DIR/.env" "SIMNOW_APP_ID" "simnow_client_test"
ensure_env_key "$APP_DIR/.env" "SIMNOW_AUTH_CODE" "0000000000000000"
update_simnow_front_in_env "$APP_DIR/.env" || true
ensure_env_key "$ENV_FILE" "SIMNOW_ENV" "实盘"
ensure_env_key "$ENV_FILE" "CTP_AUTO_RECONNECT" "true"
ensure_env_key "$ENV_FILE" "SIMNOW_BROKER_ID" "9999"
ensure_env_key "$ENV_FILE" "SIMNOW_APP_ID" "simnow_client_test"
ensure_env_key "$ENV_FILE" "SIMNOW_AUTH_CODE" "0000000000000000"
update_simnow_front_in_env "$ENV_FILE" || true
mkdir -p "$APP_DIR/logs"
echo "==> 验证 CTP 环境..."
if grep -q "^SIMNOW_USER=.\+" "$APP_DIR/.env" 2>/dev/null && \
grep -q "^SIMNOW_PASSWORD=.\+" "$APP_DIR/.env" 2>/dev/null; then
if grep -q "^SIMNOW_USER=.\+" "$ENV_FILE" 2>/dev/null && \
grep -q "^SIMNOW_PASSWORD=.\+" "$ENV_FILE" 2>/dev/null; then
set +e
python "$APP_DIR/scripts/test_simnow.py"
CTP_TEST=$?
+50
View File
@@ -0,0 +1,50 @@
# 主目录结构
```
qihuo/ # 主文件夹(仓库根)
├── app.py # 主程序入口(Flask 启动)
├── requirements.txt
├── deploy.sh # 一键部署脚本
├── ecosystem.config.cjs # PM2 启动配置
├── config/
│ ├── .env.example # 环境变量模板
│ └── .env # 运行时配置(git 忽略)
├── modules/ # 业务模块(每个模块 register(deps)
│ ├── core/ # DB、路径、公共工具
│ ├── web/ # 页面路由 + static/ + templates/
│ ├── trading/ # 下单监控、持仓、推荐
│ ├── ctp/ # vn.py / CTP 连接与报单
│ ├── risk/ # 账户风控
│ ├── strategy/ # 趋势、滚仓策略
│ ├── keys/ # 关键位
│ ├── plans/ # 开单计划
│ ├── market/ # 行情、K 线
│ ├── records/ # 交易记录、复盘
│ ├── stats/ # 统计、看板
│ ├── settings/ # 系统设置
│ ├── notify/ # 微信、AI 消息
│ ├── fees/ # 手续费
│ └── backup/ # 备份
├── _legacy/ # 旧 import 兼容 shimPM2 PYTHONPATH
├── data/ # 静态数据(如 fee_rates.json
├── docs/ # 文档
├── scripts/ # 运维/诊断脚本(非运行时)
├── futures.db # SQLite(未配 PG 时)
├── uploads/
└── logs/
```
根目录 `_legacy/` 为旧 `import db_conn` 等路径的兼容层;新代码请 `from modules.xxx import ...`
## 进程模型
- **单进程**PM2 仅 `qihuo``app.py` + CTP 同进程)
- 详见 [DEPLOY.md](./DEPLOY.md)
## 模块契约
每个 `modules/<name>/` 提供 `register(deps: AppDeps)`;主程序 `app.py` 只做串联,不写业务。
## 发布
见 [DEPLOY.md](./DEPLOY.md)**本地修改 → git push → 服务器 git pull**,禁止 SCP。
+15 -16
View File
@@ -43,7 +43,7 @@ pm2 save
以下文件 **不**`git pull` 更新,卸载/重装时须 **单独备份与恢复**
- `/opt/qihuo/.env`
- `/opt/qihuo/config/.env`(兼容旧版 `/opt/qihuo/.env`
- `/opt/qihuo/futures.db`SQLite)或 PostgreSQL 数据
- `/opt/qihuo/uploads/`
- `/opt/qihuo/backups/`(若有)
@@ -58,18 +58,17 @@ pm2 save
| 运行用户 | `root`(与 `deploy.sh` / PM2 配置一致) |
| Web 端口 | `6600`(对外) |
| CTP Worker 端口 | `6601`(仅 `127.0.0.1`,Web 进程 IPC 调用,勿对外开放) |
| 进程管理 | PM2`qihuo`Flask Web+ `qihuo-ctp`CTP / vn.py 独立进程) |
| 进程管理 | PM2**仅** `qihuo`Flask + CTP 单进程) |
| 数据库 | **生产推荐 PostgreSQL**(见 [POSTGRES.md](./POSTGRES.md));未配置 `DATABASE_URL` 时使用 SQLite `futures.db` |
| 仓库 | https://git.bz121.com/dekun/qihuo.git |
### 进程架构(2026-03 起)
### 进程架构(2026-07:单进程
| PM2 应用 | 角色 | 说明 |
|----------|------|------|
| `qihuo` | Web`QIHUO_CTP_ROLE=client`) | Flask、页面、API、数据库;通过 HTTP 调用本机 Worker |
| `qihuo-ctp` | Worker`QIHUO_CTP_ROLE=worker` | **唯一** 加载 vn.py / vnpy_ctp;CTP 连接、报单、持仓回调、止盈止损 tick、滚仓监控 |
| PM2 应用 | 说明 |
|----------|------|
| `qihuo` | Flask Web + **vn.py / CTP 同进程**`vnpy_bridge.CtpBridge` |
Web 进程崩溃或重启 **不会** 直接带走 CTP 原生连接;Worker 重启后 Web 会自动通过 IPC 恢复读写。两个进程的 Token 须一致(见 `ecosystem.config.cjs``QIHUO_CTP_WORKER_TOKEN`
详见 [ARCHITECTURE.md](./ARCHITECTURE.md)。旧版 `qihuo-ctp` 独立 Worker **已废弃**`ecosystem.config.cjs` 不再启动该进程
---
@@ -110,7 +109,7 @@ bash deploy.sh
6. 首次生成 `.env`,并补全 `SIMNOW_ENV=实盘``CTP_AUTO_RECONNECT=true` 等缺项
7. **自动探测 SimNow 前置**`nc` 测端口),写入可用的 `SIMNOW_TD/MD_ADDRESS`(优先 `182.254.243.31`,其次 `180.168.146.187`
8. 若已配置 SimNow 账号,运行 `scripts/test_simnow.py` 验证连接
9. `pm2 restart ecosystem.config.cjs --update-env` 或首次 `pm2 start ecosystem.config.cjs`,并 `pm2 save`同时启动 **`qihuo`** **`qihuo-ctp`**
9. `pm2 restart ecosystem.config.cjs --update-env` 或首次 `pm2 start ecosystem.config.cjs`,并 `pm2 save` **`qihuo`** 一个进程
部署完成后访问:`http://<服务器IP>:6600`
@@ -141,7 +140,7 @@ MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
```bash
# 在服务器上
cp /opt/qihuo/.env /root/qihuo.env.bak
cp /opt/qihuo/config/.env /root/qihuo.env.bak 2>/dev/null || cp /opt/qihuo/.env /root/qihuo.env.bak 2>/dev/null || true
# SQLite
cp /opt/qihuo/futures.db /root/futures.db.bak 2>/dev/null || true
# PostgreSQL 见 POSTGRES.md 备份命令
@@ -151,8 +150,8 @@ 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 stop qihuo 2>/dev/null || true
pm2 delete qihuo 2>/dev/null || true
pm2 save
rm -rf /opt/qihuo
```
@@ -174,7 +173,7 @@ bash deploy.sh
```bash
cd /opt/qihuo && git log -1 --oneline # 须与远端 main 最新提交一致
pm2 status # qihuo、qihuo-ctp 均为 online
pm2 status # qihuo 为 online
```
浏览器访问 `http://<服务器IP>:6600` 登录验证。
@@ -225,8 +224,8 @@ python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
若提示找不到模块,查看本文「CTP / vnpy 故障排查」一节。
```bash
cp .env.example .env
nano .env
cp config/.env.example config/.env
nano config/.env
```
| 变量 | 说明 |
@@ -318,7 +317,7 @@ pm2 restart ecosystem.config.cjs --update-env
pm2 save
```
> **同时重启** `qihuo``qihuo-ctp`。仅 `pm2 restart qihuo` 会导致 Web 与 Worker 代码/协议不一致
> 更新后执行 `pm2 restart ecosystem.config.cjs --update-env` 即可(仅 `qihuo`
若服务器曾用 SCP 覆盖文件导致 `git pull` 冲突,用 `git reset --hard origin/main` 与远端对齐。
+1 -27
View File
@@ -21,12 +21,10 @@ module.exports = {
max_memory_restart: "8192M",
env: {
NODE_ENV: "production",
PYTHONPATH: path.join(ROOT, "_legacy"),
LANG: "zh_CN.UTF-8",
LC_ALL: "zh_CN.UTF-8",
LC_CTYPE: "zh_CN.UTF-8",
QIHUO_CTP_ROLE: "client",
QIHUO_CTP_WORKER_URL: "http://127.0.0.1:6601",
QIHUO_CTP_WORKER_TOKEN: "qihuo-local-ctp",
QIHUO_STARTUP_WORKERS: "8",
QIHUO_MEMORY_MB: "8192",
},
@@ -34,29 +32,5 @@ module.exports = {
out_file: path.join(ROOT, "logs", "pm2-out.log"),
time: true,
},
{
name: "qihuo-ctp",
script: "ctp_worker.py",
cwd: ROOT,
interpreter,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "8192M",
env: {
NODE_ENV: "production",
LANG: "zh_CN.UTF-8",
LC_ALL: "zh_CN.UTF-8",
LC_CTYPE: "zh_CN.UTF-8",
QIHUO_CTP_ROLE: "worker",
QIHUO_CTP_WORKER_HOST: "127.0.0.1",
QIHUO_CTP_WORKER_PORT: "6601",
QIHUO_CTP_WORKER_TOKEN: "qihuo-local-ctp",
QIHUO_MEMORY_MB: "8192",
},
error_file: path.join(ROOT, "logs", "pm2-ctp-error.log"),
out_file: path.join(ROOT, "logs", "pm2-ctp-out.log"),
time: true,
},
],
};
+3 -4684
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Qihuo feature modules package."""
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
from modules.backup.routes import register
__all__ = ["register"]
+3 -2
View File
@@ -22,7 +22,7 @@ from pathlib import Path
from typing import Callable, Optional
from zoneinfo import ZoneInfo
from db_conn import DB_PATH, db_backend
from modules.core.db_conn import DB_PATH, db_backend
logger = logging.getLogger(__name__)
@@ -107,7 +107,8 @@ cp -a uploads/. /opt/qihuo/uploads/
def _app_root() -> Path:
return Path(os.path.dirname(os.path.abspath(__file__)))
from modules.core.paths import ROOT
return ROOT
def default_backup_dir() -> str:
+78
View File
@@ -0,0 +1,78 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""HTTP routes for backup module."""
from __future__ import annotations
from datetime import date, datetime
from flask import (
Response,
flash,
jsonify,
redirect,
render_template,
request,
send_file,
session,
stream_with_context,
url_for,
)
def register(deps) -> None:
app = deps.app
login_required = deps.login_required
require_nav = deps.require_nav
get_db = deps.get_db
get_setting = deps.get_setting
set_setting = deps.set_setting
fetch_price = deps.fetch_price
send_wechat_msg = deps.send_wechat_msg
touch_stats_cache = deps.touch_stats_cache
get_stats_data = deps.get_stats_data
build_market_quote_payload = deps.build_market_quote_payload
today_str = deps.today_str
expire_old_plans = deps.expire_old_plans
TZ = deps.tz
DB_PATH = deps.db_path
UPLOAD_DIR = deps.upload_dir
OPEN_TYPES = deps.open_types
EXIT_TRIGGERS = deps.exit_triggers
BEHAVIOR_TAGS = deps.behavior_tags
KLINE_PERIODS = deps.kline_periods
KLINE_CUTOFFS = deps.kline_cutoffs
calc_holding_duration = deps.calc_holding_duration
holding_to_minutes = deps.holding_to_minutes
classify_close_result = deps.classify_close_result
calc_rr_ratio = deps.calc_rr_ratio
calc_theoretical_pnl = deps.calc_theoretical_pnl
parse_review_date_filter = deps.parse_review_date_filter
_trading_mode = deps.trading_mode
_ua_is_phone = deps.ua_is_phone
_static_asset_v = deps.static_asset_v
from modules.backup.db_backup import list_backups, resolve_backup_file
@app.route("/api/backup/list")
@login_required
def api_backup_list():
return jsonify(
{
"dir": str(backup_dir()),
"last_at": get_backup_last_at(get_setting),
"running": backup_in_progress(),
"items": list_backups(),
}
)
@app.route("/api/backup/download/<filename>")
@login_required
def api_backup_download(filename):
from flask import send_file
try:
path = resolve_backup_file(filename)
except (ValueError, FileNotFoundError) as exc:
return jsonify({"error": str(exc)}), 404
return send_file(path, as_attachment=True, download_name=path.name)
+8
View File
@@ -0,0 +1,8 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Core bootstrap and shared types."""
from modules.core.bootstrap import register_all_modules, start_module_workers
from modules.core.deps import AppDeps
__all__ = ["AppDeps", "register_all_modules", "start_module_workers"]
+55
View File
@@ -0,0 +1,55 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Application module registry and startup wiring."""
from __future__ import annotations
import importlib
import logging
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from modules.core.deps import AppDeps
logger = logging.getLogger(__name__)
# Registration order: core services first, trading last among features.
_MODULE_NAMES = (
"modules.web",
"modules.market",
"modules.keys",
"modules.plans",
"modules.notify",
"modules.records",
"modules.stats",
"modules.fees",
"modules.backup",
"modules.settings",
"modules.risk",
"modules.strategy",
"modules.ctp",
"modules.trading",
)
def register_all_modules(deps: "AppDeps") -> None:
for name in _MODULE_NAMES:
mod = importlib.import_module(name)
register = getattr(mod, "register", None)
if not callable(register):
logger.warning("module %s has no register()", name)
continue
register(deps)
logger.debug("registered %s", name)
def start_module_workers(deps: "AppDeps") -> None:
"""Background threads owned by feature modules."""
from modules.ctp.vnpy_bridge import try_init_vnpy
try_init_vnpy({})
for name in ("modules.market",):
mod = importlib.import_module(name)
start = getattr(mod, "start_workers", None)
if callable(start):
start(deps)
@@ -10,8 +10,8 @@ from typing import Any, Optional
import requests
from contract_specs import get_contract_spec
from symbols import ths_to_codes, search_symbols
from modules.core.contract_specs import get_contract_spec
from modules.core.symbols import ths_to_codes, search_symbols
logger = logging.getLogger(__name__)
@@ -101,7 +101,7 @@ def margin_one_lot(
est = round(float(price) * spec["mult"] * spec["margin_rate"], 2)
if trading_mode:
try:
from vnpy_bridge import ctp_estimate_margin_one_lot, ctp_lookup_contract_spec, ctp_status
from modules.ctp.vnpy_bridge import ctp_estimate_margin_one_lot, ctp_lookup_contract_spec, ctp_status
if ctp_status(trading_mode).get("connected"):
ctp_margin = ctp_estimate_margin_one_lot(
+3 -1
View File
@@ -13,7 +13,9 @@ import threading
import time
from typing import Any, Iterable, Optional, Sequence
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
from modules.core.paths import DB_PATH as _ROOT_DB_PATH
DB_PATH = _ROOT_DB_PATH
_backend_lock = threading.Lock()
_backend: Optional[str] = None
+46
View File
@@ -0,0 +1,46 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Shared dependencies passed into each feature module at register() time."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Callable, Optional
@dataclass
class AppDeps:
app: Any
get_db: Callable
get_setting: Callable
set_setting: Callable
login_required: Callable
require_nav: Callable
fetch_price: Callable
send_wechat_msg: Callable
touch_stats_cache: Callable
get_stats_data: Callable
build_market_quote_payload: Callable
today_str: Callable
expire_old_plans: Callable
check_order_plans: Callable
check_key_monitors: Callable
background_task: Callable
start_background_threads: Callable
tz: Any
db_path: str
upload_dir: str
open_types: list
exit_triggers: list
behavior_tags: list
kline_periods: list
kline_cutoffs: list
calc_holding_duration: Callable
holding_to_minutes: Callable
classify_close_result: Callable
calc_rr_ratio: Callable
calc_theoretical_pnl: Callable
parse_review_date_filter: Callable
trading_mode: Callable
static_asset_v: Callable
ua_is_phone: Callable
+16 -2
View File
@@ -9,12 +9,26 @@ from __future__ import annotations
import os
import re
ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
from modules.core.paths import ENV_FILE, LEGACY_ENV_FILE
def _default_env_path() -> str:
if ENV_FILE.is_file():
return str(ENV_FILE)
if LEGACY_ENV_FILE.is_file():
return str(LEGACY_ENV_FILE)
return str(ENV_FILE)
ENV_PATH = _default_env_path()
_KEY_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*=")
def env_file_path(path: str | None = None) -> str:
return path or ENV_PATH
if path:
return path
from modules.core.paths import resolve_env_file
return resolve_env_file()
def _quote_env_value(value: str) -> str:
+37
View File
@@ -0,0 +1,37 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""Repository layout paths — single source for config, data, uploads."""
from __future__ import annotations
import os
from pathlib import Path
# .../qihuo/modules/core/paths.py -> repo root
ROOT = Path(__file__).resolve().parents[2]
CONFIG_DIR = ROOT / "config"
ENV_FILE = CONFIG_DIR / ".env"
LEGACY_ENV_FILE = ROOT / ".env"
DATA_DIR = ROOT / "data"
UPLOADS_DIR = ROOT / "uploads"
LOGS_DIR = ROOT / "logs"
DB_PATH = str(ROOT / "futures.db")
def ensure_runtime_dirs() -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
UPLOADS_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
def resolve_env_file() -> str:
"""Prefer config/.env, fall back to legacy root .env."""
if ENV_FILE.is_file():
return str(ENV_FILE)
if LEGACY_ENV_FILE.is_file():
return str(LEGACY_ENV_FILE)
return str(ENV_FILE)
+6 -6
View File
@@ -15,7 +15,7 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import date
from typing import Optional
from market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX
from modules.market.market import fetch_raw_for_volume, get_price as market_get_price, THS_EX_SUFFIX
PRODUCTS = [
{"name": "白银", "ths": "ag", "sina": "AG", "exchange": "上期所", "ex": "SHFE"},
@@ -106,7 +106,7 @@ def product_has_night_session(ths_or_product) -> bool:
def filter_for_trading_session(rows: list[dict]) -> list[dict]:
"""夜盘时段隐藏无夜盘品种。"""
from market_sessions import is_night_trading_session
from modules.market.market_sessions import is_night_trading_session
if not is_night_trading_session():
return rows
@@ -467,8 +467,8 @@ def search_symbols(query: str, *, capital: float | None = None, ctp_connected: b
return []
q_lower = q.lower()
from market_sessions import is_night_trading_session
from product_recommend import filter_products_for_capital, should_apply_small_account_scope
from modules.market.market_sessions import is_night_trading_session
from modules.trading.product_recommend import filter_products_for_capital, should_apply_small_account_scope
night_only = is_night_trading_session()
product_pool = PRODUCTS
@@ -503,7 +503,7 @@ def search_symbols(query: str, *, capital: float | None = None, ctp_connected: b
if capital is not None and should_apply_small_account_scope(
capital, ctp_connected=ctp_connected,
):
from product_recommend import product_in_small_account_whitelist
from modules.trading.product_recommend import product_in_small_account_whitelist
if not product or not product_in_small_account_whitelist(product):
return results
raw = fetch_raw_for_volume(codes["sina_code"])
@@ -631,7 +631,7 @@ def list_recommended_symbols_grouped(recommend_rows: list[dict]) -> list[dict]:
if not product:
continue
if not product_has_night_session(product):
from market_sessions import is_night_trading_session
from modules.market.market_sessions import is_night_trading_session
if is_night_trading_session():
continue
seen.add(ths_key)
@@ -18,7 +18,7 @@ def get_trading_mode(get_setting: Callable[[str, str], str]) -> str:
def get_sizing_mode(get_setting: Callable[[str, str], str]) -> str:
from position_sizing import normalize_sizing_mode
from modules.trading.position_sizing import normalize_sizing_mode
return normalize_sizing_mode(get_setting("position_sizing_mode", "fixed"))
@@ -84,7 +84,7 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
import json
try:
from position_stream import position_hub
from modules.trading.position_stream import position_hub
snap = position_hub.get_snapshot() or {}
cap = float(snap.get("capital") or 0)
@@ -93,7 +93,7 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
except Exception:
pass
try:
from db_conn import connect_db
from modules.core.db_conn import connect_db
conn = connect_db()
try:
@@ -121,7 +121,7 @@ def _cached_ctp_account(mode: str) -> dict[str, float]:
def _ctp_status_from_snapshot(mode: str) -> Optional[dict]:
"""读持仓快照中的 CTP 状态,避免页面渲染同步 IPC。"""
try:
from position_stream import position_hub
from modules.trading.position_stream import position_hub
snap = position_hub.get_snapshot() or {}
st = snap.get("ctp_status")
@@ -142,7 +142,7 @@ def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
if balance > 0:
return balance
try:
from vnpy_bridge import ctp_status, get_ctp_balance
from modules.ctp.vnpy_bridge import ctp_status, get_ctp_balance
st = ctp_status(mode)
if st.get("connected"):
@@ -159,7 +159,7 @@ def get_account_capital(conn, get_setting: Callable[[str, str], str]) -> float:
def get_recommend_capital(conn, get_setting: Callable[[str, str], str]) -> float:
"""可开仓品种表用权益:已连接 CTP 用柜台权益,未连接固定 10 万。"""
from product_recommend import DISCONNECTED_RECOMMEND_CAPITAL
from modules.trading.product_recommend import DISCONNECTED_RECOMMEND_CAPITAL
if is_ctp_connected(get_setting):
return get_account_capital(conn, get_setting)
@@ -173,7 +173,7 @@ def is_ctp_connected(get_setting: Callable[[str, str], str]) -> bool:
if st is not None:
return bool(st.get("connected"))
try:
from vnpy_bridge import ctp_status
from modules.ctp.vnpy_bridge import ctp_status
return bool(ctp_status(mode).get("connected"))
except Exception:
+10
View File
@@ -0,0 +1,10 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
"""CTP / vn.py integration — single-process mode."""
def register(deps) -> None:
del deps
__all__ = ["register"]
@@ -6,9 +6,9 @@ from __future__ import annotations
from typing import Any, Optional
from contract_specs import get_contract_spec
from ctp_symbol import ths_to_vnpy_symbol
from symbols import ths_to_codes
from modules.core.contract_specs import get_contract_spec
from modules.ctp.ctp_symbol import ths_to_vnpy_symbol
from modules.core.symbols import ths_to_codes
def symbols_match(ctp_sym: str, ths: str) -> bool:
@@ -11,9 +11,9 @@ import re
import time
from typing import Optional
from contract_specs import get_contract_spec
from fee_specs import upsert_fee_rate
from vnpy_bridge import get_bridge
from modules.core.contract_specs import get_contract_spec
from modules.fees.fee_specs import upsert_fee_rate
from modules.ctp.vnpy_bridge import get_bridge
logger = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ def _collect_main_ths_codes() -> list[str]:
"""从主力列表收集同花顺合约代码(供 CTP 手续费查询)。"""
from datetime import date
from symbols import PRODUCTS, build_ths_code, list_main_contracts_grouped
from modules.core.symbols import PRODUCTS, build_ths_code, list_main_contracts_grouped
symbols: list[str] = []
for group in list_main_contracts_grouped():
@@ -58,7 +58,7 @@ def try_daily_ctp_fee_sync(
return 0, "今日已从 CTP 同步过"
t0 = time.monotonic()
from ctp_fee_sync import sync_fees_from_ctp
from modules.ctp.ctp_fee_sync import sync_fees_from_ctp
count, msg = sync_fees_from_ctp(mode)
elapsed = time.monotonic() - t0
@@ -113,7 +113,7 @@ def start_ctp_fee_worker(
time.sleep(20)
while True:
try:
from vnpy_bridge import ctp_status
from modules.ctp.vnpy_bridge import ctp_status
mode = get_mode_fn()
st = ctp_status(mode)
+2 -2
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
import logging
from typing import Optional
from kline_chart import (
from modules.market.kline_chart import (
PERIOD_MINUTES,
_aggregate_bars,
_bar_datetime,
@@ -76,7 +76,7 @@ def compose_period_bars(bars_1m: list, period: str) -> list:
def fetch_ctp_klines(symbol: str, period: str, mode: str) -> Optional[list]:
"""CTP 已连接时由 tick 聚合 K 线;失败返回 None。"""
try:
from vnpy_bridge import ctp_status, get_bridge
from modules.ctp.vnpy_bridge import ctp_status, get_bridge
if not ctp_status(mode).get("connected"):
return None
@@ -12,13 +12,13 @@ import threading
import time
from typing import Callable
from market_sessions import (
from modules.market.market_sessions import (
in_premarket_connect_window,
in_postmarket_grace_window,
is_trading_session,
should_keep_ctp_connected,
)
from vnpy_bridge import ctp_start_connect, ctp_status
from modules.ctp.vnpy_bridge import ctp_start_connect, ctp_status
logger = logging.getLogger(__name__)
@@ -12,9 +12,9 @@ import threading
import time
from typing import Callable
from ctp_premarket_connect import premarket_minutes_before, should_auto_connect_now
from market_sessions import in_premarket_connect_window, is_trading_session
from vnpy_bridge import ctp_try_auto_reconnect
from modules.ctp.ctp_premarket_connect import premarket_minutes_before, should_auto_connect_now
from modules.market.market_sessions import in_premarket_connect_window, is_trading_session
from modules.ctp.vnpy_bridge import ctp_try_auto_reconnect
logger = logging.getLogger(__name__)
@@ -41,7 +41,7 @@ CTP_DISABLED_HINT = "CTP 自动连接已关闭(非交易时段不重连;开
def is_ctp_auto_connect_enabled(get_setting=None) -> bool:
"""系统设置:是否允许手动连接及非交易时段自动重连(盘前/交易时段计划连接不受此限制)。"""
if get_setting is None:
from fee_specs import get_setting as _gs
from modules.fees.fee_specs import get_setting as _gs
get_setting = _gs
val = (get_setting(CTP_AUTO_CONNECT_KEY, "1") or "1").strip().lower()
@@ -60,7 +60,7 @@ def save_ctp_auto_connect(form: Any, set_setting: Callable[[str, str], None]) ->
def _get_db_setting(key: str, default: str = "") -> str:
from fee_specs import get_setting
from modules.fees.fee_specs import get_setting
return (get_setting(key, default) or default).strip()
+1 -1
View File
@@ -9,7 +9,7 @@ from __future__ import annotations
import re
from typing import Optional, Tuple
from symbols import ths_to_codes
from modules.core.symbols import ths_to_codes
try:
from vnpy.trader.constant import Exchange
@@ -12,17 +12,17 @@ from datetime import datetime
from typing import Any, Callable, Optional
from zoneinfo import ZoneInfo
from contract_specs import calc_position_metrics
from ctp_symbol import ths_to_vnpy_symbol
from fee_specs import calc_round_trip_fee
from symbols import ths_to_codes
from trade_log_lib import (
from modules.core.contract_specs import calc_position_metrics
from modules.ctp.ctp_symbol import ths_to_vnpy_symbol
from modules.fees.fee_specs import calc_round_trip_fee
from modules.core.symbols import ths_to_codes
from modules.trading.trade_log_lib import (
calc_equity_after,
purge_duplicate_local_trade_logs,
ensure_trade_log_columns,
refresh_trade_log_equity_chain,
)
from vnpy_bridge import ctp_list_trades, ctp_status
from modules.ctp.vnpy_bridge import ctp_list_trades, ctp_status
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
@@ -291,11 +291,11 @@ def sync_trade_logs_from_ctp(
)
stats["synced"] += 1
try:
from trade_notify import notify_trade_log_close
from trading_context import trading_mode_label
from modules.trading.trade_notify import notify_trade_log_close
from modules.core.trading_context import trading_mode_label
from app import get_setting, send_wechat_msg
from ai_worker import schedule_ai_event_analysis
from db_conn import DB_PATH
from modules.notify.ai_worker import schedule_ai_event_analysis
from modules.core.db_conn import DB_PATH
notify_trade_log_close(
send_wechat=send_wechat_msg,
@@ -323,7 +323,7 @@ def sync_trade_logs_from_ctp(
if stats["synced"] or stats["updated"]:
try:
from stats_engine import refresh_stats_cache
from modules.stats.stats_engine import refresh_stats_cache
refresh_stats_cache(conn, capital)
except Exception as exc:
logger.debug("stats refresh after ctp trade sync: %s", exc)
@@ -44,7 +44,7 @@ def reconcile_position_avg(
) -> dict[str, Any]:
"""手数变化时采用柜台回报均价;手数不变时保持已锁定柜台价。"""
del tick, trades
from ctp_entry_price import round_to_tick
from modules.ctp.ctp_entry_price import round_to_tick
row = dict(new)
lots = int(row.get("lots") or 0)
+14 -14
View File
@@ -19,15 +19,15 @@ os.environ.setdefault("QIHUO_CTP_ROLE", "worker")
from flask import Flask, jsonify, request
from ctp_ipc_client import worker_token
from db_conn import DB_PATH, commit_retry, connect_db
from fee_specs import get_setting, set_setting
from locale_fix import ensure_process_locale
from market_sessions import is_trading_session
from sl_tp_guard import check_sl_tp_on_tick, ensure_monitor_order_columns, start_sl_tp_guard_worker
from modules.ctp.ctp_ipc_client import worker_token
from modules.core.db_conn import DB_PATH, commit_retry, connect_db
from modules.fees.fee_specs import get_setting, set_setting
from modules.core.locale_fix import ensure_process_locale
from modules.market.market_sessions import is_trading_session
from modules.trading.sl_tp_guard import check_sl_tp_on_tick, ensure_monitor_order_columns, start_sl_tp_guard_worker
from strategy.strategy_db import init_strategy_tables
from trading_context import get_account_capital, get_trading_mode, get_trailing_be_tick_buffer
from vnpy_bridge import (
from modules.core.trading_context import get_account_capital, get_trading_mode, get_trailing_be_tick_buffer
from modules.ctp.vnpy_bridge import (
_ctp_td_lock,
ctp_cancel_order,
ctp_disconnect,
@@ -103,7 +103,7 @@ def _mode_from_request() -> str:
def _fast_status(mode: str) -> dict[str, Any]:
"""Return worker/native bridge state without slow network probing."""
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
from modules.ctp.ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
try:
st = dict(get_bridge().status(mode) or {})
@@ -255,11 +255,11 @@ def _start_background_workers() -> None:
set_tick_sl_tp_callback(_on_tick_sl_tp)
set_ctp_connected_callback(_on_ctp_connected)
from ctp_fee_worker import start_ctp_fee_worker
from ctp_premarket_connect import start_ctp_premarket_connect_worker
from ctp_reconnect import start_ctp_reconnect_worker
from order_pending import reconcile_pending_orders
from pending_order_worker import start_pending_order_worker
from modules.ctp.ctp_fee_worker import start_ctp_fee_worker
from modules.ctp.ctp_premarket_connect import start_ctp_premarket_connect_worker
from modules.ctp.ctp_reconnect import start_ctp_reconnect_worker
from modules.trading.order_pending import reconcile_pending_orders
from modules.trading.pending_order_worker import start_pending_order_worker
def _mode() -> str:
return get_trading_mode(get_setting)
+30 -26
View File
@@ -15,14 +15,14 @@ from collections import deque
from typing import Any, Callable, Optional
import ctp_ipc_client
from locale_fix import ensure_process_locale
from modules.core.locale_fix import ensure_process_locale
if ctp_ipc_client.is_worker_role():
ensure_process_locale()
from ctp_settings import live_setting_dict, simnow_setting_dict
from ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
from contract_specs import get_contract_spec
from modules.ctp.ctp_settings import live_setting_dict, simnow_setting_dict
from modules.ctp.ctp_symbol import ths_to_vnpy_symbol, to_vnpy_exchange
from modules.core.contract_specs import get_contract_spec
logger = logging.getLogger(__name__)
@@ -37,11 +37,15 @@ CTP_LAST_ERROR_KEY = "ctp_last_error"
def _use_ctp_worker_client() -> bool:
"""默认单进程直连 CTP;仅当显式设置 QIHUO_CTP_WORKER=1 时使用独立 Worker IPC。"""
flag = (os.getenv("QIHUO_CTP_WORKER", "") or "").strip().lower()
if flag not in ("1", "true", "yes"):
return False
return not ctp_ipc_client.is_worker_role()
def _persist_login_cooldown(seconds: float) -> None:
from fee_specs import get_setting, set_setting
from modules.fees.fee_specs import get_setting, set_setting
new_until = time.time() + max(0.0, seconds)
try:
@@ -53,7 +57,7 @@ def _persist_login_cooldown(seconds: float) -> None:
def _persisted_login_cooldown_remaining() -> int:
from fee_specs import get_setting
from modules.fees.fee_specs import get_setting
try:
until = float(get_setting(CTP_COOLDOWN_UNTIL_KEY, "0") or 0)
@@ -63,19 +67,19 @@ def _persisted_login_cooldown_remaining() -> int:
def _clear_persisted_login_cooldown() -> None:
from fee_specs import set_setting
from modules.fees.fee_specs import set_setting
set_setting(CTP_COOLDOWN_UNTIL_KEY, "0")
def _persist_last_error(msg: str) -> None:
from fee_specs import set_setting
from modules.fees.fee_specs import set_setting
set_setting(CTP_LAST_ERROR_KEY, (msg or "").strip())
def _load_persisted_last_error() -> str:
from fee_specs import get_setting
from modules.fees.fee_specs import get_setting
return (get_setting(CTP_LAST_ERROR_KEY, "") or "").strip()
@@ -382,7 +386,7 @@ class CtpBridge:
def _on_position(event) -> None:
try:
from ctp_trading_state import trading_state
from modules.ctp.ctp_trading_state import trading_state
pos = event.data
row = self._position_row_from_vnpy(pos)
@@ -401,7 +405,7 @@ class CtpBridge:
if vol <= 0:
exchange = getattr(pos, "exchange", None)
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
from ctp_trading_state import position_key
from modules.ctp.ctp_trading_state import position_key
trading_state.remove_position(
position_key(ex_name, sym, d), notify=False,
@@ -433,7 +437,7 @@ class CtpBridge:
def _on_order(event) -> None:
try:
from ctp_trading_state import trading_state
from modules.ctp.ctp_trading_state import trading_state
order = event.data
row = self._order_row_from_vnpy(order)
@@ -540,7 +544,7 @@ class CtpBridge:
"td_volume": td,
}
try:
from ctp_entry_price import round_to_tick
from modules.ctp.ctp_entry_price import round_to_tick
ths = CtpBridge._vnpy_sym_to_ths(sym, ex_name) or sym
if price > 0:
@@ -555,7 +559,7 @@ class CtpBridge:
def calibrate_trading_state(self) -> None:
"""全量校准内存簿(读 vnpy 缓存,不 query 柜台)。"""
try:
from ctp_trading_state import trading_state
from modules.ctp.ctp_trading_state import trading_state
with _ctp_td_lock:
orders = self.list_active_orders()
@@ -650,7 +654,7 @@ class CtpBridge:
self._last_position_query_ts = 0.0
self._last_instruments_ready_ts = 0.0
try:
from ctp_trading_state import trading_state
from modules.ctp.ctp_trading_state import trading_state
trading_state.clear()
except Exception:
@@ -736,7 +740,7 @@ class CtpBridge:
}
def connect(self, mode: str, *, force: bool = False, scheduled: bool = False) -> None:
from ctp_settings import CTP_DISABLED_HINT
from modules.ctp.ctp_settings import CTP_DISABLED_HINT
if not _ctp_connect_permitted(scheduled=scheduled):
self._last_error = CTP_DISABLED_HINT
@@ -852,7 +856,7 @@ class CtpBridge:
self, mode: str, *, force: bool = False, scheduled: bool = False,
) -> dict[str, Any]:
"""后台连接,不阻塞 HTTP 请求。"""
from ctp_settings import CTP_DISABLED_HINT
from modules.ctp.ctp_settings import CTP_DISABLED_HINT
if not _ctp_connect_permitted(scheduled=scheduled):
self._last_error = CTP_DISABLED_HINT
@@ -1033,7 +1037,7 @@ class CtpBridge:
def reconnect_after_settings_saved(self, mode: str) -> dict[str, Any]:
"""保存前置/账号后关闭旧连接,并用数据库中的新配置重连。"""
from ctp_settings import is_ctp_auto_connect_enabled
from modules.ctp.ctp_settings import is_ctp_auto_connect_enabled
self._close_gateway()
self._last_error = ""
@@ -1048,14 +1052,14 @@ class CtpBridge:
def _run() -> None:
time.sleep(45)
try:
from ctp_fee_worker import try_daily_ctp_fee_sync
from modules.ctp.ctp_fee_worker import try_daily_ctp_fee_sync
def _gs(key: str, default: str = "") -> str:
from fee_specs import get_setting
from modules.fees.fee_specs import get_setting
return get_setting(key, default)
def _ss(key: str, val: str) -> None:
from fee_specs import set_setting
from modules.fees.fee_specs import set_setting
set_setting(key, val)
try_daily_ctp_fee_sync(
@@ -1472,7 +1476,7 @@ class CtpBridge:
price = self._price_from_tick(tick)
if price and price > 0:
try:
from ctp_trading_state import trading_state
from modules.ctp.ctp_trading_state import trading_state
trading_state.set_tick_price(ex_s, sym, price)
except Exception:
@@ -2359,13 +2363,13 @@ def vnpy_available() -> bool:
def _ctp_connect_permitted(*, scheduled: bool = False) -> bool:
"""scheduled=True:盘前/交易时段计划连接,不受「自动连接」开关限制。"""
from ctp_settings import is_ctp_auto_connect_enabled
from modules.ctp.ctp_settings import is_ctp_auto_connect_enabled
if is_ctp_auto_connect_enabled():
return True
if not scheduled:
return False
from ctp_premarket_connect import should_auto_connect_now
from modules.ctp.ctp_premarket_connect import should_auto_connect_now
return should_auto_connect_now()
@@ -2375,7 +2379,7 @@ def ctp_disconnect(*, set_disabled_hint: bool = False) -> None:
if _use_ctp_worker_client():
ctp_ipc_client.disconnect(set_disabled_hint=set_disabled_hint)
return
from ctp_settings import CTP_DISABLED_HINT
from modules.ctp.ctp_settings import CTP_DISABLED_HINT
b = get_bridge()
b._close_gateway()
@@ -2450,7 +2454,7 @@ def ctp_try_auto_reconnect(mode: str) -> bool:
def ctp_status(mode: str) -> dict[str, Any]:
from ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
from modules.ctp.ctp_settings import CTP_DISABLED_HINT, is_ctp_auto_connect_enabled
if _use_ctp_worker_client():
st = ctp_ipc_client.status(mode)
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025-2026 马建军. All rights reserved.
from modules.fees.routes import register
__all__ = ["register"]
+3 -3
View File
@@ -10,9 +10,9 @@ import re
from datetime import datetime
from typing import Optional
from contract_specs import get_contract_spec
from modules.core.contract_specs import get_contract_spec
from db_conn import connect_db, is_benign_migration_error
from modules.core.db_conn import connect_db, is_benign_migration_error
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "futures.db")
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
@@ -145,7 +145,7 @@ def get_fee_spec(ths_code: str, *, trading_mode: str = "simulation") -> dict:
if row:
return _row_to_spec(row, mult)
try:
from ctp_fee_sync import sync_fee_for_symbol
from modules.ctp.ctp_fee_sync import sync_fee_for_symbol
fields = sync_fee_for_symbol(trading_mode, ths_code)
if fields:
return {"product": product, **fields}
+2 -2
View File
@@ -7,8 +7,8 @@
import re
from typing import Any, Optional
from contract_specs import get_contract_spec
from fee_specs import get_fee_multiplier, upsert_fee_rate
from modules.core.contract_specs import get_contract_spec
from modules.fees.fee_specs import get_fee_multiplier, upsert_fee_rate
def _to_float(val: Any) -> float:

Some files were not shown because too many files have changed in this diff Show More