Normalize fullwidth punctuation to ASCII across codebase.

Add scripts/normalize_ambiguous_unicode.py; fix corrupted patch_instance_theme_templates.py. Preserves curly quotes in string literals; removes Git homoglyph warnings on .env.example.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-08 23:42:26 +08:00
parent aaa72c7961
commit b733e551a0
392 changed files with 71522 additions and 71369 deletions
+70 -70
View File
@@ -1,70 +1,70 @@
# AI 复盘与模型配置说明
三个 `crypto_monitor_*` 实例共用仓库根目录 **`ai_client.py`**通过 `PYTHONPATH=..` 导入)。用于 **交易记录与复盘** 页的 AI 点评短评建议以及从复盘截图提取结构化 JSON
---
## 一二选一`AI_PROVIDER`
| 值 | 说明 |
|----|------|
| **`openai`**默认 | OpenAI 兼容 **Chat Completions** 接口 |
| **`ollama`** | 本机 Ollama **`/api/generate`**流式 NDJSON |
在对应子目录 **`.env`** 中设置各所 `.env.example` 已含模板):
```bash
AI_PROVIDER=openai
AI_TIMEOUT_SECONDS=120
# OpenAI 兼容网关默认
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
# 本机 Ollama仅当 AI_PROVIDER=ollama
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
```
### OpenAI 兼容网关
- **Base URL**`https://op.bz121.com/v1`请求路径为 `{base}/chat/completions`)。
- **API Key**在 [op.bz121.com](https://op.bz121.com/) 登录后**`gateway.json`** 页面复制与网关账号一致)。
- **默认模型**`gemma4:e4b`可通过 `OPENAI_MODEL` 覆盖)。
### Ollama
- 需本机已安装并拉取对应模型`AI_PROVIDER=ollama` 时使用 `OLLAMA_API``AI_MODEL`
- 三所 `app.py` **不再** 直连 Ollama统一走 `ai_client.ai_generate` / `ai_review` / `ai_short_advice`
---
## 二部署注意
1. **PM2 / 手工启动**`ecosystem.config.cjs`**`PYTHONPATH=..`** 必须包含仓库根否则无法 `from ai_client import ...`
2. 修改 `.env` 后重启对应实例例如`pm2 restart crypto_binance`名称以你机器为准)。
3. **`git pull`** 不会改 `.env``.env.example` 新增 AI 变量请手动补进本机 `.env`
4. **勿** 将含真实 `OPENAI_API_KEY``.env` 提交 Git
---
## 三功能入口网页
登录后进入 **「交易记录与复盘」**
- 单条记录 **AI 复盘** / **短评**依赖上述配置)。
- 上传复盘图后 **从图片提取** 字段内部调用 `ai_generate`与所选 provider 一致)。
若请求超时或返回错误请检查密钥是否有效网关是否可达`AI_TIMEOUT_SECONDS` 是否过短Ollama 是否已启动仅 ollama 模式)。
---
## 四相关文件
| 路径 | 说明 |
|------|------|
| `ai_client.py` | 统一封装 OpenAI / Ollama |
| `crypto_monitor_*/.env.example` | 各所环境变量模板 |
| 各所《部署文档.md》§ AI 复盘 | 与本文一致的简表 |
| 各所《使用说明.md》 | 运行前配置中的 AI 项 |
# AI 复盘与模型配置说明
三个 `crypto_monitor_*` 实例共用仓库根目录 **`ai_client.py`**(通过 `PYTHONPATH=..` 导入).用于 **交易记录与复盘** 页的 AI 点评,短评建议,以及从复盘截图提取结构化 JSON.
---
## 一,二选一:`AI_PROVIDER`
| 值 | 说明 |
|----|------|
| **`openai`**(默认) | OpenAI 兼容 **Chat Completions** 接口 |
| **`ollama`** | 本机 Ollama **`/api/generate`**(流式 NDJSON) |
在对应子目录 **`.env`** 中设置(各所 `.env.example` 已含模板):
```bash
AI_PROVIDER=openai
AI_TIMEOUT_SECONDS=120
# OpenAI 兼容网关(默认)
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
# 本机 Ollama(仅当 AI_PROVIDER=ollama)
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
```
### OpenAI 兼容网关
- **Base URL**:`https://op.bz121.com/v1`(请求路径为 `{base}/chat/completions`).
- **API Key**:在 [op.bz121.com](https://op.bz121.com/) 登录后,**`gateway.json`** 页面复制(与网关账号一致).
- **默认模型**:`gemma4:e4b`(可通过 `OPENAI_MODEL` 覆盖).
### Ollama
- 需本机已安装并拉取对应模型;`AI_PROVIDER=ollama` 时使用 `OLLAMA_API``AI_MODEL`.
- 三所 `app.py` **不再** 直连 Ollama;统一走 `ai_client.ai_generate` / `ai_review` / `ai_short_advice`.
---
## 二,部署注意
1. **PM2 / 手工启动**:`ecosystem.config.cjs`**`PYTHONPATH=..`** 必须包含仓库根,否则无法 `from ai_client import ...`.
2. 修改 `.env` 后重启对应实例,例如:`pm2 restart crypto_binance`(名称以你机器为准).
3. **`git pull`** 不会改 `.env`;`.env.example` 新增 AI 变量,请手动补进本机 `.env`.
4. **勿** 将含真实 `OPENAI_API_KEY``.env` 提交 Git.
---
## 三,功能入口(网页)
登录后进入 **「交易记录与复盘」**:
- 单条记录 **AI 复盘** / **短评**(依赖上述配置).
- 上传复盘图后 **从图片提取** 字段(内部调用 `ai_generate`,与所选 provider 一致).
若请求超时或返回错误,请检查:密钥是否有效,网关是否可达,`AI_TIMEOUT_SECONDS` 是否过短,Ollama 是否已启动(仅 ollama 模式).
---
## 四,相关文件
| 路径 | 说明 |
|------|------|
| `ai_client.py` | 统一封装 OpenAI / Ollama |
| `crypto_monitor_*/.env.example` | 各所环境变量模板 |
| 各所《部署文档.md》§ AI 复盘 | 与本文一致的简表 |
| 各所《使用说明.md》 | 运行前配置中的 AI 项 |
+87 -87
View File
@@ -1,87 +1,87 @@
# 复盘交易系统crypto_monitor
多交易所 **USDT 永续** 的下单监控**关键位****策略交易****止盈止损 / 移动保本** 与 **AI 复盘**三所独立部署 + 可选 **中控** 聚合监控
**远程仓库**[https://git.bz121.com/dekun/crypto_monitor.git](https://git.bz121.com/dekun/crypto_monitor.git)
---
## 部署环境必读
| 项 | 约定 |
|----|------|
| 系统 | **Ubuntu 22.04 / 24.04** |
| 用户 | **root** |
| 路径 | **`/opt/crypto_monitor`** |
| 进程 | **PM2**唯一推荐的常驻方式 |
**环境详解**Python 3.10+NodePM2 安装与启动顺序):**[docs/ubuntu-server.md](./docs/ubuntu-server.md)**
**一键 venv**`bash deploy/setup_env.sh`**[deploy/README.md](./deploy/README.md)**
```bash
cd /opt
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
cd /opt/crypto_monitor
bash deploy/setup_env.sh --install-system-deps
```
配置与运维脚本 **[docs/env-sync-scripts.md](./docs/env-sync-scripts.md)** · **[备份与恢复.md](./备份与恢复.md)**
---
## 功能导航
| 功能 | 说明 | 文档 |
|------|------|------|
| **关键位监控** | 箱体/收敛自动开仓阻力支撑提醒斐波限价止盈止损方案与 **移动保本** 开关 | 各所 [关键位自动下单说明.md](./crypto_monitor_binance/关键位自动下单说明.md)Gate/OKX 目录内同名);方案细则 **[关键位止盈止损与移动保本更新说明.md](./关键位止盈止损与移动保本更新说明.md)** |
| **实盘下单 / 下单监控** | 首仓以损定仓监控内 **止盈 / 止损****移动保本**步进 R偏移% | 各所 [使用说明.md](./crypto_monitor_binance/使用说明.md) · 顶栏「实盘下单」`/trade` |
| **策略交易** | **趋势回调** + **顺势加仓**`/strategy` 双栏 | **[策略交易说明.md](./策略交易说明.md)** · 趋势细则 [docs/trend-pullback-strategy.md](./docs/trend-pullback-strategy.md) |
| **策略交易记录** | 已结束计划快照最近 100 条)、筛选与展开详情 | [策略交易说明.md §五](./策略交易说明.md) · 顶栏 `/strategy/records` |
| **交易复盘** | 平仓记录错过机会图表**AI 点评** | **[AI复盘与模型配置说明.md](./AI复盘与模型配置说明.md)** · 顶栏「交易记录与复盘」`/records` |
| **中控** | 多账户持仓/委托聚合行情 K 线紧急全平**不在中控网页下单** | [manual_trading_hub/使用说明.md](./manual_trading_hub/使用说明.md) · [部署文档.md](./manual_trading_hub/部署文档.md) |
其它专题[计仓模式](./docs/position-sizing-mode.md) · [每日自动划转](./docs/auto-transfer-daily.md) · [Chrome 快捷方式图标](./docs/shortcut-icon.md)
---
## 仓库目录
| 目录 | 交易所 / 角色 | 部署文档 |
|------|----------------|----------|
| `crypto_monitor_binance/` | Binance U 本位永续 | [部署文档.md](./crypto_monitor_binance/部署文档.md) |
| `crypto_monitor_gate/` | Gate | [部署文档.md](./crypto_monitor_gate/部署文档.md) |
| `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) |
| `manual_trading_hub/` | 中控 + 子代理 | [部署文档.md](./manual_trading_hub/部署文档.md) |
| `lib/` | **共用模块**策略关键位交易中控库AI静态与模板 | **[docs/lib-structure.md](./docs/lib-structure.md)** |
| `brand/` | 各所共用图标与 manifest | — |
| `docs/``deploy/``scripts/``tests/` | 文档环境脚本单元测试 | — |
共用代码 import 示例`from lib.strategy.strategy_db import init_strategy_tables`各所启动时仍将仓库根加入 `PYTHONPATH`)。详见 **[docs/lib-structure.md](./docs/lib-structure.md)**
---
## 技术要点
- **Python 3.10+**FlaskccxtSQLite`crypto.db`
- 三所 `.env` 前缀不同`BINANCE_*` / `GATE_*` / `OKX_*`),**不可混用**
- 实盘须 `LIVE_TRADING_ENABLED=true` 且理解 API 权限与 IP 白名单风险
-**SOCKS** 访问交易所时配置各所 `*_SOCKS_PROXY` 并安装 PySocks
---
## 推荐阅读顺序
1. [docs/ubuntu-server.md](./docs/ubuntu-server.md) — 装 Python / Node / PM2PM2 启动三所 + 中控
2. 各所 **`.env`**`.env.example` 复制
3. 所用功能对应上表 **功能导航** 文档
4. [备份与恢复.md](./备份与恢复.md) — 生产机备份习惯
---
## 安全
- **勿** 将 `.env`API Secret`.pem` 提交 Git
- 公网暴露中控须配置登录`HUB_BRIDGE_TOKEN`HTTPS Cookie
- 实盘风险由使用者自行承担
若子目录 README 与本文冲突**子目录《部署文档》与当前代码** 为准
# 复盘交易系统(crypto_monitor)
多交易所 **USDT 永续** 的下单监控,**关键位**,**策略交易**,**止盈止损 / 移动保本** 与 **AI 复盘**,三所独立部署 + 可选 **中控** 聚合监控.
**远程仓库**:[https://git.bz121.com/dekun/crypto_monitor.git](https://git.bz121.com/dekun/crypto_monitor.git)
---
## 部署环境(必读)
| 项 | 约定 |
|----|------|
| 系统 | **Ubuntu 22.04 / 24.04** |
| 用户 | **root** |
| 路径 | **`/opt/crypto_monitor`** |
| 进程 | **PM2**(唯一推荐的常驻方式) |
**环境详解**(Python 3.10+,Node,PM2 安装与启动顺序):**[docs/ubuntu-server.md](./docs/ubuntu-server.md)**
**一键 venv**:`bash deploy/setup_env.sh`**[deploy/README.md](./deploy/README.md)**
```bash
cd /opt
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
cd /opt/crypto_monitor
bash deploy/setup_env.sh --install-system-deps
```
配置与运维脚本: **[docs/env-sync-scripts.md](./docs/env-sync-scripts.md)** · **[备份与恢复.md](./备份与恢复.md)**
---
## 功能导航
| 功能 | 说明 | 文档 |
|------|------|------|
| **关键位监控** | 箱体/收敛自动开仓,阻力支撑提醒,斐波限价;止盈止损方案与 **移动保本** 开关 | 各所 [关键位自动下单说明.md](./crypto_monitor_binance/关键位自动下单说明.md)(Gate/OKX 目录内同名);方案细则 **[关键位止盈止损与移动保本更新说明.md](./关键位止盈止损与移动保本更新说明.md)** |
| **实盘下单 / 下单监控** | 首仓,以损定仓;监控内 **止盈 / 止损**,**移动保本**(步进 R,偏移%) | 各所 [使用说明.md](./crypto_monitor_binance/使用说明.md) · 顶栏「实盘下单」`/trade` |
| **策略交易** | **趋势回调** + **顺势加仓**(`/strategy` 双栏) | **[策略交易说明.md](./策略交易说明.md)** · 趋势细则 [docs/trend-pullback-strategy.md](./docs/trend-pullback-strategy.md) |
| **策略交易记录** | 已结束计划快照(最近 100 条),筛选与展开详情 | [策略交易说明.md §五](./策略交易说明.md) · 顶栏 `/strategy/records` |
| **交易复盘** | 平仓记录,错过机会,图表;**AI 点评** | **[AI复盘与模型配置说明.md](./AI复盘与模型配置说明.md)** · 顶栏「交易记录与复盘」`/records` |
| **中控** | 多账户持仓/委托聚合,行情 K 线,紧急全平(**不在中控网页下单**) | [manual_trading_hub/使用说明.md](./manual_trading_hub/使用说明.md) · [部署文档.md](./manual_trading_hub/部署文档.md) |
其它专题:[计仓模式](./docs/position-sizing-mode.md) · [每日自动划转](./docs/auto-transfer-daily.md) · [Chrome 快捷方式图标](./docs/shortcut-icon.md)
---
## 仓库目录
| 目录 | 交易所 / 角色 | 部署文档 |
|------|----------------|----------|
| `crypto_monitor_binance/` | Binance U 本位永续 | [部署文档.md](./crypto_monitor_binance/部署文档.md) |
| `crypto_monitor_gate/` | Gate | [部署文档.md](./crypto_monitor_gate/部署文档.md) |
| `crypto_monitor_okx/` | OKX 永续 | [部署文档.md](./crypto_monitor_okx/部署文档.md) |
| `manual_trading_hub/` | 中控 + 子代理 | [部署文档.md](./manual_trading_hub/部署文档.md) |
| `lib/` | **共用模块**(策略,关键位,交易,中控库,AI,静态与模板) | **[docs/lib-structure.md](./docs/lib-structure.md)** |
| `brand/` | 各所共用图标与 manifest | — |
| `docs/`,`deploy/`,`scripts/`,`tests/` | 文档,环境,脚本,单元测试 | — |
共用代码 import 示例:`from lib.strategy.strategy_db import init_strategy_tables`(各所启动时仍将仓库根加入 `PYTHONPATH`).详见 **[docs/lib-structure.md](./docs/lib-structure.md)**.
---
## 技术要点
- **Python 3.10+**,Flask,ccxt,SQLite(`crypto.db`)
- 三所 `.env` 前缀不同(`BINANCE_*` / `GATE_*` / `OKX_*`),**不可混用**
- 实盘须 `LIVE_TRADING_ENABLED=true` 且理解 API 权限与 IP 白名单风险
-**SOCKS** 访问交易所时配置各所 `*_SOCKS_PROXY` 并安装 PySocks
---
## 推荐阅读顺序
1. [docs/ubuntu-server.md](./docs/ubuntu-server.md) — 装 Python / Node / PM2,PM2 启动三所 + 中控
2. 各所 **`.env`**(`.env.example` 复制)
3. 所用功能对应上表 **功能导航** 文档
4. [备份与恢复.md](./备份与恢复.md) — 生产机备份习惯
---
## 安全
- **勿** 将 `.env`,API Secret,`.pem` 提交 Git
- 公网暴露中控须配置登录,`HUB_BRIDGE_TOKEN`,HTTPS Cookie
- 实盘风险由使用者自行承担
若子目录 README 与本文冲突,**子目录《部署文档》与当前代码** 为准.
+84 -84
View File
@@ -1,157 +1,157 @@
# =============================================================================
# 环境配置模板可提交 Git)。程序运行时只读取同目录下的 .env
# 环境配置模板(可提交 Git).程序运行时只读取同目录下的 .env.
#
# 首次部署 / 新机
# 首次部署 / 新机:
# cp .env.example .env
# nano .env # 填入真实密钥端口代理等
# nano .env # 填入真实密钥,端口,代理等
#
# 升级代码git pull前建议备份.env 不在 Git 中pull 不会覆盖):
# 升级代码(git pull)前建议备份(.env 不在 Git 中,pull 不会覆盖):
# cp .env .env.backup.$(date +%Y%m%d)
#
# 从备份恢复
# 从备份恢复:
# cp .env.backup.YYYYMMDD .env
# =============================================================================
APP_ENV=production
# 服务监听地址云服务器通常用 0.0.0.0
# 服务监听地址(云服务器通常用 0.0.0.0)
APP_HOST=0.0.0.0
# 服务端口
APP_PORT=5001
# 是否开启调试模式生产建议 false
# 是否开启调试模式(生产建议 false)
APP_DEBUG=false
# 登录账号
APP_USERNAME=admin
# 登录密码请改成你自己的强密码
# 登录密码(请改成你自己的强密码)
APP_PASSWORD=admin123
# 是否关闭登录校验局域网可设 true公网务必 false
# 是否关闭登录校验(局域网可设 true;公网务必 false)
APP_AUTH_DISABLED=true
# --- 多账户交易中控 manual_trading_hub ---
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时仅网页登录后可访问本机联调可保持 APP_AUTH_DISABLED=true
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true
# HUB_BRIDGE_TOKEN=your-long-random-token
# Flask 会话密钥必须替换为长随机字符串
# Flask 会话密钥(必须替换为长随机字符串)
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
# 企业微信机器人 Webhook用于行情/风控推送
# 企业微信机器人 Webhook(用于行情/风控推送)
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_WITH_REAL_KEY
# 数据库文件路径相对路径会自动按项目目录解析
# 数据库文件路径(相对路径会自动按项目目录解析)
DB_PATH=crypto.db
# 交易截图上传目录
UPLOAD_DIR=static/images
# 自动备份scripts/backup_data.sh + cron可选默认即可
# 自动备份(scripts/backup_data.sh + cron,可选;默认即可)
# BACKUP_ROOT=/root/backups
# BACKUP_RETENTION_DAYS=30
# BACKUP_INSTANCE=crypto_monitor_binance
# 已废弃资金账户仅显示交易所 funding 余额不再读取此变量
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
# TOTAL_CAPITAL=100
# 页顶「资金账户」默认仅 Binance Funding 钱包若 USDT 主要在现货可改为 true 合并 Spot
# 页顶「资金账户」默认仅 Binance Funding 钱包;若 USDT 主要在现货,可改为 true 合并 Spot
# BINANCE_FUNDING_INCLUDE_SPOT=false
# 计仓risk=以损定仓默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆须无仓后重启
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk
# 方向限制默认 false=双向均可true 时按 TRADE_DIRECTION 限制修改后须重启
# TRADE_DIRECTION=long_only | short_only | both或 多/空/双向
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单默认 false=全币种可手输true 时关键位/下单/策略仅下拉选择
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数U
# 每天起始基数(U)
DAILY_START_CAPITAL=30
# 日内回撤后基数U
# 日内回撤后基数(U)
DAILY_LOSS_CAPITAL=20
# 日内盈利后基数U
# 日内盈利后基数(U)
DAILY_PROFIT_CAPITAL=50
# BTC 默认杠杆倍数
BTC_LEVERAGE=10
# 山寨币默认杠杆倍数
ALT_LEVERAGE=5
# 交易日重置小时北京时间
# 交易日重置小时(北京时间)
TRADING_DAY_RESET_HOUR=8
# 整点前禁止新开仓true=启用默认),false=关闭仍可保留 8 点作为交易日划分
# 整点前禁止新开仓:true=启用(默认),false=关闭(仍可保留 8 点作为交易日划分)
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
# 是否开启 Binance 实盘下单false=只做本地流程true=真实下单
# 是否开启 Binance 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true
# Binance API Key需开通合约万向划转等权限
# Binance API Key(需开通合约,万向划转等权限)
BINANCE_API_KEY=REPLACE_WITH_BINANCE_API_KEY
# Binance API Secret
BINANCE_API_SECRET=REPLACE_WITH_BINANCE_API_SECRET
# 保证金模式cross=全仓isolated=逐仓
# 保证金模式:cross=全仓,isolated=逐仓
BINANCE_MARGIN_MODE=cross
# 持仓模式hedge=双向需账户开启双向持仓下单带 positionSide);oneway=单向
# 持仓模式:hedge=双向(需账户开启双向持仓,下单带 positionSide);oneway=单向
BINANCE_POSITION_MODE=hedge
# 条件单触发参考价CONTRACT_PRICE=最新成交价 MARK_PRICE=标记价更易触发时用标记价
# 条件单触发参考价:CONTRACT_PRICE=最新成交价 MARK_PRICE=标记价(更易触发时用标记价)
BINANCE_TRIGGER_WORKING_TYPE=CONTRACT_PRICE
# 页面与浏览器标签展示的交易所名称多环境区分时可改成例如 Binance·测试网
# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Binance·测试网)
# EXCHANGE_DISPLAY_NAME=Binance
# 企业微信推送里展示的账户备注
# BINANCE_ACCOUNT_LABEL=binance实盘账户
# 平仓盈亏估算false=按仓位历史口径已实现盈亏+手续费不含资金费);true=含资金费
# 平仓盈亏估算:false=按仓位历史口径(已实现盈亏+手续费,不含资金费);true=含资金费
# BINANCE_PNL_INCLUDE_FUNDING=false
# =============================================================================
# 关键位程序自动下单与 POSITION_SIZING_MODE 联动修改后须重启 PM2
# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2)
# =============================================================================
# 默认 false = 关闭所有关键位程序自动单箱体/收敛/斐波/假突破/触价
# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价)
#
# POSITION_SIZING_MODE=risk以损定仓
# false → 不执行任何关键位自动单支撑/阻力提醒人工下单顺势加仓不受影响
# true → 允许关键位全套自动含触价
# POSITION_SIZING_MODE=risk(以损定仓)
# false → 不执行任何关键位自动单;支撑/阻力提醒,人工下单,顺势加仓不受影响
# true → 允许关键位全套自动(含触价)
#
# POSITION_SIZING_MODE=full_margin全仓杠杆须无仓切换
# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换)
# false → 不执行触价自动单
# true → 仅回调/突破触价可程序自动开仓箱体/斐波等仍禁止
# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止
#
# 顺势加仓趋势回调不受本开关控制全仓模式下策略自动仍禁止
# 顺势加仓,趋势回调不受本开关控制;全仓模式下策略自动仍禁止.
KEY_AUTO_ORDER_ENABLED=false
# =============================================================================
# 关键位门控页面「关键位监控」规则条与 _key_hard_checks 共用
# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用)
# =============================================================================
# 【周期】门控 K 线周期如 5m15m仅影响关键位硬条件不改变顶栏分区
# 【周期】门控 K 线周期,如 5m,15m;仅影响关键位硬条件,不改变顶栏分区
KLINE_TIMEFRAME=5m
# 【确认K】闭合 K 序列中的棒偏移突破棒默认 -2倒数第2根),确认棒默认 -1倒数第1根
# 【确认K】闭合 K 序列中的棒偏移:突破棒默认 -2(倒数第2根),确认棒默认 -1(倒数第1根)
KEY_CONFIRM_BREAKOUT_BAR=-2
KEY_CONFIRM_BAR=-1
# 【量能】突破棒成交量 > 前 N 根均量 × 倍数默认 N=20倍数=1.3 即放大 30%
# 【量能】突破棒成交量 > 前 N 根均量 × 倍数(默认 N=20,倍数=1.3 即放大 30%)
KEY_VOLUME_MA_BARS=20
KEY_VOLUME_RATIO_MIN=1.3
# 【箱体/收敛】突破K收盘越过关键位占该侧价格%的下限无上限过猛由计划RR过滤
# 【箱体/收敛】突破K收盘越过关键位(占该侧价格%)的下限;无上限(过猛由计划RR过滤)
KEY_BREAKOUT_AMP_MIN_PCT=0.03
# 已不参与门控可保留配置项兼容旧环境
# 已不参与门控,可保留配置项兼容旧环境
KEY_BREAKOUT_AMP_MAX_PCT=0.5
# 【阻力/支撑】突破后微信提醒次数与间隔分钟
# 【阻力/支撑】突破后微信提醒次数与间隔(分钟)
KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5
# 【日成交量排名】品种须在该排名前 N 名添加关键位与运行时门控均校验
# 【日成交量排名】品种须在该排名前 N 名(添加关键位与运行时门控均校验)
KEY_DAILY_VOLUME_RANK_MAX=30
# 【关键位自动开仓盈亏比】按确认K收盘 E 计算严格大于该值才市价开仓如 1.5 表示须 >1.5:1
# 【关键位自动开仓盈亏比】按确认K收盘 E 计算,严格大于该值才市价开仓(如 1.5 表示须 >1.5:1)
KEY_AUTO_MIN_PLANNED_RR=1.5
# 止损突破 K 极值向外缓冲的百分比默认 0.5 即 0.5%
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
# 趋势单方案止损在突破 K 极值外侧的百分比默认 1 即 1%
# 趋势单方案:止损在突破 K 极值外侧的百分比(默认 1 即 1%)
KEY_TREND_STOP_OUTSIDE_PCT=1
# =============================================================================
# 交易执行 / 人工风控页面「实盘下单」
# 交易执行 / 人工风控(页面「实盘下单」)
# =============================================================================
# 【最大同时持仓】active 订单数达到该值后禁止人工与关键位自动再加仓默认 1=单仓
# 【最大同时持仓】active 订单数达到该值后禁止人工与关键位自动再加仓(默认 1=单仓)
MAX_ACTIVE_POSITIONS=1
# 【人工下单最低盈亏比】按当前价与 SL/TP 计算低于该值前后端均拒绝默认 1.4即须 >=1.4:1
# 【人工下单最低盈亏比】按当前价与 SL/TP 计算,低于该值前后端均拒绝(默认 1.4,即须 >=1.4:1)
MANUAL_MIN_PLANNED_RR=1.4
# 【关键位连开计仓】true=已有持仓时关键位自动单仍按「无仓时」资金快照算保证金基数
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒不拦单
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒(不拦单)
DAILY_OPEN_ALERT_THRESHOLD=5
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日(北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
DAILY_OPEN_HARD_LIMIT=0
# =============================================================================
# 账户冷静期 / 日冻结风控手动平仓外部平仓复盘情绪标签
# 账户冷静期 / 日冻结风控(手动平仓,外部平仓,复盘情绪标签)
# 详见 docs/account-risk-cooldown.md
# =============================================================================
RISK_CONTROL_ENABLED=true
@@ -160,74 +160,74 @@ RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
RISK_MOOD_ISSUES_DAILY_FREEZE=true
# 资金与仓位刷新周期(秒)
# 资金与仓位刷新周期(秒)
BALANCE_REFRESH_SECONDS=60
# 前端价格快照轮询(秒)
# 前端价格快照轮询(秒)
PRICE_REFRESH_SECONDS=5
# 后台监控轮询周期(秒)
# 后台监控轮询周期(秒)
MONITOR_POLL_SECONDS=3
# 重启后多少秒内不做「外部平仓」同步避免 API 未就绪误判
# 重启后多少秒内不做「外部平仓」同步(避免 API 未就绪误判)
RECONCILE_STARTUP_GRACE_SEC=90
# 连续多少次轮询确认交易所空仓后才记为外部平仓默认 3 次 ≈ 9 秒
# 连续多少次轮询确认交易所空仓后,才记为外部平仓(默认 3 次 ≈ 9 秒)
RECONCILE_FLAT_CONFIRM_POLLS=3
# 使用可用资金时的缓冲比例如0.98代表用98%
# 使用可用资金时的缓冲比例(如0.98代表用98%)
FULL_MARGIN_BUFFER_RATIO=0.98
# =============================================================================
# 自动划转页顶「将 swap 补足到 XU」与 DAILY_START_CAPITAL 独立需一致时请设为相同值
# 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值)
# =============================================================================
AUTO_TRANSFER_ENABLED=false
# 交易账户(swap)目标余额 U每日 8 点(北京)自动划入或划出至 funding持仓中不划转
# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转
AUTO_TRANSFER_AMOUNT=30
AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap
TRANSFER_CCY=USDT
# 北京时间该整点小时内尝试账簿按 UTC 自然日去重
# 北京时间该整点小时内尝试;账簿按 UTC 自然日去重
AUTO_TRANSFER_BJ_HOUR=8
# 强制清仓整点北京时间默认 0=凌晨00点
# 强制清仓整点(北京时间,默认 0=凌晨00点)
FORCE_CLOSE_BJ_HOUR=0
# 是否启用强制清仓默认关闭true 才会在整点执行
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
FORCE_CLOSE_ENABLED=false
# 推送与AI超时(秒)
# 推送与AI超时(秒)
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 提供方openai默认OpenAI 兼容网关| ollama本机 Ollama
# AI 提供方:openai(默认,OpenAI 兼容网关)| ollama(本机 Ollama)
AI_PROVIDER=openai
# OpenAI 兼容接口示例https://op.bz121.com/v1 账号见 gateway.json
# OpenAI 兼容接口(示例:https://op.bz121.com/v1 ,账号见 gateway.json)
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
# 本机 OllamaAI_PROVIDER=ollama 时使用
# 本机 Ollama(AI_PROVIDER=ollama 时使用)
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# Binance 代理可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
# 1) 先在本机建立隧道示例):
# Binance 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
# 1) 先在本机建立隧道(示例):
# ssh -N -D 127.0.0.1:1080 user@vps -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes
# 2) 再启用下面这一行推荐 socks5h让远端解析域名):
# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名):
# BINANCE_SOCKS_PROXY=socks5h://127.0.0.1:1080
#
# 如你更偏向 HTTP 代理VPS 上跑 tinyproxy 之类),可用
# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用:
# BINANCE_HTTP_PROXY=http://127.0.0.1:3128
# BINANCE_HTTPS_PROXY=http://127.0.0.1:3128
# 开仓多周期K线图可选
# 开仓多周期K线图(可选)
# ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts
# 详见上文 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT说明文档 docs/daily-open-limit.md
# 以损定仓按交易账户资金的百分比
# 详见上文 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT;说明文档 docs/daily-open-limit.md
# 以损定仓(按交易账户资金的百分比)
# RISK_PERCENT=2
# 移动保本触发达到多少R触发与偏移百分比
# 移动保本触发(达到多少R触发)与偏移(百分比)
# BREAKEVEN_RR_TRIGGER=1.0
# 移动保本阶梯每多少R继续上移一次默认1R
# 移动保本阶梯(每多少R继续上移一次,默认1R)
# BREAKEVEN_STEP_R=1.0
# BREAKEVEN_OFFSET_PCT=0.02
# 开单风格默认值trend / swing
# 开单风格默认值:trend / swing
# DEFAULT_TRADE_STYLE=trend
APP_TIMEZONE=Asia/Shanghai
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点默认 8 点起算新交易日开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
+31 -31
View File
@@ -1,22 +1,22 @@
# crypto_monitor_binance
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统行情与实盘接口统一走 **BinanceUSDT-M 永续**通过 **ccxt** 访问
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Binance(USDT-M 永续)**,通过 **ccxt** 访问.
## 功能概要
- **关键位监控**价格与硬条件校验企业微信推送可选
- **下单监控**本地风控含移动保本逻辑)、触达止盈/止损后尝试市价平仓并记账
- **策略交易**顶栏 `/strategy`趋势回调 + 顺势加仓),见仓库根 [策略交易说明.md](../策略交易说明.md)
- **AI 复盘**OpenAI 兼容网关默认或 Ollama见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)
- **实盘可选**`LIVE_TRADING_ENABLED=true` 且配置 `BINANCE_API_KEY` / `BINANCE_API_SECRET`支持合约开仓平仓余额读取与内部划转依赖 API 权限
- **止盈止损Binance**市价成交后挂 **`STOP_MARKET`**止损)、**`TAKE_PROFIT_MARKET`**止盈);双向持仓带 `positionSide`不显式传 `reduceOnly`避免 API `-1106`)。触发参考价由 `BINANCE_TRIGGER_WORKING_TYPE` 控制最新价 / 标记价
- **关键位监控**:价格与硬条件校验,企业微信推送(可选)
- **下单监控**:本地风控(含移动保本逻辑),触达止盈/止损后尝试市价平仓并记账
- **策略交易**:顶栏 `/strategy`(趋势回调 + 顺势加仓),见仓库根 [策略交易说明.md](../策略交易说明.md)
- **AI 复盘**:OpenAI 兼容网关(默认)或 Ollama,见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)
- **实盘(可选)**:`LIVE_TRADING_ENABLED=true` 且配置 `BINANCE_API_KEY` / `BINANCE_API_SECRET`,支持合约开仓,平仓,余额读取与内部划转(依赖 API 权限)
- **止盈止损(Binance)**:市价成交后挂 **`STOP_MARKET`**(止损),**`TAKE_PROFIT_MARKET`**(止盈);双向持仓带 `positionSide`;不显式传 `reduceOnly`(避免 API `-1106`).触发参考价由 `BINANCE_TRIGGER_WORKING_TYPE` 控制(最新价 / 标记价)
## 环境要求
- Python 3.10+建议
- 依赖`flask``requests``ccxt``werkzeug``Pillow`K 线图可选);经 SOCKS 代理时需 **`PySocks`**
- Python 3.10+(建议)
- 依赖:`flask`,`requests`,`ccxt`,`werkzeug`,`Pillow`(K 线图可选);经 SOCKS 代理时需 **`PySocks`**
安装示例
安装示例:
```bash
# 推荐在 /opt/crypto_monitor 执行仓库根目录 deploy/setup_env.sh
@@ -25,31 +25,31 @@ source .venv/bin/activate
pip install -r ../requirements.txt
```
页面上的 **「当日资金(交易账户)」** 与 **「可开仓」可用 U** 仅统计 **Binance U 本位永续合约账户**`fetch_balance``swap` / FAPI `assets` 中的 USDT),**不会**再用现货余额顶替
页面上的 **「当日资金(交易账户)」** 与 **「可开仓」可用 U** 仅统计 **Binance U 本位永续合约账户**(`fetch_balance``swap` / FAPI `assets` 中的 USDT),**不会**再用现货余额顶替.
## 配置说明`.env.example` → `.env`
## 配置说明(`.env.example` → `.env`)
- **`.env.example`**模板可提交 Git);首次`cp .env.example .env` 后编辑
- **`.env`**本机真实配置勿提交);`app.py` 只读此文件`git pull` 不覆盖 `.env`升级前可 `cp .env .env.backup.$(date +%Y%m%d)`
- **`.env.example`**:模板(可提交 Git);首次:`cp .env.example .env` 后编辑.
- **`.env`**:本机真实配置(勿提交);`app.py` 只读此文件.`git pull` 不覆盖 `.env`;升级前可 `cp .env .env.backup.$(date +%Y%m%d)`.
与 Binance 相关的常用变量
与 Binance 相关的常用变量:
| 变量 | 说明 |
|------|------|
| `BINANCE_API_KEY` / `BINANCE_API_SECRET` | 币安 API需合约等权限 |
| `LIVE_TRADING_ENABLED` | `true` 时允许真实下单`false` 仅本地逻辑 |
| `BINANCE_API_KEY` / `BINANCE_API_SECRET` | 币安 API(需合约等权限) |
| `LIVE_TRADING_ENABLED` | `true` 时允许真实下单;`false` 仅本地逻辑 |
| `BINANCE_MARGIN_MODE` | `cross` 全仓 / `isolated` 逐仓 |
| `BINANCE_POSITION_MODE` | `hedge` 双向需账户开启双向持仓/ `oneway` 单向 |
| `BINANCE_TRIGGER_WORKING_TYPE` | `CONTRACT_PRICE``MARK_PRICE`条件单触发参考 |
| `BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY` | 可选代理与部署文档一致 |
| `EXCHANGE_DISPLAY_NAME` | 页面展示的交易所名称默认 `Binance` |
| `BINANCE_POSITION_MODE` | `hedge` 双向(需账户开启双向持仓)/ `oneway` 单向 |
| `BINANCE_TRIGGER_WORKING_TYPE` | `CONTRACT_PRICE``MARK_PRICE`(条件单触发参考) |
| `BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY` | 可选代理(与部署文档一致) |
| `EXCHANGE_DISPLAY_NAME` | 页面展示的交易所名称,默认 `Binance` |
| `BINANCE_ACCOUNT_LABEL` | 推送文案中的账户备注 |
其余变量登录企业微信风控参数**`AI_PROVIDER` / `OPENAI_*` / `OLLAMA_*`**数据库路径等**`.env.example` 内注释** 或 `app.py` 顶部默认值
其余变量(登录,企业微信,风控参数,**`AI_PROVIDER` / `OPENAI_*` / `OLLAMA_*`**,数据库路径等)**`.env.example` 内注释** 或 `app.py` 顶部默认值.
## 运行
生产环境使用 **PM2**`ecosystem.config.cjs`)。临时调试
生产环境使用 **PM2**(`ecosystem.config.cjs`).临时调试:
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
@@ -57,13 +57,13 @@ source .venv/bin/activate
python app.py
```
环境说明见 [docs/ubuntu-server.md](../docs/ubuntu-server.md)
环境说明见 [docs/ubuntu-server.md](../docs/ubuntu-server.md).
默认监听端口由 `.env``APP_PORT` 决定未设置时多为 `5000`)。
默认监听端口由 `.env``APP_PORT` 决定(未设置时多为 `5000`).
## 部署Linux / PM2 / SSH SOCKS
## 部署(Linux / PM2 / SSH SOCKS)
详见 **[部署文档.md](./部署文档.md)**Ubuntu + PM2 + 可选 SOCKS 访问 Binance)。
详见 **[部署文档.md](./部署文档.md)**(Ubuntu + PM2 + 可选 SOCKS 访问 Binance).
## 自检脚本
@@ -71,13 +71,13 @@ python app.py
python scripts/verify_binance_funding.py
```
用于核对 Key 前缀不含 Secret并尝试读取资金钱包 / 合约钱包 USDT需网络与 API 权限)。
用于核对 Key 前缀(不含 Secret)并尝试读取资金钱包 / 合约钱包 USDT(需网络与 API 权限).
## 数据与脚本
- 默认 SQLite`crypto.db`路径由 `DB_PATH` 指定
- `scripts/fix_breakeven_labels.py`批量修正「止损」但盈亏为正的记录标签见部署文档附录
- 默认 SQLite:`crypto.db`(路径由 `DB_PATH` 指定)
- `scripts/fix_breakeven_labels.py`:批量修正「止损」但盈亏为正的记录标签(见部署文档附录)
## 风险与合规
实盘交易有亏损风险请自行确认 API 权限IP 白名单杠杆与保证金模式与币安账户设置一致并遵守当地法律法规与 Binance 用户协议
实盘交易有亏损风险.请自行确认 API 权限,IP 白名单,杠杆与保证金模式与币安账户设置一致,并遵守当地法律法规与 Binance 用户协议.
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,14 +1,14 @@
/**
* PM2 进程定义Ubuntu / Linux)。
* PM2 进程定义(Ubuntu / Linux).
*
* 仅托管 Flask 应用**SSH SOCKS 隧道**用 `ssh -D` 常驻可用 tmux / autossh),勿交给 PM2
* 与 `.env` 里 `BINANCE_SOCKS_PROXY` 端口一致即可不必交给 PM2
* 仅托管 Flask 应用.**SSH SOCKS 隧道**用 `ssh -D` 常驻(可用 tmux / autossh),勿交给 PM2.
* 与 `.env` 里 `BINANCE_SOCKS_PROXY` 端口一致即可;不必交给 PM2.
*
* 使用前项目根目录存在 `.venv`且已安装依赖走 SOCKS 时需 PySocks)。
* 使用前:项目根目录存在 `.venv`,且已安装依赖(走 SOCKS 时需 PySocks).
*
* 启动
* 启动:
* pm2 start ecosystem.config.cjs
* 保存开机列表
* 保存开机列表:
* pm2 save && pm2 startup
*/
const path = require("path");
@@ -1,18 +1,18 @@
#!/usr/bin/env python3
"""
一次性修复历史交易记录标签
将 trade_records 里“止损但实际盈利”的记录改为“保本止盈”
一次性修复历史交易记录标签:
将 trade_records 里“止损但实际盈利”的记录改为“保本止盈”.
默认条件可通过参数修改):
默认条件(可通过参数修改):
- monitor_type = 下单监控
- result = 止损
- pnl_amount > 0
用法示例
1) 仅预览不落库):
用法示例:
1) 仅预览(不落库):
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
2) 执行修复
2) 执行修复:
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
"""
@@ -115,15 +115,15 @@ def build_section(order_loop: str) -> str:
{{% endif %}}
</{t}>
<{t} class="rule-tip" id="order-rule-tip">
规则最多 {{{{ max_active_positions }}}}BTC {{{{ btc_leverage }}}}x / 山寨 {{{{ alt_leverage }}}}x
{{% if can_trade %}}可开仓{{% else %}}不可开仓持仓已满或未到北京时间 {{{{ reset_hour }}}}:00{{% endif %}}
规则:最多 {{{{ max_active_positions }}}};BTC {{{{ btc_leverage }}}}x / 山寨 {{{{ alt_leverage }}}}x;
{{% if can_trade %}}可开仓{{% else %}}不可开仓(持仓已满或未到北京时间 {{{{ reset_hour }}}}:00){{% endif %}};
人工开仓盈亏比不得低于 {{{{ manual_min_planned_rr }}}}:1
</{t}>
<{t} class="rule-tip">
以损定仓风险 {{{{ risk_percent }}}}% |移动保本下单可勾选关闭开启时 {{{{ breakeven_rr_trigger }}}}R 触发每 1R 阶梯上移),偏移 {{{{ breakeven_offset_pct }}}}%
以损定仓:风险 {{{{ risk_percent }}}}% |移动保本:下单可勾选关闭;开启时 {{{{ breakeven_rr_trigger }}}}R 触发(每 1R 阶梯上移),偏移 {{{{ breakeven_offset_pct }}}}%
</{t}>
<{t} class="rule-tip">
划转自动划转 {{{{ '开启' if auto_transfer_enabled else '关闭' }}}}每天<strong>北京时间 {{{{ auto_transfer_bj_hour }}}}:00</strong>起该整点小时内尝试账簿按 <strong>UTC 自然日</strong>去重界面时间为北京{{{{ auto_transfer_to }}}} 补足到 {{{{ auto_transfer_amount }}}}U来自 {{{{ auto_transfer_from }}}}
划转:自动划转 {{{{ '开启' if auto_transfer_enabled else '关闭' }}}}(每天<strong>北京时间 {{{{ auto_transfer_bj_hour }}}}:00</strong>起该整点小时内尝试;账簿按 <strong>UTC 自然日</strong>去重;界面时间为北京;{{{{ auto_transfer_to }}}} 补足到 {{{{ auto_transfer_amount }}}}U,来自 {{{{ auto_transfer_from }}}})
</{t}>
<form action="/manual_transfer" method="post" class="form-row">
<input name="amount" type="number" min="0.01" step="0.01" placeholder="手动划转金额U" required>
@@ -145,8 +145,8 @@ def build_section(order_loop: str) -> str:
<option value="">方向</option><option value="long">做多</option><option value="short">做空</option>
</select>
<select id="sltp-mode" name="sltp_mode">
<option value="price">止盈止损价格模式</option>
<option value="pct">止盈止损百分比模式</option>
<option value="price">止盈止损:价格模式</option>
<option value="pct">止盈止损:百分比模式</option>
</select>
<select name="trade_style" required>
<option value="trend">趋势单</option>
@@ -154,17 +154,17 @@ def build_section(order_loop: str) -> str:
</select>
<input id="order-leverage" name="leverage" type="number" min="1" step="1" placeholder="杠杆(可选)">
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本关闭则仅保留初始止损与交易所挂单
<input type="checkbox" name="breakeven_enabled" value="1" checked> 启用移动保本(关闭则仅保留初始止损与交易所挂单)
</label>
<label style="display:flex;align-items:center;gap:4px;font-size:.82rem;color:#cfd3ef">
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图各周期100根含开平仓标记
<input type="checkbox" name="order_chart" value="true"> 开仓后生成多周期K线图(各周期100根,含开平仓标记)
</label>
<span style="display:flex;align-items:center;padding:0 10px;font-size:.8rem;color:#8fc8ff">成交价自动取交易所实时+成交回报</span>
<input id="order-sl" name="sl" step="any" placeholder="止损价格" required>
<input id="order-tp" name="tgt" step="any" placeholder="止盈价格" required>
<input id="order-sl-pct" name="sl_pct" type="number" min="0.01" step="0.01" placeholder="止损%" style="display:none">
<input id="order-tp-pct" name="tp_pct" type="number" min="0.01" step="0.01" placeholder="止盈%" style="display:none">
<button type="submit">开仓以损定仓</button>
<button type="submit">开仓(以损定仓)</button>
</form>
</{t}>
<{t} class="card">
@@ -218,7 +218,7 @@ if(addOrderForm){
const mode = (document.getElementById("sltp-mode")||{}).value || "price";
let sl, tp, entry;
if(mode === "pct"){
alert("百分比模式请确认盈亏比后再提交建议使用价格模式以便校验");
alert("百分比模式请确认盈亏比后再提交;建议使用价格模式以便校验.");
return;
}
sl = Number((document.getElementById("order-sl")||{}).value);
@@ -231,12 +231,12 @@ if(addOrderForm){
if(px) entry = Number(px);
const rr = calcClientRr(direction, entry, sl, tp);
if(rr === null || rr < MANUAL_MIN_PLANNED_RR){
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1已阻止人工下单`);
alert(`计划盈亏比 ${rr === null ? '无效' : rr.toFixed(2)}:1 低于最低要求 ${MANUAL_MIN_PLANNED_RR}:1,已阻止人工下单.`);
return;
}
addOrderForm.submit();
})
.catch(()=>{ ev.preventDefault(); alert("无法校验盈亏比请稍后重试"); });
.catch(()=>{ ev.preventDefault(); alert("无法校验盈亏比,请稍后重试"); });
ev.preventDefault();
});
}
@@ -244,27 +244,27 @@ if(addOrderForm){
text = text.replace("refreshOrderDefaults();", hook + "\nrefreshOrderDefaults();")
if "max_active_positions" not in text and "order-rule-tip" in text:
text = text.replace(
"规则单仓",
"规则最多 {{ max_active_positions }} 仓",
"规则:单仓;",
"规则:最多 {{ max_active_positions }} 仓;",
)
# account snapshot tip
old_tip = '`规则单仓BTC {{ btc_leverage }}x'
old_tip = '`规则:单仓;BTC {{ btc_leverage }}x'
if old_tip in text:
text = text.replace(
old_tip,
"`规则最多 ${data.max_active_positions || {{ max_active_positions }}} 仓BTC {{ btc_leverage }}x",
"`规则:最多 ${data.max_active_positions || {{ max_active_positions }}} 仓;BTC {{ btc_leverage }}x",
)
text = text.replace(
'const canTradeText = data.can_trade ? "可开仓" : "不可开仓有持仓或未到北京时间 {{ reset_hour }}:00";',
'const canTradeText = data.can_trade ? "可开仓" : `不可开仓持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}} 或未到北京时间 {{ reset_hour }}:00`;',
'const canTradeText = data.can_trade ? "可开仓" : "不可开仓(有持仓或未到北京时间 {{ reset_hour }}:00)";',
'const canTradeText = data.can_trade ? "可开仓" : `不可开仓(持仓 ${data.active_count||0}/${data.max_active_positions||{{ max_active_positions }}} 或未到北京时间 {{ reset_hour }}:00)`;',
)
text = text.replace(
"if(!data.in_top30){",
"const rankMax = data.rank_max || 30;\n if(!data.in_top30){",
)
text = text.replace(
"不在前30已拦截",
"不在前${rankMax}已拦截",
"不在前30,已拦截",
"不在前${rankMax},已拦截",
)
# conditional price refresh
if "data-page" in text and "refreshPriceSnapshotConditional" not in text:
@@ -68,8 +68,8 @@ if "key_monitor_page" not in g:
" can_trade = trading_day_reset_allows_new_open(now) and active_count == 0\n conn.close()\n return render_template(",
""" can_trade = trading_day_reset_allows_new_open(now) and active_count < MAX_ACTIVE_POSITIONS
key_gate_rule_text = (
f"周期 {KLINE_TIMEFRAME}|确认K突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR}确认棒偏移 {KEY_CONFIRM_BAR}"
f"量能突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"
f"周期 {KLINE_TIMEFRAME}|确认K:突破棒偏移 {KEY_CONFIRM_BREAKOUT_BAR},确认棒偏移 {KEY_CONFIRM_BAR}"
f"量能:突破量 > 前{KEY_VOLUME_MA_BARS}均量×{KEY_VOLUME_RATIO_MIN}"
f"自动开仓盈亏比 > {KEY_AUTO_MIN_PLANNED_RR}:1|日成交量排名前 {KEY_DAILY_VOLUME_RANK_MAX}"
)
conn.close()
@@ -2,8 +2,8 @@
"""
python scripts/verify_binance_funding.py
打印 BINANCE_API_KEY 前 8 位便于与 Binance 控制台核对不含 Secret)。用于服务器自检
对比 App资产 → 资金账户(Funding) / 现货账户(Spot) / U本位合约
打印 BINANCE_API_KEY 前 8 位便于与 Binance 控制台核对(不含 Secret).用于服务器自检.
对比 App:资产 → 资金账户(Funding) / 现货账户(Spot) / U本位合约.
"""
import os
import sys
@@ -30,9 +30,9 @@ def main():
k = (os.getenv("BINANCE_API_KEY") or "").strip()
s = (os.getenv("BINANCE_API_SECRET") or "").strip()
if not k or "REPLACE" in k.upper():
print("WARN: BINANCE_API_KEY 为空或仍像占位符请核对 .env")
print("WARN: BINANCE_API_KEY 为空或仍像占位符,请核对 .env")
if not s or "REPLACE" in s.upper():
print("WARN: BINANCE_API_SECRET 为空或仍像占位符请核对 .env")
print("WARN: BINANCE_API_SECRET 为空或仍像占位符,请核对 .env")
print("BINANCE_API_KEY prefix (8 chars):", (k[:8] + "") if len(k) > 8 else "(short)")
print("BINANCE_FUNDING_INCLUDE_SPOT:", os.getenv("BINANCE_FUNDING_INCLUDE_SPOT", "false"))
@@ -29,9 +29,9 @@
<div class="row" style="justify-content:space-between">
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大100根K线</strong>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
</div>
<div class="status">最近刷新<span id="updated-at">--</span></div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
{% if orders %}
<div class="row" style="margin-top:10px">
@@ -53,7 +53,7 @@
<span id="load-status" class="status"></span>
</div>
{% else %}
<div class="empty">当前没有激活订单无法展示放大K线</div>
<div class="empty">当前没有激活订单,无法展示放大K线.</div>
{% endif %}
</div>
+139 -139
View File
@@ -1,139 +1,139 @@
# 使用说明
**本文件对应仓库`crypto_monitor_binance`Binance U 本位永续)。**
功能界面与 **Gate.io USDT 永续版**目录 `crypto_monitor_gate`基本一致差异主要在 **`.env` 里交易所密钥与部分参数名**`BINANCE_*` / `GATE_*`),文末有对照
**部署代理PM2 等**请参考本仓库说明或 **`crypto_monitor_gate`** 下的 **`部署文档.md`**该文以 Gate + SSH SOCKS 为例Binance 侧将 API 与密钥改为 `BINANCE_*` 即可类比)。
**关键位自动开仓的规则RR结案原因**见本目录 **`关键位自动下单说明.md`**
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**主要能力包括
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型**5m 收线** 做硬条件过滤符合条件后 **企业微信** 提醒部分类型可 **自动市价开仓**见第 4 节与专门文档)。 |
| **实盘下单监控** | 手工填止损/止盈**以损定仓** 市价开单挂上条件止盈止损并在页面跟踪浮盈亏保本逻辑等 |
| **交易记录 / 复盘** | 平仓结果盈亏错过的单等归档与导出可选 **AI 复盘**见仓库根 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md))。 |
| **策略交易** | 顶栏 `/strategy`**趋势回调**(左)**顺势加仓**(右)左右并列细则见 [策略交易说明.md](../策略交易说明.md) |
| **策略交易记录** | 顶栏 `/strategy/records`趋势/顺势分两栏可筛选库内保留最近 100 条结束快照 |
后台按 **`MONITOR_POLL_SECONDS`**默认几秒轮询行情与监控逻辑**切勿**在未理解规则时同时运行两套程序共用一个实盘账户
---
## 2. 运行前必须配置`.env`
首次在本目录执行 **`cp .env.example .env`**再编辑 `.env``.env` 勿提交 Git`git pull` 不会改你的 `.env`升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`)。
至少检查以下项具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`打开站点后的登录口令`FLASK_SECRET_KEY`Session 密钥请勿使用默认值 |
| **企业微信** | `WECHAT_WEBHOOK`告警与关键位推送机器人的 Webhook |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`**不会**向交易所发送开仓指令适合测试流程)。改为 `true` 且密钥正确才会实盘 |
| **交易所 API** | **本仓库** `BINANCE_API_KEY``BINANCE_API_SECRET`永续相关见 `BINANCE_MARGIN_MODE``BINANCE_POSITION_MODE``BINANCE_TRIGGER_WORKING_TYPE`**勿**把 `.env` 提交到 Git |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR``KEY_STOP_OUTSIDE_BREAKOUT_PCT`详见 `关键位自动下单说明.md`)。 |
| **AI 复盘** | 默认 `AI_PROVIDER=openai``OPENAI_API_BASE=https://op.bz121.com/v1``OPENAI_API_KEY``OPENAI_MODEL=gemma4:e4b``AI_PROVIDER=ollama` + `OLLAMA_API` / `AI_MODEL`详见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md) |
网络需要代理时可配置 **`BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY`**与 Gate 版 `GATE_*_PROXY` 用法类似)。
---
## 3. 如何启动与登录
1. 准备 Python 虚拟环境并安装依赖`flask``requests``ccxt`按需 `Pillow``PySocks`),配置好 `.env`
2. 启动 Flask 应用可用 **`ecosystem.config.cjs`** 交给 PM2或本地 `python app.py` / `flask run`以你当前脚本为准)。
3. 浏览器访问站点打开 **`/login`**使用 **`.env` 里的 `APP_PASSWORD`** 登录
登录后顶栏**关键位监控** | **实盘下单**默认首页| **策略交易**`/strategy`趋势回调 + 顺势加仓双栏| **策略交易记录**`/strategy/records`最近 100 条结束快照| **交易记录与复盘** | **统计分析**
---
## 4. 关键位监控顶栏「关键位监控」→ `/key_monitor`
### 4.1 添加一条关键位
1. **币种**`BTC``BTC/USDT`会规范成内部符号)。
2. **类型**必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**`LIVE_TRADING_ENABLED=true` 且无其他持仓占位)。结案后本条从列表消失并记入历史 |
| **收敛突破** | 同上自动开仓类)。 |
| **关键阻力位** | **不自动开仓**触发后 **发 1 次微信**然后本条 **结案进历史** |
| **关键支撑位** | 同上仅提醒)。 |
| **回调触价开仓** | **不挂交易所限价**标记价回调触达 E 后 **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**标记价 **穿越 E 立即市价开仓**先触 SL/TP 侧失效有效期 **24h** |
3. **方向**做多 / 做空回调/突破触价箱体 / 收敛 / 斐波必选阻力/支撑不选)。
4. **价位**箱体/收敛/阻力/支撑填 **上沿 / 下沿**触价填 **入场 E / 止损 SL / 止盈 TP**
**限制**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**仍可添加「**关键阻力位 / 支撑位**」
**4h EMA55** 与你的方向逆势页面会 **额外 Flash 提示****不阻挡**提交
### 4.2 触发后会发生什么简版
- **箱体 / 收敛**门控通过后算计划 SL/TP 与 RR不达标 → 微信说明 + **`rr_insufficient`** 结案达标 → **市价开仓**成功 **`auto_opened`** / 失败 **`exchange_failed`**均不重试同一关键位
- **阻力 / 支撑****单次推送****`key_level_alert_only`** 结案
详细公式与字段见 **`关键位自动下单说明.md`**
### 4.3 列表与历史
当前条目与历史记录的用法与 Gate 版相同结案后可在历史区查阅 **`close_reason`**
---
## 5. 实盘下单顶栏「实盘下单」→ `/trade`
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制默认 1)。
- **人工开仓**计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**默认 1.4:1)。
- 填写币种方向杠杆可选)、止损/止盈价格或百分比按表单)。
- 移动保本等选项按页面与 `.env` 默认
开仓成功后卡片 **「来源」**手工一般为 **下单监控**关键位自动为 **关键位监控**
---
## 6. 企业微信
推送逻辑与 Gate 版一致未配置 **`WECHAT_WEBHOOK`** 时可能没有消息请以 **交易所端** 核对持仓与挂单
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 熟悉流程再实盘
2. **API 权限**最小化密钥勿泄露
3. **同一账户避免多程序重复开仓**
4. **自动备份**服务器上执行 `bash scripts/install_backup_cron.sh`每天北京时间 0:00 → `/root/backups`保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次
5. 升级代码后留意 **首轮启动**有无数据库迁移报错
---
## 8. 常见问题简要
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 门控五项日成交量排名`KLINE_TIMEFRAME` |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`RR 阈值是否已有持仓API/保证金错误信息 |
| 加不了箱体/收敛 | 是否已有持仓 |
| 推送收不到 | Webhook网络 |
---
## 9. Gate 版`crypto_monitor_gate`差异速查
| 项目 | Binance 本仓库 | Gate 版 |
|------|----------------|--------|
| API 变量 | `BINANCE_API_KEY``BINANCE_API_SECRET``BINANCE_*` | `GATE_API_KEY``GATE_API_SECRET``GATE_*` |
| 代理示例 | `BINANCE_SOCKS_PROXY` | `GATE_SOCKS_PROXY` |
| TP/SL 实现 | `_binance_place_tp_sl_orders` | `_gate_place_tp_sl_orders``GATE_TPSL_*` |
| 资金舍入口径 | **`FUNDS_DECIMALS`**与记账一致 | 以 Gate 仓库实现为准 |
业务流程登录四种关键位手工单单仓两份程序对齐仅需更换目录与 `.env`
# 使用说明
**本文件对应仓库:`crypto_monitor_binance`(Binance U 本位永续).**
功能,界面与 **Gate.io USDT 永续版**(目录 `crypto_monitor_gate`)基本一致,差异主要在 **`.env` 里交易所密钥与部分参数名**(`BINANCE_*` / `GATE_*`),文末有对照.
**部署,代理,PM2 等**请参考本仓库说明或 **`crypto_monitor_gate`** 下的 **`部署文档.md`**(该文以 Gate + SSH SOCKS 为例;Binance 侧将 API 与密钥改为 `BINANCE_*` 即可类比).
**关键位自动开仓的规则,RR,结案原因**见本目录 **`关键位自动下单说明.md`**.
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**,主要能力包括:
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型,**5m 收线** 做硬条件过滤;符合条件后 **企业微信** 提醒,部分类型可 **自动市价开仓**(见第 4 节与专门文档). |
| **实盘下单监控** | 手工填止损/止盈,**以损定仓** 市价开单,挂上条件止盈止损,并在页面跟踪浮盈亏,保本逻辑等. |
| **交易记录 / 复盘** | 平仓结果,盈亏,错过的单等归档与导出;可选 **AI 复盘**(见仓库根 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)). |
| **策略交易** | 顶栏 `/strategy`:**趋势回调**(左)**顺势加仓**(右)左右并列;细则见 [策略交易说明.md](../策略交易说明.md). |
| **策略交易记录** | 顶栏 `/strategy/records`:趋势/顺势分两栏,可筛选,库内保留最近 100 条结束快照. |
后台按 **`MONITOR_POLL_SECONDS`**(默认几秒)轮询行情与监控逻辑.**切勿**在未理解规则时同时运行两套程序共用一个实盘账户.
---
## 2. 运行前必须配置(`.env`)
首次在本目录执行 **`cp .env.example .env`**,再编辑 `.env`(`.env` 勿提交 Git;`git pull` 不会改你的 `.env`,升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`).
至少检查以下项(具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`:打开站点后的登录口令.`FLASK_SECRET_KEY`:Session 密钥,请勿使用默认值. |
| **企业微信** | `WECHAT_WEBHOOK`:告警与关键位推送机器人的 Webhook. |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`:**不会**向交易所发送开仓指令(适合测试流程).改为 `true` 且密钥正确才会实盘. |
| **交易所 API** | **本仓库:** `BINANCE_API_KEY`,`BINANCE_API_SECRET`;永续相关见 `BINANCE_MARGIN_MODE`,`BINANCE_POSITION_MODE`,`BINANCE_TRIGGER_WORKING_TYPE`.**勿**把 `.env` 提交到 Git. |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR`,`KEY_STOP_OUTSIDE_BREAKOUT_PCT`(详见 `关键位自动下单说明.md`). |
| **AI 复盘** | 默认 `AI_PROVIDER=openai`,`OPENAI_API_BASE=https://op.bz121.com/v1`,`OPENAI_API_KEY`,`OPENAI_MODEL=gemma4:e4b`;`AI_PROVIDER=ollama` + `OLLAMA_API` / `AI_MODEL`.详见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md). |
网络需要代理时可配置 **`BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY`**(与 Gate 版 `GATE_*_PROXY` 用法类似).
---
## 3. 如何启动与登录
1. 准备 Python 虚拟环境并安装依赖(`flask`,`requests`,`ccxt`,按需 `Pillow`,`PySocks`),配置好 `.env`.
2. 启动 Flask 应用(可用 **`ecosystem.config.cjs`** 交给 PM2,或本地 `python app.py` / `flask run`,以你当前脚本为准).
3. 浏览器访问站点,打开 **`/login`**,使用 **`.env` 里的 `APP_PASSWORD`** 登录.
登录后顶栏:**关键位监控** | **实盘下单**(默认首页)| **策略交易**(`/strategy`,趋势回调 + 顺势加仓双栏)| **策略交易记录**(`/strategy/records`,最近 100 条结束快照)| **交易记录与复盘** | **统计分析**.
---
## 4. 关键位监控(顶栏「关键位监控」→ `/key_monitor`)
### 4.1 添加一条关键位
1. **币种**:`BTC``BTC/USDT`(会规范成内部符号).
2. **类型**(必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**(`LIVE_TRADING_ENABLED=true` 且无其他持仓占位).结案后本条从列表消失并记入历史. |
| **收敛突破** | 同上(自动开仓类). |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**. |
| **关键支撑位** | 同上(仅提醒). |
| **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(回调/突破触价,箱体 / 收敛 / 斐波必选;阻力/支撑不选).
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价填 **入场 E / 止损 SL / 止盈 TP**.
**限制:**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**;仍可添加「**关键阻力位 / 支撑位**」.
**4h EMA55** 与你的方向逆势,页面会 **额外 Flash 提示**,**不阻挡**提交.
### 4.2 触发后会发生什么(简版)
- **箱体 / 收敛**:门控通过后算计划 SL/TP 与 RR;不达标 → 微信说明 + **`rr_insufficient`** 结案;达标 → **市价开仓**,成功 **`auto_opened`** / 失败 **`exchange_failed`**,均不重试同一关键位.
- **阻力 / 支撑**:**单次推送****`key_level_alert_only`** 结案.
详细公式与字段见 **`关键位自动下单说明.md`**.
### 4.3 列表与历史
当前条目与历史记录的用法与 Gate 版相同;结案后可在历史区查阅 **`close_reason`**.
---
## 5. 实盘下单(顶栏「实盘下单」→ `/trade`)
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制(默认 1).
- **人工开仓**计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**(默认 1.4:1).
- 填写币种,方向,杠杆(可选),止损/止盈(价格或百分比按表单).
- 移动保本等选项按页面与 `.env` 默认.
开仓成功后卡片 **「来源」**:手工一般为 **下单监控**;关键位自动为 **关键位监控**.
---
## 6. 企业微信
推送逻辑与 Gate 版一致;未配置 **`WECHAT_WEBHOOK`** 时可能没有消息,请以 **交易所端** 核对持仓与挂单.
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 熟悉流程再实盘.
2. **API 权限**最小化,密钥勿泄露.
3. **同一账户避免多程序重复开仓**.
4. **自动备份**:服务器上执行 `bash scripts/install_backup_cron.sh`(每天北京时间 0:00 → `/root/backups`,保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次.
5. 升级代码后留意 **首轮启动**有无数据库迁移报错.
---
## 8. 常见问题(简要)
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 门控五项,日成交量排名,`KLINE_TIMEFRAME`. |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`,RR 阈值,是否已有持仓,API/保证金错误信息. |
| 加不了箱体/收敛 | 是否已有持仓. |
| 推送收不到 | Webhook,网络. |
---
## 9. Gate 版(`crypto_monitor_gate`)差异速查
| 项目 | Binance 本仓库 | Gate 版 |
|------|----------------|--------|
| API 变量 | `BINANCE_API_KEY`,`BINANCE_API_SECRET`,`BINANCE_*` | `GATE_API_KEY`,`GATE_API_SECRET`,`GATE_*` |
| 代理示例 | `BINANCE_SOCKS_PROXY` | `GATE_SOCKS_PROXY` |
| TP/SL 实现 | `_binance_place_tp_sl_orders` | `_gate_place_tp_sl_orders`,`GATE_TPSL_*` |
| 资金舍入口径 | **`FUNDS_DECIMALS`**(与记账一致) | 以 Gate 仓库实现为准 |
业务流程(登录,四种关键位,手工单,单仓)两份程序对齐;仅需更换目录与 `.env`.
@@ -1,192 +1,192 @@
# 关键位监控说明自动开仓 + 人工盯盘
**适用Gate / Binance / OKX 三所实例共用 `lib/key_monitor/key_auto_order_lib.py`**
## 环境开关 `KEY_AUTO_ORDER_ENABLED`默认 `false`
| 计仓模式 | 开关 | 关键位程序自动单 |
|----------|------|------------------|
| `risk`以损定仓 | `false` | **全部关闭**含触价);支撑/阻力微信提醒仍可用 |
| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动旧行为 |
| `full_margin`全仓 | `false` | 全部关闭含触价 |
| `full_margin` | `true` | **仅触价**自动箱体/斐波等仍禁止 |
**不受本开关影响** 人工实盘下单关键支撑/阻力提醒**顺势加仓**`risk`)、趋势回调`risk`)。全仓模式下策略自动仍禁止
修改 `.env` 后须 **重启 PM2**复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项
---
**适用`crypto_monitor_binance`Binance U 本位**
Gate / OKX 见各自目录下同名文档共享逻辑在 `lib/key_monitor/`
本文档与 `.env``check_key_monitors``add_key``_key_hard_checks``_process_key_rs_level_alert` 一致
---
## 一监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**门控 + RR | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**同上 | 同上 |
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上与阻力位**相同规则**填上沿+下沿程序双向监控 |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明**不在下文展开** |
| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30**上沿 **>** 下沿触价开仓填 E/SL/TP上下沿仅作展示占位)。
---
## 二关键阻力位 / 关键支撑位人工盯盘
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**程序同时监控两侧**无法预先判定**做多还是做空)。
- 页面 **不显示不要求** 方向库中 `direction` 初始为 `watch`**首次突破后** 写入 `long`向上突破上沿`short`向下突破下沿)。
### 2.2 触发极简
- 周期**`KLINE_TIMEFRAME`默认 5m最近一根已闭合 K** 的 **收盘价**非影线)。
- **向上突破上沿** `收盘 > upper` → 推断方向 **多 / 向上**本次监控任务开始按节奏提醒
- **向下突破下沿** `收盘 < lower` → 推断方向 **空 / 向下**本次任务同样开始提醒
- **任一侧突破即结束本条监控周期**不会在突破后再等待另一侧上沿下沿谁先满足用谁同根 K 仅可能满足一侧)。
**不参与** 量能二确 K越过幅度下限日成交排名运行时)、计划 RR自动开仓
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次首次检测到突破的当次轮询若已闭合 5m 满足条件)。
- 第 23 次仅按间隔推送**不要求**价格仍在箱外)。
- 第 3 次推送后写入 `key_monitor_history``close_reason=**key_level_alert_done**``key_monitors` **删除**
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根突破 K + 确认 K |
| 提醒次数 | 3 次后结案 | 自动单触发后 1 次业务推送并结案 |
---
## 三箱体突破 / 收敛突破自动开仓
### 3.1 K 线结构默认索引
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控须全部通过
1. **有效突破收盘越界**
- 多`突破 K 收盘 > upper`
- 空`突破 K 收盘 < lower`
2. **突破越过幅度仅下限**
- 多`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`默认 **0.03%**
- 空`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**突破过猛由 **计划 RR** 过滤
- **不再**使用 K 线实体占开盘价比例`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**
3. **确认 K 不进箱体**
- 多确认 K 收盘 **`> upper`**不得在 `[lower, upper]`
- 空确认 K 收盘 **`< lower`**
4. **量能** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`默认 20根均量 × `KEY_VOLUME_RATIO_MIN`默认 1.3
5. **日成交量排名** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30
6. **计划 RR最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`默认 1.5才市价开仓
### 3.3 止损 / 止盈确认 K 收盘为 E
箱体高 **H = |upper lower|**止损锚在 **突破 K 极值** 外侧
| 方向 | 止损标准/趋势方案 |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表与改版前一致):
| 方案 | `sl_tp_mode` | 多SL / TP | 空SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案`close_reason`
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿 |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四回调 / 突破触价开仓程序触价无交易所挂单
### 4.1 录入
- **回调触价开仓**方向必选多/空填写 **计划入场价 E****止损 SL****止盈 TP**做多须 `SL < E < TP`)。
- **突破触价开仓**同上添加时当前价须在突破方向一侧做多价低于 E做空价高于 E)。
- 计划 RR 以 **E** 为基准**严格大于** `KEY_AUTO_MIN_PLANNED_RR`默认 1.5)。
- 可选移动保本时间平仓**全仓杠杆模式**下可用
### 4.2 触发与结案
| 类型 | 触发条件标记价 |
|------|-------------------|
| **回调触价** | 做多 `≤ E`做空 `≥ E` → 下一轮询市价开仓 |
| **突破触价** | 做多**向上穿越** E做空**向下穿越** E → **立即**市价开仓 |
- 未成交前标记价先触 **TP 侧**`trigger_tp_invalidate`
- **突破触价**另未穿越 E 先触 **SL 侧**`trigger_sl_invalidate`
- **24h** 未触发 → `trigger_entry_expired`
- 成功 → `trigger_entry_filled`触发后开仓失败 → `trigger_exchange_failed`
### 4.3 计仓与占位
- **以损定仓**按 ESL 反推保证金触发时重算**全仓杠杆**可用×缓冲比例BTC/ETH 10x其它 5x
- **占当日开仓意图**已开 + 待触发),未成交不占持仓同币仅 1 条触价监控含回调/突破)。
共享逻辑`trigger_entry_key_monitor_lib.py`轮询`check_trigger_entry_key_monitors`
---
## 五环境与参数`.env` 摘要
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限默认 0.03 | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用默认 3 次 / 5 分钟 |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 六相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
# 关键位监控说明(自动开仓 + 人工盯盘)
**适用:Gate / Binance / OKX 三所实例(共用 `lib/key_monitor/key_auto_order_lib.py`)**
## 环境开关 `KEY_AUTO_ORDER_ENABLED`(默认 `false`)
| 计仓模式 | 开关 | 关键位程序自动单 |
|----------|------|------------------|
| `risk`(以损定仓) | `false` | **全部关闭**(含触价);支撑/阻力微信提醒仍可用 |
| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动(旧行为) |
| `full_margin`(全仓) | `false` | 全部关闭(含触价) |
| `full_margin` | `true` | **仅触价**自动;箱体/斐波等仍禁止 |
**不受本开关影响:** 人工实盘下单,关键支撑/阻力提醒,**顺势加仓**(`risk`),趋势回调(`risk`).全仓模式下策略自动仍禁止.
修改 `.env` 后须 **重启 PM2**.复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项.
---
**适用:`crypto_monitor_binance`(Binance U 本位)**
Gate / OKX 见各自目录下同名文档;共享逻辑在 `lib/key_monitor/`.
本文档与 `.env`,`check_key_monitors`,`add_key`,`_key_hard_checks`,`_process_key_rs_level_alert` 一致.
---
## 一,监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**(门控 + RR) | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**(同上) | 同上 |
| **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位).
---
## 二,关键阻力位 / 关键支撑位(人工盯盘)
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**(程序同时监控两侧,**无法预先判定**做多还是做空).
- 页面 **不显示,不要求** 方向;库中 `direction` 初始为 `watch`,**首次突破后** 写入 `long`(向上突破上沿)`short`(向下突破下沿).
### 2.2 触发(极简)
- 周期:**`KLINE_TIMEFRAME`(默认 5m)最近一根已闭合 K** 的 **收盘价**(非影线).
- **向上突破上沿:** `收盘 > upper` → 推断方向 **多 / 向上**,本次监控任务开始按节奏提醒.
- **向下突破下沿:** `收盘 < lower` → 推断方向 **空 / 向下**,本次任务同样开始提醒.
- **任一侧突破即结束本条监控周期**(不会在突破后再等待另一侧;上沿,下沿谁先满足用谁,同根 K 仅可能满足一侧).
**不参与:** 量能,二确 K,越过幅度下限,日成交排名(运行时),计划 RR,自动开仓.
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次:首次检测到突破的当次轮询(若已闭合 5m 满足条件).
- 第 2,3 次:仅按间隔推送(**不要求**价格仍在箱外).
- 第 3 次推送后:写入 `key_monitor_history`,`close_reason=**key_level_alert_done**`,`key_monitors` **删除**.
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根(突破 K + 确认 K) |
| 提醒次数 | 3 次后结案 | 自动单:触发后 1 次业务推送并结案 |
---
## 三,箱体突破 / 收敛突破(自动开仓)
### 3.1 K 线结构(默认索引)
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控(须全部通过)
1. **有效突破(收盘越界)**
- 多:`突破 K 收盘 > upper`
- 空:`突破 K 收盘 < lower`
2. **突破越过幅度(仅下限)**
- 多:`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`(默认 **0.03%**)
- 空:`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**;突破过猛由 **计划 RR** 过滤.
- **不再**使用 K 线实体占开盘价比例;`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**.
3. **确认 K 不进箱体**
- 多:确认 K 收盘 **`> upper`**(不得在 `[lower, upper]`)
- 空:确认 K 收盘 **`< lower`**
4. **量能:** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`(默认 20)根均量 × `KEY_VOLUME_RATIO_MIN`(默认 1.3)
5. **日成交量排名:** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)
6. **计划 RR(最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后,`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)才市价开仓
### 3.3 止损 / 止盈(确认 K 收盘为 E)
箱体高 **H = |upper lower|**.止损锚在 **突破 K 极值** 外侧:
| 方向 | 止损(标准/趋势方案) |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表(与改版前一致):
| 方案 | `sl_tp_mode` | 多:SL / TP | 空:SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案(`close_reason`)
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿) |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四,回调 / 突破触价开仓(程序触价,无交易所挂单)
### 4.1 录入
- **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**,**止损 SL**,**止盈 TP**(做多须 `SL < E < TP`).
- **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E).
- 计划 RR 以 **E** 为基准,**严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5).
- 可选移动保本,时间平仓;**全仓杠杆模式**下可用.
### 4.2 触发与结案
| 类型 | 触发条件(标记价) |
|------|-------------------|
| **回调触价** | 做多 `≤ E`;做空 `≥ E` → 下一轮询市价开仓 |
| **突破触价** | 做多**向上穿越** E;做空**向下穿越** E → **立即**市价开仓 |
- 未成交前标记价先触 **TP 侧**`trigger_tp_invalidate`.
- **突破触价**另:未穿越 E 先触 **SL 侧**`trigger_sl_invalidate`.
- **24h** 未触发 → `trigger_entry_expired`.
- 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`.
### 4.3 计仓与占位
- **以损定仓**:按 E,SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x,其它 5x.
- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破).
共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`.
---
## 五,环境与参数(`.env` 摘要)
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限(默认 0.03) | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用(默认 3 次 / 5 分钟) |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 六,相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
+147 -147
View File
@@ -1,147 +1,147 @@
# 界面与风控更新说明Binance 实例
## 顶栏导航4 项
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加实时门控历史 |
| 2 | 实盘下单 | `/trade` | 人工开仓划转实时持仓**默认首页** `/``/trade` |
| 3 | 交易记录与复盘 | `/records` | 交易记录复盘表单AI 历史受顶栏 UTC 时间窗筛选 |
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 关键位监控页
- 标题去掉「5m」规则条从 `.env` 读取周期确认K量能自动开仓盈亏比日成交量排名)。
- 左列活跃关键位**pos-card** 样式展示现价/距上沿/距下沿/门控
- 右列关键位历史失效/结案),与左列等高滚动**受顶栏 UTC 列表时间窗筛选**默认 UTC 当日)。
- 监控类型新增**斐波回调0.618****斐波回调0.786**与 Gate 主站同一套规则计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
### 斐波关键位监控方案 A交易所限价
| 项 | 说明 |
|----|------|
| 同币互斥 | 每个币种只能有一条斐波监控0.618 与 0.786 不可并存 |
| 上下沿 | 上沿 **H**下沿 **L**须 H > L |
| 挂单价 E | **做多** `E = H ratio × (H L)`自 H 向下回撤);**做空** `E = L + ratio × (H L)`自 L 向上反弹 |
| 做多 | 限价 @ E止损 L止盈 H |
| 做空 | 限价 @ E止损 H止盈 L |
| 添加后 | **立即**在 Binance U 本位挂限价单卡片显示 **挂E**限价单 ID |
| 失效 | 以**标记价**判断做多且标记价 ≥ H做空且标记价 ≤ L且限价**未成交** → 撤销该限价单并结案 |
| 成交后 | 挂交易所 TP/SL含 Algo 通道条件单→ 写入 **实盘下单监控**`monitor_type=关键位监控``key_signal_type=斐波回调…`→ 从关键位列表移除 |
| 撤单 | 仅撤本条斐波的订单 ID**不会**对该合约 `cancel_all_orders` / 全撤 Algo避免误伤其他委托 |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`0.618 理论约 1.6:10.786 约 3.7:1 |
| 日成交量 | 与箱体/收敛相同须在前 `KEY_DAILY_VOLUME_RANK_MAX` 名内方可添加 |
后台轮询`check_fib_key_monitors()`箱体/收敛仍走 `check_key_monitors()`
手动删除关键位时未成交斐波会先撤限价再删库
### 箱体 / 收敛自动开仓来源标注
- 自动开仓写入 `order_monitors.key_signal_type``箱体突破``收敛突破`
- 持仓与交易记录展示「来源 · 信号类型」
## 列表时间窗UTC全站顶栏
共用模块仓库根目录 `history_window_lib.py`Gate / Binance 主站一致)。
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**`win_preset=utc_today`从 UTC 0:00 至当前时刻 |
| 可选 | 近 24 小时近 7 天自定义起止UTC`datetime-local` |
| 作用范围 | 关键位历史交易记录列表复盘记录 APIAI 历史 API导出「交易记录」「关键位历史」 |
| 与统计的关系 | **仅影响列表/导出****统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`默认 8:00切交易日** |
| 库内时间 | DB 存北京时间字符串后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
| 切换方式 | 顶栏「列表筛选UTC」→ 选预设 → **应用**保留当前路由`/records?win_preset=…` |
查询参数示例
- `?win_preset=utc_today`
- `?win_preset=utc_last24h` / `utc_last7d`
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
## 交易记录与复盘
- 交易记录盈亏以**本地估算**为准平仓时按成交/计划价计算);盈亏列可标注 **估**
- 与币安 App 不一致时请在「核对修改」或复盘中 **手工填写** `reviewed_pnl_amount` 覆盖展示不再提供批量「同步交易所盈亏」)。
- **列表默认只显示当前 UTC 时间窗内**的记录见上节);导出 CSV 同步该时间窗
- 表头 **「止损(开仓)」**展示开仓快照 `initial_stop_loss`无则回退 `stop_loss`);核对/复盘仍可用有效止损字段
- 平仓写入 `trade_records``stop_loss``initial_stop_loss` 均写入**开仓时止损快照**`key_signal_type` 保留箱体/收敛/斐波来源`fib_key_monitor_lib.key_signal_type_for_trade_record`)。
- **开仓类型**`entry_reason`):机器单平仓入库时若未手填`key_signal_type` 自动映射见下表);列表/导出「开仓类型」列 = 复盘核对值优先否则入库值否则按信号映射
| `key_signal_type` | 自动写入的 `entry_reason` |
|-------------------|---------------------------|
| 箱体突破 | 关键位箱体突破 |
| 收敛突破 | 关键位收敛突破 |
| 斐波回调0.618 | 关键位斐波0.618 |
| 斐波回调0.786 | 关键位斐波0.786 |
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案与趋势/波段类并列)。
- 复盘 **离场触发** 新增 **「止盈」**从交易记录「填入复盘」时若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项并按 `key_signal_type` 预填开仓类型
- 勾选「保存时自动生成多周期 K 线图」时**平仓时间** 为锚点各周期向前约 `ORDER_CHART_LIMIT`默认 100根 K 线`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」
- `/api/journals``/api/reviews` 支持同一时间窗 query与列表一致
### 导出交易记录 v3
- 文件名`trade_records_v3_YYYYMMDD.csv`
- 相对 v2 增加`key_signal_type``initial_stop_loss`及开仓快照列)、`planned_rr``actual_rr``risk_amount`交易所盈亏与时间字段等末列「开仓类型」为有效展示文案
- 「关键位历史」导出同样受 UTC 时间窗限制
## 实盘下单页
- 左列实盘下单监控表单划转规则)。
- 右列实时持仓独立模块)。
- **人工开仓门控**计划盈亏比 &lt; `MANUAL_MIN_PLANNED_RR`默认 **1.4**时前端弹窗 + 后端拒绝
- **移动保本**勾选启用):监控轮询达到触发 RR 后止损阶梯上移时**同步交易所**——**先撤**该合约全部 TP/SL含 Algo 条件单**再挂**新止损 + 原止盈`replace_active_monitor_tpsl_on_exchange`)。仅交易所成功后才写库失败发企业微信告警未配置实盘 API 时仍只更新本地
## 统计分析页`/stats`
| 项 | 说明 |
|----|------|
| 切日 | **北京时间**交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00``.env` 默认 **8** |
| 品类下拉 | 页顶 **「统计品类」** 下拉切换默认「全部交易」):全部交易下单监控关键位箱体突破关键位收敛结构关键位斐波0.618关键位斐波0.786一次只显示所选品类的日/周/月 |
| URL | 切换后写入 `stats_segment=``all``manual``key_box``key_conv``key_fib618``key_fib786`),刷新 `/stats` 可保持选项 |
| 每块指标 | 日 / 周 / 月开单次数平仓笔数胜率净盈亏回撤连续亏损等与原口径一致 |
| 开单次数 | 人工块`monitor_type=下单监控` 且无 `key_signal_type`关键位块`order_monitors.key_signal_type` 计数 |
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录按北京交易日归类**不**随顶栏 UTC 列表窗切换 |
## 持仓与计仓
- `MAX_ACTIVE_POSITIONS` 默认 **1**可在 `.env` 调大)。
- 关键位自动开仓在已有持仓时`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true`按**首笔开仓前**交易账户资金快照计仓字段 `trading_sessions.key_sizing_capital_snapshot`)。
## 配置
详见 `.env.example` 中「关键位门控」「交易执行 / 人工风控」注释段
## 自动备份服务器
- 脚本`scripts/backup_data.sh``crypto.db` + `static/images`
- 定时`scripts/install_backup_cron.sh` → 每天 **北京时间 0:00**目录 **`/root/backups/<实例名>/YYYY-MM-DD/`**保留 **30**
- 详见 `部署文档.md` 第 5.4 节自动备份
## 数据库启动时自动迁移
`key_monitors` 斐波字段`fib_limit_order_id``fib_entry_price``fib_stop_loss``fib_take_profit``fib_order_amount``fib_margin_capital``fib_leverage`
`trade_records` / `order_monitors``key_signal_type``exchange_realized_pnl``exchange_opened_at``exchange_closed_at``exchange_sync_key``entry_reason``reviewed_entry_reason``initial_stop_loss`
**历史数据**本次**不做**旧记录的批量回填`entry_reason` / `initial_stop_loss` / `key_signal_type`);仅**新产生**的平仓与复盘按新逻辑写入旧行展示可回退已有字段
## 涉及文件便于排查
| 路径 | 说明 |
|------|------|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
| `fib_key_monitor_lib.py` | 斐波计算`KEY_ENTRY_REASON_BY_SIGNAL``entry_reason_from_key_signal` |
| `crypto_monitor_binance/app.py` | 列表筛选统计分块导出 v3复盘 K 线锚点入库逻辑 |
| `crypto_monitor_binance/templates/index.html` | 顶栏时间窗统计分块 UI止损(开仓)列复盘预填 |
## 升级步骤
1. `git pull` 后对比 `.env.example`把新增变量合并进本地 `.env`
2. 在 VPS 上为 Binance / Gate / **各执行一次** `bash scripts/install_backup_cron.sh`若尚未安装)。
3. 重启 Binance 实例`pm2 restart crypto_binance`);SQLite 会自动 `ALTER` 缺列斐波交易所盈亏`entry_reason`)。
4. 浏览器强刷Ctrl+F5避免旧版 `index.html` 缓存
5. 打开任意页确认顶栏出现 **「列表筛选UTC」**`/stats` 可见分品类统计与「北京 8:00 切日」说明
6. 建议先用测试币验证斐波限价挂出标记价失效撤单成交后 TP/SL 与订单监控是否正常平仓后检查交易记录止损(开仓)与开仓类型
# 界面与风控更新说明(Binance 实例)
## 顶栏导航(4 项)
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加,实时门控,历史 |
| 2 | 实盘下单 | `/trade` | 人工开仓,划转,实时持仓(**默认首页** `/``/trade`) |
| 3 | 交易记录与复盘 | `/records` | 交易记录,复盘表单,AI 历史(受顶栏 UTC 时间窗筛选) |
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 关键位监控页
- 标题去掉「5m」;规则条从 `.env` 读取(周期,确认K,量能,自动开仓盈亏比,日成交量排名).
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控.
- 右列:关键位历史(失效/结案),与左列等高滚动;**受顶栏 UTC 列表时间窗筛选**(默认 UTC 当日).
- 监控类型新增:**斐波回调0.618**,**斐波回调0.786**(与 Gate 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`).
### 斐波关键位监控(方案 A:交易所限价)
| 项 | 说明 |
|----|------|
| 同币互斥 | 每个币种只能有一条斐波监控(0.618 与 0.786 不可并存) |
| 上下沿 | 上沿 **H**,下沿 **L**(须 H > L) |
| 挂单价 E | **做多** `E = H ratio × (H L)`(自 H 向下回撤);**做空** `E = L + ratio × (H L)`(自 L 向上反弹) |
| 做多 | 限价 @ E,止损 L,止盈 H |
| 做空 | 限价 @ E,止损 H,止盈 L |
| 添加后 | **立即**在 Binance U 本位挂限价单;卡片显示 **挂E**,限价单 ID |
| 失效 | 以**标记价**判断:做多且标记价 ≥ H,做空且标记价 ≤ L,且限价**未成交** → 撤销该限价单并结案 |
| 成交后 | 挂交易所 TP/SL(含 Algo 通道条件单)→ 写入 **实盘下单监控**(`monitor_type=关键位监控`,`key_signal_type=斐波回调…`)→ 从关键位列表移除 |
| 撤单 | 仅撤本条斐波的订单 ID,**不会**对该合约 `cancel_all_orders` / 全撤 Algo,避免误伤其他委托 |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`;0.618 理论约 1.6:1,0.786 约 3.7:1 |
| 日成交量 | 与箱体/收敛相同,须在前 `KEY_DAILY_VOLUME_RANK_MAX` 名内方可添加 |
后台轮询:`check_fib_key_monitors()`;箱体/收敛仍走 `check_key_monitors()`.
手动删除关键位时,未成交斐波会先撤限价再删库.
### 箱体 / 收敛自动开仓(来源标注)
- 自动开仓写入 `order_monitors.key_signal_type`:`箱体突破``收敛突破`.
- 持仓与交易记录展示「来源 · 信号类型」.
## 列表时间窗(UTC,全站顶栏)
共用模块:仓库根目录 `history_window_lib.py`(Gate / Binance 主站一致).
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**(`win_preset=utc_today`,从 UTC 0:00 至当前时刻) |
| 可选 | 近 24 小时,近 7 天,自定义起止(UTC,`datetime-local`) |
| 作用范围 | 关键位历史,交易记录列表,复盘记录 API,AI 历史 API,导出「交易记录」「关键位历史」 |
| 与统计的关系 | **仅影响列表/导出**;**统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切交易日** |
| 库内时间 | DB 存北京时间字符串;后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
| 切换方式 | 顶栏「列表筛选(UTC)」→ 选预设 → **应用**(保留当前路由,`/records?win_preset=…`) |
查询参数示例:
- `?win_preset=utc_today`
- `?win_preset=utc_last24h` / `utc_last7d`
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
## 交易记录与复盘
- 交易记录盈亏以**本地估算**为准(平仓时按成交/计划价计算);盈亏列可标注 **估**.
- 与币安 App 不一致时,请在「核对修改」或复盘中 **手工填写** `reviewed_pnl_amount` 覆盖展示(不再提供批量「同步交易所盈亏」).
- **列表默认只显示当前 UTC 时间窗内**的记录(见上节);导出 CSV 同步该时间窗.
- 表头 **「止损(开仓)」**:展示开仓快照 `initial_stop_loss`(无则回退 `stop_loss`);核对/复盘仍可用有效止损字段.
- 平仓写入 `trade_records`:`stop_loss``initial_stop_loss` 均写入**开仓时止损快照**;`key_signal_type` 保留箱体/收敛/斐波来源(`fib_key_monitor_lib.key_signal_type_for_trade_record`).
- **开仓类型**(`entry_reason`):机器单平仓入库时,若未手填,`key_signal_type` 自动映射(见下表);列表/导出「开仓类型」列 = 复盘核对值优先,否则入库值,否则按信号映射.
| `key_signal_type` | 自动写入的 `entry_reason` |
|-------------------|---------------------------|
| 箱体突破 | 关键位箱体突破 |
| 收敛突破 | 关键位收敛突破 |
| 斐波回调0.618 | 关键位斐波0.618 |
| 斐波回调0.786 | 关键位斐波0.786 |
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案(与趋势/波段类并列).
- 复盘 **离场触发** 新增 **「止盈」**;从交易记录「填入复盘」时,若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项,并按 `key_signal_type` 预填开仓类型.
- 勾选「保存时自动生成多周期 K 线图」时:**平仓时间** 为锚点,各周期向前约 `ORDER_CHART_LIMIT`(默认 100)根 K 线(`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」.
- `/api/journals`,`/api/reviews` 支持同一时间窗 query,与列表一致.
### 导出(交易记录 v3)
- 文件名:`trade_records_v3_YYYYMMDD.csv`
- 相对 v2 增加:`key_signal_type`,`initial_stop_loss`(及开仓快照列),`planned_rr`,`actual_rr`,`risk_amount`,交易所盈亏与时间字段等;末列「开仓类型」为有效展示文案.
- 「关键位历史」导出同样受 UTC 时间窗限制.
## 实盘下单页
- 左列:实盘下单监控(表单,划转,规则).
- 右列:实时持仓(独立模块).
- **人工开仓门控**:计划盈亏比 &lt; `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝.
- **移动保本**(勾选启用):监控轮询达到触发 RR 后,止损阶梯上移时**同步交易所**——**先撤**该合约全部 TP/SL(含 Algo 条件单)**再挂**新止损 + 原止盈(`replace_active_monitor_tpsl_on_exchange`).仅交易所成功后才写库;失败发企业微信告警.未配置实盘 API 时仍只更新本地.
## 统计分析页(`/stats`)
| 项 | 说明 |
|----|------|
| 切日 | **北京时间**;交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00`(`.env` 默认 **8**) |
| 品类下拉 | 页顶 **「统计品类」** 下拉切换(默认「全部交易」):全部交易,下单监控,关键位箱体突破,关键位收敛结构,关键位斐波0.618,关键位斐波0.786;一次只显示所选品类的日/周/月 |
| URL | 切换后写入 `stats_segment=`(`all`,`manual`,`key_box`,`key_conv`,`key_fib618`,`key_fib786`),刷新 `/stats` 可保持选项 |
| 每块指标 | 日 / 周 / 月:开单次数,平仓笔数,胜率,净盈亏,回撤,连续亏损等(与原口径一致) |
| 开单次数 | 人工块:`monitor_type=下单监控` 且无 `key_signal_type`;关键位块:`order_monitors.key_signal_type` 计数 |
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录,按北京交易日归类,**不**随顶栏 UTC 列表窗切换 |
## 持仓与计仓
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大).
- 关键位自动开仓:在已有持仓时,`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true`,按**首笔开仓前**交易账户资金快照计仓(字段 `trading_sessions.key_sizing_capital_snapshot`).
## 配置
详见 `.env.example` 中「关键位门控」「交易执行 / 人工风控」注释段.
## 自动备份(服务器)
- 脚本:`scripts/backup_data.sh`(`crypto.db` + `static/images`)
- 定时:`scripts/install_backup_cron.sh` → 每天 **北京时间 0:00**,目录 **`/root/backups/<实例名>/YYYY-MM-DD/`**,保留 **30**
- 详见 `部署文档.md` 第 5.4 节(自动备份)
## 数据库(启动时自动迁移)
`key_monitors` 斐波字段:`fib_limit_order_id`,`fib_entry_price`,`fib_stop_loss`,`fib_take_profit`,`fib_order_amount`,`fib_margin_capital`,`fib_leverage`.
`trade_records` / `order_monitors`:`key_signal_type`,`exchange_realized_pnl`,`exchange_opened_at`,`exchange_closed_at`,`exchange_sync_key`,`entry_reason`,`reviewed_entry_reason`,`initial_stop_loss`.
**历史数据**:本次**不做**旧记录的批量回填(`entry_reason` / `initial_stop_loss` / `key_signal_type`);仅**新产生**的平仓与复盘按新逻辑写入.旧行展示可回退已有字段.
## 涉及文件(便于排查)
| 路径 | 说明 |
|------|------|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
| `fib_key_monitor_lib.py` | 斐波计算,`KEY_ENTRY_REASON_BY_SIGNAL`,`entry_reason_from_key_signal` |
| `crypto_monitor_binance/app.py` | 列表筛选,统计分块,导出 v3,复盘 K 线锚点,入库逻辑 |
| `crypto_monitor_binance/templates/index.html` | 顶栏时间窗,统计分块 UI,止损(开仓)列,复盘预填 |
## 升级步骤
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`.
2. 在 VPS 上为 Binance / Gate / **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装).
3. 重启 Binance 实例(`pm2 restart crypto_binance`);SQLite 会自动 `ALTER` 缺列(斐波,交易所盈亏,`entry_reason`).
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存.
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**;`/stats` 可见分品类统计与「北京 8:00 切日」说明.
6. 建议先用测试币验证斐波:限价挂出,标记价失效撤单,成交后 TP/SL 与订单监控是否正常;平仓后检查交易记录止损(开仓)与开仓类型.
+389 -389
View File
@@ -1,389 +1,389 @@
# `crypto_monitor_binance` 部署指南SSH SOCKS + Binance + PM2Ubuntu
项目功能环境变量总览见 **[README.md](./README.md)**Ubuntu 环境Python / Node / PM2**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**
本文面向**在本机或 VPS 上运行本项目****直连 Binance API 不稳定超时或被网络策略拦截** 的场景思路是
- 本机用 `ssh -D` 做动态转发把 **SOCKS5 出口**放到能稳定访问 Binance 的机器常见为一台境外 VPS
- 项目在 `.env` 中设置 **`BINANCE_SOCKS_PROXY=socks5h://127.0.0.1:1080`**或你实际端口),`ccxt` 经 SOCKS 访问交易所
- **SSH 隧道**`ssh -D` 在本机常驻可用 **tmux****autossh** 保持连接),**不要** 把 `ssh` 交给 PM2
- 使用 **PM2** 仅托管 **Flask 应用**仓库根目录 **`ecosystem.config.cjs`** 默认进程名为 **`crypto-monitor-binance`**
> 安全提醒不要把 `.env`私钥 `.pem`Binance API Key / Secret 提交到 Git下文只用占位符
---
## 0. 你需要准备的东西
- 一台 **Ubuntu**或同类 Linux运行项目的机器下文称「本机」
- 一台可 SSH 登录**能正常访问 Binance API** 的 VPS示例`HostName` 填你的服务器 IP用户如 `root`
- SSH**私钥登录**推荐便于隧道脚本无人值守
- 本机已安装`python3``python3-venv``pip``curl``ssh``git`可选)、`node` + `npm`安装 PM2
- Binance 账户已开通 **USDT-M 永续合约**API Key 勾选 **合约****万向划转**若使用资金↔合约划转等所需权限并配置 **IP 白名单**若启用
---
## 1. 获取代码与目录
将包含 `app.py` 的项目放到固定目录例如
```bash
mkdir -p /opt/crypto_monitor
cd /opt/crypto_monitor
git clone https://git.bz121.com/dekun/crypto_monitor.git
cd crypto_monitor/crypto_monitor_binance
```
下文用 **`/opt/crypto_monitor/crypto_monitor_binance`** 仅为示例请换成你的实际绝对路径
拉取代码后若目录下尚无 `.env`先从模板生成**勿**把填好密钥的 `.env` 提交 Git):
```bash
cp -n .env.example .env # -n已存在 .env 时不覆盖
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 私钥示例~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`示例别名 **`bn-vps`**与你手工启动 `ssh -D ... bn-vps` 一致即可):
```sshconfig
Host bn-vps
HostName 你的_VPS_IP
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试
```bash
ssh bn-vps true
```
> 若尚未完全改为密钥登录可暂时注释 `BatchMode yes`调试完成后再打开
---
## 3. 手工验证SSH SOCKS + Binance API
### 3.1 本地 SOCKS示例端口 1080
```bash
ssh -N -D 127.0.0.1:1080 bn-vps
```
保持运行另开终端继续
### 3.2 验证经 SOCKS 可访问 Binance公开接口
```bash
curl -4 -sS --max-time 15 --proxy socks5h://127.0.0.1:1080 https://api.binance.com/api/v3/time
```
应返回 JSON`serverTime` 字段)。若此处失败**不要先启动应用**先修隧道或 VPS 出站
---
## 4. Python 虚拟环境
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
走 SOCKS 时 **必须** 安装 **`PySocks`**否则易出现代理相关报错
可选
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量`.env.example``.env`
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
cp -n .env.example .env # 已存在 .env 时不覆盖
nano .env # 填入 API登录密码端口代理等
```
### 5.2 备份与 `git pull`
- **`.env` 已被仓库根目录 `.gitignore` 忽略**`git pull` **不会**覆盖或删除你本地的 `.env`
- 若远端更新了 **`.env.example`**新增变量名),pull 后请对照模板**手动把新行补进你的 `.env`**不会自动合并进 `.env`)。
- **建议在每次 `git pull` 或大批量改配置前备份**
```bash
cp .env .env.backup.$(date +%Y%m%d)
# 恢复示例cp .env.backup.20260516 .env
```
- **换机 / 迁移**`scp` 复制整份 `.env` 到新机器对应目录或在新机重新 `cp .env.example .env` 后填写
### 5.3 AI 复盘与模型可选
三所共用仓库根目录 **`ai_client.py`**PM2 的 **`PYTHONPATH=..`** 须包含仓库根)。`.env` 中配置 **`AI_PROVIDER`**
| 模式 | 主要变量 |
|------|----------|
| **`openai`**默认 | `OPENAI_API_BASE=https://op.bz121.com/v1``OPENAI_API_KEY``OPENAI_MODEL=gemma4:e4b` |
| **`ollama`** | `OLLAMA_API``AI_MODEL`本机 Ollama |
密钥在 [op.bz121.com](https://op.bz121.com/) 的 **`gateway.json`** 页面获取`.env` 后需 **`pm2 restart`** 对应进程详见根目录 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**
### 5.4 自动备份数据库 + 复盘图片
默认每天 **北京时间 0:00** 备份到 **`/root/backups`**保留 **30 天** 后自动删除更早的目录
备份内容路径来自 `.env``DB_PATH``UPLOAD_DIR`):
- `crypto.db`优先 `sqlite3 .backup` 热备
- `static/images` 打包为 `static_images.tar.gz`
目录结构示例
```text
/root/backups/crypto_monitor_binance/2026-05-17/
crypto.db
static_images.tar.gz
manifest.txt
```
**一次性安装定时任务**在对应项目目录执行Binance / Gate 各执行一次):
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
chmod +x scripts/backup_data.sh scripts/install_backup_cron.sh
bash scripts/install_backup_cron.sh
```
Gate 实例
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
bash scripts/install_backup_cron.sh
```
实例趋势回调等):
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
bash scripts/install_backup_cron.sh
```
**立即试跑**不写 cron):
```bash
bash scripts/backup_data.sh
```
日志默认`/var/log/crypto-monitor-backup-<项目目录名>.log`可选在 `.env` 中覆盖`BACKUP_ROOT``BACKUP_RETENTION_DAYS``BACKUP_INSTANCE`
**恢复示例**先停 PM2再覆盖文件):
```bash
pm2 stop crypto-monitor-binance
cp /root/backups/crypto_monitor_binance/2026-05-16/crypto.db ./crypto.db
tar -xzf /root/backups/crypto_monitor_binance/2026-05-16/static_images.tar.gz -C .
pm2 start ecosystem.config.cjs
```
建议安装`apt install -y sqlite3`热备更稳)。
### 5.5 必填项检查Binance + 代理
与交易所相关的变量使用 **`BINANCE_`** 前缀与代码一致)。至少确认
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# 实盘按需
LIVE_TRADING_ENABLED=false
BINANCE_API_KEY=你的_Key
BINANCE_API_SECRET=你的_Secret
# 保证金cross=全仓 isolated=逐仓与币安账户/习惯一致
BINANCE_MARGIN_MODE=cross
# 持仓模式hedge=双向需在币安开启双向持仓);oneway=单向
BINANCE_POSITION_MODE=hedge
# 条件单触发参考CONTRACT_PRICE=最新成交价 MARK_PRICE=标记价
BINANCE_TRIGGER_WORKING_TYPE=CONTRACT_PRICE
# 经本机 SSH 动态转发访问 Binance端口与隧道一致
BINANCE_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 若不用 SOCKS可改用 HTTP 代理一般二选一
# BINANCE_HTTP_PROXY=http://127.0.0.1:7890
# BINANCE_HTTPS_PROXY=http://127.0.0.1:7890
```
说明**推荐 `socks5h://`**由 SOCKS 端解析域名`curl --proxy socks5h://...` 行为一致
**止盈止损说明应用逻辑**实盘开仓后程序会在 Binance USDT-M 永续上挂 **`STOP_MARKET`止损** 与 **`TAKE_PROFIT_MARKET`止盈**`BINANCE_POSITION_MODE=hedge` 时会自动带 **`positionSide`**须与币安合约「双向持仓」开关一致不显式传 **`reduceOnly`**否则易触发 API **`-1106`**`Parameter 'reduceOnly' sent when not required`)。
---
## 6. 自检脚本可选
在已配置 `.env` 且网络可达的前提下
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
source .venv/bin/activate
python scripts/verify_binance_funding.py
```
用于粗测资金钱包与合约钱包 USDT 读取需有效 API 与权限)。
---
## 7. 手工启动 Flask验证
1. SOCKS 已监听 `127.0.0.1:1080`若使用代理
2. 已 `source .venv/bin/activate`
3. `.env` 已按需配置 `BINANCE_SOCKS_PROXY`
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
source .venv/bin/activate
python app.py
```
浏览器访问`http://127.0.0.1:5000`或你在 `.env` 中的端口)。
---
## 8. 安装 PM2
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 9. PM2使用仓库内 `ecosystem.config.cjs`推荐
在项目根目录
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
pm2 start ecosystem.config.cjs
pm2 status
pm2 logs --lines 200
```
默认只启动 **`crypto-monitor-binance`**`.venv/bin/python app.py`)。
### 本机已可直连 Binance不需要隧道时
`.env` 里应 **去掉或留空** `BINANCE_SOCKS_PROXY`除非仍要走别的代理),`pm2 start ecosystem.config.cjs`
### 开机自启
```bash
pm2 save
pm2 startup
# 按屏幕提示执行一条 sudo 命令
```
---
## 10. 等价手工命令不使用 ecosystem 文件时
### 10.1 SSH SOCKS自行后台常驻不推荐用 PM2
示例前台调试生产请用 **PM2**见本文 §6 与 [docs/ubuntu-server.md](../docs/ubuntu-server.md)):
```bash
ssh -N -D 127.0.0.1:1080 bn-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes
```
### 10.2 Flask
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
pm2 start /opt/crypto_monitor/crypto_monitor_binance/.venv/bin/python --name crypto-monitor-binance -- \
/opt/crypto_monitor/crypto_monitor_binance/app.py
```
---
## 11. 交易所「连接不上」排查清单
1. **`.env` 是否为 Binance 变量**`BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY` / `BINANCE_API_KEY` / `BINANCE_API_SECRET` 等前缀需与代码一致
2. **隧道是否在本机端口监听**若配置了 `BINANCE_SOCKS_PROXY`):
```bash
ss -lntp | grep 1080 || true
```
3. **curl 复测 Binance**与第 3.2 节相同);curl 不通则应用也不会通
4. **PySocks**`pip show PySocks`缺失则 `pip install PySocks`
5. **SSH 隧道连不上**检查私钥权限`~/.ssh/config`VPS 出站与端口是否与 `.env` 一致
6. **API 权限与 IP 白名单**Secret 错误权限不足未放行当前出口 IP 时私有接口会失败
7. **启动顺序**若走代理先保证 SOCKS 已监听`pm2 start` 应用或重启应用)。
---
## 12. 推荐启动顺序习惯
1. 若走代理先启动并确认 SSH SOCKS 已监听`curl --proxy socks5h://127.0.0.1:1080 https://api.binance.com/api/v3/time` 成功
2. `pm2 start ecosystem.config.cjs`
3. 再确认页面与余额等接口正常
---
## 13. 免责声明
交易所有合规与地区政策要求请确保使用方式符合当地法律法规与交易所条款本文仅描述网络与工程部署路径
---
## 附录数据库标签修复脚本 `scripts/fix_breakeven_labels.py`
在 Ubuntu 上
1预览不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2确认后执行
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件`monitor_type='下单监控'``result='止损'``pnl_amount > 0` → 改为 `result='保本止盈'`
# `crypto_monitor_binance` 部署指南:SSH SOCKS + Binance + PM2(Ubuntu)
项目功能,环境变量总览见 **[README.md](./README.md)**.Ubuntu 环境(Python / Node / PM2)**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.
本文面向:**在本机或 VPS 上运行本项目**,**直连 Binance API 不稳定,超时或被网络策略拦截** 的场景.思路是:
- 本机用 `ssh -D` 做动态转发,把 **SOCKS5 出口**放到能稳定访问 Binance 的机器(常见为一台境外 VPS)
- 项目在 `.env` 中设置 **`BINANCE_SOCKS_PROXY=socks5h://127.0.0.1:1080`**(或你实际端口),`ccxt` 经 SOCKS 访问交易所
- **SSH 隧道**:`ssh -D` 在本机常驻(可用 **tmux****autossh** 保持连接),**不要** 把 `ssh` 交给 PM2
- 使用 **PM2** 仅托管 **Flask 应用**;仓库根目录 **`ecosystem.config.cjs`** 默认进程名为 **`crypto-monitor-binance`**
> 安全提醒:不要把 `.env`,私钥 `.pem`,Binance API Key / Secret 提交到 Git;下文只用占位符.
---
## 0. 你需要准备的东西
- 一台 **Ubuntu**(或同类 Linux)运行项目的机器(下文称「本机」)
- 一台可 SSH 登录,**能正常访问 Binance API** 的 VPS(示例:`HostName` 填你的服务器 IP,用户如 `root`)
- SSH:**私钥登录**(推荐,便于隧道脚本无人值守)
- 本机已安装:`python3`,`python3-venv`,`pip`,`curl`,`ssh`,`git`(可选),`node` + `npm`(安装 PM2)
- Binance 账户:已开通 **USDT-M 永续合约**;API Key 勾选 **合约**,**万向划转**(若使用资金↔合约划转)等所需权限,并配置 **IP 白名单**(若启用)
---
## 1. 获取代码与目录
将包含 `app.py` 的项目放到固定目录,例如:
```bash
mkdir -p /opt/crypto_monitor
cd /opt/crypto_monitor
git clone https://git.bz121.com/dekun/crypto_monitor.git
cd crypto_monitor/crypto_monitor_binance
```
下文用 **`/opt/crypto_monitor/crypto_monitor_binance`** 仅为示例,请换成你的实际绝对路径.
拉取代码后,若目录下尚无 `.env`,先从模板生成(**勿**把填好密钥的 `.env` 提交 Git):
```bash
cp -n .env.example .env # -n:已存在 .env 时不覆盖
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 私钥示例:~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`(示例别名 **`bn-vps`**,与你手工启动 `ssh -D ... bn-vps` 一致即可):
```sshconfig
Host bn-vps
HostName 你的_VPS_IP
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试:
```bash
ssh bn-vps true
```
> 若尚未完全改为密钥登录,可暂时注释 `BatchMode yes`,调试完成后再打开.
---
## 3. 手工验证:SSH SOCKS + Binance API
### 3.1 本地 SOCKS(示例端口 1080)
```bash
ssh -N -D 127.0.0.1:1080 bn-vps
```
保持运行,另开终端继续.
### 3.2 验证经 SOCKS 可访问 Binance(公开接口)
```bash
curl -4 -sS --max-time 15 --proxy socks5h://127.0.0.1:1080 https://api.binance.com/api/v3/time
```
应返回 JSON(`serverTime` 字段).若此处失败,**不要先启动应用**:先修隧道或 VPS 出站.
---
## 4. Python 虚拟环境
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
走 SOCKS 时 **必须** 安装 **`PySocks`**,否则易出现代理相关报错.
可选:
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量(`.env.example``.env`)
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释,可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置;`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
cp -n .env.example .env # 已存在 .env 时不覆盖
nano .env # 填入 API,登录密码,端口,代理等
```
### 5.2 备份与 `git pull`
- **`.env` 已被仓库根目录 `.gitignore` 忽略**:`git pull` **不会**覆盖或删除你本地的 `.env`.
- 若远端更新了 **`.env.example`**(新增变量名),pull 后请对照模板,**手动把新行补进你的 `.env`**(不会自动合并进 `.env`).
- **建议在每次 `git pull` 或大批量改配置前备份**:
```bash
cp .env .env.backup.$(date +%Y%m%d)
# 恢复示例:cp .env.backup.20260516 .env
```
- **换机 / 迁移**:`scp` 复制整份 `.env` 到新机器对应目录;或在新机重新 `cp .env.example .env` 后填写.
### 5.3 AI 复盘与模型(可选)
三所共用仓库根目录 **`ai_client.py`**(PM2 的 **`PYTHONPATH=..`** 须包含仓库根).`.env` 中配置 **`AI_PROVIDER`**:
| 模式 | 主要变量 |
|------|----------|
| **`openai`**(默认) | `OPENAI_API_BASE=https://op.bz121.com/v1`,`OPENAI_API_KEY`,`OPENAI_MODEL=gemma4:e4b` |
| **`ollama`** | `OLLAMA_API`,`AI_MODEL`(本机 Ollama) |
密钥在 [op.bz121.com](https://op.bz121.com/) 的 **`gateway.json`** 页面获取.`.env` 后需 **`pm2 restart`** 对应进程.详见根目录 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**.
### 5.4 自动备份(数据库 + 复盘图片)
默认每天 **北京时间 0:00** 备份到 **`/root/backups`**,保留 **30 天** 后自动删除更早的目录.
备份内容(路径来自 `.env``DB_PATH`,`UPLOAD_DIR`):
- `crypto.db`(优先 `sqlite3 .backup` 热备)
- `static/images` 打包为 `static_images.tar.gz`
目录结构示例:
```text
/root/backups/crypto_monitor_binance/2026-05-17/
crypto.db
static_images.tar.gz
manifest.txt
```
**一次性安装定时任务**(在对应项目目录执行,Binance / Gate 各执行一次):
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
chmod +x scripts/backup_data.sh scripts/install_backup_cron.sh
bash scripts/install_backup_cron.sh
```
Gate 实例:
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
bash scripts/install_backup_cron.sh
```
实例(趋势回调等):
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
bash scripts/install_backup_cron.sh
```
**立即试跑**(不写 cron):
```bash
bash scripts/backup_data.sh
```
日志默认:`/var/log/crypto-monitor-backup-<项目目录名>.log`.可选在 `.env` 中覆盖:`BACKUP_ROOT`,`BACKUP_RETENTION_DAYS`,`BACKUP_INSTANCE`.
**恢复示例**(先停 PM2,再覆盖文件):
```bash
pm2 stop crypto-monitor-binance
cp /root/backups/crypto_monitor_binance/2026-05-16/crypto.db ./crypto.db
tar -xzf /root/backups/crypto_monitor_binance/2026-05-16/static_images.tar.gz -C .
pm2 start ecosystem.config.cjs
```
建议安装:`apt install -y sqlite3`(热备更稳).
### 5.5 必填项检查(Binance + 代理)
与交易所相关的变量使用 **`BINANCE_`** 前缀(与代码一致).至少确认:
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# 实盘(按需)
LIVE_TRADING_ENABLED=false
BINANCE_API_KEY=你的_Key
BINANCE_API_SECRET=你的_Secret
# 保证金:cross=全仓 isolated=逐仓(与币安账户/习惯一致)
BINANCE_MARGIN_MODE=cross
# 持仓模式:hedge=双向(需在币安开启双向持仓);oneway=单向
BINANCE_POSITION_MODE=hedge
# 条件单触发参考:CONTRACT_PRICE=最新成交价 MARK_PRICE=标记价
BINANCE_TRIGGER_WORKING_TYPE=CONTRACT_PRICE
# 经本机 SSH 动态转发访问 Binance(端口与隧道一致)
BINANCE_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 若不用 SOCKS,可改用 HTTP 代理(一般二选一)
# BINANCE_HTTP_PROXY=http://127.0.0.1:7890
# BINANCE_HTTPS_PROXY=http://127.0.0.1:7890
```
说明:**推荐 `socks5h://`**,由 SOCKS 端解析域名,`curl --proxy socks5h://...` 行为一致.
**止盈止损说明(应用逻辑)**:实盘开仓后,程序会在 Binance USDT-M 永续上挂 **`STOP_MARKET`(止损)** 与 **`TAKE_PROFIT_MARKET`(止盈)**;`BINANCE_POSITION_MODE=hedge` 时会自动带 **`positionSide`**,须与币安合约「双向持仓」开关一致.不显式传 **`reduceOnly`**(否则易触发 API **`-1106`**:`Parameter 'reduceOnly' sent when not required`).
---
## 6. 自检脚本(可选)
在已配置 `.env` 且网络可达的前提下:
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
source .venv/bin/activate
python scripts/verify_binance_funding.py
```
用于粗测资金钱包与合约钱包 USDT 读取(需有效 API 与权限).
---
## 7. 手工启动 Flask(验证)
1. SOCKS 已监听 `127.0.0.1:1080`(若使用代理)
2. 已 `source .venv/bin/activate`
3. `.env` 已按需配置 `BINANCE_SOCKS_PROXY`
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
source .venv/bin/activate
python app.py
```
浏览器访问:`http://127.0.0.1:5000`(或你在 `.env` 中的端口).
---
## 8. 安装 PM2
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 9. PM2:使用仓库内 `ecosystem.config.cjs`(推荐)
在项目根目录:
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
pm2 start ecosystem.config.cjs
pm2 status
pm2 logs --lines 200
```
默认只启动 **`crypto-monitor-binance`**(`.venv/bin/python app.py`).
### 本机已可直连 Binance,不需要隧道时
`.env` 里应 **去掉或留空** `BINANCE_SOCKS_PROXY`(除非仍要走别的代理),`pm2 start ecosystem.config.cjs`.
### 开机自启
```bash
pm2 save
pm2 startup
# 按屏幕提示执行一条 sudo 命令
```
---
## 10. 等价手工命令(不使用 ecosystem 文件时)
### 10.1 SSH SOCKS(自行后台常驻,不推荐用 PM2)
示例(前台调试;生产请用 **PM2**,见本文 §6 与 [docs/ubuntu-server.md](../docs/ubuntu-server.md)):
```bash
ssh -N -D 127.0.0.1:1080 bn-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes
```
### 10.2 Flask
```bash
cd /opt/crypto_monitor/crypto_monitor_binance
pm2 start /opt/crypto_monitor/crypto_monitor_binance/.venv/bin/python --name crypto-monitor-binance -- \
/opt/crypto_monitor/crypto_monitor_binance/app.py
```
---
## 11. 交易所「连接不上」排查清单
1. **`.env` 是否为 Binance 变量**:`BINANCE_SOCKS_PROXY` / `BINANCE_HTTP_PROXY` / `BINANCE_API_KEY` / `BINANCE_API_SECRET` 等前缀需与代码一致.
2. **隧道是否在本机端口监听**(若配置了 `BINANCE_SOCKS_PROXY`):
```bash
ss -lntp | grep 1080 || true
```
3. **curl 复测 Binance**(与第 3.2 节相同);curl 不通则应用也不会通.
4. **PySocks**:`pip show PySocks`,缺失则 `pip install PySocks`.
5. **SSH 隧道连不上**:检查私钥权限,`~/.ssh/config`,VPS 出站与端口是否与 `.env` 一致.
6. **API 权限与 IP 白名单**:Secret 错误,权限不足,未放行当前出口 IP 时,私有接口会失败.
7. **启动顺序**:若走代理,先保证 SOCKS 已监听,`pm2 start` 应用(或重启应用).
---
## 12. 推荐启动顺序(习惯)
1. 若走代理:先启动并确认 SSH SOCKS 已监听,`curl --proxy socks5h://127.0.0.1:1080 https://api.binance.com/api/v3/time` 成功
2. `pm2 start ecosystem.config.cjs`
3. 再确认页面与余额等接口正常
---
## 13. 免责声明
交易所有合规与地区政策要求.请确保使用方式符合当地法律法规与交易所条款.本文仅描述网络与工程部署路径.
---
## 附录:数据库标签修复脚本 `scripts/fix_breakeven_labels.py`
在 Ubuntu 上:
1)预览(不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2)确认后执行:
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件:`monitor_type='下单监控'``result='止损'``pnl_amount > 0` → 改为 `result='保本止盈'`.
+78 -78
View File
@@ -1,127 +1,127 @@
# =============================================================================
# 环境配置模板可提交 Git)。程序运行时只读取同目录下的 .env
# 环境配置模板(可提交 Git).程序运行时只读取同目录下的 .env.
#
# 首次部署 / 新机
# 首次部署 / 新机:
# cp .env.example .env
# nano .env # 填入真实密钥端口代理等
# nano .env # 填入真实密钥,端口,代理等
#
# 升级代码git pull前建议备份.env 不在 Git 中pull 不会覆盖):
# 升级代码(git pull)前建议备份(.env 不在 Git 中,pull 不会覆盖):
# cp .env .env.backup.$(date +%Y%m%d)
#
# 从备份恢复
# 从备份恢复:
# cp .env.backup.YYYYMMDD .env
# =============================================================================
APP_ENV=production
# 服务监听地址云服务器通常用 0.0.0.0
# 服务监听地址(云服务器通常用 0.0.0.0)
APP_HOST=0.0.0.0
# 服务端口
APP_PORT=5000
# 是否开启调试模式生产建议 false
# 是否开启调试模式(生产建议 false)
APP_DEBUG=false
# 登录账号
APP_USERNAME=admin
# 登录密码请改成你自己的强密码
# 登录密码(请改成你自己的强密码)
APP_PASSWORD=admin123
# 是否关闭登录校验局域网可设 true公网务必 false
# 是否关闭登录校验(局域网可设 true;公网务必 false)
APP_AUTH_DISABLED=true
# --- 多账户交易中控 manual_trading_hub ---
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时仅网页登录后可访问本机联调可保持 APP_AUTH_DISABLED=true
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true
# HUB_BRIDGE_TOKEN=your-long-random-token
# Flask 会话密钥必须替换为长随机字符串
# Flask 会话密钥(必须替换为长随机字符串)
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
# 企业微信机器人 Webhook用于行情/风控推送
# 企业微信机器人 Webhook(用于行情/风控推送)
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_WITH_REAL_KEY
# 数据库文件路径相对路径会自动按项目目录解析
# 数据库文件路径(相对路径会自动按项目目录解析)
DB_PATH=crypto.db
# 交易截图上传目录
UPLOAD_DIR=static/images
# 自动备份scripts/backup_data.sh + cron可选默认即可
# 自动备份(scripts/backup_data.sh + cron,可选;默认即可)
# BACKUP_ROOT=/root/backups
# BACKUP_RETENTION_DAYS=30
# BACKUP_INSTANCE=crypto_monitor_gate
# 已废弃资金账户仅显示交易所 funding 余额不再读取此变量
# 已废弃:资金账户仅显示交易所 funding 余额,不再读取此变量
# TOTAL_CAPITAL=100
# 计仓risk=以损定仓默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆须无仓后重启
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk
# 方向限制默认 false=双向均可true 时按 TRADE_DIRECTION 限制修改后须重启
# TRADE_DIRECTION=long_only | short_only | both或 多/空/双向
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单默认 false=全币种可手输true 时关键位/下单/策略仅下拉选择
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数U
# 每天起始基数(U)
DAILY_START_CAPITAL=30
# 日内回撤后基数U
# 日内回撤后基数(U)
DAILY_LOSS_CAPITAL=20
# 日内盈利后基数U
# 日内盈利后基数(U)
DAILY_PROFIT_CAPITAL=50
# BTC 默认杠杆倍数
BTC_LEVERAGE=10
# 山寨币默认杠杆倍数
ALT_LEVERAGE=5
# 交易日重置小时北京时间
# 交易日重置小时(北京时间)
TRADING_DAY_RESET_HOUR=8
# 整点前禁止新开仓true=启用默认),false=关闭仍可保留 8 点作为交易日划分
# 整点前禁止新开仓:true=启用(默认),false=关闭(仍可保留 8 点作为交易日划分)
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
# 是否开启 Gate 实盘下单false=只做本地流程true=真实下单
# 是否开启 Gate 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true
# Gate API Key实盘
# Gate API Key(实盘)
GATE_API_KEY=REPLACE_WITH_GATE_API_KEY
# Gate API Secret实盘
# Gate API Secret(实盘)
GATE_API_SECRET=REPLACE_WITH_GATE_API_SECRET
# 保证金模式cross=全仓isolated=逐仓
# 保证金模式:cross=全仓,isolated=逐仓
GATE_TD_MODE=cross
# 持仓筛选hedge=双向持仓下按多空腿过滤其它值如 single不按腿过滤
# 持仓筛选:hedge=双向持仓下按多空腿过滤;其它值(如 single)不按腿过滤
GATE_POS_MODE=hedge
# 永续止盈止损是否优先用官方仓位类触发单POST price_ordersclose-*-position);false=仅用旧版两张 ccxt 条件单
# 永续止盈止损:是否优先用官方仓位类触发单(POST price_orders,close-*-position);false=仅用旧版两张 ccxt 条件单
GATE_TPSL_USE_POSITION_ORDER=true
# 触发单超时(秒),默认 604800=7 天设为 0 或负数则不向 API 传 expiration
# 触发单超时(秒),默认 604800=7 天;设为 0 或负数则不向 API 传 expiration
GATE_TPSL_TRIGGER_EXPIRATION=604800
# 触发参考价0=最新成交 1=标记价 2=指数价非法值按 0
# 触发参考价:0=最新成交 1=标记价 2=指数价(非法值按 0)
GATE_TPSL_PRICE_TYPE=0
# 仓位类 TP/SL 相对现价的最小间距%),避免 Gate 1026「触发价须高于/低于现价」
# 仓位类 TP/SL 相对现价的最小间距(%),避免 Gate 1026「触发价须高于/低于现价」
GATE_TPSL_LAST_PRICE_GAP_PCT=0.05
# 页面与浏览器标签展示的交易所名称多环境区分时可改成例如 Gate·模拟
# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Gate·模拟)
# EXCHANGE_DISPLAY_NAME=Gate.io
# =============================================================================
# 关键位程序自动下单与 POSITION_SIZING_MODE 联动修改后须重启 PM2
# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2)
# =============================================================================
# 默认 false = 关闭所有关键位程序自动单箱体/收敛/斐波/假突破/触价
# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价)
#
# POSITION_SIZING_MODE=risk以损定仓
# false → 不执行任何关键位自动单支撑/阻力提醒人工下单顺势加仓不受影响
# true → 允许关键位全套自动含触价
# POSITION_SIZING_MODE=risk(以损定仓)
# false → 不执行任何关键位自动单;支撑/阻力提醒,人工下单,顺势加仓不受影响
# true → 允许关键位全套自动(含触价)
#
# POSITION_SIZING_MODE=full_margin全仓杠杆须无仓切换
# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换)
# false → 不执行触价自动单
# true → 仅回调/突破触价可程序自动开仓箱体/斐波等仍禁止
# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止
#
# 顺势加仓趋势回调不受本开关控制全仓模式下策略自动仍禁止
# 顺势加仓,趋势回调不受本开关控制;全仓模式下策略自动仍禁止.
KEY_AUTO_ORDER_ENABLED=false
# =============================================================================
# 关键位门控页面「关键位监控」规则条与 _key_hard_checks 共用
# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用)
# =============================================================================
# 【周期】门控 K 线周期如 5m15m
# 【周期】门控 K 线周期,如 5m,15m
KLINE_TIMEFRAME=5m
# 【确认K】闭合 K 序列中的棒偏移突破棒默认 -2确认棒默认 -1
# 【确认K】闭合 K 序列中的棒偏移:突破棒默认 -2,确认棒默认 -1
KEY_CONFIRM_BREAKOUT_BAR=-2
KEY_CONFIRM_BAR=-1
# 【量能】突破棒成交量 > 前 N 根均量 × 倍数
KEY_VOLUME_MA_BARS=20
KEY_VOLUME_RATIO_MIN=1.3
# 【突破K实体幅度】占开盘价百分比区间
# 【箱体/收敛】突破K收盘越过关键位下限%无上限过猛由计划RR过滤
# 【箱体/收敛】突破K收盘越过关键位下限%;无上限(过猛由计划RR过滤)
KEY_BREAKOUT_AMP_MIN_PCT=0.03
KEY_BREAKOUT_AMP_MAX_PCT=0.5
# 【阻力/支撑】突破后微信提醒
@@ -131,29 +131,29 @@ KEY_ALERT_INTERVAL_MINUTES=5
KEY_DAILY_VOLUME_RANK_MAX=30
# 【关键位自动开仓盈亏比】严格大于该值才市价开仓
KEY_AUTO_MIN_PLANNED_RR=1.5
# 止损突破 K 极值向外缓冲的百分比默认 0.5 即 0.5%
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
# 趋势单方案止损在突破 K 极值外侧的百分比默认 1 即 1%
# 趋势单方案:止损在突破 K 极值外侧的百分比(默认 1 即 1%)
KEY_TREND_STOP_OUTSIDE_PCT=1
KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5
# =============================================================================
# 交易执行 / 人工风控页面「实盘下单」
# 交易执行 / 人工风控(页面「实盘下单」)
# =============================================================================
# 【最大同时持仓】默认 1=单仓
MAX_ACTIVE_POSITIONS=1
# 【人工下单最低盈亏比】低于该值前后端均拒绝默认 1.4即须 >=1.4:1
# 【人工下单最低盈亏比】低于该值前后端均拒绝(默认 1.4,即须 >=1.4:1)
MANUAL_MIN_PLANNED_RR=1.4
# 【关键位连开计仓】已有持仓时按无仓时资金快照算基数
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒不拦单
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒(不拦单)
DAILY_OPEN_ALERT_THRESHOLD=5
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日(北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
DAILY_OPEN_HARD_LIMIT=0
# =============================================================================
# 账户冷静期 / 日冻结风控手动平仓外部平仓复盘情绪标签
# 账户冷静期 / 日冻结风控(手动平仓,外部平仓,复盘情绪标签)
# 详见 docs/account-risk-cooldown.md
# =============================================================================
RISK_CONTROL_ENABLED=true
@@ -162,40 +162,40 @@ RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
RISK_MOOD_ISSUES_DAILY_FREEZE=true
# 资金与仓位刷新周期(秒)
# 资金与仓位刷新周期(秒)
BALANCE_REFRESH_SECONDS=60
# 前端价格快照轮询(秒)
# 前端价格快照轮询(秒)
PRICE_REFRESH_SECONDS=5
# 后台监控轮询周期(秒)
# 后台监控轮询周期(秒)
MONITOR_POLL_SECONDS=3
# 重启后多少秒内不做「外部平仓」同步避免 API 未就绪误判
# 重启后多少秒内不做「外部平仓」同步(避免 API 未就绪误判)
RECONCILE_STARTUP_GRACE_SEC=90
# 连续多少次轮询确认交易所空仓后才记为外部平仓默认 3 次 ≈ 9 秒
# 连续多少次轮询确认交易所空仓后,才记为外部平仓(默认 3 次 ≈ 9 秒)
RECONCILE_FLAT_CONFIRM_POLLS=3
# 使用可用资金时的缓冲比例如0.98代表用98%
# 使用可用资金时的缓冲比例(如0.98代表用98%)
FULL_MARGIN_BUFFER_RATIO=0.98
# =============================================================================
# 自动划转页顶「将 swap 补足到 XU」与 DAILY_START_CAPITAL 独立需一致时请设为相同值
# 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值)
# =============================================================================
AUTO_TRANSFER_ENABLED=false
# 交易账户(swap)目标余额 U每日 8 点(北京)自动划入或划出至 funding持仓中不划转
# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转
AUTO_TRANSFER_AMOUNT=30
AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap
TRANSFER_CCY=USDT
# 北京时间该整点小时内尝试账簿按 UTC 自然日去重
# 北京时间该整点小时内尝试;账簿按 UTC 自然日去重
AUTO_TRANSFER_BJ_HOUR=8
# 强制清仓整点北京时间默认 0=凌晨00点
# 强制清仓整点(北京时间,默认 0=凌晨00点)
FORCE_CLOSE_BJ_HOUR=0
# 是否启用强制清仓默认关闭true 才会在整点执行
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
FORCE_CLOSE_ENABLED=false
# 推送与AI超时(秒)
# 推送与AI超时(秒)
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 提供方openai默认| ollama
# AI 提供方:openai(默认)| ollama
AI_PROVIDER=openai
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
@@ -203,31 +203,31 @@ OPENAI_MODEL=gemma4:e4b
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# Gate 代理可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
# 1) 先在本机建立隧道示例):
# Gate 代理(可选):本机网络不稳定时通过 SSH 动态转发 SOCKS5 出口
# 1) 先在本机建立隧道(示例):
# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes
# 2) 再启用下面这一行推荐 socks5h让远端解析域名):
# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名):
# GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080
#
# 如你更偏向 HTTP 代理VPS 上跑 tinyproxy 之类),可用
# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用:
# GATE_HTTP_PROXY=http://127.0.0.1:3128
# GATE_HTTPS_PROXY=http://127.0.0.1:3128
# 开仓多周期K线图可选
# 开仓多周期K线图(可选)
# ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts
# 详见 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT说明文档 docs/daily-open-limit.md
# 以损定仓按交易账户资金的百分比
# 详见 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT;说明文档 docs/daily-open-limit.md
# 以损定仓(按交易账户资金的百分比)
# RISK_PERCENT=2
# 移动保本触发达到多少R触发与偏移百分比
# 移动保本触发(达到多少R触发)与偏移(百分比)
# BREAKEVEN_RR_TRIGGER=1.0
# 移动保本阶梯每多少R继续上移一次默认1R
# 移动保本阶梯(每多少R继续上移一次,默认1R)
# BREAKEVEN_STEP_R=1.0
# BREAKEVEN_OFFSET_PCT=0.02
# 开单风格默认值trend / swing
# 开单风格默认值:trend / swing
# DEFAULT_TRADE_STYLE=trend
APP_TIMEZONE=Asia/Shanghai
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点默认 8 点起算新交易日开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
+29 -29
View File
@@ -1,34 +1,34 @@
# crypto_monitor_gate
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统行情与实盘接口统一走 **Gate.io USDT 永续**通过 **ccxt** 访问
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Gate.io USDT 永续**,通过 **ccxt** 访问.
## 文档导航
| 文档 | 说明 |
|------|------|
| **[使用说明.md](./使用说明.md)** | 日常怎么用登录关键位四类手工开仓单仓与微信等 |
| **[关键位自动下单说明.md](./关键位自动下单说明.md)** | 关键位自动开仓的 RR止盈止损结案原因与 `.env` |
| **[部署文档.md](./部署文档.md)** | UbuntuPM2**SSH SOCKS** 访问 Gate API 等 |
| **[使用说明.md](./使用说明.md)** | 日常怎么用:登录,关键位四类,手工开仓,单仓与微信等 |
| **[关键位自动下单说明.md](./关键位自动下单说明.md)** | 关键位自动开仓的 RR,止盈止损,结案原因与 `.env` |
| **[部署文档.md](./部署文档.md)** | Ubuntu,PM2,**SSH SOCKS** 访问 Gate API 等 |
**Binance U 本位** 对等实现见同级的 **`crypto_monitor_binance`** 仓库
:**Binance U 本位** 对等实现见同级的 **`crypto_monitor_binance`** 仓库.
---
## 功能概要
- **关键位监控**5m 收线硬条件企业微信推送**箱体 / 收敛** 在 RR 达标时可 **自动市价开仓**见专门文档);**阻力 / 支撑** 仅单次提醒结案
- **下单监控**本地风控含移动保本)、止盈/止损触达后轮询尝试平仓并记账
- **实盘可选**`LIVE_TRADING_ENABLED=true` 且配置 **`GATE_API_KEY` / `GATE_API_SECRET`** 时支持开仓挂单 TP/SL余额与划转权限依账户而定
- **止盈止损Gate**市价成交后经 **`_gate_place_tp_sl_orders`** 挂单优先 **仓位类 `price_orders`**`GATE_TPSL_USE_POSITION_ORDER``GATE_TPSL_PRICE_TYPE``GATE_POS_MODE` 等影响
- **关键位监控**:5m 收线硬条件,企业微信推送;**箱体 / 收敛** 在 RR 达标时可 **自动市价开仓**(见专门文档);**阻力 / 支撑** 仅单次提醒结案
- **下单监控**:本地风控(含移动保本),止盈/止损触达后轮询尝试平仓并记账
- **实盘(可选)**:`LIVE_TRADING_ENABLED=true` 且配置 **`GATE_API_KEY` / `GATE_API_SECRET`** 时,支持开仓,挂单 TP/SL,余额与划转(权限依账户而定)
- **止盈止损(Gate)**:市价成交后经 **`_gate_place_tp_sl_orders`** 挂单;优先 **仓位类 `price_orders`**(`GATE_TPSL_USE_POSITION_ORDER`,`GATE_TPSL_PRICE_TYPE`,`GATE_POS_MODE` 等影响)
---
## 环境要求
- Python 3.10+建议
- 依赖`flask``requests``ccxt``werkzeug``PySocks`经 SOCKS 代理时);`Pillow`K 线导出等可选用
- Python 3.10+(建议)
- 依赖:`flask`,`requests`,`ccxt`,`werkzeug`,`PySocks`(经 SOCKS 代理时);`Pillow`(K 线导出等可选用)
安装示例
安装示例:
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
@@ -36,41 +36,41 @@ source .venv/bin/activate
pip install -r ../requirements.txt
```
## 配置`.env.example``.env`
## 配置(`.env.example``.env`)
- **`.env.example`**模板可提交 Git);首次`cp .env.example .env` 后编辑
- **`.env`**本机真实配置勿提交);`git pull` 不覆盖升级前建议备份见《部署文档》§5.2)。
- **`.env.example`**:模板(可提交 Git);首次:`cp .env.example .env` 后编辑.
- **`.env`**:本机真实配置(勿提交);`git pull` 不覆盖;升级前建议备份(见《部署文档》§5.2).
项目启动时加载**仓库根目录**下的 `.env`常用项
项目启动时加载**仓库根目录**下的 `.env`.常用项:
| 变量 | 说明 |
|------|------|
| `GATE_API_KEY` / `GATE_API_SECRET` | Gate API需合约与对应权限 |
| `LIVE_TRADING_ENABLED` | `true` 允许真实下单`false` 仅本地与推送逻辑 |
| `GATE_API_KEY` / `GATE_API_SECRET` | Gate API(需合约与对应权限) |
| `LIVE_TRADING_ENABLED` | `true` 允许真实下单;`false` 仅本地与推送逻辑 |
| `GATE_MARGIN_MODE` / `GATE_POS_MODE` | 保证金与持仓模式 |
| `GATE_TPSL_USE_POSITION_ORDER` / `GATE_TPSL_PRICE_TYPE` 等 | 条件止盈止损行为 |
| `GATE_SOCKS_PROXY` | 可选直连不稳时 SSH 动态转发详见部署文档 |
| `GATE_SOCKS_PROXY` | 可选;直连不稳时 SSH 动态转发(详见部署文档) |
| `APP_PASSWORD` / `FLASK_SECRET_KEY` | Web 登录与 Session |
| `WECHAT_WEBHOOK` | 企业微信机器人 |
| `EXCHANGE_DISPLAY_NAME` / `GATE_ACCOUNT_LABEL` | 页面与推送展示的账户文案 |
其余见 **`.env.example` 内注释** 或 **`app.py` 顶部默认值**
其余见 **`.env.example` 内注释** 或 **`app.py` 顶部默认值**.
## 运行
生产使用 **PM2**`ecosystem.config.cjs`)。调试
生产使用 **PM2**(`ecosystem.config.cjs`).调试:
```bash
source .venv/bin/activate && python app.py
```
见 [docs/ubuntu-server.md](../docs/ubuntu-server.md)
见 [docs/ubuntu-server.md](../docs/ubuntu-server.md).
端口由 **`APP_PORT`** 控制未设置默认 **5000**)。浏览器登录 **`/login`**口令为 **`APP_PASSWORD`**
端口由 **`APP_PORT`** 控制(未设置默认 **5000**).浏览器登录 **`/login`**,口令为 **`APP_PASSWORD`**.
## 部署Linux / PM2 / SSH SOCKS
## 部署(Linux / PM2 / SSH SOCKS)
**[部署文档.md](./部署文档.md)**
**[部署文档.md](./部署文档.md)**.
## 自检脚本
@@ -78,13 +78,13 @@ source .venv/bin/activate && python app.py
python scripts/verify_gate_funding.py
```
用于核对密钥前缀不落 Secret)、资金/合约可读性等需网络与权限)。
用于核对密钥前缀(不落 Secret),资金/合约可读性等(需网络与权限).
## 数据与脚本
- 默认 SQLite**`DB_PATH`** 指定常见为项目下 `crypto.db`
- `scripts/fix_breakeven_labels.py`修正「止损」但盈亏为正的记录标签参见部署文档说明
- 默认 SQLite:**`DB_PATH`** 指定(常见为项目下 `crypto.db`)
- `scripts/fix_breakeven_labels.py`:修正「止损」但盈亏为正的记录标签(参见部署文档说明)
## 风险与合规
实盘有亏损风险请确认 API 权限IP 白名单杠杆与保证金模式与 **Gate.io** 后台一致并遵守当地法律法规与交易所用户协议
实盘有亏损风险.请确认 API 权限,IP 白名单,杠杆与保证金模式与 **Gate.io** 后台一致,并遵守当地法律法规与交易所用户协议.
+378 -378
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,14 +1,14 @@
/**
* PM2 进程定义Ubuntu / Linux
* PM2 进程定义(Ubuntu / Linux).
*
* 仅托管 Flask 应用**SSH SOCKS 隧道** `ssh -D` 常驻可用 tmux / autossh勿交给 PM2
* `.env` `GATE_SOCKS_PROXY` 端口一致即可不必交给 PM2
* 仅托管 Flask 应用.**SSH SOCKS 隧道** `ssh -D` 常驻(可用 tmux / autossh),勿交给 PM2.
* `.env` `GATE_SOCKS_PROXY` 端口一致即可;不必交给 PM2.
*
* 使用前项目根目录存在 `.venv`且已安装依赖 SOCKS 时需 PySocks
* 使用前:项目根目录存在 `.venv`,且已安装依赖( SOCKS 时需 PySocks).
*
* 启动
* 启动:
* pm2 start ecosystem.config.cjs
* 保存开机列表
* 保存开机列表:
* pm2 save && pm2 startup
*/
const path = require("path");
@@ -1,18 +1,18 @@
#!/usr/bin/env python3
"""
一次性修复历史交易记录标签
trade_records 止损但实际盈利的记录改为保本止盈
一次性修复历史交易记录标签:
trade_records 止损但实际盈利的记录改为保本止盈.
默认条件可通过参数修改
默认条件(可通过参数修改):
- monitor_type = 下单监控
- result = 止损
- pnl_amount > 0
用法示例
1) 仅预览不落库
用法示例:
1) 仅预览(不落库):
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
2) 执行修复
2) 执行修复:
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
"""
@@ -1,9 +1,9 @@
"""
在项目根目录执行会加载根目录 .env
在项目根目录执行(会加载根目录 .env):
python scripts/verify_gate_funding.py
依次探测[0] swap 余额 App交易账户同源[1][3] 现货 / 统一账户资金路径
打印 GATE_API_KEY 8 位便于与 Gate 控制台核对不含 Secret用于服务器自检
依次探测:[0] swap 余额( App交易账户同源);[1][3] 现货 / 统一账户资金路径.
打印 GATE_API_KEY 8 位便于与 Gate 控制台核对(不含 Secret).用于服务器自检.
"""
from __future__ import annotations
@@ -39,12 +39,12 @@ def main():
k = (os.getenv("GATE_API_KEY") or "").strip()
s = (os.getenv("GATE_API_SECRET") or "").strip()
if not k or "REPLACE" in k.upper():
print("WARN: GATE_API_KEY 为空或仍像占位符请核对 .env")
print("WARN: GATE_API_KEY 为空或仍像占位符,请核对 .env")
if not s or "REPLACE" in s.upper():
print("WARN: GATE_API_SECRET 为空或仍像占位符请核对 .env")
print("WARN: GATE_API_SECRET 为空或仍像占位符,请核对 .env")
print("GATE_API_KEY prefix (8 chars):", (k[:8] + "") if len(k) > 8 else "(short)")
# 0) swap — 与 App「交易账户」余额同源优先看此项是否与网页一致
# 0) swap — 与 App「交易账户」余额同源(优先看此项是否与网页一致)
try:
bal = mod.exchange.fetch_balance({"type": "swap"})
v0 = mod._extract_usdt_total(bal)
@@ -29,9 +29,9 @@
<div class="row" style="justify-content:space-between">
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大100根K线</strong>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
</div>
<div class="status">最近刷新<span id="updated-at">--</span></div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
{% if orders %}
<div class="row" style="margin-top:10px">
@@ -53,7 +53,7 @@
<span id="load-status" class="status"></span>
</div>
{% else %}
<div class="empty">当前没有激活订单无法展示放大K线</div>
<div class="empty">当前没有激活订单,无法展示放大K线.</div>
{% endif %}
</div>
+147 -147
View File
@@ -1,147 +1,147 @@
# 使用说明
**本文件对应仓库`crypto_monitor_gate`Gate.io USDT 永续)。**
功能界面与 **Binance U 本位版**目录 `crypto_monitor_binance`基本一致差异主要在 **`.env` 里交易所密钥与部分参数名**`GATE_*` / `BINANCE_*`),文末有对照
**更细的部署SSH 代理PM2依赖安装** 见同目录 **`部署文档.md`**
**关键位自动开仓的规则RR结案原因** 见 **`关键位自动下单说明.md`**
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**主要能力包括
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型**5m 收线** 做硬条件过滤符合条件后 **企业微信** 提醒部分类型可 **自动市价开仓**见第 4 节与专门文档)。 |
| **实盘下单监控** | 手工填止损/止盈**以损定仓** 市价开单挂上条件止盈止损并在页面跟踪浮盈亏保本逻辑等 |
| **交易记录 / 复盘** | 平仓结果盈亏错过的单等归档与导出可选 **AI 复盘**见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md))。 |
| **策略交易** | 顶栏 `/strategy`趋势回调 + 顺势加仓双栏见 [策略交易说明.md](../策略交易说明.md) |
后台按 **`MONITOR_POLL_SECONDS`**默认几秒轮询行情与监控逻辑**切勿**在未理解规则时同时运行两套程序共用一个实盘账户
---
## 2. 运行前必须配置`.env`
首次在本目录执行 **`cp .env.example .env`**再编辑 `.env``.env` 勿提交 Git`git pull` 不会改你的 `.env`升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`)。
至少检查以下项具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`打开站点后的登录口令`FLASK_SECRET_KEY`Session 密钥请勿使用默认值 |
| **企业微信** | `WECHAT_WEBHOOK`告警与关键位推送机器人的 Webhook |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`**不会**向交易所发送开仓指令适合测试流程)。改为 `true` 且密钥正确才会实盘 |
| **交易所 API** | **本仓库** `GATE_API_KEY``GATE_API_SECRET`合约相关见 `GATE_MARGIN_MODE``GATE_POS_MODE``GATE_TPSL_*`**勿**把 `.env` 提交到 Git |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR``KEY_STOP_OUTSIDE_BREAKOUT_PCT`详见 `关键位自动下单说明.md`)。 |
| **AI 复盘** | `AI_PROVIDER=openai`默认`ollama`变量见 `.env.example` 与 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md) |
网络不稳定时可为 Gate 配置 **`GATE_SOCKS_PROXY`** 等**`部署文档.md`**)。
---
## 3. 如何启动与登录
1. 按 **`部署文档.md`** 建好虚拟环境安装依赖`flask``requests``ccxt`按需 `Pillow``PySocks`),配置好 `.env`
2. 启动 Flask 应用本仓库可用 **`ecosystem.config.cjs`** 交给 PM2或本地 `python app.py` / `flask run`以你当前脚本为准)。
3. 浏览器访问站点打开 **`/login`**使用 **`.env` 里的 `APP_PASSWORD`** 登录
登录后顶栏**关键位监控** | **实盘下单** | **策略交易**`/strategy`| **策略交易记录**`/strategy/records`| **交易记录与复盘** | **统计分析**
---
## 4. 关键位监控顶栏「关键位监控」→ `/key_monitor`
### 4.1 添加一条关键位
1. **币种**`BTC``BTC/USDT`会规范成内部符号)。
2. **类型**必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**`LIVE_TRADING_ENABLED=true` 且无其他持仓占位)。结案后本条从列表消失并记入历史 |
| **收敛突破** | 同上自动开仓类)。 |
| **关键阻力位** | **不自动开仓**触发后 **发 1 次微信**然后本条 **结案进历史** |
| **关键支撑位** | 同上仅提醒)。 |
| **回调触价开仓** | **不挂交易所限价**标记价回调触达 E 后 **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**标记价 **穿越 E 立即市价开仓**先触 SL/TP 侧失效有效期 **24h** |
3. **方向**做多 / 做空触价开仓 / 箱体 / 收敛 / 斐波必选阻力/支撑不选)。
4. **价位**箱体/收敛/阻力/支撑填 **上沿 / 下沿**触价开仓填 **入场 E / 止损 SL / 止盈 TP**
**限制**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**仍可添加「**关键阻力位 / 支撑位**」
**4h EMA55** 与你的方向逆势页面会 **额外 Flash 提示****不阻挡**提交
### 4.2 触发后会发生什么简版
- **箱体 / 收敛**门控通过后计算计划 SL/TP 与 RR不达标则 **微信说明 + `rr_insufficient` 结案**达标则尝试 **市价开仓**成功 **`auto_opened`**失败 **`exchange_failed`**——均 **不重试同一关键位**
- **阻力 / 支撑****单次推送****`key_level_alert_only`** 结案
详细公式结案字段与企业微信文案口径见 **`关键位自动下单说明.md`**
### 4.3 列表与历史
- 当前条目可 **删除**会按规则记入历史的情形见页面说明)。
- **关键位历史**已结案记录可配合导出链接若有做备份
---
## 5. 实盘下单顶栏「实盘下单」→ `/trade`
用于 **自己点按钮** 开单
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制默认 1与关键位自动单共用)。
- **人工开仓**时计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**默认 1.4:1),否则页面弹窗且后端拒绝
- 填写币种方向杠杆可选)、止损/止盈价格或百分比按表单说明)。
- 勾选是否启用 **移动保本** 等行为以 `.env`/页面默认值为准
平仓通过页面 **平仓**或等价入口),会从交易所市价处理并更新记录**删除/误操作可能造成真实盈亏**请先确认环境与方向
开仓成功后持仓卡片上会显示 **「来源」**手工单一般为 **下单监控**来自关键位自动单的为 **关键位监控**
---
## 6. 企业微信会看到什么
- 关键位按类型与结案结果推送RR 不足下单失败自动开仓成功仅阻力支撑提醒等),**每条关键位结案路径原则上一条主推送**详见 `关键位自动下单说明.md`)。
- 手工开仓平仓部分异常也会在规则满足时推送以代码与配置为准)。
若未配置 **`WECHAT_WEBHOOK`** 或网络失败可能只是看不到推送不代表逻辑未执行要紧操作请以 **交易所端持仓与挂单** 为准核对
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 验证页面录入推送再开小资金开实盘
2. **API 权限**仅开所需合约权限勿泄露密钥定期轮换
3. **单进程控盘**同一账户避免本程序与其他机器人 **重复开仓**
4. **自动备份**服务器上执行 `bash scripts/install_backup_cron.sh`每天北京时间 0:00 → `/root/backups`保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次
5. **升级代码后**启动时会跑 **数据库迁移**如新列 `order_monitors.monitor_type`);首次启动关注一下日志或无报错页面
---
## 8. 常见问题简要
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 5m 门控是否全通过页面门控摘要)、币种日成交量是否在规则内`KLINE_TIMEFRAME` |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED``KEY_AUTO_MIN_PLANNED_RR`计划 RR是否已有持仓API/余额报错微信或日志)。 |
| 加不了箱体/收敛 | 是否已有活跃持仓先平仓或改用「阻力/支撑位」仅提醒 |
| 推送收不到 | `WECHAT_WEBHOOK`企业微信机器人配额与网络 |
---
## 9. Binance 版`crypto_monitor_binance`差异速查
| 项目 | Gate 本仓库 | Binance 版 |
|------|-------------|------------|
| API 变量 | `GATE_API_KEY``GATE_API_SECRET``GATE_*` | `BINANCE_API_KEY``BINANCE_API_SECRET``BINANCE_*` |
| 实盘开关 | `LIVE_TRADING_ENABLED`通用 | 同上 |
| 止盈止损挂载路径 | `_gate_place_tp_sl_orders``GATE_TPSL_*` | `_binance_place_tp_sl_orders`U 本位条件单 |
| 资金显示舍入 | 以本仓库为准 | 与 **`FUNDS_DECIMALS`** 等一致 |
| 专门文档 | **`关键位自动下单说明.md`**各仓库有一份开头标明交易所 | 同左 |
操作流程登录关键位四类手工单单仓**两份程序一致**换目录`.env` 即可对照使用
# 使用说明
**本文件对应仓库:`crypto_monitor_gate`(Gate.io USDT 永续).**
功能,界面与 **Binance U 本位版**(目录 `crypto_monitor_binance`)基本一致,差异主要在 **`.env` 里交易所密钥与部分参数名**(`GATE_*` / `BINANCE_*`),文末有对照.
**更细的部署(SSH 代理,PM2,依赖安装)** 见同目录 **`部署文档.md`**.
**关键位自动开仓的规则,RR,结案原因** 见 **`关键位自动下单说明.md`**.
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**,主要能力包括:
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型,**5m 收线** 做硬条件过滤;符合条件后 **企业微信** 提醒,部分类型可 **自动市价开仓**(见第 4 节与专门文档). |
| **实盘下单监控** | 手工填止损/止盈,**以损定仓** 市价开单,挂上条件止盈止损,并在页面跟踪浮盈亏,保本逻辑等. |
| **交易记录 / 复盘** | 平仓结果,盈亏,错过的单等归档与导出;可选 **AI 复盘**(见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)). |
| **策略交易** | 顶栏 `/strategy`:趋势回调 + 顺势加仓双栏;见 [策略交易说明.md](../策略交易说明.md). |
后台按 **`MONITOR_POLL_SECONDS`**(默认几秒)轮询行情与监控逻辑.**切勿**在未理解规则时同时运行两套程序共用一个实盘账户.
---
## 2. 运行前必须配置(`.env`)
首次在本目录执行 **`cp .env.example .env`**,再编辑 `.env`(`.env` 勿提交 Git;`git pull` 不会改你的 `.env`,升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`).
至少检查以下项(具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`:打开站点后的登录口令.`FLASK_SECRET_KEY`:Session 密钥,请勿使用默认值. |
| **企业微信** | `WECHAT_WEBHOOK`:告警与关键位推送机器人的 Webhook. |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`:**不会**向交易所发送开仓指令(适合测试流程).改为 `true` 且密钥正确才会实盘. |
| **交易所 API** | **本仓库:** `GATE_API_KEY`,`GATE_API_SECRET`;合约相关见 `GATE_MARGIN_MODE`,`GATE_POS_MODE`,`GATE_TPSL_*`.**勿**把 `.env` 提交到 Git. |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR`,`KEY_STOP_OUTSIDE_BREAKOUT_PCT`(详见 `关键位自动下单说明.md`). |
| **AI 复盘** | `AI_PROVIDER=openai`(默认)`ollama`;变量见 `.env.example` 与 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md). |
网络不稳定时可为 Gate 配置 **`GATE_SOCKS_PROXY`** 等(**`部署文档.md`**).
---
## 3. 如何启动与登录
1. 按 **`部署文档.md`** 建好虚拟环境,安装依赖(`flask`,`requests`,`ccxt`,按需 `Pillow`,`PySocks`),配置好 `.env`.
2. 启动 Flask 应用(本仓库可用 **`ecosystem.config.cjs`** 交给 PM2,或本地 `python app.py` / `flask run`,以你当前脚本为准).
3. 浏览器访问站点,打开 **`/login`**,使用 **`.env` 里的 `APP_PASSWORD`** 登录.
登录后顶栏:**关键位监控** | **实盘下单** | **策略交易**(`/strategy`)| **策略交易记录**(`/strategy/records`)| **交易记录与复盘** | **统计分析**.
---
## 4. 关键位监控(顶栏「关键位监控」→ `/key_monitor`)
### 4.1 添加一条关键位
1. **币种**:`BTC``BTC/USDT`(会规范成内部符号).
2. **类型**(必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**(`LIVE_TRADING_ENABLED=true` 且无其他持仓占位).结案后本条从列表消失并记入历史. |
| **收敛突破** | 同上(自动开仓类). |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**. |
| **关键支撑位** | 同上(仅提醒). |
| **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选).
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**.
**限制:**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**;仍可添加「**关键阻力位 / 支撑位**」.
**4h EMA55** 与你的方向逆势,页面会 **额外 Flash 提示**,**不阻挡**提交.
### 4.2 触发后会发生什么(简版)
- **箱体 / 收敛**:门控通过后计算计划 SL/TP 与 RR;不达标则 **微信说明 + `rr_insufficient` 结案**;达标则尝试 **市价开仓**,成功 **`auto_opened`**,失败 **`exchange_failed`**——均 **不重试同一关键位**.
- **阻力 / 支撑**:**单次推送****`key_level_alert_only`** 结案.
详细公式,结案字段,与企业微信文案口径见 **`关键位自动下单说明.md`**.
### 4.3 列表与历史
- 当前条目可 **删除**(会按规则记入历史的情形见页面说明).
- **关键位历史**:已结案记录;可配合导出链接(若有)做备份.
---
## 5. 实盘下单(顶栏「实盘下单」→ `/trade`)
用于 **自己点按钮** 开单:
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制(默认 1,与关键位自动单共用).
- **人工开仓**时计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**(默认 1.4:1),否则页面弹窗且后端拒绝.
- 填写币种,方向,杠杆(可选),止损/止盈(价格或百分比按表单说明).
- 勾选是否启用 **移动保本** 等行为以 `.env`/页面默认值为准.
平仓通过页面 **平仓**(或等价入口),会从交易所市价处理并更新记录.**删除/误操作可能造成真实盈亏**,请先确认环境与方向.
开仓成功后持仓卡片上会显示 **「来源」**:手工单一般为 **下单监控**;来自关键位自动单的为 **关键位监控**.
---
## 6. 企业微信会看到什么
- 关键位:按类型与结案结果推送(RR 不足,下单失败,自动开仓成功,仅阻力支撑提醒等),**每条关键位结案路径原则上一条主推送**(详见 `关键位自动下单说明.md`).
- 手工开仓,平仓,部分异常也会在规则满足时推送(以代码与配置为准).
若未配置 **`WECHAT_WEBHOOK`** 或网络失败,可能只是看不到推送,不代表逻辑未执行;要紧操作请以 **交易所端持仓与挂单** 为准核对.
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 验证页面,录入,推送,再开小资金开实盘.
2. **API 权限**:仅开所需合约权限;勿泄露密钥;定期轮换.
3. **单进程控盘**:同一账户避免本程序与其他机器人 **重复开仓**.
4. **自动备份**:服务器上执行 `bash scripts/install_backup_cron.sh`(每天北京时间 0:00 → `/root/backups`,保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次.
5. **升级代码后**:启动时会跑 **数据库迁移**(如新列 `order_monitors.monitor_type`);首次启动关注一下日志或无报错页面.
---
## 8. 常见问题(简要)
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 5m 门控是否全通过(页面门控摘要),币种日成交量是否在规则内,`KLINE_TIMEFRAME`. |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`,`KEY_AUTO_MIN_PLANNED_RR`,计划 RR,是否已有持仓,API/余额报错(微信或日志). |
| 加不了箱体/收敛 | 是否已有活跃持仓;先平仓或改用「阻力/支撑位」仅提醒. |
| 推送收不到 | `WECHAT_WEBHOOK`,企业微信机器人配额与网络. |
---
## 9. Binance 版(`crypto_monitor_binance`)差异速查
| 项目 | Gate 本仓库 | Binance 版 |
|------|-------------|------------|
| API 变量 | `GATE_API_KEY`,`GATE_API_SECRET`,`GATE_*` | `BINANCE_API_KEY`,`BINANCE_API_SECRET`,`BINANCE_*` |
| 实盘开关 | `LIVE_TRADING_ENABLED`(通用) | 同上 |
| 止盈止损挂载路径 | `_gate_place_tp_sl_orders``GATE_TPSL_*` | `_binance_place_tp_sl_orders`(U 本位条件单) |
| 资金显示舍入 | 以本仓库为准 | 与 **`FUNDS_DECIMALS`** 等一致 |
| 专门文档 | **`关键位自动下单说明.md`**(各仓库有一份,开头标明交易所) | 同左 |
操作流程(登录,关键位四类,手工单,单仓)**两份程序一致**:换目录,`.env` 即可对照使用.
+192 -192
View File
@@ -1,192 +1,192 @@
# 关键位监控说明自动开仓 + 人工盯盘
**适用Gate / Binance / OKX 三所实例共用 `lib/key_monitor/key_auto_order_lib.py`**
## 环境开关 `KEY_AUTO_ORDER_ENABLED`默认 `false`
| 计仓模式 | 开关 | 关键位程序自动单 |
|----------|------|------------------|
| `risk`以损定仓 | `false` | **全部关闭**含触价);支撑/阻力微信提醒仍可用 |
| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动旧行为 |
| `full_margin`全仓 | `false` | 全部关闭含触价 |
| `full_margin` | `true` | **仅触价**自动箱体/斐波等仍禁止 |
**不受本开关影响** 人工实盘下单关键支撑/阻力提醒**顺势加仓**`risk`)、趋势回调`risk`)。全仓模式下策略自动仍禁止
修改 `.env` 后须 **重启 PM2**复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项
---
**适用`crypto_monitor_gate`Gate U 本位永续**
Binance / OKX 见各自目录下同名文档共享逻辑在 `lib/key_monitor/`
本文档与 `.env``check_key_monitors``add_key``_key_hard_checks``_process_key_rs_level_alert` 一致
---
## 一监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**门控 + RR | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**同上 | 同上 |
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上与阻力位**相同规则**填上沿+下沿程序双向监控 |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明**不在下文展开** |
| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30**上沿 **>** 下沿触价开仓填 E/SL/TP上下沿仅作展示占位)。
---
## 二关键阻力位 / 关键支撑位人工盯盘
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**程序同时监控两侧**无法预先判定**做多还是做空)。
- 页面 **不显示不要求** 方向库中 `direction` 初始为 `watch`**首次突破后** 写入 `long`向上突破上沿`short`向下突破下沿)。
### 2.2 触发极简
- 周期**`KLINE_TIMEFRAME`默认 5m最近一根已闭合 K** 的 **收盘价**非影线)。
- **向上突破上沿** `收盘 > upper` → 推断方向 **多 / 向上**本次监控任务开始按节奏提醒
- **向下突破下沿** `收盘 < lower` → 推断方向 **空 / 向下**本次任务同样开始提醒
- **任一侧突破即结束本条监控周期**不会在突破后再等待另一侧上沿下沿谁先满足用谁同根 K 仅可能满足一侧)。
**不参与** 量能二确 K越过幅度下限日成交排名运行时)、计划 RR自动开仓
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次首次检测到突破的当次轮询若已闭合 5m 满足条件)。
- 第 23 次仅按间隔推送**不要求**价格仍在箱外)。
- 第 3 次推送后写入 `key_monitor_history``close_reason=**key_level_alert_done**``key_monitors` **删除**
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根突破 K + 确认 K |
| 提醒次数 | 3 次后结案 | 自动单触发后 1 次业务推送并结案 |
---
## 三箱体突破 / 收敛突破自动开仓
### 3.1 K 线结构默认索引
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控须全部通过
1. **有效突破收盘越界**
- 多`突破 K 收盘 > upper`
- 空`突破 K 收盘 < lower`
2. **突破越过幅度仅下限**
- 多`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`默认 **0.03%**
- 空`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**突破过猛由 **计划 RR** 过滤
- **不再**使用 K 线实体占开盘价比例`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**
3. **确认 K 不进箱体**
- 多确认 K 收盘 **`> upper`**不得在 `[lower, upper]`
- 空确认 K 收盘 **`< lower`**
4. **量能** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`默认 20根均量 × `KEY_VOLUME_RATIO_MIN`默认 1.3
5. **日成交量排名** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30
6. **计划 RR最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`默认 1.5才市价开仓
### 3.3 止损 / 止盈确认 K 收盘为 E
箱体高 **H = |upper lower|**止损锚在 **突破 K 极值** 外侧
| 方向 | 止损标准/趋势方案 |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表与改版前一致):
| 方案 | `sl_tp_mode` | 多SL / TP | 空SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案`close_reason`
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿 |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四回调 / 突破触价开仓程序触价无交易所挂单
### 4.1 录入
- **回调触价开仓**方向必选多/空填写 **计划入场价 E****止损 SL****止盈 TP**做多须 `SL < E < TP`)。
- **突破触价开仓**同上添加时当前价须在突破方向一侧做多价低于 E做空价高于 E)。
- 计划 RR 以 **E** 为基准**严格大于** `KEY_AUTO_MIN_PLANNED_RR`默认 1.5)。
- 可选移动保本时间平仓**全仓杠杆模式**下可用
### 4.2 触发与结案
| 类型 | 触发条件标记价 |
|------|-------------------|
| **回调触价** | 做多 `≤ E`做空 `≥ E` → 下一轮询市价开仓 |
| **突破触价** | 做多**向上穿越** E做空**向下穿越** E → **立即**市价开仓 |
- 未成交前标记价先触 **TP 侧**`trigger_tp_invalidate`
- **突破触价**另未穿越 E 先触 **SL 侧**`trigger_sl_invalidate`
- **24h** 未触发 → `trigger_entry_expired`
- 成功 → `trigger_entry_filled`触发后开仓失败 → `trigger_exchange_failed`
### 4.3 计仓与占位
- **以损定仓**按 ESL 反推保证金触发时重算**全仓杠杆**可用×缓冲比例BTC/ETH 10x其它 5x
- **占当日开仓意图**已开 + 待触发),未成交不占持仓同币仅 1 条触价监控含回调/突破)。
共享逻辑`trigger_entry_key_monitor_lib.py`轮询`check_trigger_entry_key_monitors`
---
## 五环境与参数`.env` 摘要
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限默认 0.03 | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用默认 3 次 / 5 分钟 |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 六相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
# 关键位监控说明(自动开仓 + 人工盯盘)
**适用:Gate / Binance / OKX 三所实例(共用 `lib/key_monitor/key_auto_order_lib.py`)**
## 环境开关 `KEY_AUTO_ORDER_ENABLED`(默认 `false`)
| 计仓模式 | 开关 | 关键位程序自动单 |
|----------|------|------------------|
| `risk`(以损定仓) | `false` | **全部关闭**(含触价);支撑/阻力微信提醒仍可用 |
| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动(旧行为) |
| `full_margin`(全仓) | `false` | 全部关闭(含触价) |
| `full_margin` | `true` | **仅触价**自动;箱体/斐波等仍禁止 |
**不受本开关影响:** 人工实盘下单,关键支撑/阻力提醒,**顺势加仓**(`risk`),趋势回调(`risk`).全仓模式下策略自动仍禁止.
修改 `.env` 后须 **重启 PM2**.复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项.
---
**适用:`crypto_monitor_gate`(Gate U 本位永续)**
Binance / OKX 见各自目录下同名文档;共享逻辑在 `lib/key_monitor/`.
本文档与 `.env`,`check_key_monitors`,`add_key`,`_key_hard_checks`,`_process_key_rs_level_alert` 一致.
---
## 一,监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**(门控 + RR) | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**(同上) | 同上 |
| **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位).
---
## 二,关键阻力位 / 关键支撑位(人工盯盘)
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**(程序同时监控两侧,**无法预先判定**做多还是做空).
- 页面 **不显示,不要求** 方向;库中 `direction` 初始为 `watch`,**首次突破后** 写入 `long`(向上突破上沿)`short`(向下突破下沿).
### 2.2 触发(极简)
- 周期:**`KLINE_TIMEFRAME`(默认 5m)最近一根已闭合 K** 的 **收盘价**(非影线).
- **向上突破上沿:** `收盘 > upper` → 推断方向 **多 / 向上**,本次监控任务开始按节奏提醒.
- **向下突破下沿:** `收盘 < lower` → 推断方向 **空 / 向下**,本次任务同样开始提醒.
- **任一侧突破即结束本条监控周期**(不会在突破后再等待另一侧;上沿,下沿谁先满足用谁,同根 K 仅可能满足一侧).
**不参与:** 量能,二确 K,越过幅度下限,日成交排名(运行时),计划 RR,自动开仓.
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次:首次检测到突破的当次轮询(若已闭合 5m 满足条件).
- 第 2,3 次:仅按间隔推送(**不要求**价格仍在箱外).
- 第 3 次推送后:写入 `key_monitor_history`,`close_reason=**key_level_alert_done**`,`key_monitors` **删除**.
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根(突破 K + 确认 K) |
| 提醒次数 | 3 次后结案 | 自动单:触发后 1 次业务推送并结案 |
---
## 三,箱体突破 / 收敛突破(自动开仓)
### 3.1 K 线结构(默认索引)
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控(须全部通过)
1. **有效突破(收盘越界)**
- 多:`突破 K 收盘 > upper`
- 空:`突破 K 收盘 < lower`
2. **突破越过幅度(仅下限)**
- 多:`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`(默认 **0.03%**)
- 空:`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**;突破过猛由 **计划 RR** 过滤.
- **不再**使用 K 线实体占开盘价比例;`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**.
3. **确认 K 不进箱体**
- 多:确认 K 收盘 **`> upper`**(不得在 `[lower, upper]`)
- 空:确认 K 收盘 **`< lower`**
4. **量能:** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`(默认 20)根均量 × `KEY_VOLUME_RATIO_MIN`(默认 1.3)
5. **日成交量排名:** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)
6. **计划 RR(最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后,`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)才市价开仓
### 3.3 止损 / 止盈(确认 K 收盘为 E)
箱体高 **H = |upper lower|**.止损锚在 **突破 K 极值** 外侧:
| 方向 | 止损(标准/趋势方案) |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表(与改版前一致):
| 方案 | `sl_tp_mode` | 多:SL / TP | 空:SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案(`close_reason`)
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿) |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四,回调 / 突破触价开仓(程序触价,无交易所挂单)
### 4.1 录入
- **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**,**止损 SL**,**止盈 TP**(做多须 `SL < E < TP`).
- **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E).
- 计划 RR 以 **E** 为基准,**严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5).
- 可选移动保本,时间平仓;**全仓杠杆模式**下可用.
### 4.2 触发与结案
| 类型 | 触发条件(标记价) |
|------|-------------------|
| **回调触价** | 做多 `≤ E`;做空 `≥ E` → 下一轮询市价开仓 |
| **突破触价** | 做多**向上穿越** E;做空**向下穿越** E → **立即**市价开仓 |
- 未成交前标记价先触 **TP 侧**`trigger_tp_invalidate`.
- **突破触价**另:未穿越 E 先触 **SL 侧**`trigger_sl_invalidate`.
- **24h** 未触发 → `trigger_entry_expired`.
- 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`.
### 4.3 计仓与占位
- **以损定仓**:按 E,SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x,其它 5x.
- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破).
共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`.
---
## 五,环境与参数(`.env` 摘要)
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限(默认 0.03) | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用(默认 3 次 / 5 分钟) |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 六,相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
+148 -148
View File
@@ -1,148 +1,148 @@
# 界面与风控更新说明Gate 实例
## 顶栏导航4 项
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加实时门控历史 |
| 2 | 实盘下单 | `/trade` | 人工开仓划转实时持仓**默认首页** `/``/trade` |
| 3 | 交易记录与复盘 | `/records` | 交易记录复盘表单AI 历史受顶栏 UTC 时间窗筛选 |
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 关键位监控页
- 标题去掉「5m」规则条从 `.env` 读取周期确认K量能自动开仓盈亏比日成交量排名)。
- 左列活跃关键位**pos-card** 样式展示现价/距上沿/距下沿/门控
- 右列关键位历史失效/结案),与左列等高滚动**受顶栏 UTC 列表时间窗筛选**默认 UTC 当日)。
- 监控类型新增**斐波回调0.618****斐波回调0.786**与 Binance 主站同一套规则计算逻辑见仓库根目录 `fib_key_monitor_lib.py`)。
### 斐波关键位监控方案 A交易所限价
| 项 | 说明 |
|----|------|
| 同币互斥 | 每个币种只能有一条斐波监控0.618 与 0.786 不可并存 |
| 上下沿 | 上沿 **H**下沿 **L**须 H > L |
| 挂单价 E | **做多** `E = H ratio × (H L)`自 H 向下回撤);**做空** `E = L + ratio × (H L)`自 L 向上反弹 |
| 做多 | 限价 @ E止损 L止盈 H |
| 做空 | 限价 @ E止损 H止盈 L |
| 添加后 | **立即**在 Gate 挂限价单卡片显示 **挂E**限价单 ID |
| 失效 | 以**标记价**判断做多且标记价 ≥ H做空且标记价 ≤ L且限价**未成交** → 撤销该限价单并结案不写历史开仓 |
| 成交后 | 按仓位挂交易所 TP/SL → 写入 **实盘下单监控**`monitor_type=关键位监控``key_signal_type=斐波回调0.618/0.786`→ 从关键位列表移除 |
| 撤单 | 仅撤本条斐波的 `fib_limit_order_id`**不会** `cancel_all`避免误伤其他委托 |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`与箱体/收敛一致);0.618 理论约 1.6:10.786 约 3.7:1 |
| 日成交量 | 与箱体/收敛相同须在前 `KEY_DAILY_VOLUME_RANK_MAX` 名内方可添加 |
后台轮询`check_fib_key_monitors()`标记价失效 / 成交检测);箱体/收敛仍走 `check_key_monitors()`互不干扰
手动删除关键位时若斐波限价尚未成交会先撤交易所限价再删库记录
### 箱体 / 收敛自动开仓来源标注
- 自动开仓写入 `order_monitors.key_signal_type``箱体突破``收敛突破`
- 持仓卡片交易记录列表会显示「来源 · 信号类型」
## 列表时间窗UTC全站顶栏
共用模块仓库根目录 `history_window_lib.py`Gate / Binance 主站一致)。
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**`win_preset=utc_today`从 UTC 0:00 至当前时刻 |
| 可选 | 近 24 小时近 7 天自定义起止UTC`datetime-local` |
| 作用范围 | 关键位历史交易记录列表复盘记录 APIAI 历史 API导出「交易记录」「关键位历史」 |
| 与统计的关系 | **仅影响列表/导出****统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`默认 8:00切交易日** |
| 库内时间 | DB 存北京时间字符串后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
| 切换方式 | 顶栏「列表筛选UTC」→ 选预设 → **应用**保留当前路由`/records?win_preset=…` |
查询参数示例
- `?win_preset=utc_today`
- `?win_preset=utc_last24h` / `utc_last7d`
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
## 交易记录与复盘
- 平仓记录可同步交易所已实现盈亏Gate 仓位历史等);列表盈亏列优先显示交易所数据标注 **所** / **估**
- 记录页提供 **立即同步**`POST /api/sync_exchange_pnl`),用于补全或刷新 `exchange_realized_pnl` 等字段
- 未做人工复盘时展示以交易所盈亏为准有同步数据时)。
- **列表默认只显示当前 UTC 时间窗内**的记录见上节);导出 CSV 同步该时间窗
- 表头 **「止损(开仓)」**展示开仓快照 `initial_stop_loss`无则回退 `stop_loss`);核对/复盘仍可用有效止损字段
- 平仓写入 `trade_records``stop_loss``initial_stop_loss` 均写入**开仓时止损快照**`key_signal_type` 保留箱体/收敛/斐波来源`fib_key_monitor_lib.key_signal_type_for_trade_record`)。
- **开仓类型**`entry_reason`):机器单平仓入库时若未手填`key_signal_type` 自动映射见下表);列表/导出「开仓类型」列 = 复盘核对值优先否则入库值否则按信号映射
| `key_signal_type` | 自动写入的 `entry_reason` |
|-------------------|---------------------------|
| 箱体突破 | 关键位箱体突破 |
| 收敛突破 | 关键位收敛突破 |
| 斐波回调0.618 | 关键位斐波0.618 |
| 斐波回调0.786 | 关键位斐波0.786 |
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案与趋势/波段类并列)。
- 复盘 **离场触发** 新增 **「止盈」**从交易记录「填入复盘」时若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项并按 `key_signal_type` 预填开仓类型
- 勾选「保存时自动生成多周期 K 线图」时**平仓时间** 为锚点各周期向前约 `ORDER_CHART_LIMIT`默认 100根 K 线`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」
- `/api/journals``/api/reviews` 支持同一时间窗 query与列表一致
### 导出交易记录 v3
- 文件名`trade_records_v3_YYYYMMDD.csv`
- 相对 v2 增加`key_signal_type``initial_stop_loss`及开仓快照列)、`planned_rr``actual_rr``risk_amount`交易所盈亏与时间字段等末列「开仓类型」为有效展示文案
- 「关键位历史」导出同样受 UTC 时间窗限制
## 实盘下单页
- 左列实盘下单监控表单划转规则)。
- 右列实时持仓独立模块)。
- **人工开仓门控**计划盈亏比 &lt; `MANUAL_MIN_PLANNED_RR`默认 **1.4**时前端弹窗 + 后端拒绝
- **移动保本**勾选启用):监控轮询达到触发 RR 后止损阶梯上移时**同步交易所**——调用与页面「挂止盈止损」相同的 **先撤后挂**`replace_active_monitor_tpsl_on_exchange`撤该合约全部 TP/SL 条件单 → 按新止损 + 原止盈重挂)。仅交易所成功后才写库失败发企业微信告警本地止损不变未配置实盘 API 时仍只更新本地与旧行为一致)。
## 统计分析页`/stats`
| 项 | 说明 |
|----|------|
| 切日 | **北京时间**交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00``.env` 默认 **8** |
| 品类下拉 | 页顶 **「统计品类」** 下拉切换默认「全部交易」):全部交易下单监控关键位箱体突破关键位收敛结构关键位斐波0.618关键位斐波0.786一次只显示所选品类的日/周/月 |
| URL | 切换后写入 `stats_segment=``all``manual``key_box``key_conv``key_fib618``key_fib786`),刷新 `/stats` 可保持选项 |
| 每块指标 | 日 / 周 / 月开单次数平仓笔数胜率净盈亏回撤连续亏损等与原口径一致 |
| 开单次数 | 人工块`monitor_type=下单监控` 且无 `key_signal_type`关键位块`order_monitors.key_signal_type` 计数 |
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录按北京交易日归类**不**随顶栏 UTC 列表窗切换 |
## 持仓与计仓
- `MAX_ACTIVE_POSITIONS` 默认 **1**可在 `.env` 调大)。
- 关键位自动开仓在已有持仓时`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true`按**首笔开仓前**交易账户资金快照计仓`trading_sessions.key_sizing_capital_snapshot`)。
## 配置
详见 `.env.example` 中「关键位门控」「交易执行 / 人工风控」注释段Gate 专用项`GATE_*`止盈止损触发等保持原有段落不变
## 自动备份服务器
- 脚本`scripts/backup_data.sh``crypto.db` + `static/images`
- 定时`scripts/install_backup_cron.sh` → 每天 **北京时间 0:00**目录 **`/root/backups/<实例名>/YYYY-MM-DD/`**保留 **30**
- 详见 `部署文档.md` 第 5.4 节自动备份
## 数据库启动时自动迁移
`key_monitors` 新增斐波字段示例):`fib_limit_order_id``fib_entry_price``fib_stop_loss``fib_take_profit``fib_order_amount``fib_margin_capital``fib_leverage`
`trade_records` / `order_monitors` 新增或沿用`key_signal_type``exchange_realized_pnl``exchange_opened_at``exchange_closed_at``exchange_sync_key``entry_reason``reviewed_entry_reason``initial_stop_loss`
**历史数据**本次**不做**旧记录的批量回填`entry_reason` / `initial_stop_loss` / `key_signal_type`);仅**新产生**的平仓与复盘按新逻辑写入旧行展示可回退已有字段
## 涉及文件便于排查
| 路径 | 说明 |
|------|------|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
| `fib_key_monitor_lib.py` | 斐波计算`KEY_ENTRY_REASON_BY_SIGNAL``entry_reason_from_key_signal` |
| `crypto_monitor_gate/app.py` | 列表筛选统计分块导出 v3复盘 K 线锚点入库逻辑 |
| `crypto_monitor_gate/templates/index.html` | 顶栏时间窗统计分块 UI止损(开仓)列复盘预填 |
## 升级步骤
1. `git pull` 后对比 `.env.example`把新增变量合并进本地 `.env`
2. 在 VPS 上为 Binance / Gate / **各执行一次** `bash scripts/install_backup_cron.sh`若尚未安装)。
3. 重启 Gate 实例服务`pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 缺列斐波交易所盈亏`entry_reason`)。
4. 浏览器强刷Ctrl+F5避免旧版 `index.html` 缓存
5. 打开任意页确认顶栏出现 **「列表筛选UTC」**`/stats` 可见分品类统计与「北京 8:00 切日」说明
6. 建议在测试币上先添加一条斐波监控确认限价已挂出标记价失效会撤单成交后出现持仓监控且 TP/SL 已挂上平仓后交易记录止损(开仓)与开仓类型是否正确
# 界面与风控更新说明(Gate 实例)
## 顶栏导航(4 项)
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 关键位监控 | `/key_monitor` | 关键位添加,实时门控,历史 |
| 2 | 实盘下单 | `/trade` | 人工开仓,划转,实时持仓(**默认首页** `/``/trade`) |
| 3 | 交易记录与复盘 | `/records` | 交易记录,复盘表单,AI 历史(受顶栏 UTC 时间窗筛选) |
| 4 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 关键位监控页
- 标题去掉「5m」;规则条从 `.env` 读取(周期,确认K,量能,自动开仓盈亏比,日成交量排名).
- 左列:活跃关键位,**pos-card** 样式展示现价/距上沿/距下沿/门控.
- 右列:关键位历史(失效/结案),与左列等高滚动;**受顶栏 UTC 列表时间窗筛选**(默认 UTC 当日).
- 监控类型新增:**斐波回调0.618**,**斐波回调0.786**(与 Binance 主站同一套规则,计算逻辑见仓库根目录 `fib_key_monitor_lib.py`).
### 斐波关键位监控(方案 A:交易所限价)
| 项 | 说明 |
|----|------|
| 同币互斥 | 每个币种只能有一条斐波监控(0.618 与 0.786 不可并存) |
| 上下沿 | 上沿 **H**,下沿 **L**(须 H > L) |
| 挂单价 E | **做多** `E = H ratio × (H L)`(自 H 向下回撤);**做空** `E = L + ratio × (H L)`(自 L 向上反弹) |
| 做多 | 限价 @ E,止损 L,止盈 H |
| 做空 | 限价 @ E,止损 H,止盈 L |
| 添加后 | **立即**在 Gate 挂限价单;卡片显示 **挂E**,限价单 ID |
| 失效 | 以**标记价**判断:做多且标记价 ≥ H,做空且标记价 ≤ L,且限价**未成交** → 撤销该限价单并结案(不写历史开仓) |
| 成交后 | 按仓位挂交易所 TP/SL → 写入 **实盘下单监控**(`monitor_type=关键位监控`,`key_signal_type=斐波回调0.618/0.786`)→ 从关键位列表移除 |
| 撤单 | 仅撤本条斐波的 `fib_limit_order_id`,**不会** `cancel_all`,避免误伤其他委托 |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`(与箱体/收敛一致);0.618 理论约 1.6:1,0.786 约 3.7:1 |
| 日成交量 | 与箱体/收敛相同,须在前 `KEY_DAILY_VOLUME_RANK_MAX` 名内方可添加 |
后台轮询:`check_fib_key_monitors()`(标记价失效 / 成交检测);箱体/收敛仍走 `check_key_monitors()`,互不干扰.
手动删除关键位时,若斐波限价尚未成交,会先撤交易所限价再删库记录.
### 箱体 / 收敛自动开仓(来源标注)
- 自动开仓写入 `order_monitors.key_signal_type`:`箱体突破``收敛突破`.
- 持仓卡片,交易记录列表会显示「来源 · 信号类型」.
## 列表时间窗(UTC,全站顶栏)
共用模块:仓库根目录 `history_window_lib.py`(Gate / Binance 主站一致).
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**(`win_preset=utc_today`,从 UTC 0:00 至当前时刻) |
| 可选 | 近 24 小时,近 7 天,自定义起止(UTC,`datetime-local`) |
| 作用范围 | 关键位历史,交易记录列表,复盘记录 API,AI 历史 API,导出「交易记录」「关键位历史」 |
| 与统计的关系 | **仅影响列表/导出**;**统计分析页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切交易日** |
| 库内时间 | DB 存北京时间字符串;后端用 `utc_window_to_bj_sql_strings()` 换算后再 SQL 比较 |
| 切换方式 | 顶栏「列表筛选(UTC)」→ 选预设 → **应用**(保留当前路由,`/records?win_preset=…`) |
查询参数示例:
- `?win_preset=utc_today`
- `?win_preset=utc_last24h` / `utc_last7d`
- `?win_preset=custom&from_utc=2026-05-18 00:00:00&to_utc=2026-05-19 12:00:00`
## 交易记录与复盘
- 平仓记录可同步交易所已实现盈亏(Gate 仓位历史等);列表盈亏列优先显示交易所数据,标注 **所** / **估**.
- 记录页提供 **立即同步**(`POST /api/sync_exchange_pnl`),用于补全或刷新 `exchange_realized_pnl` 等字段.
- 未做人工复盘时,展示以交易所盈亏为准(有同步数据时).
- **列表默认只显示当前 UTC 时间窗内**的记录(见上节);导出 CSV 同步该时间窗.
- 表头 **「止损(开仓)」**:展示开仓快照 `initial_stop_loss`(无则回退 `stop_loss`);核对/复盘仍可用有效止损字段.
- 平仓写入 `trade_records`:`stop_loss``initial_stop_loss` 均写入**开仓时止损快照**;`key_signal_type` 保留箱体/收敛/斐波来源(`fib_key_monitor_lib.key_signal_type_for_trade_record`).
- **开仓类型**(`entry_reason`):机器单平仓入库时,若未手填,`key_signal_type` 自动映射(见下表);列表/导出「开仓类型」列 = 复盘核对值优先,否则入库值,否则按信号映射.
| `key_signal_type` | 自动写入的 `entry_reason` |
|-------------------|---------------------------|
| 箱体突破 | 关键位箱体突破 |
| 收敛突破 | 关键位收敛突破 |
| 斐波回调0.618 | 关键位斐波0.618 |
| 斐波回调0.786 | 关键位斐波0.786 |
- 复盘表单 **开仓类型** 下拉新增上述四条固定文案(与趋势/波段类并列).
- 复盘 **离场触发** 新增 **「止盈」**;从交易记录「填入复盘」时,若结果为「止盈/保本止盈/移动止盈/止损/手动平仓」会自动选中对应触发项,并按 `key_signal_type` 预填开仓类型.
- 勾选「保存时自动生成多周期 K 线图」时:**平仓时间** 为锚点,各周期向前约 `ORDER_CHART_LIMIT`(默认 100)根 K 线(`_fetch_ohlcv_ending_at`),不再固定拉「最近 100 根」.
- `/api/journals`,`/api/reviews` 支持同一时间窗 query,与列表一致.
### 导出(交易记录 v3)
- 文件名:`trade_records_v3_YYYYMMDD.csv`
- 相对 v2 增加:`key_signal_type`,`initial_stop_loss`(及开仓快照列),`planned_rr`,`actual_rr`,`risk_amount`,交易所盈亏与时间字段等;末列「开仓类型」为有效展示文案.
- 「关键位历史」导出同样受 UTC 时间窗限制.
## 实盘下单页
- 左列:实盘下单监控(表单,划转,规则).
- 右列:实时持仓(独立模块).
- **人工开仓门控**:计划盈亏比 &lt; `MANUAL_MIN_PLANNED_RR`(默认 **1.4**)时前端弹窗 + 后端拒绝.
- **移动保本**(勾选启用):监控轮询达到触发 RR 后,止损阶梯上移时**同步交易所**——调用与页面「挂止盈止损」相同的 **先撤后挂**(`replace_active_monitor_tpsl_on_exchange`:撤该合约全部 TP/SL 条件单 → 按新止损 + 原止盈重挂).仅交易所成功后才写库;失败发企业微信告警,本地止损不变.未配置实盘 API 时仍只更新本地(与旧行为一致).
## 统计分析页(`/stats`)
| 项 | 说明 |
|----|------|
| 切日 | **北京时间**;交易日边界 = 每日 `TRADING_DAY_RESET_HOUR:00`(`.env` 默认 **8**) |
| 品类下拉 | 页顶 **「统计品类」** 下拉切换(默认「全部交易」):全部交易,下单监控,关键位箱体突破,关键位收敛结构,关键位斐波0.618,关键位斐波0.786;一次只显示所选品类的日/周/月 |
| URL | 切换后写入 `stats_segment=`(`all`,`manual`,`key_box`,`key_conv`,`key_fib618`,`key_fib786`),刷新 `/stats` 可保持选项 |
| 每块指标 | 日 / 周 / 月:开单次数,平仓笔数,胜率,净盈亏,回撤,连续亏损等(与原口径一致) |
| 开单次数 | 人工块:`monitor_type=下单监控` 且无 `key_signal_type`;关键位块:`order_monitors.key_signal_type` 计数 |
| 不受 UTC 窗影响 | 统计始终基于库内全部已平仓记录,按北京交易日归类,**不**随顶栏 UTC 列表窗切换 |
## 持仓与计仓
- `MAX_ACTIVE_POSITIONS` 默认 **1**(可在 `.env` 调大).
- 关键位自动开仓:在已有持仓时,`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true`,按**首笔开仓前**交易账户资金快照计仓(`trading_sessions.key_sizing_capital_snapshot`).
## 配置
详见 `.env.example` 中「关键位门控」「交易执行 / 人工风控」注释段.Gate 专用项(`GATE_*`,止盈止损触发等)保持原有段落不变.
## 自动备份(服务器)
- 脚本:`scripts/backup_data.sh`(`crypto.db` + `static/images`)
- 定时:`scripts/install_backup_cron.sh` → 每天 **北京时间 0:00**,目录 **`/root/backups/<实例名>/YYYY-MM-DD/`**,保留 **30**
- 详见 `部署文档.md` 第 5.4 节(自动备份)
## 数据库(启动时自动迁移)
`key_monitors` 新增斐波字段(示例):`fib_limit_order_id`,`fib_entry_price`,`fib_stop_loss`,`fib_take_profit`,`fib_order_amount`,`fib_margin_capital`,`fib_leverage`.
`trade_records` / `order_monitors` 新增或沿用:`key_signal_type`,`exchange_realized_pnl`,`exchange_opened_at`,`exchange_closed_at`,`exchange_sync_key`,`entry_reason`,`reviewed_entry_reason`,`initial_stop_loss`.
**历史数据**:本次**不做**旧记录的批量回填(`entry_reason` / `initial_stop_loss` / `key_signal_type`);仅**新产生**的平仓与复盘按新逻辑写入.旧行展示可回退已有字段.
## 涉及文件(便于排查)
| 路径 | 说明 |
|------|------|
| `history_window_lib.py` | UTC 时间窗解析与转北京时间 SQL 字符串 |
| `fib_key_monitor_lib.py` | 斐波计算,`KEY_ENTRY_REASON_BY_SIGNAL`,`entry_reason_from_key_signal` |
| `crypto_monitor_gate/app.py` | 列表筛选,统计分块,导出 v3,复盘 K 线锚点,入库逻辑 |
| `crypto_monitor_gate/templates/index.html` | 顶栏时间窗,统计分块 UI,止损(开仓)列,复盘预填 |
## 升级步骤
1. `git pull` 后对比 `.env.example`,把新增变量合并进本地 `.env`.
2. 在 VPS 上为 Binance / Gate / **各执行一次** `bash scripts/install_backup_cron.sh`(若尚未安装).
3. 重启 Gate 实例服务(`pm2 restart crypto_gate`);首次启动会自动 `ALTER TABLE` 缺列(斐波,交易所盈亏,`entry_reason`).
4. 浏览器强刷(Ctrl+F5)避免旧版 `index.html` 缓存.
5. 打开任意页确认顶栏出现 **「列表筛选(UTC)」**;`/stats` 可见分品类统计与「北京 8:00 切日」说明.
6. 建议在测试币上先添加一条斐波监控,确认:限价已挂出,标记价失效会撤单,成交后出现持仓监控且 TP/SL 已挂上;平仓后交易记录止损(开仓)与开仓类型是否正确.
+305 -305
View File
@@ -1,305 +1,305 @@
# `crypto_monitor_gate` 部署指南SSH SOCKS + Gate.io + PM2Ubuntu
Ubuntu 环境Python / Node / PM2/opt 路径**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**
本文面向**在本机运行本项目****直连 Gate.io API 不稳定或被重置** 的场景思路是
- 本机用 `ssh -D` 做动态转发把 **SOCKS5 出口**放到能正常访问 Gate 的机器常见为一台境外 VPS
- 项目在 `.env` 中设置 **`GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080`**或你实际端口),`ccxt` 经 SOCKS 访问交易所
- **SSH 隧道**`ssh -D` 在本机常驻可用 **tmux****autossh** 保持连接),**不要** 把 `ssh` 交给 PM2
- 使用 **PM2** 仅托管 **Flask 应用**仓库根目录 **`ecosystem.config.cjs`** 只定义 `crypto-monitor-gate`
> 安全提醒不要把 `.env`私钥 `.pem`Gate API Key 提交到 Git下文只用占位符
---
## 0. 你需要准备的东西
- 一台 **Ubuntu**或同类 Linux运行项目的机器下文称「本机」
- 一台可 SSH 登录**能正常访问 Gate.io API** 的 VPS示例`HostName` 填你的服务器 IP用户如 `root`
- SSH**私钥登录**推荐便于隧道脚本无人值守
- 本机已安装`python3``python3-venv``pip``curl``ssh``git`可选)、`node` + `npm`安装 PM2
---
## 1. 获取代码与目录
将包含 `app.py` 的项目放到固定目录例如
```bash
mkdir -p /opt/crypto_monitor
cd /opt/crypto_monitor
git clone https://git.bz121.com/dekun/crypto_monitor.git
cd crypto_monitor/crypto_monitor_gate
```
下文用 **`/opt/crypto_monitor/crypto_monitor_gate`** 仅为示例请换成你的实际绝对路径
拉取代码后若目录下尚无 `.env`
```bash
cp -n .env.example .env
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 私钥示例~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`示例别名 **`gate-vps`**与你手工启动 `ssh -D ... gate-vps` 一致即可):
```sshconfig
Host gate-vps
HostName 你的_VPS_IP
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试
```bash
ssh gate-vps true
```
> 若尚未完全改为密钥登录可暂时注释 `BatchMode yes`调试完成后再打开
---
## 3. 手工验证SSH SOCKS + Gate API
### 3.1 本地 SOCKS示例端口 1080
```bash
ssh -N -D 127.0.0.1:1080 gate-vps
```
保持运行另开终端继续
### 3.2 验证经 SOCKS 可访问 Gate
```bash
curl -4 -sS --max-time 15 --proxy socks5h://127.0.0.1:1080 https://api.gateio.ws/api/v4/spot/time
```
应返回 JSON含服务器时间字段)。若此处失败**不要先启动应用**先修隧道或 VPS 出站
---
## 4. Python 虚拟环境
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
走 SOCKS 时 **必须** 安装 **`PySocks`**否则易出现代理相关报错
可选
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量`.env.example``.env`
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
cp -n .env.example .env
nano .env
```
### 5.2 备份与 `git pull`
- **`.env` 不在 Git 中**`git pull` **不会**覆盖本地 `.env`
- 远端若更新 **`.env.example`**pull 后请**手动**把新增变量补进你的 `.env`
- **升级前备份**`cp .env .env.backup.$(date +%Y%m%d)`恢复`cp .env.backup.YYYYMMDD .env`
- **换机**`scp` 复制 `.env`或新机 `cp .env.example .env` 后重填
### 5.3 AI 复盘与模型可选
共用根目录 **`ai_client.py`**`PYTHONPATH=..`)。`.env`**`AI_PROVIDER=openai`**默认时使用 `OPENAI_API_BASE=https://op.bz121.com/v1``OPENAI_API_KEY``OPENAI_MODEL=gemma4:e4b`**`ollama`** 则用 `OLLAMA_API` + `AI_MODEL`详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**
### 5.4 自动备份数据库 + 复盘图片
与 Binance 实例相同每天 **北京时间 0:00****`/root/backups`**保留 **30 天**
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
chmod +x scripts/backup_data.sh scripts/install_backup_cron.sh
bash scripts/install_backup_cron.sh
bash scripts/backup_data.sh # 试跑
```
备份目录`/root/backups/crypto_monitor_gate/YYYY-MM-DD/`详见 Binance 项目 `部署文档.md` 第 5.4 节恢复步骤可选 `.env` 变量相同)。
若还部署了 **`crypto_monitor_okx`**请在该目录同样执行 `bash scripts/install_backup_cron.sh`
### 5.5 必填项检查Gate + 代理
与交易所相关的变量必须是 **Gate** 前缀**不要**再写 OKX 变量否则代理不会生效密钥也不会被识别)。至少确认
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# 实盘按需
LIVE_TRADING_ENABLED=false
GATE_API_KEY=你的_Key
GATE_API_SECRET=你的_Secret
# 经本机 SSH 动态转发访问 Gate端口与隧道一致
GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 若不用 SOCKS可改用 HTTP 代理一般二选一
# GATE_HTTP_PROXY=http://127.0.0.1:7890
# GATE_HTTPS_PROXY=http://127.0.0.1:7890
```
说明**推荐 `socks5h://`**由 SOCKS 端解析域名`curl --proxy socks5h://...` 行为一致
---
## 6. 手工启动 Flask验证
1. SOCKS 已监听 `127.0.0.1:1080`
2. 已 `source .venv/bin/activate`
3. `.env` 已含 `GATE_SOCKS_PROXY`
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
source .venv/bin/activate
python app.py
```
浏览器访问`http://127.0.0.1:5000`或你在 `.env` 中的端口)。
---
## 7. 安装 PM2
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 8. PM2使用仓库内 `ecosystem.config.cjs`推荐
在项目根目录
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
pm2 start ecosystem.config.cjs
pm2 status
pm2 logs --lines 200
```
默认只启动 **`crypto-monitor-gate`**`.venv/bin/python app.py`)。
### 本机已可直连 Gate不需要隧道时
`.env` 里应 **去掉或留空** `GATE_SOCKS_PROXY`除非仍要走别的代理),`pm2 start ecosystem.config.cjs`
### 开机自启
```bash
pm2 save
pm2 startup
# 按屏幕提示执行一条 sudo 命令
```
---
## 9. 等价手工命令不使用 ecosystem 文件时
### 9.1 SSH SOCKS自行后台常驻不推荐用 PM2
示例前台调试生产请用 **PM2**见本文与 [docs/ubuntu-server.md](../docs/ubuntu-server.md)):
```bash
ssh -N -D 127.0.0.1:1080 gate-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes
```
### 9.2 Flask
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
pm2 start /opt/crypto_monitor/crypto_monitor_gate/.venv/bin/python --name crypto-monitor-gate -- \
/opt/crypto_monitor/crypto_monitor_gate/app.py
```
---
## 10. 交易所「连接不上」排查清单
1. **`.env` 是否为 Gate 变量**必须是 `GATE_SOCKS_PROXY` / `GATE_API_KEY` / `GATE_API_SECRET`不是 OKX
2. **隧道是否在本机端口监听**若配置了 `GATE_SOCKS_PROXY`):
```bash
ss -lntp | grep 1080 || true
```
3. **curl 复测 Gate**与第 3.2 节相同);curl 不通则应用也不会通
4. **PySocks**`pip show PySocks`缺失则 `pip install PySocks`
5. **SSH 隧道连不上**检查私钥权限`~/.ssh/config`VPS 出站与端口是否与 `.env` 一致
6. **启动顺序**先保证 SOCKS 已监听`pm2 start` 应用或重启应用)。
---
## 11. 推荐启动顺序习惯
1. 若走代理先启动并确认 SSH SOCKS 已监听`curl --proxy socks5h://127.0.0.1:1080 https://api.gateio.ws/api/v4/spot/time` 成功
2. `pm2 start ecosystem.config.cjs`
3. 再确认页面与余额等接口正常
---
## 12. 免责声明
交易所有合规与地区政策要求请确保使用方式符合当地法律法规与交易所条款本文仅描述网络与工程部署路径
---
## 附录数据库标签修复脚本 `scripts/fix_breakeven_labels.py`
在 Ubuntu 上
1预览不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2确认后执行
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件`monitor_type='下单监控'``result='止损'``pnl_amount > 0` → 改为 `result='保本止盈'`
# `crypto_monitor_gate` 部署指南:SSH SOCKS + Gate.io + PM2(Ubuntu)
Ubuntu 环境(Python / Node / PM2,/opt 路径)**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.
本文面向:**在本机运行本项目**,**直连 Gate.io API 不稳定或被重置** 的场景.思路是:
- 本机用 `ssh -D` 做动态转发,把 **SOCKS5 出口**放到能正常访问 Gate 的机器(常见为一台境外 VPS)
- 项目在 `.env` 中设置 **`GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080`**(或你实际端口),`ccxt` 经 SOCKS 访问交易所
- **SSH 隧道**:`ssh -D` 在本机常驻(可用 **tmux****autossh** 保持连接),**不要** 把 `ssh` 交给 PM2
- 使用 **PM2** 仅托管 **Flask 应用**;仓库根目录 **`ecosystem.config.cjs`** 只定义 `crypto-monitor-gate`
> 安全提醒:不要把 `.env`,私钥 `.pem`,Gate API Key 提交到 Git;下文只用占位符.
---
## 0. 你需要准备的东西
- 一台 **Ubuntu**(或同类 Linux)运行项目的机器(下文称「本机」)
- 一台可 SSH 登录,**能正常访问 Gate.io API** 的 VPS(示例:`HostName` 填你的服务器 IP,用户如 `root`)
- SSH:**私钥登录**(推荐,便于隧道脚本无人值守)
- 本机已安装:`python3`,`python3-venv`,`pip`,`curl`,`ssh`,`git`(可选),`node` + `npm`(安装 PM2)
---
## 1. 获取代码与目录
将包含 `app.py` 的项目放到固定目录,例如:
```bash
mkdir -p /opt/crypto_monitor
cd /opt/crypto_monitor
git clone https://git.bz121.com/dekun/crypto_monitor.git
cd crypto_monitor/crypto_monitor_gate
```
下文用 **`/opt/crypto_monitor/crypto_monitor_gate`** 仅为示例,请换成你的实际绝对路径.
拉取代码后,若目录下尚无 `.env`:
```bash
cp -n .env.example .env
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 私钥示例:~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`(示例别名 **`gate-vps`**,与你手工启动 `ssh -D ... gate-vps` 一致即可):
```sshconfig
Host gate-vps
HostName 你的_VPS_IP
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试:
```bash
ssh gate-vps true
```
> 若尚未完全改为密钥登录,可暂时注释 `BatchMode yes`,调试完成后再打开.
---
## 3. 手工验证:SSH SOCKS + Gate API
### 3.1 本地 SOCKS(示例端口 1080)
```bash
ssh -N -D 127.0.0.1:1080 gate-vps
```
保持运行,另开终端继续.
### 3.2 验证经 SOCKS 可访问 Gate
```bash
curl -4 -sS --max-time 15 --proxy socks5h://127.0.0.1:1080 https://api.gateio.ws/api/v4/spot/time
```
应返回 JSON(含服务器时间字段).若此处失败,**不要先启动应用**:先修隧道或 VPS 出站.
---
## 4. Python 虚拟环境
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
走 SOCKS 时 **必须** 安装 **`PySocks`**,否则易出现代理相关报错.
可选:
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量(`.env.example``.env`)
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释,可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置;`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
cp -n .env.example .env
nano .env
```
### 5.2 备份与 `git pull`
- **`.env` 不在 Git 中**:`git pull` **不会**覆盖本地 `.env`.
- 远端若更新 **`.env.example`**,pull 后请**手动**把新增变量补进你的 `.env`.
- **升级前备份**:`cp .env .env.backup.$(date +%Y%m%d)`;恢复:`cp .env.backup.YYYYMMDD .env`.
- **换机**:`scp` 复制 `.env`,或新机 `cp .env.example .env` 后重填.
### 5.3 AI 复盘与模型(可选)
共用根目录 **`ai_client.py`**(`PYTHONPATH=..`).`.env`**`AI_PROVIDER=openai`**(默认)时使用 `OPENAI_API_BASE=https://op.bz121.com/v1`,`OPENAI_API_KEY`,`OPENAI_MODEL=gemma4:e4b`;**`ollama`** 则用 `OLLAMA_API` + `AI_MODEL`.详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**.
### 5.4 自动备份(数据库 + 复盘图片)
与 Binance 实例相同:每天 **北京时间 0:00****`/root/backups`**,保留 **30 天**.
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
chmod +x scripts/backup_data.sh scripts/install_backup_cron.sh
bash scripts/install_backup_cron.sh
bash scripts/backup_data.sh # 试跑
```
备份目录:`/root/backups/crypto_monitor_gate/YYYY-MM-DD/`.详见 Binance 项目 `部署文档.md` 第 5.4 节(恢复步骤,可选 `.env` 变量相同).
若还部署了 **`crypto_monitor_okx`**,请在该目录同样执行 `bash scripts/install_backup_cron.sh`.
### 5.5 必填项检查(Gate + 代理)
与交易所相关的变量必须是 **Gate** 前缀(**不要**再写 OKX 变量,否则代理不会生效,密钥也不会被识别).至少确认:
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# 实盘(按需)
LIVE_TRADING_ENABLED=false
GATE_API_KEY=你的_Key
GATE_API_SECRET=你的_Secret
# 经本机 SSH 动态转发访问 Gate(端口与隧道一致)
GATE_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 若不用 SOCKS,可改用 HTTP 代理(一般二选一)
# GATE_HTTP_PROXY=http://127.0.0.1:7890
# GATE_HTTPS_PROXY=http://127.0.0.1:7890
```
说明:**推荐 `socks5h://`**,由 SOCKS 端解析域名,`curl --proxy socks5h://...` 行为一致.
---
## 6. 手工启动 Flask(验证)
1. SOCKS 已监听 `127.0.0.1:1080`
2. 已 `source .venv/bin/activate`
3. `.env` 已含 `GATE_SOCKS_PROXY`
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
source .venv/bin/activate
python app.py
```
浏览器访问:`http://127.0.0.1:5000`(或你在 `.env` 中的端口).
---
## 7. 安装 PM2
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 8. PM2:使用仓库内 `ecosystem.config.cjs`(推荐)
在项目根目录:
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
pm2 start ecosystem.config.cjs
pm2 status
pm2 logs --lines 200
```
默认只启动 **`crypto-monitor-gate`**(`.venv/bin/python app.py`).
### 本机已可直连 Gate,不需要隧道时
`.env` 里应 **去掉或留空** `GATE_SOCKS_PROXY`(除非仍要走别的代理),`pm2 start ecosystem.config.cjs`.
### 开机自启
```bash
pm2 save
pm2 startup
# 按屏幕提示执行一条 sudo 命令
```
---
## 9. 等价手工命令(不使用 ecosystem 文件时)
### 9.1 SSH SOCKS(自行后台常驻,不推荐用 PM2)
示例(前台调试;生产请用 **PM2**,见本文与 [docs/ubuntu-server.md](../docs/ubuntu-server.md)):
```bash
ssh -N -D 127.0.0.1:1080 gate-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes
```
### 9.2 Flask
```bash
cd /opt/crypto_monitor/crypto_monitor_gate
pm2 start /opt/crypto_monitor/crypto_monitor_gate/.venv/bin/python --name crypto-monitor-gate -- \
/opt/crypto_monitor/crypto_monitor_gate/app.py
```
---
## 10. 交易所「连接不上」排查清单
1. **`.env` 是否为 Gate 变量**:必须是 `GATE_SOCKS_PROXY` / `GATE_API_KEY` / `GATE_API_SECRET`,不是 OKX.
2. **隧道是否在本机端口监听**(若配置了 `GATE_SOCKS_PROXY`):
```bash
ss -lntp | grep 1080 || true
```
3. **curl 复测 Gate**(与第 3.2 节相同);curl 不通则应用也不会通.
4. **PySocks**:`pip show PySocks`,缺失则 `pip install PySocks`.
5. **SSH 隧道连不上**:检查私钥权限,`~/.ssh/config`,VPS 出站与端口是否与 `.env` 一致.
6. **启动顺序**:先保证 SOCKS 已监听,`pm2 start` 应用(或重启应用).
---
## 11. 推荐启动顺序(习惯)
1. 若走代理:先启动并确认 SSH SOCKS 已监听,`curl --proxy socks5h://127.0.0.1:1080 https://api.gateio.ws/api/v4/spot/time` 成功
2. `pm2 start ecosystem.config.cjs`
3. 再确认页面与余额等接口正常
---
## 12. 免责声明
交易所有合规与地区政策要求.请确保使用方式符合当地法律法规与交易所条款.本文仅描述网络与工程部署路径.
---
## 附录:数据库标签修复脚本 `scripts/fix_breakeven_labels.py`
在 Ubuntu 上:
1)预览(不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2)确认后执行:
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件:`monitor_type='下单监控'``result='止损'``pnl_amount > 0` → 改为 `result='保本止盈'`.
+90 -90
View File
@@ -1,107 +1,107 @@
# =============================================================================
# 环境配置模板可提交 Git)。程序运行时只读取同目录下的 .env
# 环境配置模板(可提交 Git).程序运行时只读取同目录下的 .env.
#
# 首次部署 / 新机
# 首次部署 / 新机:
# cp .env.example .env
# nano .env # 填入真实密钥端口代理等
# nano .env # 填入真实密钥,端口,代理等
#
# 升级代码git pull前建议备份.env 不在 Git 中pull 不会覆盖):
# 升级代码(git pull)前建议备份(.env 不在 Git 中,pull 不会覆盖):
# cp .env .env.backup.$(date +%Y%m%d)
#
# 从备份恢复
# 从备份恢复:
# cp .env.backup.YYYYMMDD .env
# =============================================================================
APP_ENV=production
# 服务监听地址云服务器通常用 0.0.0.0
# 服务监听地址(云服务器通常用 0.0.0.0)
APP_HOST=0.0.0.0
# 服务端口
APP_PORT=5004
# 是否开启调试模式生产建议 false
# 是否开启调试模式(生产建议 false)
APP_DEBUG=false
# 登录账号
APP_USERNAME=admin
# 登录密码请改成你自己的强密码
# 登录密码(请改成你自己的强密码)
APP_PASSWORD=admin123
# 是否关闭登录校验局域网可设 true公网务必 false
# 是否关闭登录校验(局域网可设 true;公网务必 false)
APP_AUTH_DISABLED=true
# --- 多账户交易中控 manual_trading_hub ---
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时仅网页登录后可访问本机联调可保持 APP_AUTH_DISABLED=true
# 中控请求本实例 /api/hub/* 时携带请求头 X-Hub-Token,须与中控启动环境变量 HUB_BRIDGE_TOKEN 一致
# 未设置且 APP_AUTH_DISABLED=false 时,仅网页登录后可访问;本机联调可保持 APP_AUTH_DISABLED=true
# HUB_BRIDGE_TOKEN=your-long-random-token
# 允许复盘中控 iframe 内嵌本实例与 hub 域名一致默认已开启
# 允许复盘中控 iframe 内嵌本实例(与 hub 域名一致;默认已开启)
# APP_ALLOW_HUB_EMBED=true
# HUB_EMBED_PARENT_ORIGINS=https://hub.example.com
# HTTPS 且经 iframe 打开时建议 true不设则 hub-sso 在 HTTPS 下也会自动尝试 SameSite=None
# HTTPS 且经 iframe 打开时建议 true;不设则 hub-sso 在 HTTPS 下也会自动尝试 SameSite=None
# APP_COOKIE_SECURE=true
# Flask 会话密钥必须替换为长随机字符串
# Flask 会话密钥(必须替换为长随机字符串)
FLASK_SECRET_KEY=CHANGE_TO_LONG_RANDOM_SECRET
# 企业微信机器人 Webhook用于行情/风控推送
# 企业微信机器人 Webhook(用于行情/风控推送)
WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=REPLACE_WITH_REAL_KEY
# 数据库文件路径相对路径会自动按项目目录解析
# 数据库文件路径(相对路径会自动按项目目录解析)
DB_PATH=crypto.db
# 交易截图上传目录
UPLOAD_DIR=static/images
# 自动备份scripts/backup_data.sh + cron可选默认即可
# 自动备份(scripts/backup_data.sh + cron,可选;默认即可)
# BACKUP_ROOT=/root/backups
# BACKUP_RETENTION_DAYS=30
# BACKUP_INSTANCE=crypto_monitor_okx
# 训练总资金U
# TOTAL_CAPITAL=100 # 已弃用资金展示读交易所
# 计仓risk=以损定仓默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆须无仓后重启
# 训练总资金(U)
# TOTAL_CAPITAL=100 # 已弃用,资金展示读交易所
# 计仓:risk=以损定仓(默认);full_margin=合约可用×FULL_MARGIN_BUFFER_RATIO 全仓杠杆(须无仓后重启)
POSITION_SIZING_MODE=risk
# 方向限制默认 false=双向均可true 时按 TRADE_DIRECTION 限制修改后须重启
# TRADE_DIRECTION=long_only | short_only | both或 多/空/双向
# 方向限制(默认 false=双向均可;true 时按 TRADE_DIRECTION 限制,修改后须重启)
# TRADE_DIRECTION=long_only | short_only | both(或 多/空/双向)
TRADE_DIRECTION_RESTRICT_ENABLED=false
TRADE_DIRECTION=both
# 币种白名单默认 false=全币种可手输true 时关键位/下单/策略仅下拉选择
# 币种白名单(默认 false=全币种可手输;true 时关键位/下单/策略仅下拉选择)
TRADE_SYMBOL_RESTRICT_ENABLED=false
TRADE_SYMBOL_WHITELIST=BTC,ETH
# 每天起始基数U
# 每天起始基数(U)
DAILY_START_CAPITAL=30
# 日内回撤后基数U
# 日内回撤后基数(U)
DAILY_LOSS_CAPITAL=20
# 日内盈利后基数U
# 日内盈利后基数(U)
DAILY_PROFIT_CAPITAL=50
# BTC 默认杠杆倍数
BTC_LEVERAGE=10
# 山寨币默认杠杆倍数
ALT_LEVERAGE=5
# 交易日重置小时北京时间
# 交易日重置小时(北京时间)
TRADING_DAY_RESET_HOUR=8
# 整点前禁止新开仓true=启用默认),false=关闭仍可保留 8 点作为交易日划分
# 整点前禁止新开仓:true=启用(默认),false=关闭(仍可保留 8 点作为交易日划分)
TRADING_DAY_RESET_OPEN_GUARD_ENABLED=true
# 是否开启 OKX 实盘下单false=只做本地流程true=真实下单
# 是否开启 OKX 实盘下单(false=只做本地流程,true=真实下单)
LIVE_TRADING_ENABLED=true
# OKX API Key实盘
# OKX API Key(实盘)
OKX_API_KEY=REPLACE_WITH_OKX_API_KEY
# OKX API Secret实盘
# OKX API Secret(实盘)
OKX_API_SECRET=REPLACE_WITH_OKX_API_SECRET
# OKX API Passphrase实盘
# OKX API Passphrase(实盘)
OKX_API_PASSPHRASE=REPLACE_WITH_OKX_API_PASSPHRASE
# 保证金模式cross=全仓isolated=逐仓
# 保证金模式:cross=全仓,isolated=逐仓
OKX_TD_MODE=cross
# 持仓模式hedge=双向持仓net=单向净持仓
# 持仓模式:hedge=双向持仓,net=单向净持仓
OKX_POS_MODE=hedge
# 仓位查询 instTypeOKX
# 仓位查询 instType(OKX)
OKX_POSITION_INST_TYPE=SWAP
# 从 OKX 历史仓位同步已实现盈亏北京时间起点空=近 90 天 0 点起
# 从 OKX 历史仓位同步已实现盈亏(北京时间起点,空=近 90 天 0 点起)
# EXCHANGE_POSITION_SYNC_FROM_BJ=2026-01-01
# 单次拉取历史仓位条数上限OKX 每页最多 100程序会分页
# 单次拉取历史仓位条数上限(OKX 每页最多 100,程序会分页)
# EXCHANGE_POSITION_HISTORY_LIMIT=200
# 页面与浏览器标签展示的交易所名称多环境区分时可改成例如 OKX·测试网
# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 OKX·测试网)
# EXCHANGE_DISPLAY_NAME=OKX
# 企业微信推送里展示的账户备注
# OKX_ACCOUNT_LABEL=
# =============================================================================
# 期权主账户 API与永续子账户 OKX_API_* 分离修改后须重启 PM2
# 期权(主账户 API,与永续子账户 OKX_API_* 分离;修改后须重启 PM2)
# 详见 docs/期权方案.md 与 docs/期权用法.md
# =============================================================================
OKX_OPTIONS_ENABLED=false
@@ -122,66 +122,66 @@ OKX_OPTIONS_TD_MODE=isolated
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
# =============================================================================
# 关键位程序自动下单与 POSITION_SIZING_MODE 联动修改后须重启 PM2
# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2)
# =============================================================================
# 默认 false = 关闭所有关键位程序自动单箱体/收敛/斐波/假突破/触价
# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价)
#
# POSITION_SIZING_MODE=risk以损定仓
# false → 不执行任何关键位自动单支撑/阻力提醒人工下单顺势加仓不受影响
# true → 允许关键位全套自动含触价
# POSITION_SIZING_MODE=risk(以损定仓)
# false → 不执行任何关键位自动单;支撑/阻力提醒,人工下单,顺势加仓不受影响
# true → 允许关键位全套自动(含触价)
#
# POSITION_SIZING_MODE=full_margin全仓杠杆须无仓切换
# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换)
# false → 不执行触价自动单
# true → 仅回调/突破触价可程序自动开仓箱体/斐波等仍禁止
# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止
#
# 顺势加仓趋势回调不受本开关控制全仓模式下策略自动仍禁止
# 顺势加仓,趋势回调不受本开关控制;全仓模式下策略自动仍禁止.
KEY_AUTO_ORDER_ENABLED=false
# =============================================================================
# 关键位门控页面「关键位监控」规则条与 _key_hard_checks 共用
# 关键位门控(页面「关键位监控」规则条与 _key_hard_checks 共用)
# =============================================================================
# 【周期】门控 K 线周期如 5m15m仅影响关键位硬条件不改变顶栏分区
# 【周期】门控 K 线周期,如 5m,15m;仅影响关键位硬条件,不改变顶栏分区
KLINE_TIMEFRAME=5m
# OKX 遗留突破过滤百分比与 KEY_BREAKOUT_AMP_* 并存程序仍读取
# OKX 遗留:突破过滤百分比(与 KEY_BREAKOUT_AMP_* 并存,程序仍读取)
KEY_BREAKOUT_LIMIT_PCT=1.5
# 【确认K】闭合 K 序列中的棒偏移突破棒默认 -2倒数第2根),确认棒默认 -1倒数第1根
# 【确认K】闭合 K 序列中的棒偏移:突破棒默认 -2(倒数第2根),确认棒默认 -1(倒数第1根)
KEY_CONFIRM_BREAKOUT_BAR=-2
KEY_CONFIRM_BAR=-1
# 【量能】突破棒成交量 > 前 N 根均量 × 倍数默认 N=20倍数=1.3 即放大 30%
# 【量能】突破棒成交量 > 前 N 根均量 × 倍数(默认 N=20,倍数=1.3 即放大 30%)
KEY_VOLUME_MA_BARS=20
KEY_VOLUME_RATIO_MIN=1.3
# 【箱体/收敛】突破K收盘越过关键位占该侧价格%的下限无上限过猛由计划RR过滤
# 【箱体/收敛】突破K收盘越过关键位(占该侧价格%)的下限;无上限(过猛由计划RR过滤)
KEY_BREAKOUT_AMP_MIN_PCT=0.03
# 已不参与门控可保留配置项兼容旧环境
# 已不参与门控,可保留配置项兼容旧环境
KEY_BREAKOUT_AMP_MAX_PCT=0.5
# 【阻力/支撑】突破后微信提醒次数与间隔分钟
# 【阻力/支撑】突破后微信提醒次数与间隔(分钟)
KEY_ALERT_MAX_TIMES=3
KEY_ALERT_INTERVAL_MINUTES=5
# 【日成交量排名】品种须在该排名前 N 名添加关键位与运行时门控均校验
# 【日成交量排名】品种须在该排名前 N 名(添加关键位与运行时门控均校验)
KEY_DAILY_VOLUME_RANK_MAX=30
# 【关键位自动开仓盈亏比】按确认K收盘 E 计算严格大于该值才市价开仓如 1.5 表示须 >1.5:1
# 【关键位自动开仓盈亏比】按确认K收盘 E 计算,严格大于该值才市价开仓(如 1.5 表示须 >1.5:1)
KEY_AUTO_MIN_PLANNED_RR=1.5
# 止损突破 K 极值向外缓冲的百分比默认 0.5 即 0.5%
# 止损:突破 K 极值向外缓冲的百分比(默认 0.5 即 0.5%)
KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5
# 趋势单方案止损在突破 K 极值外侧的百分比默认 1 即 1%
# 趋势单方案:止损在突破 K 极值外侧的百分比(默认 1 即 1%)
KEY_TREND_STOP_OUTSIDE_PCT=1
# =============================================================================
# 交易执行 / 人工风控页面「实盘下单」
# 交易执行 / 人工风控(页面「实盘下单」)
# =============================================================================
# 【最大同时持仓】active 订单数达到该值后禁止人工与关键位自动再加仓默认 1=单仓
# 【最大同时持仓】active 订单数达到该值后禁止人工与关键位自动再加仓(默认 1=单仓)
MAX_ACTIVE_POSITIONS=1
# 【人工下单最低盈亏比】按当前价与 SL/TP 计算低于该值前后端均拒绝默认 1.4即须 >=1.4:1
# 【人工下单最低盈亏比】按当前价与 SL/TP 计算,低于该值前后端均拒绝(默认 1.4,即须 >=1.4:1)
MANUAL_MIN_PLANNED_RR=1.4
# 【关键位连开计仓】true=已有持仓时关键位自动单仍按「无仓时」资金快照算保证金基数
KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT=true
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒不拦单
# 【单日开仓 AI 提醒】本交易日开仓达到该次数时推送企业微信 AI 克制提醒(不拦单)
DAILY_OPEN_ALERT_THRESHOLD=5
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
# 【单日开仓硬上限】本交易日开仓次数>=该值后禁止一切新开仓直至下一交易日(北京时间 TRADING_DAY_RESET_HOUR 切日);0=不启用
DAILY_OPEN_HARD_LIMIT=0
# =============================================================================
# 账户冷静期 / 日冻结风控手动平仓外部平仓复盘情绪标签
# 账户冷静期 / 日冻结风控(手动平仓,外部平仓,复盘情绪标签)
# 详见 docs/account-risk-cooldown.md
# =============================================================================
RISK_CONTROL_ENABLED=true
@@ -190,42 +190,42 @@ RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
RISK_MOOD_ISSUES_DAILY_FREEZE=true
# 资金与仓位刷新周期(秒)
# 资金与仓位刷新周期(秒)
BALANCE_REFRESH_SECONDS=60
# 前端价格快照轮询(秒)
# 前端价格快照轮询(秒)
PRICE_REFRESH_SECONDS=5
# 后台监控轮询周期(秒)
# 后台监控轮询周期(秒)
MONITOR_POLL_SECONDS=3
# 移动保本同步交易所止盈止损的最小间隔(秒),避免频繁撤挂叠单
# 移动保本同步交易所止盈止损的最小间隔(秒),避免频繁撤挂叠单
BREAKEVEN_EXCHANGE_MIN_INTERVAL_SEC=60
# 重启后多少秒内不做「外部平仓」同步避免 API 未就绪误判
# 重启后多少秒内不做「外部平仓」同步(避免 API 未就绪误判)
RECONCILE_STARTUP_GRACE_SEC=90
# 连续多少次轮询确认交易所空仓后才记为外部平仓默认 3 次 ≈ 9 秒
# 连续多少次轮询确认交易所空仓后,才记为外部平仓(默认 3 次 ≈ 9 秒)
RECONCILE_FLAT_CONFIRM_POLLS=3
# 使用可用资金时的缓冲比例如0.98代表用98%
# 使用可用资金时的缓冲比例(如0.98代表用98%)
FULL_MARGIN_BUFFER_RATIO=0.98
# =============================================================================
# 自动划转页顶「将 swap 补足到 XU」与 DAILY_START_CAPITAL 独立需一致时请设为相同值
# 自动划转(页顶「将 swap 补足到 XU」;与 DAILY_START_CAPITAL 独立,需一致时请设为相同值)
# =============================================================================
AUTO_TRANSFER_ENABLED=false
# 交易账户(swap)目标余额 U每日 8 点(北京)自动划入或划出至 funding持仓中不划转
# 交易账户(swap)目标余额 U:每日 8 点(北京)自动划入或划出至 funding;持仓中不划转
AUTO_TRANSFER_AMOUNT=30
AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap
TRANSFER_CCY=USDT
# 北京时间该整点小时内尝试账簿按 UTC 自然日去重
# 北京时间该整点小时内尝试;账簿按 UTC 自然日去重
AUTO_TRANSFER_BJ_HOUR=8
# 强制清仓整点北京时间默认 0=凌晨00点
# 强制清仓整点(北京时间,默认 0=凌晨00点)
FORCE_CLOSE_BJ_HOUR=0
# 是否启用强制清仓默认关闭true 才会在整点执行
# 是否启用强制清仓(默认关闭,true 才会在整点执行)
FORCE_CLOSE_ENABLED=false
# 推送与AI超时(秒)
# 推送与AI超时(秒)
WECHAT_TIMEOUT_SECONDS=10
AI_TIMEOUT_SECONDS=120
# AI 复盘服务地址本机 Ollama 默认地址
# AI 复盘服务地址(本机 Ollama 默认地址)
AI_PROVIDER=openai
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
@@ -233,31 +233,31 @@ OPENAI_MODEL=gemma4:e4b
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest
# OKX 代理可选仅本地开发网络受限时用云服务器部署请留空直连 OKX 即可
# 1) 先在本机建立隧道示例):
# OKX 代理(可选,仅本地开发网络受限时用;云服务器部署请留空,直连 OKX 即可)
# 1) 先在本机建立隧道(示例):
# ssh -N -D 127.0.0.1:1080 root@你的VPS_IP -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes
# 2) 再启用下面这一行推荐 socks5h让远端解析域名):
# 2) 再启用下面这一行(推荐 socks5h,让远端解析域名):
# OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080
#
# 如你更偏向 HTTP 代理VPS 上跑 tinyproxy 之类),可用
# 如你更偏向 HTTP 代理(VPS 上跑 tinyproxy 之类),可用:
# OKX_HTTP_PROXY=http://127.0.0.1:3128
# OKX_HTTPS_PROXY=http://127.0.0.1:3128
# 开仓多周期K线图可选
# 开仓多周期K线图(可选)
# ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts
# 详见上文 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT说明文档 docs/daily-open-limit.md
# 以损定仓按交易账户资金的百分比
# 详见上文 DAILY_OPEN_ALERT_THRESHOLD / DAILY_OPEN_HARD_LIMIT;说明文档 docs/daily-open-limit.md
# 以损定仓(按交易账户资金的百分比)
# RISK_PERCENT=2
# 移动保本触发达到多少R触发与偏移百分比
# 移动保本触发(达到多少R触发)与偏移(百分比)
# BREAKEVEN_RR_TRIGGER=1.0
# 移动保本阶梯每多少R继续上移一次默认1R
# 移动保本阶梯(每多少R继续上移一次,默认1R)
# BREAKEVEN_STEP_R=1.0
# BREAKEVEN_OFFSET_PCT=0.02
# 开单风格默认值trend / swing
# 开单风格默认值:trend / swing
# DEFAULT_TRADE_STYLE=trend
APP_TIMEZONE=Asia/Shanghai
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点默认 8 点起算新交易日开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
# TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED
+14 -14
View File
@@ -1,32 +1,32 @@
# crypto_monitor_okx
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统行情与实盘接口统一走 **OKXUSDT 永续**通过 **ccxt** 访问功能与界面已与 **`crypto_monitor_binance`** 对齐顶栏分栏风控参数交易所 TP/SL 管理等),差异主要在 **`.env``OKX_*` 变量** 与 OKX API含 Passphrase)。
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **OKX(USDT 永续)**,通过 **ccxt** 访问.功能与界面已与 **`crypto_monitor_binance`** 对齐(顶栏分栏,风控参数,交易所 TP/SL 管理等),差异主要在 **`.env``OKX_*` 变量** 与 OKX API(含 Passphrase).
## 功能概要
- **关键位监控**`/key_monitor`5m 门控企业微信部分类型自动开仓`关键位自动下单说明.md`
- **实盘下单**`/trade`以损定仓移动保本页面内撤挂止盈止损
- **策略交易**`/strategy`趋势回调 + 顺势加仓),见 [策略交易说明.md](../策略交易说明.md)
- **AI 复盘**见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)
- **实盘可选**`LIVE_TRADING_ENABLED=true` 且配置 `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE`
- **止盈止损OKX**市价成交后通过 ccxt 挂 **止损 / 止盈** 条件单`attachAlgoOrds` 或 reduceOnly 市价单路径`app.py`
- **关键位监控**:`/key_monitor`,5m 门控,企业微信,部分类型自动开仓(`关键位自动下单说明.md`)
- **实盘下单**:`/trade`,以损定仓,移动保本,页面内撤挂止盈止损
- **策略交易**:`/strategy`(趋势回调 + 顺势加仓),见 [策略交易说明.md](../策略交易说明.md)
- **AI 复盘**:见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)
- **实盘(可选)**:`LIVE_TRADING_ENABLED=true` 且配置 `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE`
- **止盈止损(OKX)**:市价成交后通过 ccxt 挂 **止损 / 止盈** 条件单(`attachAlgoOrds` 或 reduceOnly 市价单路径,`app.py`)
## 环境要求
- Python 3.10+
- 依赖见仓库根 `requirements.txt`**SSH SOCKS** 访问 OKX 时需 **`PySocks`**并配置 `OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080`
- 依赖见仓库根 `requirements.txt`;**SSH SOCKS** 访问 OKX 时需 **`PySocks`**,并配置 `OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080`
## 配置说明
| 变量 | 说明 |
|------|------|
| `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE` | OKX API |
| `OKX_TD_MODE` / `OKX_POS_MODE` | 全仓/逐仓单向/双向 |
| `OKX_TD_MODE` / `OKX_POS_MODE` | 全仓/逐仓,单向/双向 |
| `OKX_SOCKS_PROXY` | 本机 SSH 动态转发时常用 |
| `MAX_ACTIVE_POSITIONS` / `MANUAL_MIN_PLANNED_RR` | 与币安版一致的风控 |
| `EXCHANGE_DISPLAY_NAME` | 页面展示名默认 `OKX` |
| `EXCHANGE_DISPLAY_NAME` | 页面展示名,默认 `OKX` |
完整模板见 **`.env.example`**
完整模板见 **`.env.example`**.
## 运行
@@ -36,11 +36,11 @@ source .venv/bin/activate
python app.py
```
生产使用 **PM2**见 [docs/ubuntu-server.md](../docs/ubuntu-server.md)默认 **`APP_PORT`** 常为 `5004`
生产使用 **PM2**;见 [docs/ubuntu-server.md](../docs/ubuntu-server.md).默认 **`APP_PORT`** 常为 `5004`.
## 部署
详见 **[部署文档.md](./部署文档.md)****[使用说明.md](./使用说明.md)**
详见 **[部署文档.md](./部署文档.md)**,**[使用说明.md](./使用说明.md)**.
## 自检
@@ -50,4 +50,4 @@ python scripts/verify_okx_funding.py
## 风险与合规
实盘风险自负请确认 API 权限IP 白名单与 OKX 账户设置一致
实盘风险自负;请确认 API 权限,IP 白名单与 OKX 账户设置一致.
+333 -333
View File
File diff suppressed because it is too large Load Diff
+34 -34
View File
@@ -1,34 +1,34 @@
/**
* PM2 进程定义Ubuntu / Linux
*
* 仅托管 Flask 应用**SSH SOCKS 隧道** `ssh -D` 常驻可用 tmux / autossh勿交给 PM2
* `.env` `OKX_SOCKS_PROXY` 端口一致即可不必交给 PM2
*
* 使用前项目根目录存在 `.venv`且已安装依赖 SOCKS 时需 PySocks
*
* 启动
* pm2 start ecosystem.config.cjs
* 保存开机列表
* pm2 save && pm2 startup
*/
const path = require("path");
const ROOT = __dirname;
const REPO_ROOT = path.join(ROOT, "..");
const PY = path.join(ROOT, ".venv", "bin", "python");
module.exports = {
apps: [
{
name: "crypto_okx",
cwd: ROOT,
script: path.join(ROOT, "app.py"),
interpreter: PY,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "800M",
env: { PYTHONPATH: REPO_ROOT },
},
],
};
/**
* PM2 进程定义(Ubuntu / Linux).
*
* 仅托管 Flask 应用.**SSH SOCKS 隧道** `ssh -D` 常驻(可用 tmux / autossh),勿交给 PM2.
* `.env` `OKX_SOCKS_PROXY` 端口一致即可;不必交给 PM2.
*
* 使用前:项目根目录存在 `.venv`,且已安装依赖( SOCKS 时需 PySocks).
*
* 启动:
* pm2 start ecosystem.config.cjs
* 保存开机列表:
* pm2 save && pm2 startup
*/
const path = require("path");
const ROOT = __dirname;
const REPO_ROOT = path.join(ROOT, "..");
const PY = path.join(ROOT, ".venv", "bin", "python");
module.exports = {
apps: [
{
name: "crypto_okx",
cwd: ROOT,
script: path.join(ROOT, "app.py"),
interpreter: PY,
instances: 1,
autorestart: true,
watch: false,
max_memory_restart: "800M",
env: { PYTHONPATH: REPO_ROOT },
},
],
};
@@ -1,18 +1,18 @@
#!/usr/bin/env python3
"""
一次性修复历史交易记录标签
trade_records 止损但实际盈利的记录改为保本止盈
一次性修复历史交易记录标签:
trade_records 止损但实际盈利的记录改为保本止盈.
默认条件可通过参数修改
默认条件(可通过参数修改):
- monitor_type = 下单监控
- result = 止损
- pnl_amount > 0
用法示例
1) 仅预览不落库
用法示例:
1) 仅预览(不落库):
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
2) 执行修复
2) 执行修复:
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
"""
@@ -1,52 +1,52 @@
#!/usr/bin/env python3
"""
python scripts/verify_okx_funding.py
打印 OKX_API_KEY 8 位便于与 Binance 控制台核对不含 Secret用于服务器自检
"""
import os
import sys
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE)
def load_env(path):
if not os.path.exists(path):
return
for line in open(path, "r", encoding="utf-8", errors="ignore"):
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
k = k.strip().lstrip("\ufeff")
if k.replace("_", "").isalnum():
os.environ[k] = v.strip().strip('"').strip("'")
def main():
load_env(os.path.join(BASE, ".env"))
k = (os.getenv("OKX_API_KEY") or "").strip()
s = (os.getenv("OKX_API_SECRET") or "").strip()
if not k or "REPLACE" in k.upper():
print("WARN: OKX_API_KEY 为空或仍像占位符请核对 .env")
if not s or "REPLACE" in s.upper():
print("WARN: OKX_API_SECRET 为空或仍像占位符请核对 .env")
print("OKX_API_KEY prefix (8 chars):", (k[:8] + "") if len(k) > 8 else "(short)")
import app as mod # noqa: E402
mod.ensure_markets_loaded()
fu = mod._fetch_okx_funding_usdt()
print(">>> _fetch_okx_funding_usdt() =", fu)
try:
sw = mod._fetch_okx_swap_usdt_total()
print(">>> _fetch_okx_swap_usdt_total() (合约账户) =", sw)
sf = mod._fetch_okx_swap_usdt_free()
print(">>> _fetch_okx_swap_usdt_free() (合约可用) =", sf)
except Exception as e:
print(">>> swap balance fetch error:", e)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
python scripts/verify_okx_funding.py
打印 OKX_API_KEY 8 位便于与 Binance 控制台核对(不含 Secret).用于服务器自检.
"""
import os
import sys
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE)
def load_env(path):
if not os.path.exists(path):
return
for line in open(path, "r", encoding="utf-8", errors="ignore"):
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
k, v = line.split("=", 1)
k = k.strip().lstrip("\ufeff")
if k.replace("_", "").isalnum():
os.environ[k] = v.strip().strip('"').strip("'")
def main():
load_env(os.path.join(BASE, ".env"))
k = (os.getenv("OKX_API_KEY") or "").strip()
s = (os.getenv("OKX_API_SECRET") or "").strip()
if not k or "REPLACE" in k.upper():
print("WARN: OKX_API_KEY 为空或仍像占位符,请核对 .env")
if not s or "REPLACE" in s.upper():
print("WARN: OKX_API_SECRET 为空或仍像占位符,请核对 .env")
print("OKX_API_KEY prefix (8 chars):", (k[:8] + "") if len(k) > 8 else "(short)")
import app as mod # noqa: E402
mod.ensure_markets_loaded()
fu = mod._fetch_okx_funding_usdt()
print(">>> _fetch_okx_funding_usdt() =", fu)
try:
sw = mod._fetch_okx_swap_usdt_total()
print(">>> _fetch_okx_swap_usdt_total() (合约账户) =", sw)
sf = mod._fetch_okx_swap_usdt_free()
print(">>> _fetch_okx_swap_usdt_free() (合约可用) =", sf)
except Exception as e:
print(">>> swap balance fetch error:", e)
if __name__ == "__main__":
main()
@@ -29,9 +29,9 @@
<div class="row" style="justify-content:space-between">
<div class="row">
<a class="btn" href="/">返回首页</a>
<strong style="color:#dbe4ff">实盘下单放大100根K线</strong>
<strong style="color:#dbe4ff">实盘下单放大(100根K线)</strong>
</div>
<div class="status">最近刷新<span id="updated-at">--</span></div>
<div class="status">最近刷新:<span id="updated-at">--</span></div>
</div>
{% if orders %}
<div class="row" style="margin-top:10px">
@@ -53,7 +53,7 @@
<span id="load-status" class="status"></span>
</div>
{% else %}
<div class="empty">当前没有激活订单无法展示放大K线</div>
<div class="empty">当前没有激活订单,无法展示放大K线.</div>
{% endif %}
</div>
+138 -138
View File
@@ -1,138 +1,138 @@
# 使用说明
**本文件对应仓库`crypto_monitor_okx`OKX USDT 本位永续)。**
功能界面与 **Gate.io USDT 永续版**目录 `crypto_monitor_gate`基本一致差异主要在 **`.env` 里交易所密钥与部分参数名**`OKX_*` / `GATE_*`),文末有对照
**部署代理PM2 等**请参考本仓库说明或 **`crypto_monitor_gate`** 下的 **`部署文档.md`**该文以 Gate + SSH SOCKS 为例OKX 侧将 API 与密钥改为 `OKX_*` 即可类比)。
**关键位自动开仓的规则RR结案原因**见本目录 **`关键位自动下单说明.md`**
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**主要能力包括
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型**5m 收线** 做硬条件过滤符合条件后 **企业微信** 提醒部分类型可 **自动市价开仓**见第 4 节与专门文档)。 |
| **实盘下单监控** | 手工填止损/止盈**以损定仓** 市价开单挂上条件止盈止损并在页面跟踪浮盈亏保本逻辑等 |
| **交易记录 / 复盘** | 平仓结果盈亏错过的单等归档与导出可选 **AI 复盘**见仓库根 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md))。 |
| **策略交易** | 顶栏 `/strategy`**趋势回调**(左)**顺势加仓**(右)左右并列细则见 [策略交易说明.md](../策略交易说明.md) |
后台按 **`MONITOR_POLL_SECONDS`**默认几秒轮询行情与监控逻辑**切勿**在未理解规则时同时运行两套程序共用一个实盘账户
---
## 2. 运行前必须配置`.env`
首次在本目录执行 **`cp .env.example .env`**再编辑 `.env``.env` 勿提交 Git`git pull` 不会改你的 `.env`升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`)。
至少检查以下项具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`打开站点后的登录口令`FLASK_SECRET_KEY`Session 密钥请勿使用默认值 |
| **企业微信** | `WECHAT_WEBHOOK`告警与关键位推送机器人的 Webhook |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`**不会**向交易所发送开仓指令适合测试流程)。改为 `true` 且密钥正确才会实盘 |
| **交易所 API** | **本仓库** `OKX_API_KEY``OKX_API_SECRET`永续相关见 `OKX_TD_MODE``OKX_POS_MODE``OKX_TRIGGER_WORKING_TYPE`**勿**把 `.env` 提交到 Git |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR``KEY_STOP_OUTSIDE_BREAKOUT_PCT`详见 `关键位自动下单说明.md`)。 |
| **AI 复盘** | 默认 `AI_PROVIDER=openai``OPENAI_API_BASE=https://op.bz121.com/v1``OPENAI_API_KEY``OPENAI_MODEL=gemma4:e4b``AI_PROVIDER=ollama` + `OLLAMA_API` / `AI_MODEL`详见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md) |
网络需要代理时可配置 **`OKX_SOCKS_PROXY` / `OKX_HTTP_PROXY`**与 Gate 版 `GATE_*_PROXY` 用法类似)。
---
## 3. 如何启动与登录
1. 准备 Python 虚拟环境并安装依赖`flask``requests``ccxt`按需 `Pillow``PySocks`),配置好 `.env`
2. 启动 Flask 应用可用 **`ecosystem.config.cjs`** 交给 PM2或本地 `python app.py` / `flask run`以你当前脚本为准)。
3. 浏览器访问站点打开 **`/login`**使用 **`.env` 里的 `APP_PASSWORD`** 登录
登录后顶栏**关键位监控** | **实盘下单**默认首页| **策略交易**`/strategy`趋势回调 + 顺势加仓双栏| **策略交易记录**`/strategy/records`| **交易记录与复盘** | **统计分析**
---
## 4. 关键位监控顶栏「关键位监控」→ `/key_monitor`
### 4.1 添加一条关键位
1. **币种**`BTC``BTC/USDT`会规范成内部符号)。
2. **类型**必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**`LIVE_TRADING_ENABLED=true` 且无其他持仓占位)。结案后本条从列表消失并记入历史 |
| **收敛突破** | 同上自动开仓类)。 |
| **关键阻力位** | **不自动开仓**触发后 **发 1 次微信**然后本条 **结案进历史** |
| **关键支撑位** | 同上仅提醒)。 |
| **回调触价开仓** | **不挂交易所限价**标记价回调触达 E 后 **下一轮询市价开仓**RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**标记价 **穿越 E 立即市价开仓**先触 SL/TP 侧失效有效期 **24h** |
3. **方向**做多 / 做空触价开仓 / 箱体 / 收敛 / 斐波必选阻力/支撑不选)。
4. **价位**箱体/收敛/阻力/支撑填 **上沿 / 下沿**触价开仓填 **入场 E / 止损 SL / 止盈 TP**
**限制**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**仍可添加「**关键阻力位 / 支撑位**」
**4h EMA55** 与你的方向逆势页面会 **额外 Flash 提示****不阻挡**提交
### 4.2 触发后会发生什么简版
- **箱体 / 收敛**门控通过后算计划 SL/TP 与 RR不达标 → 微信说明 + **`rr_insufficient`** 结案达标 → **市价开仓**成功 **`auto_opened`** / 失败 **`exchange_failed`**均不重试同一关键位
- **阻力 / 支撑****单次推送****`key_level_alert_only`** 结案
详细公式与字段见 **`关键位自动下单说明.md`**
### 4.3 列表与历史
当前条目与历史记录的用法与 Gate 版相同结案后可在历史区查阅 **`close_reason`**
---
## 5. 实盘下单顶栏「实盘下单」→ `/trade`
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制默认 1)。
- **人工开仓**计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**默认 1.4:1)。
- 填写币种方向杠杆可选)、止损/止盈价格或百分比按表单)。
- 移动保本等选项按页面与 `.env` 默认
开仓成功后卡片 **「来源」**手工一般为 **下单监控**关键位自动为 **关键位监控**
---
## 6. 企业微信
推送逻辑与 Gate 版一致未配置 **`WECHAT_WEBHOOK`** 时可能没有消息请以 **交易所端** 核对持仓与挂单
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 熟悉流程再实盘
2. **API 权限**最小化密钥勿泄露
3. **同一账户避免多程序重复开仓**
4. **自动备份**服务器上执行 `bash scripts/install_backup_cron.sh`每天北京时间 0:00 → `/root/backups`保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次
5. 升级代码后留意 **首轮启动**有无数据库迁移报错
---
## 8. 常见问题简要
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 门控五项日成交量排名`KLINE_TIMEFRAME` |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`RR 阈值是否已有持仓API/保证金错误信息 |
| 加不了箱体/收敛 | 是否已有持仓 |
| 推送收不到 | Webhook网络 |
---
## 9. 与币安版`crypto_monitor_binance`差异速查
| 项目 | OKX 本仓库 | 币安版 |
|------|------------|--------|
| API 变量 | `OKX_API_KEY``OKX_API_SECRET``OKX_API_PASSPHRASE` | `BINANCE_API_KEY``BINANCE_API_SECRET` |
| 代理 | `OKX_SOCKS_PROXY` | `BINANCE_SOCKS_PROXY` |
| 默认端口 | 常为 `5004` | 常为 `5001` |
| TP/SL 实现 | `_okx_place_tp_sl_orders`页面 `/api/order/.../cancel_tpsl` | `_binance_place_tp_sl_orders` |
业务流程顶栏分栏策略交易风控参数名已与币安版对齐仅需更换目录与 `.env`
# 使用说明
**本文件对应仓库:`crypto_monitor_okx`(OKX USDT 本位永续).**
功能,界面与 **Gate.io USDT 永续版**(目录 `crypto_monitor_gate`)基本一致,差异主要在 **`.env` 里交易所密钥与部分参数名**(`OKX_*` / `GATE_*`),文末有对照.
**部署,代理,PM2 等**请参考本仓库说明或 **`crypto_monitor_gate`** 下的 **`部署文档.md`**(该文以 Gate + SSH SOCKS 为例;OKX 侧将 API 与密钥改为 `OKX_*` 即可类比).
**关键位自动开仓的规则,RR,结案原因**见本目录 **`关键位自动下单说明.md`**.
---
## 1. 它能做什么
面向个人盘面的 **Web 控制台**,主要能力包括:
| 模块 | 说明 |
|------|------|
| **关键位监控** | 录入上/下沿与类型,**5m 收线** 做硬条件过滤;符合条件后 **企业微信** 提醒,部分类型可 **自动市价开仓**(见第 4 节与专门文档). |
| **实盘下单监控** | 手工填止损/止盈,**以损定仓** 市价开单,挂上条件止盈止损,并在页面跟踪浮盈亏,保本逻辑等. |
| **交易记录 / 复盘** | 平仓结果,盈亏,错过的单等归档与导出;可选 **AI 复盘**(见仓库根 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)). |
| **策略交易** | 顶栏 `/strategy`:**趋势回调**(左)**顺势加仓**(右)左右并列;细则见 [策略交易说明.md](../策略交易说明.md). |
后台按 **`MONITOR_POLL_SECONDS`**(默认几秒)轮询行情与监控逻辑.**切勿**在未理解规则时同时运行两套程序共用一个实盘账户.
---
## 2. 运行前必须配置(`.env`)
首次在本目录执行 **`cp .env.example .env`**,再编辑 `.env`(`.env` 勿提交 Git;`git pull` 不会改你的 `.env`,升级前建议 `cp .env .env.backup.$(date +%Y%m%d)`).
至少检查以下项(具体键名以 **`.env.example`** 为准):
| 类别 | 说明 |
|------|------|
| **登录网页** | `APP_PASSWORD`:打开站点后的登录口令.`FLASK_SECRET_KEY`:Session 密钥,请勿使用默认值. |
| **企业微信** | `WECHAT_WEBHOOK`:告警与关键位推送机器人的 Webhook. |
| **是否真下单** | `LIVE_TRADING_ENABLED=false`:**不会**向交易所发送开仓指令(适合测试流程).改为 `true` 且密钥正确才会实盘. |
| **交易所 API** | **本仓库:** `OKX_API_KEY`,`OKX_API_SECRET`;永续相关见 `OKX_TD_MODE`,`OKX_POS_MODE`,`OKX_TRIGGER_WORKING_TYPE`.**勿**把 `.env` 提交到 Git. |
| **关键位 RR / 止损外扩** | `KEY_AUTO_MIN_PLANNED_RR`,`KEY_STOP_OUTSIDE_BREAKOUT_PCT`(详见 `关键位自动下单说明.md`). |
| **AI 复盘** | 默认 `AI_PROVIDER=openai`,`OPENAI_API_BASE=https://op.bz121.com/v1`,`OPENAI_API_KEY`,`OPENAI_MODEL=gemma4:e4b`;`AI_PROVIDER=ollama` + `OLLAMA_API` / `AI_MODEL`.详见 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md). |
网络需要代理时可配置 **`OKX_SOCKS_PROXY` / `OKX_HTTP_PROXY`**(与 Gate 版 `GATE_*_PROXY` 用法类似).
---
## 3. 如何启动与登录
1. 准备 Python 虚拟环境并安装依赖(`flask`,`requests`,`ccxt`,按需 `Pillow`,`PySocks`),配置好 `.env`.
2. 启动 Flask 应用(可用 **`ecosystem.config.cjs`** 交给 PM2,或本地 `python app.py` / `flask run`,以你当前脚本为准).
3. 浏览器访问站点,打开 **`/login`**,使用 **`.env` 里的 `APP_PASSWORD`** 登录.
登录后顶栏:**关键位监控** | **实盘下单**(默认首页)| **策略交易**(`/strategy`,趋势回调 + 顺势加仓双栏)| **策略交易记录**(`/strategy/records`)| **交易记录与复盘** | **统计分析**.
---
## 4. 关键位监控(顶栏「关键位监控」→ `/key_monitor`)
### 4.1 添加一条关键位
1. **币种**:`BTC``BTC/USDT`(会规范成内部符号).
2. **类型**(必选其一):
| 类型 | 行为摘要 |
|------|----------|
| **箱体突破** | 通过门控且计划 RR 达标 → **自动市价开仓**(`LIVE_TRADING_ENABLED=true` 且无其他持仓占位).结案后本条从列表消失并记入历史. |
| **收敛突破** | 同上(自动开仓类). |
| **关键阻力位** | **不自动开仓**;触发后 **发 1 次微信**,然后本条 **结案进历史**. |
| **关键支撑位** | 同上(仅提醒). |
| **回调触价开仓** | **不挂交易所限价**;标记价回调触达 E 后 **下一轮询市价开仓**(RR 门槛同 `KEY_AUTO_MIN_PLANNED_RR`);有效期 **24h** |
| **突破触价开仓** | **不挂交易所限价**;标记价 **穿越 E 立即市价开仓**;先触 SL/TP 侧失效;有效期 **24h** |
3. **方向**:做多 / 做空(触价开仓 / 箱体 / 收敛 / 斐波必选;阻力/支撑不选).
4. **价位**:箱体/收敛/阻力/支撑填 **上沿 / 下沿**;触价开仓填 **入场 E / 止损 SL / 止盈 TP**.
**限制:**
活跃持仓数达到 **`MAX_ACTIVE_POSITIONS`**(默认 1)时,**不允许**再添加「**箱体突破** / **收敛突破**;仍可添加「**关键阻力位 / 支撑位**」.
**4h EMA55** 与你的方向逆势,页面会 **额外 Flash 提示**,**不阻挡**提交.
### 4.2 触发后会发生什么(简版)
- **箱体 / 收敛**:门控通过后算计划 SL/TP 与 RR;不达标 → 微信说明 + **`rr_insufficient`** 结案;达标 → **市价开仓**,成功 **`auto_opened`** / 失败 **`exchange_failed`**,均不重试同一关键位.
- **阻力 / 支撑**:**单次推送****`key_level_alert_only`** 结案.
详细公式与字段见 **`关键位自动下单说明.md`**.
### 4.3 列表与历史
当前条目与历史记录的用法与 Gate 版相同;结案后可在历史区查阅 **`close_reason`**.
---
## 5. 实盘下单(顶栏「实盘下单」→ `/trade`)
- 持仓上限由 **`MAX_ACTIVE_POSITIONS`** 控制(默认 1).
- **人工开仓**计划盈亏比不得低于 **`MANUAL_MIN_PLANNED_RR`**(默认 1.4:1).
- 填写币种,方向,杠杆(可选),止损/止盈(价格或百分比按表单).
- 移动保本等选项按页面与 `.env` 默认.
开仓成功后卡片 **「来源」**:手工一般为 **下单监控**;关键位自动为 **关键位监控**.
---
## 6. 企业微信
推送逻辑与 Gate 版一致;未配置 **`WECHAT_WEBHOOK`** 时可能没有消息,请以 **交易所端** 核对持仓与挂单.
---
## 7. 强烈建议的风险与运维习惯
1. **先用 `LIVE_TRADING_ENABLED=false`** 熟悉流程再实盘.
2. **API 权限**最小化,密钥勿泄露.
3. **同一账户避免多程序重复开仓**.
4. **自动备份**:服务器上执行 `bash scripts/install_backup_cron.sh`(每天北京时间 0:00 → `/root/backups`,保留 30 天);升级前也可 `bash scripts/backup_data.sh` 手动跑一次.
5. 升级代码后留意 **首轮启动**有无数据库迁移报错.
---
## 8. 常见问题(简要)
| 现象 | 可自查 |
|------|--------|
| 关键位永远不触发 | 门控五项,日成交量排名,`KLINE_TIMEFRAME`. |
| 有信号但不自动开仓 | `LIVE_TRADING_ENABLED`,RR 阈值,是否已有持仓,API/保证金错误信息. |
| 加不了箱体/收敛 | 是否已有持仓. |
| 推送收不到 | Webhook,网络. |
---
## 9. 与币安版(`crypto_monitor_binance`)差异速查
| 项目 | OKX 本仓库 | 币安版 |
|------|------------|--------|
| API 变量 | `OKX_API_KEY`,`OKX_API_SECRET`,`OKX_API_PASSPHRASE` | `BINANCE_API_KEY`,`BINANCE_API_SECRET` |
| 代理 | `OKX_SOCKS_PROXY` | `BINANCE_SOCKS_PROXY` |
| 默认端口 | 常为 `5004` | 常为 `5001` |
| TP/SL 实现 | `_okx_place_tp_sl_orders`,页面 `/api/order/.../cancel_tpsl` | `_binance_place_tp_sl_orders` |
业务流程,顶栏分栏,策略交易,风控参数名已与币安版对齐;仅需更换目录与 `.env`.
+192 -192
View File
@@ -1,192 +1,192 @@
# 关键位监控说明自动开仓 + 人工盯盘
**适用Gate / Binance / OKX 三所实例共用 `lib/key_monitor/key_auto_order_lib.py`**
## 环境开关 `KEY_AUTO_ORDER_ENABLED`默认 `false`
| 计仓模式 | 开关 | 关键位程序自动单 |
|----------|------|------------------|
| `risk`以损定仓 | `false` | **全部关闭**含触价);支撑/阻力微信提醒仍可用 |
| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动旧行为 |
| `full_margin`全仓 | `false` | 全部关闭含触价 |
| `full_margin` | `true` | **仅触价**自动箱体/斐波等仍禁止 |
**不受本开关影响** 人工实盘下单关键支撑/阻力提醒**顺势加仓**`risk`)、趋势回调`risk`)。全仓模式下策略自动仍禁止
修改 `.env` 后须 **重启 PM2**复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项
---
**适用`crypto_monitor_gate`Gate U 本位永续**
Binance / OKX 见各自目录下同名文档共享逻辑在 `lib/key_monitor/`
本文档与 `.env``check_key_monitors``add_key``_key_hard_checks``_process_key_rs_level_alert` 一致
---
## 一监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**门控 + RR | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**同上 | 同上 |
| **关键阻力位** | **不选**`direction=watch` | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上与阻力位**相同规则**填上沿+下沿程序双向监控 |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明**不在下文展开** |
| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30**上沿 **>** 下沿触价开仓填 E/SL/TP上下沿仅作展示占位)。
---
## 二关键阻力位 / 关键支撑位人工盯盘
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**程序同时监控两侧**无法预先判定**做多还是做空)。
- 页面 **不显示不要求** 方向库中 `direction` 初始为 `watch`**首次突破后** 写入 `long`向上突破上沿`short`向下突破下沿)。
### 2.2 触发极简
- 周期**`KLINE_TIMEFRAME`默认 5m最近一根已闭合 K** 的 **收盘价**非影线)。
- **向上突破上沿** `收盘 > upper` → 推断方向 **多 / 向上**本次监控任务开始按节奏提醒
- **向下突破下沿** `收盘 < lower` → 推断方向 **空 / 向下**本次任务同样开始提醒
- **任一侧突破即结束本条监控周期**不会在突破后再等待另一侧上沿下沿谁先满足用谁同根 K 仅可能满足一侧)。
**不参与** 量能二确 K越过幅度下限日成交排名运行时)、计划 RR自动开仓
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次首次检测到突破的当次轮询若已闭合 5m 满足条件)。
- 第 23 次仅按间隔推送**不要求**价格仍在箱外)。
- 第 3 次推送后写入 `key_monitor_history``close_reason=**key_level_alert_done**``key_monitors` **删除**
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根突破 K + 确认 K |
| 提醒次数 | 3 次后结案 | 自动单触发后 1 次业务推送并结案 |
---
## 三箱体突破 / 收敛突破自动开仓
### 3.1 K 线结构默认索引
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控须全部通过
1. **有效突破收盘越界**
- 多`突破 K 收盘 > upper`
- 空`突破 K 收盘 < lower`
2. **突破越过幅度仅下限**
- 多`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`默认 **0.03%**
- 空`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**突破过猛由 **计划 RR** 过滤
- **不再**使用 K 线实体占开盘价比例`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**
3. **确认 K 不进箱体**
- 多确认 K 收盘 **`> upper`**不得在 `[lower, upper]`
- 空确认 K 收盘 **`< lower`**
4. **量能** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`默认 20根均量 × `KEY_VOLUME_RATIO_MIN`默认 1.3
5. **日成交量排名** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30
6. **计划 RR最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`默认 1.5才市价开仓
### 3.3 止损 / 止盈确认 K 收盘为 E
箱体高 **H = |upper lower|**止损锚在 **突破 K 极值** 外侧
| 方向 | 止损标准/趋势方案 |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表与改版前一致):
| 方案 | `sl_tp_mode` | 多SL / TP | 空SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案`close_reason`
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿 |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四回调 / 突破触价开仓程序触价无交易所挂单
### 4.1 录入
- **回调触价开仓**方向必选多/空填写 **计划入场价 E****止损 SL****止盈 TP**做多须 `SL < E < TP`)。
- **突破触价开仓**同上添加时当前价须在突破方向一侧做多价低于 E做空价高于 E)。
- 计划 RR 以 **E** 为基准**严格大于** `KEY_AUTO_MIN_PLANNED_RR`默认 1.5)。
- 可选移动保本时间平仓**全仓杠杆模式**下可用
### 4.2 触发与结案
| 类型 | 触发条件标记价 |
|------|-------------------|
| **回调触价** | 做多 `≤ E`做空 `≥ E` → 下一轮询市价开仓 |
| **突破触价** | 做多**向上穿越** E做空**向下穿越** E → **立即**市价开仓 |
- 未成交前标记价先触 **TP 侧**`trigger_tp_invalidate`
- **突破触价**另未穿越 E 先触 **SL 侧**`trigger_sl_invalidate`
- **24h** 未触发 → `trigger_entry_expired`
- 成功 → `trigger_entry_filled`触发后开仓失败 → `trigger_exchange_failed`
### 4.3 计仓与占位
- **以损定仓**按 ESL 反推保证金触发时重算**全仓杠杆**可用×缓冲比例BTC/ETH 10x其它 5x
- **占当日开仓意图**已开 + 待触发),未成交不占持仓同币仅 1 条触价监控含回调/突破)。
共享逻辑`trigger_entry_key_monitor_lib.py`轮询`check_trigger_entry_key_monitors`
---
## 五环境与参数`.env` 摘要
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限默认 0.03 | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用默认 3 次 / 5 分钟 |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 六相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
# 关键位监控说明(自动开仓 + 人工盯盘)
**适用:Gate / Binance / OKX 三所实例(共用 `lib/key_monitor/key_auto_order_lib.py`)**
## 环境开关 `KEY_AUTO_ORDER_ENABLED`(默认 `false`)
| 计仓模式 | 开关 | 关键位程序自动单 |
|----------|------|------------------|
| `risk`(以损定仓) | `false` | **全部关闭**(含触价);支撑/阻力微信提醒仍可用 |
| `risk` | `true` | 箱体/收敛/斐波/假突破/触价均可自动(旧行为) |
| `full_margin`(全仓) | `false` | 全部关闭(含触价) |
| `full_margin` | `true` | **仅触价**自动;箱体/斐波等仍禁止 |
**不受本开关影响:** 人工实盘下单,关键支撑/阻力提醒,**顺势加仓**(`risk`),趋势回调(`risk`).全仓模式下策略自动仍禁止.
修改 `.env` 后须 **重启 PM2**.复盘「开仓类型」与统计分段会随开关联动隐藏关键位选项.
---
**适用:`crypto_monitor_gate`(Gate U 本位永续)**
Binance / OKX 见各自目录下同名文档;共享逻辑在 `lib/key_monitor/`.
本文档与 `.env`,`check_key_monitors`,`add_key`,`_key_hard_checks`,`_process_key_rs_level_alert` 一致.
---
## 一,监控类型总览
| 录入类型 | 录入时选方向 | 自动市价开仓 | 触发与结案 |
|----------|--------------|--------------|------------|
| **箱体突破** | **必选** 多/空 | **是**(门控 + RR) | 条件满足 → 开仓或 `rr_insufficient` / `exchange_failed`**一次性删除** |
| **收敛突破** | **必选** 多/空 | **是**(同上) | 同上 |
| **关键阻力位** | **不选**(`direction=watch`) | **否** | 5m 收盘突破上/下沿 → 微信 **3 次**`key_level_alert_done` |
| **关键支撑位** | **不选** | **否** | 同上(与阻力位**相同规则**:填上沿+下沿,程序双向监控) |
| 斐波回调 0.618 / 0.786 | 必选 | 限价挂单逻辑 | 见斐波说明(**不在下文展开**) |
| **回调触价开仓** | **必选** 多/空 | **程序盯价 → 回调触 E 后市价** | 见下文 **§四** |
| **突破触价开仓** | **必选** 多/空 | **程序盯价 → 穿越 E 立即市价** | 见下文 **§四** |
**添加时(箱体/收敛/斐波/触价):** 品种须 **日成交量排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)**;上沿 **>** 下沿(触价开仓填 E/SL/TP,上下沿仅作展示占位).
---
## 二,关键阻力位 / 关键支撑位(人工盯盘)
### 2.1 录入
- 填写 **上沿 `upper`****下沿 `lower`**(程序同时监控两侧,**无法预先判定**做多还是做空).
- 页面 **不显示,不要求** 方向;库中 `direction` 初始为 `watch`,**首次突破后** 写入 `long`(向上突破上沿)`short`(向下突破下沿).
### 2.2 触发(极简)
- 周期:**`KLINE_TIMEFRAME`(默认 5m)最近一根已闭合 K** 的 **收盘价**(非影线).
- **向上突破上沿:** `收盘 > upper` → 推断方向 **多 / 向上**,本次监控任务开始按节奏提醒.
- **向下突破下沿:** `收盘 < lower` → 推断方向 **空 / 向下**,本次任务同样开始提醒.
- **任一侧突破即结束本条监控周期**(不会在突破后再等待另一侧;上沿,下沿谁先满足用谁,同根 K 仅可能满足一侧).
**不参与:** 量能,二确 K,越过幅度下限,日成交排名(运行时),计划 RR,自动开仓.
### 2.3 微信提醒次数
| 配置 | 默认 | 含义 |
|------|------|------|
| `KEY_ALERT_MAX_TIMES` | `3` | 突破后最多推送 3 次 |
| `KEY_ALERT_INTERVAL_MINUTES` | `5` | 相邻两次推送至少间隔 5 分钟 |
- 第 1 次:首次检测到突破的当次轮询(若已闭合 5m 满足条件).
- 第 2,3 次:仅按间隔推送(**不要求**价格仍在箱外).
- 第 3 次推送后:写入 `key_monitor_history`,`close_reason=**key_level_alert_done**`,`key_monitors` **删除**.
### 2.4 与箱体/收敛的区别
| 项目 | 阻力/支撑 | 箱体/收敛 |
|------|-----------|-----------|
| 方向 | 程序推断 | 人工选择 |
| K 线根数 | 1 根闭合 5m | 2 根(突破 K + 确认 K) |
| 提醒次数 | 3 次后结案 | 自动单:触发后 1 次业务推送并结案 |
---
## 三,箱体突破 / 收敛突破(自动开仓)
### 3.1 K 线结构(默认索引)
| 角色 | 环境变量 | 默认 | 含义 |
|------|----------|------|------|
| 突破 K | `KEY_CONFIRM_BREAKOUT_BAR` | `-2` | 倒数第 2 根闭合 K |
| 确认 K | `KEY_CONFIRM_BAR` | `-1` | 倒数第 1 根闭合 K |
### 3.2 硬门控(须全部通过)
1. **有效突破(收盘越界)**
- 多:`突破 K 收盘 > upper`
- 空:`突破 K 收盘 < lower`
2. **突破越过幅度(仅下限)**
- 多:`(突破 K 收盘 upper) / upper × 100 > KEY_BREAKOUT_AMP_MIN_PCT`(默认 **0.03%**)
- 空:`(lower 突破 K 收盘) / lower × 100 >` 同上
- **无上限**;突破过猛由 **计划 RR** 过滤.
- **不再**使用 K 线实体占开盘价比例;`KEY_BREAKOUT_AMP_MAX_PCT` **已不参与门控**.
3. **确认 K 不进箱体**
- 多:确认 K 收盘 **`> upper`**(不得在 `[lower, upper]`)
- 空:确认 K 收盘 **`< lower`**
4. **量能:** 突破 K 成交量 > 前 `KEY_VOLUME_MA_BARS`(默认 20)根均量 × `KEY_VOLUME_RATIO_MIN`(默认 1.3)
5. **日成交量排名:** 运行时仍须前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30)
6. **计划 RR(最后经济门控):** 按确认 K 收盘 **E** 计算 SL/TP 后,`RR` **严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5)才市价开仓
### 3.3 止损 / 止盈(确认 K 收盘为 E)
箱体高 **H = |upper lower|**.止损锚在 **突破 K 极值** 外侧:
| 方向 | 止损(标准/趋势方案) |
|------|------------------------|
| 多 | 突破 K **最低价** × (1 `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
| 空 | 突破 K **最高价** × (1 + `KEY_STOP_OUTSIDE_BREAKOUT_PCT`%) |
止盈方案见下表(与改版前一致):
| 方案 | `sl_tp_mode` | 多:SL / TP | 空:SL / TP |
|------|--------------|-------------|-------------|
| 标准突破 | `standard` | 突破 K 低外侧% / **E+H** | 突破 K 高外侧% / **EH** |
| 箱体 1R·止盈 1.5H | `box_1p5` | **EH** / **E+1.5×H** | **E+H** / **E1.5×H** |
| 趋势单·自填止盈 | `trend_manual` | 突破 K 低 × (1`KEY_TREND_STOP_OUTSIDE_PCT`%) / **录入止盈** | 突破 K 高外侧% / **录入止盈** |
### 3.4 一次性结案(`close_reason`)
| `close_reason` | 含义 |
|----------------|------|
| `box_opposite_break` | 标记价先突破反向边界(多:≤下沿;空:≥上沿) |
| `rr_insufficient` | 门控通过但 RR 不达标或 SL/TP 几何无效 |
| `exchange_failed` | RR 达标但实盘/交易所等原因未开仓 |
| `auto_opened` | RR 达标且市价开仓成功 |
| `key_level_alert_done` | 阻力/支撑 **3 次提醒** 完成 |
---
## 四,回调 / 突破触价开仓(程序触价,无交易所挂单)
### 4.1 录入
- **回调触价开仓**:方向必选多/空;填写 **计划入场价 E**,**止损 SL**,**止盈 TP**(做多须 `SL < E < TP`).
- **突破触价开仓**:同上;添加时当前价须在突破方向一侧(做多:价低于 E;做空:价高于 E).
- 计划 RR 以 **E** 为基准,**严格大于** `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5).
- 可选移动保本,时间平仓;**全仓杠杆模式**下可用.
### 4.2 触发与结案
| 类型 | 触发条件(标记价) |
|------|-------------------|
| **回调触价** | 做多 `≤ E`;做空 `≥ E` → 下一轮询市价开仓 |
| **突破触价** | 做多**向上穿越** E;做空**向下穿越** E → **立即**市价开仓 |
- 未成交前标记价先触 **TP 侧**`trigger_tp_invalidate`.
- **突破触价**另:未穿越 E 先触 **SL 侧**`trigger_sl_invalidate`.
- **24h** 未触发 → `trigger_entry_expired`.
- 成功 → `trigger_entry_filled`;触发后开仓失败 → `trigger_exchange_failed`.
### 4.3 计仓与占位
- **以损定仓**:按 E,SL 反推保证金,触发时重算;**全仓杠杆**:可用×缓冲比例,BTC/ETH 10x,其它 5x.
- **占当日开仓意图**(已开 + 待触发),未成交不占持仓;同币仅 1 条触价监控(含回调/突破).
共享逻辑:`trigger_entry_key_monitor_lib.py`;轮询:`check_trigger_entry_key_monitors`.
---
## 五,环境与参数(`.env` 摘要)
| 变量 | 箱体/收敛 | 阻力/支撑 |
|------|-----------|-----------|
| `KEY_BREAKOUT_AMP_MIN_PCT` | 突破越过下限(默认 0.03) | 不用 |
| `KEY_BREAKOUT_AMP_MAX_PCT` | **已废弃门控** | 不用 |
| `KEY_VOLUME_*` / `KEY_CONFIRM_*` | 用 | 不用 |
| `KEY_AUTO_MIN_PLANNED_RR` | 用 | 不用 |
| `KEY_ALERT_MAX_TIMES` / `KEY_ALERT_INTERVAL_MINUTES` | 不用 | 用(默认 3 次 / 5 分钟) |
| `KEY_DAILY_VOLUME_RANK_MAX` | 添加时 + 运行时 | **仅添加时** |
---
## 六,相关代码
| 说明 | 位置 |
|------|------|
| 共享判定 | `key_monitor_lib.py` |
| 主循环 | `check_key_monitors` |
| 自动门控 | `_key_hard_checks` |
| 阻力/支撑提醒 | `_process_key_rs_level_alert` |
| 录入 | `add_key` |
| 开仓 | `_market_open_for_key_monitor` |
+92 -92
View File
@@ -1,92 +1,92 @@
# 界面与风控更新说明OKX 实例
与 Gate / Binance 主站对齐的列表窗统计分品类交易记录展示复盘与移动保本交易所同步OKX 仍为 **三页导航**交易执行 / 记录复盘 / 统计),关键位监控合并在 **交易执行****无** Gate 独立「关键位监控」页与斐波限价监控
## 顶栏导航3 项
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 交易执行 | `/trade` | 关键位监控 + 实盘下单**默认首页** `/``/trade` |
| 2 | 交易记录与复盘 | `/records` | 交易记录复盘表单AI 历史受顶栏 UTC 时间窗筛选 |
| 3 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 列表时间窗UTC全站顶栏
共用模块仓库根目录 `history_window_lib.py`与 Gate / Binance 一致)。
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**`win_preset=utc_today` |
| 可选 | 近 24 小时近 7 天自定义起止UTC |
| 作用范围 | 关键位历史交易记录列表复盘 APIAI 历史 API导出「交易记录」「关键位历史」 |
| 与统计 | **仅影响列表/导出**统计页仍按北京时间 `TRADING_DAY_RESET_HOUR`默认 8:00切日 |
| 切换 | 顶栏「列表筛选UTC」→ 应用保留当前路由 query |
## 交易记录与复盘
- 列表 **止损(开仓)**展示 `initial_stop_loss` 快照`display_open_stop_loss`)。
- 类型列显示 `monitor_type``key_signal_type`若有)。
- 平仓入库`stop_loss` / `initial_stop_loss` 为开仓止损快照机器单 `entry_reason` 可按 `key_signal_type` 自动映射箱体突破 / 收敛突破 → 四条固定关键位开仓类型文案)。
- 复盘开仓类型下拉含四条关键位固定文案 +「其他」离场触发含 **「止盈」**从交易记录填入时按结果与信号预填
- 复盘 K 线图**平仓时间** 为锚点向前约 `ORDER_CHART_LIMIT`默认 100)根(`_fetch_ohlcv_ending_at`)。
- `/api/journals``/api/reviews` 与顶栏 UTC 窗一致
### 导出交易记录 v3
- 文件名`trade_records_v3_YYYYMMDD.csv`
- 含 `key_signal_type``initial_stop_loss`计划/实际 RR`risk_amount`末列「开仓类型」为有效展示文案
- 受 UTC 列表窗限制关键位历史导出同理
## 实盘下单交易执行页
- **移动保本**表单可勾选「启用移动保本」触发阶梯上移后 **先撤后挂** 交易所 TP/SL`replace_active_monitor_tpsl_on_exchange`),仅成功后才写库企业微信提示含「交易所已先撤后挂止盈止损」未配置实盘 API 时仅更新本地止损
- 开仓 TP/SL 仍通过 OKX `attachAlgoOrds`与原有逻辑一致);重挂使用 ccxt `stopLoss` / `takeProfit` 参数触发价经 `_okx_algo_trigger_price_str` 格式化
## 统计分析页`/stats`
| 项 | 说明 |
|----|------|
| 切日 | 北京时间边界 = `TRADING_DAY_RESET_HOUR:00`默认 8 |
| 品类下拉 | 全部交易下单监控关键位箱体突破关键位收敛结构关键位斐波0.618关键位斐波0.786 |
| URL | `stats_segment=``all` / `manual` / `key_box` / `key_conv` / `key_fib618` / `key_fib786` |
| 与 UTC 窗 | 统计 **不** 随顶栏列表窗变化 |
## 斐波关键位监控与 Gate / Binance 对齐
| 项 | 说明 |
|----|------|
| 类型 | **斐波回调0.618****斐波回调0.786**交易执行页关键位表单 |
| 同币互斥 | 每币仅一条斐波监控 |
| 挂单价 E | 做多 `E = H ratio×(HL)`做空 `E = L + ratio×(HL)`SL/TP 为 L/H |
| 添加后 | 立即在 OKX 挂限价单卡片显示 **挂E**限价单 ID |
| 失效 | 标记价触达止盈侧且限价未成交 → 仅撤本条限价单`cancel_fib_limit_order` |
| 成交后 | 挂交易所 TP/SL → 写入 `order_monitors``monitor_type=关键位监控``key_signal_type=斐波回调…`→ 从关键位表移除 |
| 轮询 | `check_fib_key_monitors()`与箱体/收敛 `check_key_monitors()` 分离 |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`默认 1.5 |
| 日成交量 | 排名前 `KEY_DAILY_VOLUME_RANK_MAX`默认 30 |
计算逻辑见仓库根目录 `fib_key_monitor_lib.py`
## 与 Gate 的差异其余
- 无独立「关键位监控」导航页斐波在 **交易执行** 页添加)。
- 箱体/收敛与 Gate/Binance 相同**门控 + RR 达标后自动市价开仓**`LIVE_TRADING_ENABLED=true`)。
## 交易所已实现盈亏与 Gate 一致
- 打开 **交易执行 / 交易记录** 等主页面时若已配置 `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE`只读即可),同进程约 **25 秒**内最多调用一次 OKX **历史仓位**`fetch_positions_history`),为未写入 `exchange_sync_key` 的记录匹配并回填 `exchange_realized_pnl`
- 复盘列表盈亏优先展示交易所 U旁标 **所**);本地公式估算标 **估**人工复核优先
- 手动强制同步`GET /api/sync_exchange_pnl`需登录)。
- 可选 `.env``EXCHANGE_POSITION_SYNC_FROM_BJ`北京时间起点)、`EXCHANGE_POSITION_HISTORY_LIMIT`默认 200)。
## 企业微信推送与 Gate 对齐
- 平仓`📉 … 平仓完成` 模板盈亏 ±X.XX U价位两位/按币价精度账户资金 2 位小数)。
- 开仓成功与 Gate 相同的 emoji 分段条件委托状态文案RR/张数/名义 2 位小数)。
- 移动保本仅首次触发推送交易所同步失败同一监控单只告警一次
- 斐波/关键位/划转等推送数值格式与 Gate 一致`format_wechat_scalar_2dp`)。
## 配置与部署
- 详见 `.env.example` 中 OKX`OKX_*`与通用风控项
- 代码更新后请 **重启 OKX 监控进程**旧库行不做批量回填展示字段有则用之无则回退
# 界面与风控更新说明(OKX 实例)
与 Gate / Binance 主站对齐的列表窗,统计分品类,交易记录展示,复盘与移动保本交易所同步;OKX 仍为 **三页导航**(交易执行 / 记录复盘 / 统计),关键位监控合并在 **交易执行**,**无** Gate 独立「关键位监控」页与斐波限价监控.
## 顶栏导航(3 项)
| 顺序 | 名称 | 路由 | 说明 |
|------|------|------|------|
| 1 | 交易执行 | `/trade` | 关键位监控 + 实盘下单(**默认首页** `/``/trade`) |
| 2 | 交易记录与复盘 | `/records` | 交易记录,复盘表单,AI 历史(受顶栏 UTC 时间窗筛选) |
| 3 | 统计分析 | `/stats` | 按北京时间交易日切日 + 分品类统计块 |
## 列表时间窗(UTC,全站顶栏)
共用模块:仓库根目录 `history_window_lib.py`(与 Gate / Binance 一致).
| 项 | 说明 |
|----|------|
| 默认 | **UTC 当日**(`win_preset=utc_today`) |
| 可选 | 近 24 小时,近 7 天,自定义起止(UTC) |
| 作用范围 | 关键位历史,交易记录列表,复盘 API,AI 历史 API,导出「交易记录」「关键位历史」 |
| 与统计 | **仅影响列表/导出**;统计页仍按北京时间 `TRADING_DAY_RESET_HOUR`(默认 8:00)切日 |
| 切换 | 顶栏「列表筛选(UTC)」→ 应用(保留当前路由 query) |
## 交易记录与复盘
- 列表 **止损(开仓)**:展示 `initial_stop_loss` 快照(`display_open_stop_loss`).
- 类型列显示 `monitor_type``key_signal_type`(若有).
- 平仓入库:`stop_loss` / `initial_stop_loss` 为开仓止损快照;机器单 `entry_reason` 可按 `key_signal_type` 自动映射(箱体突破 / 收敛突破 → 四条固定关键位开仓类型文案).
- 复盘:开仓类型下拉含四条关键位固定文案 +「其他」;离场触发含 **「止盈」**;从交易记录填入时按结果与信号预填.
- 复盘 K 线图:**平仓时间** 为锚点向前约 `ORDER_CHART_LIMIT`(默认 100)根(`_fetch_ohlcv_ending_at`).
- `/api/journals`,`/api/reviews` 与顶栏 UTC 窗一致.
### 导出(交易记录 v3)
- 文件名:`trade_records_v3_YYYYMMDD.csv`
- 含 `key_signal_type`,`initial_stop_loss`,计划/实际 RR,`risk_amount`;末列「开仓类型」为有效展示文案.
- 受 UTC 列表窗限制;关键位历史导出同理.
## 实盘下单(交易执行页)
- **移动保本**:表单可勾选「启用移动保本」;触发阶梯上移后 **先撤后挂** 交易所 TP/SL(`replace_active_monitor_tpsl_on_exchange`),仅成功后才写库;企业微信提示含「交易所:已先撤后挂止盈止损」.未配置实盘 API 时仅更新本地止损.
- 开仓 TP/SL 仍通过 OKX `attachAlgoOrds`(与原有逻辑一致);重挂使用 ccxt `stopLoss` / `takeProfit` 参数,触发价经 `_okx_algo_trigger_price_str` 格式化.
## 统计分析页(`/stats`)
| 项 | 说明 |
|----|------|
| 切日 | 北京时间;边界 = `TRADING_DAY_RESET_HOUR:00`(默认 8) |
| 品类下拉 | 全部交易,下单监控,关键位箱体突破,关键位收敛结构,关键位斐波0.618,关键位斐波0.786 |
| URL | `stats_segment=`(`all` / `manual` / `key_box` / `key_conv` / `key_fib618` / `key_fib786`) |
| 与 UTC 窗 | 统计 **不** 随顶栏列表窗变化 |
## 斐波关键位监控(与 Gate / Binance 对齐)
| 项 | 说明 |
|----|------|
| 类型 | **斐波回调0.618**,**斐波回调0.786**(交易执行页关键位表单) |
| 同币互斥 | 每币仅一条斐波监控 |
| 挂单价 E | 做多 `E = H ratio×(HL)`;做空 `E = L + ratio×(HL)`;SL/TP 为 L/H |
| 添加后 | 立即在 OKX 挂限价单;卡片显示 **挂E**,限价单 ID |
| 失效 | 标记价触达止盈侧且限价未成交 → 仅撤本条限价单(`cancel_fib_limit_order`) |
| 成交后 | 挂交易所 TP/SL → 写入 `order_monitors`(`monitor_type=关键位监控`,`key_signal_type=斐波回调…`)→ 从关键位表移除 |
| 轮询 | `check_fib_key_monitors()`(与箱体/收敛 `check_key_monitors()` 分离) |
| 盈亏比 | 计划 RR 须 > `KEY_AUTO_MIN_PLANNED_RR`(默认 1.5) |
| 日成交量 | 排名前 `KEY_DAILY_VOLUME_RANK_MAX`(默认 30) |
计算逻辑见仓库根目录 `fib_key_monitor_lib.py`.
## 与 Gate 的差异(其余)
- 无独立「关键位监控」导航页(斐波在 **交易执行** 页添加).
- 箱体/收敛与 Gate/Binance 相同:**门控 + RR 达标后自动市价开仓**(`LIVE_TRADING_ENABLED=true`).
## 交易所已实现盈亏(与 Gate 一致)
- 打开 **交易执行 / 交易记录** 等主页面时,若已配置 `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE`(只读即可),同进程约 **25 秒**内最多调用一次 OKX **历史仓位**(`fetch_positions_history`),为未写入 `exchange_sync_key` 的记录匹配并回填 `exchange_realized_pnl`.
- 复盘列表盈亏优先展示交易所 U(旁标 **所**);本地公式估算标 **估**;人工复核优先.
- 手动强制同步:`GET /api/sync_exchange_pnl`(需登录).
- 可选 `.env`:`EXCHANGE_POSITION_SYNC_FROM_BJ`(北京时间起点),`EXCHANGE_POSITION_HISTORY_LIMIT`(默认 200).
## 企业微信推送(与 Gate 对齐)
- 平仓:`📉 … 平仓完成` 模板(盈亏 ±X.XX U,价位两位/按币价精度,账户资金 2 位小数).
- 开仓成功:与 Gate 相同的 emoji 分段(条件委托状态文案,RR/张数/名义 2 位小数).
- 移动保本:仅首次触发推送;交易所同步失败同一监控单只告警一次.
- 斐波/关键位/划转等推送数值格式与 Gate 一致(`format_wechat_scalar_2dp`).
## 配置与部署
- 详见 `.env.example` 中 OKX(`OKX_*`)与通用风控项.
- 代码更新后请 **重启 OKX 监控进程**;旧库行不做批量回填,展示字段有则用之,无则回退.
+367 -367
View File
@@ -1,367 +1,367 @@
# `crypto_monitor_okx` 部署文档Ubuntu
**功能与页面操作** 见同目录 **[使用说明.md](./使用说明.md)**Ubuntu 环境Python / Node / PM2**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**策略与 AI 见 **[策略交易说明.md](../策略交易说明.md)****[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**
---
# 本地部署 + SSH SOCKS 转发 + PM2 启动指南Ubuntu
本文面向**本地 Ubuntu 机器运行项目****本机直连 OKX 会被 TLS/SNI reset** 的场景解决思路是
- 本机启动 `ssh -D` 动态转发把 **SOCKS5 出口**放到你可正常访问 OKX 的 VPS 上
- 项目通过环境变量 `OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080``ccxt` 走 SOCKS
- **SSH 隧道**用 `ssh -D` 常驻可用 tmux / autossh);**Flask 应用** 仅用 **PM2** 托管见 [docs/ubuntu-server.md](../docs/ubuntu-server.md)
> 安全提醒不要把 `.env`私钥 `.pem`OKX API Key 提交到 Git文档里只用占位符
---
## 0. 你需要准备的东西
- 一台 **Ubuntu** 本地机器下文称“本机”
- 一台可 SSH 登录**能正常访问 OKX** 的 VPS示例公网 IP`47.76.87.111`用户`root`
- VPS 登录方式**SSH 私钥**推荐或密码不推荐用于无人值守
- 本机已安装
- `python3``python3-venv``pip``python3-pip`
- `git`可选
- `curl``ssh`
- `node` + `npm`用于安装 `pm2`
---
## 1. 从云服务器把项目同步到本地推荐打包下载
在云服务器项目目录包含 `app.py` 的目录执行
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
# 可选清理 Python 缓存减少小文件传输
find . -type d -name __pycache__ -prune -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
tar -czf crypto_monitor.tgz .
```
下载 `crypto_monitor.tgz` 到本机后解压
```bash
mkdir -p /opt/crypto_monitor/crypto_monitor_okx
cd /opt/crypto_monitor
tar -xzf crypto_monitor.tgz -C crypto_monitor_okx
cd crypto_monitor_okx
cp -n .env.example .env # 若尚无 .env
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`推荐
把私钥放到本机示例`~/.ssh/vps1.pem`),并设置权限
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
mv ~/Downloads/vps1.pem ~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`没有就创建),添加
```sshconfig
Host okx-vps
HostName 47.76.87.111
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试
```bash
ssh okx-vps true
```
> 如果你还没完全切到密钥登录还会交互要密码),先把 `BatchMode yes` 注释掉等密钥登录稳定后再打开
---
## 3. 先手工验证SSH SOCKS + OKX API
### 3.1 开一个本地 SOCKS1080
```bash
ssh -N -D 127.0.0.1:1080 okx-vps
```
保持该进程运行另开终端继续下面步骤)。
### 3.2 验证 OKX 走 SOCKS 可用
```bash
curl -4 -Iv --max-time 15 --proxy socks5h://127.0.0.1:1080 https://www.okx.com/api/v5/public/time
```
看到 `HTTP/2 200`或至少 TLS 握手成功且返回 JSON即 OK
---
## 4. Python 虚拟环境venv
在本机项目目录
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
> 说明本仓库当前没有 `requirements.txt`如果你希望“完全复刻云服务器依赖”可以在云服务器项目环境里执行 `pip freeze > requirements.txt` 带回本机再 `pip install -r requirements.txt`记得删掉明显无关/体积巨大的包)。
建议减少 `.pyc` 垃圾文件可选):
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量`.env.example``.env`
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
cp -n .env.example .env # 已存在 .env 时不覆盖
nano .env
```
### 5.2 备份与 `git pull`
- **`.env` 不在 Git 中**`git pull` **不会**覆盖本地 `.env`
- 远端若更新 **`.env.example`**pull 后请**手动**把新增变量补进你的 `.env`
- **升级前备份**`cp .env .env.backup.$(date +%Y%m%d)`恢复`cp .env.backup.YYYYMMDD .env`
- **换机**`scp` 复制 `.env`或新机 `cp .env.example .env` 后重填
**AI 复盘**三所共用根目录 **`ai_client.py`**默认 **`AI_PROVIDER=openai`**网关 `https://op.bz121.com/v1`模型 `gemma4:e4b`或改 **`ollama`** 走本机 OllamaPM2 须 **`PYTHONPATH=..`**详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**
### 5.3 必填项检查OKX + 代理
至少确认/填写这些关键项示例):
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# OKX如需实盘
LIVE_TRADING_ENABLED=false
OKX_API_KEY=...
OKX_API_SECRET=...
OKX_API_PASSPHRASE=...
# OKX 出口走本机 SSH 动态转发 SOCKS
OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 开仓多周期K线图可选
# ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts
# DAILY_OPEN_ALERT_THRESHOLD=5
# DAILY_OPEN_HARD_LIMIT=0
# 说明见仓库 docs/daily-open-limit.md
# AI 复盘默认 OpenAI 兼容网关与 Ollama 二选一
AI_PROVIDER=openai
AI_TIMEOUT_SECONDS=120
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
# 本机 Ollama仅 AI_PROVIDER=ollama
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=你的模型名
```
> 完整说明见仓库根 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**`OPENAI_API_KEY` 在 [op.bz121.com](https://op.bz121.com/) 的 `gateway.json` 获取
> `OKX_SOCKS_PROXY` 使用 `socks5h`让 SOCKS 侧做域名解析更贴近你 `curl --proxy socks5h://...` 的成功路径)。
---
## 6. 本机手工启动验证 Flask
确保
1. SOCKS 隧道已运行127.0.0.1:1080
2. 虚拟环境已 `activate`
3. `.env` 已配置
启动
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
source .venv/bin/activate
python app.py
```
浏览器访问`http://127.0.0.1:5000`或你在 `.env` 配的端口)。
---
## 7. 安装 PM2Node
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 8. 用 PM2 启动 SSH SOCKS 隧道推荐密钥免交互
### 8.1 启动隧道进程
```bash
pm2 start "ssh" --name okx-socks-tunnel -- \
-N -D 127.0.0.1:1080 okx-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes -o BatchMode=yes
```
查看日志
```bash
pm2 logs okx-socks-tunnel --lines 200
```
### 8.2 仍然验证 OKX
```bash
curl -4 -Iv --max-time 15 --proxy socks5h://127.0.0.1:1080 https://www.okx.com/api/v5/public/time
```
### 8.3 开机自启可选
```bash
pm2 save
pm2 startup
```
---
## 9. 用 PM2 启动 Flask`app.py`
`pm2` 管理 Python 的常用方式是直接启动解释器
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
pm2 start /opt/crypto_monitor/crypto_monitor_okx/.venv/bin/python --name crypto-monitor -- \
/opt/crypto_monitor/crypto_monitor_okx/app.py
```
> 若项目目录与上文不一致请替换为实际绝对路径或用 `readlink -f app.py` 得到绝对路径
查看日志
```bash
pm2 logs crypto-monitor --lines 200
```
保存进程列表
```bash
pm2 save
```
---
## 10. 常见问题排查高频
### 10.1 OKX 仍然失败先看隧道是否在
```bash
ss -lntp | grep 1080 || true
pm2 status
```
### 10.2 `pm2` 里的 `ssh` 立刻退出
常见原因
- 私钥权限不对`chmod 600`
- `~/.ssh/config` 写错 `HostName/User/IdentityFile`
- 开了 `BatchMode yes` 但仍需要密码会失败
### 10.3 `ccxt` SOCKS 报错 / 代理不生效
本机 Python 依赖通常需要
```bash
source .venv/bin/activate
pip install PySocks
```
### 10.4 `.pyc` 很多导致同步慢
`.pyc` 是缓存删除不影响功能
```bash
find . -type d -name __pycache__ -prune -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
```
---
## 11. 推荐的启动顺序固定习惯
1. `pm2` 启动 `okx-socks-tunnel`
2. `curl --proxy socks5h://127.0.0.1:1080 ...` 验证 OKX
3. `pm2` 启动 `crypto-monitor`
---
## 12. 免责声明
交易所有合规与地区政策要求请确保你的使用方式符合当地法律法规与交易所条款本文仅描述网络与工程部署技术路径
写好了脚本路径
- `scripts/fix_breakeven_labels.py`
你在 Ubuntu 上这样用
1) 先预览不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2) 确认后执行
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件就是你要的
- `monitor_type='下单监控'`
- `result='止损'`
- `pnl_amount > 0`
- 改成 `result='保本止盈'`
如果你想我还可以再给你一条“先自动备份 DB 再执行”的一键命令
# `crypto_monitor_okx` 部署文档(Ubuntu)
**功能与页面操作** 见同目录 **[使用说明.md](./使用说明.md)**.Ubuntu 环境(Python / Node / PM2)**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.策略与 AI 见 **[策略交易说明.md](../策略交易说明.md)**,**[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**.
---
# 本地部署 + SSH SOCKS 转发 + PM2 启动指南(Ubuntu)
本文面向:**本地 Ubuntu 机器运行项目**,**本机直连 OKX 会被 TLS/SNI reset** 的场景.解决思路是:
- 本机启动 `ssh -D` 动态转发,把 **SOCKS5 出口**放到你可正常访问 OKX 的 VPS 上
- 项目通过环境变量 `OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080``ccxt` 走 SOCKS
- **SSH 隧道**用 `ssh -D` 常驻(可用 tmux / autossh);**Flask 应用** 仅用 **PM2** 托管(见 [docs/ubuntu-server.md](../docs/ubuntu-server.md))
> 安全提醒:不要把 `.env`,私钥 `.pem`,OKX API Key 提交到 Git;文档里只用占位符.
---
## 0. 你需要准备的东西
- 一台 **Ubuntu** 本地机器(下文称“本机”)
- 一台可 SSH 登录,**能正常访问 OKX** 的 VPS(示例公网 IP:`47.76.87.111`,用户:`root`)
- VPS 登录方式:**SSH 私钥**(推荐)或密码(不推荐用于无人值守)
- 本机已安装:
- `python3`,`python3-venv`,`pip`(`python3-pip`)
- `git`(可选)
- `curl`,`ssh`
- `node` + `npm`(用于安装 `pm2`)
---
## 1. 从云服务器把项目同步到本地(推荐:打包下载)
在云服务器项目目录(包含 `app.py` 的目录)执行:
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
# 可选:清理 Python 缓存,减少小文件传输
find . -type d -name __pycache__ -prune -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
tar -czf crypto_monitor.tgz .
```
下载 `crypto_monitor.tgz` 到本机后解压:
```bash
mkdir -p /opt/crypto_monitor/crypto_monitor_okx
cd /opt/crypto_monitor
tar -xzf crypto_monitor.tgz -C crypto_monitor_okx
cd crypto_monitor_okx
cp -n .env.example .env # 若尚无 .env
```
---
## 2. 配置 SSH 私钥与 `~/.ssh/config`(推荐)
把私钥放到本机(示例:`~/.ssh/vps1.pem`),并设置权限:
```bash
mkdir -p ~/.ssh
chmod 700 ~/.ssh
mv ~/Downloads/vps1.pem ~/.ssh/vps1.pem
chmod 600 ~/.ssh/vps1.pem
```
编辑 `~/.ssh/config`(没有就创建),添加:
```sshconfig
Host okx-vps
HostName 47.76.87.111
User root
IdentityFile ~/.ssh/vps1.pem
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
BatchMode yes
```
测试:
```bash
ssh okx-vps true
```
> 如果你还没完全切到密钥登录(还会交互要密码),先把 `BatchMode yes` 注释掉,等密钥登录稳定后再打开.
---
## 3. 先手工验证:SSH SOCKS + OKX API
### 3.1 开一个本地 SOCKS(1080)
```bash
ssh -N -D 127.0.0.1:1080 okx-vps
```
保持该进程运行(另开终端继续下面步骤).
### 3.2 验证 OKX 走 SOCKS 可用
```bash
curl -4 -Iv --max-time 15 --proxy socks5h://127.0.0.1:1080 https://www.okx.com/api/v5/public/time
```
看到 `HTTP/2 200`(或至少 TLS 握手成功且返回 JSON)即 OK.
---
## 4. Python 虚拟环境(venv)
在本机项目目录:
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
pip install flask requests ccxt werkzeug PySocks Pillow
```
> 说明:本仓库当前没有 `requirements.txt`.如果你希望“完全复刻云服务器依赖”,可以在云服务器项目环境里执行 `pip freeze > requirements.txt` 带回本机再 `pip install -r requirements.txt`(记得删掉明显无关/体积巨大的包).
建议减少 `.pyc` 垃圾文件(可选):
```bash
export PYTHONDONTWRITEBYTECODE=1
```
---
## 5. 配置环境变量(`.env.example``.env`)
| 文件 | 是否进 Git | 说明 |
|------|------------|------|
| **`.env.example`** | ✅ 是 | 变量模板与注释,可随 `git pull` 更新 |
| **`.env`** | ❌ 否 | 本机真实配置;`app.py` **只读此文件** |
### 5.1 首次配置
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
cp -n .env.example .env # 已存在 .env 时不覆盖
nano .env
```
### 5.2 备份与 `git pull`
- **`.env` 不在 Git 中**:`git pull` **不会**覆盖本地 `.env`.
- 远端若更新 **`.env.example`**,pull 后请**手动**把新增变量补进你的 `.env`.
- **升级前备份**:`cp .env .env.backup.$(date +%Y%m%d)`;恢复:`cp .env.backup.YYYYMMDD .env`.
- **换机**:`scp` 复制 `.env`,或新机 `cp .env.example .env` 后重填.
**AI 复盘**:三所共用根目录 **`ai_client.py`**.默认 **`AI_PROVIDER=openai`**,网关 `https://op.bz121.com/v1`,模型 `gemma4:e4b`;或改 **`ollama`** 走本机 Ollama.PM2 须 **`PYTHONPATH=..`**.详见 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**.
### 5.3 必填项检查(OKX + 代理)
至少确认/填写这些关键项(示例):
```env
APP_HOST=127.0.0.1
APP_PORT=5000
# OKX(如需实盘)
LIVE_TRADING_ENABLED=false
OKX_API_KEY=...
OKX_API_SECRET=...
OKX_API_PASSPHRASE=...
# OKX 出口:走本机 SSH 动态转发 SOCKS
OKX_SOCKS_PROXY=socks5h://127.0.0.1:1080
# 开仓多周期K线图(可选)
# ORDER_CHART_ENABLED=true
# ORDER_CHART_TFS=4h,1h,15m,5m
# ORDER_CHART_LIMIT=100
# ORDER_CHART_DIR=static/images/order_charts
# DAILY_OPEN_ALERT_THRESHOLD=5
# DAILY_OPEN_HARD_LIMIT=0
# 说明见仓库 docs/daily-open-limit.md
# AI 复盘(默认 OpenAI 兼容网关;与 Ollama 二选一)
AI_PROVIDER=openai
AI_TIMEOUT_SECONDS=120
OPENAI_API_BASE=https://op.bz121.com/v1
OPENAI_API_KEY=你的密钥
OPENAI_MODEL=gemma4:e4b
# 本机 Ollama(仅 AI_PROVIDER=ollama)
OLLAMA_API=http://127.0.0.1:11434/api/generate
AI_MODEL=你的模型名
```
> 完整说明见仓库根 **[AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)**.`OPENAI_API_KEY` 在 [op.bz121.com](https://op.bz121.com/) 的 `gateway.json` 获取.
> `OKX_SOCKS_PROXY` 使用 `socks5h`:让 SOCKS 侧做域名解析(更贴近你 `curl --proxy socks5h://...` 的成功路径).
---
## 6. 本机手工启动(验证 Flask)
确保:
1. SOCKS 隧道已运行(127.0.0.1:1080)
2. 虚拟环境已 `activate`
3. `.env` 已配置
启动:
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
source .venv/bin/activate
python app.py
```
浏览器访问:`http://127.0.0.1:5000`(或你在 `.env` 配的端口).
---
## 7. 安装 PM2(Node)
```bash
sudo npm i -g pm2
pm2 -v
```
---
## 8. 用 PM2 启动 SSH SOCKS 隧道(推荐:密钥免交互)
### 8.1 启动隧道进程
```bash
pm2 start "ssh" --name okx-socks-tunnel -- \
-N -D 127.0.0.1:1080 okx-vps \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 \
-o ExitOnForwardFailure=yes -o BatchMode=yes
```
查看日志:
```bash
pm2 logs okx-socks-tunnel --lines 200
```
### 8.2 仍然验证 OKX
```bash
curl -4 -Iv --max-time 15 --proxy socks5h://127.0.0.1:1080 https://www.okx.com/api/v5/public/time
```
### 8.3 开机自启(可选)
```bash
pm2 save
pm2 startup
```
---
## 9. 用 PM2 启动 Flask(`app.py`)
`pm2` 管理 Python 的常用方式是直接启动解释器:
```bash
cd /opt/crypto_monitor/crypto_monitor_okx
pm2 start /opt/crypto_monitor/crypto_monitor_okx/.venv/bin/python --name crypto-monitor -- \
/opt/crypto_monitor/crypto_monitor_okx/app.py
```
> 若项目目录与上文不一致,请替换为实际绝对路径;或用 `readlink -f app.py` 得到绝对路径.
查看日志:
```bash
pm2 logs crypto-monitor --lines 200
```
保存进程列表:
```bash
pm2 save
```
---
## 10. 常见问题排查(高频)
### 10.1 OKX 仍然失败:先看隧道是否在
```bash
ss -lntp | grep 1080 || true
pm2 status
```
### 10.2 `pm2` 里的 `ssh` 立刻退出
常见原因:
- 私钥权限不对(`chmod 600`)
- `~/.ssh/config` 写错 `HostName/User/IdentityFile`
- 开了 `BatchMode yes` 但仍需要密码(会失败)
### 10.3 `ccxt` SOCKS 报错 / 代理不生效
本机 Python 依赖通常需要:
```bash
source .venv/bin/activate
pip install PySocks
```
### 10.4 `.pyc` 很多导致同步慢
`.pyc` 是缓存,删除不影响功能:
```bash
find . -type d -name __pycache__ -prune -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
```
---
## 11. 推荐的启动顺序(固定习惯)
1. `pm2` 启动 `okx-socks-tunnel`
2. `curl --proxy socks5h://127.0.0.1:1080 ...` 验证 OKX
3. `pm2` 启动 `crypto-monitor`
---
## 12. 免责声明
交易所有合规与地区政策要求.请确保你的使用方式符合当地法律法规与交易所条款.本文仅描述网络与工程部署技术路径.
写好了,脚本路径:
- `scripts/fix_breakeven_labels.py`
你在 Ubuntu 上这样用:
1) 先预览(不写库):
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --dry-run
```
2) 确认后执行:
```bash
python scripts/fix_breakeven_labels.py --db ./crypto.db --apply
```
默认修复条件就是你要的:
- `monitor_type='下单监控'`
- `result='止损'`
- `pnl_amount > 0`
- 改成 `result='保本止盈'`
如果你想,我还可以再给你一条“先自动备份 DB 再执行”的一键命令.
+16 -16
View File
@@ -1,15 +1,15 @@
# 环境一键部署Ubuntu / root /opt
# 环境一键部署(Ubuntu / root /opt)
**`/opt/crypto_monitor`** 下以 **root** 为各子项目创建 Python **`.venv`**安装依赖`.env.example` 生成 `.env`不覆盖已有),并可选安装 **PM2**
**`/opt/crypto_monitor`** 下以 **root** 为各子项目创建 Python **`.venv`**,安装依赖,`.env.example` 生成 `.env`(不覆盖已有),并可选安装 **PM2**.
完整系统要求Python / Node / PM2 版本启动顺序**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**
完整系统要求(Python / Node / PM2 版本,启动顺序)**[docs/ubuntu-server.md](../docs/ubuntu-server.md)**.
---
## 前置条件
- **Ubuntu 22.04 / 24.04**用户 **root**
- 已安装 **git**仓库位于 **`/opt/crypto_monitor`**
- **Ubuntu 22.04 / 24.04**,用户 **root**
- 已安装 **git**,仓库位于 **`/opt/crypto_monitor`**
```bash
apt update
@@ -26,7 +26,7 @@ cd /opt/crypto_monitor
bash deploy/setup_env.sh --install-system-deps
```
常用参数
常用参数:
```bash
bash deploy/setup_env.sh --only binance,gate # 仅部分子项目
@@ -35,9 +35,9 @@ bash deploy/setup_env.sh --skip-pm2 # 不尝试安装 pm2
bash deploy/setup_env.sh --skip-env-copy # 不复制 .env.example
```
**整目录重装**保留 `.env`清库去脏 PM2**[reinstall-plan-b.md](./reinstall-plan-b.md)**执行 `bash deploy/reinstall.sh``setup_env.sh` 独立不影响首次一键安装
**整目录重装**(保留 `.env`,清库,去脏 PM2)**[reinstall-plan-b.md](./reinstall-plan-b.md)**,执行 `bash deploy/reinstall.sh`.`setup_env.sh` 独立,不影响首次一键安装.
若在其它环境编辑过脚本后报 `pipefail` 错误先转 LF
若在其它环境编辑过脚本后报 `pipefail` 错误,先转 LF:
```bash
sed -i 's/\r$//' deploy/setup_env.sh
@@ -53,16 +53,16 @@ sed -i 's/\r$//' deploy/setup_env.sh
| `crypto_monitor_*` | 各目录 `.venv` + `pip install -r ../requirements.txt` |
| `manual_trading_hub` | 独立 `requirements.txt` |
| `.env` | 不存在则从 `.env.example` 复制 |
| 部署密钥 | `python3 scripts/bootstrap_deploy_secrets.py`不覆盖已有值 |
| 目录 | `static/images``static/images/order_charts` |
| 部署密钥 | `python3 scripts/bootstrap_deploy_secrets.py`(不覆盖已有值) |
| 目录 | `static/images`,`static/images/order_charts` |
| PM2 | 已装 Node 时 `npm install -g pm2` |
---
## 部署之后
1. 编辑各子目录 **`.env`**交易所 API企业微信等**AI 请在中控系统设置 → AI 配置**)。
2. **仅用 PM2 常驻**见 [docs/ubuntu-server.md](../docs/ubuntu-server.md) §3):
1. 编辑各子目录 **`.env`**(交易所 API,企业微信等;**AI 请在中控系统设置 → AI 配置**).
2. **仅用 PM2 常驻**(见 [docs/ubuntu-server.md](../docs/ubuntu-server.md) §3):
```bash
cd /opt/crypto_monitor/crypto_monitor_binance && pm2 start ecosystem.config.cjs
@@ -71,13 +71,13 @@ sed -i 's/\r$//' deploy/setup_env.sh
pm2 save
```
或一条命令`bash deploy/pm2_start_all.sh`
或一条命令:`bash deploy/pm2_start_all.sh`
3. 三所 `.env` 同步脚本见 **[docs/env-sync-scripts.md](../docs/env-sync-scripts.md)**
3. 三所 `.env` 同步脚本见 **[docs/env-sync-scripts.md](../docs/env-sync-scripts.md)**.
---
## 依赖说明
- 三个监控子项目共用根目录 **[requirements.txt](../requirements.txt)**
- 走 SOCKS 须 **PySocks**已包含在 requirements 中)。
- 三个监控子项目共用根目录 **[requirements.txt](../requirements.txt)**.
- 走 SOCKS 须 **PySocks**(已包含在 requirements 中).
+5 -5
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env bash
# 按推荐顺序启动三所 Flask + 中控 hub/三 agentPM2)。
# 用法仓库根或任意目录:
# 按推荐顺序启动三所 Flask + 中控 hub/三 agent(PM2).
# 用法(仓库根或任意目录):
# bash deploy/pm2_start_all.sh
#
# 与 deploy/setup_env.sh 独立setup_env 只建 venv本脚本负责 PM2 启动
# 与 deploy/setup_env.sh 独立:setup_env 只建 venv;本脚本负责 PM2 启动.
set -e
set -u
if [ -n "${BASH_VERSION:-}" ]; then
@@ -26,7 +26,7 @@ start_one() {
}
if ! command -v pm2 >/dev/null 2>&1; then
echo "未找到 pm2请先安装 Node.js 与 pm2见 docs/ubuntu-server.md" >&2
echo "未找到 pm2,请先安装 Node.js 与 pm2(见 docs/ubuntu-server.md)" >&2
exit 1
fi
@@ -37,5 +37,5 @@ start_one manual_trading_hub
pm2 save 2>/dev/null || true
echo ""
echo "PM2 进程"
echo "PM2 进程:"
pm2 list
+2 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# 服务器上拉代码同步 env应用强制清仓策略并重启 PM2
# 用法/opt/crypto_monitor 下 root):
# 服务器上拉代码,同步 env,应用强制清仓策略并重启 PM2.
# 用法(/opt/crypto_monitor 下 root):
# bash deploy/pull_and_restart.sh
# bash deploy/pull_and_restart.sh --dry-run
set -euo pipefail
+19 -19
View File
@@ -1,32 +1,32 @@
# Plan B整目录重装生产清库
# Plan B:整目录重装(生产清库)
适用于**保留三所 `.env` 与中控配置丢弃旧代码旧 SQLite脏 PM2 名单**例如移除 `gate_bot` 后偶发重启)。
适用于:**保留三所 `.env` 与中控配置,丢弃旧代码,旧 SQLite,脏 PM2 名单**(例如移除 `gate_bot` 后偶发重启).
**[setup_env.sh](./setup_env.sh)** 的关系
**[setup_env.sh](./setup_env.sh)** 的关系:
| 脚本 | 用途 |
|------|------|
| `setup_env.sh` | **首次安装 / 日常**建 venv装依赖`.env.example` 复制**不变** |
| `reinstall.sh` | **整目录重装**备份 → 移走旧目录 → `git clone` → 调 `setup_env.sh` → 恢复配置 → PM2 |
| `setup_env.sh` | **首次安装 / 日常**:建 venv,装依赖,`.env.example` 复制(**不变**) |
| `reinstall.sh` | **整目录重装**:备份 → 移走旧目录 → `git clone` → 调 `setup_env.sh` → 恢复配置 → PM2 |
---
## 一键执行推荐
## 一键执行(推荐)
在现有服务器安装上以 **root** 执行
在现有服务器安装上以 **root** 执行:
```bash
cd /opt/crypto_monitor
bash deploy/reinstall.sh --yes
```
交互确认不加 `--yes`):
交互确认(不加 `--yes`):
```bash
bash deploy/reinstall.sh
```
仅预览步骤
仅预览步骤:
```bash
bash deploy/reinstall.sh --dry-run
@@ -39,20 +39,20 @@ bash deploy/reinstall.sh --dry-run
1. 备份到 **`/root/backups/pre-reinstall-YYYYMMDD-HHMMSS/`**
- 三所 `crypto_monitor_*/.env`
- `manual_trading_hub/.env`
- `manual_trading_hub/hub_settings.json`若有
- 可选仓库内 `one_shot` 备份目录
- `manual_trading_hub/hub_settings.json`(若有)
- 可选:仓库内 `one_shot` 备份目录
2. **`pm2 stop all` + `pm2 delete all`**
3. **`mv /opt/crypto_monitor /opt/crypto_monitor.old.时间戳`**
4. **`git clone`** 到 `/opt/crypto_monitor`默认 `main`
4. **`git clone`** 到 `/opt/crypto_monitor`(默认 `main`)
5. **`bash deploy/setup_env.sh --skip-env-copy --recreate-venv --skip-pm2`**
6. 从备份 **恢复 `.env` / `hub_settings.json`**
7. **`deploy/sanitize_hub_settings.py`** 去掉 `gate_bot` / 第四账户
8. **`deploy/pm2_start_all.sh`** + `pm2 save`
9. 为三所重装 **每日 0 点备份 cron**可用 `--no-backup-cron` 跳过
9. 为三所重装 **每日 0 点备份 cron**(可用 `--no-backup-cron` 跳过)
**不会备份/恢复**`crypto.db`hub `data/*.db``static/images`符合「全新启动」)。
**不会备份/恢复**:`crypto.db`,hub `data/*.db`,`static/images`(符合「全新启动」).
**不会动**宝塔/Nginx 反代SSH SOCKS 隧道tmux 内)。
**不会动**:宝塔/Nginx 反代,SSH SOCKS 隧道(tmux 内).
---
@@ -77,13 +77,13 @@ pm2 list
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5100/
```
浏览器中控 `/monitor` 登录三所 LINK 绿监控区为空库
浏览器:中控 `/monitor` 登录,三所 LINK 绿,监控区为空库.
---
## 回滚
旧目录默认保留为 `/opt/crypto_monitor.old.时间戳`配置在 `/root/backups/pre-reinstall-*`
旧目录默认保留为 `/opt/crypto_monitor.old.时间戳`,配置在 `/root/backups/pre-reinstall-*`:
```bash
pm2 delete all
@@ -92,7 +92,7 @@ mv /opt/crypto_monitor.old.XXXXXXXX /opt/crypto_monitor
bash /opt/crypto_monitor/deploy/pm2_start_all.sh
```
确认新环境稳定后再删 `.old.*` 目录
确认新环境稳定后再删 `.old.*` 目录.
---
@@ -100,7 +100,7 @@ bash /opt/crypto_monitor/deploy/pm2_start_all.sh
| 文件 | 说明 |
|------|------|
| [pm2_start_all.sh](./pm2_start_all.sh) | 按顺序 PM2 启动三所 + hubsetup_env 之后手动用 |
| [pm2_start_all.sh](./pm2_start_all.sh) | 按顺序 PM2 启动三所 + hub(setup_env 之后手动用) |
| [sanitize_hub_settings.py](./sanitize_hub_settings.py) | 清理 `hub_settings.json` 中 gate_bot 条目 |
---
+20 -20
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env bash
# Plan B整目录重装 /opt/crypto_monitor备份 .env → 移走旧目录 → git clone → setup_env → 恢复配置 → PM2
# Plan B:整目录重装 /opt/crypto_monitor(备份 .env → 移走旧目录 → git clone → setup_env → 恢复配置 → PM2)
#
# 与 deploy/setup_env.sh 分工
# setup_env.sh — 首次 / 日常建 venv装依赖复制 .env.example一键安装不变
# reinstall.sh — 生产清库重装保留密钥与 hub 配置丢弃旧代码/旧库/脏 PM2
# 与 deploy/setup_env.sh 分工:
# setup_env.sh — 首次 / 日常:建 venv,装依赖,复制 .env.example(一键安装,不变)
# reinstall.sh — 生产清库重装:保留密钥与 hub 配置,丢弃旧代码/旧库/脏 PM2
#
# 用法在现有安装目录以 root 执行:
# 用法(在现有安装目录以 root 执行):
# cd /opt/crypto_monitor
# bash deploy/reinstall.sh # 交互确认
# bash deploy/reinstall.sh --yes # 跳过确认
@@ -109,7 +109,7 @@ backup_configs() {
fi
done
if [[ "${copied}" -eq 0 ]]; then
echo "错误: 未备份到任何配置文件请检查 ${src_root}" >&2
echo "错误: 未备份到任何配置文件,请检查 ${src_root}" >&2
exit 1
fi
if [[ -f "${src_root}/scripts/one_shot_backup_config_before_cleanup.py" ]]; then
@@ -174,20 +174,20 @@ install_instance_backup_cron() {
}
verify_pm2() {
log "预期 PM2 进程7 个: crypto_binance crypto_gate crypto_okx manual-trading-hub manual-agent-*"
log "预期 PM2 进程(7 个): crypto_binance crypto_gate crypto_okx manual-trading-hub manual-agent-*"
if [[ "${DRY_RUN}" -eq 1 ]]; then
return 0
fi
pm2 list || true
if pm2 list 2>/dev/null | grep -qiE 'gate_bot|15203'; then
log "警告: PM2 列表仍含 gate_bot 相关进程请 pm2 delete 后 pm2 save"
log "警告: PM2 列表仍含 gate_bot 相关进程,请 pm2 delete 后 pm2 save"
fi
}
# --- 前置检查 ---
if [[ "$(id -u)" -ne 0 ]]; then
echo "请使用 root 执行推荐路径 ${INSTALL_ROOT}" >&2
echo "请使用 root 执行(推荐路径 ${INSTALL_ROOT})" >&2
exit 1
fi
@@ -197,7 +197,7 @@ if [[ ! -f "${REPO_ROOT}/deploy/setup_env.sh" ]]; then
fi
if [[ "${REPO_ROOT}" != "${INSTALL_ROOT}" ]]; then
log "提示: 当前仓库 ${REPO_ROOT} 与 INSTALL_ROOT=${INSTALL_ROOT} 不一致将备份当前仓库并克隆到 INSTALL_ROOT"
log "提示: 当前仓库 ${REPO_ROOT} 与 INSTALL_ROOT=${INSTALL_ROOT} 不一致;将备份当前仓库并克隆到 INSTALL_ROOT"
fi
STAMP="$(TZ="${TZ_NAME}" date +%Y%m%d-%H%M%S)"
@@ -216,9 +216,9 @@ echo " 旧目录移走: ${OLD_DIR}"
echo " 新克隆: ${GIT_URL} (${GIT_BRANCH}) -> ${INSTALL_ROOT}"
echo " 环境: deploy/setup_env.sh --skip-env-copy --recreate-venv --skip-pm2"
echo ""
echo " 将停止并 delete 全部 PM2 进程不备份 crypto.db / hub data / 图片"
echo " 将停止并 delete 全部 PM2 进程;不备份 crypto.db / hub data / 图片."
if ! confirm "确认执行 Plan B 整目录重装"; then
if ! confirm "确认执行 Plan B 整目录重装?"; then
log "已取消"
exit 0
fi
@@ -235,7 +235,7 @@ if command -v pm2 >/dev/null 2>&1; then
run pm2 stop all || true
run pm2 delete all || true
else
log "未安装 pm2跳过"
log "未安装 pm2,跳过"
fi
# --- 3. 移走旧目录 ---
@@ -248,7 +248,7 @@ if [[ -d "${INSTALL_ROOT}" ]]; then
mv "${INSTALL_ROOT}" "${OLD_DIR}"
fi
else
log "目标目录不存在跳过 mv"
log "目标目录不存在,跳过 mv"
fi
# --- 4. 克隆 ---
@@ -260,7 +260,7 @@ else
git clone -b "${GIT_BRANCH}" "${GIT_URL}" "${INSTALL_ROOT}"
fi
# --- 5. setup_env一键安装逻辑不复制 .env---
# --- 5. setup_env(一键安装逻辑,不复制 .env)---
step "重建 Python 虚拟环境 (setup_env.sh)"
if [[ "${DRY_RUN}" -eq 1 ]]; then
@@ -281,10 +281,10 @@ if command -v pm2 >/dev/null 2>&1; then
run bash "${INSTALL_ROOT}/deploy/pm2_start_all.sh"
run pm2 save
else
log "未安装 pm2请手动: bash ${INSTALL_ROOT}/deploy/pm2_start_all.sh"
log "未安装 pm2;请手动: bash ${INSTALL_ROOT}/deploy/pm2_start_all.sh"
fi
# --- 8. 定时备份 cron可选---
# --- 8. 定时备份 cron(可选)---
if [[ "${INSTALL_BACKUP_CRON}" -eq 1 ]]; then
step "安装三所每日备份 cron"
@@ -297,14 +297,14 @@ step "完成"
verify_pm2
echo ""
echo "备份: ${BACKUP_DIR}"
echo "旧目录确认无误后可删: ${OLD_DIR}"
echo "旧目录(确认无误后可删): ${OLD_DIR}"
echo ""
echo "验收建议:"
echo " pm2 list"
echo " curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5100/"
echo " 浏览器打开中控 /monitor确认三所 LINK 正常"
echo " 浏览器打开中控 /monitor,确认三所 LINK 正常"
echo ""
echo "回滚未删旧目录时:"
echo "回滚(未删旧目录时):"
echo " pm2 delete all"
echo " rm -rf ${INSTALL_ROOT}"
echo " mv ${OLD_DIR} ${INSTALL_ROOT}"
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""重装后清理 hub_settings.json 中已废弃的 gate_bot / 第四账户条目"""
"""重装后清理 hub_settings.json 中已废弃的 gate_bot / 第四账户条目."""
from __future__ import annotations
import json
@@ -92,7 +92,7 @@ def main(argv: list[str] | None = None) -> int:
for line in removed:
print(f" - {line}")
else:
print("无需修改未发现 gate_bot / 第四账户")
print("无需修改(未发现 gate_bot / 第四账户)")
return 0
+17 -17
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# crypto_monitor 一键环境部署Ubuntu / root /opt/crypto_monitor
# crypto_monitor 一键环境部署(Ubuntu / root /opt/crypto_monitor)
#
# 用法:
# bash deploy/setup_env.sh
@@ -10,7 +10,7 @@
#
set -e
set -u
# 避免 Windows CRLF 导致 set -euo pipefail 一行报错pipefail 仅 bash 支持
# 避免 Windows CRLF 导致 set -euo pipefail 一行报错;pipefail 仅 bash 支持
if [ -n "${BASH_VERSION:-}" ]; then
set -o pipefail
fi
@@ -69,7 +69,7 @@ find_python() {
echo python
return
fi
echo "未找到 python3/python请先安装 Python 3.10+" >&2
echo "未找到 python3/python,请先安装 Python 3.10+" >&2
exit 1
}
@@ -81,7 +81,7 @@ check_python_version() {
major="${ver%%.*}"
minor="${ver#*.}"
if [[ "${major}" -lt 3 ]] || [[ "${major}" -eq 3 && "${minor}" -lt 10 ]]; then
echo "需要 Python 3.10+当前: ${ver}" >&2
echo "需要 Python 3.10+,当前: ${ver}" >&2
exit 1
fi
echo "Python: $("${py}" --version 2>&1)"
@@ -109,11 +109,11 @@ install_debian_venv_packages() {
local ver
ver="$(python_minor_version "${py}")"
if ! command -v apt-get >/dev/null 2>&1; then
echo " 未检测到 apt-get请手动安装 python${ver}-venv 与 python3-pip" >&2
echo " 未检测到 apt-get,请手动安装 python${ver}-venv 与 python3-pip" >&2
return 1
fi
if [[ "$(id -u)" -ne 0 ]]; then
echo " 需要 root 安装系统包请执行:" >&2
echo " 需要 root 安装系统包,请执行:" >&2
echo " sudo apt update && sudo apt install -y python${ver}-venv python3-pip curl" >&2
echo " 或: sudo bash deploy/setup_env.sh --install-system-deps" >&2
return 1
@@ -131,7 +131,7 @@ ensure_venv_prereqs() {
if check_venv_available "${py}"; then
return 0
fi
echo " 当前 Python 无法创建 venv缺少 ensurepip常见于未安装 python*-venv" >&2
echo " 当前 Python 无法创建 venv(缺少 ensurepip,常见于未安装 python*-venv)" >&2
if [[ "${INSTALL_APT_DEPS}" -eq 1 ]] || [[ "$(id -u)" -eq 0 ]]; then
install_debian_venv_packages "${py}" || exit 1
if check_venv_available "${py}"; then
@@ -171,7 +171,7 @@ setup_monitor() {
local dir_name="$1"
local proj="${REPO_ROOT}/${dir_name}"
if [[ ! -d "${proj}" ]]; then
echo " 跳过目录不存在: ${dir_name}"
echo " 跳过(目录不存在): ${dir_name}"
return
fi
step "${dir_name}"
@@ -188,7 +188,7 @@ setup_monitor() {
elif [[ -f .env ]]; then
echo " 保留已有 .env"
else
echo " 无 .env.example请手动配置 .env"
echo " 无 .env.example,请手动配置 .env"
fi
fi
mkdir -p static/images/order_charts
@@ -198,7 +198,7 @@ setup_monitor() {
setup_hub() {
local proj="${REPO_ROOT}/manual_trading_hub"
if [[ ! -d "${proj}" ]]; then
echo " 跳过 hub目录不存在"
echo " 跳过 hub(目录不存在)"
return
fi
step "manual_trading_hub"
@@ -219,9 +219,9 @@ install_pm2() {
if [[ "${SKIP_PM2}" -eq 1 ]]; then
return
fi
step "PM2可选"
step "PM2(可选)"
if ! command -v node >/dev/null 2>&1; then
echo " 未检测到 Node.js跳过安装后执行: npm install -g pm2"
echo " 未检测到 Node.js,跳过.安装后执行: npm install -g pm2"
return
fi
if command -v pm2 >/dev/null 2>&1; then
@@ -249,15 +249,15 @@ should_include hub && setup_hub
install_pm2
step "部署密钥首次自动生成不覆盖已有"
step "部署密钥(首次自动生成,不覆盖已有)"
if command -v python3 >/dev/null 2>&1; then
python3 "${REPO_ROOT}/scripts/bootstrap_deploy_secrets.py" || true
else
echo " 跳过 bootstrap_deploy_secrets未找到 python3"
echo " 跳过 bootstrap_deploy_secrets(未找到 python3)"
fi
echo ""
echo "部署完成下一步"
echo " 1. 编辑各子目录 .env交易所 API 等AI 请在中控系统设置配置"
echo " 2. 编辑各目录 .env 后使用 PM2: pm2 start ecosystem.config.cjs见 docs/ubuntu-server.md"
echo "部署完成.下一步:"
echo " 1. 编辑各子目录 .env(交易所 API 等;AI 请在中控系统设置配置)"
echo " 2. 编辑各目录 .env 后使用 PM2: pm2 start ecosystem.config.cjs(见 docs/ubuntu-server.md)"
echo ""
+130 -130
View File
@@ -1,130 +1,130 @@
# 账户冷静期 / 日冻结风控
三所实例币安 / OKX / Gate / Gate共用 `account_risk_lib.py`
**仅用户主动平仓**计入风控交易所止盈/止损空仓同步改保本/改委托等**不触发**冷静期
## 状态展示
实例页顶中控监控卡片账户名旁显示风控徽章
| 状态 | 含义 | 倒计时 |
|------|------|--------|
| 正常 | 可新开仓 | 无 |
| 1h冻结 | 冷静期中通常为复盘后缩短的 1 小时 | 剩余时间`1h冻结 · 52m 08s` |
| 4h冻结 | 冷静期中默认 4 小时 | 剩余时间`4h冻结 · 3h 12m` |
| 日冻结 | 当日禁止一切新开仓 | 至下一 **交易日切点**`TRADING_DAY_RESET_HOUR` |
- 倒计时每秒刷新到期后徽章自动恢复为 **正常**下次轮询/API 刷新会再次对齐服务端状态)。
- 鼠标悬停徽章可见完整说明含解除时刻如有)。
## 什么算「手动平仓」计入风控
以下操作通过 `close_source` 登记为 **用户主动平仓**
| 来源标识 | 操作 |
|----------|------|
| `user_instance` | 实例页删单/手动平仓`del_order` |
| `user_hub` | 中控「平仓」「全平」「紧急全平」 |
| `user_trend_stop` | 趋势计划 **「结束计划」**手动结束 |
**不算**手动平仓不触发风控):
- 趋势 **「保本移交下单监控」**
- 中控/实例修改委托挂止盈止损移动保本
- 交易所止盈/止损/条件单成交
- 后台 `reconcile_external_closes` 空仓同步即使记账为「外部平仓」
- 监控轮询自动止盈/止损/保本
## 触发规则
| 事件 | 行为 |
|------|------|
| 第 1 次用户主动平仓 | 默认 **4h** 冷静期 |
| 第 2 次用户主动平仓同一交易日 | **日冻结** |
| 复盘勾选任意情绪标签 | **日冻结** |
| 复盘离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**须处于 4h 档冷静期中 |
情绪标签怕踏空报复开仓盈利飘了拿不住单扛单重仓违规
### 复盘缩短为 1h
任选一种方式并填写说明
| 方式 | 必填 |
|------|------|
| **复盘表单**提交 | 离场触发 = **手动平仓****离场补充** 非空不是下方「备注」 |
| **核对修改**保存 | 结果 = **手动平仓****备注** 非空 |
说明
- 中控全平 / 实例手动平仓后只要在 4h 窗口内完成上述操作即可降为 1h
- 复盘保存后会同步更新 `last_close_at_ms`倒计时以 **最后一次手动平仓 + 当前档位数** 为准不会继续读库内旧 4h 结束时间
- 1h 窗口已结束后即使库里残留旧 `cooloff_until_ms`状态也会恢复 **正常**
- 若超过「平仓 + 1h」才复盘则从 **保存复盘时刻** 起再计 1h不延长原 4h)。
- **止盈 / 保本止盈 / 止损** 等自动平仓不触发风控也不会刷新冷静期
- 代码更新后需 **重启对应实例** 并硬刷新页面
### 倒计时与标签
- 结束时刻 = `last_close_at_ms + cooloff_hours``APP_TIMEZONE` 默认北京时间
- 1h / 4h 标签按实际剩余时长判断与倒计时一致
- 切交易日后若冷静期已过期自动清库内残留字段
## 环境变量
```env
RISK_CONTROL_ENABLED=true
RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
RISK_MOOD_ISSUES_DAILY_FREEZE=true
TRADING_DAY_RESET_HOUR=8
APP_TIMEZONE=Asia/Shanghai
```
`RISK_COOLING_HOURS_EXTERNAL` 已废弃外部平仓不再触发风控)。
## API 与 `risk_status` 字段
| 接口 | 说明 |
|------|------|
| `GET /api/account_snapshot` | 实例页轮询`risk_status` |
| `GET /api/account_risk_status` | hub_bridge 专用 |
| `GET /api/hub/monitor` | 中控监控板每账户含 `risk_status` |
| `POST /api/hub/account-risk/user-close` | 中控登记用户平仓`body: { source, count }` |
`risk_status` 主要字段
| 字段 | 说明 |
|------|------|
| `status` | `normal` / `freeze_1h` / `freeze_4h` / `freeze_daily` / `freeze_position` |
| `status_label` | 中文标签 |
| `can_trade` | 是否允许新开仓仅风控维度 |
| `reason` | 悬停提示文案 |
| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env``MAX_ACTIVE_POSITIONS` |
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳毫秒 |
| `freeze_until_ms` | 倒计时结束时间戳日冻结为下一交易日切点 |
| `freeze_remaining_sec` | 服务端计算的剩余秒数供调试 |
**仓位上限冻结****计入上限的** 活跃持仓数不含趋势回调≥ 实例 `.env``MAX_ACTIVE_POSITIONS`默认 1且账户无时间类冻结时徽章显示 **仓位上限冻结**此时 **新开仓** 被禁止**顺势加仓**在已有同向监控持仓上加仓仍可用仅存在趋势回调持仓时不触发该冻结时间冻结1h/4h/日优先展示
`risk_status.can_roll`仓位上限冻结时为 `true`表示顺势加仓不受该冻结限制
## 前端倒计时
- 共用脚本`static/account_risk_badge.js?v=4`
- 样式`static/account_risk_badge.css`
- 展示格式`4h冻结 · 3h 12m`日冻结为距下一交易日切点剩余时间
- 倒计时优先用服务端 `freeze_remaining_sec` 推算结束时刻避免绝对时间戳与时区/脏数据偏差
- 服务端在冷静期**已结束**或锚点无效时**自动清库**避免重启后误读旧 `account_risk_state` 仍显示冻结
- 无效的未来 `last_close_at_ms` **不会**被当作「现在」重启计时
- 若当日手动平仓**已复盘**journal 有说明且 1h 窗口已过即使 risk 表被误写也会强制恢复 **正常**
- 勿与交易记录列表中的历史平仓时间混淆风控只看 `account_risk_state` 表内 **最后一次用户主动平仓** 及其复盘结果
## 相关代码
- `account_risk_lib.py` — 状态机`enrich_risk_status_countdown``apply_position_limit_risk``on_user_initiated_close`
- `hub_bridge.py``/api/hub/account-risk/user-close`
- `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close
- `strategy_trend_register.py``stop_trend_pullback` 结束计划时登记风控
- `tests/test_account_risk_lib.py`
# 账户冷静期 / 日冻结风控
三所实例(币安 / OKX / Gate / Gate)共用 `account_risk_lib.py`.
**仅用户主动平仓**计入风控;交易所止盈/止损,空仓同步,改保本/改委托等**不触发**冷静期.
## 状态展示
实例页顶,中控监控卡片账户名旁显示风控徽章:
| 状态 | 含义 | 倒计时 |
|------|------|--------|
| 正常 | 可新开仓 | 无 |
| 1h冻结 | 冷静期中(通常为复盘后缩短的 1 小时) | 剩余时间,`1h冻结 · 52m 08s` |
| 4h冻结 | 冷静期中(默认 4 小时) | 剩余时间,`4h冻结 · 3h 12m` |
| 日冻结 | 当日禁止一切新开仓 | 至下一 **交易日切点**(`TRADING_DAY_RESET_HOUR`) |
- 倒计时每秒刷新;到期后徽章自动恢复为 **正常**(下次轮询/API 刷新会再次对齐服务端状态).
- 鼠标悬停徽章可见完整说明(含解除时刻,如有).
## 什么算「手动平仓」(计入风控)
以下操作通过 `close_source` 登记为 **用户主动平仓**:
| 来源标识 | 操作 |
|----------|------|
| `user_instance` | 实例页删单/手动平仓(`del_order`) |
| `user_hub` | 中控「平仓」「全平」「紧急全平」 |
| `user_trend_stop` | 趋势计划 **「结束计划」**(手动结束) |
**不算**手动平仓(不触发风控):
- 趋势 **「保本移交下单监控」**
- 中控/实例修改委托,挂止盈止损,移动保本
- 交易所止盈/止损/条件单成交
- 后台 `reconcile_external_closes` 空仓同步(即使记账为「外部平仓」)
- 监控轮询自动止盈/止损/保本
## 触发规则
| 事件 | 行为 |
|------|------|
| 第 1 次用户主动平仓 | 默认 **4h** 冷静期 |
| 第 2 次用户主动平仓(同一交易日) | **日冻结** |
| 复盘勾选任意情绪标签 | **日冻结** |
| 复盘:离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**(须处于 4h 档冷静期中) |
情绪标签:怕踏空,报复开仓,盈利飘了,拿不住单,扛单,重仓违规.
### 复盘缩短为 1h
任选一种方式,并填写说明:
| 方式 | 必填 |
|------|------|
| **复盘表单**提交 | 离场触发 = **手动平仓**;**离场补充** 非空(不是下方「备注」) |
| **核对修改**保存 | 结果 = **手动平仓**;**备注** 非空 |
说明:
- 中控全平 / 实例手动平仓后,只要在 4h 窗口内完成上述操作即可降为 1h.
- 复盘保存后会同步更新 `last_close_at_ms`,倒计时以 **最后一次手动平仓 + 当前档位数** 为准,不会继续读库内旧 4h 结束时间.
- 1h 窗口已结束后,即使库里残留旧 `cooloff_until_ms`,状态也会恢复 **正常**.
- 若超过「平仓 + 1h」才复盘,则从 **保存复盘时刻** 起再计 1h(不延长原 4h).
- **止盈 / 保本止盈 / 止损** 等自动平仓不触发风控,也不会刷新冷静期.
- 代码更新后需 **重启对应实例** 并硬刷新页面.
### 倒计时与标签
- 结束时刻 = `last_close_at_ms + cooloff_hours`(`APP_TIMEZONE` 默认北京时间)
- 1h / 4h 标签按实际剩余时长判断,与倒计时一致
- 切交易日后,若冷静期已过期,自动清库内残留字段
## 环境变量
```env
RISK_CONTROL_ENABLED=true
RISK_COOLING_HOURS_MANUAL=4
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
RISK_MOOD_ISSUES_DAILY_FREEZE=true
TRADING_DAY_RESET_HOUR=8
APP_TIMEZONE=Asia/Shanghai
```
`RISK_COOLING_HOURS_EXTERNAL` 已废弃(外部平仓不再触发风控).
## API 与 `risk_status` 字段
| 接口 | 说明 |
|------|------|
| `GET /api/account_snapshot` | 实例页轮询,`risk_status` |
| `GET /api/account_risk_status` | hub_bridge 专用 |
| `GET /api/hub/monitor` | 中控监控板,每账户含 `risk_status` |
| `POST /api/hub/account-risk/user-close` | 中控登记用户平仓,`body: { source, count }` |
`risk_status` 主要字段:
| 字段 | 说明 |
|------|------|
| `status` | `normal` / `freeze_1h` / `freeze_4h` / `freeze_daily` / `freeze_position` |
| `status_label` | 中文标签 |
| `can_trade` | 是否允许新开仓(仅风控维度) |
| `reason` | 悬停提示文案 |
| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env``MAX_ACTIVE_POSITIONS` |
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
**仓位上限冻结**:**计入上限的** 活跃持仓数(不含趋势回调)≥ 实例 `.env``MAX_ACTIVE_POSITIONS`(默认 1)且账户无时间类冻结时,徽章显示 **仓位上限冻结**;此时 **新开仓** 被禁止,**顺势加仓**(在已有同向监控持仓上加仓)仍可用.仅存在趋势回调持仓时不触发该冻结.时间冻结(1h/4h/日)优先展示.
`risk_status.can_roll`:仓位上限冻结时为 `true`,表示顺势加仓不受该冻结限制.
## 前端倒计时
- 共用脚本:`static/account_risk_badge.js?v=4`
- 样式:`static/account_risk_badge.css`
- 展示格式:`4h冻结 · 3h 12m`;日冻结为距下一交易日切点剩余时间
- 倒计时优先用服务端 `freeze_remaining_sec` 推算结束时刻,避免绝对时间戳与时区/脏数据偏差
- 服务端在冷静期**已结束**或锚点无效时**自动清库**,避免重启后误读旧 `account_risk_state` 仍显示冻结
- 无效的未来 `last_close_at_ms` **不会**被当作「现在」重启计时
- 若当日手动平仓**已复盘**(journal 有说明)且 1h 窗口已过,即使 risk 表被误写也会强制恢复 **正常**
- 勿与交易记录列表中的历史平仓时间混淆:风控只看 `account_risk_state` 表内 **最后一次用户主动平仓** 及其复盘结果
## 相关代码
- `account_risk_lib.py` — 状态机,`enrich_risk_status_countdown`,`apply_position_limit_risk`,`on_user_initiated_close`
- `hub_bridge.py``/api/hub/account-risk/user-close`
- `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close
- `strategy_trend_register.py``stop_trend_pullback` 结束计划时登记风控
- `tests/test_account_risk_lib.py`
+45 -45
View File
@@ -1,45 +1,45 @@
# 每日自动划转三所统一
## 行为
`.env` 开启 `AUTO_TRANSFER_ENABLED=true`监控轮询在**北京时间 `AUTO_TRANSFER_BJ_HOUR` 整点所在小时**内默认 8:008:59执行一次**UTC 自然日** 去重):
| 交易账户 (`AUTO_TRANSFER_TO`默认 swap) | 动作 |
|------------------------------------------|------|
| 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`默认 funding划入差额 |
| 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` |
| 与目标相差 &lt; 0.01U | 跳过不写划转 |
| 存在 **active** 持仓`order_monitors`或 Gate回调已开仓计划 | **不划转**写账簿 `skipped`并**企业微信**说明「持仓中本次资金无划转」 |
## 配置示例目标 50U
```env
AUTO_TRANSFER_ENABLED=true
AUTO_TRANSFER_AMOUNT=50
AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap
AUTO_TRANSFER_BJ_HOUR=8
```
`AUTO_TRANSFER_AMOUNT``DAILY_START_CAPITAL`每日开仓基数**独立**
API Key 须具备万向划转权限与手动划转相同)。
## 用脚本更新三所 `.env`
详见 **[env-sync-scripts.md](./env-sync-scripts.md)**常用命令
```bash
git pull
# 仅补全划转相关项
python scripts/sync_four_exchange_transfer_env.py
# 目标 50U 并开启自动划转
python scripts/sync_four_exchange_transfer_env.py --set-amount 50 --enable-auto-transfer
# 计仓 + 划转一并补全
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
```
# 每日自动划转(三所统一)
## 行为
`.env` 开启 `AUTO_TRANSFER_ENABLED=true`,监控轮询在**北京时间 `AUTO_TRANSFER_BJ_HOUR` 整点所在小时**内(默认 8:008:59)执行一次(**UTC 自然日** 去重):
| 交易账户 (`AUTO_TRANSFER_TO`,默认 swap) | 动作 |
|------------------------------------------|------|
| 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`(默认 funding)划入差额 |
| 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` |
| 与目标相差 &lt; 0.01U | 跳过,不写划转 |
| 存在 **active** 持仓(`order_monitors`,或 Gate回调已开仓计划) | **不划转**,写账簿 `skipped`,并**企业微信**说明「持仓中,本次资金无划转」 |
## 配置示例(目标 50U)
```env
AUTO_TRANSFER_ENABLED=true
AUTO_TRANSFER_AMOUNT=50
AUTO_TRANSFER_FROM=funding
AUTO_TRANSFER_TO=swap
AUTO_TRANSFER_BJ_HOUR=8
```
`AUTO_TRANSFER_AMOUNT``DAILY_START_CAPITAL`(每日开仓基数)**独立**.
API Key 须具备万向划转权限(与手动划转相同).
## 用脚本更新三所 `.env`
详见 **[env-sync-scripts.md](./env-sync-scripts.md)**.常用命令:
```bash
git pull
# 仅补全划转相关项
python scripts/sync_four_exchange_transfer_env.py
# 目标 50U 并开启自动划转
python scripts/sync_four_exchange_transfer_env.py --set-amount 50 --enable-auto-transfer
# 计仓 + 划转一并补全
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
```
+81 -81
View File
@@ -1,81 +1,81 @@
# 单日开仓次数限制三所统一
各交易实例Binance / OKX / Gate`.env` 中独立配置互不影响
## 交易日口径
- 以 **北京时间** `TRADING_DAY_RESET_HOUR`默认 **8:00**切分交易日与统计顶栏「交易日」一致
- **次日恢复**过了切日时刻后 `session_date` 变为新日期计数自动归零无需清库
## 计数口径
每成功新建一条 `order_monitors` 记录计 **1 次**包括
- 人工「实盘下单」
- 关键位自动开仓
- 其他写入 `order_monitors` 的成功开仓
平仓后再开仍算新的一单当日总次数到硬上限后 **当天不再允许新开**即使已空仓)。
## 环境变量
在「交易执行 / 人工风控」段配置
```env
# 【单日开仓 AI 提醒】本交易日开仓次数达到该值时企业微信推送 AI 克制提醒不拦单
DAILY_OPEN_ALERT_THRESHOLD=5
# 【单日开仓硬上限】本交易日开仓次数 >= 该值后禁止一切新开仓直至下一交易日0=不启用
DAILY_OPEN_HARD_LIMIT=0
```
### 配置示例
```env
# 保守户3 次提醒5 次封死
DAILY_OPEN_ALERT_THRESHOLD=3
DAILY_OPEN_HARD_LIMIT=5
# 仅提醒不封与旧版行为接近
DAILY_OPEN_ALERT_THRESHOLD=5
DAILY_OPEN_HARD_LIMIT=0
# 严格户到 3 次即封
DAILY_OPEN_ALERT_THRESHOLD=2
DAILY_OPEN_HARD_LIMIT=3
```
建议 `DAILY_OPEN_ALERT_THRESHOLD <= DAILY_OPEN_HARD_LIMIT`硬上限为 0 时除外)。
## 程序行为
| 次数 | 行为 |
|------|------|
| 未达提醒阈值 | 正常开仓 |
| 达到 `DAILY_OPEN_ALERT_THRESHOLD` | 成功开仓后 AI 企业微信提醒 |
| 达到 `DAILY_OPEN_HARD_LIMIT`>0 | `precheck_risk` 拒绝人工/关键位开仓顶栏 `can_trade=false` |
硬限制与以下规则 **同时生效**取交集):
- `TRADING_DAY_RESET_OPEN_GUARD_ENABLED`切日前禁止新开
- `MAX_ACTIVE_POSITIONS`同时持仓上限
- Gate`precheck_trend_pullback_start` 同样校验单日硬上限
## 页面与接口
- 顶栏 / `api/account_snapshot` 返回 `opens_today``daily_open_hard_limit``daily_open_alert_threshold`
- 达硬上限时提示`本交易日开仓 N/M 已达上限次日 8:00 后恢复``M` 为配置的硬上限)。
## 部署
修改各实例 `.env` 后重启对应 pm2 进程例如
```bash
pm2 restart crypto_binance crypto_okx crypto_gate
```
## 实现位置
- 共享逻辑`daily_open_limit_lib.py`
- 三所 `app.py``precheck_risk``can_trade``api/account_snapshot`开仓成功后的 AI 提醒文案
- 单元测试`tests/test_daily_open_limit_lib.py`
# 单日开仓次数限制(三所统一)
各交易实例(Binance / OKX / Gate)`.env` 中独立配置,互不影响.
## 交易日口径
- 以 **北京时间** `TRADING_DAY_RESET_HOUR`(默认 **8:00**)切分交易日,与统计,顶栏「交易日」一致.
- **次日恢复**:过了切日时刻后 `session_date` 变为新日期,计数自动归零,无需清库.
## 计数口径
每成功新建一条 `order_monitors` 记录计 **1 次**,包括:
- 人工「实盘下单」
- 关键位自动开仓
- 其他写入 `order_monitors` 的成功开仓
平仓后再开仍算新的一单.当日总次数到硬上限后 **当天不再允许新开**(即使已空仓).
## 环境变量
在「交易执行 / 人工风控」段配置:
```env
# 【单日开仓 AI 提醒】本交易日开仓次数达到该值时,企业微信推送 AI 克制提醒(不拦单)
DAILY_OPEN_ALERT_THRESHOLD=5
# 【单日开仓硬上限】本交易日开仓次数 >= 该值后,禁止一切新开仓直至下一交易日;0=不启用
DAILY_OPEN_HARD_LIMIT=0
```
### 配置示例
```env
# 保守户:3 次提醒,5 次封死
DAILY_OPEN_ALERT_THRESHOLD=3
DAILY_OPEN_HARD_LIMIT=5
# 仅提醒,不封(与旧版行为接近)
DAILY_OPEN_ALERT_THRESHOLD=5
DAILY_OPEN_HARD_LIMIT=0
# 严格户:到 3 次即封
DAILY_OPEN_ALERT_THRESHOLD=2
DAILY_OPEN_HARD_LIMIT=3
```
建议 `DAILY_OPEN_ALERT_THRESHOLD <= DAILY_OPEN_HARD_LIMIT`(硬上限为 0 时除外).
## 程序行为
| 次数 | 行为 |
|------|------|
| 未达提醒阈值 | 正常开仓 |
| 达到 `DAILY_OPEN_ALERT_THRESHOLD` | 成功开仓后 AI 企业微信提醒 |
| 达到 `DAILY_OPEN_HARD_LIMIT`(>0) | `precheck_risk` 拒绝人工/关键位开仓;顶栏 `can_trade=false` |
硬限制与以下规则 **同时生效**(取交集):
- `TRADING_DAY_RESET_OPEN_GUARD_ENABLED`:切日前禁止新开
- `MAX_ACTIVE_POSITIONS`:同时持仓上限
- Gate:`precheck_trend_pullback_start` 同样校验单日硬上限
## 页面与接口
- 顶栏 / `api/account_snapshot` 返回 `opens_today`,`daily_open_hard_limit`,`daily_open_alert_threshold`.
- 达硬上限时提示:`本交易日开仓 N/M 已达上限,次日 8:00 后恢复`(`M` 为配置的硬上限).
## 部署
修改各实例 `.env` 后重启对应 pm2 进程,例如:
```bash
pm2 restart crypto_binance crypto_okx crypto_gate
```
## 实现位置
- 共享逻辑:`daily_open_limit_lib.py`
- 三所 `app.py`:`precheck_risk`,`can_trade`,`api/account_snapshot`,开仓成功后的 AI 提醒文案
- 单元测试:`tests/test_daily_open_limit_lib.py`
+139 -139
View File
@@ -1,139 +1,139 @@
# 三所 `.env` 同步脚本说明
在**仓库根目录**执行仅处理三所实例目录下的 `.env`**不覆盖** API 密钥与已存在的自定义值若某目录无 `.env``SKIP`需先 `cp .env.example .env`)。
| 目录 |
|------|
| `crypto_monitor_binance` |
| `crypto_monitor_okx` |
| `crypto_monitor_gate` |
| `crypto_monitor_gate` |
修改 `.env` 后须 **`pm2 restart`** 对应实例后生效
---
## 一键同步推荐
`scripts/sync_four_exchange_env.py`依次执行**计仓** + **自动划转** 两个子脚本
```bash
cd /path/to/crypto_monitor
git pull
# 仅补全缺失项已有值保留
python scripts/sync_four_exchange_env.py
# 预览不写文件
python scripts/sync_four_exchange_env.py --dry-run
# 划转目标 50U 并开启自动划转计仓仍只补缺失项
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
# 无仓后切换全仓杠杆须先确认交易所无持仓
python scripts/sync_four_exchange_env.py --set-mode full_margin
```
| 参数 | 说明 |
|------|------|
| `--dry-run` | 只打印将做的变更不写 `.env` |
| `--set-mode risk\|full_margin` | 强制三所 `POSITION_SIZING_MODE` |
| `--set-transfer-amount U` | 强制三所 `AUTO_TRANSFER_AMOUNT` |
| `--enable-auto-transfer` | 强制三所 `AUTO_TRANSFER_ENABLED=true` |
---
## 仅自动划转
`scripts/sync_four_exchange_transfer_env.py`
行为说明见 [auto-transfer-daily.md](./auto-transfer-daily.md)
```bash
# 补全缺失项
python scripts/sync_four_exchange_transfer_env.py
python scripts/sync_four_exchange_transfer_env.py --dry-run
# 目标 50U 并开启
python scripts/sync_four_exchange_transfer_env.py --set-amount 50 --enable-auto-transfer
```
| 参数 | 说明 |
|------|------|
| `--dry-run` | 预览 |
| `--set-amount U` | 强制 `AUTO_TRANSFER_AMOUNT` |
| `--enable-auto-transfer` | 强制 `AUTO_TRANSFER_ENABLED=true` |
**缺项默认**未使用 `--set-amount` 且文件中无该键时):
1. 若已有 `AUTO_TRANSFER_AMOUNT` → 保留
2. 否则若存在 `DAILY_START_CAPITAL` → 沿用其值
3. 否则 → **50**
补全时会写入若缺失):`AUTO_TRANSFER_FROM=funding``AUTO_TRANSFER_TO=swap``TRANSFER_CCY=USDT``AUTO_TRANSFER_BJ_HOUR=8`币安额外补 `BINANCE_FUNDING_INCLUDE_SPOT=false`
---
## 仅计仓模式
`scripts/sync_four_exchange_position_sizing_env.py`
行为说明见 [position-sizing-mode.md](./position-sizing-mode.md)
```bash
# 补全缺失项默认 riskFULL_MARGIN_BUFFER_RATIO=0.98
python scripts/sync_four_exchange_position_sizing_env.py
python scripts/sync_four_exchange_position_sizing_env.py --dry-run
# 无仓后切全仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode full_margin
# 无仓后切回以损定仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
# 强制缓冲比例
python scripts/sync_four_exchange_position_sizing_env.py --set-buffer 0.98
```
| 参数 | 说明 |
|------|------|
| `--dry-run` | 预览 |
| `--set-mode risk\|full_margin` | 强制 `POSITION_SIZING_MODE`**须无持仓**后 restart |
| `--set-buffer RATIO` | 强制 `FULL_MARGIN_BUFFER_RATIO` |
---
## 共用交易 / 关键位 / 轮询项
`scripts/sync_common_trading_env.py`以 Gate `.env.example` 为基准向**币安OKX** 的 `.env` **追加缺失项**不覆盖 API 密钥与已有自定义值)。
```bash
python scripts/sync_common_trading_env.py
python scripts/sync_common_trading_env.py --dry-run
python scripts/sync_common_trading_env.py --instances crypto_monitor_okx
python scripts/sync_common_trading_env.py --apply-force-close-policy
```
补全项含`RECONCILE_*``PRICE_REFRESH_SECONDS``KEY_*` 门控`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT`**不含** `FORCE_CLOSE_*``--apply-force-close-policy` 单独处理)。
**强制清仓策略**仅 Gate 开启):
```bash
python scripts/sync_common_trading_env.py --apply-force-close-policy
```
或服务器一键`bash deploy/pull_and_restart.sh`
---
## 部署后重启
```bash
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
```
## 相关文档
- [计仓模式](./position-sizing-mode.md)
- [每日自动划转](./auto-transfer-daily.md)
- [部署说明](../deploy/README.md)
# 三所 `.env` 同步脚本说明
在**仓库根目录**执行.仅处理三所实例目录下的 `.env`,**不覆盖** API 密钥与已存在的自定义值;若某目录无 `.env``SKIP`(需先 `cp .env.example .env`).
| 目录 |
|------|
| `crypto_monitor_binance` |
| `crypto_monitor_okx` |
| `crypto_monitor_gate` |
| `crypto_monitor_gate` |
修改 `.env` 后须 **`pm2 restart`** 对应实例后生效.
---
## 一键同步(推荐)
`scripts/sync_four_exchange_env.py`:依次执行**计仓** + **自动划转** 两个子脚本.
```bash
cd /path/to/crypto_monitor
git pull
# 仅补全缺失项(已有值保留)
python scripts/sync_four_exchange_env.py
# 预览,不写文件
python scripts/sync_four_exchange_env.py --dry-run
# 划转目标 50U 并开启自动划转(计仓仍只补缺失项)
python scripts/sync_four_exchange_env.py --set-transfer-amount 50 --enable-auto-transfer
# 无仓后切换全仓杠杆(须先确认交易所无持仓)
python scripts/sync_four_exchange_env.py --set-mode full_margin
```
| 参数 | 说明 |
|------|------|
| `--dry-run` | 只打印将做的变更,不写 `.env` |
| `--set-mode risk\|full_margin` | 强制三所 `POSITION_SIZING_MODE` |
| `--set-transfer-amount U` | 强制三所 `AUTO_TRANSFER_AMOUNT` |
| `--enable-auto-transfer` | 强制三所 `AUTO_TRANSFER_ENABLED=true` |
---
## 仅自动划转
`scripts/sync_four_exchange_transfer_env.py`
行为说明见 [auto-transfer-daily.md](./auto-transfer-daily.md).
```bash
# 补全缺失项
python scripts/sync_four_exchange_transfer_env.py
python scripts/sync_four_exchange_transfer_env.py --dry-run
# 目标 50U 并开启
python scripts/sync_four_exchange_transfer_env.py --set-amount 50 --enable-auto-transfer
```
| 参数 | 说明 |
|------|------|
| `--dry-run` | 预览 |
| `--set-amount U` | 强制 `AUTO_TRANSFER_AMOUNT` |
| `--enable-auto-transfer` | 强制 `AUTO_TRANSFER_ENABLED=true` |
**缺项默认**(未使用 `--set-amount` 且文件中无该键时):
1. 若已有 `AUTO_TRANSFER_AMOUNT` → 保留
2. 否则若存在 `DAILY_START_CAPITAL` → 沿用其值
3. 否则 → **50**
补全时会写入(若缺失):`AUTO_TRANSFER_FROM=funding`,`AUTO_TRANSFER_TO=swap`,`TRANSFER_CCY=USDT`,`AUTO_TRANSFER_BJ_HOUR=8`;币安额外补 `BINANCE_FUNDING_INCLUDE_SPOT=false`.
---
## 仅计仓模式
`scripts/sync_four_exchange_position_sizing_env.py`
行为说明见 [position-sizing-mode.md](./position-sizing-mode.md).
```bash
# 补全缺失项(默认 risk,FULL_MARGIN_BUFFER_RATIO=0.98)
python scripts/sync_four_exchange_position_sizing_env.py
python scripts/sync_four_exchange_position_sizing_env.py --dry-run
# 无仓后切全仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode full_margin
# 无仓后切回以损定仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
# 强制缓冲比例
python scripts/sync_four_exchange_position_sizing_env.py --set-buffer 0.98
```
| 参数 | 说明 |
|------|------|
| `--dry-run` | 预览 |
| `--set-mode risk\|full_margin` | 强制 `POSITION_SIZING_MODE`(**须无持仓**后 restart) |
| `--set-buffer RATIO` | 强制 `FULL_MARGIN_BUFFER_RATIO` |
---
## 共用交易 / 关键位 / 轮询项
`scripts/sync_common_trading_env.py`:以 Gate `.env.example` 为基准,向**币安,OKX** 的 `.env` **追加缺失项**(不覆盖 API 密钥与已有自定义值).
```bash
python scripts/sync_common_trading_env.py
python scripts/sync_common_trading_env.py --dry-run
python scripts/sync_common_trading_env.py --instances crypto_monitor_okx
python scripts/sync_common_trading_env.py --apply-force-close-policy
```
补全项含:`RECONCILE_*`,`PRICE_REFRESH_SECONDS`,`KEY_*` 门控,`KEY_SIZING_USE_ZERO_POSITION_SNAPSHOT`(**不含** `FORCE_CLOSE_*`,`--apply-force-close-policy` 单独处理).
**强制清仓策略**(仅 Gate 开启):
```bash
python scripts/sync_common_trading_env.py --apply-force-close-policy
```
或服务器一键:`bash deploy/pull_and_restart.sh`
---
## 部署后重启
```bash
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
```
## 相关文档
- [计仓模式](./position-sizing-mode.md)
- [每日自动划转](./auto-transfer-daily.md)
- [部署说明](../deploy/README.md)
+75 -75
View File
@@ -1,8 +1,8 @@
# env 配置页说明
本文档描述各交易实例 Web 端 **「env 配置」** 页展示项含义生效方式以及与系统设置中控密钥的分工
本文档描述各交易实例 Web 端 **「env 配置」** 页展示项,含义,生效方式,以及与系统设置,中控密钥的分工.
> **不在本页展示的配置**服务端口数据库路径关键位门控轮询间隔等仍保存在实例目录 `.env`需 SSH 编辑或部署脚本维护见文末「隐藏项」
> **不在本页展示的配置**(服务端口,数据库路径,关键位门控,轮询间隔等)仍保存在实例目录 `.env`,需 SSH 编辑或部署脚本维护,见文末「隐藏项」.
---
@@ -10,66 +10,66 @@
| 原则 | 说明 |
|------|------|
| **只展示运营相关项** | 不暴露全量 `.env`避免误改基础设施 |
| **前端仅中文** | 页面只显示中文标签与说明不显示 `APP_XXX` 等变量名 |
| **只展示运营相关项** | 不暴露全量 `.env`,避免误改基础设施 |
| **前端仅中文** | 页面只显示中文标签与说明,不显示 `APP_XXX` 等变量名 |
| **账户密码不进本页** | 登录用户名/密码在 **系统设置 → 账户密码修改** 中维护 |
| **密钥自动托管** | 中控通信密钥登录会话密钥由 **首次部署脚本自动生成并写入**一次生成不轮换),本页不提供编辑 |
| **AI 仅中控配置** | OpenAI / Ollama 等 AI 项已从中控 **系统设置 → AI 配置** 统一维护并同步三所本页不再展示 |
| **保存标注** | 每项标注「保存即生效」或「需重启」含需重启项时可用「保存并重启」 |
| **密钥自动托管** | 中控通信密钥,登录会话密钥由 **首次部署脚本自动生成并写入**(一次生成,不轮换),本页不提供编辑 |
| **AI 仅中控配置** | OpenAI / Ollama 等 AI 项已从中控 **系统设置 → AI 配置** 统一维护并同步三所,本页不再展示 |
| **保存标注** | 每项标注「保存即生效」或「需重启」;含需重启项时可用「保存并重启」 |
---
## 2. 密钥分工自动生成本页不可见
## 2. 密钥分工(自动生成,本页不可见)
部署时由脚本统一生成并写入对应 `.env`已有值则跳过避免覆盖生产环境)。
部署时由脚本统一生成并写入对应 `.env`(已有值则跳过,避免覆盖生产环境).
| 类型 | 环境变量 | 写入位置 | 用途 |
|------|----------|----------|------|
| **中控通信密钥** | `HUB_BRIDGE_TOKEN` | 中控 `manual_trading_hub/.env` + 三实例 `.env`**相同值** | 中控调用实例 API`X-Hub-Token`)、iframe SSO 签发与校验 |
| **登录会话密钥** | `FLASK_SECRET_KEY` | 三实例 `.env`**三所相同** | Flask Session 签名与网页登录态相关与中控密钥 **分离** |
| **中控通信密钥** | `HUB_BRIDGE_TOKEN` | 中控 `manual_trading_hub/.env` + 三实例 `.env`(**相同值**) | 中控调用实例 API(`X-Hub-Token`),iframe SSO 签发与校验 |
| **登录会话密钥** | `FLASK_SECRET_KEY` | 三实例 `.env`(**三所相同**) | Flask Session 签名;与网页登录态相关,与中控密钥 **分离** |
| **中控会话密钥** | `HUB_SESSION_SECRET` | 中控 `.env` | 中控登录 Cookie 签名 |
| **登录账号** | `APP_USERNAME` / `APP_PASSWORD` | 三实例 `.env`建议三所统一 | 直链实例 `/login` 使用**仅在系统设置中修改** |
| **中控登录** | `HUB_USERNAME` / `HUB_PASSWORD` | 中控 `.env` | 中控网页登录**仅在中控系统设置改密** |
| **登录账号** | `APP_USERNAME` / `APP_PASSWORD` | 三实例 `.env`(建议三所统一) | 直链实例 `/login` 使用;**仅在系统设置中修改** |
| **中控登录** | `HUB_USERNAME` / `HUB_PASSWORD` | 中控 `.env` | 中控网页登录;**仅在中控系统设置改密** |
说明
说明:
- **长期密钥一次生成不轮换**`setup_env.sh` 末尾调用 `scripts/bootstrap_deploy_secrets.py`已有非占位值不会被覆盖
- **SSO 链接不变**仍为中控每次签发默认 2 小时有效单次使用`HUB_SSO_TTL_SEC`),与长期 `HUB_BRIDGE_TOKEN` 分离
- 经中控 iframe / SSO 打开实例时可免输实例密码直链 IP/域名仍走 `/login`
- 首次部署可生成随机强密码用户日后在 **系统设置** 改密不经过本页
- **长期密钥一次生成,不轮换**:`setup_env.sh` 末尾调用 `scripts/bootstrap_deploy_secrets.py`;已有非占位值不会被覆盖.
- **SSO 链接不变**:仍为中控每次签发,默认 2 小时有效,单次使用(`HUB_SSO_TTL_SEC`),与长期 `HUB_BRIDGE_TOKEN` 分离.
- 经中控 iframe / SSO 打开实例时,可免输实例密码;直链 IP/域名仍走 `/login`.
- 首次部署可生成随机强密码;用户日后在 **系统设置** 改密,不经过本页.
---
## 3. 页面布局三列卡片
## 3. 页面布局(三列卡片)
| 列 1 | 列 2 | 列 3 |
|------|------|------|
| 交易所与实盘 | 企业微信 | 交易执行 |
| 交易风控 | 账户冷静期 | 自动划转 |
| 当日资金 | 期权账户仅 OKX | |
| 当日资金 | 期权账户(仅 OKX) | |
> **AI 复盘**OpenAI / Ollama已移至中控 **系统设置 → AI 配置**保存后强制同步三所 `.env`详见 [中控AI与密钥配置.md](./中控AI与密钥配置.md)
> **AI 复盘**(OpenAI / Ollama)已移至中控 **系统设置 → AI 配置**,保存后强制同步三所 `.env`.详见 [中控AI与密钥配置.md](./中控AI与密钥配置.md).
Binance / Gate 无期权模块时第三列最后一格不显示或显示「本所无期权」
Binance / Gate 无期权模块时,第三列最后一格不显示或显示「本所无期权」.
---
## 4. 各卡片字段中文展示名
## 4. 各卡片字段(中文展示名)
### 4.1 交易所与实盘
| 中文名 | 说明 | 重启 |
|--------|------|------|
| 开启实盘下单 | 关闭时仅走本地流程不向交易所发单 | 需重启 |
| 开启实盘下单 | 关闭时仅走本地流程,不向交易所发单 | 需重启 |
| API Key | 永续子账户 API Key | 需重启 |
| API Secret | 永续子账户 Secret | 需重启 |
| API Passphrase | 仅 OKX 显示 | 需重启 |
| 保证金模式 | 全仓 / 逐仓 | 需重启 |
| 持仓模式 | 双向 / 单向净持仓等按所 | 需重启 |
| 仓位查询类型 | 仅 OKX如 SWAP | 需重启 |
| 持仓模式 | 双向 / 单向净持仓等(按所) | 需重启 |
| 仓位查询类型 | 仅 OKX:如 SWAP | 需重启 |
| 账户备注 | 企业微信推送中显示的交易所备注 | 保存即生效 |
**本卡片不包含**网页登录账号密码是否关闭登录校验中控通信密钥
**本卡片不包含**:网页登录账号密码,是否关闭登录校验,中控通信密钥.
---
@@ -77,16 +77,16 @@ Binance / Gate 无期权模块时,第三列最后一格不显示或显示「
| 中文名 | 说明 |
|--------|------|
| 机器人 Webhook | 行情风控提醒推送地址 |
| 推送超时(秒) | 可选默认 10 |
| 机器人 Webhook | 行情,风控,提醒推送地址 |
| 推送超时(秒) | 可选,默认 10 |
---
### 4.3 AI 复盘已移至中控
### 4.3 AI 复盘(已移至中控)
AI 相关环境变量`AI_PROVIDER``OPENAI_*``OLLAMA_*``AI_MODEL``AI_TIMEOUT_SECONDS`**不再在本页展示**
AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT_SECONDS`)**不再在本页展示**.
请在中控 **系统设置 → AI 配置** 修改保存后写入中控 `.env`**强制同步** 至 OKX / Binance / Gate 三实例详见 [中控AI与密钥配置.md](./中控AI与密钥配置.md)
请在中控 **系统设置 → AI 配置** 修改;保存后写入中控 `.env`**强制同步** 至 OKX / Binance / Gate 三实例.详见 [中控AI与密钥配置.md](./中控AI与密钥配置.md).
---
@@ -103,23 +103,23 @@ AI 相关环境变量(`AI_PROVIDER`、`OPENAI_*`、`OLLAMA_*`、`AI_MODEL`、`
| 允许方向 | 多 / 空 / 双向 |
| 币种白名单开关 | |
| 白名单币种 | 逗号分隔 |
| 交易日切点北京时间 | 默认 8 点 |
| 交易日切点(北京时间) | 默认 8 点 |
| 切点前禁止新开仓 | |
| 最大同时持仓 | |
| 人工最低盈亏比 | |
| 强制清仓开关 | |
| 强制清仓整点北京 | |
| 强制清仓整点(北京) | |
---
### 4.5 交易风控日内开仓
### 4.5 交易风控(日内开仓)
| 中文名 | 说明 |
|--------|------|
| 单日开仓提醒阈值 | 达到次数后 AI 克制提醒不拦单 |
| 单日开仓硬上限 | 0 表示不启用达到后禁止新开仓 |
| 单日开仓提醒阈值 | 达到次数后 AI 克制提醒(不拦单) |
| 单日开仓硬上限 | 0 表示不启用;达到后禁止新开仓 |
详见 [daily-open-limit.md](./daily-open-limit.md)
详见 [daily-open-limit.md](./daily-open-limit.md).
---
@@ -128,12 +128,12 @@ AI 相关环境变量(`AI_PROVIDER`、`OPENAI_*`、`OLLAMA_*`、`AI_MODEL`、`
| 中文名 | 说明 |
|--------|------|
| 冷静期总开关 | |
| 手动平仓冷静小时 | |
| 复盘情绪冷静小时 | |
| 手动平仓冷静(小时) | |
| 复盘情绪冷静(小时) | |
| 日手动平仓次数上限 | |
| 情绪标签日冻结 | |
详见 [account-risk-cooldown.md](./account-risk-cooldown.md)
详见 [account-risk-cooldown.md](./account-risk-cooldown.md).
---
@@ -142,13 +142,13 @@ AI 相关环境变量(`AI_PROVIDER`、`OPENAI_*`、`OLLAMA_*`、`AI_MODEL`、`
| 中文名 | 说明 |
|--------|------|
| 启用自动划转 | |
| 目标余额U | 交易账户目标 USDT |
| 目标余额(U) | 交易账户目标 USDT |
| 划出账户 | funding / swap |
| 划入账户 | swap / funding |
| 执行整点北京时间 | |
| 执行整点(北京时间) | |
| 划转币种 | 默认 USDT |
详见 [auto-transfer-daily.md](./auto-transfer-daily.md)
详见 [auto-transfer-daily.md](./auto-transfer-daily.md).
---
@@ -156,53 +156,53 @@ AI 相关环境变量(`AI_PROVIDER`、`OPENAI_*`、`OLLAMA_*`、`AI_MODEL`、`
| 中文名 | 说明 |
|--------|------|
| 日起始基数U | |
| 回撤后基数U | |
| 盈利后基数U | |
| 日起始基数(U) | |
| 回撤后基数(U) | |
| 盈利后基数(U) | |
与自动划转目标余额相互独立若需一致请手动对齐
与自动划转目标余额相互独立;若需一致请手动对齐.
---
### 4.9 期权账户仅 OKX
### 4.9 期权账户(仅 OKX)
| 中文名 | 说明 |
|--------|------|
| 启用期权模块 | |
| 期权 API Key / Secret / Passphrase | 主账户与永续子账户分离 |
| 期权 API Key / Secret / Passphrase | 主账户,与永续子账户分离 |
| 期权账户备注 | |
| 单笔预算USDC | |
| 单笔预算(USDC) | |
| 预算缓冲比例 | |
| 默认标的 | 如 ETH |
| 最大到期天数 | 等常用策略参数 |
高级参数与完整说明见 [期权方案.md](./期权方案.md)[期权用法.md](./期权用法.md)
高级参数与完整说明见 [期权方案.md](./期权方案.md),[期权用法.md](./期权用法.md).
---
## 5. 操作说明
1. 修改后点 **保存**即时生效项立即应用需重启项写入 `.env` 但未重启进程
2. 含需重启项时点 **保存并重启**`.env` 后 PM2 重启当前实例
3. **重新加载**从磁盘重新读取 `.env` 刷新表单放弃未保存修改)。
4. 敏感项API密钥显示为掩码**留空提交表示不修改原值**
1. 修改后点 **保存**:即时生效项立即应用;需重启项写入 `.env` 但未重启进程.
2. 含需重启项时点 **保存并重启**:`.env` 后 PM2 重启当前实例.
3. **重新加载**:从磁盘重新读取 `.env` 刷新表单(放弃未保存修改).
4. 敏感项(API,密钥)显示为掩码;**留空提交表示不修改原值**.
---
## 6. 隐藏项本页不展示
## 6. 隐藏项(本页不展示)
以下仍存在于 `.env`仅供运维或 SSH 修改
以下仍存在于 `.env`,仅供运维或 SSH 修改:
- 服务`APP_HOST``APP_PORT``APP_DEBUG`
- 数据`DB_PATH``UPLOAD_DIR`
- 关键位门控全部 `KEY_*``KLINE_*`
- 轮询与同步`BALANCE_REFRESH_SECONDS``PRICE_REFRESH_SECONDS``MONITOR_POLL_SECONDS``BREAKEVEN_*``RECONCILE_*`
- 代理`OKX_SOCKS_PROXY``BINANCE_HTTP_PROXY`
- 备份`BACKUP_*`
- 中控嵌入细节`APP_ALLOW_HUB_EMBED``HUB_EMBED_*``APP_COOKIE_SECURE`
- 登录相关`APP_AUTH_DISABLED``APP_USERNAME``APP_PASSWORD``FLASK_SECRET_KEY``HUB_BRIDGE_TOKEN`
- 服务:`APP_HOST`,`APP_PORT`,`APP_DEBUG`
- 数据:`DB_PATH`,`UPLOAD_DIR`
- 关键位门控:全部 `KEY_*`,`KLINE_*`
- 轮询与同步:`BALANCE_REFRESH_SECONDS`,`PRICE_REFRESH_SECONDS`,`MONITOR_POLL_SECONDS`,`BREAKEVEN_*`,`RECONCILE_*`
- 代理:`OKX_SOCKS_PROXY`,`BINANCE_HTTP_PROXY`
- 备份:`BACKUP_*`
- 中控嵌入细节:`APP_ALLOW_HUB_EMBED`,`HUB_EMBED_*`,`APP_COOKIE_SECURE`
- 登录相关:`APP_AUTH_DISABLED`,`APP_USERNAME`,`APP_PASSWORD`,`FLASK_SECRET_KEY`,`HUB_BRIDGE_TOKEN`
后续若需要可增加「高级模式」折叠区默认关闭
后续若需要可增加「高级模式」折叠区,默认关闭.
---
@@ -211,19 +211,19 @@ AI 相关环境变量(`AI_PROVIDER`、`OPENAI_*`、`OLLAMA_*`、`AI_MODEL`、`
| 能力 | env 配置 | 系统设置 | 中控系统设置 |
|------|----------|----------|--------------|
| 登录用户名/密码 | ❌ | ✅ 账户密码修改 | ✅ 中控账户密码 |
| 交易所 API | ✅各所自配 | ❌ | ❌ |
| AI / OpenAI | ❌ | ❌ | ✅ AI 配置同步三所 |
| 交易所 API | ✅(各所自配) | ❌ | ❌ |
| AI / OpenAI | ❌ | ❌ | ✅ AI 配置(同步三所) |
| 导航/区块显示 | ❌ | ✅ 导航显示 | ✅ 显示与导航 |
| 手动资金划转 | ❌ | ✅ 永续资金划转 | ❌ |
| 数据导出 | ❌ | ✅ 数据导出 | ❌ |
| 期权兑换/划转 UI | ❌ | ✅OKX可开关 | ❌ |
| 期权兑换/划转 UI | ❌ | ✅(OKX,可开关) | ❌ |
系统设置说明见 [系统设置说明.md](./系统设置说明.md)中控 AI 与部署密钥见 [中控AI与密钥配置.md](./中控AI与密钥配置.md)
系统设置说明见 [系统设置说明.md](./系统设置说明.md);中控 AI 与部署密钥见 [中控AI与密钥配置.md](./中控AI与密钥配置.md).
---
## 8. 实现备注开发用
## 8. 实现备注(开发用)
- 白名单分组`lib/env/env_ui_manifest.py``exchange_key` 过滤
- 中文标签`ENV_UI_LABELS` 映射模板只渲染 `label` / `note`
- 全量校验仍基于 `.env.example`POST 仅接受 manifest 内 key
- 白名单分组:`lib/env/env_ui_manifest.py`(`exchange_key` 过滤)
- 中文标签:`ENV_UI_LABELS` 映射,模板只渲染 `label` / `note`
- 全量校验仍基于 `.env.example`;POST 仅接受 manifest 内 key
+135 -135
View File
@@ -1,135 +1,135 @@
# 内照明心与永久 K 线
## 概述
「内照明心」页`/archive`用于 **复盘语录 + 交易记录回顾 + 按需 K 线**左侧维护每日复盘语录最多 100 条);右侧按日期区间列出开仓记录展示区间统计并可展开 K 线图表对照单笔交易
与行情区 `hub_kline.db`15 天滚动缓存**完全独立**档案库只增不删从建档起永久保留
## 页面布局
| 区域 | 说明 |
|------|------|
| **复盘语录** | 左栏按日期添加/编辑/删除一日一条 |
| **日期与筛选** | 顶栏本日 / 本周 / 本月 / 自选区间盈利单亏损单犯病交易所搜索 |
| **区间统计** | 统计栏随日期选择自动更新见下 |
| **K 线图表** | 默认折叠点「图表」或展开后按需加载 |
| **交易记录** | 默认展开犯病行 **红色字体**无红底);可编辑标签与备注 |
## 日期区间
交易日按北京时间 **8:00** 切日`TRADING_DAY_RESET_HOUR`)。
| 模式 | 范围 |
|------|------|
| **本日** | 可选单个交易日默认当前交易日 |
| **本周** | 当周周一至当前交易日 |
| **本月** | 当月 1 日至当前交易日 |
| **区间** | 自选 `date_from``date_to`含首尾交易日 |
## 区间统计统计栏
基于当前 **列表筛选结果**含盈利/亏损/犯病勾选合约搜索交易所下拉仍限定数据源):
| 指标 | 说明 |
|------|------|
| 总开仓次数 | 区间内开仓笔数 |
| 盈利单 / 亏损单 | 盈亏 &gt; 0 / &lt; 0 的笔数持平不计 |
| 平均盈利 / 平均亏损 | 盈利单亏损单各自的均值U |
| 最大盈利 / 最大亏损 | 单笔最大盈利最大亏损U |
| 犯病次数 / 占比 | `behavior_tag = sick` 的笔数及占开仓比例 |
| 盈亏 | 区间内全部已平仓盈亏合计 |
| 剔除犯病盈亏 | 排除犯病单后的盈亏合计 |
| 各交易所 | 每所同上分项 |
在搜索框输入币种`BTC`)后,统计栏与下方列表同步按该条件收窄
## 数据约定
| 项 | 约定 |
|----|------|
| 交易来源 | 三所 `trade_records` + 未落库的 `strategy_trade_snapshots``/api/hub/trades/archive` 拉取 |
| 犯病标签 | 中控 `trade_overlay.behavior_tag = sick` |
| K 线真源 | 仅 **5m** 写入 `hub_symbol_archive.db` |
| 建档种子 | 该币 **最早开仓** 向前 **30 天** 5m |
| 增量同步 | 默认每 **4 小时** 补新 5m 至当前 |
| 展示周期 | Tab**5m / 15m / 1h / 4h**默认 **15m** |
| 视窗模式 | **持仓过程**锚平仓默认/ **进场决策**锚开仓 |
| 时间跳转 | 输入 `YYYY-MM-DD HH:MM` 后点「跳转」 |
## 存储
- 默认路径`manual_trading_hub/data/hub_symbol_archive.db`
- 环境变量`HUB_ARCHIVE_DB_PATH`
- 表
- `archive_meta` — 建档元数据
- `archive_bars_5m` — 永久 5m K 线
- `archive_trade_cache` — 从实例同步的交易快照
- `trade_overlay` — 犯病标签与备注仅中控
- `archive_review_quotes` — 复盘语录
## API中控 FastAPI
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/archive/meta` | 周期交易所同步间隔等 |
| GET | `/api/archive/daily-trades` | 区间交易列表与统计见 query |
| GET | `/api/archive/quotes` | 复盘语录列表 |
| POST | `/api/archive/quotes` | 新增语录 |
| PATCH | `/api/archive/quotes/{id}` | 更新语录 |
| DELETE | `/api/archive/quotes/{id}` | 删除语录 |
| GET | `/api/archive/ohlcv` | K 线视窗`timeframe` / `mode` / `anchor_ms` / `at` |
| PATCH | `/api/archive/trade/{exchange_key}/{trade_id}` | 更新标签/备注 |
| POST | `/api/archive/sync` | 立即同步三所交易 + K 线 |
`GET /api/archive/daily-trades` 主要 query
| 参数 | 说明 |
|------|------|
| `period` | `today` / `week` / `month` / `range` |
| `trading_day` | 本日模式下的交易日 `YYYY-MM-DD` |
| `date_from` / `date_to` | 区间模式起止日 |
| `exchange_key` | 可选按交易所筛选 |
| `filter_profit` / `filter_loss` / `filter_sick` | 过滤列表与统计 |
| `search` | 合约 / 交易所 / 备注搜索同步过滤列表与统计 |
返回 `stats``open_count``win_count``loss_count``win_rate``avg_win``avg_loss``profit_loss_ratio``max_win``max_loss``sick_count``sick_pct``pnl_total``pnl_ex_sick``by_exchange`
实例侧
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/hub/trades/archive` | 近 N 天已平仓`days` / `limit` |
## 后台任务
Hub 启动后在 lifespan 中运行 `hub-archive-sync`
1. 对各启用交易所调用 `/api/hub/trades/archive`
2. 写入 `archive_trade_cache`
3. 未建档币种拉 30 天 5m 种子
4. 已建档币种增量补 5m
间隔`HUB_ARCHIVE_SYNC_INTERVAL_SEC`默认 14400)。
## 代码位置
- `hub_symbol_archive_lib.py` — 库表区间统计种子增量聚合
- `hub_trades_lib.py``fetch_trades_for_archive`
- `hub_bridge.py` — 实例 `/api/hub/trades/archive`
- `manual_trading_hub/hub.py` — 路由与后台同步
- `manual_trading_hub/static/archive.js` — 内照明心前端
## 与行情区的区别
| | 行情区 | 内照明心 |
|--|--------|----------|
| DB | `hub_kline.db` | `hub_symbol_archive.db` |
| 保留 | 15 天滚动删除 | 建档起永久 |
| 周期 | 多周期直存/拉取 | 仅存 5m高周期聚合 |
| 用途 | 实时看盘 | 复盘语录与交易回顾 |
## 相关文档
- [中控平仓与交易记录](trend-hub-close-and-trade-records.md)
- [中控使用说明](../manual_trading_hub/使用说明.md)
# 内照明心与永久 K 线
## 概述
「内照明心」页(`/archive`)用于 **复盘语录 + 交易记录回顾 + 按需 K 线**.左侧维护每日复盘语录(最多 100 条);右侧按日期区间列出开仓记录,展示区间统计,并可展开 K 线图表对照单笔交易.
与行情区 `hub_kline.db`(15 天滚动缓存)**完全独立**:档案库只增不删,从建档起永久保留.
## 页面布局
| 区域 | 说明 |
|------|------|
| **复盘语录** | 左栏;按日期添加/编辑/删除,一日一条 |
| **日期与筛选** | 顶栏:本日 / 本周 / 本月 / 自选区间;盈利单,亏损单,犯病,交易所,搜索 |
| **区间统计** | 统计栏随日期选择自动更新(见下) |
| **K 线图表** | 默认折叠;点「图表」或展开后按需加载 |
| **交易记录** | 默认展开;犯病行 **红色字体**(无红底);可编辑标签与备注 |
## 日期区间
交易日按北京时间 **8:00** 切日(`TRADING_DAY_RESET_HOUR`).
| 模式 | 范围 |
|------|------|
| **本日** | 可选单个交易日(默认当前交易日) |
| **本周** | 当周周一至当前交易日 |
| **本月** | 当月 1 日至当前交易日 |
| **区间** | 自选 `date_from``date_to`(含首尾交易日) |
## 区间统计(统计栏)
基于当前 **列表筛选结果**(含盈利/亏损/犯病勾选,合约搜索;交易所下拉仍限定数据源):
| 指标 | 说明 |
|------|------|
| 总开仓次数 | 区间内开仓笔数 |
| 盈利单 / 亏损单 | 盈亏 &gt; 0 / &lt; 0 的笔数(持平不计) |
| 平均盈利 / 平均亏损 | 盈利单,亏损单各自的均值(U) |
| 最大盈利 / 最大亏损 | 单笔最大盈利,最大亏损(U) |
| 犯病次数 / 占比 | `behavior_tag = sick` 的笔数及占开仓比例 |
| 盈亏 | 区间内全部已平仓盈亏合计 |
| 剔除犯病盈亏 | 排除犯病单后的盈亏合计 |
| 各交易所 | 每所同上分项 |
在搜索框输入币种(`BTC`)后,统计栏与下方列表同步按该条件收窄.
## 数据约定
| 项 | 约定 |
|----|------|
| 交易来源 | 三所 `trade_records` + 未落库的 `strategy_trade_snapshots`,`/api/hub/trades/archive` 拉取 |
| 犯病标签 | 中控 `trade_overlay.behavior_tag = sick` |
| K 线真源 | 仅 **5m** 写入 `hub_symbol_archive.db` |
| 建档种子 | 该币 **最早开仓** 向前 **30 天** 5m |
| 增量同步 | 默认每 **4 小时** 补新 5m 至当前 |
| 展示周期 | Tab:**5m / 15m / 1h / 4h**,默认 **15m** |
| 视窗模式 | **持仓过程**(锚平仓,默认)/ **进场决策**(锚开仓) |
| 时间跳转 | 输入 `YYYY-MM-DD HH:MM` 后点「跳转」 |
## 存储
- 默认路径:`manual_trading_hub/data/hub_symbol_archive.db`
- 环境变量:`HUB_ARCHIVE_DB_PATH`
- 表:
- `archive_meta` — 建档元数据
- `archive_bars_5m` — 永久 5m K 线
- `archive_trade_cache` — 从实例同步的交易快照
- `trade_overlay` — 犯病标签与备注(仅中控)
- `archive_review_quotes` — 复盘语录
## API(中控 FastAPI)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/archive/meta` | 周期,交易所,同步间隔等 |
| GET | `/api/archive/daily-trades` | 区间交易列表与统计(见 query) |
| GET | `/api/archive/quotes` | 复盘语录列表 |
| POST | `/api/archive/quotes` | 新增语录 |
| PATCH | `/api/archive/quotes/{id}` | 更新语录 |
| DELETE | `/api/archive/quotes/{id}` | 删除语录 |
| GET | `/api/archive/ohlcv` | K 线视窗(`timeframe` / `mode` / `anchor_ms` / `at`) |
| PATCH | `/api/archive/trade/{exchange_key}/{trade_id}` | 更新标签/备注 |
| POST | `/api/archive/sync` | 立即同步三所交易 + K 线 |
`GET /api/archive/daily-trades` 主要 query:
| 参数 | 说明 |
|------|------|
| `period` | `today` / `week` / `month` / `range` |
| `trading_day` | 本日模式下的交易日 `YYYY-MM-DD` |
| `date_from` / `date_to` | 区间模式起止日 |
| `exchange_key` | 可选,按交易所筛选 |
| `filter_profit` / `filter_loss` / `filter_sick` | 过滤列表与统计 |
| `search` | 合约 / 交易所 / 备注搜索(同步过滤列表与统计) |
返回 `stats``open_count`,`win_count`,`loss_count`,`win_rate`,`avg_win`,`avg_loss`,`profit_loss_ratio`,`max_win`,`max_loss`,`sick_count`,`sick_pct`,`pnl_total`,`pnl_ex_sick`,`by_exchange`.
实例侧:
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/hub/trades/archive` | 近 N 天已平仓(`days` / `limit`) |
## 后台任务
Hub 启动后在 lifespan 中运行 `hub-archive-sync`:
1. 对各启用交易所调用 `/api/hub/trades/archive`
2. 写入 `archive_trade_cache`
3. 未建档币种:拉 30 天 5m 种子
4. 已建档币种:增量补 5m
间隔:`HUB_ARCHIVE_SYNC_INTERVAL_SEC`(默认 14400).
## 代码位置
- `hub_symbol_archive_lib.py` — 库表,区间统计,种子,增量,聚合
- `hub_trades_lib.py``fetch_trades_for_archive`
- `hub_bridge.py` — 实例 `/api/hub/trades/archive`
- `manual_trading_hub/hub.py` — 路由与后台同步
- `manual_trading_hub/static/archive.js` — 内照明心前端
## 与行情区的区别
| | 行情区 | 内照明心 |
|--|--------|----------|
| DB | `hub_kline.db` | `hub_symbol_archive.db` |
| 保留 | 15 天滚动删除 | 建档起永久 |
| 周期 | 多周期直存/拉取 | 仅存 5m,高周期聚合 |
| 用途 | 实时看盘 | 复盘语录与交易回顾 |
## 相关文档
- [中控平仓与交易记录](trend-hub-close-and-trade-records.md)
- [中控使用说明](../manual_trading_hub/使用说明.md)
+147 -147
View File
@@ -1,147 +1,147 @@
# lib/ 共用模块结构
三所实例与中控共用的 Python 库模板与静态资源统一放在仓库根目录的 **`lib/`** 下部署单元`crypto_monitor_*``manual_trading_hub`仍保持独立目录与 PM2 配置不变
**重构前快照 Git 标签**`pre-lib-modularization`可用 `git checkout pre-lib-modularization` 查看旧布局)。
**移除 gate_bot 前快照 Git 标签**`pre-remove-gate-bot`
---
## 顶层目录
```
crypto_monitor/
├── crypto_monitor_binance/ # 三所各自 app + .env + PM2
├── crypto_monitor_gate/
├── crypto_monitor_okx/
├── manual_trading_hub/ # 中控 + 子代理 agent
├── lib/ # 共用模块本说明
│ ├── strategy/
│ ├── key_monitor/
│ ├── trade/
│ ├── hub/
│ ├── ai/
│ ├── instance/
│ ├── exchange/
│ ├── common/
│ └── paths.py
├── brand/ # 各所共用图标
├── docs/
├── deploy/
├── scripts/
├── tests/
├── requirements.txt
└── README.md
```
---
## lib/ 子包说明
| 子包 | 职责 | 主要模块 |
|------|------|----------|
| **`lib/strategy/`** | 策略交易顺势加仓趋势回调快照与记录 | `strategy_register.py``strategy_trend_register.py``strategy_db.py``strategy_roll_*``strategy_trend_*` |
| **`lib/strategy/templates/`** | 策略页 Jinja 模板`strategy_templates/` | `strategy_trading_page.html``strategy_roll_panel.html` 等 |
| **`lib/key_monitor/`** | 关键位监控斐波假突破止盈止损方案 | `key_monitor_lib.py``fib_key_monitor_lib.py``key_sl_tp_lib.py` 等 |
| **`lib/trade/`** | 下单监控展示计仓账户风控手动 SL/TP | `order_monitor_display_lib.py``position_sizing_lib.py``account_risk_lib.py` 等 |
| **`lib/hub/`** | 中控 APIK 线归档计仓器SSO/Bridge | `hub_bridge.py``hub_kline_store.py``hub_trades_lib.py` 等 |
| **`lib/ai/`** | AI 复盘与文本生成 | `ai_client.py``ai_review_lib.py` |
| **`lib/instance/`** | 中控 iframe 嵌入导航复盘图表 | `instance_embed_lib.py``focus_chart_lib.py``journal_chart_lib.py` |
| **`lib/instance/templates/`** | 嵌入页片段`embed_templates/` | `embed_page_fragment.html` |
| **`lib/exchange/`** | 特定交易所工具 | `gate_transfer_lib.py``okx_orders_lib.py` 等 |
| **`lib/common/`** | 跨功能小工具 | `form_submit_lib.py``wechat_notify_lib.py` 等 |
| **`lib/common/static/`** | 三所与中控共用的 JS/CSS原根目录 `static/` | `instance_theme.js``strategy_roll.js` 等 |
> **说明**`hub_*` 命名表示「中控侧能力或行情聚合」但部分模块`hub_volume_rank_lib``hub_market_info_lib`三所 `app.py` 也会调用并非中控独占
---
## 路径辅助函数
`lib/paths.py` 集中维护资源目录避免硬编码
```python
from lib.paths import strategy_templates_dir, embed_templates_dir, common_static_dir
strategy_templates_dir() # .../lib/strategy/templates
embed_templates_dir() # .../lib/instance/templates
common_static_dir() # .../lib/common/static
```
可选传入 `repo_root`字符串或 `Path`),默认使用 `lib/` 的上级目录即仓库根
---
## Python 导入约定
各部署目录在启动时将 **仓库根** 加入 `sys.path`与重构前相同):
```python
_REPO_ROOT = os.path.dirname(BASE_DIR) # 或 Path(__file__).resolve().parent.parent
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
```
之后使用 **`lib.<子包>.<模块>`** 形式导入例如
```python
from lib.strategy.strategy_db import init_strategy_tables
from lib.key_monitor.key_monitor_lib import check_key_monitors
from lib.hub.hub_bridge import install_on_app
from lib.ai.ai_client import ai_review
```
策略注册仍在各所 `app.py` 末尾
```python
from lib.strategy.strategy_register import install_strategy_trading
from lib.strategy.strategy_trend_register import install_strategy_trend
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
```
---
## 静态资源与 URL
- 三所页面仍通过 **`/static/...`** 访问共用脚本`hub_bridge.install_instance_theme_static``lib/common/static/` 提供部分根级静态路由
- 各所目录下 **`static/`**图标上传图片等仍为实例私有未迁入 `lib/`
- 中控 `manual_trading_hub/hub.py` 通过 `_REPO_ROOT / "lib" / "common" / "static"` 挂载与三所共用的 badge复盘 JS 等
---
## 测试
在仓库根执行需将根目录置于 Python 路径或从根目录运行):
```bash
cd /opt/crypto_monitor
python -m unittest discover -s tests -p "test_*.py"
```
测试文件内统一 `from lib.<子包>.<模块> import ...`使用 `@patch` 时目标写完整模块路径例如 `lib.hub.hub_calculator_lib._resolve_market`
---
## 迁移脚本
一次性迁移由 `scripts/migrate_to_lib.py` 完成移动文件 + 批量改写 import)。**不要在已迁移后的仓库上重复执行**
---
## 后续可选整理
- 三所 `app.py` 体量接近可逐步抽取公共 `exchange_app` 基座改动面大单独规划)。
- `manual_trading_hub/okx_orders_lib.py` 为 agent 本地副本可与 `lib/exchange/okx_orders_lib.py` 合并去重
- 可引入 `pyproject.toml` + `pip install -e .`替代 `sys.path.insert`长期维护更规范)。
---
## 相关文档
- [README.md](../README.md) — 总览与部署
- [策略交易说明.md](../策略交易说明.md)
- [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md)
# lib/ 共用模块结构
三所实例与中控共用的 Python 库,模板与静态资源统一放在仓库根目录的 **`lib/`** 下.部署单元(`crypto_monitor_*`,`manual_trading_hub`)仍保持独立目录与 PM2 配置不变.
**重构前快照 Git 标签**:`pre-lib-modularization`(可用 `git checkout pre-lib-modularization` 查看旧布局).
**移除 gate_bot 前快照 Git 标签**:`pre-remove-gate-bot`.
---
## 顶层目录
```
crypto_monitor/
├── crypto_monitor_binance/ # 三所:各自 app + .env + PM2
├── crypto_monitor_gate/
├── crypto_monitor_okx/
├── manual_trading_hub/ # 中控 + 子代理 agent
├── lib/ # 共用模块(本说明)
│ ├── strategy/
│ ├── key_monitor/
│ ├── trade/
│ ├── hub/
│ ├── ai/
│ ├── instance/
│ ├── exchange/
│ ├── common/
│ └── paths.py
├── brand/ # 各所共用图标
├── docs/
├── deploy/
├── scripts/
├── tests/
├── requirements.txt
└── README.md
```
---
## lib/ 子包说明
| 子包 | 职责 | 主要模块 |
|------|------|----------|
| **`lib/strategy/`** | 策略交易(顺势加仓,趋势回调,快照与记录) | `strategy_register.py`,`strategy_trend_register.py`,`strategy_db.py`,`strategy_roll_*`,`strategy_trend_*` |
| **`lib/strategy/templates/`** | 策略页 Jinja 模板(`strategy_templates/`) | `strategy_trading_page.html`,`strategy_roll_panel.html` 等 |
| **`lib/key_monitor/`** | 关键位监控,斐波,假突破,止盈止损方案 | `key_monitor_lib.py`,`fib_key_monitor_lib.py`,`key_sl_tp_lib.py` 等 |
| **`lib/trade/`** | 下单监控展示,计仓,账户风控,手动 SL/TP | `order_monitor_display_lib.py`,`position_sizing_lib.py`,`account_risk_lib.py` 等 |
| **`lib/hub/`** | 中控 API,K 线,归档,计仓器,SSO/Bridge | `hub_bridge.py`,`hub_kline_store.py`,`hub_trades_lib.py` 等 |
| **`lib/ai/`** | AI 复盘与文本生成 | `ai_client.py`,`ai_review_lib.py` |
| **`lib/instance/`** | 中控 iframe 嵌入,导航,复盘图表 | `instance_embed_lib.py`,`focus_chart_lib.py`,`journal_chart_lib.py` |
| **`lib/instance/templates/`** | 嵌入页片段(`embed_templates/`) | `embed_page_fragment.html` |
| **`lib/exchange/`** | 特定交易所工具 | `gate_transfer_lib.py`,`okx_orders_lib.py` 等 |
| **`lib/common/`** | 跨功能小工具 | `form_submit_lib.py`,`wechat_notify_lib.py` 等 |
| **`lib/common/static/`** | 三所与中控共用的 JS/CSS(原根目录 `static/`) | `instance_theme.js`,`strategy_roll.js` 等 |
> **说明**:`hub_*` 命名表示「中控侧能力或行情聚合」,但部分模块(`hub_volume_rank_lib`,`hub_market_info_lib`)三所 `app.py` 也会调用,并非中控独占.
---
## 路径辅助函数
`lib/paths.py` 集中维护资源目录,避免硬编码:
```python
from lib.paths import strategy_templates_dir, embed_templates_dir, common_static_dir
strategy_templates_dir() # .../lib/strategy/templates
embed_templates_dir() # .../lib/instance/templates
common_static_dir() # .../lib/common/static
```
可选传入 `repo_root`(字符串或 `Path`),默认使用 `lib/` 的上级目录即仓库根.
---
## Python 导入约定
各部署目录在启动时将 **仓库根** 加入 `sys.path`(与重构前相同):
```python
_REPO_ROOT = os.path.dirname(BASE_DIR) # 或 Path(__file__).resolve().parent.parent
if _REPO_ROOT not in sys.path:
sys.path.insert(0, _REPO_ROOT)
```
之后使用 **`lib.<子包>.<模块>`** 形式导入,例如:
```python
from lib.strategy.strategy_db import init_strategy_tables
from lib.key_monitor.key_monitor_lib import check_key_monitors
from lib.hub.hub_bridge import install_on_app
from lib.ai.ai_client import ai_review
```
策略注册仍在各所 `app.py` 末尾:
```python
from lib.strategy.strategy_register import install_strategy_trading
from lib.strategy.strategy_trend_register import install_strategy_trend
install_strategy_trading(app, _REPO_ROOT, app_module=sys.modules[__name__])
install_strategy_trend(app, _REPO_ROOT, app_module=sys.modules[__name__])
```
---
## 静态资源与 URL
- 三所页面仍通过 **`/static/...`** 访问共用脚本;`hub_bridge.install_instance_theme_static``lib/common/static/` 提供部分根级静态路由.
- 各所目录下 **`static/`**(图标,上传图片等)仍为实例私有,未迁入 `lib/`.
- 中控 `manual_trading_hub/hub.py` 通过 `_REPO_ROOT / "lib" / "common" / "static"` 挂载与三所共用的 badge,复盘 JS 等.
---
## 测试
在仓库根执行(需将根目录置于 Python 路径,或从根目录运行):
```bash
cd /opt/crypto_monitor
python -m unittest discover -s tests -p "test_*.py"
```
测试文件内统一 `from lib.<子包>.<模块> import ...`.使用 `@patch` 时目标写完整模块路径,例如 `lib.hub.hub_calculator_lib._resolve_market`.
---
## 迁移脚本
一次性迁移由 `scripts/migrate_to_lib.py` 完成(移动文件 + 批量改写 import).**不要在已迁移后的仓库上重复执行**.
---
## 后续可选整理
- 三所 `app.py` 体量接近,可逐步抽取公共 `exchange_app` 基座(改动面大,单独规划).
- `manual_trading_hub/okx_orders_lib.py` 为 agent 本地副本,可与 `lib/exchange/okx_orders_lib.py` 合并去重.
- 可引入 `pyproject.toml` + `pip install -e .`,替代 `sys.path.insert`(长期维护更规范).
---
## 相关文档
- [README.md](../README.md) — 总览与部署
- [策略交易说明.md](../策略交易说明.md)
- [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md)
+22 -22
View File
@@ -1,7 +1,7 @@
# 宏观关键数据 · 风控前置
中控 **系统设置** 手动录入 FOMC / CPI / 就业数据发布时间**监控区** 发布前后各 1 小时给出风险提示
**不看公布结果不解读数据**仅作波动窗口前的行为提醒**不拦截下单**与账户冷静期/日冻结独立)。
中控 **系统设置** 手动录入 FOMC / CPI / 就业数据发布时间,**监控区** 发布前后各 1 小时给出风险提示.
**不看公布结果,不解读数据**,仅作波动窗口前的行为提醒;**不拦截下单**(与账户冷静期/日冻结独立).
## 支持的数据类型
@@ -11,15 +11,15 @@
| `cpi` | 美国 CPI 通胀 |
| `employment` | 就业与劳工数据 |
每项在设置中 **名称下拉三选一****发布时间** 手动输入北京时间精确到分钟)。FOMC 只录 **一条**决议公布时刻即可)。
每项在设置中 **名称下拉三选一**,**发布时间** 手动输入(北京时间,精确到分钟).FOMC 只录 **一条**(决议公布时刻即可).
## 风险窗口
- 默认**发布时间 ±1 小时**
- 发布前 **30 分钟内**文案加强为「即将发布」
- 窗口结束后横幅自动消失设置列表中过期记录逐步不再展示
- 默认:**发布时间 ±1 小时**
- 发布前 **30 分钟内**:文案加强为「即将发布」
- 窗口结束后横幅自动消失;设置列表中过期记录逐步不再展示
环境变量可选):
环境变量(可选):
```env
HUB_MACRO_WINDOW_BEFORE_SEC=3600
@@ -30,34 +30,34 @@ HUB_MACRO_LIST_FUTURE_DAYS=60
## 监控区提示文案
读取当前监控板**任意交易所有持仓 = 有仓**否则 = 无仓
读取当前监控板:**任意交易所有持仓 = 有仓**,否则 = 无仓.
| 场景 | 提示要点 |
|------|----------|
| 无仓 · 窗口内 | 建议等待避免新开仓 |
| 有仓 · 窗口内 | 注意仓位勿加仓检查止损/减仓 |
| 即将发布30 分钟内 | 在上述基础上标注剩余分钟数 |
| 无仓 · 窗口内 | 建议等待,避免新开仓 |
| 有仓 · 窗口内 | 注意仓位,勿加仓,检查止损/减仓 |
| 即将发布(30 分钟内) | 在上述基础上标注剩余分钟数 |
## 存储
- SQLite`manual_trading_hub/data/hub_macro_calendar.db`
- 可覆盖`HUB_MACRO_CALENDAR_DB_PATH`
- SQLite:`manual_trading_hub/data/hub_macro_calendar.db`
- 可覆盖:`HUB_MACRO_CALENDAR_DB_PATH`
`macro_events``event_type`, `event_at_ms`, `note`, `created_at_ms`, `updated_at_ms`
同类型 + 同一发布时间不可重复录入
`macro_events`:`event_type`, `event_at_ms`, `note`, `created_at_ms`, `updated_at_ms`
同类型 + 同一发布时间不可重复录入.
## API均需中控登录
## API(均需中控登录)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/macro-calendar/meta` | 类型列表与窗口说明 |
| GET | `/api/macro-calendar/events` | 设置页列表 |
| GET | `/api/macro-calendar/active` | 当前处于窗口内的事件监控横幅 |
| GET | `/api/macro-calendar/active` | 当前处于窗口内的事件(监控横幅) |
| POST | `/api/macro-calendar/events` | 新增 |
| PATCH | `/api/macro-calendar/events/{id}` | 更新 |
| DELETE | `/api/macro-calendar/events/{id}` | 删除 |
请求体示例
请求体示例:
```json
{
@@ -69,15 +69,15 @@ HUB_MACRO_LIST_FUTURE_DAYS=60
## 使用习惯
1. 每月在金十/日历查看 **FOMCCPI非农** 公布时间
1. 每月在金十/日历查看 **FOMC,CPI,非农** 公布时间
2. 中控 **系统设置 → 宏观关键数据** 录入 13 条
3. 到点前后监控区顶栏出现 **宏观风控** 横幅无操作则窗口结束后自动消失
3. 到点前后监控区顶栏出现 **宏观风控** 横幅;无操作则窗口结束后自动消失
## 与账户风控的关系
| 模块 | 时机 | 作用 |
|------|------|------|
| 宏观日历 | **事前** | 已知高波动窗口提醒等待或管仓 |
| 宏观日历 | **事前** | 已知高波动窗口,提醒等待或管仓 |
| 账户冷静期/日冻结 | **事后** | 用户主动平仓后的惩罚性限制 |
宏观提醒 **不触发** 冷静期不计入手动平仓次数
宏观提醒 **不触发** 冷静期,不计入手动平仓次数.
+31 -31
View File
@@ -1,31 +1,31 @@
# 实盘下单 · 预估盈亏比
## 功能
三所Binance / OKX / Gate**实盘下单监控**表单中在「开仓」按钮前显示 **预估盈亏比**
- **价格模式**填完币种方向止损价止盈价后调用 `GET /api/order_defaults` 取标记价按几何距离计算 RR
- **百分比模式**填完币种方向止损%止盈% 后拉快照校验币种再显示 RR`止盈% / 止损%`)。
- **固定盈亏比模式**盈亏比由输入框直接指定下方预览条显示预估风险/盈利/盈亏比不再在表单行内显示预估止盈价)。
- **以损定仓**`POSITION_SIZING_MODE=risk`):预估风险 = 当前交易基数 × `risk%`
- **全仓杠杆**`full_margin`):预估风险 = 合约可用 × 缓冲比例 × 杠杆BTC/ETH 与山寨按 `.env` 配置× 止损距离比例与开仓时 `calc_risk_amount_from_plan` 一致
## 前端实现
- 共享脚本`static/manual_order_rr_preview.js`
- 各所 `templates/index.html` 引入并在 `MANUAL_MIN_PLANNED_RR` 定义后执行
```js
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
```
- 展示元素`#order-rr-preview`开仓按钮左侧
- 颜色≥ 最低要求为绿色低于为红色无效/取价失败为红色或灰色
## 与提交校验
提交时仍走原有 `calcClientRr` / `calcClientRrFromPct``rejectManualOrderRr`预估仅用于下单前参考不替代服务端风控
## 校验记录
- `node --check static/manual_order_rr_preview.js`
- `tests/test_manual_order_rr_preview.py`RR 公式与三所 `calc_rr_ratio` 口径一致
# 实盘下单 · 预估盈亏比
## 功能
三所(Binance / OKX / Gate)**实盘下单监控**表单中,在「开仓」按钮前显示 **预估盈亏比**.
- **价格模式**:填完币种,方向,止损价,止盈价后,调用 `GET /api/order_defaults` 取标记价,按几何距离计算 RR.
- **百分比模式**:填完币种,方向,止损%,止盈% 后拉快照校验币种,再显示 RR(`止盈% / 止损%`).
- **固定盈亏比模式**:盈亏比由输入框直接指定;下方预览条显示预估风险/盈利/盈亏比(不再在表单行内显示预估止盈价).
- **以损定仓**(`POSITION_SIZING_MODE=risk`):预估风险 = 当前交易基数 × `risk%`.
- **全仓杠杆**(`full_margin`):预估风险 = 合约可用 × 缓冲比例 × 杠杆(BTC/ETH 与山寨按 `.env` 配置)× 止损距离比例,与开仓时 `calc_risk_amount_from_plan` 一致.
## 前端实现
- 共享脚本:`static/manual_order_rr_preview.js`
- 各所 `templates/index.html` 引入并在 `MANUAL_MIN_PLANNED_RR` 定义后执行:
```js
ManualOrderRrPreview.wire({ minRr: MANUAL_MIN_PLANNED_RR });
```
- 展示元素:`#order-rr-preview`(开仓按钮左侧)
- 颜色:≥ 最低要求为绿色,低于为红色,无效/取价失败为红色或灰色
## 与提交校验
提交时仍走原有 `calcClientRr` / `calcClientRrFromPct``rejectManualOrderRr`;预估仅用于下单前参考,不替代服务端风控.
## 校验记录
- `node --check static/manual_order_rr_preview.js`
- `tests/test_manual_order_rr_preview.py`:RR 公式与三所 `calc_rr_ratio` 口径一致
+73 -73
View File
@@ -1,73 +1,73 @@
# 计仓模式三所统一
## 配置
在各实例 `.env` 中设置**仅能通过 env 切换修改后须重启进程**):
```env
# risk默认= 以损定仓
# full_margin = 全仓杠杆合约可用保证金 × 比例
POSITION_SIZING_MODE=risk
FULL_MARGIN_BUFFER_RATIO=0.98
# 关键位程序自动单默认 false详见各所 关键位自动下单说明.md
KEY_AUTO_ORDER_ENABLED=false
```
切换为全仓杠杆前**交易所须无持仓**`MAX_ACTIVE_POSITIONS` 默认 1全仓模式会强制单仓)。
## 模式说明
| 模式 | 保证金计算 | 杠杆 | 允许入口 |
|------|------------|------|----------|
| `risk` | `RISK_PERCENT` × 交易资金按止损距离反推 | 表单可选 / 同步交易所 | 实盘人工关键位自动`KEY_AUTO_ORDER_ENABLED=true`)、趋势回调顺势加仓 |
| `full_margin` | **合约账户可用 USDT × `FULL_MARGIN_BUFFER_RATIO`**保留 2 位小数 | BTC/ETH **10x**其它 **5x**`BTC_LEVERAGE`/`ALT_LEVERAGE` 一致 | **实盘人工下单****关键位触价**`KEY_AUTO_ORDER_ENABLED=true`);阻力/支撑仅提醒 |
全仓模式下
- **`KEY_AUTO_ORDER_ENABLED=false`默认** 时触价程序自动单也不执行
- **`KEY_AUTO_ORDER_ENABLED=true`** 时仅触价可程序自动开仓箱体/斐波等仍禁止
- 仍校验 **计划盈亏比**实盘用 `MANUAL_MIN_PLANNED_RR`触价开仓用 `KEY_AUTO_MIN_PLANNED_RR`)。
- 下单张数由 `prepare_order_amount` + 交易所 `amount_to_precision` 决定
- `order_monitors.initial_stop_loss` 仍记录**开仓时**止损快照交易记录复盘以该快照为准
- 已存在的 **箱体突破 / 收敛突破 / 斐波 / 假突破** 监控进程启动时**自动撤销**并企业微信通知
## 不允许全仓模式
- 关键位箱体突破收敛突破斐波假突破添加时拒绝已存在则启动时撤销)。
- 趋势回调顺势加仓策略入口返回明确错误)。
**允许`KEY_AUTO_ORDER_ENABLED=true`):** 关键位 **回调触价开仓** / **突破触价开仓**程序盯价触达/穿越计划入场后市价成交无交易所挂单全仓下仅允许一条待触发)。
## `KEY_AUTO_ORDER_ENABLED`三所统一默认 `false`
| 计仓 | 开关 | 效果 |
|------|------|------|
| `risk` | `false` | 关闭全部关键位程序自动单含触价);顺势加仓不受影响 |
| `risk` | `true` | 关键位全套自动旧行为 |
| `full_margin` | `false` | 关闭触价自动 |
| `full_margin` | `true` | 仅触价自动 |
详见各实例目录 `关键位自动下单说明.md`
## 用脚本更新三所 `.env`
详见 **[env-sync-scripts.md](./env-sync-scripts.md)**常用命令
```bash
git pull
# 仅补全计仓相关项缺省 risk缓冲 0.98
python scripts/sync_four_exchange_position_sizing_env.py
# 无仓后切换全仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode full_margin
# 无仓后切回以损定仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
# 计仓 + 划转一并补全
python scripts/sync_four_exchange_env.py
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
```
# 计仓模式(三所统一)
## 配置
在各实例 `.env` 中设置(**仅能通过 env 切换,修改后须重启进程**):
```env
# risk(默认)= 以损定仓
# full_margin = 全仓杠杆(合约可用保证金 × 比例)
POSITION_SIZING_MODE=risk
FULL_MARGIN_BUFFER_RATIO=0.98
# 关键位程序自动单(默认 false,详见各所 关键位自动下单说明.md)
KEY_AUTO_ORDER_ENABLED=false
```
切换为全仓杠杆前:**交易所须无持仓**(`MAX_ACTIVE_POSITIONS` 默认 1,全仓模式会强制单仓).
## 模式说明
| 模式 | 保证金计算 | 杠杆 | 允许入口 |
|------|------------|------|----------|
| `risk` | `RISK_PERCENT` × 交易资金,按止损距离反推 | 表单可选 / 同步交易所 | 实盘人工,关键位自动(`KEY_AUTO_ORDER_ENABLED=true`),趋势回调,顺势加仓 |
| `full_margin` | **合约账户可用 USDT × `FULL_MARGIN_BUFFER_RATIO`**(保留 2 位小数) | BTC/ETH **10x**,其它 **5x**(`BTC_LEVERAGE`/`ALT_LEVERAGE` 一致) | **实盘人工下单**,**关键位触价**(`KEY_AUTO_ORDER_ENABLED=true`);阻力/支撑仅提醒 |
全仓模式下:
- **`KEY_AUTO_ORDER_ENABLED=false`(默认)** 时,触价程序自动单也不执行.
- **`KEY_AUTO_ORDER_ENABLED=true`** 时,仅触价可程序自动开仓;箱体/斐波等仍禁止.
- 仍校验 **计划盈亏比**(实盘用 `MANUAL_MIN_PLANNED_RR`;触价开仓用 `KEY_AUTO_MIN_PLANNED_RR`).
- 下单张数由 `prepare_order_amount` + 交易所 `amount_to_precision` 决定.
- `order_monitors.initial_stop_loss` 仍记录**开仓时**止损快照;交易记录复盘以该快照为准.
- 已存在的 **箱体突破 / 收敛突破 / 斐波 / 假突破** 监控:进程启动时**自动撤销**并企业微信通知.
## 不允许(全仓模式)
- 关键位:箱体突破,收敛突破,斐波,假突破(添加时拒绝;已存在则启动时撤销).
- 趋势回调,顺势加仓(策略入口返回明确错误).
**允许(`KEY_AUTO_ORDER_ENABLED=true`):** 关键位 **回调触价开仓** / **突破触价开仓**(程序盯价,触达/穿越计划入场后市价成交,无交易所挂单;全仓下仅允许一条待触发).
## `KEY_AUTO_ORDER_ENABLED`(三所统一,默认 `false`)
| 计仓 | 开关 | 效果 |
|------|------|------|
| `risk` | `false` | 关闭全部关键位程序自动单(含触价);顺势加仓不受影响 |
| `risk` | `true` | 关键位全套自动(旧行为) |
| `full_margin` | `false` | 关闭触价自动 |
| `full_margin` | `true` | 仅触价自动 |
详见各实例目录 `关键位自动下单说明.md`.
## 用脚本更新三所 `.env`
详见 **[env-sync-scripts.md](./env-sync-scripts.md)**.常用命令:
```bash
git pull
# 仅补全计仓相关项(缺省 risk,缓冲 0.98)
python scripts/sync_four_exchange_position_sizing_env.py
# 无仓后切换全仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode full_margin
# 无仓后切回以损定仓
python scripts/sync_four_exchange_position_sizing_env.py --set-mode risk
# 计仓 + 划转一并补全
python scripts/sync_four_exchange_env.py
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate
```
+41 -41
View File
@@ -1,41 +1,41 @@
# Chrome 桌面快捷方式图标说明
## 图标从哪来
用 Chrome **「创建快捷方式」** 或 **「安装应用」** 时桌面/开始菜单图标**不是**操作系统自带的而是浏览器从**你打开的网站**读取的优先级大致为
1. `manifest.webmanifest` 里的 `icons`192×192512×512
2. `link rel="apple-touch-icon"`约 180×180
3. `link rel="icon"` / `favicon.ico`
4. 若都没有 → 灰色地球或网页标题首字
本仓库已在 **中控****三所监控页** 配置统一品牌图标深色圆角底 + 青绿趋势线 + 简化的 K 线),与页面 UI 一致PNG/ICO 由 **Pillow** 生成避免损坏的 favicon 出现花屏
## 文件位置
| 位置 | 访问路径 |
|------|----------|
| 源稿 | `brand/icon.svg``brand/icons/*.png` |
| 中控 | `manual_trading_hub/static/icons/``/assets/icons/...` |
| 三所 | `crypto_monitor_*/static/icons/``/static/icons/...` |
## 重新生成 / 同步
```bash
python scripts/generate_brand_icons.py
python scripts/sync_brand_icons.py
git pull # 服务器部署后
pm2 restart …
```
## 快捷方式仍显示旧图标
Chrome / Windows 会**缓存** favicon
1. 浏览器打开站点**Ctrl+F5** 强刷
2. 删除旧快捷方式重新「创建快捷方式」
3. 必要时清除 Chrome 站点数据该域名后再创建
## 自定义图标
可替换 `brand/icon.svg` 后重新运行上面两条命令或把设计好的 `icon-192.png``icon-512.png` 放入 `brand/icons/``sync_brand_icons.py`
# Chrome 桌面快捷方式图标说明
## 图标从哪来?
用 Chrome **「创建快捷方式」** 或 **「安装应用」** 时,桌面/开始菜单图标**不是**操作系统自带的,而是浏览器从**你打开的网站**读取的,优先级大致为:
1. `manifest.webmanifest` 里的 `icons`(192×192,512×512)
2. `link rel="apple-touch-icon"`(约 180×180)
3. `link rel="icon"` / `favicon.ico`
4. 若都没有 → 灰色地球或网页标题首字
本仓库已在 **中控****三所监控页** 配置统一品牌图标(深色圆角底 + 青绿趋势线 + 简化的 K 线),与页面 UI 一致.PNG/ICO 由 **Pillow** 生成,避免损坏的 favicon 出现花屏.
## 文件位置
| 位置 | 访问路径 |
|------|----------|
| 源稿 | `brand/icon.svg`,`brand/icons/*.png` |
| 中控 | `manual_trading_hub/static/icons/``/assets/icons/...` |
| 三所 | `crypto_monitor_*/static/icons/``/static/icons/...` |
## 重新生成 / 同步
```bash
python scripts/generate_brand_icons.py
python scripts/sync_brand_icons.py
git pull # 服务器部署后
pm2 restart …
```
## 快捷方式仍显示旧图标?
Chrome / Windows 会**缓存** favicon:
1. 浏览器打开站点,**Ctrl+F5** 强刷
2. 删除旧快捷方式,重新「创建快捷方式」
3. 必要时清除 Chrome 站点数据(该域名)后再创建
## 自定义图标
可替换 `brand/icon.svg` 后重新运行上面两条命令;或把设计好的 `icon-192.png`,`icon-512.png` 放入 `brand/icons/``sync_brand_icons.py`.
+34 -34
View File
@@ -1,34 +1,34 @@
# 策略文档
各交易实例的人工下单策略供 UI / 复盘对齐
| 文档 | 实例 | 状态 |
|------|------|------|
| [binance-alt-trend-long.md](./binance-alt-trend-long.md) | 币安山寨·多头趋势 | v0.4 讨论稿 |
| [okx-trend-both.md](./okx-trend-both.md) | OKX·多空趋势 | v0.4 讨论稿 |
| [gate-intraday.md](./gate-intraday.md) | Gate·BTC 日内 | v0.2 |
## 约定
- **不写盈亏比趋势户**币安/OKX 止盈/止损随行情人工设定趋势 MD 不量化 RR
- **日内例外**Gate 日内 **最低 1:1** 才开仓持仓目标可动态调整见 [gate-intraday.md](./gate-intraday.md))。
- **界面趋势户 · v0.4**两级 — **反转**启动 A/B| **顺势**大分歧 A/B| **波段**小分歧);`lib/trade/entry_model_lib.py` 三所共用
- **界面短标签日内户**`假破` / `结构突破`
- **多空共用三字**方向由「做多/做空」表达不复用为「大分歧A多」等
- **自动联动**大分歧 A/B → 趋势单**小分歧 → 波段单**平仓写入交易记录 `entry_reason`复盘「填入」自动带入
- **杠杆默认**BTC/ETH **10x**其它 **5x**与开仓类型无关env `BTC_LEVERAGE` / `ALT_LEVERAGE`)。
- **日内 profile 独立**env 启用 `TRADE_SYMBOL_WHITELIST=BTC,ETH` 且限制开启时**不显示** 大分歧三项Gate);开仓类型为 **假破 / 结构突破**见 gate-intraday.md)。
- **日内 0 点出场**策略称「0 点平仓」程序为 `FORCE_CLOSE_ENABLED` + `FORCE_CLOSE_BJ_HOUR=0`交易记录 `result=强制清仓`与表单 1h/2h/4h `time_close` 无关)。
- **策略模块独立**趋势回调顺势加仓不走上述三项
## 系统实现
- 库`lib/trade/entry_model_lib.py`
- 趋势户表单`lib/instance/templates/order_entry_model_fields.html`
- 日内判定`is_intraday_trading_profile()`白名单仅含 BTC/ETH
- 0 点强平`force_close_before_reset()`三所 `app.py`);env `FORCE_CLOSE_ENABLED` / `FORCE_CLOSE_BJ_HOUR`
## 相关文档
- [计仓模式](../position-sizing-mode.md)
- [趋势回调策略](../trend-pullback-strategy.md)
# 策略文档
各交易实例的人工下单策略,供 UI / 复盘对齐.
| 文档 | 实例 | 状态 |
|------|------|------|
| [binance-alt-trend-long.md](./binance-alt-trend-long.md) | 币安山寨·多头趋势 | v0.4 讨论稿 |
| [okx-trend-both.md](./okx-trend-both.md) | OKX·多空趋势 | v0.4 讨论稿 |
| [gate-intraday.md](./gate-intraday.md) | Gate·BTC 日内 | v0.2 |
## 约定
- **不写盈亏比(趋势户)**:币安/OKX 止盈/止损随行情人工设定,趋势 MD 不量化 RR.
- **日内例外**:Gate 日内 **最低 1:1** 才开仓,持仓目标可动态调整(见 [gate-intraday.md](./gate-intraday.md)).
- **界面(趋势户 · v0.4)**:两级 — **反转**(启动 A/B)| **顺势**(大分歧 A/B)| **波段**(小分歧);`lib/trade/entry_model_lib.py` 三所共用.
- **界面短标签(日内户)**:`假破` / `结构突破`.
- **多空共用三字**:方向由「做多/做空」表达,不复用为「大分歧A多」等.
- **自动联动**:大分歧 A/B → 趋势单;**小分歧 → 波段单**;平仓写入交易记录 `entry_reason`;复盘「填入」自动带入.
- **杠杆默认**:BTC/ETH **10x**,其它 **5x**;与开仓类型无关(env `BTC_LEVERAGE` / `ALT_LEVERAGE`).
- **日内 profile 独立**:env 启用 `TRADE_SYMBOL_WHITELIST=BTC,ETH` 且限制开启时,**不显示** 大分歧三项(Gate);开仓类型为 **假破 / 结构突破**(见 gate-intraday.md).
- **日内 0 点出场**:策略称「0 点平仓」;程序为 `FORCE_CLOSE_ENABLED` + `FORCE_CLOSE_BJ_HOUR=0`,交易记录 `result=强制清仓`(与表单 1h/2h/4h `time_close` 无关).
- **策略模块独立**:趋势回调,顺势加仓不走上述三项.
## 系统实现
- 库:`lib/trade/entry_model_lib.py`
- 趋势户表单:`lib/instance/templates/order_entry_model_fields.html`
- 日内判定:`is_intraday_trading_profile()`(白名单仅含 BTC/ETH)
- 0 点强平:`force_close_before_reset()`(三所 `app.py`);env `FORCE_CLOSE_ENABLED` / `FORCE_CLOSE_BJ_HOUR`
## 相关文档
- [计仓模式](../position-sizing-mode.md)
- [趋势回调策略](../trend-pullback-strategy.md)
+194 -194
View File
@@ -1,194 +1,194 @@
# 币安山寨·多头趋势账户
> **状态**v0.4反转·启动 A/B 两级 UI 已实现策略正文 + `entry_model_lib`
---
## 1. 账户定位
| 项 | 说明 |
|----|------|
| 交易所 | 币安合约 |
| 方向 | **仅做多**`TRADE_DIRECTION=long_only` |
| 计仓 | `POSITION_SIZING_MODE=risk` |
| 关键位自动单 | `KEY_AUTO_ORDER_ENABLED=false` |
| UI profile | **趋势户**非 BTC/ETH 白名单日内 |
---
## 2. 开仓类型两级 UI · 三所共用
### 2.1 趋势户反转 / 顺势 / 波段
| 第一级 | 第二级 | code | 联动 |
|--------|--------|------|------|
| **反转** | 启动 A / 启动 B | `launch_a` / `launch_b` | 趋势单 |
| **顺势** | 大分歧 A / 大分歧 B | `big_div_a` / `big_div_b` | 趋势单 |
| **波段** | 小分歧 | `small_div` | 波段单 |
实现`lib/trade/entry_model_lib.py` + `order_entry_model_fields.html` + `order_entry_model.js`币安 / OKX / Gate 趋势户共用)。
### 2.2 杠杆与开仓类型无关
| 币种 | 默认杠杆 |
|------|----------|
| BTCETH | **10x**`BTC_LEVERAGE` |
| 其它山寨 | **5x**`ALT_LEVERAGE` |
---
## 3. 反转·启动做多讨论定稿
**性质**反转 — 在**新主升确认之前**的作战与大分歧顺势不是同一行情阶段
**周期**背离与箱体以 **4h** 为主**日线与 4h 同处筑底阶段时结构边界以日线为准**作战后半段可用 **5m** 节奏启动 A2)。
### 3.0 流程总览
```text
MACD 背离严格筛选→ 标「参考高点」
第 1 次到高点附近 → 不做V 形
回落箱内中间复杂形态不盯
第 2 次到高点附近 → 开战
├─ 见小收敛 ──────────→ 启动 AA1
└─ 不见收敛 ──────────→ 启动 B实体突破
├─ 成 → 主升后续才用顺势·大分歧 / 波段·小分歧
└─ 败 → 止损正常
跌破箱高一半 → 暂弃等再次到高点附近
仍在上半区 + 5m 不创新低 + N 字 → 启动 AA2
```
### 3.1 背离何时进入候选池MACD · 4h
全部满足才承认「有过背离」
| 规则 | 说明 |
|------|------|
| 级别 | **至少 4h** MACD 底背离 |
| 首次背离不做 | 第一段背离只观察不交易 |
| 通道式下跌不做 | 顺滑通道下滑中的背离不当反转依据 |
| 波段结构 | 有明显的波段高点低点 |
| 分段下跌 | **三段及以上**明显下跌之后才开始寻找背离 |
背离确认后**标注最后一个显著高点为「参考高点」**后文「高点附近」均相对此点)。
> 横盘**时长不量化**关键是顺序**跌 → 背离 → 背离后的震荡**而非下跌中继
### 3.2 参考高点两次摸高
| 次序 | 规则 |
|------|------|
| **第 1 次**到参考高点附近 | **不关注不做**假突破未突破都算「到过附近」— 视为 **V 形反弹**风险区 |
| 回落箱内 | 箱内复杂形态**不是主战场**不强行交易 |
| **第 2 次**到参考高点附近 | **开始关注**进入作战区 |
### 3.3 启动 B实体突破
**条件**第二次及以后靠近参考高点且**未见**再次摸高前的小收敛
| 项 | 说明 |
|----|------|
| 入场 | **实体突破**参考高点 / 箱体上沿影线刺破不算 |
| 成功 | 走出主升 → 后续单型转为顺势·大分歧 / 波段·小分歧 |
| 失败 | 突破后未主升回到箱内震荡 → **止损属正常** → 若满足 §3.4下一笔用 **启动 AA2** |
### 3.4 启动 A结构内 / 试仓含 A1 与 A2
**性质**主升**确认前**在箱内找风险可控入场**不是**小分歧
#### A1 · 第二次摸高前的小收敛
- 在第 2 次到参考高点**之前**出现**小的收敛结构** → 突破前企稳进场
- 若**看不到**小收敛不强行做 A1改走 **启动 B**§3.3)。
#### A2 · 启动 B 失败后的 5m N 字仍记启动 A
在启动 B 止损后
| 暂弃 | 可试启动 A2 |
|------|-------------|
| 回箱后继续跌且**跌破箱体高度一半** | **未**跌破箱一半 |
| 暂不看直至**再次**到参考高点附近 | 且 **5m 不创新低** |
| | 且 **突破 5m 高点** → 按 **5m N 字形突破** 试仓 |
- 5m 止损一般不大此位置**允许多次试错**突破路径约可错 2 次5m 试仓约可错 3 次 — 同一参考高点周期内具体计数实操自定)。
- **5m N 字试仓不单列开仓类型**复盘统一记 **启动 A**可备注「A2 / 5m N」)。
### 3.5 与大分歧小分歧的边界
| | 反转·启动 | 顺势·大分歧 | 波段·小分歧 |
|---|-----------|-------------|-------------|
| 前端 | 跌 → 背离 → 箱 → 两次摸高 | 主升已确立 | 主升已确立 |
| V 形 | 第 1 次摸高不做 | — | — |
| 5m N 试仓 | **启动 AA2** | — | 不同于小分歧 |
| 第三次不做 | — | — | ✓ |
---
## 4. 顺势·大分歧趋势单
**前提****主升浪已确立**上方仍有空间前端是主升里的整理**不要**求「跌 → 背离 → 箱 → 两次摸高」那条反转链
### 4.1 大分歧 A
- 4h/日线大结构向上
- 5m/15m 收敛**不创新低** 企稳进不等突破
### 4.2 大分歧 B
- 同上大级别多头结构
- **突破确认** 后入场实体突破优先
---
## 5. 波段·小分歧波段单
**前提**主升**已走出**反转链启动 A/B进行中**不做**小分歧
- **前两次**可做**第三次不做**
- 低吸为主不追突破
- 入场二次探底 → N 字突破或 5m 三均线重新多头排列
---
## 6. 纪律
1. 不做空
2. 反转首次背离不做通道跌背离不做第 1 次摸参考高点不做V 形
3. 顺势第三次小分歧不做新单小分歧不追突破
4. 止盈/止损/是否手平**随行情**本文档不量化 RR
---
## 7. 持仓与出场定性
| 单型 | 说明 |
|------|------|
| 启动 A/B | 赌新主升B 失败可转 A2未确立主升前不做小分歧 |
| 大分歧 | 可长持途中两次小分歧后远目标未到**可手平** |
| 小分歧 | 短拿常手平 |
---
## 8. 系统字段
| 操作 | 字段 |
|------|------|
| 下单 | `order_monitors.entry_model``launch_a` / `launch_b` / `big_div_a` / `big_div_b` / `small_div` |
| 平仓 | `trade_records.entry_reason` = 界面标签 |
| 复盘 | 与开仓类型一致 + 策略项 +「其他」 |
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.4 | 2026-07-06 | 反转·启动 A/B 两级 UI 上线共用 entry_model_lib |
| v0.2 | 2026-07-06 | 定稿 UI 短标签大分歧/小分歧);实现代码联动 |
| v0.1 | 2026-07-06 | 讨论稿 |
# 币安山寨·多头趋势账户
> **状态**:v0.4(反转·启动 A/B 两级 UI 已实现;策略正文 + `entry_model_lib`)
---
## 1. 账户定位
| 项 | 说明 |
|----|------|
| 交易所 | 币安合约 |
| 方向 | **仅做多**(`TRADE_DIRECTION=long_only`) |
| 计仓 | `POSITION_SIZING_MODE=risk` |
| 关键位自动单 | `KEY_AUTO_ORDER_ENABLED=false` |
| UI profile | **趋势户**(非 BTC/ETH 白名单日内) |
---
## 2. 开仓类型(两级 UI · 三所共用)
### 2.1 趋势户:反转 / 顺势 / 波段
| 第一级 | 第二级 | code | 联动 |
|--------|--------|------|------|
| **反转** | 启动 A / 启动 B | `launch_a` / `launch_b` | 趋势单 |
| **顺势** | 大分歧 A / 大分歧 B | `big_div_a` / `big_div_b` | 趋势单 |
| **波段** | 小分歧 | `small_div` | 波段单 |
实现:`lib/trade/entry_model_lib.py` + `order_entry_model_fields.html` + `order_entry_model.js`(币安 / OKX / Gate 趋势户共用).
### 2.2 杠杆(与开仓类型无关)
| 币种 | 默认杠杆 |
|------|----------|
| BTC,ETH | **10x**(`BTC_LEVERAGE`) |
| 其它山寨 | **5x**(`ALT_LEVERAGE`) |
---
## 3. 反转·启动(做多,讨论定稿)
**性质**:反转 — 在**新主升确认之前**的作战;与大分歧(顺势)不是同一行情阶段.
**周期**:背离与箱体以 **4h** 为主;**日线与 4h 同处筑底阶段时,结构边界以日线为准**.作战后半段可用 **5m** 节奏(启动 A2).
### 3.0 流程总览
```text
MACD 背离(严格筛选)→ 标「参考高点」
第 1 次到高点附近 → 不做(V 形)
回落箱内(中间复杂形态不盯)
第 2 次到高点附近 → 开战
├─ 见小收敛 ──────────→ 启动 A(A1)
└─ 不见收敛 ──────────→ 启动 B(实体突破)
├─ 成 → 主升(后续才用顺势·大分歧 / 波段·小分歧)
└─ 败 → 止损(正常)
跌破箱高一半 → 暂弃,等再次到高点附近
仍在上半区 + 5m 不创新低 + N 字 → 启动 A(A2)
```
### 3.1 背离:何时进入候选池(MACD · 4h)
全部满足才承认「有过背离」:
| 规则 | 说明 |
|------|------|
| 级别 | **至少 4h** MACD 底背离 |
| 首次背离不做 | 第一段背离只观察,不交易 |
| 通道式下跌不做 | 顺滑通道下滑中的背离,不当反转依据 |
| 波段结构 | 有明显的波段高点,低点 |
| 分段下跌 | **三段及以上**明显下跌之后,才开始寻找背离 |
背离确认后:**标注最后一个显著高点为「参考高点」**(后文「高点附近」均相对此点).
> 横盘**时长不量化**;关键是顺序:**跌 → 背离 → 背离后的震荡**,而非下跌中继.
### 3.2 参考高点:两次摸高
| 次序 | 规则 |
|------|------|
| **第 1 次**到参考高点附近 | **不关注,不做**.假突破,未突破都算「到过附近」— 视为 **V 形反弹**风险区 |
| 回落箱内 | 箱内复杂形态**不是主战场**,不强行交易 |
| **第 2 次**到参考高点附近 | **开始关注**,进入作战区 |
### 3.3 启动 B(实体突破)
**条件**:第二次(及以后)靠近参考高点,且**未见**再次摸高前的小收敛.
| 项 | 说明 |
|----|------|
| 入场 | **实体突破**参考高点 / 箱体上沿(影线刺破不算) |
| 成功 | 走出主升 → 后续单型转为顺势·大分歧 / 波段·小分歧 |
| 失败 | 突破后未主升,回到箱内震荡 → **止损属正常** → 若满足 §3.4,下一笔用 **启动 A(A2)** |
### 3.4 启动 A(结构内 / 试仓,含 A1 与 A2)
**性质**:主升**确认前**在箱内找风险可控入场;**不是**小分歧.
#### A1 · 第二次摸高前的小收敛
- 在第 2 次到参考高点**之前**,出现**小的收敛结构** → 突破前企稳进场.
- 若**看不到**小收敛,不强行做 A1,改走 **启动 B**(§3.3).
#### A2 · 启动 B 失败后的 5m N 字(仍记启动 A)
在启动 B 止损后:
| 暂弃 | 可试启动 A2 |
|------|-------------|
| 回箱后继续跌,且**跌破箱体高度一半** | **未**跌破箱一半 |
| 暂不看,直至**再次**到参考高点附近 | 且 **5m 不创新低** |
| | 且 **突破 5m 高点** → 按 **5m N 字形突破** 试仓 |
- 5m 止损一般不大;此位置**允许多次试错**(突破路径约可错 2 次;5m 试仓约可错 3 次 — 同一参考高点周期内,具体计数实操自定).
- **5m N 字试仓不单列开仓类型**,复盘统一记 **启动 A**(可备注「A2 / 5m N」).
### 3.5 与大分歧,小分歧的边界
| | 反转·启动 | 顺势·大分歧 | 波段·小分歧 |
|---|-----------|-------------|-------------|
| 前端 | 跌 → 背离 → 箱 → 两次摸高 | 主升已确立 | 主升已确立 |
| V 形 | 第 1 次摸高不做 | — | — |
| 5m N 试仓 | **启动 A(A2)** | — | 不同于小分歧 |
| 第三次不做 | — | — | ✓ |
---
## 4. 顺势·大分歧(趋势单)
**前提**:**主升浪已确立**,上方仍有空间;前端是主升里的整理,**不要**求「跌 → 背离 → 箱 → 两次摸高」那条反转链.
### 4.1 大分歧 A
- 4h/日线大结构向上
- 5m/15m 收敛,**不创新低** 企稳进(不等突破)
### 4.2 大分歧 B
- 同上大级别多头结构
- **突破确认** 后入场(实体突破优先)
---
## 5. 波段·小分歧(波段单)
**前提**:主升**已走出**;反转链(启动 A/B)进行中**不做**小分歧.
- **前两次**可做,**第三次不做**
- 低吸为主,不追突破
- 入场:二次探底 → N 字突破;或 5m 三均线重新多头排列
---
## 6. 纪律
1. 不做空
2. 反转:首次背离不做;通道跌背离不做;第 1 次摸参考高点不做(V 形)
3. 顺势:第三次小分歧不做新单;小分歧不追突破
4. 止盈/止损/是否手平:**随行情**,本文档不量化 RR
---
## 7. 持仓与出场(定性)
| 单型 | 说明 |
|------|------|
| 启动 A/B | 赌新主升;B 失败可转 A2;未确立主升前不做小分歧 |
| 大分歧 | 可长持;途中两次小分歧后远目标未到,**可手平** |
| 小分歧 | 短拿,常手平 |
---
## 8. 系统字段
| 操作 | 字段 |
|------|------|
| 下单 | `order_monitors.entry_model`:`launch_a` / `launch_b` / `big_div_a` / `big_div_b` / `small_div` |
| 平仓 | `trade_records.entry_reason` = 界面标签 |
| 复盘 | 与开仓类型一致 + 策略项 +「其他」 |
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.4 | 2026-07-06 | 反转·启动 A/B 两级 UI 上线(共用 entry_model_lib) |
| v0.2 | 2026-07-06 | 定稿 UI 短标签(大分歧/小分歧);实现代码联动 |
| v0.1 | 2026-07-06 | 讨论稿 |
+26 -26
View File
@@ -6,63 +6,63 @@
{
"title": "账户与方向",
"items": [
"本账户仅做多不做空",
"计仓模式为以损定仓risk),关键位自动单已关闭",
"已明确第一级反转 / 顺势 / 波段两级下拉已上线"
"本账户仅做多,不做空",
"计仓模式为以损定仓(risk),关键位自动单已关闭",
"已明确第一级:反转 / 顺势 / 波段(两级下拉已上线)"
]
},
{
"title": "反转 · 背离与箱体启动 A/B 共同前置",
"title": "反转 · 背离与箱体(启动 A/B 共同前置)",
"items": [
"4h MACD 底背离非首次背离非通道式下跌中的背离",
"有明显波段高/低点且三段及以上明显下跌后才认背离",
"背离后处于震荡箱体时长不量化非 V 形急跌急拉",
"已标注参考高点最后一个显著高点",
"第 1 次到参考高点附近 → 不做第 2 次到附近 → 才进入作战区"
"4h MACD 底背离(非首次背离,非通道式下跌中的背离)",
"有明显波段高/低点,且三段及以上明显下跌后才认背离",
"背离后处于震荡箱体(时长不量化;非 V 形急跌急拉)",
"已标注参考高点(最后一个显著高点)",
"第 1 次到参考高点附近 → 不做;第 2 次到附近 → 才进入作战区"
]
},
{
"title": "反转 · 启动 B实体突破",
"title": "反转 · 启动 B(实体突破)",
"items": [
"第 2 次到参考高点附近且未见再次摸高前的小收敛",
"实体突破参考高点/箱体上沿非仅影线",
"突破失败回箱止损属正常跌破箱高一半则暂弃直至再次到高点附近"
"第 2 次到参考高点附近,且未见再次摸高前的小收敛",
"实体突破参考高点/箱体上沿(非仅影线)",
"突破失败回箱止损属正常;跌破箱高一半则暂弃直至再次到高点附近"
]
},
{
"title": "反转 · 启动 A结构内含 A1 / A2",
"title": "反转 · 启动 A(结构内,含 A1 / A2)",
"items": [
"性质主升确认前不是小分歧",
"A1第 2 次摸高前出现小收敛 → 突破前企稳",
"A2启动 B 止损后未跌破箱高一半 + 5m 不创新低 + 5m N 字突破试仓",
"5m 试仓记为启动 A不单列类型);小止损允许多次试错"
"性质:主升确认前;不是小分歧",
"A1:第 2 次摸高前出现小收敛 → 突破前企稳",
"A2:启动 B 止损后,未跌破箱高一半 + 5m 不创新低 + 5m N 字突破试仓",
"5m 试仓记为启动 A(不单列类型);小止损允许多次试错"
]
},
{
"title": "顺势 · 大分歧 A / B",
"items": [
"主升已确立上方仍有空间非跌后背离筑底阶段",
"大分歧A5m/15m 收敛且不创新低企稳",
"大分歧B突破已确认优先实体突破"
"主升已确立,上方仍有空间(非跌后背离筑底阶段)",
"大分歧A:5m/15m 收敛且不创新低企稳",
"大分歧B:突破已确认,优先实体突破"
]
},
{
"title": "波段 · 小分歧",
"items": [
"主升已确立反转链进行中不做小分歧",
"主升已确立;反转链进行中不做小分歧",
"第三次小分歧 → 不做新单",
"不追突破二次探底 N 字或 5m 三均线重新多头"
"不追突破;二次探底 N 字或 5m 三均线重新多头"
]
},
{
"title": "杠杆与出场",
"items": [
"杠杆BTC/ETH 10x其它山寨 5x可选手改但须有理由",
"止盈止损随行情人工设定不在此清单量化 RR"
"杠杆:BTC/ETH 10x,其它山寨 5x(可选手改但须有理由)",
"止盈止损随行情人工设定,不在此清单量化 RR"
]
}
],
"footnotes": [
"v0.4两级 UI 已实现启动 A 含 A1 收敛与 A2B 失败后 5m N 字)。"
"v0.4:两级 UI 已实现;启动 A 含 A1 收敛与 A2(B 失败后 5m N 字)."
]
}
+13 -13
View File
@@ -6,33 +6,33 @@
{
"title": "方向与均线过滤",
"items": [
"仅交易 BTC同时仅 1 仓",
"15m 21/55/144 排列清晰多或空),纠缠则不做",
"仅交易 BTC,同时仅 1 仓",
"15m 21/55/144 排列清晰(多或空),纠缠则不做",
"1H 方向与 15m 不冲突",
"21 均线关系满足回踩支撑 / 站稳上方(多)或反弹承压 / 压在下方(空)"
"21 均线关系满足:回踩支撑 / 站稳上方(多)或反弹承压 / 压在下方(空)"
]
},
{
"title": "开仓类型 A / B",
"items": [
"已选定假破 或 结构突破二选一",
"假破扫流动性后回到结构内5m N 字 + 15m 顶/底分型齐全",
"结构突破15m 收盘价站稳关键位非仅刺破",
"止损带宽 0.4%1.5%超出则不做",
"下单前空间至少 1:1不足则不做"
"已选定:假破 或 结构突破(二选一)",
"假破:扫流动性后回到结构内,5m N 字 + 15m 顶/底分型齐全",
"结构突破:15m 收盘价站稳关键位,非仅刺破",
"止损带宽 0.4%1.5%,超出则不做",
"下单前空间至少 1:1,不足则不做"
]
},
{
"title": "一日节奏与笔数",
"items": [
"非周末在早窗 / 晚窗计划时段内",
"今日笔数未达上限 3连错未达 2 笔",
"宽幅震荡S1时降频或不做",
"0 点前须了结系统强制清仓);本清单不含手动平仓"
"非周末;在早窗 / 晚窗计划时段内",
"今日笔数未达上限 3,连错未达 2 笔",
"宽幅震荡(S1)时降频或不做",
"0 点前须了结(系统强制清仓);本清单不含手动平仓"
]
}
],
"footnotes": [
"系统FORCE_CLOSE_ENABLED 开启时北京时间 0 点自动强制清仓result=强制清仓"
"系统:FORCE_CLOSE_ENABLED 开启时,北京时间 0 点自动强制清仓(result=强制清仓)"
]
}
+28 -28
View File
@@ -6,67 +6,67 @@
{
"title": "账户与方向",
"items": [
"已选定做多或做空且与 4H/大级别结构方向一致",
"趋势户 profile非 Gate 日内 BTC/ETH 白名单",
"计仓模式为以损定仓risk),关键位自动单已关闭",
"已选定做多或做空,且与 4H/大级别结构方向一致",
"趋势户 profile(非 Gate 日内 BTC/ETH 白名单)",
"计仓模式为以损定仓(risk),关键位自动单已关闭",
"同一币种无未计划的对冲叠仓",
"已明确第一级反转 / 顺势 / 波段两级下拉已上线"
"已明确第一级:反转 / 顺势 / 波段(两级下拉已上线)"
]
},
{
"title": "反转 · 背离与箱体启动 A/B 共同前置",
"title": "反转 · 背离与箱体(启动 A/B 共同前置)",
"items": [
"做多4h MACD 底背离做空4h MACD 顶背离",
"非首次背离非通道式涨跌中的背离明显波段高低点 + 三段及以上涨/跌后才认背离",
"背离后处于震荡箱体时长不量化非 V 形急拉急杀",
"做多已标参考高点做空已标参考低点",
"第 1 次到参考极值附近 → 不做第 2 次到附近 → 才进入作战区"
"做多:4h MACD 底背离;做空:4h MACD 顶背离",
"非首次背离;非通道式涨跌中的背离;明显波段高低点 + 三段及以上涨/跌后才认背离",
"背离后处于震荡箱体(时长不量化;非 V 形急拉急杀)",
"做多:已标参考高点;做空:已标参考低点",
"第 1 次到参考极值附近 → 不做;第 2 次到附近 → 才进入作战区"
]
},
{
"title": "反转 · 启动 B实体突破",
"title": "反转 · 启动 B(实体突破)",
"items": [
"第 2 次到参考极值附近且未见再次摸极值前的小收敛",
"做多实体突破参考高点/箱顶做空实体跌破参考低点/箱底",
"第 2 次到参考极值附近,且未见再次摸极值前的小收敛",
"做多:实体突破参考高点/箱顶;做空:实体跌破参考低点/箱底",
"突破失败回箱止损属正常",
"做多跌破箱高一半暂弃做空涨破箱低一半暂弃直至再次到极值附近"
"做多:跌破箱高一半暂弃;做空:涨破箱低一半暂弃;直至再次到极值附近"
]
},
{
"title": "反转 · 启动 A结构内含 A1 / A2",
"title": "反转 · 启动 A(结构内,含 A1 / A2)",
"items": [
"性质主趋势确认前不是小分歧",
"A1第 2 次摸极值前出现小收敛 → 突破前企稳",
"A2 做多B 止损后未跌破箱一半 + 5m 不创新低 + 5m N 字",
"A2 做空B 止损后未涨破箱一半 + 5m 不创新高 + 5m 倒 N 字",
"5m 试仓记为启动 A不单列);小止损允许多次试错"
"性质:主趋势确认前;不是小分歧",
"A1:第 2 次摸极值前出现小收敛 → 突破前企稳",
"A2 做多:B 止损后未跌破箱一半 + 5m 不创新低 + 5m N 字",
"A2 做空:B 止损后未涨破箱一半 + 5m 不创新高 + 5m 倒 N 字",
"5m 试仓记为启动 A(不单列);小止损允许多次试错"
]
},
{
"title": "顺势 · 大分歧 A / B",
"items": [
"主趋势已确立非涨/跌后背离筑底/筑顶阶段",
"做多大分歧A5m/15m 收敛且不创新低做空不创新高",
"大分歧B突破已确认优先实体突破"
"主趋势已确立(非涨/跌后背离筑底/筑顶阶段)",
"做多大分歧A:5m/15m 收敛且不创新低;做空:不创新高",
"大分歧B:突破已确认,优先实体突破"
]
},
{
"title": "波段 · 小分歧",
"items": [
"主趋势已确立反转链进行中不做小分歧",
"主趋势已确立;反转链进行中不做小分歧",
"第三次小分歧 → 不做新单",
"做多二次探底 N 字 / 5m 三均线多头做空二次探顶倒 N / 5m 空头"
"做多:二次探底 N 字 / 5m 三均线多头;做空:二次探顶倒 N / 5m 空头"
]
},
{
"title": "杠杆与出场",
"items": [
"杠杆BTC/ETH 10x其它山寨 5x可选手改但须有理由",
"止盈止损随行情人工设定不在此清单量化 RR"
"杠杆:BTC/ETH 10x,其它山寨 5x(可选手改但须有理由)",
"止盈止损随行情人工设定,不在此清单量化 RR"
]
}
],
"footnotes": [
"v0.4两级 UI 反转/顺势/波段做多细则见 binance-alt-trend-long.md §3"
"v0.4:两级 UI 反转/顺势/波段;做多细则见 binance-alt-trend-long.md §3."
]
}
+101 -101
View File
@@ -1,6 +1,6 @@
# Gate·BTC 日内账户
> **状态**v0.2策略定稿**0 点强平已实现**日内 UI 隐藏平仓/委托/移动保本 **待实现**
> **状态**:v0.2(策略定稿;**0 点强平已实现**;日内 UI 隐藏平仓/委托/移动保本 **待实现**)
---
@@ -10,143 +10,143 @@
|----|------|
| 交易所 | Gate 合约 |
| 品种 | **仅 BTC** |
| 方向 | **多空都做**由过滤条件决定非手选方向 |
| 计仓 | `POSITION_SIZING_MODE=full_margin`全仓杠杆 |
| UI profile | **日内户**`TRADE_SYMBOL_WHITELIST=BTC,ETH` 且限制开启本策略只交易 BTC |
| 与趋势户关系 | **不使用** 大分歧 A/B/小分歧**不使用**「趋势单 / 波段单」手选 |
| 方向 | **多空都做**(由过滤条件决定,非手选方向) |
| 计仓 | `POSITION_SIZING_MODE=full_margin`(全仓杠杆) |
| UI profile | **日内户**(`TRADE_SYMBOL_WHITELIST=BTC,ETH` 且限制开启;本策略只交易 BTC) |
| 与趋势户关系 | **不使用** 大分歧 A/B/小分歧;**不使用**「趋势单 / 波段单」手选 |
### 资金与杠杆执行约定
### 资金与杠杆(执行约定)
| 项 | 说明 |
|----|------|
| 账户规模 | 约 300U测试阶段 |
| 日交易基数 | **50U**早 8:00 重置为 50U不延续前日阶梯 |
| 单笔阶梯 | 上一笔 **+10U / 10U** 调节下一笔基数赢 60U / 亏 40U 等 |
| 账户规模 | 约 300U(测试阶段) |
| 日交易基数 | **50U**(早 8:00 重置为 50U,不延续前日阶梯) |
| 单笔阶梯 | 上一笔 **+10U / 10U** 调节下一笔基数(赢 60U / 亏 40U 等) |
| 杠杆 | **10× 全仓** |
| 一次一单 | 同时仅 **1** 个 Gate 仓位 |
| 止损带宽 | **0.4%1.5%**结构要求更宽则 **不做** |
| 止损带宽 | **0.4%1.5%**(结构要求更宽则 **不做**) |
---
## 2. 周期分层
自上而下**先定能不能做再做哪一类**
自上而下,**先定能不能做,再做哪一类**:
| 层级 | 周期 | 作用 |
|------|------|------|
| 方向过滤 | **1H** | 大方向**不得与 15m 排列反向** |
| 均线 + 结构 | **15m** | 21/55/144 排列顶底分型结构识别**B 类收盘突破** |
| 触发 | **5m** | **A 类**N 字形突破配合 15m 分型 |
| 方法 | 裸 K + 三均线 | 形态确认入场与止损锚点 |
| 方向过滤 | **1H** | 大方向;**不得与 15m 排列反向** |
| 均线 + 结构 | **15m** | 21/55/144 排列,顶底分型,结构识别,**B 类收盘突破** |
| 触发 | **5m** | **A 类**:N 字形突破(配合 15m 分型) |
| 方法 | 裸 K + 三均线 | 形态确认,入场与止损锚点 |
**不做「趋势单」概念**持仓以 **小时**当日了结与币安/OKX 多日趋势户区分
**不做「趋势单」概念**:持仓以 **小时**,当日了结;与币安/OKX 多日趋势户区分.
---
## 3. 方向过滤必过
## 3. 方向过滤(必过)
### 3.1 15m 三均线21 / 55 / 144
### 3.1 15m 三均线(21 / 55 / 144)
| 15m 排列 | 只允许 |
|----------|--------|
| **多头排列**21 > 55 > 144 | **只做多** |
| **空头排列**21 < 55 < 144 | **只做空** |
| 纠缠粘合不符合 | **不做** |
| **多头排列**(21 > 55 > 144) | **只做多** |
| **空头排列**(21 < 55 < 144) | **只做空** |
| 纠缠,粘合,不符合 | **不做** |
### 3.2 与 1H 同向
- **做多**15m 多头排列**1H 不得为空头排列**1H 均线不能与 15m 方向相反)。
- **做空**15m 空头排列**1H 不得为多头排列**
- 1H/15m 方向冲突 → **当日该方向不做**
- **做多**:15m 多头排列,**1H 不得为空头排列**(1H 均线不能与 15m 方向相反).
- **做空**:15m 空头排列,**1H 不得为多头排列**.
- 1H/15m 方向冲突 → **当日该方向不做**.
### 3.3 21 均线关系才允许开仓
### 3.3 21 均线关系(才允许开仓)
入场须与 **21 均线** 发生有效关系避免 distant 追单
入场须与 **21 均线** 发生有效关系,避免 distant 追单:
| 方向 | 要求定性 |
| 方向 | 要求(定性) |
|------|----------------|
| **做多** | 多头排列下**回踩 21 附近获支撑** 或 **站稳 21 上方** 后再按 playbook 入场 |
| **做空** | 空头排列下**反弹 21 附近承压** 或 **压在 21 下方** 后再按 playbook 入场 |
| **做多** | 多头排列下,**回踩 21 附近获支撑** 或 **站稳 21 上方** 后再按 playbook 入场 |
| **做空** | 空头排列下,**反弹 21 附近承压** 或 **压在 21 下方** 后再按 playbook 入场 |
---
## 4. 开仓类型仅两类
## 4. 开仓类型(仅两类)
界面日后仅两个短标签全称见下表 hover / 本文):
界面日后仅两个短标签(全称见下表 hover / 本文):
| 界面标签 | 存储 code建议 | 本质 |
| 界面标签 | 存储 code(建议) | 本质 |
|----------|-------------------|------|
| **假破** | `liquidity_false_break` | 流动性扫单 → 假突破验证 → **5m N 字****15m 顶/底分型** |
| **结构突破** | `structure_breakout` | **15m 结构有效突破****收盘确认** |
| **结构突破** | `structure_breakout` | **15m 结构有效突破**(**收盘确认**) |
子结构 **不单独占主下拉**可在复盘备注或二级标签中记录
子结构 **不单独占主下拉**,可在复盘备注或二级标签中记录.
---
## 5. A 类假破流动性 / 假突破
## 5. A 类:假破(流动性 / 假突破)
**适用**关键位附近 **扫止损** 后价格 **回到结构内**陷阱确认后再反向做
**适用**:关键位附近 **扫止损** 后价格 **回到结构内**,陷阱确认后再反向做.
### 5.1 流程
```text
1H/15m 方向 + 21 均线过滤通过
→ 假突破出现扫高/扫低
→ 验证为「假」收回结构内 / 反向裸 K 确认
→ 5m 走出 N 字二次探底/探顶后沿允许方向突破
→ 15m 出现底分型(多)或顶分型(空)
→ 假突破出现(扫高/扫低)
→ 验证为「假」(收回结构内 / 反向裸 K 确认)
→ 5m 走出 N 字(二次探底/探顶后,沿允许方向突破)
→ 15m 出现底分型(多)或顶分型(空)
→ 入场
```
### 5.2 做多 / 做空对称
### 5.2 做多 / 做空(对称)
| 步骤 | 做多 | 做空 |
|------|------|------|
| 假破 | 向下扫低后快速拉回支撑/箱上 | 向上扫高后跌回阻力/箱下 |
| 5m N 字 | 扫低 → 反弹 → 不破前低 → 向上突破 | 扫高 → 回落 → 不过前高 → 向下突破 |
| 15m 确认 | **底分型** | **顶分型** |
| 止损 | 假破极值或 N 字低点 **外侧**仍须落在 0.4%1.5% | 对称 |
| 目标 | **最低 1:1**之后 **随行情动态** 部分止盈移动止损或延伸 | 对称 |
| 止损 | 假破极值或 N 字低点 **外侧**(仍须落在 0.4%1.5%) | 对称 |
| 目标 | **最低 1:1**;之后 **随行情动态** 部分止盈,移动止损或延伸 | 对称 |
### 5.3 注意
- **须等假破验证完成**扫完不追
- **5m N + 15m 分型** 为入场必要条件缺一不可
- 与大级别 **宽幅震荡S1** 叠加时假信号多优先 **降频或不做**
- **须等假破验证完成**,扫完不追.
- **5m N + 15m 分型** 为入场必要条件,缺一不可.
- 与大级别 **宽幅震荡(S1)** 叠加时假信号多,优先 **降频或不做**.
---
## 6. B 类结构突破
## 6. B 类:结构突破
**适用**15m 上结构清晰方向与均线排列一致**收盘突破** 后顺势做
**适用**:15m 上结构清晰,方向与均线排列一致,**收盘突破** 后顺势做.
### 6.1 子结构均属 B 类
### 6.1 子结构(均属 B 类)
双顶双底头肩顶/底收敛三角/楔形)、箱体等——**统一记为「结构突破」**
双顶,双底,头肩顶/底,收敛(三角/楔形),箱体等——**统一记为「结构突破」**.
### 6.2 突破确认
- **以 15m K 线收盘价为准** 突破关键位颈线箱边收敛边界等)。
- **仅刺破未收盘站稳****不算** 有效突破不做
- 可选**收盘突破后回踩** 再进裸 K 确认),仍须满足 21 均线关系与 **≥1:1** 空间
- **以 15m K 线收盘价为准** 突破关键位(颈线,箱边,收敛边界等).
- **仅刺破,未收盘站稳****不算** 有效突破,不做.
- 可选:**收盘突破后回踩** 再进(裸 K 确认),仍须满足 21 均线关系与 **≥1:1** 空间.
### 6.3 止损与目标
| 项 | 说明 |
|----|------|
| 止损 | 结构另一侧或突破位回退点 **外侧**0.4%1.5%超出则不做 |
| 目标 | 下单前 **至少 1:1**到位后 **随行情动态** 调整不写死固定 RR |
| 止损 | 结构另一侧或突破位回退点 **外侧**(0.4%1.5%,超出则不做) |
| 目标 | 下单前 **至少 1:1**;到位后 **随行情动态** 调整,不写死固定 RR |
| 空间不足 | 最近阻力/支撑导致 **达不到 1:1****不做** |
---
## 7. 行情状态辅助过滤
## 7. 行情状态(辅助过滤)
| 状态 | 特征 | Gate 动作 |
|------|------|-----------|
| **S0 趋势** | 1H/15m 排列清晰高低点有序 | 正常A/B 均可 |
| **S1 宽幅震荡** | 大箱横盘多日均线反复穿 | **降频或不做** |
| **S2 末期/选边** | 贴边收敛刚突破或假破频发 | 优先 **A 假破****B 收敛突破** |
| **S0 趋势** | 1H/15m 排列清晰,高低点有序 | 正常:A/B 均可 |
| **S1 宽幅震荡** | 大箱横盘多日,均线反复穿 | **降频或不做** |
| **S2 末期/选边** | 贴边收敛,刚突破或假破频发 | 优先 **A 假破****B 收敛突破** |
---
@@ -155,19 +155,19 @@
| 项 | 规则 |
|----|------|
| 周末 | **不开新仓** |
| 早窗 | 约 **9:00**8:0012:00 内),**计划内第 1 笔** |
| 下午 | **默认不开新仓**持仓可保留至晚窗 |
| 晚窗 | 约 **21:00**20:0023:00 内),**计划内第 2 笔** |
| 第 3 笔 | 仅当 **未连错 2 笔****早/晚有一笔为止损出场****补 1 笔** |
| 早窗 | 约 **9:00**(8:0012:00 内),**计划内第 1 笔** |
| 下午 | **默认不开新仓**(持仓可保留至晚窗) |
| 晚窗 | 约 **21:00**(20:0023:00 内),**计划内第 2 笔** |
| 第 3 笔 | 仅当 **未连错 2 笔**,**早/晚有一笔为止损出场**,**补 1 笔** |
| 日上限 | **最多 3 笔** |
| **连错 2 笔** | **当日不再开新仓**第 3 笔名额作废 |
| **连错 2 笔** | **当日不再开新仓**(第 3 笔名额作废) |
**连错计数**
**连错计数**:
| 出场 | 是否算「错 1 笔」 |
|------|------------------|
| **计划止损**触发 | ✅ 算 |
| **0 点强制清仓**系统结果 `强制清仓`且亏损 | ✅ 算 |
| **0 点强制清仓**(系统结果 `强制清仓`)且亏损 | ✅ 算 |
| 止盈 / ≥1:1 按计划平 | ❌ 不算 |
| 0 点强制清仓且盈利或平推 | ❌ 不算 |
@@ -177,53 +177,53 @@
### 9.1 盈亏比
- 开仓前**第一目标空间 ≥ 止损距离最低 1:1**
- 持仓中目标 **随行情动态** 调整本文档 **不量化** 固定止盈比例
- 开仓前:**第一目标空间 ≥ 止损距离(最低 1:1)**.
- 持仓中:目标 **随行情动态** 调整;本文档 **不量化** 固定止盈比例.
### 9.2 禁止「手动止损」
- **亏损出场** 必须来自 **开仓时设定的计划止损**交易所或监控等价执行)。
- **禁止** 盘中亏着 **手点平仓** 充当止损破坏统计与连错规则)。
- 若违规手动平亏**视为当日纪律失败建议停手**复盘结果 **不得** 记为「止损」糊弄统计
- **亏损出场** 必须来自 **开仓时设定的计划止损**(交易所或监控等价执行).
- **禁止** 盘中亏着 **手点平仓** 充当止损(破坏统计与连错规则).
- 若违规手动平亏:**视为当日纪律失败,建议停手**;复盘结果 **不得** 记为「止损」糊弄统计.
### 9.3 时间出场仅 0 点程序已实现
### 9.3 时间出场:仅 0 点(程序已实现)
- **唯一** 时间类出场**当日 0:00北京时间前必须空仓**赚赔都平)。
- **不使用** 下单表单里的 1h / 2h / 4h「开仓后 N 小时平」`time_close`);与本策略无关
- **程序兜底**三所共用Gate 已启用):
- **唯一** 时间类出场:**当日 0:00(北京时间)前必须空仓**(赚赔都平).
- **不使用** 下单表单里的 1h / 2h / 4h「开仓后 N 小时平」(`time_close`);与本策略无关.
- **程序兜底**(三所共用,Gate 已启用):
| env | 说明 |
|-----|------|
| `FORCE_CLOSE_ENABLED=true` | 开启整点强制清仓 |
| `FORCE_CLOSE_BJ_HOUR=0` | 北京时间 **0 点那一小时**00:0000:59执行 |
| `FORCE_CLOSE_BJ_HOUR=0` | 北京时间 **0 点那一小时**(00:0000:59)执行 |
- 实现`force_close_before_reset()`各实例 `app.py` 后台循环调用)。
- 行为对该小时仍 **active**`order_monitors` **市价全平**取消交易所触发单写交易记录
- **系统结果字段**`result = 强制清仓`备注含「北京时间 0:00 整点风控清仓」
- **策略口语「0 点平仓」= 系统「强制清仓」**统计连错时按 §8 盈亏判定不按字段名区分
- 实现:`force_close_before_reset()`(各实例 `app.py` 后台循环调用).
- 行为:对该小时仍 **active**`order_monitors` **市价全平**,取消交易所触发单,写交易记录.
- **系统结果字段**:`result = 强制清仓`;备注含「北京时间 0:00 整点风控清仓」.
- **策略口语「0 点平仓」= 系统「强制清仓」**,统计连错时按 §8 盈亏判定,不按字段名区分.
> **与 `TRADING_DAY_RESET_HOUR=8` 无关**后者只切 **交易日**统计8 点前禁开等),**不会**自动平仓
> **与 `TRADING_DAY_RESET_HOUR=8` 无关**:后者只切 **交易日**(统计,8 点前禁开等),**不会**自动平仓.
### 9.4 允许的出场类型统计用
### 9.4 允许的出场类型(统计用)
| 策略说法 | 系统 `result` | 说明 |
|----------|---------------|------|
| 止盈 | 止盈 / 移动止盈 / 保本止盈 等 | 计划止盈或 ≥1:1 后按计划/动态平 |
| 止损 | 止损 | 仅 **计划止损** 触发 |
| 0 点平仓 | **强制清仓** | 整点风控兜底§9.3 |
| ~~手动平仓~~ | 手动平仓 | **策略禁止**除极端技术故障等须复盘说明 |
| 0 点平仓 | **强制清仓** | 整点风控兜底(§9.3) |
| ~~手动平仓~~ | 手动平仓 | **策略禁止**(除极端技术故障等,须复盘说明) |
---
## 10. A / B 如何选择当日
## 10. A / B 如何选择(当日)
| 盘面 | 优先 |
|------|------|
| 刚扫流动性回到箱内 | **A 假破** |
| 结构清晰排列已顺收敛末端 | **B 结构突破** |
| 大箱乱扫均线粘合 | **不做** |
| 刚扫流动性,回到箱内 | **A 假破** |
| 结构清晰,排列已顺,收敛末端 | **B 结构突破** |
| 大箱乱扫,均线粘合 | **不做** |
早/晚窗 **有形态才做**无形态 = **0 笔**不占额度
早/晚窗 **有形态才做**,无形态 = **0 笔**,不占额度.
---
@@ -243,13 +243,13 @@
| 项 | 说明 |
|----|------|
| 日内 profile 判定 | `is_intraday_trading_profile()``lib/trade/entry_model_lib.py` |
| 0 点强制清仓 | `FORCE_CLOSE_ENABLED` + `FORCE_CLOSE_BJ_HOUR``force_close_before_reset()`结果 **`强制清仓`** |
| UI 标识 | 顶栏 **强制清仓 已开启** 徽章 + 持仓卡片 **倒计时**三所 + 中控 |
| 交易记录展示 | 三所 UI / 中控`强制清仓` 与止损同类 badge |
| 三所统一 | 币安 / OKX / Gate 同一函数与 env**将来改日内只需各所 `.env` 打开无需改代码** |
| 日内 profile 判定 | `is_intraday_trading_profile()`(`lib/trade/entry_model_lib.py`) |
| 0 点强制清仓 | `FORCE_CLOSE_ENABLED` + `FORCE_CLOSE_BJ_HOUR`;`force_close_before_reset()`;结果 **`强制清仓`** |
| UI 标识 | 顶栏 **强制清仓 已开启** 徽章 + 持仓卡片 **倒计时**(三所 + 中控) |
| 交易记录展示 | 三所 UI / 中控:`强制清仓` 与止损同类 badge |
| 三所统一 | 币安 / OKX / Gate 同一函数与 env;**将来改日内只需各所 `.env` 打开,无需改代码** |
Gate 当前建议 env节选):
Gate 当前建议 env(节选):
```env
FORCE_CLOSE_ENABLED=true
@@ -257,14 +257,14 @@ FORCE_CLOSE_BJ_HOUR=0
TRADING_DAY_RESET_HOUR=8
```
### 12.2 待实现UI / 纪律
### 12.2 待实现(UI / 纪律)
| 项 | 说明 |
|----|------|
| 开仓类型 | 界面 **`假破` / `结构突破`**code`liquidity_false_break` / `structure_breakout`);**无** trend/swing 手选 |
| 开仓类型 | 界面 **`假破` / `结构突破`**(code:`liquidity_false_break` / `structure_breakout`);**无** trend/swing 手选 |
| 写入字段 | `trade_records.entry_model` / 复盘下拉同两项 |
| 隐藏操作 | 日内 profile 下 **隐藏** 平仓委托移动保本**实例页 + 中控**`intraday_discipline` / `order_entry_profile=intraday` |
| 隐藏表单项 | 不展示 1h/2h/4h 时间平仓移动保本勾选避免与 §9.3 混用 |
| 隐藏操作 | 日内 profile 下 **隐藏** 平仓,委托,移动保本(**实例页 + 中控**,`intraday_discipline` / `order_entry_profile=intraday`) |
| 隐藏表单项 | 不展示 1h/2h/4h 时间平仓,移动保本勾选(避免与 §9.3 混用) |
| 后端可选 | 严格模式下拒绝 `del_order` / 改委托 API |
---
@@ -273,5 +273,5 @@ TRADING_DAY_RESET_HOUR=8
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.1 | 2026-07-06 | 定稿BTC 日内1H+15m 均线A 假破5m N+15m 分型);B 结构突破15m 收盘);早1晚1/最多3笔/连错2停禁手动止损仅 0 点强平 |
| v0.2 | 2026-07-06 | §9.3/§12对齐 `FORCE_CLOSE_*` 与系统结果「强制清仓」区分 `time_close` / `TRADING_DAY_RESET_HOUR`Gate 已启用说明 |
| v0.1 | 2026-07-06 | 定稿:BTC 日内;1H+15m 均线;A 假破(5m N+15m 分型);B 结构突破(15m 收盘);早1晚1/最多3笔/连错2停;禁手动止损;仅 0 点强平 |
| v0.2 | 2026-07-06 | §9.3/§12:对齐 `FORCE_CLOSE_*` 与系统结果「强制清仓」;区分 `time_close` / `TRADING_DAY_RESET_HOUR`;Gate 已启用说明 |
+157 -157
View File
@@ -1,157 +1,157 @@
# OKX·多空趋势账户
> **状态**v0.4讨论稿反转·启动 A/B**UI 仍为 v0.2 三档**两级下拉待实现
---
## 1. 账户定位
| 项 | 说明 |
|----|------|
| 交易所 | OKX 永续合约 |
| 方向 | **做多 + 做空**`TRADE_DIRECTION=both`可按需限制 |
| 计仓 | `POSITION_SIZING_MODE=risk` |
| 关键位自动单 | `KEY_AUTO_ORDER_ENABLED=false` |
| UI profile | **趋势户**非 Gate BTC/ETH 日内白名单 |
**选方向原则**开仓前确认 4H / 日线大级别与所选「做多/做空」一致逆势单不在本策略范围内
做多侧反转细则与 [binance-alt-trend-long.md §3](./binance-alt-trend-long.md) 同构本文 **§3.2** 给出做空镜像
---
## 2. 开仓类型规划与现状
### 2.1 规划两级选择与币安共用实现
| 第一级 | 第二级 | 做多 | 做空 |
|--------|--------|------|------|
| **反转** | 启动 A / 启动 B | `launch_a` / `launch_b` | 同 code方向在表单 |
| **顺势** | 大分歧 A / B | `big_div_a` / `big_div_b` | 同左 |
| **波段** | 小分歧 | `small_div` | 同左 |
`lib/trade/entry_model_lib.py` + 共用模板/JS三所趋势户一致
### 2.2 现状下单监控 UI
两级下拉**性质** → **类型**提交 `entry_model` code
### 2.3 杠杆
BTC/ETH **10x**其它 **5x**与方向开仓类型无关
---
## 3. 反转·启动讨论定稿
**性质**反转 — 在新一轮主趋势**确认之前**作战与顺势·大分歧不是同一阶段
**周期**背离与箱体以 **4h** 为主**日线与 4h 同阶段时结构边界以日线为准**A2 可用 **5m** 节奏
### 3.1 做多跌后筑底 → 新主升
与币安 [§3](./binance-alt-trend-long.md) 一致摘要如下
```text
4h MACD 底背离严格筛选→ 标「参考高点」
第 1 次到高点附近 → 不做V 形
回落箱内 → 第 2 次到高点附近 → 开战
├─ 见小收敛 → 启动 AA1
└─ 不见收敛 → 启动 B实体突破
B 失败回箱 → 跌破箱一半暂弃否则 5m 不创新低 + N 字 → 启动 AA2
```
| 环节 | 规则 |
|------|------|
| 背离 | 4h MACD **底背离****首次**不做**通道式下跌**不做明显波段高低点 + **三段及以上**跌后才开始找背离 |
| 参考点 | 背离后标 **最后一个显著高点** |
| 两次摸高 | 第 1 次到高点附近假破/不破都算**不做**第 2 次才关注 |
| 启动 B | 无小收敛 → **实体突破**参考高点/箱顶 |
| 启动 A | A1摸高前小收敛A2B 止损后**未跌破箱一半** + **5m 不创新低** + **5m N 字** |
### 3.2 做空涨后筑顶 → 新主跌镜像
| 环节 | 做多 | 做空镜像 |
|------|------|----------------|
| 前端结构 | **跌** → 背离 → 箱 | **涨** → 背离 → 箱 |
| 背离 | 4h MACD **底背离** | 4h MACD **顶背离** |
| 背离过滤 | 首次不做**通道式下跌**不做三段及以上**跌** | 首次不做**通道式上涨**不做三段及以上**涨** |
| 参考点 | **参考高点** | **参考低点**最后一个显著低点 |
| 两次摸极值 | 第 1 次到**高点**附近不做V 形反弹 | 第 1 次到**低点**附近不做V 形下跌 |
| 作战区 | 第 2 次到**高点**附近 | 第 2 次到**低点**附近 |
| 启动 B | **实体跌破**参考低点/箱底 | 同上向下实体突破 |
| 启动 A1 | 第二次摸高前**小收敛** | 第二次摸低前**小收敛** |
| 启动 A2 | B 失败后**未跌破**箱一半**5m 不创新低****5m 高** N 字 | B 失败后**未涨破**箱一半**5m 不创新高**破 **5m 低**倒 N 字 |
| 成功后 | 主升 → 顺势大分歧 / 小分歧 | 主跌 → 顺势大分歧 / 小分歧 |
> 横盘**时长不量化**顺序为 **涨/跌 → 背离 → 背离后震荡**非趋势中继
### 3.3 与顺势波段的边界
| | 反转·启动 | 顺势·大分歧 | 波段·小分歧 |
|---|-----------|-------------|-------------|
| 做多前端 | 跌→背离→箱→两次摸高 | 主升已确立 | 主升已确立 |
| 做空前端 | 涨→背离→箱→两次摸低 | 主跌已确立 | 主跌已确立 |
| V 形 | 第 1 次摸极值不做 | — | — |
| 5m N 试仓 | **启动 AA2**非小分歧 | — | — |
| 第三次不做 | — | — | ✓ |
---
## 4. 顺势·大分歧趋势单
**前提**主趋势**已确立**上方(多)或下方(空)仍有空间**不要**求反转链§3)。
| | 做多 | 做空 |
|---|------|------|
| **大分歧 A** | 4h/日线多头5m/15m 收敛**不创新低**企稳 | 4h/日线空头5m/15m 收敛**不创新高**企稳 |
| **大分歧 B** | **向上突破**确认实体优先 | **向下突破**确认实体优先 |
---
## 5. 波段·小分歧波段单
**前提**主趋势**已走出**反转链进行中**不做**小分歧
- **前两次**可做**第三次不做**不追突破
- **做多**二次探底 → N 字或 5m 三均线重新多头
- **做空**二次探顶 → 倒 N或 5m 三均线重新空头
---
## 6. 纪律
1. 方向与大级别一致不做顺手反向单同一币种避免未计划对冲叠仓
2. **反转**首次背离不做通道式涨跌中的背离不做第 1 次摸参考极值不做V 形
3. **顺势**第三次小分歧不做小分歧不追突破
4. 止盈/止损/是否手平**随行情**本文档不量化 RR
---
## 7. 持仓与出场定性
| 单型 | 做多 | 做空 |
|------|------|------|
| 启动 A/B | 赌新主升B 失败可 A2确立前不做小分歧 | 赌新主跌同上镜像 |
| 大分歧 | 可长持两次小分歧后远目标未到可手平 | 同左 |
| 小分歧 | 短拿常手平 | 短拿常手平 |
---
## 8. 系统字段
| 操作 | 字段 |
|------|------|
| 下单现状 | `entry_model` + `trade_style`方向在订单侧 |
| 下单规划 | `launch_a` / `launch_b` + 两级 UI |
| 平仓 / 复盘 | `entry_reason` = 界面标签与币安一致 |
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.4 | 2026-07-06 | 反转·启动 A/B做多同币安 §3做空镜像);两级 UI 规划 |
| v0.3 | 2026-07-06 | 独立完整策略说明 |
| v0.2 | 2026-07-06 | 定稿 UI 短标签 |
# OKX·多空趋势账户
> **状态**:v0.4(讨论稿:反转·启动 A/B;**UI 仍为 v0.2 三档**,两级下拉待实现)
---
## 1. 账户定位
| 项 | 说明 |
|----|------|
| 交易所 | OKX 永续合约 |
| 方向 | **做多 + 做空**(`TRADE_DIRECTION=both`,可按需限制) |
| 计仓 | `POSITION_SIZING_MODE=risk` |
| 关键位自动单 | `KEY_AUTO_ORDER_ENABLED=false` |
| UI profile | **趋势户**(非 Gate BTC/ETH 日内白名单) |
**选方向原则**:开仓前确认 4H / 日线大级别与所选「做多/做空」一致;逆势单不在本策略范围内.
做多侧反转细则与 [binance-alt-trend-long.md §3](./binance-alt-trend-long.md) 同构;本文 **§3.2** 给出做空镜像.
---
## 2. 开仓类型(规划与现状)
### 2.1 规划:两级选择(与币安共用实现)
| 第一级 | 第二级 | 做多 | 做空 |
|--------|--------|------|------|
| **反转** | 启动 A / 启动 B | `launch_a` / `launch_b` | 同 code,方向在表单 |
| **顺势** | 大分歧 A / B | `big_div_a` / `big_div_b` | 同左 |
| **波段** | 小分歧 | `small_div` | 同左 |
`lib/trade/entry_model_lib.py` + 共用模板/JS,三所趋势户一致.
### 2.2 现状:下单监控 UI
两级下拉:**性质** → **类型**;提交 `entry_model` code.
### 2.3 杠杆
BTC/ETH **10x**,其它 **5x**;与方向,开仓类型无关.
---
## 3. 反转·启动(讨论定稿)
**性质**:反转 — 在新一轮主趋势**确认之前**作战;与顺势·大分歧不是同一阶段.
**周期**:背离与箱体以 **4h** 为主;**日线与 4h 同阶段时,结构边界以日线为准**;A2 可用 **5m** 节奏.
### 3.1 做多(跌后筑底 → 新主升)
与币安 [§3](./binance-alt-trend-long.md) 一致,摘要如下:
```text
4h MACD 底背离(严格筛选)→ 标「参考高点」
第 1 次到高点附近 → 不做(V 形)
回落箱内 → 第 2 次到高点附近 → 开战
├─ 见小收敛 → 启动 A(A1)
└─ 不见收敛 → 启动 B(实体突破)
B 失败回箱 → 跌破箱一半暂弃;否则 5m 不创新低 + N 字 → 启动 A(A2)
```
| 环节 | 规则 |
|------|------|
| 背离 | 4h MACD **底背离**;**首次**不做;**通道式下跌**不做;明显波段高低点 + **三段及以上**跌后才开始找背离 |
| 参考点 | 背离后标 **最后一个显著高点** |
| 两次摸高 | 第 1 次到高点附近(假破/不破都算)**不做**;第 2 次才关注 |
| 启动 B | 无小收敛 → **实体突破**参考高点/箱顶 |
| 启动 A | A1:摸高前小收敛;A2:B 止损后,**未跌破箱一半** + **5m 不创新低** + **5m N 字** |
### 3.2 做空(涨后筑顶 → 新主跌,镜像)
| 环节 | 做多 | 做空(镜像) |
|------|------|----------------|
| 前端结构 | **跌** → 背离 → 箱 | **涨** → 背离 → 箱 |
| 背离 | 4h MACD **底背离** | 4h MACD **顶背离** |
| 背离过滤 | 首次不做;**通道式下跌**不做;三段及以上**跌** | 首次不做;**通道式上涨**不做;三段及以上**涨** |
| 参考点 | **参考高点** | **参考低点**(最后一个显著低点) |
| 两次摸极值 | 第 1 次到**高点**附近不做(V 形反弹) | 第 1 次到**低点**附近不做(V 形下跌) |
| 作战区 | 第 2 次到**高点**附近 | 第 2 次到**低点**附近 |
| 启动 B | **实体跌破**参考低点/箱底 | 同上(向下实体突破) |
| 启动 A1 | 第二次摸高前**小收敛** | 第二次摸低前**小收敛** |
| 启动 A2 | B 失败后**未跌破**箱一半;**5m 不创新低**;**5m 高** N 字 | B 失败后**未涨破**箱一半;**5m 不创新高**;破 **5m 低**倒 N 字 |
| 成功后 | 主升 → 顺势大分歧 / 小分歧 | 主跌 → 顺势大分歧 / 小分歧 |
> 横盘**时长不量化**;顺序为 **涨/跌 → 背离 → 背离后震荡**,非趋势中继.
### 3.3 与顺势,波段的边界
| | 反转·启动 | 顺势·大分歧 | 波段·小分歧 |
|---|-----------|-------------|-------------|
| 做多前端 | 跌→背离→箱→两次摸高 | 主升已确立 | 主升已确立 |
| 做空前端 | 涨→背离→箱→两次摸低 | 主跌已确立 | 主跌已确立 |
| V 形 | 第 1 次摸极值不做 | — | — |
| 5m N 试仓 | **启动 A(A2)**,非小分歧 | — | — |
| 第三次不做 | — | — | ✓ |
---
## 4. 顺势·大分歧(趋势单)
**前提**:主趋势**已确立**;上方(多)或下方(空)仍有空间;**不要**求反转链(§3).
| | 做多 | 做空 |
|---|------|------|
| **大分歧 A** | 4h/日线多头;5m/15m 收敛,**不创新低**企稳 | 4h/日线空头;5m/15m 收敛,**不创新高**企稳 |
| **大分歧 B** | **向上突破**确认(实体优先) | **向下突破**确认(实体优先) |
---
## 5. 波段·小分歧(波段单)
**前提**:主趋势**已走出**;反转链进行中**不做**小分歧.
- **前两次**可做,**第三次不做**;不追突破
- **做多**:二次探底 → N 字;或 5m 三均线重新多头
- **做空**:二次探顶 → 倒 N;或 5m 三均线重新空头
---
## 6. 纪律
1. 方向与大级别一致;不做顺手反向单;同一币种避免未计划对冲叠仓
2. **反转**:首次背离不做;通道式涨跌中的背离不做;第 1 次摸参考极值不做(V 形)
3. **顺势**:第三次小分歧不做;小分歧不追突破
4. 止盈/止损/是否手平:**随行情**,本文档不量化 RR
---
## 7. 持仓与出场(定性)
| 单型 | 做多 | 做空 |
|------|------|------|
| 启动 A/B | 赌新主升;B 失败可 A2;确立前不做小分歧 | 赌新主跌;同上镜像 |
| 大分歧 | 可长持;两次小分歧后远目标未到可手平 | 同左 |
| 小分歧 | 短拿,常手平 | 短拿,常手平 |
---
## 8. 系统字段
| 操作 | 字段 |
|------|------|
| 下单(现状) | `entry_model` + `trade_style`;方向在订单侧 |
| 下单(规划) | `launch_a` / `launch_b` + 两级 UI |
| 平仓 / 复盘 | `entry_reason` = 界面标签;与币安一致 |
---
## 修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.4 | 2026-07-06 | 反转·启动 A/B(做多同币安 §3;做空镜像);两级 UI 规划 |
| v0.3 | 2026-07-06 | 独立完整策略说明 |
| v0.2 | 2026-07-06 | 定稿 UI 短标签 |
+184 -184
View File
@@ -1,184 +1,184 @@
# 趋势回调中控平仓与交易记录检阅备忘
本文档汇总 **中控手动结束趋势计划****交易记录 / 策略记录** 写入规则以及 **三所展示统一****补仓表计价** 相关修复便于自行检阅与排错
适用仓库`crypto_monitor`Binance / OKX / + `manual_trading_hub`)。
---
## 1. 中控手动平仓会不会写交易记录
**会** 在实例已部署 **`80226ee` 及之后** 代码并 **重启对应 Flask** 的前提下
中控点击 **「结束计划」** → 实例执行市价平仓 + 结束计划 → **同时写入**
| 目标 | 表 | 页面入口 |
|------|-----|----------|
| 策略记录 | `strategy_trade_snapshots` | 顶栏 **策略交易记录** → 左栏「趋势回调记录」 |
| 交易记录 | `trade_records` | 顶栏 **交易记录与复盘** |
手动结束的结果字段为 **「手动平仓」**亏损时也不会被改成「止损」)。
---
## 2. 调用链三所统一
```
manual_trading_hub
POST /api/trend/{exchange_id}/stop
→ 实例 POST /api/hub/trend/stop/{plan_id}
→ stop_trend_pullback(pid)
→ 市价平仓 + 撤单
→ _finalize_plan(cfg, conn, row, "手动平仓", exit_price)
```
共用实现`strategy_trend_register.py`三所同一套各所的 `stop_trend_pullback` 也调用 `_finalize_plan`)。
---
## 3. `_finalize_plan` 写入顺序修复后
1. 写 **策略快照** `save_trend_plan_snapshot``strategy_trade_snapshots`
2. 撤该品种挂单
3. 若尚无 `trade_records.trend_plan_id = 计划ID`
- 更新当日 session 资金
- **`insert_trade_record`** 写入交易记录
4. 更新 `trend_pullback_plans.status``stopped_manual` / `stopped_sl` / `stopped_tp`
5. **`conn.commit()`** 一次提交
要点**先写交易记录再结束计划**避免「计划已结束交易记录未写入」的半成功状态
---
## 4. 曾出现的 Bug#4 ONDO 漏记
**现象**策略记录有止损 -2.71U),**交易记录没有**
**原因**各所的 `insert_trade_record`**缺少 `entry_reason` 参数**`_finalize_plan` 固定传入 `entry_reason="趋势回调"`触发
```text
TypeError: insert_trade_record() got an unexpected keyword argument 'entry_reason'
```
策略快照在异常 **之前** 已插入交易记录插入失败故只出现在策略记录页
**修复提交**`80226ee`
- `insert_trade_record` 增加 `entry_reason`
- `_call_insert_trade_record`按各所函数 **签名过滤** 参数避免未知字段导致失败
- 调整写入顺序交易记录 → 计划结束 → commit
---
## 5. 历史漏记补录
对已结束策略快照在交易记录缺的计划#4):
```bash
cd /opt/crypto_monitor # 或本机仓库根目录
# 先预览
python scripts/backfill_trend_trade_records.py \
--db crypto_monitor_gate/crypto.db --dry-run
# 确认后写入
python scripts/backfill_trend_trade_records.py \
--db crypto_monitor_gate/crypto.db --apply
```
其它所将 `--db` 换成对应 `crypto.db` 路径即可
---
## 6. 与「保本移交」的区别
| 操作 | 策略记录 | 交易记录 |
|------|----------|----------|
| 中控 **结束计划**手动平仓 | 计划结束时写入 | **同一时刻**写入 |
| **保本移交** | 移交时写入策略快照 | **不立即写**持仓移交到 `order_monitors`**后续平仓** 再写入 `trade_records` |
---
## 7. 三所展示统一中控 ↔ 实例
### 7.1 数据 enrich 入口
| 场景 | 函数 |
|------|------|
| 实例策略页 | `enrich_trend_plan` |
| 中控 `/api/hub/monitor` | `enrich_trend_plan_for_hub` → 同上 |
| 补仓明细表 | `attach_trend_dca_levels``enrich_trend_dca_levels_with_tp` |
`hub_bridge` 安装后调用 `patch_trend_hub_enrich`与另外三所 `install_strategy_trend` 行为一致
### 7.2 补仓表「触发价 / 加仓后均价」
**禁止**为凑均价 **反推虚构成交价**曾错误出现做多补仓触发价 0.3941 等离谱数值)。
**`trend_leg_display_price`三所唯一口径**
| 列 | 规则 |
|----|------|
| **触发价** | `leg_fill_prices_json` 有记录 → 实际成交价无记录 → **计划网格价** |
| **末档已补仓的加仓后均价** | 与顶部均价一致**交易所持仓 `entry_price`**`avg_entry_price` |
| **顶部均价** | 优先交易所 live `entry_price`非计划库内估算值 |
修复提交`08082eb`移除反推成交价逻辑)。
### 7.3 中控静态页
`manual_trading_hub/static/app.js`趋势浮盈亏计算 **优先** `trendPlan.avg_entry_price`与计划卡一致
---
## 8. 部署与自检
### 8.1 升级
```bash
cd /opt/crypto_monitor
git pull # 需含 80226ee08082eb
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate manual-trading-hub
pm2 save
```
### 8.2 手动平仓后自检
1. 中控结束一笔测试计划或极小仓位
2. **策略交易记录**出现对应条目
3. **交易记录与复盘**出现 `类型=趋势回调``结果=手动平仓``trend_plan_id` 与计划 ID 一致
4. 若实例 flash / 日志出现「计划已结束但记账可能不完整」说明 `insert_trade_record` 仍失败需查 PM2 日志
### 8.3 相关代码文件
| 文件 | 作用 |
|------|------|
| `strategy_trend_register.py` | `_finalize_plan``_call_insert_trade_record``enrich_trend_plan` |
| `strategy_trend_lib.py` | `trend_leg_display_price``enrich_trend_dca_levels_with_tp` |
| `strategy_snapshot_lib.py` | 策略快照写入 |
| `hub_bridge.py` | `/api/hub/trend/stop/<pid>` |
| `crypto_monitor_gate/app.py` | `insert_trade_record``entry_reason` |
| `scripts/backfill_trend_trade_records.py` | 漏记交易记录补录 |
### 8.4 相关提交
| 提交 | 说明 |
|------|------|
| `6a4ec69` | 中控与三所趋势展示 enrich 统一 |
| `08082eb` | 移除补仓表反推虚构成交价 |
| `80226ee` | 修复 中控平仓漏写 `trade_records` |
---
## 9. 相关文档
| 文档 | 内容 |
|------|------|
| [策略交易说明.md](../策略交易说明.md) | 策略总览策略交易记录页 |
| [crypto_monitor_gate/趋势回调策略说明.md](../crypto_monitor_gate/趋势回调策略说明.md) | 趋势回调业务细则 |
| [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md) | 中控监控与趋势卡布局 |
| [hub-symbol-archive-kline.md](./hub-symbol-archive-kline.md) | 币种档案永久 5m K 线交易 overlay |
---
*最后整理2026-06-07与对话中修复项同步*
# 趋势回调:中控平仓与交易记录(检阅备忘)
本文档汇总 **中控手动结束趋势计划**,**交易记录 / 策略记录** 写入规则,以及 **三所展示统一**,**补仓表计价** 相关修复,便于自行检阅与排错.
适用仓库:`crypto_monitor`(Binance / OKX / + `manual_trading_hub`).
---
## 1. 中控手动平仓会不会写交易记录?
**会.** 在实例已部署 **`80226ee` 及之后** 代码并 **重启对应 Flask** 的前提下:
中控点击 **「结束计划」** → 实例执行市价平仓 + 结束计划 → **同时写入**:
| 目标 | 表 | 页面入口 |
|------|-----|----------|
| 策略记录 | `strategy_trade_snapshots` | 顶栏 **策略交易记录** → 左栏「趋势回调记录」 |
| 交易记录 | `trade_records` | 顶栏 **交易记录与复盘** |
手动结束的结果字段为 **「手动平仓」**(亏损时也不会被改成「止损」).
---
## 2. 调用链(三所统一)
```
manual_trading_hub
POST /api/trend/{exchange_id}/stop
→ 实例 POST /api/hub/trend/stop/{plan_id}
→ stop_trend_pullback(pid)
→ 市价平仓 + 撤单
→ _finalize_plan(cfg, conn, row, "手动平仓", exit_price)
```
共用实现:`strategy_trend_register.py`(三所同一套,各所的 `stop_trend_pullback` 也调用 `_finalize_plan`).
---
## 3. `_finalize_plan` 写入顺序(修复后)
1. 写 **策略快照** `save_trend_plan_snapshot``strategy_trade_snapshots`
2. 撤该品种挂单
3. 若尚无 `trade_records.trend_plan_id = 计划ID`:
- 更新当日 session 资金
- **`insert_trade_record`** 写入交易记录
4. 更新 `trend_pullback_plans.status`(`stopped_manual` / `stopped_sl` / `stopped_tp`)
5. **`conn.commit()`** 一次提交
要点:**先写交易记录,再结束计划**,避免「计划已结束,交易记录未写入」的半成功状态.
---
## 4. 曾出现的 Bug(#4 ONDO 漏记)
**现象**:策略记录有(止损 -2.71U),**交易记录没有**.
**原因**:各所的 `insert_trade_record`**缺少 `entry_reason` 参数**,`_finalize_plan` 固定传入 `entry_reason="趋势回调"`,触发:
```text
TypeError: insert_trade_record() got an unexpected keyword argument 'entry_reason'
```
策略快照在异常 **之前** 已插入,交易记录插入失败,故只出现在策略记录页.
**修复提交**:`80226ee`
- `insert_trade_record` 增加 `entry_reason`
- `_call_insert_trade_record`:按各所函数 **签名过滤** 参数,避免未知字段导致失败
- 调整写入顺序:交易记录 → 计划结束 → commit
---
## 5. 历史漏记补录
对已结束,策略快照在,交易记录缺的计划(#4):
```bash
cd /opt/crypto_monitor # 或本机仓库根目录
# 先预览
python scripts/backfill_trend_trade_records.py \
--db crypto_monitor_gate/crypto.db --dry-run
# 确认后写入
python scripts/backfill_trend_trade_records.py \
--db crypto_monitor_gate/crypto.db --apply
```
其它所将 `--db` 换成对应 `crypto.db` 路径即可.
---
## 6. 与「保本移交」的区别
| 操作 | 策略记录 | 交易记录 |
|------|----------|----------|
| 中控 **结束计划**(手动平仓) | 计划结束时写入 | **同一时刻**写入 |
| **保本移交** | 移交时写入策略快照 | **不立即写**;持仓移交到 `order_monitors`,**后续平仓** 再写入 `trade_records` |
---
## 7. 三所展示统一(中控 ↔ 实例)
### 7.1 数据 enrich 入口
| 场景 | 函数 |
|------|------|
| 实例策略页 | `enrich_trend_plan` |
| 中控 `/api/hub/monitor` | `enrich_trend_plan_for_hub` → 同上 |
| 补仓明细表 | `attach_trend_dca_levels``enrich_trend_dca_levels_with_tp` |
`hub_bridge` 安装后调用 `patch_trend_hub_enrich`,与另外三所 `install_strategy_trend` 行为一致.
### 7.2 补仓表「触发价 / 加仓后均价」
**禁止**为凑均价 **反推虚构成交价**(曾错误出现做多补仓触发价 0.3941 等离谱数值).
**`trend_leg_display_price`(三所唯一口径)**:
| 列 | 规则 |
|----|------|
| **触发价** | `leg_fill_prices_json` 有记录 → 实际成交价;无记录 → **计划网格价** |
| **末档已补仓的加仓后均价** | 与顶部均价一致,**交易所持仓 `entry_price`**(`avg_entry_price`) |
| **顶部均价** | 优先交易所 live `entry_price`,非计划库内估算值 |
修复提交:`08082eb`(移除反推成交价逻辑).
### 7.3 中控静态页
`manual_trading_hub/static/app.js`:趋势浮盈亏计算 **优先** `trendPlan.avg_entry_price`,与计划卡一致.
---
## 8. 部署与自检
### 8.1 升级
```bash
cd /opt/crypto_monitor
git pull # 需含 80226ee,08082eb
pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate manual-trading-hub
pm2 save
```
### 8.2 手动平仓后自检
1. 中控结束一笔测试计划(或极小仓位)
2. **策略交易记录**:出现对应条目
3. **交易记录与复盘**:出现 `类型=趋势回调`,`结果=手动平仓`,`trend_plan_id` 与计划 ID 一致
4. 若实例 flash / 日志出现「计划已结束但记账可能不完整」,说明 `insert_trade_record` 仍失败,需查 PM2 日志
### 8.3 相关代码文件
| 文件 | 作用 |
|------|------|
| `strategy_trend_register.py` | `_finalize_plan`,`_call_insert_trade_record`,`enrich_trend_plan` |
| `strategy_trend_lib.py` | `trend_leg_display_price`,`enrich_trend_dca_levels_with_tp` |
| `strategy_snapshot_lib.py` | 策略快照写入 |
| `hub_bridge.py` | `/api/hub/trend/stop/<pid>` |
| `crypto_monitor_gate/app.py` | `insert_trade_record`(`entry_reason`) |
| `scripts/backfill_trend_trade_records.py` | 漏记交易记录补录 |
### 8.4 相关提交
| 提交 | 说明 |
|------|------|
| `6a4ec69` | 中控与三所趋势展示 enrich 统一 |
| `08082eb` | 移除补仓表反推虚构成交价 |
| `80226ee` | 修复 中控平仓漏写 `trade_records` |
---
## 9. 相关文档
| 文档 | 内容 |
|------|------|
| [策略交易说明.md](../策略交易说明.md) | 策略总览,策略交易记录页 |
| [crypto_monitor_gate/趋势回调策略说明.md](../crypto_monitor_gate/趋势回调策略说明.md) | 趋势回调业务细则 |
| [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md) | 中控监控与趋势卡布局 |
| [hub-symbol-archive-kline.md](./hub-symbol-archive-kline.md) | 币种档案,永久 5m K 线,交易 overlay |
---
*最后整理:2026-06-07(与对话中修复项同步)*
+67 -67
View File
@@ -1,17 +1,17 @@
# 趋势回调策略说明
本文描述 **「趋势回调」** 自动交易计划的业务规则与实现口径
本文描述 **「趋势回调」** 自动交易计划的业务规则与实现口径.
**三所主站**Binance / Gate / OKX均在顶栏 **策略交易 → `/strategy`** 左栏提供同一套逻辑共用 `strategy_trend_register.py`);各所使用各自 API 与 `crypto.db`
**三所主站**(Binance / Gate / OKX)均在顶栏 **策略交易 → `/strategy`** 左栏提供同一套逻辑(共用 `strategy_trend_register.py`);各所使用各自 API 与 `crypto.db`.
**检阅备忘**中控平仓交易记录补仓展示漏记补录):[trend-hub-close-and-trade-records.md](./trend-hub-close-and-trade-records.md)
**检阅备忘**(中控平仓,交易记录,补仓展示,漏记补录):[trend-hub-close-and-trade-records.md](./trend-hub-close-and-trade-records.md)
---
## 1. 适用场景
- 各 **USDT 永续** 实例独立部署使用各自 API 与 `crypto.db`
- 你已明确**方向止损价补仓区间边界价止盈价杠杆**并接受程序按风险预算拆分 **首仓 50% + 多档补仓 50%**
- 各 **USDT 永续** 实例独立部署,使用各自 API 与 `crypto.db`.
- 你已明确:**方向,止损价,补仓区间边界价,止盈价,杠杆**,并接受程序按风险预算拆分 **首仓 50% + 多档补仓 50%**.
---
@@ -19,67 +19,67 @@
| 名称 | 含义 |
|------|------|
| **合约 USDT 可用余额** | **生成预览**时通过 API 读取的 **swap 账户 USDT `free`** 快照**确认执行**时再次读取并与快照比对偏差 |
| **风险比例** | 默认 **5%**指「若整笔计划在 **补仓区间远侧边界**做多=上沿做空=下沿这一侧的最坏价格结构下触及止损」目标亏损上限约为 **可用余额快照 × 风险比例**实现上用 `calc_risk_fraction``prepare_order_amount` 反推总张数受交易所最小张数与精度约束)。 |
| **止损价** | 用户填写开仓后挂 **交易所仓位类止损触发单**全平)。 |
| **补仓区间边界**库字段 `add_upper` | 用户填写**仅在该价位与止损价构成的区间内** 才允许程序触发剩余 50% 的市价补仓**界面文案**做多显示「补仓上沿」做空显示「补仓下沿」校验做多 `止损 < 边界价`做空 `止损 > 边界价` |
| **止盈价** | 用户填写的 **固定价格****不由交易所条件止盈单触发**由应用后台 **按标记价/行情价轮询**达到后 **市价全平** |
| **杠杆** | 计划内固定写入用于 `set_leverage` 与名义换算 |
| **补仓档位数** | 默认 **5**环境变量 `TREND_PULLBACK_DCA_LEGS` 可调);程序在满足最小张数前提下可能 **自动减少档数** |
| **合约 USDT 可用余额** | **生成预览**时通过 API 读取的 **swap 账户 USDT `free`** 快照;**确认执行**时再次读取并与快照比对偏差. |
| **风险比例** | 默认 **5%**:指「若整笔计划在 **补仓区间远侧边界**(做多=上沿,做空=下沿)这一侧的最坏价格结构下触及止损」,目标亏损上限约为 **可用余额快照 × 风险比例**(实现上用 `calc_risk_fraction``prepare_order_amount` 反推总张数,受交易所最小张数与精度约束). |
| **止损价** | 用户填写;开仓后挂 **交易所仓位类止损触发单**(全平). |
| **补仓区间边界**(库字段 `add_upper`) | 用户填写;**仅在该价位与止损价构成的区间内** 才允许程序触发剩余 50% 的市价补仓.**界面文案**:做多显示「补仓上沿」,做空显示「补仓下沿」.校验:做多 `止损 < 边界价`;做空 `止损 > 边界价`. |
| **止盈价** | 用户填写的 **固定价格**;**不由交易所条件止盈单触发**,由应用后台 **按标记价/行情价轮询**,达到后 **市价全平**. |
| **杠杆** | 计划内固定写入;用于 `set_leverage` 与名义换算. |
| **补仓档位数** | 默认 **5**(环境变量 `TREND_PULLBACK_DCA_LEGS` 可调);程序在满足最小张数前提下可能 **自动减少档数**. |
---
## 3. 执行流程时间顺序
## 3. 执行流程(时间顺序)
### 3.0 列表时间窗交易记录 / 计划历史
### 3.0 列表时间窗(交易记录 / 计划历史)
- **交易记录****计划历史**含预览快照列表与 **交易记录 CSV 导出** 支持 **UTC** 时间筛选默认 UTC 当日可选近 24h近 7d自定义起止)。
- 查询参数`win_preset``utc_today` / `utc_last24h` / `utc_last7d` / `custom`)、自定义时另传 `from_utc``to_utc`
- **统计分析**页仍按北京时间 `TRADING_DAY_RESET_HOUR` 切日不受列表窗影响
- **交易记录**,**计划历史**(含预览快照)列表与 **交易记录 CSV 导出** 支持 **UTC** 时间筛选(默认 UTC 当日;可选近 24h,近 7d,自定义起止).
- 查询参数:`win_preset`(`utc_today` / `utc_last24h` / `utc_last7d` / `custom`),自定义时另传 `from_utc`,`to_utc`.
- **统计分析**页仍按北京时间 `TRADING_DAY_RESET_HOUR` 切日,不受列表窗影响.
### 3.1 预览阶段不下单
### 3.1 预览阶段(不下单)
1. **风控**与「机器人下单监控」**互斥**——存在活跃机器人持仓或运行中趋势计划时不可生成预览
2. **读取可用余额快照** `get_available_trading_usdt()`失败则拒绝
3. **计算**写入表 `trend_pullback_previews`并跳转带 `preview_id`):
- 在 **补仓区间边界 ↔ 止损** 区间内生成 `N` 个补仓触发价做多从上沿向止损做空从下沿向止损);
- 将 **剩余 50% 计划张数** 拆成 `N` 份写入 `leg_amounts_json`
4. **预览有效期**默认 **120 秒**`TREND_PULLBACK_PREVIEW_TTL_SECONDS`),超时须重新点「生成预览」
1. **风控**:与「机器人下单监控」**互斥**——存在活跃机器人持仓或运行中趋势计划时,不可生成预览.
2. **读取可用余额快照** `get_available_trading_usdt()`,失败则拒绝.
3. **计算**(写入表 `trend_pullback_previews`,并跳转带 `preview_id`):
- 在 **补仓区间边界 ↔ 止损** 区间内生成 `N` 个补仓触发价(做多从上沿向止损,做空从下沿向止损);
- 将 **剩余 50% 计划张数** 拆成 `N` 份写入 `leg_amounts_json`.
4. **预览有效期**:默认 **120 秒**(`TREND_PULLBACK_PREVIEW_TTL_SECONDS`),超时须重新点「生成预览」.
### 3.2 确认执行实盘
### 3.2 确认执行(实盘)
5. 再次校验预览未过期**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`默认 **5%**),否则拒绝执行并要求重新预览
6. **首仓****立即市价** 开立 **总计划张数 × 50%**不附带交易所止盈单)。
7. **止损**撤销旧条件单后**仅止损** 的仓位触发单之后每次补仓成交会 **刷新** 止损挂单
7b. **保本移交下单监控**可选):首仓完成且交易所有持仓后可点击「保本移交下单监控」——将止损移至 **持仓均价 ± 偏移%**默认 **+0.3%** 多 / **0.3%** 空),仅当新止损 **优于** 当前止损时生效**本次趋势计划随即结束**持仓写入 **下单监控**备注 **趋势回调计划**),交易所在 **同一时刻挂保本止损 + 计划止盈**后续无论中控平仓或交易所手动平仓均经下单监控轮询 **`reconcile_external_closes` / `check_order_monitors`** 写入 **交易记录**`trend_plan_id`开仓类型「趋势回调」),供人工核对
8. **补仓**当价格 **穿越** 下一档触发价做多为自上向下穿越做空为自下向上穿越)时,按该档张数 **市价加仓**直至 `N` 档执行完毕或计划结束
9. **止盈监控**后台线程若发现价格触及止盈**市价全平**
10. **止损触发**若仓位被交易所止损打光本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划
11. **计划结束**任一结束路径止盈 / 止损 / 用户手动结束均会 **撤单**条件单 + 普通挂单尽力而为)。
5. 再次校验:预览未过期;**当前可用余额**与预览快照相对偏差 ≤ `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT`(默认 **5%**),否则拒绝执行并要求重新预览.
6. **首仓**:**立即市价** 开立 **总计划张数 × 50%**(不附带交易所止盈单).
7. **止损**:撤销旧条件单后,**仅止损** 的仓位触发单;之后每次补仓成交会 **刷新** 止损挂单.
7b. **保本移交下单监控**(可选):首仓完成且交易所有持仓后,可点击「保本移交下单监控」——将止损移至 **持仓均价 ± 偏移%**(默认 **+0.3%** 多 / **0.3%** 空),仅当新止损 **优于** 当前止损时生效;**本次趋势计划随即结束**,持仓写入 **下单监控**(备注 **趋势回调计划**),交易所在 **同一时刻挂保本止损 + 计划止盈**;后续无论中控平仓或交易所手动平仓,均经下单监控轮询 **`reconcile_external_closes` / `check_order_monitors`** 写入 **交易记录**(`trend_plan_id`,开仓类型「趋势回调」),供人工核对.
8. **补仓**:当价格 **穿越** 下一档触发价(做多为自上向下穿越,做空为自下向上穿越)时,按该档张数 **市价加仓**;直至 `N` 档执行完毕或计划结束.
9. **止盈监控**:后台线程若发现价格触及止盈,**市价全平**.
10. **止损触发**:若仓位被交易所止损打光,本地检测到 **持仓为 0** 后记账为 **止损** 并结束计划.
11. **计划结束**:任一结束路径(止盈 / 止损 / 用户手动结束)均会 **撤单**(条件单 + 普通挂单,尽力而为).
### 3.3 取消预览
用户可「取消预览」删除 `trend_pullback_previews` 中对应记录过期记录会在新预览或页面加载时清理
用户可「取消预览」删除 `trend_pullback_previews` 中对应记录;过期记录会在新预览或页面加载时清理.
### 3.4 界面计划历史与运行中浮动盈亏
### 3.4 界面:计划历史与运行中浮动盈亏
- **计划历史页顶卡片**
- 仅展示 **`trend_pullback_plans` 中已结束的计划**`status != 'active'`如止盈结束止损结束手动结束)。
- **不包含**仅存在于 `trend_pullback_previews`从未「确认执行」的预览
- 每行提供 **删除**删除该计划行并删除 `trade_records`**`trend_plan_id` 与之相同** 且类型为「趋势回调」的记录用于与计划一一对应的新数据历史旧行若无 `trend_plan_id` 则不会随删)。
- **运行中的计划交易执行页**
- 在计划摘要下方展示 **浮盈亏交易所**来自 Gate 当前持仓接口的 **未实现盈亏**及标记价若可得);与本地按均价估算可能略有差异以交易所为准便于对照
- **补仓边界**按方向显示「补仓上沿」或「补仓下沿」数值仍为 `add_upper` 字段)。
- **手动保本**表单可改偏移 %默认见 `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT`);成功后显示「已保本」时间与原止损若与当前不同)。
- **计划历史(页顶卡片)**
- 仅展示 **`trend_pullback_plans` 中已结束的计划**(`status != 'active'`,如止盈结束,止损结束,手动结束).
- **不包含**仅存在于 `trend_pullback_previews`,从未「确认执行」的预览.
- 每行提供 **删除**:删除该计划行,并删除 `trade_records`**`trend_plan_id` 与之相同** 且类型为「趋势回调」的记录(用于与计划一一对应的新数据;历史旧行若无 `trend_plan_id` 则不会随删).
- **运行中的计划(交易执行页)**
- 在计划摘要下方展示 **浮盈亏(交易所)**:来自 Gate 当前持仓接口的 **未实现盈亏**(及标记价,若可得);与本地按均价估算可能略有差异,以交易所为准便于对照.
- **补仓边界**按方向显示「补仓上沿」或「补仓下沿」(数值仍为 `add_upper` 字段).
- **手动保本**:表单可改偏移 %(默认见 `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT`);成功后显示「已保本」时间与原止损(若与当前不同).
### 3.5 交易记录与交易所「已实现盈亏」对齐
- 平仓时仍会写入一条 **`trade_records`**`monitor_type=趋势回调`),其中的 **`pnl_amount` 等为本地估算**`calc_pnl`不含手续费资金费等完整账单口径)。
- 打开 **「交易执行」或「交易记录」** 页面时若已配置 **`GATE_API_KEY` / `GATE_API_SECRET`**不要求 `LIVE_TRADING_ENABLED=true`只读即可),应用会按节流策略同进程约 **25 秒**内最多一次调用 Gate **`fetch_positions_history`平仓历史**为尚未写入 `exchange_sync_key` 的趋势回调记录 **匹配一条平仓记录**并回填
- **`exchange_realized_pnl`**交易所口径已实现盈亏与 App「历史仓位」更接近);
- **`exchange_opened_at` / `exchange_closed_at`**换算为应用时区默认北京下的开平时间字符串
- **交易记录表**展示列「开仓(展示) / 平仓(展示) / 盈亏U(展示)」对「趋势回调」行若已同步则优先显示交易所字段界面小字 **「所」**);未同步前仍显示本地复盘字段小字 **「估」**)。
- 匹配规则概要同品种同方向平仓时间与本地 `closed_at` 接近并结合 **`trend_plan_id`** 对应计划的 `opened_at` 收窄时间窗极端情况下若短时间多笔同向同品种仍存在错配可能可对照 `exchange_sync_key` 与交易所记录
- 平仓时仍会写入一条 **`trade_records`**(`monitor_type=趋势回调`),其中的 **`pnl_amount` 等为本地估算**(`calc_pnl`,不含手续费,资金费等完整账单口径).
- 打开 **「交易执行」或「交易记录」** 页面时,若已配置 **`GATE_API_KEY` / `GATE_API_SECRET`**(不要求 `LIVE_TRADING_ENABLED=true`,只读即可),应用会按节流策略(同进程约 **25 秒**内最多一次)调用 Gate **`fetch_positions_history`(平仓历史)**,为尚未写入 `exchange_sync_key` 的趋势回调记录 **匹配一条平仓记录**,并回填:
- **`exchange_realized_pnl`**:交易所口径已实现盈亏(与 App「历史仓位」更接近);
- **`exchange_opened_at` / `exchange_closed_at`**:换算为应用时区(默认北京)下的开,平时间字符串.
- **交易记录表**展示列「开仓(展示) / 平仓(展示) / 盈亏U(展示)」:对「趋势回调」行,若已同步则优先显示交易所字段(界面小字 **「所」**);未同步前仍显示本地复盘字段(小字 **「估」**).
- 匹配规则概要:同品种,同方向,平仓时间与本地 `closed_at` 接近,并结合 **`trend_plan_id`** 对应计划的 `opened_at` 收窄时间窗;极端情况下若短时间多笔同向同品种,仍存在错配可能,可对照 `exchange_sync_key` 与交易所记录.
---
@@ -89,17 +89,17 @@
|------|------------------|----------|
| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** |
| 止盈 | 条件单 + 本地监控 | **仅本地监控市价止盈** |
| 仓位基数 | 以损定仓表单/会话基数 | **可用余额快照 × 风险比例** 推导 |
| 移动保本 | 支持按 R 自动上移 | **保本移交**结束计划→下单监控交易所 TP+SL**无**自动 R 保本 |
| 仓位基数 | 以损定仓(表单/会话基数) | **可用余额快照 × 风险比例** 推导 |
| 移动保本 | 支持(按 R 自动上移) | **保本移交**(结束计划→下单监控;交易所 TP+SL;**无**自动 R 保本) |
---
## 5. 风险声明必读
## 5. 风险声明(必读)
- 市价单存在 **滑点**极端行情下实际亏损可能 **大于** 理论 5%
- 补仓触发依赖应用 **轮询间隔**`MONITOR_POLL_SECONDS`),非毫秒级高频
- 交易所 **最小张数 / 精度** 可能导致计划张数被截断实际风险略低于或偏离纸面计算
- 请使用 **单独 API Key / 子账户**并先在 `LIVE_TRADING_ENABLED=false` 环境验证流程若需沙盒请自行对接测试网本仓库默认实盘接口)。
- 市价单存在 **滑点**;极端行情下实际亏损可能 **大于** 理论 5%.
- 补仓触发依赖应用 **轮询间隔**(`MONITOR_POLL_SECONDS`),非毫秒级高频.
- 交易所 **最小张数 / 精度** 可能导致计划张数被截断,实际风险略低于或偏离纸面计算.
- 请使用 **单独 API Key / 子账户**,并先在 `LIVE_TRADING_ENABLED=false` 环境验证流程(若需沙盒请自行对接测试网,本仓库默认实盘接口).
---
@@ -107,23 +107,23 @@
| 变量 | 说明 | 默认 |
|------|------|------|
| `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT` | 手动保本默认偏移相对持仓均价% | `0.3` |
| `TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT` | 手动保本默认偏移(相对持仓均价,%) | `0.3` |
| `TREND_PULLBACK_DCA_LEGS` | 剩余 50% 拆档数量上限 | `5` |
| `TREND_PULLBACK_PREVIEW_TTL_SECONDS` | 预览有效时间(秒) | `120` |
| `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT` | 确认执行时允许「当前可用 / 预览快照」最大相对偏差% | `5` |
| `MONITOR_POLL_SECONDS` | 监控轮询间隔(秒) | `3` |
| `TREND_PULLBACK_PREVIEW_TTL_SECONDS` | 预览有效时间(秒) | `120` |
| `TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT` | 确认执行时允许「当前可用 / 预览快照」最大相对偏差(%) | `5` |
| `MONITOR_POLL_SECONDS` | 监控轮询间隔(秒) | `3` |
| `LIVE_TRADING_ENABLED` | 是否允许真实下单 | `false` |
| `FULL_MARGIN_BUFFER_RATIO` | 计划保证金相对可用余额上限比例 | `0.98` |
| `APP_TIMEZONE` | 应用墙钟与「北京日期」同步起点时区`Asia/Shanghai` | `Asia/Shanghai` |
| `EXCHANGE_POSITION_SYNC_FROM_BJ` | 拉取 Gate **平仓历史** 的最早日期`YYYY-MM-DD``APP_TIMEZONE` 当日 **00:00** 起算)。**留空**则从近 **90 天** 起拉取 | 空 |
| `EXCHANGE_POSITION_HISTORY_LIMIT` | 单次拉取平仓历史条数上限501000 | `200` |
| `APP_TIMEZONE` | 应用墙钟与「北京日期」同步起点时区(`Asia/Shanghai`) | `Asia/Shanghai` |
| `EXCHANGE_POSITION_SYNC_FROM_BJ` | 拉取 Gate **平仓历史** 的最早日期(`YYYY-MM-DD`,`APP_TIMEZONE` 当日 **00:00** 起算).**留空**则从近 **90 天** 起拉取 | 空 |
| `EXCHANGE_POSITION_HISTORY_LIMIT` | 单次拉取平仓历史条数上限(501000) | `200` |
---
## 7. 数据库
- **`trend_pullback_previews`**未执行的预览行`expires_at_ms`),执行成功或取消后删除过期可被清理
- **`trend_pullback_plans`**趋势回调计划执行后写入一行`status='active'` 表示运行中止盈 / 止损 / 手动结束后变为 **`stopped_tp` / `stopped_sl` / `stopped_manual`** 等非 `active` 状态并出现在页顶 **计划历史**字段含快照可用余额计划保证金总张数首仓张数补仓 JSON网格价 JSON已补仓档数均价`opened_at``message`结束说明)等;**`add_upper`** 存补仓区间远侧边界价做多=上沿做空=下沿)。
- **`trade_records`**`monitor_type=趋势回调`):每次计划结束插入一行含本地估算盈亏等新写入行带 **`trend_plan_id`** 指向 `trend_pullback_plans.id`另含 **`exchange_realized_pnl``exchange_opened_at``exchange_closed_at``exchange_sync_key`**由页面触发的交易所平仓历史同步填充见 3.5)。
- **`trend_pullback_previews`**:未执行的预览行(`expires_at_ms`),执行成功或取消后删除;过期可被清理.
- **`trend_pullback_plans`**:趋势回调计划.执行后写入一行,`status='active'` 表示运行中;止盈 / 止损 / 手动结束后变为 **`stopped_tp` / `stopped_sl` / `stopped_manual`** 等非 `active` 状态,并出现在页顶 **计划历史**.字段含快照可用余额,计划保证金,总张数,首仓张数,补仓 JSON,网格价 JSON,已补仓档数,均价,`opened_at`,`message`(结束说明)等;**`add_upper`** 存补仓区间远侧边界价(做多=上沿,做空=下沿).
- **`trade_records`**(`monitor_type=趋势回调`):每次计划结束插入一行;含本地估算盈亏等.新写入行带 **`trend_plan_id`** 指向 `trend_pullback_plans.id`.另含 **`exchange_realized_pnl`,`exchange_opened_at`,`exchange_closed_at`,`exchange_sync_key`**,由页面触发的交易所平仓历史同步填充(见 3.5).
**CSV 导出**交易记录导出为 **v3**包含上述交易所对齐字段及 `trend_plan_id`
**CSV 导出**:交易记录导出为 **v3**,包含上述交易所对齐字段及 `trend_plan_id`.
+169 -169
View File
@@ -1,169 +1,169 @@
# Ubuntu 服务器部署与环境说明
本文档为 **生产环境唯一推荐路径****Ubuntu****root** 用户代码目录 **`/opt/crypto_monitor`**进程托管 **PM2**不使用 Windows 部署不使用 systemd/screen/nohup 托管应用SSH 隧道除外)。
---
## 1. 系统要求
| 项 | 要求 |
|----|------|
| 操作系统 | **Ubuntu 22.04 LTS****24.04 LTS**64 位 |
| 运行用户 | **root**下文命令均按 root 编写 |
| 项目路径 | **`/opt/crypto_monitor`**整仓克隆到此目录 |
| 进程管理 | **PM2**全局安装见 §3 |
| 网络 | 能 `git clone` 私有仓库访问交易所不稳定时需 **SSH SOCKS**见各所《部署文档》 |
---
## 2. Python 环境
| 项 | 说明 |
|----|------|
| **版本** | **Python 3.10 或 3.11**`python3 --version` ≥ 3.10);脚本会拒绝 3.9 及以下 |
| **虚拟环境** | 每个子项目独立 **`.venv`**`deploy/setup_env.sh` 自动创建 |
| **依赖文件** | 三所监控共用仓库根目录 **`requirements.txt`**中控用 **`manual_trading_hub/requirements.txt`** |
| **SOCKS** | 走代理时必须安装 **PySocks**已写入 requirements |
### 2.1 系统包root
```bash
apt update
apt install -y python3 python3-pip python3-venv curl git ca-certificates
# 若 python3 为 3.10
apt install -y python3.10-venv
# 若为 3.12
apt install -y python3.12-venv
```
### 2.2 一键创建各目录 venv
```bash
cd /opt/crypto_monitor
bash deploy/setup_env.sh --install-system-deps
# 或已是 root 且已装 venv 包
bash deploy/setup_env.sh
```
完成后各目录使用 **`.venv/bin/python`** 运行 `app.py` / `hub.py`**PM2 的 ecosystem 脚本已指向该解释器**
---
## 3. Node.js 与 PM2
| 项 | 说明 |
|----|------|
| **Node.js** | 建议 **18 LTS****20 LTS**用于安装 PM2应用本体为 Python |
| **PM2** | 全局安装托管所有 Flask 与中控/子代理 |
### 3.1 安装 Node + PM2root
```bash
# 方式 ANodeSource示例 Node 20
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node -v # v20.x
npm -v
npm install -g pm2
pm2 -v
pm2 startup # 按提示执行保证重启后 PM2 自启
```
`deploy/setup_env.sh` 在检测到 Node 时也会尝试 `npm install -g pm2`未装 Node 则跳过并提示手动安装)。
### 3.2 PM2 启动顺序推荐
```bash
# 1) 三所 Flask在各子目录执行或分别 start
cd /opt/crypto_monitor/crypto_monitor_binance && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_gate && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_okx && pm2 start ecosystem.config.cjs
# 2) 中控 + 三子代理一条配置 4 进程hub + 3 agent
cd /opt/crypto_monitor/manual_trading_hub
pm2 start ecosystem.config.cjs
pm2 save
pm2 list
```
升级代码后
```bash
cd /opt/crypto_monitor && git pull
# 若 requirements 有变对各目录 .venv/bin/pip install -r ...
pm2 restart all # 或按进程名 restart
```
**不要** 再用 systemd unitscreennohup 启动 `app.py` / `hub.py` / `agent.py`避免与 PM2 抢端口
### 3.3 常见 PM2 进程名
| 目录 | ecosystem 内典型名称 |
|------|---------------------|
| `crypto_monitor_binance` | `crypto_binance` |
| `crypto_monitor_gate` | `crypto_gate` |
| `crypto_monitor_okx` | `crypto_okx` |
| `manual_trading_hub` | `manual-trading-hub``manual-agent-*` |
以各目录 **`ecosystem.config.cjs`** 为准
### 3.4 整目录重装清库 / 去脏 PM2
保留 `.env`丢弃旧库与旧 PM2 名单时**[deploy/reinstall-plan-b.md](../deploy/reinstall-plan-b.md)**
```bash
cd /opt/crypto_monitor
bash deploy/reinstall.sh --yes
```
首次安装仍只用 `deploy/setup_env.sh`二者互不影响
---
## 4. 目录与权限
```bash
mkdir -p /opt
cd /opt
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
chown -R root:root /opt/crypto_monitor
```
- 数据库默认各所 **`crypto.db`**SQLite
- 备份目录建议**`/root/backups`**见 [备份与恢复.md](../备份与恢复.md)
- **`.env`**仅本机编辑**勿提交 Git**升级前 `cp .env .env.backup.$(date +%Y%m%d)`
---
## 5. SSH 动态转发SOCKS
若交易所 API 需经境外 VPS
- 在本机用 **`ssh -N -D 127.0.0.1:1080 别名`** 建立隧道配置见各所《部署文档》`~/.ssh/config`
- 隧道进程可用 **tmux****autossh** 保持常驻**不必** 也不建议把 `ssh` 交给 PM2
- 各所 `.env` 设置对应 `*_SOCKS_PROXY=socks5h://127.0.0.1:1080`
---
## 6. 部署后检查
```bash
# 中控验收需已 start hub
bash /opt/crypto_monitor/manual_trading_hub/scripts/verify_hub_deploy.sh
pm2 logs manual-trading-hub --lines 50
curl -sS http://127.0.0.1:5100/api/monitor/board | head
```
---
## 7. 相关文档
| 文档 | 内容 |
|------|------|
| [deploy/README.md](../deploy/README.md) | `setup_env.sh` 参数说明 |
| [备份与恢复.md](../备份与恢复.md) | 数据库与 `.env` 备份 |
| 各 `crypto_monitor_*/部署文档.md` | 交易所 SOCKS`.env`PM2 细节 |
| [manual_trading_hub/部署文档.md](../manual_trading_hub/部署文档.md) | 中控 PM2端口反代 |
# Ubuntu 服务器部署与环境说明
本文档为 **生产环境唯一推荐路径**:**Ubuntu**,**root** 用户,代码目录 **`/opt/crypto_monitor`**,进程托管 **PM2**.不使用 Windows 部署,不使用 systemd/screen/nohup 托管应用(SSH 隧道除外).
---
## 1. 系统要求
| 项 | 要求 |
|----|------|
| 操作系统 | **Ubuntu 22.04 LTS****24.04 LTS**(64 位) |
| 运行用户 | **root**(下文命令均按 root 编写) |
| 项目路径 | **`/opt/crypto_monitor`**(整仓克隆到此目录) |
| 进程管理 | **PM2**(全局安装,见 §3) |
| 网络 | 能 `git clone` 私有仓库;访问交易所不稳定时需 **SSH SOCKS**(见各所《部署文档》) |
---
## 2. Python 环境
| 项 | 说明 |
|----|------|
| **版本** | **Python 3.10 或 3.11**(`python3 --version` ≥ 3.10);脚本会拒绝 3.9 及以下 |
| **虚拟环境** | 每个子项目独立 **`.venv`**(`deploy/setup_env.sh` 自动创建) |
| **依赖文件** | 三所监控共用仓库根目录 **`requirements.txt`**;中控用 **`manual_trading_hub/requirements.txt`** |
| **SOCKS** | 走代理时必须安装 **PySocks**(已写入 requirements) |
### 2.1 系统包(root)
```bash
apt update
apt install -y python3 python3-pip python3-venv curl git ca-certificates
# 若 python3 为 3.10:
apt install -y python3.10-venv
# 若为 3.12:
apt install -y python3.12-venv
```
### 2.2 一键创建各目录 venv
```bash
cd /opt/crypto_monitor
bash deploy/setup_env.sh --install-system-deps
# 或已是 root 且已装 venv 包:
bash deploy/setup_env.sh
```
完成后各目录使用 **`.venv/bin/python`** 运行 `app.py` / `hub.py`;**PM2 的 ecosystem 脚本已指向该解释器**.
---
## 3. Node.js 与 PM2
| 项 | 说明 |
|----|------|
| **Node.js** | 建议 **18 LTS****20 LTS**(用于安装 PM2;应用本体为 Python) |
| **PM2** | 全局安装,托管所有 Flask 与中控/子代理 |
### 3.1 安装 Node + PM2(root)
```bash
# 方式 A:NodeSource(示例 Node 20)
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install -y nodejs
node -v # v20.x
npm -v
npm install -g pm2
pm2 -v
pm2 startup # 按提示执行,保证重启后 PM2 自启
```
`deploy/setup_env.sh` 在检测到 Node 时也会尝试 `npm install -g pm2`(未装 Node 则跳过并提示手动安装).
### 3.2 PM2 启动顺序(推荐)
```bash
# 1) 三所 Flask(在各子目录执行,或分别 start)
cd /opt/crypto_monitor/crypto_monitor_binance && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_gate && pm2 start ecosystem.config.cjs
cd /opt/crypto_monitor/crypto_monitor_okx && pm2 start ecosystem.config.cjs
# 2) 中控 + 三子代理(一条配置 4 进程:hub + 3 agent)
cd /opt/crypto_monitor/manual_trading_hub
pm2 start ecosystem.config.cjs
pm2 save
pm2 list
```
升级代码后:
```bash
cd /opt/crypto_monitor && git pull
# 若 requirements 有变,对各目录 .venv/bin/pip install -r ...
pm2 restart all # 或按进程名 restart
```
**不要** 再用 systemd unit,screen,nohup 启动 `app.py` / `hub.py` / `agent.py`,避免与 PM2 抢端口.
### 3.3 常见 PM2 进程名
| 目录 | ecosystem 内典型名称 |
|------|---------------------|
| `crypto_monitor_binance` | `crypto_binance` |
| `crypto_monitor_gate` | `crypto_gate` |
| `crypto_monitor_okx` | `crypto_okx` |
| `manual_trading_hub` | `manual-trading-hub`,`manual-agent-*` |
以各目录 **`ecosystem.config.cjs`** 为准.
### 3.4 整目录重装(清库 / 去脏 PM2)
保留 `.env`,丢弃旧库与旧 PM2 名单时,**[deploy/reinstall-plan-b.md](../deploy/reinstall-plan-b.md)**:
```bash
cd /opt/crypto_monitor
bash deploy/reinstall.sh --yes
```
首次安装仍只用 `deploy/setup_env.sh`,二者互不影响.
---
## 4. 目录与权限
```bash
mkdir -p /opt
cd /opt
git clone https://git.bz121.com/dekun/crypto_monitor.git crypto_monitor
chown -R root:root /opt/crypto_monitor
```
- 数据库默认:各所 **`crypto.db`**(SQLite)
- 备份目录建议:**`/root/backups`**(见 [备份与恢复.md](../备份与恢复.md))
- **`.env`**:仅本机编辑,**勿提交 Git**;升级前 `cp .env .env.backup.$(date +%Y%m%d)`
---
## 5. SSH 动态转发(SOCKS)
若交易所 API 需经境外 VPS:
- 在本机用 **`ssh -N -D 127.0.0.1:1080 别名`** 建立隧道(配置见各所《部署文档》`~/.ssh/config`)
- 隧道进程可用 **tmux****autossh** 保持常驻;**不必** 也不建议把 `ssh` 交给 PM2
- 各所 `.env` 设置对应 `*_SOCKS_PROXY=socks5h://127.0.0.1:1080`
---
## 6. 部署后检查
```bash
# 中控验收(需已 start hub)
bash /opt/crypto_monitor/manual_trading_hub/scripts/verify_hub_deploy.sh
pm2 logs manual-trading-hub --lines 50
curl -sS http://127.0.0.1:5100/api/monitor/board | head
```
---
## 7. 相关文档
| 文档 | 内容 |
|------|------|
| [deploy/README.md](../deploy/README.md) | `setup_env.sh` 参数说明 |
| [备份与恢复.md](../备份与恢复.md) | 数据库与 `.env` 备份 |
| 各 `crypto_monitor_*/部署文档.md` | 交易所 SOCKS,`.env`,PM2 细节 |
| [manual_trading_hub/部署文档.md](../manual_trading_hub/部署文档.md) | 中控 PM2,端口,反代 |
+25 -25
View File
@@ -1,6 +1,6 @@
# 中控 AI 与部署密钥配置
本文档说明**长期部署密钥**一次生成不轮换)、**SSO 临时链接**保持不动)、以及 **AI 配置**中控统一维护并同步三所)。
本文档说明:**长期部署密钥**(一次生成,不轮换),**SSO 临时链接**(保持不动),以及 **AI 配置**(中控统一维护并同步三所).
---
@@ -8,19 +8,19 @@
| 类型 | 变量 / 机制 | 谁维护 | 是否自动过期 |
|------|-------------|--------|--------------|
| **长期通信密钥** | `HUB_BRIDGE_TOKEN` | 部署脚本首次写入四份 `.env` | 否不轮换 |
| **长期通信密钥** | `HUB_BRIDGE_TOKEN` | 部署脚本首次写入四份 `.env` | 否,不轮换 |
| **实例 Session 签名** | `FLASK_SECRET_KEY` | 部署脚本首次写入三实例 | 否 |
| **中控 Session 签名** | `HUB_SESSION_SECRET` | 部署脚本首次写入中控 | 否 |
| **SSO 开门链接** | `/hub-sso?token=...` | 中控每次点「打开实例」签发 | 默认 2h + 单次 |
| **AI 配置** | `OPENAI_*``AI_*` 等 | 中控系统设置 → AI 配置 | 否 |
| **AI 配置** | `OPENAI_*`,`AI_*` 等 | 中控系统设置 → AI 配置 | 否 |
**SSO 保持不动**仍为随机 nonce默认 `HUB_SSO_TTL_SEC=7200`成功登录一次后链接作废长期 `HUB_BRIDGE_TOKEN` 只用于签名不会每 2 小时变化
**SSO 保持不动**:仍为随机 nonce,默认 `HUB_SSO_TTL_SEC=7200`,成功登录一次后链接作废.长期 `HUB_BRIDGE_TOKEN` 只用于签名,不会每 2 小时变化.
---
## 2. 首次部署自动生成
`bash deploy/setup_env.sh` 在复制 `.env.example` 后自动执行
`bash deploy/setup_env.sh` 在复制 `.env.example` 后自动执行:
```bash
python3 scripts/bootstrap_deploy_secrets.py
@@ -28,21 +28,21 @@ python3 scripts/bootstrap_deploy_secrets.py
| 写入项 | 位置 | 规则 |
|--------|------|------|
| `HUB_BRIDGE_TOKEN` | 中控 + 三实例同值 | 仅空或占位符时写入 |
| `FLASK_SECRET_KEY` | 三实例同值 | 仅空或占位符时写入 |
| `HUB_BRIDGE_TOKEN` | 中控 + 三实例(同值) | 仅空或占位符时写入 |
| `FLASK_SECRET_KEY` | 三实例(同值) | 仅空或占位符时写入 |
| `HUB_SESSION_SECRET` | 中控 | 仅空时写入 |
| `HUB_USERNAME` / `HUB_PASSWORD` | 中控 | 默认 admin / admin123仅空时 |
| `APP_USERNAME` / `APP_PASSWORD` | 三实例 | 默认 admin / admin123仅空时 |
| `HUB_USERNAME` / `HUB_PASSWORD` | 中控 | 默认 admin / admin123(仅空时) |
| `APP_USERNAME` / `APP_PASSWORD` | 三实例 | 默认 admin / admin123(仅空时) |
**已有非空生产值不会被覆盖**一次生成不轮换)。
**已有非空生产值不会被覆盖**(一次生成,不轮换).
子代理 `agent.py` 优先读取 `HUB_BRIDGE_TOKEN` 作为 `X-Control-Token` 校验独立配置 `CONTROL_TOKEN` 已废弃
子代理 `agent.py` 优先读取 `HUB_BRIDGE_TOKEN` 作为 `X-Control-Token` 校验;独立配置 `CONTROL_TOKEN` 已废弃.
---
## 3. 中控系统设置 → AI 配置
路径**中控 Web → 系统设置 → AI 配置** Tab
路径:**中控 Web → 系统设置 → AI 配置** Tab.
### 3.1 可配置项
@@ -50,34 +50,34 @@ python3 scripts/bootstrap_deploy_secrets.py
|--------|----------|
| AI 提供方 | `AI_PROVIDER` |
| API 地址 | `OPENAI_API_BASE` |
| API 密钥 | `OPENAI_API_KEY`掩码留空不修改 |
| API 密钥 | `OPENAI_API_KEY`(掩码,留空不修改) |
| 云端模型 | `OPENAI_MODEL` |
| Ollama 地址 | `OLLAMA_API` |
| Ollama 模型 | `AI_MODEL` |
| 请求超时(秒) | `AI_TIMEOUT_SECONDS` |
| 请求超时(秒) | `AI_TIMEOUT_SECONDS` |
### 3.2 保存行为
1. 写入 `manual_trading_hub/.env`
2. **强制同步**`crypto_monitor_okx/binance/gate/.env` 相同键
3. 自动 `pm2 restart` 中控 + 三实例`--update-env`
3. 自动 `pm2 restart` 中控 + 三实例(`--update-env`)
### 3.3 API需已登录中控
### 3.3 API(需已登录中控)
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/settings/ai-env` | 读取字段与三所同步状态 |
| POST | `/api/settings/ai-env` | body: `{ "values": {...}, "restart": true }` |
实现`lib/env/shared_env_lib.py``manual_trading_hub/hub_env_lib.py`
实现:`lib/env/shared_env_lib.py`,`manual_trading_hub/hub_env_lib.py`.
---
## 4. 实例 env 配置页变更
三所 **env 配置** 页已 **移除「AI 复盘」卡片**交易所 API企业微信交易执行等仍各所自配
三所 **env 配置** 页已 **移除「AI 复盘」卡片**.交易所 API,企业微信,交易执行等仍各所自配.
实例侧若通过 API 提交已移除的 AI 键会被白名单过滤不会写入
实例侧若通过 API 提交已移除的 AI 键,会被白名单过滤,不会写入.
---
@@ -86,18 +86,18 @@ python3 scripts/bootstrap_deploy_secrets.py
| 能力 | 实例 env | 实例系统设置 | 中控系统设置 |
|------|----------|--------------|--------------|
| 交易所 API | ✅ | ❌ | ❌ |
| OpenAI / AI | ❌ | ❌ | ✅同步三所 |
| OpenAI / AI | ❌ | ❌ | ✅(同步三所) |
| 实例登录密码 | ❌ | ✅ | ❌ |
| 中控登录密码 | ❌ | ❌ | ✅ |
| Bridge / Flask 长期密钥 | ❌自动 | ❌ | ❌自动 |
| Bridge / Flask 长期密钥 | ❌(自动) | ❌ | ❌(自动) |
| SSO 链接 | — | — | 每次打开实例自动签发 |
---
## 6. 运维提示
- 修改 AI 后若未自动重启成功手动`pm2 restart manual-trading-hub crypto_okx crypto_binance crypto_gate --update-env`
- 三所 AI 不一致时中控 AI 配置页会提示「未完全同步」**保存并同步** 即可对齐
- 备份包可选包含各 `.env`中控设置 → 备份恢复
- 修改 AI 后若未自动重启成功,手动:`pm2 restart manual-trading-hub crypto_okx crypto_binance crypto_gate --update-env`
- 三所 AI 不一致时,中控 AI 配置页会提示「未完全同步」;**保存并同步** 即可对齐
- 备份包可选包含各 `.env`(中控设置 → 备份恢复)
相关文档[env配置说明.md](./env配置说明.md)[系统设置说明.md](./系统设置说明.md)[manual_trading_hub/局域网与反代部署说明.md](../manual_trading_hub/局域网与反代部署说明.md)SSO 2h 说明
相关文档:[env配置说明.md](./env配置说明.md),[系统设置说明.md](./系统设置说明.md),[manual_trading_hub/局域网与反代部署说明.md](../manual_trading_hub/局域网与反代部署说明.md)(SSO 2h 说明)
+35 -35
View File
@@ -1,37 +1,37 @@
# OKX 期权模块 — 技术方案
> 适用范围`crypto_monitor_okx` 实例与永续子账户并行不新增 PM2 进程
> 适用范围:`crypto_monitor_okx` 实例;与永续子账户并行,不新增 PM2 进程.
## 1. 目标
在现有 OKX 监控实例中增加 **USDⓈ 本位期权买方** 能力
在现有 OKX 监控实例中增加 **USDⓈ 本位期权(买方)** 能力:
- 永续/关键位继续走 **子账户 API-A**现有 `OKX_API_*`
- 期权**主账户 API-B**`OKX_OPTIONS_API_*`
- 资金展示对齐 OKX**资金账户 / 交易账户**分币种显示 USDTUSDCUSDG
- 永续/关键位:继续走 **子账户 API-A**(现有 `OKX_API_*`)
- 期权:**主账户 API-B**(`OKX_OPTIONS_API_*`)
- 资金展示对齐 OKX:**资金账户 / 交易账户**,分币种显示 USDT,USDC,USDG
- 支持 **手动 USDT→USDC 兑换** 与 **USDC 账户划转**
- **无总资金池上限**单笔权利金上限可配置默认 10 USDC
- **无总资金池上限**;单笔权利金上限可配置(默认 10 USDC)
## 2. 交易规则硬约束
## 2. 交易规则(硬约束)
| 规则 | 说明 |
|------|------|
| 仅买方 | 开仓 `buy`平仓 `sell`禁止卖方开仓 |
| 产品 | `BTC-USD_UM` / `ETH-USD_UM`线性USDC/USDG 结算 |
| 到期 | 仅展示 ≤2 日到期合约可配置 `OKX_OPTIONS_MAX_DTE_DAYS` |
| 虚实 | 仅 **轻度实值**`OKX_OPTIONS_ITM_ONLY` |
| 合约规格 | **1 张 = 0.01 ETH/BTC**`ctMult=0.01`以接口为准 |
| 仅买方 | 开仓 `buy`,平仓 `sell`;禁止卖方开仓 |
| 产品 | `BTC-USD_UM` / `ETH-USD_UM`(线性,USDC/USDG 结算) |
| 到期 | 仅展示 ≤2 日到期合约(可配置 `OKX_OPTIONS_MAX_DTE_DAYS`) |
| 虚实 | 仅 **轻度实值**(`OKX_OPTIONS_ITM_ONLY`) |
| 合约规格 | **1 张 = 0.01 ETH/BTC**(`ctMult=0.01`,以接口为准) |
| 报价单位 | 盘口 ask/bid = **每 1 ETH/BTC** 的 USD 价 |
| 权利金 | `总权利金 = 报价 × ETH数量``张数 = ETH数量 / 0.01` |
| 单笔预算 | `≤ OKX_OPTIONS_TRADE_BUDGET_USDC`默认 10),算张数 × `OKX_OPTIONS_BUDGET_BUFFER`默认 0.95 |
| 开仓 | 限价买单价格 = 卖一 |
| 平仓 | 限价卖单价格 = 买一市价需显式开启且二次确认 |
| 权利金 | `总权利金 = 报价 × ETH数量`;`张数 = ETH数量 / 0.01` |
| 单笔预算 | `≤ OKX_OPTIONS_TRADE_BUDGET_USDC`(默认 10),算张数 × `OKX_OPTIONS_BUDGET_BUFFER`(默认 0.95) |
| 开仓 | 限价买单,价格 = 卖一 |
| 平仓 | 限价卖单,价格 = 买一(市价需显式开启且二次确认) |
| 监控 | 浮盈 / 已付权利金 ≥ 100% → 企业微信推送一次 |
## 3. 架构
```
crypto_okx单 PM2
crypto_okx(单 PM2)
├── exchange (swap) ← OKX_API_* 子账户
└── exchange_options ← OKX_OPTIONS_API_* 主账户
@@ -43,25 +43,25 @@ lib/options/
└── options_register.py # 路由 + 监控线程
```
**隔离** 期权模块只调用 `exchange_options`永续逻辑只调用 `exchange`
**隔离:** 期权模块只调用 `exchange_options`;永续逻辑只调用 `exchange`.
## 4. 资金与兑换
### 4.1 展示期权页顶栏
### 4.1 展示(期权页顶栏)
| 账户 | 币种 |
|------|------|
| 资金账户 | USDTUSDC若有 |
| 交易账户 | USDTUSDCUSDG若有 |
| 资金账户 | USDT,USDC(若有) |
| 交易账户 | USDT,USDC,USDG(若有) |
不展示「练手池」等抽象记账名称
不展示「练手池」等抽象记账名称.
### 4.2 推荐操作流程
```
资金账户 USDT
→ [手动兑换 USDT→USDC]OKX Convert API资金账户内
→ [划转到交易账户]USDC
→ [手动兑换 USDT→USDC](OKX Convert API,资金账户内)
→ [划转到交易账户](USDC)
→ 交易账户 USDC
→ [限价买入期权]
```
@@ -70,12 +70,12 @@ lib/options/
| 接口 | OKX |
|------|-----|
| 余额 | `fetch_balance`funding / trading+ `GET /api/v5/asset/balances` |
| 余额 | `fetch_balance`(funding / trading)+ `GET /api/v5/asset/balances` |
| 询价兑换 | `POST /api/v5/asset/convert/estimate-quote` |
| 确认兑换 | `POST /api/v5/asset/convert/trade` |
| 划转 | `exchange.transfer(ccy, amt, from, to)` |
## 5. 配置项`.env`
## 5. 配置项(`.env`)
```bash
OKX_OPTIONS_ENABLED=false
@@ -95,17 +95,17 @@ OKX_OPTIONS_TD_MODE=cross
OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
```
修改 `.env` 后须 `pm2 restart crypto_okx`
修改 `.env` 后须 `pm2 restart crypto_okx`.
## 6. 数据库
### `options_trades`
记录本地开仓/平仓权利金翻倍提醒状态
记录本地开仓/平仓,权利金,翻倍提醒状态.
### `options_convert_log` / `options_transfer_log`
可选记录兑换与划转操作
可选记录兑换与划转操作.
## 7. HTTP 路由
@@ -124,20 +124,20 @@ OKX_OPTIONS_ALLOW_MARKET_CLOSE=false
## 8. 分阶段交付
1. **基础设施**双 API余额文档设置页说明
2. **兑换 + 划转**资金账户 USDT→USDC划转到交易户
3. **交易**:链、报价开平仓持仓
4. **监控**翻倍微信提醒
1. **基础设施**:双 API,余额,文档,设置页说明
2. **兑换 + 划转**:资金账户 USDT→USDC,划转到交易户
3. **交易**:链,报价,开平仓,持仓
4. **监控**:翻倍微信提醒
## 9. 不在一期范围
- 卖方组合单RFQ
- 卖方,组合单,RFQ
- 自动 USDT↔USDC
- `manual-agent-okx` / 中控聚合
- 币本位期权
## 10. 安全
- 期权 API**交易 + 读**禁止提币
- 期权 API:**交易 + 读**,禁止提币
- 日志不输出 Secret
- 下单前校验 `client is exchange_options`
+38 -38
View File
@@ -2,8 +2,8 @@
## 1. 前置条件
1. OKX **主账户**已开通期权USDⓈ 本位),且 App 中可见 `ETHUSD UM` / `BTCUSD UM`
2. 在 `crypto_monitor_okx/.env` 配置 **期权专用 API**与永续子账户分开):
1. OKX **主账户**已开通期权(USDⓈ 本位),且 App 中可见 `ETHUSD UM` / `BTCUSD UM`.
2. 在 `crypto_monitor_okx/.env` 配置 **期权专用 API**(与永续子账户分开):
```bash
OKX_OPTIONS_ENABLED=true
@@ -12,77 +12,77 @@ OKX_OPTIONS_API_SECRET=...
OKX_OPTIONS_API_PASSPHRASE=...
```
3. 重启实例`pm2 restart crypto_okx`
3. 重启实例:`pm2 restart crypto_okx`
> 永续仍用原有 `OKX_API_*`子账户);期权只用 `OKX_OPTIONS_API_*`主账户)。
> 永续仍用原有 `OKX_API_*`(子账户);期权只用 `OKX_OPTIONS_API_*`(主账户).
## 2. 资金准备
期权权利金使用 **USDC 或 USDG**不能直接用 USDT 买入
期权权利金使用 **USDC 或 USDG**,不能直接用 USDT 买入.
### 推荐步骤
1. 打开 **期权**查看顶栏
- **资金账户**USDT 余额
- **交易账户**USDC 余额买期权从这里扣
2. **币种兑换**资金账户内
1. 打开 **期权**,查看顶栏:
- **资金账户**:USDT 余额
- **交易账户**:USDC 余额(买期权从这里扣)
2. **币种兑换**(资金账户内)
- 从 USDT 兑换为 USDC
- 先点 **询价**确认预估获得量后点 **确认兑换**
- 先点 **询价**,确认预估获得量后点 **确认兑换**
3. **账户划转**
- 从资金账户 → 到交易账户
- 币种USDC
- 从:资金账户 → 到:交易账户
- 币种:USDC
- 将兑换得到的 USDC 划到交易账户
4. 确认 **交易账户 USDC** 足够支付本笔权利金
系统 **不会** 自动兑换或划转避免误动资金
系统 **不会** 自动兑换或划转,避免误动资金.
## 3. 下单流程
1. 顶栏进入 **期权**
2. 选择 **ETH** 或 **BTC**
3. 选择 **到期日**默认仅 12 日
3. 选择 **到期日**(默认仅 12 日)
4. 选择 **看涨 Call** 或 **看跌 Put**
5. 在行权价列表中选 **轻度实值** 合约
6. 查看
- **卖一价**每 1 ETH/BTC 的报价
6. 查看:
- **卖一价**(每 1 ETH/BTC 的报价)
- **张数 / ETH 数量**
- **预估权利金**USDC
7. 选择 **按预算打满**默认 10U×0.95或 **指定 ETH 数量**
8. 点击 **限价买入**价格 = 卖一
- **预估权利金**(USDC)
7. 选择 **按预算打满**(默认 10U×0.95)或 **指定 ETH 数量**
8. 点击 **限价买入**(价格 = 卖一)
### 张数说明
- **1 张 = 0.01 ETH**或 0.01 BTC— 与 OKX App「合约价值」一致
- **1 张 = 0.01 ETH**(或 0.01 BTC)— 与 OKX App「合约价值」一致
- 盘口报价是 **每 1 ETH** 的价格
报价 15.6买 0.5 ETH50 张→ 权利金 ≈ 15.6 × 0.5 = **7.8 USDC**
:报价 15.6,买 0.5 ETH(50 张)→ 权利金 ≈ 15.6 × 0.5 = **7.8 USDC**
## 4. 持仓与平仓
持仓表字段对齐 OKX合约张数开仓均价标记价浮盈收益率到期等
持仓表字段对齐 OKX:合约,张数,开仓均价,标记价,浮盈,收益率,到期等.
**平仓锁利/止损):**
**平仓(锁利/止损):**
1. 在持仓行点击 **平仓**
2. 查看 **买一价** 与预估收回
3. 确认 **限价卖出**价格 = 买一
3. 确认 **限价卖出**(价格 = 买一)
> 默认不使用市价平仓`.env` 开启 `OKX_OPTIONS_ALLOW_MARKET_CLOSE=true`市价按钮会出现并带风险提示
> 默认不使用市价平仓.`.env` 开启 `OKX_OPTIONS_ALLOW_MARKET_CLOSE=true`,市价按钮会出现并带风险提示.
## 5. 微信提醒
当某笔持仓 **未实现盈亏 ≥ 已付权利金的 100%**翻倍)时,会发 **一条** 企业微信提醒同一笔只提醒一次)。
当某笔持仓 **未实现盈亏 ≥ 已付权利金的 100%**(翻倍)时,会发 **一条** 企业微信提醒(同一笔只提醒一次).
需已配置 `WECHAT_WEBHOOK`
需已配置 `WECHAT_WEBHOOK`.
## 6. 与永续的关系
| | 永续子账户 | 期权主账户 |
| | 永续(子账户) | 期权(主账户) |
|--|----------------|----------------|
| API | `OKX_API_*` | `OKX_OPTIONS_API_*` |
| 页面 | 实盘下单 / 关键位 | 期权 |
| 资金顶栏 | USDT 资金户+交易户 | 期权页单独显示 USDC 等 |
两套资金 **不合并** 显示
两套资金 **不合并** 显示.
## 7. 配置说明
@@ -91,24 +91,24 @@ OKX_OPTIONS_API_PASSPHRASE=...
| `OKX_OPTIONS_TRADE_BUDGET_USDC` | 10 | 单笔权利金上限 |
| `OKX_OPTIONS_BUDGET_BUFFER` | 0.95 | 算张数时预留 5% 缓冲 |
| `OKX_OPTIONS_MAX_DTE_DAYS` | 2 | 最多选几天内到期 |
| `OKX_OPTIONS_ITM_MAX_DIST_USD` | 30 | 轻度实值价内不超过多少 USD |
| `OKX_OPTIONS_ITM_MAX_DIST_USD` | 30 | 轻度实值:价内不超过多少 USD |
| `OKX_OPTIONS_PROFIT_ALERT_RATIO` | 1.0 | 浮盈/权利金 ≥ 此值推送 |
## 8. 常见问题
**Q为什么买不了**
**Q:为什么买不了?**
- 交易账户 USDC 不足 → 先兑换再划转
- 卖一价过高10U 预算买不到 1 张 → 选更便宜合约或提高 `OKX_OPTIONS_TRADE_BUDGET_USDC`
- 卖一价过高,10U 预算买不到 1 张 → 选更便宜合约或提高 `OKX_OPTIONS_TRADE_BUDGET_USDC`
- 期权 API 未配置或 `OKX_OPTIONS_ENABLED=false`
**Q报价 15 是每张 15U 吗**
- 不是15 是 **每 1 ETH** 的报价每张0.01 ETH约 0.15 USDC
**Q:报价 15 是每张 15U 吗?**
- 不是.15 是 **每 1 ETH** 的报价;每张(0.01 ETH)约 0.15 USDC.
**Q子账户能开期权吗**
- 本系统期权走主账户 API子账户永续不受影响
**Q:子账户能开期权吗?**
- 本系统期权走主账户 API;子账户永续不受影响.
## 9. 风险说明
- 买方最大亏损为 **权利金**近期实值仍会时间衰减
- 买方最大亏损为 **权利金**;近期实值仍会时间衰减
- 限价单可能因无流动性未成交
- 请先在小额下验证兑换划转开平仓全流程
- 请先在小额下验证兑换,划转,开平仓全流程
+42 -42
View File
@@ -1,12 +1,12 @@
# 系统设置页说明
本文档描述各交易实例 Web 端 **「系统设置」** 页各区块功能与 env 配置页的分工以及导航显示开关规则
本文档描述各交易实例 Web 端 **「系统设置」** 页各区块功能,与 env 配置页的分工,以及导航显示开关规则.
---
## 1. 页面结构
系统设置为 **两列卡片** 布局各区块可在「导航显示」中单独开关见第 2 节)。
系统设置为 **两列卡片** 布局,各区块可在「导航显示」中单独开关(见第 2 节).
| 区块 | 默认显示 | 说明 |
|------|----------|------|
@@ -16,9 +16,9 @@
| 数据导出 | 可关 | 下载 CSV |
| 币种兑换 | 可关 | 仅 OKX 期权相关 |
| 期权资金划转 | 可关 | 仅 OKX |
| 期权设置面板 | OKX 有模块时 | 较大块占整行 |
| 期权设置面板 | OKX 有模块时 | 较大块,占整行 |
**固定不可隐藏**顶栏):关键位监控实盘下单系统设置
**固定不可隐藏**(顶栏):关键位监控,实盘下单,系统设置.
---
@@ -34,9 +34,9 @@
| 统计分析 | 统计分析 |
| 风控说明 | 风控说明 |
| env 配置 | env 配置 |
| 期权 | 期权仅 OKX 等有期权模块时有效 |
| 期权 | 期权(仅 OKX 等有期权模块时有效) |
保存后 **立即生效**无需重启中控 iframe 内嵌导航同步生效
保存后 **立即生效**,无需重启.中控 iframe 内嵌导航同步生效.
### 2.2 系统设置内区块开关
@@ -54,28 +54,28 @@
### 用途
- 修改 **直链打开实例**`/login` 使用的用户名与密码
- 写入本实例目录 `.env``APP_USERNAME``APP_PASSWORD`
- **三所建议使用相同账号**便于记忆本页仅改 **当前实例**`.env`若需三所一致请分别保存或后续做批量同步
- 修改 **直链打开实例**`/login` 使用的用户名与密码.
- 写入本实例目录 `.env``APP_USERNAME`,`APP_PASSWORD`.
- **三所建议使用相同账号**,便于记忆;本页仅改 **当前实例**`.env`,若需三所一致请分别保存或后续做批量同步.
### 与中控 / 密钥的关系
| 项目 | 是否在系统设置改 | 说明 |
|------|------------------|------|
| 网页登录密码 | ✅ | 本区块 |
| 中控通信密钥 `HUB_BRIDGE_TOKEN` | ❌ | 部署时自动生成中控与实例一致 |
| 登录会话密钥 `FLASK_SECRET_KEY` | ❌ | 部署时自动生成三所相同 |
| 交易所 API | ❌ | 在 **env 配置**各所自配 |
| AI 复盘 / OpenAI | ❌ | 在中控 **系统设置 → AI 配置**同步三所 |
| 中控通信密钥 `HUB_BRIDGE_TOKEN` | ❌ | 部署时自动生成,中控与实例一致 |
| 登录会话密钥 `FLASK_SECRET_KEY` | ❌ | 部署时自动生成,三所相同 |
| 交易所 API | ❌ | 在 **env 配置**(各所自配) |
| AI 复盘 / OpenAI | ❌ | 在中控 **系统设置 → AI 配置**(同步三所) |
### 操作流程
1. 输入 **当前密码**`.env``APP_PASSWORD` 一致)。
2. 可选填 **新用户名**不填则保持原用户名
3. 输入 **新密码****确认密码**至少 6 位)。
4. 保存后 **自动重启当前实例**PM2),请用新密码登录
1. 输入 **当前密码**(`.env``APP_PASSWORD` 一致).
2. 可选填 **新用户名**;不填则保持原用户名.
3. 输入 **新密码****确认密码**(至少 6 位).
4. 保存后 **自动重启当前实例**(PM2),请用新密码登录.
经中控 SSO 打开实例时通常无需输入实例密码改密主要影响 **直链访问**
经中控 SSO 打开实例时,通常无需输入实例密码;改密主要影响 **直链访问**.
---
@@ -83,7 +83,7 @@
### 用途
**子账户永续** 场景下**资金账户funding****交易账户swap** 之间手动划转 USDT
**子账户永续** 场景下,**资金账户(funding)****交易账户(swap)** 之间手动划转 USDT.
### 与 env 配置的关系
@@ -92,36 +92,36 @@
| **手动**划转一笔 | ✅ 本区块 | ❌ |
| **自动**每日划转规则 | ❌ | ✅「自动划转」卡片 |
自动划转规则开关目标余额整点等在 env 配置中维护见 [env配置说明.md](./env配置说明.md)
自动划转规则(开关,目标余额,整点等)在 env 配置中维护,见 [env配置说明.md](./env配置说明.md).
---
## 5. 数据导出
提供 CSV 下载版本号见页内标注):
提供 CSV 下载(版本号见页内标注):
| 链接 | 内容 |
|------|------|
| 交易记录 | 成交/订单相关导出 |
| 复盘记录 | 复盘日记 |
| 关键位当前 | 当前关键位列表 |
| 关键位(当前) | 当前关键位列表 |
| 关键位历史 | 历史关键位 |
导出为只读操作不修改配置
导出为只读操作,不修改配置.
---
## 6. 期权相关仅 OKX
## 6. 期权相关(仅 OKX)
当实例启用期权模块时系统设置可能包含
当实例启用期权模块时,系统设置可能包含:
- **币种兑换**期权账户内币种兑换操作
- **期权资金划转**期权与永续/资金账户间划转
- **期权设置面板**页内期权参数与状态大块区域
- **币种兑换**:期权账户内币种兑换操作
- **期权资金划转**:期权与永续/资金账户间划转
- **期权设置面板**:页内期权参数与状态(大块区域)
是否在顶栏显示「期权」Tab**导航显示 → 期权** 控制是否在设置页显示兑换/划转卡片由对应子开关控制
是否在顶栏显示「期权」Tab,**导航显示 → 期权** 控制;是否在设置页显示兑换/划转卡片,由对应子开关控制.
期权 env 参数API预算策略默认值**env 配置 → 期权账户** 维护见 [期权用法.md](./期权用法.md)
期权 env 参数(API,预算,策略默认值)**env 配置 → 期权账户** 维护,见 [期权用法.md](./期权用法.md).
---
@@ -129,30 +129,30 @@
| 页面 | 顶栏资金信息 | 说明 |
|------|--------------|------|
| 关键位实盘策略等 | 显示 | 含资金盈亏等 |
| 系统设置风控说明env 配置 | 隐藏资金条 | 与实盘顶栏共用组件设置类页面简化展示 |
| 关键位,实盘,策略等 | 显示 | 含资金,盈亏等 |
| 系统设置,风控说明,env 配置 | 隐藏资金条 | 与实盘顶栏共用组件,设置类页面简化展示 |
主题切换明/暗在系统设置页可用若已接入主题切换 UI)。
主题切换(明/暗)在系统设置页可用(若已接入主题切换 UI).
---
## 8. 权限与安全
- 所有设置 API 需 **已登录**或部署时 `APP_AUTH_DISABLED=true` 的联调环境)。
- 改密env 保存PM2 重启等写操作 **不接受** 仅带 `X-Hub-Token` 的中控请求修改防止中控误改实例配置)。
- 生产环境建议 `APP_AUTH_DISABLED=false`公网务必开启登录校验
- 所有设置 API 需 **已登录**(或部署时 `APP_AUTH_DISABLED=true` 的联调环境).
- 改密,env 保存,PM2 重启等写操作 **不接受** 仅带 `X-Hub-Token` 的中控请求修改(防止中控误改实例配置).
- 生产环境建议 `APP_AUTH_DISABLED=false`,公网务必开启登录校验.
---
## 9. 首次部署时的账号与密钥规划
## 9. 首次部署时的账号与密钥(规划)
以下由 **部署脚本** 自动完成**不在** 系统设置或 env 配置页手工填写
以下由 **部署脚本** 自动完成,**不在** 系统设置或 env 配置页手工填写:
1. **生成 `HUB_BRIDGE_TOKEN`** → 写入中控 + 三实例 `.env`相同)。
2. **生成 `FLASK_SECRET_KEY`** → 写入三实例 `.env`三所相同)。
3. **生成初始 `APP_USERNAME=admin``APP_PASSWORD=admin123`** → 写入三实例仅当尚未配置时);用户日后在 **系统设置** 改密
1. **生成 `HUB_BRIDGE_TOKEN`** → 写入中控 + 三实例 `.env`(相同).
2. **生成 `FLASK_SECRET_KEY`** → 写入三实例 `.env`(三所相同).
3. **生成初始 `APP_USERNAME=admin`,`APP_PASSWORD=admin123`** → 写入三实例(仅当尚未配置时);用户日后在 **系统设置** 改密.
脚本应对 **已有非空值** 跳过写入避免覆盖生产环境
脚本应对 **已有非空值** 跳过写入,避免覆盖生产环境.
---
+45 -45
View File
@@ -1,7 +1,7 @@
"""大模型调用OpenAI 兼容接口默认或本机 Ollama 二选一
"""大模型调用:OpenAI 兼容接口(默认)或本机 Ollama 二选一.
配置从 os.environ 惰性读取各实例 app.py import 本模块后才 load_env_file(.env)
若在 import 时缓存变量会导致 OPENAI_API_KEY 始终为空
配置从 os.environ 惰性读取:各实例 app.py import 本模块后才 load_env_file(.env),
若在 import 时缓存变量会导致 OPENAI_API_KEY 始终为空.
"""
from __future__ import annotations
@@ -142,7 +142,7 @@ def _openai_chat_completion(
) -> Tuple[str, str]:
api_key = _openai_api_key()
if not api_key:
return "AI 调用失败未配置 OPENAI_API_KEY请在当前实例目录 .env 中设置修改后需重启服务", "error"
return "AI 调用失败:未配置 OPENAI_API_KEY(请在当前实例目录 .env 中设置,修改后需重启服务)", "error"
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
@@ -164,7 +164,7 @@ def _openai_chat_completion(
data = r.json()
choices = data.get("choices") or []
if not choices:
return "AI 生成失败响应无 choices", "error"
return "AI 生成失败:响应无 choices", "error"
choice = choices[0] or {}
msg = choice.get("message") or {}
text = _openai_message_text(msg)
@@ -187,7 +187,7 @@ def _openai_chat_completion(
if text2:
return text2, str((choices2[0] or {}).get("finish_reason") or finish)
if not text:
return "AI 生成失败空内容", finish or "error"
return "AI 生成失败:空内容", finish or "error"
return text, finish
@@ -257,7 +257,7 @@ def ai_generate(
temperature: float = 0.2,
max_tokens: int | None = None,
) -> str:
"""统一文本生成失败时返回以「AI 调用失败」开头的说明"""
"""统一文本生成;失败时返回以「AI 调用失败」开头的说明."""
images = _collect_images(image_paths, images_b64)
try:
if _use_openai():
@@ -271,17 +271,17 @@ def ai_generate(
except Exception:
pass
prov = "OpenAI" if _use_openai() else "Ollama"
return f"AI 调用失败{prov} HTTP {e.response.status_code if e.response else '?'}):{detail or str(e)}"
return f"AI 调用失败({prov} HTTP {e.response.status_code if e.response else '?'}):{detail or str(e)}"
except Exception as e:
prov = "OpenAI" if _use_openai() else "Ollama"
return f"AI 调用失败{prov}):{str(e)}"
return f"AI 调用失败({prov}):{str(e)}"
_CHAT_CONTINUE_USER = (
"你上一条回复在中途截断了请从断点处继续写完不要重复已写内容"
"保持同一语气编号列表每条单独一行"
"你上一条回复在中途截断了.请从断点处继续写完,不要重复已写内容,"
"保持同一语气;编号列表每条单独一行."
)
_CHAT_END_CHARS = "。!?.!?\"」』)>】"
_CHAT_END_CHARS = ".!?.!?\"」』))>】"
_INCOMPLETE_TAIL_RE = re.compile(
r"(不会|不能|没有|会不会|是不是|够不够|能不能|要不要|如何|怎么|什么|哪里|多少|对吗|怎么样|"
r"这个\.\.\.|这个…|\.\.\.\d+\.|\d+\.)$"
@@ -300,7 +300,7 @@ def _looks_truncated(text: str) -> bool:
return True
if re.search(r"\d+\.\s*$", t):
return True
return t[-1] not in ",、,;:\n"
return t[-1] not in ",,,;;::\n"
def _should_continue(reason: str, full_text: str) -> bool:
@@ -313,16 +313,16 @@ def _chat_continue_message(full_text: str) -> str:
tail = full_text[-500:] if len(full_text) > 500 else full_text
return (
f"{_CHAT_CONTINUE_USER}\n\n"
f"已写到最后这几句\n{tail}\n\n"
f"请从断点接着写完不要重复前文最后一句话必须以句号问号或感叹号结束"
f"已写到最后这几句:\n{tail}\n\n"
f"请从断点接着写完.不要重复前文;最后一句话必须以句号,问号或感叹号结束."
)
def _chat_continue_system(system: str) -> str:
return (
f"{system.strip()}\n\n"
"【续写模式】只输出断点后的剩余内容不要重复前文"
"列表每条单独一行必须以句号问号或感叹号收尾"
"【续写模式】只输出断点后的剩余内容,不要重复前文;"
"列表每条单独一行;必须以句号,问号或感叹号收尾."
)
@@ -335,7 +335,7 @@ def ai_generate_chat(
max_tokens: int = 8192,
max_continuations: int = 4,
) -> str:
"""聊天专用system/user 分消息输出触顶时轻量续写不重复巨型上下文)。"""
"""聊天专用:system/user 分消息;输出触顶时轻量续写(不重复巨型上下文)."""
images = _collect_images(None, images_b64)
max_rounds = max(1, int(max_continuations) + 1)
try:
@@ -377,7 +377,7 @@ def ai_generate_chat(
{"role": "assistant", "content": full},
{"role": "user", "content": _chat_continue_message(full)},
]
return "".join(parts).strip() or "AI 生成失败空内容"
return "".join(parts).strip() or "AI 生成失败:空内容"
prompt = f"{system.strip()}\n\n---\n\n{user.strip()}"
parts: list[str] = []
@@ -405,7 +405,7 @@ def ai_generate_chat(
full = "".join(parts)
if not _should_continue(reason, full) or attempt >= max_rounds - 1:
break
return "".join(parts).strip() or "AI 生成失败空内容"
return "".join(parts).strip() or "AI 生成失败:空内容"
except requests.HTTPError as e:
detail = ""
try:
@@ -413,46 +413,46 @@ def ai_generate_chat(
except Exception:
pass
prov = "OpenAI" if _use_openai() else "Ollama"
return f"AI 调用失败{prov} HTTP {e.response.status_code if e.response else '?'}):{detail or str(e)}"
return f"AI 调用失败({prov} HTTP {e.response.status_code if e.response else '?'}):{detail or str(e)}"
except Exception as e:
prov = "OpenAI" if _use_openai() else "Ollama"
return f"AI 调用失败{prov}):{str(e)}"
return f"AI 调用失败({prov}):{str(e)}"
def ai_review(trades_text: str, period_title: str, image_paths=None) -> str:
n_img = len(image_paths or [])
period_label = "" if "" in str(period_title) else ""
attach_note = (
f"️ 【系统说明已向模型附带 {n_img} 张复盘附图自动K线或上传截图),请结合附图分析第5节\n\n"
f"️ 【系统说明:已向模型附带 {n_img} 张复盘附图(自动K线或上传截图),请结合附图分析第5节.\n\n"
if n_img
else "️ 【系统说明本次未附带复盘附图第5节请写明「无附图无法看图」保存复盘记录时可勾选「自动生成K线图」\n\n"
else "️ 【系统说明:本次未附带复盘附图,第5节请写明「无附图,无法看图」;保存复盘记录时可勾选「自动生成K线图」.\n\n"
)
prompt = f"""
你是一位专业交易教练下面是用户的{period_title}交易记录请做简洁可执行的复盘中文
你是一位专业交易教练.下面是用户的{period_title}交易记录,请做简洁,可执行的复盘(中文).
硬性规则 必须遵守
- 你只能根据交易记录**明确出现的字段**陈述事实禁止编造是否触发止损是否扛单亏损是否扩大图上具体结构/进出场点位等记录里**没有**的信息
- 平仓/离场只是交易员自述摘要不是客观成交明细若记录未写明代币是否打到止损价是否软件平仓等不要断言执行路径可用在记录有限前提下一种可能是或简短写执行路径记录不足无法判断
- 提前离场类结论必须优先依据记录中的提前离场记录字段若该段全为或未出现有效内容不得写道明显扛单拒不止损未执行硬止损
- 实际RR为负只说明结果相对于预期RR不利不等同于风控失灵止损纪律崩溃除非记录里另有依据
- 禁止用语人身攻击夸张定性致命伤灾难语气克制对事不对人
- 若有截图且你能辨认再结合图讨论看不清或无明确定位则明确说无法从图确认不得虚构 K 线故事
- 你只能根据交易记录**明确出现的字段**陈述事实;禁止编造:是否触发止损,是否扛单,亏损是否扩大,图上具体结构/进出场点位等记录里**没有**的信息.
- 平仓/离场只是交易员自述摘要,不是客观成交明细;若记录未写明代币是否打到止损价,是否软件平仓等,不要断言执行路径,可用在记录有限前提下,一种可能是或简短写执行路径记录不足,无法判断.
- 提前离场类结论必须优先依据记录中的提前离场记录字段;若该段全为或未出现有效内容,不得写道明显扛单拒不止损未执行硬止损.
- 实际RR为负只说明结果相对于预期RR不利,不等同于风控失灵止损纪律崩溃,除非记录里另有依据.
- 禁止用语:人身攻击,夸张定性(致命伤灾难);语气克制,对事不对人.
- 若有截图且你能辨认,再结合图讨论;看不清或无明确定位则明确说无法从图确认,不得虚构 K 线故事.
输出格式 Markdown必须严格遵守
- 第一行**交易复盘报告{period_label}**
- 五个大节标题必须**完全一致** emoji不要用其它编号或改名
输出格式 Markdown,必须严格遵守
- 第一行:**交易复盘报告({period_label})**
- 五个大节标题必须**完全一致**( emoji,不要用其它编号或改名):
**1. 📊 总体盈亏结构**
**2. 🧠 心态与执行**
**3. 🏷 行为标签**
**4. 改进建议**
**5. 📈 图表分析**
- 每节正文用 `- **子项名**内容` 列表第4节改进建议用有序列表 `1. 2. 3.`
- 第1节至少包含**笔数/盈亏****风险回报比****总结**
- 第2节至少包含**得分**110**依据**对应记录字段
- 第5节至少包含**趋势确认****执行路径**记录不足则写明
- 语气简洁少形容词不要输出代码块不要表格
- 每节正文用 `- **子项名**:内容` 列表;第4节改进建议用有序列表 `1. 2. 3.`
- 第1节至少包含:**笔数/盈亏**,**风险回报比**,**总结**
- 第2节至少包含:**得分**(110),**依据**(对应记录字段)
- 第5节至少包含:**趋势确认**,**执行路径**(记录不足则写明)
- 语气简洁,少形容词;不要输出代码块,不要表格
交易记录
交易记录:
{trades_text}
""".strip()
return attach_note + ai_generate(prompt, image_paths=image_paths, temperature=0.2)
@@ -460,12 +460,12 @@ def ai_review(trades_text: str, period_title: str, image_paths=None) -> str:
def ai_short_advice(prompt_text: str) -> str:
prompt = f"""
你是交易风控助理请用中文给出**最多 3 **提醒要求
你是交易风控助理.请用中文给出**最多 3 **提醒,要求:
- 每条不超过 25 个字
- 语气克制具体可执行
- 不要输出 Markdown不要编号前缀以外的废话
- 语气克制,具体,可执行
- 不要输出 Markdown,不要编号前缀以外的废话
场景
场景:
{prompt_text}
""".strip()
return ai_generate(prompt, temperature=0.2)
@@ -478,7 +478,7 @@ def ai_provider_label() -> str:
def ai_config_status() -> dict:
"""调试用当前进程内读到的 AI 配置不含密钥明文)。"""
"""调试用:当前进程内读到的 AI 配置(不含密钥明文)."""
key = _openai_api_key()
return {
"provider": _ai_provider(),
+178 -178
View File
@@ -1,178 +1,178 @@
"""AI 日复盘 / 周复盘附图收集与 journal 文本格式化三所共用)。"""
from __future__ import annotations
import os
import uuid
from typing import Any, Callable, List, Mapping, Optional, Sequence
from lib.instance.journal_chart_lib import (
JOURNAL_CHART_ANCHOR_CLOSE,
JOURNAL_CHART_DEFAULT_LIMIT,
JOURNAL_CHART_DEFAULT_TF1,
JOURNAL_CHART_DEFAULT_TF2,
normalize_chart_timeframe,
)
from lib.instance.journal_images_lib import journal_image_paths
def _journal_nz(v: Any, default: str = "") -> str:
if v is None:
return default
s = str(v).strip()
return s if s else default
def _row_get(row: Any, key: str, default: Any = None) -> Any:
"""兼容 dict 与 sqlite3.RowRow 无 .get 方法)。"""
if row is None:
return default
getter = getattr(row, "get", None)
if callable(getter):
return getter(key, default)
try:
keys = row.keys() if hasattr(row, "keys") else ()
if key in keys:
return row[key]
except Exception:
pass
try:
return row[key]
except (KeyError, TypeError, IndexError):
return default
def journal_row_lines_for_ai(
idx: int,
row: Any,
*,
include_hold_duration: bool = True,
) -> str:
"""把 journal 字段拼成给 AI 的文本三所日复盘/周复盘共用"""
lines = [
(
f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} "
f"| 盈亏:{_journal_nz(_row_get(row, 'pnl'))}U "
f"| 实际RR:{_journal_nz(_row_get(row, 'real_rr'))} "
f"| 预期RR:{_journal_nz(_row_get(row, 'expect_rr'))}"
),
f" 开仓逻辑{_journal_nz(_row_get(row, 'entry_reason'))}",
f" 平仓/离场交易员自述):{_journal_nz(_row_get(row, 'exit_reason'))}",
]
if include_hold_duration:
lines.append(f" 持仓时长{_journal_nz(_row_get(row, 'hold_duration'))}")
ee_bits = [
_journal_nz(_row_get(row, "early_exit")),
_journal_nz(_row_get(row, "early_exit_reason")),
_journal_nz(_row_get(row, "early_exit_trigger")),
_journal_nz(_row_get(row, "early_exit_note")),
]
if any(x != "" for x in ee_bits):
lines.append(
" 提前离场记录"
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
)
mood_bits = f"心态标签{_journal_nz(_row_get(row, 'mood_issues'))}"
mood_score = _row_get(row, "mood_score")
if mood_score is not None:
mood_bits += f" | 自评心态分{mood_score}"
lines.append(f" {mood_bits}")
if _journal_nz(_row_get(row, "post_breakeven_stare")) != "":
lines.append(f" 保本后盯盘{_journal_nz(_row_get(row, 'post_breakeven_stare'))}")
if _journal_nz(_row_get(row, "note")) != "":
lines.append(f" 备注{_journal_nz(_row_get(row, 'note'))}")
return "\n".join(lines) + "\n"
def collect_images_for_ai_review(
rows: Sequence,
upload_folder: str,
*,
build_chart_if_missing: Optional[Callable] = None,
) -> List[str]:
"""
收集传给视觉模型的本地图片路径
- 优先 journal_entries.images_json / image 已存附图含多周期手动上传
- 若无附图且提供 build_chart_if_missing则临时生成 K 线图
"""
paths: List[str] = []
seen = set()
upload_folder = os.path.abspath(upload_folder or "")
for row in rows or []:
row_paths = journal_image_paths(row, upload_folder)
if row_paths:
for candidate in row_paths:
if candidate not in seen:
seen.add(candidate)
paths.append(candidate)
continue
if build_chart_if_missing:
try:
candidate = build_chart_if_missing(row)
except Exception:
candidate = None
if not candidate:
continue
candidate = os.path.abspath(candidate)
if os.path.isfile(candidate) and candidate not in seen:
seen.add(candidate)
paths.append(candidate)
return paths
def build_journal_ai_chart_path(
row,
upload_folder: str,
*,
order_chart_enabled: bool,
normalize_exchange_symbol_fn: Callable[[str], str],
generate_chart_fn: Callable,
local_datetime_to_ms_fn: Callable[[str], Optional[int]],
now_ts_ms_fn: Callable[[], int],
) -> Optional[str]:
"""无已存附图时按复盘记录开平仓时间临时生成 K 线图路径"""
if not order_chart_enabled:
return None
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
return None
coin = (row["coin"] if "coin" in keys else "") or ""
coin = str(coin).strip()
if not coin:
return None
try:
symbol = normalize_exchange_symbol_fn(coin)
except Exception:
return None
open_dt = row["open_datetime"] if "open_datetime" in keys else ""
close_dt = row["close_datetime"] if "close_datetime" in keys else ""
entry_ms = local_datetime_to_ms_fn(open_dt)
exit_ms = local_datetime_to_ms_fn(close_dt)
if not entry_ms:
return None
row_tf = row["tf"] if "tf" in keys else ""
tf1 = normalize_chart_timeframe(row_tf) or JOURNAL_CHART_DEFAULT_TF1
tf2 = JOURNAL_CHART_DEFAULT_TF2 if tf1 != JOURNAL_CHART_DEFAULT_TF2 else "1h"
row_id = str(row["id"] if "id" in keys else "")[:8] or uuid.uuid4().hex[:8]
marker = {
"entry_ts_ms": entry_ms,
"exit_ts_ms": exit_ms,
"chart_anchor": JOURNAL_CHART_ANCHOR_CLOSE,
"now_ts_ms": int(now_ts_ms_fn()),
}
fname = f"ai_rev_{row_id}_{uuid.uuid4().hex[:6]}.png"
saved = generate_chart_fn(
symbol,
f"AI复盘 {coin}",
timeframes=[tf1, tf2],
limit=JOURNAL_CHART_DEFAULT_LIMIT,
out_dir=upload_folder,
filename=fname,
marker_payload=marker,
marker_timeframes={tf1, tf2},
layout="vertical",
)
if not saved:
return None
path = os.path.join(upload_folder, saved)
return path if os.path.isfile(path) else None
"""AI 日复盘 / 周复盘:附图收集与 journal 文本格式化(三所共用)."""
from __future__ import annotations
import os
import uuid
from typing import Any, Callable, List, Mapping, Optional, Sequence
from lib.instance.journal_chart_lib import (
JOURNAL_CHART_ANCHOR_CLOSE,
JOURNAL_CHART_DEFAULT_LIMIT,
JOURNAL_CHART_DEFAULT_TF1,
JOURNAL_CHART_DEFAULT_TF2,
normalize_chart_timeframe,
)
from lib.instance.journal_images_lib import journal_image_paths
def _journal_nz(v: Any, default: str = "") -> str:
if v is None:
return default
s = str(v).strip()
return s if s else default
def _row_get(row: Any, key: str, default: Any = None) -> Any:
"""兼容 dict 与 sqlite3.Row(Row 无 .get 方法)."""
if row is None:
return default
getter = getattr(row, "get", None)
if callable(getter):
return getter(key, default)
try:
keys = row.keys() if hasattr(row, "keys") else ()
if key in keys:
return row[key]
except Exception:
pass
try:
return row[key]
except (KeyError, TypeError, IndexError):
return default
def journal_row_lines_for_ai(
idx: int,
row: Any,
*,
include_hold_duration: bool = True,
) -> str:
"""把 journal 字段拼成给 AI 的文本;三所日复盘/周复盘共用."""
lines = [
(
f"{idx}. {_journal_nz(_row_get(row, 'coin'))} {_journal_nz(_row_get(row, 'tf'))} "
f"| 盈亏:{_journal_nz(_row_get(row, 'pnl'))}U "
f"| 实际RR:{_journal_nz(_row_get(row, 'real_rr'))} "
f"| 预期RR:{_journal_nz(_row_get(row, 'expect_rr'))}"
),
f" 开仓逻辑:{_journal_nz(_row_get(row, 'entry_reason'))}",
f" 平仓/离场(交易员自述):{_journal_nz(_row_get(row, 'exit_reason'))}",
]
if include_hold_duration:
lines.append(f" 持仓时长:{_journal_nz(_row_get(row, 'hold_duration'))}")
ee_bits = [
_journal_nz(_row_get(row, "early_exit")),
_journal_nz(_row_get(row, "early_exit_reason")),
_journal_nz(_row_get(row, "early_exit_trigger")),
_journal_nz(_row_get(row, "early_exit_note")),
]
if any(x != "" for x in ee_bits):
lines.append(
" 提前离场记录:"
f"{ee_bits[0]} | 原因:{ee_bits[1]} | 触发:{ee_bits[2]} | 备注:{ee_bits[3]}"
)
mood_bits = f"心态标签:{_journal_nz(_row_get(row, 'mood_issues'))}"
mood_score = _row_get(row, "mood_score")
if mood_score is not None:
mood_bits += f" | 自评心态分:{mood_score}"
lines.append(f" {mood_bits}")
if _journal_nz(_row_get(row, "post_breakeven_stare")) != "":
lines.append(f" 保本后盯盘:{_journal_nz(_row_get(row, 'post_breakeven_stare'))}")
if _journal_nz(_row_get(row, "note")) != "":
lines.append(f" 备注:{_journal_nz(_row_get(row, 'note'))}")
return "\n".join(lines) + "\n"
def collect_images_for_ai_review(
rows: Sequence,
upload_folder: str,
*,
build_chart_if_missing: Optional[Callable] = None,
) -> List[str]:
"""
收集传给视觉模型的本地图片路径.
- 优先 journal_entries.images_json / image 已存附图(含多周期手动上传);
- 若无附图且提供 build_chart_if_missing,则临时生成 K 线图.
"""
paths: List[str] = []
seen = set()
upload_folder = os.path.abspath(upload_folder or "")
for row in rows or []:
row_paths = journal_image_paths(row, upload_folder)
if row_paths:
for candidate in row_paths:
if candidate not in seen:
seen.add(candidate)
paths.append(candidate)
continue
if build_chart_if_missing:
try:
candidate = build_chart_if_missing(row)
except Exception:
candidate = None
if not candidate:
continue
candidate = os.path.abspath(candidate)
if os.path.isfile(candidate) and candidate not in seen:
seen.add(candidate)
paths.append(candidate)
return paths
def build_journal_ai_chart_path(
row,
upload_folder: str,
*,
order_chart_enabled: bool,
normalize_exchange_symbol_fn: Callable[[str], str],
generate_chart_fn: Callable,
local_datetime_to_ms_fn: Callable[[str], Optional[int]],
now_ts_ms_fn: Callable[[], int],
) -> Optional[str]:
"""无已存附图时,按复盘记录开平仓时间临时生成 K 线图路径."""
if not order_chart_enabled:
return None
try:
keys = row.keys() if hasattr(row, "keys") else []
except Exception:
return None
coin = (row["coin"] if "coin" in keys else "") or ""
coin = str(coin).strip()
if not coin:
return None
try:
symbol = normalize_exchange_symbol_fn(coin)
except Exception:
return None
open_dt = row["open_datetime"] if "open_datetime" in keys else ""
close_dt = row["close_datetime"] if "close_datetime" in keys else ""
entry_ms = local_datetime_to_ms_fn(open_dt)
exit_ms = local_datetime_to_ms_fn(close_dt)
if not entry_ms:
return None
row_tf = row["tf"] if "tf" in keys else ""
tf1 = normalize_chart_timeframe(row_tf) or JOURNAL_CHART_DEFAULT_TF1
tf2 = JOURNAL_CHART_DEFAULT_TF2 if tf1 != JOURNAL_CHART_DEFAULT_TF2 else "1h"
row_id = str(row["id"] if "id" in keys else "")[:8] or uuid.uuid4().hex[:8]
marker = {
"entry_ts_ms": entry_ms,
"exit_ts_ms": exit_ms,
"chart_anchor": JOURNAL_CHART_ANCHOR_CLOSE,
"now_ts_ms": int(now_ts_ms_fn()),
}
fname = f"ai_rev_{row_id}_{uuid.uuid4().hex[:6]}.png"
saved = generate_chart_fn(
symbol,
f"AI复盘 {coin}",
timeframes=[tf1, tf2],
limit=JOURNAL_CHART_DEFAULT_LIMIT,
out_dir=upload_folder,
filename=fname,
marker_payload=marker,
marker_timeframes={tf1, tf2},
layout="vertical",
)
if not saved:
return None
path = os.path.join(upload_folder, saved)
return path if os.path.isfile(path) else None
+16 -16
View File
@@ -1,9 +1,9 @@
"""
每日自动划转北京时间指定整点小时内将交易账户(AUTO_TRANSFER_TO)余额调整至目标额
每日自动划转:北京时间指定整点小时内,将交易账户(AUTO_TRANSFER_TO)余额调整至目标额.
- 交易账户 < 目标从资金账户划入差额
- 交易账户 > 目标将多余划回资金账户
- active 持仓不划转写账簿并企业微信说明
- 交易账户 < 目标:从资金账户划入差额
- 交易账户 > 目标:将多余划回资金账户
- active 持仓:不划转,写账簿并企业微信说明
"""
from __future__ import annotations
@@ -65,12 +65,12 @@ def run_auto_transfer_once_per_day(
active = get_active_position_count(conn)
if active > 0:
msg = f"持仓中{active}),本次资金无划转"
msg = f"持仓中({active}),本次资金无划转"
_log(0, from_account, to_account, "skipped", msg)
send_wechat_msg(
f"自动划转{msg}\n"
f"目标{to_account} 调整至 {round(float(target_amount), funds_decimals)}U\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
f"自动划转:{msg}\n"
f"目标:{to_account} 调整至 {round(float(target_amount), funds_decimals)}U\n"
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
)
return
@@ -95,7 +95,7 @@ def run_auto_transfer_once_per_day(
from_account,
to_account,
"skipped",
f"{to_account}账户已为{trade}U目标{target}U",
f"{to_account}账户已为{trade}U(目标{target}U)",
)
return
@@ -109,10 +109,10 @@ def run_auto_transfer_once_per_day(
from_bal = get_account_usdt_total(fr)
if from_bal is not None and round(float(from_bal), funds_decimals) < amount:
cur = round(float(from_bal), funds_decimals)
_log(amount, fr, to, "failed", f"{fr}账户USDT不足{amount}U当前{cur}U")
_log(amount, fr, to, "failed", f"{fr}账户USDT不足,{amount}U,当前{cur}U")
send_wechat_msg(
f"自动划转失败{fr}余额不足{amount}U当前{cur}U{action}{to_account}目标{target}U\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
f"自动划转失败:{fr}余额不足,{amount}U,当前{cur}U({action}{to_account}目标{target}U)\n"
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
)
return
@@ -120,11 +120,11 @@ def run_auto_transfer_once_per_day(
_log(amount, fr, to, "success" if ok else "failed", msg)
if ok:
send_wechat_msg(
f"自动划转成功{to_account} {trade}U→目标{target}U{action}{amount}U {fr}->{to}\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
f"自动划转成功:{to_account} {trade}U→目标{target}U,{action}{amount}U {fr}->{to}\n"
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
)
else:
send_wechat_msg(
f"自动划转失败计划{action}{amount}U {fr}->{to}目标{target}U\n原因{msg}\n"
f"账簿日(UTC){transfer_day}|触发时刻(北京){app_now_str()}"
f"自动划转失败:计划{action}{amount}U {fr}->{to}(目标{target}U)\n原因:{msg}\n"
f"账簿日(UTC):{transfer_day}|触发时刻(北京):{app_now_str()}"
)
+4 -4
View File
@@ -1,4 +1,4 @@
"""防重复提交Flask session 短窗口去重下单 / 关键位等)。"""
"""防重复提交:Flask session 短窗口去重(下单 / 关键位等)."""
from __future__ import annotations
import time
@@ -19,8 +19,8 @@ def check_duplicate_submit(
ttl: float = DEFAULT_SUBMIT_GUARD_TTL,
) -> Optional[str]:
"""
同一 scope ttl 秒内仅允许通过一次
返回提示文案表示应拒绝返回 None 表示可继续处理
同一 scope ttl 秒内仅允许通过一次.
返回提示文案表示应拒绝;返回 None 表示可继续处理.
"""
scope = (scope or "").strip()
if not scope:
@@ -28,7 +28,7 @@ def check_duplicate_submit(
now = time.time()
locks = _prune_locks(session.get("_form_submit_guard") or {}, now)
if scope in locks:
return "请求正在处理或刚提交过请勿重复点击请等待页面刷新后再试"
return "请求正在处理或刚提交过,请勿重复点击(请等待页面刷新后再试)"
locks[scope] = now + float(ttl)
session["_form_submit_guard"] = locks
try:
+187 -187
View File
@@ -1,187 +1,187 @@
"""列表/导出用 UTC 时间窗Gate / Binance 主站共用)。"""
from datetime import datetime, timedelta, timezone
PRESET_UTC_TODAY = "utc_today"
PRESET_UTC_LAST24H = "utc_last24h"
PRESET_UTC_LAST7D = "utc_last7d"
PRESET_UTC_THIS_MONTH = "utc_this_month"
PRESET_UTC_LAST3M = "utc_last3m"
PRESET_UTC_LAST6M = "utc_last6m"
PRESET_ALL = "all"
PRESET_CUSTOM = "custom"
PRESET_DEFAULT = PRESET_UTC_THIS_MONTH
def utc_now():
return datetime.now(timezone.utc)
def utc_today_bounds(now=None):
now = now or utc_now()
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
return start, now
def resolve_window(query_mapping, default_preset=PRESET_DEFAULT):
"""
?win_preset= & from_utc= & to_utc= 解析窗口
返回 dict: preset, start_utc, end_utc, label, start_ms, end_ms
"""
preset = (query_mapping.get("win_preset") or default_preset or PRESET_DEFAULT).strip().lower()
now = utc_now()
if preset == PRESET_UTC_LAST24H:
start = now - timedelta(hours=24)
end = now
label = "近24小时(UTC)"
elif preset == PRESET_UTC_LAST7D:
start = now - timedelta(days=7)
end = now
label = "近7天(UTC)"
elif preset == PRESET_UTC_THIS_MONTH:
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = now
label = f"本月 {start.strftime('%Y-%m')}"
elif preset == PRESET_UTC_LAST3M:
start = now - timedelta(days=90)
end = now
label = "近3月"
elif preset == PRESET_UTC_LAST6M:
start = now - timedelta(days=180)
end = now
label = "近6月"
elif preset == PRESET_ALL:
start = datetime(2000, 1, 1, tzinfo=timezone.utc)
end = now
label = "全部"
elif preset == PRESET_CUSTOM:
start = _parse_utc_input(query_mapping.get("from_utc")) or utc_today_bounds(now)[0]
end = _parse_utc_input(query_mapping.get("to_utc")) or now
if end < start:
start, end = end, start
label = f"{start.strftime('%Y-%m-%d %H:%M')} ~ {end.strftime('%Y-%m-%d %H:%M')} UTC"
elif preset == PRESET_UTC_TODAY:
start, end = utc_today_bounds(now)
label = f"UTC当日 {start.strftime('%Y-%m-%d')}"
else:
return resolve_window(
{**(query_mapping or {}), "win_preset": default_preset},
default_preset=default_preset,
)
return {
"preset": preset,
"start_utc": start,
"end_utc": end,
"label": label,
"start_ms": int(start.timestamp() * 1000),
"end_ms": int(end.timestamp() * 1000),
}
def _parse_utc_input(raw):
s = (raw or "").strip().replace("T", " ").replace("Z", "").strip()
if not s:
return None
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
try:
dt = datetime.strptime(s[:n], fmt)
return dt.replace(tzinfo=timezone.utc)
except Exception:
continue
return None
def utc_window_to_bj_sql_strings(start_utc, end_utc, app_tz):
"""DB 存北京时间字符串时用于 SQLite 字符串范围比较"""
start_bj = start_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
end_bj = end_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
return start_bj, end_bj
def utc_window_to_utc_sql_strings(start_utc, end_utc):
"""SQLite CURRENT_TIMESTAMP 写入 UTC 时用于 created_at 范围比较"""
return (
start_utc.strftime("%Y-%m-%d %H:%M:%S"),
end_utc.strftime("%Y-%m-%d %H:%M:%S"),
)
def normalize_bj_datetime_storage(raw):
"""表单 datetime-local含 T入库前统一为 YYYY-MM-DD HH:MM:SS北京时间)。"""
s = (raw or "").strip().replace("T", " ").replace("Z", "").strip()
if not s:
return ""
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
try:
return datetime.strptime(s[:n], fmt).strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
continue
return s
def sql_list_time_field(*columns):
"""
SQLite 列表时间窗比较表达式
journal_entries open/close 可能含 'T'直接与 bounds空格格式比会误判为超出上界
单列时不用 COALESCESQLite 要求 COALESCE 至少 2 个参数
"""
cols = [c for c in columns if c]
if not cols:
raise ValueError("sql_list_time_field requires at least one column")
if len(cols) == 1:
return f"REPLACE({cols[0]}, 'T', ' ')"
return f"REPLACE(COALESCE({', '.join(cols)}), 'T', ' ')"
SESSION_KEY_LIST_WIN = "list_win_filter"
def query_mapping_from_session(session_store):
"""从 Flask session 恢复 win_preset / from_utc / to_utc"""
if not session_store:
return {}
block = session_store.get(SESSION_KEY_LIST_WIN)
if not isinstance(block, dict):
return {}
preset = (block.get("preset") or "").strip()
if not preset:
return {}
return {
"win_preset": preset,
"from_utc": (block.get("from_utc") or "").strip(),
"to_utc": (block.get("to_utc") or "").strip(),
}
def resolve_list_window(query_mapping, session_store=None, default_preset=PRESET_DEFAULT):
"""
URL win_preset 时解析并写入 session无参数时用 session 中上次应用的预设
"""
qm = query_mapping or {}
preset_in_q = (qm.get("win_preset") or "").strip()
if preset_in_q:
win = resolve_window(qm, default_preset=default_preset)
if session_store is not None:
session_store[SESSION_KEY_LIST_WIN] = {
"preset": win["preset"],
"from_utc": (qm.get("from_utc") or "").strip(),
"to_utc": (qm.get("to_utc") or "").strip(),
}
return win
stored = query_mapping_from_session(session_store)
if stored.get("win_preset"):
return resolve_window(stored, default_preset=default_preset)
return resolve_window(qm, default_preset=default_preset)
def list_window_redirect_query(session_store):
"""复盘/表单 POST 后重定向时附带列表筛选 query"""
from urllib.parse import urlencode
stored = query_mapping_from_session(session_store)
if not stored.get("win_preset"):
return ""
params = {k: v for k, v in stored.items() if v}
return urlencode(params)
"""列表/导出用 UTC 时间窗(Gate / Binance 主站共用)."""
from datetime import datetime, timedelta, timezone
PRESET_UTC_TODAY = "utc_today"
PRESET_UTC_LAST24H = "utc_last24h"
PRESET_UTC_LAST7D = "utc_last7d"
PRESET_UTC_THIS_MONTH = "utc_this_month"
PRESET_UTC_LAST3M = "utc_last3m"
PRESET_UTC_LAST6M = "utc_last6m"
PRESET_ALL = "all"
PRESET_CUSTOM = "custom"
PRESET_DEFAULT = PRESET_UTC_THIS_MONTH
def utc_now():
return datetime.now(timezone.utc)
def utc_today_bounds(now=None):
now = now or utc_now()
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
return start, now
def resolve_window(query_mapping, default_preset=PRESET_DEFAULT):
"""
?win_preset= & from_utc= & to_utc= 解析窗口.
返回 dict: preset, start_utc, end_utc, label, start_ms, end_ms
"""
preset = (query_mapping.get("win_preset") or default_preset or PRESET_DEFAULT).strip().lower()
now = utc_now()
if preset == PRESET_UTC_LAST24H:
start = now - timedelta(hours=24)
end = now
label = "近24小时(UTC)"
elif preset == PRESET_UTC_LAST7D:
start = now - timedelta(days=7)
end = now
label = "近7天(UTC)"
elif preset == PRESET_UTC_THIS_MONTH:
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = now
label = f"本月 {start.strftime('%Y-%m')}"
elif preset == PRESET_UTC_LAST3M:
start = now - timedelta(days=90)
end = now
label = "近3月"
elif preset == PRESET_UTC_LAST6M:
start = now - timedelta(days=180)
end = now
label = "近6月"
elif preset == PRESET_ALL:
start = datetime(2000, 1, 1, tzinfo=timezone.utc)
end = now
label = "全部"
elif preset == PRESET_CUSTOM:
start = _parse_utc_input(query_mapping.get("from_utc")) or utc_today_bounds(now)[0]
end = _parse_utc_input(query_mapping.get("to_utc")) or now
if end < start:
start, end = end, start
label = f"{start.strftime('%Y-%m-%d %H:%M')} ~ {end.strftime('%Y-%m-%d %H:%M')} UTC"
elif preset == PRESET_UTC_TODAY:
start, end = utc_today_bounds(now)
label = f"UTC当日 {start.strftime('%Y-%m-%d')}"
else:
return resolve_window(
{**(query_mapping or {}), "win_preset": default_preset},
default_preset=default_preset,
)
return {
"preset": preset,
"start_utc": start,
"end_utc": end,
"label": label,
"start_ms": int(start.timestamp() * 1000),
"end_ms": int(end.timestamp() * 1000),
}
def _parse_utc_input(raw):
s = (raw or "").strip().replace("T", " ").replace("Z", "").strip()
if not s:
return None
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
try:
dt = datetime.strptime(s[:n], fmt)
return dt.replace(tzinfo=timezone.utc)
except Exception:
continue
return None
def utc_window_to_bj_sql_strings(start_utc, end_utc, app_tz):
"""DB 存北京时间字符串时,用于 SQLite 字符串范围比较."""
start_bj = start_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
end_bj = end_utc.astimezone(app_tz).strftime("%Y-%m-%d %H:%M:%S")
return start_bj, end_bj
def utc_window_to_utc_sql_strings(start_utc, end_utc):
"""SQLite CURRENT_TIMESTAMP 写入 UTC 时,用于 created_at 范围比较."""
return (
start_utc.strftime("%Y-%m-%d %H:%M:%S"),
end_utc.strftime("%Y-%m-%d %H:%M:%S"),
)
def normalize_bj_datetime_storage(raw):
"""表单 datetime-local(含 T)入库前统一为 YYYY-MM-DD HH:MM:SS(北京时间)."""
s = (raw or "").strip().replace("T", " ").replace("Z", "").strip()
if not s:
return ""
for fmt, n in (("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%d %H:%M", 16), ("%Y-%m-%d", 10)):
try:
return datetime.strptime(s[:n], fmt).strftime("%Y-%m-%d %H:%M:%S")
except ValueError:
continue
return s
def sql_list_time_field(*columns):
"""
SQLite 列表时间窗比较表达式.
journal_entries open/close 可能含 'T',直接与 bounds(空格格式)比会误判为超出上界.
单列时不用 COALESCE(SQLite 要求 COALESCE 至少 2 个参数).
"""
cols = [c for c in columns if c]
if not cols:
raise ValueError("sql_list_time_field requires at least one column")
if len(cols) == 1:
return f"REPLACE({cols[0]}, 'T', ' ')"
return f"REPLACE(COALESCE({', '.join(cols)}), 'T', ' ')"
SESSION_KEY_LIST_WIN = "list_win_filter"
def query_mapping_from_session(session_store):
"""从 Flask session 恢复 win_preset / from_utc / to_utc."""
if not session_store:
return {}
block = session_store.get(SESSION_KEY_LIST_WIN)
if not isinstance(block, dict):
return {}
preset = (block.get("preset") or "").strip()
if not preset:
return {}
return {
"win_preset": preset,
"from_utc": (block.get("from_utc") or "").strip(),
"to_utc": (block.get("to_utc") or "").strip(),
}
def resolve_list_window(query_mapping, session_store=None, default_preset=PRESET_DEFAULT):
"""
URL win_preset 时解析并写入 session;无参数时用 session 中上次应用的预设.
"""
qm = query_mapping or {}
preset_in_q = (qm.get("win_preset") or "").strip()
if preset_in_q:
win = resolve_window(qm, default_preset=default_preset)
if session_store is not None:
session_store[SESSION_KEY_LIST_WIN] = {
"preset": win["preset"],
"from_utc": (qm.get("from_utc") or "").strip(),
"to_utc": (qm.get("to_utc") or "").strip(),
}
return win
stored = query_mapping_from_session(session_store)
if stored.get("win_preset"):
return resolve_window(stored, default_preset=default_preset)
return resolve_window(qm, default_preset=default_preset)
def list_window_redirect_query(session_store):
"""复盘/表单 POST 后重定向时附带列表筛选 query."""
from urllib.parse import urlencode
stored = query_mapping_from_session(session_store)
if not stored.get("win_preset"):
return ""
params = {k: v for k, v in stored.items() if v}
return urlencode(params)
+150 -150
View File
@@ -1,150 +1,150 @@
/* 账户风控状态徽章 — 三所实例 + 中控共用兼容 data-theme light/dark */
:root,
html[data-theme="dark"] {
--risk-normal-fg: #9cf0c4;
--risk-normal-bg: rgba(36, 140, 96, 0.16);
--risk-normal-border: rgba(72, 190, 130, 0.42);
--risk-normal-glow: rgba(72, 190, 130, 0.35);
--risk-1h-fg: #ffd27a;
--risk-1h-bg: rgba(210, 150, 40, 0.16);
--risk-1h-border: rgba(230, 170, 60, 0.45);
--risk-1h-glow: rgba(230, 170, 60, 0.32);
--risk-4h-fg: #ffab8a;
--risk-4h-bg: rgba(210, 90, 55, 0.16);
--risk-4h-border: rgba(230, 110, 70, 0.48);
--risk-4h-glow: rgba(230, 110, 70, 0.34);
--risk-daily-fg: #ff9ec4;
--risk-daily-bg: rgba(190, 55, 100, 0.18);
--risk-daily-border: rgba(210, 75, 120, 0.5);
--risk-daily-glow: rgba(210, 75, 120, 0.36);
--risk-position-fg: #8ec8ff;
--risk-position-bg: rgba(55, 120, 210, 0.18);
--risk-position-border: rgba(75, 145, 230, 0.48);
--risk-position-glow: rgba(75, 145, 230, 0.34);
--risk-badge-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
}
html[data-theme="light"] {
--risk-normal-fg: #056b44;
--risk-normal-bg: rgba(10, 143, 92, 0.14);
--risk-normal-border: rgba(8, 122, 80, 0.38);
--risk-normal-glow: rgba(10, 143, 92, 0.22);
--risk-1h-fg: #8a5a00;
--risk-1h-bg: rgba(200, 140, 20, 0.14);
--risk-1h-border: rgba(170, 115, 10, 0.38);
--risk-1h-glow: rgba(200, 140, 20, 0.2);
--risk-4h-fg: #a83812;
--risk-4h-bg: rgba(210, 85, 35, 0.12);
--risk-4h-border: rgba(180, 65, 25, 0.36);
--risk-4h-glow: rgba(210, 85, 35, 0.2);
--risk-daily-fg: #9a1248;
--risk-daily-bg: rgba(180, 35, 80, 0.1);
--risk-daily-border: rgba(155, 28, 68, 0.34);
--risk-daily-glow: rgba(180, 35, 80, 0.18);
--risk-position-fg: #0b5cab;
--risk-position-bg: rgba(20, 100, 190, 0.12);
--risk-position-border: rgba(15, 85, 165, 0.36);
--risk-position-glow: rgba(20, 100, 190, 0.2);
--risk-badge-shadow: 0 1px 2px rgba(20, 50, 80, 0.1);
}
.risk-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.03em;
line-height: 1.15;
padding: 5px 12px 5px 10px;
border-radius: 999px;
border: 1px solid var(--risk-border, transparent);
background: var(--risk-bg, transparent);
color: var(--risk-fg, inherit);
box-shadow: var(--risk-badge-shadow);
white-space: nowrap;
vertical-align: middle;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
/* 中控 iframe 内切页避免徽章过渡动画造成 header 闪动 */
html[data-hub-linked="1"] .header-row .risk-status-badge {
transition: none;
}
.risk-status-badge::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent),
0 0 8px var(--risk-glow, currentColor);
opacity: 0.92;
}
.risk-status-normal {
--risk-fg: var(--risk-normal-fg);
--risk-bg: var(--risk-normal-bg);
--risk-border: var(--risk-normal-border);
--risk-glow: var(--risk-normal-glow);
}
.risk-status-freeze_1h {
--risk-fg: var(--risk-1h-fg);
--risk-bg: var(--risk-1h-bg);
--risk-border: var(--risk-1h-border);
--risk-glow: var(--risk-1h-glow);
}
.risk-status-freeze_4h {
--risk-fg: var(--risk-4h-fg);
--risk-bg: var(--risk-4h-bg);
--risk-border: var(--risk-4h-border);
--risk-glow: var(--risk-4h-glow);
}
.risk-status-freeze_daily {
--risk-fg: var(--risk-daily-fg);
--risk-bg: var(--risk-daily-bg);
--risk-border: var(--risk-daily-border);
--risk-glow: var(--risk-daily-glow);
}
.risk-status-freeze_position {
--risk-fg: var(--risk-position-fg);
--risk-bg: var(--risk-position-bg);
--risk-border: var(--risk-position-border);
--risk-glow: var(--risk-position-glow);
}
/* 实例页与交易所标签并排 */
.header-row .risk-status-badge {
min-height: 28px;
}
/* 中控卡片标题内 */
.card-title .risk-status-badge,
.hub-tile-name .risk-status-badge {
font-size: 0.7rem;
padding: 3px 10px 3px 8px;
vertical-align: middle;
}
.card-title .risk-status-badge::before,
.hub-tile-name .risk-status-badge::before {
width: 6px;
height: 6px;
}
/* 账户风控状态徽章 — 三所实例 + 中控共用;兼容 data-theme light/dark */
:root,
html[data-theme="dark"] {
--risk-normal-fg: #9cf0c4;
--risk-normal-bg: rgba(36, 140, 96, 0.16);
--risk-normal-border: rgba(72, 190, 130, 0.42);
--risk-normal-glow: rgba(72, 190, 130, 0.35);
--risk-1h-fg: #ffd27a;
--risk-1h-bg: rgba(210, 150, 40, 0.16);
--risk-1h-border: rgba(230, 170, 60, 0.45);
--risk-1h-glow: rgba(230, 170, 60, 0.32);
--risk-4h-fg: #ffab8a;
--risk-4h-bg: rgba(210, 90, 55, 0.16);
--risk-4h-border: rgba(230, 110, 70, 0.48);
--risk-4h-glow: rgba(230, 110, 70, 0.34);
--risk-daily-fg: #ff9ec4;
--risk-daily-bg: rgba(190, 55, 100, 0.18);
--risk-daily-border: rgba(210, 75, 120, 0.5);
--risk-daily-glow: rgba(210, 75, 120, 0.36);
--risk-position-fg: #8ec8ff;
--risk-position-bg: rgba(55, 120, 210, 0.18);
--risk-position-border: rgba(75, 145, 230, 0.48);
--risk-position-glow: rgba(75, 145, 230, 0.34);
--risk-badge-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
}
html[data-theme="light"] {
--risk-normal-fg: #056b44;
--risk-normal-bg: rgba(10, 143, 92, 0.14);
--risk-normal-border: rgba(8, 122, 80, 0.38);
--risk-normal-glow: rgba(10, 143, 92, 0.22);
--risk-1h-fg: #8a5a00;
--risk-1h-bg: rgba(200, 140, 20, 0.14);
--risk-1h-border: rgba(170, 115, 10, 0.38);
--risk-1h-glow: rgba(200, 140, 20, 0.2);
--risk-4h-fg: #a83812;
--risk-4h-bg: rgba(210, 85, 35, 0.12);
--risk-4h-border: rgba(180, 65, 25, 0.36);
--risk-4h-glow: rgba(210, 85, 35, 0.2);
--risk-daily-fg: #9a1248;
--risk-daily-bg: rgba(180, 35, 80, 0.1);
--risk-daily-border: rgba(155, 28, 68, 0.34);
--risk-daily-glow: rgba(180, 35, 80, 0.18);
--risk-position-fg: #0b5cab;
--risk-position-bg: rgba(20, 100, 190, 0.12);
--risk-position-border: rgba(15, 85, 165, 0.36);
--risk-position-glow: rgba(20, 100, 190, 0.2);
--risk-badge-shadow: 0 1px 2px rgba(20, 50, 80, 0.1);
}
.risk-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.76rem;
font-weight: 600;
letter-spacing: 0.03em;
line-height: 1.15;
padding: 5px 12px 5px 10px;
border-radius: 999px;
border: 1px solid var(--risk-border, transparent);
background: var(--risk-bg, transparent);
color: var(--risk-fg, inherit);
box-shadow: var(--risk-badge-shadow);
white-space: nowrap;
vertical-align: middle;
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}
/* 中控 iframe 内切页:避免徽章过渡动画造成 header 闪动 */
html[data-hub-linked="1"] .header-row .risk-status-badge {
transition: none;
}
.risk-status-badge::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 30%, transparent),
0 0 8px var(--risk-glow, currentColor);
opacity: 0.92;
}
.risk-status-normal {
--risk-fg: var(--risk-normal-fg);
--risk-bg: var(--risk-normal-bg);
--risk-border: var(--risk-normal-border);
--risk-glow: var(--risk-normal-glow);
}
.risk-status-freeze_1h {
--risk-fg: var(--risk-1h-fg);
--risk-bg: var(--risk-1h-bg);
--risk-border: var(--risk-1h-border);
--risk-glow: var(--risk-1h-glow);
}
.risk-status-freeze_4h {
--risk-fg: var(--risk-4h-fg);
--risk-bg: var(--risk-4h-bg);
--risk-border: var(--risk-4h-border);
--risk-glow: var(--risk-4h-glow);
}
.risk-status-freeze_daily {
--risk-fg: var(--risk-daily-fg);
--risk-bg: var(--risk-daily-bg);
--risk-border: var(--risk-daily-border);
--risk-glow: var(--risk-daily-glow);
}
.risk-status-freeze_position {
--risk-fg: var(--risk-position-fg);
--risk-bg: var(--risk-position-bg);
--risk-border: var(--risk-position-border);
--risk-glow: var(--risk-position-glow);
}
/* 实例页:与交易所标签并排 */
.header-row .risk-status-badge {
min-height: 28px;
}
/* 中控卡片标题内 */
.card-title .risk-status-badge,
.hub-tile-name .risk-status-badge {
font-size: 0.7rem;
padding: 3px 10px 3px 8px;
vertical-align: middle;
}
.card-title .risk-status-badge::before,
.hub-tile-name .risk-status-badge::before {
width: 6px;
height: 6px;
}
+120 -120
View File
@@ -1,120 +1,120 @@
/**
* 账户风控徽章倒计时 三所实例 + 中控共用
*/
(function (global) {
"use strict";
function formatRemaining(totalSec) {
const sec = Math.max(0, Math.floor(Number(totalSec) || 0));
if (sec <= 0) return "";
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
if (m > 0) return `${m}m ${String(s).padStart(2, "0")}s`;
return `${s}s`;
}
function baseLabel(riskStatus, el) {
if (riskStatus && riskStatus.status_label) return String(riskStatus.status_label);
if (el && el.dataset && el.dataset.statusLabel) return String(el.dataset.statusLabel);
return "正常";
}
function resolveFreezeUntilMs(riskStatus) {
if (!riskStatus) return null;
const sec = Number(riskStatus.freeze_remaining_sec);
if (Number.isFinite(sec) && sec > 0) {
return Date.now() + sec * 1000;
}
const until = Number(riskStatus.freeze_until_ms);
return Number.isFinite(until) && until > 0 ? until : null;
}
function badgeText(riskStatus) {
const label = baseLabel(riskStatus, null);
const until = resolveFreezeUntilMs(riskStatus);
if (!until || until <= Date.now()) return label;
const cd = formatRemaining((until - Date.now()) / 1000);
return cd ? `${label} · ${cd}` : label;
}
function setNormalBadge(el) {
el.className = "risk-status-badge risk-status-normal";
el.dataset.statusLabel = "正常";
el.textContent = "正常";
el.title = "";
if (el.dataset) delete el.dataset.freezeUntilMs;
}
function refreshElement(el) {
if (!el) return;
const label = baseLabel(null, el);
const until = Number(el.dataset && el.dataset.freezeUntilMs);
if (!Number.isFinite(until) || until <= Date.now()) {
if (el.dataset && el.dataset.freezeUntilMs) {
setNormalBadge(el);
} else {
el.textContent = label;
}
return;
}
const cd = formatRemaining((until - Date.now()) / 1000);
el.textContent = cd ? `${label} · ${cd}` : label;
}
function applyToElement(el, riskStatus) {
if (!el || !riskStatus) return;
const st = riskStatus.status || "normal";
el.className = "risk-status-badge risk-status-" + st;
el.dataset.statusLabel = baseLabel(riskStatus, el);
const until = resolveFreezeUntilMs(riskStatus);
if (until) {
el.dataset.freezeUntilMs = String(until);
} else if (el.dataset) {
delete el.dataset.freezeUntilMs;
}
el.textContent = badgeText(riskStatus);
el.title = riskStatus.reason || "";
}
function formatBadgeHtml(riskStatus, esc) {
if (!riskStatus || typeof riskStatus !== "object") return "";
const safe = typeof esc === "function" ? esc : (s) => String(s);
const st = riskStatus.status || "normal";
const label = safe(riskStatus.status_label || "正常");
const title = safe(riskStatus.reason || "");
const text = safe(badgeText(riskStatus));
const until = resolveFreezeUntilMs(riskStatus);
const untilAttr =
until != null
? ` data-freeze-until-ms="${safe(String(Math.floor(until)))}"`
: "";
return (
`<span class="risk-status-badge risk-status-${safe(st)}" role="status"` +
` title="${title}" data-status-label="${label}"${untilAttr}>${text}</span>`
);
}
function tickAll(root) {
const scope = root || document;
scope.querySelectorAll(".risk-status-badge[data-freeze-until-ms]").forEach(refreshElement);
}
let timer = null;
function startTicker() {
if (timer) return;
tickAll();
timer = setInterval(() => tickAll(), 1000);
}
global.AccountRiskBadge = {
formatRemaining,
badgeText,
refreshElement,
applyToElement,
formatBadgeHtml,
tickAll,
startTicker,
};
})(typeof window !== "undefined" ? window : globalThis);
/**
* 账户风控徽章倒计时 三所实例 + 中控共用.
*/
(function (global) {
"use strict";
function formatRemaining(totalSec) {
const sec = Math.max(0, Math.floor(Number(totalSec) || 0));
if (sec <= 0) return "";
const h = Math.floor(sec / 3600);
const m = Math.floor((sec % 3600) / 60);
const s = sec % 60;
if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m`;
if (m > 0) return `${m}m ${String(s).padStart(2, "0")}s`;
return `${s}s`;
}
function baseLabel(riskStatus, el) {
if (riskStatus && riskStatus.status_label) return String(riskStatus.status_label);
if (el && el.dataset && el.dataset.statusLabel) return String(el.dataset.statusLabel);
return "正常";
}
function resolveFreezeUntilMs(riskStatus) {
if (!riskStatus) return null;
const sec = Number(riskStatus.freeze_remaining_sec);
if (Number.isFinite(sec) && sec > 0) {
return Date.now() + sec * 1000;
}
const until = Number(riskStatus.freeze_until_ms);
return Number.isFinite(until) && until > 0 ? until : null;
}
function badgeText(riskStatus) {
const label = baseLabel(riskStatus, null);
const until = resolveFreezeUntilMs(riskStatus);
if (!until || until <= Date.now()) return label;
const cd = formatRemaining((until - Date.now()) / 1000);
return cd ? `${label} · ${cd}` : label;
}
function setNormalBadge(el) {
el.className = "risk-status-badge risk-status-normal";
el.dataset.statusLabel = "正常";
el.textContent = "正常";
el.title = "";
if (el.dataset) delete el.dataset.freezeUntilMs;
}
function refreshElement(el) {
if (!el) return;
const label = baseLabel(null, el);
const until = Number(el.dataset && el.dataset.freezeUntilMs);
if (!Number.isFinite(until) || until <= Date.now()) {
if (el.dataset && el.dataset.freezeUntilMs) {
setNormalBadge(el);
} else {
el.textContent = label;
}
return;
}
const cd = formatRemaining((until - Date.now()) / 1000);
el.textContent = cd ? `${label} · ${cd}` : label;
}
function applyToElement(el, riskStatus) {
if (!el || !riskStatus) return;
const st = riskStatus.status || "normal";
el.className = "risk-status-badge risk-status-" + st;
el.dataset.statusLabel = baseLabel(riskStatus, el);
const until = resolveFreezeUntilMs(riskStatus);
if (until) {
el.dataset.freezeUntilMs = String(until);
} else if (el.dataset) {
delete el.dataset.freezeUntilMs;
}
el.textContent = badgeText(riskStatus);
el.title = riskStatus.reason || "";
}
function formatBadgeHtml(riskStatus, esc) {
if (!riskStatus || typeof riskStatus !== "object") return "";
const safe = typeof esc === "function" ? esc : (s) => String(s);
const st = riskStatus.status || "normal";
const label = safe(riskStatus.status_label || "正常");
const title = safe(riskStatus.reason || "");
const text = safe(badgeText(riskStatus));
const until = resolveFreezeUntilMs(riskStatus);
const untilAttr =
until != null
? ` data-freeze-until-ms="${safe(String(Math.floor(until)))}"`
: "";
return (
`<span class="risk-status-badge risk-status-${safe(st)}" role="status"` +
` title="${title}" data-status-label="${label}"${untilAttr}>${text}</span>`
);
}
function tickAll(root) {
const scope = root || document;
scope.querySelectorAll(".risk-status-badge[data-freeze-until-ms]").forEach(refreshElement);
}
let timer = null;
function startTicker() {
if (timer) return;
tickAll();
timer = setInterval(() => tickAll(), 1000);
}
global.AccountRiskBadge = {
formatRemaining,
badgeText,
refreshElement,
applyToElement,
formatBadgeHtml,
tickAll,
startTicker,
};
})(typeof window !== "undefined" ? window : globalThis);
+6 -6
View File
@@ -1,5 +1,5 @@
/**
* AI 日复盘 / 周复盘Markdown 子集渲染 + 五节大标题图标兜底
* AI 日复盘 / 周复盘:Markdown 子集渲染 + 五节大标题图标兜底
*/
(function (global) {
"use strict";
@@ -40,8 +40,8 @@
if (/^【系统说明/m.test(out) && !/^️/m.test(out)) {
out = out.replace(/^【系统说明/gm, "️ 【系统说明");
}
if (/^原始记录/m.test(out) && !/^📎/m.test(out)) {
out = out.replace(/^原始记录/gm, "📎 **原始记录**");
if (/^原始记录:/m.test(out) && !/^📎/m.test(out)) {
out = out.replace(/^原始记录:/gm, "📎 **原始记录**");
}
return out;
}
@@ -53,7 +53,7 @@
return false;
}
/** 编号列表项之间的空行不拆段避免每条都从 1 重新开始 */
/** 编号列表项之间的空行不拆段,避免每条都从 1 重新开始 */
function preprocessListBlanks(text) {
var lines = String(text || "").replace(/\r\n/g, "\n").split("\n");
var out = [];
@@ -141,7 +141,7 @@
return;
}
closeLists();
if (/^📎\s*\*\*原始记录\*\*/.test(trimmed) || /^原始记录/.test(trimmed)) {
if (/^📎\s*\*\*原始记录\*\*/.test(trimmed) || /^原始记录:/.test(trimmed)) {
html.push('<div class="md-raw-block-title">' + parseInline(trimmed) + "</div>");
return;
}
@@ -164,7 +164,7 @@
el.classList.remove("ai-result-md");
el.classList.add("is-loading");
el.innerHTML = "";
el.innerText = opts.message || "生成复盘中请稍候…";
el.innerText = opts.message || "生成复盘中,请稍候…";
}
if (btn) {
btn.disabled = true;
+1 -1
View File
@@ -1,4 +1,4 @@
/* 实盘/关键位放大页与 instance_theme 联动高对比 meta + 主题感知图表区 */
/* 实盘/关键位放大页:与 instance_theme 联动,高对比 meta + 主题感知图表区 */
body.focus-page {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
padding: 14px;
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* 实盘/关键位放大 K 线交易所 tick 精度主题感知图表高对比 meta
* 实盘/关键位放大 K 线:交易所 tick 精度,主题感知图表,高对比 meta.
*/
(function (global) {
"use strict";
+3 -3
View File
@@ -1,5 +1,5 @@
/**
* 表单提交防重复网络慢时禁用按钮并显示提交中
* 表单提交防重复:网络慢时禁用按钮并显示提交中.
*/
(function (global) {
"use strict";
@@ -49,7 +49,7 @@
return !!(form && form.dataset.submitGuard === "locked");
}
/** 已锁定时仅更新按钮文案校验通过 → 真正提交前 */
/** 已锁定时仅更新按钮文案(校验通过 → 真正提交前) */
function setSubmitLabel(form, label) {
if (!form || !label) return;
submitButtons(form).forEach(function (btn) {
@@ -58,7 +58,7 @@
});
}
/** 已通过前端校验发起最终 POST页面将跳转 */
/** 已通过前端校验,发起最终 POST(页面将跳转) */
function nativeSubmitOnce(form, label) {
if (!form) return;
var text = label || "提交中…";
+4 -4
View File
@@ -1,6 +1,6 @@
/**
* 中控 iframe 顶栏/统计常驻tab 内容走 /api/embed/page/<tab>
* tab 面板常驻 DOM切换时 show/hide脚本延后到首次激活回访零请求
* 中控 iframe :顶栏/统计常驻,tab 内容走 /api/embed/page/<tab>.
* tab 面板常驻 DOM,切换时 show/hide;脚本延后到首次激活,回访零请求.
*/
(function (global) {
const TAB_PATH = {
@@ -22,7 +22,7 @@
const tabPanes = new Map();
const tabBooted = new Set();
/** 自带校验后 form.submit() 的表单勿在捕获阶段再 fetch 一份会双发 POST */
/** 自带校验后 form.submit() 的表单,勿在捕获阶段再 fetch 一份(会双发 POST) */
const CUSTOM_SUBMIT_FORM_IDS = new Set(["add-order-form", "key-form", "roll-form"]);
function isEmbedShell() {
@@ -234,7 +234,7 @@
});
const ct = (r.headers.get("content-type") || "").toLowerCase();
if (!ct.includes("application/json")) {
throw new Error("加载失败HTTP " + r.status + "");
throw new Error("加载失败(HTTP " + r.status + ")");
}
const j = await r.json();
if (!j.ok || !j.html) throw new Error(j.msg || "加载失败");
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* embed SSE 收到后台 tick 后拉 JSON 快照更新 DOM切换 tab 不再重复请求 HTML
* embed :SSE 收到后台 tick 后拉 JSON 快照更新 DOM,切换 tab 不再重复请求 HTML.
*/
(function (global) {
let liveEventSource = null;
+240 -240
View File
@@ -1,240 +1,240 @@
.order-trade-style-hint{font-size:.78rem;color:#8fc8ff;margin-left:4px;white-space:nowrap}
.order-entry-model-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.order-entry-model-row select.order-entry-category{min-width:4.8em;max-width:6.5em}
.order-entry-model-row select.order-entry-model-sub{min-width:7em;max-width:10rem}
.order-leverage-hint{font-size:.78rem;color:#cfd3ef;white-space:nowrap;align-self:center}
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px 20px}
.container{width:100%;max-width:min(1440px,94vw);margin:0 auto;padding:0 clamp(8px,1.5vw,20px)}
.header{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:12px}
.header h1{font-size:1.75rem;color:#dbe4ff;text-align:center;line-height:1.25}
.exchange-tag{font-size:.82rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:5px 14px;border-radius:999px;letter-spacing:.06em}
.header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}
.top-nav{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:12px}
.top-nav a{padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a;color:#8fc8ff;text-decoration:none}
.top-nav a.active{background:#2a3f6c;color:#dbe4ff}
.stat-box{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-bottom:16px;align-items:stretch}
.stat-item{min-width:0;min-height:76px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:6px;background:#151a2a;padding:12px 10px;border-radius:10px;text-align:center;border:1px solid #2a3152}
.stat-item .label{font-size:.8rem;color:#aaa;line-height:1.25;max-width:100%}
.stat-item .value{font-size:1.25rem;font-weight:600;color:#fff;line-height:1.3;min-height:1.35em;display:flex;align-items:center;justify-content:center}
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150}
.full{grid-column:1/-1}
.card h2{font-size:1rem;margin-bottom:10px;color:#d4d9ff}
.form-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center}
.form-row > input:not([type=checkbox]):not([type=radio]),.form-row > select{flex:0 1 auto;width:10rem;max-width:200px;min-width:7rem}
#add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto}
.order-plan-preview{display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin:4px 0 10px;padding:10px 12px;background:#151a28;border:1px solid #2a3150;border-radius:8px;font-size:.85rem}
.order-preview-risk{color:#ff6b6b}
.order-preview-risk strong{color:#ff8f8f;font-weight:600}
.order-preview-profit{color:#4cd97f}
.order-preview-profit strong{color:#6ee7a0;font-weight:600}
.order-preview-rr{color:#cfd3ef}
.order-preview-rr strong{font-weight:600;color:#dbe4ff}
.order-preview-rr.order-preview-rr-low strong{color:#ff8f8f}
.order-preview-rr.order-preview-rr-ok strong{color:#8fc8ff}
.form-row > button,.form-row > label{flex:0 0 auto}
.form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
/* 复盘表单长下拉文案需可收缩否则会撑破四列网格 */
.journal-card .form-grid{gap:10px}
.journal-card .form-grid > input,
.journal-card .form-grid > select{
min-width:0;
width:100%;
max-width:100%;
box-sizing:border-box;
}
.journal-card #journal-form textarea[name="note"]{
display:block;width:100%;max-width:100%;box-sizing:border-box;margin-top:8px;
}
input,select,button,textarea{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.88rem;outline:none}
button{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer}
.list{display:flex;flex-direction:column;gap:8px;margin-top:8px;max-height:240px;overflow:auto}
.list-item{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:9px;background:#1a2034;border:1px solid #2a3150;border-radius:8px}
.btn-del{padding:5px 9px;background:#2f2134;color:#ff7b7b;border-radius:8px;text-decoration:none;font-size:.8rem}
.rule-tip{font-size:.8rem;color:#95a2c2;margin-bottom:8px}
table{width:100%;border-collapse:collapse}
th,td{padding:8px;text-align:left;border-bottom:1px solid #25253b;font-size:.85rem}
th{color:#a9a9ff}
.badge{padding:2px 6px;border-radius:6px;font-size:.72rem}
.profit{background:#1e332f;color:#4cd97f}
.loss{background:#331e24;color:#ff6666}
.miss{background:#29241e;color:#eac147}
.direction{background:#1e2533;color:#4cc2ff}
.direction-long{background:#1e332f;color:#4cd97f}
.direction-short{background:#331e24;color:#ff6666}
.pnl-profit{color:#4cd97f;font-weight:600}
.pnl-loss{color:#ff6666;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
form.is-form-submitting{opacity:.88;pointer-events:none}
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
.panel-list{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.panel-item{background:#141423;border:1px solid #24243b;border-radius:10px;padding:10px;max-height:260px;overflow:auto}
.entry{border-bottom:1px solid #2b2b43;padding:8px 0}
.entry:last-child{border-bottom:none}
.table-del{padding:4px 8px;background:#2f2134;color:#ff7b7b;border:none;border-radius:6px;cursor:pointer;font-size:.78rem}
.mood-grid{display:flex;gap:10px;flex-wrap:wrap;font-size:.82rem;color:#d7d7ea}
.mood-grid label{display:flex;align-items:center;gap:3px}
.screenshot{width:100px;border-radius:6px;cursor:pointer;margin-top:6px}
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1210}
.modal img{max-width:90%;max-height:90%;border-radius:8px}
.detail-modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1200;padding:20px}
.detail-modal .panel{width:min(92vw,980px);max-height:88vh;overflow:auto;background:#121726;border:1px solid #2a3150;border-radius:10px;padding:14px}
.detail-modal .panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}
.detail-modal .panel-title{font-size:1rem;color:#dbe4ff}
.detail-modal .panel-close{padding:6px 10px;background:#2f2134;color:#ffb2b2;border:none;border-radius:8px;cursor:pointer}
.detail-modal .panel-body{white-space:pre-wrap;line-height:1.5;font-size:.86rem;color:#e5e9ff}
.detail-modal .panel-image{margin-top:10px;max-width:min(100%,680px);border-radius:8px;cursor:pointer;border:1px solid #2a3150}
.detail-modal .panel-actions{display:flex;gap:8px;align-items:center;flex-shrink:0}
.detail-modal .panel-fs{padding:6px 10px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem}
.detail-modal.fullscreen{padding:10px}
.detail-modal.fullscreen .panel{width:100%;height:100%;max-width:none;max-height:none;display:flex;flex-direction:column;overflow:hidden}
.detail-modal.fullscreen .panel-body{flex:1;overflow:auto;min-height:0;font-size:.9rem}
.ai-result-wrap{margin-top:8px}
.ai-result-toolbar{display:flex;gap:8px;margin-top:6px}
.ai-result-toolbar .btn-fs{padding:4px 10px;font-size:.78rem;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:6px;cursor:pointer}
.table-wrap{overflow-x:auto}
.dual-panel-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.dual-panel-grid .card{height:100%;display:flex;flex-direction:column}
.panel-scroll{flex:1;min-height:280px;max-height:420px;overflow:auto}
.records-card{grid-column:1/-1}
.review-card{grid-column:1/-1}
.review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap}
.review-card-head h2{margin:0}
.review-card-fs-btn{padding:6px 12px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;white-space:nowrap}
.review-card-fs-btn:hover{filter:brightness(1.08)}
body.review-card-fullscreen-open{overflow:hidden}
.review-card.is-fullscreen{
position:fixed;inset:12px;z-index:1100;margin:0;
width:auto !important;max-width:none;height:auto;
overflow:auto;display:flex;flex-direction:column;
box-shadow:0 12px 48px rgba(0,0,0,.55);
}
.review-card.is-fullscreen .panel-list{flex:1;min-height:320px}
.review-card.is-fullscreen .panel-item{max-height:none;height:auto;min-height:280px}
.review-card.is-fullscreen .ai-result{max-height:min(36vh, 320px)}
@media (max-width: 1200px){
.stat-box{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
}
@media (min-width: 1440px){
.panel-scroll,.pos-list{max-height:420px}
.records-card .table-wrap{max-height:620px;overflow:auto}
}
@media (min-width: 2200px){
.container{max-width:min(1720px,90vw)}
}
@media (min-width: 2560px){
.container{max-width:min(1860px,88vw)}
.dual-panel-grid{gap:18px}
}
@media (min-width: 3000px){
.container{max-width:min(1980px,86vw)}
.pos-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
}
@media (max-width: 1100px){
.grid{grid-template-columns:1fr}
.dual-panel-grid{grid-template-columns:1fr}
.records-card,.review-card{grid-column:auto}
.panel-list{grid-template-columns:1fr}
}
@media (max-width: 960px){
body{padding:10px}
.form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.stat-box{grid-template-columns:repeat(2,minmax(0,1fr))}
}
.stats-detail{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-top:10px}
.stats-detail .stat-item{min-width:0;min-height:0;display:block;text-align:left;padding:10px 12px;align-items:stretch;gap:4px}
.stats-detail .stat-item .value{min-height:0;display:block;font-size:1.05rem}
.stats-detail .stat-item .label{font-size:.75rem}
.stats-detail .stat-item .value{font-size:1.05rem;word-break:break-all}
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
.export-bar a:hover{background:#1f2740}
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
.key-history .list{max-height:200px}
.pos-section{margin-top:12px}
.pos-section-title{font-size:.82rem;color:#8892b0;margin-bottom:8px;font-weight:500}
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
.dual-panel-grid .pos-list-live{max-height:none;overflow:visible;flex:1 1 auto}
.dual-panel-grid .panel-scroll.pos-list-live{max-height:none;overflow:visible}
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
.pos-meta-item{display:inline-flex;align-items:center}
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
.pos-meta-on{color:#6eb5ff}
.pos-meta-off{color:#7d8799}
.pos-breakeven-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:600;background:#1a3d2e;color:#4cd97f}
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
.pos-side-long{background:#253a6e;color:#6eb5ff}
.pos-side-short{background:#4a2230;color:#ff8a8a}
.pos-head-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
.pos-entrust-btn{padding:6px 12px;background:#2a4a7a;color:#8fc8ff;border:none;border-radius:8px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap}
.pos-entrust-btn:hover{background:#355d96}
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap;border:none;cursor:pointer;display:inline-block}
.pos-close-btn:hover{background:#d66565;color:#fff}
.pos-ex-orders{margin-top:10px;padding-top:10px;border-top:1px dashed #2a3348}
.pos-ex-orders-title{font-size:.74rem;color:#7d8799;margin-bottom:6px}
.pos-ex-order-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.78rem;color:#c5cce0;margin-top:5px}
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
.tpsl-modal-backdrop.open{display:flex}
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
.tpsl-modal h3{margin:0 0 12px;font-size:1rem;color:#fff}
.tpsl-modal .form-row{margin-bottom:10px}
.tpsl-modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.tpsl-modal-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
.tpsl-modal-submit{background:#2d6a4f;color:#fff}
.tpsl-modal-cancel{background:#3a3f52;color:#ddd}
.review-entry-reason-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9100;align-items:center;justify-content:center;padding:16px}
.review-entry-reason-backdrop.open{display:flex}
.review-entry-reason-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(480px,100%);max-height:90vh;overflow:auto}
.review-entry-reason-modal h3{margin:0 0 8px;font-size:1rem;color:#fff}
.review-entry-reason-hint{margin:0 0 12px;font-size:.82rem;color:#9aa3c7;line-height:1.45}
.review-entry-reason-select{width:100%;padding:8px 10px;border-radius:8px;border:1px solid #3a4a66;background:#121726;color:#e8ecff;font-size:.9rem}
.review-entry-reason-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.review-entry-reason-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
.review-entry-reason-ok{background:#2d6a4f;color:#fff}
.review-entry-reason-cancel{background:#3a3f52;color:#ddd}
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
.pos-label{font-size:.72rem;color:#7d8799}
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
.pos-val-dash{opacity:.75;color:#8b95a8}
.pos-value.price-up{color:#4cd97f}
.pos-value.price-down{color:#ff6666}
.pos-value.price-flat{color:#e8ecf4}
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
.stats-card{grid-column:1/-1;margin-top:14px}
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
.stats-card.collapsed .stats-content{display:none}
.stats-period-block{margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid #2a3150}
.stats-period-block:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
#embed-page-root{min-height:120px;position:relative}
.embed-tab-pane[hidden]{display:none!important}
.order-trade-style-hint{font-size:.78rem;color:#8fc8ff;margin-left:4px;white-space:nowrap}
.order-entry-model-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.order-entry-model-row select.order-entry-category{min-width:4.8em;max-width:6.5em}
.order-entry-model-row select.order-entry-model-sub{min-width:7em;max-width:10rem}
.order-leverage-hint{font-size:.78rem;color:#cfd3ef;white-space:nowrap;align-self:center}
body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:#0b0d14;color:#eaeaea;padding:14px 20px}
.container{width:100%;max-width:min(1440px,94vw);margin:0 auto;padding:0 clamp(8px,1.5vw,20px)}
.header{display:flex;flex-direction:column;align-items:center;gap:8px;margin-bottom:12px}
.header h1{font-size:1.75rem;color:#dbe4ff;text-align:center;line-height:1.25}
.exchange-tag{font-size:.82rem;font-weight:600;color:#b8f5d0;background:#14241e;border:1px solid #2d6a4f;padding:5px 14px;border-radius:999px;letter-spacing:.06em}
.header-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;justify-content:center}
.top-nav{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-bottom:12px}
.top-nav a{padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a;color:#8fc8ff;text-decoration:none}
.top-nav a.active{background:#2a3f6c;color:#dbe4ff}
.stat-box{display:grid;grid-template-columns:repeat(auto-fit,minmax(148px,1fr));gap:12px;margin-bottom:16px;align-items:stretch}
.stat-item{min-width:0;min-height:76px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:6px;background:#151a2a;padding:12px 10px;border-radius:10px;text-align:center;border:1px solid #2a3152}
.stat-item .label{font-size:.8rem;color:#aaa;line-height:1.25;max-width:100%}
.stat-item .value{font-size:1.25rem;font-weight:600;color:#fff;line-height:1.3;min-height:1.35em;display:flex;align-items:center;justify-content:center}
.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
.card{background:#121726;border-radius:10px;padding:12px;border:1px solid #2a3150}
.full{grid-column:1/-1}
.card h2{font-size:1rem;margin-bottom:10px;color:#d4d9ff}
.form-row{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center}
.form-row > input:not([type=checkbox]):not([type=radio]),.form-row > select{flex:0 1 auto;width:10rem;max-width:200px;min-width:7rem}
#add-order-form #sltp-mode{min-width:12.5rem;max-width:16rem;width:auto}
.order-plan-preview{display:flex;gap:18px;flex-wrap:wrap;align-items:center;margin:4px 0 10px;padding:10px 12px;background:#151a28;border:1px solid #2a3150;border-radius:8px;font-size:.85rem}
.order-preview-risk{color:#ff6b6b}
.order-preview-risk strong{color:#ff8f8f;font-weight:600}
.order-preview-profit{color:#4cd97f}
.order-preview-profit strong{color:#6ee7a0;font-weight:600}
.order-preview-rr{color:#cfd3ef}
.order-preview-rr strong{font-weight:600;color:#dbe4ff}
.order-preview-rr.order-preview-rr-low strong{color:#ff8f8f}
.order-preview-rr.order-preview-rr-ok strong{color:#8fc8ff}
.form-row > button,.form-row > label{flex:0 0 auto}
.form-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
/* 复盘表单:长下拉文案需可收缩,否则会撑破四列网格 */
.journal-card .form-grid{gap:10px}
.journal-card .form-grid > input,
.journal-card .form-grid > select{
min-width:0;
width:100%;
max-width:100%;
box-sizing:border-box;
}
.journal-card #journal-form textarea[name="note"]{
display:block;width:100%;max-width:100%;box-sizing:border-box;margin-top:8px;
}
input,select,button,textarea{padding:8px 10px;border-radius:8px;border:1px solid #2e2e45;background:#1a1a29;color:#fff;font-size:.88rem;outline:none}
button{background:linear-gradient(90deg,#4285f4,#7b42ff);border:none;cursor:pointer}
.list{display:flex;flex-direction:column;gap:8px;margin-top:8px;max-height:240px;overflow:auto}
.list-item{display:flex;justify-content:space-between;align-items:center;gap:8px;padding:9px;background:#1a2034;border:1px solid #2a3150;border-radius:8px}
.btn-del{padding:5px 9px;background:#2f2134;color:#ff7b7b;border-radius:8px;text-decoration:none;font-size:.8rem}
.rule-tip{font-size:.8rem;color:#95a2c2;margin-bottom:8px}
table{width:100%;border-collapse:collapse}
th,td{padding:8px;text-align:left;border-bottom:1px solid #25253b;font-size:.85rem}
th{color:#a9a9ff}
.badge{padding:2px 6px;border-radius:6px;font-size:.72rem}
.profit{background:#1e332f;color:#4cd97f}
.loss{background:#331e24;color:#ff6666}
.miss{background:#29241e;color:#eac147}
.direction{background:#1e2533;color:#4cc2ff}
.direction-long{background:#1e332f;color:#4cd97f}
.direction-short{background:#331e24;color:#ff6666}
.pnl-profit{color:#4cd97f;font-weight:600}
.pnl-loss{color:#ff6666;font-weight:600}
.flash{padding:10px;background:#1e2533;color:#4cc2ff;border-radius:10px;margin-bottom:12px;text-align:center;border:1px solid #304164}
form.is-form-submitting{opacity:.88;pointer-events:none}
form.is-form-submitting button[type=submit],form.is-form-submitting input[type=submit]{cursor:wait}
.ai-result{background:#1a1a29;border:1px solid #2e2e45;border-radius:8px;padding:10px;white-space:pre-wrap;max-height:220px;overflow:auto;font-size:.84rem;line-height:1.45;margin-top:8px}
.ai-result.ai-result-md,.detail-modal .panel-body.md-review{white-space:normal}
.ai-result-md p,.detail-modal .panel-body.md-review p{margin:6px 0;color:#dde2ff}
.ai-result-md ul,.ai-result-md ol,.detail-modal .panel-body.md-review ul,.detail-modal .panel-body.md-review ol{margin:6px 0 8px 1.25em;padding:0}
.ai-result-md li,.detail-modal .panel-body.md-review li{margin:5px 0;line-height:1.5}
.ai-result-md strong,.detail-modal .panel-body.md-review strong{color:#f0f3ff;font-weight:600}
.ai-result-md h2,.detail-modal .panel-body.md-review h2{font-size:1.02rem;color:#b8c8ff;margin:14px 0 8px;padding-bottom:4px;border-bottom:1px solid #2e2e45}
.ai-result-md h3,.detail-modal .panel-body.md-review h3{font-size:.92rem;color:#c9d4ff;margin:10px 0 6px}
.ai-result-md code,.detail-modal .panel-body.md-review code{background:#252538;padding:1px 4px;border-radius:4px;font-size:.82em}
.ai-result-md .md-raw-block-title,.detail-modal .panel-body.md-review .md-raw-block-title{margin-top:14px;padding-top:10px;border-top:1px dashed #3a3a55;color:#a8b0d8;font-weight:600}
.price-up{color:#4cd97f}
.price-down{color:#ff6666}
.price-flat{color:#cfd3ef}
.panel-list{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.panel-item{background:#141423;border:1px solid #24243b;border-radius:10px;padding:10px;max-height:260px;overflow:auto}
.entry{border-bottom:1px solid #2b2b43;padding:8px 0}
.entry:last-child{border-bottom:none}
.table-del{padding:4px 8px;background:#2f2134;color:#ff7b7b;border:none;border-radius:6px;cursor:pointer;font-size:.78rem}
.mood-grid{display:flex;gap:10px;flex-wrap:wrap;font-size:.82rem;color:#d7d7ea}
.mood-grid label{display:flex;align-items:center;gap:3px}
.screenshot{width:100px;border-radius:6px;cursor:pointer;margin-top:6px}
.modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1210}
.modal img{max-width:90%;max-height:90%;border-radius:8px}
.detail-modal{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.78);justify-content:center;align-items:center;z-index:1200;padding:20px}
.detail-modal .panel{width:min(92vw,980px);max-height:88vh;overflow:auto;background:#121726;border:1px solid #2a3150;border-radius:10px;padding:14px}
.detail-modal .panel-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px}
.detail-modal .panel-title{font-size:1rem;color:#dbe4ff}
.detail-modal .panel-close{padding:6px 10px;background:#2f2134;color:#ffb2b2;border:none;border-radius:8px;cursor:pointer}
.detail-modal .panel-body{white-space:pre-wrap;line-height:1.5;font-size:.86rem;color:#e5e9ff}
.detail-modal .panel-image{margin-top:10px;max-width:min(100%,680px);border-radius:8px;cursor:pointer;border:1px solid #2a3150}
.detail-modal .panel-actions{display:flex;gap:8px;align-items:center;flex-shrink:0}
.detail-modal .panel-fs{padding:6px 10px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem}
.detail-modal.fullscreen{padding:10px}
.detail-modal.fullscreen .panel{width:100%;height:100%;max-width:none;max-height:none;display:flex;flex-direction:column;overflow:hidden}
.detail-modal.fullscreen .panel-body{flex:1;overflow:auto;min-height:0;font-size:.9rem}
.ai-result-wrap{margin-top:8px}
.ai-result-toolbar{display:flex;gap:8px;margin-top:6px}
.ai-result-toolbar .btn-fs{padding:4px 10px;font-size:.78rem;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:6px;cursor:pointer}
.table-wrap{overflow-x:auto}
.dual-panel-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:stretch}
.dual-panel-grid .card{height:100%;display:flex;flex-direction:column}
.panel-scroll{flex:1;min-height:280px;max-height:420px;overflow:auto}
.records-card{grid-column:1/-1}
.review-card{grid-column:1/-1}
.review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap}
.review-card-head h2{margin:0}
.review-card-fs-btn{padding:6px 12px;background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;white-space:nowrap}
.review-card-fs-btn:hover{filter:brightness(1.08)}
body.review-card-fullscreen-open{overflow:hidden}
.review-card.is-fullscreen{
position:fixed;inset:12px;z-index:1100;margin:0;
width:auto !important;max-width:none;height:auto;
overflow:auto;display:flex;flex-direction:column;
box-shadow:0 12px 48px rgba(0,0,0,.55);
}
.review-card.is-fullscreen .panel-list{flex:1;min-height:320px}
.review-card.is-fullscreen .panel-item{max-height:none;height:auto;min-height:280px}
.review-card.is-fullscreen .ai-result{max-height:min(36vh, 320px)}
@media (max-width: 1200px){
.stat-box{grid-template-columns:repeat(auto-fill,minmax(140px,1fr))}
}
@media (min-width: 1440px){
.panel-scroll,.pos-list{max-height:420px}
.records-card .table-wrap{max-height:620px;overflow:auto}
}
@media (min-width: 2200px){
.container{max-width:min(1720px,90vw)}
}
@media (min-width: 2560px){
.container{max-width:min(1860px,88vw)}
.dual-panel-grid{gap:18px}
}
@media (min-width: 3000px){
.container{max-width:min(1980px,86vw)}
.pos-grid{grid-template-columns:repeat(4,minmax(0,1fr))}
}
@media (max-width: 1100px){
.grid{grid-template-columns:1fr}
.dual-panel-grid{grid-template-columns:1fr}
.records-card,.review-card{grid-column:auto}
.panel-list{grid-template-columns:1fr}
}
@media (max-width: 960px){
body{padding:10px}
.form-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.stat-box{grid-template-columns:repeat(2,minmax(0,1fr))}
}
.stats-detail{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:10px;margin-top:10px}
.stats-detail .stat-item{min-width:0;min-height:0;display:block;text-align:left;padding:10px 12px;align-items:stretch;gap:4px}
.stats-detail .stat-item .value{min-height:0;display:block;font-size:1.05rem}
.stats-detail .stat-item .label{font-size:.75rem}
.stats-detail .stat-item .value{font-size:1.05rem;word-break:break-all}
.export-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;font-size:.85rem}
.export-bar a{color:#8fc8ff;text-decoration:none;padding:6px 10px;border:1px solid #304164;border-radius:8px;background:#151a2a}
.export-bar a:hover{background:#1f2740}
.list-window-bar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px;padding:10px 12px;background:#151a2a;border:1px solid #304164;border-radius:10px;font-size:.82rem}
.list-window-bar label{color:#9aa;display:flex;align-items:center;gap:6px}
.stats-segment-block{margin-top:20px;padding-top:14px;border-top:1px solid #3a4468}
.stats-segment-block h2{font-size:1.05rem;color:#dbe4ff;margin-bottom:8px}
.key-history{margin-top:12px;padding-top:10px;border-top:1px solid #2a3150}
.key-history h3{font-size:.88rem;color:#b8c4ff;margin-bottom:6px}
.key-history .sub{font-size:.72rem;color:#8892b0;margin-bottom:6px}
.key-history .list{max-height:200px}
.pos-section{margin-top:12px}
.pos-section-title{font-size:.82rem;color:#8892b0;margin-bottom:8px;font-weight:500}
.pos-list{display:flex;flex-direction:column;gap:10px;max-height:280px;overflow:auto}
.dual-panel-grid .pos-list-live{max-height:none;overflow:visible;flex:1 1 auto}
.dual-panel-grid .panel-scroll.pos-list-live{max-height:none;overflow:visible}
.pos-card{background:#141923;border:1px solid #2a3348;border-radius:10px;padding:12px 14px}
.pos-card-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.pos-meta{font-size:.74rem;color:#8b95a8;line-height:1.45;margin-bottom:12px;display:flex;flex-wrap:wrap;align-items:center;gap:4px 0}
.pos-meta-item{display:inline-flex;align-items:center}
.pos-meta-item:not(:last-child)::after{content:'|';margin:0 8px;color:#3d4659}
.pos-meta-on{color:#6eb5ff}
.pos-meta-off{color:#7d8799}
.pos-breakeven-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:6px;font-size:.72rem;font-weight:600;background:#1a3d2e;color:#4cd97f}
.pos-card-symbol{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}
.pos-card-symbol strong{font-size:.95rem;color:#fff;font-weight:600}
.pos-side-badge{padding:3px 8px;border-radius:6px;font-size:.72rem;font-weight:500;line-height:1.2}
.pos-side-long{background:#253a6e;color:#6eb5ff}
.pos-side-short{background:#4a2230;color:#ff8a8a}
.pos-head-actions{display:flex;align-items:center;gap:6px;flex-shrink:0}
.pos-entrust-btn{padding:6px 12px;background:#2a4a7a;color:#8fc8ff;border:none;border-radius:8px;font-size:.82rem;font-weight:500;cursor:pointer;white-space:nowrap}
.pos-entrust-btn:hover{background:#355d96}
.pos-close-btn{padding:6px 14px;background:#c45454;color:#fff;border-radius:8px;text-decoration:none;font-size:.82rem;font-weight:500;flex-shrink:0;white-space:nowrap;border:none;cursor:pointer;display:inline-block}
.pos-close-btn:hover{background:#d66565;color:#fff}
.pos-ex-orders{margin-top:10px;padding-top:10px;border-top:1px dashed #2a3348}
.pos-ex-orders-title{font-size:.74rem;color:#7d8799;margin-bottom:6px}
.pos-ex-order-row{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:.78rem;color:#c5cce0;margin-top:5px}
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
.tpsl-modal-backdrop.open{display:flex}
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
.tpsl-modal h3{margin:0 0 12px;font-size:1rem;color:#fff}
.tpsl-modal .form-row{margin-bottom:10px}
.tpsl-modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.tpsl-modal-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
.tpsl-modal-submit{background:#2d6a4f;color:#fff}
.tpsl-modal-cancel{background:#3a3f52;color:#ddd}
.review-entry-reason-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9100;align-items:center;justify-content:center;padding:16px}
.review-entry-reason-backdrop.open{display:flex}
.review-entry-reason-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(480px,100%);max-height:90vh;overflow:auto}
.review-entry-reason-modal h3{margin:0 0 8px;font-size:1rem;color:#fff}
.review-entry-reason-hint{margin:0 0 12px;font-size:.82rem;color:#9aa3c7;line-height:1.45}
.review-entry-reason-select{width:100%;padding:8px 10px;border-radius:8px;border:1px solid #3a4a66;background:#121726;color:#e8ecff;font-size:.9rem}
.review-entry-reason-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}
.review-entry-reason-actions button{padding:8px 16px;border-radius:8px;border:none;cursor:pointer;font-size:.85rem}
.review-entry-reason-ok{background:#2d6a4f;color:#fff}
.review-entry-reason-cancel{background:#3a3f52;color:#ddd}
.pos-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px 14px;margin-bottom:12px}
.pos-cell{display:flex;flex-direction:column;gap:4px;min-width:0}
.pos-label{font-size:.72rem;color:#7d8799}
.pos-value{font-size:.88rem;color:#e8ecf4;font-weight:500;line-height:1.25}
.pos-val-dash{opacity:.75;color:#8b95a8}
.pos-value.price-up{color:#4cd97f}
.pos-value.price-down{color:#ff6666}
.pos-value.price-flat{color:#e8ecf4}
.pos-footer{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:.75rem;color:#6d7689}
.pos-empty{padding:18px;text-align:center;color:#8892b0;font-size:.85rem;background:#141923;border:1px dashed #2a3348;border-radius:10px}
@media (max-width:520px){.pos-grid{grid-template-columns:repeat(2,1fr)}}
.stats-card{grid-column:1/-1;margin-top:14px}
.stats-card .stats-toggle{background:#1f3a5a;color:#8fc8ff;border:none;border-radius:8px;padding:6px 10px;cursor:pointer}
.stats-card.collapsed .stats-content{display:none}
.stats-period-block{margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid #2a3150}
.stats-period-block:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}
.stats-period-block h3{font-size:1rem;color:#dbe4ff;margin-bottom:4px}
.stats-period-block .sub{font-size:.78rem;color:#8892b0;margin-bottom:10px;line-height:1.4}
#embed-page-root{min-height:120px;position:relative}
.embed-tab-pane[hidden]{display:none!important}
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* 手机端交易记录 / 复盘记录紧凑列表币种 · 方向 · 盈亏点击展开详情
* 手机端:交易记录 / 复盘记录紧凑列表(币种 · 方向 · 盈亏),点击展开详情.
*/
(function (global) {
"use strict";
+8 -8
View File
@@ -1,5 +1,5 @@
/**
* 实例导航显示env 配置改密PM2 重启
* 实例:导航显示,env 配置,改密,PM2 重启.
*/
(function (global) {
const DISPLAY = () => global.__INSTANCE_DISPLAY__ || {};
@@ -135,7 +135,7 @@
body: JSON.stringify({ display }),
});
applyDisplayToNav(data.display || display);
setStatus(status, "已保存导航已更新");
setStatus(status, "已保存,导航已更新");
} catch (e) {
setStatus(status, e.message || "保存失败", true);
}
@@ -194,7 +194,7 @@
cur.appendChild(masked);
row.appendChild(cur);
}
input.placeholder = field.has_value ? "修改时填写新值留空不修改" : "请输入";
input.placeholder = field.has_value ? "修改时填写新值,留空不修改" : "请输入";
} else {
input.type = "text";
input.value = field.current || field.default || "";
@@ -239,7 +239,7 @@
if (group.has_restart) {
const hint = document.createElement("p");
hint.className = "env-panel-hint";
hint.textContent = "本组含需重启项修改后请点「保存并重启」";
hint.textContent = "本组含需重启项,修改后请点「保存并重启」.";
panel.appendChild(hint);
}
const grid = document.createElement("div");
@@ -303,12 +303,12 @@
});
const needRestart = restartAfter || data.restart_required;
if (needRestart) {
setStatus(status, "已保存正在重启实例…");
setStatus(status, "已保存,正在重启实例…");
await restartInstance();
setStatus(status, "保存并重启完成");
await loadEnvConfig();
} else {
setStatus(status, "已保存即时生效项已应用");
setStatus(status, "已保存(即时生效项已应用)");
await loadEnvConfig();
}
} catch (e) {
@@ -344,9 +344,9 @@
body: JSON.stringify(body),
});
if (data.restart_required) {
setStatus(status, "密码已保存正在重启…");
setStatus(status, "密码已保存,正在重启…");
await restartInstance();
setStatus(status, "密码已更新请用新密码登录");
setStatus(status, "密码已更新,请用新密码登录");
} else {
setStatus(status, "密码已更新");
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
/* 紧接 instance_theme.js 之后加载避免亮色下先闪暗色底 */
/* 紧接 instance_theme.js 之后加载,避免亮色下先闪暗色底 */
html {
background: #0b0d14;
color-scheme: dark;
+417 -417
View File
@@ -1,417 +1,417 @@
/**
* 三所实例共用 UI复盘详情盈亏着色等
*/
(function (global) {
"use strict";
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function pnlClassFromValue(val) {
const n = Number(String(val == null ? "" : val).replace(/[^\d.-]/g, ""));
if (!Number.isFinite(n) || n === 0) return "";
return n > 0 ? "pnl-profit" : "pnl-loss";
}
function formatPnlSpan(val, suffix) {
const sfx = suffix == null ? "U" : suffix;
const cls = pnlClassFromValue(val);
const text = escapeHtml(val == null || val === "" ? "-" : val) + sfx;
return cls ? `<span class="${cls}">${text}</span>` : text;
}
function buildJournalDetailHtml(o, formatExitLine) {
const moodTags =
Array.isArray(o.mood_issues) && o.mood_issues.length
? o.mood_issues.join(",")
: o.mood_issues || "无";
const exitText =
typeof formatExitLine === "function" ? formatExitLine(o) : o.exit_reason || "无";
const lines = [
`币种/周期${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "-")}`,
`开仓时间${escapeHtml(o.open_datetime || "-")}`,
`平仓时间${escapeHtml(o.close_datetime || "-")}`,
`持仓时长${escapeHtml(o.hold_duration || "-")}`,
`盈亏${formatPnlSpan(o.pnl)}`,
`开仓类型${escapeHtml(o.entry_reason || "无")}`,
`平仓/离场${escapeHtml(exitText)}`,
`预期RR${escapeHtml(o.expect_rr || "-")}`,
`实际RR${escapeHtml(o.real_rr || "-")}`,
`保本后盯盘${escapeHtml(o.post_breakeven_stare || "-")}`,
`心态标签${escapeHtml(moodTags)}`,
`备注${escapeHtml(o.note || "无")}`,
];
return lines.join("<br>");
}
function resolveJournalImages(o) {
if (Array.isArray(o.images) && o.images.length) return o.images;
if (o.image) return [{ tf: "", file: o.image }];
return [];
}
function setJournalDetailImages(o) {
const grid = document.getElementById("detailImages");
const legacyImg = document.getElementById("detailImage");
const images = resolveJournalImages(o || {});
if (grid) {
if (!images.length) {
grid.innerHTML = "";
grid.style.display = "none";
} else {
grid.innerHTML = images
.map(function (img) {
const tf = String(img.tf || "").trim();
const file = String(img.file || "").trim();
if (!file) return "";
const label = tf ? escapeHtml(tf) : "截图";
const src = "/static/images/" + encodeURIComponent(file).replace(/%2F/g, "/");
return (
'<div class="journal-detail-img-cell">' +
'<span class="journal-detail-img-label">' +
label +
"</span>" +
'<img class="journal-detail-img-thumb" src="' +
src +
'" alt="' +
label +
'" onclick="showImage(this.src)">' +
"</div>"
);
})
.join("");
grid.style.display = "grid";
}
if (legacyImg) {
legacyImg.src = "";
legacyImg.style.display = "none";
}
return;
}
if (legacyImg) {
if (images.length === 1) {
legacyImg.src = "/static/images/" + images[0].file;
legacyImg.style.display = "block";
} else {
legacyImg.src = "";
legacyImg.style.display = "none";
}
}
}
function clearJournalDetailImages() {
const grid = document.getElementById("detailImages");
if (grid) {
grid.innerHTML = "";
grid.style.display = "none";
}
const legacyImg = document.getElementById("detailImage");
if (legacyImg) {
legacyImg.src = "";
legacyImg.style.display = "none";
}
}
function setJournalDetailBody(o, formatExitLine) {
const body = document.getElementById("detailBody");
if (!body) return;
body.classList.remove("md-review", "trade-record-detail-wrap");
body.classList.add("journal-detail-meta");
body.innerHTML = buildJournalDetailHtml(o, formatExitLine);
}
function openJournalDetailModal(id, journalCache, formatExitLine) {
const o = journalCache && journalCache[id];
if (!o) return;
const titleEl = document.getElementById("detailTitle");
if (titleEl) {
titleEl.innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
}
setJournalDetailBody(o, formatExitLine);
clearDetailActions();
setJournalDetailImages(o);
if (typeof setDetailModalFullscreen === "function") {
setDetailModalFullscreen(false);
}
const modal = document.getElementById("detailModal");
if (modal) modal.style.display = "flex";
}
function isMobileCompactRecords() {
if (typeof window === "undefined" || !window.matchMedia) return false;
return window.matchMedia("(max-width: 720px)").matches;
}
function inferJournalDirection(o) {
const text = String((o && o.entry_reason) || "");
if (/做空|空头|short/i.test(text)) {
return { text: "做空", cls: "direction-short" };
}
if (/做多|多头|long/i.test(text)) {
return { text: "做多", cls: "direction-long" };
}
return null;
}
function renderJournalListHtml(data) {
if (!data || !data.length) return "";
const mobile = isMobileCompactRecords();
return data
.map(function (o) {
if (mobile) {
const dir = inferJournalDirection(o);
const pnlCls = pnlClassFromValue(o.pnl);
const dirHtml = dir
? `<span class="badge ${dir.cls}">${escapeHtml(dir.text)}</span>`
: `<span class="mrr-muted">-</span>`;
const id = escapeHtml(o.id);
return `<div class="mobile-record-row-wrap">
<button type="button" class="mobile-record-row" onclick="openJournalDetail('${id}')">
<span class="mrr-symbol">${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "")}</span>
<span class="mrr-dir">${dirHtml}</span>
<span class="mrr-pnl ${pnlCls}">${escapeHtml(o.pnl == null || o.pnl === "" ? "-" : o.pnl)}U</span>
</button>
<button type="button" class="mobile-record-del" title="删除" onclick="deleteJournal('${id}')">×</button>
</div>`;
}
const moodTags = (o.mood_issues || []).join(",") || "无";
const id = escapeHtml(o.id);
return `<div class="entry">
<div><strong>${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "-")}</strong> | :${escapeHtml(o.pnl == null || o.pnl === "" ? "-" : o.pnl)}U</div>
<div>:${escapeHtml(o.open_datetime || "-")} :${escapeHtml(o.close_datetime || "-")} 持仓:${escapeHtml(o.hold_duration || "-")}</div>
<div>心态标签:${escapeHtml(moodTags)}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">
<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openJournalDetail('${id}')">查看详情</button>
<button type="button" class="btn-del" onclick="deleteJournal('${id}')">删除</button>
</div>
</div>`;
})
.join("");
}
function parseTradeRecordRow(tr) {
const cells = tr.querySelectorAll("td");
if (cells.length < 14) return null;
const dirBadge = cells[2].querySelector(".badge");
return {
rowId: tr.id,
symbol: cells[0].textContent.trim(),
type: cells[1].textContent.trim(),
directionHtml: (dirBadge ? dirBadge.outerHTML : cells[2].innerHTML).trim(),
directionText: cells[2].textContent.trim(),
trigger: cells[3].textContent.trim(),
stopLoss: cells[4].textContent.trim(),
takeProfit: cells[5].textContent.trim(),
margin: cells[6].textContent.trim(),
leverage: cells[7].textContent.trim(),
holdMinutes: cells[8].textContent.trim(),
openedAt: cells[9].textContent.trim(),
closedAt: cells[10].textContent.trim(),
pnlHtml: cells[11].innerHTML.trim(),
pnlText: cells[11].textContent.trim(),
resultHtml: cells[12].innerHTML.trim(),
resultText: cells[12].textContent.trim(),
actionsHtml: cells[13].innerHTML,
};
}
function renderMobileTradeRow(tr) {
const row = parseTradeRecordRow(tr);
if (!row) return "";
const pnlCls = pnlClassFromValue(row.pnlText);
return `<button type="button" class="mobile-record-row" data-row-id="${escapeHtml(row.rowId)}">
<span class="mrr-symbol">${escapeHtml(row.symbol)}</span>
<span class="mrr-dir">${row.directionHtml}</span>
<span class="mrr-pnl ${pnlCls}">${escapeHtml(row.pnlText || "-")}</span>
</button>`;
}
function tradeDetailRow(label, valueHtml) {
return `<div class="trd-row"><span class="trd-label">${escapeHtml(label)}</span><span class="trd-value">${valueHtml}</span></div>`;
}
function buildTradeRecordDetailHtml(row) {
return `<div class="trade-record-detail">${
tradeDetailRow("品种", escapeHtml(row.symbol)) +
tradeDetailRow("类型", escapeHtml(row.type)) +
tradeDetailRow("方向", row.directionHtml) +
tradeDetailRow("成交价", escapeHtml(row.trigger)) +
tradeDetailRow("止损(开仓)", escapeHtml(row.stopLoss)) +
tradeDetailRow("止盈", escapeHtml(row.takeProfit)) +
tradeDetailRow("基数", escapeHtml(row.margin)) +
tradeDetailRow("杠杆", escapeHtml(row.leverage)) +
tradeDetailRow("持仓分钟", escapeHtml(row.holdMinutes)) +
tradeDetailRow("开仓时间", escapeHtml(row.openedAt)) +
tradeDetailRow("平仓时间", escapeHtml(row.closedAt)) +
tradeDetailRow("盈亏U", row.pnlHtml) +
tradeDetailRow("结果", row.resultHtml)
}</div>`;
}
function clearDetailActions() {
const el = document.getElementById("detailActions");
if (el) {
el.innerHTML = "";
el.style.display = "none";
}
}
function setDetailActionsHtml(html) {
let el = document.getElementById("detailActions");
if (!el) {
const panel = document.querySelector("#detailModal .panel");
if (!panel) return;
el = document.createElement("div");
el.id = "detailActions";
el.className = "detail-actions";
const body = document.getElementById("detailBody");
if (body && body.parentNode === panel) {
panel.insertBefore(el, body.nextSibling);
} else {
panel.appendChild(el);
}
}
el.innerHTML = html || "";
el.style.display = html ? "flex" : "none";
}
function promptReviewEntryReason(options, currentValue) {
const opts = Array.isArray(options) ? options : [];
const cur = String(currentValue == null ? "" : currentValue).trim();
return new Promise(function (resolve) {
const backdrop = document.createElement("div");
backdrop.className = "review-entry-reason-backdrop open";
const modal = document.createElement("div");
modal.className = "review-entry-reason-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
const title = document.createElement("h3");
title.textContent = "开仓类型";
modal.appendChild(title);
const hint = document.createElement("p");
hint.className = "review-entry-reason-hint";
hint.textContent = "请选择下拉选项之一选「不改该项」则保留原值";
modal.appendChild(hint);
const select = document.createElement("select");
select.className = "review-entry-reason-select";
const emptyOpt = document.createElement("option");
emptyOpt.value = "";
emptyOpt.textContent = "不改该项";
select.appendChild(emptyOpt);
const seen = new Set([""]);
if (cur && opts.indexOf(cur) < 0) {
const curOpt = document.createElement("option");
curOpt.value = cur;
curOpt.textContent = cur + "当前";
select.appendChild(curOpt);
seen.add(cur);
}
opts.forEach(function (opt) {
const v = String(opt || "").trim();
if (!v || seen.has(v)) return;
const o = document.createElement("option");
o.value = v;
o.textContent = v;
select.appendChild(o);
seen.add(v);
});
if (cur) select.value = cur;
modal.appendChild(select);
const actions = document.createElement("div");
actions.className = "review-entry-reason-actions";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "review-entry-reason-cancel";
cancelBtn.textContent = "取消";
const okBtn = document.createElement("button");
okBtn.type = "button";
okBtn.className = "review-entry-reason-ok";
okBtn.textContent = "确定";
actions.appendChild(cancelBtn);
actions.appendChild(okBtn);
modal.appendChild(actions);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
function cleanup(result) {
document.removeEventListener("keydown", onKey);
backdrop.remove();
resolve(result);
}
function onKey(ev) {
if (ev.key === "Escape") cleanup(null);
}
cancelBtn.addEventListener("click", function () {
cleanup(null);
});
backdrop.addEventListener("click", function (ev) {
if (ev.target === backdrop) cleanup(null);
});
okBtn.addEventListener("click", function () {
cleanup(select.value);
});
document.addEventListener("keydown", onKey);
select.focus();
});
}
function openTradeRecordDetailModal(tr) {
const row = parseTradeRecordRow(tr);
if (!row) return;
const titleEl = document.getElementById("detailTitle");
if (titleEl) {
titleEl.innerText = `交易记录|${row.symbol}`;
}
const body = document.getElementById("detailBody");
if (body) {
body.classList.remove("md-review", "journal-detail-meta");
body.classList.add("trade-record-detail-wrap");
body.innerHTML = buildTradeRecordDetailHtml(row);
}
setDetailActionsHtml(
`<div class="detail-actions-inner">${row.actionsHtml}</div>`
);
const imgEl = document.getElementById("detailImage");
if (imgEl) {
imgEl.src = "";
imgEl.style.display = "none";
}
if (typeof setDetailModalFullscreen === "function") {
setDetailModalFullscreen(false);
}
const modal = document.getElementById("detailModal");
if (modal) modal.style.display = "flex";
}
global.InstanceUI = {
escapeHtml: escapeHtml,
pnlClassFromValue: pnlClassFromValue,
formatPnlSpan: formatPnlSpan,
buildJournalDetailHtml: buildJournalDetailHtml,
setJournalDetailBody: setJournalDetailBody,
openJournalDetailModal: openJournalDetailModal,
isMobileCompactRecords: isMobileCompactRecords,
inferJournalDirection: inferJournalDirection,
renderJournalListHtml: renderJournalListHtml,
parseTradeRecordRow: parseTradeRecordRow,
renderMobileTradeRow: renderMobileTradeRow,
buildTradeRecordDetailHtml: buildTradeRecordDetailHtml,
openTradeRecordDetailModal: openTradeRecordDetailModal,
clearDetailActions: clearDetailActions,
clearJournalDetailImages: clearJournalDetailImages,
setJournalDetailImages: setJournalDetailImages,
promptReviewEntryReason: promptReviewEntryReason,
};
})(typeof window !== "undefined" ? window : globalThis);
/**
* 三所实例共用 UI:复盘详情,盈亏着色等.
*/
(function (global) {
"use strict";
function escapeHtml(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function pnlClassFromValue(val) {
const n = Number(String(val == null ? "" : val).replace(/[^\d.-]/g, ""));
if (!Number.isFinite(n) || n === 0) return "";
return n > 0 ? "pnl-profit" : "pnl-loss";
}
function formatPnlSpan(val, suffix) {
const sfx = suffix == null ? "U" : suffix;
const cls = pnlClassFromValue(val);
const text = escapeHtml(val == null || val === "" ? "-" : val) + sfx;
return cls ? `<span class="${cls}">${text}</span>` : text;
}
function buildJournalDetailHtml(o, formatExitLine) {
const moodTags =
Array.isArray(o.mood_issues) && o.mood_issues.length
? o.mood_issues.join(",")
: o.mood_issues || "无";
const exitText =
typeof formatExitLine === "function" ? formatExitLine(o) : o.exit_reason || "无";
const lines = [
`币种/周期:${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "-")}`,
`开仓时间:${escapeHtml(o.open_datetime || "-")}`,
`平仓时间:${escapeHtml(o.close_datetime || "-")}`,
`持仓时长:${escapeHtml(o.hold_duration || "-")}`,
`盈亏:${formatPnlSpan(o.pnl)}`,
`开仓类型:${escapeHtml(o.entry_reason || "无")}`,
`平仓/离场:${escapeHtml(exitText)}`,
`预期RR:${escapeHtml(o.expect_rr || "-")}`,
`实际RR:${escapeHtml(o.real_rr || "-")}`,
`保本后盯盘:${escapeHtml(o.post_breakeven_stare || "-")}`,
`心态标签:${escapeHtml(moodTags)}`,
`备注:${escapeHtml(o.note || "无")}`,
];
return lines.join("<br>");
}
function resolveJournalImages(o) {
if (Array.isArray(o.images) && o.images.length) return o.images;
if (o.image) return [{ tf: "", file: o.image }];
return [];
}
function setJournalDetailImages(o) {
const grid = document.getElementById("detailImages");
const legacyImg = document.getElementById("detailImage");
const images = resolveJournalImages(o || {});
if (grid) {
if (!images.length) {
grid.innerHTML = "";
grid.style.display = "none";
} else {
grid.innerHTML = images
.map(function (img) {
const tf = String(img.tf || "").trim();
const file = String(img.file || "").trim();
if (!file) return "";
const label = tf ? escapeHtml(tf) : "截图";
const src = "/static/images/" + encodeURIComponent(file).replace(/%2F/g, "/");
return (
'<div class="journal-detail-img-cell">' +
'<span class="journal-detail-img-label">' +
label +
"</span>" +
'<img class="journal-detail-img-thumb" src="' +
src +
'" alt="' +
label +
'" onclick="showImage(this.src)">' +
"</div>"
);
})
.join("");
grid.style.display = "grid";
}
if (legacyImg) {
legacyImg.src = "";
legacyImg.style.display = "none";
}
return;
}
if (legacyImg) {
if (images.length === 1) {
legacyImg.src = "/static/images/" + images[0].file;
legacyImg.style.display = "block";
} else {
legacyImg.src = "";
legacyImg.style.display = "none";
}
}
}
function clearJournalDetailImages() {
const grid = document.getElementById("detailImages");
if (grid) {
grid.innerHTML = "";
grid.style.display = "none";
}
const legacyImg = document.getElementById("detailImage");
if (legacyImg) {
legacyImg.src = "";
legacyImg.style.display = "none";
}
}
function setJournalDetailBody(o, formatExitLine) {
const body = document.getElementById("detailBody");
if (!body) return;
body.classList.remove("md-review", "trade-record-detail-wrap");
body.classList.add("journal-detail-meta");
body.innerHTML = buildJournalDetailHtml(o, formatExitLine);
}
function openJournalDetailModal(id, journalCache, formatExitLine) {
const o = journalCache && journalCache[id];
if (!o) return;
const titleEl = document.getElementById("detailTitle");
if (titleEl) {
titleEl.innerText = `交易复盘详情|${o.coin || "-"} ${o.tf || "-"}`;
}
setJournalDetailBody(o, formatExitLine);
clearDetailActions();
setJournalDetailImages(o);
if (typeof setDetailModalFullscreen === "function") {
setDetailModalFullscreen(false);
}
const modal = document.getElementById("detailModal");
if (modal) modal.style.display = "flex";
}
function isMobileCompactRecords() {
if (typeof window === "undefined" || !window.matchMedia) return false;
return window.matchMedia("(max-width: 720px)").matches;
}
function inferJournalDirection(o) {
const text = String((o && o.entry_reason) || "");
if (/做空|空头|short/i.test(text)) {
return { text: "做空", cls: "direction-short" };
}
if (/做多|多头|long/i.test(text)) {
return { text: "做多", cls: "direction-long" };
}
return null;
}
function renderJournalListHtml(data) {
if (!data || !data.length) return "";
const mobile = isMobileCompactRecords();
return data
.map(function (o) {
if (mobile) {
const dir = inferJournalDirection(o);
const pnlCls = pnlClassFromValue(o.pnl);
const dirHtml = dir
? `<span class="badge ${dir.cls}">${escapeHtml(dir.text)}</span>`
: `<span class="mrr-muted">-</span>`;
const id = escapeHtml(o.id);
return `<div class="mobile-record-row-wrap">
<button type="button" class="mobile-record-row" onclick="openJournalDetail('${id}')">
<span class="mrr-symbol">${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "")}</span>
<span class="mrr-dir">${dirHtml}</span>
<span class="mrr-pnl ${pnlCls}">${escapeHtml(o.pnl == null || o.pnl === "" ? "-" : o.pnl)}U</span>
</button>
<button type="button" class="mobile-record-del" title="删除" onclick="deleteJournal('${id}')">×</button>
</div>`;
}
const moodTags = (o.mood_issues || []).join(",") || "无";
const id = escapeHtml(o.id);
return `<div class="entry">
<div><strong>${escapeHtml(o.coin || "-")} ${escapeHtml(o.tf || "-")}</strong> | :${escapeHtml(o.pnl == null || o.pnl === "" ? "-" : o.pnl)}U</div>
<div>:${escapeHtml(o.open_datetime || "-")} :${escapeHtml(o.close_datetime || "-")} 持仓:${escapeHtml(o.hold_duration || "-")}</div>
<div>心态标签:${escapeHtml(moodTags)}</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:6px">
<button type="button" class="btn-del" style="border:none;cursor:pointer;background:#1f3a5a;color:#8fc8ff" onclick="openJournalDetail('${id}')">查看详情</button>
<button type="button" class="btn-del" onclick="deleteJournal('${id}')">删除</button>
</div>
</div>`;
})
.join("");
}
function parseTradeRecordRow(tr) {
const cells = tr.querySelectorAll("td");
if (cells.length < 14) return null;
const dirBadge = cells[2].querySelector(".badge");
return {
rowId: tr.id,
symbol: cells[0].textContent.trim(),
type: cells[1].textContent.trim(),
directionHtml: (dirBadge ? dirBadge.outerHTML : cells[2].innerHTML).trim(),
directionText: cells[2].textContent.trim(),
trigger: cells[3].textContent.trim(),
stopLoss: cells[4].textContent.trim(),
takeProfit: cells[5].textContent.trim(),
margin: cells[6].textContent.trim(),
leverage: cells[7].textContent.trim(),
holdMinutes: cells[8].textContent.trim(),
openedAt: cells[9].textContent.trim(),
closedAt: cells[10].textContent.trim(),
pnlHtml: cells[11].innerHTML.trim(),
pnlText: cells[11].textContent.trim(),
resultHtml: cells[12].innerHTML.trim(),
resultText: cells[12].textContent.trim(),
actionsHtml: cells[13].innerHTML,
};
}
function renderMobileTradeRow(tr) {
const row = parseTradeRecordRow(tr);
if (!row) return "";
const pnlCls = pnlClassFromValue(row.pnlText);
return `<button type="button" class="mobile-record-row" data-row-id="${escapeHtml(row.rowId)}">
<span class="mrr-symbol">${escapeHtml(row.symbol)}</span>
<span class="mrr-dir">${row.directionHtml}</span>
<span class="mrr-pnl ${pnlCls}">${escapeHtml(row.pnlText || "-")}</span>
</button>`;
}
function tradeDetailRow(label, valueHtml) {
return `<div class="trd-row"><span class="trd-label">${escapeHtml(label)}</span><span class="trd-value">${valueHtml}</span></div>`;
}
function buildTradeRecordDetailHtml(row) {
return `<div class="trade-record-detail">${
tradeDetailRow("品种", escapeHtml(row.symbol)) +
tradeDetailRow("类型", escapeHtml(row.type)) +
tradeDetailRow("方向", row.directionHtml) +
tradeDetailRow("成交价", escapeHtml(row.trigger)) +
tradeDetailRow("止损(开仓)", escapeHtml(row.stopLoss)) +
tradeDetailRow("止盈", escapeHtml(row.takeProfit)) +
tradeDetailRow("基数", escapeHtml(row.margin)) +
tradeDetailRow("杠杆", escapeHtml(row.leverage)) +
tradeDetailRow("持仓分钟", escapeHtml(row.holdMinutes)) +
tradeDetailRow("开仓时间", escapeHtml(row.openedAt)) +
tradeDetailRow("平仓时间", escapeHtml(row.closedAt)) +
tradeDetailRow("盈亏U", row.pnlHtml) +
tradeDetailRow("结果", row.resultHtml)
}</div>`;
}
function clearDetailActions() {
const el = document.getElementById("detailActions");
if (el) {
el.innerHTML = "";
el.style.display = "none";
}
}
function setDetailActionsHtml(html) {
let el = document.getElementById("detailActions");
if (!el) {
const panel = document.querySelector("#detailModal .panel");
if (!panel) return;
el = document.createElement("div");
el.id = "detailActions";
el.className = "detail-actions";
const body = document.getElementById("detailBody");
if (body && body.parentNode === panel) {
panel.insertBefore(el, body.nextSibling);
} else {
panel.appendChild(el);
}
}
el.innerHTML = html || "";
el.style.display = html ? "flex" : "none";
}
function promptReviewEntryReason(options, currentValue) {
const opts = Array.isArray(options) ? options : [];
const cur = String(currentValue == null ? "" : currentValue).trim();
return new Promise(function (resolve) {
const backdrop = document.createElement("div");
backdrop.className = "review-entry-reason-backdrop open";
const modal = document.createElement("div");
modal.className = "review-entry-reason-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
const title = document.createElement("h3");
title.textContent = "开仓类型";
modal.appendChild(title);
const hint = document.createElement("p");
hint.className = "review-entry-reason-hint";
hint.textContent = "请选择下拉选项之一;选「不改该项」则保留原值.";
modal.appendChild(hint);
const select = document.createElement("select");
select.className = "review-entry-reason-select";
const emptyOpt = document.createElement("option");
emptyOpt.value = "";
emptyOpt.textContent = "(不改该项)";
select.appendChild(emptyOpt);
const seen = new Set([""]);
if (cur && opts.indexOf(cur) < 0) {
const curOpt = document.createElement("option");
curOpt.value = cur;
curOpt.textContent = cur + "(当前)";
select.appendChild(curOpt);
seen.add(cur);
}
opts.forEach(function (opt) {
const v = String(opt || "").trim();
if (!v || seen.has(v)) return;
const o = document.createElement("option");
o.value = v;
o.textContent = v;
select.appendChild(o);
seen.add(v);
});
if (cur) select.value = cur;
modal.appendChild(select);
const actions = document.createElement("div");
actions.className = "review-entry-reason-actions";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "review-entry-reason-cancel";
cancelBtn.textContent = "取消";
const okBtn = document.createElement("button");
okBtn.type = "button";
okBtn.className = "review-entry-reason-ok";
okBtn.textContent = "确定";
actions.appendChild(cancelBtn);
actions.appendChild(okBtn);
modal.appendChild(actions);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
function cleanup(result) {
document.removeEventListener("keydown", onKey);
backdrop.remove();
resolve(result);
}
function onKey(ev) {
if (ev.key === "Escape") cleanup(null);
}
cancelBtn.addEventListener("click", function () {
cleanup(null);
});
backdrop.addEventListener("click", function (ev) {
if (ev.target === backdrop) cleanup(null);
});
okBtn.addEventListener("click", function () {
cleanup(select.value);
});
document.addEventListener("keydown", onKey);
select.focus();
});
}
function openTradeRecordDetailModal(tr) {
const row = parseTradeRecordRow(tr);
if (!row) return;
const titleEl = document.getElementById("detailTitle");
if (titleEl) {
titleEl.innerText = `交易记录|${row.symbol}`;
}
const body = document.getElementById("detailBody");
if (body) {
body.classList.remove("md-review", "journal-detail-meta");
body.classList.add("trade-record-detail-wrap");
body.innerHTML = buildTradeRecordDetailHtml(row);
}
setDetailActionsHtml(
`<div class="detail-actions-inner">${row.actionsHtml}</div>`
);
const imgEl = document.getElementById("detailImage");
if (imgEl) {
imgEl.src = "";
imgEl.style.display = "none";
}
if (typeof setDetailModalFullscreen === "function") {
setDetailModalFullscreen(false);
}
const modal = document.getElementById("detailModal");
if (modal) modal.style.display = "flex";
}
global.InstanceUI = {
escapeHtml: escapeHtml,
pnlClassFromValue: pnlClassFromValue,
formatPnlSpan: formatPnlSpan,
buildJournalDetailHtml: buildJournalDetailHtml,
setJournalDetailBody: setJournalDetailBody,
openJournalDetailModal: openJournalDetailModal,
isMobileCompactRecords: isMobileCompactRecords,
inferJournalDirection: inferJournalDirection,
renderJournalListHtml: renderJournalListHtml,
parseTradeRecordRow: parseTradeRecordRow,
renderMobileTradeRow: renderMobileTradeRow,
buildTradeRecordDetailHtml: buildTradeRecordDetailHtml,
openTradeRecordDetailModal: openTradeRecordDetailModal,
clearDetailActions: clearDetailActions,
clearJournalDetailImages: clearJournalDetailImages,
setJournalDetailImages: setJournalDetailImages,
promptReviewEntryReason: promptReviewEntryReason,
};
})(typeof window !== "undefined" ? window : globalThis);
+145 -145
View File
@@ -1,145 +1,145 @@
/**
* 复盘表单四周期截图即时上传与状态展示
*/
(function (global) {
"use strict";
function newDraftId() {
if (global.crypto && typeof global.crypto.randomUUID === "function") {
return global.crypto.randomUUID().replace(/-/g, "");
}
var s = "";
for (var i = 0; i < 32; i++) {
s += Math.floor(Math.random() * 16).toString(16);
}
return s;
}
function ensureDraftId(root) {
var scope = root || document;
var el = scope.querySelector("#journal-draft-id");
if (!el) return "";
if (!el.value) {
el.value = newDraftId();
}
return el.value;
}
function rowParts(input) {
var row = input.closest(".journal-upload-row");
if (!row) return {};
return {
row: row,
status: row.querySelector(".journal-upload-status"),
hidden: row.querySelector(".journal-upload-hidden-file"),
};
}
function setStatus(statusEl, text, kind) {
if (!statusEl) return;
statusEl.textContent = text || "";
statusEl.classList.remove(
"journal-upload-status--pending",
"journal-upload-status--ok",
"journal-upload-status--err"
);
if (kind) {
statusEl.classList.add("journal-upload-status--" + kind);
}
}
function uploadSlotFile(input, file) {
var parts = rowParts(input);
var draftId = ensureDraftId(input.form || document);
if (!draftId || !file) {
setStatus(parts.status, "上传失败", "err");
return;
}
setStatus(parts.status, "上传中…", "pending");
if (parts.hidden) {
parts.hidden.value = "";
}
var fd = new FormData();
fd.append("journal_draft_id", draftId);
fd.append("tf", input.getAttribute("data-tf") || "");
fd.append("file", file);
fetch("/api/journal_upload_slot", { method: "POST", body: fd, credentials: "same-origin" })
.then(function (res) {
return res.json().then(function (data) {
return { ok: res.ok, data: data };
});
})
.then(function (result) {
if (!result.ok || !result.data || !result.data.ok) {
throw new Error(
(result.data && result.data.error) || "upload failed"
);
}
var fname = String(result.data.file || "").trim();
if (parts.hidden) {
parts.hidden.value = fname;
}
input.value = "";
setStatus(parts.status, "上传成功 " + fname, "ok");
})
.catch(function () {
if (parts.hidden) {
parts.hidden.value = "";
}
setStatus(parts.status, "上传失败", "err");
});
}
function bindInput(input) {
if (!input || input.dataset.journalSlotBound === "1") return;
input.dataset.journalSlotBound = "1";
input.addEventListener("change", function () {
var file = input.files && input.files[0];
if (!file) {
var parts = rowParts(input);
if (parts.hidden) {
parts.hidden.value = "";
}
setStatus(parts.status, "", "");
return;
}
uploadSlotFile(input, file);
});
}
function resetSlots(root) {
var scope = root || document;
var draftEl = scope.querySelector("#journal-draft-id");
if (draftEl) {
draftEl.value = newDraftId();
}
scope.querySelectorAll(".journal-upload-hidden-file").forEach(function (el) {
el.value = "";
});
scope.querySelectorAll(".journal-upload-slot-input").forEach(function (el) {
el.value = "";
});
scope.querySelectorAll(".journal-upload-status").forEach(function (el) {
setStatus(el, "", "");
});
}
function init(root) {
var scope = root || document;
ensureDraftId(scope);
scope.querySelectorAll(".journal-upload-slot-input").forEach(bindInput);
}
global.JournalUploadSlots = { init: init, reset: resetSlots };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
init(document);
});
} else {
init(document);
}
})(typeof window !== "undefined" ? window : globalThis);
/**
* 复盘表单:四周期截图即时上传与状态展示.
*/
(function (global) {
"use strict";
function newDraftId() {
if (global.crypto && typeof global.crypto.randomUUID === "function") {
return global.crypto.randomUUID().replace(/-/g, "");
}
var s = "";
for (var i = 0; i < 32; i++) {
s += Math.floor(Math.random() * 16).toString(16);
}
return s;
}
function ensureDraftId(root) {
var scope = root || document;
var el = scope.querySelector("#journal-draft-id");
if (!el) return "";
if (!el.value) {
el.value = newDraftId();
}
return el.value;
}
function rowParts(input) {
var row = input.closest(".journal-upload-row");
if (!row) return {};
return {
row: row,
status: row.querySelector(".journal-upload-status"),
hidden: row.querySelector(".journal-upload-hidden-file"),
};
}
function setStatus(statusEl, text, kind) {
if (!statusEl) return;
statusEl.textContent = text || "";
statusEl.classList.remove(
"journal-upload-status--pending",
"journal-upload-status--ok",
"journal-upload-status--err"
);
if (kind) {
statusEl.classList.add("journal-upload-status--" + kind);
}
}
function uploadSlotFile(input, file) {
var parts = rowParts(input);
var draftId = ensureDraftId(input.form || document);
if (!draftId || !file) {
setStatus(parts.status, "上传失败", "err");
return;
}
setStatus(parts.status, "上传中…", "pending");
if (parts.hidden) {
parts.hidden.value = "";
}
var fd = new FormData();
fd.append("journal_draft_id", draftId);
fd.append("tf", input.getAttribute("data-tf") || "");
fd.append("file", file);
fetch("/api/journal_upload_slot", { method: "POST", body: fd, credentials: "same-origin" })
.then(function (res) {
return res.json().then(function (data) {
return { ok: res.ok, data: data };
});
})
.then(function (result) {
if (!result.ok || !result.data || !result.data.ok) {
throw new Error(
(result.data && result.data.error) || "upload failed"
);
}
var fname = String(result.data.file || "").trim();
if (parts.hidden) {
parts.hidden.value = fname;
}
input.value = "";
setStatus(parts.status, "上传成功 " + fname, "ok");
})
.catch(function () {
if (parts.hidden) {
parts.hidden.value = "";
}
setStatus(parts.status, "上传失败", "err");
});
}
function bindInput(input) {
if (!input || input.dataset.journalSlotBound === "1") return;
input.dataset.journalSlotBound = "1";
input.addEventListener("change", function () {
var file = input.files && input.files[0];
if (!file) {
var parts = rowParts(input);
if (parts.hidden) {
parts.hidden.value = "";
}
setStatus(parts.status, "", "");
return;
}
uploadSlotFile(input, file);
});
}
function resetSlots(root) {
var scope = root || document;
var draftEl = scope.querySelector("#journal-draft-id");
if (draftEl) {
draftEl.value = newDraftId();
}
scope.querySelectorAll(".journal-upload-hidden-file").forEach(function (el) {
el.value = "";
});
scope.querySelectorAll(".journal-upload-slot-input").forEach(function (el) {
el.value = "";
});
scope.querySelectorAll(".journal-upload-status").forEach(function (el) {
setStatus(el, "", "");
});
}
function init(root) {
var scope = root || document;
ensureDraftId(scope);
scope.querySelectorAll(".journal-upload-slot-input").forEach(bindInput);
}
global.JournalUploadSlots = { init: init, reset: resetSlots };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
init(document);
});
} else {
init(document);
}
})(typeof window !== "undefined" ? window : globalThis);
+160 -160
View File
@@ -1,160 +1,160 @@
/**
* 关键位监控添加表单类型切换显隐成交量排名校验三所实例共用
*/
(function (global) {
const RS_TYPES = new Set([
"关键支撑阻力",
"关键阻力位",
"关键支撑位",
]);
function syncKeyMonitorFormFields() {
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if (!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破", "收敛突破"]);
const fibTypes = new Set(["斐波回调0.618", "斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["回调触价开仓", "突破触价开仓", "触价开仓"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !RS_TYPES.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if (dirEl) {
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if (!showDir) dirEl.value = "";
}
if (modeEl) modeEl.style.display = showAuto ? "" : "none";
if (manualTp) {
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if (beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if (global.TimeCloseUI) global.TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if (upperEl) {
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if (hideBounds) upperEl.value = "";
}
if (lowerEl) {
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if (hideBounds) lowerEl.value = "";
}
if (fbPriceEl) {
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if (!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder =
dirEl && dirEl.value === "short"
? "高点阻力"
: dirEl && dirEl.value === "long"
? "低点支撑"
: "做空填高点/做多填低点";
}
[teEntryEl, teSlEl, teTpEl].forEach((el) => {
if (!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if (!showTe) el.value = "";
});
}
function submitKeyForm(keyForm, label) {
if (
document.body &&
document.body.getAttribute("data-embed-shell") === "1" &&
global.InstanceEmbed &&
typeof global.InstanceEmbed.postFormAndReload === "function"
) {
global.InstanceEmbed.postFormAndReload(keyForm, label || "提交中…");
return;
}
if (global.FormSubmitGuard) global.FormSubmitGuard.nativeSubmitOnce(keyForm, label || "提交中…");
else keyForm.submit();
}
function bindKeyMonitorForm() {
const keyForm = document.getElementById("key-form");
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if (keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if (keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if (keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if (global.TimeCloseUI) {
global.TimeCloseUI.bindTimeCloseForm(
"key-time-close-cb",
"key-time-close-hours",
"key-time-close-wrap"
);
}
if (!keyForm || keyForm.dataset.keyFormBound === "1") return;
keyForm.dataset.keyFormBound = "1";
keyForm.addEventListener("submit", (e) => {
e.preventDefault();
if (global.FormSubmitGuard && global.FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if (!symbol) {
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if (typeVal === "假突破") {
submitKeyForm(keyForm, "提交中…");
return;
}
if (global.FormSubmitGuard) global.FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then((r) => r.json().then((d) => ({ status: r.status, data: d })))
.then(({ status, data }) => {
if (status >= 400 || !data.ok) {
alert((data && data.msg) || "日成交量排名读取失败");
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
const inTop = data.in_top != null ? data.in_top : data.in_top30;
if (data.rank == null || !inTop) {
alert(
`${data.symbol} 当前日成交量排名 ${data.rank == null ? "—" : data.rank}/${data.total}不在前${rankMax}已拦截`
);
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
return;
}
submitKeyForm(keyForm, "提交中…");
})
.catch(() => {
alert("日成交量排名检查失败请稍后重试");
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
});
});
}
global.KeyMonitorForm = {
syncFields: syncKeyMonitorFormFields,
init: bindKeyMonitorForm,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bindKeyMonitorForm);
} else {
bindKeyMonitorForm();
}
})(typeof window !== "undefined" ? window : globalThis);
/**
* 关键位监控添加表单:类型切换显隐,成交量排名校验(三所实例共用).
*/
(function (global) {
const RS_TYPES = new Set([
"关键支撑阻力",
"关键阻力位",
"关键支撑位",
]);
function syncKeyMonitorFormFields() {
const typeEl = document.querySelector('#key-form [name="type"]');
const dirEl = document.getElementById("key-direction");
const modeEl = document.getElementById("key-sl-tp-mode");
const manualTp = document.getElementById("key-manual-tp");
const beWrap = document.getElementById("key-breakeven-wrap");
if (!typeEl) return;
const t = (typeEl.value || "").trim();
const autoTypes = new Set(["箱体突破", "收敛突破"]);
const fibTypes = new Set(["斐波回调0.618", "斐波回调0.786"]);
const fbTypes = new Set(["假突破"]);
const teTypes = new Set(["回调触价开仓", "突破触价开仓", "触价开仓"]);
const showAuto = autoTypes.has(t);
const showFb = fbTypes.has(t);
const showTe = teTypes.has(t);
const showBe = showAuto || fibTypes.has(t) || showFb || showTe;
const showDir = !RS_TYPES.has(t);
const upperEl = document.getElementById("key-upper");
const lowerEl = document.getElementById("key-lower");
const fbPriceEl = document.getElementById("key-fb-price");
const teEntryEl = document.getElementById("key-trigger-entry");
const teSlEl = document.getElementById("key-trigger-sl");
const teTpEl = document.getElementById("key-trigger-tp");
if (dirEl) {
dirEl.style.display = showDir ? "" : "none";
dirEl.required = showDir;
if (!showDir) dirEl.value = "";
}
if (modeEl) modeEl.style.display = showAuto ? "" : "none";
if (manualTp) {
const trend = showAuto && modeEl && modeEl.value === "trend_manual";
manualTp.style.display = trend ? "" : "none";
manualTp.required = !!trend;
}
if (beWrap) beWrap.style.display = showBe ? "inline-flex" : "none";
if (global.TimeCloseUI) global.TimeCloseUI.syncKeyTimeCloseVisibility(showBe);
const hideBounds = showFb || showTe;
if (upperEl) {
upperEl.style.display = hideBounds ? "none" : "";
upperEl.required = !hideBounds;
if (hideBounds) upperEl.value = "";
}
if (lowerEl) {
lowerEl.style.display = hideBounds ? "none" : "";
lowerEl.required = !hideBounds;
if (hideBounds) lowerEl.value = "";
}
if (fbPriceEl) {
fbPriceEl.style.display = showFb ? "" : "none";
fbPriceEl.required = showFb;
if (!showFb) fbPriceEl.value = "";
fbPriceEl.placeholder =
dirEl && dirEl.value === "short"
? "高点(阻力)"
: dirEl && dirEl.value === "long"
? "低点(支撑)"
: "做空填高点/做多填低点";
}
[teEntryEl, teSlEl, teTpEl].forEach((el) => {
if (!el) return;
el.style.display = showTe ? "" : "none";
el.required = showTe;
if (!showTe) el.value = "";
});
}
function submitKeyForm(keyForm, label) {
if (
document.body &&
document.body.getAttribute("data-embed-shell") === "1" &&
global.InstanceEmbed &&
typeof global.InstanceEmbed.postFormAndReload === "function"
) {
global.InstanceEmbed.postFormAndReload(keyForm, label || "提交中…");
return;
}
if (global.FormSubmitGuard) global.FormSubmitGuard.nativeSubmitOnce(keyForm, label || "提交中…");
else keyForm.submit();
}
function bindKeyMonitorForm() {
const keyForm = document.getElementById("key-form");
const keyTypeSel = document.querySelector('#key-form [name="type"]');
const keyModeSel = document.getElementById("key-sl-tp-mode");
const keyDirSel = document.getElementById("key-direction");
if (keyTypeSel) keyTypeSel.addEventListener("change", syncKeyMonitorFormFields);
if (keyModeSel) keyModeSel.addEventListener("change", syncKeyMonitorFormFields);
if (keyDirSel) keyDirSel.addEventListener("change", syncKeyMonitorFormFields);
syncKeyMonitorFormFields();
if (global.TimeCloseUI) {
global.TimeCloseUI.bindTimeCloseForm(
"key-time-close-cb",
"key-time-close-hours",
"key-time-close-wrap"
);
}
if (!keyForm || keyForm.dataset.keyFormBound === "1") return;
keyForm.dataset.keyFormBound = "1";
keyForm.addEventListener("submit", (e) => {
e.preventDefault();
if (global.FormSubmitGuard && global.FormSubmitGuard.isLocked(keyForm)) return;
const symbolEl = keyForm.querySelector('[name="symbol"]');
const symbol = (symbolEl ? symbolEl.value : "").trim();
if (!symbol) {
alert("请先输入交易对");
return;
}
const typeVal = (keyForm.querySelector('[name="type"]') || {}).value || "";
if (typeVal === "假突破") {
submitKeyForm(keyForm, "提交中…");
return;
}
if (global.FormSubmitGuard) global.FormSubmitGuard.lock(keyForm, "校验排名中…");
fetch(`/api/symbol_liquidity_rank?symbol=${encodeURIComponent(symbol)}`)
.then((r) => r.json().then((d) => ({ status: r.status, data: d })))
.then(({ status, data }) => {
if (status >= 400 || !data.ok) {
alert((data && data.msg) || "日成交量排名读取失败");
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
return;
}
const rankMax = data.rank_max || 30;
const inTop = data.in_top != null ? data.in_top : data.in_top30;
if (data.rank == null || !inTop) {
alert(
`${data.symbol} 当前日成交量排名 ${data.rank == null ? "—" : data.rank}/${data.total},不在前${rankMax},已拦截.`
);
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
return;
}
submitKeyForm(keyForm, "提交中…");
})
.catch(() => {
alert("日成交量排名检查失败,请稍后重试");
if (global.FormSubmitGuard) global.FormSubmitGuard.unlock(keyForm);
});
});
}
global.KeyMonitorForm = {
syncFields: syncKeyMonitorFormFields,
init: bindKeyMonitorForm,
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", bindKeyMonitorForm);
} else {
bindKeyMonitorForm();
}
})(typeof window !== "undefined" ? window : globalThis);
+4 -4
View File
@@ -1,7 +1,7 @@
/**
* 实盘下单填完币种与止盈止损后在表单下方显示预估风险 / 预估盈利 / 预估盈亏比
* 以损定仓风险 = 当前交易基数 × risk%
* 全仓杠杆风险 = 可用保证金×缓冲 × 杠杆 × |SL-入场|/ calc_risk_amount_from_plan
* 实盘下单:填完币种与止盈止损后,在表单下方显示预估风险 / 预估盈利 / 预估盈亏比.
* 以损定仓:风险 = 当前交易基数 × risk%.
* 全仓杠杆:风险 = 可用保证金×缓冲 × 杠杆 × |SL-入场|/( calc_risk_amount_from_plan ).
*/
(function (global) {
"use strict";
@@ -35,7 +35,7 @@
function setMetric(el, label, valueText) {
if (!el) return;
el.innerHTML = label + "<strong>" + valueText + "</strong>";
el.innerHTML = label + ":<strong>" + valueText + "</strong>";
}
function sizingMode() {
@@ -1,5 +1,5 @@
/**
* 期权到期倒计时实例期权页 + 中控监控/看板共用
* 期权到期倒计时(实例期权页 + 中控监控/看板共用)
*/
(function (global) {
function normalizeExpMs(v) {
+5 -5
View File
@@ -414,7 +414,7 @@
body: JSON.stringify(body),
});
const msgEl = document.getElementById("opt-order-msg");
msgEl.textContent = d.ok ? "下单已提交可在 OKX 委托中查看" : (d.msg || "失败");
msgEl.textContent = d.ok ? "下单已提交,可在 OKX 委托中查看" : (d.msg || "失败");
msgEl.classList.toggle("opt-error", !d.ok);
if (d.ok) {
refreshAllPositions();
@@ -470,10 +470,10 @@
}
const bid = q.bid;
if (bid == null || bid <= 0) {
alert("暂无买一价请稍后在 OKX App 平仓或等盘口恢复");
alert("暂无买一价,请稍后在 OKX App 平仓或等盘口恢复");
return;
}
if (!confirm("限价卖出 @ 买一 " + fmt(bid, 4) + "每 1 ETH/BTC)?\n合约" + inst)) return;
if (!confirm("限价卖出 @ 买一 " + fmt(bid, 4) + "(每 1 ETH/BTC)?\n合约:" + inst)) return;
if (btn) btn.disabled = true;
try {
const r = await apiJson("/api/options/close", {
@@ -549,8 +549,8 @@
async function deleteHistoryRow(id, status) {
const warn = status === "open"
? "该记录仍为持仓中仅删除本地记录不影响交易所持仓确认删除"
: "确认删除该条期权历史记录";
? "该记录仍为持仓中,仅删除本地记录,不影响交易所持仓.确认删除?"
: "确认删除该条期权历史记录?";
if (!confirm(warn)) return;
const r = await apiJson("/api/options/history/" + encodeURIComponent(id), { method: "DELETE" });
if (!r.ok) {
+10 -10
View File
@@ -84,14 +84,14 @@
function syncDirectionLock() {
const opt = selectedOption();
if (!opt || !opt.value) {
riskBanner.textContent = "当前风险请选择持仓币种";
riskBanner.textContent = "当前风险:请选择持仓币种";
return;
}
const dir = opt.getAttribute("data-direction") || "long";
const rp = opt.getAttribute("data-risk-percent") || "—";
dirInput.value = dir;
riskBanner.textContent =
"当前风险" + rp + "%来自监控单 #" + (opt.getAttribute("data-monitor-id") || "?") + "";
"当前风险:" + rp + "%(来自监控单 #" + (opt.getAttribute("data-monitor-id") || "?") + ")";
}
function syncSubmitButton() {
@@ -144,9 +144,9 @@
p.avg_entry_after +
" · 打到止损约 " +
p.loss_at_sl_usdt +
"U风险预算 " +
"U(风险预算 " +
(p.risk_budget_usdt != null ? p.risk_budget_usdt : "—") +
"U";
"U)";
}
function syncFieldVisibility() {
@@ -211,7 +211,7 @@
})
.catch(function () {
if (previewBtn) previewBtn.disabled = false;
showReject("预览请求失败请稍后重试");
showReject("预览请求失败,请稍后重试");
});
}
@@ -243,7 +243,7 @@
(p.add_price_display != null ? p.add_price_display : p.add_price) +
" · 新止损 " +
(p.new_sl_display != null ? p.new_sl_display : p.new_stop_loss);
if (!confirm("确认提交「" + modeLabel + "」\n" + summary)) {
if (!confirm("确认提交「" + modeLabel + "」?\n" + summary)) {
return;
}
submitRollForm(form);
@@ -254,7 +254,7 @@
submitBtn.disabled = false;
submitBtn.removeAttribute("disabled");
}
showReject("校验请求失败请稍后重试");
showReject("校验请求失败,请稍后重试");
});
}
@@ -263,7 +263,7 @@
if (submitBtn) submitBtn.disabled = true;
if (countdownEl) {
countdownEl.style.display = "block";
countdownEl.textContent = "市价加仓" + left + " 秒后可执行修改表单将取消预览";
countdownEl.textContent = "市价加仓:" + left + " 秒后可执行(修改表单将取消预览)";
}
countdownTimer = setInterval(function () {
left -= 1;
@@ -274,7 +274,7 @@
syncSubmitButton();
return;
}
if (countdownEl) countdownEl.textContent = "市价加仓" + left + " 秒后可执行";
if (countdownEl) countdownEl.textContent = "市价加仓:" + left + " 秒后可执行";
}, 1000);
}
@@ -303,7 +303,7 @@
return;
}
const modeLabel = modeSel.options[modeSel.selectedIndex].text;
if (!confirm("确认提交「" + modeLabel + "」")) {
if (!confirm("确认提交「" + modeLabel + "」?")) {
return;
}
submitRollForm(form);
+169 -169
View File
@@ -1,169 +1,169 @@
/**
* 表单币种输入防抖 + 定时刷新展示交易所最新价/api/order_defaults
*/
(function (global) {
"use strict";
const DEFAULT_DEBOUNCE_MS = 350;
const DEFAULT_POLL_MS = 5000;
const bound = new WeakSet();
function $(id) {
return id ? document.getElementById(id) : null;
}
function symbolValue(el) {
if (!el) return "";
return (el.value || "").trim();
}
function directionValue(dirId) {
const el = dirId ? $(dirId) : null;
const v = (el && el.value ? el.value : "long").trim().toLowerCase();
return v === "short" ? "short" : "long";
}
function formatPrice(px, sym) {
const n = Number(px);
if (!Number.isFinite(n)) return "—";
const u = (sym || "").trim().toUpperCase();
let digits = 4;
if (u.startsWith("BTC") || u.startsWith("ETH") || n >= 1000) digits = 2;
else if (n >= 10) digits = 3;
else if (n >= 1) digits = 4;
else if (n >= 0.01) digits = 5;
else digits = 6;
return n.toFixed(digits);
}
function pollMs() {
const raw =
(document.body && document.body.getAttribute("data-price-refresh-ms")) || "";
const n = Number(raw);
return Number.isFinite(n) && n >= 2000 ? n : DEFAULT_POLL_MS;
}
function paint(el, sym, px, err) {
if (!el) return;
if (err) {
el.textContent = "现价—";
el.classList.add("symbol-live-price--err");
el.classList.remove("symbol-live-price--ok");
el.title = err;
return;
}
if (px === null || typeof px === "undefined") {
el.textContent = "现价—";
el.classList.remove("symbol-live-price--ok", "symbol-live-price--err");
el.title = sym ? "无法读取交易所价格" : "";
return;
}
const label = sym ? sym.toUpperCase().replace(/\/USDT.*/, "") : "";
el.textContent = label ? label + " 现价 " + formatPrice(px, sym) : "现价 " + formatPrice(px, sym);
el.classList.add("symbol-live-price--ok");
el.classList.remove("symbol-live-price--err");
el.title = "交易所最新价约 " + pollMs() / 1000 + "s 刷新";
}
function bindOne(el) {
if (!el || bound.has(el)) return;
bound.add(el);
const symId = el.getAttribute("data-symbol-input");
const dirId = el.getAttribute("data-direction-input") || "";
let debounceTimer = null;
let pollTimer = null;
let fetchSeq = 0;
function clearPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPoll() {
clearPoll();
pollTimer = setInterval(refresh, pollMs());
}
function refresh() {
const symEl = $(symId);
const sym = symbolValue(symEl);
if (!sym) {
paint(el, "", null, "");
clearPoll();
return;
}
const dir = directionValue(dirId);
const seq = ++fetchSeq;
el.classList.add("symbol-live-price--loading");
fetch(
"/api/order_defaults?symbol=" +
encodeURIComponent(sym) +
"&direction=" +
encodeURIComponent(dir)
)
.then(function (r) {
return r.json().then(function (d) {
return { status: r.status, data: d };
}).catch(function () {
return { status: r.status, data: null };
});
})
.then(function (res) {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
const data = res.data || {};
if (res.status >= 400 || !data || !data.ok) {
paint(el, sym, null, (data && data.msg) || "读取失败");
return;
}
const px = data.last_price != null ? data.last_price : data.price;
if (px === null || typeof px === "undefined") {
paint(el, data.symbol || sym, null, "无法读取交易所价格");
return;
}
paint(el, data.symbol || sym, px, "");
if (!pollTimer) startPoll();
})
.catch(function () {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
paint(el, sym, null, "网络错误");
});
}
function schedule() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(refresh, DEFAULT_DEBOUNCE_MS);
}
const symEl = $(symId);
if (symEl) {
symEl.addEventListener("input", schedule);
symEl.addEventListener("change", schedule);
}
const dirEl = dirId ? $(dirId) : null;
if (dirEl) {
dirEl.addEventListener("change", schedule);
}
schedule();
}
function init(root) {
const scope = root || document;
scope.querySelectorAll(".symbol-live-price").forEach(bindOne);
}
global.SymbolLivePrice = { init: init, bind: bindOne };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
init(document);
});
} else {
init(document);
}
})(typeof window !== "undefined" ? window : globalThis);
/**
* 表单币种输入:防抖 + 定时刷新,展示交易所最新价(/api/order_defaults).
*/
(function (global) {
"use strict";
const DEFAULT_DEBOUNCE_MS = 350;
const DEFAULT_POLL_MS = 5000;
const bound = new WeakSet();
function $(id) {
return id ? document.getElementById(id) : null;
}
function symbolValue(el) {
if (!el) return "";
return (el.value || "").trim();
}
function directionValue(dirId) {
const el = dirId ? $(dirId) : null;
const v = (el && el.value ? el.value : "long").trim().toLowerCase();
return v === "short" ? "short" : "long";
}
function formatPrice(px, sym) {
const n = Number(px);
if (!Number.isFinite(n)) return "—";
const u = (sym || "").trim().toUpperCase();
let digits = 4;
if (u.startsWith("BTC") || u.startsWith("ETH") || n >= 1000) digits = 2;
else if (n >= 10) digits = 3;
else if (n >= 1) digits = 4;
else if (n >= 0.01) digits = 5;
else digits = 6;
return n.toFixed(digits);
}
function pollMs() {
const raw =
(document.body && document.body.getAttribute("data-price-refresh-ms")) || "";
const n = Number(raw);
return Number.isFinite(n) && n >= 2000 ? n : DEFAULT_POLL_MS;
}
function paint(el, sym, px, err) {
if (!el) return;
if (err) {
el.textContent = "现价:—";
el.classList.add("symbol-live-price--err");
el.classList.remove("symbol-live-price--ok");
el.title = err;
return;
}
if (px === null || typeof px === "undefined") {
el.textContent = "现价:—";
el.classList.remove("symbol-live-price--ok", "symbol-live-price--err");
el.title = sym ? "无法读取交易所价格" : "";
return;
}
const label = sym ? sym.toUpperCase().replace(/\/USDT.*/, "") : "";
el.textContent = label ? label + " 现价 " + formatPrice(px, sym) : "现价 " + formatPrice(px, sym);
el.classList.add("symbol-live-price--ok");
el.classList.remove("symbol-live-price--err");
el.title = "交易所最新价(约 " + pollMs() / 1000 + "s 刷新)";
}
function bindOne(el) {
if (!el || bound.has(el)) return;
bound.add(el);
const symId = el.getAttribute("data-symbol-input");
const dirId = el.getAttribute("data-direction-input") || "";
let debounceTimer = null;
let pollTimer = null;
let fetchSeq = 0;
function clearPoll() {
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
}
function startPoll() {
clearPoll();
pollTimer = setInterval(refresh, pollMs());
}
function refresh() {
const symEl = $(symId);
const sym = symbolValue(symEl);
if (!sym) {
paint(el, "", null, "");
clearPoll();
return;
}
const dir = directionValue(dirId);
const seq = ++fetchSeq;
el.classList.add("symbol-live-price--loading");
fetch(
"/api/order_defaults?symbol=" +
encodeURIComponent(sym) +
"&direction=" +
encodeURIComponent(dir)
)
.then(function (r) {
return r.json().then(function (d) {
return { status: r.status, data: d };
}).catch(function () {
return { status: r.status, data: null };
});
})
.then(function (res) {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
const data = res.data || {};
if (res.status >= 400 || !data || !data.ok) {
paint(el, sym, null, (data && data.msg) || "读取失败");
return;
}
const px = data.last_price != null ? data.last_price : data.price;
if (px === null || typeof px === "undefined") {
paint(el, data.symbol || sym, null, "无法读取交易所价格");
return;
}
paint(el, data.symbol || sym, px, "");
if (!pollTimer) startPoll();
})
.catch(function () {
if (seq !== fetchSeq) return;
el.classList.remove("symbol-live-price--loading");
paint(el, sym, null, "网络错误");
});
}
function schedule() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(refresh, DEFAULT_DEBOUNCE_MS);
}
const symEl = $(symId);
if (symEl) {
symEl.addEventListener("input", schedule);
symEl.addEventListener("change", schedule);
}
const dirEl = dirId ? $(dirId) : null;
if (dirEl) {
dirEl.addEventListener("change", schedule);
}
schedule();
}
function init(root) {
const scope = root || document;
scope.querySelectorAll(".symbol-live-price").forEach(bindOne);
}
global.SymbolLivePrice = { init: init, bind: bindOne };
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", function () {
init(document);
});
} else {
init(document);
}
})(typeof window !== "undefined" ? window : globalThis);
+1 -1
View File
@@ -1,5 +1,5 @@
/**
* 时间平仓 + 整点强制清仓表单开关 + 持仓/顶栏倒计时
* 时间平仓 + 整点强制清仓:表单开关 + 持仓/顶栏倒计时.
*/
(function (global) {
"use strict";
+160 -160
View File
@@ -1,160 +1,160 @@
/* 交易日历内照明心 + 三所统计分析共用随 data-theme 浅/深切换 */
.trade-cal-wrap {
--trade-cal-wrap-bg: var(--inset-surface, rgba(0, 0, 0, 0.22));
--trade-cal-cell-bg: var(--section-surface, var(--inset-surface, rgba(0, 0, 0, 0.32)));
--trade-cal-cell-hover-bg: color-mix(in srgb, var(--accent, #6366f1) 12%, var(--trade-cal-cell-bg));
--trade-cal-cell-hover-border: color-mix(in srgb, var(--accent, #6366f1) 45%, transparent);
--trade-cal-selected-border: rgba(59, 130, 246, 0.85);
--trade-cal-selected-bg: color-mix(in srgb, #3b82f6 16%, var(--trade-cal-cell-bg));
--trade-cal-selected-shadow: rgba(59, 130, 246, 0.45);
--trade-cal-sick-bg: color-mix(in srgb, var(--red, #ef4444) 14%, var(--trade-cal-cell-bg));
--trade-cal-sick-border: color-mix(in srgb, var(--red, #ef4444) 55%, transparent);
--trade-cal-sick-shadow: color-mix(in srgb, var(--red, #ef4444) 45%, transparent);
--trade-cal-sick-tag-bg: color-mix(in srgb, var(--red, #ef4444) 25%, transparent);
--trade-cal-sick-tag-fg: color-mix(in srgb, var(--red, #ef4444) 70%, #fff);
--trade-cal-pos: var(--green, #22c55e);
--trade-cal-neg: var(--red, #ef4444);
margin-top: 4px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border-soft, rgba(120, 140, 200, 0.28));
background: var(--trade-cal-wrap-bg);
}
.stats-calendar-wrap {
margin-bottom: 14px;
}
.trade-cal-wrap button.trade-cal-cell {
background: var(--trade-cal-cell-bg) !important;
background-image: none !important;
border: 1px solid transparent;
padding: 4px 3px;
min-height: 68px;
width: 100%;
box-shadow: none;
line-height: 1.15;
font-size: inherit;
text-align: center;
}
.trade-cal-wrap button.trade-cal-cell:disabled {
opacity: 1;
cursor: default;
}
.trade-cal-wrap .trade-cal-head .btn,
.trade-cal-wrap .trade-cal-head button {
min-height: 0;
min-width: 34px;
padding: 4px 12px;
line-height: 1.2;
}
.trade-cal-head {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 8px;
}
.trade-cal-title {
font-size: 0.95rem;
font-weight: 600;
min-width: 120px;
text-align: center;
color: var(--text, #e8ecff);
}
.trade-cal-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 4px;
}
.trade-cal-wd {
text-align: center;
font-size: 0.72rem;
color: var(--muted, #8892b0);
}
.trade-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.trade-cal-cell {
min-height: 62px;
padding: 4px 3px;
border-radius: 8px;
border: 1px solid transparent;
background: var(--trade-cal-cell-bg);
color: inherit;
font: inherit;
cursor: default;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 2px;
}
.trade-cal-cell.has-trade {
cursor: pointer;
}
.trade-cal-wrap button.trade-cal-cell.has-trade:hover {
background: var(--trade-cal-cell-hover-bg) !important;
background-image: none !important;
border-color: var(--trade-cal-cell-hover-border);
}
.trade-cal-cell.is-selected {
border-color: var(--trade-cal-selected-border);
background: var(--trade-cal-selected-bg);
box-shadow: 0 0 0 2px var(--trade-cal-selected-shadow);
}
.trade-cal-cell.is-sick-day {
border-color: var(--trade-cal-sick-border);
background: var(--trade-cal-sick-bg);
}
.trade-cal-cell.is-sick-day.is-selected {
border-color: var(--trade-cal-selected-border);
background: color-mix(in srgb, #3b82f6 14%, var(--trade-cal-sick-bg));
box-shadow: 0 0 0 2px var(--trade-cal-selected-shadow);
}
.trade-cal-day-num {
font-size: 0.78rem;
font-weight: 600;
color: var(--text, #e8ecff);
}
.trade-cal-pnl {
font-size: 0.72rem;
font-weight: 600;
line-height: 1.1;
color: var(--text, #e8ecff);
}
.trade-cal-cell.pnl-pos .trade-cal-pnl {
color: var(--trade-cal-pos);
}
.trade-cal-cell.pnl-neg .trade-cal-pnl {
color: var(--trade-cal-neg);
}
.trade-cal-cnt {
font-size: 0.65rem;
color: var(--muted, #8892b0);
font-weight: 500;
}
.trade-cal-sick-tag {
font-size: 0.62rem;
padding: 1px 4px;
border-radius: 4px;
background: var(--trade-cal-sick-tag-bg);
color: var(--trade-cal-sick-tag-fg);
font-weight: 600;
}
.trade-cal-pad {
background: transparent;
border: none;
min-height: 0;
}
html[data-theme="light"] .trade-cal-wrap {
--trade-cal-wrap-bg: var(--inset-surface, #eef3f8);
--trade-cal-cell-bg: var(--section-surface, #f6f9fc);
--trade-cal-cell-hover-bg: color-mix(in srgb, var(--accent, #2563eb) 10%, #f6f9fc);
--trade-cal-selected-border: rgba(37, 99, 235, 0.75);
--trade-cal-selected-bg: color-mix(in srgb, #2563eb 12%, #f6f9fc);
--trade-cal-selected-shadow: rgba(37, 99, 235, 0.35);
--trade-cal-sick-tag-fg: #b91c1c;
}
/* 交易日历:内照明心 + 三所统计分析共用,随 data-theme 浅/深切换 */
.trade-cal-wrap {
--trade-cal-wrap-bg: var(--inset-surface, rgba(0, 0, 0, 0.22));
--trade-cal-cell-bg: var(--section-surface, var(--inset-surface, rgba(0, 0, 0, 0.32)));
--trade-cal-cell-hover-bg: color-mix(in srgb, var(--accent, #6366f1) 12%, var(--trade-cal-cell-bg));
--trade-cal-cell-hover-border: color-mix(in srgb, var(--accent, #6366f1) 45%, transparent);
--trade-cal-selected-border: rgba(59, 130, 246, 0.85);
--trade-cal-selected-bg: color-mix(in srgb, #3b82f6 16%, var(--trade-cal-cell-bg));
--trade-cal-selected-shadow: rgba(59, 130, 246, 0.45);
--trade-cal-sick-bg: color-mix(in srgb, var(--red, #ef4444) 14%, var(--trade-cal-cell-bg));
--trade-cal-sick-border: color-mix(in srgb, var(--red, #ef4444) 55%, transparent);
--trade-cal-sick-shadow: color-mix(in srgb, var(--red, #ef4444) 45%, transparent);
--trade-cal-sick-tag-bg: color-mix(in srgb, var(--red, #ef4444) 25%, transparent);
--trade-cal-sick-tag-fg: color-mix(in srgb, var(--red, #ef4444) 70%, #fff);
--trade-cal-pos: var(--green, #22c55e);
--trade-cal-neg: var(--red, #ef4444);
margin-top: 4px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border-soft, rgba(120, 140, 200, 0.28));
background: var(--trade-cal-wrap-bg);
}
.stats-calendar-wrap {
margin-bottom: 14px;
}
.trade-cal-wrap button.trade-cal-cell {
background: var(--trade-cal-cell-bg) !important;
background-image: none !important;
border: 1px solid transparent;
padding: 4px 3px;
min-height: 68px;
width: 100%;
box-shadow: none;
line-height: 1.15;
font-size: inherit;
text-align: center;
}
.trade-cal-wrap button.trade-cal-cell:disabled {
opacity: 1;
cursor: default;
}
.trade-cal-wrap .trade-cal-head .btn,
.trade-cal-wrap .trade-cal-head button {
min-height: 0;
min-width: 34px;
padding: 4px 12px;
line-height: 1.2;
}
.trade-cal-head {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 8px;
}
.trade-cal-title {
font-size: 0.95rem;
font-weight: 600;
min-width: 120px;
text-align: center;
color: var(--text, #e8ecff);
}
.trade-cal-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
margin-bottom: 4px;
}
.trade-cal-wd {
text-align: center;
font-size: 0.72rem;
color: var(--muted, #8892b0);
}
.trade-cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.trade-cal-cell {
min-height: 62px;
padding: 4px 3px;
border-radius: 8px;
border: 1px solid transparent;
background: var(--trade-cal-cell-bg);
color: inherit;
font: inherit;
cursor: default;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 2px;
}
.trade-cal-cell.has-trade {
cursor: pointer;
}
.trade-cal-wrap button.trade-cal-cell.has-trade:hover {
background: var(--trade-cal-cell-hover-bg) !important;
background-image: none !important;
border-color: var(--trade-cal-cell-hover-border);
}
.trade-cal-cell.is-selected {
border-color: var(--trade-cal-selected-border);
background: var(--trade-cal-selected-bg);
box-shadow: 0 0 0 2px var(--trade-cal-selected-shadow);
}
.trade-cal-cell.is-sick-day {
border-color: var(--trade-cal-sick-border);
background: var(--trade-cal-sick-bg);
}
.trade-cal-cell.is-sick-day.is-selected {
border-color: var(--trade-cal-selected-border);
background: color-mix(in srgb, #3b82f6 14%, var(--trade-cal-sick-bg));
box-shadow: 0 0 0 2px var(--trade-cal-selected-shadow);
}
.trade-cal-day-num {
font-size: 0.78rem;
font-weight: 600;
color: var(--text, #e8ecff);
}
.trade-cal-pnl {
font-size: 0.72rem;
font-weight: 600;
line-height: 1.1;
color: var(--text, #e8ecff);
}
.trade-cal-cell.pnl-pos .trade-cal-pnl {
color: var(--trade-cal-pos);
}
.trade-cal-cell.pnl-neg .trade-cal-pnl {
color: var(--trade-cal-neg);
}
.trade-cal-cnt {
font-size: 0.65rem;
color: var(--muted, #8892b0);
font-weight: 500;
}
.trade-cal-sick-tag {
font-size: 0.62rem;
padding: 1px 4px;
border-radius: 4px;
background: var(--trade-cal-sick-tag-bg);
color: var(--trade-cal-sick-tag-fg);
font-weight: 600;
}
.trade-cal-pad {
background: transparent;
border: none;
min-height: 0;
}
html[data-theme="light"] .trade-cal-wrap {
--trade-cal-wrap-bg: var(--inset-surface, #eef3f8);
--trade-cal-cell-bg: var(--section-surface, #f6f9fc);
--trade-cal-cell-hover-bg: color-mix(in srgb, var(--accent, #2563eb) 10%, #f6f9fc);
--trade-cal-selected-border: rgba(37, 99, 235, 0.75);
--trade-cal-selected-bg: color-mix(in srgb, #2563eb 12%, #f6f9fc);
--trade-cal-selected-shadow: rgba(37, 99, 235, 0.35);
--trade-cal-sick-tag-fg: #b91c1c;
}
+314 -314
View File
@@ -1,314 +1,314 @@
/**
* 交易日历组件内照明心档案 + 三所统计分析共用
*/
(function (global) {
"use strict";
var WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"];
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function monthLabel(y, m) {
return y + "年" + m + "月";
}
function formatCalPnl(pnl) {
var n = Number(pnl);
if (!Number.isFinite(n)) n = 0;
return (n >= 0 ? "+" : "") + n.toFixed(1) + "U";
}
function dayHasTrade(info) {
if (!info) return false;
var cnt = Number(info.open_count);
if (Number.isFinite(cnt) && cnt > 0) return true;
var pnl = Number(info.pnl_total);
return Number.isFinite(pnl) && Math.abs(pnl) > 0.0001;
}
function dayOpenCount(info) {
var cnt = Number(info && info.open_count);
return Number.isFinite(cnt) && cnt > 0 ? cnt : 0;
}
function dayPnl(info) {
return Number(info && info.pnl_total) || 0;
}
function TradeStatsCalendar(config) {
this.gridEl = config.gridEl;
this.titleEl = config.titleEl;
this.prevBtn = config.prevBtn || null;
this.nextBtn = config.nextBtn || null;
this.apiUrl = config.apiUrl || "/api/stats/calendar";
this.buildQuery =
config.buildQuery ||
function (year, month) {
var q = new URLSearchParams();
q.set("year", String(year));
q.set("month", String(month));
return q;
};
this.parseResponse =
config.parseResponse ||
function (data) {
if (data && data.ok === false) return {};
return (data && data.days) || {};
};
this.fetchFn = config.fetchFn || null;
this.showSick = config.showSick !== false;
this.selectedDay = config.selectedDay || "";
this.onDayClick = config.onDayClick || null;
this.onMonthChange = config.onMonthChange || null;
this.year = config.year || 0;
this.month = config.month || 0;
this.days = {};
this.monthPnlTotal = 0;
this.monthOpenCount = 0;
this._navBound = false;
this._bindNav();
}
TradeStatsCalendar.prototype.ensureMonth = function (ref) {
if (this.year > 0 && this.month > 0) return;
var d;
if (ref instanceof Date) d = ref;
else if (typeof ref === "string" && ref.length >= 7) {
var p = ref.slice(0, 10).split("-");
this.year = parseInt(p[0], 10) || new Date().getFullYear();
this.month = parseInt(p[1], 10) || new Date().getMonth() + 1;
return;
} else d = new Date();
this.year = d.getFullYear();
this.month = d.getMonth() + 1;
};
TradeStatsCalendar.prototype.applyPayload = function (data) {
if (!data) return;
var y = Number(data.year);
var m = Number(data.month);
if (Number.isFinite(y) && y > 0) this.year = y;
if (Number.isFinite(m) && m > 0) this.month = m;
this.days = this.parseResponse(data) || {};
this.monthPnlTotal = Number(data.month_pnl_total) || 0;
this.monthOpenCount = Number(data.month_open_count) || 0;
if (!this.monthOpenCount) {
var self = this;
Object.keys(this.days).forEach(function (k) {
if (dayHasTrade(self.days[k])) {
self.monthOpenCount += dayOpenCount(self.days[k]);
self.monthPnlTotal += dayPnl(self.days[k]);
}
});
this.monthPnlTotal = Math.round(this.monthPnlTotal * 10000) / 10000;
}
};
function readStatsCalendarBootstrap() {
var el = document.getElementById("stats-calendar-bootstrap");
if (!el || !el.textContent) return null;
try {
return JSON.parse(el.textContent);
} catch (e) {
console.warn("[trade calendar] bootstrap parse", e);
return null;
}
}
TradeStatsCalendar.prototype.setSelectedDay = function (day) {
this.selectedDay = day || "";
this.render();
};
TradeStatsCalendar.prototype.render = function () {
if (!this.gridEl || !this.titleEl) return;
if (this.year <= 0 || this.month <= 0) this.ensureMonth(new Date());
var title = monthLabel(this.year, this.month);
if (this.monthOpenCount > 0) {
title +=
" · " + formatCalPnl(this.monthPnlTotal) + " · " + this.monthOpenCount + "笔";
}
this.titleEl.textContent = title;
var first = new Date(this.year, this.month - 1, 1);
var lastDay = new Date(this.year, this.month, 0).getDate();
var startWd = first.getDay();
var html =
'<div class="trade-cal-weekdays">' +
WEEKDAYS.map(function (w) {
return '<span class="trade-cal-wd">' + w + "</span>";
}).join("") +
'</div><div class="trade-cal-grid">';
var i;
for (i = 0; i < startWd; i++) {
html += '<span class="trade-cal-cell trade-cal-pad"></span>';
}
for (var d = 1; d <= lastDay; d++) {
var dayStr =
this.year +
"-" +
String(this.month).padStart(2, "0") +
"-" +
String(d).padStart(2, "0");
var info = this.days[dayStr];
var hasTrade = dayHasTrade(info);
var sick = this.showSick && info && info.has_sick;
var pnl = hasTrade ? dayPnl(info) : null;
var cnt = hasTrade ? dayOpenCount(info) : 0;
var cls =
"trade-cal-cell" +
(hasTrade ? " has-trade" : "") +
(sick ? " is-sick-day" : "") +
(this.selectedDay === dayStr ? " is-selected" : "") +
(pnl != null && pnl > 0.0001
? " pnl-pos"
: pnl != null && pnl < -0.0001
? " pnl-neg"
: "");
var body = '<span class="trade-cal-day-num">' + d + "</span>";
if (hasTrade) {
body +=
'<span class="trade-cal-pnl">' +
esc(formatCalPnl(pnl)) +
"</span>" +
'<span class="trade-cal-cnt">' +
cnt +
"笔</span>";
if (sick) body += '<span class="trade-cal-sick-tag">犯病</span>';
}
html +=
'<button type="button" class="' +
cls +
'" data-day="' +
dayStr +
'" data-sick="' +
(sick ? "1" : "0") +
'"' +
(hasTrade ? "" : " disabled") +
">" +
body +
"</button>";
}
html += "</div>";
this.gridEl.innerHTML = html;
var self = this;
this.gridEl.querySelectorAll(".trade-cal-cell[data-day]").forEach(function (btn) {
btn.addEventListener("click", function () {
var day = btn.getAttribute("data-day");
if (!day || !self.onDayClick) return;
self.selectedDay = day;
self.render();
self.onDayClick(day, btn.getAttribute("data-sick") === "1", self.days[day] || null);
});
});
};
TradeStatsCalendar.prototype.load = async function () {
this.ensureMonth(new Date());
this.render();
var q = this.buildQuery(this.year, this.month);
if (!q.has("year")) q.set("year", String(this.year));
if (!q.has("month")) q.set("month", String(this.month));
try {
var data;
if (this.fetchFn) {
data = await this.fetchFn(q);
} else {
var resp = await fetch(this.apiUrl + "?" + q.toString(), {
credentials: "same-origin",
});
if (!resp.ok) {
console.warn("[trade calendar] api", resp.status);
this.render();
return;
}
data = await resp.json();
}
this.applyPayload(data);
this.render();
if (this.onMonthChange) this.onMonthChange(this.year, this.month, this.days);
} catch (e) {
console.warn("[trade calendar]", e);
this.render();
}
};
TradeStatsCalendar.prototype.shiftMonth = function (delta) {
this.ensureMonth(new Date());
this.month += delta;
if (this.month > 12) {
this.month = 1;
this.year += 1;
} else if (this.month < 1) {
this.month = 12;
this.year -= 1;
}
void this.load();
};
TradeStatsCalendar.prototype._bindNav = function () {
if (this._navBound) return;
var self = this;
if (this.prevBtn) {
this.prevBtn.addEventListener("click", function () {
self.shiftMonth(-1);
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener("click", function () {
self.shiftMonth(1);
});
}
this._navBound = true;
};
global.TradeStatsCalendar = TradeStatsCalendar;
global.statsCalendarWidget = null;
global.initInstanceStatsCalendar = function () {
var grid = document.getElementById("stats-calendar");
if (!grid || !global.TradeStatsCalendar) return null;
var bootstrap = readStatsCalendarBootstrap();
if (
global.statsCalendarWidget &&
global.statsCalendarWidget.gridEl === grid
) {
if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap);
global.statsCalendarWidget.render();
void global.statsCalendarWidget.load();
return global.statsCalendarWidget;
}
global.statsCalendarWidget = new TradeStatsCalendar({
gridEl: grid,
titleEl: document.getElementById("stats-cal-title"),
prevBtn: document.getElementById("stats-cal-prev"),
nextBtn: document.getElementById("stats-cal-next"),
apiUrl: "/api/stats/calendar",
showSick: false,
buildQuery: function (year, month) {
var q = new URLSearchParams();
q.set("year", String(year));
q.set("month", String(month));
var sel = document.getElementById("stats-segment-select");
if (sel) q.set("segment", sel.value || "all");
return q;
},
parseResponse: function (data) {
if (data && data.ok === false) return {};
return (data && data.days) || {};
},
});
if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap);
global.statsCalendarWidget.render();
void global.statsCalendarWidget.load();
return global.statsCalendarWidget;
};
global.initStatsCalendarWidget = global.initInstanceStatsCalendar;
})(window);
/**
* 交易日历组件:内照明心档案 + 三所统计分析共用.
*/
(function (global) {
"use strict";
var WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"];
function esc(s) {
return String(s == null ? "" : s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function monthLabel(y, m) {
return y + "年" + m + "月";
}
function formatCalPnl(pnl) {
var n = Number(pnl);
if (!Number.isFinite(n)) n = 0;
return (n >= 0 ? "+" : "") + n.toFixed(1) + "U";
}
function dayHasTrade(info) {
if (!info) return false;
var cnt = Number(info.open_count);
if (Number.isFinite(cnt) && cnt > 0) return true;
var pnl = Number(info.pnl_total);
return Number.isFinite(pnl) && Math.abs(pnl) > 0.0001;
}
function dayOpenCount(info) {
var cnt = Number(info && info.open_count);
return Number.isFinite(cnt) && cnt > 0 ? cnt : 0;
}
function dayPnl(info) {
return Number(info && info.pnl_total) || 0;
}
function TradeStatsCalendar(config) {
this.gridEl = config.gridEl;
this.titleEl = config.titleEl;
this.prevBtn = config.prevBtn || null;
this.nextBtn = config.nextBtn || null;
this.apiUrl = config.apiUrl || "/api/stats/calendar";
this.buildQuery =
config.buildQuery ||
function (year, month) {
var q = new URLSearchParams();
q.set("year", String(year));
q.set("month", String(month));
return q;
};
this.parseResponse =
config.parseResponse ||
function (data) {
if (data && data.ok === false) return {};
return (data && data.days) || {};
};
this.fetchFn = config.fetchFn || null;
this.showSick = config.showSick !== false;
this.selectedDay = config.selectedDay || "";
this.onDayClick = config.onDayClick || null;
this.onMonthChange = config.onMonthChange || null;
this.year = config.year || 0;
this.month = config.month || 0;
this.days = {};
this.monthPnlTotal = 0;
this.monthOpenCount = 0;
this._navBound = false;
this._bindNav();
}
TradeStatsCalendar.prototype.ensureMonth = function (ref) {
if (this.year > 0 && this.month > 0) return;
var d;
if (ref instanceof Date) d = ref;
else if (typeof ref === "string" && ref.length >= 7) {
var p = ref.slice(0, 10).split("-");
this.year = parseInt(p[0], 10) || new Date().getFullYear();
this.month = parseInt(p[1], 10) || new Date().getMonth() + 1;
return;
} else d = new Date();
this.year = d.getFullYear();
this.month = d.getMonth() + 1;
};
TradeStatsCalendar.prototype.applyPayload = function (data) {
if (!data) return;
var y = Number(data.year);
var m = Number(data.month);
if (Number.isFinite(y) && y > 0) this.year = y;
if (Number.isFinite(m) && m > 0) this.month = m;
this.days = this.parseResponse(data) || {};
this.monthPnlTotal = Number(data.month_pnl_total) || 0;
this.monthOpenCount = Number(data.month_open_count) || 0;
if (!this.monthOpenCount) {
var self = this;
Object.keys(this.days).forEach(function (k) {
if (dayHasTrade(self.days[k])) {
self.monthOpenCount += dayOpenCount(self.days[k]);
self.monthPnlTotal += dayPnl(self.days[k]);
}
});
this.monthPnlTotal = Math.round(this.monthPnlTotal * 10000) / 10000;
}
};
function readStatsCalendarBootstrap() {
var el = document.getElementById("stats-calendar-bootstrap");
if (!el || !el.textContent) return null;
try {
return JSON.parse(el.textContent);
} catch (e) {
console.warn("[trade calendar] bootstrap parse", e);
return null;
}
}
TradeStatsCalendar.prototype.setSelectedDay = function (day) {
this.selectedDay = day || "";
this.render();
};
TradeStatsCalendar.prototype.render = function () {
if (!this.gridEl || !this.titleEl) return;
if (this.year <= 0 || this.month <= 0) this.ensureMonth(new Date());
var title = monthLabel(this.year, this.month);
if (this.monthOpenCount > 0) {
title +=
" · " + formatCalPnl(this.monthPnlTotal) + " · " + this.monthOpenCount + "笔";
}
this.titleEl.textContent = title;
var first = new Date(this.year, this.month - 1, 1);
var lastDay = new Date(this.year, this.month, 0).getDate();
var startWd = first.getDay();
var html =
'<div class="trade-cal-weekdays">' +
WEEKDAYS.map(function (w) {
return '<span class="trade-cal-wd">' + w + "</span>";
}).join("") +
'</div><div class="trade-cal-grid">';
var i;
for (i = 0; i < startWd; i++) {
html += '<span class="trade-cal-cell trade-cal-pad"></span>';
}
for (var d = 1; d <= lastDay; d++) {
var dayStr =
this.year +
"-" +
String(this.month).padStart(2, "0") +
"-" +
String(d).padStart(2, "0");
var info = this.days[dayStr];
var hasTrade = dayHasTrade(info);
var sick = this.showSick && info && info.has_sick;
var pnl = hasTrade ? dayPnl(info) : null;
var cnt = hasTrade ? dayOpenCount(info) : 0;
var cls =
"trade-cal-cell" +
(hasTrade ? " has-trade" : "") +
(sick ? " is-sick-day" : "") +
(this.selectedDay === dayStr ? " is-selected" : "") +
(pnl != null && pnl > 0.0001
? " pnl-pos"
: pnl != null && pnl < -0.0001
? " pnl-neg"
: "");
var body = '<span class="trade-cal-day-num">' + d + "</span>";
if (hasTrade) {
body +=
'<span class="trade-cal-pnl">' +
esc(formatCalPnl(pnl)) +
"</span>" +
'<span class="trade-cal-cnt">' +
cnt +
"笔</span>";
if (sick) body += '<span class="trade-cal-sick-tag">犯病</span>';
}
html +=
'<button type="button" class="' +
cls +
'" data-day="' +
dayStr +
'" data-sick="' +
(sick ? "1" : "0") +
'"' +
(hasTrade ? "" : " disabled") +
">" +
body +
"</button>";
}
html += "</div>";
this.gridEl.innerHTML = html;
var self = this;
this.gridEl.querySelectorAll(".trade-cal-cell[data-day]").forEach(function (btn) {
btn.addEventListener("click", function () {
var day = btn.getAttribute("data-day");
if (!day || !self.onDayClick) return;
self.selectedDay = day;
self.render();
self.onDayClick(day, btn.getAttribute("data-sick") === "1", self.days[day] || null);
});
});
};
TradeStatsCalendar.prototype.load = async function () {
this.ensureMonth(new Date());
this.render();
var q = this.buildQuery(this.year, this.month);
if (!q.has("year")) q.set("year", String(this.year));
if (!q.has("month")) q.set("month", String(this.month));
try {
var data;
if (this.fetchFn) {
data = await this.fetchFn(q);
} else {
var resp = await fetch(this.apiUrl + "?" + q.toString(), {
credentials: "same-origin",
});
if (!resp.ok) {
console.warn("[trade calendar] api", resp.status);
this.render();
return;
}
data = await resp.json();
}
this.applyPayload(data);
this.render();
if (this.onMonthChange) this.onMonthChange(this.year, this.month, this.days);
} catch (e) {
console.warn("[trade calendar]", e);
this.render();
}
};
TradeStatsCalendar.prototype.shiftMonth = function (delta) {
this.ensureMonth(new Date());
this.month += delta;
if (this.month > 12) {
this.month = 1;
this.year += 1;
} else if (this.month < 1) {
this.month = 12;
this.year -= 1;
}
void this.load();
};
TradeStatsCalendar.prototype._bindNav = function () {
if (this._navBound) return;
var self = this;
if (this.prevBtn) {
this.prevBtn.addEventListener("click", function () {
self.shiftMonth(-1);
});
}
if (this.nextBtn) {
this.nextBtn.addEventListener("click", function () {
self.shiftMonth(1);
});
}
this._navBound = true;
};
global.TradeStatsCalendar = TradeStatsCalendar;
global.statsCalendarWidget = null;
global.initInstanceStatsCalendar = function () {
var grid = document.getElementById("stats-calendar");
if (!grid || !global.TradeStatsCalendar) return null;
var bootstrap = readStatsCalendarBootstrap();
if (
global.statsCalendarWidget &&
global.statsCalendarWidget.gridEl === grid
) {
if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap);
global.statsCalendarWidget.render();
void global.statsCalendarWidget.load();
return global.statsCalendarWidget;
}
global.statsCalendarWidget = new TradeStatsCalendar({
gridEl: grid,
titleEl: document.getElementById("stats-cal-title"),
prevBtn: document.getElementById("stats-cal-prev"),
nextBtn: document.getElementById("stats-cal-next"),
apiUrl: "/api/stats/calendar",
showSick: false,
buildQuery: function (year, month) {
var q = new URLSearchParams();
q.set("year", String(year));
q.set("month", String(month));
var sel = document.getElementById("stats-segment-select");
if (sel) q.set("segment", sel.value || "all");
return q;
},
parseResponse: function (data) {
if (data && data.ok === false) return {};
return (data && data.days) || {};
},
});
if (bootstrap) global.statsCalendarWidget.applyPayload(bootstrap);
global.statsCalendarWidget.render();
void global.statsCalendarWidget.load();
return global.statsCalendarWidget;
};
global.initStatsCalendarWidget = global.initInstanceStatsCalendar;
})(window);
+14 -14
View File
@@ -1,4 +1,4 @@
"""企业微信机器人 Webhook 推送多实例共用)。"""
"""企业微信机器人 Webhook 推送(多实例共用)."""
from __future__ import annotations
import re
@@ -69,10 +69,10 @@ def send_wechat_webhook(
def wechat_direction_label(direction: str) -> str:
d = (direction or "").strip().lower()
if d == "long":
return "多头long"
return "多头(long)"
if d == "short":
return "空头short"
return "双向watch"
return "空头(short)"
return "双向(watch)"
def build_wechat_rs_level_message(
@@ -92,23 +92,23 @@ def build_wechat_rs_level_message(
interval_min: int,
extra_note: Optional[str] = None,
) -> str:
"""阻力/支撑突破提醒与开平仓推送一致的 emoji 纯文本风格)。"""
"""阻力/支撑突破提醒(与开平仓推送一致的 emoji 纯文本风格)."""
head = "📈" if (direction or "").strip().lower() == "long" else "📉"
dir_txt = wechat_direction_label(direction)
lines = [
f"{head} {symbol} 关键位突破提醒{notify_index}/{notify_max}",
f"💼 账户{account_label}",
f"{head} {symbol} 关键位突破提醒({notify_index}/{notify_max})",
f"💼 账户:{account_label}",
"",
"🧾 突破概要",
f"📌 类型{monitor_type}",
f"⏱ 触发时间{trigger_time}",
f"📊 上沿{upper_txt}|下沿{lower_txt}",
f"💹 触发收盘{close_txt}",
f"🎯 {break_label}{dir_txt}",
f"📍 突破价位{edge_txt}",
f"📌 类型:{monitor_type}",
f"⏱ 触发时间:{trigger_time}",
f"📊 上沿:{upper_txt}|下沿:{lower_txt}",
f"💹 触发收盘:{close_txt}",
f"🎯 {break_label}({dir_txt})",
f"📍 突破价位:{edge_txt}",
"",
"📎 说明",
f"· 人工盯盘共推送 {notify_max}间隔约 {interval_min} 分钟",
f"· 人工盯盘,共推送 {notify_max}(间隔约 {interval_min} 分钟)",
"· 推送完毕后本条监控自动结案",
"· 不参与自动开仓",
]

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