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:
+70
-70
@@ -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 项 |
|
||||
|
||||
@@ -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+、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 与本文冲突,以 **子目录《部署文档》与当前代码** 为准。
|
||||
# 复盘交易系统(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 与本文冲突,以 **子目录《部署文档》与当前代码** 为准.
|
||||
|
||||
@@ -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 线周期,如 5m、15m;仅影响关键位硬条件,不改变顶栏分区
|
||||
# 【周期】门控 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
|
||||
# 本机 Ollama(AI_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
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# crypto_monitor_binance
|
||||
|
||||
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Binance(USDT-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 用户协议.
|
||||
|
||||
+388
-388
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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`.
|
||||
|
||||
+192
-192
@@ -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 满足条件)。
|
||||
- 第 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 高外侧% / **E−H** |
|
||||
| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.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` |
|
||||
# 关键位监控说明(自动开仓 + 人工盯盘)
|
||||
|
||||
**适用: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 高外侧% / **E−H** |
|
||||
| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.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
@@ -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: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 时间窗限制。
|
||||
|
||||
## 实盘下单页
|
||||
|
||||
- 左列:实盘下单监控(表单、划转、规则)。
|
||||
- 右列:实时持仓(独立模块)。
|
||||
- **人工开仓门控**:计划盈亏比 < `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 时间窗限制.
|
||||
|
||||
## 实盘下单页
|
||||
|
||||
- 左列:实盘下单监控(表单,划转,规则).
|
||||
- 右列:实时持仓(独立模块).
|
||||
- **人工开仓门控**:计划盈亏比 < `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
@@ -1,389 +1,389 @@
|
||||
# `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='保本止盈'`。
|
||||
# `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='保本止盈'`.
|
||||
|
||||
@@ -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_orders,close-*-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 线周期,如 5m、15m
|
||||
# 【周期】门控 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
|
||||
|
||||
@@ -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)** | Ubuntu、PM2、**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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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 满足条件)。
|
||||
- 第 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 高外侧% / **E−H** |
|
||||
| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.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` |
|
||||
# 关键位监控说明(自动开仓 + 人工盯盘)
|
||||
|
||||
**适用: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 高外侧% / **E−H** |
|
||||
| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.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
@@ -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: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 时间窗限制。
|
||||
|
||||
## 实盘下单页
|
||||
|
||||
- 左列:实盘下单监控(表单、划转、规则)。
|
||||
- 右列:实时持仓(独立模块)。
|
||||
- **人工开仓门控**:计划盈亏比 < `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 时间窗限制.
|
||||
|
||||
## 实盘下单页
|
||||
|
||||
- 左列:实盘下单监控(表单,划转,规则).
|
||||
- 右列:实时持仓(独立模块).
|
||||
- **人工开仓门控**:计划盈亏比 < `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
@@ -1,305 +1,305 @@
|
||||
# `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='保本止盈'`。
|
||||
# `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='保本止盈'`.
|
||||
|
||||
@@ -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
|
||||
# 仓位查询 instType(OKX)
|
||||
# 仓位查询 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 线周期,如 5m、15m;仅影响关键位硬条件,不改变顶栏分区
|
||||
# 【周期】门控 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
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
# crypto_monitor_okx
|
||||
|
||||
基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **OKX(USDT 永续)**,通过 **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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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 满足条件)。
|
||||
- 第 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 高外侧% / **E−H** |
|
||||
| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.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` |
|
||||
# 关键位监控说明(自动开仓 + 人工盯盘)
|
||||
|
||||
**适用: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 高外侧% / **E−H** |
|
||||
| 箱体 1R·止盈 1.5H | `box_1p5` | **E−H** / **E+1.5×H** | **E+H** / **E−1.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
@@ -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) |
|
||||
| 作用范围 | 关键位历史、交易记录列表、复盘 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×(H−L)`;做空 `E = L + ratio×(H−L)`;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×(H−L)`;做空 `E = L + ratio×(H−L)`;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
@@ -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 开一个本地 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 再执行”的一键命令。
|
||||
# `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
@@ -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 中).
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# 按推荐顺序启动三所 Flask + 中控 hub/三 agent(PM2)。
|
||||
# 用法(仓库根或任意目录):
|
||||
# 按推荐顺序启动三所 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
|
||||
|
||||
@@ -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
@@ -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 启动三所 + hub(setup_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
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -1,45 +1,45 @@
|
||||
# 每日自动划转(三所统一)
|
||||
|
||||
## 行为
|
||||
|
||||
在 `.env` 开启 `AUTO_TRANSFER_ENABLED=true` 后,监控轮询在**北京时间 `AUTO_TRANSFER_BJ_HOUR` 整点所在小时**内(默认 8:00–8:59)执行一次(按 **UTC 自然日** 去重):
|
||||
|
||||
| 交易账户 (`AUTO_TRANSFER_TO`,默认 swap) | 动作 |
|
||||
|------------------------------------------|------|
|
||||
| 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`(默认 funding)划入差额 |
|
||||
| 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` |
|
||||
| 与目标相差 < 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:00–8:59)执行一次(按 **UTC 自然日** 去重):
|
||||
|
||||
| 交易账户 (`AUTO_TRANSFER_TO`,默认 swap) | 动作 |
|
||||
|------------------------------------------|------|
|
||||
| 余额 **低于** `AUTO_TRANSFER_AMOUNT` | 从 `AUTO_TRANSFER_FROM`(默认 funding)划入差额 |
|
||||
| 余额 **高于** `AUTO_TRANSFER_AMOUNT` | 将多余划回 `AUTO_TRANSFER_FROM` |
|
||||
| 与目标相差 < 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
@@ -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
@@ -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
|
||||
# 补全缺失项(默认 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)
|
||||
# 三所 `.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
@@ -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
@@ -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`(含首尾交易日) |
|
||||
|
||||
## 区间统计(统计栏)
|
||||
|
||||
基于当前 **列表筛选结果**(含盈利/亏损/犯病勾选、合约搜索;交易所下拉仍限定数据源):
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| 总开仓次数 | 区间内开仓笔数 |
|
||||
| 盈利单 / 亏损单 | 盈亏 > 0 / < 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`(含首尾交易日) |
|
||||
|
||||
## 区间统计(统计栏)
|
||||
|
||||
基于当前 **列表筛选结果**(含盈利/亏损/犯病勾选,合约搜索;交易所下拉仍限定数据源):
|
||||
|
||||
| 指标 | 说明 |
|
||||
|------|------|
|
||||
| 总开仓次数 | 区间内开仓笔数 |
|
||||
| 盈利单 / 亏损单 | 盈亏 > 0 / < 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
@@ -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/`** | 中控 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)
|
||||
# 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
@@ -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. 每月在金十/日历查看 **FOMC、CPI、非农** 公布时间
|
||||
1. 每月在金十/日历查看 **FOMC,CPI,非农** 公布时间
|
||||
2. 中控 **系统设置 → 宏观关键数据** 录入 1~3 条
|
||||
3. 到点前后监控区顶栏出现 **宏观风控** 横幅;无操作则窗口结束后自动消失
|
||||
3. 到点前后监控区顶栏出现 **宏观风控** 横幅;无操作则窗口结束后自动消失
|
||||
|
||||
## 与账户风控的关系
|
||||
|
||||
| 模块 | 时机 | 作用 |
|
||||
|------|------|------|
|
||||
| 宏观日历 | **事前** | 已知高波动窗口,提醒等待或管仓 |
|
||||
| 宏观日历 | **事前** | 已知高波动窗口,提醒等待或管仓 |
|
||||
| 账户冷静期/日冻结 | **事后** | 用户主动平仓后的惩罚性限制 |
|
||||
|
||||
宏观提醒 **不触发** 冷静期、不计入手动平仓次数。
|
||||
宏观提醒 **不触发** 冷静期,不计入手动平仓次数.
|
||||
|
||||
@@ -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` 口径一致
|
||||
|
||||
@@ -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
@@ -1,41 +1,41 @@
|
||||
# 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`。
|
||||
# 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
@@ -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)
|
||||
|
||||
@@ -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 杠杆(与开仓类型无关)
|
||||
|
||||
| 币种 | 默认杠杆 |
|
||||
|------|----------|
|
||||
| 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 | 讨论稿 |
|
||||
# 币安山寨·多头趋势账户
|
||||
|
||||
> **状态**: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 | 讨论稿 |
|
||||
|
||||
@@ -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": [
|
||||
"主升已确立,上方仍有空间(非跌后背离筑底阶段)",
|
||||
"大分歧A:5m/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 收敛与 A2(B 失败后 5m N 字)。"
|
||||
"v0.4:两级 UI 已实现;启动 A 含 A1 收敛与 A2(B 失败后 5m N 字)."
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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=强制清仓)"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
"主趋势已确立(非涨/跌后背离筑底/筑顶阶段)",
|
||||
"做多大分歧A:5m/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
@@ -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:00~12:00 内),**计划内第 1 笔** |
|
||||
| 下午 | **默认不开新仓**(持仓可保留至晚窗) |
|
||||
| 晚窗 | 约 **21:00**(20:00~23:00 内),**计划内第 2 笔** |
|
||||
| 第 3 笔 | 仅当 **未连错 2 笔**,且 **早/晚有一笔为止损出场**,可 **补 1 笔** |
|
||||
| 早窗 | 约 **9:00**(8:00~12:00 内),**计划内第 1 笔** |
|
||||
| 下午 | **默认不开新仓**(持仓可保留至晚窗) |
|
||||
| 晚窗 | 约 **21:00**(20:00~23: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:00~00:59)执行 |
|
||||
| `FORCE_CLOSE_BJ_HOUR=0` | 北京时间 **0 点那一小时**(00:00~00: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
@@ -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 次到高点附近 → 开战
|
||||
├─ 见小收敛 → 启动 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 短标签 |
|
||||
# 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 短标签 |
|
||||
|
||||
@@ -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 # 需含 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(与对话中修复项同步)*
|
||||
# 趋势回调:中控平仓与交易记录(检阅备忘)
|
||||
|
||||
本文档汇总 **中控手动结束趋势计划**,**交易记录 / 策略记录** 写入规则,以及 **三所展示统一**,**补仓表计价** 相关修复,便于自行检阅与排错.
|
||||
|
||||
适用仓库:`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(与对话中修复项同步)*
|
||||
|
||||
@@ -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` | 单次拉取平仓历史条数上限(50–1000) | `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` | 单次拉取平仓历史条数上限(50–1000) | `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
@@ -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 + 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、端口、反代 |
|
||||
# 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
@@ -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
@@ -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:**资金账户 / 交易账户**,分币种显示 USDT、USDC、USDG
|
||||
- 永续/关键位:继续走 **子账户 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 展示(期权页顶栏)
|
||||
|
||||
| 账户 | 币种 |
|
||||
|------|------|
|
||||
| 资金账户 | USDT、USDC(若有) |
|
||||
| 交易账户 | USDT、USDC、USDG(若有) |
|
||||
| 资金账户 | 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
@@ -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. 选择 **到期日**(默认仅 1~2 日)
|
||||
3. 选择 **到期日**(默认仅 1~2 日)
|
||||
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 ETH(50 张)→ 权利金 ≈ 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
@@ -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
@@ -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节至少包含:**得分**(1–10)、**依据**(对应记录字段)
|
||||
- 第5节至少包含:**趋势确认**、**执行路径**(记录不足则写明)
|
||||
- 语气简洁,少形容词;不要输出代码块、不要表格
|
||||
- 每节正文用 `- **子项名**:内容` 列表;第4节改进建议用有序列表 `1. 2. 3.`
|
||||
- 第1节至少包含:**笔数/盈亏**,**风险回报比**,**总结**
|
||||
- 第2节至少包含:**得分**(1–10),**依据**(对应记录字段)
|
||||
- 第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
@@ -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.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
|
||||
"""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
|
||||
|
||||
@@ -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()}"
|
||||
)
|
||||
|
||||
@@ -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
@@ -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(空格格式)比会误判为超出上界。
|
||||
单列时不用 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)
|
||||
"""列表/导出用 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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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,5 +1,5 @@
|
||||
/**
|
||||
* 实盘/关键位放大 K 线:交易所 tick 精度、主题感知图表、高对比 meta。
|
||||
* 实盘/关键位放大 K 线:交易所 tick 精度,主题感知图表,高对比 meta.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
@@ -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 || "提交中…";
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
/**
|
||||
* embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM,切换 tab 不再重复请求 HTML。
|
||||
* embed 壳:SSE 收到后台 tick 后拉 JSON 快照更新 DOM,切换 tab 不再重复请求 HTML.
|
||||
*/
|
||||
(function (global) {
|
||||
let liveEventSource = null;
|
||||
|
||||
+240
-240
@@ -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,5 +1,5 @@
|
||||
/**
|
||||
* 手机端:交易记录 / 复盘记录紧凑列表(币种 · 方向 · 盈亏),点击展开详情。
|
||||
* 手机端:交易记录 / 复盘记录紧凑列表(币种 · 方向 · 盈亏),点击展开详情.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
@@ -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, "密码已更新");
|
||||
}
|
||||
|
||||
+3494
-3494
File diff suppressed because it is too large
Load Diff
+566
-566
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
/* 紧接 instance_theme.js 之后加载,避免亮色下先闪暗色底 */
|
||||
/* 紧接 instance_theme.js 之后加载,避免亮色下先闪暗色底 */
|
||||
html {
|
||||
background: #0b0d14;
|
||||
color-scheme: dark;
|
||||
|
||||
+417
-417
@@ -1,417 +1,417 @@
|
||||
/**
|
||||
* 三所实例共用 UI:复盘详情、盈亏着色等。
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
/**
|
||||
* 时间平仓 + 整点强制清仓:表单开关 + 持仓/顶栏倒计时。
|
||||
* 时间平仓 + 整点强制清仓:表单开关 + 持仓/顶栏倒计时.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,314 +1,314 @@
|
||||
/**
|
||||
* 交易日历组件:内照明心档案 + 三所统计分析共用。
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var WEEKDAYS = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? "" : s)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user