Compare commits
236 Commits
38ff40111a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 61481d5749 | |||
| 08be5f34c8 | |||
| 2081bf2da9 | |||
| b6c3266a9e | |||
| d46cd7c3e1 | |||
| 2ec534c547 | |||
| 0bade8a01f | |||
| 1cd3039605 | |||
| 50bb04e2bb | |||
| 6b2c7ade95 | |||
| 16d90814a1 | |||
| 125cc60a3d | |||
| c5b822a92e | |||
| ae28bc6273 | |||
| 93bdce3568 | |||
| 6710692dfb | |||
| 530738ae93 | |||
| 3c53b2063f | |||
| 4657d26f5e | |||
| 075fae37ec | |||
| dca773d6be | |||
| 870dfb3bc0 | |||
| b0afff53af | |||
| 8ebe1a3c77 | |||
| 9379bc4f4f | |||
| 481086eddc | |||
| aae897b7eb | |||
| 5328673ce8 | |||
| 972ab5d08b | |||
| 98d63f38bf | |||
| 658982d2b9 | |||
| a34bcb6bfc | |||
| e5a586f903 | |||
| b354d6c701 | |||
| 7748a88219 | |||
| 5aba31f530 | |||
| 95156ca595 | |||
| 9cd81a3ea7 | |||
| 08d55411aa | |||
| cd636ff1c3 | |||
| d7ea7b9e8a | |||
| 06fbff04a7 | |||
| ef790c8a80 | |||
| 6abe06d935 | |||
| e418c2dcec | |||
| 1bb0028402 | |||
| 8e1baf9a73 | |||
| 47ee9602d1 | |||
| 843b68b412 | |||
| 2347276bf4 | |||
| 008f042fcc | |||
| a1f22624de | |||
| bc79ad308c | |||
| 1b276b5897 | |||
| 52aca456e9 | |||
| 39eac983ff | |||
| 1b3a7f1bdc | |||
| 2386eca324 | |||
| d324d30332 | |||
| 32838daae0 | |||
| b6b7bfb248 | |||
| 4552f4ef9c | |||
| 0b924fca87 | |||
| 8d2d09396b | |||
| e5f264b774 | |||
| 726ed7adef | |||
| 8ebad6e8a2 | |||
| d07fc4b70d | |||
| e6208e403e | |||
| 6e954da4e1 | |||
| 6c68808318 | |||
| fb23ee891c | |||
| 2f58159372 | |||
| ec4199b82c | |||
| 963ed141e5 | |||
| f41276806e | |||
| c8fef65de7 | |||
| 92c222584e | |||
| a6b3c4a657 | |||
| 21d68f6269 | |||
| 9dbf6b1f1e | |||
| c6c6c3fe83 | |||
| 2f5b5c4aae | |||
| d1ad0f9253 | |||
| 8b4b1a875c | |||
| 9a10ac8a51 | |||
| d8c6428eb5 | |||
| b460c6c4e5 | |||
| 28c54b1a3f | |||
| df79017b30 | |||
| 94c566fbe5 | |||
| c5262a0a54 | |||
| 44bec23296 | |||
| 7ce59d2d71 | |||
| 19676943d0 | |||
| 71c480a587 | |||
| fd2dba22fd | |||
| d366344b0f | |||
| 45ae57ed43 | |||
| b02c9d6ca0 | |||
| f8ff97f93d | |||
| 4d2463c9a9 | |||
| bfb1b95471 | |||
| 169136dd4a | |||
| 840e88daad | |||
| 0109b59f27 | |||
| e18d5feb72 | |||
| 4f4c4bb9fc | |||
| 7bb80ba538 | |||
| 24190bf679 | |||
| d3b92de703 | |||
| 69cf94d1df | |||
| 7f8b4cfefd | |||
| f2940d41e9 | |||
| fd49b08c08 | |||
| 9613fb0737 | |||
| 6d55a54946 | |||
| c79bb2ea4b | |||
| ddfe2a52aa | |||
| 631aa2c0ab | |||
| 3a150dd3d6 | |||
| 4ef33a367f | |||
| 382a9a0e14 | |||
| 6905373401 | |||
| 98239d29c1 | |||
| 508d85a282 | |||
| d3955309d9 | |||
| 9a55c61678 | |||
| 038eb9a403 | |||
| 7a4a3f08e5 | |||
| deb9501cbe | |||
| 42f2dad52a | |||
| cababd67f5 | |||
| aaf69329cb | |||
| 4eb5709d71 | |||
| ab9987e4c7 | |||
| 7b60f0dce5 | |||
| bdfa21def8 | |||
| 1cc0cd5f8d | |||
| 64adce0e24 | |||
| f457e7d6c9 | |||
| aad88a9e98 | |||
| e5f675c6ca | |||
| eb4414f741 | |||
| 7133a0e448 | |||
| 9f48f22d16 | |||
| a23f2c80ca | |||
| 7ea8fb6301 | |||
| ca541d5fc3 | |||
| b641a4eaa0 | |||
| a56370d2af | |||
| 04b6f5e72d | |||
| aba67a3d16 | |||
| 47dd946d66 | |||
| 480000e195 | |||
| 259d9e812d | |||
| 9c8b92d2bd | |||
| 4aebc2df49 | |||
| 5a6c89c662 | |||
| 72361233a0 | |||
| 240fbe7994 | |||
| 649c064c2f | |||
| 0741997818 | |||
| de74ffe5b9 | |||
| 9772f3d986 | |||
| c302e1f3ca | |||
| 12a30d4f0c | |||
| 4d60b958ce | |||
| 7daed9bd3a | |||
| 01de8dfb69 | |||
| 63beda3c71 | |||
| 367f32dd82 | |||
| 040436e9cc | |||
| 86e61df993 | |||
| bbcc5607ad | |||
| f31164076f | |||
| 598a1407e1 | |||
| f05362ea74 | |||
| fc425c0e9f | |||
| d127a53870 | |||
| 32f1fa2c66 | |||
| 074551490f | |||
| 9875ee6d44 | |||
| fe1b651900 | |||
| 397e9cd9d8 | |||
| f6ee13765d | |||
| 73b9dfdfdb | |||
| 23d0f1d6fa | |||
| 5af04ef661 | |||
| 3aa3e1ad30 | |||
| 049aaffdcf | |||
| 1d95950b5c | |||
| e01c011df5 | |||
| de6815d481 | |||
| 3fe4add8e1 | |||
| 09f4649d79 | |||
| 59420e0550 | |||
| eaca3d43ec | |||
| 9d4aea60f0 | |||
| aea9aca472 | |||
| 3ba3be6035 | |||
| 67683f5562 | |||
| 528d9811e3 | |||
| ca894dfd4d | |||
| b4250171d5 | |||
| 36e26973fb | |||
| d368317c1b | |||
| f73d436077 | |||
| 350d0fed6b | |||
| 28d6ae52b2 | |||
| 8726760b12 | |||
| 62cd868f79 | |||
| 709801305f | |||
| 38a38cb51d | |||
| 55d95b4c39 | |||
| 1688452f3f | |||
| 87aef80594 | |||
| 7b8a660309 | |||
| 6e423eebfb | |||
| 9c0e5d9c57 | |||
| 404872007f | |||
| 65992eb35e | |||
| b804bd19a7 | |||
| a9f4e2b1a5 | |||
| 6f3ac3deb6 | |||
| 28875078f1 | |||
| e8b4dbbaca | |||
| 0e385b057d | |||
| 2b383b84ce | |||
| a12da042cc | |||
| b77f30b3ff | |||
| 706a0fd1a3 | |||
| 613b88f2d3 | |||
| bea7804d47 | |||
| 9ba9733523 | |||
| 5aa9f11733 |
+2
-21
@@ -1,21 +1,2 @@
|
||||
# 服务配置
|
||||
HOST=0.0.0.0
|
||||
PORT=6600
|
||||
DEBUG=false
|
||||
|
||||
# Flask Session 密钥(部署时务必改为随机字符串,deploy.sh 首次会自动生成)
|
||||
SECRET_KEY=change-this-to-a-random-secret-key
|
||||
|
||||
# 初始管理员(首次建库自动写入;已建库后修改需设 ADMIN_SYNC_FROM_ENV=true 并重启)
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=change-me-on-first-login
|
||||
ADMIN_SYNC_FROM_ENV=false
|
||||
|
||||
# 企业微信 Webhook(也可在系统设置页面修改)
|
||||
WECHAT_WEBHOOK=
|
||||
|
||||
# 行情数据源: sina(默认,免费)| auto(有机构 token 时优先同花顺)| ths
|
||||
QUOTE_SOURCE=sina
|
||||
|
||||
# 同花顺 iFinD refresh_token(仅机构用户,普通用户留空即可)
|
||||
THS_REFRESH_TOKEN=
|
||||
# 环境变量模板已迁移至 config/.env.example
|
||||
# 使用: cp config/.env.example config/.env
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.env
|
||||
config/.env
|
||||
*.db
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
国内期货 · 交易复盘系统 — 软件使用许可与版权声明
|
||||
|
||||
著作权人:马建军
|
||||
Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
|
||||
【权利声明】
|
||||
本软件(含源代码、文档、界面、脚本及后续更新版本)之著作权及相关知识产权,
|
||||
均归马建军所有。除本许可明确允许的范围外,保留一切权利。
|
||||
|
||||
【授权范围 — 个人版】
|
||||
经著作权人书面或付费交付同意的自然人购买者,仅可在本人名下单一服务器或
|
||||
个人设备上部署并使用本软件,用于个人期货交易纪律管理、记录与复盘,且须
|
||||
遵守中华人民共和国相关法律法规及期货监管规定。
|
||||
|
||||
【严禁用途】
|
||||
未经著作权人事先书面许可,严禁将本软件用于包括但不限于以下用途:
|
||||
(1)带单、代客理财、代客下单、跟单室、信号群、付费喊单、向他人推荐具体
|
||||
期货买卖方向或具体合约;
|
||||
(2)向他人推荐、介绍、引导参与特定期货品种或交易机会(若构成投资咨询或
|
||||
其他需许可之业务,使用者依法另行承担法律责任);
|
||||
(3)融资、配资、分仓、分润、对赌、非法吸收资金等与期货相关的资金融通
|
||||
或变相配资业务;
|
||||
(4)复制、传播、转售、出租、出借源代码或编译产物,或授权第三方使用;
|
||||
(5)搭建共享交易室、多租户 SaaS、白标系统对外经营(须另行签订机构版协议);
|
||||
(6)删除、篡改或隐藏本版权及许可声明。
|
||||
|
||||
【免责声明】
|
||||
本软件为交易纪律与记录辅助工具,不构成任何投资建议、咨询或收益承诺。
|
||||
期货交易具有高风险,使用者须独立决策并自行承担全部盈亏及法律责任。
|
||||
因使用者违反法律法规、监管规定或本许可导致的后果,由使用者自行承担。
|
||||
|
||||
【更新与维护】
|
||||
源代码更新、部署服务及共享交易室等机构授权,以双方另行书面约定为准。
|
||||
未经约定,不视为自动授予新版本或扩展用途之权利。
|
||||
|
||||
【联系】
|
||||
著作权人:马建军
|
||||
手机:18364911125
|
||||
微信:dekun03
|
||||
|
||||
详细购买条款见 docs/软件购买与使用协议.md。
|
||||
本许可之解释与适用以中华人民共和国法律为准(法律强制性规定除外)。
|
||||
@@ -1,178 +1,94 @@
|
||||
# 国内期货交易监控复盘系统
|
||||
# 国内期货 · 交易复盘系统
|
||||
|
||||
基于 Flask 的国内期货监控与复盘 Web 应用,支持开单计划、关键位监控、止盈止损自动跟踪、企业微信推送与统计分析。
|
||||
基于 Flask 的国内期货 **CTP 下单 + 监控 + 复盘 + 统计** Web 应用。模拟盘连接 SimNow,实盘连接期货公司 CTP;支持关键位/计划提醒、交易记录同步、资金曲线、可开仓品种(仓位纪律)与企业微信推送。
|
||||
|
||||
## 功能模块
|
||||
## 文档
|
||||
|
||||
| 模块 | 说明 |
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| **开单计划** | 品种、方向、决策区间、止损/止盈;价格进入区间后激活并推送 |
|
||||
| **关键位监控** | 箱体/收敛突破、阻力/支撑位突破提醒(触发后去重) |
|
||||
| **交易记录与复盘** | 自动记录止盈/止损结果 |
|
||||
| **统计分析** | 总交易、胜率,按品种/类型/方向统计 |
|
||||
| **系统设置** | 修改密码、配置企业微信 Webhook |
|
||||
| **[功能说明](docs/FEATURES.md)** | 各模块功能、页面路径、数据库与后台任务 |
|
||||
| **[部署文档](docs/DEPLOY.md)** | 一键部署、更新、PM2、故障排查 |
|
||||
| **[备份与恢复](docs/BACKUP.md)** | 自动备份、下载、跨服务器恢复 |
|
||||
| **[SimNow 接入](docs/SIMNOW.md)** | 仿真账号注册与 CTP 前置 |
|
||||
| **[期货公司实盘 CTP](docs/CTP_LIVE.md)** | 实盘接入、与 SimNow 开平仓对比 |
|
||||
| **[交易与策略](docs/TRADING.md)** | 下单、持仓、可开仓品种、策略 API |
|
||||
| **[手续费与导航](docs/FEES.md)** | CTP 费率同步、导航开关 |
|
||||
| **[软件购买与使用协议](docs/软件购买与使用协议.md)** | 个人版授权模板(含签署栏) |
|
||||
|
||||
## 品种与合约代码(同花顺格式)
|
||||
## 功能一览
|
||||
|
||||
输入中文品种名(如「白银」「螺纹钢」)或同花顺合约代码(如 `ag2606`、`SR609`、`IF2606`),系统自动匹配**当前主力月份合约**。
|
||||
| 模块 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| **下单监控**(默认首页) | `/positions` | CTP 连接、期货下单、当前持仓、可开仓品种 |
|
||||
| **策略交易** | `/strategy` | 趋势回调 / 顺势加仓(可导航开关) |
|
||||
| **开单计划** | `/plans` | 当日决策区间、触发推送(可开关) |
|
||||
| **关键位监控** | `/keys` | 箱体/阻力支撑突破提醒 |
|
||||
| **行情 K 线** | `/market` | 多周期 K 线(可开关) |
|
||||
| **交易记录与复盘** | `/records` | 资金曲线、CTP 成交同步、复盘上传 |
|
||||
| **统计分析** | `/stats` | 汇总指标 + 多维度分项统计 |
|
||||
| **手续费配置** | `/fees` | CTP / 本地费率(可开关) |
|
||||
| **系统设置** | `/settings` | 交易模式、CTP、计仓、微信、主题 |
|
||||
|
||||
| 交易所 | 同花顺示例 | 说明 |
|
||||
|--------|-----------|------|
|
||||
| 上期所 / 大商所 / 上期能源 | `ag2606`、`rb2605`、`m2609` | 小写品种 + 4 位年月 |
|
||||
| 郑商所 | `SR609`、`MA606` | 大写品种 + 3 位年月 |
|
||||
| 中金所 | `IF2606`、`IH2606` | 大写品种 + 4 位年月 |
|
||||
登录后默认进入 **下单监控**;刷新当前页不会跳转,仅访问根路径 `/` 或新登录时进入默认页。
|
||||
|
||||
界面展示**同花顺合约代码**(ag2608、IF2606),与看盘软件一致;**行情默认走新浪财经**(免费,普通用户无需 token)。
|
||||
## 快速开始
|
||||
|
||||
## 行情说明
|
||||
> **发布铁律**:本地改代码 → 提交并 `git push` → 服务器 **仅** `git pull` / `git reset --hard origin/main` 更新。**禁止 SCP 复制代码到服务器。** 详见 [部署文档 · 代码发布铁律](docs/DEPLOY.md#代码发布铁律强制不容置疑)。
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 合约代码 | 同花顺格式,输入中文自动匹配主力月份 |
|
||||
| 价格数据 | 新浪财经 API(免费) |
|
||||
| 同花顺 iFinD | 仅机构/付费数据接口用户可用,**普通期货通用户无 refresh_token** |
|
||||
|
||||
因此个人用户使用本系统:**看同花顺代码,价格走新浪**,两者在主力合约价格上基本一致,满足监控需求。
|
||||
|
||||
## 快速部署(Ubuntu root + /opt/qihuo)
|
||||
**服务器(Ubuntu)**
|
||||
|
||||
```bash
|
||||
# root 登录后执行
|
||||
cd /opt/qihuo # 或先 git clone 再 bash deploy.sh
|
||||
bash deploy.sh
|
||||
cd /opt/qihuo && bash deploy.sh
|
||||
# 访问 http://<IP>:6600
|
||||
```
|
||||
|
||||
默认安装路径:`/opt/qihuo`,服务端口:`6600`。
|
||||
|
||||
部署完成后访问:`http://服务器IP:6600`
|
||||
|
||||
## 环境要求
|
||||
|
||||
- Ubuntu 20.04+(推荐)
|
||||
- **root 用户**运行(部署目录 `/opt/qihuo`)
|
||||
- Python 3.10+
|
||||
- Node.js + PM2(进程守护)
|
||||
- 网络可访问 `hq.sinajs.cn`(行情)及企业微信 API
|
||||
|
||||
## 手动部署
|
||||
|
||||
### 1. 安装系统依赖
|
||||
**更新**(须先在本机 `git push`)
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y python3 python3-venv python3-pip git nodejs npm
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
### 2. 克隆到 /opt/qihuo
|
||||
|
||||
```bash
|
||||
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
|
||||
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
|
||||
```
|
||||
|
||||
### 3. 虚拟环境与依赖
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
**禁止** 使用 `scp` / 手工复制更新服务器代码。
|
||||
|
||||
### 4. 配置环境变量
|
||||
生产环境须同时维护 **`qihuo`**(Web)与 **`qihuo-ctp`**(CTP Worker)两个 PM2 进程。
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# 编辑 .env,至少修改 SECRET_KEY 和 ADMIN_PASSWORD
|
||||
nano .env
|
||||
```
|
||||
详见 [部署文档](docs/DEPLOY.md)。
|
||||
|
||||
`.env` 主要字段:
|
||||
|
||||
```env
|
||||
HOST=0.0.0.0
|
||||
PORT=6600
|
||||
SECRET_KEY=随机长字符串
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=你的密码
|
||||
ADMIN_SYNC_FROM_ENV=false
|
||||
WECHAT_WEBHOOK=企业微信机器人地址(可选)
|
||||
QUOTE_SOURCE=sina
|
||||
```
|
||||
|
||||
**改密码说明**:账号存在 `futures.db` 里,改 `.env` 后不会自动生效。
|
||||
|
||||
- **首次部署**:写好 `ADMIN_USERNAME` / `ADMIN_PASSWORD` 后启动即可。
|
||||
- **已部署后**:在 `.env` 设 `ADMIN_SYNC_FROM_ENV=true`,改密码后 `pm2 restart qihuo`;或在网页「系统设置」改密。
|
||||
- **忘记密码**:`source venv/bin/activate && python reset_admin.py`
|
||||
|
||||
普通用户保持 `QUOTE_SOURCE=sina` 即可。
|
||||
|
||||
### 5. PM2 启动
|
||||
|
||||
```bash
|
||||
pm2 start ecosystem.config.cjs
|
||||
pm2 save
|
||||
pm2 startup # 按提示执行生成的命令,实现开机自启
|
||||
```
|
||||
|
||||
### 6. 常用 PM2 命令
|
||||
|
||||
```bash
|
||||
pm2 status
|
||||
pm2 logs qihuo
|
||||
pm2 restart qihuo
|
||||
pm2 stop qihuo
|
||||
```
|
||||
|
||||
## 本地开发
|
||||
**本地开发**
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
python app.py
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
## 品种与行情
|
||||
|
||||
```
|
||||
qihuo/
|
||||
├── app.py # 主程序
|
||||
├── market.py # 同花顺/新浪行情拉取
|
||||
├── symbols.py # 期货品种与同花顺代码映射
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
├── deploy.sh # Ubuntu 一键部署
|
||||
├── ecosystem.config.cjs # PM2 配置
|
||||
├── static/js/symbol.js # 品种联想
|
||||
├── templates/ # 页面模板
|
||||
└── futures.db # SQLite 数据库(运行后生成)
|
||||
```
|
||||
- 合约代码:**同花顺格式**(`ag2606`、`SR609`、`IF2606`),中文联想匹配主力
|
||||
- 行情:默认 **新浪财经**;机构用户可配置同花顺 iFinD token
|
||||
|
||||
## 监控逻辑说明
|
||||
## 环境要求
|
||||
|
||||
### 开单计划
|
||||
- Python 3.10+(vnpy_ctp)
|
||||
- PM2(生产部署:**qihuo** + **qihuo-ctp** 两个进程)
|
||||
- 网络:新浪行情、Git 仓库、SimNow/CTP 前置(见部署文档)
|
||||
|
||||
1. **待触发**:当前价进入「决策区间 [下限, 上限]」→ 企业微信通知,状态变为「已激活」
|
||||
2. **已激活**:监控止盈止损直至触发,写入交易记录,计划关闭
|
||||
|
||||
### 关键位监控
|
||||
|
||||
- 箱体/收敛:突破上沿或跌破下沿各推送一次
|
||||
- 阻力/支撑:单向突破推送一次
|
||||
|
||||
后台线程每 3 秒轮询行情。
|
||||
|
||||
## 安全建议
|
||||
|
||||
- 部署后立即修改默认密码
|
||||
- 勿将 `.env` 提交到仓库
|
||||
- 生产环境建议用 Nginx 反代并配置 HTTPS
|
||||
- 限制 6600 端口仅内网或 VPN 访问
|
||||
|
||||
## 仓库地址
|
||||
## 仓库
|
||||
|
||||
https://git.bz121.com/dekun/qihuo.git
|
||||
|
||||
## License
|
||||
## 版权与授权
|
||||
|
||||
Private / 个人使用
|
||||
- 著作权人:**马建军**
|
||||
- 许可说明:[LICENSE.zh-CN.txt](LICENSE.zh-CN.txt)
|
||||
- 个人购买协议模板:[docs/软件购买与使用协议.md](docs/软件购买与使用协议.md)
|
||||
|
||||
本软件为 **专有软件**,仅供经授权的个人自用部署。严禁用于带单、向他人推荐期货品种或买卖建议、融资配资、转售源码或搭建共享交易室等用途。本软件不构成投资建议,期货交易风险由使用者自行承担。
|
||||
|
||||
联系:手机 18364911125 · 微信 dekun03
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.settings.admin_settings
|
||||
from modules.settings.admin_settings import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.notify.ai_client
|
||||
from modules.notify.ai_client import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.notify.ai_messages
|
||||
from modules.notify.ai_messages import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.notify.ai_worker
|
||||
from modules.notify.ai_worker import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.contract_profile
|
||||
from modules.core.contract_profile import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.contract_specs
|
||||
from modules.core.contract_specs import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_entry_price
|
||||
from modules.ctp.ctp_entry_price import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_fee_sync
|
||||
from modules.ctp.ctp_fee_sync import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_fee_worker
|
||||
from modules.ctp.ctp_fee_worker import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_ipc_client
|
||||
from modules.ctp.ctp_ipc_client import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_kline
|
||||
from modules.ctp.ctp_kline import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_premarket_connect
|
||||
from modules.ctp.ctp_premarket_connect import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_reconnect
|
||||
from modules.ctp.ctp_reconnect import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_settings
|
||||
from modules.ctp.ctp_settings import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_symbol
|
||||
from modules.ctp.ctp_symbol import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_trade_sync
|
||||
from modules.ctp.ctp_trade_sync import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_trading_state
|
||||
from modules.ctp.ctp_trading_state import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.ctp_worker
|
||||
from modules.ctp.ctp_worker import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.stats.dashboard_lib
|
||||
from modules.stats.dashboard_lib import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.backup.db_backup
|
||||
from modules.backup.db_backup import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.db_conn
|
||||
from modules.core.db_conn import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.doc_render
|
||||
from modules.core.doc_render import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.env_file
|
||||
from modules.core.env_file import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.fees.fee_specs
|
||||
from modules.fees.fee_specs import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.fees.fee_sync
|
||||
from modules.fees.fee_sync import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.keys.key_monitor_lib
|
||||
from modules.keys.key_monitor_lib import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.market.kline_chart
|
||||
from modules.market.kline_chart import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.market.kline_store
|
||||
from modules.market.kline_store import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.market.kline_stream
|
||||
from modules.market.kline_stream import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.locale_fix
|
||||
from modules.core.locale_fix import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.market.market
|
||||
from modules.market.market import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.market.market_sessions
|
||||
from modules.market.market_sessions import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.settings.nav_settings
|
||||
from modules.settings.nav_settings import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.order_pending
|
||||
from modules.trading.order_pending import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.pending_order_worker
|
||||
from modules.trading.pending_order_worker import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.position_sizing
|
||||
from modules.trading.position_sizing import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.position_stream
|
||||
from modules.trading.position_stream import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.product_recommend
|
||||
from modules.trading.product_recommend import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.recommend_store
|
||||
from modules.trading.recommend_store import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.recommend_stream
|
||||
from modules.trading.recommend_stream import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.recommend_trend
|
||||
from modules.trading.recommend_trend import * # noqa: F401,F403
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
# 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
|
||||
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.risk.account_risk_lib
|
||||
from modules.risk.account_risk_lib import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.sl_tp_guard
|
||||
from modules.trading.sl_tp_guard import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.stats.stats_engine
|
||||
from modules.stats.stats_engine import * # noqa: F401,F403
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
# 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
|
||||
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.strategy.fib_lib
|
||||
from modules.strategy.fib_lib import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.strategy.strategy_db
|
||||
from modules.strategy.strategy_db import * # noqa: F401,F403
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.strategy.strategy_trend_lib
|
||||
from modules.strategy.strategy_trend_lib import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.symbols
|
||||
from modules.core.symbols import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.trade_log_lib
|
||||
from modules.trading.trade_log_lib import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.trading.trade_notify
|
||||
from modules.trading.trade_notify import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.core.trading_context
|
||||
from modules.core.trading_context import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.ctp.vnpy_bridge
|
||||
from modules.ctp.vnpy_bridge import * # noqa: F401,F403
|
||||
@@ -0,0 +1,2 @@
|
||||
# Compatibility shim — use modules.notify.wechat_notify
|
||||
from modules.notify.wechat_notify import * # noqa: F401,F403
|
||||
@@ -0,0 +1,57 @@
|
||||
# 服务配置
|
||||
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=SimNow,live=期货公司(系统设置页可改)
|
||||
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
|
||||
|
||||
# —— 数据库(SQLite futures.db,路径见 modules/core/paths.py)——
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"ag": {"exchange": "SHFE", "mult": 15, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
|
||||
"au": {"exchange": "SHFE", "mult": 1000, "open_fixed": 4.0, "open_ratio": 0, "close_yesterday_fixed": 4.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"cu": {"exchange": "SHFE", "mult": 5, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.2, "close_today_ratio": 0},
|
||||
"al": {"exchange": "SHFE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
|
||||
"zn": {"exchange": "SHFE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"pb": {"exchange": "SHFE", "mult": 5, "open_fixed": 0.08, "open_ratio": 0, "close_yesterday_fixed": 0.08, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"ni": {"exchange": "SHFE", "mult": 1, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
|
||||
"sn": {"exchange": "SHFE", "mult": 1, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 6.0, "close_today_ratio": 0},
|
||||
"rb": {"exchange": "SHFE", "mult": 10, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
|
||||
"hc": {"exchange": "SHFE", "mult": 10, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
|
||||
"ru": {"exchange": "SHFE", "mult": 10, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 12.0, "close_today_ratio": 0},
|
||||
"fu": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"bu": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.2, "close_today_ratio": 0},
|
||||
"sp": {"exchange": "SHFE", "mult": 10, "open_fixed": 0.1, "open_ratio": 0, "close_yesterday_fixed": 0.1, "close_yesterday_ratio": 0, "close_today_fixed": 0.1, "close_today_ratio": 0},
|
||||
"sc": {"exchange": "INE", "mult": 1000, "open_fixed": 40.0, "open_ratio": 0, "close_yesterday_fixed": 40.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"i": {"exchange": "DCE", "mult": 100, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
|
||||
"j": {"exchange": "DCE", "mult": 100, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
|
||||
"jm": {"exchange": "DCE", "mult": 60, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
|
||||
"m": {"exchange": "DCE", "mult": 10, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
|
||||
"y": {"exchange": "DCE", "mult": 10, "open_fixed": 5.0, "open_ratio": 0, "close_yesterday_fixed": 5.0, "close_yesterday_ratio": 0, "close_today_fixed": 5.0, "close_today_ratio": 0},
|
||||
"p": {"exchange": "DCE", "mult": 10, "open_fixed": 5.0, "open_ratio": 0, "close_yesterday_fixed": 5.0, "close_yesterday_ratio": 0, "close_today_fixed": 5.0, "close_today_ratio": 0},
|
||||
"c": {"exchange": "DCE", "mult": 10, "open_fixed": 2.4, "open_ratio": 0, "close_yesterday_fixed": 2.4, "close_yesterday_ratio": 0, "close_today_fixed": 2.4, "close_today_ratio": 0},
|
||||
"l": {"exchange": "DCE", "mult": 5, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 2.0, "close_today_ratio": 0},
|
||||
"pp": {"exchange": "DCE", "mult": 5, "open_fixed": 2.0, "open_ratio": 0, "close_yesterday_fixed": 2.0, "close_yesterday_ratio": 0, "close_today_fixed": 2.0, "close_today_ratio": 0},
|
||||
"if": {"exchange": "CFFEX", "mult": 300, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
|
||||
"ih": {"exchange": "CFFEX", "mult": 300, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
|
||||
"ic": {"exchange": "CFFEX", "mult": 200, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
|
||||
"im": {"exchange": "CFFEX", "mult": 200, "open_fixed": 0, "open_ratio": 0.000092, "close_yesterday_fixed": 0, "close_yesterday_ratio": 0.000092, "close_today_fixed": 0, "close_today_ratio": 0.000276},
|
||||
"ma": {"exchange": "CZCE", "mult": 10, "open_fixed": 4.0, "open_ratio": 0, "close_yesterday_fixed": 4.0, "close_yesterday_ratio": 0, "close_today_fixed": 4.0, "close_today_ratio": 0},
|
||||
"ta": {"exchange": "CZCE", "mult": 5, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"sr": {"exchange": "CZCE", "mult": 10, "open_fixed": 6.0, "open_ratio": 0, "close_yesterday_fixed": 6.0, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"cf": {"exchange": "CZCE", "mult": 5, "open_fixed": 8.6, "open_ratio": 0, "close_yesterday_fixed": 8.6, "close_yesterday_ratio": 0, "close_today_fixed": 0, "close_today_ratio": 0},
|
||||
"fg": {"exchange": "CZCE", "mult": 20, "open_fixed": 12.0, "open_ratio": 0, "close_yesterday_fixed": 12.0, "close_yesterday_ratio": 0, "close_today_fixed": 12.0, "close_today_ratio": 0},
|
||||
"sa": {"exchange": "CZCE", "mult": 20, "open_fixed": 7.2, "open_ratio": 0, "close_yesterday_fixed": 7.2, "close_yesterday_ratio": 0, "close_today_fixed": 7.2, "close_today_ratio": 0}
|
||||
}
|
||||
@@ -1,6 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# 国内期货监控系统 - Ubuntu 一键部署
|
||||
# 国内期货 · 交易复盘系统 - Ubuntu 一键部署 / 更新
|
||||
# root 用户 | 目录 /opt/qihuo | 端口 6600 | PM2
|
||||
#
|
||||
# 已内置修复(避免重复踩坑):
|
||||
# - vnpy_ctp 编译:build-essential python3-dev pkg-config
|
||||
# - CTP 登录崩溃:zh_CN.GB18030 + zh_CN.UTF-8 locale
|
||||
# - 时区:Asia/Shanghai(与 SimNow 交易时段一致)
|
||||
# - SimNow 前置:自动探测可用线路并写入 .env
|
||||
# - PM2 环境变量:LANG/LC_ALL(见 ecosystem.config.cjs)
|
||||
# - .env 缺项补全:SIMNOW_ENV、CTP_AUTO_RECONNECT
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -8,12 +16,81 @@ APP_DIR="/opt/qihuo"
|
||||
REPO_URL="https://git.bz121.com/dekun/qihuo.git"
|
||||
SERVICE_NAME="qihuo"
|
||||
|
||||
# SimNow 前置候选(按优先级;部署时自动 nc 探测)
|
||||
SIMNOW_FRONTS=(
|
||||
"180.168.146.187:10201:10211"
|
||||
"180.168.146.187:10202:10212"
|
||||
"180.168.146.187:10130:10131"
|
||||
"218.202.237.33:10203:10213"
|
||||
)
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "请使用 root 用户运行: sudo bash deploy.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
probe_tcp() {
|
||||
local host="$1" port="$2"
|
||||
if command -v nc &>/dev/null; then
|
||||
nc -z -w 3 "$host" "$port" &>/dev/null
|
||||
return $?
|
||||
fi
|
||||
timeout 3 bash -c "echo >/dev/tcp/${host}/${port}" 2>/dev/null
|
||||
}
|
||||
|
||||
ensure_locale_gen() {
|
||||
local pat="$1"
|
||||
if [ -f /etc/locale.gen ] && grep -q "^# ${pat}" /etc/locale.gen; then
|
||||
sed -i "s/^# ${pat}/${pat}/" /etc/locale.gen
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_env_key() {
|
||||
local file="$1" key="$2" val="$3"
|
||||
if [ ! -f "$file" ]; then
|
||||
return
|
||||
fi
|
||||
if grep -q "^${key}=" "$file"; then
|
||||
return
|
||||
fi
|
||||
echo "${key}=${val}" >>"$file"
|
||||
echo " .env 补全: ${key}=${val}"
|
||||
}
|
||||
|
||||
pick_simnow_front() {
|
||||
local host td_port md_port
|
||||
for entry in "${SIMNOW_FRONTS[@]}"; do
|
||||
IFS=: read -r host td_port md_port <<<"$entry"
|
||||
if probe_tcp "$host" "$td_port" && probe_tcp "$host" "$md_port"; then
|
||||
echo "tcp://${host}:${td_port}|tcp://${host}:${md_port}"
|
||||
return 0
|
||||
fi
|
||||
echo " SimNow 前置不可达: ${host} ${td_port}/${md_port}" >&2
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
update_simnow_front_in_env() {
|
||||
local env_file="$1"
|
||||
local picked td md
|
||||
picked="$(pick_simnow_front)" || {
|
||||
echo "警告: 未能探测到可用 SimNow 前置,请手动编辑 ${env_file}(见 docs/SIMNOW.md)"
|
||||
return 1
|
||||
}
|
||||
td="${picked%%|*}"
|
||||
md="${picked##*|}"
|
||||
echo "==> SimNow 可用前置: TD=${td} MD=${md}"
|
||||
if grep -q "^SIMNOW_TD_ADDRESS=" "$env_file"; then
|
||||
sed -i "s|^SIMNOW_TD_ADDRESS=.*|SIMNOW_TD_ADDRESS=${td}|" "$env_file"
|
||||
sed -i "s|^SIMNOW_MD_ADDRESS=.*|SIMNOW_MD_ADDRESS=${md}|" "$env_file"
|
||||
else
|
||||
echo "SIMNOW_TD_ADDRESS=${td}" >>"$env_file"
|
||||
echo "SIMNOW_MD_ADDRESS=${md}" >>"$env_file"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> 检查系统依赖..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
|
||||
need_install() {
|
||||
@@ -26,6 +103,29 @@ need_install python3 python3
|
||||
need_install python3-venv python3-venv
|
||||
need_install git git
|
||||
|
||||
echo "==> 安装 vnpy_ctp 编译依赖..."
|
||||
apt-get install -y build-essential python3-dev pkg-config locales netcat-openbsd
|
||||
|
||||
echo "==> 配置时区 Asia/Shanghai..."
|
||||
if command -v timedatectl &>/dev/null; then
|
||||
timedatectl set-timezone Asia/Shanghai || true
|
||||
fi
|
||||
|
||||
echo "==> 配置 CTP 所需 locale(zh_CN.GB18030 等)..."
|
||||
ensure_locale_gen "zh_CN.GB18030 GB18030"
|
||||
ensure_locale_gen "zh_CN.UTF-8 UTF-8"
|
||||
ensure_locale_gen "en_US.UTF-8 UTF-8"
|
||||
locale-gen zh_CN.GB18030 zh_CN.UTF-8 en_US.UTF-8 2>/dev/null || locale-gen
|
||||
update-locale LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8 2>/dev/null || true
|
||||
export LANG=zh_CN.UTF-8
|
||||
export LC_ALL=zh_CN.UTF-8
|
||||
|
||||
if ! locale -a 2>/dev/null | grep -qi gb18030; then
|
||||
echo "错误: zh_CN.GB18030 未生成,CTP 连接后会崩溃"
|
||||
exit 1
|
||||
fi
|
||||
echo " locale OK: $(locale -a 2>/dev/null | grep -i gb18030 | head -1)"
|
||||
|
||||
if ! command -v pm2 &>/dev/null; then
|
||||
echo "==> 安装 PM2..."
|
||||
if ! command -v npm &>/dev/null; then
|
||||
@@ -51,34 +151,67 @@ else
|
||||
cd "$APP_DIR"
|
||||
fi
|
||||
|
||||
echo "==> 创建 Python 虚拟环境..."
|
||||
echo "==> Python 虚拟环境与依赖..."
|
||||
if [ ! -d "$APP_DIR/venv" ]; then
|
||||
python3 -m venv "$APP_DIR/venv"
|
||||
fi
|
||||
# shellcheck disable=SC1091
|
||||
source "$APP_DIR/venv/bin/activate"
|
||||
pip install --upgrade pip -q
|
||||
pip install -r "$APP_DIR/requirements.txt" -q
|
||||
pip install -r "$APP_DIR/requirements.txt"
|
||||
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
|
||||
|
||||
if [ ! -f "$APP_DIR/.env" ]; then
|
||||
echo "==> 生成 .env(请编辑 ADMIN_PASSWORD 后重启)..."
|
||||
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"
|
||||
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 "$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=.\+" "$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=$?
|
||||
set -e
|
||||
if [ "$CTP_TEST" -ne 0 ]; then
|
||||
echo "警告: SimNow 连接测试未通过,请检查 .env 账号与网络(见 docs/SIMNOW.md)"
|
||||
else
|
||||
echo " SimNow CTP 连接测试通过"
|
||||
fi
|
||||
else
|
||||
echo " 跳过 CTP 测试(未配置 SIMNOW_USER / SIMNOW_PASSWORD)"
|
||||
fi
|
||||
|
||||
echo "==> PM2 启动/重启服务..."
|
||||
cd "$APP_DIR"
|
||||
pm2 delete "$SERVICE_NAME" 2>/dev/null || true
|
||||
if pm2 describe "$SERVICE_NAME" &>/dev/null; then
|
||||
pm2 restart ecosystem.config.cjs --update-env
|
||||
else
|
||||
pm2 start ecosystem.config.cjs
|
||||
fi
|
||||
pm2 save
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 部署完成"
|
||||
echo " 目录: ${APP_DIR}"
|
||||
echo " 用户: root"
|
||||
echo " 时区: $(date +%Z) $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo " 端口: 6600"
|
||||
echo " 访问: http://<服务器IP>:6600"
|
||||
echo " 访问: http://$(hostname -I 2>/dev/null | awk '{print $1}'):6600"
|
||||
echo " 日志: pm2 logs ${SERVICE_NAME}"
|
||||
echo " SimNow 注册: docs/SIMNOW.md"
|
||||
echo " 开机自启: pm2 startup && pm2 save"
|
||||
echo "=========================================="
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
# AI 分析
|
||||
|
||||
**页面路径**:`/ai`(须在系统设置 → 导航显示 中开启「AI 分析」)
|
||||
|
||||
**相关文件**:`ai_client.py`、`ai_worker.py`、`ai_messages.py`、`templates/ai_messages.html`
|
||||
|
||||
---
|
||||
|
||||
## 功能概述
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 开仓分析 | 手动/关键位开仓成交后后台分析 |
|
||||
| 平仓分析 | 写入 `trade_logs` 平仓记录后分析 |
|
||||
| 日终报告 | 每日定时汇总当日交易与持仓 |
|
||||
| 消息存档 | 全部写入 `ai_messages` 表,页面可浏览 |
|
||||
| 微信推送 | 可选:分析成功后推送到企业微信 |
|
||||
|
||||
---
|
||||
|
||||
## 配置项(系统设置 → AI 分析)
|
||||
|
||||
| 设置键 | 说明 | 默认 |
|
||||
|--------|------|------|
|
||||
| `ai_enabled` | 总开关 | 关 |
|
||||
| `ai_provider` | `ollama` / `openai` | ollama |
|
||||
| `ai_ollama_base_url` | Ollama 地址 | `http://127.0.0.1:11434` |
|
||||
| `ai_ollama_model` | Ollama 模型 | `qwen2.5:7b` |
|
||||
| `ai_openai_base_url` | OpenAI 兼容 API | `https://api.openai.com/v1` |
|
||||
| `ai_openai_api_key` | API Key | 空 |
|
||||
| `ai_openai_model` | 模型名 | `gpt-4o-mini` |
|
||||
| `ai_daily_report_enabled` | 日终报告开关 | 开 |
|
||||
| `ai_daily_report_hour` | 报告时刻(时) | 15 |
|
||||
| `ai_daily_report_minute` | 报告时刻(分) | 5 |
|
||||
|
||||
---
|
||||
|
||||
## 触发时机
|
||||
|
||||
### 1. 开仓分析(`kind=open`)
|
||||
|
||||
- **触发**:下单监控手动开仓 **成交** 且填写止损 → `notify_manual_open_filled()` 调度。
|
||||
- **标题**:`{品种名} 开仓`
|
||||
- **Payload**:symbol、direction、entry、stop_loss、take_profit、lots、capital
|
||||
|
||||
### 2. 关键位开仓(`kind=key_open`)
|
||||
|
||||
- **触发**:箱体/收敛突破自动单成交 → `notify_key_breakout_open()` 调度。
|
||||
- **标题**:`{品种名} 关键位开仓`
|
||||
- **Payload**:monitor_type、trade_mode、break_side、entry、stop_loss、take_profit、lots
|
||||
|
||||
### 3. 平仓分析(`kind=close`)
|
||||
|
||||
- **触发**:`trade_logs` 新增平仓 → `notify_trade_log_close()` 调度。
|
||||
- **标题**:`{品种名} 平仓`
|
||||
- **Payload**:source、result、pnl_net、entry、close_price、lots
|
||||
|
||||
### 4. 日终报告(`kind=daily_report`)
|
||||
|
||||
- **触发**:后台 `ai_worker` 每分钟检查;到达设定时刻且当日未生成过。
|
||||
- **标题**:`{YYYY-MM-DD} 日终持仓与交易报告`
|
||||
- **Payload**:当日成交汇总、胜负次数、净盈亏、active 持仓列表
|
||||
|
||||
> 开仓/平仓的 AI 分析 **默认不推微信**(仅写入页面);日终报告与可在设置中开启的推送见下文。
|
||||
|
||||
---
|
||||
|
||||
## 分析逻辑
|
||||
|
||||
**System Prompt**(固定):
|
||||
|
||||
```
|
||||
你是国内期货交易复盘助手。根据提供的结构化交易数据,
|
||||
用简洁中文给出 3~6 条要点:风险、纪律、改进建议。
|
||||
不要编造未提供的数据;金额单位为元。
|
||||
```
|
||||
|
||||
**User Prompt**:
|
||||
|
||||
```
|
||||
事件类型:{event_kind}
|
||||
|
||||
数据:
|
||||
{JSON payload}
|
||||
```
|
||||
|
||||
**接口**:
|
||||
|
||||
- Ollama:`POST {base}/api/chat`
|
||||
- OpenAI 兼容:`POST {base}/chat/completions`,temperature=0.4
|
||||
|
||||
失败时内容前加 `⚠`,仍写入 `ai_messages`。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
| 类型 | 条件 | 模板 |
|
||||
|------|------|------|
|
||||
| 事件分析 | AI 调用成功 **且** 调度时传入 `send_wechat_fn` | 见 [WECHAT.md §12](./WECHAT.md#12-ai-分析) |
|
||||
| 日终报告 | `ai_daily_report_enabled=1` 且 AI 成功 | `🤖 {date} 日终持仓与交易报告\n\n{content[:1800]}` |
|
||||
|
||||
**说明**:
|
||||
|
||||
- 当前实现中,开仓/平仓触发的 `schedule_ai_event_analysis()` **未绑定** `send_wechat_fn`,故 **仅存档到 /ai 页面**。
|
||||
- 日终报告在 `maybe_run_daily_ai_report()` 中会推微信(成功时)。
|
||||
- 正文截断为 **1800 字符**,避免超长。
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
AI **不参与下单**,仅事后分析。无 `assert_can_open` 或 CTP 交互。
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
AI 模块无独立风控。其输入数据来自已发生的交易事件,不改变账户冻结、保证金等状态。
|
||||
|
||||
---
|
||||
|
||||
## 后台任务
|
||||
|
||||
- 线程名:`ai-worker`
|
||||
- 启动延迟 30 秒后,每 **60 秒** 检查是否该跑日终报告
|
||||
- 与 `background_task`(计划/关键位)独立运行
|
||||
|
||||
---
|
||||
|
||||
## 数据存储
|
||||
|
||||
表 `ai_messages`:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| kind | open / key_open / close / daily_report |
|
||||
| title | 标题 |
|
||||
| content | AI 回复正文 |
|
||||
| meta | JSON payload |
|
||||
| created_at | 创建时间 |
|
||||
|
||||
页面 `/ai` 按时间倒序展示;上方含使用说明卡片。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [WECHAT.md §12](./WECHAT.md#12-ai-分析) — 推送模板
|
||||
- [ORDER_MONITOR.md](./ORDER_MONITOR.md) — 手动开仓触发 AI
|
||||
- [KEY_MONITORS.md](./KEY_MONITORS.md) — 关键位开仓触发 AI
|
||||
- [SETTINGS.md](./SETTINGS.md) — 配置入口
|
||||
@@ -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 兼容 shim(PM2 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。
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
# 数据备份与恢复
|
||||
|
||||
qihuo 支持自动备份数据库与复盘附件,生成可在其他 Linux 服务器恢复的压缩包。
|
||||
|
||||
存储后端由 `.env` 决定:
|
||||
|
||||
| 后端 | 备份包内主文件 | 说明 |
|
||||
|------|----------------|------|
|
||||
| SQLite(默认) | `futures.db` | 本地单文件库 |
|
||||
| PostgreSQL | `postgres_dump.sql` | `pg_dump` 逻辑备份 |
|
||||
|
||||
PostgreSQL 部署与迁移见 **[POSTGRES.md](./POSTGRES.md)**。
|
||||
|
||||
---
|
||||
|
||||
## 备份内容
|
||||
|
||||
| 内容 | 说明 |
|
||||
|------|------|
|
||||
| `futures.db` | SQLite 主库(仅 SQLite 模式) |
|
||||
| `postgres_dump.sql` | PostgreSQL 逻辑备份(仅 PostgreSQL 模式) |
|
||||
| `uploads/` | 复盘截图、自动 K 线图(若存在) |
|
||||
| `manifest.json` | 备份时间、**backend** 字段、文件清单 |
|
||||
| `RESTORE.md` | 包内恢复说明 |
|
||||
| `restore.sh` | 一键恢复脚本 |
|
||||
|
||||
**不包含** `.env`(含 CTP 密码、`DATABASE_URL` 等),请单独安全保管或在新服务器重新配置。
|
||||
|
||||
---
|
||||
|
||||
## 备份目录
|
||||
|
||||
默认:**`/root/qihuo_backup`**
|
||||
|
||||
可通过环境变量覆盖:
|
||||
|
||||
```bash
|
||||
# /opt/qihuo/.env 或 systemd/PM2 环境
|
||||
QIHUO_BACKUP_DIR=/data/qihuo_backup
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 系统设置页
|
||||
|
||||
路径:**系统设置 → 数据备份与恢复**
|
||||
|
||||
- **立即备份**:后台生成 `qihuo_backup_YYYYMMDD_HHMMSS.tar.gz`
|
||||
- **每日自动备份**:默认每天 **03:00**(Asia/Shanghai)执行
|
||||
- **保留份数**:默认保留最近 **30** 份,超出自动删除最旧文件
|
||||
- **下载**:列表中点击「下载」获取压缩包
|
||||
|
||||
PostgreSQL 模式下需服务器已安装 `pg_dump`(`apt install postgresql-client` 或完整 `postgresql` 包)。
|
||||
|
||||
---
|
||||
|
||||
## 在新服务器恢复
|
||||
|
||||
### 方式一:使用包内脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 1. 上传压缩包到目标机
|
||||
scp qihuo_backup_20260626_030015.tar.gz root@新服务器:/root/
|
||||
|
||||
# 2. 解压并恢复
|
||||
cd /root
|
||||
tar -xzf qihuo_backup_20260626_030015.tar.gz
|
||||
cd qihuo_backup_20260626_030015
|
||||
chmod +x restore.sh
|
||||
|
||||
# SQLite:直接恢复 futures.db
|
||||
RESTORE_DIR=/opt/qihuo ./restore.sh
|
||||
|
||||
# PostgreSQL:先配置 /opt/qihuo/.env 的 DATABASE_URL,再执行
|
||||
export RESTORE_DIR=/opt/qihuo
|
||||
# 若 .env 在 RESTORE_DIR 下且含 DATABASE_URL,restore.sh 会自动 source
|
||||
./restore.sh
|
||||
```
|
||||
|
||||
默认恢复到 **`/root/qihuo`**。若生产目录为 `/opt/qihuo`:
|
||||
|
||||
```bash
|
||||
RESTORE_DIR=/opt/qihuo ./restore.sh
|
||||
```
|
||||
|
||||
### 方式二:手工复制(SQLite)
|
||||
|
||||
```bash
|
||||
tar -xzf qihuo_backup_20260626_030015.tar.gz
|
||||
cd qihuo_backup_20260626_030015
|
||||
pm2 stop qihuo
|
||||
cp futures.db /opt/qihuo/futures.db
|
||||
cp -a uploads/. /opt/qihuo/uploads/
|
||||
pm2 restart qihuo
|
||||
```
|
||||
|
||||
### 方式三:手工导入(PostgreSQL)
|
||||
|
||||
```bash
|
||||
pm2 stop qihuo
|
||||
export DATABASE_URL=postgresql://qihuo:密码@127.0.0.1:5432/qihuo
|
||||
psql "$DATABASE_URL" -f postgres_dump.sql
|
||||
cp -a uploads/. /opt/qihuo/uploads/
|
||||
pm2 restart qihuo
|
||||
```
|
||||
|
||||
### 恢复后检查清单
|
||||
|
||||
1. 已部署 qihuo 代码与 Python 虚拟环境(见 [DEPLOY.md](./DEPLOY.md))
|
||||
2. 已配置 `.env`(`DATABASE_URL` 或 SQLite、`SECRET_KEY`、CTP 账号等)
|
||||
3. PostgreSQL:库已创建且 `DATABASE_URL` 可连接
|
||||
4. 访问 Web 登录,检查交易记录、统计页是否正常
|
||||
5. CTP 模式需在新环境重新连接柜台
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- **恢复前务必停止 qihuo**,避免进程占用数据库导致覆盖不完整
|
||||
- SQLite 备份使用 SQLite `backup` API,并在 WAL 模式下尝试 checkpoint
|
||||
- PostgreSQL 备份使用 `pg_dump`,恢复使用 `psql -f`
|
||||
- 自动备份在应用后台线程执行,与 Web 服务同进程
|
||||
- 大体积 `uploads/` 会使压缩包变大,可按需定期清理无用截图
|
||||
- 不要将含 `.env`、数据库的压缩包上传到公开网盘
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 现象 | 处理 |
|
||||
|------|------|
|
||||
| 设置页无备份列表 | 检查 `/root/qihuo_backup` 目录权限,进程需可写 |
|
||||
| 立即备份无反应 | 查看 PM2 日志;可能上一任务仍在进行 |
|
||||
| PostgreSQL 备份失败 | 安装 `postgresql-client`;检查 `DATABASE_URL` |
|
||||
| 下载 404 | 文件名须为系统生成的 `qihuo_backup_*.tar.gz` |
|
||||
| 恢复后无法登录 | 确认数据已导入实际使用的库(SQLite 文件或 PG) |
|
||||
| 恢复后 CTP 连不上 | 在新服务器配置正确的 `.env` CTP 参数 |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [POSTGRES.md](./POSTGRES.md) — PostgreSQL 一键部署、迁移、备份恢复
|
||||
- [DEPLOY.md](./DEPLOY.md) — 部署与目录结构
|
||||
- [FEATURES.md](./FEATURES.md) — 功能与路由一览
|
||||
@@ -0,0 +1,238 @@
|
||||
# 期货公司实盘 CTP
|
||||
|
||||
本文说明如何接入 **期货公司实盘 CTP**,并对比 **SimNow 模拟盘** 与 **实盘** 的开平仓逻辑是否一致。
|
||||
|
||||
相关代码:`vnpy_bridge.py`、`ctp_worker.py`、`ctp_ipc_client.py`、`ctp_settings.py`、`trading_context.py`、`install_trading.py`。
|
||||
|
||||
---
|
||||
|
||||
## 进程架构(Web / CTP Worker)
|
||||
|
||||
自 2026-03 起,CTP 与 vn.py **不在 Flask 进程内**运行:
|
||||
|
||||
| 组件 | PM2 名 | 说明 |
|
||||
|------|--------|------|
|
||||
| Web | `qihuo` | 页面与 API;`QIHUO_CTP_ROLE=client`,经 `127.0.0.1:6601` IPC 访问 Worker |
|
||||
| CTP Worker | `qihuo-ctp` | 唯一加载 vnpy_ctp;连接、报单、持仓回调、止盈止损、滚仓 pending 监控 |
|
||||
|
||||
SimNow 与期货公司实盘的 **报单代码路径不变**,仍走 `execute_order()` → `vnpy_bridge.send_order()`;Web 侧为 IPC 代理,Worker 侧为原生调用。
|
||||
|
||||
部署与重启见 [DEPLOY.md](./DEPLOY.md)。
|
||||
|
||||
---
|
||||
|
||||
## 一、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 ecosystem.config.cjs --update-env`(同时重启 Web 与 Worker)。
|
||||
|
||||
---
|
||||
|
||||
## 三、开平仓逻辑(代码层)
|
||||
|
||||
模拟盘与实盘共用 **`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**(买平) | 见下表 |
|
||||
|
||||
**开平标志** 由 VeighNa **`OffsetConverter`**(`vnpy/trader/converter.py`)自动转换:策略层发 `Offset.CLOSE`,框架按今/昨仓拆单为 `CLOSETODAY` / `CLOSEYESTERDAY`(与 CTA 引擎 `sell()`/`cover()` 一致)。持仓今昨数据来自 CTP `OnRspQryInvestorPosition` 的 **PositionDate** 字段,修正 vnpy 网关合并误差后喂给 OffsetConverter。
|
||||
|
||||
| 交易所 | 规则 |
|
||||
|--------|------|
|
||||
| **大商所(DCE)** 等 | 使用通用 **CLOSE** |
|
||||
| **上期所(SHFE)、能源(INE)** | OffsetConverter 按今/昨可平量自动拆单 |
|
||||
| 持仓来源 | CTP **PositionDate**(1=今仓、2=昨仓)缓存,修正 `PositionData.yd_volume` |
|
||||
|
||||
这与国内期货公司 CTP 客户端、快期等 **标准投机平仓逻辑** 一致。
|
||||
|
||||
### 委托类型
|
||||
|
||||
| 页面选项 | vnpy 类型 | 说明 |
|
||||
|----------|-----------|------|
|
||||
| 限价 | `OrderType.LIMIT` | 价格按最小变动价位取整 |
|
||||
| 市价 | `OrderType.FAK` + **对手价(买一/卖一)** + 滑点 | 非「无价格市价单」;止损约 12 跳、强平约 20 跳(强平另受权益滑点预留上限约束) |
|
||||
|
||||
止盈止损触发、手动平仓走 `urgency=stop_loss`;日亏损强平走 `urgency=risk_flatten`。
|
||||
|
||||
---
|
||||
|
||||
## 四、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、防火墙、强密码)
|
||||
+590
@@ -0,0 +1,590 @@
|
||||
# 部署文档
|
||||
|
||||
国内期货 · 交易复盘系统 — Ubuntu 服务器部署、更新与运维说明。
|
||||
|
||||
---
|
||||
|
||||
## 代码发布铁律(强制,不容置疑)
|
||||
|
||||
**所有代码变更必须且只能按以下三步执行,不得跳过、不得变通:**
|
||||
|
||||
| 步骤 | 在哪里 | 做什么 |
|
||||
|------|--------|--------|
|
||||
| **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/config/.env`(兼容旧版 `/opt/qihuo/.env`)
|
||||
- `/opt/qihuo/futures.db`(SQLite)或 PostgreSQL 数据
|
||||
- `/opt/qihuo/uploads/`
|
||||
- `/opt/qihuo/backups/`(若有)
|
||||
|
||||
---
|
||||
|
||||
## 部署概要
|
||||
|
||||
| 项目 | 默认值 |
|
||||
|------|--------|
|
||||
| 部署目录 | `/opt/qihuo` |
|
||||
| 运行用户 | `root`(与 `deploy.sh` / PM2 配置一致) |
|
||||
| Web 端口 | `6600`(对外) |
|
||||
| CTP Worker 端口 | `6601`(仅 `127.0.0.1`,Web 进程 IPC 调用,勿对外开放) |
|
||||
| 进程管理 | PM2:**仅** `qihuo`(Flask + CTP 单进程) |
|
||||
| 数据库 | **生产推荐 PostgreSQL**(见 [POSTGRES.md](./POSTGRES.md));未配置 `DATABASE_URL` 时使用 SQLite `futures.db` |
|
||||
| 仓库 | https://git.bz121.com/dekun/qihuo.git |
|
||||
|
||||
### 进程架构(2026-07 起:单进程)
|
||||
|
||||
| PM2 应用 | 说明 |
|
||||
|----------|------|
|
||||
| `qihuo` | Flask Web + **vn.py / CTP 同进程**(`vnpy_bridge.CtpBridge`) |
|
||||
|
||||
详见 [ARCHITECTURE.md](./ARCHITECTURE.md)。旧版 `qihuo-ctp` 独立 Worker **已废弃**,`ecosystem.config.cjs` 不再启动该进程。
|
||||
|
||||
---
|
||||
|
||||
## 环境要求
|
||||
|
||||
- **系统**:Ubuntu 20.04+(推荐)
|
||||
- **Python**:3.10+(vnpy_ctp 要求 ≥3.10)
|
||||
- **Node.js + PM2**:进程守护与开机自启
|
||||
- **编译工具**(安装 vnpy_ctp 时需要):`build-essential`、`python3-dev`、`pkg-config`
|
||||
- **网络**:
|
||||
- `hq.sinajs.cn`(新浪行情)
|
||||
- 企业微信 API(若启用推送)
|
||||
- `git.bz121.com`(拉取代码)
|
||||
- `pypi.org`(pip 安装依赖)
|
||||
- SimNow / 期货公司 **CTP 前置地址**(下单与持仓,见下文)
|
||||
|
||||
---
|
||||
|
||||
## 一键部署(推荐)
|
||||
|
||||
以 **root** 登录服务器后执行:
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
# 若目录不存在,先克隆:
|
||||
# git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
|
||||
|
||||
bash deploy.sh
|
||||
```
|
||||
|
||||
`deploy.sh` 会自动完成:
|
||||
|
||||
1. 安装系统依赖:`python3`、`git`、`build-essential`、`python3-dev`、`pkg-config`、`locales`、`netcat-openbsd`、`pm2`
|
||||
2. **时区**设为 `Asia/Shanghai`(与 SimNow 交易时段一致)
|
||||
3. **locale**:生成 `zh_CN.GB18030`、`zh_CN.UTF-8`(CTP 登录必需,缺则进程崩溃)
|
||||
4. `git pull` 或 `git clone` 到 `/opt/qihuo`
|
||||
5. 创建/保留虚拟环境 `venv`,`pip install -r requirements.txt`,验证 `vnpy_ctp`
|
||||
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`** 一个进程)
|
||||
|
||||
部署完成后访问:`http://<服务器IP>:6600`
|
||||
|
||||
### PostgreSQL 生产库(推荐)
|
||||
|
||||
消除 SQLite 并发 `database is locked`,一键安装 PostgreSQL 并迁移:
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
git pull
|
||||
# 新装 PostgreSQL + 空库
|
||||
sudo bash scripts/deploy_postgres.sh
|
||||
# 从现有 futures.db 迁移
|
||||
MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
|
||||
```
|
||||
|
||||
完整说明、手动步骤、备份恢复见 **[POSTGRES.md](./POSTGRES.md)**。
|
||||
|
||||
> 再次部署只需 `cd /opt/qihuo && bash deploy.sh`,无需手工装 locale 或改前置地址。
|
||||
|
||||
---
|
||||
|
||||
## 服务器卸载与全新部署(Git 唯一来源)
|
||||
|
||||
当服务器代码被 SCP 弄乱、版本不可信、或需要与仓库 **完全对齐** 时,按本节 **卸载后重装**。全程 **只** 通过 Git 获取代码,**不得** SCP 复制业务文件。
|
||||
|
||||
### 1. 备份(必做)
|
||||
|
||||
```bash
|
||||
# 在服务器上
|
||||
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 备份命令
|
||||
tar czf /root/qihuo_uploads.bak.tar.gz -C /opt/qihuo uploads 2>/dev/null || true
|
||||
```
|
||||
|
||||
### 2. 卸载 PM2 与代码目录
|
||||
|
||||
```bash
|
||||
pm2 stop qihuo 2>/dev/null || true
|
||||
pm2 delete qihuo 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 为 online
|
||||
```
|
||||
|
||||
浏览器访问 `http://<服务器IP>:6600` 登录验证。
|
||||
|
||||
此后所有更新 **只** 走上文「代码发布铁律」三步,**禁止** 再使用 SCP 更新代码。
|
||||
|
||||
---
|
||||
|
||||
## 手动部署
|
||||
|
||||
### 1. 安装系统依赖
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y python3 python3-venv python3-pip python3-dev pkg-config git nodejs npm build-essential locales netcat-openbsd
|
||||
timedatectl set-timezone Asia/Shanghai
|
||||
sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen
|
||||
sed -i '/^# zh_CN.UTF-8/s/^# //' /etc/locale.gen
|
||||
locale-gen zh_CN.GB18030 zh_CN.UTF-8
|
||||
update-locale LANG=zh_CN.UTF-8 LC_ALL=zh_CN.UTF-8
|
||||
npm install -g pm2
|
||||
```
|
||||
|
||||
`build-essential`、`python3-dev`、`pkg-config` 用于编译安装 **vnpy_ctp**(CTP 网关)。Meson 通过 pkg-config 查找 Python 头文件;缺 `pkg-config` 时会报 `Python dependency not found`。
|
||||
|
||||
### 2. 克隆代码
|
||||
|
||||
```bash
|
||||
git clone https://git.bz121.com/dekun/qihuo.git /opt/qihuo
|
||||
cd /opt/qihuo
|
||||
```
|
||||
|
||||
### 3. Python 虚拟环境与依赖
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
依赖已包含 **vnpy**、**vnpy_ctp**(CTP 报单)、**akshare**(手续费同步)。安装完成后可验证:
|
||||
|
||||
```bash
|
||||
python -c "from vnpy_ctp import CtpGateway; print('vnpy_ctp OK')"
|
||||
```
|
||||
|
||||
若提示找不到模块,查看本文「CTP / vnpy 故障排查」一节。
|
||||
|
||||
```bash
|
||||
cp config/.env.example config/.env
|
||||
nano config/.env
|
||||
```
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `HOST` | 监听地址,默认 `0.0.0.0` |
|
||||
| `PORT` | 端口,默认 `6600` |
|
||||
| `SECRET_KEY` | Flask Session 密钥,务必随机 |
|
||||
| `ADMIN_USERNAME` | 初始管理员用户名 |
|
||||
| `ADMIN_PASSWORD` | 初始管理员密码(仅首次建库生效) |
|
||||
| `ADMIN_SYNC_FROM_ENV` | `true` 时重启可从 `.env` 同步账号密码 |
|
||||
| `WECHAT_WEBHOOK` | 企业微信机器人地址(可选) |
|
||||
| `QUOTE_SOURCE` | `sina`(默认)/ `ths` / `auto` |
|
||||
| `THS_REFRESH_TOKEN` | 同花顺 iFinD token(机构用户) |
|
||||
| `SIMNOW_USER` | SimNow 仿真账号(模拟盘必填) |
|
||||
| `SIMNOW_PASSWORD` | SimNow 密码 |
|
||||
| `SIMNOW_TD_ADDRESS` | SimNow 交易前置(以官网最新为准) |
|
||||
| `SIMNOW_MD_ADDRESS` | SimNow 行情前置 |
|
||||
| `CTP_LIVE_*` | 期货公司实盘 CTP(后期接入,见 `.env.example`) |
|
||||
| `TRADING_MODE` | `simulation`(SimNow)/ `live`(实盘) |
|
||||
| `QIHUO_CTP_WORKER_TOKEN` | Web ↔ Worker IPC 鉴权(默认见 `ecosystem.config.cjs`,生产建议改随机串并保持两进程一致) |
|
||||
| `QIHUO_CTP_WORKER_URL` | Web 侧 Worker 地址,默认 `http://127.0.0.1:6601` |
|
||||
| `DATABASE_URL` | PostgreSQL 连接串(可选,见 [POSTGRES.md](./POSTGRES.md)) |
|
||||
|
||||
示例:
|
||||
|
||||
```env
|
||||
HOST=0.0.0.0
|
||||
PORT=6600
|
||||
SECRET_KEY=请替换为随机长字符串
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=你的强密码
|
||||
ADMIN_SYNC_FROM_ENV=false
|
||||
WECHAT_WEBHOOK=
|
||||
QUOTE_SOURCE=sina
|
||||
|
||||
# —— SimNow 模拟盘(注册步骤见 docs/SIMNOW.md)——
|
||||
SIMNOW_USER=你的SimNow账号
|
||||
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=实盘
|
||||
TRADING_MODE=simulation
|
||||
```
|
||||
|
||||
SimNow 前置地址会随官网更新,部署前请到 [SimNow 官网](https://www.simnow.com.cn/) 核对 **7×24** 或交易时段地址。
|
||||
|
||||
### 6. PM2 启动
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
pm2 start ecosystem.config.cjs # 启动 qihuo + qihuo-ctp
|
||||
pm2 save
|
||||
pm2 startup # 按提示执行命令,实现开机自启
|
||||
```
|
||||
|
||||
确认两个进程均为 `online`:
|
||||
|
||||
```bash
|
||||
pm2 status
|
||||
# 应看到 qihuo 与 qihuo-ctp
|
||||
```
|
||||
|
||||
### 7. 创建日志目录(若不存在)
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/qihuo/logs /opt/qihuo/uploads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更新部署
|
||||
|
||||
> **强制流程**:本地修改 → `git push` → 服务器 `git fetch && git reset --hard origin/main` → 迁移 → `pm2 restart`。
|
||||
> **禁止 SCP 复制代码。** 详见上文 [代码发布铁律](#代码发布铁律强制不容置疑)。
|
||||
|
||||
代码已推送后,在服务器执行:
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
> 更新后执行 `pm2 restart ecosystem.config.cjs --update-env` 即可(仅 `qihuo`)。
|
||||
|
||||
若服务器曾用 SCP 覆盖文件导致 `git pull` 冲突,用 `git reset --hard origin/main` 与远端对齐。
|
||||
|
||||
若 `vnpy_ctp` 安装失败(常见于缺少编译环境):
|
||||
|
||||
```bash
|
||||
apt install -y build-essential python3-dev pkg-config
|
||||
source venv/bin/activate
|
||||
pip install --no-cache-dir vnpy vnpy_ctp
|
||||
pm2 restart ecosystem.config.cjs --update-env
|
||||
```
|
||||
|
||||
应用启动时会自动执行 SQLite 表结构迁移(`ALTER TABLE` 容错),一般无需手工改库。
|
||||
|
||||
### 首次启用 CTP 下单
|
||||
|
||||
1. 浏览器登录 → **系统设置** 确认 **模拟盘 · SimNow**
|
||||
2. 确认 `pm2 status` 中 **`qihuo-ctp` 为 online**
|
||||
3. 打开 **下单监控** 页 → 点击 **连接 CTP**(或由后台自动重连)
|
||||
4. 连接成功后:权益来自柜台、显示 CTP 持仓、可报单与可开仓品种筛选
|
||||
|
||||
CTP 连接与重连在 **`qihuo-ctp` Worker** 内执行;页面仅轮询状态,**切换页面不会重复发起连接**。
|
||||
|
||||
详见 [TRADING.md](./TRADING.md)。
|
||||
|
||||
---
|
||||
|
||||
## PM2 常用命令
|
||||
|
||||
```bash
|
||||
pm2 status # 查看 qihuo / qihuo-ctp 状态
|
||||
pm2 logs qihuo # Web 日志
|
||||
pm2 logs qihuo-ctp # CTP Worker 日志
|
||||
pm2 logs qihuo --lines 100
|
||||
pm2 restart ecosystem.config.cjs --update-env # 同时重启两个进程(推荐)
|
||||
pm2 restart qihuo # 仅重启 Web
|
||||
pm2 restart qihuo-ctp # 仅重启 CTP Worker(Web 应仍可访问)
|
||||
pm2 stop qihuo # 停止 Web
|
||||
pm2 delete qihuo # 删除 Web 进程
|
||||
pm2 save # 保存进程列表
|
||||
```
|
||||
|
||||
日志文件:
|
||||
|
||||
- `/opt/qihuo/logs/pm2-out.log`、`pm2-error.log` — Web(`qihuo`)
|
||||
- `/opt/qihuo/logs/pm2-ctp-out.log`、`pm2-ctp-error.log` — CTP Worker(`qihuo-ctp`)
|
||||
|
||||
---
|
||||
|
||||
## 本地开发
|
||||
|
||||
```bash
|
||||
git clone https://git.bz121.com/dekun/qihuo.git
|
||||
cd qihuo
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
cp .env.example .env
|
||||
python app.py
|
||||
```
|
||||
|
||||
浏览器访问:`http://127.0.0.1:6600`
|
||||
|
||||
---
|
||||
|
||||
## 账号与密码
|
||||
|
||||
| 场景 | 操作 |
|
||||
|------|------|
|
||||
| 首次部署 | `.env` 中设置 `ADMIN_USERNAME` / `ADMIN_PASSWORD` 后启动 |
|
||||
| 已部署后改 `.env` 密码 | 设 `ADMIN_SYNC_FROM_ENV=true`,`pm2 restart ecosystem.config.cjs --update-env` |
|
||||
| 网页改密码 | 登录 → 系统设置 |
|
||||
| 忘记密码 | `cd /opt/qihuo && source venv/bin/activate && python reset_admin.py` |
|
||||
|
||||
账号数据在 `futures.db` 的 `settings` 表,不会仅因改 `.env` 自动更新(除非开启 `ADMIN_SYNC_FROM_ENV`)。
|
||||
|
||||
---
|
||||
|
||||
## 数据库与数据文件
|
||||
|
||||
| 路径 | 说明 |
|
||||
|------|------|
|
||||
| `/opt/qihuo/futures.db` | 主数据库 |
|
||||
| `/opt/qihuo/uploads/` | 复盘截图、自动 K 线图 |
|
||||
| `/opt/qihuo/data/fee_rates.json` | 默认手续费表(可重载) |
|
||||
| `/root/qihuo_backup/` | 系统自动备份目录(`.tar.gz`) |
|
||||
|
||||
### 自动备份(推荐)
|
||||
|
||||
系统设置 → **数据备份与恢复**:
|
||||
|
||||
- 默认每天 03:00 自动备份到 `/root/qihuo_backup`
|
||||
- 含 `futures.db` 与 `uploads/`,可在其他服务器恢复
|
||||
- 设置页可立即备份、下载历史压缩包
|
||||
|
||||
完整说明见 **[BACKUP.md](./BACKUP.md)**。
|
||||
|
||||
### 手工备份(备选)
|
||||
|
||||
```bash
|
||||
cp /opt/qihuo/futures.db /opt/qihuo/futures.db.bak.$(date +%Y%m%d)
|
||||
```
|
||||
|
||||
### 手工补列(极少需要)
|
||||
|
||||
若极老版本库缺少字段,可对照报错执行(新版本启动会自动迁移):
|
||||
|
||||
```bash
|
||||
sqlite3 /opt/qihuo/futures.db "ALTER TABLE key_monitors ADD COLUMN sina_code TEXT;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nginx 反向代理(可选)
|
||||
|
||||
将 6600 反代到 80/443,并配置 HTTPS:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your.domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:6600;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 防火墙
|
||||
|
||||
若使用 `ufw`,开放端口:
|
||||
|
||||
```bash
|
||||
ufw allow 6600/tcp
|
||||
# 或使用 Nginx 时只开放 80/443
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 处理 |
|
||||
|------|----------|------|
|
||||
| 无法访问 6600 | 服务未启动 / 防火墙 | `pm2 status`、`pm2 logs qihuo` |
|
||||
| **`qihuo-ctp` 不在线 / 反复重启** | vnpy 崩溃、SimNow 前置不可达、locale 缺失 | `pm2 logs qihuo-ctp --lines 200`;核对 SimNow 前置与 `zh_CN.GB18030` |
|
||||
| **页面显示 CTP 未连接但 Worker 正常** | Web 与 Worker Token 不一致 | 检查 `ecosystem.config.cjs` 两进程 `QIHUO_CTP_WORKER_TOKEN` 相同后重启 |
|
||||
| **API 报 `CTP worker unavailable`** | Worker 未启动或 6601 不可达 | `curl -s http://127.0.0.1:6601/health`;`pm2 restart qihuo-ctp` |
|
||||
| 登录失败 | 密码未同步 | 网页改密或 `reset_admin.py` |
|
||||
| 现价一直 `--` | 新浪网络不可达 | 检查服务器能否访问 `hq.sinajs.cn` |
|
||||
| 关键位 500 | 缺 `sina_code` 列 | `git pull` 重启;或手工 `ALTER TABLE` |
|
||||
| K 线生成失败 | matplotlib 未装 | `pip install matplotlib==3.9.2` |
|
||||
| 手续费同步失败 | akshare 异常 | 使用「重载 JSON」或检查 akshare |
|
||||
| **未安装 vnpy / vnpy_ctp** | 依赖未装或编译失败 | 见下方「CTP / vnpy 故障排查」 |
|
||||
| **CTP 连接超时** | SimNow 地址/账号/非交易时段 | 核对 `.env` 与 SimNow 官网前置 |
|
||||
| **下单监控无持仓** | 未连接 CTP 或确实无仓 | 先点「连接 CTP」 |
|
||||
| **`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) |
|
||||
| `git pull` 冲突 | 曾用 SCP 覆盖文件(**禁止**) | 按 [服务器卸载与全新部署](#服务器卸载与全新部署git-唯一来源) 或 `git reset --hard origin/main` 与远端对齐 |
|
||||
|
||||
查看应用是否在监听:
|
||||
|
||||
```bash
|
||||
ss -tlnp | grep 6600
|
||||
```
|
||||
|
||||
### DNS 无法解析(git / curl 均失败)
|
||||
|
||||
若 `curl cip.cc` 或 `git pull` 报 `Could not resolve host`:
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/systemd/resolved.conf.d
|
||||
cat > /etc/systemd/resolved.conf.d/dns.conf <<'EOF'
|
||||
[Resolve]
|
||||
DNS=223.5.5.5 8.8.8.8
|
||||
FallbackDNS=1.1.1.1
|
||||
EOF
|
||||
systemctl restart systemd-resolved
|
||||
resolvectl flush-caches
|
||||
```
|
||||
|
||||
验证:`resolvectl query git.bz121.com`、`curl cip.cc`
|
||||
|
||||
---
|
||||
|
||||
页面提示 **「未安装 vnpy / vnpy_ctp」** 表示 Python 环境未成功安装 CTP 网关,下单与柜台持仓不可用(看盘、策略、复盘仍可用)。
|
||||
|
||||
**1. 安装依赖**
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
source venv/bin/activate
|
||||
apt install -y build-essential python3-dev pkg-config # 首次需要
|
||||
pip install -r requirements.txt
|
||||
python -c "from vnpy_ctp import CtpGateway; print('OK')"
|
||||
pm2 restart ecosystem.config.cjs --update-env
|
||||
```
|
||||
|
||||
**2. 配置 SimNow(`.env`)**
|
||||
|
||||
注册与查投资者代码见 [SIMNOW.md](./SIMNOW.md)。填写 `SIMNOW_USER`(投资者代码)、`SIMNOW_PASSWORD`,前置地址以 SimNow 官网为准。
|
||||
|
||||
**3. 连接**
|
||||
|
||||
登录系统 → **下单监控** → **连接 CTP**。成功则顶栏显示「CTP 已连接」,权益变为 SimNow 账户资金。
|
||||
|
||||
**4. 常见错误**
|
||||
|
||||
| 日志/现象 | 处理 |
|
||||
|-----------|------|
|
||||
| `pip install vnpy_ctp` 编译失败 / `Python dependency not found` | 安装 `build-essential python3-dev pkg-config` 后重试 |
|
||||
| CTP 连接超时 | 检查前置 IP、端口、SimNow 是否维护、是否在允许连接时段 |
|
||||
| 连接后立即崩溃 `locale::facet::_S_create_c_locale` | CTP 需 **zh_CN.GB18030**:`sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen && locale-gen zh_CN.GB18030`,再 `pm2 restart ecosystem.config.cjs --update-env` |
|
||||
| 服务器 `180.168.146.187` 超时 | 换 SimNow 备用前置 `182.254.243.31:30001/30011`(见 [SIMNOW.md](./SIMNOW.md)) |
|
||||
| 已连接但下单拒单 | 检查合约代码、价格精度、是否有足够保证金 |
|
||||
|
||||
---
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. 部署后立即修改默认密码
|
||||
2. 勿将 `.env`、`futures.db` 提交到公开仓库
|
||||
3. 生产环境使用 HTTPS + 限制访问 IP
|
||||
4. 定期备份:系统设置页自动备份至 `/root/qihuo_backup`,或见 [BACKUP.md](docs/BACKUP.md)
|
||||
|
||||
---
|
||||
|
||||
## 目录结构(部署后)
|
||||
|
||||
```
|
||||
/opt/qihuo/
|
||||
├── app.py
|
||||
├── vnpy_bridge.py # CTP 桥接(Web=IPC 代理,Worker=原生 vn.py)
|
||||
├── ctp_ipc_client.py # Web → Worker HTTP 客户端
|
||||
├── ctp_worker.py # 独立 CTP Worker 入口(PM2: qihuo-ctp)
|
||||
├── recommend_store.py # 可开仓品种缓存
|
||||
├── recommend_stream.py # 可开仓品种 SSE 推送
|
||||
├── venv/
|
||||
├── futures.db
|
||||
├── .env
|
||||
├── logs/
|
||||
│ ├── pm2-out.log
|
||||
│ ├── pm2-error.log
|
||||
│ ├── pm2-ctp-out.log
|
||||
│ └── pm2-ctp-error.log
|
||||
├── uploads/
|
||||
├── data/fee_rates.json
|
||||
├── ecosystem.config.cjs # PM2:qihuo + qihuo-ctp
|
||||
├── deploy.sh
|
||||
├── requirements.txt # 含 vnpy、vnpy_ctp
|
||||
└── docs/
|
||||
├── FEATURES.md
|
||||
├── DEPLOY.md
|
||||
└── TRADING.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [功能说明文档](./FEATURES.md)
|
||||
- [SimNow 注册与接入说明](./SIMNOW.md)
|
||||
- [手续费与导航设置](./FEES.md)
|
||||
- [交易与 SimNow 配置](./TRADING.md)
|
||||
- [README](../README.md)
|
||||
@@ -0,0 +1,291 @@
|
||||
# 功能说明文档
|
||||
|
||||
国内期货 · 交易复盘系统(Flask + SQLite + vnpy_ctp + PM2)。
|
||||
|
||||
> **分板块详细说明(含下单逻辑、风控、微信模板)** → [INDEX.md](./INDEX.md)
|
||||
> 重点:[WECHAT.md](./WECHAT.md) · [AI.md](./AI.md) · [RISK.md](./RISK.md)
|
||||
|
||||
---
|
||||
|
||||
## 系统概览
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| 访问端口 | 默认 `6600` |
|
||||
| 默认首页 | 登录后 `/` → **下单监控** `/positions` |
|
||||
| 数据存储 | SQLite `futures.db` |
|
||||
| 行情 | 默认新浪;可选同花顺 iFinD |
|
||||
| 合约代码 | 同花顺格式(`ag2606`、`SR609`、`IF2606`) |
|
||||
| 主题 | 页头深色 / 浅色切换 |
|
||||
|
||||
### 导航结构
|
||||
|
||||
| 菜单 | 路径 | 可关闭 |
|
||||
|------|------|--------|
|
||||
| **下单监控** | `/positions` | 否(默认首页) |
|
||||
| 策略交易 | `/strategy` | 是 |
|
||||
| 开单计划 | `/plans` | 是 |
|
||||
| 关键位监控 | `/keys` | 否 |
|
||||
| 行情 K 线 | `/market` | 是 |
|
||||
| 交易记录与复盘 | `/records` | 否 |
|
||||
| 统计分析 | `/stats` | 否 |
|
||||
| AI 分析 | `/ai` | 是 |
|
||||
| 手续费配置 | `/fees` | 是 |
|
||||
| 系统设置 | `/settings` | 否 |
|
||||
|
||||
关闭项在 **系统设置 → 导航显示** 配置;直接访问 URL 会提示并跳回下单监控。
|
||||
|
||||
---
|
||||
|
||||
## 下单监控
|
||||
|
||||
**路径**:`/positions` · 详见 [ORDER_MONITOR.md](./ORDER_MONITOR.md)
|
||||
|
||||
### 顶栏
|
||||
|
||||
- 模拟盘 / 实盘模式、CTP 连接状态、风险状态
|
||||
- 权益、可用资金(连接 CTP 后来自柜台)
|
||||
- **连接 CTP** / 重连;断线自动重连;开盘前 30 分钟自动连接
|
||||
|
||||
### 期货下单
|
||||
|
||||
- 品种联想(可开仓品种表与下拉一致;小账户或 CTP 未连接时仅四品种,见 [TRADING.md](./TRADING.md))
|
||||
- 方向、手数(固定手数 / 固定金额计仓)
|
||||
- 限价 / 市价(FAK)、止盈、止损
|
||||
- **移动保本**(默认关闭):开启后隐藏止盈与盈亏比,仅填止损;由移动止损监控平仓,不设固定止盈
|
||||
- 非交易时段禁止报单
|
||||
|
||||
### 当前持仓
|
||||
|
||||
- 开仓委托先显示 **挂单中**,成交后显示为 active 持仓
|
||||
- 挂单超时自动撤单;交易时段内可 **手动撤单**
|
||||
- 持仓卡片:浮盈亏、保证金、止盈止损、平仓等
|
||||
- 数据经 SSE 推送,无需整页刷新
|
||||
|
||||
### 可开仓品种
|
||||
|
||||
- 按当前权益与保证金上限筛选可开品种,养成开仓纪律、限制仓位
|
||||
- **权益 ≤20 万** 或 **CTP 未连接** 时,仅展示并可交易:玉米、豆粕、甲醇、螺纹钢(SimNow/实盘一致);未连接时最大手数按 **10 万权益** 估算
|
||||
- **夜盘时段** 仅显示有夜盘品种,并标注「夜盘」
|
||||
- **行业分类**、走势(多头/空头/震荡/转多/转空)、跳空、昨日成交量(手)、成交额
|
||||
- 支持行业筛选与多字段排序
|
||||
- 每日后台刷新缓存
|
||||
|
||||
详见 [TRADING.md](./TRADING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 策略交易
|
||||
|
||||
**路径**:`/strategy`、`/strategy/records` · 详见 [STRATEGY.md](./STRATEGY.md)
|
||||
|
||||
- 趋势回调(首仓/补仓/止盈须 CTP + 交易时段)
|
||||
- 顺势加仓:**市价**须交易时段;**突破**可休盘提交监控,开盘触价成交
|
||||
- 策略记录单独归档
|
||||
|
||||
---
|
||||
|
||||
## 开单计划
|
||||
|
||||
**路径**:`/plans` · 详见 [PLANS.md](./PLANS.md)
|
||||
|
||||
- 录入当日计划:主力合约、方向、决策区间、止损、止盈
|
||||
- 状态:`planned` → `active` → `closed` / `expired`
|
||||
- 现价进入区间 → 企业微信推送并激活
|
||||
- 激活后监控止盈/止损,触发写入 `trade_records` 并关闭计划
|
||||
- 列表约 1 秒轮询 `/api/plan_prices`
|
||||
|
||||
---
|
||||
|
||||
## 关键位监控
|
||||
|
||||
**路径**:`/keys` · 详细规则见 [KEY_MONITORS.md](./KEY_MONITORS.md)
|
||||
|
||||
- **箱体突破 / 收敛突破**:5m 收盘突破 → 顺势/反转自动市价单;止损=突破 K±2 跳;盈亏比默认 2(可改);可选移动保本(默认 3R 止盈)
|
||||
- **关键支阻区**:上沿阻力 + 下沿支撑;5m 收盘突破 → 微信提醒最多 3 次(间隔约 5 分钟),不自动开仓
|
||||
- 删除后归档至监控历史;列表约 1 秒轮询 `/api/key_prices`
|
||||
|
||||
---
|
||||
|
||||
## 行情 K 线
|
||||
|
||||
**路径**:`/market` · 详见 [MARKET.md](./MARKET.md)
|
||||
|
||||
- 多周期 K 线(TradingView Lightweight Charts)
|
||||
- 支持 CTP 连接后部分数据增强
|
||||
- 需在导航中开启
|
||||
|
||||
---
|
||||
|
||||
## 交易记录与复盘
|
||||
|
||||
**路径**:`/records`(`/trades` 重定向至此) · 详见 [RECORDS.md](./RECORDS.md)
|
||||
|
||||
### 资金曲线
|
||||
|
||||
- 页顶 Lightweight Charts 资金曲线
|
||||
- 随深色/浅色主题自动切换颜色
|
||||
|
||||
### 交易记录
|
||||
|
||||
- **CTP 已连接** 时打开页面自动同步柜台成交(来源「柜台」)
|
||||
- 程序写入的记录来源为「本地」,可核对、删除
|
||||
- 表头固定,表体约 10 行高度内滚动
|
||||
- **修改/核对开关**:开启后可编辑并「核对修改」
|
||||
- **填入复盘**:预填复盘表单
|
||||
|
||||
主要字段:品种、类型、方向、成交价、止损/止盈、手数、保证金、盈亏、手续费、净盈亏、最新资金、结果。
|
||||
|
||||
### 复盘上传 / 复盘历史
|
||||
|
||||
- 手动复盘表单、截图、自动 K 线图(matplotlib)
|
||||
- 按本日/本周/本月/自定义日期筛选历史
|
||||
|
||||
---
|
||||
|
||||
## 统计分析
|
||||
|
||||
**路径**:`/stats` · 详见 [STATS.md](./STATS.md)
|
||||
|
||||
### 汇总指标(单行卡片)
|
||||
|
||||
总交易次数、胜率、平均盈利/亏损、盈亏比、连续亏损、最大回撤、最大盈亏金额及占比、累计手续费、情绪单数量/占比。
|
||||
|
||||
进入页面自动加载(`/api/stats`),无手动「重新计算」按钮。
|
||||
|
||||
### 分项统计
|
||||
|
||||
下拉选择维度:按时间、周、月、品种、手续费、方向、交易类型、情绪单等,表格展示分组指标。
|
||||
|
||||
数据来源:`trade_logs`(主)+ `review_records`(情绪单等)。
|
||||
|
||||
---
|
||||
|
||||
## AI 分析
|
||||
|
||||
**路径**:`/ai` · 详见 [AI.md](./AI.md)
|
||||
|
||||
- 开仓/平仓/关键位成交后后台 AI 复盘(Ollama 或 OpenAI 兼容 API)
|
||||
- 日终持仓与交易报告(可推微信)
|
||||
- 消息存档于 `/ai` 页面;配置见系统设置 → AI 分析
|
||||
|
||||
---
|
||||
|
||||
## 手续费配置
|
||||
|
||||
**路径**:`/fees`
|
||||
|
||||
- **默认**:连接 CTP 后同步柜台费率(`source=ctp`)
|
||||
- 备选:本地 `data/fee_rates.json`、AKShare 参考表 × 倍率
|
||||
- 详见 [FEES.md](./FEES.md)
|
||||
|
||||
---
|
||||
|
||||
## 系统设置
|
||||
|
||||
**路径**:`/settings` · 详见 [SETTINGS.md](./SETTINGS.md)
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 导航显示 | 开关可选菜单项 |
|
||||
| 交易模式 | SimNow / 实盘 CTP |
|
||||
| 计仓模式 | 固定手数、固定金额 |
|
||||
| 保证金上限、移动保本缓冲、挂单超时 | 保证金上限默认 30%;移动保本缓冲为达 1R 后止损相对开仓价的跳数(默认 2 跳) |
|
||||
| CTP 连接 | SimNow / 实盘前置与账号(可覆盖 `.env`) |
|
||||
| 参考资金 | CTP 未连接时用于可开仓筛选与估算 |
|
||||
| 企业微信 Webhook | 计划/关键位/交易/AI 推送 · 见 [WECHAT.md](./WECHAT.md) |
|
||||
| 登录账号 | 用户名/密码,同步写入 `.env` |
|
||||
| 数据备份与恢复 | 自动/手动备份、下载压缩包、恢复说明 |
|
||||
| 深色/浅色主题 | 页头切换 |
|
||||
|
||||
备份详情见 [BACKUP.md](./BACKUP.md)。
|
||||
|
||||
忘记密码:`python reset_admin.py`
|
||||
|
||||
---
|
||||
|
||||
## 品种与行情
|
||||
|
||||
### 合约代码格式
|
||||
|
||||
| 交易所 | 示例 |
|
||||
|--------|------|
|
||||
| 上期所 / 大商所 / 能源 | `ag2606`、`rb2605`(小写+4位年月) |
|
||||
| 郑商所 | `SR609`、`MA606`(大写+3位年月) |
|
||||
| 中金所 | `IF2606` |
|
||||
|
||||
### 行情源
|
||||
|
||||
| 配置 | 说明 |
|
||||
|------|------|
|
||||
| `QUOTE_SOURCE=sina` | 默认新浪 |
|
||||
| `QUOTE_SOURCE=ths` | iFinD token |
|
||||
| `QUOTE_SOURCE=auto` | 有 token 优先同花顺 |
|
||||
|
||||
---
|
||||
|
||||
## 数据库表(简要)
|
||||
|
||||
| 表名 | 用途 |
|
||||
|------|------|
|
||||
| `settings` | 密码、微信、资金、导航、交易参数 |
|
||||
| `order_plans` | 开单计划 |
|
||||
| `key_monitors` | 关键位监控 |
|
||||
| `trade_logs` | 平仓交易记录(含 `source`、`ctp_trade_key`) |
|
||||
| `review_records` | 复盘 |
|
||||
| `trade_records` | 计划自动止盈止损记录 |
|
||||
| `fee_rates` | 手续费缓存 |
|
||||
| `product_recommend_cache` | 可开仓品种缓存 |
|
||||
| `stats_cache` | 统计缓存 |
|
||||
|
||||
数据库文件:项目根目录 `futures.db`。
|
||||
|
||||
---
|
||||
|
||||
## 后台任务
|
||||
|
||||
| 任务 | 说明 |
|
||||
|------|------|
|
||||
| 计划/关键位轮询 | 约 3 秒,触发判断与微信推送 |
|
||||
| 可开仓品种刷新 | 每日 + 按需 |
|
||||
| 持仓 SSE | 前端订阅 `/api/trading/stream` |
|
||||
| CTP 开盘前连接 | 默认开盘前 30 分钟(`qihuo-ctp` Worker) |
|
||||
| 挂单超时撤单 | 可配置分钟数 |
|
||||
| 止盈止损守护 | `qihuo-ctp` Worker 内 tick 监控 |
|
||||
| 滚仓 pending 监控 | `qihuo-ctp` Worker,交易时段扫描突破触价 |
|
||||
| 数据库自动备份 | 每日定时(默认 03:00)写入 `/root/qihuo_backup` |
|
||||
|
||||
---
|
||||
|
||||
## 核心文件
|
||||
|
||||
```
|
||||
qihuo/
|
||||
├── app.py # 主路由、计划/关键位/记录/统计
|
||||
├── install_trading.py # 下单、可开仓品种、策略路由
|
||||
├── vnpy_bridge.py # CTP 桥接(Web=IPC,Worker=vn.py)
|
||||
├── ctp_worker.py # 独立 CTP Worker(PM2: qihuo-ctp)
|
||||
├── ctp_ipc_client.py # Web → Worker HTTP 客户端
|
||||
├── ctp_trade_sync.py # 柜台成交同步到 trade_logs
|
||||
├── product_recommend.py # 可开仓品种计算
|
||||
├── stats_engine.py # 统计分析
|
||||
├── db_backup.py # 数据库备份与恢复包
|
||||
├── fee_specs.py / ctp_fee_sync.py
|
||||
├── market.py / kline_chart.py
|
||||
├── templates/ static/
|
||||
└── docs/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全提示
|
||||
|
||||
- 部署后立即修改默认密码
|
||||
- 勿将 `.env` 提交仓库
|
||||
- 生产建议 Nginx + HTTPS,限制 6600 访问范围
|
||||
|
||||
---
|
||||
|
||||
## 仓库
|
||||
|
||||
https://git.bz121.com/dekun/qihuo.git
|
||||
@@ -0,0 +1,53 @@
|
||||
# 手续费与导航设置
|
||||
|
||||
## 手续费数据源
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| **CTP 柜台**(推荐) | 连接 SimNow/实盘 CTP 后,查询柜台费率并缓存到 `fee_rates`(`source=ctp`) |
|
||||
| **本地 / AKShare** | `data/fee_rates.json` 或 AKShare 参考表 × 倍率,离线估算 |
|
||||
|
||||
### 计算公式
|
||||
|
||||
```
|
||||
单边手续费 = 固定(元/手) × 手数 + 比例 × 成交价 × 合约乘数 × 手数
|
||||
往返手续费 = 开仓费 + 平仓费(同日持仓用平今,否则平昨)
|
||||
```
|
||||
|
||||
### 同步时机
|
||||
|
||||
1. 连接 CTP 成功后 — 后台自动同步主力合约费率
|
||||
2. **手续费配置页** — 「从 CTP 同步费率」
|
||||
3. 计算某品种时 — 缓存缺失则单品种查询
|
||||
|
||||
---
|
||||
|
||||
## 导航显示开关
|
||||
|
||||
**系统设置 → 导航显示** 可开关:
|
||||
|
||||
| key | 菜单 |
|
||||
|-----|------|
|
||||
| `fees` | 手续费配置 |
|
||||
| `plans` | 开单计划 |
|
||||
| `market` | 行情 K 线 |
|
||||
| `strategy` | 策略交易 |
|
||||
|
||||
关闭后顶栏隐藏;直接访问 URL 会提示并跳转到 **下单监控**。
|
||||
|
||||
始终显示:**下单监控**、关键位监控、交易记录与复盘、统计分析、系统设置。
|
||||
|
||||
设置保存在 `settings.nav_items`(JSON)。
|
||||
|
||||
---
|
||||
|
||||
## 相关文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `fee_specs.py` | 费率计算 |
|
||||
| `ctp_fee_sync.py` | CTP 同步 |
|
||||
| `nav_settings.py` | 导航开关 |
|
||||
| `vnpy_bridge.py` | CTP 连接 |
|
||||
|
||||
详见 [DEPLOY.md](./DEPLOY.md)、[TRADING.md](./TRADING.md)。
|
||||
@@ -0,0 +1,64 @@
|
||||
# 文档索引
|
||||
|
||||
国内期货 · 交易复盘系统各板块说明。每篇文档包含 **下单逻辑**、**风控规则**,并在需要时引用 [微信推送模板](./WECHAT.md)。
|
||||
|
||||
---
|
||||
|
||||
## 总览
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [FEATURES.md](./FEATURES.md) | 功能总览与导航结构 |
|
||||
| [RISK.md](./RISK.md) | **全局账户风控**(冷静期、仓位上限、保证金、品种范围) |
|
||||
| [风控说明.md](./风控说明.md) | **数据看板风控卡片**(各指标含义与颜色规则) |
|
||||
| [WECHAT.md](./WECHAT.md) | **企业微信推送**(全部消息类型与完整模板) |
|
||||
| [AI.md](./AI.md) | **AI 分析**(配置、触发、输出、推送) |
|
||||
|
||||
---
|
||||
|
||||
## 各板块说明
|
||||
|
||||
| 板块 | 路径 | 文档 |
|
||||
|------|------|------|
|
||||
| 数据看板 | `/dashboard` | [风控说明.md](./风控说明.md)(看板内嵌摘要) |
|
||||
| 风控说明 | `/risk-guide` | [风控说明.md](./风控说明.md)(完整页面) |
|
||||
| 下单监控 | `/positions` | [ORDER_MONITOR.md](./ORDER_MONITOR.md) |
|
||||
| 策略交易 | `/strategy` | [STRATEGY.md](./STRATEGY.md) |
|
||||
| 开单计划 | `/plans` | [PLANS.md](./PLANS.md) |
|
||||
| 关键位监控 | `/keys` | [KEY_MONITORS.md](./KEY_MONITORS.md) |
|
||||
| 行情 K 线 | `/market` | [MARKET.md](./MARKET.md) |
|
||||
| 交易记录与复盘 | `/records` | [RECORDS.md](./RECORDS.md) |
|
||||
| 统计分析 | `/stats` | [STATS.md](./STATS.md) |
|
||||
| 手续费配置 | `/fees` | [FEES.md](./FEES.md) |
|
||||
| 系统设置 | `/settings` | [SETTINGS.md](./SETTINGS.md) |
|
||||
|
||||
---
|
||||
|
||||
## 部署与运维
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [TRADING.md](./TRADING.md) | 可开仓品种、计仓、SimNow/实盘 |
|
||||
| [SIMNOW.md](./SIMNOW.md) | SimNow 仿真注册与接入 |
|
||||
| [CTP_LIVE.md](./CTP_LIVE.md) | **期货公司实盘 CTP** 与开平仓对比 |
|
||||
| [DEPLOY.md](./DEPLOY.md) | 部署说明(含 **代码发布铁律**:仅 Git 三步,禁止 SCP) |
|
||||
| [POSTGRES.md](./POSTGRES.md) | **PostgreSQL 生产库**(一键部署、迁移、备份恢复) |
|
||||
| [BACKUP.md](./BACKUP.md) | 数据备份与恢复 |
|
||||
|
||||
---
|
||||
|
||||
## 快速查找
|
||||
|
||||
| 想了解… | 看这里 |
|
||||
|---------|--------|
|
||||
| 手动下单怎么校验 | [ORDER_MONITOR.md](./ORDER_MONITOR.md) |
|
||||
| 关键位自动单规则 | [KEY_MONITORS.md](./KEY_MONITORS.md) |
|
||||
| 开单计划何时推送 | [PLANS.md](./PLANS.md) + [WECHAT.md#开单计划](./WECHAT.md) |
|
||||
| 开仓/平仓微信长文格式 | [WECHAT.md#结构化推送](./WECHAT.md) |
|
||||
| AI 何时分析、推什么 | [AI.md](./AI.md) |
|
||||
| 手动平仓后为何冻结 | [RISK.md#冷静期与日冻结](./RISK.md) |
|
||||
| 看板风控各指标什么意思 | [风控说明.md](./风控说明.md) |
|
||||
| 移动保本怎么抬止损 | [ORDER_MONITOR.md#移动保本](./ORDER_MONITOR.md) |
|
||||
| 实盘 CTP 怎么接、和 SimNow 开平是否一样 | [CTP_LIVE.md](./CTP_LIVE.md) |
|
||||
| CTP 双进程、Worker 日志、6601 端口 | [DEPLOY.md](./DEPLOY.md) |
|
||||
| 休盘能否提交突破滚仓 | [STRATEGY.md](./STRATEGY.md) |
|
||||
@@ -0,0 +1,93 @@
|
||||
# 关键位监控
|
||||
|
||||
**页面路径**:`/keys`
|
||||
|
||||
关键位监控用于在指定价格区间上设置 **5 分钟收盘** 触发规则,分为 **自动单**(箱体/收敛突破)与 **仅微信提醒**(关键支阻区)两类。
|
||||
|
||||
**文档索引**:[INDEX.md](./INDEX.md) · 微信模板:[WECHAT.md §6–§8](./WECHAT.md) · 风控:[RISK.md](./RISK.md)
|
||||
|
||||
---
|
||||
|
||||
## 监控类型
|
||||
|
||||
### 箱体突破 / 收敛突破(自动单)
|
||||
|
||||
| 项目 | 规则 |
|
||||
|------|------|
|
||||
| 触发 | 5 分钟 K 线 **收盘价** 高于上沿或低于下沿 |
|
||||
| 顺势 / 反转 | 顺势:上破做多、下破做空;反转:上破做空、下破做多 |
|
||||
| 下单 | CTP 已连接且在交易时段内,自动 **市价开仓** |
|
||||
| 手数 | 按系统设置的风险比例与保证金上限计算 |
|
||||
| 止损 | 突破 K 线最低价(多)/ 最高价(空)± **2 个最小变动价位** |
|
||||
| 盈亏比 | 默认 **2**,可在新增监控时修改(0.5~10) |
|
||||
| 移动保本 | 可选;开启后盈亏比默认 **3**,达 3R 止盈价自动平仓;同时启用移动保本止损逻辑(达 1R 后抬止损) |
|
||||
| 成交后 | 进入 **下单监控** 持仓列表,`monitor_type` 显示为「箱体突破」或「收敛突破」 |
|
||||
| 结案 | 触发并尝试下单后,本条监控移入历史(无论成败,同一根 5m K 线不重复触发) |
|
||||
|
||||
**前提**:CTP 已连接、处于交易时段、账户风控允许开仓。
|
||||
|
||||
### 关键支阻区(仅提醒)
|
||||
|
||||
| 项目 | 规则 |
|
||||
|------|------|
|
||||
| 区间 | **上沿 = 阻力**,**下沿 = 支撑**,合并为一个关键支阻区 |
|
||||
| 触发 | 5m 收盘突破上沿或跌破下沿 |
|
||||
| 推送 | 企业微信,见 [WECHAT §8](./WECHAT.md#8-关键支阻区提醒) |
|
||||
| 次数 | 最多 **3 次**,间隔约 **5 分钟**(人工盯盘提醒) |
|
||||
| 自动开仓 | **否** |
|
||||
| 结案 | 第 3 次推送后自动归档 |
|
||||
|
||||
历史数据中的「关键阻力位」「关键支撑位」按 **关键支阻区** 同样规则处理。
|
||||
|
||||
---
|
||||
|
||||
## 与旧版差异
|
||||
|
||||
- 旧版:tick 现价触碰即推送,箱体/收敛仅微信提醒
|
||||
- 新版:**统一 5m 收盘** 触发;箱体/收敛改为 **自动市价单**;阻力/支撑合并为 **关键支阻区** 三轮微信提醒
|
||||
|
||||
---
|
||||
|
||||
## 相关配置
|
||||
|
||||
- **企业微信 Webhook**:系统设置 → 企业微信推送
|
||||
- **风险比例 / 保证金上限**:系统设置 → 交易相关(影响自动单手数)
|
||||
- **移动保本跳数缓冲**:系统设置 → `trailing_be_tick_buffer`(自动单开启移动保本时生效)
|
||||
|
||||
---
|
||||
|
||||
## 技术说明
|
||||
|
||||
- 后台任务 `background_task` 约每 3 秒扫描一次 `key_monitors`
|
||||
- 5m K 线优先 CTP,否则新浪/本地缓存
|
||||
- 自动单逻辑:`key_monitor_lib.py` + `install_trading._execute_key_breakout`
|
||||
- 止盈止损监控:`sl_tp_guard.py`(移动保本 + 显式止盈价可同时生效)
|
||||
|
||||
## 下单逻辑(自动单)
|
||||
|
||||
1. 后台 `background_task` 约每 3 秒调用 `run_key_monitor_check()`。
|
||||
2. 拉取 5m K 线(优先 CTP),判断收盘价是否突破上/下沿。
|
||||
3. **箱体突破 / 收敛突破**:
|
||||
- 顺势/反转决定方向(上破做多或做空等)。
|
||||
- 校验:交易时段、CTP、`assert_can_open()`、[RISK.md](./RISK.md) 全部规则。
|
||||
- 手数:按 `risk_percent` + `max_margin_pct` 计算。
|
||||
- **止损**:突破 K 线最低(多)/ 最高(空)± **2 跳**。
|
||||
- **止盈**:默认盈亏比 **2**(可改 0.5~10);开启移动保本时默认 **3R** TP。
|
||||
- **报单**:CTP 市价开仓 → 写入 `trade_order_monitors` → `sl_tp_guard` 守护。
|
||||
4. 同一根 5m K 线只触发一次;触发后移入历史(成败皆然)。
|
||||
5. 成交:微信 [§7](./WECHAT.md#7-关键位开仓成功);失败/摘要:[§6](./WECHAT.md#6-关键位自动单)。
|
||||
|
||||
## 风控规则
|
||||
|
||||
| 规则 | 自动单 | 支阻区 |
|
||||
|------|--------|--------|
|
||||
| assert_can_open | ✓ | — |
|
||||
| 保证金 / 品种 | ✓ | — |
|
||||
| 交易时段 | ✓ | — |
|
||||
| 自动下单 | ✓ | ✗ |
|
||||
|
||||
全局规则见 [RISK.md](./RISK.md)。
|
||||
|
||||
---
|
||||
|
||||
详见 [FEATURES.md](./FEATURES.md) · [INDEX.md](./INDEX.md)
|
||||
@@ -0,0 +1,51 @@
|
||||
# 行情 K 线
|
||||
|
||||
**页面路径**:`/market`
|
||||
|
||||
**相关文件**:`market.py`、`kline_chart.py`、`templates/market.html`
|
||||
|
||||
---
|
||||
|
||||
## 功能概述
|
||||
|
||||
- 多周期 K 线图表(TradingView Lightweight Charts)
|
||||
- 品种选择与周期切换
|
||||
- CTP 连接后部分品种可获取更及时数据
|
||||
|
||||
须在 **系统设置 → 导航显示** 开启「行情 K 线」;关闭后直接访问 URL 会提示并跳回下单监控。
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
**本板块无下单功能**,纯行情展示。
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
无独立风控。不涉及报单、推送或账户状态变更。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
**无**。行情页不发送任何企业微信消息。
|
||||
|
||||
---
|
||||
|
||||
## 数据来源
|
||||
|
||||
| 优先级 | 来源 |
|
||||
|--------|------|
|
||||
| 1 | CTP tick/K 线(已连接时) |
|
||||
| 2 | 新浪 / 同花顺 iFinD(见 `QUOTE_SOURCE` 配置) |
|
||||
|
||||
合约代码格式见 [FEATURES.md](./FEATURES.md#合约代码格式)。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [FEATURES.md](./FEATURES.md)
|
||||
- [SETTINGS.md](./SETTINGS.md)
|
||||
@@ -0,0 +1,151 @@
|
||||
# 下单监控
|
||||
|
||||
**页面路径**:`/positions`(默认首页)
|
||||
|
||||
**相关文件**:`install_trading.py`、`sl_tp_guard.py`、`pending_order_worker.py`、`product_recommend.py`
|
||||
|
||||
---
|
||||
|
||||
## 功能结构
|
||||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| 顶栏 | 模拟/实盘、CTP 状态、风险状态、权益、连接按钮 |
|
||||
| 期货下单 | 手动开仓/平仓表单 |
|
||||
| 当前持仓 | pending 挂单 + active 持仓卡片 |
|
||||
| 可开仓品种 | 按权益与保证金筛选的推荐表 |
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
### 手动开仓
|
||||
|
||||
1. 选择品种、方向、限价/市价(FAK)、手数或计仓模式。
|
||||
2. **前置校验**(全部通过才报单):
|
||||
- CTP 已连接且不在连接中
|
||||
- 处于 **交易时段**
|
||||
- `assert_can_open()` — 见 [RISK.md](./RISK.md)
|
||||
- `assert_product_allowed_for_capital()` — 小账户四品种限制
|
||||
- 固定金额模式须填 **止损价**
|
||||
- 开启 **移动保本** 须填止损(不可只填止盈)
|
||||
- 保证金占用 ≤ `max_margin_pct`
|
||||
- 单笔手数 ≤ 50
|
||||
3. **计仓**:
|
||||
- 固定手数 → 使用 `fixed_lots`
|
||||
- 固定金额 → `calc_lots_by_amount()` 按止损距离算手数
|
||||
4. **报单**:`execute_order()` → CTP。
|
||||
5. **成交后**:
|
||||
- 写入 `trade_order_monitors`(status=active 或 pending)
|
||||
- 注册本地 SL/TP 守护(`sl_tp_guard`)
|
||||
- 微信:结构化开仓推送(有止损)或简版
|
||||
- AI:后台开仓分析(有止损时)
|
||||
|
||||
### 挂单(限价未立即成交)
|
||||
|
||||
- status=`pending`,显示「挂单中」。
|
||||
- **超时撤单**:`pending_order_worker` 每 10 秒 reconcile(有 pending 时);默认超时见系统设置 `pending_order_timeout_sec`。
|
||||
- 微信:`委托已提交 · … 挂单中(N 分钟未成交自动撤单)`
|
||||
|
||||
### 手动平仓
|
||||
|
||||
- 市价平仓当前品种方向持仓。
|
||||
- 关闭对应 monitor,写入 `trade_logs`。
|
||||
- 若当日手动平仓次数超限 → 触发冷静期([RISK.md](./RISK.md))。
|
||||
- 微信:结构化平仓推送(成交同步后)。
|
||||
|
||||
### 止盈止损守护
|
||||
|
||||
后台 `sl_tp_guard` 线程 + tick 事件:
|
||||
|
||||
| 条件 | 动作 |
|
||||
|------|------|
|
||||
| 价格触及 **止盈价** | 市价平仓,结果=止盈 |
|
||||
| 价格触及 **止损价** | 市价平仓,结果=止损 |
|
||||
| 开启 **移动保本** | 见下节 |
|
||||
|
||||
触发时先推简版「平仓委托已提交」,成交写入 trade_logs 后推 **结构化平仓**。
|
||||
|
||||
---
|
||||
|
||||
## 移动保本
|
||||
|
||||
**开启条件**:下单表单勾选「移动保本」,**必须填写止损**,不设固定止盈。
|
||||
|
||||
**逻辑**(`sl_tp_guard._update_trailing_stop_loss`):
|
||||
|
||||
| 浮盈 R 倍数 | 止损移动至 |
|
||||
|-------------|------------|
|
||||
| ≥ 1R | 开仓价 ± `trailing_be_tick_buffer` 跳(保本) |
|
||||
| ≥ 2R | 开仓价 ± 1R |
|
||||
| ≥ nR | 开仓价 ± (n−1)R |
|
||||
|
||||
**平仓结果标签**:
|
||||
|
||||
- 1R 锁定后止损出场 → **保本止盈**
|
||||
- 2R 及以上锁定后止损出场 → **移动止盈**
|
||||
|
||||
关键位自动单若开启移动保本且设目标盈亏比(默认 3R),达 TP 仍按 **止盈** 平仓;移动止损与显式 TP **可同时生效**。
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
本板块执行 [RISK.md](./RISK.md) 全部规则,另加:
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 非交易时段 | 禁止开仓 |
|
||||
| 移动保本 | 必须填止损 |
|
||||
| 固定金额 | 必须填止损才能算手数 |
|
||||
| 保证金 | 新开仓前校验总占用 |
|
||||
| 单笔 50 手 | 超限拒绝 |
|
||||
|
||||
顶栏 **风险状态** 实时反映:正常 / 冷静期 / 日冻结 / 仓位上限。
|
||||
|
||||
---
|
||||
|
||||
## 可开仓品种表
|
||||
|
||||
- 按当前权益、`max_margin_pct`、合约保证金计算可开手数。
|
||||
- 权益 ≤20 万或 CTP 未连接:仅四品种;未连接按 10 万权益估算。
|
||||
- 夜盘时段过滤无夜盘品种。
|
||||
- 每日后台刷新 `product_recommend_cache`。
|
||||
|
||||
详见 [TRADING.md](./TRADING.md)。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
| 事件 | 模板 |
|
||||
|------|------|
|
||||
| 开仓成交(有止损) | [WECHAT §1](./WECHAT.md#1-手动开仓成功) |
|
||||
| 开仓成交(无止损) | [WECHAT §2](./WECHAT.md#2-简版开仓) |
|
||||
| 挂单提交 | [WECHAT §3](./WECHAT.md#3-挂单提交) |
|
||||
| 平仓完成 | [WECHAT §4](./WECHAT.md#4-平仓完成) |
|
||||
| SL/TP 触发 | [WECHAT §5](./WECHAT.md#5-本地止盈止损触发) |
|
||||
| AI 分析 | [AI.md](./AI.md)(仅页面,默认不推微信) |
|
||||
|
||||
---
|
||||
|
||||
## 数据流
|
||||
|
||||
```
|
||||
用户下单 → API /api/trade/order
|
||||
→ CTP 报单
|
||||
→ trade_order_monitors
|
||||
→ sl_tp_guard 监控
|
||||
→ 平仓 → trade_logs → 微信 + AI
|
||||
```
|
||||
|
||||
SSE:`/api/trading/stream` 推送持仓快照,无需整页刷新。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [RISK.md](./RISK.md)
|
||||
- [WECHAT.md](./WECHAT.md)
|
||||
- [TRADING.md](./TRADING.md)
|
||||
- [SETTINGS.md](./SETTINGS.md)
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# 开单计划
|
||||
|
||||
**页面路径**:`/plans`
|
||||
|
||||
**相关文件**:`app.py`(`check_order_plans`、`background_task`)
|
||||
|
||||
---
|
||||
|
||||
## 功能概述
|
||||
|
||||
录入 **当日交易计划**:品种、方向、决策价格区间、止损、止盈、决策理由。系统轮询现价,进入区间后 **微信提醒** 并激活监控;触及 TP/SL 后记录结果并结案。
|
||||
|
||||
> **不自动向 CTP 下单**,仅提醒 + 记录。
|
||||
|
||||
---
|
||||
|
||||
## 计划状态
|
||||
|
||||
```
|
||||
planned → active → closed
|
||||
↘ expired(过期,非当日)
|
||||
```
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| `planned` | 已录入,等待价格进入决策区间 |
|
||||
| `active` | 已进入区间,监控 TP/SL |
|
||||
| `closed` | 触及止盈或止损,已结案 |
|
||||
| `expired` | 非当日计划自动过期 |
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
本模块 **无 CTP 报单**。
|
||||
|
||||
### 1. 录入(用户操作)
|
||||
|
||||
- 主力合约、方向(多/空)
|
||||
- **决策区间**:`zone_lower` ~ `zone_upper`
|
||||
- 止损价、止盈价
|
||||
- 决策理由(可选,推送时展示)
|
||||
|
||||
### 2. 触发激活(后台 ~3 秒)
|
||||
|
||||
条件:`status=planned` 且 `zone_lower ≤ 现价 ≤ zone_upper`
|
||||
|
||||
动作:
|
||||
|
||||
1. 发送微信 [WECHAT §9](./WECHAT.md#9-开单计划)
|
||||
2. `status → active`,记录 `triggered_at`
|
||||
|
||||
### 3. 止盈止损监控(active)
|
||||
|
||||
| 方向 | 止盈 | 止损 |
|
||||
|------|------|------|
|
||||
| 多 | 现价 ≥ take_profit | 现价 ≤ stop_loss |
|
||||
| 空 | 现价 ≤ take_profit | 现价 ≥ stop_loss |
|
||||
|
||||
触发后:
|
||||
|
||||
1. 微信 [WECHAT §10](./WECHAT.md#10-开单计划结果)
|
||||
2. 写入 `trade_records`(monitor_type=开单计划)
|
||||
3. `status → closed`
|
||||
|
||||
### 4. 前端轮询
|
||||
|
||||
列表约 1 秒请求 `/api/plan_prices` 刷新现价。
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 账户冷静期 | **不校验** — 计划不自动开仓 |
|
||||
| 保证金 / 品种 | **不校验** |
|
||||
| 交易时段 | 轮询全天运行,非交易时段仍可推送 |
|
||||
| 同日计划 | `plan_date` 为当日;跨日 `expire_old_plans()` |
|
||||
|
||||
用户收到提醒后若手动下单,须自行遵守 [RISK.md](./RISK.md)。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
| 事件 | 文档 |
|
||||
|------|------|
|
||||
| 进入决策区间 | [WECHAT §9](./WECHAT.md#9-开单计划) |
|
||||
| 触及 TP/SL | [WECHAT §10](./WECHAT.md#10-开单计划结果) |
|
||||
|
||||
**配置**:系统设置 → 企业微信 Webhook。
|
||||
|
||||
---
|
||||
|
||||
## 数据库
|
||||
|
||||
表 `order_plans`:symbol、direction、zone_upper/lower、stop_loss、take_profit、status、plan_date 等。
|
||||
|
||||
触发结果写入 `trade_records`(非 `trade_logs`,不计入 CTP 同步统计主表)。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [WECHAT.md](./WECHAT.md)
|
||||
- [ORDER_MONITOR.md](./ORDER_MONITOR.md) — 收到提醒后手动下单
|
||||
- [RECORDS.md](./RECORDS.md)
|
||||
@@ -0,0 +1,271 @@
|
||||
# PostgreSQL 生产数据库
|
||||
|
||||
qihuo 支持两种存储后端:
|
||||
|
||||
| 模式 | 配置 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| **SQLite**(默认) | 不设置 `DATABASE_URL` | 本地开发、单机轻量试用 |
|
||||
| **PostgreSQL**(推荐生产) | `.env` 中 `DATABASE_URL=postgresql://...` | 7×24 运行、多线程并发、消除 `database is locked` |
|
||||
|
||||
配置 `DATABASE_URL` 后,应用自动使用 **连接池**(默认 2–20 连接),无需改业务代码。
|
||||
|
||||
---
|
||||
|
||||
## 为什么用 PostgreSQL
|
||||
|
||||
SQLite 在同一文件上同一时刻只允许一个写者。qihuo 单进程内有多路后台线程(持仓刷新、止盈守护、挂单同步、统计缓存等)和 HTTP 请求同时写库,容易出现:
|
||||
|
||||
```
|
||||
position worker failed: database is locked
|
||||
bootstrap position snapshot: database is locked
|
||||
```
|
||||
|
||||
PostgreSQL 面向并发读写设计,多连接、行级锁、连接池,与专业交易软件「服务端数据库 + 内存快照」的思路一致。
|
||||
|
||||
---
|
||||
|
||||
## 一键部署(新服务器 / 已有 qihuo)
|
||||
|
||||
在已执行过 `deploy.sh` 的服务器上,以 **root** 运行:
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
git pull # 获取最新代码
|
||||
sudo bash scripts/deploy_postgres.sh
|
||||
```
|
||||
|
||||
脚本会自动:
|
||||
|
||||
1. 安装 `postgresql` / `postgresql-contrib`
|
||||
2. 创建数据库 `qihuo`、用户 `qihuo`(随机密码,终端会打印)
|
||||
3. 写入 `/opt/qihuo/.env` 的 `DATABASE_URL`、`PG_POOL_MIN`、`PG_POOL_MAX`
|
||||
4. `pip install psycopg psycopg-pool`
|
||||
5. 执行 `init_db()` 建表
|
||||
6. `pm2 restart qihuo --update-env`
|
||||
|
||||
### 从现有 SQLite 迁移
|
||||
|
||||
若 `/opt/qihuo/futures.db` 已有数据:
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
|
||||
```
|
||||
|
||||
会:
|
||||
|
||||
- 初始化 PostgreSQL 表结构
|
||||
- 运行 `scripts/migrate_sqlite_to_postgres.py` 导入全部表
|
||||
- 将旧库备份为 `futures.db.pre_pg.YYYYMMDD_HHMMSS`(可用 `BACKUP_SQLITE=0` 跳过)
|
||||
|
||||
迁移前建议先做一次 Web 设置页 **立即备份** 或:
|
||||
|
||||
```bash
|
||||
cp /opt/qihuo/futures.db /root/futures.db.bak.$(date +%Y%m%d)
|
||||
pm2 stop qihuo
|
||||
MIGRATE_SQLITE=1 sudo bash scripts/deploy_postgres.sh
|
||||
```
|
||||
|
||||
### 环境变量(可选)
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `APP_DIR` | `/opt/qihuo` | 应用目录 |
|
||||
| `PG_DB` | `qihuo` | 数据库名 |
|
||||
| `PG_USER` | `qihuo` | 数据库用户 |
|
||||
| `PG_PASSWORD` | 随机 | 不设则脚本生成 |
|
||||
| `PG_HOST` | `127.0.0.1` | 主机 |
|
||||
| `PG_PORT` | `5432` | 端口 |
|
||||
| `MIGRATE_SQLITE` | `0` | `1` 时从 `futures.db` 迁移 |
|
||||
| `BACKUP_SQLITE` | `1` | 迁移后是否备份旧 SQLite 文件 |
|
||||
|
||||
---
|
||||
|
||||
## 手动部署
|
||||
|
||||
### 1. 安装 PostgreSQL(Ubuntu)
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y postgresql postgresql-contrib
|
||||
systemctl enable postgresql
|
||||
systemctl start postgresql
|
||||
```
|
||||
|
||||
### 2. 创建库与用户
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql <<'SQL'
|
||||
CREATE USER qihuo WITH PASSWORD '请改为强密码';
|
||||
CREATE DATABASE qihuo OWNER qihuo;
|
||||
GRANT ALL PRIVILEGES ON DATABASE qihuo TO qihuo;
|
||||
SQL
|
||||
```
|
||||
|
||||
### 3. 配置 `.env`
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo
|
||||
cat >> .env <<'EOF'
|
||||
|
||||
DATABASE_URL=postgresql://qihuo:请改为强密码@127.0.0.1:5432/qihuo
|
||||
PG_POOL_MIN=2
|
||||
PG_POOL_MAX=20
|
||||
EOF
|
||||
```
|
||||
|
||||
### 4. 安装 Python 驱动并初始化
|
||||
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
export $(grep -v '^#' .env | xargs) # 或手动 export DATABASE_URL
|
||||
python3 -c "from app import init_db; init_db()"
|
||||
```
|
||||
|
||||
### 5. 迁移 SQLite(可选)
|
||||
|
||||
```bash
|
||||
python3 scripts/migrate_sqlite_to_postgres.py --sqlite /opt/qihuo/futures.db
|
||||
# 仅预览行数:
|
||||
python3 scripts/migrate_sqlite_to_postgres.py --dry-run
|
||||
```
|
||||
|
||||
### 6. 重启应用
|
||||
|
||||
```bash
|
||||
pm2 restart qihuo --update-env
|
||||
pm2 logs qihuo --lines 30
|
||||
```
|
||||
|
||||
启动后日志中不应再频繁出现 `database is locked`(SQLite 特有)。
|
||||
|
||||
---
|
||||
|
||||
## 连接池
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `PG_POOL_MIN` | `2` | 池内最少连接 |
|
||||
| `PG_POOL_MAX` | `20` | 池内最多连接 |
|
||||
|
||||
每个 HTTP 请求 / 后台 worker 从池中借连接,用毕归还。PM2 请保持 **`instances: 1`**(见 `ecosystem.config.cjs`);若要多实例,共用同一 `DATABASE_URL` 即可,PostgreSQL 可承受。
|
||||
|
||||
---
|
||||
|
||||
## 备份
|
||||
|
||||
### 方式一:系统设置页(推荐)
|
||||
|
||||
**系统设置 → 数据备份与恢复 → 立即备份**
|
||||
|
||||
PostgreSQL 模式下包内为 `postgres_dump.sql`(`pg_dump` 逻辑备份),而非 `futures.db`。
|
||||
|
||||
### 方式二:命令行
|
||||
|
||||
```bash
|
||||
# 需与 .env 中 DATABASE_URL 一致
|
||||
source /opt/qihuo/venv/bin/activate
|
||||
set -a && source /opt/qihuo/.env && set +a
|
||||
pg_dump --no-owner --no-acl -f /root/qihuo_backup/manual_$(date +%Y%m%d_%H%M%S).sql "$DATABASE_URL"
|
||||
```
|
||||
|
||||
### 方式三:每日自动备份
|
||||
|
||||
设置页开启 **每日自动备份**(默认 03:00),保留份数默认 30。备份目录默认 `/root/qihuo_backup`。
|
||||
|
||||
详见 [BACKUP.md](./BACKUP.md)。
|
||||
|
||||
---
|
||||
|
||||
## 恢复
|
||||
|
||||
### 从 qihuo 备份包恢复(含 restore.sh)
|
||||
|
||||
```bash
|
||||
pm2 stop qihuo
|
||||
cd /root
|
||||
tar -xzf qihuo_backup_YYYYMMDD_HHMMSS.tar.gz
|
||||
cd qihuo_backup_YYYYMMDD_HHMMSS
|
||||
# 确保 /opt/qihuo/.env 已配置 DATABASE_URL
|
||||
export RESTORE_DIR=/opt/qihuo
|
||||
chmod +x restore.sh
|
||||
./restore.sh
|
||||
pm2 restart qihuo
|
||||
```
|
||||
|
||||
`manifest.json` 中 `"backend": "postgres"` 表示包内为 `postgres_dump.sql`。
|
||||
|
||||
### 手工 psql 恢复
|
||||
|
||||
```bash
|
||||
pm2 stop qihuo
|
||||
export DATABASE_URL=postgresql://qihuo:密码@127.0.0.1:5432/qihuo
|
||||
# 空库或需覆盖的库
|
||||
psql "$DATABASE_URL" -f /path/to/postgres_dump.sql
|
||||
cp -a uploads_backup/. /opt/qihuo/uploads/ # 若有附件
|
||||
pm2 restart qihuo
|
||||
```
|
||||
|
||||
### 恢复后检查
|
||||
|
||||
1. Web 登录正常
|
||||
2. **交易记录**、**统计** 页数据完整
|
||||
3. **系统设置** 中 CTP、资金等配置仍在
|
||||
4. 连接 CTP,持仓页刷新正常
|
||||
5. `pm2 logs qihuo` 无持续数据库报错
|
||||
|
||||
---
|
||||
|
||||
## 回退到 SQLite
|
||||
|
||||
1. `pm2 stop qihuo`
|
||||
2. 注释或删除 `.env` 中 `DATABASE_URL`
|
||||
3. 确保 `/opt/qihuo/futures.db` 存在(可用迁移前备份 `futures.db.pre_pg.*`)
|
||||
4. `pm2 restart qihuo`
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
| 现象 | 可能原因 | 处理 |
|
||||
|------|----------|------|
|
||||
| `未安装 psycopg` | 未 pip install | `pip install -r requirements.txt` |
|
||||
| `pg_dump 失败` | 未装客户端 / URL 错误 | `apt install postgresql-client`;检查 `DATABASE_URL` |
|
||||
| 迁移后缺表 | 未 init_db | `python3 -c "from app import init_db; init_db()"` 后重跑迁移 |
|
||||
| 登录失败 | 只恢复了 SQL 未恢复 settings | 检查 `settings` 表是否有 `admin_password_hash` |
|
||||
| 连接拒绝 | PostgreSQL 未启动 | `systemctl status postgresql` |
|
||||
| 仍见 locked | 未切到 PG,仍用 SQLite | `grep DATABASE_URL /opt/qihuo/.env`;`pm2 restart --update-env` |
|
||||
|
||||
### 验证当前后端
|
||||
|
||||
```bash
|
||||
cd /opt/qihuo && source venv/bin/activate
|
||||
set -a && source .env && set +a
|
||||
python3 -c "from db_conn import database_label, db_backend; print(db_backend(), database_label())"
|
||||
```
|
||||
|
||||
应输出 `postgres PostgreSQL (...)`。
|
||||
|
||||
### 查看 PostgreSQL 连接
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql -d qihuo -c "SELECT count(*) FROM pg_stat_activity WHERE datname='qihuo';"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安全建议
|
||||
|
||||
- `DATABASE_URL` 含密码,勿提交到 git;`.env` 权限建议 `chmod 600`
|
||||
- 备份包、`postgres_dump.sql` 含交易与账号数据,勿上传公开网盘
|
||||
- 生产库仅监听 `127.0.0.1`,不暴露 5432 到公网
|
||||
- 定期测试 **备份 → 解压 → restore.sh → 登录** 全流程
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [DEPLOY.md](./DEPLOY.md) — 应用一键部署
|
||||
- [BACKUP.md](./BACKUP.md) — 备份策略与设置页说明
|
||||
- [FEATURES.md](./FEATURES.md) — 功能与数据表概览
|
||||
@@ -0,0 +1,85 @@
|
||||
# 交易记录与复盘
|
||||
|
||||
**页面路径**:`/records`(`/trades` 重定向)
|
||||
|
||||
**相关文件**:`app.py`、`ctp_trade_sync.py`、`sl_tp_guard.py`(write_trade_log)
|
||||
|
||||
---
|
||||
|
||||
## 功能结构
|
||||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| 资金曲线 | Lightweight Charts,随主题变色 |
|
||||
| 交易记录 | `trade_logs` 主表 |
|
||||
| 复盘表单 | 手动复盘 + 截图 + 自动 K 线图 |
|
||||
| 复盘历史 | 按日/周/月/自定义筛选 |
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
本板块 **不提供报单**。记录来源:
|
||||
|
||||
| 来源 | source / monitor_type | 写入方式 |
|
||||
|------|----------------------|----------|
|
||||
| 程序平仓 | 本地 | sl_tp_guard、手动平仓 API |
|
||||
| 柜台同步 | 柜台 | CTP 成交同步 `ctp_trade_sync` |
|
||||
| 开单计划 | 开单计划 | 仅 `trade_records`,非 trade_logs 主统计 |
|
||||
|
||||
### CTP 同步
|
||||
|
||||
打开页面且 CTP 已连接时,自动拉取柜台成交写入 `trade_logs`(去重键 `ctp_trade_key`)。
|
||||
|
||||
### 手动编辑
|
||||
|
||||
「修改/核对开关」开启后可编辑字段并「核对修改」。
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
### 复盘与账户冻结
|
||||
|
||||
提交复盘表单时若勾选 **情绪问题选项**(怕踏空、报复开仓等),可能触发:
|
||||
|
||||
- **日冻结** — 当日禁止新开仓([RISK.md](./RISK.md))
|
||||
- 手动平仓超限后的冷静期可因填写日记缩短为 **1 小时**
|
||||
|
||||
### 记录本身
|
||||
|
||||
交易记录页不改变持仓;删除/编辑仅影响本地数据库。
|
||||
|
||||
---
|
||||
|
||||
## 平仓后的联动
|
||||
|
||||
`trade_logs` 新增平仓记录时:
|
||||
|
||||
1. **微信**:结构化平仓 [WECHAT §4](./WECHAT.md#4-平仓完成)
|
||||
2. **AI**:后台平仓分析 → `/ai` 页面([AI.md](./AI.md))
|
||||
|
||||
---
|
||||
|
||||
## 主要字段
|
||||
|
||||
品种、类型(monitor_type)、方向、开仓/平仓价、止损/止盈、手数、保证金、盈亏、手续费、净盈亏、最新资金、结果、持仓时长。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
| 事件 | 模板 |
|
||||
|------|------|
|
||||
| 程序/同步平仓 | [WECHAT §4](./WECHAT.md#4-平仓完成) |
|
||||
|
||||
开单计划结果见 [PLANS.md](./PLANS.md)(§10 简版模板)。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [STATS.md](./STATS.md) — 统计来源 trade_logs
|
||||
- [RISK.md](./RISK.md) — 复盘触发冻结
|
||||
- [WECHAT.md](./WECHAT.md)
|
||||
- [AI.md](./AI.md)
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
# 全局账户风控
|
||||
|
||||
本文汇总 **所有板块共用** 的风控规则。各板块下单前均会调用 `assert_can_open()` 或等价校验。
|
||||
|
||||
相关代码:`risk/account_risk_lib.py`、`position_sizing.py`、`product_recommend.py`。
|
||||
|
||||
---
|
||||
|
||||
## 日亏损风控(强平线)
|
||||
|
||||
| 项 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `daily_loss_force_close_pct` | 2 | 系统设置:当日亏损(已实现+浮亏)占 **权益** 比例;**≥ 即强制平掉全部持仓** 并当日禁止开仓 |
|
||||
| `daily_loss_slippage_buffer_pct` | 1 | 强平执行允许的额外滑点占权益比例;与强平线合计默认 **3%** 上限 |
|
||||
| 环境变量兜底 | `RISK_DAILY_TRADING_RISK_PCT` | 未配置系统设置时强平线可回退到此 env |
|
||||
|
||||
- 亏损口径:**当日已平仓亏损 + 当前持仓浮亏**(含隔夜跳空),除以当前 CTP 权益。
|
||||
- 达限后:后台 `daily_loss_guard` 撤平仓挂单 → 对手价 FAK 强平 → `daily_frozen` → 看板/下单页显示 **风控**,开仓按钮灰色。
|
||||
- 与单笔止损关系:止损为常规退出;日亏损线为账户级熔断。
|
||||
|
||||
---
|
||||
|
||||
| 项 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `MAX_ACTIVE_POSITIONS` | 1 | 同时 **active** 持仓监控数量上限 |
|
||||
| 环境变量 | `.env` | `MAX_ACTIVE_POSITIONS=1` |
|
||||
|
||||
- 达到上限后:**禁止新开仓**,但允许 **滚仓/顺势加仓**(`can_roll=True`)。
|
||||
- 状态标签:**仓位上限冻结**。
|
||||
|
||||
---
|
||||
|
||||
## 冷静期与日冻结
|
||||
|
||||
| 项 | 默认值 | 说明 |
|
||||
|----|--------|------|
|
||||
| `RISK_CONTROL_ENABLED` | true | 关闭后跳过冷静期逻辑 |
|
||||
| `RISK_MANUAL_CLOSE_DAILY_LIMIT` | 2 | 当日 **手动平仓** 次数上限 |
|
||||
| `RISK_COOLING_HOURS_MANUAL` | 4 | 超限后冷静期(小时) |
|
||||
| `RISK_COOLING_HOURS_MANUAL_JOURNAL` | 1 | 填写复盘情绪日记后缩短为 1 小时 |
|
||||
| `TRADING_DAY_RESET_HOUR` | 8 | 交易日重置时刻(8 点前算上一交易日) |
|
||||
|
||||
### 触发流程
|
||||
|
||||
1. 用户在下单监控 **手动平仓**,且当日手动平仓次数 ≥ 上限。
|
||||
2. 进入 **4h 冻结**(或填写复盘表单中的情绪问题选项后 → **1h 冻结**)。
|
||||
3. 冻结期间 `can_trade=False`,禁止一切新开仓。
|
||||
|
||||
### 日冻结
|
||||
|
||||
- 复盘表单勾选情绪问题(怕踏空、报复开仓、盈利飘了、拿不住单、扛单、重仓违规等)并提交后,可触发 **当日日冻结**。
|
||||
- 日冻结期间禁止新开仓,次日(按 `TRADING_DAY_RESET_HOUR`)重置。
|
||||
|
||||
---
|
||||
|
||||
## 保证金占用上限
|
||||
|
||||
| 项 | 配置位置 | 默认值 | 用途 |
|
||||
|----|----------|--------|------|
|
||||
| 单仓保证金上限 | 系统设置 `max_margin_pct` | 30% | **新开仓**:拟开 + 已有占用,占权益不得超过此值 |
|
||||
| 综合保证金上限 | 系统设置 `roll_max_margin_pct` | 50% | **单仓模式**:滚仓/加仓合计上限;**多仓模式**:所有持仓合计上限 |
|
||||
|
||||
- 看板 **综合保证金占比** 的分母为 **50%(综合上限)**,不是 30%。详见 [风控说明.md](./风控说明.md#保证金占比核心规则)。
|
||||
- 新开仓前仍按 30% 收紧手数;滚仓/多仓合计按 50% 校验(见 [STRATEGY.md](./STRATEGY.md))。
|
||||
|
||||
---
|
||||
|
||||
## 计仓与单笔手数
|
||||
|
||||
| 模式 | 说明 |
|
||||
|------|------|
|
||||
| 固定手数 | 使用系统设置 `fixed_lots` |
|
||||
| 固定金额(以损定仓) | 须填止损价;手数 = 固定风险金额 ÷ 每手风险;同时受保证金上限约束 |
|
||||
| 单笔上限 | `DEFAULT_MAX_ORDER_LOTS = 50` |
|
||||
|
||||
固定金额模式公式(简化):
|
||||
|
||||
```
|
||||
每手风险 = |入场价 − 止损价| × 合约乘数
|
||||
手数 = floor(固定风险金额 / 每手风险)
|
||||
再按 max_margin_pct 收紧
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 可交易品种范围
|
||||
|
||||
| 条件 | 规则 |
|
||||
|------|------|
|
||||
| 权益 ≤ 20 万 **或** CTP 未连接 | 仅可交易:**玉米、豆粕、甲醇、螺纹钢** |
|
||||
| CTP 未连接时估算 | 可开仓表按 **10 万权益** 估算最大手数 |
|
||||
| 夜盘时段 | 可开仓表仅显示有夜盘品种 |
|
||||
|
||||
详见 [TRADING.md](./TRADING.md)、[ORDER_MONITOR.md](./ORDER_MONITOR.md)。
|
||||
|
||||
---
|
||||
|
||||
## 交易时段
|
||||
|
||||
- 非交易时段:**禁止报单**(开仓/平仓 API 返回 403)。
|
||||
- CTP 未连接:禁止下单;连接中提示稍候。
|
||||
|
||||
---
|
||||
|
||||
## 各板块风控对照
|
||||
|
||||
| 板块 | 开仓前校验 | 特殊规则 |
|
||||
|------|------------|----------|
|
||||
| 下单监控 | 全部全局规则 + 保证金 + 品种 | 移动保本须填止损 |
|
||||
| 关键位自动单 | 同上 + 交易时段 + CTP | 手数按 risk_percent 计算 |
|
||||
| 策略首仓 | 同上 | 趋势回调计划单独表 |
|
||||
| 滚仓/加仓 | 仓位上限冻结时仍可滚仓 | 滚仓保证金单独上限 |
|
||||
| 开单计划 | **不自动下单**,仅提醒 | 无 assert_can_open |
|
||||
| 关键支阻区 | 不自动下单 | 仅微信提醒 |
|
||||
|
||||
---
|
||||
|
||||
## 风险状态展示
|
||||
|
||||
下单监控顶栏、**数据看板 → 风控说明** 均展示当前状态。看板各指标释义与颜色见 [风控说明.md](./风控说明.md)。
|
||||
|
||||
| 状态 | 含义 |
|
||||
|------|------|
|
||||
| 正常 | 可新开仓 |
|
||||
| 1h / 4h 冻结 | 冷静期中 |
|
||||
| 日冻结 | 当日禁止新开仓 |
|
||||
| 仓位上限冻结 | 已达 active 上限,可滚仓不可开新仓 |
|
||||
@@ -0,0 +1,81 @@
|
||||
# 系统设置
|
||||
|
||||
**页面路径**:`/settings`
|
||||
|
||||
**相关文件**:`app.py`、`install_trading.py`、`nav_settings.py`
|
||||
|
||||
---
|
||||
|
||||
## 配置分区
|
||||
|
||||
| 分区 | 主要项 | 影响板块 |
|
||||
|------|--------|----------|
|
||||
| 导航显示 | 策略、计划、行情、手续费、AI 等开关 | 全部可关闭菜单 |
|
||||
| 交易模式 | SimNow / 实盘 CTP | 下单、策略、同步 |
|
||||
| 计仓与风险 | 固定手数/固定金额、risk_percent、max_margin_pct、roll_max_margin_pct、日亏损强平线 | [ORDER_MONITOR](./ORDER_MONITOR.md)、[STRATEGY](./STRATEGY.md) |
|
||||
| 移动保本 | trailing_be_tick_buffer | 下单、关键位自动单 |
|
||||
| 挂单超时 | pending_order_timeout_sec | 下单监控 pending |
|
||||
| CTP 连接 | 前置、账号(可覆盖 .env) | 全部交易 |
|
||||
| 参考资金 | CTP 未连接时权益估算 | 可开仓品种表 |
|
||||
| 企业微信 | wechat_webhook | [WECHAT.md](./WECHAT.md) 全部推送 |
|
||||
| AI 分析 | 见 [AI.md](./AI.md) | /ai 页面 |
|
||||
| 登录账号 | 用户名/密码 → .env | 登录 |
|
||||
| 备份恢复 | 见 [BACKUP.md](./BACKUP.md) | 数据库 |
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
设置页 **不直接下单**。修改项在下次 API 调用时生效;部分 CTP 配置需重连。
|
||||
|
||||
---
|
||||
|
||||
## 风控相关默认值
|
||||
|
||||
| 设置键 | 默认 | 说明 |
|
||||
|--------|------|------|
|
||||
| risk_percent | 1 | 单笔风险占权益 % |
|
||||
| max_margin_pct | 30 | 新开仓保证金上限 |
|
||||
| roll_max_margin_pct | 单独 | 滚仓保证金上限 |
|
||||
| daily_loss_force_close_pct | 2 | 日亏损强平线(%权益) |
|
||||
| daily_loss_slippage_buffer_pct | 1 | 强平滑点预留(%权益),与强平线合计默认 3% |
|
||||
| fixed_lots / fixed_amount | — | 计仓模式 |
|
||||
| trailing_be_tick_buffer | 2 | 移动保本 1R 缓冲跳数 |
|
||||
|
||||
环境变量风控(冷静期、仓位上限)见 [RISK.md](./RISK.md),通常在 `.env` 配置。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
配置 **企业微信 Webhook** 后,以下模块推送生效:
|
||||
|
||||
- 开单计划、关键位、下单、策略、AI 日终(见 [WECHAT.md](./WECHAT.md) 索引)
|
||||
|
||||
未配置 Webhook:所有 `send_wechat_msg()` 静默跳过。
|
||||
|
||||
---
|
||||
|
||||
## 导航开关
|
||||
|
||||
`nav_settings.py` 中 `NAV_TOGGLES` 控制菜单可见性;`@require_nav` 保护路由。
|
||||
|
||||
| 键 | 菜单 |
|
||||
|----|------|
|
||||
| strategy | 策略交易 |
|
||||
| plans | 开单计划 |
|
||||
| market | 行情 K 线 |
|
||||
| fees | 手续费配置 |
|
||||
| ai | AI 分析 |
|
||||
|
||||
下单监控、关键位、记录、统计、设置 **不可关闭**。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [INDEX.md](./INDEX.md)
|
||||
- [RISK.md](./RISK.md)
|
||||
- [WECHAT.md](./WECHAT.md)
|
||||
- [AI.md](./AI.md)
|
||||
- [BACKUP.md](./BACKUP.md)
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
# 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** 使用同一套报单代码路径(见 [CTP_LIVE.md](./CTP_LIVE.md))。
|
||||
|
||||
---
|
||||
|
||||
## 二、注册前须知
|
||||
|
||||
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` 后需重启应用;也可在 **系统设置 → CTP 连接** 中维护(优先于 `.env`)。
|
||||
|
||||
```bash
|
||||
pm2 restart qihuo
|
||||
```
|
||||
|
||||
**云服务器网络说明**:`182.254.243.31` 段前置已停用(Connection refused),请勿再使用。官方前置为 `180.168.146.187:10201/10211`。若服务器 `nc -zv 180.168.146.187 10201` 超时,属于**出网/防火墙**问题,需联系云厂商放行或换能访问 SimNow 的网络,无法仅靠改代码解决。
|
||||
|
||||
旧文档备用地址(已失效,仅作排查参考):
|
||||
|
||||
```env
|
||||
# 勿用 — 已 dead
|
||||
# SIMNOW_TD_ADDRESS=tcp://182.254.243.31:30001
|
||||
```
|
||||
|
||||
### 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=实盘` |
|
||||
| **不合法的登录** | 投资者代码/密码错,或未在快期改过一次密码 |
|
||||
| **连续登录失败次数超限(75)** | 短时间失败太多次被临时封禁;等待 30~60 分钟,快期验证密码后再连,勿反复点连接 |
|
||||
| 快期能登、脚本不能 | 多为网络或前置地址,换 SimNow 官网其他组前置试 |
|
||||
| 连上后进程崩溃 `locale::facet::_S_create_c_locale` | **必须**安装 `zh_CN.GB18030`:`sed -i '/^# zh_CN.GB18030/s/^# //' /etc/locale.gen && locale-gen zh_CN.GB18030` |
|
||||
|
||||
### 提示「未安装 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、页面说明 |
|
||||
| [CTP_LIVE.md](./CTP_LIVE.md) | 期货公司实盘 CTP 与 SimNow 开平仓对比 |
|
||||
| [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** 成功
|
||||
@@ -0,0 +1,56 @@
|
||||
# 统计分析
|
||||
|
||||
**页面路径**:`/stats`
|
||||
|
||||
**相关文件**:`stats_engine.py`、`app.py`(`/api/stats`)
|
||||
|
||||
---
|
||||
|
||||
## 功能概述
|
||||
|
||||
### 汇总指标(页顶卡片)
|
||||
|
||||
总交易次数、胜率、平均盈利/亏损、盈亏比、连续亏损、最大回撤、最大盈亏金额及占比、累计手续费、情绪单数量/占比。
|
||||
|
||||
进入页面自动加载 `/api/stats`,无手动「重新计算」按钮。
|
||||
|
||||
### 分项统计
|
||||
|
||||
下拉维度:按时间、周、月、品种、手续费、方向、交易类型、情绪单等,表格展示分组指标。
|
||||
|
||||
---
|
||||
|
||||
## 下单逻辑
|
||||
|
||||
**无**。只读展示,不触发报单。
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
**无**。统计结果不影响账户状态。
|
||||
|
||||
---
|
||||
|
||||
## 数据来源
|
||||
|
||||
| 表 | 用途 |
|
||||
|----|------|
|
||||
| `trade_logs` | 主:成交、盈亏、手续费 |
|
||||
| `review_records` | 情绪单标记等 |
|
||||
| `stats_cache` | 缓存加速 |
|
||||
|
||||
开单计划的 `trade_records` **不计入** 此处主统计(除非另有合并逻辑)。
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
**无**。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [RECORDS.md](./RECORDS.md)
|
||||
- [FEES.md](./FEES.md) — 手续费计算
|
||||
@@ -0,0 +1,131 @@
|
||||
# 策略交易
|
||||
|
||||
**页面路径**:`/strategy`、`/strategy/records`
|
||||
|
||||
**相关文件**:`install_trading.py`、`strategy/strategy_roll_lib.py`、`strategy/strategy_roll_monitor_lib.py`
|
||||
|
||||
---
|
||||
|
||||
## 功能概述
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| 趋势回调 | 首仓 + 网格补仓 + 统一止盈 |
|
||||
| 顺势加仓(滚仓) | 对已有 active 持仓加仓,单独保证金上限 |
|
||||
|
||||
趋势首仓与 **市价滚仓** 须 **交易时段内** 且 **CTP 已连接**。
|
||||
**突破加仓** 可在 **休盘**(小节休息、午间、日盘收盘后)提交监控,开盘触价后由 Worker 自动市价加仓。
|
||||
|
||||
---
|
||||
|
||||
## 趋势回调 — 下单逻辑
|
||||
|
||||
### 1. 创建计划(预览 → 确认首仓)
|
||||
|
||||
用户填写:品种、方向、止损、补仓上沿、止盈、风险比例等 → 系统预览手数/网格 → 确认后 **市价开首仓**。
|
||||
|
||||
**校验**(与下单监控相同):
|
||||
|
||||
- 交易时段、CTP 连接
|
||||
- `assert_can_open()`、[RISK.md](./RISK.md)
|
||||
- 品种范围、保证金上限
|
||||
|
||||
**成交后**:
|
||||
|
||||
- 写入 `trend_pullback_plans`(status=active)
|
||||
- 创建/关联 `trade_order_monitors`(monitor_type=trend)
|
||||
- 微信:`趋势回调首仓 {sym} {first_lots}手`
|
||||
|
||||
### 2. 运行中监控
|
||||
|
||||
后台扫描 active 计划:
|
||||
|
||||
| 条件 | 动作 |
|
||||
|------|------|
|
||||
| 价格触及 **take_profit** | 全部平仓,计划结案 |
|
||||
| 价格回落至 **网格档位** | 按档位补仓(不超过 remainder_lots) |
|
||||
|
||||
**补仓**:市价加仓,更新均价与监控。
|
||||
|
||||
**止盈**:市价全平。
|
||||
|
||||
微信:
|
||||
|
||||
- 止盈 → `趋势回调止盈 {sym}`
|
||||
- 补仓 → `趋势回调补仓 {sym} +{add_lots}手 @档位{N}`
|
||||
|
||||
### 3. 策略记录
|
||||
|
||||
`/strategy/records` 单独归档已结束计划。
|
||||
|
||||
---
|
||||
|
||||
## 顺势加仓(滚仓)— 下单逻辑
|
||||
|
||||
针对 **已有 active 持仓监控** 的加仓预览与执行。须 **固定金额(以损定仓)** 模式;**移动保本** 持仓不可滚仓。
|
||||
|
||||
### 加仓方式
|
||||
|
||||
| 方式 | 提交时机 | 执行 |
|
||||
|------|----------|------|
|
||||
| **市价加仓** | 仅 **交易时段** | 预览 → 10 秒倒计时 → 立即 CTP 市价成交 |
|
||||
| **突破加仓** | **任意时间**(含休盘) | 预览 → **提交监控** → 标记价穿越突破价后 Worker 自动市价加仓 |
|
||||
|
||||
休盘提交突破加仓时,几何校验放宽为「止损 vs 突破价」关系,不强制要求实时现价;开盘后有行情后按触价逻辑成交。
|
||||
|
||||
### 特殊风控
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 仓位上限冻结 | **仍允许滚仓**(`can_roll=True`) |
|
||||
| `roll_max_margin_pct` | 滚仓后总保证金占用单独上限 |
|
||||
| 手数收紧 | `cap_lots_for_margin_budget()` 按滚仓上限裁剪 |
|
||||
|
||||
流程:
|
||||
|
||||
- **市价**:预览 → 确认 → CTP 市价加仓 → 更新 monitor
|
||||
- **突破**:预览 → 提交 pending 腿 → `check_roll_monitors`(在 `qihuo-ctp` Worker 内)触价成交
|
||||
|
||||
---
|
||||
|
||||
## 风控规则
|
||||
|
||||
| 规则 | 趋势首仓 | 市价滚仓 | 突破滚仓(pending) |
|
||||
|------|----------|----------|---------------------|
|
||||
| assert_can_open | ✓ | 仓位冻结时仍可 | 仓位冻结时仍可 |
|
||||
| max_margin_pct | ✓ 首仓 | — | — |
|
||||
| roll_max_margin_pct | — | ✓ | ✓(预览时按突破价估算) |
|
||||
| 交易时段 | ✓ | ✓ | 提交 **不要求**;成交须交易时段 |
|
||||
| CTP 连接 | ✓ | ✓ | 提交 **不要求**;触价成交须 CTP |
|
||||
| 品种范围 | ✓ | ✓ | ✓ |
|
||||
| 单笔 50 手 | ✓ | ✓ | ✓ |
|
||||
|
||||
全局规则见 [RISK.md](./RISK.md)。
|
||||
|
||||
---
|
||||
|
||||
## 止盈止损
|
||||
|
||||
趋势持仓纳入 `sl_tp_guard` 本地监控(与下单监控相同机制)。
|
||||
|
||||
- monitor_type = `trend` / `roll`
|
||||
- 平仓写入 `trade_logs`,来源标签「趋势回调」「顺势加仓」
|
||||
|
||||
---
|
||||
|
||||
## 微信推送
|
||||
|
||||
| 事件 | 模板 |
|
||||
|------|------|
|
||||
| 首仓 | [WECHAT §11](./WECHAT.md#11-策略趋势回调) |
|
||||
| 止盈 | 同上 |
|
||||
| 补仓 | 同上 |
|
||||
| 结构化平仓 | [WECHAT §4](./WECHAT.md#4-平仓完成) |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [ORDER_MONITOR.md](./ORDER_MONITOR.md)
|
||||
- [RISK.md](./RISK.md)
|
||||
- [WECHAT.md](./WECHAT.md)
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
# 下单监控与策略交易
|
||||
|
||||
## 默认首页
|
||||
|
||||
登录或访问 `/` 后进入 **下单监控**(`/positions`)。页面包含:
|
||||
|
||||
| 区域 | 说明 |
|
||||
|------|------|
|
||||
| 顶栏 | 交易模式、CTP 状态、权益/可用、连接 CTP |
|
||||
| 期货下单 | 限价/市价报单、止盈/止损、移动保本、以损定仓/固定手数 |
|
||||
| 当前持仓 | CTP 持仓卡片、挂单中、撤单、平仓 |
|
||||
| 可开仓品种 | 按权益与保证金上限筛选、行业分类、走势/跳空/成交量排序 |
|
||||
|
||||
`/trade`、`/recommend` 均重定向到 `/positions`(可开仓品种锚点 `#recommend`)。
|
||||
|
||||
## 两种交易通道
|
||||
|
||||
| 设置 | 实际连接 | 资金 |
|
||||
|------|----------|------|
|
||||
| **模拟盘** | SimNow(vnpy → CTP 仿真前置) | SimNow 账户权益 |
|
||||
| **实盘** | 期货公司 CTP(`.env` 中 `CTP_LIVE_*`) | 柜台权益 |
|
||||
|
||||
模拟盘与实盘均走 **vnpy_ctp**,无本地假撮合。
|
||||
**实盘接入与开平仓对比** → [CTP_LIVE.md](./CTP_LIVE.md) · SimNow → [SIMNOW.md](./SIMNOW.md)
|
||||
|
||||
### CTP 双进程与连接行为
|
||||
|
||||
| 项目 | 说明 |
|
||||
|------|------|
|
||||
| Web | PM2 `qihuo`,端口 6600 |
|
||||
| CTP Worker | PM2 `qihuo-ctp`,端口 6601(本机) |
|
||||
| 连接/重连 | 在 Worker 内执行;**小节休息、午间休盘** 仍保持连接(便于读缓存权益/持仓) |
|
||||
| 页面行为 | 各页 **仅轮询** CTP 状态,**不会在打开页面时自动发起连接** |
|
||||
| 权益缓存 | CTP 短暂断开时,看板/持仓可回退读最近快照 |
|
||||
|
||||
手动 **连接 CTP** 仍通过 **下单监控** 页按钮或 API;成功后顶栏显示「CTP 已连接」。
|
||||
|
||||
## 下单与持仓
|
||||
|
||||
- **限价开仓**:先显示「挂单中」,柜台成交后进入持仓监控;超时未成交自动撤单(时长见系统设置)
|
||||
- **撤单**:交易时段内可手动撤单;非交易时段按钮不可用
|
||||
- **平仓**:程序平仓写入 `trade_logs`(来源「本地」)
|
||||
- **持仓数据**:SSE `/api/trading/stream` 推送,约 1 秒刷新
|
||||
|
||||
## 可开仓品种
|
||||
|
||||
- 用于开仓纪律与仓位限制:按保证金上限计算最大手数,仅展示当前权益下可开的品种
|
||||
- 每日后台刷新列表(`/api/recommend/stream`)
|
||||
- 最大手数 = floor(权益 × 保证金上限 ÷ 1 手保证金)
|
||||
- **1 手保证金**:**CTP 已连接** 时优先读取柜台合约的 `long_margin_ratio` / `short_margin_ratio` 与乘数计算(表格标注「柜台」);未连接或合约信息暂不可用时,才用本地参考保证金率估算
|
||||
- 开仓前校验、固定金额计仓、保证金占用比例检查均与上述规则一致,避免交易所上调保证金后仍按旧比例显示可开手数
|
||||
- 展示近一周日线走势、跳空、昨日成交量(手)、成交额
|
||||
- 可按 **行业** 筛选,支持多字段排序
|
||||
- **夜盘时段**:品种下拉与可开仓表仅显示有夜盘交易的品种,并带「夜盘」标记
|
||||
|
||||
### 小账户品种范围(≤20 万)
|
||||
|
||||
权益 **不超过 20 万元** 时,系统限制可浏览、可搜索、可报单的品种为以下 **4 个**:
|
||||
|
||||
| 品种 | 代码 |
|
||||
|------|------|
|
||||
| 玉米 | `c` |
|
||||
| 豆粕 | `m` |
|
||||
| 甲醇 | `MA` |
|
||||
| 螺纹钢 | `rb` |
|
||||
|
||||
适用范围:
|
||||
|
||||
- 可开仓品种表
|
||||
- 期货下单品种联想 / 下拉
|
||||
- 开仓报单校验(含趋势策略首仓)
|
||||
|
||||
**SimNow 与实盘规则一致**:**CTP 未连接** 时,可开仓表 **当前权益固定按 10 万** 估算最大手数,并 **仅展示四品种**(玉米、豆粕、甲醇、螺纹钢);与系统设置中的参考资金无关。连接 CTP 后改用柜台权益;若柜台权益 ≤20 万,同样仅上述四品种。
|
||||
|
||||
页面会提示:「未连接 CTP,按 10 万权益估算最大手数,仅显示并可交易 …」。
|
||||
|
||||
## 期货下单 · 止盈止损与移动保本
|
||||
|
||||
本地止盈止损由程序监控持仓,触发后市价平仓(与 CTP 柜台委托/持仓数据独立)。
|
||||
|
||||
### 默认模式(移动保本关闭)
|
||||
|
||||
- 可同时填写 **止盈**、**止损**
|
||||
- 填写入场价与止损/止盈后,表单下方显示 **盈亏比**、止损金额、止盈金额(如有)
|
||||
- 本地监控:价格触及止盈或止损即平仓
|
||||
|
||||
### 移动保本(可选,默认关闭)
|
||||
|
||||
勾选 **移动保本** 后:
|
||||
|
||||
| 表单项 | 行为 |
|
||||
|--------|------|
|
||||
| 止盈 | **隐藏**,不提交止盈价 |
|
||||
| 盈亏比 / 止盈金额 | **不显示** |
|
||||
| 止损 | **必填**,仅保留止损输入 |
|
||||
|
||||
平仓逻辑:
|
||||
|
||||
- **不再** 按固定止盈价监控
|
||||
- 程序按 **移动止损** 管理出场:浮盈达 **1R** 后止损移至开仓价 ± N 跳(保本);达 **2R** 移至 1R,依次类推(N 见系统设置「移动保本缓冲」)
|
||||
- 开启移动保本 **必须填写止损价**,否则无法开仓
|
||||
|
||||
持仓卡片在开启移动保本时同样 **不展示盈亏比、盈利金额、止盈状态**,仅保留止损与移动保本进度(如已锁 N R)。
|
||||
|
||||
## 策略交易
|
||||
|
||||
| 页面 | 路径 |
|
||||
|------|------|
|
||||
| 策略交易 | `/strategy` |
|
||||
| 策略记录 | `/strategy/records` |
|
||||
|
||||
趋势回调、顺势加仓等策略需先在下单监控完成开仓,再在策略页配置。
|
||||
|
||||
## 参考资金
|
||||
|
||||
系统设置中的「参考资金」在 **CTP 未连接** 时用于以损定仓估算;连接后自动改用柜台权益。
|
||||
|
||||
可开仓品种与品种白名单:**未连接 CTP 时** 可开仓表按 **10 万权益** 估算最大手数,且仅四品种;连接后若柜台权益 ≤20 万,同样仅上述四品种。
|
||||
|
||||
## 首次使用 SimNow
|
||||
|
||||
完整步骤见 **[SimNow 注册与接入说明](./SIMNOW.md)**。
|
||||
|
||||
简要:注册 SimNow → 填写 `.env` → 安装 vnpy_ctp → 登录系统 → **下单监控** → **连接 CTP**。
|
||||
|
||||
## 主要 API
|
||||
|
||||
| 接口 | 说明 |
|
||||
|------|------|
|
||||
| `POST /api/ctp/connect` | 连接 SimNow 或实盘 CTP |
|
||||
| `GET /api/ctp/status` | 连接状态 |
|
||||
| `POST /api/trade/order` | 报单(需已连接 CTP) |
|
||||
| `POST /api/trading/order/cancel` | 撤单(交易时段) |
|
||||
| `POST /api/trading/close` | 平仓 |
|
||||
| `GET /api/trading/stream` | 持仓 SSE |
|
||||
| `GET /api/recommend/list` | 可开仓品种 JSON |
|
||||
| `GET /api/recommend/stream` | 可开仓品种 SSE |
|
||||
| `POST /api/strategy/trend/execute` | 执行趋势策略 |
|
||||
|
||||
详见 [DEPLOY.md](./DEPLOY.md) 中 CTP 故障排查。
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
# 企业微信推送
|
||||
|
||||
**配置**:系统设置 → 企业微信 Webhook(`wechat_webhook`)。
|
||||
|
||||
**发送函数**:`app.send_wechat_msg()` — 所有推送正文前自动加前缀:
|
||||
|
||||
```
|
||||
【国内期货】
|
||||
{正文}
|
||||
```
|
||||
|
||||
未配置 Webhook 时静默跳过,不报错。
|
||||
|
||||
---
|
||||
|
||||
## 推送类型索引
|
||||
|
||||
| # | 类型 | 触发场景 | 模板章节 |
|
||||
|---|------|----------|----------|
|
||||
| 1 | 手动开仓成功(结构化) | 下单监控成交且填写止损 | [§1](#1-手动开仓成功) |
|
||||
| 2 | 手动开仓(简版) | 成交但未填止损 | [§2](#2-简版开仓) |
|
||||
| 3 | 挂单提交 | 限价单未立即成交 | [§3](#3-挂单提交) |
|
||||
| 4 | 平仓完成(结构化) | trade_logs 写入平仓记录 | [§4](#4-平仓完成) |
|
||||
| 5 | SL/TP 触发(简版) | 本地止盈止损市价平仓委托已提交 | [§5](#5-本地止盈止损触发) |
|
||||
| 6 | 关键位自动单结果 | 箱体/收敛突破尝试下单 | [§6](#6-关键位自动单) |
|
||||
| 7 | 关键位开仓成功(结构化) | 自动单成交 | [§7](#7-关键位开仓成功) |
|
||||
| 8 | 关键支阻区提醒 | 5m 收盘突破区间 | [§8](#8-关键支阻区提醒) |
|
||||
| 9 | 开单计划触发 | 现价进入决策区间 | [§9](#9-开单计划) |
|
||||
| 10 | 开单计划止盈/止损 | 激活后价格触及 TP/SL | [§10](#10-开单计划结果) |
|
||||
| 11 | 策略趋势回调 | 首仓/止盈/补仓 | [§11](#11-策略趋势回调) |
|
||||
| 12 | AI 分析 | 开仓/平仓/日终报告 | [§12](#12-ai-分析) |
|
||||
|
||||
实现文件:`wechat_notify.py`、`trade_notify.py`、`key_monitor_lib.py`、`app.py`、`install_trading.py`、`ai_worker.py`。
|
||||
|
||||
---
|
||||
|
||||
## 结构化推送
|
||||
|
||||
以下模板由 `wechat_notify.py` 生成,字段随实际成交数据填充。
|
||||
|
||||
### 1. 手动开仓成功
|
||||
|
||||
**函数**:`format_open_success()` · **来源**:`trade_notify.notify_manual_open_filled()`
|
||||
|
||||
**触发**:下单监控开仓 **成交** 且填写了 **止损价**。
|
||||
|
||||
**模板**:
|
||||
|
||||
```
|
||||
📈 {品种名} 开仓成功
|
||||
💼 账户:{模拟盘/实盘}
|
||||
|
||||
🧾 订单基础信息
|
||||
📌 来源:期货下单
|
||||
🔖 委托号:{order_id}
|
||||
📈 方向:多头(long)/ 空头(short)
|
||||
⚠ 单笔风控:{risk_percent}%≈{risk_amount}元
|
||||
|
||||
📊 仓位配置
|
||||
账户权益:{capital} 元
|
||||
开仓手数:{lots} 手
|
||||
占用保证金:{margin} 元
|
||||
仓位占比:{margin_pct}%
|
||||
|
||||
🎯 价位 & 盈亏比
|
||||
开仓价:{entry}
|
||||
止损价:{stop_loss}
|
||||
止盈价:{take_profit}
|
||||
计划盈亏比:RR {rr} : 1
|
||||
移动保本:1.0R → {be_px}(缓冲 {be_tick_buffer} 跳) ← 仅开启移动保本时
|
||||
|
||||
📌 状态
|
||||
✅ 已进入下单监控,本地 SL/TP 守护
|
||||
```
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
📈 螺纹钢 开仓成功
|
||||
💼 账户:SimNow 模拟
|
||||
|
||||
🧾 订单基础信息
|
||||
📌 来源:期货下单
|
||||
📈 方向:多头(long)
|
||||
⚠ 单笔风控:1%≈1000.00元
|
||||
|
||||
📊 仓位配置
|
||||
账户权益:100000.00 元
|
||||
开仓手数:2 手
|
||||
占用保证金:8500.00 元
|
||||
仓位占比:8.50%
|
||||
|
||||
🎯 价位 & 盈亏比
|
||||
开仓价:3200
|
||||
止损价:3180
|
||||
止盈价:3240
|
||||
计划盈亏比:RR 2 : 1
|
||||
|
||||
📌 状态
|
||||
✅ 已进入下单监控,本地 SL/TP 守护
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 平仓完成
|
||||
|
||||
**函数**:`format_close_done()` · **来源**:`trade_notify.notify_trade_log_close()`
|
||||
|
||||
**触发**:`trade_logs` 新增平仓记录(SL/TP 守护、CTP 同步、手动平仓等)。
|
||||
|
||||
**模板**:
|
||||
|
||||
```
|
||||
📈/📉 {品种名} 平仓完成
|
||||
💼 账户:{mode_label}
|
||||
|
||||
🧾 平仓概要
|
||||
📌 方向:多头(long)/ 空头(short)
|
||||
📌 平仓结果:{止盈|止损|保本止盈|移动止盈|手动平仓}
|
||||
💰 本单净盈亏:+/-{pnl_net} 元
|
||||
⏱ 持仓时长:{N分钟|N小时N分钟|N天…}
|
||||
💵 账户权益:{equity_after} 元
|
||||
|
||||
🎯 价位(计划)
|
||||
开仓价:{entry}
|
||||
平仓价:{close_price}
|
||||
止盈价:{take_profit}
|
||||
止损价:{stop_loss}
|
||||
|
||||
📎 备注
|
||||
成交价不在计划止盈/止损带内(可能为手动或其他类型平仓) ← 可选
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 7. 关键位开仓成功
|
||||
|
||||
**函数**:`format_key_open_success()` — 在 §1 基础上追加:
|
||||
|
||||
```
|
||||
📎 关键位触发
|
||||
类型:{箱体突破|收敛突破}
|
||||
模式:{顺势|反转} · {向上突破|向下突破}
|
||||
5m 收盘:{bar_time}
|
||||
```
|
||||
|
||||
**来源字段**:`{monitor_type}·{trade_mode}`(如「箱体突破·顺势」)。
|
||||
|
||||
---
|
||||
|
||||
### 8. 关键支阻区提醒
|
||||
|
||||
**函数**:`format_zone_alert()` · **最多 3 次**,间隔约 **5 分钟**。
|
||||
|
||||
**模板**:
|
||||
|
||||
```
|
||||
📌 {品种名} 关键位突破提醒({1|2|3}/3)
|
||||
|
||||
🧾 突破概要
|
||||
📌 类型:关键支阻区
|
||||
⏱ 触发时间:{bar_time}
|
||||
📊 上沿:{upper}|下沿:{lower}
|
||||
💹 触发收盘:{close_price}
|
||||
🎯 {向上突破/向下突破}({多头/空头})
|
||||
📍 突破价位:{boundary}
|
||||
|
||||
📎 说明
|
||||
· 人工盯盘,共推送 3 次(间隔约 5 分钟)
|
||||
· 推送完毕后本条监控自动结案
|
||||
· 不参与自动开仓
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 简版与业务推送
|
||||
|
||||
### 2. 简版开仓
|
||||
|
||||
**触发**:成交但未填止损。
|
||||
|
||||
```
|
||||
{模拟盘/实盘} 开仓 {symbol} {direction} {lots}手 @{price}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 挂单提交
|
||||
|
||||
**触发**:限价开仓委托已提交、尚未成交。
|
||||
|
||||
```
|
||||
委托已提交 · {symbol} {direction} {lots}手挂单中({N} 分钟未成交自动撤单)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 本地止盈止损触发
|
||||
|
||||
**触发**:`sl_tp_guard` 本地检测到 TP/SL,已提交市价平仓委托(结构化平仓推送在成交写入 trade_logs 后另行发送)。
|
||||
|
||||
```
|
||||
{止盈|止损} {symbol} {direction} {lots}手 @{mark},平仓委托已提交
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 关键位自动单
|
||||
|
||||
**函数**:`format_auto_breakout_msg()` · **成败均推送**。
|
||||
|
||||
**模板**:
|
||||
|
||||
```
|
||||
✅/❌ {品种名} {箱体突破|收敛突破}自动单
|
||||
⏱ 5m 收盘:{bar_time}
|
||||
🎯 {向上突破|向下突破} · {顺势|反转} · {做多|做空}
|
||||
💹 入场:{entry} 止损:{sl} 止盈:{tp}(盈亏比 {rr})
|
||||
📦 手数:{lots}
|
||||
🛡 已开启移动保本(达目标盈亏比自动止盈) ← 可选
|
||||
{detail} ← 失败原因等
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 开单计划
|
||||
|
||||
**触发**:`planned` → 现价进入 `[zone_lower, zone_upper]`。
|
||||
|
||||
```
|
||||
【开单计划触发】{name} ({symbol})
|
||||
方向:做多/做空
|
||||
决策区间:{zone_lower} ~ {zone_upper}
|
||||
决策理由:{reason}
|
||||
当前价:{p}
|
||||
止损:{stop_loss} 止盈:{take_profit}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. 开单计划结果
|
||||
|
||||
**触发**:`active` 状态下触及止盈或止损。
|
||||
|
||||
```
|
||||
[做多/做空] {name} 已{止盈|止损}
|
||||
决策区间:{zone_lower} ~ {zone_upper}
|
||||
止损:{stop_loss} 止盈:{take_profit}
|
||||
当前价:{p}
|
||||
```
|
||||
|
||||
> 开单计划 **不自动下单**,仅微信提醒并写入 `trade_records`。
|
||||
|
||||
---
|
||||
|
||||
### 11. 策略趋势回调
|
||||
|
||||
| 事件 | 模板 |
|
||||
|------|------|
|
||||
| 首仓成交 | `趋势回调首仓 {sym} {first_lots}手` |
|
||||
| 止盈平仓 | `趋势回调止盈 {sym}` |
|
||||
| 补仓成交 | `趋势回调补仓 {sym} +{add_lots}手 @档位{done+1}` |
|
||||
|
||||
---
|
||||
|
||||
### 12. AI 分析
|
||||
|
||||
详见 [AI.md#微信推送](./AI.md#微信推送)。
|
||||
|
||||
**事件分析**(开仓/平仓后,AI 成功时):
|
||||
|
||||
```
|
||||
🤖 AI 分析 · {title}
|
||||
|
||||
{content 前 1800 字}
|
||||
```
|
||||
|
||||
**日终报告**:
|
||||
|
||||
```
|
||||
🤖 {YYYY-MM-DD} 日终持仓与交易报告
|
||||
|
||||
{content 前 1800 字}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 推送与板块对照
|
||||
|
||||
| 板块 | 推送类型 |
|
||||
|------|----------|
|
||||
| 下单监控 | §1 §2 §3 §4 §5 §12 |
|
||||
| 关键位监控 | §6 §7 §8 |
|
||||
| 开单计划 | §9 §10 |
|
||||
| 策略交易 | §11 §4 |
|
||||
| AI 分析 | §12 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有消息经 `【国内期货】` 前缀发送,企业微信群机器人 `msgtype=text`。
|
||||
2. 结构化开仓/平仓需 **填写止损** 才会发送完整模板;否则为简版。
|
||||
3. AI 推送仅在 AI 调用 **成功** 时发送;失败内容只写入 `/ai` 页面。
|
||||
4. 关键支阻区第 3 次推送后监控自动归档,不再推送。
|
||||
@@ -0,0 +1,185 @@
|
||||
# 软件购买与使用协议(个人版)
|
||||
|
||||
> **说明**:本协议为个人购买者使用模板。正式交付时可打印或转为 PDF,由双方签字/确认。
|
||||
> 本模板不构成法律意见;金额较大或机构/共享交易室合作,建议由执业律师审阅后使用。
|
||||
|
||||
---
|
||||
|
||||
**协议编号**:_______________
|
||||
**签订日期**:_______________
|
||||
|
||||
---
|
||||
|
||||
## 甲方(著作权人 / 许可方)
|
||||
|
||||
- **姓名**:马建军
|
||||
- **联系电话**:18364911125
|
||||
- **微信**:dekun03
|
||||
|
||||
## 乙方(被许可方 / 购买方)
|
||||
|
||||
- **姓名**:_______________
|
||||
- **联系电话**:_______________
|
||||
- **微信/邮箱**:_______________
|
||||
|
||||
---
|
||||
|
||||
## 第一条 软件与交付内容
|
||||
|
||||
1.1 甲方向乙方提供的软件名称为 **「国内期货 · 交易复盘系统」**(以下简称「本软件」),包括甲方交付时约定版本的源代码、部署说明及必要配置指导。
|
||||
|
||||
1.2 **交付方式**(勾选适用项):
|
||||
|
||||
- [ ] 部署服务:甲方协助乙方在乙方指定服务器完成安装与基础配置
|
||||
- [ ] 源代码:甲方提供约定版本源代码(Git 归档 / 压缩包 / 私有仓库只读权限,择一填写:_______________)
|
||||
- [ ] 其他:_______________
|
||||
|
||||
1.3 **交付版本标识**(建议填写 Git 提交号或日期):_______________
|
||||
|
||||
---
|
||||
|
||||
## 第二条 授权范围
|
||||
|
||||
2.1 甲方授予乙方 **非独占、不可转让、不可再许可** 的个人使用许可。
|
||||
|
||||
2.2 乙方仅可将本软件部署在 **乙方本人名下单一实例**(一台 VPS 或一台个人电脑服务器,二选一或填写:_______________),供 **乙方本人** 用于个人期货交易的纪律管理、记录与复盘。
|
||||
|
||||
2.3 本授权 **不包括** 以下权利(须另行书面协议并支付费用):
|
||||
|
||||
- 共享交易室、培训室、跟单室等多人共用或对外经营
|
||||
- 白标、OEM、二次分发、转售源码
|
||||
- 将本软件作为带单、荐品种、配资等业务的工具或平台
|
||||
|
||||
---
|
||||
|
||||
## 第三条 严禁用途(乙方承诺)
|
||||
|
||||
乙方承诺 **不得** 利用本软件从事以下行为:
|
||||
|
||||
1. **带单、代客理财、代客下单、信号群喊单、跟单服务** 等可能违反期货监管及咨询资质要求的行为;
|
||||
2. **向他人推荐、介绍特定期货品种、合约或具体买卖方向**,并以此向他人收费或获利;
|
||||
3. **融资、配资、分仓、对赌、非法吸收资金** 等资金融通或变相配资行为;
|
||||
4. **复制、传播、转售、出租、出借** 源代码或部署包给任何第三方;
|
||||
5. **删除、篡改** 软件内或文档中的版权声明与许可说明;
|
||||
6. 其他违反中国法律法规及期货监管规定的行为。
|
||||
|
||||
乙方违反本条,甲方有权 **立即终止许可**;乙方已付费用 **不予退还**(法律另有强制性规定的除外)。因乙方违规导致甲方损失的,乙方应依法赔偿。
|
||||
|
||||
---
|
||||
|
||||
## 第四条 费用与支付
|
||||
|
||||
4.1 乙方应向甲方支付:
|
||||
|
||||
| 项目 | 金额(元) | 备注 |
|
||||
|------|------------|------|
|
||||
| 部署服务费 | | |
|
||||
| 源代码许可费 | | |
|
||||
| 其他 | | |
|
||||
| **合计** | | |
|
||||
|
||||
4.2 支付方式:_______________
|
||||
4.3 甲方收到约定款项后 ___ 个工作日内完成交付(或双方另行约定)。
|
||||
|
||||
---
|
||||
|
||||
## 第五条 更新、维护与支持
|
||||
|
||||
5.1 **版本更新**(勾选):
|
||||
|
||||
- [ ] 本次交付为固定版本,后续大版本更新需 **另行付费**
|
||||
- [ ] 含 ___ 个月内的缺陷修复与小版本更新(不含新功能模块)
|
||||
- [ ] 其他:_______________
|
||||
|
||||
5.2 支持方式与范围:_______________(如:微信答疑、远程协助次数等)。
|
||||
5.3 超出约定范围的支持,双方可 **另行协商费用**。
|
||||
|
||||
---
|
||||
|
||||
## 第六条 知识产权
|
||||
|
||||
6.1 本软件之著作权及其他知识产权 **均归甲方所有**。乙方仅获得本协议第二条约定之 **有限使用权**,不取得著作权转让或共有。
|
||||
|
||||
6.2 乙方可在本协议授权范围内备份源代码供 **自用**,不得用于再分发。
|
||||
|
||||
---
|
||||
|
||||
## 第七条 免责声明与风险提示
|
||||
|
||||
7.1 本软件为 **交易纪律与记录辅助工具**,不提供投资咨询,不构成任何 **投资建议、收益承诺或交易信号**。
|
||||
|
||||
7.2 **期货交易风险极大**,乙方须具备相应风险承受能力,独立作出交易决策,盈亏由乙方 **自行承担**。
|
||||
|
||||
7.3 因 CTP/SimNow/网络/服务器/第三方接口故障、断线、延迟等导致的数据偏差、下单失败或损失,甲方在已尽合理交付与说明义务的前提下, **不承担** 由此产生的交易损失(法律强制性规定除外)。
|
||||
|
||||
7.4 甲方不保证软件持续符合某一交易所或期货公司的全部最新规则;监管或接口变化时,乙方应配合升级或调整配置。
|
||||
|
||||
---
|
||||
|
||||
## 第八条 责任限制
|
||||
|
||||
8.1 除因甲方 **故意或重大过失** 直接导致乙方人身或财产损害的情形外,甲方对乙方因使用或无法使用本软件产生的 **间接损失、交易亏损、数据丢失、业务中断** 等不承担责任。
|
||||
|
||||
8.2 在任何情况下,甲方对乙方的 **累计赔偿责任** 不超过乙方就本协议 **实际已支付给甲方的费用总额**(法律强制性规定除外)。
|
||||
|
||||
---
|
||||
|
||||
## 第九条 保密
|
||||
|
||||
9.1 乙方对交付的 **未公开源代码、部署文档、配置信息** 负有保密义务,不得向无关第三方披露,法律法规或监管要求除外。
|
||||
|
||||
9.2 保密期限:许可终止后 **三(3)年** 内仍有效(源代码本身仍不得非法传播)。
|
||||
|
||||
---
|
||||
|
||||
## 第十条 协议期限与终止
|
||||
|
||||
10.1 本协议自双方签字/确认之日起生效。个人使用许可为 **长期有效**,直至依本条终止。
|
||||
|
||||
10.2 有下列情形之一的,甲方有权终止许可,乙方应停止使用并销毁多余副本(保留一份备份法律允许的范围内自用备份除外):
|
||||
|
||||
- 乙方违反第三条严禁用途或第二条授权范围;
|
||||
- 乙方非法转售、传播源码;
|
||||
- 乙方从事违法经营活动并使用本软件。
|
||||
|
||||
10.3 终止后,乙方 **不得** 继续使用本软件开展新业务;已产生的法律责任不因终止而免除。
|
||||
|
||||
---
|
||||
|
||||
## 第十一条 争议解决
|
||||
|
||||
11.1 本协议之订立、效力、解释、履行及争议解决均适用 **中华人民共和国法律**。
|
||||
|
||||
11.2 双方因本协议发生争议,应先友好协商;协商不成的,任一方可向 **甲方住所地有管辖权的人民法院** 提起诉讼。
|
||||
|
||||
---
|
||||
|
||||
## 第十二条 其他
|
||||
|
||||
12.1 本协议与仓库根目录 `LICENSE.zh-CN.txt` 内容不一致的, **以本协议为准**(仅针对甲乙双方之间)。
|
||||
|
||||
12.2 本协议一式两份,甲乙双方各执一份,具有同等效力(电子确认、微信确认截图与纸质同等有效,双方认可时)。
|
||||
|
||||
12.3 未尽事宜,双方可签订 **补充协议**;补充协议与本协议具有同等效力。
|
||||
|
||||
---
|
||||
|
||||
## 签署栏
|
||||
|
||||
**甲方(许可方)**
|
||||
|
||||
签名:_______________
|
||||
日期:_______________
|
||||
|
||||
**乙方(被许可方)**
|
||||
|
||||
签名:_______________
|
||||
日期:_______________
|
||||
|
||||
---
|
||||
|
||||
## 附件(可选)
|
||||
|
||||
- [ ] 交付清单(版本号、文件列表、服务器信息)
|
||||
- [ ] 部署完成确认单
|
||||
- [ ] 乙方身份证复印件(线下签约时)
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
# 风控说明
|
||||
|
||||
**页面**:`/risk-guide`(顶栏「风控说明」)· 数据看板内嵌卡片同步展示摘要指标
|
||||
|
||||
本文说明账户 **保证金占比**、各风控指标含义、颜色规则及配置来源。全局风控逻辑详见 [RISK.md](./RISK.md)。
|
||||
|
||||
---
|
||||
|
||||
## 保证金占比(核心规则)
|
||||
|
||||
系统设置中有两个保证金上限,默认 **单仓 30%**、**综合 50%**(`max_margin_pct` / `roll_max_margin_pct`)。
|
||||
|
||||
| 模式 | 判定 | 30%(单仓上限) | 50%(综合上限) |
|
||||
|------|------|-----------------|-----------------|
|
||||
| **单仓模式** | `MAX_ACTIVE_POSITIONS = 1` | **单仓保证金上限**:新开仓时,拟开仓位 + 已有持仓占用保证金,占权益不得超过 30% | **滚仓保证金上限**:滚仓/顺势加仓时,总占用保证金占权益不得超过 50% |
|
||||
| **多仓模式** | `MAX_ACTIVE_POSITIONS > 1` | **单仓保证金上限**:每一新开仓仍按 30% 约束该笔/该品种保证金 | **多仓保证金上限**:所有持仓合计占用保证金占权益不得超过 50% |
|
||||
|
||||
### 看板如何展示
|
||||
|
||||
| 看板指标 | 含义 | 对比上限 |
|
||||
|----------|------|----------|
|
||||
| **综合保证金占比** | 当前 **全部持仓** 占用保证金 ÷ 账户权益 | 斜杠后为 **50%**(单仓模式=滚仓上限,多仓模式=多仓上限) |
|
||||
| **单仓保证金上限** | 新开仓单笔/单品种保证金天花板 | 固定显示 **30%**(系统设置) |
|
||||
| **滚仓保证金上限** / **多仓保证金上限** | 单仓模式下为滚仓专用;多仓模式下为合计上限 | 固定显示 **50%**(系统设置) |
|
||||
|
||||
> **示例**:权益 10 万、占用保证金 2.55 万 → 综合保证金占比 **25.55% / 50%**(不是 30%)。新开仓仍受 30% 单仓上限约束;滚仓或多仓合计最高可到 50%。
|
||||
|
||||
---
|
||||
|
||||
## 状态行(看板卡片顶部)
|
||||
|
||||
顶栏一行文字为 **当前风控结论**,例如:
|
||||
|
||||
| 显示 | 含义 |
|
||||
|------|------|
|
||||
| 正常 · 可新开仓 | 未触发冻结,可新开仓 |
|
||||
| 仓位上限冻结 · 已达仓位上限 1/1 | 同时 active 持仓数已达上限,禁止新开仓,**滚仓/加仓仍允许** |
|
||||
| 日冻结 | 复盘勾选情绪问题、当日手动平仓超限或日限额触发,禁止新开仓 |
|
||||
|
||||
- **绿色**:当前可交易(`can_trade=true`)
|
||||
- **红色**:当前禁止新开仓(`can_trade=false`)
|
||||
|
||||
---
|
||||
|
||||
## 指标一览
|
||||
|
||||
| 指标 | 说明 | 配置来源 |
|
||||
|------|------|----------|
|
||||
| **风控开关** | 是否启用账户风控(持仓/日限额等) | `.env` → `RISK_CONTROL_ENABLED` |
|
||||
| **持仓限制** | 当前 active 持仓数 / 同时持仓上限 | `.env` → `MAX_ACTIVE_POSITIONS` |
|
||||
| **日持仓限制** | 当日已开仓次数(含已平)/ 日开仓上限 | `.env` → `RISK_DAILY_POSITION_LIMIT`(默认 5) |
|
||||
| **日亏损风控** | 当日亏损(已实现+浮亏)占权益 / 强平线 | 系统设置 `daily_loss_force_close_pct`(默认 2%)+ `daily_loss_slippage_buffer_pct`(默认 1%) |
|
||||
| **手动平仓次数** | 当日手动平仓次数 / 上限(超限日冻结) | `.env` → `RISK_MANUAL_CLOSE_DAILY_LIMIT` |
|
||||
| **综合保证金占比** | 占用保证金占权益 / **综合上限(50%)** | 实时计算 + 系统设置 `roll_max_margin_pct` |
|
||||
| **单仓保证金上限** | 新开仓保证金占权益上限 | 系统设置 `max_margin_pct`(默认 30%) |
|
||||
| **滚仓/多仓保证金上限** | 单仓=滚仓上限;多仓=合计上限 | 系统设置 `roll_max_margin_pct`(默认 50%) |
|
||||
| **计仓模式** | 固定金额(以损定仓)或固定手数 | 系统设置 |
|
||||
| **交易日切** | 统计日重置时刻 | `.env` → `TRADING_DAY_RESET_HOUR`(默认 8:00) |
|
||||
|
||||
---
|
||||
|
||||
## 颜色规则(看板 UI)
|
||||
|
||||
### 风控开关
|
||||
|
||||
| 状态 | 颜色 |
|
||||
|------|------|
|
||||
| 开启 | **绿色** |
|
||||
| 关闭 | **红色** |
|
||||
|
||||
### 综合保证金占比
|
||||
|
||||
显示格式:`已用% / 综合上限%`(综合上限默认 **50%**)
|
||||
|
||||
| 已用占综合上限比例 | 已用部分颜色 |
|
||||
|--------------------|--------------|
|
||||
| < 85% | **绿色**(安全) |
|
||||
| 85% ~ 100% | **琥珀色**(接近上限) |
|
||||
| ≥ 100% | **红色**(已达或超过综合上限) |
|
||||
|
||||
斜杠后的 **50%** 为 **琥珀色**,与「滚仓/多仓保证金上限」一致。
|
||||
|
||||
### 单仓 / 综合保证金上限
|
||||
|
||||
| 指标 | 数值颜色 |
|
||||
|------|----------|
|
||||
| 单仓保证金上限(30%) | **蓝色** |
|
||||
| 滚仓/多仓保证金上限(50%) | **琥珀色** |
|
||||
|
||||
### 持仓方向(持仓信息、平仓记录)
|
||||
|
||||
| 方向 | 颜色 |
|
||||
|------|------|
|
||||
| 做多 | **绿色** |
|
||||
| 做空 | **红色** |
|
||||
|
||||
---
|
||||
|
||||
## 导航与设置
|
||||
|
||||
- 顶栏 **风控说明** 即本页(`/risk-guide`),内容由 `docs/风控说明.md` 同步渲染。
|
||||
- 可在 **系统设置 → 导航显示** 中关闭「风控说明」入口;关闭后顶栏隐藏,直接访问 URL 将跳回下单监控。
|
||||
|
||||
---
|
||||
|
||||
## 与全局风控的关系
|
||||
|
||||
- 看板 **实时展示** 账户风控状态;下单前各板块仍调用 `assert_can_open()` 做相同校验。
|
||||
- **日亏损风控**、**日持仓限制** 与「同时持仓上限」并列生效;达日亏损强平线将 **强制清仓** 并禁止新开仓。
|
||||
- **期货不使用本系统「手动平仓冷静期」**(交易所自有规则);手动平仓仅计入当日次数,超限触发日冻结。
|
||||
- **综合保证金占比** 使用 CTP 柜台权益与占用保证金实时计算;断线时可能短暂显示 `—`。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [RISK.md](./RISK.md) | 全局账户风控规则与 env 变量 |
|
||||
| [SETTINGS.md](./SETTINGS.md) | 保证金上限、计仓模式、导航开关 |
|
||||
| [ORDER_MONITOR.md](./ORDER_MONITOR.md) | 下单监控顶栏风控状态 |
|
||||
| [INDEX.md](./INDEX.md) | 文档总索引 |
|
||||
+21
-5
@@ -1,19 +1,35 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
const ROOT = __dirname;
|
||||
const venvCandidates = [
|
||||
path.join(ROOT, "venv", "bin", "python"),
|
||||
path.join(ROOT, "venv", "Scripts", "python.exe"),
|
||||
];
|
||||
const interpreter = venvCandidates.find((p) => fs.existsSync(p)) || "python3";
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
name: "qihuo",
|
||||
script: "app.py",
|
||||
cwd: "/opt/qihuo",
|
||||
interpreter: "/opt/qihuo/venv/bin/python",
|
||||
cwd: ROOT,
|
||||
interpreter,
|
||||
instances: 1,
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
max_memory_restart: "300M",
|
||||
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_STARTUP_WORKERS: "8",
|
||||
QIHUO_MEMORY_MB: "8192",
|
||||
},
|
||||
error_file: "/opt/qihuo/logs/pm2-error.log",
|
||||
out_file: "/opt/qihuo/logs/pm2-out.log",
|
||||
error_file: path.join(ROOT, "logs", "pm2-error.log"),
|
||||
out_file: path.join(ROOT, "logs", "pm2-out.log"),
|
||||
time: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
"""Backward-compatible shim — implementation in modules.trading.install."""
|
||||
|
||||
from modules.trading.install import install_trading
|
||||
|
||||
__all__ = ["install_trading"]
|
||||
-257
@@ -1,257 +0,0 @@
|
||||
"""复盘 K 线:新浪拉取 + matplotlib 生成截图。"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import requests
|
||||
|
||||
from symbols import ths_to_codes
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
TZ = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
PERIOD_MINUTES = {
|
||||
"1m": "1",
|
||||
"3m": "3",
|
||||
"5m": "5",
|
||||
"15m": "15",
|
||||
"30m": "30",
|
||||
"1h": "60",
|
||||
"4h": "240",
|
||||
}
|
||||
|
||||
|
||||
def ths_to_sina_chart_symbol(symbol: str) -> Optional[str]:
|
||||
"""ag2608 -> AG2608(新浪 K 线接口合约代码)。"""
|
||||
code = (symbol or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
codes = ths_to_codes(code)
|
||||
if codes:
|
||||
sina = codes.get("sina_code", "")
|
||||
if sina.startswith("nf_"):
|
||||
return sina[3:]
|
||||
if sina.startswith("CFF_RE_"):
|
||||
return sina[7:]
|
||||
ths = codes.get("ths_code", "")
|
||||
return ths.upper() if ths else None
|
||||
m = re.match(r"^([A-Za-z]+)(\d+)$", code)
|
||||
if m:
|
||||
return m.group(1).upper() + m.group(2)
|
||||
return None
|
||||
|
||||
|
||||
def _parse_jsonp(text: str) -> Optional[list]:
|
||||
m = re.search(r"\((.*)\)\s*;?\s*$", text.strip(), re.DOTALL)
|
||||
if not m:
|
||||
return None
|
||||
try:
|
||||
data = json.loads(m.group(1))
|
||||
return data if isinstance(data, list) else None
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def fetch_sina_klines(symbol: str, period: str) -> list:
|
||||
"""拉取新浪期货分钟 K 线。"""
|
||||
chart_sym = ths_to_sina_chart_symbol(symbol)
|
||||
if not chart_sym:
|
||||
return []
|
||||
if period == "1d":
|
||||
return _fetch_sina_daily(chart_sym)
|
||||
typ = PERIOD_MINUTES.get(period)
|
||||
if not typ:
|
||||
return []
|
||||
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
|
||||
url = (
|
||||
"https://stock2.finance.sina.com.cn/futures/api/jsonp.php/"
|
||||
f"var_{chart_sym}_{typ}_{ts}=/InnerFuturesNewService.getFewMinLine"
|
||||
f"?symbol={chart_sym}&type={typ}"
|
||||
)
|
||||
try:
|
||||
resp = requests.get(
|
||||
url,
|
||||
timeout=20,
|
||||
headers={"Referer": "https://finance.sina.com.cn"},
|
||||
)
|
||||
bars = _parse_jsonp(resp.text)
|
||||
return bars or []
|
||||
except Exception as exc:
|
||||
logger.warning("fetch kline failed %s %s: %s", chart_sym, period, exc)
|
||||
return []
|
||||
|
||||
|
||||
def _fetch_sina_daily(chart_sym: str) -> list:
|
||||
url = (
|
||||
"https://stock2.finance.sina.com.cn/futures/api/json.php/"
|
||||
f"IndexService.getInnerFuturesDailyKLine?symbol={chart_sym}"
|
||||
)
|
||||
try:
|
||||
resp = requests.get(url, timeout=20, headers={"Referer": "https://finance.sina.com.cn"})
|
||||
raw = resp.json()
|
||||
if not raw:
|
||||
return []
|
||||
out = []
|
||||
for row in raw:
|
||||
if isinstance(row, list) and len(row) >= 5:
|
||||
out.append({
|
||||
"d": row[0],
|
||||
"o": row[1],
|
||||
"h": row[2],
|
||||
"l": row[3],
|
||||
"c": row[4],
|
||||
})
|
||||
return out
|
||||
except Exception as exc:
|
||||
logger.warning("fetch daily kline failed %s: %s", chart_sym, exc)
|
||||
return []
|
||||
|
||||
|
||||
def _parse_dt(value: str) -> Optional[datetime]:
|
||||
if not value:
|
||||
return None
|
||||
v = value.strip().replace("T", " ")
|
||||
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M"):
|
||||
try:
|
||||
return datetime.strptime(v, fmt).replace(tzinfo=TZ)
|
||||
except ValueError:
|
||||
continue
|
||||
try:
|
||||
return datetime.fromisoformat(value.strip()).replace(tzinfo=TZ)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _bar_datetime(bar: dict) -> Optional[datetime]:
|
||||
d = bar.get("d")
|
||||
if not d:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(d, "%Y-%m-%d %H:%M:%S").replace(tzinfo=TZ)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _select_bars(
|
||||
bars: list,
|
||||
cutoff: datetime,
|
||||
count: int,
|
||||
) -> list:
|
||||
filtered = []
|
||||
for bar in bars:
|
||||
dt = _bar_datetime(bar)
|
||||
if dt and dt <= cutoff:
|
||||
filtered.append(bar)
|
||||
if not filtered:
|
||||
filtered = bars
|
||||
if count > 0 and len(filtered) > count:
|
||||
filtered = filtered[-count:]
|
||||
return filtered
|
||||
|
||||
|
||||
def generate_review_kline_chart(
|
||||
symbol: str,
|
||||
periods: list[str],
|
||||
count: int,
|
||||
cutoff_label: str,
|
||||
open_time: str,
|
||||
close_time: str,
|
||||
entry_price: Optional[float],
|
||||
stop_loss: Optional[float],
|
||||
take_profit: Optional[float],
|
||||
close_price: Optional[float],
|
||||
upload_dir: str,
|
||||
) -> Optional[str]:
|
||||
"""生成双周期 K 线复盘图,返回 uploads 目录下的文件名。"""
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
|
||||
now = datetime.now(TZ)
|
||||
if cutoff_label == "开仓时间":
|
||||
cutoff = _parse_dt(open_time) or now
|
||||
elif cutoff_label == "当前时间":
|
||||
cutoff = now
|
||||
else:
|
||||
cutoff = _parse_dt(close_time) or now
|
||||
|
||||
open_dt = _parse_dt(open_time)
|
||||
close_dt = _parse_dt(close_time)
|
||||
|
||||
valid_periods = [p for p in periods if p]
|
||||
if not valid_periods:
|
||||
valid_periods = ["15m", "1h"]
|
||||
|
||||
fig, axes = plt.subplots(
|
||||
len(valid_periods), 1,
|
||||
figsize=(14, 4.5 * len(valid_periods)),
|
||||
facecolor="#0a0a10",
|
||||
squeeze=False,
|
||||
)
|
||||
|
||||
plotted = False
|
||||
for idx, period in enumerate(valid_periods):
|
||||
ax = axes[idx, 0]
|
||||
bars = fetch_sina_klines(symbol, period)
|
||||
bars = _select_bars(bars, cutoff, count)
|
||||
if not bars:
|
||||
ax.set_facecolor("#12121a")
|
||||
ax.text(0.5, 0.5, f"No {period} data", ha="center", va="center", color="#888")
|
||||
ax.set_xticks([])
|
||||
ax.set_yticks([])
|
||||
continue
|
||||
|
||||
times = [_bar_datetime(b) for b in bars]
|
||||
closes = [float(b["c"]) for b in bars]
|
||||
highs = [float(b["h"]) for b in bars]
|
||||
lows = [float(b["l"]) for b in bars]
|
||||
|
||||
ax.set_facecolor("#12121a")
|
||||
ax.plot(times, closes, color="#4cc2ff", linewidth=1.2)
|
||||
ax.fill_between(
|
||||
times, lows, highs,
|
||||
color="#4cc2ff", alpha=0.12,
|
||||
)
|
||||
|
||||
levels = [
|
||||
(entry_price, "#eac147", "Entry"),
|
||||
(stop_loss, "#ff6666", "SL"),
|
||||
(take_profit, "#4cd97f", "TP"),
|
||||
(close_price, "#c4c4ff", "Close"),
|
||||
]
|
||||
for price, color, label in levels:
|
||||
if price is not None:
|
||||
ax.axhline(price, color=color, linewidth=0.9, linestyle="--", alpha=0.85)
|
||||
ax.text(times[-1], price, label, color=color, fontsize=8, va="bottom")
|
||||
|
||||
if open_dt:
|
||||
ax.axvline(open_dt, color="#888", linewidth=0.8, linestyle=":", alpha=0.7)
|
||||
if close_dt:
|
||||
ax.axvline(close_dt, color="#aaa", linewidth=0.8, linestyle=":", alpha=0.7)
|
||||
|
||||
chart_sym = ths_to_sina_chart_symbol(symbol) or symbol
|
||||
ax.set_title(f"{chart_sym} {period}", color="#eaeaea", fontsize=11, pad=8)
|
||||
ax.tick_params(colors="#888", labelsize=8)
|
||||
for spine in ax.spines.values():
|
||||
spine.set_color("#2e2e45")
|
||||
ax.xaxis.set_major_formatter(mdates.DateFormatter("%m-%d %H:%M"))
|
||||
ax.grid(True, color="#1e1e30", linewidth=0.5)
|
||||
plotted = True
|
||||
|
||||
if not plotted:
|
||||
plt.close(fig)
|
||||
return None
|
||||
|
||||
fig.tight_layout()
|
||||
ts = datetime.now(TZ).strftime("%Y%m%d%H%M%S")
|
||||
chart_sym = ths_to_sina_chart_symbol(symbol) or "chart"
|
||||
filename = f"{ts}_kline_{chart_sym}.png"
|
||||
path = os.path.join(upload_dir, filename)
|
||||
fig.savefig(path, dpi=120, facecolor=fig.get_facecolor())
|
||||
plt.close(fig)
|
||||
return filename
|
||||
@@ -0,0 +1,3 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
|
||||
"""Qihuo feature modules package."""
|
||||
@@ -0,0 +1,5 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
|
||||
from modules.backup.routes import register
|
||||
|
||||
__all__ = ["register"]
|
||||
@@ -0,0 +1,767 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
# 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
|
||||
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
||||
|
||||
"""数据库备份:SQLite futures.db,含 uploads 与一键恢复脚本。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, IO, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from modules.core.db_conn import DB_PATH
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TZ = ZoneInfo("Asia/Shanghai")
|
||||
BACKUP_FILENAME_RE = re.compile(r"^qihuo_backup_\d{8}_\d{6}\.tar\.gz$")
|
||||
BACKUP_LAST_KEY = "backup_last_at"
|
||||
BACKUP_KEEP_KEY = "backup_keep_count"
|
||||
BACKUP_AUTO_KEY = "backup_auto_enabled"
|
||||
BACKUP_HOUR_KEY = "backup_auto_hour"
|
||||
DEFAULT_KEEP_COUNT = 30
|
||||
DEFAULT_AUTO_HOUR = 3
|
||||
CHECK_INTERVAL_SEC = 3600
|
||||
_backup_lock = threading.Lock()
|
||||
RESTORE_STATUS_FILE = "restore_status.json"
|
||||
RESTORE_CONFIRM_TOKEN = "RESTORE"
|
||||
|
||||
RESTORE_MD = """# qihuo 备份恢复说明
|
||||
|
||||
本压缩包由 qihuo 系统自动生成,可在另一台 Linux 服务器上恢复交易数据。
|
||||
|
||||
## 包内文件
|
||||
|
||||
| 文件/目录 | 说明 |
|
||||
|-----------|------|
|
||||
| `futures.db` | SQLite 主库 |
|
||||
| `uploads/` | 复盘截图与 K 线图(若备份时存在) |
|
||||
| `.env` | 环境配置(`config/.env` 或根目录 `.env`) |
|
||||
| `manifest.json` | 备份元数据 |
|
||||
| `restore.sh` | 一键恢复脚本 |
|
||||
|
||||
## 快速恢复(推荐)
|
||||
|
||||
1. 将本压缩包上传到目标服务器(例如 `/root/`)
|
||||
2. 解压并执行恢复脚本:
|
||||
|
||||
```bash
|
||||
cd /root
|
||||
tar -xzf qihuo_backup_YYYYMMDD_HHMMSS.tar.gz
|
||||
cd qihuo_backup_YYYYMMDD_HHMMSS
|
||||
chmod +x restore.sh
|
||||
./restore.sh
|
||||
```
|
||||
|
||||
默认恢复到 **`/root/qihuo`**。指定应用目录:
|
||||
|
||||
```bash
|
||||
RESTORE_DIR=/opt/qihuo ./restore.sh
|
||||
```
|
||||
|
||||
3. 在新服务器部署 qihuo 代码与 Python 环境(见 `docs/DEPLOY.md`)
|
||||
4. 若包内含 `.env`,`restore.sh` 会自动恢复到应用目录(无需手工复制)
|
||||
5. 重启服务:`pm2 restart qihuo`
|
||||
|
||||
## 手工恢复
|
||||
|
||||
```bash
|
||||
mkdir -p /opt/qihuo/uploads
|
||||
cp futures.db /opt/qihuo/futures.db
|
||||
cp -a uploads/. /opt/qihuo/uploads/
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
- 恢复前请停止 qihuo 进程
|
||||
- `.env` 含敏感信息,请妥善保管备份包
|
||||
- 详见 `docs/BACKUP.md`
|
||||
"""
|
||||
|
||||
|
||||
def _app_root() -> Path:
|
||||
from modules.core.paths import ROOT
|
||||
return ROOT
|
||||
|
||||
|
||||
def default_backup_dir() -> str:
|
||||
env = (os.getenv("QIHUO_BACKUP_DIR") or "").strip()
|
||||
if env:
|
||||
return env
|
||||
if os.name == "nt":
|
||||
return str(_app_root() / "qihuo_backup")
|
||||
return "/root/qihuo_backup"
|
||||
|
||||
|
||||
def default_restore_dir() -> str:
|
||||
env = (os.getenv("QIHUO_RESTORE_DIR") or "").strip()
|
||||
if env:
|
||||
return env
|
||||
if os.name == "nt":
|
||||
return str(_app_root())
|
||||
return "/root/qihuo"
|
||||
|
||||
|
||||
def restore_target_dir() -> Path:
|
||||
"""Web/API 恢复目标目录,默认当前应用根目录。"""
|
||||
env = (os.getenv("QIHUO_RESTORE_DIR") or "").strip()
|
||||
if env:
|
||||
return Path(env)
|
||||
return _app_root()
|
||||
|
||||
|
||||
def backup_dir() -> Path:
|
||||
path = Path(default_backup_dir())
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def backup_in_progress() -> bool:
|
||||
return _backup_lock.locked()
|
||||
|
||||
|
||||
def _restore_status_path() -> Path:
|
||||
from modules.core.paths import DATA_DIR
|
||||
|
||||
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return DATA_DIR / RESTORE_STATUS_FILE
|
||||
|
||||
|
||||
def _write_restore_status(state: str, message: str = "", **extra: Any) -> None:
|
||||
payload = {
|
||||
"state": state,
|
||||
"message": message,
|
||||
"updated_at": datetime.now(TZ).isoformat(timespec="seconds"),
|
||||
}
|
||||
payload.update(extra)
|
||||
_restore_status_path().write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def get_restore_status() -> dict:
|
||||
path = _restore_status_path()
|
||||
if not path.is_file():
|
||||
return {"state": "idle", "message": "", "updated_at": ""}
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(data, dict):
|
||||
data.setdefault("state", "idle")
|
||||
data.setdefault("message", "")
|
||||
return data
|
||||
except Exception:
|
||||
pass
|
||||
return {"state": "idle", "message": "", "updated_at": ""}
|
||||
|
||||
|
||||
def restore_in_progress() -> bool:
|
||||
return get_restore_status().get("state") in ("pending", "running")
|
||||
|
||||
|
||||
def _manifest_root_prefix(tar: tarfile.TarFile) -> str:
|
||||
for member in tar.getmembers():
|
||||
name = member.name.rstrip("/")
|
||||
if name.endswith("/manifest.json") or name == "manifest.json":
|
||||
if name == "manifest.json":
|
||||
return ""
|
||||
return name[: -len("/manifest.json")]
|
||||
raise ValueError("备份包缺少 manifest.json")
|
||||
|
||||
|
||||
def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict:
|
||||
root = _manifest_root_prefix(tar)
|
||||
manifest_name = f"{root}/manifest.json" if root else "manifest.json"
|
||||
try:
|
||||
member = tar.getmember(manifest_name)
|
||||
except KeyError as exc:
|
||||
raise ValueError("备份包缺少 manifest.json") from exc
|
||||
extracted = tar.extractfile(member)
|
||||
if not extracted:
|
||||
raise ValueError("无法读取 manifest.json")
|
||||
data = json.loads(extracted.read().decode("utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError("manifest.json 格式无效")
|
||||
return data
|
||||
|
||||
|
||||
def _validate_manifest(manifest: dict) -> None:
|
||||
if manifest.get("app") != "qihuo":
|
||||
raise ValueError("不是有效的 qihuo 备份包")
|
||||
backend = (manifest.get("backend") or "sqlite").strip()
|
||||
if backend == "postgres":
|
||||
raise ValueError("不再支持 PostgreSQL 备份,请使用 SQLite 备份包")
|
||||
if backend != "sqlite":
|
||||
raise ValueError("manifest 缺少或无效的数据库类型")
|
||||
|
||||
|
||||
def _member_exists(tar: tarfile.TarFile, root: str, name: str) -> bool:
|
||||
candidates = [name]
|
||||
if root:
|
||||
candidates.append(f"{root}/{name}")
|
||||
return any(_tar_has_member(tar, path) for path in candidates)
|
||||
|
||||
|
||||
def _tar_has_member(tar: tarfile.TarFile, path: str) -> bool:
|
||||
try:
|
||||
tar.getmember(path)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
def _validate_archive_contents(tar: tarfile.TarFile, root: str) -> None:
|
||||
if not _member_exists(tar, root, "futures.db"):
|
||||
raise ValueError("备份缺少 futures.db")
|
||||
|
||||
|
||||
def _manifest_preview(manifest: dict, path: Path) -> dict:
|
||||
stat = path.stat()
|
||||
created_at = (manifest.get("created_at") or "").strip()
|
||||
return {
|
||||
"name": path.name,
|
||||
"created_at": created_at,
|
||||
"includes_uploads": bool(manifest.get("includes_uploads")),
|
||||
"includes_env": bool(manifest.get("includes_env")),
|
||||
"env_restore_path": (manifest.get("env_restore_path") or "").strip(),
|
||||
"size": stat.st_size,
|
||||
"size_mb": round(stat.st_size / (1024 * 1024), 2),
|
||||
"mtime": datetime.fromtimestamp(stat.st_mtime, TZ).isoformat(timespec="seconds"),
|
||||
}
|
||||
|
||||
|
||||
def peek_manifest(path: Path) -> dict:
|
||||
with tarfile.open(path, "r:gz") as tar:
|
||||
return _read_manifest_from_tar(tar)
|
||||
|
||||
|
||||
def inspect_backup_archive(path: Path) -> dict:
|
||||
with tarfile.open(path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
_validate_manifest(manifest)
|
||||
root = _manifest_root_prefix(tar)
|
||||
_validate_archive_contents(tar, root)
|
||||
return _manifest_preview(manifest, path)
|
||||
|
||||
|
||||
def _allocate_backup_filename(manifest: dict, preferred: str = "") -> str:
|
||||
preferred = (preferred or "").strip()
|
||||
if preferred and BACKUP_FILENAME_RE.match(preferred):
|
||||
candidate = backup_dir() / preferred
|
||||
if not candidate.exists():
|
||||
return preferred
|
||||
created = (manifest.get("created_at") or "").strip()
|
||||
stamp = ""
|
||||
if created:
|
||||
try:
|
||||
stamp = datetime.fromisoformat(created).strftime("%Y%m%d_%H%M%S")
|
||||
except ValueError:
|
||||
stamp = ""
|
||||
if not stamp:
|
||||
stamp = datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
|
||||
name = f"qihuo_backup_{stamp}.tar.gz"
|
||||
if not (backup_dir() / name).exists():
|
||||
return name
|
||||
stamp = datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
|
||||
return f"qihuo_backup_{stamp}.tar.gz"
|
||||
|
||||
|
||||
def save_uploaded_backup(stream: IO[bytes], original_filename: str = "") -> tuple[str, dict]:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".tar.gz") as tmp:
|
||||
shutil.copyfileobj(stream, tmp)
|
||||
tmp_path = Path(tmp.name)
|
||||
try:
|
||||
info = inspect_backup_archive(tmp_path)
|
||||
manifest = peek_manifest(tmp_path)
|
||||
filename = _allocate_backup_filename(manifest, original_filename)
|
||||
dest = backup_dir() / filename
|
||||
shutil.move(str(tmp_path), str(dest))
|
||||
info["name"] = filename
|
||||
return filename, info
|
||||
except Exception:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
|
||||
def _pm2_available() -> bool:
|
||||
return shutil.which("pm2") is not None
|
||||
|
||||
|
||||
def _pm2_stop() -> None:
|
||||
if not _pm2_available():
|
||||
logger.warning("pm2 not found, skip stop")
|
||||
return
|
||||
proc = subprocess.run(
|
||||
["pm2", "stop", "qihuo"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
logger.warning("pm2 stop qihuo: %s", proc.stderr.strip() or proc.stdout.strip())
|
||||
|
||||
|
||||
def _pm2_restart() -> None:
|
||||
if not _pm2_available():
|
||||
logger.warning("pm2 not found, skip restart")
|
||||
return
|
||||
proc = subprocess.run(
|
||||
["pm2", "restart", "qihuo"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
proc = subprocess.run(
|
||||
["pm2", "start", "qihuo"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(proc.stderr.strip() or proc.stdout.strip() or "pm2 restart 失败")
|
||||
|
||||
|
||||
def _extract_member_to_path(tar: tarfile.TarFile, member_name: str, dest: Path) -> None:
|
||||
try:
|
||||
member = tar.getmember(member_name)
|
||||
except KeyError:
|
||||
return
|
||||
extracted = tar.extractfile(member)
|
||||
if not extracted:
|
||||
return
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(dest, "wb") as out:
|
||||
shutil.copyfileobj(extracted, out)
|
||||
|
||||
|
||||
def _restore_uploads_dir(tar: tarfile.TarFile, root: str, restore_dir: Path) -> None:
|
||||
prefix = f"{root}/uploads" if root else "uploads"
|
||||
uploads_dest = restore_dir / "uploads"
|
||||
uploads_dest.mkdir(parents=True, exist_ok=True)
|
||||
found = False
|
||||
for member in tar.getmembers():
|
||||
if member.name == prefix or member.name.startswith(prefix + "/"):
|
||||
found = True
|
||||
rel = member.name[len(prefix) :].lstrip("/")
|
||||
if not rel:
|
||||
continue
|
||||
target = uploads_dest / rel
|
||||
if member.isdir():
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
extracted = tar.extractfile(member)
|
||||
if extracted:
|
||||
with open(target, "wb") as out:
|
||||
shutil.copyfileobj(extracted, out)
|
||||
if not found:
|
||||
logger.info("backup has no uploads/")
|
||||
|
||||
|
||||
def _reload_env_file(env_path: Path) -> None:
|
||||
if not env_path.is_file():
|
||||
return
|
||||
try:
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(str(env_path), override=True)
|
||||
except Exception as exc:
|
||||
logger.warning("reload .env failed: %s", exc)
|
||||
|
||||
|
||||
def _perform_restore(archive_path: Path, restore_dir: Path) -> dict:
|
||||
restore_dir.mkdir(parents=True, exist_ok=True)
|
||||
with tarfile.open(archive_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
_validate_manifest(manifest)
|
||||
root = _manifest_root_prefix(tar)
|
||||
_validate_archive_contents(tar, root)
|
||||
|
||||
env_member = f"{root}/.env" if root else ".env"
|
||||
env_restore_path = (manifest.get("env_restore_path") or "config/.env").strip()
|
||||
if manifest.get("includes_env") and _tar_has_member(tar, env_member):
|
||||
env_dest = restore_dir / env_restore_path
|
||||
_extract_member_to_path(tar, env_member, env_dest)
|
||||
_reload_env_file(env_dest)
|
||||
|
||||
db_member = f"{root}/futures.db" if root else "futures.db"
|
||||
db_dest = Path(DB_PATH)
|
||||
if not db_dest.is_absolute():
|
||||
db_dest = restore_dir / db_dest.name
|
||||
_extract_member_to_path(tar, db_member, db_dest)
|
||||
_restore_uploads_dir(tar, root, restore_dir)
|
||||
|
||||
return {
|
||||
"restore_dir": str(restore_dir),
|
||||
"includes_env": bool(manifest.get("includes_env")),
|
||||
"includes_uploads": bool(manifest.get("includes_uploads")),
|
||||
}
|
||||
|
||||
|
||||
def run_restore_job(archive_path: Path) -> None:
|
||||
filename = archive_path.name
|
||||
restore_dir = restore_target_dir()
|
||||
try:
|
||||
_write_restore_status(
|
||||
"running",
|
||||
"正在停止服务…",
|
||||
filename=filename,
|
||||
step="stop",
|
||||
restore_dir=str(restore_dir),
|
||||
)
|
||||
_pm2_stop()
|
||||
|
||||
_write_restore_status(
|
||||
"running",
|
||||
"正在恢复数据…",
|
||||
filename=filename,
|
||||
step="restore",
|
||||
restore_dir=str(restore_dir),
|
||||
)
|
||||
summary = _perform_restore(archive_path.resolve(), restore_dir)
|
||||
|
||||
_write_restore_status(
|
||||
"running",
|
||||
"正在重启服务…",
|
||||
filename=filename,
|
||||
step="restart",
|
||||
restore_dir=str(restore_dir),
|
||||
)
|
||||
_pm2_restart()
|
||||
|
||||
_write_restore_status(
|
||||
"done",
|
||||
"恢复完成,服务已重启",
|
||||
filename=filename,
|
||||
restore_dir=str(restore_dir),
|
||||
summary=summary,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("restore failed: %s", exc)
|
||||
_write_restore_status(
|
||||
"error",
|
||||
str(exc),
|
||||
filename=filename,
|
||||
restore_dir=str(restore_dir),
|
||||
)
|
||||
try:
|
||||
_pm2_restart()
|
||||
except Exception as restart_exc:
|
||||
logger.warning("restart after restore error: %s", restart_exc)
|
||||
|
||||
|
||||
def schedule_restore(filename: str) -> tuple[bool, str]:
|
||||
if _backup_lock.locked():
|
||||
return False, "备份进行中,请稍后再试"
|
||||
if restore_in_progress():
|
||||
return False, "恢复进行中,请稍后再试"
|
||||
try:
|
||||
path = resolve_backup_file(filename)
|
||||
inspect_backup_archive(path)
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
return False, str(exc)
|
||||
|
||||
_write_restore_status(
|
||||
"pending",
|
||||
"恢复任务已提交…",
|
||||
filename=filename,
|
||||
restore_dir=str(restore_target_dir()),
|
||||
)
|
||||
script = Path(__file__).resolve().parent / "restore_job.py"
|
||||
subprocess.Popen(
|
||||
[sys.executable, str(script), str(path.resolve())],
|
||||
start_new_session=True,
|
||||
cwd=str(_app_root()),
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return True, "恢复已开始,服务将短暂中断后自动重启"
|
||||
|
||||
|
||||
def get_backup_last_at(get_setting: Callable[[str, str], str]) -> str:
|
||||
return (get_setting(BACKUP_LAST_KEY, "") or "").strip()
|
||||
|
||||
|
||||
def _backup_sqlite(src_path: str, dst_path: str) -> None:
|
||||
src = sqlite3.connect(src_path, timeout=30)
|
||||
try:
|
||||
try:
|
||||
src.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
dst = sqlite3.connect(dst_path)
|
||||
try:
|
||||
src.backup(dst)
|
||||
dst.commit()
|
||||
finally:
|
||||
dst.close()
|
||||
finally:
|
||||
src.close()
|
||||
|
||||
|
||||
def _env_backup_info() -> tuple[Optional[Path], str]:
|
||||
"""返回 (源 .env 路径, 恢复到应用目录的相对路径)。"""
|
||||
from modules.core.paths import CONFIG_DIR, ENV_FILE, LEGACY_ENV_FILE
|
||||
|
||||
if ENV_FILE.is_file():
|
||||
return ENV_FILE, "config/.env"
|
||||
if LEGACY_ENV_FILE.is_file():
|
||||
return LEGACY_ENV_FILE, ".env"
|
||||
return None, "config/.env"
|
||||
|
||||
|
||||
def _copy_env_into_backup(work: Path) -> tuple[bool, str]:
|
||||
env_src, env_restore_path = _env_backup_info()
|
||||
if env_src and env_src.is_file():
|
||||
shutil.copy2(env_src, work / ".env")
|
||||
return True, env_restore_path
|
||||
return False, env_restore_path
|
||||
|
||||
|
||||
def _write_restore_script(dest: Path, *, env_restore_path: str = "") -> None:
|
||||
env_block = ""
|
||||
if env_restore_path:
|
||||
env_block = f"""
|
||||
if [ -f "$SCRIPT_DIR/.env" ]; then
|
||||
ENV_DEST="$RESTORE_DIR/{env_restore_path}"
|
||||
mkdir -p "$(dirname "$ENV_DEST")"
|
||||
cp -f "$SCRIPT_DIR/.env" "$ENV_DEST"
|
||||
echo "已复制 .env -> $ENV_DEST"
|
||||
fi
|
||||
"""
|
||||
script = f"""#!/bin/bash
|
||||
set -euo pipefail
|
||||
RESTORE_DIR="${{RESTORE_DIR:-{default_restore_dir()}}}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
mkdir -p "$RESTORE_DIR/uploads"
|
||||
{env_block}
|
||||
if [ -f "$SCRIPT_DIR/futures.db" ]; then
|
||||
cp -f "$SCRIPT_DIR/futures.db" "$RESTORE_DIR/futures.db"
|
||||
echo "已复制 futures.db -> $RESTORE_DIR/futures.db"
|
||||
fi
|
||||
if [ -d "$SCRIPT_DIR/uploads" ]; then
|
||||
cp -a "$SCRIPT_DIR/uploads/." "$RESTORE_DIR/uploads/"
|
||||
echo "已复制 uploads -> $RESTORE_DIR/uploads/"
|
||||
fi
|
||||
echo ""
|
||||
echo "恢复完成。目标目录: $RESTORE_DIR"
|
||||
echo "下一步: pm2 restart qihuo"
|
||||
echo "详见 RESTORE.md 与 docs/BACKUP.md"
|
||||
"""
|
||||
dest.write_text(script, encoding="utf-8")
|
||||
|
||||
|
||||
def create_backup(*, include_uploads: bool = True, include_env: bool = True) -> tuple[str, str]:
|
||||
"""创建 tar.gz 备份,返回 (文件名, 说明)。"""
|
||||
if not os.path.isfile(DB_PATH):
|
||||
raise FileNotFoundError(f"数据库不存在: {DB_PATH}")
|
||||
|
||||
with _backup_lock:
|
||||
stamp = datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
|
||||
folder_name = f"qihuo_backup_{stamp}"
|
||||
filename = f"{folder_name}.tar.gz"
|
||||
out_path = backup_dir() / filename
|
||||
app_root = _app_root()
|
||||
upload_src = app_root / "uploads"
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="qihuo_bak_") as tmp:
|
||||
work = Path(tmp) / folder_name
|
||||
work.mkdir()
|
||||
_backup_sqlite(DB_PATH, str(work / "futures.db"))
|
||||
|
||||
if include_uploads and upload_src.is_dir():
|
||||
shutil.copytree(upload_src, work / "uploads", dirs_exist_ok=True)
|
||||
|
||||
includes_env = False
|
||||
env_restore_path = "config/.env"
|
||||
if include_env:
|
||||
includes_env, env_restore_path = _copy_env_into_backup(work)
|
||||
if not includes_env:
|
||||
logger.warning("backup: .env not found (checked config/.env and root .env)")
|
||||
|
||||
manifest = {
|
||||
"app": "qihuo",
|
||||
"backend": "sqlite",
|
||||
"created_at": datetime.now(TZ).isoformat(timespec="seconds"),
|
||||
"db_path": DB_PATH,
|
||||
"includes_uploads": include_uploads and upload_src.is_dir(),
|
||||
"includes_env": includes_env,
|
||||
"env_restore_path": env_restore_path if includes_env else "",
|
||||
"default_restore_dir": default_restore_dir(),
|
||||
"files": sorted(p.name for p in work.iterdir()),
|
||||
}
|
||||
(work / "manifest.json").write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(work / "RESTORE.md").write_text(RESTORE_MD, encoding="utf-8")
|
||||
_write_restore_script(
|
||||
work / "restore.sh",
|
||||
env_restore_path=env_restore_path if includes_env else "",
|
||||
)
|
||||
|
||||
with tarfile.open(out_path, "w:gz") as tar:
|
||||
tar.add(work, arcname=folder_name)
|
||||
|
||||
size_mb = out_path.stat().st_size / (1024 * 1024)
|
||||
env_note = "含 .env" if includes_env else "未含 .env"
|
||||
return filename, f"备份已生成 {filename}({size_mb:.2f} MB,{env_note})"
|
||||
|
||||
|
||||
def list_backups(*, with_manifest: bool = True) -> list[dict]:
|
||||
items: list[dict] = []
|
||||
for path in sorted(backup_dir().glob("qihuo_backup_*.tar.gz"), reverse=True):
|
||||
if not BACKUP_FILENAME_RE.match(path.name):
|
||||
continue
|
||||
stat = path.stat()
|
||||
item = {
|
||||
"name": path.name,
|
||||
"size": stat.st_size,
|
||||
"size_mb": round(stat.st_size / (1024 * 1024), 2),
|
||||
"mtime": datetime.fromtimestamp(stat.st_mtime, TZ).isoformat(timespec="seconds"),
|
||||
"created_at": "",
|
||||
"includes_env": False,
|
||||
"includes_uploads": False,
|
||||
}
|
||||
if with_manifest:
|
||||
try:
|
||||
manifest = peek_manifest(path)
|
||||
item["created_at"] = (manifest.get("created_at") or "").strip()
|
||||
item["includes_env"] = bool(manifest.get("includes_env"))
|
||||
item["includes_uploads"] = bool(manifest.get("includes_uploads"))
|
||||
except Exception as exc:
|
||||
logger.debug("read manifest %s: %s", path.name, exc)
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def resolve_backup_file(filename: str) -> Path:
|
||||
name = (filename or "").strip()
|
||||
if not BACKUP_FILENAME_RE.match(name):
|
||||
raise ValueError("无效的备份文件名")
|
||||
path = (backup_dir() / name).resolve()
|
||||
root = backup_dir().resolve()
|
||||
if not str(path).startswith(str(root) + os.sep) and path != root:
|
||||
raise ValueError("无效的备份路径")
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError("备份文件不存在")
|
||||
return path
|
||||
|
||||
|
||||
def prune_old_backups(keep: int) -> int:
|
||||
keep_n = max(1, int(keep or DEFAULT_KEEP_COUNT))
|
||||
files = list_backups()
|
||||
removed = 0
|
||||
for item in files[keep_n:]:
|
||||
try:
|
||||
resolve_backup_file(item["name"]).unlink()
|
||||
removed += 1
|
||||
except Exception as exc:
|
||||
logger.warning("prune backup %s: %s", item["name"], exc)
|
||||
return removed
|
||||
|
||||
|
||||
def run_backup_job(
|
||||
*,
|
||||
get_setting: Callable[[str, str], str],
|
||||
set_setting: Callable[[str, str], None],
|
||||
include_uploads: bool = True,
|
||||
include_env: bool = True,
|
||||
) -> tuple[str, str]:
|
||||
keep = DEFAULT_KEEP_COUNT
|
||||
try:
|
||||
keep = max(5, min(200, int(get_setting(BACKUP_KEEP_KEY, str(DEFAULT_KEEP_COUNT)) or DEFAULT_KEEP_COUNT)))
|
||||
except ValueError:
|
||||
pass
|
||||
filename, msg = create_backup(include_uploads=include_uploads, include_env=include_env)
|
||||
set_setting(BACKUP_LAST_KEY, datetime.now(TZ).isoformat(timespec="seconds"))
|
||||
removed = prune_old_backups(keep)
|
||||
if removed:
|
||||
msg = f"{msg},已清理 {removed} 个旧备份"
|
||||
return filename, msg
|
||||
|
||||
|
||||
def schedule_backup(
|
||||
*,
|
||||
get_setting: Callable[[str, str], str],
|
||||
set_setting: Callable[[str, str], None],
|
||||
include_uploads: bool = True,
|
||||
include_env: bool = True,
|
||||
) -> tuple[bool, str]:
|
||||
if _backup_lock.locked():
|
||||
return False, "备份进行中,请稍后再试"
|
||||
if restore_in_progress():
|
||||
return False, "恢复进行中,请稍后再试"
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
run_backup_job(
|
||||
get_setting=get_setting,
|
||||
set_setting=set_setting,
|
||||
include_uploads=include_uploads,
|
||||
include_env=include_env,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception("backup failed: %s", exc)
|
||||
|
||||
threading.Thread(target=_run, daemon=True, name="qihuo-backup-run").start()
|
||||
return True, "已在后台开始备份,请稍后刷新本页查看"
|
||||
|
||||
|
||||
def _should_auto_backup(get_setting: Callable[[str, str], str]) -> bool:
|
||||
if (get_setting(BACKUP_AUTO_KEY, "1") or "1").strip() not in ("1", "true", "yes"):
|
||||
return False
|
||||
try:
|
||||
hour = int(get_setting(BACKUP_HOUR_KEY, str(DEFAULT_AUTO_HOUR)) or DEFAULT_AUTO_HOUR)
|
||||
except ValueError:
|
||||
hour = DEFAULT_AUTO_HOUR
|
||||
hour = max(0, min(23, hour))
|
||||
now = datetime.now(TZ)
|
||||
if now.hour != hour:
|
||||
return False
|
||||
last = get_backup_last_at(get_setting)
|
||||
if last and last[:10] == now.date().isoformat():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def start_backup_worker(
|
||||
*,
|
||||
get_setting_fn: Callable[[str, str], str],
|
||||
set_setting_fn: Callable[[str, str], None],
|
||||
interval: int = CHECK_INTERVAL_SEC,
|
||||
) -> None:
|
||||
"""后台线程:按设定小时每日自动备份。"""
|
||||
|
||||
def _loop() -> None:
|
||||
time.sleep(30)
|
||||
while True:
|
||||
try:
|
||||
if _should_auto_backup(get_setting_fn):
|
||||
filename, msg = run_backup_job(
|
||||
get_setting=get_setting_fn,
|
||||
set_setting=set_setting_fn,
|
||||
include_uploads=True,
|
||||
include_env=True,
|
||||
)
|
||||
logger.info("auto backup: %s — %s", filename, msg)
|
||||
except Exception as exc:
|
||||
logger.warning("backup worker: %s", exc)
|
||||
time.sleep(max(300, interval))
|
||||
|
||||
threading.Thread(target=_loop, daemon=True, name="qihuo-backup-worker").start()
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
"""Detached restore worker — survives pm2 stop of the parent web process."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = argv if argv is not None else sys.argv[1:]
|
||||
if len(args) != 1:
|
||||
print("usage: python -m modules.backup.restore_job <backup.tar.gz>", file=sys.stderr)
|
||||
return 2
|
||||
archive = Path(args[0]).resolve()
|
||||
if not archive.is_file():
|
||||
print(f"backup not found: {archive}", file=sys.stderr)
|
||||
return 1
|
||||
from modules.backup.db_backup import run_restore_job
|
||||
|
||||
run_restore_job(archive)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,102 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
"""HTTP routes for backup module."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from flask import jsonify, request, send_file
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register(deps) -> None:
|
||||
app = deps.app
|
||||
login_required = deps.login_required
|
||||
get_setting = deps.get_setting
|
||||
|
||||
from modules.backup.db_backup import (
|
||||
RESTORE_CONFIRM_TOKEN,
|
||||
backup_dir,
|
||||
backup_in_progress,
|
||||
get_backup_last_at,
|
||||
get_restore_status,
|
||||
inspect_backup_archive,
|
||||
list_backups,
|
||||
resolve_backup_file,
|
||||
restore_in_progress,
|
||||
save_uploaded_backup,
|
||||
schedule_restore,
|
||||
)
|
||||
|
||||
@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(),
|
||||
"restore": get_restore_status(),
|
||||
"items": list_backups(),
|
||||
}
|
||||
)
|
||||
|
||||
@app.route("/api/backup/download/<filename>")
|
||||
@login_required
|
||||
def api_backup_download(filename):
|
||||
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)
|
||||
|
||||
@app.route("/api/backup/upload", methods=["POST"])
|
||||
@login_required
|
||||
def api_backup_upload():
|
||||
if backup_in_progress():
|
||||
return jsonify({"error": "备份进行中,请稍后再试"}), 409
|
||||
if restore_in_progress():
|
||||
return jsonify({"error": "恢复进行中,请稍后再试"}), 409
|
||||
upload = request.files.get("file")
|
||||
if not upload or not upload.filename:
|
||||
return jsonify({"error": "请选择备份文件"}), 400
|
||||
if not upload.filename.lower().endswith(".tar.gz"):
|
||||
return jsonify({"error": "仅支持 .tar.gz 备份包"}), 400
|
||||
try:
|
||||
name, info = save_uploaded_backup(upload.stream, upload.filename)
|
||||
return jsonify({"ok": True, "name": name, "info": info})
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
except Exception:
|
||||
logger.exception("backup upload failed")
|
||||
return jsonify({"error": "上传失败,请检查备份包是否完整"}), 500
|
||||
|
||||
@app.route("/api/backup/info/<filename>")
|
||||
@login_required
|
||||
def api_backup_info(filename):
|
||||
try:
|
||||
path = resolve_backup_file(filename)
|
||||
return jsonify(inspect_backup_archive(path))
|
||||
except (ValueError, FileNotFoundError) as exc:
|
||||
return jsonify({"error": str(exc)}), 404
|
||||
|
||||
@app.route("/api/backup/restore", methods=["POST"])
|
||||
@login_required
|
||||
def api_backup_restore():
|
||||
data = request.get_json(silent=True) or {}
|
||||
filename = (data.get("filename") or request.form.get("filename") or "").strip()
|
||||
confirm = (data.get("confirm") or request.form.get("confirm") or "").strip()
|
||||
if confirm != RESTORE_CONFIRM_TOKEN:
|
||||
return jsonify({"error": "请确认恢复操作"}), 400
|
||||
if not filename:
|
||||
return jsonify({"error": "缺少备份文件名"}), 400
|
||||
ok, msg = schedule_restore(filename)
|
||||
if ok:
|
||||
return jsonify({"ok": True, "message": msg}), 202
|
||||
return jsonify({"error": msg}), 409
|
||||
|
||||
@app.route("/api/backup/restore/status")
|
||||
@login_required
|
||||
def api_backup_restore_status():
|
||||
return jsonify(get_restore_status())
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
@@ -0,0 +1,280 @@
|
||||
# Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
# 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
|
||||
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
|
||||
|
||||
"""期货合约简介:东方财富 / 新浪 / AKShare。"""
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from modules.core.contract_specs import get_contract_spec
|
||||
from modules.core.symbols import ths_to_codes, search_symbols
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EM_LABEL_MAP = {
|
||||
"vname": "交易品种",
|
||||
"vcode": "交易代码",
|
||||
"jydw": "交易单位",
|
||||
"bjdw": "报价单位",
|
||||
"market": "交易所",
|
||||
"zxbddw": "最小变动价位",
|
||||
"zdtbfd": "涨跌停幅度",
|
||||
"hyjgyf": "合约月份",
|
||||
"jysj": "交易时间",
|
||||
"zhjyr": "最后交易日",
|
||||
"zhjgr": "交割日期",
|
||||
"jgpj": "交割品级",
|
||||
"zcjybzj": "最低交易保证金",
|
||||
"jgfs": "交割方式",
|
||||
"jgdd": "交割地点",
|
||||
"ssrq": "上市日期",
|
||||
}
|
||||
|
||||
DISPLAY_ORDER = [
|
||||
"交易品种",
|
||||
"交易代码",
|
||||
"交易单位",
|
||||
"报价单位",
|
||||
"最小变动价位",
|
||||
"最低交易保证金",
|
||||
"涨跌停幅度",
|
||||
"合约月份",
|
||||
"交易时间",
|
||||
"最后交易日",
|
||||
"交割日期",
|
||||
"交割方式",
|
||||
"交割地点",
|
||||
"交割品级",
|
||||
"上市日期",
|
||||
"交易所",
|
||||
]
|
||||
|
||||
SKIP_ITEMS = {"", "-", "None", "nan", "null"}
|
||||
|
||||
|
||||
def _normalize_ths_code(raw: str) -> Optional[str]:
|
||||
code = (raw or "").strip()
|
||||
if not code:
|
||||
return None
|
||||
# 已是完整合约
|
||||
if re.match(r"^[A-Za-z]+\d{3,4}$", code):
|
||||
return code
|
||||
# 仅品种字母时尝试匹配主力
|
||||
results = search_symbols(code)
|
||||
if results:
|
||||
return results[0].get("ths_code") or code
|
||||
codes = ths_to_codes(code)
|
||||
if codes:
|
||||
return codes["ths_code"]
|
||||
return code
|
||||
|
||||
|
||||
def _to_sina_quote_symbol(ths_code: str) -> str:
|
||||
m = re.match(r"^([A-Za-z]+)(\d+)$", ths_code.strip())
|
||||
if not m:
|
||||
return ths_code.upper()
|
||||
return m.group(1).upper() + m.group(2)
|
||||
|
||||
|
||||
def _to_em_page_symbol(ths_code: str) -> str:
|
||||
return ths_code.strip().lower() + "F"
|
||||
|
||||
|
||||
def _clean_value(val: Any) -> str:
|
||||
if val is None:
|
||||
return ""
|
||||
s = str(val).strip()
|
||||
if s in SKIP_ITEMS:
|
||||
return ""
|
||||
return s
|
||||
|
||||
|
||||
def _rows_from_dict(data: dict[str, str]) -> list[dict]:
|
||||
rows: list[dict] = []
|
||||
seen: set[str] = set()
|
||||
for label in DISPLAY_ORDER:
|
||||
val = _clean_value(data.get(label))
|
||||
if not val:
|
||||
continue
|
||||
hint = _clean_value(data.get(f"{label}_hint"))
|
||||
rows.append({"label": label, "value": val, "hint": hint})
|
||||
seen.add(label)
|
||||
for label, val in data.items():
|
||||
if label.endswith("_hint") or label in seen:
|
||||
continue
|
||||
val = _clean_value(val)
|
||||
if val:
|
||||
rows.append({"label": label, "value": val, "hint": ""})
|
||||
return rows
|
||||
|
||||
|
||||
def _add_computed_hints(ths_code: str, data: dict[str, str]) -> None:
|
||||
spec = get_contract_spec(ths_code)
|
||||
mult = spec.get("mult") or 0
|
||||
tick_raw = data.get("最小变动价位", "")
|
||||
m = re.search(r"([\d.]+)", tick_raw)
|
||||
if m and mult:
|
||||
tick = float(m.group(1))
|
||||
data["最小变动价位_hint"] = f"一手合约最小波动{round(tick * mult, 2)}元"
|
||||
|
||||
|
||||
def _fetch_em_direct(em_symbol: str) -> dict[str, str]:
|
||||
page_url = f"https://quote.eastmoney.com/qihuo/{em_symbol}.html"
|
||||
r = requests.get(page_url, timeout=12)
|
||||
r.encoding = r.apparent_encoding or "utf-8"
|
||||
inner = None
|
||||
for pat in [
|
||||
r"futures_([A-Za-z0-9_]+)",
|
||||
r"#(futures_[A-Za-z0-9_]+)",
|
||||
r"/(futures_[A-Za-z0-9_]+)",
|
||||
]:
|
||||
m = re.search(pat, r.text)
|
||||
if m:
|
||||
inner = m.group(1).replace("futures_", "")
|
||||
break
|
||||
if not inner:
|
||||
raise ValueError("无法解析东方财富合约标识")
|
||||
|
||||
info_url = f"https://futsse-static.eastmoney.com/redis?msgid={inner}_info"
|
||||
r2 = requests.get(info_url, timeout=12)
|
||||
payload = r2.json()
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("东方财富返回数据无效")
|
||||
|
||||
out: dict[str, str] = {}
|
||||
for key, label in EM_LABEL_MAP.items():
|
||||
val = _clean_value(payload.get(key))
|
||||
if val:
|
||||
out[label] = val
|
||||
if not out:
|
||||
raise ValueError("东方财富合约字段为空")
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_em_akshare(em_symbol: str) -> dict[str, str]:
|
||||
import akshare as ak
|
||||
|
||||
df = ak.futures_contract_detail_em(symbol=em_symbol)
|
||||
out: dict[str, str] = {}
|
||||
for _, row in df.iterrows():
|
||||
label = _clean_value(row.get("item"))
|
||||
val = _clean_value(row.get("value"))
|
||||
if label and val:
|
||||
if label == "跌涨停板幅度":
|
||||
label = "涨跌停幅度"
|
||||
if label == "最后交割日":
|
||||
label = "交割日期"
|
||||
if label == "上市交易所":
|
||||
label = "交易所"
|
||||
if label == "合约交割月份":
|
||||
label = "合约月份"
|
||||
if label == "最初交易保证金":
|
||||
label = "最低交易保证金"
|
||||
if label == "最小变动价格":
|
||||
label = "最小变动价位"
|
||||
out[label] = val
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_sina_direct(sina_symbol: str) -> dict[str, str]:
|
||||
from io import StringIO
|
||||
|
||||
import pandas as pd
|
||||
|
||||
url = f"https://finance.sina.com.cn/futures/quotes/{sina_symbol}.shtml"
|
||||
r = requests.get(url, timeout=12, headers={"Referer": "https://finance.sina.com.cn/"})
|
||||
r.encoding = "gb2312"
|
||||
tables = pd.read_html(StringIO(r.text))
|
||||
if len(tables) < 7:
|
||||
raise ValueError("新浪页面结构变化")
|
||||
temp_df = tables[6]
|
||||
parts = []
|
||||
for ncol in [slice(0, 2), slice(2, 4), slice(4, None)]:
|
||||
part = temp_df.iloc[:, ncol]
|
||||
part.columns = ["item", "value"]
|
||||
parts.append(part)
|
||||
merged = pd.concat(parts, axis=0, ignore_index=True)
|
||||
out: dict[str, str] = {}
|
||||
for _, row in merged.iterrows():
|
||||
label = _clean_value(row["item"])
|
||||
val = _clean_value(row["value"])
|
||||
if not label or not val or len(label) > 80 or "发帖" in val:
|
||||
continue
|
||||
out[label] = val
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_sina_akshare(sina_symbol: str) -> dict[str, str]:
|
||||
import akshare as ak
|
||||
|
||||
df = ak.futures_contract_detail(symbol=sina_symbol)
|
||||
out: dict[str, str] = {}
|
||||
for _, row in df.iterrows():
|
||||
label = _clean_value(row.get("item"))
|
||||
val = _clean_value(row.get("value"))
|
||||
if label and val and "发帖" not in val:
|
||||
out[label] = val
|
||||
return out
|
||||
|
||||
|
||||
def _merge_profile(primary: dict[str, str], secondary: dict[str, str]) -> dict[str, str]:
|
||||
merged = dict(secondary)
|
||||
merged.update(primary)
|
||||
return merged
|
||||
|
||||
|
||||
def get_contract_profile(raw_symbol: str) -> Optional[dict]:
|
||||
ths_code = _normalize_ths_code(raw_symbol)
|
||||
if not ths_code:
|
||||
return None
|
||||
|
||||
em_symbol = _to_em_page_symbol(ths_code)
|
||||
sina_symbol = _to_sina_quote_symbol(ths_code)
|
||||
data: dict[str, str] = {}
|
||||
source_parts: list[str] = []
|
||||
|
||||
# 东方财富(字段与看盘软件简介接近)
|
||||
try:
|
||||
try:
|
||||
data = _fetch_em_akshare(em_symbol)
|
||||
source_parts.append("东方财富")
|
||||
except ImportError:
|
||||
data = _fetch_em_direct(em_symbol)
|
||||
source_parts.append("东方财富")
|
||||
except Exception as exc:
|
||||
logger.warning("eastmoney profile failed %s: %s", em_symbol, exc)
|
||||
|
||||
# 新浪补充交割地点、上市日期等
|
||||
sina_data: dict[str, str] = {}
|
||||
try:
|
||||
try:
|
||||
sina_data = _fetch_sina_akshare(sina_symbol)
|
||||
except ImportError:
|
||||
sina_data = _fetch_sina_direct(sina_symbol)
|
||||
if sina_data:
|
||||
source_parts.append("新浪")
|
||||
except Exception as exc:
|
||||
logger.warning("sina profile failed %s: %s", sina_symbol, exc)
|
||||
|
||||
if sina_data:
|
||||
data = _merge_profile(data, sina_data)
|
||||
|
||||
if not data:
|
||||
return None
|
||||
|
||||
_add_computed_hints(ths_code, data)
|
||||
rows = _rows_from_dict(data)
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
return {
|
||||
"ths_code": ths_code,
|
||||
"symbol_name": data.get("交易品种", ""),
|
||||
"exchange": data.get("交易所", ""),
|
||||
"rows": rows,
|
||||
"source": " + ".join(source_parts) if source_parts else "未知",
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user