From 16ef0f31e38b42e3687322570051bb7b1d069df0 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 17 Jul 2026 16:30:58 +0800 Subject: [PATCH] User edition: prune docs, license gate, obfuscate core lib. Keep deploy/basic docs only; integrate sq.bz121.com license client; encrypt strategy/trade/key_monitor/options/hedge_plan for release. Co-authored-by: Cursor --- .gitignore | 11 +- AI复盘与模型配置说明.md | 70 - README.md | 88 +- crypto_monitor_binance/.env.example | 5 + crypto_monitor_binance/README.md | 83 +- crypto_monitor_binance/app.py | 6 + crypto_monitor_binance/crypto.db | Bin 0 -> 114688 bytes crypto_monitor_binance/使用说明.md | 139 -- crypto_monitor_binance/关键位自动下单说明.md | 192 -- crypto_monitor_binance/更新文档.md | 147 -- crypto_monitor_gate/.env.example | 5 + crypto_monitor_gate/README.md | 89 +- crypto_monitor_gate/app.py | 6 + crypto_monitor_gate/crypto.db | Bin 0 -> 122880 bytes crypto_monitor_gate/使用说明.md | 147 -- crypto_monitor_gate/关键位自动下单说明.md | 192 -- crypto_monitor_gate/更新文档.md | 148 -- crypto_monitor_okx/.env.example | 5 + crypto_monitor_okx/README.md | 52 +- crypto_monitor_okx/app.py | 6 + crypto_monitor_okx/crypto.db | Bin 0 -> 233472 bytes crypto_monitor_okx/使用说明.md | 138 -- crypto_monitor_okx/关键位自动下单说明.md | 192 -- crypto_monitor_okx/更新文档.md | 98 - deploy/README.md | 2 + docs/account-risk-cooldown.md | 130 -- docs/auto-transfer-daily.md | 45 - docs/daily-open-limit.md | 81 - docs/hub-symbol-archive-kline.md | 135 -- docs/lib-structure.md | 147 -- docs/license.md | 38 + docs/macro-calendar.md | 83 - docs/manual-order-rr-preview.md | 31 - docs/position-sizing-mode.md | 73 - docs/shortcut-icon.md | 45 - docs/strategy/README.md | 34 - docs/strategy/binance-alt-trend-long.md | 194 -- docs/strategy/checklists/binance.json | 68 - docs/strategy/checklists/gate.json | 38 - docs/strategy/checklists/okx.json | 72 - docs/strategy/gate-intraday.md | 277 --- docs/strategy/okx-trend-both.md | 157 -- docs/trend-hub-close-and-trade-records.md | 184 -- docs/trend-pullback-strategy.md | 129 -- docs/ubuntu-server.md | 3 +- docs/中控AI与密钥配置.md | 103 - docs/对冲计划开发方案.md | 678 ------ docs/对冲计划策略与P0校验.md | 109 - docs/更新文档.md | 464 ---- docs/期权对冲方案分析.md | 322 --- docs/期权开平仓与监控说明.md | 93 - docs/期权方案.md | 151 -- docs/期权用法.md | 149 -- docs/系统设置说明.md | 168 -- lib/hedge_plan/__init__.py | 13 +- lib/hedge_plan/hedge_plan_calc_lib.py | 397 +--- lib/hedge_plan/hedge_plan_db.py | 385 +--- lib/hedge_plan/hedge_plan_monitor_lib.py | 426 +--- lib/hedge_plan/hedge_plan_notify_lib.py | 198 +- lib/hedge_plan/hedge_plan_orders_lib.py | 441 +--- lib/hedge_plan/hedge_plan_register.py | 828 +------ lib/hedge_plan/hedge_plan_settle_lib.py | 74 +- lib/key_monitor/__init__.py | 13 +- .../false_breakout_key_monitor_lib.py | 157 +- lib/key_monitor/fib_key_monitor_lib.py | 152 +- lib/key_monitor/key_auto_order_lib.py | 176 +- .../key_monitor_full_margin_lib.py | 73 +- lib/key_monitor/key_monitor_lib.py | 402 +--- lib/key_monitor/key_monitor_schema_lib.py | 27 +- lib/key_monitor/key_sl_tp_lib.py | 151 +- .../trigger_entry_key_monitor_lib.py | 336 +-- lib/license/__init__.py | 19 + lib/license/fastapi_gate.py | 115 + lib/license/flask_gate.py | 88 + lib/license/license_lib.py | 393 ++++ lib/license/templates/license.html | 100 + lib/options/__init__.py | 12 + lib/options/options_close_exec_lib.py | 392 +--- lib/options/options_close_gate_lib.py | 196 +- lib/options/options_db.py | 146 +- lib/options/options_history_lib.py | 98 +- lib/options/options_hub_lib.py | 116 +- lib/options/options_monitor_lib.py | 346 +-- lib/options/options_pending_lib.py | 136 +- lib/options/options_positions_lib.py | 167 +- lib/options/options_pricing_lib.py | 555 +---- lib/options/options_register.py | 1295 +---------- lib/options/options_review_db.py | 155 +- lib/options/options_review_images_lib.py | 150 +- lib/options/options_review_lib.py | 983 +------- lib/options/options_review_register.py | 291 +-- lib/options/options_stats_lib.py | 185 +- lib/options/options_target_lib.py | 455 +--- lib/strategy/__init__.py | 13 +- lib/strategy/strategy_config.py | 244 +- lib/strategy/strategy_db.py | 176 +- lib/strategy/strategy_exchange_base.py | 60 +- lib/strategy/strategy_exchange_binance.py | 16 +- lib/strategy/strategy_exchange_gate.py | 21 +- lib/strategy/strategy_exchange_okx.py | 16 +- lib/strategy/strategy_records_register.py | 84 +- lib/strategy/strategy_register.py | 666 +----- lib/strategy/strategy_roll_lib.py | 404 +--- lib/strategy/strategy_roll_monitor_lib.py | 574 +---- lib/strategy/strategy_roll_ui_lib.py | 446 +--- lib/strategy/strategy_snapshot_lib.py | 541 +---- lib/strategy/strategy_trade_labels.py | 204 +- lib/strategy/strategy_trend_exchange.py | 109 +- lib/strategy/strategy_trend_lib.py | 713 +----- lib/strategy/strategy_trend_register.py | 1984 +---------------- lib/strategy/strategy_ui.py | 155 +- lib/strategy/strategy_wechat_notify.py | 204 +- lib/trade/__init__.py | 13 +- lib/trade/account_risk_lib.py | 857 +------ lib/trade/compensating_close_lib.py | 28 +- lib/trade/daily_open_limit_lib.py | 152 +- lib/trade/entry_model_lib.py | 521 +---- lib/trade/force_close_lib.py | 332 +-- lib/trade/manual_sltp_lib.py | 148 +- lib/trade/order_monitor_display_lib.py | 464 +--- lib/trade/position_sizing_lib.py | 148 +- lib/trade/time_close_lib.py | 162 +- lib/trade/trade_exchange_stats_lib.py | 241 +- lib/trade/trade_fee_lib.py | 106 +- lib/trade/trade_policy_app_lib.py | 64 +- lib/trade/trade_policy_lib.py | 217 +- lib/trade/trade_result_lib.py | 73 +- lib/trade/trade_stats_calendar_lib.py | 127 +- license.env.example | 7 + manual_trading_hub/.env.example | 5 + manual_trading_hub/AI教练说明.md | 66 - manual_trading_hub/README.md | 109 +- manual_trading_hub/SNAPSHOT_ROLLBACK.md | 22 - manual_trading_hub/docs/help/01-quickstart.md | 34 - manual_trading_hub/docs/help/02-hub-nav.md | 26 - manual_trading_hub/docs/help/03-monitor.md | 43 - manual_trading_hub/docs/help/04-instance.md | 30 - manual_trading_hub/docs/help/05-settings.md | 38 - manual_trading_hub/hub.py | 7 + manual_trading_hub/scripts/后台运行-Ubuntu.md | 42 - manual_trading_hub/云服务器部署说明.md | 289 --- manual_trading_hub/交易监管说明.md | 84 - manual_trading_hub/使用说明.md | 518 +---- manual_trading_hub/局域网与反代部署说明.md | 226 -- manual_trading_hub/开仓计划说明.md | 85 - manual_trading_hub/数据看板说明.md | 50 - manual_trading_hub/本地数据迁移到云端.md | 268 --- manual_trading_hub/行情区说明.md | 130 -- manual_trading_hub/资金概况说明.md | 94 - scripts/obfuscate_release.py | 206 ++ tests/test_license_lib.py | 88 + 关键位止盈止损与移动保本更新说明.md | 164 -- 策略交易说明.md | 160 -- 顺势加仓滚仓说明.md | 174 -- 154 files changed, 1998 insertions(+), 27914 deletions(-) delete mode 100644 AI复盘与模型配置说明.md create mode 100644 crypto_monitor_binance/crypto.db delete mode 100644 crypto_monitor_binance/使用说明.md delete mode 100644 crypto_monitor_binance/关键位自动下单说明.md delete mode 100644 crypto_monitor_binance/更新文档.md create mode 100644 crypto_monitor_gate/crypto.db delete mode 100644 crypto_monitor_gate/使用说明.md delete mode 100644 crypto_monitor_gate/关键位自动下单说明.md delete mode 100644 crypto_monitor_gate/更新文档.md create mode 100644 crypto_monitor_okx/crypto.db delete mode 100644 crypto_monitor_okx/使用说明.md delete mode 100644 crypto_monitor_okx/关键位自动下单说明.md delete mode 100644 crypto_monitor_okx/更新文档.md delete mode 100644 docs/account-risk-cooldown.md delete mode 100644 docs/auto-transfer-daily.md delete mode 100644 docs/daily-open-limit.md delete mode 100644 docs/hub-symbol-archive-kline.md delete mode 100644 docs/lib-structure.md create mode 100644 docs/license.md delete mode 100644 docs/macro-calendar.md delete mode 100644 docs/manual-order-rr-preview.md delete mode 100644 docs/position-sizing-mode.md delete mode 100644 docs/shortcut-icon.md delete mode 100644 docs/strategy/README.md delete mode 100644 docs/strategy/binance-alt-trend-long.md delete mode 100644 docs/strategy/checklists/binance.json delete mode 100644 docs/strategy/checklists/gate.json delete mode 100644 docs/strategy/checklists/okx.json delete mode 100644 docs/strategy/gate-intraday.md delete mode 100644 docs/strategy/okx-trend-both.md delete mode 100644 docs/trend-hub-close-and-trade-records.md delete mode 100644 docs/trend-pullback-strategy.md delete mode 100644 docs/中控AI与密钥配置.md delete mode 100644 docs/对冲计划开发方案.md delete mode 100644 docs/对冲计划策略与P0校验.md delete mode 100644 docs/更新文档.md delete mode 100644 docs/期权对冲方案分析.md delete mode 100644 docs/期权开平仓与监控说明.md delete mode 100644 docs/期权方案.md delete mode 100644 docs/期权用法.md delete mode 100644 docs/系统设置说明.md create mode 100644 lib/license/__init__.py create mode 100644 lib/license/fastapi_gate.py create mode 100644 lib/license/flask_gate.py create mode 100644 lib/license/license_lib.py create mode 100644 lib/license/templates/license.html create mode 100644 license.env.example delete mode 100644 manual_trading_hub/AI教练说明.md delete mode 100644 manual_trading_hub/SNAPSHOT_ROLLBACK.md delete mode 100644 manual_trading_hub/docs/help/01-quickstart.md delete mode 100644 manual_trading_hub/docs/help/02-hub-nav.md delete mode 100644 manual_trading_hub/docs/help/03-monitor.md delete mode 100644 manual_trading_hub/docs/help/04-instance.md delete mode 100644 manual_trading_hub/docs/help/05-settings.md delete mode 100644 manual_trading_hub/scripts/后台运行-Ubuntu.md delete mode 100644 manual_trading_hub/云服务器部署说明.md delete mode 100644 manual_trading_hub/交易监管说明.md delete mode 100644 manual_trading_hub/局域网与反代部署说明.md delete mode 100644 manual_trading_hub/开仓计划说明.md delete mode 100644 manual_trading_hub/数据看板说明.md delete mode 100644 manual_trading_hub/本地数据迁移到云端.md delete mode 100644 manual_trading_hub/行情区说明.md delete mode 100644 manual_trading_hub/资金概况说明.md create mode 100644 scripts/obfuscate_release.py create mode 100644 tests/test_license_lib.py delete mode 100644 关键位止盈止损与移动保本更新说明.md delete mode 100644 策略交易说明.md delete mode 100644 顺势加仓滚仓说明.md diff --git a/.gitignore b/.gitignore index 6b82791..e3a7b0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,16 @@ # 本地真实配置(含 API 密钥),勿提交 **/.env .env +license.env # 允许提交模板 !**/.env.example !.env.example +!license.env.example + +# 许可状态(运行时) +data/license_state.json +data/ # Python **/__pycache__/ @@ -25,6 +31,5 @@ manual_trading_hub/hub_ai_fund_history.json manual_trading_hub/data/ backups/ -# 数据库与上传(运行时生成) -**/*.sqlite -**/crypto.db +# 加密发布产物与明文备份(勿提交) +dist/ diff --git a/AI复盘与模型配置说明.md b/AI复盘与模型配置说明.md deleted file mode 100644 index 560ea28..0000000 --- a/AI复盘与模型配置说明.md +++ /dev/null @@ -1,70 +0,0 @@ -# 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 项 | diff --git a/README.md b/README.md index 02c690a..49434fb 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,53 @@ -# 复盘交易系统(crypto_monitor_user · 用户版) +# 复盘交易系统(crypto_monitor_user · 用户版) -多交易所 **USDT 永续** 的下单监控,**关键位**,**策略交易**,**止盈止损 / 移动保本** 与 **AI 复盘**,三所独立部署 + 可选 **中控** 聚合监控. +多交易所 USDT 永续监控与中控聚合。本仓库为**用户发行版**:含部署文档与基础说明;核心业务逻辑以加密形式交付,不以策略源码文档形式公开。 -**远程仓库**:[https://git.bz121.com/dekun/crypto_monitor_user.git](https://git.bz121.com/dekun/crypto_monitor_user.git) +**远程仓库**:[https://git.bz121.com/dekun/crypto_monitor_user.git](https://git.bz121.com/dekun/crypto_monitor_user.git) +**授权服务**:[https://sq.bz121.com/](https://sq.bz121.com/) · 说明见 [docs/license.md](./docs/license.md) --- -## 部署环境(必读) +## 部署环境 | 项 | 约定 | |----|------| -| 系统 | **Ubuntu 22.04 / 24.04** | -| 用户 | **root** | -| 路径 | **`/opt/crypto_monitor_user`** | -| 进程 | **PM2**(唯一推荐的常驻方式) | +| 系统 | Ubuntu 22.04 / 24.04 | +| 用户 | root | +| 路径 | `/opt/crypto_monitor_user` | +| 进程 | PM2 | -**环境详解**(Python 3.10+,Node,PM2 安装与启动顺序):**[docs/ubuntu-server.md](./docs/ubuntu-server.md)** -**一键部署管理器**:`curl -fsSL .../deploy/manage.sh | bash` → **[deploy/README.md](./deploy/README.md)** +详情:[docs/ubuntu-server.md](./docs/ubuntu-server.md) · [deploy/README.md](./deploy/README.md) ```bash -# 新服务器(推荐) curl -fsSL https://git.bz121.com/dekun/crypto_monitor_user/raw/branch/main/deploy/manage.sh | bash - -# 或手动 clone +# 或 cd /opt git clone https://git.bz121.com/dekun/crypto_monitor_user.git crypto_monitor_user cd /opt/crypto_monitor_user bash deploy/manage.sh ``` -配置与运维脚本: **[docs/env-sync-scripts.md](./docs/env-sync-scripts.md)** · **[备份与恢复.md](./备份与恢复.md)** +部署后请配置仓库根目录 **`license.env`**(从 `license.env.example` 复制),打开任意实例或中控的 **`/license`** 激活。 + +运维:[备份与恢复.md](./备份与恢复.md) · [docs/env配置说明.md](./docs/env配置说明.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) — 生产机备份习惯 +| 目录 | 说明 | +|------|------| +| `crypto_monitor_binance/` | Binance 实例 | +| `crypto_monitor_gate/` | Gate 实例 | +| `crypto_monitor_okx/` | OKX 实例 | +| `manual_trading_hub/` | 中控 | +| `lib/` | 共用模块(发行版核心逻辑已加密) | +| `deploy/` / `docs/` | 部署与基础文档 | --- ## 安全 -- **勿** 将 `.env`,API Secret,`.pem` 提交 Git -- 公网暴露中控须配置登录,`HUB_BRIDGE_TOKEN`,HTTPS Cookie +- 勿将 `.env`、`license.env`、API Secret 提交 Git +- 公网须改默认密码并配置 HTTPS - 实盘风险由使用者自行承担 - -若子目录 README 与本文冲突,以 **子目录《部署文档》与当前代码** 为准. diff --git a/crypto_monitor_binance/.env.example b/crypto_monitor_binance/.env.example index 46d99b9..becbb15 100644 --- a/crypto_monitor_binance/.env.example +++ b/crypto_monitor_binance/.env.example @@ -231,3 +231,8 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest APP_TIMEZONE=Asia/Shanghai # TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED + +# --- 软件授权(整机共用,也可写仓库根 license.env)--- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=与授权站 CLIENT_API_KEY 相同 +# LICENSE_OFFLINE_GRACE_HOURS=72 diff --git a/crypto_monitor_binance/README.md b/crypto_monitor_binance/README.md index f217cd7..d3d5820 100644 --- a/crypto_monitor_binance/README.md +++ b/crypto_monitor_binance/README.md @@ -1,83 +1,12 @@ -# crypto_monitor_binance +# crypto_monitor_binance(用户版) -基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Binance(USDT-M 永续)**,通过 **ccxt** 访问. +Binance USDT-M 永续实例。 -## 功能概要 - -- **关键位监控**:价格与硬条件校验,企业微信推送(可选) -- **下单监控**:本地风控(含移动保本逻辑),触达止盈/止损后尝试市价平仓并记账 -- **策略交易**:顶栏 `/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`** - -安装示例: - -```bash -# 推荐在 /opt/crypto_monitor_user 执行仓库根目录 deploy/setup_env.sh -cd /opt/crypto_monitor_user/crypto_monitor_binance -source .venv/bin/activate -pip install -r ../requirements.txt -``` - -页面上的 **「当日资金(交易账户)」** 与 **「可开仓」可用 U** 仅统计 **Binance U 本位永续合约账户**(`fetch_balance` 的 `swap` / FAPI `assets` 中的 USDT),**不会**再用现货余额顶替. - -## 配置说明(`.env.example` → `.env`) - -- **`.env.example`**:模板(可提交 Git);首次:`cp .env.example .env` 后编辑. -- **`.env`**:本机真实配置(勿提交);`app.py` 只读此文件.`git pull` 不覆盖 `.env`;升级前可 `cp .env .env.backup.$(date +%Y%m%d)`. - -与 Binance 相关的常用变量: - -| 变量 | 说明 | -|------|------| -| `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_ACCOUNT_LABEL` | 推送文案中的账户备注 | - -其余变量(登录,企业微信,风控参数,**`AI_PROVIDER` / `OPENAI_*` / `OLLAMA_*`**,数据库路径等)见 **`.env.example` 内注释** 或 `app.py` 顶部默认值. - -## 运行 - -生产环境使用 **PM2**(`ecosystem.config.cjs`).临时调试: +- **部署**:[部署文档.md](./部署文档.md) · 仓库根 [docs/ubuntu-server.md](../docs/ubuntu-server.md) +- **授权**:启动后打开 `/license`,见 [docs/license.md](../docs/license.md) +- **配置**:复制 `.env.example` → `.env`,填写 API 与登录信息 ```bash cd /opt/crypto_monitor_user/crypto_monitor_binance -source .venv/bin/activate -python app.py +# 推荐用仓库根 deploy/setup_env.sh 创建 .venv ``` - -环境说明见 [docs/ubuntu-server.md](../docs/ubuntu-server.md). - -默认监听端口由 `.env` 的 `APP_PORT` 决定(未设置时多为 `5000`). - -## 部署(Linux / PM2 / SSH SOCKS) - -详见 **[部署文档.md](./部署文档.md)**(Ubuntu + PM2 + 可选 SOCKS 访问 Binance). - -## 自检脚本 - -```bash -python scripts/verify_binance_funding.py -``` - -用于核对 Key 前缀(不含 Secret)并尝试读取资金钱包 / 合约钱包 USDT(需网络与 API 权限). - -## 数据与脚本 - -- 默认 SQLite:`crypto.db`(路径由 `DB_PATH` 指定) -- `scripts/fix_breakeven_labels.py`:批量修正「止损」但盈亏为正的记录标签(见部署文档附录) - -## 风险与合规 - -实盘交易有亏损风险.请自行确认 API 权限,IP 白名单,杠杆与保证金模式与币安账户设置一致,并遵守当地法律法规与 Binance 用户协议. diff --git a/crypto_monitor_binance/app.py b/crypto_monitor_binance/app.py index 07dc57d..4feed25 100644 --- a/crypto_monitor_binance/app.py +++ b/crypto_monitor_binance/app.py @@ -306,6 +306,12 @@ app.secret_key = os.getenv("FLASK_SECRET_KEY", "crypto_monitor_2026_secret_key") from lib.instance.instance_embed_lib import attach_embed_templates attach_embed_templates(app, _REPO_ROOT) +try: + from lib.license.flask_gate import install_license_gate + + install_license_gate(app) +except Exception as _lic_err: + print(f"[license] binance: {_lic_err}") # ====================== 登录配置 ====================== USERNAME = os.getenv("APP_USERNAME", "dekun") diff --git a/crypto_monitor_binance/crypto.db b/crypto_monitor_binance/crypto.db new file mode 100644 index 0000000000000000000000000000000000000000..76b114f5ebd16a04527766e855515158d6d8a23b GIT binary patch literal 114688 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 下沿(触价开仓填 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` | diff --git a/crypto_monitor_binance/更新文档.md b/crypto_monitor_binance/更新文档.md deleted file mode 100644 index 5289238..0000000 --- a/crypto_monitor_binance/更新文档.md +++ /dev/null @@ -1,147 +0,0 @@ -# 界面与风控更新说明(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 与订单监控是否正常;平仓后检查交易记录止损(开仓)与开仓类型. diff --git a/crypto_monitor_gate/.env.example b/crypto_monitor_gate/.env.example index b6371fb..b8b7818 100644 --- a/crypto_monitor_gate/.env.example +++ b/crypto_monitor_gate/.env.example @@ -231,3 +231,8 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest APP_TIMEZONE=Asia/Shanghai # TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED + +# --- 软件授权(整机共用,也可写仓库根 license.env)--- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=与授权站 CLIENT_API_KEY 相同 +# LICENSE_OFFLINE_GRACE_HOURS=72 diff --git a/crypto_monitor_gate/README.md b/crypto_monitor_gate/README.md index e74af0a..8fd7565 100644 --- a/crypto_monitor_gate/README.md +++ b/crypto_monitor_gate/README.md @@ -1,90 +1,11 @@ -# crypto_monitor_gate +# crypto_monitor_gate(用户版) -基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **Gate.io USDT 永续**,通过 **ccxt** 访问. +Gate USDT 永续实例。 -## 文档导航 - -| 文档 | 说明 | -|------|------| -| **[使用说明.md](./使用说明.md)** | 日常怎么用:登录,关键位四类,手工开仓,单仓与微信等 | -| **[关键位自动下单说明.md](./关键位自动下单说明.md)** | 关键位自动开仓的 RR,止盈止损,结案原因与 `.env` | -| **[部署文档.md](./部署文档.md)** | Ubuntu,PM2,**SSH SOCKS** 访问 Gate API 等 | - -另:**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` 等影响) - ---- - -## 环境要求 - -- Python 3.10+(建议) -- 依赖:`flask`,`requests`,`ccxt`,`werkzeug`,`PySocks`(经 SOCKS 代理时);`Pillow`(K 线导出等可选用) - -安装示例: +- **部署**:[部署文档.md](./部署文档.md) · 仓库根 [docs/ubuntu-server.md](../docs/ubuntu-server.md) +- **授权**:启动后打开 `/license`,见 [docs/license.md](../docs/license.md) +- **配置**:复制 `.env.example` → `.env` ```bash cd /opt/crypto_monitor_user/crypto_monitor_gate -source .venv/bin/activate -pip install -r ../requirements.txt ``` - -## 配置(`.env.example` → `.env`) - -- **`.env.example`**:模板(可提交 Git);首次:`cp .env.example .env` 后编辑. -- **`.env`**:本机真实配置(勿提交);`git pull` 不覆盖;升级前建议备份(见《部署文档》§5.2). - -项目启动时加载**仓库根目录**下的 `.env`.常用项: - -| 变量 | 说明 | -|------|------| -| `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 动态转发(详见部署文档) | -| `APP_PASSWORD` / `FLASK_SECRET_KEY` | Web 登录与 Session | -| `WECHAT_WEBHOOK` | 企业微信机器人 | -| `EXCHANGE_DISPLAY_NAME` / `GATE_ACCOUNT_LABEL` | 页面与推送展示的账户文案 | - -其余见 **`.env.example` 内注释** 或 **`app.py` 顶部默认值**. - -## 运行 - -生产使用 **PM2**(`ecosystem.config.cjs`).调试: - -```bash -source .venv/bin/activate && python app.py -``` - -见 [docs/ubuntu-server.md](../docs/ubuntu-server.md). - -端口由 **`APP_PORT`** 控制(未设置默认 **5000**).浏览器登录 **`/login`**,口令为 **`APP_PASSWORD`**. - -## 部署(Linux / PM2 / SSH SOCKS) - -见 **[部署文档.md](./部署文档.md)**. - -## 自检脚本 - -```bash -python scripts/verify_gate_funding.py -``` - -用于核对密钥前缀(不落 Secret),资金/合约可读性等(需网络与权限). - -## 数据与脚本 - -- 默认 SQLite:由 **`DB_PATH`** 指定(常见为项目下 `crypto.db`) -- `scripts/fix_breakeven_labels.py`:修正「止损」但盈亏为正的记录标签(参见部署文档说明) - -## 风险与合规 - -实盘有亏损风险.请确认 API 权限,IP 白名单,杠杆与保证金模式与 **Gate.io** 后台一致,并遵守当地法律法规与交易所用户协议. diff --git a/crypto_monitor_gate/app.py b/crypto_monitor_gate/app.py index fe54bdd..1ead05a 100644 --- a/crypto_monitor_gate/app.py +++ b/crypto_monitor_gate/app.py @@ -305,6 +305,12 @@ app.secret_key = os.getenv("FLASK_SECRET_KEY", "crypto_monitor_2026_secret_key") from lib.instance.instance_embed_lib import attach_embed_templates attach_embed_templates(app, _REPO_ROOT) +try: + from lib.license.flask_gate import install_license_gate + + install_license_gate(app) +except Exception as _lic_err: + print(f"[license] gate: {_lic_err}") # ====================== 登录配置 ====================== USERNAME = os.getenv("APP_USERNAME", "dekun") diff --git a/crypto_monitor_gate/crypto.db b/crypto_monitor_gate/crypto.db new file mode 100644 index 0000000000000000000000000000000000000000..f7499093e8473e06c9702c6a0e661137fa0f658f GIT binary patch literal 122880 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 O00JNY0w8dL2>dVJr7hb4 literal 0 HcmV?d00001 diff --git a/crypto_monitor_gate/使用说明.md b/crypto_monitor_gate/使用说明.md deleted file mode 100644 index 937e86e..0000000 --- a/crypto_monitor_gate/使用说明.md +++ /dev/null @@ -1,147 +0,0 @@ -# 使用说明 - -**本文件对应仓库:`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` 即可对照使用. diff --git a/crypto_monitor_gate/关键位自动下单说明.md b/crypto_monitor_gate/关键位自动下单说明.md deleted file mode 100644 index a8ca535..0000000 --- a/crypto_monitor_gate/关键位自动下单说明.md +++ /dev/null @@ -1,192 +0,0 @@ -# 关键位监控说明(自动开仓 + 人工盯盘) - -**适用: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` | diff --git a/crypto_monitor_gate/更新文档.md b/crypto_monitor_gate/更新文档.md deleted file mode 100644 index 158e370..0000000 --- a/crypto_monitor_gate/更新文档.md +++ /dev/null @@ -1,148 +0,0 @@ -# 界面与风控更新说明(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 已挂上;平仓后交易记录止损(开仓)与开仓类型是否正确. diff --git a/crypto_monitor_okx/.env.example b/crypto_monitor_okx/.env.example index 41b86d1..b31bff1 100644 --- a/crypto_monitor_okx/.env.example +++ b/crypto_monitor_okx/.env.example @@ -274,3 +274,8 @@ AI_MODEL=huihui_ai/deepseek-r1-abliterated:latest APP_TIMEZONE=Asia/Shanghai # TRADING_DAY_RESET_HOUR 现在表示「北京时间」整点,默认 8 点起算新交易日;开仓整点限制见 TRADING_DAY_RESET_OPEN_GUARD_ENABLED + +# --- 软件授权(整机共用,也可写仓库根 license.env)--- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=与授权站 CLIENT_API_KEY 相同 +# LICENSE_OFFLINE_GRACE_HOURS=72 diff --git a/crypto_monitor_okx/README.md b/crypto_monitor_okx/README.md index 3ac412c..c2db9b8 100644 --- a/crypto_monitor_okx/README.md +++ b/crypto_monitor_okx/README.md @@ -1,53 +1,11 @@ -# crypto_monitor_okx +# crypto_monitor_okx(用户版) -基于 **Flask** 的加密货币 **下单监控 / 关键位监控 / 交易复盘** 小系统,行情与实盘接口统一走 **OKX(USDT 永续)**,通过 **ccxt** 访问.功能与界面已与 **`crypto_monitor_binance`** 对齐(顶栏分栏,风控参数,交易所 TP/SL 管理等),差异主要在 **`.env` 的 `OKX_*` 变量** 与 OKX API(含 Passphrase). +OKX 永续实例。 -## 功能概要 - -- **关键位监控**:`/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` - -## 配置说明 - -| 变量 | 说明 | -|------|------| -| `OKX_API_KEY` / `OKX_API_SECRET` / `OKX_API_PASSPHRASE` | OKX API | -| `OKX_TD_MODE` / `OKX_POS_MODE` | 全仓/逐仓,单向/双向 | -| `OKX_SOCKS_PROXY` | 本机 SSH 动态转发时常用 | -| `MAX_ACTIVE_POSITIONS` / `MANUAL_MIN_PLANNED_RR` | 与币安版一致的风控 | -| `EXCHANGE_DISPLAY_NAME` | 页面展示名,默认 `OKX` | - -完整模板见 **`.env.example`**. - -## 运行 +- **部署**:[部署文档.md](./部署文档.md) · 仓库根 [docs/ubuntu-server.md](../docs/ubuntu-server.md) +- **授权**:启动后打开 `/license`,见 [docs/license.md](../docs/license.md) +- **配置**:复制 `.env.example` → `.env` ```bash cd /opt/crypto_monitor_user/crypto_monitor_okx -source .venv/bin/activate -python app.py ``` - -生产使用 **PM2**;见 [docs/ubuntu-server.md](../docs/ubuntu-server.md).默认 **`APP_PORT`** 常为 `5004`. - -## 部署 - -详见 **[部署文档.md](./部署文档.md)**,**[使用说明.md](./使用说明.md)**. - -## 自检 - -```bash -python scripts/verify_okx_funding.py -``` - -## 风险与合规 - -实盘风险自负;请确认 API 权限,IP 白名单与 OKX 账户设置一致. diff --git a/crypto_monitor_okx/app.py b/crypto_monitor_okx/app.py index 7a43a2d..73eaa2d 100644 --- a/crypto_monitor_okx/app.py +++ b/crypto_monitor_okx/app.py @@ -301,6 +301,12 @@ app.secret_key = os.getenv("FLASK_SECRET_KEY", "crypto_monitor_2026_secret_key") from lib.instance.instance_embed_lib import attach_embed_templates, redirect_to_embed_shell_if_enabled attach_embed_templates(app, _REPO_ROOT) +try: + from lib.license.flask_gate import install_license_gate + + install_license_gate(app) +except Exception as _lic_err: + print(f"[license] okx: {_lic_err}") # ====================== 登录配置 ====================== USERNAME = os.getenv("APP_USERNAME", "dekun") diff --git a/crypto_monitor_okx/crypto.db b/crypto_monitor_okx/crypto.db new file mode 100644 index 0000000000000000000000000000000000000000..51961cab3a51ac29e3bbddfd765cb2c74c433e7f GIT binary patch literal 233472 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 r{yzj8_D}SZedqlL8@6Iiv0*Eq** 下沿(触价开仓填 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` | diff --git a/crypto_monitor_okx/更新文档.md b/crypto_monitor_okx/更新文档.md deleted file mode 100644 index aaa80a8..0000000 --- a/crypto_monitor_okx/更新文档.md +++ /dev/null @@ -1,98 +0,0 @@ -# 界面与风控更新说明(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 监控进程**;旧库行不做批量回填,展示字段有则用之,无则回退. - ---- - -## 共享更新记录 - -自 2026-07-16 起,期权/对冲等共享逻辑的变更统一记在仓库根目录 **[docs/更新文档.md](../docs/更新文档.md)**(含原因、改动文件、目标、验收)。最新一条:期权/对冲开仓仅认真实卖一深度。 diff --git a/deploy/README.md b/deploy/README.md index fee7c32..de0ce8d 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -145,3 +145,5 @@ BACKUP_ROOT=/root/backups - 三个监控子项目共用根目录 **[requirements.txt](../requirements.txt)**. - 走 SOCKS 须 **PySocks**(已包含在 requirements 中). +- **用户版核心包**(`lib/strategy` 等)为加密发行;本地开发若需重新加密,见 `scripts/obfuscate_release.py`。 +- **授权**:配置仓库根 `license.env`(见 `license.env.example`),对接 [https://sq.bz121.com/](https://sq.bz121.com/)。 diff --git a/docs/account-risk-cooldown.md b/docs/account-risk-cooldown.md deleted file mode 100644 index b34abb5..0000000 --- a/docs/account-risk-cooldown.md +++ /dev/null @@ -1,130 +0,0 @@ -# 账户冷静期 / 日冻结风控 - -三所实例(币安 / 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` diff --git a/docs/auto-transfer-daily.md b/docs/auto-transfer-daily.md deleted file mode 100644 index 3a6ff73..0000000 --- a/docs/auto-transfer-daily.md +++ /dev/null @@ -1,45 +0,0 @@ -# 每日自动划转(三所统一) - -## 行为 - -在 `.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 -``` diff --git a/docs/daily-open-limit.md b/docs/daily-open-limit.md deleted file mode 100644 index 0e92295..0000000 --- a/docs/daily-open-limit.md +++ /dev/null @@ -1,81 +0,0 @@ -# 单日开仓次数限制(三所统一) - -各交易实例(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` diff --git a/docs/hub-symbol-archive-kline.md b/docs/hub-symbol-archive-kline.md deleted file mode 100644 index c609050..0000000 --- a/docs/hub-symbol-archive-kline.md +++ /dev/null @@ -1,135 +0,0 @@ -# 内照明心与永久 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) diff --git a/docs/lib-structure.md b/docs/lib-structure.md deleted file mode 100644 index b241cd6..0000000 --- a/docs/lib-structure.md +++ /dev/null @@ -1,147 +0,0 @@ -# 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_user -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) diff --git a/docs/license.md b/docs/license.md new file mode 100644 index 0000000..c8cc08f --- /dev/null +++ b/docs/license.md @@ -0,0 +1,38 @@ +# 软件授权说明(用户版) + +授权服务:[https://sq.bz121.com/](https://sq.bz121.com/) + +## 激活流程 + +1. 部署并启动本系统(三所或中控任一页面)。 +2. 浏览器打开 **`/license`**(未授权时会自动跳转)。 +3. 复制页面上的 **设备 ID**。 +4. 联系微信 **dekun03** 购买套餐,获得激活码(新购 / 续费 / 换机)。 +5. 在 `/license` 粘贴激活码并兑换。 + +## 套餐(参考) + +| 套餐 | 天数 | +|------|------| +| 月卡 | 30 | +| 季卡 | 90 | +| 年卡 | 365 | + +续费:在原到期日基础上叠加天数。换机:旧设备立即失效,新设备需换机码。 + +## 环境变量 + +在仓库根目录创建 **`license.env`**(勿提交 Git),或写入各实例 `.env`: + +```env +LICENSE_API_URL=https://sq.bz121.com +LICENSE_CLIENT_KEY=与授权站 CLIENT_API_KEY 相同 +# 可选:断网宽限小时数(默认 72) +# LICENSE_OFFLINE_GRACE_HOURS=72 +# 本地调试可临时关闭门禁(生产勿开) +# LICENSE_DISABLED=false +``` + +许可状态保存在仓库根目录 **`data/license_state.json`**(三所 + 中控共用)。 + +系统约每 **3 天** 向授权站校验一次;明确过期或换机后旧设备将无法使用。 diff --git a/docs/macro-calendar.md b/docs/macro-calendar.md deleted file mode 100644 index 2803ac1..0000000 --- a/docs/macro-calendar.md +++ /dev/null @@ -1,83 +0,0 @@ -# 宏观关键数据 · 风控前置 - -中控 **系统设置** 手动录入 FOMC / CPI / 就业数据发布时间,在 **监控区** 发布前后各 1 小时给出风险提示. -**不看公布结果,不解读数据**,仅作波动窗口前的行为提醒;**不拦截下单**(与账户冷静期/日冻结独立). - -## 支持的数据类型 - -| 类型 ID | 显示名称 | -|---------|----------| -| `fomc` | FOMC 联邦基金利率 | -| `cpi` | 美国 CPI 通胀 | -| `employment` | 就业与劳工数据 | - -每项在设置中 **名称下拉三选一**,**发布时间** 手动输入(北京时间,精确到分钟).FOMC 只录 **一条**(决议公布时刻即可). - -## 风险窗口 - -- 默认:**发布时间 ±1 小时** -- 发布前 **30 分钟内**:文案加强为「即将发布」 -- 窗口结束后横幅自动消失;设置列表中过期记录逐步不再展示 - -环境变量(可选): - -```env -HUB_MACRO_WINDOW_BEFORE_SEC=3600 -HUB_MACRO_WINDOW_AFTER_SEC=3600 -HUB_MACRO_IMMINENT_BEFORE_SEC=1800 -HUB_MACRO_LIST_FUTURE_DAYS=60 -``` - -## 监控区提示文案 - -读取当前监控板:**任意交易所有持仓 = 有仓**,否则 = 无仓. - -| 场景 | 提示要点 | -|------|----------| -| 无仓 · 窗口内 | 建议等待,避免新开仓 | -| 有仓 · 窗口内 | 注意仓位,勿加仓,检查止损/减仓 | -| 即将发布(30 分钟内) | 在上述基础上标注剩余分钟数 | - -## 存储 - -- 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` -同类型 + 同一发布时间不可重复录入. - -## API(均需中控登录) - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/macro-calendar/meta` | 类型列表与窗口说明 | -| GET | `/api/macro-calendar/events` | 设置页列表 | -| GET | `/api/macro-calendar/active` | 当前处于窗口内的事件(监控横幅) | -| POST | `/api/macro-calendar/events` | 新增 | -| PATCH | `/api/macro-calendar/events/{id}` | 更新 | -| DELETE | `/api/macro-calendar/events/{id}` | 删除 | - -请求体示例: - -```json -{ - "event_type": "cpi", - "event_at": "2026-06-18 20:30", - "note": "可选备注" -} -``` - -## 使用习惯 - -1. 每月在金十/日历查看 **FOMC,CPI,非农** 公布时间 -2. 中控 **系统设置 → 宏观关键数据** 录入 1~3 条 -3. 到点前后监控区顶栏出现 **宏观风控** 横幅;无操作则窗口结束后自动消失 - -## 与账户风控的关系 - -| 模块 | 时机 | 作用 | -|------|------|------| -| 宏观日历 | **事前** | 已知高波动窗口,提醒等待或管仓 | -| 账户冷静期/日冻结 | **事后** | 用户主动平仓后的惩罚性限制 | - -宏观提醒 **不触发** 冷静期,不计入手动平仓次数. diff --git a/docs/manual-order-rr-preview.md b/docs/manual-order-rr-preview.md deleted file mode 100644 index 60c5acf..0000000 --- a/docs/manual-order-rr-preview.md +++ /dev/null @@ -1,31 +0,0 @@ -# 实盘下单 · 预估盈亏比 - -## 功能 - -三所(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` 口径一致 diff --git a/docs/position-sizing-mode.md b/docs/position-sizing-mode.md deleted file mode 100644 index 22a964f..0000000 --- a/docs/position-sizing-mode.md +++ /dev/null @@ -1,73 +0,0 @@ -# 计仓模式(三所统一) - -## 配置 - -在各实例 `.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 -``` diff --git a/docs/shortcut-icon.md b/docs/shortcut-icon.md deleted file mode 100644 index ca869cf..0000000 --- a/docs/shortcut-icon.md +++ /dev/null @@ -1,45 +0,0 @@ -# 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. 若都没有 → 灰色地球或网页标题首字 - -本仓库已在 **中控** 与 **三所监控页** 配置品牌图标.PNG/ICO 由 **Pillow** 生成. - -- **中控**:深色圆角底 + 青绿趋势线 + K 线,安装名「复盘系统中控」 -- **三所**:各所用交易所标识色与字标(币安菱形 / OKX 方块 / Gate G),安装名分别为「Binance 交易系统」「OKX 交易系统」「Gate 交易系统」 - -## 文件位置 - -| 位置 | 访问路径 | -|------|----------| -| 源稿 | `brand/icon.svg`,`brand/icons/*.png`,`brand/icons/{binance,okx,gate}/` | -| Manifest | `brand/manifest.webmanifest`(中控),`brand/manifest.{binance,okx,gate}.webmanifest` | -| 中控 | `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`. diff --git a/docs/strategy/README.md b/docs/strategy/README.md deleted file mode 100644 index 20f88b6..0000000 --- a/docs/strategy/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# 策略文档 - -各交易实例的人工下单策略,供 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) diff --git a/docs/strategy/binance-alt-trend-long.md b/docs/strategy/binance-alt-trend-long.md deleted file mode 100644 index 95bad40..0000000 --- a/docs/strategy/binance-alt-trend-long.md +++ /dev/null @@ -1,194 +0,0 @@ -# 币安山寨·多头趋势账户 - -> **状态**: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 | 讨论稿 | diff --git a/docs/strategy/checklists/binance.json b/docs/strategy/checklists/binance.json deleted file mode 100644 index 84560d9..0000000 --- a/docs/strategy/checklists/binance.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "exchange": "binance", - "title": "币安 · 开仓检查清单", - "version": "v0.4", - "groups": [ - { - "title": "账户与方向", - "items": [ - "本账户仅做多,不做空", - "计仓模式为以损定仓(risk),关键位自动单已关闭", - "已明确第一级:反转 / 顺势 / 波段(两级下拉已上线)" - ] - }, - { - "title": "反转 · 背离与箱体(启动 A/B 共同前置)", - "items": [ - "4h MACD 底背离(非首次背离,非通道式下跌中的背离)", - "有明显波段高/低点,且三段及以上明显下跌后才认背离", - "背离后处于震荡箱体(时长不量化;非 V 形急跌急拉)", - "已标注参考高点(最后一个显著高点)", - "第 1 次到参考高点附近 → 不做;第 2 次到附近 → 才进入作战区" - ] - }, - { - "title": "反转 · 启动 B(实体突破)", - "items": [ - "第 2 次到参考高点附近,且未见再次摸高前的小收敛", - "实体突破参考高点/箱体上沿(非仅影线)", - "突破失败回箱止损属正常;跌破箱高一半则暂弃直至再次到高点附近" - ] - }, - { - "title": "反转 · 启动 A(结构内,含 A1 / A2)", - "items": [ - "性质:主升确认前;不是小分歧", - "A1:第 2 次摸高前出现小收敛 → 突破前企稳", - "A2:启动 B 止损后,未跌破箱高一半 + 5m 不创新低 + 5m N 字突破试仓", - "5m 试仓记为启动 A(不单列类型);小止损允许多次试错" - ] - }, - { - "title": "顺势 · 大分歧 A / B", - "items": [ - "主升已确立,上方仍有空间(非跌后背离筑底阶段)", - "大分歧A:5m/15m 收敛且不创新低企稳", - "大分歧B:突破已确认,优先实体突破" - ] - }, - { - "title": "波段 · 小分歧", - "items": [ - "主升已确立;反转链进行中不做小分歧", - "第三次小分歧 → 不做新单", - "不追突破;二次探底 N 字或 5m 三均线重新多头" - ] - }, - { - "title": "杠杆与出场", - "items": [ - "杠杆:BTC/ETH 10x,其它山寨 5x(可选手改但须有理由)", - "止盈止损随行情人工设定,不在此清单量化 RR" - ] - } - ], - "footnotes": [ - "v0.4:两级 UI 已实现;启动 A 含 A1 收敛与 A2(B 失败后 5m N 字)." - ] -} diff --git a/docs/strategy/checklists/gate.json b/docs/strategy/checklists/gate.json deleted file mode 100644 index 01a0181..0000000 --- a/docs/strategy/checklists/gate.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "exchange": "gate", - "title": "Gate · BTC 日内 · 开仓检查清单", - "version": "v0.2", - "groups": [ - { - "title": "方向与均线过滤", - "items": [ - "仅交易 BTC,同时仅 1 仓", - "15m 21/55/144 排列清晰(多或空),纠缠则不做", - "1H 方向与 15m 不冲突", - "21 均线关系满足:回踩支撑 / 站稳上方(多)或反弹承压 / 压在下方(空)" - ] - }, - { - "title": "开仓类型 A / B", - "items": [ - "已选定:假破 或 结构突破(二选一)", - "假破:扫流动性后回到结构内,5m N 字 + 15m 顶/底分型齐全", - "结构突破:15m 收盘价站稳关键位,非仅刺破", - "止损带宽 0.4%~1.5%,超出则不做", - "下单前空间至少 1:1,不足则不做" - ] - }, - { - "title": "一日节奏与笔数", - "items": [ - "非周末;在早窗 / 晚窗计划时段内", - "今日笔数未达上限 3,连错未达 2 笔", - "宽幅震荡(S1)时降频或不做", - "0 点前须了结(系统强制清仓);本清单不含手动平仓" - ] - } - ], - "footnotes": [ - "系统:FORCE_CLOSE_ENABLED 开启时,北京时间 0 点自动强制清仓(result=强制清仓)" - ] -} diff --git a/docs/strategy/checklists/okx.json b/docs/strategy/checklists/okx.json deleted file mode 100644 index 7680011..0000000 --- a/docs/strategy/checklists/okx.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "exchange": "okx", - "title": "OKX · 开仓检查清单", - "version": "v0.4", - "groups": [ - { - "title": "账户与方向", - "items": [ - "已选定做多或做空,且与 4H/大级别结构方向一致", - "趋势户 profile(非 Gate 日内 BTC/ETH 白名单)", - "计仓模式为以损定仓(risk),关键位自动单已关闭", - "同一币种无未计划的对冲叠仓", - "已明确第一级:反转 / 顺势 / 波段(两级下拉已上线)" - ] - }, - { - "title": "反转 · 背离与箱体(启动 A/B 共同前置)", - "items": [ - "做多:4h MACD 底背离;做空:4h MACD 顶背离", - "非首次背离;非通道式涨跌中的背离;明显波段高低点 + 三段及以上涨/跌后才认背离", - "背离后处于震荡箱体(时长不量化;非 V 形急拉急杀)", - "做多:已标参考高点;做空:已标参考低点", - "第 1 次到参考极值附近 → 不做;第 2 次到附近 → 才进入作战区" - ] - }, - { - "title": "反转 · 启动 B(实体突破)", - "items": [ - "第 2 次到参考极值附近,且未见再次摸极值前的小收敛", - "做多:实体突破参考高点/箱顶;做空:实体跌破参考低点/箱底", - "突破失败回箱止损属正常", - "做多:跌破箱高一半暂弃;做空:涨破箱低一半暂弃;直至再次到极值附近" - ] - }, - { - "title": "反转 · 启动 A(结构内,含 A1 / A2)", - "items": [ - "性质:主趋势确认前;不是小分歧", - "A1:第 2 次摸极值前出现小收敛 → 突破前企稳", - "A2 做多:B 止损后未跌破箱一半 + 5m 不创新低 + 5m N 字", - "A2 做空:B 止损后未涨破箱一半 + 5m 不创新高 + 5m 倒 N 字", - "5m 试仓记为启动 A(不单列);小止损允许多次试错" - ] - }, - { - "title": "顺势 · 大分歧 A / B", - "items": [ - "主趋势已确立(非涨/跌后背离筑底/筑顶阶段)", - "做多大分歧A:5m/15m 收敛且不创新低;做空:不创新高", - "大分歧B:突破已确认,优先实体突破" - ] - }, - { - "title": "波段 · 小分歧", - "items": [ - "主趋势已确立;反转链进行中不做小分歧", - "第三次小分歧 → 不做新单", - "做多:二次探底 N 字 / 5m 三均线多头;做空:二次探顶倒 N / 5m 空头" - ] - }, - { - "title": "杠杆与出场", - "items": [ - "杠杆:BTC/ETH 10x,其它山寨 5x(可选手改但须有理由)", - "止盈止损随行情人工设定,不在此清单量化 RR" - ] - } - ], - "footnotes": [ - "v0.4:两级 UI 反转/顺势/波段;做多细则见 binance-alt-trend-long.md §3." - ] -} diff --git a/docs/strategy/gate-intraday.md b/docs/strategy/gate-intraday.md deleted file mode 100644 index 661a929..0000000 --- a/docs/strategy/gate-intraday.md +++ /dev/null @@ -1,277 +0,0 @@ -# Gate·BTC 日内账户 - -> **状态**:v0.2(策略定稿;**0 点强平已实现**;日内 UI 隐藏平仓/委托/移动保本 **待实现**) - ---- - -## 1. 账户定位 - -| 项 | 说明 | -|----|------| -| 交易所 | Gate 合约 | -| 品种 | **仅 BTC** | -| 方向 | **多空都做**(由过滤条件决定,非手选方向) | -| 计仓 | `POSITION_SIZING_MODE=full_margin`(全仓杠杆) | -| UI profile | **日内户**(`TRADE_SYMBOL_WHITELIST=BTC,ETH` 且限制开启;本策略只交易 BTC) | -| 与趋势户关系 | **不使用** 大分歧 A/B/小分歧;**不使用**「趋势单 / 波段单」手选 | - -### 资金与杠杆(执行约定) - -| 项 | 说明 | -|----|------| -| 账户规模 | 约 300U(测试阶段) | -| 日交易基数 | **50U**(早 8:00 重置为 50U,不延续前日阶梯) | -| 单笔阶梯 | 上一笔 **+10U / −10U** 调节下一笔基数(赢 60U / 亏 40U 等) | -| 杠杆 | **10× 全仓** | -| 一次一单 | 同时仅 **1** 个 Gate 仓位 | -| 止损带宽 | **0.4%~1.5%**(结构要求更宽则 **不做**) | - ---- - -## 2. 周期分层 - -自上而下,**先定能不能做,再做哪一类**: - -| 层级 | 周期 | 作用 | -|------|------|------| -| 方向过滤 | **1H** | 大方向;**不得与 15m 排列反向** | -| 均线 + 结构 | **15m** | 21/55/144 排列,顶底分型,结构识别,**B 类收盘突破** | -| 触发 | **5m** | **A 类**:N 字形突破(配合 15m 分型) | -| 方法 | 裸 K + 三均线 | 形态确认,入场与止损锚点 | - -**不做「趋势单」概念**:持仓以 **小时** 计,当日了结;与币安/OKX 多日趋势户区分. - ---- - -## 3. 方向过滤(必过) - -### 3.1 15m 三均线(21 / 55 / 144) - -| 15m 排列 | 只允许 | -|----------|--------| -| **多头排列**(21 > 55 > 144) | **只做多** | -| **空头排列**(21 < 55 < 144) | **只做空** | -| 纠缠,粘合,不符合 | **不做** | - -### 3.2 与 1H 同向 - -- **做多**:15m 多头排列,且 **1H 不得为空头排列**(1H 均线不能与 15m 方向相反). -- **做空**:15m 空头排列,且 **1H 不得为多头排列**. -- 1H/15m 方向冲突 → **当日该方向不做**. - -### 3.3 21 均线关系(才允许开仓) - -入场须与 **21 均线** 发生有效关系,避免 distant 追单: - -| 方向 | 要求(定性) | -|------|----------------| -| **做多** | 多头排列下,**回踩 21 附近获支撑** 或 **站稳 21 上方** 后再按 playbook 入场 | -| **做空** | 空头排列下,**反弹 21 附近承压** 或 **压在 21 下方** 后再按 playbook 入场 | - ---- - -## 4. 开仓类型(仅两类) - -界面日后仅两个短标签(全称见下表 hover / 本文): - -| 界面标签 | 存储 code(建议) | 本质 | -|----------|-------------------|------| -| **假破** | `liquidity_false_break` | 流动性扫单 → 假突破验证 → **5m N 字** → **15m 顶/底分型** | -| **结构突破** | `structure_breakout` | **15m 结构有效突破**(**收盘确认**) | - -子结构 **不单独占主下拉**,可在复盘备注或二级标签中记录. - ---- - -## 5. A 类:假破(流动性 / 假突破) - -**适用**:关键位附近 **扫止损** 后价格 **回到结构内**,陷阱确认后再反向做. - -### 5.1 流程 - -```text -1H/15m 方向 + 21 均线过滤通过 - → 假突破出现(扫高/扫低) - → 验证为「假」(收回结构内 / 反向裸 K 确认) - → 5m 走出 N 字(二次探底/探顶后,沿允许方向突破) - → 15m 出现底分型(多)或顶分型(空) - → 入场 -``` - -### 5.2 做多 / 做空(对称) - -| 步骤 | 做多 | 做空 | -|------|------|------| -| 假破 | 向下扫低后快速拉回支撑/箱上 | 向上扫高后跌回阻力/箱下 | -| 5m N 字 | 扫低 → 反弹 → 不破前低 → 向上突破 | 扫高 → 回落 → 不过前高 → 向下突破 | -| 15m 确认 | **底分型** | **顶分型** | -| 止损 | 假破极值或 N 字低点 **外侧**(仍须落在 0.4%~1.5%) | 对称 | -| 目标 | **最低 1:1**;之后 **随行情动态** 部分止盈,移动止损或延伸 | 对称 | - -### 5.3 注意 - -- **须等假破验证完成**,扫完不追. -- **5m N + 15m 分型** 为入场必要条件,缺一不可. -- 与大级别 **宽幅震荡(S1)** 叠加时假信号多,优先 **降频或不做**. - ---- - -## 6. B 类:结构突破 - -**适用**:15m 上结构清晰,方向与均线排列一致,**收盘突破** 后顺势做. - -### 6.1 子结构(均属 B 类) - -双顶,双底,头肩顶/底,收敛(三角/楔形),箱体等——**统一记为「结构突破」**. - -### 6.2 突破确认 - -- **以 15m K 线收盘价为准** 突破关键位(颈线,箱边,收敛边界等). -- **仅刺破,未收盘站稳** → **不算** 有效突破,不做. -- 可选:**收盘突破后回踩** 再进(裸 K 确认),仍须满足 21 均线关系与 **≥1:1** 空间. - -### 6.3 止损与目标 - -| 项 | 说明 | -|----|------| -| 止损 | 结构另一侧或突破位回退点 **外侧**(0.4%~1.5%,超出则不做) | -| 目标 | 下单前 **至少 1:1**;到位后 **随行情动态** 调整,不写死固定 RR | -| 空间不足 | 最近阻力/支撑导致 **达不到 1:1** → **不做** | - ---- - -## 7. 行情状态(辅助过滤) - -| 状态 | 特征 | Gate 动作 | -|------|------|-----------| -| **S0 趋势** | 1H/15m 排列清晰,高低点有序 | 正常:A/B 均可 | -| **S1 宽幅震荡** | 大箱横盘多日,均线反复穿 | **降频或不做** | -| **S2 末期/选边** | 贴边收敛,刚突破或假破频发 | 优先 **A 假破** 或 **B 收敛突破** | - ---- - -## 8. 一日节奏与笔数 - -| 项 | 规则 | -|----|------| -| 周末 | **不开新仓** | -| 早窗 | 约 **9:00**(8:00~12:00 内),**计划内第 1 笔** | -| 下午 | **默认不开新仓**(持仓可保留至晚窗) | -| 晚窗 | 约 **21:00**(20:00~23:00 内),**计划内第 2 笔** | -| 第 3 笔 | 仅当 **未连错 2 笔**,且 **早/晚有一笔为止损出场**,可 **补 1 笔** | -| 日上限 | **最多 3 笔** | -| **连错 2 笔** | **当日不再开新仓**(第 3 笔名额作废) | - -**连错计数**: - -| 出场 | 是否算「错 1 笔」 | -|------|------------------| -| **计划止损**触发 | ✅ 算 | -| **0 点强制清仓**(系统结果 `强制清仓`)且亏损 | ✅ 算 | -| 止盈 / ≥1:1 按计划平 | ❌ 不算 | -| 0 点强制清仓且盈利或平推 | ❌ 不算 | - ---- - -## 9. 出场与统计纪律 - -### 9.1 盈亏比 - -- 开仓前:**第一目标空间 ≥ 止损距离(最低 1:1)**. -- 持仓中:目标 **随行情动态** 调整;本文档 **不量化** 固定止盈比例. - -### 9.2 禁止「手动止损」 - -- **亏损出场** 必须来自 **开仓时设定的计划止损**(交易所或监控等价执行). -- **禁止** 盘中亏着 **手点平仓** 充当止损(破坏统计与连错规则). -- 若违规手动平亏:**视为当日纪律失败,建议停手**;复盘结果 **不得** 记为「止损」糊弄统计. - -### 9.3 时间出场:仅 0 点(程序已实现) - -- **唯一** 时间类出场:**当日 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_before_reset()`(各实例 `app.py` 后台循环调用). -- 行为:对该小时仍 **active** 的 `order_monitors` **市价全平**,取消交易所触发单,写交易记录. -- **系统结果字段**:`result = 强制清仓`;备注含「北京时间 0:00 整点风控清仓」. -- **策略口语「0 点平仓」= 系统「强制清仓」**,统计连错时按 §8 盈亏判定,不按字段名区分. - -> **与 `TRADING_DAY_RESET_HOUR=8` 无关**:后者只切 **交易日**(统计,8 点前禁开等),**不会**自动平仓. - -### 9.4 允许的出场类型(统计用) - -| 策略说法 | 系统 `result` | 说明 | -|----------|---------------|------| -| 止盈 | 止盈 / 移动止盈 / 保本止盈 等 | 计划止盈或 ≥1:1 后按计划/动态平 | -| 止损 | 止损 | 仅 **计划止损** 触发 | -| 0 点平仓 | **强制清仓** | 整点风控兜底(§9.3) | -| ~~手动平仓~~ | 手动平仓 | **策略禁止**(除极端技术故障等,须复盘说明) | - ---- - -## 10. A / B 如何选择(当日) - -| 盘面 | 优先 | -|------|------| -| 刚扫流动性,回到箱内 | **A 假破** | -| 结构清晰,排列已顺,收敛末端 | **B 结构突破** | -| 大箱乱扫,均线粘合 | **不做** | - -早/晚窗 **有形态才做**,无形态 = **0 笔**,不占额度. - ---- - -## 11. 与其它账户边界 - -| 账户 | 周期 | 持仓 | 本户勿混 | -|------|------|------|----------| -| 币安 | 日线/4H 事件 | 数天~数周 | 不要用 Gate 扛隔夜趋势 | -| OKX | 4H 波段滚仓 | 数小时~数天 | 勿与 Gate 同向同结构叠隔夜 | -| **Gate 日内** | 1H 过滤 + 15m/5m | **当日 0 点前** | 见上文 | - ---- - -## 12. 系统对接 - -### 12.1 已实现 - -| 项 | 说明 | -|----|------| -| 日内 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(节选): - -```env -FORCE_CLOSE_ENABLED=true -FORCE_CLOSE_BJ_HOUR=0 -TRADING_DAY_RESET_HOUR=8 -``` - -### 12.2 待实现(UI / 纪律) - -| 项 | 说明 | -|----|------| -| 开仓类型 | 界面 **`假破` / `结构突破`**(code:`liquidity_false_break` / `structure_breakout`);**无** trend/swing 手选 | -| 写入字段 | `trade_records.entry_model` / 复盘下拉同两项 | -| 隐藏操作 | 日内 profile 下 **隐藏** 平仓,委托,移动保本(**实例页 + 中控**,`intraday_discipline` / `order_entry_profile=intraday`) | -| 隐藏表单项 | 不展示 1h/2h/4h 时间平仓,移动保本勾选(避免与 §9.3 混用) | -| 后端可选 | 严格模式下拒绝 `del_order` / 改委托 API | - ---- - -## 13. 修订记录 - -| 版本 | 日期 | 说明 | -|------|------|------| -| 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 已启用说明 | diff --git a/docs/strategy/okx-trend-both.md b/docs/strategy/okx-trend-both.md deleted file mode 100644 index c9b32ed..0000000 --- a/docs/strategy/okx-trend-both.md +++ /dev/null @@ -1,157 +0,0 @@ -# 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 短标签 | diff --git a/docs/trend-hub-close-and-trade-records.md b/docs/trend-hub-close-and-trade-records.md deleted file mode 100644 index 5a7f802..0000000 --- a/docs/trend-hub-close-and-trade-records.md +++ /dev/null @@ -1,184 +0,0 @@ -# 趋势回调:中控平仓与交易记录(检阅备忘) - -本文档汇总 **中控手动结束趋势计划**,**交易记录 / 策略记录** 写入规则,以及 **三所展示统一**,**补仓表计价** 相关修复,便于自行检阅与排错. - -适用仓库:`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_user # 或本机仓库根目录 - -# 先预览 -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_user -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/` | -| `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(与对话中修复项同步)* diff --git a/docs/trend-pullback-strategy.md b/docs/trend-pullback-strategy.md deleted file mode 100644 index f27129c..0000000 --- a/docs/trend-pullback-strategy.md +++ /dev/null @@ -1,129 +0,0 @@ -# 趋势回调策略说明 - -本文描述 **「趋势回调」** 自动交易计划的业务规则与实现口径. - -**三所主站**(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) - ---- - -## 1. 适用场景 - -- 各 **USDT 永续** 实例独立部署,使用各自 API 与 `crypto.db`. -- 你已明确:**方向,止损价,补仓区间边界价,止盈价,杠杆**,并接受程序按风险预算拆分 **首仓 50% + 多档补仓 50%**. - ---- - -## 2. 名词与参数 - -| 名称 | 含义 | -|------|------| -| **合约 USDT 可用余额** | **生成预览**时通过 API 读取的 **swap 账户 USDT `free`** 快照;**确认执行**时再次读取并与快照比对偏差. | -| **风险比例** | 默认 **5%**:指「若整笔计划在 **补仓区间远侧边界**(做多=上沿,做空=下沿)这一侧的最坏价格结构下触及止损」,目标亏损上限约为 **可用余额快照 × 风险比例**(实现上用 `calc_risk_fraction` 与 `prepare_order_amount` 反推总张数,受交易所最小张数与精度约束). | -| **止损价** | 用户填写;开仓后挂 **交易所仓位类止损触发单**(全平). | -| **补仓区间边界**(库字段 `add_upper`) | 用户填写;**仅在该价位与止损价构成的区间内** 才允许程序触发剩余 50% 的市价补仓.**界面文案**:做多显示「补仓上沿」,做空显示「补仓下沿」.校验:做多 `止损 < 边界价`;做空 `止损 > 边界价`. | -| **止盈价** | 用户填写的 **固定价格**;**不由交易所条件止盈单触发**,由应用后台 **按标记价/行情价轮询**,达到后 **市价全平**. | -| **杠杆** | 计划内固定写入;用于 `set_leverage` 与名义换算. | -| **补仓档位数** | 默认 **5** 档(环境变量 `TREND_PULLBACK_DCA_LEGS` 可调);程序在满足最小张数前提下可能 **自动减少档数**. | - ---- - -## 3. 执行流程(时间顺序) - -### 3.0 列表时间窗(交易记录 / 计划历史) - -- **交易记录**,**计划历史**(含预览快照)列表与 **交易记录 CSV 导出** 支持 **UTC** 时间筛选(默认 UTC 当日;可选近 24h,近 7d,自定义起止). -- 查询参数:`win_preset`(`utc_today` / `utc_last24h` / `utc_last7d` / `custom`),自定义时另传 `from_utc`,`to_utc`. -- **统计分析**页仍按北京时间 `TRADING_DAY_RESET_HOUR` 切日,不受列表窗影响. - -### 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`),超时须重新点「生成预览」. - -### 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. **计划结束**:任一结束路径(止盈 / 止损 / 用户手动结束)均会 **撤单**(条件单 + 普通挂单,尽力而为). - -### 3.3 取消预览 - -用户可「取消预览」删除 `trend_pullback_previews` 中对应记录;过期记录会在新预览或页面加载时清理. - -### 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`);成功后显示「已保本」时间与原止损(若与当前不同). - -### 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` 与交易所记录. - ---- - -## 4. 与「机器人下单监控」的差异 - -| 项目 | 机器人下单监控 | 趋势回调 | -|------|------------------|----------| -| 开仓 | 单次市价 + 条件止盈+止损 | 首仓 50% 市价 + 多档补仓 + **仅止损在交易所** | -| 止盈 | 条件单 + 本地监控 | **仅本地监控市价止盈** | -| 仓位基数 | 以损定仓(表单/会话基数) | **可用余额快照 × 风险比例** 推导 | -| 移动保本 | 支持(按 R 自动上移) | **保本移交**(结束计划→下单监控;交易所 TP+SL;**无**自动 R 保本) | - ---- - -## 5. 风险声明(必读) - -- 市价单存在 **滑点**;极端行情下实际亏损可能 **大于** 理论 5%. -- 补仓触发依赖应用 **轮询间隔**(`MONITOR_POLL_SECONDS`),非毫秒级高频. -- 交易所 **最小张数 / 精度** 可能导致计划张数被截断,实际风险略低于或偏离纸面计算. -- 请使用 **单独 API Key / 子账户**,并先在 `LIVE_TRADING_ENABLED=false` 环境验证流程(若需沙盒请自行对接测试网,本仓库默认实盘接口). - ---- - -## 6. 相关环境变量 - -| 变量 | 说明 | 默认 | -|------|------|------| -| `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` | -| `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` | - ---- - -## 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). - -**CSV 导出**:交易记录导出为 **v3**,包含上述交易所对齐字段及 `trend_plan_id`. diff --git a/docs/ubuntu-server.md b/docs/ubuntu-server.md index 98e3af1..02755df 100644 --- a/docs/ubuntu-server.md +++ b/docs/ubuntu-server.md @@ -12,7 +12,8 @@ | 运行用户 | **root**(下文命令均按 root 编写) | | 项目路径 | **`/opt/crypto_monitor_user`**(整仓克隆到此目录) | | 进程管理 | **PM2**(全局安装,见 §3) | -| 网络 | 能 `git clone` 私有仓库;访问交易所不稳定时需 **SSH SOCKS**(见各所《部署文档》) | +| 网络 | 能 `git clone` 私有仓库;能访问授权站 **https://sq.bz121.com**;访问交易所不稳定时需 **SSH SOCKS**(见各所《部署文档》) | +| 授权 | 部署后配置仓库根 `license.env`,打开 `/license` 激活(见 [license.md](./license.md)) | --- diff --git a/docs/中控AI与密钥配置.md b/docs/中控AI与密钥配置.md deleted file mode 100644 index 23cf8a7..0000000 --- a/docs/中控AI与密钥配置.md +++ /dev/null @@ -1,103 +0,0 @@ -# 中控 AI 与部署密钥配置 - -本文档说明:**长期部署密钥**(一次生成,不轮换),**SSO 临时链接**(保持不动),以及 **AI 配置**(中控统一维护并同步三所). - ---- - -## 1. 三类「密钥」分工 - -| 类型 | 变量 / 机制 | 谁维护 | 是否自动过期 | -|------|-------------|--------|--------------| -| **长期通信密钥** | `HUB_BRIDGE_TOKEN` | 部署脚本首次写入四份 `.env` | 否,不轮换 | -| **实例 Session 签名** | `FLASK_SECRET_KEY` | 部署脚本首次写入三实例 | 否 | -| **中控 Session 签名** | `HUB_SESSION_SECRET` | 部署脚本首次写入中控 | 否 | -| **SSO 开门链接** | `/hub-sso?token=...` | 中控每次点「打开实例」签发 | 默认 2h + 单次 | -| **AI 配置** | `OPENAI_*`,`AI_*` 等 | 中控系统设置 → AI 配置 | 否 | - -**SSO 保持不动**:仍为随机 nonce,默认 `HUB_SSO_TTL_SEC=7200`,成功登录一次后链接作废.长期 `HUB_BRIDGE_TOKEN` 只用于签名,不会每 2 小时变化. - ---- - -## 2. 首次部署自动生成 - -`bash deploy/setup_env.sh` 在复制 `.env.example` 后自动执行: - -```bash -python3 scripts/bootstrap_deploy_secrets.py -``` - -| 写入项 | 位置 | 规则 | -|--------|------|------| -| `HUB_BRIDGE_TOKEN` | 中控 + 三实例(同值) | 仅空或占位符时写入 | -| `FLASK_SECRET_KEY` | 三实例(同值) | 仅空或占位符时写入 | -| `HUB_SESSION_SECRET` | 中控 | 仅空时写入 | -| `HUB_USERNAME` / `HUB_PASSWORD` | 中控 | 默认 admin / admin123(仅空时) | -| `APP_USERNAME` / `APP_PASSWORD` | 三实例 | 默认 admin / admin123(仅空时) | - -**已有非空生产值不会被覆盖**(一次生成,不轮换). - -子代理 `agent.py` 优先读取 `HUB_BRIDGE_TOKEN` 作为 `X-Control-Token` 校验;独立配置 `CONTROL_TOKEN` 已废弃. - ---- - -## 3. 中控系统设置 → AI 配置 - -路径:**中控 Web → 系统设置 → AI 配置** Tab. - -### 3.1 可配置项 - -| 中文名 | 环境变量 | -|--------|----------| -| AI 提供方 | `AI_PROVIDER` | -| API 地址 | `OPENAI_API_BASE` | -| API 密钥 | `OPENAI_API_KEY`(掩码,留空不修改) | -| 云端模型 | `OPENAI_MODEL` | -| Ollama 地址 | `OLLAMA_API` | -| Ollama 模型 | `AI_MODEL` | -| 请求超时(秒) | `AI_TIMEOUT_SECONDS` | - -### 3.2 保存行为 - -1. 写入 `manual_trading_hub/.env` -2. **强制同步** 至 `crypto_monitor_okx/binance/gate/.env` 相同键 -3. 自动 `pm2 restart` 中控 + 三实例(`--update-env`) - -### 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`. - ---- - -## 4. 实例 env 配置页变更 - -三所 **env 配置** 页已 **移除「AI 复盘」卡片**.交易所 API,企业微信,交易执行等仍各所自配. - -实例侧若通过 API 提交已移除的 AI 键,会被白名单过滤,不会写入. - ---- - -## 5. 与系统设置 / env 页对照 - -| 能力 | 实例 env | 实例系统设置 | 中控系统设置 | -|------|----------|--------------|--------------| -| 交易所 API | ✅ | ❌ | ❌ | -| OpenAI / AI | ❌ | ❌ | ✅(同步三所) | -| 实例登录密码 | ❌ | ✅ | ❌ | -| 中控登录密码 | ❌ | ❌ | ✅ | -| Bridge / Flask 长期密钥 | ❌(自动) | ❌ | ❌(自动) | -| SSO 链接 | — | — | 每次打开实例自动签发 | - ---- - -## 6. 运维提示 - -- 修改 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 说明) diff --git a/docs/对冲计划开发方案.md b/docs/对冲计划开发方案.md deleted file mode 100644 index b8d2e39..0000000 --- a/docs/对冲计划开发方案.md +++ /dev/null @@ -1,678 +0,0 @@ -# OKX 对冲计划 — 开发方案 - -> 状态:**方案冻结**(实现前对照本页;改需求先改本文). -> 范围:**仅 `crypto_monitor_okx` 实例**;中控不做对冲开平. -> 相关策略说明:[期权对冲方案分析.md](./期权对冲方案分析.md) - ---- - -## 1. 目标与命名 - -在 OKX 实例增加独立模块 **「对冲计划」**,把「行情选腿 → 情景测算 →(条件满足时)自动开仓 → 规则退出 → 独立复盘统计」串成一套可状态跟踪的计划. - -| 产品名 | 英文键 | 含义 | -|--------|--------|------| -| **永期对冲** | `perp_options` | 永续(子账户) + 买方期权(主账户) | -| **期期对冲** | `options_options` | 主账户内两条买方期权腿 | - -页面/导航展示用中文名;API/DB 用英文键. - -**不做(本方案外):** - -- 卖方期权、组合单原子成交 -- 中控代下单 -- 跨所对冲 -- 替代现有关键位/策略/期权页(可共存,但同标的限制见 §9) - ---- - -## 2. 计仓模式门禁 - -沿用 `[docs/position-sizing-mode.md](./position-sizing-mode.md)`: - -| `POSITION_SIZING_MODE` | 永期对冲 | 期期对冲 | -|------------------------|----------|----------| -| `risk`(以损定仓 / **非全仓**) | **仅测算**(左右行情 + 情景表);**禁止开仓/启动计划** | **允许开仓**(测算 + 启动计划) | -| `full_margin`(全仓杠杆) | **允许开仓** | **允许开仓** | - -说明: - -- **永期**依赖永续全仓名义与保证金节奏,故开仓仅限全仓. -- **期期**不占永续保证金,非全仓也允许开期权腿;非全仓下 UI 隐藏/禁用「启动永期」,仍可做永期**只读测算**. -- 切模式后若存在 `active` 永期计划,禁止切到 `risk`,或强制要求先结束计划(实现时二选一并写死校验). - -额外硬门: - -- 须 `OKX_OPTIONS_ENABLED=true` 且期权 API 可用. -- 实际开仓须实例允许实盘(`LIVE_ORDER` 等与永续/期权现有开关一致),并对冲计划自有开关见 §10. - ---- - -## 3. 永期对冲 — 仓位与选期权 - -### 3.1 永续开仓量(全仓) - -与现网全仓逻辑一致,对冲计划固定用于 **BTC / ETH**: - -``` -可用保证金 = 合约账户可用 USDT -占用保证金 = 可用 × FULL_MARGIN_BUFFER_RATIO(默认 0.98) -杠杆 = BTC_LEVERAGE / ETH 对应杠杆(默认 10x) -名义 ≈ 占用保证金 × 杠杆 -张数 = amount_to_precision(名义 / 价格 / 合约面值) -``` - -页面左侧展示:**建议张数、名义、止损亏损额、止盈盈利额**(用户填止盈价/止损价后按该仓位即时算). - -用户流程: - -1. 选标的 ETH/BTC、方向(多/空). -2. 系统按全仓 ×0.98×10x 算出永续张数与到止盈/止损的 U 盈亏. -3. **再据此挑选右侧期权**(保费、张数、行权价),使「止损时保险腿」与「止盈保费损耗」可接受. -4. 通过情景表确认后启动计划. - -### 3.2 期权腿选取 - -- 行情自动拉 OKX 期权链(复用 `build_option_chain`). -- **报价形态:列表式**;多仓默认筛 **Put**,空仓默认筛 **Call**. -- 权利金默认按 **卖一 ask** 估算;开仓限价买入. - -### 3.3 左右布局 - -``` -左:永续列表行情(标记/买卖一) + 方向/建议张数/开仓价/止盈/止损 -右:期权列表(可「选用」一条腿) -下:情景测算 → [保存草稿] [启动计划] -``` - ---- - -## 4. 期期对冲 — 仓位与选腿 - -### 4.1 报价与选腿 - -- **T 型报价链**(复用期权页 T 型样式/数据结构). -- 用户选 **腿 A + 腿 B**(通常 Call + Put,或主方向 + 尾部). -- 预算受 `OKX_OPTIONS_TRADE_BUDGET_USDC` 等既有约束;可拆预算到两腿. - -### 4.2 目标价 - -用户填 **预判价格 S\*** (「价格能到的位置」): - -- 系统标明在 S\* 时哪条腿为 **盈利方**、哪条为 **亏损方**. -- 到达规则见 §5.2. - -### 4.3 左右布局 - -``` -左:指数价 + 到期日 + 预算 + 目标价 S* -右:T 型链,依次选用两腿 -下:情景测算 → [保存草稿] [启动计划] -``` - -非全仓模式下期期布局同上(无永续区). - ---- - -## 5. 退出规则(冻结) - -### 5.1 永期对冲 - -| 事件 | 永续 | 期权 | 计划是否结束 | 设计意图 | -|------|------|------|--------------|----------| -| **永续止盈触发** | 交易所 TP 平仓 | **不强制平**(保险腿可自生自灭/人工) | **算结束** | 对冲计划以永续兑现目标收口 | -| **永续止损触发** | 交易所 SL 平仓 | **必须强制平仓** | **算结束** | 保护机制 | -| 期权单独到期 | — | 结算 | 若计划已因止盈结束则只更新腿快照,不再改计划合计 | | -| 人工结束计划 | 可选平永续 | 按选项 | **算结束** | | - -**计划结束时盈亏口径(写入 `realized_pnl_*`,推送与统计共用):** - -| 结束原因 | 公式(≈U,1:1) | 字段落库 | -|----------|--------------|----------| -| **止盈** `perp_tp` | **永续止盈已实现盈利 − 期权已付权利金** | `realized_pnl_perp` = 止盈盈利;`realized_pnl_options` = **−premium_total**(按权利金全额计成本,不论期权是否仍持仓);`realized_pnl_total` = 上两式之和 | -| **止损** `perp_sl` | **期权平仓盈利 − 永续止损亏损额** | `realized_pnl_options` = 期权强制平后已实现;`realized_pnl_perp` = 永续止损已实现(为负或记亏损额);`realized_pnl_total` = 期权盈利 − \|永续亏损\|(即有符号相加) | - -说明: - -- 止盈时期权**物理上可不平**,但 **计划账** 已按「保费打掉」收口,后续期权 thrift/到期盈亏 **不再回写计划合计**(可在腿上另记备注/浮盈,不进 `realized_pnl_total`). -- 止损时期权必须先强平再结账,用真实平仓盈亏,不是只扣权利金. - -永续侧 TP/SL:沿用实例 **交易所条件单**,监控识别成交后触发计划结束逻辑 + 微信推送. - -### 5.2 期期对冲 - -| 事件 | 盈利方 | 亏损方 | 计划是否结束 | -|------|--------|--------|--------------| -| **标的价到达用户目标价 S\*** | **自动平仓** | **不平**,持有至到期 | 平盈利腿后计划可标 `closing`;**全部腿终态后结束**(亏损腿到期后结账) | -| **到期且整体无盈利** | — | 到期结算 | **算结束**;合计记 **总亏损**(通常 ≈ −全部权利金,或到期结算净值 < 0 的合计) | -| 到期时组合合计仍盈利 | — | 到期结算 | **算结束**;按实际结算盈亏入账 | -| 未达 S\* 至到期 | 两腿均到期 | | 同上,按结算合计结束 | - -判定「整体无盈利」:到期(或计划收口)时 `realized_pnl_total ≤ 0`(含双腿权利金全损). - -盈利方判定规则仍按前文(触达 S\* 时按浮盈较大一侧平仓;皆亏则等到期). - -### 5.2.1 期权腿实盘平仓执行(与期权页共用) - -对冲计划凡**必须物理平掉期权腿**时(如永期止损联动、期期平盈利腿),执行口径与独立期权模块一致: - -| 规则 | 说明 | -|------|------| -| 禁市价 | 代码硬关闭,无市价兜底 | -| 只锁买一 | 本轮 `min(仓位, 买一深度)` × 买一限价;`reduceOnly` | -| 分批 | 买一不够则剩余下一轮再平再锁新买一 | -| 有效流动性 | 残档买一禁止按买盘平 | -| 2× 门控 | 目标位/自动类路径首次需可回收≥2×权利金并持续 hold;手动买一平只验流动性 | -| 平仓挂单 TTL | 卖出限价超 `OKX_OPTIONS_PENDING_TTL_SECONDS`(默认 10 分钟)自动撤;UI「委托」可见 | - -完整说明(可单独打开):**[期权开平仓与监控说明.md](./期权开平仓与监控说明.md)** · 线上 `/options/guide`. - ---- - -### 5.3 企业微信推送(起止必发) - -| 时机 | 是否必发 | 内容要点 | -|------|----------|----------| -| **计划开始**(开仓成功 → `active`) | **必发** | 类型(永期/期期)、标的方向、关键价位、张数/保费、计划 id | -| **计划结束** | **必发** | 结束原因、`realized_pnl_total`、分项(永续/期权)、是否止盈/止损/到期亏损 | -| 半腿失败 / 强平失败 | 必发告警 | 便于人工介入 | -| 目标价平掉盈利腿(期期中间态) | 建议发 | 注明亏损腿仍持有 | - -结束推送触发点与「算结束」一致:永期止盈、永期止损、期期到期收口(含无盈利总亏)、人工结束等. - ---- - -## 6. 情景测算(开仓前必显) - -### 6.1 永期 - -| 情景 | 含义 | -|------|------| -| 止盈 | 永续到 TP 的盈利 − 期权保费(期权按不强制平时的损耗估算) | -| 宽止损 | 永续到 SL 的亏损 + 期权平仓估值(强制平,用 mark/买一估算) | -| 到期横盘 | 永续≈0 + 期权权利金全损 | -| 价格扫描 | index ± 若干档合计 | - -核心输出:**宽止损合计亏损**(人工评估是否开仓). - -### 6.2 期期 - -| 情景 | 含义 | -|------|------| -| 到达 S\* | 盈利腿兑现估值 − 已付总保费中亏损腿残留 | -| 到期横盘 | 双腿权利金近似全损 | -| 到期大涨/大跌 | 结算内在价值 | - ---- - -## 7. 数据来源(行情自动) - -| 侧 | 来源 | -|----|------| -| 永续行情/规格 | OKX 子账户 ccxt:ticker + 现有 `/api/hub/market` 规格逻辑 | -| 期权链 | `build_option_chain` / `/api/options/chain`(本实例直连,无需中控代理) | -| 指数价 | 期权 `index_px`,左右对齐 | - -报价刷新:页面手动刷新 + 计划编辑态可选 10~30s 自动刷新. - ---- - -## 8. 自动开仓编排 - -### 8.1 永期(仅全仓) - -建议默认顺序:**先期权、后永续**(期权失败成本低;永续失败则提示处理刚开的期权). - -``` -校验 full_margin + LIVE + 无冲突计划 -→ 期权限价买入(卖一) -→ 永续全仓张数市价开仓 + 挂交易所 TP/SL -→ 写 hedge_plans / legs,状态 active -``` - -部分失败补偿(最小集): - -| 情况 | 动作 | -|------|------| -| 期权成、永续败 | 告警;建议自动平期权(可配置)或转人工 | -| 永续成、期权败 | 告警;可选撤永续或重试期权;计划标 `partial` | - -### 8.2 期期(全仓与非全仓均可开) - -``` -校验 LIVE + 期权资金 -→ 腿1 限价买 -→ 腿2 限价买 -→ active;记录 target_price S* -``` - -两腿间勿留长时间单腿敞口;第二腿失败则标 `partial` 并告警. - ---- - -## 9. 状态机与冲突 - -``` -draft → opening → active → closing → closed - ↘ partial / failed -draft → cancelled -``` - -冲突规则: - -- **同标的同时至多 1 条 active 永期计划**(全局可 `MAX_ACTIVE_HEDGE_PLANS`). -- 永期 active 时与全仓「单仓」一致:**不与额外永续仓并存**(启动前校验无其它持仓,或本计划即为该仓). -- 期权页对手动平「计划绑定 inst」应提示归属对冲计划. - ---- - -## 10. 历史记录、统计与复盘(独立) - -**入口:** OKX 顶栏 **「对冲计划」** 页内三个 Tab(不单开顶栏项): - -| Tab | 路由建议 | 内容 | -|-----|----------|------| -| **计划** | `/hedge-plan` | 新建 / 草稿 / 进行中 | -| **历史** | `/hedge-plan?tab=history` | 已结束与取消的计划列表 + 详情复盘 | -| **统计** | `/hedge-plan?tab=stats` | 独立统计看板 | - -**不得**并入普通「交易记录与复盘」`/records`、全站「统计分析」`/stats`、策略交易记录. -腿可可选关联 `options_trades` / 监控 id,但 **计划合计盈亏与胜率只读本模块表**. - -币种展示约定:永续腿 USDT、期权腿 USDC;合计列标注 **「≈U(1:1)」**,不做实时汇率换算. - ---- - -### 10.1 数据落库 - -库文件:OKX 实例 `crypto.db`(与其它表同库). - -#### `hedge_plans`(一条计划) - -| 字段 | 类型建议 | 说明 | -|------|----------|------| -| `id` | INTEGER PK | | -| `plan_type` | TEXT | `perp_options` 永期 / `options_options` 期期 | -| `status` | TEXT | draft / opening / active / closing / closed / partial / failed / cancelled | -| `underlying` | TEXT | BTC / ETH | -| `direction` | TEXT | 永期:long/short;期期可空 | -| `entry_mark` | REAL | 开仓参考价(标记/指数快照) | -| `tp` | REAL | 永期止盈价;可空 | -| `sl` | REAL | 永期止损价;可空 | -| `target_price` | REAL | 期期目标价 S\*;可空 | -| `sizing_mode_at_open` | TEXT | 开仓时 `risk`/`full_margin` 快照 | -| `perp_size` | REAL | 永期张数/币量快照;期期空 | -| `margin` | REAL | 占用保证金快照 | -| `leverage` | REAL | 杠杆快照 | -| `premium_total` | REAL | 期权已付权利金合计(USDC) | -| `realized_pnl_perp` | REAL | 永续已实现(USDT);止盈为正,止损为负 | -| `realized_pnl_options` | REAL | 期权账:止盈场景记 **−权利金**;止损场景记 **强平真实盈亏** | -| `realized_pnl_total` | REAL | 见 §5.1 / §5.2 公式;统计与微信共用此值 | -| `stats_bucket` | TEXT | 可选冗余:`tp` / `sl` / `oo_expiry_loss` / `oo_target` / `other` 便于统计筛选 | -| `close_reason` | TEXT | 见下表枚举 | -| `wechat_start_sent` | INTEGER | 开仓推送是否已发 | -| `wechat_end_sent` | INTEGER | 结束推送是否已发 | -| `note` | TEXT | 人工复盘短评 | -| `created_at` | TEXT | | -| `opened_at` | TEXT | 首次腿成交时间 | -| `closed_at` | TEXT | **计划结束时间**(止盈/止损/到期收口等) | -| `preview_json` | TEXT | 开仓前情景测算快照(可选) | - -**`close_reason` 枚举** - -| 值 | 含义 | 是否算计划结束 | 合计口径 | -|----|------|----------------|----------| -| `perp_tp` | 永续止盈 | **是**(立刻 closed) | **止盈盈利 − 权利金** | -| `perp_sl` | 永续止损 + 期权强制平 | **是** | **期权盈利 − 永续亏损** | -| `target_win_leg` | 期期已平盈利腿(中间态可暂不 closed) | 腿未齐前可不结束 | 待亏损腿到期后定合计 | -| `oo_expiry_loss` | 期期到期且合计无盈利 | **是** | **总亏损**(settled ≤ 0,常 ≈ −保费) | -| `oo_expiry_win` | 期期到期合计仍盈利 | **是** | 实际到期合计 | -| `expiry` | 其它到期收口 | **是** | 实际结算 | -| `manual` | 人工结束 | **是** | 按当时已实现 | -| `partial_fail` | 半腿失败收尾 | **是** | 按补偿结果 | -| `cancelled` | 未真正开仓取消 | 是(无盈亏) | 0 | - -说明:止盈结束时期权腿可标 `hold_to_expiry`/`orphaned_after_tp`,**计划已 closed**,后续期权盈亏不回写 `realized_pnl_total`. - -#### `hedge_plan_legs`(一条腿) - -| 字段 | 类型建议 | 说明 | -|------|----------|------| -| `id` | INTEGER PK | | -| `plan_id` | INTEGER FK | | -| `leg_role` | TEXT | `perp` / `option_hedge` / `option_a` / `option_b` | -| `symbol` 或 `inst_id` | TEXT | 永续符号或期权合约 id | -| `opt_type` | TEXT | C/P;永续空 | -| `strike` | REAL | 期权行权价 | -| `side` | TEXT | long/short 或 buy | -| `size` | REAL | 张数或币量 | -| `avg_open` | REAL | 开仓均价/权利金单价 | -| `premium` | REAL | 该腿已付权利金(期权) | -| `status` | TEXT | open / closed / hold_to_expiry | -| `linked_monitor_id` | INTEGER | 可选,永续监控 | -| `options_trade_id` | INTEGER | 可选,期权成交表 | -| `realized_pnl` | REAL | 该腿已实现 | -| `close_reason` | TEXT | 腿级原因 | -| `opened_at` / `closed_at` | TEXT | | - ---- - -### 10.2 历史列表(列定义) - -筛选:**类型**(全部/永期/期期)、**状态**、**标的**、**日期**(按 `opened_at` 或 `closed_at`). - -| 列 | 来源 | 展示 | -|----|------|------| -| ID | id | `#12` | -| 类型 | plan_type | 永期对冲 / 期期对冲 | -| 标的 | underlying + direction | 如 `ETH 多` / `ETH 双买` | -| 状态 | status | 中文标签 | -| 开仓时间 | opened_at | | -| 结束时间 | closed_at | 进行中显示 — | -| 平仓原因 | close_reason | 中文(止盈离场/止损联动平/目标价平盈利腿/到期…) | -| 保费 | premium_total | `x.xx USDC` | -| 合计盈亏 | realized_pnl_total | 着色 +/- ,单位 ≈U | -| 腿摘要 | legs | 如 `永续✓ · Put持仓` / `Call已平 · Put到期` | -| 操作 | | 详情 | - -行操作:**详情**(主)、可选「补写短评」. - ---- - -### 10.3 计划详情 / 复盘页(字段) - -从历史点进去的详情页 = **主复盘面**,分块如下. - -#### A. 计划摘要 - -| 项 | 字段 | -|----|------| -| 类型 / 标的 / 方向 | plan_type, underlying, direction | -| 状态 / 平仓原因 | status, close_reason | -| 时间线 | created_at → opened_at → closed_at | -| 开仓时计仓 | sizing_mode_at_open, margin, leverage, perp_size | -| 关键价位 | entry_mark, tp, sl(永期), target_price(期期) | -| 盈亏 | realized_pnl_perp / realized_pnl_options / realized_pnl_total | -| 开仓情景快照 | preview_json 折叠展示(止盈合计/宽止损合计等) | - -#### B. 腿明细表 - -| 列 | 说明 | -|----|------| -| 角色 | 永续 / 保险期权 / 期期腿A/B | -| 合约 | symbol / inst_id | -| 数量 | size | -| 开仓价/保费 | avg_open, premium | -| 状态 | open / closed / 持有至到期 | -| 盈亏 | realized_pnl | -| 平仓原因 | close_reason | -| 关联 | 链到期权成交或监控(有则显示) | - -#### C. 复盘短评(必做入口) - -| 项 | 说明 | -|----|------| -| `note` | 多行文本,可空;保存 `PATCH /api/hedge-plan//note` | -| 提示文案 | 建议写:开仓理由、结果是否符合情景测算、下次调整 | - -**不做(本期):** 复盘截图上传、填入「交易记录与复盘」表单、纳入 AI 日/周复盘. -**可后置:** 以计划摘要生成 AI 点评(独立按钮,不写进 trade_records). - -#### D. 终态示例文案(便于复盘理解) - -| 场景 | 详情页状态说明 | 合计 | -|------|----------------|------| -| 永期止盈 | 「计划已结束(止盈);期权腿可不强平,账上已扣全部权利金」 | 止盈盈利 − 权利金 | -| 永期止损 + 期权已强平 | 「计划已结束(止损保护:期权已联动平仓)」 | 期权盈利 − 永续亏损 | -| 期期到期无盈利 | 「计划已结束(到期无盈利)」 | 总亏损(计入统计) | -| 期期达 S\* 后亏损腿仍持有 | 「盈利腿已平;待亏损腿到期后结账」 | 暂不入 closed 统计,或单独「收尾中」 | - ---- - -### 10.4 统计页(独立看板) - -**筛选:** 日期区间、类型(全部/永期/期期)、标的、结束桶(`tp`/`sl`/`oo_expiry_loss`/…). - -**聚合规则:** 仅 `status=closed`;胜场 = `realized_pnl_total > 0`. - -#### 永期口径(冻结) - -| 统计桶 | `close_reason` | 单笔盈亏公式 | 汇总 | -|--------|----------------|--------------|------| -| **止盈统计** | `perp_tp` | **止盈盈利 − 期权权利金** | sum / 笔数 / 胜率 | -| **止损统计** | `perp_sl` | **期权盈利 − 永续亏损** | sum / 笔数 /「保护后净亏」均值 | - -实现校验示例: - -``` -# 止盈 -realized_pnl_total = pnl_perp_tp - premium_total - -# 止损(亏损额取绝对值) -realized_pnl_total = pnl_option_close - abs(pnl_perp_sl) -# 等价有符号: pnl_option_close + pnl_perp_sl(后者为负) -``` - -#### 期期口径(冻结) - -| 统计桶 | 条件 | 单笔盈亏 | -|--------|------|----------| -| **到期无盈利** | 到期收口且合计 ≤ 0 | **总亏损**写入 `realized_pnl_total`(负值),计入区间净亏与「到期亏损」汇总 | -| 目标价路径 | 盈利腿已平 + 亏损腿到期后 | 两腿 realized 之和 | -| 到期仍盈利 | 合计 > 0 | 实际结算合计 | - -#### 总览卡片 - -| 指标 | 计算 | -|------|------| -| 计划笔数 | count(closed) | -| 胜率 | 胜场 / 笔数 | -| 区间净盈亏 | sum(realized_pnl_total) | -| 止盈桶净盈亏 | sum where stats_bucket=tp | -| 止损桶净盈亏 | sum where stats_bucket=sl | -| 期期到期亏损合计 | sum where oo_expiry_loss(绝对值或带符号合计) | -| 总保费支出 | sum(premium_total) | -| 平均持仓时长 | avg(closed_at − opened_at) | - -#### 分类型卡片 - -永期、期期各一套:笔数、胜率、净盈亏、平均保费;永期再拆 **止盈桶 / 止损桶**. - -#### 退出结构 - -| 指标 | 过滤 | -|------|------| -| 止盈结束笔数 | `perp_tp` | -| 止损结束笔数 | `perp_sl` | -| 期期到期无盈利笔数 | `oo_expiry_loss` | -| 目标价路径完结 | 含 `target_win_leg` 后收尾 | -| 半腿/失败 | `partial_fail` / failed | - -#### 简易表(可选) - -最近 N 条已结束计划迷你列表,点击跳详情. - -**导出(P5 可选):** CSV. - ---- - -### 10.5 与现有页面关系 - -| 现有页 | 关系 | -|--------|------| -| 交易记录与复盘 | **不写入**;永续腿若系统仍落 `trade_records`,可标记来源「对冲计划#id」,但人工复盘以对冲详情为准 | -| 统计分析 | **不合并**对冲净盈亏到全站数字(避免重复或口径混乱) | -| 期权页成交/持仓 | 腿 `options_trade_id` 可跳转对照;期权页仍可看单腿 | -| 中控 | V1 不聚合;V2 可选只读摘要 | - ---- - -### 10.6 API(历史 / 统计 / 复盘) - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/hedge-plan/list` | 进行中+草稿;`status` 过滤 | -| GET | `/api/hedge-plan/history` | 已结束列表;类型/日期/标的 | -| GET | `/api/hedge-plan/` | 详情 = 计划 + legs + note + preview | -| PATCH | `/api/hedge-plan//note` | 保存复盘短评 | -| GET | `/api/hedge-plan/stats` | query:`from`,`to`,`plan_type`,`underlying` → 总览+分类型+退出结构 | - ---- - -## 11. 监控线程 - -在 `crypto_okx` 同进程内新增 **`hedge_plan_monitor_loop`**(可与 `options_monitor_loop` 并列): - -| 职责 | | -|------|--| -| 永期 | 侦测永续 TP/SL → 按 §5.1 结束计划并结账;SL 时强制平期权 | -| 期期 | 侦测价触 S\* → 平盈利腿;到期无盈利 → 结束并记总亏损 | -| 微信 | **开始必推、结束必推**(§5.3);半腿/强平失败告警 | -| 幂等 | `wechat_start_sent` / `wechat_end_sent` 防重复推送 | - ---- - -## 12. 配置项与前端 env 页 - -对冲相关开关 **一律在 OKX 实例「env 配置」页维护**,不要求 SSH 改 `.env`. -实现对齐现有白名单模式(`lib/env/env_ui_manifest.py` 的「期权账户」分组). - -### 12.1 前端分组 - -在 env 配置页新增独立卡片,标题 **「对冲计划」**: - -- **仅 OKX** 展示(Binance/Gate 不出现) -- 放在 **「期权账户」下方**(依赖期权模块) -- 卡片说明文案建议: - - 永期开仓还要求「交易执行」里计仓模式为 **全仓**(`POSITION_SIZING_MODE=full_margin`) - - 真实下单还与「交易所与实盘」→ `LIVE_TRADING_ENABLED`、本卡片 `HEDGE_PLAN_LIVE_ORDER` 同时开启 - - `HEDGE_PLAN_ENABLED` 关闭时隐藏顶栏「对冲计划」并拒绝启动计划 - -### 12.2 本分组字段(前端可配) - -| 变量 | 前端标签 | 默认 | 控件 | 热更新 | 说明 | -|------|----------|------|------|--------|------| -| `HEDGE_PLAN_ENABLED` | 启用对冲计划 | false | bool | 热更优先 | 总开关:导航 + API | -| `HEDGE_PLAN_LIVE_ORDER` | 允许对冲真实下单 | false | bool | 热更 | 关则只测算/草稿 | -| `HEDGE_PLAN_OPEN_ORDER` | 永期开仓顺序 | options_first | select:`options_first`/`perp_first` | 热更 | 默认先期权后永续 | -| `HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS` | 永期止损后强制平期权 | true | bool | 热更 | **保护机制,默认 true** | -| `HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS` | 永期止盈后强制平期权 | false | bool | 热更 | **默认 false,保险腿不平** | -| `HEDGE_PLAN_OO_CLOSE_WINNER_ONLY` | 期期只平盈利腿 | true | bool | 热更 | 达目标价只平盈利方 | -| `MAX_ACTIVE_HEDGE_PLANS` | 最大同时活跃计划数 | 1 | number | 热更 | 建议保持 1 | -| `HEDGE_PLAN_MONITOR_POLL_SECONDS` | 对冲监控轮询(秒) | 15 | number | 热更 | 侦测 TP/SL/目标价 | -| `HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION` | 半腿失败时自动平期权 | true | bool | 热更 | 期权成、永续败时的补偿 | - -**不放入本分组、沿用已有卡片:** - -| 已有位置 | 键 | 对冲用途 | -|----------|-----|----------| -| 交易执行 | `POSITION_SIZING_MODE` | 全仓才允许永期开仓 | -| 交易执行 | `FULL_MARGIN_BUFFER_RATIO` | 默认 0.98 | -| 交易执行 | `BTC_LEVERAGE` | BTC/ETH 档(默认 10) | -| 交易所与实盘 | `LIVE_TRADING_ENABLED` | 总实盘门 | -| 期权账户 | `OKX_OPTIONS_*` | 期权 API、预算、标的 | - -### 12.3 `.env.example` 片段(实现时写入 OKX) - -```env -# --- 对冲计划(仅 OKX;前端 env「对冲计划」) --- -HEDGE_PLAN_ENABLED=false -HEDGE_PLAN_LIVE_ORDER=false -HEDGE_PLAN_OPEN_ORDER=options_first -HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS=true -HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS=false -HEDGE_PLAN_OO_CLOSE_WINNER_ONLY=true -MAX_ACTIVE_HEDGE_PLANS=1 -HEDGE_PLAN_MONITOR_POLL_SECONDS=15 -HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION=true -``` - -### 12.4 编码触点 - -| 文件 | 改动 | -|------|------| -| `lib/env/env_ui_manifest.py` | `_HEDGE_PLAN_SECTION`,`exchanges={"okx"}`,接入 `ui_sections_for_exchange` | -| `crypto_monitor_okx/.env.example` | 增加上表键与分组注释 | -| `lib/env/env_schema.py` | 纳入 `HOT_RELOAD_EXACT`(及必要时重启列表) | -| `docs/env配置说明.md` | 上线时补「对冲计划」小节 | - -推荐分期:**P0 先做 env 白名单 + 开关可读**,页面按 `HEDGE_PLAN_ENABLED` 显隐导航. - ---- - -## 13. 前端与路由(实现要点) - -- 导航:OKX 实例顶栏 **「对冲计划」**(display prefs 可加 `show_nav_hedge_plan`). -- 路由建议:`/hedge-plan`(主)、`/hedge-plan/history`、`/hedge-plan/stats`(或单页 Tab). -- 模板/静态:`lib/options` 旁新增 `lib/hedge_plan/`(或 `lib/hedge/`),复用: - - 期权链 CSS/T 型渲染思路(`options_panel.js` / `options-strike-table--t`) - - `compute_full_margin_sizing` / `position_sizing_lib` - - 永续下单 + TPSL、期权限价开平 API - -API 草图: - -- `GET /api/hedge-plan/market` — 永续行情 + 全仓试算 -- `GET /api/hedge-plan/options-chain` — 期权链 -- `POST /api/hedge-plan/preview` — 情景测算 -- `GET /api/hedge-plan/list` / `history` / `` / `stats` -- `PATCH /api/hedge-plan//note` — 复盘短评 -- `POST /api/hedge-plan` — 保存草稿/启动 -- `POST /api/hedge-plan//cancel|close` - -非全仓请求启动永期 → `400` 明确:「永期对冲开仓仅全仓模式可用」. - ---- - -## 14. 分期实施 - -| 阶段 | 交付 | 验收 | -|------|------|------| -| **P0** | env「对冲计划」分组 + 页框 + 行情 + 永期列表/期期 T + 情景测算 + 门禁 | env 可改开关;非全仓无法启动永期;全仓可看建议张数 | -| **P1** | 表结构 + 草稿/列表 | DB 可查 | -| **P2** | 永期自动开仓(全仓) | 双腿成交入计划 | -| **P3** | 监控:止损强制平期权;止盈不平期权 | 用例测 TP/SL 分支 | -| **P4** | 期期开仓 + 目标价只平盈利腿 | 达价仅平一侧 | -| **P5** | 历史列表列 + 详情复盘(note) + 统计看板 + 微信 | 与 /records /stats 隔离;止损/止盈文案可区分 | - -建议顺序严格;P3 规则错误会误平保险腿,上线前用 dry-run / paper flags. - ---- - -## 15. 与现有文档关系 - -| 文档 | 关系 | -|------|------| -| [期权对冲方案分析.md](./期权对冲方案分析.md) | 策略观念;本模块是其「计划化 + 自动执行」实现 | -| [对冲计划策略与P0校验.md](./对冲计划策略与P0校验.md) | P0 交付与口径校验 | -| [期权方案.md](./期权方案.md) / [期权用法.md](./期权用法.md) | 期权 API、仅买方、限价规则必须遵守 | -| [期权开平仓与监控说明.md](./期权开平仓与监控说明.md) | 买一平仓、门控、监控与风险;线上 `/options/guide` | -| [position-sizing-mode.md](./position-sizing-mode.md) | 全仓公式与缓冲 0.98 | - -本模块上线后,可在《期权对冲方案分析》末尾增加「系统对冲计划」链接指向本文. - ---- - -## 16. 已拍板规则摘要(校验清单) - -- [x] 放在 **OKX 实例**,名 **永期对冲 / 期期对冲** -- [x] 永期止盈 → **期权不强制平**,但 **计划算结束**;统计 = **止盈盈利 − 权利金** -- [x] 永期止损 → **期权必须强制平**,计划结束;统计 = **期权盈利 − 永续亏损** -- [x] 期期按目标价 → **只平盈利方**;到期无盈利 → **算结束并统计总亏损** -- [x] 对冲计划 **开始与结束均企业微信推送** -- [x] **独立历史 + 独立统计 + 计划详情复盘**(短评 note;不进普通交易复盘) -- [x] 永期开仓 **仅全仓**;非全仓永期只算账 -- [x] 非全仓允许 **期期**开仓;全仓永期+期期均可 -- [x] 永期张数:**ETH/BTC 10x 全仓 × 0.98**,先算盈亏再选期权 -- [x] 行情自动;永期期权 **列表式**;期期 **T 型** -- [x] 对冲开关在前端 **env 配置 →「对冲计划」** 维护(仅 OKX);计仓/杠杆/期权 API 复用已有分组 -- [x] 期权腿实盘平仓:**禁市价、只锁买一、分批;流动性/2×门控见开平仓说明** - ---- - -## 17. 免责与边界 - -- 双账户非原子成交,存在半腿风险. -- 止盈结束账上按权利金全额计成本;期权若仍持仓,后续行情 **不再改计划合计**,属设计意图. -- 止盈后期权可能继续损耗直至到期,与「计划已结束」并存. -- 买方权利金可能全部损失;期期到期无盈利记总亏损,属设计意图. -- 本文不构成投资建议. diff --git a/docs/对冲计划策略与P0校验.md b/docs/对冲计划策略与P0校验.md deleted file mode 100644 index 04258fd..0000000 --- a/docs/对冲计划策略与P0校验.md +++ /dev/null @@ -1,109 +0,0 @@ -# 对冲计划策略说明与 P0 校验 - -> 配套实现方案:[对冲计划开发方案.md](./对冲计划开发方案.md) -> 本文记录 **策略口径** 与 **P0 代码校验**,确认可继续 P1+. - ---- - -## 1. 策略摘要 - -| 类型 | 账户 | 作用 | -|------|------|------| -| **永期对冲** | 永续子账户 + 期权主账户买方 | 全仓做方向,期权买保险 | -| **期期对冲** | 仅期权主账户双买方 | 目标价兑现盈利腿,亏损腿到期 | - -### 永期结束与统计 - -| 事件 | 期权处理 | 计划 | 统计公式 | -|------|----------|------|----------| -| 止盈 | 不强平 | **结束** | **止盈盈利 − 权利金** | -| 止损 | **强制平** | **结束** | **期权盈利 − 永续亏损**(有符号相加) | - -### 期期结束与统计 - -| 事件 | 处理 | 统计 | -|------|------|------| -| 达目标价 | 只平盈利腿 | 待亏损腿到期后结账 | -| 到期无盈利 | **结束** | **总亏损** | - -起止均企业微信推送(P2+ 监控落地后再接). - -### 门禁 - -- 永期开仓:**仅** `POSITION_SIZING_MODE=full_margin` -- 非全仓:永期可测算不可开;期期 P0 起可测算(开仓后续版本) -- env:OKX「对冲计划」分组;须 `HEDGE_PLAN_ENABLED=true` 才显示导航 - ---- - -## 2. P0 已交付 - -| 项 | 状态 | -|----|------| -| env 白名单「对冲计划」9 字段 | 有 | -| OKX `.env.example` 字段 | 有 | -| `/hedge-plan` 页(永期列表 / 期期 T) | 有 | -| `/api/hedge-plan/market|options-chain|preview|gates` | 有 | -| 全仓建议张数(可用×0.98×10x) | 有 | -| 情景测算止盈/止损口径 | 有 | -| 启动开仓 | **禁用**(明示 P0) | -| 历史/统计/监控/微信 | **未做**(P1–P5) | - -关键文件: - -- `lib/hedge_plan/hedge_plan_calc_lib.py` -- `lib/hedge_plan/hedge_plan_register.py` -- `lib/hedge_plan/templates/hedge_plan_panel.html` -- `lib/common/static/hedge_plan.js` -- `tests/test_hedge_plan_calc.py` - ---- - -## 3. 校验清单(可行性) - -### 计算口径 - -``` -止盈: perp_pnl(tp) - premium -止损: option_expiry_pnl(spot=sl) + perp_pnl(sl) -期期到期无盈利: expiry_flat_total <= 0 → 记总亏损 -``` - -单测覆盖:`tests/test_hedge_plan_calc.py`. - -### 门禁 - -- `risk` + 永期 → `can_start=false`,文案含「全仓」 -- `HEDGE_PLAN_ENABLED=false` → 导航隐藏(服务端 template 读 env) - -### 行情 - -- 永续:`exchange.fetch_ticker` + `get_available_trading_usdt` + `compute_full_margin_sizing` -- 期权:`build_option_chain`(与期权页同源) - -### 已知边界(非 P0 bug) - -1. 建议张数未强制 `amount_to_precision`(开仓阶段再对齐交易所精度). -2. 止盈账扣全额权利金,与期权是否仍持仓无关(策略如此). -3. 热更新 `HEDGE_PLAN_ENABLED` 后需刷新页面才显隐导航. -4. 嵌入壳 Tab 已注册 `hedge_plan`;中控能力勾选若需显式「对冲」可后续加. - ---- - -## 4. 服务器启用步骤 - -```bash -# env 配置页 → 对冲计划 → HEDGE_PLAN_ENABLED=true → 保存 -# 或服务器: -cd /opt/crypto_monitor_user/crypto_monitor_okx -# 确保 .env 含 HEDGE_PLAN_* 字段后 -pm2 restart crypto_okx --update-env -``` - -验收: - -1. 顶栏出现「对冲计划」 -2. 永期可见建议张数(全仓时) -3. 点「计算」得到止盈/止损合计 -4. 「启动计划」禁用 -5. env 页可见「对冲计划」分组 diff --git a/docs/更新文档.md b/docs/更新文档.md deleted file mode 100644 index 3d85bc5..0000000 --- a/docs/更新文档.md +++ /dev/null @@ -1,464 +0,0 @@ -# 更新文档(仓库级) - -自 2026-07-16 起:**凡修改或更新功能,必须在本文件追加一条记录**,写明原因、改动位置、目标与交付验收。实例目录下旧版说明可保留,但共享逻辑(`lib/`)以本文为准。 - ---- - -## 2026-07-17 · 修复 pip>=26 部署依赖安装失败 - -### 修改原因 - -单所验证时 `setup_env` 升级到 pip 26 后,`--progress-bar ascii` 非法,依赖安装中断;腾讯源偶发空索引也会导致一次失败. - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `deploy/lib/common.sh` | `pip_progress_bar_arg` 改为 `on`(兼容 pip 26);`pip install` 加 `--retries 5` | - -### 达成的目标 - -非 TTY / SSH 下一键安装可顺利 `pip install -r`. - -### 交付之后的验收 - -`bash deploy/lib/install.sh --exchange okx` 能过依赖安装并起 `crypto_okx`. - ---- - -## 2026-07-17 · 一键部署支持单所仅实例(不含中控) - -### 修改原因 - -新机或专用机只需跑某一所 Flask 时,全套 7 进程过重;希望菜单可直接选「仅 OKX / Binance / Gate」,不起中控与 agent. - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `deploy/manage.sh` | 菜单增加 4/5/6 单所实例入口 | -| `deploy/lib/install.sh` | `--exchange okx\|binance\|gate` 单所流水线 | -| `deploy/pm2_start_all.sh` | `--only` 只启对应 ecosystem | -| `deploy/lib/common.sh` | 单所验收 / 完成提示 / 辅助映射 | -| `deploy/README.md` | 菜单说明 | - -### 达成的目标 - -1. 选 4/5/6: `setup_env --only <所>` + 仅启动该所 PM2,不含 hub/agent. -2. 选 1: 全套行为与改前一致. -3. 仍共用整仓 `/opt/crypto_monitor_user`,不拆仓库. - -### 交付之后的验收 - -1. 菜单可见 4/5/6. -2. `bash deploy/lib/install.sh --exchange okx` 后仅 `crypto_okx` 起来,`:5004` 可访问. -3. 全套选项 1 仍可部署三所+中控. - ---- - -## 2026-07-17 · 永续估算盈亏统一扣双边 taker 手续费 - -### 修改原因 - -中控/实例「盈利金额」、微信推送「本单盈亏」、交易记录 `pnl_amount` 使用价差毛利,未扣开平手续费,与交易所实际净盈亏及盈亏比体感偏差较大。 - -### 定稿口径 - -| 项 | 约定 | -|----|------| -| 浮盈亏 | 仍读交易所,不改 | -| 费率 | taker 单边 **0.05%**(`PERP_TAKER_FEE_RATE`,默认 `0.0005`),开+平双边 | -| 净盈亏 | 毛利 − 开仓名义×费率 − 平仓名义×费率(不考虑滑点) | -| RR | 净盈利 / 原风险(风险侧加费第二步再做) | -| 历史记录 | 不回算 | - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `lib/trade/trade_fee_lib.py` | 新增公共扣费 / 净盈亏 | -| `lib/strategy/strategy_roll_ui_lib.py` | `reward_at_tp_usdt` → 净盈利 | -| `lib/strategy/strategy_roll_lib.py` | 同上 | -| `lib/strategy/strategy_trend_lib.py` | `calc_tp_profit_usdt` → 净盈利 | -| `lib/hub/hub_calculator_lib.py` | 滚仓预览止盈盈利 / 首仓盈利扣费;RR 跟净盈利 | -| `crypto_monitor_okx/app.py` | `calc_pnl` → 净盈亏(推送/记账) | -| `crypto_monitor_gate/app.py` | 同上 | -| `crypto_monitor_binance/app.py` | `calc_pnl` / 成交回退扣费;income 真费路径优先不改 | -| `tests/test_trade_fee_lib.py` | 新增 | -| `tests/test_strategy_roll_ui_lib.py` | 断言改净额 | -| `tests/test_order_monitor_display_lib.py` | 断言改净额 | - -### 达成的目标 - -1. 中控持仓「盈利金额」、实例「盈利金额」、计算器止盈盈利、趋势/滚仓预览一致为净盈亏。 -2. 微信推送与新建交易记录的 `pnl_amount` 与上述估算口径一致。 -3. 币安若能拉到 income 净额(已含真实手续费)仍优先用交易所数。 -4. 浮盈亏展示仍跟交易所。 - -### 交付之后的验收 - -1. 同一笔持仓:中控盈利金额 ≈ 实例盈利金额(均为扣费后)。 -2. 平仓推送「本单盈亏」与新写入记录接近,不再明显大于交易所净利。 -3. 浮盈亏与交易所 App 一致(本改不动)。 -4. 单测:`python -m unittest tests.test_trade_fee_lib tests.test_strategy_roll_ui_lib tests.test_order_monitor_display_lib tests.test_trend_preview_tp -v` 通过。 - ---- - -## 2026-07-16 · 计算器左侧 Tab + 三行输入 - -### 修改原因 - -电脑端顶部横向 Tab 与纵向表单不协调,输入项行数偏多。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/index.html` | 增加计算器工作区容器,更新 CSS 缓存 | -| `manual_trading_hub/static/app.css` | 电脑端 Tab 改为左侧竖排;≥1200px 基础输入改为五列、三行排列 | - -### 达成的目标 - -电脑端左侧切换计算器,右侧集中填写;宽屏基础输入压缩为三行。 - -### 交付之后的验收 - -1. 电脑端两个计算器 Tab 位于左侧。 -2. 1920px 宽屏基础输入区为三行。 -3. Tab 切换和计算功能正常;手机端保持原布局。 - ---- - -## 2026-07-16 · 电脑端计算器改为 Tab 切换 - -### 修改原因 - -电脑端同时并排显示趋势回调与滚仓计算器,横向空间利用和操作聚焦不理想。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/index.html` | 计算器 Tab 增加电脑端完整名称,更新 CSS 缓存 | -| `manual_trading_hub/static/app.css` | 电脑端显示 Tab、单列展示当前计算器;手机端沿用原紧凑 Tab | - -### 达成的目标 - -电脑端通过「趋势回调计算器 / 滚仓计算器」Tab 切换,一次只显示一个计算器。 - -### 交付之后的验收 - -1. 电脑端默认显示趋势回调计算器。 -2. 点击滚仓计算器 Tab 后只显示滚仓计算器,切回正常。 -3. 手机端原有计算器 Tab 样式和交互不变。 - ---- - -## 2026-07-16 · 资金概况移除累计盈亏长条 - -### 修改原因 - -「同步快照」后方的累计盈亏长条与下方汇总卡片重复,占用横向空间。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/index.html` | 删除资金工具栏中的累计盈亏/较昨日长条 | - -### 达成的目标 - -资金概况工具栏仅保留「同步快照」和状态信息,累计盈亏继续由下方汇总卡展示。 - -### 交付之后的验收 - -1. 「同步快照」按钮后不再显示累计盈亏长条。 -2. 下方累计盈亏、较昨日汇总卡数据正常显示。 - ---- - -## 2026-07-16 · 今日统计默认折叠 + 交易所标题行下移 - -### 修改原因 - -今日统计常占一行挤空间;默认只需看总浮盈亏。交易所卡标题/打开实例贴顶过紧。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | 今日统计默认折叠只露总浮盈亏;展开显示明细;状态写入 localStorage | -| `manual_trading_hub/static/app.css` | 折叠/展开样式;展开后明细字号加大;分栏卡 `card-head` 上内边距加大 | -| `manual_trading_hub/static/index.html` | 缓存 `20260716-hub-stats-fold` | - -### 达成的目标 - -监控区默认更省高;需要时一键展开更大明细;交易所标识行不再贴顶。 - -### 交付之后的验收 - -1. 默认只见「今日统计 + 总浮盈亏」与「展开明细」。 -2. 点展开后六项明细可见且数字更大。 -3. OKX/币安等卡标题与按钮相对顶边有更明显间距。 - ---- - -## 2026-07-16 · 监控芯片还原 + 底栏贴底 + 1080p 留白 - -### 修改原因 - -「监控位」本意是原先关键位/趋势回调/顺势加仓芯片,不是单独「无监控位」槽;全屏提示须贴卡片最底;1920×1080 两侧需留白,带鱼屏保持现宽。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | 去掉错误的监控位槽;恢复策略芯片(仍隐藏期权 N仓) | -| `manual_trading_hub/static/app.css` | 分栏卡 `card-expand-hint` `margin-top:auto` 贴底;≤2000px 加大左右留白,>2000px 保持 1860 内容宽 | -| `manual_trading_hub/static/index.html` | 缓存 `20260716-hub-chips-margin` | - -### 达成的目标 - -有关键位/趋势/顺势时仍以芯片显示;提示条在卡底;1080p 两侧有留白,带鱼屏观感不变。 - -### 交付之后的验收 - -1. 无「监控位 · 0 / 无监控位」区块;有关键位等时出现原芯片样式。 -2. 「点击标题栏进入全屏…」贴在各分栏卡最底部。 -3. 1920×1080 内容两侧留白明显;带鱼屏内容宽度仍约 1860。 - ---- - -## 2026-07-16 · 监控区 2×2 细化(目标监控列/预留行/去平板专属) - -### 修改原因 - -四卡对齐后需:左右等宽;期权表改目标监控列;去掉打开期权页与永续卡「期权 N仓」;合约卡预留仓位+监控位两行并统一全屏提示;多仓时同行左右一起长高;平板改用浏览器 80% 缩放,去掉 hub-tablet 专属样式(手机 UI 不动)。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | 平铺 `monitor-split-2x2`;期权表删指数/到期平衡/平掉回本,加目标监控列(有=绿/无=`—`);去打开期权页;永续卡隐藏期权徽章;预留仓位/监控位槽;去掉 `isTabletLayout` | -| `manual_trading_hub/static/app.css` | 左右 1:1;`minmax(min-content,1fr)` 同行同高可外扩;删除全部 `hub-tablet` 规则 | -| `manual_trading_hub/static/index.html` | 缓存 `20260716-hub-2x2-slots` | - -### 达成的目标 - -桌面监控区四卡等宽对齐;期权看目标监控列;永续卡结构为仓位行+监控位行+提示行;≥2 仓左右一起加高;平板不再走单独 CSS。 - -### 交付之后的验收 - -1. 左右列等宽;永续卡无「期权 1仓」、无「打开期权页」。 -2. 期权表有「目标监控」列,有监控绿色、无则 `—`;无指数/到期平衡/平掉回本。 -3. 永续/币安卡可见预留仓位行、监控位行与完整全屏提示文案。 -4. 多开仓后该行变高且左右同高;`body` 无 `hub-tablet`;手机布局未改。 - ---- - -## 2026-07-16 · OKX 拆成永续/期权双卡(2×2 对齐) - -### 修改原因 - -OKX 单卡内嵌永续+期权过高,右侧币安/Gate 两卡对不齐,1080p 观感不协调。按产品建议改为四卡对齐。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | 期权分栏时 OKX 渲染为「·永续」「·期权」两张独立卡;标题与操作按钮按卡片分流 | -| `manual_trading_hub/static/app.css` | 左右列均 `1fr/1fr`,四卡 2×2 等高对齐;卡体内滚 | -| `manual_trading_hub/static/index.html` | 静态资源缓存 `20260716-hub-okx-2x2` | - -### 达成的目标 - -监控区呈现:左上永续 / 左下期权 / 右上币安 / 右下 Gate,四卡对齐。 - -### 交付之后的验收 - -1. 桌面监控区可见 `OKX_趋势 · 永续` 与 `OKX_趋势 · 期权` 两张独立卡。 -2. 四卡与右侧币安、Gate 同行等高,不再一大两小。 -3. 点击任一张 OKX 卡标题仍可全屏;期权卡「全平」不重复出现。 - ---- - -## 2026-07-16 · 1080p 监控区无持仓空洞收紧 - -### 修改原因 - -1920×1080 上一屏适配用 `1fr/1fr` 把永续/分所空卡强行均分拉高,「无持仓」下方大片空洞;带鱼屏尚可,短屏观感差。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.css` | OKX 内卡改为 `auto + 1fr`(永续按内容、期权吃剩余);右侧 Gate/币安改为 `auto auto` 按内容收紧;1080p 短屏左侧略加宽 | -| `manual_trading_hub/static/index.html` | CSS 缓存 `20260716-hub-1080-tight` | - -### 达成的目标 - -无持仓区块不再被拉成半屏空黑;有期权/持仓的区域拿到更多可视高度。 - -### 交付之后的验收 - -1. 1920×1080 监控区:永续「无持仓」仅占内容高度,期权表区域明显变高。 -2. 右侧币安/Gate 无仓时卡片贴内容,不再卡片内大片空洞。 -3. 带鱼屏布局仍可用;持仓变多时右侧列可内滚。 - ---- - -## 2026-07-16 · 电脑端误判平板导致发糊 - -### 修改原因 - -`isTabletLayout()` 曾用「高度 ≤920 即平板」;1080p 电脑有任务栏/浏览器栏时 `innerHeight` 常落在此区间,桌面被套上平板压缩字号(约 10px),观感发糊发虚。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | 平板判定改为横屏 `721–1366 × ≤900` / 竖屏 `≤920 × ≥900`;去掉仅按高度命中 | -| `manual_trading_hub/static/index.html` | JS 缓存 `20260716-hub-desktop-clear` | - -### 达成的目标 - -常规电脑端不再误加 `hub-tablet`,恢复桌面字号与清晰度;真平板视口仍走一屏密度样式。 - -### 交付之后的验收 - -1. 1080p/1440p 电脑打开中控,正文与表格清晰,非异常小字。 -2. 浏览器开发者工具确认 `body` 无 `hub-tablet`(窄窗模拟平板除外)。 -3. 平板横/竖仍为一屏密度布局。 - ---- - -## 2026-07-16 · 平板一屏密度适配(不裁切) - -### 修改原因 - -平板字号偏大、留白空、卡片半截裁切或底部大片空黑,显得 low;需要「一屏看完」且正文不被拦腰裁掉。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.css` | `hub-tablet`:锁 100dvh;压缩字号/间距;监控区 flex 填满;竖屏 OKX 上 + Gate/币安并排;表体内滚兜底;资金页缩曲线、压汇总数字、分户卡完整可见 | -| `manual_trading_hub/static/index.html` | CSS 缓存 `20260716-hub-tablet-onescreen` | - -### 达成的目标 - -平板监控区/资金概况一屏呈现、信息密度接近桌面;常规持仓与分户名称/余额不被裁半;持仓很多时仅表体内滚。 - -### 交付之后的验收 - -1. 平板强制刷新后,监控区三所卡片同屏,余额与持仓列完整可读。 -2. 资金概况:四格汇总 + 曲线 + 分户卡同一屏,账户名不被切半。 -3. 底部不再大片空黑;桌面大屏一屏规则不受影响。 - ---- - -## 2026-07-16 · 平板监控卡片内容裁切修复(已由一屏密度方案取代) - -先前改为整页可滚以避免裁切;用户要求改为「一屏 + 不裁切 + 提密度」,由上一条覆盖。 - ---- - -## 2026-07-16 · 中控平板一屏适配(2560×1600) - -### 修改原因 - -平板物理分辨率 2560×1600 在 2× 缩放下 CSS 视口约为 **1280×800**,进不了此前桌面规则 `min-width:1600px`,监控/行情/资金仍整页滚动。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | 新增 `isTabletLayout()`;`hub-tablet` body class | -| `manual_trading_hub/static/app.css` | 一屏适配门槛降为 `min-width:721px` + `min-height:650px`;另增平板矮/窄视口加密度规则 | -| `manual_trading_hub/static/index.html` | 缓存版本 `20260716-hub-fit-tablet` | - -### 达成的目标 - -平板(含 2560×1600@2x)与桌面一样:监控区 / 行情区 / 资金概况尽量一屏、无整页下拉。 - -### 交付之后的验收 - -1. 平板横屏打开中控,强制刷新后 body 应有 `hub-tablet`(开发者工具)。 -2. 监控区(OKX 一期权 ± 永续空/一仓,另两所各 ≤1 仓):无整页纵向滚动。 -3. 行情区、资金概况同屏无整页滚动。 -4. 手机(≤720px)仍走 `hub-phone`,不受影响。 - ---- - -## 2026-07-16 · 中控三页 1920×1080 一屏显示 - -### 修改原因 - -监控区在 OKX「一永续 + 一期权」、另两所各一仓(或空仓)时,以及行情区、资金概况在 1920×1080 下出现整页纵向滚动,无法一屏看完。 - -### 修改的地方 - -| 文件 | 改动摘要 | -|------|----------| -| `manual_trading_hub/static/app.js` | `setActiveNav` 增加 `hub-page-monitor` / `hub-page-market` body class | -| `manual_trading_hub/static/app.css` | `@media (min-width:1600px) and (min-height:900px)` 一屏适配:壳层 `100dvh` 不滚动;三页 flex 填满;监控卡片/表格压缩;行情 K 线区 flex 吃剩余高度;资金曲线与分户区压缩 | -| `manual_trading_hub/static/index.html` | `app.css` / `app.js` 缓存版本 `20260716-hub-fit-1080` | - -**未改:** 期权开平仓规则、实例交易页、手机端 `hub-phone` 布局。 - -### 达成的目标 - -1. **监控区**:桌面大屏下整页无纵向滚动;OKX 左右分栏(永续+期权)与 Binance/Gate 同屏可见。仓位表过长时仅卡片内部滚动。 -2. **行情区**:工具条 + K 线同屏,图表占满剩余高度。 -3. **资金概况**:统计卡 + 曲线 + 三分户同屏。 - -### 交付之后的验收(1920×1080,浏览器缩放 100%) - -1. 打开中控监控区:服务器状态与操作栏保持折叠时,页面**无**浏览器纵向滚动条;OKX 期权 1 仓 + 另两所空仓/各 1 仓均一屏可见。 -2. 行情区:加载 BTC 日线后,OHLCV + 图在一屏内,无整页滚动。 -3. 资金概况:曲线与三分户卡片同屏,无整页滚动。 -4. 窄屏/手机(`hub-phone`)布局不受影响。 -5. 展开「服务器状态」后若内容过高,允许监控网格内部滚动,仍尽量避免整页滚动。 - ---- - -## 2026-07-16 · 期权/对冲开仓仅认真实卖一深度 - -### 修改原因 - -此前报价在无盘口卖一时会用**标记价顶进 `ask`**,界面仍显示「限价买入 @ 卖一」,造成误以为在吃卖一;深度实值合约还容易「链上有 `~` 价、点选却失败或按估算价下单」。需要与产品规则对齐:**开仓只吃真实卖一,且必须有卖一量**。 - -### 修改的地方(明确清单) - -| 文件 | 改动摘要 | -|------|----------| -| `lib/exchange/okx_options_lib.py` | 新增 `option_buy_liquidity_ok` / `cap_option_buy_sheets_to_ask_depth`;`quote_option_contract` **不再**用 mark 填充开仓 `ask`;返回 `can_open` / `ref_ask` / `open_block_msg` / `ask_source` | -| `lib/options/options_register.py` | `/api/options/quote` 开仓 sizing 仅在 `can_open` 时计算,张数 cap 到卖一深度;`/api/options/open` 服务端再验深度并 cap 张数 | -| `lib/common/static/options_panel.js` | 面板展示参考标记价;无深度禁用开仓按钮与说明文案;开仓前校验 `can_open` | -| `lib/options/templates/options_panel.html` | 提示文案;增加「参考标记价」字段;脚本 `?v=37` | -| `lib/hedge_plan/hedge_plan_orders_lib.py` | **仅** `_buy_option`(买入开仓)同步深度门禁与张数 cap;**未改** `_sell_option` 平仓 | -| `lib/hedge_plan/templates/hedge_plan_panel.html` | 单位说明补充开仓规则 | -| `tests/test_option_buy_liquidity.py` | 新增门禁/深度 cap 单测 | -| `tests/test_hedge_plan_orders.py` | mock 补 `ask_sz`;无深度拒绝 / 深度 cap 用例 | - -**铁律:未改动任何平仓规则**(期权买一平仓、`_sell_option`、close_preview / close 执行路径逻辑保持原样;报价里买一仍可用 mark 补展示,仅服务平仓读 bid)。 - -### 达成的目标 - -1. 开仓条件:`askPx` 有效 **且** `askSz > 0`。 -2. 无卖一/无深度:可展示参考标记价 `ref_ask`/`mark`,明确「不可用于开仓」,按钮禁用。 -3. 有深度时:限价买 @ 真实卖一;张数不超过卖一深度(向下取整)。 -4. 期权面板与对冲计划买入路径规则一致。 - -### 交付之后的验收 - -1. **有卖一深度**:选合约 → 卖一显示 `价/量` → 按钮「限价买入 @ 卖一」可点 → 下单张数 ≤ 卖一量。 -2. **无卖一或深度为 0**:卖一为 `—`;参考标记价显示 `~xx (不可开仓)`;红色说明含「仅供参考,不可用于开仓」;按钮为「暂无卖一深度,无法开仓」且不可点;直接调 open API 应返回失败文案。 -3. **链上 `~` 估算**:仍可浏览;点选后若无真实深度,不得用估算价成交。 -4. **平仓**:持仓「买一平仓」行为与改前一致(抽测一条即可)。 -5. **对冲计划**:执行买入腿时无深度应失败并提示;有深度 dry_run/实盘张数不超过卖一量。 -6. 单测:`python -m pytest tests/test_option_buy_liquidity.py tests/test_hedge_plan_orders.py -q` 通过。 - -### 未纳入本次(另单) - -硬刷新链可能毁掉下单面板、限频 fallback tick、CSS `?v=` 缓存等,见会话审计清单,不在本条范围。 diff --git a/docs/期权对冲方案分析.md b/docs/期权对冲方案分析.md deleted file mode 100644 index 65e7c32..0000000 --- a/docs/期权对冲方案分析.md +++ /dev/null @@ -1,322 +0,0 @@ -# 期权对冲方案分析 - -> 适用范围:OKX **永续子账户**(USDT 本位) + **期权主账户**(USDⓈ 本位买方). -> 本文档为 **策略与操盘说明**,非系统自动下单功能;组合须 **人工** 在永续页与期权页分别执行. - ---- - -## 1. 前提与账户分工 - -| 维度 | 永续合约(子账户) | 期权(主账户) | -|------|------------------|--------------| -| API | `OKX_API_*` | `OKX_OPTIONS_API_*` | -| 系统页面 | 实盘下单 / 关键位 / 策略 | 期权 | -| 保证金 | USDT | USDC / USDG | -| 本系统能力 | 开平仓、止损、关键位 | **仅买方** 开平仓,无组合单 | - -**重要约束:** - -- 两套资金 **不自动合并**;对冲是逻辑上的组合,资金与下单 **分账户** 管理. -- 系统 **不支持** 卖方开仓、跨式组合单、自动 Delta 调仓;下文操盘均为 **手工流程**. -- 期权模块默认:≤2 日到期、轻度实值、单笔权利金预算(如 10 USDC).对冲设计需与此预算协调. - ---- - -## 2. 期权 + 永续合约对冲 - -指用 **期权买方头寸** 去对冲或替代 **永续方向敞口**,降低单边暴跌/暴涨带来的尾部风险,或锁定部分利润. - -### 2.1 使用场景 - -| 场景 | 永续侧状态 | 期权侧动作 | 目的 | -|------|------------|------------|------| -| **保护性 Put** | 子账户 **多头** 永续(策略/关键位持仓) | 主账户买入 **看跌 Put**(轻度实值、近月) | 大跌时 Put 升值,抵消永续浮亏 | -| **看涨保险** | 子账户 **空头** 永续 | 主账户买入 **看涨 Call** | 急涨时 Call 升值,限制空头亏损 | -| **事件前对冲** | 永续有仓,临近 FOMC/CPI/非农 | 临时买 **Put 或 Call**(方向视净敞口) | 降低数据公布瞬间波动伤害 | -| **锁利减仓** | 永续浮盈较大,不愿全平 | 买 **反向期权** 作「便宜保险」 | 保留永续吃趋势,用权利金买回撤保护 | -| **替代硬止损** | 永续止损易被扫或滑点大 | Put/Call 权利金 ≈ 可接受最大额外损失 | 用期权时间价值换「软止损」 | - -**不适合:** - -- 长期持仓 + 远期期权(本系统偏 ≤2 日 DTE,时间价值流失快). -- 指望「完全对冲」零波动:买方期权有 **Theta 衰减**,永续有 **资金费**,不可能完美镜像. -- 资金不足:期权需先 **USDT→USDC 兑换 + 划转到交易户**,再下单. - -### 2.2 案例分析 - -#### 案例 A:保护性 Put(多头永续 + 买 Put) - -**背景** - -- 子账户 ETH 永续 **多 0.5 ETH**,均价 3,200,浮盈 +5%. -- 当晚有宏观数据,担心 30 分钟内急跌 3%~8%. - -**操作** - -1. 永续:维持多单,或略减杠杆(系统「实盘下单」/策略仓). -2. 期权主账户:USDT→USDC→划转到交易户. -3. 期权页:ETH **看跌 Put**,行权价略低于现价(轻度实值),1~2 日到期. -4. 按预算 10 USDC 买满(约 0.0x ETH 名义),记录权利金 **C**. - -**结果推演** - -| ETH 走势 | 永续盈亏(示意) | Put 盈亏(示意) | 组合效果 | -|----------|----------------|----------------|----------| -| 横盘 | 小亏资金费 | Put 因 Theta 贬值 | 净成本 ≈ 权利金 C | -| 跌 5% | 永续大亏 | Put 明显升值 | 部分对冲,净亏 < 无 Put | -| 涨 3% | 永续盈利 | Put 接近归零 | 永续盈利 − C | - -**要点:** 买的是 **保险**,不是赚钱工具;权利金 C 是「保费」. - -#### 案例 B:空头永续 + 买 Call - -**背景** - -- 子账户 BTC 永续 **空 0.02 BTC**,判断短期震荡偏空,但担心消息拉升. - -**操作** - -- 买 **看涨 Call**(轻度实值、近月),权利金控制在预算内. - -**结果推演** - -- 下跌:永续盈利,Call 损耗 → 净赚略少于裸空. -- 急涨:永续亏损,Call 盈利 → 涨幅越大对冲越有效. - -#### 案例 C:事件窗口「临时对冲」 - -**背景** - -- 永续多仓持有中,事件前 2 小时不想平仓(怕踏空). - -**操作** - -- 事件前:买小仓位 Put,事件后 1~2 小时内无论盈亏 **平掉 Put**. -- 永续:按原策略止损/止盈,不因期权改变永续规则. - -**要点:** 短期 Put 的 Theta 极快;**事件结束尽快平仓**,避免「保险变持仓」. - -### 2.3 风险评估 - -| 风险类型 | 说明 | 等级 | -|----------|------|------| -| **权利金损耗(Theta)** | 买方期权每天衰减;横盘即亏保费 | 高 | -| **对冲比例不足** | 10U 预算买到的名义远小于永续仓位 | 高 | -| **方向错配** | 多头却买 Call、空头却买 Put | 高 | -| **双账户操作延迟** | 永续已亏,期权尚未成交 | 中 | -| **流动性** | 限价未成交,极端行情无法对冲 | 中 | -| **资金费 vs 保费** | 长期持仓资金费 + 反复买 Put 成本叠加 | 中 | -| **汇率/币种** | 永续 USDT、期权 USDC,汇率波动次要 | 低 | -| **API/权限** | 子账户与主账户密钥混用 | 低(配置隔离即可) | - -**风险量化思路(手工估算):** - -``` -可接受保费上限 ≈ 永续名义价值 × 愿意承担的单日额外损失比例 -例:0.5 ETH × 3,200 × 0.5% ≈ 8 USDC → 与 OKX_OPTIONS_TRADE_BUDGET_USDC 对齐 -``` - -若保费 << 永续风险敞口,属于 **部分对冲**,需心里有数. - -### 2.4 操盘说明(期权 + 永续) - -**准备(每次对冲前)** - -1. 确认子账户永续:**方向、数量、均价、浮盈亏**(实盘顶栏/持仓). -2. 确认主账户期权:**交易户 USDC 余额** 足够(兑换+划转). -3. 明确本次是 **保护多仓(Put)** 还是 **保护空仓(Call)**. - -**执行顺序(推荐)** - -``` -① 永续侧:确认仓位与风控(止损/关键位)已就绪 -② 主账户:资金户 USDT → USDC(币种兑换) → 划转到交易户 -③ 期权页:选标的(ETH/BTC) → 到期日(1~2日) → Call/Put → 轻度实值行权价 -④ 限价买入(卖一),成交后记录:张数、权利金、行权价、到期时间 -⑤ 企业微信:关注翻倍提醒(浮盈≥100%权利金)作为减仓信号,非必须平永续 -``` - -**平仓/退出** - -| 情况 | 永续 | 期权 | -|------|------|------| -| 趋势延续、保险未触发 | 按原策略 | 临近到期或 Theta 损耗大时 **平 Put/Call** | -| 期权浮盈翻倍(系统提醒) | 可选:减永续仓或上移止损 | **平期权锁利**,保留永续 | -| 永续已止损离场 | 无仓 | **立即平期权**,避免裸买权衰减 | -| 事件结束 | 照旧 | 平期权,勿长期持有近月买方 | - -**检查清单** - -- [ ] 永续方向与期权类型匹配(多→Put,空→Call) -- [ ] 权利金 ≤ 预算,且 ≤ 心理「保费」上限 -- [ ] 成交后两边持仓在各自页面可核对 -- [ ] 到期日前 24h 评估是否平仓期权 - ---- - -## 3. 期权 + 期权对冲 - -指 **仅主账户** 内,用两个(或多个)买方期权组合,表达 **波动、方向区间或尾部保护**,不直接动永续仓位. - -> 本系统 **仅买方**;下列组合均为 **买 Call + 买 Put** 或 **不同行权价双买**,不含卖权收权利金策略. - -### 3.1 使用场景 - -| 场景 | 组合结构 | 目的 | -|------|----------|------| -| **Long Straddle(双买)** | 同行权附近 **Call + Put** | 赌大波动(突破),不怕方向 | -| **Long Strangle(宽双买)** | OTM **Call + OTM Put** | 降低成本,赌更大波动才盈利 | -| **风险逆转(买方版)** | 轻度 ITM Call + 轻度 ITM Put(不同行权) | 同时防暴涨暴跌(保费更高) | -| **方向 + 尾部** | 主方向 Call(或 Put) + 反向少量 Put(或 Call) | 主观点明确,反向作灾难保险 | -| **到期滚动** | 近月双买 → 波动未出则平掉 → 换远 1 日 | 控制 Theta,需严格纪律 | - -**不适合:** - -- 预期 **窄幅震荡**(双买最亏 Theta). -- 预算只够一笔 10U(双买需 **两倍保费** 或各减半张数). -- 把双买当「稳赚」:横盘天天亏. - -### 3.2 案例分析 - -#### 案例 D:Long Strangle(赌突破) - -**背景** - -- 认为 ETH 未来 24~48h 将选方向突破,但不确定涨跌. -- 永续不想开仓,仅用期权表达波动观点. - -**操作** - -1. 选 1~2 日到期. -2. 买 **轻度 OTM Call**(行权价略高于现价). -3. 买 **轻度 OTM Put**(行权价略低于现价). -4. 各用约一半预算(如各 5 USDC),总保费 ≈ 10U. - -**盈亏示意** - -| 市场 | 结果 | -|------|------| -| 横盘 | Call、Put 均衰减 → **最大亏全部保费** | -| 大涨 | Call 盈利可能覆盖 Put 亏损 | -| 大跌 | Put 盈利可能覆盖 Call 亏损 | - -**要点:** 需要波动 **幅度** 超过「总保费对应的隐含波动门槛」才划算. - -#### 案例 E:主多观点 + 尾部 Put(期权 + 期权) - -**背景** - -- 强烈看多 24h,但怕黑天鹅砸盘. - -**操作** - -- 70% 预算买 **Call**(进攻). -- 30% 预算买 **Put**(尾部保险). - -**与「永续 + Put」区别** - -- 不占用永续保证金,无资金费. -- 但 **无 Delta 线性收益**:涨得慢可能 Call 仍亏 Theta. - -#### 案例 F:事件双买 - -**背景** - -- 非农数据公布前后 2h. - -**操作** - -- 公布前 30min:Strangle 双买. -- 公布后:波动释放则 **平盈利腿 + 平亏损腿**;勿持仓过夜除非仍看好波动. - -### 3.3 风险评估 - -| 风险类型 | 说明 | 等级 | -|----------|------|------| -| **双倍 Theta** | 两条腿同时衰减 | 很高 | -| **预算分裂** | 单腿名义过小,波动不够覆盖成本 | 高 | -| **行权价选错** | Strangle 过宽,突破仍不够回本 | 高 | -| **执行误差** | 两腿须分别下单,一腿成交一腿未成交 | 中 | -| **IV crush** | 事件后隐含波动率骤降,双买同时贬值 | 中 | -| **仅买方限制** | 无法做卖方收窄成本(如 Iron Condor) | 中 | - -**与永续对冲对比** - -| 维度 | 期权 + 永续 | 期权 + 期权 | -|------|-------------|-------------| -| 资金账户 | 两账户 | 仅主账户 | -| 趋势收益 | 永续线性 | 非线性,近月衰减快 | -| 保费成本 | 永续资金费 + 期权 | 仅期权(常更高) | -| 适合行情 | 有主仓需保 | 无仓赌波动/事件 | - -### 3.4 操盘说明(期权 + 期权) - -**预算拆分建议** - -``` -总预算 B (如 10 USDC) -├── 腿 A(主观点):B × 60%~70% -└── 腿 B(对冲/反向):B × 30%~40% - -双买 Strangle:各 50%,但须接受单腿名义减半 -``` - -**执行顺序** - -1. 先下 **流动性更好的一腿**(通常轻度 ITM 或更近 ATM),减少单腿敞口时间. -2. 再下第二腿;若第二腿限价未成交,评估是否撤单重报或放弃组合. -3. 记录两笔 `options_trades` 对应关系(备注:组合 ID / 事件名). - -**平仓纪律** - -- **时间止损:** 距到期 < 12h 且未盈利 → 考虑双平,避免 Theta 加速. -- **盈利止损:** 组合净浮盈达保费 50%~100% → 可分批平(系统翻倍提醒可作参考). -- **单边平仓风险:** 只平盈利腿会留下裸反向腿,除非有意转方向. - ---- - -## 4. 方案选型简表 - -| 你的状态 | 推荐方案 | 理由 | -|----------|----------|------| -| 子账户已有永续多仓 | **永续 + Put** | 直接保护现有 Delta | -| 子账户已有永续空仓 | **永续 + Call** | 限制逼空风险 | -| 无永续仓,赌大波动 | **期权 Strangle** | 不付资金费,纯波动 | -| 有方向观点,不想开永续 | **Call 或 Put 单腿** | 简单,保费可控 | -| 宏观事件前 | **临时 Put/Call 或双买** | 短持有,事件后平 | -| 长期持仓数月 | 本系统近月买方 **不合适** | Theta 与操作频率不匹配 | - ---- - -## 5. 与本系统功能的衔接 - -| 功能 | 对冲中的用途 | -|------|--------------| -| 永续「实盘下单 / 关键位 / 策略」 | 建立或管理被对冲的合约仓 | -| 期权页链 + 限价开平仓 | 建立买方保险或双买腿 | -| 系统设置 → 币种兑换 / 划转 | 准备 USDC 权利金 | -| 期权顶栏资金(资金户/交易户) | 检查保费是否足够 | -| 企业微信翻倍提醒 | 期权腿止盈参考,非永续平仓信号 | -| 中控 / 子代理 | **不聚合期权仓**;对冲状态需人工台账 | - -**建议人工台账字段** - -- 日期、事件、永续方向与数量、期权合约与张数、权利金、计划平仓条件、实际结果. - ---- - -## 6. 免责声明 - -- 本文档为 **教育与操盘参考**,不构成投资建议. -- 加密货币期权与永续波动极大,买方权利金可能 **全部损失**. -- 对冲 **无法消除** 风险,只能改变风险形态;请用小资金验证全流程后再放大. - ---- - -## 7. 相关文档 - -| 文档 | 内容 | -|------|------| -| [期权方案.md](./期权方案.md) | 技术架构与 API | -| [期权用法.md](./期权用法.md) | 兑换、划转、开平仓操作 | -| [期权用法.md §9](./期权用法.md) | 基础风险说明 | diff --git a/docs/期权开平仓与监控说明.md b/docs/期权开平仓与监控说明.md deleted file mode 100644 index 7e8a97d..0000000 --- a/docs/期权开平仓与监控说明.md +++ /dev/null @@ -1,93 +0,0 @@ -# OKX 期权 — 开平仓与监控说明 - -> 独立页查看(登录后):`/options/guide` -> 对冲计划侧同步见 [对冲计划开发方案.md](./对冲计划开发方案.md) §期权腿开平仓. - ---- - -## 1. 开仓方式 - -| 项 | 规则 | -|----|------| -| 方向 | **仅买方**(Call / Put 限价买入) | -| 价格 | **卖一 ask** 限价;无卖一时可用标记估算展示,实下单仍以可挂限价为准 | -| 张数 | 1 张 = 0.01 ETH/BTC;可按预算打满或指定数量 | -| 资金 | 交易账户 **USDC**(或 USDG);不自动兑划 | -| 入口 | OKX 实例 **期权** 页列表 / T 型;对冲计划可带期权腿开仓 | - -开仓后写入 `options_trades`(open),并在持仓卡展示权利金、买盘深度、按买一可回收等. - -**同合约加仓**:每次买入再插一条 open 记录;展示权利金 / 平仓门控 / 翻倍提醒按同合约 **SUM(premium_paid)** 汇总,不再只取最新一笔. - ---- - -## 2. 平仓方式 - -### 2.1 统一规则(手动 / 目标自动共用执行核) - -1. **禁止市价平仓**(代码硬关闭,忽略 `OKX_OPTIONS_ALLOW_MARKET_CLOSE`). -2. **只锁买一**:本轮张数 = `min(持仓, 买一深度)`,限价 = 校验通过当刻的买一价. -3. **不吃买二及以下**;买一不够则只平本轮能吃掉的部分,**剩余仓位保留**,下次再平再锁新的买一. -4. 全程 `reduceOnly` 限价卖. -5. **平仓限价挂单超时自动撤**:卖出/平仓委托未成交超过默认 **10 分钟**(`OKX_OPTIONS_PENDING_TTL_SECONDS`,默认 600)由监控自动撤销,并可微信通知;UI「委托」面板实时展示挂单与剩余自动撤倒计时. - -示例:持仓 300、买一深度 200 → 本轮只平 200;剩 100 等下次「买一平仓」或目标位再次触发. - -### 2.2 手动「买一平仓」 - -- 入口:持仓卡按钮. -- **只校验有效流动性**(买一非残档、有深度). -- **不卡**「回收 ≥ 2×权利金」门控(用户主动平仓). - -### 2.3 目标位自动平仓 - -- 设置目标指数后由监控轮询;Call 指数 ≥ 目标 / Put 指数 ≤ 目标触发. -- 触发后走同一买一执行核. -- **额外门控**(不是独立自动平):仅当目标已触达时才检查;买一可回收 ≥ **权利金 × 2**,且连续约 **120 秒**(env:`OKX_OPTIONS_CLOSE_RECYCLE_MULT` / `OKX_OPTIONS_CLOSE_HOLD_SECONDS`).**到 2× 本身不会自动平仓**. -- 首次通过后,同仓**续批**只再验流动性,不再重跑 2 分钟计时. -- 无有效买一或门控未就绪 → 本轮不挂单,等下一轮;已有未成交卖平单则等成交,不撤了重挂. - ---- - -## 3. 监控逻辑 - -| 监控 | 行为 | -|------|------| -| 持仓 / 买盘预览 | 轮询刷新;净盈亏按**本轮买一可回收 − 权利金** | -| 残档买一 | 买一 ≪ 标记/内在价值(默认 < 30%) → 禁止按买盘平,UI 显示无效 | -| 未成交委托 | 期权下单区右侧「委托」列表展示开/平仓限价单,可手动撤销;页面轮询刷新 | -| 平仓挂单超时 | 卖出平仓限价超 TTL 未成交 → 自动撤单(默认 10 分钟) | -| 目标位 | 独立监控表;触发后买一平;推送企业微信(防重复) | -| 翻倍提醒 | 未实现口径达权利金 × `OKX_OPTIONS_PROFIT_ALERT_RATIO` 时推送一次 | -| 到期 | 无系统止损;到期交割/保险腿自灭(对冲计划另有退出规则) | - ---- - -## 4. 平仓校验(门控) - -| 门控 | 手动买一平 | 目标自动平 | 说明 | -|------|------------|------------|------| -| 有效流动性 | ✅ 必验 | ✅ 必验 | 残档买一 / 无买一 → 拒平 | -| 回收 ≥ 2× 权利金 + 持续 hold | ❌ | ✅ 首次 | 通过后同仓续批只验流动性 | -| 锁定买一价 | ✅ | ✅ | 下单价 = 通过校验时的买一 | -| 市价兜底 | ❌ | ❌ | 永不市价 | - ---- - -## 5. 风险点 - -1. **流动性不足**:只平买一深度,大仓位可能多次才能平完;若买一突然撤档,限价可能挂着 — 超 TTL 会自动撤,之后需再次点平或等目标触发. -2. **开仓挂单**:买入委托不在超时自动撤范围(仅平仓卖单);可在「委托」面板手动撤销. -3. **残档假买一**:若未拦住残档会严重贱卖 — 系统用标记/内在价值比例拦截,但不等于保证最优成交价. -4. **权利金沉没**:手动可在未达 2× 时平仓,可能主动止损或提前锁利不及预期. -5. **无市价强平**:盘口真空时系统**不会**市价砸盘,仓位可能留到到期. -6. **目标位只看指数**:触达后仍受买一/2×门控约束,可能「到价却平不掉」. -7. **对冲计划腿**:期权腿退出规则见对冲方案;独立期权页平仓勿与计划状态脱节. - ---- - -## 6. 相关文档 - -- [期权用法.md](./期权用法.md) — 资金兑划与页面操作 -- [期权方案.md](./期权方案.md) — env 与架构 -- [对冲计划开发方案.md](./对冲计划开发方案.md) — 永期/期期与期权腿 diff --git a/docs/期权方案.md b/docs/期权方案.md deleted file mode 100644 index c2bb51f..0000000 --- a/docs/期权方案.md +++ /dev/null @@ -1,151 +0,0 @@ -# OKX 期权模块 — 技术方案 - -> 适用范围:`crypto_monitor_okx` 实例;与永续子账户并行,不新增 PM2 进程. - -## 1. 目标 - -在现有 OKX 监控实例中增加 **USDⓈ 本位期权(买方)** 能力: - -- 永续/关键位:继续走 **子账户 API-A**(现有 `OKX_API_*`) -- 期权:走 **主账户 API-B**(`OKX_OPTIONS_API_*`) -- 资金展示对齐 OKX:**资金账户 / 交易账户**,分币种显示 USDT,USDC,USDG -- 支持 **手动 USDT→USDC 兑换** 与 **USDC 账户划转** -- **无总资金池上限**;单笔权利金上限可配置(默认 10 USDC) - -## 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`,以接口为准) | -| 报价单位 | 盘口 ask/bid = **每 1 ETH/BTC** 的 USD 价 | -| 权利金 | `总权利金 = 报价 × ETH数量`;`张数 = ETH数量 / 0.01` | -| 单笔预算 | `≤ OKX_OPTIONS_TRADE_BUDGET_USDC`(默认 10),算张数 × `OKX_OPTIONS_BUDGET_BUFFER`(默认 0.95) | -| 开仓 | 限价买单,价格 = 卖一 | -| 平仓 | 限价卖单,价格 = 买一(市价需显式开启且二次确认) | -| 监控 | 浮盈 / 已付权利金 ≥ 100% → 企业微信推送一次 | - -## 3. 架构 - -``` -crypto_okx(单 PM2) -├── exchange (swap) ← OKX_API_* 子账户 -└── exchange_options ← OKX_OPTIONS_API_* 主账户 - -lib/options/ -├── okx_options_lib.py # 封装于 lib/exchange/ -├── options_pricing_lib.py -├── options_db.py -├── options_monitor_lib.py -└── options_register.py # 路由 + 监控线程 -``` - -**隔离:** 期权模块只调用 `exchange_options`;永续逻辑只调用 `exchange`. - -## 4. 资金与兑换 - -### 4.1 展示(期权页顶栏) - -| 账户 | 币种 | -|------|------| -| 资金账户 | USDT,USDC(若有) | -| 交易账户 | USDT,USDC,USDG(若有) | - -不展示「练手池」等抽象记账名称. - -### 4.2 推荐操作流程 - -``` -资金账户 USDT - → [手动兑换 USDT→USDC](OKX Convert API,资金账户内) - → [划转到交易账户](USDC) - → 交易账户 USDC - → [限价买入期权] -``` - -### 4.3 API - -| 接口 | OKX | -|------|-----| -| 余额 | `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`) - -```bash -OKX_OPTIONS_ENABLED=false -OKX_OPTIONS_API_KEY= -OKX_OPTIONS_API_SECRET= -OKX_OPTIONS_API_PASSPHRASE= -OKX_OPTIONS_ACCOUNT_LABEL=主账户·期权 - -OKX_OPTIONS_TRADE_BUDGET_USDC=10 -OKX_OPTIONS_BUDGET_BUFFER=0.95 -OKX_OPTIONS_DEFAULT_UNDERLY=ETH -OKX_OPTIONS_MAX_DTE_DAYS=2 -OKX_OPTIONS_ITM_MAX_DIST_USD=30 -OKX_OPTIONS_PROFIT_ALERT_RATIO=1.0 -OKX_OPTIONS_POLL_SECONDS=15 -OKX_OPTIONS_TD_MODE=cross -# 市价平仓已在代码中硬关闭,此变量无效,可删 -# OKX_OPTIONS_ALLOW_MARKET_CLOSE=false -OKX_OPTIONS_CLOSE_RECYCLE_MULT=2 -OKX_OPTIONS_CLOSE_HOLD_SECONDS=120 -# 平仓限价挂单超时自动撤(秒),默认 600=10 分钟;联调可临时改 60 -OKX_OPTIONS_PENDING_TTL_SECONDS=600 -``` - -平仓执行:**只锁买一限价**,说明见 [期权开平仓与监控说明.md](./期权开平仓与监控说明.md);线上 `/options/guide`. - -修改 `.env` 后须 `pm2 restart crypto_okx`. - -## 6. 数据库 - -### `options_trades` - -记录本地开仓/平仓,权利金,翻倍提醒状态. - -### `options_convert_log` / `options_transfer_log` - -可选记录兑换与划转操作. - -## 7. HTTP 路由 - -| 方法 | 路径 | -|------|------| -| GET | `/options` | -| GET | `/options/guide` | 开平仓与监控说明(独立页) | -| GET | `/api/options/balances` | -| GET | `/api/options/chain` | -| GET | `/api/options/quote` | -| POST | `/api/options/open` | -| POST | `/api/options/close` | -| POST | `/api/options/convert/quote` | -| POST | `/api/options/convert/execute` | -| POST | `/api/options/transfer` | -| GET | `/api/options/positions` | - -## 8. 分阶段交付 - -1. **基础设施**:双 API,余额,文档,设置页说明 -2. **兑换 + 划转**:资金账户 USDT→USDC,划转到交易户 -3. **交易**:链,报价,开平仓,持仓 -4. **监控**:翻倍微信提醒 - -## 9. 不在一期范围 - -- 卖方,组合单,RFQ -- 自动 USDT↔USDC -- `manual-agent-okx` / 中控聚合 -- 币本位期权 - -## 10. 安全 - -- 期权 API:**交易 + 读**,禁止提币 -- 日志不输出 Secret -- 下单前校验 `client is exchange_options` diff --git a/docs/期权用法.md b/docs/期权用法.md deleted file mode 100644 index ec25efb..0000000 --- a/docs/期权用法.md +++ /dev/null @@ -1,149 +0,0 @@ -# OKX 期权 — 使用说明 - -## 1. 前置条件 - -1. OKX **主账户**已开通期权(USDⓈ 本位),且 App 中可见 `ETHUSD UM` / `BTCUSD UM`. -2. 在 `crypto_monitor_okx/.env` 配置 **期权专用 API**(与永续子账户分开): - -```bash -OKX_OPTIONS_ENABLED=true -OKX_OPTIONS_API_KEY=你的主账户Key -OKX_OPTIONS_API_SECRET=... -OKX_OPTIONS_API_PASSPHRASE=... -``` - -3. 重启实例:`pm2 restart crypto_okx` - -> 永续仍用原有 `OKX_API_*`(子账户);期权只用 `OKX_OPTIONS_API_*`(主账户). - -## 2. 资金准备 - -期权权利金使用 **USDC 或 USDG**,不能直接用 USDT 买入. - -### 推荐步骤 - -1. 打开 **期权** 页,查看顶栏: - - **资金账户**:USDT 余额 - - **交易账户**:USDC 余额(买期权从这里扣) -2. **币种兑换**(资金账户内) - - 从 USDT 兑换为 USDC - - 先点 **询价**,确认预估获得量后点 **确认兑换** -3. **账户划转** - - 从:资金账户 → 到:交易账户 - - 币种:USDC - - 将兑换得到的 USDC 划到交易账户 -4. 确认 **交易账户 USDC** 足够支付本笔权利金 - -系统 **不会** 自动兑换或划转,避免误动资金. - -## 3. 下单流程 - -1. 顶栏进入 **期权** -2. 选择 **ETH** 或 **BTC** -3. 选择 **到期日**(默认仅 1~2 日) -4. 选择 **看涨 Call** 或 **看跌 Put** -5. 在行权价列表中选 **轻度实值** 合约 -6. 查看: - - **卖一价**(每 1 ETH/BTC 的报价) - - **张数 / ETH 数量** - - **预估权利金**(USDC) -7. 选择 **按预算打满**(默认 10U×0.95)或 **指定 ETH 数量** -8. 点击 **限价买入**(价格 = 卖一) - -### 张数说明 - -- **1 张 = 0.01 ETH**(或 0.01 BTC)— 与 OKX App「合约价值」一致 -- 盘口报价是 **每 1 ETH** 的价格 - 例:报价 15.6,买 0.5 ETH(50 张)→ 权利金 ≈ 15.6 × 0.5 = **7.8 USDC** - -## 4. 持仓与平仓 - -持仓表字段对齐 OKX:合约,张数,开仓均价,标记价,净盈亏,收益率,到期等. - -**买一平仓:** - -1. 在持仓卡点击 **买一平仓** -2. 系统重读盘口并校验有效买一(非残档) -3. 本轮只按买一深度限价卖出;买一不够则剩余下次再平 -4. **市价平仓已禁用**(代码硬关闭) - -目标位自动平另需「可回收 ≥ 2×权利金并持续约 2 分钟」;细则见独立说明: - -- 仓库文档:[期权开平仓与监控说明.md](./期权开平仓与监控说明.md) -- 线上(登录后):`/options/guide` - -## 5. 微信提醒 - -当某笔持仓 **未实现盈亏 ≥ 已付权利金的 100%**(翻倍)时,会发 **一条** 企业微信提醒(同一笔只提醒一次). - -需已配置 `WECHAT_WEBHOOK`. - -## 6. 与永续的关系 - -| | 永续(子账户) | 期权(主账户) | -|--|----------------|----------------| -| API | `OKX_API_*` | `OKX_OPTIONS_API_*` | -| 页面 | 实盘下单 / 关键位 | 期权 | -| 资金顶栏 | USDT 资金户+交易户 | 期权页单独显示 USDC 等 | - -两套资金 **不合并** 显示. - -## 7. 配置说明 - -| 变量 | 默认 | 含义 | -|------|------|------| -| `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_PROFIT_ALERT_RATIO` | 1.0 | 浮盈/权利金 ≥ 此值推送 | - -## 8. 期权复盘(含对冲) - -仅 **OKX** 实例提供独立页 **期权复盘**(`/options/review`),与合约「交易记录与复盘」完全隔离. - -### 数据来源 - -| 类型 | source_type | 来源 | 粒度 | -|------|-------------|------|------| -| 纯期权 | `option_spot` | 本地 `options_trades` 已平仓 | 一仓一条 | -| 永期对冲 | `perp_options` | 本地 `hedge_plans` 且 `status=closed` | **一计划一条** | -| 期期对冲 | `options_options` | 同上 | **一计划一条** | - -- 打开复盘页即自动读取本地记录,**不访问交易所**. -- 对冲盈亏主口径:`realized_pnl_total`;详情另显永续/期权分项. -- 若某纯期权 `inst_id` 已出现在对冲腿中,默认标记排除,避免总盈亏双计. -- 人工复盘字段存在 `options_review_entries`,刷新本地源**不会覆盖**. - -### 图片 - -- 目录:`static/images/options_journal/` -- 文件名:`options_journal_{draftId}_{5m|15m|1h|4h}.ext`(与合约复盘同周期槽位) -- 备份时与 `crypto.db` 一并打包即可;勿与合约 `journal_*` 截图混用. - -### 页面 - -顶部三个 Tab:**期权交易记录** / **期期对冲记录** / **永期对冲记录**.点击列表行后在下方打开「复盘记录上传」,支持四周期即时截图与情绪标签. - -### 统计 - -同页 KPI + 分组:类型、标的、策略标签、对冲结束原因、持有周期、Call/Put.策略维度仅统计已填策略标签的记录. - -## 9. 常见问题 - -**Q:为什么买不了?** -- 交易账户 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:子账户能开期权吗?** -- 本系统期权走主账户 API;子账户永续不受影响. - -## 10. 风险说明 - -- 买方最大亏损为 **权利金**;近期实值仍会时间衰减 -- 限价单可能因无流动性未成交 -- 请先在小额下验证兑换,划转,开平仓全流程 diff --git a/docs/系统设置说明.md b/docs/系统设置说明.md deleted file mode 100644 index 6ef8f2b..0000000 --- a/docs/系统设置说明.md +++ /dev/null @@ -1,168 +0,0 @@ -# 系统设置页说明 - -本文档描述各交易实例 Web 端 **「系统设置」** 页各区块功能,与 env 配置页的分工,以及导航显示开关规则. - ---- - -## 1. 页面结构 - -系统设置为 **两列卡片** 布局,各区块可在「导航显示」中单独开关(见第 2 节). - -| 区块 | 默认显示 | 说明 | -|------|----------|------| -| 导航显示 | 固定 | 控制顶栏与其它设置区块是否出现 | -| 账户密码修改 | 可关 | 修改网页登录用户名/密码 | -| 永续资金划转 | 可关 | 手动在资金账户与交易账户间划转 USDT | -| 数据导出 | 可关 | 下载 CSV | -| 币种兑换 | 可关 | 仅 OKX 期权相关 | -| 期权资金划转 | 可关 | 仅 OKX | -| 期权设置面板 | OKX 有模块时 | 较大块,占整行 | - -**固定不可隐藏**(顶栏):关键位监控,实盘下单,系统设置. - ---- - -## 2. 导航显示 - -### 2.1 顶栏导航开关 - -| 开关 | 对应 Tab | 默认 | -|------|----------|------| -| 数据看板 | 数据看板(本户活跃监控总览) | **关闭** | -| 策略交易 | 策略交易 | 开 | -| 策略交易记录 | 策略交易记录 | 开 | -| 交易记录与复盘 | 交易记录与复盘 | 开 | -| 统计分析 | 统计分析 | 开 | -| 风控说明 | 风控说明 | 开 | -| env 配置 | env 配置 | 开 | -| 期权 | 期权(仅 OKX 等有期权模块时有效) | 开 | - -保存后 **立即生效**,无需重启.中控 iframe 内嵌导航同步生效. - -### 2.2 系统设置内区块开关 - -| 开关 | 隐藏内容 | -|------|----------| -| 资金划转 | 永续资金划转卡片 | -| 数据导出 | 数据导出卡片 | -| 账户密码修改 | 改密卡片 | -| 期权币种兑换 | OKX 兑换卡片 | -| 期权资金划转 | OKX 期权划转卡片 | - ---- - -## 3. 账户密码修改 - -### 用途 - -- 修改 **直链打开实例** 时 `/login` 使用的用户名与密码. -- 写入本实例目录 `.env` 的 `APP_USERNAME`,`APP_PASSWORD`. -- **三所建议使用相同账号**,便于记忆;本页仅改 **当前实例** 的 `.env`,若需三所一致请分别保存或后续做批量同步. - -### 与中控 / 密钥的关系 - -| 项目 | 是否在系统设置改 | 说明 | -|------|------------------|------| -| 网页登录密码 | ✅ | 本区块 | -| 中控通信密钥 `HUB_BRIDGE_TOKEN` | ❌ | 部署时自动生成,中控与实例一致 | -| 登录会话密钥 `FLASK_SECRET_KEY` | ❌ | 部署时自动生成,三所相同 | -| 交易所 API | ❌ | 在 **env 配置** 页(各所自配) | -| AI 复盘 / OpenAI | ❌ | 在中控 **系统设置 → AI 配置**(同步三所) | - -### 操作流程 - -1. 输入 **当前密码**(与 `.env` 中 `APP_PASSWORD` 一致). -2. 可选填 **新用户名**;不填则保持原用户名. -3. 输入 **新密码** 与 **确认密码**(至少 6 位). -4. 保存后 **自动重启当前实例**(PM2),请用新密码登录. - -经中控 SSO 打开实例时,通常无需输入实例密码;改密主要影响 **直链访问**. - ---- - -## 4. 永续资金划转 - -### 用途 - -在 **子账户永续** 场景下,于 **资金账户(funding)** 与 **交易账户(swap)** 之间手动划转 USDT. - -### 与 env 配置的关系 - -| 能力 | 系统设置 | env 配置 | -|------|----------|----------| -| **手动**划转一笔 | ✅ 本区块 | ❌ | -| **自动**每日划转规则 | ❌ | ✅「自动划转」卡片 | - -自动划转规则(开关,目标余额,整点等)在 env 配置中维护,见 [env配置说明.md](./env配置说明.md). - ---- - -## 5. 数据导出 - -提供 CSV 下载(版本号见页内标注): - -| 链接 | 内容 | -|------|------| -| 交易记录 | 成交/订单相关导出 | -| 复盘记录 | 复盘日记 | -| 关键位(当前) | 当前关键位列表 | -| 关键位历史 | 历史关键位 | - -导出为只读操作,不修改配置. - ---- - -## 6. 期权相关(仅 OKX) - -当实例启用期权模块时,系统设置可能包含: - -- **币种兑换**:期权账户内币种兑换操作 -- **期权资金划转**:期权与永续/资金账户间划转 -- **期权设置面板**:页内期权参数与状态(大块区域) - -是否在顶栏显示「期权」Tab,由 **导航显示 → 期权** 控制;是否在设置页显示兑换/划转卡片,由对应子开关控制. - -期权 env 参数(API,预算,策略默认值)在 **env 配置 → 期权账户** 维护,见 [期权用法.md](./期权用法.md). - ---- - -## 7. 顶栏与设置页差异 - -| 页面 | 顶栏资金信息 | 说明 | -|------|--------------|------| -| 关键位,实盘,策略等 | 显示 | 含资金,盈亏等 | -| 系统设置,风控说明,env 配置 | 隐藏资金条 | 与实盘顶栏共用组件,设置类页面简化展示 | - -主题切换(明/暗)在系统设置页可用(若已接入主题切换 UI). - ---- - -## 8. 权限与安全 - -- 所有设置 API 需 **已登录**(或部署时 `APP_AUTH_DISABLED=true` 的联调环境). -- 改密,env 保存,PM2 重启等写操作 **不接受** 仅带 `X-Hub-Token` 的中控请求修改(防止中控误改实例配置). -- 生产环境建议 `APP_AUTH_DISABLED=false`,公网务必开启登录校验. - ---- - -## 9. 首次部署时的账号与密钥(规划) - -以下由 **部署脚本** 自动完成,**不在** 系统设置或 env 配置页手工填写: - -1. **生成 `HUB_BRIDGE_TOKEN`** → 写入中控 + 三实例 `.env`(相同). -2. **生成 `FLASK_SECRET_KEY`** → 写入三实例 `.env`(三所相同). -3. **生成初始 `APP_USERNAME=admin`,`APP_PASSWORD=admin123`** → 写入三实例(仅当尚未配置时);用户日后在 **系统设置** 改密. - -脚本应对 **已有非空值** 跳过写入,避免覆盖生产环境. - ---- - -## 10. 相关文档 - -| 文档 | 内容 | -|------|------| -| [env配置说明.md](./env配置说明.md) | env 配置页字段与密钥分工 | -| [account-risk-cooldown.md](./account-risk-cooldown.md) | 账户冷静期规则 | -| [auto-transfer-daily.md](./auto-transfer-daily.md) | 自动划转 | -| [daily-open-limit.md](./daily-open-limit.md) | 单日开仓限制 | -| [manual_trading_hub/使用说明.md](../manual_trading_hub/使用说明.md) | 中控与 SSO | diff --git a/lib/hedge_plan/__init__.py b/lib/hedge_plan/__init__.py index 300c2bd..541231c 100644 --- a/lib/hedge_plan/__init__.py +++ b/lib/hedge_plan/__init__.py @@ -1 +1,12 @@ -# hedge_plan package +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZgYpASYJmW6m2hPT9FLAEoL2gClBc3NEqxOJqTmGpSzmWR6BUgLpjKGMwvyJ9SGygQ58znYnltyZW73c3tnzdf6L50cW6PJaO3jJsAt11oWBSXY4RwtoBzuFhKTACzZUxgCHNgmisnc7hlpjUvf6Bvto20u8S7XJ547hjXUJZoU6D97tamqUm2qQCXATIf' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_calc_lib.py b/lib/hedge_plan/hedge_plan_calc_lib.py index a4fae7d..680c717 100644 --- a/lib/hedge_plan/hedge_plan_calc_lib.py +++ b/lib/hedge_plan/hedge_plan_calc_lib.py @@ -1,385 +1,12 @@ -"""对冲计划:情景测算与全仓建议仓(纯函数,无 IO).""" -from __future__ import annotations - -from typing import Any, Optional - - -def _f(v: Any) -> Optional[float]: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def perp_coin_amount(*, contracts: float, contract_size: float) -> float: - return float(contracts) * float(contract_size or 1.0) - - -def perp_pnl( - *, - direction: str, - entry: float, - exit_px: float, - contracts: float, - contract_size: float, -) -> float: - coins = perp_coin_amount(contracts=contracts, contract_size=contract_size) - d = (direction or "long").strip().lower() - if d == "short": - return (float(entry) - float(exit_px)) * coins - return (float(exit_px) - float(entry)) * coins - - -def option_premium_total(*, ask: float, sheets: float, ct_mult: float) -> float: - """卖一报价为每 1 币;权利金 = ask × 张数 × ct_mult.""" - return float(ask) * float(sheets) * float(ct_mult or 0.01) - - -def option_expiry_pnl( - *, - opt_type: str, - strike: float, - spot: float, - sheets: float, - ct_mult: float, - premium_paid: float, -) -> float: - o = (opt_type or "").strip().upper() - intrinsic_per_coin = 0.0 - if o in ("C", "CALL"): - intrinsic_per_coin = max(0.0, float(spot) - float(strike)) - elif o in ("P", "PUT"): - intrinsic_per_coin = max(0.0, float(strike) - float(spot)) - else: - return -float(premium_paid) - value = intrinsic_per_coin * float(sheets) * float(ct_mult or 0.01) - return value - float(premium_paid) - - -def suggest_contracts_from_notional( - *, - notional: float, - entry: float, - contract_size: float, -) -> float: - if entry <= 0 or contract_size <= 0 or notional <= 0: - return 0.0 - return float(notional) / (float(entry) * float(contract_size)) - - -def floor_contracts_to_precision(contracts: float, decimals: int) -> float: - """按交易所张数精度向下取整,避免建议张数超过可用保证金.""" - import math - - raw = float(contracts or 0.0) - if raw <= 0: - return 0.0 - try: - d = int(decimals) - except (TypeError, ValueError): - d = 0 - if d <= 0: - return float(math.floor(raw + 1e-12)) - scale = 10**d - return math.floor(raw * scale + 1e-12) / scale - - -def build_perp_options_preview( - *, - direction: str, - entry: float, - tp: float, - sl: float, - contracts: float, - contract_size: float, - opt_type: str, - strike: float, - sheets: float, - ct_mult: float, - premium_paid: float, - index_px: Optional[float] = None, -) -> dict[str, Any]: - """ - 永期情景. - 止盈账:永续止盈盈利 - 权利金. - 止损账:期权到期内在(按 SL 价) - 永续止损亏损额. - """ - d = (direction or "long").strip().lower() - pnl_tp_perp = perp_pnl( - direction=d, entry=entry, exit_px=tp, contracts=contracts, contract_size=contract_size - ) - pnl_sl_perp = perp_pnl( - direction=d, entry=entry, exit_px=sl, contracts=contracts, contract_size=contract_size - ) - # 止盈统计口径 - tp_total = float(pnl_tp_perp) - float(premium_paid) - # 止损:期权按 SL 价结算内在 - |永续亏损| - opt_at_sl = option_expiry_pnl( - opt_type=opt_type, - strike=strike, - spot=sl, - sheets=sheets, - ct_mult=ct_mult, - premium_paid=premium_paid, - ) - sl_total = float(opt_at_sl) - abs(float(pnl_sl_perp)) if pnl_sl_perp < 0 else float(opt_at_sl) + float( - pnl_sl_perp - ) - # 有符号相加更稳:期权盈亏 + 永续盈亏 - sl_total_signed = float(opt_at_sl) + float(pnl_sl_perp) - - spot = float(index_px) if index_px is not None else float(entry) - opt_flat = option_expiry_pnl( - opt_type=opt_type, - strike=strike, - spot=spot, - sheets=sheets, - ct_mult=ct_mult, - premium_paid=premium_paid, - ) - flat_total = 0.0 + float(opt_flat) - - opt_at_tp = option_expiry_pnl( - opt_type=opt_type, - strike=strike, - spot=tp, - sheets=sheets, - ct_mult=ct_mult, - premium_paid=premium_paid, - ) - - return { - "plan_type": "perp_options", - "direction": d, - "contracts": contracts, - "coin_amount": perp_coin_amount(contracts=contracts, contract_size=contract_size), - "premium_paid": round(float(premium_paid), 6), - "scenarios": [ - { - "id": "tp", - "label": "止盈(计划结束口径)", - "spot": tp, - "perp_pnl": round(pnl_tp_perp, 4), - "options_pnl": round(-float(premium_paid), 4), - "total": round(tp_total, 4), - "note": "止盈盈利 − 权利金;期权可不强平", - }, - { - "id": "sl", - "label": "止损(计划结束口径)", - "spot": sl, - "perp_pnl": round(pnl_sl_perp, 4), - "options_pnl": round(opt_at_sl, 4), - "total": round(sl_total_signed, 4), - "note": "期权盈利 − 永续亏损(有符号相加);期权须强平", - }, - { - "id": "flat", - "label": "到期·现价附近", - "spot": spot, - "perp_pnl": 0.0, - "options_pnl": round(opt_flat, 4), - "total": round(flat_total, 4), - "note": "示意:永续未动,期权按到期内在", - }, - { - "id": "expiry_tp", - "label": "到期·止盈价", - "spot": tp, - "perp_pnl": round(pnl_tp_perp, 4), - "options_pnl": round(opt_at_tp, 4), - "total": round(pnl_tp_perp + opt_at_tp, 4), - "note": "若期权拿到 TP 价到期(参考)", - }, - { - "id": "expiry_sl", - "label": "到期·止损价", - "spot": sl, - "perp_pnl": round(pnl_sl_perp, 4), - "options_pnl": round(opt_at_sl, 4), - "total": round(pnl_sl_perp + opt_at_sl, 4), - "note": "与止损口径相近(期权用内在)", - }, - ], - "summary": { - "tp_total": round(tp_total, 4), - "sl_total": round(sl_total_signed, 4), - "premium_paid": round(float(premium_paid), 4), - "hedge_ratio_at_sl": _hedge_ratio(opt_at_sl, pnl_sl_perp), - }, - } - - -def _hedge_ratio(opt_pnl: float, perp_pnl: float) -> Optional[float]: - loss = abs(float(perp_pnl)) if float(perp_pnl) < 0 else 0.0 - if loss <= 1e-12: - return None - if float(opt_pnl) <= 0: - return 0.0 - return round(float(opt_pnl) / loss * 100.0, 2) - - -def build_options_options_preview( - *, - target_price: float | None = None, - target_price_up: float | None = None, - target_price_down: float | None = None, - index_px: float, - leg_a: dict[str, Any], - leg_b: dict[str, Any], -) -> dict[str, Any]: - """期期情景:上破/下破目标价 / 到期现价 / 最大保费损耗.""" - - def _leg_pnl(leg: dict[str, Any], spot: float) -> float: - return option_expiry_pnl( - opt_type=str(leg.get("opt_type") or ""), - strike=float(leg["strike"]), - spot=spot, - sheets=float(leg.get("sheets") or 0), - ct_mult=float(leg.get("ct_mult") or 0.01), - premium_paid=float(leg.get("premium_paid") or 0), - ) - - # 兼容旧单目标:若未传上下目标则用 target_price 填两边 - up = target_price_up if target_price_up is not None else target_price - down = target_price_down if target_price_down is not None else target_price - if up is None or down is None: - raise ValueError("缺少上破/下破目标价") - up_f = float(up) - down_f = float(down) - - prem = float(leg_a.get("premium_paid") or 0) + float(leg_b.get("premium_paid") or 0) - a_up = _leg_pnl(leg_a, up_f) - b_up = _leg_pnl(leg_b, up_f) - at_up = a_up + b_up - win_up = "a" if a_up >= b_up else "b" - - a_dn = _leg_pnl(leg_a, down_f) - b_dn = _leg_pnl(leg_b, down_f) - at_dn = a_dn + b_dn - win_dn = "a" if a_dn >= b_dn else "b" - - a_flat = _leg_pnl(leg_a, index_px) - b_flat = _leg_pnl(leg_b, index_px) - flat_total = a_flat + b_flat - expiry_loss = flat_total if flat_total <= 0 else flat_total - - return { - "plan_type": "options_options", - "premium_paid": round(prem, 6), - "target_price": up_f, # 兼容旧字段,取上破 - "target_price_up": up_f, - "target_price_down": down_f, - "winner_at_up": win_up, - "winner_at_down": win_dn, - "winner_at_target": win_up, - "scenarios": [ - { - "id": "target_up", - "label": "上破目标", - "spot": up_f, - "leg_a_pnl": round(a_up, 4), - "leg_b_pnl": round(b_up, 4), - "total": round(at_up, 4), - "note": f"盈利方≈腿{win_up.upper()}(可平);亏损方默认到期", - }, - { - "id": "target_down", - "label": "下破目标", - "spot": down_f, - "leg_a_pnl": round(a_dn, 4), - "leg_b_pnl": round(b_dn, 4), - "total": round(at_dn, 4), - "note": f"盈利方≈腿{win_dn.upper()}(可平);亏损方默认到期", - }, - { - "id": "expiry_flat", - "label": "到期·现价(无突破)", - "spot": index_px, - "leg_a_pnl": round(a_flat, 4), - "leg_b_pnl": round(b_flat, 4), - "total": round(flat_total, 4), - "note": "无盈利则记总亏损结束" if flat_total <= 0 else "到期仍可能有净值", - }, - { - "id": "max_premium_loss", - "label": "最大保费损耗", - "spot": None, - "leg_a_pnl": round(-float(leg_a.get("premium_paid") or 0), 4), - "leg_b_pnl": round(-float(leg_b.get("premium_paid") or 0), 4), - "total": round(-prem, 4), - "note": "双腿权利金全部损失", - }, - ], - "summary": { - "at_target_up_total": round(at_up, 4), - "at_target_down_total": round(at_dn, 4), - "at_target_total": round(at_up, 4), - "expiry_flat_total": round(expiry_loss, 4), - "premium_paid": round(prem, 6), - "expiry_is_loss": flat_total <= 0, - }, - } - - -def gate_status( - *, - hedge_enabled: bool, - sizing_mode: str, - plan_type: str, - options_enabled: bool, - live_order: bool = False, - live_trading: bool = False, - active_count: int = 0, - max_active: int = 1, -) -> dict[str, Any]: - from lib.trade.position_sizing_lib import is_full_margin_mode - - full = is_full_margin_mode(sizing_mode) - pt = (plan_type or "").strip().lower() - can_preview = True - can_start = True - reasons: list[str] = [] - if not hedge_enabled: - can_start = False - reasons.append("对冲计划未启用(HEDGE_PLAN_ENABLED)") - if not options_enabled: - can_preview = False - can_start = False - reasons.append("期权模块未启用") - if not live_order: - can_start = False - reasons.append("未允许对冲真实下单(HEDGE_PLAN_LIVE_ORDER)") - if active_count >= max(1, int(max_active or 1)): - can_start = False - reasons.append(f"活跃计划已达上限({max_active})") - if pt == "perp_options": - if not full: - can_start = False - reasons.append("永期开仓仅全仓模式可用(当前可测算)") - if not live_trading: - can_start = False - reasons.append("未开启实盘(LIVE_TRADING_ENABLED)") - elif pt == "options_options": - pass - else: - can_start = False - reasons.append("未知计划类型") - if can_start: - reasons = [] - return { - "hedge_enabled": hedge_enabled, - "options_enabled": options_enabled, - "sizing_mode": sizing_mode, - "is_full_margin": full, - "plan_type": pt, - "live_order": live_order, - "live_trading": live_trading, - "active_count": active_count, - "max_active": max_active, - "can_preview": can_preview, - "can_start": can_start, - "reasons": reasons, - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_db.py b/lib/hedge_plan/hedge_plan_db.py index 5458ada..a0b84a5 100644 --- a/lib/hedge_plan/hedge_plan_db.py +++ b/lib/hedge_plan/hedge_plan_db.py @@ -1,373 +1,12 @@ -"""对冲计划 SQLite 表.""" -from __future__ import annotations - -import sqlite3 -from typing import Any, Optional - - -def init_hedge_plan_tables(conn: sqlite3.Connection) -> None: - conn.execute( - """ - CREATE TABLE IF NOT EXISTS hedge_plans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - plan_type TEXT NOT NULL, - status TEXT NOT NULL, - underlying TEXT NOT NULL, - direction TEXT, - entry_mark REAL, - tp REAL, - sl REAL, - target_price REAL, - sizing_mode_at_open TEXT, - perp_size REAL, - margin REAL, - leverage REAL, - premium_total REAL, - realized_pnl_perp REAL, - realized_pnl_options REAL, - realized_pnl_total REAL, - stats_bucket TEXT, - close_reason TEXT, - wechat_start_sent INTEGER DEFAULT 0, - wechat_end_sent INTEGER DEFAULT 0, - note TEXT, - preview_json TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - opened_at TIMESTAMP, - closed_at TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS hedge_plan_legs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - plan_id INTEGER NOT NULL, - leg_role TEXT NOT NULL, - symbol TEXT, - inst_id TEXT, - opt_type TEXT, - strike REAL, - side TEXT, - size REAL, - avg_open REAL, - premium REAL, - status TEXT, - linked_monitor_id INTEGER, - options_trade_id INTEGER, - exchange_ord_id TEXT, - realized_pnl REAL, - close_reason TEXT, - opened_at TIMESTAMP, - closed_at TIMESTAMP, - FOREIGN KEY(plan_id) REFERENCES hedge_plans(id) - ) - """ - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_hedge_plans_status ON hedge_plans(status)" - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_hedge_plan_legs_plan ON hedge_plan_legs(plan_id)" - ) - _ensure_column(conn, "hedge_plans", "target_price_up", "REAL") - _ensure_column(conn, "hedge_plans", "target_price_down", "REAL") - - -def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None: - rows = conn.execute(f"PRAGMA table_info({table})").fetchall() - names: set[str] = set() - for r in rows: - try: - names.add(str(r["name"])) - except (TypeError, KeyError, IndexError): - names.add(str(r[1])) - if col not in names: - conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}") - - -def count_active_plans(conn: sqlite3.Connection, plan_type: Optional[str] = None) -> int: - if plan_type: - row = conn.execute( - "SELECT COUNT(1) AS c FROM hedge_plans WHERE status IN ('opening','active','partial') AND plan_type=?", - (plan_type,), - ).fetchone() - else: - row = conn.execute( - "SELECT COUNT(1) AS c FROM hedge_plans WHERE status IN ('opening','active','partial')" - ).fetchone() - return int((row["c"] if row else 0) or 0) - - -def insert_plan(conn: sqlite3.Connection, row: dict[str, Any]) -> int: - cols = list(row.keys()) - placeholders = ",".join(["?"] * len(cols)) - conn.execute( - f"INSERT INTO hedge_plans ({','.join(cols)}) VALUES ({placeholders})", - [row[c] for c in cols], - ) - return int(conn.execute("SELECT last_insert_rowid()").fetchone()[0]) - - -def insert_leg(conn: sqlite3.Connection, row: dict[str, Any]) -> int: - cols = list(row.keys()) - placeholders = ",".join(["?"] * len(cols)) - conn.execute( - f"INSERT INTO hedge_plan_legs ({','.join(cols)}) VALUES ({placeholders})", - [row[c] for c in cols], - ) - return int(conn.execute("SELECT last_insert_rowid()").fetchone()[0]) - - -def update_plan(conn: sqlite3.Connection, plan_id: int, **fields: Any) -> None: - if not fields: - return - sets = ", ".join(f"{k}=?" for k in fields) - conn.execute(f"UPDATE hedge_plans SET {sets} WHERE id=?", [*fields.values(), plan_id]) - - -def list_plans( - conn: sqlite3.Connection, - *, - status: Optional[str] = None, - plan_type: Optional[str] = None, - underlying: Optional[str] = None, - limit: int = 50, -) -> list[dict[str, Any]]: - wheres: list[str] = [] - args: list[Any] = [] - if status: - wheres.append("status=?") - args.append(status) - if plan_type: - wheres.append("plan_type=?") - args.append(plan_type) - if underlying: - wheres.append("underlying=?") - args.append(underlying) - where = (" WHERE " + " AND ".join(wheres)) if wheres else "" - rows = conn.execute( - f"SELECT * FROM hedge_plans{where} ORDER BY id DESC LIMIT ?", - [*args, int(limit)], - ).fetchall() - return [dict(r) for r in rows] - - -def get_plan(conn: sqlite3.Connection, plan_id: int) -> Optional[dict[str, Any]]: - row = conn.execute("SELECT * FROM hedge_plans WHERE id=?", (plan_id,)).fetchone() - return dict(row) if row else None - - -def get_plan_legs(conn: sqlite3.Connection, plan_id: int) -> list[dict[str, Any]]: - rows = conn.execute( - "SELECT * FROM hedge_plan_legs WHERE plan_id=? ORDER BY id", (plan_id,) - ).fetchall() - return [dict(r) for r in rows] - - -def delete_plan(conn: sqlite3.Connection, plan_id: int) -> dict[str, Any]: - """删除已结束/失败/取消的计划及其腿;活跃计划拒绝删除.""" - plan = get_plan(conn, int(plan_id)) - if not plan: - return {"ok": False, "msg": "计划不存在"} - st = str(plan.get("status") or "") - if st in ("opening", "active", "partial"): - return {"ok": False, "msg": "进行中的计划不可删除,请先结束"} - conn.execute("DELETE FROM hedge_plan_legs WHERE plan_id=?", (int(plan_id),)) - conn.execute("DELETE FROM hedge_plans WHERE id=?", (int(plan_id),)) - return {"ok": True, "deleted_id": int(plan_id)} - - -def legs_contract_summary(legs: list[dict[str, Any]]) -> str: - parts: list[str] = [] - for leg in legs: - role = str(leg.get("leg_role") or "") - if role == "perp": - name = str(leg.get("symbol") or "永续") - parts.append(f"永续 {name}") - else: - inst = str(leg.get("inst_id") or "") - ot = str(leg.get("opt_type") or "").upper() - strike = leg.get("strike") - label = inst or (f"{ot}{strike}" if ot or strike is not None else role) - parts.append(label) - return " · ".join(parts) if parts else "—" - - -def attach_legs_to_plans(conn: sqlite3.Connection, plans: list[dict[str, Any]]) -> list[dict[str, Any]]: - out: list[dict[str, Any]] = [] - for p in plans: - legs = get_plan_legs(conn, int(p["id"])) - row = dict(p) - row["legs"] = legs - row["contracts_summary"] = legs_contract_summary(legs) - out.append(row) - return out - - -def active_options_targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[str, Any]]: - """返回由进行中「期期对冲」托管的期权目标位,仅供期权页只读展示。 - - 这些目标由 hedge_plan_monitor_lib 执行,绝不能写入 options_target_monitors, - 否则两套监控会同时尝试平掉同一条期权腿。 - """ - rows = conn.execute( - """ - SELECT p.id AS plan_id, p.underlying, p.target_price_up, p.target_price_down, - l.inst_id, l.opt_type - FROM hedge_plans p - JOIN hedge_plan_legs l ON l.plan_id = p.id - WHERE p.plan_type = 'options_options' - AND p.status IN ('opening', 'active', 'partial') - AND l.status = 'open' - AND l.inst_id IS NOT NULL - AND l.inst_id != '' - ORDER BY p.id DESC, l.id DESC - """ - ).fetchall() - out: dict[str, dict[str, Any]] = {} - for raw in rows: - row = dict(raw) - inst_id = str(row.get("inst_id") or "") - opt_type = str(row.get("opt_type") or "").upper() - target = row.get("target_price_up") if opt_type == "C" else row.get("target_price_down") - target_f = _sf(target) - if not inst_id or target_f is None or target_f <= 0 or inst_id in out: - continue - out[inst_id] = { - "plan_id": int(row["plan_id"]), - "inst_id": inst_id, - "underlying": row.get("underlying"), - "opt_type": opt_type, - "target_index": target_f, - "plan_type": "options_options", - "managed_by": "hedge_plan", - } - return out - - -def _sf(v: Any) -> Optional[float]: - try: - if v is None or v == "": - return None - return float(v) - except (TypeError, ValueError): - return None - - -def _metrics_from_pnls(rows: list[dict[str, Any]]) -> dict[str, Any]: - """对一组已结束计划计算胜率/盈亏比/最大盈亏/最大回撤.""" - pnls: list[float] = [] - timed: list[tuple[str, float]] = [] - for r in rows: - pnl = _sf(r.get("realized_pnl_total")) - if pnl is None: - continue - pnls.append(pnl) - t = str(r.get("closed_at") or r.get("opened_at") or r.get("created_at") or "") - timed.append((t, pnl)) - n = len(pnls) - if n == 0: - return { - "count": 0, - "wins": 0, - "losses": 0, - "win_rate": None, - "net_pnl": 0.0, - "avg_pnl": None, - "avg_premium": None, - "profit_factor": None, - "max_profit": None, - "max_loss": None, - "max_drawdown": None, - } - wins = [x for x in pnls if x > 0] - losses = [x for x in pnls if x < 0] - gross_win = sum(wins) - gross_loss = abs(sum(losses)) - if gross_loss > 0: - profit_factor = round(gross_win / gross_loss, 4) - elif gross_win > 0: - profit_factor = None # 全胜,标无限 - else: - profit_factor = 0.0 - - timed.sort(key=lambda x: x[0] or "") - cum = 0.0 - peak = 0.0 - mdd = 0.0 - for _, p in timed: - cum += p - if cum > peak: - peak = cum - dd = peak - cum - if dd > mdd: - mdd = dd - - premiums = [_sf(r.get("premium_total")) for r in rows] - premiums_f = [x for x in premiums if x is not None] - return { - "count": n, - "wins": len(wins), - "losses": len(losses), - "win_rate": round(len(wins) / n, 4), - "net_pnl": round(sum(pnls), 4), - "avg_pnl": round(sum(pnls) / n, 4), - "avg_premium": round(sum(premiums_f) / len(premiums_f), 4) if premiums_f else None, - "profit_factor": profit_factor, - "profit_factor_infinite": bool(gross_loss <= 0 and gross_win > 0), - "max_profit": round(max(pnls), 4), - "max_loss": round(min(pnls), 4), - "max_drawdown": round(mdd, 4), - } - - -def stats_summary(conn: sqlite3.Connection) -> dict[str, Any]: - reason_rows = conn.execute( - """ - SELECT plan_type, close_reason, COUNT(1) AS n, - COALESCE(SUM(realized_pnl_total), 0) AS pnl - FROM hedge_plans - WHERE status='closed' - GROUP BY plan_type, close_reason - """ - ).fetchall() - closed_rows = [ - dict(r) - for r in conn.execute( - "SELECT * FROM hedge_plans WHERE status='closed' ORDER BY COALESCE(closed_at, opened_at, created_at), id" - ).fetchall() - ] - active = count_active_plans(conn) - overall = _metrics_from_pnls(closed_rows) - by_type = { - "perp_options": _metrics_from_pnls( - [r for r in closed_rows if r.get("plan_type") == "perp_options"] - ), - "options_options": _metrics_from_pnls( - [r for r in closed_rows if r.get("plan_type") == "options_options"] - ), - } - # 永期止盈/止损分桶 - po = [r for r in closed_rows if r.get("plan_type") == "perp_options"] - by_type["perp_options"]["buckets"] = { - "tp": _metrics_from_pnls([r for r in po if r.get("close_reason") == "perp_tp"]), - "sl": _metrics_from_pnls([r for r in po if r.get("close_reason") == "perp_sl"]), - } - oo = [r for r in closed_rows if r.get("plan_type") == "options_options"] - by_type["options_options"]["buckets"] = { - "expiry_loss": _metrics_from_pnls( - [r for r in oo if r.get("close_reason") == "oo_expiry_loss"] - ), - "expiry_win": _metrics_from_pnls( - [r for r in oo if r.get("close_reason") == "oo_expiry_win"] - ), - } - return { - "active": active, - "closed_count": overall["count"], - "closed_pnl_total": overall["net_pnl"], - "overall": overall, - "by_type": by_type, - "by_reason": [dict(r) for r in reason_rows], - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_monitor_lib.py b/lib/hedge_plan/hedge_plan_monitor_lib.py index 1048ea8..1dda20c 100644 --- a/lib/hedge_plan/hedge_plan_monitor_lib.py +++ b/lib/hedge_plan/hedge_plan_monitor_lib.py @@ -1,414 +1,12 @@ -"""对冲计划监控:永期 TP/SL、期期目标价、到期结算与微信收口推送.""" -from __future__ import annotations - -import os -from datetime import datetime, timezone -from typing import Any, Optional - -from lib.hedge_plan.hedge_plan_db import get_plan, get_plan_legs, list_plans, update_plan -from lib.hedge_plan.hedge_plan_notify_lib import notify_hedge, notify_plan_end, build_hedge_alert_message -from lib.hedge_plan.hedge_plan_orders_lib import _sell_option -from lib.hedge_plan.hedge_plan_settle_lib import leg_is_expired, settle_option_leg_at_spot - - -def _now() -> str: - return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S") - - -def _env_bool(key: str, default: bool = False) -> bool: - raw = (os.getenv(key) or "").strip().lower() - if not raw: - return default - return raw in ("1", "true", "yes", "on") - - -def _sf(v: Any) -> Optional[float]: - try: - if v is None or v == "": - return None - return float(v) - except (TypeError, ValueError): - return None - - -def _perp_live_contracts(cfg: dict[str, Any], symbol: str, direction: str) -> Optional[float]: - fn = cfg.get("get_live_position_contracts") - if not callable(fn): - return None - try: - return fn(symbol, direction) - except Exception: - return None - - -def _index_px(cfg: dict[str, Any], underlying: str) -> Optional[float]: - ex = cfg.get("exchange_options") - fn = cfg.get("fetch_index_price") - if callable(fn) and ex is not None: - try: - return fn(ex, underlying) - except Exception: - return None - # 无期权账户时回退永续 ticker - ex_perp = cfg.get("exchange") - if ex_perp is not None: - try: - base = (underlying or "ETH").upper() - sym = f"{base}/USDT:USDT" - t = ex_perp.fetch_ticker(sym) - return _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last")) - except Exception: - return None - return None - - -def tick_active_plans(cfg: dict[str, Any]) -> dict[str, Any]: - """扫描 active 计划 + 止盈后遗留期权到期收口.返回处理摘要.""" - get_db = cfg.get("get_db") - if not callable(get_db): - return {"ok": False, "msg": "get_db missing"} - conn = get_db() - acted: list[dict[str, Any]] = [] - try: - from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables - - init_hedge_plan_tables(conn) - plans = list_plans(conn, status="active", limit=40) - for plan in plans: - r = _tick_one(cfg, conn, plan) - if r: - acted.append(r) - orphaned = _settle_orphaned_after_tp(cfg, conn) - acted.extend(orphaned) - conn.commit() - finally: - conn.close() - return {"ok": True, "acted": acted} - - -def _notify_end_reload(cfg: dict[str, Any], conn: Any, plan_id: int) -> None: - plan = get_plan(conn, int(plan_id)) - if plan: - notify_plan_end(cfg, conn, plan) - - -def _tick_one(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> Optional[dict[str, Any]]: - pt = plan.get("plan_type") - legs = get_plan_legs(conn, int(plan["id"])) - if pt == "perp_options": - # 先判断期权是否已过期且永续仍在(罕见);主路径仍是永续平仓侦测 - r = _tick_po(cfg, conn, plan, legs) - return r - if pt == "options_options": - r = _tick_oo_expiry(cfg, conn, plan, legs) - if r: - return r - return _tick_oo_target(cfg, conn, plan, legs) - return None - - -def _tick_po(cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]]) -> Optional[dict[str, Any]]: - perp = next((x for x in legs if x.get("leg_role") == "perp"), None) - opt = next((x for x in legs if x.get("leg_role") == "option_hedge"), None) - if not perp or perp.get("status") != "open": - return None - symbol = perp.get("symbol") or "" - direction = (plan.get("direction") or "long").lower() - live = _perp_live_contracts(cfg, symbol, direction) - # 仍有仓 → 未触达交易所 TP/SL - if live is not None and live > 0: - return None - # 仓已平:用标记/最新粗判 TP or SL - entry = _sf(plan.get("entry_mark")) or _sf(perp.get("avg_open")) or 0 - tp = _sf(plan.get("tp")) - sl = _sf(plan.get("sl")) - mark = None - ex = cfg.get("exchange") - if ex is not None and symbol: - try: - t = ex.fetch_ticker(symbol) - mark = _sf((t.get("info") or {}).get("markPx")) or _sf(t.get("last")) - except Exception: - mark = None - reason = "perp_tp" - if mark is not None and sl is not None and entry: - if direction == "long" and mark <= sl: - reason = "perp_sl" - elif direction == "short" and mark >= sl: - reason = "perp_sl" - elif tp is not None: - if direction == "long" and mark >= tp: - reason = "perp_tp" - elif direction == "short" and mark <= tp: - reason = "perp_tp" - premium = float(plan.get("premium_total") or 0) - cs = float(cfg.get("default_contract_size") or 0.01) - get_cs = cfg.get("get_contract_size") - if callable(get_cs) and symbol: - try: - cs = float(get_cs(symbol) or cs) - except Exception: - pass - size = float(perp.get("size") or 0) - exit_px = mark or (tp if reason == "perp_tp" else sl) or entry - coins = size * cs - if direction == "short": - perp_pnl = (entry - exit_px) * coins - else: - perp_pnl = (exit_px - entry) * coins - - opt_pnl = -premium - if reason == "perp_sl" and opt and _env_bool("HEDGE_PLAN_ON_PERP_SL_CLOSE_OPTIONS", True): - close_r = _sell_option( - cfg, - inst_id=str(opt.get("inst_id") or ""), - sheets=float(opt.get("size") or 1), - ) - if not close_r.get("ok"): - notify_hedge( - cfg, - build_hedge_alert_message( - title="永续止损后期权强制平仓失败", - plan_id=plan.get("id"), - detail=str(close_r.get("msg") or close_r), - ), - ) - if close_r.get("ok"): - bid = _sf(close_r.get("bid")) - ask_open = _sf(opt.get("avg_open")) - if bid is not None and ask_open is not None: - ct = float(opt.get("ct_mult") or 0.01) - opt_pnl = (bid - ask_open) * float(opt.get("size") or 1) * ct - else: - opt_pnl = -premium - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", - ("closed", reason, _now(), opt_pnl, opt["id"]), - ) - elif reason == "perp_tp" and opt: - if _env_bool("HEDGE_PLAN_ON_PERP_TP_CLOSE_OPTIONS", False): - close_r = _sell_option(cfg, inst_id=str(opt.get("inst_id") or ""), sheets=float(opt.get("size") or 1)) - if not close_r.get("ok"): - notify_hedge( - cfg, - build_hedge_alert_message( - title="永续止盈后期权平仓失败", - plan_id=plan.get("id"), - detail=str(close_r.get("msg") or close_r), - ), - ) - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=? WHERE id=?", - ("closed", reason, _now(), opt["id"]), - ) - else: - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=? WHERE id=?", - ("hold_to_expiry", "orphaned_after_tp", opt["id"]), - ) - opt_pnl = -premium - - if reason == "perp_tp": - total = perp_pnl + opt_pnl - else: - total = opt_pnl + perp_pnl - - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", - ("closed", reason, _now(), perp_pnl, perp["id"]), - ) - update_plan( - conn, - int(plan["id"]), - status="closed", - close_reason=reason, - realized_pnl_perp=round(perp_pnl, 4), - realized_pnl_options=round(opt_pnl, 4), - realized_pnl_total=round(total, 4), - stats_bucket="tp" if reason == "perp_tp" else "sl", - closed_at=_now(), - ) - _notify_end_reload(cfg, conn, int(plan["id"])) - return {"plan_id": plan["id"], "close_reason": reason, "total": total} - - -def _tick_oo_target( - cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]] -) -> Optional[dict[str, Any]]: - """期期:触及上破或下破目标价时平盈利腿.""" - idx = _index_px(cfg, str(plan.get("underlying") or "ETH")) - if idx is None: - return None - up = _sf(plan.get("target_price_up")) - down = _sf(plan.get("target_price_down")) - # 旧计划仅有单目标:两边都用它 - legacy = _sf(plan.get("target_price")) - if up is None and legacy is not None: - up = legacy - if down is None and legacy is not None: - down = legacy - if up is None and down is None: - return None - - hit_side: Optional[str] = None - # 上破:现价接近或超过上破目标 - if up is not None and idx >= up * 0.998: - hit_side = "up" - # 下破:现价接近或低于下破目标 - elif down is not None and idx <= down * 1.002: - hit_side = "down" - if not hit_side: - return None - if not _env_bool("HEDGE_PLAN_OO_CLOSE_WINNER_ONLY", True): - return None - open_legs = [x for x in legs if x.get("status") == "open" and str(x.get("leg_role") or "").startswith("option")] - if len(open_legs) < 2: - return None - winners = [] - for leg in open_legs: - strike = _sf(leg.get("strike")) or 0 - o = (leg.get("opt_type") or "").upper() - intrinsic = max(0.0, idx - strike) if o == "C" else max(0.0, strike - idx) - premium = float(leg.get("premium") or 0) - pnl = intrinsic * float(leg.get("size") or 1) * float(leg.get("ct_mult") or 0.01) - premium - winners.append((pnl, leg)) - winners.sort(key=lambda x: x[0], reverse=True) - best_pnl, best = winners[0] - if best_pnl <= 0: - return None - close_r = _sell_option(cfg, inst_id=str(best.get("inst_id") or ""), sheets=float(best.get("size") or 1)) - if not close_r.get("ok"): - notify_hedge( - cfg, - build_hedge_alert_message( - title="期期平盈利腿失败", - plan_id=plan.get("id"), - detail=str(close_r.get("msg") or close_r), - ), - ) - return {"plan_id": plan["id"], "msg": "平盈利腿失败", "close": close_r} - reason = "target_up_win_leg" if hit_side == "up" else "target_down_win_leg" - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", - ("closed", reason, _now(), best_pnl, best["id"]), - ) - update_plan(conn, int(plan["id"]), close_reason=reason) - mid = dict(plan) - mid["close_reason"] = reason - mid["status"] = "active" - notify_plan_end(cfg, conn, mid) - return { - "plan_id": plan["id"], - "close_reason": reason, - "hit_side": hit_side, - "closed_leg": best.get("id"), - "index": idx, - } - - -def _tick_oo_expiry( - cfg: dict[str, Any], conn: Any, plan: dict[str, Any], legs: list[dict[str, Any]] -) -> Optional[dict[str, Any]]: - """期期:剩余期权腿全部到期 → 结算合计并结束计划.""" - pending = [ - x - for x in legs - if str(x.get("leg_role") or "").startswith("option") - and str(x.get("status") or "") in ("open", "hold_to_expiry") - ] - if not pending: - # 若腿已全部 closed 但计划仍 active(异常残留)则用腿合计收口 - closed_opts = [ - x for x in legs if str(x.get("leg_role") or "").startswith("option") and x.get("status") == "closed" - ] - if len(closed_opts) < 1: - return None - total_opts = sum(float(x.get("realized_pnl") or 0) for x in closed_opts) - reason = "oo_expiry_loss" if total_opts <= 0 else "oo_expiry_win" - update_plan( - conn, - int(plan["id"]), - status="closed", - close_reason=reason, - realized_pnl_options=round(total_opts, 4), - realized_pnl_total=round(total_opts, 4), - stats_bucket=reason if reason == "oo_expiry_loss" else "oo_target", - closed_at=_now(), - ) - _notify_end_reload(cfg, conn, int(plan["id"])) - return {"plan_id": plan["id"], "close_reason": reason, "total": total_opts} - - if not all(leg_is_expired(x) for x in pending): - return None - - spot = _index_px(cfg, str(plan.get("underlying") or "ETH")) - if spot is None: - return None - - settled_sum = 0.0 - for leg in pending: - pnl = settle_option_leg_at_spot(leg, float(spot)) - settled_sum += pnl - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", - ("closed", "expiry", _now(), round(pnl, 4), leg["id"]), - ) - - already = sum( - float(x.get("realized_pnl") or 0) - for x in legs - if str(x.get("leg_role") or "").startswith("option") and x.get("status") == "closed" - ) - total = already + settled_sum - reason = "oo_expiry_loss" if total <= 0 else "oo_expiry_win" - bucket = "oo_expiry_loss" if reason == "oo_expiry_loss" else "oo_target" - update_plan( - conn, - int(plan["id"]), - status="closed", - close_reason=reason, - realized_pnl_options=round(total, 4), - realized_pnl_total=round(total, 4), - stats_bucket=bucket, - closed_at=_now(), - ) - _notify_end_reload(cfg, conn, int(plan["id"])) - return {"plan_id": plan["id"], "close_reason": reason, "total": total, "spot": spot} - - -def _settle_orphaned_after_tp(cfg: dict[str, Any], conn: Any) -> list[dict[str, Any]]: - """永期止盈后 hold_to_expiry 期权到期:只更新腿,不回写计划合计.""" - rows = conn.execute( - """ - SELECT l.id AS leg_id, l.plan_id, l.inst_id, l.opt_type, l.strike, l.size, l.premium, l.status, - p.underlying, p.status AS plan_status - FROM hedge_plan_legs l - JOIN hedge_plans p ON p.id = l.plan_id - WHERE l.status = 'hold_to_expiry' AND l.close_reason = 'orphaned_after_tp' - LIMIT 40 - """ - ).fetchall() - acted: list[dict[str, Any]] = [] - for row in rows: - leg = dict(row) - if not leg_is_expired(leg): - continue - spot = _index_px(cfg, str(leg.get("underlying") or "ETH")) - if spot is None: - continue - pnl = settle_option_leg_at_spot(leg, float(spot)) - conn.execute( - "UPDATE hedge_plan_legs SET status=?, close_reason=?, closed_at=?, realized_pnl=? WHERE id=?", - ("closed", "expiry", _now(), round(pnl, 4), leg["leg_id"]), - ) - # 故意不 UPDATE hedge_plans.realized_pnl_* - acted.append( - { - "plan_id": leg["plan_id"], - "close_reason": "orphaned_option_expiry", - "leg_id": leg["leg_id"], - "leg_pnl": round(pnl, 4), - "note": "不回写计划合计", - } - ) - return acted +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_notify_lib.py b/lib/hedge_plan/hedge_plan_notify_lib.py index 76a13a3..18380a3 100644 --- a/lib/hedge_plan/hedge_plan_notify_lib.py +++ b/lib/hedge_plan/hedge_plan_notify_lib.py @@ -1,186 +1,12 @@ -"""对冲计划企业微信推送(起止必发,幂等落库标记).""" -from __future__ import annotations - -from typing import Any, Callable, Optional - -from lib.hedge_plan.hedge_plan_db import update_plan - - -def _fmt(v: Any, d: int = 2) -> str: - try: - if v is None or v == "": - return "—" - return f"{float(v):.{d}f}" - except (TypeError, ValueError): - return str(v) - - -def _type_label(plan_type: str) -> str: - return "永期对冲" if (plan_type or "") == "perp_options" else "期期对冲" - - -def _dir_label(direction: str) -> str: - d = (direction or "").lower() - if d == "long": - return "做多" - if d == "short": - return "做空" - return "—" - - -def build_hedge_start_message(plan: dict[str, Any], *, legs: Optional[list[dict[str, Any]]] = None) -> str: - pt = plan.get("plan_type") or "" - lines = [ - f"🟢 对冲计划启动 #{plan.get('id')}", - f"📌 类型:{_type_label(pt)}", - f"🪙 标的:{plan.get('underlying') or '—'}", - ] - if pt == "perp_options": - lines.extend( - [ - f"📈 方向:{_dir_label(plan.get('direction') or '')}", - f"💵 开仓参考:{_fmt(plan.get('entry_mark'))}", - f"🎯 止盈:{_fmt(plan.get('tp'))}|止损:{_fmt(plan.get('sl'))}", - f"📦 永续张数:{_fmt(plan.get('perp_size'), 4)}|杠杆:{_fmt(plan.get('leverage'), 0)}x", - f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC", - ] - ) - else: - lines.extend( - [ - f"🎯 上破:{_fmt(plan.get('target_price_up') or plan.get('target_price'))}" - f"|下破:{_fmt(plan.get('target_price_down') or plan.get('target_price'))}", - f"💎 期权保费合计:{_fmt(plan.get('premium_total'), 4)} USDC", - ] - ) - if legs: - for leg in legs: - role = leg.get("leg_role") or "" - if role == "perp": - lines.append(f"· 永续腿 {leg.get('symbol') or ''} ×{_fmt(leg.get('size'), 4)}") - else: - lines.append( - f"· {role} {(leg.get('opt_type') or '')} K{_fmt(leg.get('strike'), 0)} " - f"×{_fmt(leg.get('size'), 0)}张 {leg.get('inst_id') or ''}" - ) - lines.append("📎 独立模块推送,不进普通交易复盘") - return "\n".join(lines) - - -def build_hedge_end_message(plan: dict[str, Any]) -> str: - reason = plan.get("close_reason") or "—" - total = plan.get("realized_pnl_total") - try: - tv = float(total) if total is not None else None - except (TypeError, ValueError): - tv = None - head = "🔴" if (tv is not None and tv < 0) else "🟢" - reason_map = { - "perp_tp": "永续止盈(期权默认不平)", - "perp_sl": "永续止损(期权强制平)", - "target_win_leg": "期期已平盈利腿(中间态)", - "target_up_win_leg": "期期上破·已平盈利腿", - "target_down_win_leg": "期期下破·已平盈利腿", - "oo_expiry_loss": "期期到期无盈利·总亏损", - "oo_expiry_win": "期期到期仍盈利", - "expiry": "到期收口", - "manual": "人工结束", - "partial_fail": "半腿失败收尾", - "cancelled": "已取消", - } - lines = [ - f"{head} 对冲计划结束 #{plan.get('id')}", - f"📌 类型:{_type_label(plan.get('plan_type') or '')}", - f"🪙 标的:{plan.get('underlying') or '—'}", - f"📎 原因:{reason_map.get(reason, reason)}", - f"💰 合计≈U:{_fmt(total)}", - f"· 永续分项:{_fmt(plan.get('realized_pnl_perp'))} USDT", - f"· 期权分项:{_fmt(plan.get('realized_pnl_options'))} USDC(≈U 1:1)", - f"⏱ 开仓:{plan.get('opened_at') or '—'}|结束:{plan.get('closed_at') or '—'}", - ] - return "\n".join(lines) - - -def build_hedge_alert_message( - *, - title: str, - plan_id: Any = None, - detail: str = "", -) -> str: - lines = [f"⚠️ 对冲计划告警{(' #' + str(plan_id)) if plan_id else ''}", f"📌 {title}"] - if detail: - lines.append(str(detail)[:800]) - return "\n".join(lines) - - -def notify_hedge( - cfg: dict[str, Any], - content: str, -) -> bool: - send: Optional[Callable[[str], Any]] = cfg.get("send_wechat") - if not callable(send): - return False - try: - send(content) - return True - except Exception: - return False - - -def notify_plan_start( - cfg: dict[str, Any], - conn: Any, - plan: dict[str, Any], - legs: Optional[list[dict[str, Any]]] = None, -) -> bool: - if int(plan.get("wechat_start_sent") or 0): - return False - ok = notify_hedge(cfg, build_hedge_start_message(plan, legs=legs)) - if ok and plan.get("id") is not None: - update_plan(conn, int(plan["id"]), wechat_start_sent=1) - plan["wechat_start_sent"] = 1 - return ok - - -def notify_plan_end(cfg: dict[str, Any], conn: Any, plan: dict[str, Any]) -> bool: - if int(plan.get("wechat_end_sent") or 0): - return False - # 中间态 target_win_leg 不算正式结束推送(用告警) - if (plan.get("close_reason") or "") in ( - "target_win_leg", - "target_up_win_leg", - "target_down_win_leg", - ) and (plan.get("status") or "") != "closed": - side = "上破" if "up" in str(plan.get("close_reason")) else ( - "下破" if "down" in str(plan.get("close_reason")) else "目标价" - ) - notify_hedge( - cfg, - build_hedge_alert_message( - title=f"期期{side}已平盈利腿,亏损腿继续持有至到期", - plan_id=plan.get("id"), - detail=( - f"上破 {_fmt(plan.get('target_price_up') or plan.get('target_price'))}" - f"|下破 {_fmt(plan.get('target_price_down') or plan.get('target_price'))}" - ), - ), - ) - return True - ok = notify_hedge(cfg, build_hedge_end_message(plan)) - if ok and plan.get("id") is not None: - update_plan(conn, int(plan["id"]), wechat_end_sent=1) - plan["wechat_end_sent"] = 1 - return ok - - -def notify_partial_fail(cfg: dict[str, Any], *, plan_type: str, msg: str, results: Any = None) -> bool: - detail = msg - if results: - try: - detail = f"{msg}\n路径结果:{results}"[:800] - except Exception: - pass - return notify_hedge( - cfg, - build_hedge_alert_message(title=f"{_type_label(plan_type)}半腿失败", detail=detail), - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_orders_lib.py b/lib/hedge_plan/hedge_plan_orders_lib.py index ba9f5c5..78dbeb6 100644 --- a/lib/hedge_plan/hedge_plan_orders_lib.py +++ b/lib/hedge_plan/hedge_plan_orders_lib.py @@ -1,429 +1,12 @@ -"""对冲计划开仓/平仓编排(可 dry_run 校验下单路径).""" -from __future__ import annotations - -import json -import os -from datetime import datetime, timezone -from typing import Any, Callable, Optional - - -def _now() -> str: - return datetime.now(timezone.utc).astimezone().strftime("%Y-%m-%d %H:%M:%S") - - -def _env_bool(key: str, default: bool = False) -> bool: - raw = (os.getenv(key) or "").strip().lower() - if not raw: - return default - return raw in ("1", "true", "yes", "on") - - -def open_order_mode() -> str: - v = (os.getenv("HEDGE_PLAN_OPEN_ORDER") or "options_first").strip().lower() - return v if v in ("options_first", "perp_first") else "options_first" - - -def build_po_path_plan(body: dict[str, Any]) -> list[dict[str, Any]]: - """永期下单路径清单(不交易).""" - mode = open_order_mode() - opt = { - "step": "options_buy_limit", - "account": "options", - "inst_id": body.get("opt_inst_id"), - "sheets": float(body.get("sheets") or 1), - "side": "buy", - "price_hint": "ask", - } - perp = { - "step": "perp_market_open", - "account": "swap", - "symbol": body.get("exchange_symbol"), - "direction": body.get("direction") or "long", - "contracts": float(body.get("contracts") or 0), - "tp": body.get("tp"), - "sl": body.get("sl"), - "attach_tpsl": True, - } - return [opt, perp] if mode == "options_first" else [perp, opt] - - -def build_oo_path_plan(body: dict[str, Any]) -> list[dict[str, Any]]: - return [ - { - "step": "options_buy_limit", - "account": "options", - "leg": "a", - "inst_id": (body.get("leg_a") or {}).get("inst_id"), - "sheets": float((body.get("leg_a") or {}).get("sheets") or 1), - "side": "buy", - "price_hint": "ask", - }, - { - "step": "options_buy_limit", - "account": "options", - "leg": "b", - "inst_id": (body.get("leg_b") or {}).get("inst_id"), - "sheets": float((body.get("leg_b") or {}).get("sheets") or 1), - "side": "buy", - "price_hint": "ask", - }, - ] - - -def _buy_option( - cfg: dict[str, Any], - *, - inst_id: str, - sheets: float, - dry_run: bool, -) -> dict[str, Any]: - from lib.exchange.okx_options_lib import ( - cap_option_buy_sheets_to_ask_depth, - option_buy_liquidity_ok, - ) - - ex = cfg.get("exchange_options") - quote_fn = cfg.get("quote_option_contract") - place_fn = cfg.get("place_option_limit_order") - td_buy = cfg.get("td_mode_for_option_buy") - if not inst_id: - return {"ok": False, "msg": "缺少期权合约"} - if not callable(quote_fn) or ex is None: - return {"ok": False, "msg": "期权报价能力未就绪"} - q = quote_fn(ex, inst_id) - if not q.get("ok"): - return {"ok": False, "msg": q.get("msg") or "期权报价失败", "quote": q} - ask = q.get("ask") - ask_sz = q.get("ask_sz") - can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz) - if not can_open: - return { - "ok": False, - "msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入", - "quote": q, - "mark": q.get("mark"), - "ref_ask": q.get("ref_ask"), - "can_open": False, - } - sheets_i = max(1, int(round(float(sheets)))) - capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets_i, ask_sz, min_sz=1) - if capped is None: - return {"ok": False, "msg": cap_msg or "卖一深度不足,无法买入", "quote": q} - sheets_i = capped - ct_mult = float(q.get("ct_mult") or 0.01) - premium = float(ask) * sheets_i * ct_mult - if dry_run: - return { - "ok": True, - "dry_run": True, - "inst_id": inst_id, - "sheets": sheets_i, - "ask": float(ask), - "ask_sz": float(ask_sz), - "premium": premium, - "ct_mult": ct_mult, - "tick_sz": q.get("tick_sz"), - "meta": q.get("meta") or {}, - "strike": q.get("strike"), - "exp_time": q.get("exp_time"), - "opt_type": (q.get("meta") or {}).get("optType") or q.get("opt_type"), - "can_open": True, - } - if not callable(place_fn): - return {"ok": False, "msg": "期权限价下单未注入"} - td = "isolated" - if callable(td_buy): - td = td_buy(cfg.get("options_td_mode") or "isolated") - order = place_fn( - ex, - inst_id=inst_id, - side="buy", - sheets=sheets_i, - price=float(ask), - td_mode=td, - tick_sz=q.get("tick_sz"), - ) - if not order.get("ok"): - return order - return { - "ok": True, - "inst_id": inst_id, - "sheets": sheets_i, - "ask": float(ask), - "ask_sz": float(ask_sz), - "premium": premium, - "ct_mult": ct_mult, - "tick_sz": q.get("tick_sz"), - "meta": q.get("meta") or {}, - "strike": q.get("strike"), - "exp_time": q.get("exp_time"), - "opt_type": (q.get("meta") or {}).get("optType") or q.get("opt_type"), - "exchange_ord_id": (order.get("data") or {}).get("ordId"), - "order": order, - "can_open": True, - } - - -def _open_perp( - cfg: dict[str, Any], - *, - symbol: str, - direction: str, - contracts: float, - leverage: int, - tp: float, - sl: float, - dry_run: bool, -) -> dict[str, Any]: - if not symbol or contracts <= 0: - return {"ok": False, "msg": "永续符号或张数无效"} - amount = float(contracts) - to_prec = cfg.get("amount_to_precision") - ex = cfg.get("exchange") - if callable(to_prec) and ex is not None: - try: - amount = float(to_prec(symbol, amount)) - except Exception: - pass - if amount <= 0: - return {"ok": False, "msg": "张数经精度舍入后为 0"} - if dry_run: - return { - "ok": True, - "dry_run": True, - "symbol": symbol, - "direction": direction, - "contracts": amount, - "leverage": leverage, - "tp": tp, - "sl": sl, - } - ensure = cfg.get("ensure_okx_live_ready") - if callable(ensure): - ok, msg = ensure() - if not ok: - return {"ok": False, "msg": msg or "实盘未就绪"} - place = cfg.get("place_exchange_order") - if not callable(place): - return {"ok": False, "msg": "永续下单函数未注入"} - try: - order = place(symbol, direction, amount, leverage, stop_loss=sl, take_profit=tp) - except Exception as e: - return {"ok": False, "msg": f"永续开仓失败: {e}"} - return { - "ok": True, - "symbol": symbol, - "direction": direction, - "contracts": amount, - "leverage": leverage, - "tp": tp, - "sl": sl, - "order": order, - "exchange_ord_id": str((order or {}).get("id") or (order or {}).get("info", {}).get("ordId") or ""), - } - - -def _sell_option( - cfg: dict[str, Any], - *, - inst_id: str, - sheets: float, - dry_run: bool = False, -) -> dict[str, Any]: - ex = cfg.get("exchange_options") - quote_fn = cfg.get("quote_option_contract") - place_fn = cfg.get("place_option_limit_order") - if not callable(quote_fn) or ex is None: - return {"ok": False, "msg": "期权报价能力未就绪"} - q = quote_fn(ex, inst_id) - bid = q.get("bid") if q.get("ok") else None - if bid is None or float(bid) <= 0: - return {"ok": False, "msg": "暂无买一价,无法平期权"} - sheets_i = max(1, int(round(float(sheets)))) - if dry_run: - return {"ok": True, "dry_run": True, "inst_id": inst_id, "sheets": sheets_i, "bid": float(bid)} - if not callable(place_fn): - return {"ok": False, "msg": "期权平仓未注入"} - order = place_fn( - ex, - inst_id=inst_id, - side="sell", - sheets=sheets_i, - price=float(bid), - td_mode="isolated", - tick_sz=q.get("tick_sz"), - reduce_only=True, - ) - return order if order.get("ok") else order - - -def execute_perp_options_start( - cfg: dict[str, Any], - body: dict[str, Any], - *, - dry_run: bool = False, - persist: Optional[Callable[..., Any]] = None, -) -> dict[str, Any]: - path = build_po_path_plan(body) - results: list[dict[str, Any]] = [] - opt_res: Optional[dict[str, Any]] = None - perp_res: Optional[dict[str, Any]] = None - for step in path: - if step["step"] == "options_buy_limit": - opt_res = _buy_option( - cfg, - inst_id=str(body.get("opt_inst_id") or ""), - sheets=float(body.get("sheets") or 1), - dry_run=dry_run, - ) - results.append({"step": step["step"], **opt_res}) - if not opt_res.get("ok"): - return {"ok": False, "msg": opt_res.get("msg") or "期权开仓失败", "path": path, "results": results} - else: - perp_res = _open_perp( - cfg, - symbol=str(body.get("exchange_symbol") or ""), - direction=str(body.get("direction") or "long"), - contracts=float(body.get("contracts") or 0), - leverage=int(body.get("leverage") or 10), - tp=float(body["tp"]), - sl=float(body["sl"]), - dry_run=dry_run, - ) - results.append({"step": step["step"], **perp_res}) - if not perp_res.get("ok"): - # 半腿补偿:期权已成 + 配置允许则平期权 - if opt_res and opt_res.get("ok") and not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True): - close_r = _sell_option( - cfg, - inst_id=str(opt_res.get("inst_id") or body.get("opt_inst_id") or ""), - sheets=float(opt_res.get("sheets") or body.get("sheets") or 1), - ) - results.append({"step": "options_auto_close_on_perp_fail", **close_r}) - msg = perp_res.get("msg") or "永续开仓失败" - if not dry_run: - try: - from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail - - notify_partial_fail( - cfg, plan_type="perp_options", msg=msg, results=results - ) - except Exception: - pass - return { - "ok": False, - "msg": msg, - "path": path, - "results": results, - "partial": True, - } - - out = { - "ok": True, - "dry_run": dry_run, - "plan_type": "perp_options", - "path": path, - "results": results, - "option": opt_res, - "perp": perp_res, - "opened_at": _now(), - } - if persist and not dry_run: - out["plan_id"] = persist(out, body) - return out - - -def execute_options_options_start( - cfg: dict[str, Any], - body: dict[str, Any], - *, - dry_run: bool = False, - persist: Optional[Callable[..., Any]] = None, -) -> dict[str, Any]: - path = build_oo_path_plan(body) - results: list[dict[str, Any]] = [] - leg_a = body.get("leg_a") or {} - leg_b = body.get("leg_b") or {} - a_res = _buy_option(cfg, inst_id=str(leg_a.get("inst_id") or ""), sheets=float(leg_a.get("sheets") or 1), dry_run=dry_run) - results.append({"step": "options_buy_limit", "leg": "a", **a_res}) - if not a_res.get("ok"): - return {"ok": False, "msg": a_res.get("msg") or "腿A开仓失败", "path": path, "results": results} - b_res = _buy_option(cfg, inst_id=str(leg_b.get("inst_id") or ""), sheets=float(leg_b.get("sheets") or 1), dry_run=dry_run) - results.append({"step": "options_buy_limit", "leg": "b", **b_res}) - if not b_res.get("ok"): - if not dry_run and _env_bool("HEDGE_PLAN_PARTIAL_AUTO_CLOSE_OPTION", True): - close_r = _sell_option(cfg, inst_id=str(a_res.get("inst_id") or ""), sheets=float(a_res.get("sheets") or 1)) - results.append({"step": "options_auto_close_leg_a", **close_r}) - msg = b_res.get("msg") or "腿B开仓失败" - if not dry_run: - try: - from lib.hedge_plan.hedge_plan_notify_lib import notify_partial_fail - - notify_partial_fail(cfg, plan_type="options_options", msg=msg, results=results) - except Exception: - pass - return { - "ok": False, - "msg": msg, - "path": path, - "results": results, - "partial": True, - } - out = { - "ok": True, - "dry_run": dry_run, - "plan_type": "options_options", - "path": path, - "results": results, - "leg_a": a_res, - "leg_b": b_res, - "opened_at": _now(), - } - if persist and not dry_run: - out["plan_id"] = persist(out, body) - return out - - -def validate_start_body(plan_type: str, body: dict[str, Any]) -> Optional[str]: - pt = (plan_type or "").strip().lower() - if pt == "perp_options": - need = ("direction", "entry", "tp", "sl", "contracts", "opt_inst_id", "sheets", "exchange_symbol") - for k in need: - if body.get(k) in (None, ""): - return f"缺少字段: {k}" - try: - if float(body["contracts"]) <= 0 or float(body["sheets"]) <= 0: - return "张数必须大于 0" - if float(body["tp"]) <= 0 or float(body["sl"]) <= 0: - return "止盈/止损无效" - except (TypeError, ValueError): - return "数值字段无效" - return None - if pt == "options_options": - a = body.get("leg_a") or {} - b = body.get("leg_b") or {} - if not a.get("inst_id") or not b.get("inst_id"): - return "请选用两条期权腿" - up = body.get("target_price_up") - down = body.get("target_price_down") - legacy = body.get("target_price") - if up in (None, "") and legacy not in (None, ""): - up = legacy - if down in (None, "") and legacy not in (None, ""): - down = legacy - if up in (None, "") or down in (None, ""): - return "请填写上破与下破目标价" - try: - if float(up) <= float(down): - return "上破目标价必须大于下破目标价" - except (TypeError, ValueError): - return "目标价无效" - return None - return "未知计划类型" - - -def dump_preview(preview: Any) -> str: - try: - return json.dumps(preview, ensure_ascii=False)[:8000] - except Exception: - return "" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_register.py b/lib/hedge_plan/hedge_plan_register.py index 5a7e26a..f8d0a0a 100644 --- a/lib/hedge_plan/hedge_plan_register.py +++ b/lib/hedge_plan/hedge_plan_register.py @@ -1,816 +1,12 @@ -"""OKX 对冲计划:P0 测算页与 API 注册.""" -from __future__ import annotations - -import os -from typing import Any - -from flask import Flask, jsonify, request -from jinja2 import ChoiceLoader, FileSystemLoader - -from lib.hedge_plan.hedge_plan_calc_lib import ( - build_options_options_preview, - build_perp_options_preview, - floor_contracts_to_precision, - gate_status, - option_premium_total, - suggest_contracts_from_notional, -) -from lib.hub.hub_calculator_market_lib import amount_decimals_from_exchange -from lib.trade.position_sizing_lib import ( - compute_full_margin_sizing, - load_position_sizing_mode, -) - - -def _env_bool(key: str, default: bool = False) -> bool: - raw = (os.getenv(key) or "").strip().lower() - if not raw: - return default - return raw in ("1", "true", "yes", "on") - - -def attach_hedge_plan_templates(app: Flask, repo_root: str) -> None: - tpl_dir = os.path.join(repo_root, "lib", "hedge_plan", "templates") - if not os.path.isdir(tpl_dir): - return - existing = app.jinja_loader - loaders = [FileSystemLoader(tpl_dir)] - if existing is not None: - if isinstance(existing, ChoiceLoader): - loaders = list(existing.loaders) + loaders - else: - loaders.insert(0, existing) - app.jinja_loader = ChoiceLoader(loaders) - - -def install_hedge_plan(app: Flask, repo_root: str, app_module: Any) -> None: - attach_hedge_plan_templates(app, repo_root) - cfg = _build_cfg(app_module) - app.extensions["hedge_plan_cfg"] = cfg - register_hedge_plan_routes(app, cfg) - _maybe_start_monitor(cfg) - - -def _build_cfg(app_module: Any) -> dict[str, Any]: - from lib.exchange.okx_options_lib import ( - build_option_chain, - fetch_index_price, - options_header_balances, - place_option_limit_order, - quote_option_contract, - td_mode_for_option_buy, - ) - - def _amount_to_precision(sym: str, amt: float) -> float: - ex = getattr(app_module, "exchange", None) - if ex is None: - return float(amt) - return float(ex.amount_to_precision(sym, amt)) - - return { - "get_db": app_module.get_db, - "login_required": app_module.login_required, - "render_main_page": app_module.render_main_page, - "exchange": getattr(app_module, "exchange", None), - "exchange_options": getattr(app_module, "exchange_options", None), - "get_available_trading_usdt": getattr(app_module, "get_available_trading_usdt", None), - "get_contract_size": getattr(app_module, "get_contract_size", None), - "normalize_exchange_symbol": getattr(app_module, "normalize_exchange_symbol", None), - "ensure_markets_loaded": getattr(app_module, "ensure_markets_loaded", None), - "ensure_okx_live_ready": getattr(app_module, "ensure_okx_live_ready", None), - "place_exchange_order": getattr(app_module, "place_exchange_order", None), - "get_live_position_contracts": getattr(app_module, "get_live_position_contracts", None), - "amount_to_precision": _amount_to_precision, - "build_option_chain": build_option_chain, - "options_header_balances": options_header_balances, - "quote_option_contract": quote_option_contract, - "place_option_limit_order": place_option_limit_order, - "td_mode_for_option_buy": td_mode_for_option_buy, - "fetch_index_price": fetch_index_price, - "options_td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "isolated").strip(), - "btc_leverage": int(getattr(app_module, "BTC_LEVERAGE", 10) or 10), - "alt_leverage": int(getattr(app_module, "ALT_LEVERAGE", 5) or 5), - "full_margin_buffer": float(getattr(app_module, "FULL_MARGIN_BUFFER_RATIO", 0.98) or 0.98), - "funds_decimals": int(getattr(app_module, "FUNDS_DECIMALS", 2) or 2), - "options_enabled": _env_bool("OKX_OPTIONS_ENABLED", False), - "default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(), - "chain_max_dte": float(os.getenv("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS") or os.getenv("OKX_OPTIONS_MAX_DTE_DAYS") or "14"), - "perp_account_label": (os.getenv("OKX_ACCOUNT_LABEL") or "合约账户").strip(), - "options_account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "期权账户").strip(), - "live_trading": _env_bool("LIVE_TRADING_ENABLED", False), - "send_wechat": getattr(app_module, "send_wechat_msg", None), - } - - -def _hedge_enabled() -> bool: - return _env_bool("HEDGE_PLAN_ENABLED", False) - - -def _live_order() -> bool: - return _env_bool("HEDGE_PLAN_LIVE_ORDER", False) - - -def _max_active() -> int: - try: - return max(1, int(os.getenv("MAX_ACTIVE_HEDGE_PLANS") or "1")) - except ValueError: - return 1 - - -def _gates_dict(cfg: dict[str, Any], plan_type: str) -> dict[str, Any]: - active = 0 - try: - from lib.hedge_plan.hedge_plan_db import count_active_plans, init_hedge_plan_tables - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - active = count_active_plans(conn) - conn.commit() - finally: - conn.close() - except Exception: - active = 0 - return gate_status( - hedge_enabled=_hedge_enabled(), - sizing_mode=load_position_sizing_mode(), - plan_type=plan_type, - options_enabled=bool(cfg.get("options_enabled")), - live_order=_live_order(), - live_trading=bool(cfg.get("live_trading")) or _env_bool("LIVE_TRADING_ENABLED", False), - active_count=active, - max_active=_max_active(), - ) - - -def _maybe_start_monitor(cfg: dict[str, Any]) -> None: - if not _hedge_enabled(): - return - try: - secs = float(os.getenv("HEDGE_PLAN_MONITOR_POLL_SECONDS") or "15") - except ValueError: - secs = 15.0 - secs = max(5.0, secs) - - def _loop() -> None: - import time - - from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans - - while True: - try: - tick_active_plans(cfg) - except Exception: - pass - time.sleep(secs) - - import threading - - t = threading.Thread(target=_loop, name="hedge-plan-monitor", daemon=True) - t.start() - cfg["hedge_monitor_thread"] = t - - -def _persist_po(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int: - from lib.hedge_plan.hedge_plan_db import ( - get_plan, - get_plan_legs, - init_hedge_plan_tables, - insert_leg, - insert_plan, - ) - from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - opt = result.get("option") or {} - perp = result.get("perp") or {} - premium = float(opt.get("premium") or 0) - plan_id = insert_plan( - conn, - { - "plan_type": "perp_options", - "status": "active", - "underlying": str(body.get("underlying") or "ETH").upper(), - "direction": str(body.get("direction") or "long"), - "entry_mark": float(body.get("entry") or 0), - "tp": float(body.get("tp") or 0), - "sl": float(body.get("sl") or 0), - "sizing_mode_at_open": load_position_sizing_mode(), - "perp_size": float(perp.get("contracts") or body.get("contracts") or 0), - "margin": body.get("margin"), - "leverage": float(body.get("leverage") or 10), - "premium_total": premium, - "opened_at": result.get("opened_at"), - }, - ) - insert_leg( - conn, - { - "plan_id": plan_id, - "leg_role": "perp", - "symbol": str(body.get("exchange_symbol") or ""), - "side": str(body.get("direction") or "long"), - "size": float(perp.get("contracts") or body.get("contracts") or 0), - "avg_open": float(body.get("entry") or 0), - "status": "open", - "exchange_ord_id": str(perp.get("exchange_ord_id") or ""), - "opened_at": result.get("opened_at"), - }, - ) - insert_leg( - conn, - { - "plan_id": plan_id, - "leg_role": "option_hedge", - "inst_id": str(opt.get("inst_id") or body.get("opt_inst_id") or ""), - "opt_type": str(opt.get("opt_type") or body.get("opt_type") or ""), - "strike": opt.get("strike") or body.get("strike"), - "side": "buy", - "size": float(opt.get("sheets") or body.get("sheets") or 1), - "avg_open": float(opt.get("ask") or 0), - "premium": premium, - "status": "open", - "exchange_ord_id": str(opt.get("exchange_ord_id") or ""), - "opened_at": result.get("opened_at"), - }, - ) - conn.commit() - plan = get_plan(conn, plan_id) - legs = get_plan_legs(conn, plan_id) - if plan: - notify_plan_start(cfg, conn, plan, legs) - conn.commit() - return plan_id - finally: - conn.close() - - -def _persist_oo(cfg: dict[str, Any], result: dict[str, Any], body: dict[str, Any]) -> int: - from lib.hedge_plan.hedge_plan_db import ( - get_plan, - get_plan_legs, - init_hedge_plan_tables, - insert_leg, - insert_plan, - ) - from lib.hedge_plan.hedge_plan_notify_lib import notify_plan_start - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - a = result.get("leg_a") or {} - b = result.get("leg_b") or {} - premium = float(a.get("premium") or 0) + float(b.get("premium") or 0) - plan_id = insert_plan( - conn, - { - "plan_type": "options_options", - "status": "active", - "underlying": str(body.get("underlying") or "ETH").upper(), - "target_price": float( - body.get("target_price_up") - or body.get("target_price") - or 0 - ), - "target_price_up": float( - body.get("target_price_up") - or body.get("target_price") - or 0 - ), - "target_price_down": float( - body.get("target_price_down") - or body.get("target_price") - or 0 - ), - "sizing_mode_at_open": load_position_sizing_mode(), - "premium_total": premium, - "opened_at": result.get("opened_at"), - }, - ) - for role, res, src in (("option_a", a, body.get("leg_a") or {}), ("option_b", b, body.get("leg_b") or {})): - insert_leg( - conn, - { - "plan_id": plan_id, - "leg_role": role, - "inst_id": str(res.get("inst_id") or src.get("inst_id") or ""), - "opt_type": str(res.get("opt_type") or src.get("opt_type") or ""), - "strike": res.get("strike") or src.get("strike"), - "side": "buy", - "size": float(res.get("sheets") or src.get("sheets") or 1), - "avg_open": float(res.get("ask") or 0), - "premium": float(res.get("premium") or 0), - "status": "open", - "exchange_ord_id": str(res.get("exchange_ord_id") or ""), - "opened_at": result.get("opened_at"), - }, - ) - conn.commit() - plan = get_plan(conn, plan_id) - legs = get_plan_legs(conn, plan_id) - if plan: - notify_plan_start(cfg, conn, plan, legs) - conn.commit() - return plan_id - finally: - conn.close() - - -def register_hedge_plan_routes(app: Flask, cfg: dict[str, Any]) -> None: - lr = cfg["login_required"] - - @app.route("/hedge-plan") - @lr - def page_hedge_plan(): - from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled - - redir = redirect_to_embed_shell_if_enabled("hedge_plan") - if redir is not None: - return redir - return cfg["render_main_page"]("hedge_plan") - - @app.route("/api/hedge-plan/gates") - @lr - def api_hedge_gates(): - plan_type = (request.args.get("plan_type") or "perp_options").strip() - return jsonify({"ok": True, **_gates_dict(cfg, plan_type)}) - - @app.route("/api/hedge-plan/market") - @lr - def api_hedge_market(): - base = (request.args.get("base") or cfg.get("default_underly") or "ETH").strip().upper() - if base not in ("BTC", "ETH"): - return jsonify({"ok": False, "msg": "对冲计划仅支持 BTC/ETH"}), 400 - direction = (request.args.get("direction") or "long").strip().lower() - if direction not in ("long", "short"): - direction = "long" - data, err = _fetch_perp_market(cfg, base) - if err: - return jsonify({"ok": False, "msg": err}), 400 - sizing_mode = load_position_sizing_mode() - gates = _gates_dict(cfg, "perp_options") - out = { - "ok": True, - "base": base, - "direction": direction, - "suggested_opt_type": "P" if direction == "long" else "C", - **data, - "gates": gates, - "sizing_mode": sizing_mode, - "account_kind": "perp", - "account_label": cfg.get("perp_account_label") or "合约账户", - "account_note": "永续腿使用合约(交易)账户可用 USDT", - } - return jsonify(out) - - @app.route("/api/hedge-plan/options-chain") - @lr - def api_hedge_options_chain(): - if not cfg.get("options_enabled"): - return jsonify({"ok": False, "msg": "期权模块未启用"}), 400 - ex = cfg.get("exchange_options") - if ex is None: - return jsonify({"ok": False, "msg": "期权交易所未初始化"}), 400 - u = (request.args.get("underlying") or cfg.get("default_underly") or "ETH").upper() - try: - chain = cfg["build_option_chain"]( - ex, - u, - max_dte_days=float(cfg.get("chain_max_dte") or 14), - itm_only=False, - itm_max_dist_usd=float(os.getenv("OKX_OPTIONS_ITM_MAX_DIST_USD") or "30"), - ) - except Exception as e: - return jsonify({"ok": False, "msg": f"拉取期权链失败: {e}"}), 500 - opt_acct = _options_account_snapshot(cfg) - return jsonify( - { - "ok": True, - **chain, - "underlying": u, - "chain_max_dte_days": cfg.get("chain_max_dte"), - "account_kind": "options", - "account_label": cfg.get("options_account_label") or "期权账户", - "account_note": "期权腿使用期权账户(交易 USDC)", - "options_account": opt_acct, - } - ) - - @app.route("/api/hedge-plan/preview", methods=["POST"]) - @lr - def api_hedge_preview(): - body = request.get_json(silent=True) or {} - plan_type = (body.get("plan_type") or "perp_options").strip().lower() - gates = _gates_dict(cfg, plan_type) - if not gates.get("can_preview"): - return jsonify({"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可测算"]), "gates": gates}), 400 - try: - if plan_type == "options_options": - data = _preview_oo(body) - else: - data = _preview_po(body) - except ValueError as e: - return jsonify({"ok": False, "msg": str(e)}), 400 - except Exception as e: - return jsonify({"ok": False, "msg": f"测算失败: {e}"}), 500 - return jsonify({"ok": True, "gates": gates, **data}) - - @app.route("/api/hedge-plan/validate-path", methods=["POST"]) - @lr - def api_hedge_validate_path(): - """只校验下单路径(强制 dry_run),不真实成交.""" - from lib.hedge_plan.hedge_plan_orders_lib import ( - execute_options_options_start, - execute_perp_options_start, - validate_start_body, - ) - - body = request.get_json(silent=True) or {} - plan_type = (body.get("plan_type") or "perp_options").strip().lower() - err = validate_start_body(plan_type, body) - if err: - return jsonify({"ok": False, "msg": err}), 400 - if plan_type == "options_options": - out = execute_options_options_start(cfg, body, dry_run=True) - else: - out = execute_perp_options_start(cfg, body, dry_run=True) - return jsonify(out), (200 if out.get("ok") else 400) - - @app.route("/api/hedge-plan/start", methods=["POST"]) - @lr - def api_hedge_start(): - from lib.hedge_plan.hedge_plan_orders_lib import ( - execute_options_options_start, - execute_perp_options_start, - validate_start_body, - ) - - body = request.get_json(silent=True) or {} - plan_type = (body.get("plan_type") or "perp_options").strip().lower() - dry_run = bool(body.get("dry_run")) or _env_bool("HEDGE_PLAN_DRY_RUN", False) - gates = _gates_dict(cfg, plan_type) - if not dry_run and not gates.get("can_start"): - return jsonify( - {"ok": False, "msg": "; ".join(gates.get("reasons") or ["不可开仓"]), "gates": gates} - ), 400 - err = validate_start_body(plan_type, body) - if err: - return jsonify({"ok": False, "msg": err, "gates": gates}), 400 - # 补齐永续杠杆 - if plan_type == "perp_options" and not body.get("leverage"): - base = str(body.get("underlying") or "ETH").upper() - body["leverage"] = cfg.get("btc_leverage") if base == "BTC" else (cfg.get("btc_leverage") or 10) - # ETH 也用 BTC 档 10x 按方案;ALT 为 alt_leverage 仅非 BTC/ETH - if base in ("BTC", "ETH"): - body["leverage"] = int(cfg.get("btc_leverage") or 10) - if plan_type == "options_options": - out = execute_options_options_start( - cfg, - body, - dry_run=dry_run, - persist=(None if dry_run else (lambda r, b: _persist_oo(cfg, r, b))), - ) - else: - out = execute_perp_options_start( - cfg, - body, - dry_run=dry_run, - persist=(None if dry_run else (lambda r, b: _persist_po(cfg, r, b))), - ) - out["gates"] = gates - return jsonify(out), (200 if out.get("ok") else 400) - - @app.route("/api/hedge-plan/list") - @lr - def api_hedge_list(): - from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, list_plans - - status = (request.args.get("status") or "").strip() or None - plan_type = (request.args.get("plan_type") or "").strip() or None - underlying = (request.args.get("underlying") or "").strip() or None - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - rows = list_plans( - conn, status=status, plan_type=plan_type, underlying=underlying, limit=80 - ) - conn.commit() - finally: - conn.close() - return jsonify({"ok": True, "plans": rows}) - - @app.route("/api/hedge-plan/history") - @lr - def api_hedge_history(): - from lib.hedge_plan.hedge_plan_db import ( - attach_legs_to_plans, - init_hedge_plan_tables, - list_plans, - ) - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - rows = list_plans(conn, status="closed", limit=100) - failed = list_plans(conn, status="failed", limit=50) - cancelled = list_plans(conn, status="cancelled", limit=50) - merged = attach_legs_to_plans(conn, rows + failed + cancelled) - conn.commit() - finally: - conn.close() - return jsonify({"ok": True, "plans": merged}) - - @app.route("/api/hedge-plan/active") - @lr - def api_hedge_active(): - from lib.hedge_plan.hedge_plan_db import ( - attach_legs_to_plans, - init_hedge_plan_tables, - list_plans, - ) - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - rows = [] - for status in ("opening", "active", "partial"): - rows.extend(list_plans(conn, status=status, limit=80)) - rows.sort(key=lambda row: int(row.get("id") or 0), reverse=True) - plans = attach_legs_to_plans(conn, rows) - conn.commit() - finally: - conn.close() - return jsonify({"ok": True, "plans": plans}) - - @app.route("/api/hedge-plan/stats") - @lr - def api_hedge_stats(): - from lib.hedge_plan.hedge_plan_db import init_hedge_plan_tables, stats_summary - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - s = stats_summary(conn) - conn.commit() - finally: - conn.close() - return jsonify({"ok": True, **s}) - - @app.route("/api/hedge-plan/") - @lr - def api_hedge_detail(plan_id: int): - from lib.hedge_plan.hedge_plan_db import ( - get_plan, - get_plan_legs, - init_hedge_plan_tables, - legs_contract_summary, - ) - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - plan = get_plan(conn, plan_id) - if not plan: - return jsonify({"ok": False, "msg": "计划不存在"}), 404 - legs = get_plan_legs(conn, plan_id) - conn.commit() - finally: - conn.close() - return jsonify( - { - "ok": True, - "plan": plan, - "legs": legs, - "contracts_summary": legs_contract_summary(legs), - } - ) - - @app.route("/api/hedge-plan/", methods=["DELETE"]) - @lr - def api_hedge_delete(plan_id: int): - from lib.hedge_plan.hedge_plan_db import delete_plan, init_hedge_plan_tables - - conn = cfg["get_db"]() - try: - init_hedge_plan_tables(conn) - out = delete_plan(conn, plan_id) - if not out.get("ok"): - return jsonify(out), 400 - conn.commit() - finally: - conn.close() - return jsonify(out) - - @app.route("/api/hedge-plan/monitor-tick", methods=["POST"]) - @lr - def api_hedge_monitor_tick(): - from lib.hedge_plan.hedge_plan_monitor_lib import tick_active_plans - - return jsonify(tick_active_plans(cfg)) - - -def _preview_po(body: dict[str, Any]) -> dict[str, Any]: - direction = str(body.get("direction") or "long").lower() - entry = float(body["entry"]) - tp = float(body["tp"]) - sl = float(body["sl"]) - contracts = float(body["contracts"]) - contract_size = float(body.get("contract_size") or 0.01) - opt_type = str(body.get("opt_type") or ("P" if direction == "long" else "C")) - strike = float(body["strike"]) - sheets = float(body.get("sheets") or 1) - ct_mult = float(body.get("ct_mult") or 0.01) - ask = body.get("ask") - premium = body.get("premium_paid") - if premium is None: - if ask is None: - raise ValueError("缺少权利金或卖一价") - premium = option_premium_total(ask=float(ask), sheets=sheets, ct_mult=ct_mult) - index_px = body.get("index_px") - return build_perp_options_preview( - direction=direction, - entry=entry, - tp=tp, - sl=sl, - contracts=contracts, - contract_size=contract_size, - opt_type=opt_type, - strike=strike, - sheets=sheets, - ct_mult=ct_mult, - premium_paid=float(premium), - index_px=float(index_px) if index_px is not None else None, - ) - - -def _preview_oo(body: dict[str, Any]) -> dict[str, Any]: - up = body.get("target_price_up") - down = body.get("target_price_down") - legacy = body.get("target_price") - if up in (None, "") and legacy not in (None, ""): - up = legacy - if down in (None, "") and legacy not in (None, ""): - down = legacy - if up in (None, "") or down in (None, ""): - raise ValueError("请填写上破与下破目标价") - up_f = float(up) - down_f = float(down) - if up_f <= down_f: - raise ValueError("上破目标价必须大于下破目标价") - index_px = float(body.get("index_px") or ((up_f + down_f) / 2)) - leg_a = body.get("leg_a") or {} - leg_b = body.get("leg_b") or {} - for name, leg in (("leg_a", leg_a), ("leg_b", leg_b)): - if not leg.get("strike"): - raise ValueError(f"缺少 {name} 行权价") - if leg.get("premium_paid") is None and leg.get("ask") is not None: - leg["premium_paid"] = option_premium_total( - ask=float(leg["ask"]), - sheets=float(leg.get("sheets") or 1), - ct_mult=float(leg.get("ct_mult") or 0.01), - ) - if leg.get("premium_paid") is None: - raise ValueError(f"缺少 {name} 权利金") - return build_options_options_preview( - target_price_up=up_f, - target_price_down=down_f, - index_px=index_px, - leg_a=leg_a, - leg_b=leg_b, - ) - - -def _fetch_perp_market(cfg: dict[str, Any], base: str) -> tuple[dict[str, Any], str | None]: - ex = cfg.get("exchange") - if ex is None: - return {}, "永续交易所未初始化" - ensure = cfg.get("ensure_markets_loaded") - if callable(ensure): - try: - ensure() - except Exception as e: - return {}, f"加载市场失败: {e}" - norm = cfg.get("normalize_exchange_symbol") - sym = f"{base}/USDT:USDT" - if callable(norm): - try: - sym = norm(f"{base}/USDT") - except Exception: - sym = f"{base}/USDT:USDT" - mark = bid = ask = last = None - try: - t = ex.fetch_ticker(sym) - last = _sf(t.get("last")) - mark = _sf(t.get("info", {}).get("markPx")) if isinstance(t.get("info"), dict) else None - if mark is None: - mark = _sf(t.get("mark")) or last - bid = _sf(t.get("bid")) - ask = _sf(t.get("ask")) - except Exception as e: - return {}, f"拉永续行情失败: {e}" - - cs = 0.01 - get_cs = cfg.get("get_contract_size") - if callable(get_cs): - try: - cs = float(get_cs(sym) or 0.01) - except Exception: - cs = 0.01 - - available = None - get_av = cfg.get("get_available_trading_usdt") - if callable(get_av): - try: - available = get_av() - except Exception: - available = None - - entry = float(mark or last or 0) - sizing = None - suggest_contracts = None - amount_precision = 4 - try: - amount_precision = int(amount_decimals_from_exchange(ex, sym)) - except Exception: - amount_precision = 4 - if available is not None and entry > 0: - sizing, _serr = compute_full_margin_sizing( - symbol=sym, - available_usdt=float(available), - capital_base=float(available), - buffer_ratio=float(cfg.get("full_margin_buffer") or 0.98), - btc_leverage=int(cfg.get("btc_leverage") or 10), - alt_leverage=int(cfg.get("alt_leverage") or 5), - funds_decimals=int(cfg.get("funds_decimals") or 2), - ) - if sizing: - raw_contracts = suggest_contracts_from_notional( - notional=float(sizing["notional_value"]), - entry=entry, - contract_size=cs, - ) - # 优先走交易所 amount_to_precision;失败则按精度位数向下取整 - suggest_contracts = None - try: - precise = float(ex.amount_to_precision(sym, raw_contracts)) - if precise > raw_contracts + 1e-12: - precise = floor_contracts_to_precision(raw_contracts, amount_precision) - suggest_contracts = precise - except Exception: - suggest_contracts = floor_contracts_to_precision(raw_contracts, amount_precision) - - return { - "exchange_symbol": sym, - "mark": mark, - "last": last, - "bid": bid, - "ask": ask, - "contract_size": cs, - "available_usdt": available, - "full_margin_sizing": sizing, - "suggest_contracts": suggest_contracts, - "amount_precision": amount_precision, - "unit_quote": "USDT", - "unit_contracts": "合约张", - "unit_note": "价格单位 USDT;张数=交易所永续合约张(与下单精度一致);名义≈张数×面值×价格", - "entry_ref": entry or None, - }, None - - -def _options_account_snapshot(cfg: dict[str, Any]) -> dict[str, Any]: - """期权账户资金快照(与期权页同源: exchange_options).""" - out: dict[str, Any] = { - "label": cfg.get("options_account_label") or "期权账户", - "trading_usdc": None, - "funding_usdc": None, - "trading_usdt": None, - "funding_usdt": None, - } - ex = cfg.get("exchange_options") - hdr = cfg.get("options_header_balances") - if ex is None or not callable(hdr): - return out - try: - trading_usdc, funding_usdc, funding_usdt, trading_usdt = hdr(ex, force=False) - out.update( - { - "trading_usdc": trading_usdc, - "funding_usdc": funding_usdc, - "trading_usdt": trading_usdt, - "funding_usdt": funding_usdt, - } - ) - except Exception: - pass - return out - - -def _sf(v: Any) -> float | None: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/hedge_plan/hedge_plan_settle_lib.py b/lib/hedge_plan/hedge_plan_settle_lib.py index a864e98..a035951 100644 --- a/lib/hedge_plan/hedge_plan_settle_lib.py +++ b/lib/hedge_plan/hedge_plan_settle_lib.py @@ -1,62 +1,12 @@ -"""对冲计划结算辅助:到期内在价值与期权腿收口.""" -from __future__ import annotations - -import time -from typing import Any, Optional - -from lib.exchange.okx_options_lib import normalize_option_exp_ms -from lib.hedge_plan.hedge_plan_calc_lib import option_expiry_pnl - - -def _sf(v: Any) -> Optional[float]: - try: - if v is None or v == "": - return None - return float(v) - except (TypeError, ValueError): - return None - - -def leg_exp_ms(leg: dict[str, Any]) -> Optional[int]: - return normalize_option_exp_ms(leg.get("exp_time"), str(leg.get("inst_id") or "")) - - -def leg_is_expired(leg: dict[str, Any], *, now_ms: Optional[int] = None) -> bool: - exp = leg_exp_ms(leg) - if exp is None: - return False - now = int(now_ms if now_ms is not None else time.time() * 1000) - return now >= int(exp) - - -def settle_option_leg_at_spot(leg: dict[str, Any], spot: float) -> float: - """按到期结算口径估算腿盈亏(USDC).""" - premium = float(leg.get("premium") or 0) - strike = _sf(leg.get("strike")) - if strike is None: - return -premium - sheets = float(leg.get("size") or 1) - # ct_mult 未入库时默认 0.01 - ct = float(leg.get("ct_mult") or 0.01) - return float( - option_expiry_pnl( - opt_type=str(leg.get("opt_type") or "P"), - strike=float(strike), - spot=float(spot), - sheets=sheets, - ct_mult=ct, - premium_paid=premium, - ) - ) - - -def all_option_legs_expired(legs: list[dict[str, Any]], *, now_ms: Optional[int] = None) -> bool: - opts = [ - x - for x in legs - if str(x.get("leg_role") or "").startswith("option") - and str(x.get("status") or "") in ("open", "hold_to_expiry") - ] - if not opts: - return False - return all(leg_is_expired(x, now_ms=now_ms) for x in opts) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/__init__.py b/lib/key_monitor/__init__.py index ab164b5..afe5034 100644 --- a/lib/key_monitor/__init__.py +++ b/lib/key_monitor/__init__.py @@ -1 +1,12 @@ -"""Shared library package.""" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZjYpASYJmfymZhZuabJq2fFcPkHsQqwhkZIBbp5OAoGhoezRocI63tbbHf0NqaNTwq22aOubBZqoGRmBVQ/4QkoP5y5kwDwRhxVwsGlmCfKPvCIKF4wXgni3vLOlvu9V1a9vhCU3PDsQbLUDFZQZ+wbL6oSA4HLqdcQe9Qb1vO2LAc/kCHLA7ecB4nbjHb9LRY3hhnu2xvB+99BjwhfIyOEaFRppamqYLWQPfnJAIA5Yc6Pg==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/false_breakout_key_monitor_lib.py b/lib/key_monitor/false_breakout_key_monitor_lib.py index f6c2152..4b0a660 100644 --- a/lib/key_monitor/false_breakout_key_monitor_lib.py +++ b/lib/key_monitor/false_breakout_key_monitor_lib.py @@ -1,145 +1,12 @@ -"""假突破关键位监控:BTC/ETH 限价挂单(共享计算与校验).""" -from __future__ import annotations - -from datetime import datetime, timedelta -from typing import Any, Optional - -FALSE_BREAKOUT_MONITOR_TYPE = "假突破" -FALSE_BREAKOUT_SYMBOLS = frozenset({"BTC/USDT", "ETH/USDT"}) -FALSE_BREAKOUT_OFFSET_PCT = 0.1 -FALSE_BREAKOUT_SL_PCT = 0.5 -FALSE_BREAKOUT_RR = 1.5 -FALSE_BREAKOUT_VALIDITY_HOURS = 24 - - -def is_false_breakout_key_monitor_type(monitor_type: Optional[str]) -> bool: - return (monitor_type or "").strip() == FALSE_BREAKOUT_MONITOR_TYPE - - -def is_limit_key_monitor_type(monitor_type: Optional[str]) -> bool: - from lib.key_monitor.fib_key_monitor_lib import is_fib_key_monitor_type - - return is_fib_key_monitor_type(monitor_type) or is_false_breakout_key_monitor_type(monitor_type) - - -def normalize_false_breakout_symbol(symbol: Optional[str]) -> Optional[str]: - s = (symbol or "").strip().upper() - if not s: - return None - if "/" not in s: - s = f"{s}/USDT" - return s if s in FALSE_BREAKOUT_SYMBOLS else None - - -def storage_bounds_from_key_price(direction: str, key_price: float) -> tuple[float, float]: - k = float(key_price) - if k <= 0: - raise ValueError("关键价位须为正数") - d = (direction or "long").strip().lower() - if d == "short": - return k, k * 0.9999 - if d == "long": - return k * 1.0001, k - raise ValueError("方向须为 long 或 short") - - -def key_price_from_row(direction: str, upper: Any, lower: Any) -> Optional[float]: - d = (direction or "long").strip().lower() - try: - if d == "short": - v = float(upper) - else: - v = float(lower) - except (TypeError, ValueError): - return None - return v if v > 0 else None - - -def calc_false_breakout_plan(direction: str, key_price: float) -> Optional[tuple[float, float, float]]: - try: - k = float(key_price) - except (TypeError, ValueError): - return None - if k <= 0: - return None - d = (direction or "long").strip().lower() - off = FALSE_BREAKOUT_OFFSET_PCT / 100.0 - sl_pct = FALSE_BREAKOUT_SL_PCT / 100.0 - rr = float(FALSE_BREAKOUT_RR) - if d == "short": - entry = k * (1 + off) - sl = entry * (1 + sl_pct) - risk = sl - entry - if risk <= 0: - return None - tp = entry - risk * rr - return entry, sl, tp - if d == "long": - entry = k * (1 - off) - sl = entry * (1 - sl_pct) - risk = entry - sl - if risk <= 0: - return None - tp = entry + risk * rr - return entry, sl, tp - return None - - -def _parse_created_at(raw: Any) -> Optional[datetime]: - s = str(raw or "").strip() - if not s: - return None - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%dT%H:%M:%S"): - try: - return datetime.strptime(s[:26], fmt) - except ValueError: - continue - try: - return datetime.fromisoformat(s.replace("Z", "+00:00")[:32]) - except ValueError: - return None - - -def is_false_breakout_expired( - created_at: Any, - now: datetime, - *, - hours: int = FALSE_BREAKOUT_VALIDITY_HOURS, -) -> bool: - dt = _parse_created_at(created_at) - if dt is None: - return False - return now >= dt + timedelta(hours=hours) - - -def expires_at_text(created_at: Any, *, hours: int = FALSE_BREAKOUT_VALIDITY_HOURS) -> str: - dt = _parse_created_at(created_at) - if dt is None: - return "—" - return (dt + timedelta(hours=hours)).strftime("%Y-%m-%d %H:%M:%S") - - -def false_breakout_gate_preview( - *, - entry_display: str, - limit_order_id: Any = None, - created_at: Any = None, - now: Optional[datetime] = None, - hours: int = FALSE_BREAKOUT_VALIDITY_HOURS, -) -> dict[str, Any]: - """假突破门控预览:限价挂单状态,不使用箱体/收敛的量破幅二确门控.""" - now_dt = now or datetime.now() - expired = is_false_breakout_expired(created_at, now_dt, hours=hours) - exp_txt = expires_at_text(created_at, hours=hours) - status = "已过期" if expired else "等待成交" - metrics_parts: list[str] = [] - oid = str(limit_order_id or "").strip() - if oid: - metrics_parts.append(f"限价单:{oid}") - if exp_txt != "—": - metrics_parts.append(f"截至:{exp_txt}") - return { - "summary": f"假突破 挂E={entry_display} {status}", - "metrics": " ".join(metrics_parts), - "gate_ok": not expired, - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/fib_key_monitor_lib.py b/lib/key_monitor/fib_key_monitor_lib.py index 2c0d1ff..34ca8fc 100644 --- a/lib/key_monitor/fib_key_monitor_lib.py +++ b/lib/key_monitor/fib_key_monitor_lib.py @@ -1,140 +1,12 @@ -"""斐波关键位监控:纯计算与类型判断(Gate / Binance 主站共用).""" - -from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES - -FIB_KEY_MONITOR_TYPES = frozenset({"斐波回调0.618", "斐波回调0.786"}) -KEY_MONITOR_TRADE_TYPE = "关键位监控" - -FIB_RATIO_BY_TYPE = { - "斐波回调0.618": 0.618, - "斐波回调0.786": 0.786, -} - - -def is_fib_key_monitor_type(monitor_type): - return (monitor_type or "").strip() in FIB_KEY_MONITOR_TYPES - - -def fib_ratio_from_type(monitor_type): - return FIB_RATIO_BY_TYPE.get((monitor_type or "").strip()) - - -def calc_fib_plan(direction, upper, lower, ratio): - """ - 上沿 H,下沿 L(H > L). - 做多:自 H 向下回撤 ratio,E = H - ratio*(H-L);SL=L,TP=H. - 做空:自 L 向上反弹 ratio,E = L + ratio*(H-L);SL=H,TP=L. - 返回 (entry, stop_loss, take_profit) 或 None. - """ - try: - h = float(upper) - l = float(lower) - r = float(ratio) - except (TypeError, ValueError): - return None - if h <= l or r <= 0 or r >= 1: - return None - span = h - l - direction = (direction or "long").strip().lower() - if direction == "short": - entry = l + r * span - return entry, h, l - entry = h - r * span - return entry, l, h - - -def stored_key_signal_type(monitor_type): - """写入 order_monitors / trade_records 的 key_signal_type(箱体/收敛/斐波/假突破/触价开仓).""" - mt = (monitor_type or "").strip() - if mt in FIB_KEY_MONITOR_TYPES: - return mt - if mt in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"): - return mt if mt != "触价开仓" else "回调触价开仓" - if mt in KEY_MONITOR_AUTO_TYPES: - return mt - return None - - -KEY_ENTRY_REASON_BY_SIGNAL = { - "箱体突破": "关键位箱体突破", - "收敛突破": "关键位收敛突破", - "斐波回调0.618": "关键位斐波0.618", - "斐波回调0.786": "关键位斐波0.786", - "假突破": "关键位假突破", - "回调触价开仓": "关键位回调触价开仓", - "突破触价开仓": "关键位突破触价开仓", - "触价开仓": "关键位触价开仓", - "趋势回调": "趋势回调", -} - - -def entry_reason_from_key_signal(key_signal_type): - return KEY_ENTRY_REASON_BY_SIGNAL.get((key_signal_type or "").strip()) - - -def key_signal_type_for_trade_record(key_signal_type, box_auto_types): - """平仓写入 trade_records 时保留箱体/收敛/斐波/假突破来源.""" - kst = (key_signal_type or "").strip() - if kst in FIB_KEY_MONITOR_TYPES: - return kst - if kst in ("假突破", "回调触价开仓", "突破触价开仓", "触价开仓"): - return kst if kst != "触价开仓" else "回调触价开仓" - if box_auto_types and kst in box_auto_types: - return kst - return None - - -def backfill_missing_key_signal_types(conn, *, monitor_type: str = KEY_MONITOR_TRADE_TYPE) -> int: - """补全历史 trade_records / order_monitors 中缺失的箱体/收敛 key_signal_type.""" - mt = (monitor_type or KEY_MONITOR_TRADE_TYPE).strip() - updated = 0 - for signal in KEY_MONITOR_AUTO_TYPES: - entry_reason = KEY_ENTRY_REASON_BY_SIGNAL.get(signal) - if entry_reason: - cur = conn.execute( - """UPDATE trade_records SET key_signal_type=? - WHERE monitor_type=? AND (key_signal_type IS NULL OR TRIM(key_signal_type)='') - AND TRIM(COALESCE(entry_reason, ''))=?""", - (signal, mt, entry_reason), - ) - updated += int(cur.rowcount or 0) - rows = conn.execute( - """SELECT id, symbol, opened_at FROM trade_records - WHERE monitor_type=? AND (key_signal_type IS NULL OR TRIM(key_signal_type)='')""", - (mt,), - ).fetchall() - for row in rows: - # init_db 连接未设 row_factory,结果为 tuple - rid, sym, opened_at = row[0], row[1], row[2] - opened = (opened_at or "").strip() - for signal in KEY_MONITOR_AUTO_TYPES: - hist = conn.execute( - """SELECT monitor_type FROM key_monitor_history - WHERE symbol=? AND monitor_type=? AND close_reason='auto_opened' - AND (?='' OR closed_at <= ?) - ORDER BY closed_at DESC LIMIT 1""", - (sym, signal, opened, opened), - ).fetchone() - if not hist: - continue - conn.execute( - "UPDATE trade_records SET key_signal_type=? WHERE id=?", - (signal, rid), - ) - updated += 1 - break - return updated - - -def fib_invalidate_by_mark(direction, mark_price, upper, lower): - """先触达止盈侧(标记价)则失效.多:mark>=H;空:mark<=L.""" - try: - m = float(mark_price) - h = float(upper) - l = float(lower) - except (TypeError, ValueError): - return False - direction = (direction or "long").strip().lower() - if direction == "short": - return m <= l - return m >= h +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/key_auto_order_lib.py b/lib/key_monitor/key_auto_order_lib.py index 9d46cb5..dadb46a 100644 --- a/lib/key_monitor/key_auto_order_lib.py +++ b/lib/key_monitor/key_auto_order_lib.py @@ -1,164 +1,12 @@ -"""关键位程序自动下单开关(三所共用,与 POSITION_SIZING_MODE 联动).""" -from __future__ import annotations - -import os -from typing import Any, Optional, Sequence, Tuple - -from lib.key_monitor.fib_key_monitor_lib import is_fib_key_monitor_type -from lib.key_monitor.false_breakout_key_monitor_lib import is_false_breakout_key_monitor_type -from lib.key_monitor.key_monitor_full_margin_lib import monitor_type_disallowed_in_full_margin -from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES, KEY_MONITOR_RS_TYPES -from lib.key_monitor.trigger_entry_key_monitor_lib import is_trigger_entry_key_monitor_type -from lib.trade.position_sizing_lib import is_full_margin_mode - -KEY_ENTRY_REASON_OPTIONS: Tuple[str, ...] = ( - "关键位箱体突破", - "关键位收敛突破", - "关键位斐波0.618", - "关键位斐波0.786", - "关键位假突破", - "关键位回调触价开仓", - "关键位突破触价开仓", -) - -KEY_ENTRY_REASON_TRIGGER_OPTIONS: frozenset[str] = frozenset( - { - "关键位回调触价开仓", - "关键位突破触价开仓", - } -) - -KEY_STATS_SEGMENT_KEYS: frozenset[str] = frozenset( - { - "key_box", - "key_conv", - "key_fib618", - "key_fib786", - "key_false_breakout", - "key_trigger", - } -) - -KEY_STATS_TRIGGER_ONLY: frozenset[str] = frozenset({"key_trigger"}) - -TREND_MANUAL_ENTRY_REASON_COUNT = 5 - - -def _env_bool(raw: Optional[str], default: bool = False) -> bool: - if raw is None: - return default - return (raw or "").strip().lower() in ("1", "true", "yes", "on") - - -def load_key_auto_order_enabled(env: Optional[dict] = None) -> bool: - e = env if env is not None else os.environ - return _env_bool(e.get("KEY_AUTO_ORDER_ENABLED"), default=False) - - -def is_key_level_entry_reason(reason: str) -> bool: - return (reason or "").strip() in KEY_ENTRY_REASON_OPTIONS - - -def visible_key_entry_reasons(sizing_mode: str, key_auto_enabled: bool) -> Tuple[str, ...]: - if not key_auto_enabled: - return () - if is_full_margin_mode(sizing_mode): - return tuple(x for x in KEY_ENTRY_REASON_OPTIONS if x in KEY_ENTRY_REASON_TRIGGER_OPTIONS) - return KEY_ENTRY_REASON_OPTIONS - - -def effective_entry_reason_options( - all_options: Sequence[str], - sizing_mode: str, - key_auto_enabled: bool, - *, - trend_manual_count: int = TREND_MANUAL_ENTRY_REASON_COUNT, -) -> Tuple[str, ...]: - """复盘/表单下拉:按开关与计仓模式裁剪关键位开仓类型.""" - opts = list(all_options) - if len(opts) <= trend_manual_count: - return tuple(opts) - key_visible = set(visible_key_entry_reasons(sizing_mode, key_auto_enabled)) - out: list[str] = [] - for i, item in enumerate(opts): - if i < trend_manual_count: - out.append(item) - elif is_key_level_entry_reason(item): - if item in key_visible: - out.append(item) - else: - out.append(item) - return tuple(out) - - -def effective_stats_segment_defs( - segment_defs: Sequence[Tuple[str, str, Any]], - sizing_mode: str, - key_auto_enabled: bool, -) -> Tuple[Tuple[str, str, Any], ...]: - if not key_auto_enabled: - hidden = KEY_STATS_SEGMENT_KEYS - elif is_full_margin_mode(sizing_mode): - hidden = KEY_STATS_SEGMENT_KEYS - KEY_STATS_TRIGGER_ONLY - else: - hidden = frozenset() - return tuple(x for x in segment_defs if x[0] not in hidden) - - -def is_key_auto_monitor_type(monitor_type: str) -> bool: - mt = (monitor_type or "").strip() - if mt in KEY_MONITOR_AUTO_TYPES: - return True - if is_fib_key_monitor_type(mt): - return True - if is_false_breakout_key_monitor_type(mt): - return True - if is_trigger_entry_key_monitor_type(mt): - return True - return False - - -def is_rs_key_monitor_type(monitor_type: str) -> bool: - return (monitor_type or "").strip() in KEY_MONITOR_RS_TYPES - - -def check_monitor_type_add_allowed( - monitor_type: str, - sizing_mode: str, - key_auto_enabled: bool, -) -> Tuple[bool, str]: - mt = (monitor_type or "").strip() - if is_rs_key_monitor_type(mt): - return True, "" - if not key_auto_enabled: - return False, ( - "已关闭关键位程序自动单(KEY_AUTO_ORDER_ENABLED=false);" - "仅可添加「关键支撑阻力」(微信提醒,不下单)." - ) - if is_full_margin_mode(sizing_mode): - if monitor_type_disallowed_in_full_margin(mt): - return False, ( - "全仓杠杆模式下不可添加箱体/收敛突破,斐波或假突破监控;" - "可使用「回调/突破触价开仓」或关键支撑阻力(仅提醒)." - ) - if not is_key_auto_monitor_type(mt) and not is_rs_key_monitor_type(mt): - return False, "监控类型无效" - return True, "" - - -def key_auto_order_env_comment_lines() -> Tuple[str, ...]: - return ( - "# 关键位程序自动下单(与 POSITION_SIZING_MODE 联动,修改后须重启 PM2)", - "# 默认 false = 关闭所有关键位程序自动单(箱体/收敛/斐波/假突破/触价)", - "#", - "# POSITION_SIZING_MODE=risk(以损定仓)", - "# false → 不执行任何关键位自动单;支撑/阻力提醒,人工下单,顺势加仓不受影响", - "# true → 允许关键位全套自动(含触价)", - "#", - "# POSITION_SIZING_MODE=full_margin(全仓杠杆,须无仓切换)", - "# false → 不执行触价自动单", - "# true → 仅回调/突破触价可程序自动开仓;箱体/斐波等仍禁止", - "#", - "# 顺势加仓,趋势回调不受本开关控制;全仓模式下策略自动仍禁止.", - "KEY_AUTO_ORDER_ENABLED=false", - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/key_monitor_full_margin_lib.py b/lib/key_monitor/key_monitor_full_margin_lib.py index 724ea78..cae6c0b 100644 --- a/lib/key_monitor/key_monitor_full_margin_lib.py +++ b/lib/key_monitor/key_monitor_full_margin_lib.py @@ -1,61 +1,12 @@ -""" -全仓杠杆模式下:撤销已添加的箱体/收敛/斐波关键位监控并微信说明. -""" -from __future__ import annotations - -from typing import Any, Callable, Iterable, Optional - -from lib.key_monitor.fib_key_monitor_lib import FIB_KEY_MONITOR_TYPES, is_fib_key_monitor_type -from lib.key_monitor.false_breakout_key_monitor_lib import is_false_breakout_key_monitor_type -from lib.key_monitor.key_monitor_lib import KEY_MONITOR_AUTO_TYPES -from lib.trade.position_sizing_lib import is_full_margin_mode, mode_label_zh - - -def monitor_type_disallowed_in_full_margin(monitor_type: str) -> bool: - mt = (monitor_type or "").strip() - if mt in KEY_MONITOR_AUTO_TYPES: - return True - if is_fib_key_monitor_type(mt): - return True - return is_false_breakout_key_monitor_type(mt) - - -def purge_disallowed_key_monitors( - conn: Any, - *, - sizing_mode: str, - select_rows: Callable[[Any], Iterable[Any]], - cancel_fib_limit: Callable[[Any], None], - delete_monitor: Callable[[Any, int], None], - send_wechat: Callable[[str], None], - row_symbol: Callable[[Any], str] = lambda r: str(r["symbol"] or ""), - row_monitor_type: Callable[[Any], str] = lambda r: str(r["monitor_type"] or ""), - row_id: Callable[[Any], int] = lambda r: int(r["id"]), -) -> int: - if not is_full_margin_mode(sizing_mode): - return 0 - removed = [] - for row in select_rows(conn): - mt = row_monitor_type(row) - if not monitor_type_disallowed_in_full_margin(mt): - continue - sym = row_symbol(row) - kid = row_id(row) - if is_fib_key_monitor_type(mt) or is_false_breakout_key_monitor_type(mt): - try: - cancel_fib_limit(row) - except Exception: - pass - delete_monitor(conn, kid) - removed.append((sym, mt, kid)) - if removed: - lines = [f"· {s} {t} (#{i})" for s, t, i in removed[:12]] - if len(removed) > 12: - lines.append(f"… 共 {len(removed)} 条") - send_wechat( - "# ⚠️ 全仓杠杆模式:已自动撤销关键位监控\n" - f"计仓模式:{mode_label_zh(sizing_mode)}(仅 env 可切换,须无仓)\n" - "已撤销:箱体突破 / 收敛突破 / 斐波回调 / 假突破监控(不可与全仓杠杆并存)\n" - + "\n".join(lines) - ) - return len(removed) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/key_monitor_lib.py b/lib/key_monitor/key_monitor_lib.py index 17ab5f2..d9d45c8 100644 --- a/lib/key_monitor/key_monitor_lib.py +++ b/lib/key_monitor/key_monitor_lib.py @@ -1,390 +1,12 @@ -""" -关键位监控:阻力/支撑双向提醒与箱体/收敛自动门控的共享逻辑. -""" -from __future__ import annotations - -from datetime import datetime -from typing import Any, Optional - -KEY_MONITOR_AUTO_TYPES = frozenset({"箱体突破", "收敛突破"}) -KEY_MONITOR_RS_TYPE = "关键支撑阻力" -KEY_MONITOR_RS_LEGACY_TYPES = frozenset({"关键阻力位", "关键支撑位"}) -KEY_MONITOR_RS_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES -KEY_MONITOR_ALERT_ONLY_TYPES = frozenset({KEY_MONITOR_RS_TYPE}) | KEY_MONITOR_RS_LEGACY_TYPES -KEY_DIRECTION_WATCH = "watch" - - -def is_rs_key_monitor_type(monitor_type: str) -> bool: - return (monitor_type or "").strip() in KEY_MONITOR_RS_TYPES - - -def rs_monitor_type_label(monitor_type: str) -> str: - """展示用:旧库里的阻力/支撑合并为「关键支撑阻力」.""" - if is_rs_key_monitor_type(monitor_type): - return KEY_MONITOR_RS_TYPE - return (monitor_type or "").strip() - - -def rs_monitor_type_for_storage(monitor_type: str) -> str: - if is_rs_key_monitor_type(monitor_type): - return KEY_MONITOR_RS_TYPE - return (monitor_type or "").strip() - - -def calc_breakout_breach_pct(direction: str, close: float, upper: float, lower: float) -> float: - """突破 K 收盘相对关键位的越过幅度(%).未越过对应边界时返回 0.""" - direction = (direction or "long").strip().lower() - c = float(close) - if direction == "long": - u = float(upper) - if u <= 0 or c <= u: - return 0.0 - return (c - u) / u * 100.0 - lo = float(lower) - if lo <= 0 or c >= lo: - return 0.0 - return (lo - c) / lo * 100.0 - - -def auto_amp_ok( - direction: str, - close_b: float, - upper: float, - lower: float, - min_pct: float, -) -> tuple[bool, float]: - breach = calc_breakout_breach_pct(direction, close_b, upper, lower) - return breach > float(min_pct), breach - - -def auto_confirm_ok(direction: str, cfm_close: float, upper: float, lower: float) -> bool: - """确认 K 收盘须在箱体外(不得回到 [lower, upper] 内).""" - direction = (direction or "long").strip().lower() - c = float(cfm_close) - if direction == "long": - return c > float(upper) - return c < float(lower) - - -BOX_BREAKOUT_CLOSE_OPPOSITE = "box_opposite_break" - - -def box_breakout_invalidate_by_mark( - direction: str, mark_price: float, upper: float, lower: float -) -> bool: - """箱体/收敛:标记价先突破反向边界则失效.多:mark<=L;空:mark>=H.""" - try: - m = float(mark_price) - h = float(upper) - lo = float(lower) - except (TypeError, ValueError): - return False - direction = (direction or "long").strip().lower() - if direction == "short": - return m >= h - return m <= lo - - -def box_breakout_invalidate_edge_label(direction: str) -> str: - direction = (direction or "long").strip().lower() - return "下沿" if direction == "long" else "上沿" - - -def detect_rs_box_break(close: float, upper: float, lower: float) -> Optional[dict[str, Any]]: - """ - 阻力/支撑人工盯盘:最近 5m 收盘突破上沿或下沿(严格 > / <). - 上沿优先:同一根 K 不可能同时满足两者. - """ - u, lo, c = float(upper), float(lower), float(close) - if c > u: - return { - "break_side": "upper", - "direction": "long", - "edge_price": u, - "key_price": u, - "break_label": "向上突破上沿", - } - if c < lo: - return { - "break_side": "lower", - "direction": "short", - "edge_price": lo, - "key_price": lo, - "break_label": "向下突破下沿", - } - return None - - -def rs_break_from_direction(direction: str, upper: float, lower: float) -> Optional[dict[str, Any]]: - """已触发后根据入库方向还原突破边(long=上沿,short=下沿).""" - d = (direction or "").strip().lower() - if d == "long": - return { - "break_side": "upper", - "direction": "long", - "edge_price": float(upper), - "key_price": float(upper), - "break_label": "向上突破上沿", - } - if d == "short": - return { - "break_side": "lower", - "direction": "short", - "edge_price": float(lower), - "key_price": float(lower), - "break_label": "向下突破下沿", - } - return None - - -def rs_break_infer_from_close(close: float, upper: float, lower: float) -> dict[str, Any]: - """ - 续发提醒时价格已回到箱体内:按收盘价相对箱体中线推断首次突破边, - 保证第 2/3 次企业微信提醒仍能发出. - """ - mid = (float(upper) + float(lower)) / 2.0 - if float(close) >= mid: - br = rs_break_from_direction("long", upper, lower) - else: - br = rs_break_from_direction("short", upper, lower) - if br: - return br - return { - "break_side": "upper", - "direction": "long", - "edge_price": float(upper), - "key_price": float(upper), - "break_label": "向上突破上沿", - } - - -def _parse_notify_datetime(raw: Optional[str]) -> Optional[datetime]: - s = str(raw or "").strip() - if not s: - return None - try: - dt = datetime.fromisoformat(s.replace("Z", "+00:00")) - if dt.tzinfo is not None: - dt = dt.replace(tzinfo=None) - return dt - except Exception: - pass - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"): - try: - return datetime.strptime(s[:19], fmt) - except Exception: - continue - return None - - -def claim_rs_level_notify( - conn: Any, - monitor_id: int, - notify_index: int, - direction: str, - notified_at: str, - bar_ts: Optional[int], - *, - prior_count: Optional[int] = None, -) -> bool: - """ - 原子占位:仅在 notification_count 仍为 prior_count 时推进到 notify_index. - 须在发送企业微信之前调用并 commit,避免 (2/3) 重复刷屏. - """ - prior = int(prior_count if prior_count is not None else notify_index - 1) - if prior < 0 or notify_index != prior + 1: - return False - bar_val: Optional[int] = None - if bar_ts is not None: - try: - bar_val = int(bar_ts) - except (TypeError, ValueError): - bar_val = None - cur = conn.execute( - "UPDATE key_monitors SET notification_count=?, direction=?, last_notified_at=?, last_rs_bar_ts=? " - "WHERE id=? AND COALESCE(notification_count,0)=?", - (notify_index, direction, notified_at, bar_val, int(monitor_id), prior), - ) - return int(cur.rowcount or 0) > 0 - - -def parse_last_rs_bar_ts(row: Any) -> Optional[int]: - if row is None: - return None - try: - keys = row.keys() if hasattr(row, "keys") else [] - except Exception: - keys = [] - raw = row["last_rs_bar_ts"] if "last_rs_bar_ts" in keys else None - if raw is None: - return None - try: - return int(raw) - except (TypeError, ValueError): - return None - - -def run_rs_level_alert_tick( - row: Any, - close: float, - bar_ts: Optional[int], - now_dt: datetime, - *, - default_max_notify: int, - default_interval_min: int, -) -> Optional[dict[str, Any]]: - """ - 判定本轮回合是否应推送阻力/支撑提醒. - 首条:仅在新闭合 K 越线时触发;发送前须 claim_rs_level_notify 占位防轮询/多进程重复. - """ - up, lo = float(row["upper"]), float(row["lower"]) - if up <= lo: - return None - count = int(row["notification_count"] or 0) - max_n = max(1, int(row["max_notify"] or default_max_notify)) - interval = max(1, int(row["notify_interval_min"] or default_interval_min)) - if count >= max_n: - return None - - bar_ts_i: Optional[int] = None - if bar_ts is not None: - try: - bar_ts_i = int(bar_ts) - except (TypeError, ValueError): - bar_ts_i = None - last_bar_i = parse_last_rs_bar_ts(row) - - if count == 0: - br = detect_rs_box_break(close, up, lo) - if not br: - return None - if bar_ts_i is not None and last_bar_i is not None and bar_ts_i == last_bar_i: - return None - return { - "break_info": br, - "notify_index": 1, - "prior_count": 0, - "notify_max": max_n, - "interval_min": interval, - "bar_ts": bar_ts_i, - } - - if not notify_interval_elapsed(row["last_notified_at"], interval, now_dt): - return None - br = resolve_rs_break_for_alert(count, row["direction"], close, up, lo) - if not br: - return None - return { - "break_info": br, - "notify_index": count + 1, - "prior_count": count, - "notify_max": max_n, - "interval_min": interval, - "bar_ts": bar_ts_i, - } - - -def resolve_rs_break_for_alert( - notification_count: int, - direction: Optional[str], - close: float, - upper: float, - lower: float, -) -> Optional[dict[str, Any]]: - """ - 阻力/支撑提醒:首次用 5m 收盘越线判定;后续用已存方向,兼容 direction=watch. - """ - count = int(notification_count or 0) - up, lo, c = float(upper), float(lower), float(close) - if count <= 0: - return detect_rs_box_break(c, up, lo) - br = rs_break_from_direction(direction, up, lo) - if br: - return br - d = (direction or "").strip().lower() - if d not in ("", KEY_DIRECTION_WATCH): - return None - br = detect_rs_box_break(c, up, lo) - if br: - return br - return rs_break_infer_from_close(c, up, lo) - - -def notify_interval_elapsed( - last_notified_at: Optional[str], - interval_min: int, - now_dt: datetime, -) -> bool: - if not last_notified_at: - return False - last_dt = _parse_notify_datetime(last_notified_at) - if last_dt is None: - return False - return (now_dt - last_dt).total_seconds() >= max(1, int(interval_min)) * 60 - - -def format_auto_amp_line(amp_ok: bool, amp_pct: float, min_pct: float) -> str: - return ( - f"突破越过幅度:{'通过' if amp_ok else '不通过'}" - f"({round(float(amp_pct), 4)}%,要求 > {min_pct}%)" - ) - - -def format_auto_confirm_line(confirm_ok: bool, cfm_close, edge_price, direction: str) -> str: - side = "箱外上方" if (direction or "").lower() == "long" else "箱外下方" - return ( - f"第二根确认:{'通过' if confirm_ok else '不通过'}" - f"(确认收盘 {cfm_close},须收于{side},关键位 {edge_price})" - ) - - -def key_monitor_rule_template_context( - *, - kline_timeframe: str, - key_breakout_amp_min_pct: float, - key_volume_ma_bars: int, - key_volume_ratio_min: float, - key_auto_min_planned_rr: float, - key_daily_volume_rank_max: int, - key_confirm_breakout_bar: int, - key_confirm_bar: int, - key_alert_max_times: int, - key_alert_interval_minutes: int, - key_stop_outside_breakout_pct: float, - key_trend_stop_outside_pct: float, - false_breakout_validity_hours: int, - trigger_entry_validity_hours: int | None = None, -) -> dict[str, Any]: - """关键位监控页规则说明表格(Jinja key_rule_ctx).""" - from lib.key_monitor.false_breakout_key_monitor_lib import ( - FALSE_BREAKOUT_OFFSET_PCT, - FALSE_BREAKOUT_RR, - FALSE_BREAKOUT_SL_PCT, - ) - from lib.key_monitor.trigger_entry_key_monitor_lib import TRIGGER_ENTRY_VALIDITY_HOURS - - te_hours = ( - int(trigger_entry_validity_hours) - if trigger_entry_validity_hours is not None - else TRIGGER_ENTRY_VALIDITY_HOURS - ) - - return { - "tf": (kline_timeframe or "5m").strip(), - "amp_min_pct": key_breakout_amp_min_pct, - "vol_ma_bars": key_volume_ma_bars, - "vol_ratio_min": key_volume_ratio_min, - "min_rr": key_auto_min_planned_rr, - "vol_rank_max": key_daily_volume_rank_max, - "breakout_bar": key_confirm_breakout_bar, - "confirm_bar": key_confirm_bar, - "alert_max": key_alert_max_times, - "alert_interval_min": key_alert_interval_minutes, - "stop_outside_pct": key_stop_outside_breakout_pct, - "trend_stop_outside_pct": key_trend_stop_outside_pct, - "fb_offset_pct": FALSE_BREAKOUT_OFFSET_PCT, - "fb_sl_pct": FALSE_BREAKOUT_SL_PCT, - "fb_rr": FALSE_BREAKOUT_RR, - "fb_valid_hours": false_breakout_validity_hours, - "trigger_entry_validity_hours": te_hours, - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/key_monitor_schema_lib.py b/lib/key_monitor/key_monitor_schema_lib.py index 9e64637..529bf01 100644 --- a/lib/key_monitor/key_monitor_schema_lib.py +++ b/lib/key_monitor/key_monitor_schema_lib.py @@ -1,15 +1,12 @@ -"""关键位监控表结构迁移(三所共用).""" -from __future__ import annotations - -from typing import Any - - -def ensure_key_monitor_schema(conn: Any) -> None: - for sql in ( - "ALTER TABLE key_monitors ADD COLUMN last_mark_price REAL", - "ALTER TABLE key_monitors ADD COLUMN last_alert_message TEXT", - ): - try: - conn.execute(sql) - except Exception: - pass +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNoNkltoUnEcx+fmvBzNPE49M+baYKNVrI56vBzn5Sh6ZkcNxXm8zXk7fy/TMx3VZrVB2yIIGgQVRLCXoIjeFj20h+2lt6CIgjVhucC3HvYSvUb/5x9f+H5+388hKJLAwnEFG0mUzNYCUcaNXBsn89pRAuciZBOIbAJCAuT2kqVojuUj5XmD1LHozIOIe64RwXcLNofAYeUCREgH8/d3DzuvPx1vv3/w79W3g4/Hz3c2O2+33vx61N3rPjnSf+/sb+2929jf6P79rD/X4QjSHCr1lhIIogglKSSdUum534ZJ9loPiDLpBa2VNIMPbiymRIfYHj6dTgkTHOxnw8kCIAi8DPMN2P8Wx+WDGOwfy7cAP6YjGZcePCVC4JmNAGZSTeQ5DUmNFc2WghnUjBr7LDkIKoSufEdpPZ9uXyyOWzyN9srVFoZER5o5GZsKK7BZ9fJYS7dy6Upjqu2T9AXDdcWcH6lrvBlxVlmfmGhfl022+ZbVy0/cnBr3xObDjmTybBbNUdjw8vqkf/XumuH2qkY6wKC1QYmQgfmITxXvj6vw9SofqCyZ4iPRNIWKe40bI2JhIJyVZ0ZLdAHyFU2MUrpDeBuQ77EI7nNhwckzUZSuD0YV/nnbWpxB0Iyvsfny4dfu0cmf3XsnP37ubLtZ/7APzfHSxEJEg9B1PxYN1qSyDJkV6F1xNo14kkNVRTw4E88lJGiGbmKJaEQyUJbmRAzq6U2LMj413ddMCTzNOYUsS+W0dYY9I54TGqAfQSv8rztvKbhMVZKwlV7gX7xLsN/loBMzwjtjh3c/RwCcKsJ9AqVTU3MmwrJ2Z69Zm+lXh0K0aLE6ZAG48cDlo1B5OuatwDxWhXzaBvSPrkP/aB7yMyTc99Qhl2aylFQs5qCfKhz6abDy5prRUfkPH7XVgQ==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/key_sl_tp_lib.py b/lib/key_monitor/key_sl_tp_lib.py index 0704694..a0cfeaa 100644 --- a/lib/key_monitor/key_sl_tp_lib.py +++ b/lib/key_monitor/key_sl_tp_lib.py @@ -1,139 +1,12 @@ -"""关键位箱体/收敛:止盈止损方案(Binance / Gate / OKX 共用).""" - -KEY_SL_TP_MODES = frozenset({"standard", "box_1p5", "trend_manual"}) - -KEY_SL_TP_MODE_LABELS = { - "standard": "标准突破", - "box_1p5": "箱体1R·止盈1.5H", - "trend_manual": "趋势单·自填止盈", -} - -KEY_MONITOR_AUTO_TYPES_FOR_FORM = frozenset({"箱体突破", "收敛突破"}) - - -def normalize_sl_tp_mode(raw): - m = (raw or "standard").strip().lower() - if m in ("box_1p5", "box15", "box-1.5", "box_1.5"): - return "box_1p5" - if m in ("trend_manual", "trend", "manual"): - return "trend_manual" - if m in KEY_SL_TP_MODES: - return m - return "standard" - - -def sl_tp_mode_label(mode): - return KEY_SL_TP_MODE_LABELS.get(normalize_sl_tp_mode(mode), normalize_sl_tp_mode(mode)) - - -def sl_tp_mode_from_row(row, default="standard"): - try: - if hasattr(row, "keys") and "sl_tp_mode" in row.keys(): - raw = row["sl_tp_mode"] - else: - raw = row.get("sl_tp_mode") if isinstance(row, dict) else None - except Exception: - raw = None - return normalize_sl_tp_mode(raw if raw not in (None, "") else default) - - -def breakeven_enabled_from_row(row, default=0): - try: - if hasattr(row, "keys") and "breakeven_enabled" in row.keys(): - v = row["breakeven_enabled"] - else: - v = row.get("breakeven_enabled") if isinstance(row, dict) else None - except Exception: - v = None - if v is None: - return int(default) != 0 - return int(v) != 0 - - -def parse_breakeven_enabled_form(form_value): - return 1 if (form_value or "").strip().lower() in ("1", "true", "on", "yes") else 0 - - -def plan_key_sl_tp( - mode, - direction, - upper, - lower, - checks, - *, - outside_pct, - trend_outside_pct, - manual_take_profit=None, -): - """ - 以确认 K 收盘 E 为「当前价」计算计划 SL/TP. - 返回 (E, sl_raw, tp_raw, box_h) 或 None(几何无效 / 模式3缺止盈). - """ - try: - E = float(checks["confirm_close"]) - H = abs(float(upper) - float(lower)) - except (TypeError, ValueError, KeyError): - return None - if H <= 0: - return None - direction = (direction or "long").strip().lower() - mode = normalize_sl_tp_mode(mode) - - if mode == "box_1p5": - if direction == "long": - sl_raw = E - H - tp_raw = E + 1.5 * H - else: - sl_raw = E + H - tp_raw = E - 1.5 * H - return E, sl_raw, tp_raw, H - - if mode == "trend_manual": - try: - br_hi = float(checks["breakout_high"]) - br_lo = float(checks["breakout_low"]) - tp_raw = float(manual_take_profit) - except (TypeError, ValueError, KeyError): - return None - m = float(trend_outside_pct) / 100.0 - if direction == "long": - sl_raw = br_lo * (1.0 - m) if br_lo > 0 else 0.0 - if tp_raw <= E or sl_raw <= 0: - return None - else: - sl_raw = br_hi * (1.0 + m) if br_hi > 0 else 0.0 - if tp_raw >= E or sl_raw <= 0: - return None - return E, sl_raw, tp_raw, H - - # standard:突破 K 极值外侧 + 止盈 E±1×H - try: - br_hi = float(checks["breakout_high"]) - br_lo = float(checks["breakout_low"]) - except (TypeError, ValueError, KeyError): - return None - om = float(outside_pct) / 100.0 - if direction == "long": - sl_raw = br_lo * (1.0 - om) if br_lo > 0 else 0.0 - tp_raw = E + H - else: - sl_raw = br_hi * (1.0 + om) if br_hi > 0 else 0.0 - tp_raw = E - H - return E, sl_raw, tp_raw, H - - -def sl_tp_plan_summary_text(mode, direction, E, sl_raw, tp_raw, box_h, *, outside_pct, trend_outside_pct): - """微信/页面用一行计划 SL/TP 说明.""" - mode = normalize_sl_tp_mode(mode) - direction = (direction or "long").strip().lower() - if mode == "box_1p5": - return ( - f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=E∓1×H({box_h})|TP=E∓1.5×H" - ) - if mode == "trend_manual": - return ( - f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=突破K极值外{trend_outside_pct}%|TP={tp_raw}(录入)" - ) - return ( - f"方案:{sl_tp_mode_label(mode)}|E={E}|SL=突破K外{outside_pct}%|TP=E±1×H({box_h})" - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/key_monitor/trigger_entry_key_monitor_lib.py b/lib/key_monitor/trigger_entry_key_monitor_lib.py index 7c67aa7..e70cd18 100644 --- a/lib/key_monitor/trigger_entry_key_monitor_lib.py +++ b/lib/key_monitor/trigger_entry_key_monitor_lib.py @@ -1,324 +1,12 @@ -"""回调/突破触价开仓关键位监控:程序盯价,触达计划入场后市价成交(三所共用逻辑).""" -from __future__ import annotations - -from datetime import datetime -from typing import Any, Callable, Optional - -from lib.key_monitor.false_breakout_key_monitor_lib import ( - _parse_created_at, - expires_at_text, - is_false_breakout_expired, -) -from lib.strategy.strategy_trend_lib import trend_dca_level_reached - -# 回调触价(原「触价开仓」) -CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE = "回调触价开仓" -LEGACY_TRIGGER_ENTRY_MONITOR_TYPE = "触价开仓" - -# 突破触价:标记价穿越 E 后立即市价开仓 -BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE = "突破触价开仓" - -TRIGGER_ENTRY_MONITOR_TYPES = frozenset( - { - CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE, - BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE, - LEGACY_TRIGGER_ENTRY_MONITOR_TYPE, - } -) - -TRIGGER_ENTRY_VALIDITY_HOURS = 24 -TRIGGER_ENTRY_CLOSE_FILLED = "trigger_entry_filled" -TRIGGER_ENTRY_CLOSE_TP_INVALIDATE = "trigger_tp_invalidate" -TRIGGER_ENTRY_CLOSE_SL_INVALIDATE = "trigger_sl_invalidate" -TRIGGER_ENTRY_CLOSE_EXPIRED = "trigger_entry_expired" -TRIGGER_ENTRY_CLOSE_EXCHANGE_FAILED = "trigger_exchange_failed" - -KEY_ENTRY_REASON_CALLBACK = "关键位回调触价开仓" -KEY_ENTRY_REASON_BREAKOUT = "关键位突破触价开仓" -KEY_ENTRY_REASON_TRIGGER_LEGACY = "关键位触价开仓" - -TRIGGER_ENTRY_IN_FLIGHT_OID = "__trigger_entry_in_flight__" - - -def is_trigger_entry_in_flight_row(row: Any) -> bool: - if row is None: - return False - try: - v = row["fib_limit_order_id"] - except (KeyError, IndexError, TypeError): - v = getattr(row, "fib_limit_order_id", None) - return (v or "").strip() == TRIGGER_ENTRY_IN_FLIGHT_OID - - -def acquire_trigger_entry_exec_lock(conn: Any, monitor_id: int) -> bool: - cur = conn.execute( - "UPDATE key_monitors SET fib_limit_order_id=? WHERE id=? " - "AND (fib_limit_order_id IS NULL OR fib_limit_order_id='')", - (TRIGGER_ENTRY_IN_FLIGHT_OID, int(monitor_id)), - ) - return int(cur.rowcount or 0) == 1 - - -def release_trigger_entry_exec_lock(conn: Any, monitor_id: int) -> None: - conn.execute( - "UPDATE key_monitors SET fib_limit_order_id=NULL WHERE id=? AND fib_limit_order_id=?", - (int(monitor_id), TRIGGER_ENTRY_IN_FLIGHT_OID), - ) - - -def normalize_trigger_entry_monitor_type(monitor_type: Optional[str]) -> str: - mt = (monitor_type or "").strip() - if mt == LEGACY_TRIGGER_ENTRY_MONITOR_TYPE: - return CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE - return mt - - -def is_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool: - return (monitor_type or "").strip() in TRIGGER_ENTRY_MONITOR_TYPES - - -def is_callback_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool: - mt = normalize_trigger_entry_monitor_type(monitor_type) - return mt == CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE - - -def is_breakout_trigger_entry_key_monitor_type(monitor_type: Optional[str]) -> bool: - return (monitor_type or "").strip() == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE - - -def key_entry_reason_for_monitor_type(monitor_type: Optional[str]) -> str: - if is_breakout_trigger_entry_key_monitor_type(monitor_type): - return KEY_ENTRY_REASON_BREAKOUT - if is_trigger_entry_key_monitor_type(monitor_type): - return KEY_ENTRY_REASON_CALLBACK - return KEY_ENTRY_REASON_TRIGGER_LEGACY - - -def trigger_entry_reached(direction: str, mark_price: float, entry: float) -> bool: - """回调触价:多=价跌至 E;空=价涨至 E.""" - return trend_dca_level_reached(direction, mark_price, entry) - - -def breakout_trigger_entry_crossed( - direction: str, - prev_mark: Optional[float], - mark: float, - entry: float, -) -> bool: - """突破触价:多=向上穿越 E;空=向下穿越 E.""" - try: - m = float(mark) - e = float(entry) - pm = float(prev_mark) if prev_mark is not None else None - except (TypeError, ValueError): - return False - direction = (direction or "long").strip().lower() - if direction == "long": - if pm is None: - return m > e - return pm <= e and m > e - if pm is None: - return m < e - return pm >= e and m < e - - -def trigger_should_fire( - monitor_type: Optional[str], - direction: str, - mark: float, - entry: float, - prev_mark: Optional[float] = None, -) -> bool: - if is_breakout_trigger_entry_key_monitor_type(monitor_type): - return breakout_trigger_entry_crossed(direction, prev_mark, mark, entry) - return trigger_entry_reached(direction, mark, entry) - - -def trigger_entry_invalidate_by_tp(direction: str, mark_price: float, take_profit: float) -> bool: - """未开仓前标记价先触达止盈侧则失效.""" - try: - m = float(mark_price) - tp = float(take_profit) - except (TypeError, ValueError): - return False - d = (direction or "long").strip().lower() - if d == "short": - return m <= tp - return m >= tp - - -def trigger_entry_invalidate_by_sl(direction: str, mark_price: float, stop_loss: float) -> bool: - """突破触价:未到 E 先触达止损侧则失效.""" - try: - m = float(mark_price) - sl = float(stop_loss) - except (TypeError, ValueError): - return False - d = (direction or "long").strip().lower() - if d == "long": - return m <= sl - return m >= sl - - -def trigger_entry_invalidate( - monitor_type: Optional[str], - direction: str, - mark: float, - stop_loss: float, - take_profit: float, -) -> Optional[str]: - if trigger_entry_invalidate_by_tp(direction, mark, take_profit): - return "tp" - if is_breakout_trigger_entry_key_monitor_type(monitor_type): - if trigger_entry_invalidate_by_sl(direction, mark, stop_loss): - return "sl" - return None - - -def validate_trigger_entry_geometry( - direction: str, - entry: float, - stop_loss: float, - take_profit: float, - mark_at_add: Optional[float] = None, - *, - monitor_type: Optional[str] = None, -) -> Optional[str]: - """返回错误文案;合法则 None.""" - try: - e = float(entry) - sl = float(stop_loss) - tp = float(take_profit) - except (TypeError, ValueError): - return "入场价,止损,止盈须为有效数字" - if e <= 0 or sl <= 0 or tp <= 0: - return "入场价,止损,止盈须大于 0" - d = (direction or "long").strip().lower() - mt = normalize_trigger_entry_monitor_type(monitor_type) - label = "突破触价开仓" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调触价开仓" - if d == "long": - if not (sl < e < tp): - return "做多:须满足 止损 < 入场价 < 止盈" - if mark_at_add is not None: - m = float(mark_at_add) - if m >= tp: - return f"做多:当前价已不低于止盈,无法添加{label}" - if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m >= e: - return "做多:当前价须低于入场价(等待向上突破)" - elif d == "short": - if not (tp < e < sl): - return "做空:须满足 止盈 < 入场价 < 止损" - if mark_at_add is not None: - m = float(mark_at_add) - if m <= tp: - return f"做空:当前价已不高于止盈,无法添加{label}" - if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE and m <= e: - return "做空:当前价须高于入场价(等待向下跌破)" - else: - return "方向须为 long 或 short" - return None - - -def validate_trigger_entry_rr( - direction: str, - entry: float, - stop_loss: float, - take_profit: float, - min_rr: float, - calc_rr_ratio: Callable[..., Optional[float]], -) -> Optional[str]: - rr = calc_rr_ratio(direction, entry, stop_loss, take_profit) - if rr is None or rr <= float(min_rr): - fmt = f"{rr:.4f}" if rr is not None else "无法计算" - return f"计划盈亏比 {fmt}:1 未达要求(>{float(min_rr)}:1)" - return None - - -def is_trigger_entry_expired( - created_at: Any, - now: datetime, - *, - hours: int = TRIGGER_ENTRY_VALIDITY_HOURS, -) -> bool: - return is_false_breakout_expired(created_at, now, hours=hours) - - -def trigger_entry_expires_at_text( - created_at: Any, - *, - hours: int = TRIGGER_ENTRY_VALIDITY_HOURS, -) -> str: - return expires_at_text(created_at, hours=hours) - - -def count_pending_trigger_entries(conn: Any, trading_day: str) -> int: - td = (trading_day or "").strip() - if not td: - return 0 - placeholders = ",".join("?" * len(TRIGGER_ENTRY_MONITOR_TYPES)) - row = conn.execute( - f"SELECT COUNT(*) FROM key_monitors WHERE monitor_type IN ({placeholders}) AND session_date=?", - (*TRIGGER_ENTRY_MONITOR_TYPES, td), - ).fetchone() - return int(row[0] if row else 0) - - -def check_trigger_entry_intent_limit( - conn: Any, - trading_day: str, - opens_today: int, - hard_limit: int, -) -> tuple[bool, str]: - """当日开仓意图:已成交次数 + 待触发触价条数.""" - if int(hard_limit) <= 0: - return True, "" - pending = count_pending_trigger_entries(conn, trading_day) - total = int(opens_today) + pending - if total >= int(hard_limit): - return ( - False, - f"本交易日开仓意图已达上限(已开 {int(opens_today)} + 待触发 {pending} / 硬上限 {int(hard_limit)})", - ) - return True, "" - - -def trigger_entry_gate_preview( - *, - monitor_type: Optional[str] = None, - entry_display: str, - take_profit_display: str, - created_at: Any = None, - now: Optional[datetime] = None, - expired: bool = False, - tp_invalidated: bool = False, - sl_invalidated: bool = False, - hours: int = TRIGGER_ENTRY_VALIDITY_HOURS, -) -> dict[str, Any]: - now_dt = now or datetime.now() - is_exp = expired or is_trigger_entry_expired(created_at, now_dt, hours=hours) - exp_txt = trigger_entry_expires_at_text(created_at, hours=hours) - mt = normalize_trigger_entry_monitor_type(monitor_type) - if tp_invalidated: - status = "止盈侧失效" - elif sl_invalidated: - status = "止损侧失效" - elif is_exp: - status = "已过期" - elif mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE: - status = "突破待触发" - else: - status = "回调待触发" - mode = "突破" if mt == BREAKOUT_TRIGGER_ENTRY_MONITOR_TYPE else "回调" - metrics_parts: list[str] = [f"TP:{take_profit_display}"] - if exp_txt != "—": - metrics_parts.append(f"截至:{exp_txt}") - return { - "summary": f"{mode}触价 E={entry_display} {status}", - "metrics": " ".join(metrics_parts), - "gate_ok": not is_exp and not tp_invalidated and not sl_invalidated, - } - - -# 兼容旧 import -TRIGGER_ENTRY_MONITOR_TYPE = CALLBACK_TRIGGER_ENTRY_MONITOR_TYPE -KEY_ENTRY_REASON_TRIGGER = KEY_ENTRY_REASON_CALLBACK +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/license/__init__.py b/lib/license/__init__.py new file mode 100644 index 0000000..458e5c8 --- /dev/null +++ b/lib/license/__init__.py @@ -0,0 +1,19 @@ +"""整机许可:设备绑定、激活码兑换、联网校验。""" + +from lib.license.license_lib import ( + ensure_license_or_raise, + get_device_id, + get_license_status, + is_license_valid, + redeem_code, + validate_license, +) + +__all__ = [ + "ensure_license_or_raise", + "get_device_id", + "get_license_status", + "is_license_valid", + "redeem_code", + "validate_license", +] diff --git a/lib/license/fastapi_gate.py b/lib/license/fastapi_gate.py new file mode 100644 index 0000000..e0c893d --- /dev/null +++ b/lib/license/fastapi_gate.py @@ -0,0 +1,115 @@ +"""FastAPI 中控许可中间件与 /license 页。""" + +from __future__ import annotations + +import os +from pathlib import Path + +from fastapi import FastAPI, Request +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse + +from lib.license.license_lib import ( + get_device_id, + get_license_status, + is_license_valid, + redeem_code, + validate_license, +) + +_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html" + + +def _allowed_path(path: str) -> bool: + if path in ( + "/license", + "/api/license/status", + "/api/license/redeem", + "/api/license/validate", + "/health", + ): + return True + if path.startswith("/assets/") or path.startswith("/static/"): + return True + if path.startswith("/favicon"): + return True + return False + + +def install_license_middleware(app: FastAPI) -> None: + @app.get("/health") + async def _license_health(): + st = get_license_status(skip_remote=True) + return {"ok": True, "license_valid": bool(st.get("valid")), "service": "manual_trading_hub"} + + @app.get("/api/license/status") + async def _license_status_api(): + return get_license_status() + + @app.post("/api/license/redeem") + async def _license_redeem_api(request: Request): + try: + data = await request.json() + except Exception: + data = {} + code = (data.get("code") if isinstance(data, dict) else "") or "" + return redeem_code(str(code)) + + @app.post("/api/license/validate") + async def _license_validate_api(): + return validate_license(force=True) + + @app.api_route("/license", methods=["GET", "POST"]) + async def _license_page(request: Request): + msg = "" + err = "" + if request.method == "POST": + form = await request.form() + code = str(form.get("code") or "").strip() + result = redeem_code(code) + if result.get("ok"): + msg = result.get("message") or "激活成功" + else: + err = result.get("message") or "激活失败" + status = get_license_status() + html = _TEMPLATE_PATH.read_text(encoding="utf-8") + # 简单替换,避免 Jinja 依赖差异 + filled = ( + html.replace("{{ device_id }}", get_device_id()) + .replace("{{ api_url }}", str(status.get("api_url") or "")) + .replace("{{ wechat }}", "dekun03") + .replace("{{ message }}", msg) + .replace("{{ error }}", err) + .replace("{{ status_message }}", str(status.get("message") or "")) + .replace("{{ expires_at }}", str(status.get("expires_at") or "—")) + .replace("{{ plan }}", str(status.get("plan") or "—")) + .replace("{{ valid_text }}", "已授权" if status.get("valid") else "未授权") + ) + # Flask 模板用 Jinja;FastAPI 路径用占位符版本 + if "{%" in filled or "{{" in filled: + from jinja2 import Template + + filled = Template(html).render( + device_id=get_device_id(), + status=status, + message=msg, + error=err, + api_url=status.get("api_url") or "", + wechat="dekun03", + ) + return HTMLResponse(filled) + + @app.middleware("http") + async def _license_http_middleware(request: Request, call_next): + if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"): + return await call_next(request) + path = request.url.path or "/" + if _allowed_path(path): + return await call_next(request) + if is_license_valid(): + return await call_next(request) + if path.startswith("/api/"): + return JSONResponse( + {"ok": False, "error": "license_required", "message": "请先激活许可"}, + status_code=403, + ) + return RedirectResponse(url="/license", status_code=302) diff --git a/lib/license/flask_gate.py b/lib/license/flask_gate.py new file mode 100644 index 0000000..b48cdfa --- /dev/null +++ b/lib/license/flask_gate.py @@ -0,0 +1,88 @@ +"""Flask 实例许可门禁与 /license 页。""" + +from __future__ import annotations + +import os +from pathlib import Path + +from flask import Flask, jsonify, redirect, render_template_string, request + +from lib.license.license_lib import ( + get_device_id, + get_license_status, + is_license_valid, + redeem_code, + validate_license, +) + +_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "license.html" + + +def _allowed_path(path: str) -> bool: + if path in ("/license", "/api/license/status", "/api/license/redeem", "/api/license/validate", "/health"): + return True + if path.startswith("/static/"): + return True + if path.startswith("/favicon"): + return True + return False + + +def install_license_gate(app: Flask) -> None: + """注册 /license 与 before_request 门禁。三所 Flask 共用。""" + + @app.get("/health") + def _license_health(): + st = get_license_status(skip_remote=True) + return jsonify({"ok": True, "license_valid": bool(st.get("valid"))}) + + @app.get("/api/license/status") + def _license_status_api(): + return jsonify(get_license_status()) + + @app.post("/api/license/redeem") + def _license_redeem_api(): + data = request.get_json(silent=True) or {} + code = (data.get("code") or request.form.get("code") or "").strip() + return jsonify(redeem_code(code)) + + @app.post("/api/license/validate") + def _license_validate_api(): + return jsonify(validate_license(force=True)) + + @app.route("/license", methods=["GET", "POST"]) + def _license_page(): + msg = "" + err = "" + if request.method == "POST": + code = (request.form.get("code") or "").strip() + result = redeem_code(code) + if result.get("ok"): + msg = result.get("message") or "激活成功" + else: + err = result.get("message") or "激活失败" + status = get_license_status() + html = _TEMPLATE_PATH.read_text(encoding="utf-8") + return render_template_string( + html, + device_id=get_device_id(), + status=status, + message=msg, + error=err, + api_url=status.get("api_url") or "", + wechat="dekun03", + ) + + @app.before_request + def _license_before_request(): + if os.getenv("LICENSE_DISABLED", "").strip().lower() in ("1", "true", "yes", "on"): + return None + path = request.path or "/" + if _allowed_path(path): + return None + # hub bridge 内部调用:仍要求已授权(整机许可) + if is_license_valid(): + return None + if path.startswith("/api/"): + return jsonify({"ok": False, "error": "license_required", "message": "请先激活许可"}), 403 + return redirect("/license") diff --git a/lib/license/license_lib.py b/lib/license/license_lib.py new file mode 100644 index 0000000..e084f32 --- /dev/null +++ b/lib/license/license_lib.py @@ -0,0 +1,393 @@ +"""用户端许可客户端:对接 https://sq.bz121.com (crypto_monitor_web)。""" + +from __future__ import annotations + +import hashlib +import json +import os +import platform +import threading +import time +import uuid +from datetime import datetime, timezone +from pathlib import Path +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +from lib.paths import REPO_ROOT + +DEFAULT_API_URL = "https://sq.bz121.com" +VALIDATE_INTERVAL_SEC = 3 * 24 * 3600 # 每 3 天 +DEFAULT_OFFLINE_GRACE_HOURS = 72 +STATE_FILENAME = "license_state.json" +LICENSE_ENV_NAME = "license.env" + +_lock = threading.RLock() +_cached_device_id: str | None = None +_env_loaded = False + + +def _load_license_env() -> None: + global _env_loaded + if _env_loaded: + return + _env_loaded = True + path = REPO_ROOT / LICENSE_ENV_NAME + if not path.is_file(): + return + try: + text = path.read_text(encoding="utf-8") + except OSError: + return + for line in text.splitlines(): + raw = line.strip() + if not raw or raw.startswith("#") or "=" not in raw: + continue + key, value = raw.split("=", 1) + key = key.strip().lstrip("\ufeff") + if not key or key in os.environ: + continue + os.environ[key] = value.strip().strip('"').strip("'") + + +def api_base_url() -> str: + _load_license_env() + url = (os.getenv("LICENSE_API_URL") or DEFAULT_API_URL).strip().rstrip("/") + return url or DEFAULT_API_URL + + +def client_key() -> str: + _load_license_env() + return (os.getenv("LICENSE_CLIENT_KEY") or "").strip() + + +def offline_grace_hours() -> float: + _load_license_env() + raw = (os.getenv("LICENSE_OFFLINE_GRACE_HOURS") or str(DEFAULT_OFFLINE_GRACE_HOURS)).strip() + try: + return max(0.0, float(raw)) + except ValueError: + return float(DEFAULT_OFFLINE_GRACE_HOURS) + + +def state_path() -> Path: + _load_license_env() + override = (os.getenv("LICENSE_STATE_PATH") or "").strip() + if override: + p = Path(override) + if not p.is_absolute(): + p = REPO_ROOT / p + p.parent.mkdir(parents=True, exist_ok=True) + return p + data = REPO_ROOT / "data" + data.mkdir(parents=True, exist_ok=True) + return data / STATE_FILENAME + + +def get_device_id() -> str: + """稳定设备指纹,长度 16–64,供 redeem/validate 使用。""" + global _cached_device_id + if _cached_device_id: + return _cached_device_id + parts = [ + platform.node() or "", + platform.system() or "", + platform.machine() or "", + hex(uuid.getnode()), + ] + try: + machine_id = Path("/etc/machine-id") + if machine_id.is_file(): + parts.append(machine_id.read_text(encoding="utf-8").strip()) + except OSError: + pass + try: + win_id = Path(os.environ.get("SystemRoot", r"C:\Windows")) / "System32" / "drivers" / "etc" + parts.append(str(win_id.resolve())) + except OSError: + pass + digest = hashlib.sha256("|".join(parts).encode("utf-8", errors="ignore")).hexdigest() + _cached_device_id = digest[:40] + return _cached_device_id + + +def _read_state() -> dict[str, Any]: + path = state_path() + if not path.is_file(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except (OSError, json.JSONDecodeError): + return {} + + +def _write_state(data: dict[str, Any]) -> None: + path = state_path() + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_suffix(".tmp") + tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + tmp.replace(path) + + +def _now_ts() -> float: + return time.time() + + +def _parse_expires_at(value: str | None) -> float | None: + if not value: + return None + s = value.strip() + if not s: + return None + try: + if s.endswith("Z"): + s = s[:-1] + "+00:00" + dt = datetime.fromisoformat(s) + if dt.tzinfo is None: + # 授权站按北京时间存,无 tz 时按 UTC+8 + from datetime import timedelta, timezone as _tz + + dt = dt.replace(tzinfo=_tz(timedelta(hours=8))) + return dt.timestamp() + except ValueError: + return None + + +def _http_json(method: str, path: str, body: dict[str, Any]) -> dict[str, Any]: + key = client_key() + if not key: + return {"ok": False, "message": "未配置 LICENSE_CLIENT_KEY(与授权站 CLIENT_API_KEY 一致)"} + url = f"{api_base_url()}{path}" + payload = json.dumps(body).encode("utf-8") + req = Request( + url, + data=payload, + method=method.upper(), + headers={ + "Content-Type": "application/json", + "X-Client-Key": key, + "User-Agent": "crypto_monitor_user/license", + }, + ) + try: + with urlopen(req, timeout=20) as resp: + raw = resp.read().decode("utf-8", errors="replace") + data = json.loads(raw) if raw else {} + return data if isinstance(data, dict) else {"ok": False, "message": "响应格式错误"} + except HTTPError as e: + try: + detail = e.read().decode("utf-8", errors="replace") + except Exception: + detail = str(e) + return {"ok": False, "message": f"HTTP {e.code}: {detail[:200]}", "network_error": True} + except (URLError, TimeoutError, OSError) as e: + return {"ok": False, "message": f"无法连接授权服务: {e}", "network_error": True} + except json.JSONDecodeError: + return {"ok": False, "message": "授权服务返回非 JSON", "network_error": True} + + +def redeem_code(code: str) -> dict[str, Any]: + code = (code or "").strip().upper().replace(" ", "") + if len(code) < 8: + return {"ok": False, "message": "激活码无效"} + device_id = get_device_id() + result = _http_json("POST", "/v1/redeem", {"device_id": device_id, "code": code}) + if not result.get("ok"): + return { + "ok": False, + "message": result.get("message") or "兑换失败", + "network_error": bool(result.get("network_error")), + } + now = _now_ts() + with _lock: + state = { + "device_id": device_id, + "subscription_id": result.get("subscription_id"), + "plan": result.get("plan"), + "expires_at": result.get("expires_at"), + "last_ok_at": now, + "last_validate_at": now, + "last_reason": "", + } + _write_state(state) + return { + "ok": True, + "message": result.get("message") or "激活成功", + "subscription_id": result.get("subscription_id"), + "plan": result.get("plan"), + "expires_at": result.get("expires_at"), + "device_id": device_id, + } + + +def validate_license(*, force: bool = False) -> dict[str, Any]: + """联网校验;更新本地状态。force=True 忽略 3 天间隔。""" + with _lock: + state = _read_state() + sub_id = (state.get("subscription_id") or "").strip() + if not sub_id: + return {"ok": True, "valid": False, "reason": "not_activated", "message": "尚未激活"} + + last_v = float(state.get("last_validate_at") or 0) + if not force and last_v and (_now_ts() - last_v) < VALIDATE_INTERVAL_SEC: + return get_license_status() + + device_id = get_device_id() + result = _http_json( + "POST", + "/v1/validate", + {"device_id": device_id, "subscription_id": sub_id}, + ) + now = _now_ts() + if result.get("network_error"): + # 断网:在宽限期内沿用本地到期日 + status = get_license_status(skip_remote=True) + status["message"] = result.get("message") or "网络异常,使用本地宽限" + status["network_error"] = True + with _lock: + st = _read_state() + st["last_network_error_at"] = now + st["last_reason"] = "network_error" + _write_state(st) + return status + + valid = bool(result.get("valid")) + reason = (result.get("reason") or "").strip() + with _lock: + st = _read_state() + st["device_id"] = device_id + st["last_validate_at"] = now + if valid: + st["last_ok_at"] = now + st["last_reason"] = "" + if result.get("expires_at"): + st["expires_at"] = result.get("expires_at") + if result.get("plan"): + st["plan"] = result.get("plan") + else: + st["last_reason"] = reason or "invalid" + if reason in ("expired", "device_revoked", "subscription_not_found", "subscription_inactive"): + # 明确失效:保留字段便于页面展示,但 valid=False + pass + _write_state(st) + + return { + "ok": True, + "valid": valid, + "reason": reason if not valid else "", + "expires_at": result.get("expires_at") or state.get("expires_at"), + "plan": result.get("plan") or state.get("plan"), + "device_id": device_id, + "subscription_id": sub_id, + "message": "有效" if valid else (reason or "无效"), + } + + +def get_license_status(*, skip_remote: bool = False) -> dict[str, Any]: + """本地状态(可触发到期判断);不主动联网除非 skip_remote=False 且到期需刷新。""" + with _lock: + state = _read_state() + device_id = get_device_id() + sub_id = (state.get("subscription_id") or "").strip() + if not sub_id: + return { + "ok": True, + "valid": False, + "reason": "not_activated", + "message": "尚未激活", + "device_id": device_id, + "api_url": api_base_url(), + } + + exp_ts = _parse_expires_at(state.get("expires_at")) + now = _now_ts() + if exp_ts is not None and now > exp_ts: + return { + "ok": True, + "valid": False, + "reason": "expired", + "message": "许可已过期", + "expires_at": state.get("expires_at"), + "plan": state.get("plan"), + "device_id": device_id, + "subscription_id": sub_id, + "api_url": api_base_url(), + } + + reason = (state.get("last_reason") or "").strip() + if reason in ("expired", "device_revoked", "subscription_not_found", "subscription_inactive"): + return { + "ok": True, + "valid": False, + "reason": reason, + "message": reason, + "expires_at": state.get("expires_at"), + "plan": state.get("plan"), + "device_id": device_id, + "subscription_id": sub_id, + "api_url": api_base_url(), + } + + last_ok = float(state.get("last_ok_at") or 0) + grace = offline_grace_hours() * 3600 + if last_ok and grace > 0 and (now - last_ok) > grace: + # 超过离线宽限且长时间未成功校验 + last_v = float(state.get("last_validate_at") or 0) + if last_v and (now - last_v) >= VALIDATE_INTERVAL_SEC and not skip_remote: + return validate_license(force=True) + if (now - last_ok) > grace: + return { + "ok": True, + "valid": False, + "reason": "offline_grace_exceeded", + "message": "超过离线宽限期,请联网校验", + "expires_at": state.get("expires_at"), + "plan": state.get("plan"), + "device_id": device_id, + "subscription_id": sub_id, + "api_url": api_base_url(), + } + + return { + "ok": True, + "valid": True, + "reason": "", + "message": "已授权", + "expires_at": state.get("expires_at"), + "plan": state.get("plan"), + "device_id": device_id, + "subscription_id": sub_id, + "last_ok_at": last_ok or None, + "api_url": api_base_url(), + } + + +def is_license_valid() -> bool: + status = get_license_status() + if status.get("valid"): + # 到期需周期性联网 + last_v = 0.0 + with _lock: + st = _read_state() + last_v = float(st.get("last_validate_at") or 0) + if last_v and (_now_ts() - last_v) >= VALIDATE_INTERVAL_SEC: + status = validate_license(force=True) + return bool(status.get("valid")) + # 未激活或已失效:尝试若有 subscription 则强制校验一次(避免本地过期标志陈旧) + with _lock: + st = _read_state() + if (st.get("subscription_id") or "").strip() and status.get("reason") not in ( + "not_activated", + "expired", + "device_revoked", + ): + status = validate_license(force=True) + return bool(status.get("valid")) + return False + + +def ensure_license_or_raise() -> None: + if not is_license_valid(): + raise RuntimeError("license_required") diff --git a/lib/license/templates/license.html b/lib/license/templates/license.html new file mode 100644 index 0000000..752b4cb --- /dev/null +++ b/lib/license/templates/license.html @@ -0,0 +1,100 @@ + + + + + + 软件授权 + + + +
+

软件授权

+

复制下方设备 ID,联系微信 {{ wechat }} 购买激活码后粘贴兑换。授权站:{{ api_url }}

+ + {% if message %}

{{ message }}

{% endif %} + {% if error %}

{{ error }}

{% endif %} + +
+ +
{{ device_id }}
+
+ +
+
+ + +
+ +
+ +
+ 状态:{% if status.valid %}已授权{% else %}未授权{% endif %} + ({{ status.message or status.reason or '—' }})
+ 套餐:{{ status.plan or '—' }} 到期:{{ status.expires_at or '—' }} +
+
+ + diff --git a/lib/options/__init__.py b/lib/options/__init__.py index e69de29..5858b0a 100644 --- a/lib/options/__init__.py +++ b/lib/options/__init__.py @@ -0,0 +1,12 @@ +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZgYpASYJmW6m2hPT9FLAEoL2gClBc3NEqxOJqdmGpSzmWR6BUgLpjKGMwvyJ9SGygQ58znYnltyZW73c3tnzdf6L50cW6PJaO3jJsAt11oWBSXY4RwtoBzuFhKTACzZbSLC290uJulbRpPBK+AXbq4i9R7+9hIrhB3jlg+C6D57kYpJpaGqQAEmzEt' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_close_exec_lib.py b/lib/options/options_close_exec_lib.py index 59e2cd0..73f5a3d 100644 --- a/lib/options/options_close_exec_lib.py +++ b/lib/options/options_close_exec_lib.py @@ -1,380 +1,12 @@ -"""期权平仓执行:只锁买一限价卖出;永不市价.""" -from __future__ import annotations - -import time -from typing import Any - -from lib.options.options_close_gate_lib import ( - clear_close_gate, - is_close_gate_passed, - mark_close_gate_passed, - update_close_gate, -) -from lib.options.options_pricing_lib import ( - estimate_close_by_bids, - fetch_option_mark_px, - is_stub_bid_px, - total_premium, -) - - -def _safe_float(v: Any) -> float | None: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None: - try: - conn = cfg["get_db"]() - try: - from lib.options.options_db import init_options_tables, sum_open_premium_paid - - init_options_tables(conn) - return sum_open_premium_paid(conn, inst_id) - finally: - conn.close() - except Exception: - pass - return None - - -def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]: - from lib.exchange.okx_options_lib import option_fields_from_inst_id - from lib.options.options_pricing_lib import close_ref_prices - - inst_id = str(pos.get("instId") or pos.get("inst_id") or "") - mark = _safe_float(pos.get("markPx")) or _safe_float((quote or {}).get("mark_px") or (quote or {}).get("mark")) - if mark is None: - mark = fetch_option_mark_px(ex, inst_id) - opt_type = pos.get("optType") or (quote or {}).get("opt_type") - strike = _safe_float(pos.get("stk")) or _safe_float((quote or {}).get("strike")) - if not opt_type or strike is None: - pt, ps = option_fields_from_inst_id(inst_id) - opt_type = opt_type or pt - if strike is None: - strike = ps - idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px")) - return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx) - - -def _avail_sheets(pos: dict[str, Any]) -> int: - avail = _safe_float(pos.get("availPos")) - if avail is None or avail <= 0: - avail = abs(_safe_float(pos.get("pos")) or 0) - return max(0, int(avail or 0)) - - -def _cancel_sell_pending(ex: Any, inst_id: str) -> None: - try: - pending = ex.private_get_trade_orders_pending({"instType": "OPTION", "instId": inst_id}) or {} - for o in pending.get("data") or []: - if str(o.get("side") or "").lower() != "sell": - continue - oid = o.get("ordId") - if not oid: - continue - try: - ex.private_post_trade_cancel_order({"instId": inst_id, "ordId": oid}) - except Exception: - pass - except Exception: - pass - - -def close_option_by_bid1( - cfg: dict[str, Any], - ex: Any, - inst_id: str, - *, - sheets: int | None = None, - require_recycle_gate: bool = False, - signal_note: str | None = None, -) -> dict[str, Any]: - """ - 本轮只吃买一深度: - - 本批张数 = min(请求张数, 持仓, 买一深度) - - 限价 = 校验通过时锁定的买一价 - - 永不市价 - - 始终校验有效流动性(残档买一禁止) - - require_recycle_gate=True 时:首次还需可回收≥2×权利金并持续 hold 秒; - 一旦通过后对同仓续批只验流动性 - """ - from lib.exchange.okx_options_lib import ( - _pos_side_from_position, - invalidate_option_positions_cache, - ) - - inst_id = (inst_id or "").strip() - if not inst_id: - return {"ok": False, "msg": "缺少 inst_id"} - - q = cfg["quote_option_contract"](ex, inst_id) - if not q.get("ok"): - return {"ok": False, "msg": q.get("msg") or "报价失败"} - tick_sz = q.get("tick_sz") - ct_mult = float(q.get("ct_mult") or 0.01) - - raw_positions = cfg["fetch_option_positions"](ex) - if raw_positions is None: - return {"ok": False, "msg": "获取期权持仓失败"} - pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None) - if not pos: - clear_close_gate(inst_id) - return {"ok": False, "msg": "未找到持仓", "already_flat": True} - - avail = _avail_sheets(pos) - want = int(sheets) if sheets else avail - want = min(want, avail) - if want < 1: - clear_close_gate(inst_id) - return {"ok": False, "msg": "可平张数不足", "already_flat": True} - - td_mode = str(pos.get("mgnMode") or cfg.get("td_mode") or "isolated") - pos_side = _pos_side_from_position(pos) or "net" - mark_px, intrinsic_px = _pos_close_refs(ex, pos, q) - premium_paid = _open_premium_paid(cfg, inst_id) - if premium_paid is None: - premium_paid = _safe_float(pos.get("premium_paid")) - - # 已有未成交卖平单:等成交,不撤不重挂 - try: - pending = ex.private_get_trade_orders_pending({"instType": "OPTION", "instId": inst_id}) or {} - sell_pending = [ - o - for o in (pending.get("data") or []) - if str(o.get("side") or "").lower() == "sell" and o.get("ordId") - ] - if sell_pending: - time.sleep(0.5) - invalidate_option_positions_cache() - raw_positions = cfg["fetch_option_positions"](ex) - if raw_positions is None: - return {"ok": False, "msg": "获取期权持仓失败"} - pos = next((p for p in raw_positions if str(p.get("instId")) == inst_id), None) - if not pos or _avail_sheets(pos) < 1: - clear_close_gate(inst_id) - return { - "ok": True, - "already_flat": True, - "msg": "已有限价卖单成交", - "close_ord_id": ",".join(str(o.get("ordId")) for o in sell_pending), - "fully_closed": True, - "submitted_sheets": want, - "remaining_sheets": 0, - "mode": "bid1", - } - return { - "ok": False, - "msg": "等待已有买一限价卖单成交", - "stopped_reason": "pending_close_order", - "close_ord_id": ",".join(str(o.get("ordId")) for o in sell_pending), - } - except Exception: - pass - - book = cfg["fetch_option_book_depth"](ex, inst_id, 1) - preview = estimate_close_by_bids( - book.get("bids") or [], - want, - ct_mult=ct_mult, - premium_paid=premium_paid, - mark_px=mark_px, - intrinsic_px=intrinsic_px, - max_levels=1, - ) - if preview.get("bid_invalid") or preview.get("auto_close_blocked"): - _cancel_sell_pending(ex, inst_id) - update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid) - return { - "ok": False, - "msg": preview.get("bid_invalid_reason") or "暂无有效买盘,禁止平仓", - "stopped_reason": "stub_bid", - "auto_close_blocked": True, - "liquidity_blocked": True, - } - - levels = preview.get("levels") or [] - if not levels: - bid_px = _safe_float(q.get("bid")) - stub, stub_reason = is_stub_bid_px(bid_px, mark_px=mark_px, intrinsic_px=intrinsic_px) - if stub or bid_px is None or bid_px <= 0: - update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid) - return { - "ok": False, - "msg": stub_reason or "暂无买一,无法限价平仓", - "stopped_reason": "stub_bid" if stub else "no_bid", - "auto_close_blocked": True, - "liquidity_blocked": True, - } - return { - "ok": False, - "msg": "暂无买一深度,无法平仓", - "stopped_reason": "no_bid_depth", - "liquidity_blocked": True, - } - - level = levels[0] - level_sheets = int(level.get("sheets") or 0) - level_px = float(level.get("px") or 0) - if level_sheets <= 0 or level_px <= 0: - return {"ok": False, "msg": "买一深度无效", "stopped_reason": "invalid_bid_depth"} - - stub_lv, stub_lv_reason = is_stub_bid_px(level_px, mark_px=mark_px, intrinsic_px=intrinsic_px) - if stub_lv: - update_close_gate(inst_id, recycle_usdc=None, premium_paid=premium_paid) - return { - "ok": False, - "msg": stub_lv_reason or "暂无有效买盘,禁止平仓", - "stopped_reason": "stub_bid", - "auto_close_blocked": True, - "liquidity_blocked": True, - } - - # 自动平仓:2×权利金门控(首次);通过后同仓续批只验流动性 - gate = update_close_gate( - inst_id, - recycle_usdc=_safe_float(preview.get("total_received")), - premium_paid=premium_paid, - ) - if require_recycle_gate and not is_close_gate_passed(inst_id) and not gate.get("ready"): - return { - "ok": False, - "msg": gate.get("msg") or "平仓门控未就绪(需可回收≥2×权利金并持续一段时间)", - "stopped_reason": "close_gate", - "auto_close_blocked": True, - "close_gate": gate, - } - if gate.get("ready"): - mark_close_gate_passed(inst_id) - - locked_bid_px = level_px - before_avail = avail - order = cfg["place_option_limit_order"]( - ex, - inst_id=inst_id, - side="sell", - sheets=level_sheets, - price=locked_bid_px, - td_mode=td_mode, - tick_sz=tick_sz, - reduce_only=True, - pos_side=pos_side, - ) - if not order.get("ok"): - return { - "ok": False, - "msg": order.get("msg") or "买一限价平仓失败", - "stopped_reason": "order_failed", - "locked_bid_px": locked_bid_px, - "batch_sheets": level_sheets, - } - - px = float(order.get("px", locked_bid_px)) - oid = str((order.get("data") or {}).get("ordId") or "") - prem_recv = round(total_premium(px, level_sheets * ct_mult), 4) - time.sleep(0.6) - invalidate_option_positions_cache() - raw2 = cfg["fetch_option_positions"](ex) - after_avail = 0 - if raw2 is not None: - after_pos = next((p for p in raw2 if str(p.get("instId")) == inst_id), None) - after_avail = _avail_sheets(after_pos) if after_pos else 0 - reduced = max(0, before_avail - after_avail) if raw2 is not None else 0 - remaining_pos = after_avail if raw2 is not None else max(0, before_avail - level_sheets) - fully_closed = remaining_pos < 1 - - if fully_closed: - clear_close_gate(inst_id) - conn = cfg["get_db"]() - try: - from lib.options.options_db import init_options_tables - - init_options_tables(conn) - open_rows = conn.execute( - """ - SELECT id, premium_paid FROM options_trades - WHERE inst_id = ? AND status = 'open' - ORDER BY id ASC - """, - (inst_id,), - ).fetchall() - total_paid = sum(float(r["premium_paid"] or 0) for r in open_rows) - allocated = 0.0 - for i, row in enumerate(open_rows): - paid = float(row["premium_paid"] or 0) - if i == len(open_rows) - 1: - recv = round(prem_recv - allocated, 4) - elif total_paid > 0: - recv = round(prem_recv * (paid / total_paid), 4) - allocated += recv - else: - recv = round(prem_recv / len(open_rows), 4) - allocated += recv - pnl = round(recv - paid, 4) - note_sql = "" - params: list[Any] = [px, recv, pnl, oid or None] - if signal_note and i == len(open_rows) - 1: - note_sql = """, - signal_note = CASE - WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN ? - ELSE signal_note - END""" - params.append(signal_note) - params.append(int(row["id"])) - conn.execute( - f""" - UPDATE options_trades - SET status = 'closed', close_quote = ?, premium_received = ?, - realized_pnl = ?, close_ord_id = ?, closed_at = CURRENT_TIMESTAMP - {note_sql} - WHERE id = ? - """, - tuple(params), - ) - conn.commit() - finally: - conn.close() - elif require_recycle_gate: - # 自动平已挂过单:同仓续批只验流动性 - mark_close_gate_passed(inst_id) - - return { - "ok": True, - "mode": "bid1", - "orders": [{"order": order, "px": px, "sheets": level_sheets}], - "bid": px, - "locked_bid_px": locked_bid_px, - "submitted_sheets": level_sheets, - "filled_or_reduced_sheets": min(reduced, level_sheets) if reduced else 0, - "remaining_sheets": remaining_pos, - "premium_received": prem_recv, - "stopped_reason": None if fully_closed else ("partial_bid1" if reduced > 0 else "order_not_filled"), - "close_ord_id": oid or None, - "fully_closed": fully_closed, - "msg": ( - f"已按买一 {locked_bid_px:g} 提交 {level_sheets} 张" - + ("" if fully_closed else f",剩余 {remaining_pos} 张待下次平仓") - ), - } - - -# 兼容旧名 -def close_option_by_bid_depth( - cfg: dict[str, Any], - ex: Any, - inst_id: str, - *, - sheets: int | None = None, -) -> dict[str, Any]: - return close_option_by_bid1( - cfg, - ex, - inst_id, - sheets=sheets, - require_recycle_gate=True, - signal_note="目标位平仓", - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_close_gate_lib.py b/lib/options/options_close_gate_lib.py index d263db2..f5a8b2a 100644 --- a/lib/options/options_close_gate_lib.py +++ b/lib/options/options_close_gate_lib.py @@ -1,184 +1,12 @@ -"""期权按买盘平仓门控:可回收需 ≥ N×权利金,并持续持有一段时间后才允许平仓.""" -from __future__ import annotations - -import os -import threading -import time -from typing import Any - - -def _env_float(key: str, default: float) -> float: - try: - return float(os.getenv(key, str(default))) - except (TypeError, ValueError): - return default - - -# 可回收 ≥ 权利金 × 倍数,且该状态持续满 hold_seconds 才允许按买盘平仓 -CLOSE_RECYCLE_MIN_MULT = _env_float("OKX_OPTIONS_CLOSE_RECYCLE_MULT", 2.0) -CLOSE_RECYCLE_HOLD_SECONDS = _env_float("OKX_OPTIONS_CLOSE_HOLD_SECONDS", 120.0) - -_lock = threading.Lock() -# inst_id -> {"ok_since": float|None, "recycle": float, "premium": float, "updated": float} -_gates: dict[str, dict[str, Any]] = {} - - -def _safe_float(v: Any) -> float | None: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def clear_close_gate(inst_id: str | None = None) -> None: - with _lock: - if inst_id: - _gates.pop(str(inst_id).strip(), None) - else: - _gates.clear() - - -def mark_close_gate_passed(inst_id: str) -> None: - """标记同仓已通过 2× 门控,续批平仓只验流动性.""" - inst = (inst_id or "").strip() - if not inst: - return - with _lock: - st = _gates.get(inst) or {} - st["passed"] = True - st["updated"] = time.time() - _gates[inst] = st - - -def is_close_gate_passed(inst_id: str) -> bool: - inst = (inst_id or "").strip() - if not inst: - return False - with _lock: - return bool((_gates.get(inst) or {}).get("passed")) - - -def update_close_gate( - inst_id: str, - *, - recycle_usdc: float | None, - premium_paid: float | None, - now: float | None = None, - min_mult: float | None = None, - hold_seconds: float | None = None, -) -> dict[str, Any]: - """ - 根据当前买盘可回收金额刷新门控. - 条件不满足时重置计时;满足时从首次满足起累计持续时间. - """ - inst = (inst_id or "").strip() - if not inst: - return { - "ok": False, - "ready": False, - "recycle_ok": False, - "msg": "缺少合约", - } - ts = float(now if now is not None else time.time()) - mult = float(min_mult if min_mult is not None else CLOSE_RECYCLE_MIN_MULT) - hold = float(hold_seconds if hold_seconds is not None else CLOSE_RECYCLE_HOLD_SECONDS) - if mult <= 0: - mult = 2.0 - if hold < 0: - hold = 0.0 - - prem = _safe_float(premium_paid) - recv = _safe_float(recycle_usdc) - need = round(prem * mult, 4) if prem is not None and prem > 0 else None - recycle_ok = bool( - prem is not None and prem > 0 and recv is not None and need is not None and recv + 1e-12 >= need - ) - - with _lock: - prev = _gates.get(inst) or {} - ok_since = prev.get("ok_since") - if recycle_ok: - if ok_since is None: - ok_since = ts - else: - ok_since = None - held = (ts - float(ok_since)) if ok_since is not None else 0.0 - ready = bool(recycle_ok and held + 1e-9 >= hold) - prev_passed = bool(prev.get("passed")) - passed = prev_passed or ready - state = { - "ok_since": ok_since, - "recycle": recv, - "premium": prem, - "need": need, - "updated": ts, - "min_mult": mult, - "hold_seconds": hold, - "passed": passed, - } - _gates[inst] = state - - remain = max(0.0, hold - held) if recycle_ok and not ready else None - if prem is None or prem <= 0: - msg = "缺少权利金,无法校验平仓门控" - elif recv is None: - msg = "暂无有效买盘可回收金额" - elif not recycle_ok: - msg = f"可回收 {recv:.4f} USDC < 权利金×{mult:g}({need:.4f}),目标平仓门控未过" - elif not ready: - msg = ( - f"可回收已达×{mult:g}({recv:.4f}/{need:.4f})," - f"需再持续 {remain:.0f}s(已 {held:.0f}/{hold:.0f}s)门控才通过" - ) - else: - msg = f"可回收已达×{mult:g}且持续≥{hold:.0f}s,目标触达后可按买一平仓" - - auto_blocked = not (ready or passed) - return { - "ok": True, - "ready": ready, - "passed": passed, - "recycle_ok": recycle_ok, - "recycle_usdc": recv, - "premium_paid": prem, - "need_recycle_usdc": need, - "min_mult": mult, - "hold_seconds": hold, - "held_seconds": round(held, 1) if recycle_ok else 0.0, - "remain_seconds": round(remain, 1) if remain is not None else None, - "ok_since": ok_since, - "msg": msg, - "auto_close_blocked": auto_blocked, - "close_gate_blocked": auto_blocked, - } - - -def check_close_gate( - inst_id: str, - *, - recycle_usdc: float | None = None, - premium_paid: float | None = None, - refresh: bool = True, -) -> dict[str, Any]: - """检查是否允许平仓;默认先用最新回收/权利金刷新.""" - inst = (inst_id or "").strip() - if refresh: - if recycle_usdc is None or premium_paid is None: - with _lock: - prev = _gates.get(inst) or {} - if recycle_usdc is None: - recycle_usdc = prev.get("recycle") - if premium_paid is None: - premium_paid = prev.get("premium") - return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid) - with _lock: - prev = _gates.get(inst) - if not prev: - return update_close_gate(inst, recycle_usdc=recycle_usdc, premium_paid=premium_paid) - return update_close_gate( - inst, - recycle_usdc=recycle_usdc if recycle_usdc is not None else prev.get("recycle"), - premium_paid=premium_paid if premium_paid is not None else prev.get("premium"), - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_db.py b/lib/options/options_db.py index 2d68110..f0162fb 100644 --- a/lib/options/options_db.py +++ b/lib/options/options_db.py @@ -1,134 +1,12 @@ -"""期权模块 SQLite 表.""" -from __future__ import annotations - -import sqlite3 - - -def init_options_tables(conn: sqlite3.Connection) -> None: - from lib.options.options_review_db import init_options_review_tables - - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_trades ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - inst_id TEXT NOT NULL, - underlying TEXT NOT NULL, - opt_type TEXT NOT NULL, - strike REAL, - exp_time TEXT, - sheets INTEGER NOT NULL, - eth_amount REAL NOT NULL, - open_quote REAL, - premium_paid REAL, - status TEXT DEFAULT 'open', - close_quote REAL, - premium_received REAL, - realized_pnl REAL, - profit_alert_sent INTEGER DEFAULT 0, - signal_note TEXT, - exchange_ord_id TEXT, - close_ord_id TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - closed_at TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_convert_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - from_ccy TEXT, - to_ccy TEXT, - rfq_sz REAL, - received_sz REAL, - quote_id TEXT, - status TEXT, - message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_history_hidden ( - history_key TEXT PRIMARY KEY, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_transfer_log ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ccy TEXT, - amount REAL, - from_account TEXT, - to_account TEXT, - status TEXT, - message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_target_monitors ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - inst_id TEXT NOT NULL, - underlying TEXT, - opt_type TEXT, - target_index REAL NOT NULL, - trade_id INTEGER, - sheets INTEGER, - status TEXT DEFAULT 'active', - trigger_idx REAL, - close_ord_id TEXT, - message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - triggered_at TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE INDEX IF NOT EXISTS idx_options_target_monitors_status - ON options_target_monitors(status) - """ - ) - init_options_review_tables(conn) - - -def sum_open_premium_paid(conn: sqlite3.Connection, inst_id: str) -> float | None: - """同合约所有 open 腿权利金合计(加仓后显示/门控用).""" - inst = (inst_id or "").strip() - if not inst: - return None - row = conn.execute( - """ - SELECT SUM(premium_paid) AS total, COUNT(*) AS n - FROM options_trades - WHERE inst_id = ? AND status = 'open' AND premium_paid IS NOT NULL - """, - (inst,), - ).fetchone() - if not row or int(row["n"] or 0) < 1: - return None - return round(float(row["total"] or 0), 4) - - -def sum_open_sheets(conn: sqlite3.Connection, inst_id: str) -> int | None: - """同合约所有 open 腿张数合计.""" - inst = (inst_id or "").strip() - if not inst: - return None - row = conn.execute( - """ - SELECT SUM(sheets) AS total, COUNT(*) AS n - FROM options_trades - WHERE inst_id = ? AND status = 'open' - """, - (inst,), - ).fetchone() - if not row or int(row["n"] or 0) < 1: - return None - return int(row["total"] or 0) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_history_lib.py b/lib/options/options_history_lib.py index e54d868..e9e321b 100644 --- a/lib/options/options_history_lib.py +++ b/lib/options/options_history_lib.py @@ -1,86 +1,12 @@ -"""期权历史列表(交易所 positions-history + 当前持仓).""" -from __future__ import annotations - -from typing import Any - -from lib.options.options_db import init_options_tables, sum_open_premium_paid - - -def enrich_position_row_display( - cfg: dict[str, Any], - ex: Any, - raw_pos: dict[str, Any], - *, - meta_cache: dict[str, dict[str, Any] | None] | None = None, - premium_override: float | None = None, -) -> dict[str, Any]: - from lib.exchange.okx_options_lib import format_position_row, format_usdc_amount, tick_sz_and_ct_mult - - inst_id = str(raw_pos.get("instId") or "").strip() - tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache) - row = format_position_row(raw_pos, ct_mult=ct_mult, tick_sz=tick_sz) - if premium_override is not None: - row["premium_paid"] = premium_override - row["premium_paid_fmt"] = format_usdc_amount(premium_override) - return row - - -def load_options_history(ex: Any, cfg: dict[str, Any]) -> list[dict[str, Any]]: - """与期权历史页相同的数据源:交易所全平记录 + 当前持仓,排除本地隐藏项.""" - from lib.exchange.okx_options_lib import ( - fetch_all_option_positions_history, - format_live_option_history_row, - format_option_history_row, - tick_sz_and_ct_mult, - ) - - meta_cache: dict[str, dict[str, Any] | None] = {} - items: list[dict[str, Any]] = [] - - raw_live = cfg["fetch_option_positions"](ex) - if raw_live is None: - return [] - - conn = cfg["get_db"]() - try: - init_options_tables(conn) - hidden_keys = { - str(r["history_key"]) - for r in conn.execute("SELECT history_key FROM options_history_hidden").fetchall() - } - for p in raw_live: - inst = str(p.get("instId") or "").strip() - premium_override = sum_open_premium_paid(conn, inst) if inst else None - row = enrich_position_row_display( - cfg, - ex, - p, - meta_cache=meta_cache, - premium_override=premium_override, - ) - open_ms = None - ctime = p.get("cTime") or (row.get("raw") or {}).get("cTime") - try: - if ctime is not None and str(ctime).strip(): - open_ms = int(float(ctime)) - except (TypeError, ValueError): - open_ms = None - items.append(format_live_option_history_row(row, open_ms=open_ms)) - finally: - conn.close() - - hist_raw = fetch_all_option_positions_history(ex, limit=200) - for raw in hist_raw: - inst_id = str(raw.get("instId") or "").strip() - tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache) - items.append(format_option_history_row(raw, tick_sz=tick_sz, ct_mult=ct_mult)) - - open_rows = [x for x in items if x.get("status") == "open"] - closed = [x for x in items if x.get("status") != "open"] - closed.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True) - open_rows.sort(key=lambda x: int(x.get("close_ms") or 0), reverse=True) - return [ - x - for x in (open_rows + closed) - if str(x.get("history_key") or "") not in hidden_keys - ] +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_hub_lib.py b/lib/options/options_hub_lib.py index 7854bb0..2d10096 100644 --- a/lib/options/options_hub_lib.py +++ b/lib/options/options_hub_lib.py @@ -1,104 +1,12 @@ -"""中控只读聚合:OKX 期权持仓 / 资金 / 本地统计.""" - -from __future__ import annotations - -from typing import Any - -from lib.options.options_history_lib import load_options_history -from lib.options.options_stats_lib import compute_options_stats_from_history - - -def _compute_options_stats(ex, cfg) -> dict[str, Any]: - history = load_options_history(ex, cfg) - return compute_options_stats_from_history(history) - - -def build_options_hub_snapshot(cfg: dict[str, Any]) -> dict[str, Any]: - if not cfg.get("enabled"): - return {"ok": True, "enabled": False} - ex = cfg.get("exchange_options") - ready_fn = cfg.get("options_api_ready") - if not callable(ready_fn): - return {"ok": False, "enabled": True, "msg": "期权模块未就绪"} - ok, reason = ready_fn(ex) - if not ok: - return {"ok": False, "enabled": True, "msg": reason or "期权 API 未配置"} - try: - from lib.options.options_positions_lib import build_display_option_positions - - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return {"ok": False, "enabled": True, "msg": "获取期权持仓失败"} - positions = build_display_option_positions(cfg, ex, raw) - target_monitors: list[dict[str, Any]] = [] - try: - conn = cfg["get_db"]() - try: - from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst - from lib.options.options_target_lib import list_active_targets, list_closing_targets, targets_by_inst - - target_monitors = list_active_targets(conn) + list_closing_targets(conn) - tgt_map = targets_by_inst(conn) - hedge_target_map = active_options_targets_by_inst(conn) - target_monitors.extend(hedge_target_map.values()) - for p in positions: - mon = tgt_map.get(str(p.get("inst_id") or "")) - if mon: - p["target_index"] = mon.get("target_index") - p["target_monitor_id"] = mon.get("id") - p["target_monitor"] = mon - hedge_target = hedge_target_map.get(str(p.get("inst_id") or "")) - if hedge_target: - p["hedge_plan_target"] = hedge_target - if not mon: - # 中控卡片共用 target_index 只读展示;实际平仓仍由对冲计划监控处理。 - p["target_index"] = hedge_target.get("target_index") - try: - from lib.instance.instance_dashboard_lib import ( - _format_options_target, - _resolve_options_source, - ) - - inst = str(p.get("inst_id") or "") - source_key, source_label = _resolve_options_source(conn, inst) - p["source"] = source_key - p["source_label"] = source_label - p["target_monitor_text"] = _format_options_target(p) - except Exception: - p.setdefault("source_label", "—") - p.setdefault("target_monitor_text", "—") - finally: - conn.close() - except Exception: - target_monitors = [] - from lib.options.options_positions_lib import net_pnl_from_display_row - - upl_total = 0.0 - has_upl = False - for p in positions: - # 与持仓卡「净盈亏」一致(买一回收−权利金);不用交易所标记价 upl - net = net_pnl_from_display_row(p) - if net is None: - continue - has_upl = True - upl_total += float(net) - bal = cfg["fetch_options_balances"](ex) - stats = _compute_options_stats(ex, cfg) - return { - "ok": True, - "enabled": True, - "positions": positions, - "position_count": len(positions), - "target_monitors": target_monitors, - "upl_total_usdc": round(upl_total, 4) if has_upl else None, - "balances": bal, - "funding_usdc": bal.get("funding_usdc"), - "funding_usdt": bal.get("funding_usdt"), - "trading_usdc": bal.get("trading_usdc"), - "trading_usdt": bal.get("trading_usdt"), - "stats": stats, - "trade_budget": cfg.get("trade_budget"), - "account_label": cfg.get("account_label") or "OKX期权", - } - except Exception as e: - return {"ok": False, "enabled": True, "msg": str(e)} +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_monitor_lib.py b/lib/options/options_monitor_lib.py index 55b3b17..2f1f8df 100644 --- a/lib/options/options_monitor_lib.py +++ b/lib/options/options_monitor_lib.py @@ -1,334 +1,12 @@ -"""期权持仓监控:浮盈翻倍微信提醒 + 平仓/到期状态同步.""" -from __future__ import annotations - -import sqlite3 -import time -from datetime import datetime, timezone -from typing import Any, Callable - -from lib.exchange.okx_options_lib import normalize_option_exp_ms, resolve_option_close_from_history - - -def _safe_float(v: Any) -> float | None: - if v is None: - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def build_profit_alert_message( - *, - account_label: str, - inst_id: str, - premium_paid: float, - upl: float, - upl_ratio: float | None, - bid: float | None, -) -> str: - pct = f"{upl_ratio * 100:.1f}%" if upl_ratio is not None else "—" - bid_txt = f"{bid:.4f}" if bid is not None else "—" - return "\n".join( - [ - "【OKX期权·翻倍提醒】", - f"账户:{account_label}", - f"合约:{inst_id}", - f"已付权利金:{premium_paid:.4f} USDC", - f"未实现盈亏:{upl:+.4f} USDC({pct})", - f"当前买一:{bid_txt}(可考虑限价平仓锁利)", - ] - ) - - -def run_options_profit_alerts( - conn: sqlite3.Connection, - positions: list[dict[str, Any]], - *, - profit_ratio: float, - send_wechat: Callable[[str], None], - account_label: str, - ticker_bid_fn: Callable[[str], float | None], -) -> int: - """ - 对比 DB 中 open 记录与交易所持仓;达到阈值发微信. - 返回发送条数. - """ - sent = 0 - pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions} - rows = conn.execute( - """ - SELECT id, inst_id, premium_paid, profit_alert_sent - FROM options_trades - WHERE status = 'open' - ORDER BY id ASC - """ - ).fetchall() - # 同合约多腿加仓:按合约汇总权利金,整仓只告警一次 - by_inst: dict[str, dict[str, Any]] = {} - for row in rows: - inst_id = str(row["inst_id"] or "") - if not inst_id: - continue - bucket = by_inst.setdefault( - inst_id, - {"ids": [], "premium": 0.0, "all_sent": True, "has_prem": False}, - ) - bucket["ids"].append(int(row["id"])) - prem = _safe_float(row["premium_paid"]) - if prem is not None: - bucket["premium"] += float(prem) - bucket["has_prem"] = True - if not int(row["profit_alert_sent"] or 0): - bucket["all_sent"] = False - - for inst_id, bucket in by_inst.items(): - if bucket["all_sent"] or not bucket["has_prem"] or bucket["premium"] <= 0: - continue - pos = pos_by_inst.get(inst_id) - if not pos: - continue - prem = float(bucket["premium"]) - upl = _safe_float(pos.get("upl")) - upl_ratio = _safe_float(pos.get("upl_ratio_pct")) - if upl_ratio is not None: - ratio = upl_ratio / 100.0 - elif upl is not None: - ratio = upl / prem - else: - continue - if ratio < float(profit_ratio): - continue - bid = ticker_bid_fn(inst_id) - msg = build_profit_alert_message( - account_label=account_label, - inst_id=inst_id, - premium_paid=prem, - upl=upl or 0.0, - upl_ratio=ratio, - bid=bid, - ) - try: - send_wechat(msg) - conn.execute( - f"UPDATE options_trades SET profit_alert_sent = 1 WHERE id IN ({','.join('?' * len(bucket['ids']))})", - tuple(bucket["ids"]), - ) - sent += 1 - except Exception: - pass - return sent - - -def _created_at_ms(created_at: Any) -> int | None: - if not created_at: - return None - raw = str(created_at).strip() - if not raw: - return None - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%f"): - try: - dt = datetime.strptime(raw[:26], fmt).replace(tzinfo=timezone.utc) - return int(dt.timestamp() * 1000) - except ValueError: - continue - return None - - -def sync_open_options_trades( - conn: sqlite3.Connection, - *, - live_inst_ids: set[str], - fetch_history_fn: Callable[[str], list[dict[str, Any]]], -) -> int: - """ - 交易所已无持仓时,将本地 open 记录同步为 closed. - 优先用 positions-history 回填盈亏;否则到期后按归零处理. - """ - rows = conn.execute( - """ - SELECT id, inst_id, premium_paid, exp_time, created_at - FROM options_trades - WHERE status = 'open' - """ - ).fetchall() - updated = 0 - now_ms = int(time.time() * 1000) - for row in rows: - inst_id = str(row["inst_id"] or "") - if not inst_id or inst_id in live_inst_ids: - continue - paid = _safe_float(row["premium_paid"]) or 0.0 - open_ms = _created_at_ms(row["created_at"]) - exp_ms = normalize_option_exp_ms(row["exp_time"], inst_id) - close_quote: float | None = None - prem_recv: float | None = None - realized_pnl: float | None = None - close_ord_id: str | None = None - closed_at: str | None = None - close_reason = "exchange" - - close_info = resolve_option_close_from_history( - fetch_history_fn(inst_id), - open_ms=open_ms, - ) - if close_info: - close_quote = close_info.get("close_quote") - realized_pnl = close_info.get("realized_pnl") - close_ord_id = close_info.get("pos_id") - if realized_pnl is not None: - prem_recv = round(paid + float(realized_pnl), 4) - close_ms = close_info.get("close_ms") - if close_ms: - closed_at = datetime.fromtimestamp(int(close_ms) / 1000, tz=timezone.utc).strftime( - "%Y-%m-%d %H:%M:%S" - ) - elif exp_ms is not None and now_ms >= int(exp_ms): - close_reason = "expired" - close_quote = 0.0 - prem_recv = 0.0 - realized_pnl = round(-paid, 4) - if exp_ms: - closed_at = datetime.fromtimestamp(int(exp_ms) / 1000, tz=timezone.utc).strftime( - "%Y-%m-%d %H:%M:%S" - ) - else: - continue - - conn.execute( - """ - UPDATE options_trades - SET status = 'closed', - close_quote = ?, - premium_received = ?, - realized_pnl = ?, - close_ord_id = COALESCE(?, close_ord_id), - closed_at = COALESCE(?, closed_at, CURRENT_TIMESTAMP), - signal_note = CASE - WHEN ? = 'expired' AND (signal_note IS NULL OR TRIM(signal_note) = '') - THEN '到期结算' - ELSE signal_note - END - WHERE id = ? - """, - ( - close_quote, - prem_recv, - realized_pnl, - close_ord_id, - closed_at, - close_reason, - int(row["id"]), - ), - ) - updated += 1 - return updated - - -def reconcile_live_open_trades( - conn: sqlite3.Connection, - *, - live_inst_ids: set[str], -) -> int: - """交易所有持仓但本地误标 closed 时恢复为 open.""" - fixed = 0 - for inst_id in live_inst_ids: - if not inst_id: - continue - open_row = conn.execute( - "SELECT id FROM options_trades WHERE inst_id = ? AND status = 'open' LIMIT 1", - (inst_id,), - ).fetchone() - if open_row: - continue - row = conn.execute( - """ - SELECT id, close_ord_id, realized_pnl - FROM options_trades - WHERE inst_id = ? AND status = 'closed' - ORDER BY id DESC LIMIT 1 - """, - (inst_id,), - ).fetchone() - if not row: - continue - if row["close_ord_id"]: - continue - if row["realized_pnl"] is not None: - continue - conn.execute( - """ - UPDATE options_trades - SET status = 'open', - close_quote = NULL, - premium_received = NULL, - realized_pnl = NULL, - closed_at = NULL, - signal_note = CASE - WHEN signal_note = '到期结算' THEN NULL - ELSE signal_note - END - WHERE id = ? - """, - (int(row["id"]),), - ) - fixed += 1 - return fixed - - -def options_monitor_loop( - *, - enabled: bool, - poll_seconds: float, - get_db: Callable[[], sqlite3.Connection], - fetch_positions: Callable[[], list[dict[str, Any]]], - ticker_bid_fn: Callable[[str], float | None], - send_wechat: Callable[[str], None], - account_label: str, - profit_ratio: float, - sync_trades_fn: Callable[[sqlite3.Connection], int] | None = None, - target_close_fn: Callable[[str], dict[str, Any]] | None = None, - stale_pending_fn: Callable[[], dict[str, Any]] | None = None, - stop_event: Any = None, -) -> None: - if not enabled: - return - while True: - if stop_event is not None and getattr(stop_event, "is_set", lambda: False)(): - break - try: - conn = get_db() - try: - positions = fetch_positions() - run_options_profit_alerts( - conn, - positions, - profit_ratio=profit_ratio, - send_wechat=send_wechat, - account_label=account_label, - ticker_bid_fn=ticker_bid_fn, - ) - if target_close_fn is not None: - from lib.options.options_target_lib import run_options_target_closes - - run_options_target_closes( - conn, - positions, - close_fn=target_close_fn, - send_wechat=send_wechat, - account_label=account_label, - ) - if sync_trades_fn is not None: - sync_trades_fn(conn) - conn.commit() - finally: - conn.close() - # 平仓限价挂单超时撤单(独立于 DB 事务) - if stale_pending_fn is not None: - try: - stale_pending_fn() - except Exception: - pass - except Exception: - pass - time.sleep(max(5.0, float(poll_seconds))) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_pending_lib.py b/lib/options/options_pending_lib.py index 392df02..3f436b4 100644 --- a/lib/options/options_pending_lib.py +++ b/lib/options/options_pending_lib.py @@ -1,124 +1,12 @@ -"""期权限价挂单:展示 enrichment + 超时自动撤单.""" -from __future__ import annotations - -import time -from typing import Any - - -def _safe_float(v: Any) -> float | None: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def order_age_seconds(order: dict[str, Any], *, now_ms: float | None = None) -> float | None: - """根据交易所 cTime(ms) 估算挂单时长(秒).""" - ct = _safe_float(order.get("c_time") or order.get("cTime")) - if ct is None or ct <= 0: - return None - # OKX 一般为毫秒时间戳 - if ct < 1e12: - ct *= 1000.0 - now = float(now_ms if now_ms is not None else time.time() * 1000.0) - age = (now - ct) / 1000.0 - return age if age >= 0 else 0.0 - - -def is_close_pending_order(order: dict[str, Any]) -> bool: - """平仓向限价挂单:卖出 / reduceOnly.""" - side = str(order.get("side") or "").lower() - if side == "sell": - return True - return bool(order.get("reduce_only")) - - -def enrich_pending_orders( - orders: list[dict[str, Any]] | None, - *, - ttl_seconds: float = 600.0, - now_ms: float | None = None, -) -> list[dict[str, Any]]: - """为 UI 附加挂单时长与自动撤倒计时.""" - ttl = max(0.0, float(ttl_seconds or 0)) - now = float(now_ms if now_ms is not None else time.time() * 1000.0) - out: list[dict[str, Any]] = [] - for raw in orders or []: - o = dict(raw) - age = order_age_seconds(o, now_ms=now) - is_close = is_close_pending_order(o) - o["age_sec"] = round(age, 1) if age is not None else None - o["is_close_order"] = is_close - o["auto_cancel_enabled"] = bool(is_close and ttl > 0) - if age is not None and is_close and ttl > 0: - remain = max(0.0, ttl - age) - o["ttl_seconds"] = ttl - o["expire_in_sec"] = round(remain, 1) - o["stale"] = remain <= 0 - else: - o["ttl_seconds"] = ttl if is_close else None - o["expire_in_sec"] = None - o["stale"] = False - out.append(o) - return out - - -def cancel_stale_close_pending_orders( - *, - fetch_pending: Any, - cancel_order: Any, - ttl_seconds: float = 600.0, - now_ms: float | None = None, - ex: Any = None, -) -> dict[str, Any]: - """ - 平仓限价挂单超过 ttl 自动撤销. - fetch_pending(ex) -> list; cancel_order(ex, inst_id=..., ord_id=...). - """ - ttl = float(ttl_seconds or 0) - if ttl <= 0: - return {"ok": True, "cancelled": 0, "checked": 0, "skipped": "ttl_disabled"} - try: - orders = fetch_pending(ex) if ex is not None else fetch_pending() - except TypeError: - orders = fetch_pending(ex) - except Exception as e: - return {"ok": False, "msg": str(e), "cancelled": 0, "checked": 0} - enriched = enrich_pending_orders(orders or [], ttl_seconds=ttl, now_ms=now_ms) - cancelled: list[dict[str, Any]] = [] - errors: list[str] = [] - checked = 0 - for o in enriched: - if not o.get("is_close_order"): - continue - checked += 1 - if not o.get("stale"): - continue - inst = str(o.get("inst_id") or "").strip() - oid = str(o.get("ord_id") or "").strip() - if not inst or not oid: - continue - try: - if ex is not None: - res = cancel_order(ex, inst_id=inst, ord_id=oid) - else: - res = cancel_order(inst_id=inst, ord_id=oid) - except TypeError: - res = cancel_order(ex, inst_id=inst, ord_id=oid) - except Exception as e: - errors.append(f"{oid}:{e}") - continue - if res.get("ok"): - cancelled.append({"inst_id": inst, "ord_id": oid, "age_sec": o.get("age_sec")}) - else: - errors.append(f"{oid}:{res.get('msg') or 'cancel_failed'}") - return { - "ok": True, - "cancelled": len(cancelled), - "checked": checked, - "orders": cancelled, - "errors": errors, - "ttl_seconds": ttl, - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_positions_lib.py b/lib/options/options_positions_lib.py index e94ef25..fd94d03 100644 --- a/lib/options/options_positions_lib.py +++ b/lib/options/options_positions_lib.py @@ -1,155 +1,12 @@ -"""期权持仓展示(实例页 / 中控快照共用).""" -from __future__ import annotations - -from typing import Any - -from lib.options.options_db import init_options_tables, sum_open_premium_paid -from lib.options.options_history_lib import enrich_position_row_display -from lib.options.options_close_gate_lib import clear_close_gate, is_close_gate_passed, update_close_gate -from lib.options.options_pricing_lib import estimate_close_by_bids, intrinsic_px_per_unit - - -def _safe_float(v: Any) -> float | None: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def attach_close_preview( - cfg: dict[str, Any], - ex: Any, - row: dict[str, Any], - *, - sheets: int | None = None, - premium_paid: float | None = None, -) -> dict[str, Any]: - inst_id = str(row.get("inst_id") or row.get("instId") or "").strip() - if not inst_id: - return row - ct_mult = float(row.get("ct_mult") or 0.01) - target_sheets = int(sheets) if sheets is not None else int(abs(_safe_float(row.get("pos")) or 0)) - paid = premium_paid if premium_paid is not None else _safe_float(row.get("premium_paid")) - book = cfg["fetch_option_book_depth"](ex, inst_id, 5) - row["bid_depth"] = book.get("bids") or [] - row["ask_depth"] = book.get("asks") or [] - mark_px = _safe_float(row.get("mark_px") or row.get("markPx")) - intrinsic = intrinsic_px_per_unit( - row.get("opt_type") or row.get("optType"), - _safe_float(row.get("strike") or row.get("stk")), - _safe_float(row.get("idx_px") or row.get("idxPx")), - ) - # 与实盘一致:只按买一估算本轮可平 - preview = estimate_close_by_bids( - row["bid_depth"], - target_sheets, - ct_mult=ct_mult, - premium_paid=paid, - mark_px=mark_px, - intrinsic_px=intrinsic, - max_levels=1, - ) - # 残档时不累计 2×门控;有效买一时刷新计时(仅自动平仓需要) - if preview.get("bid_invalid") or preview.get("auto_close_blocked"): - gate = update_close_gate(inst_id, recycle_usdc=None, premium_paid=paid) - preview["close_gate"] = gate - preview["close_gate_blocked"] = True - preview["close_gate_msg"] = preview.get("bid_invalid_reason") or gate.get("msg") - preview["manual_close_blocked"] = True - preview["liquidity_ok"] = False - else: - gate = update_close_gate( - inst_id, - recycle_usdc=_safe_float(preview.get("total_received")), - premium_paid=paid, - ) - passed = bool(gate.get("passed") or is_close_gate_passed(inst_id) or gate.get("ready")) - preview["close_gate"] = gate - preview["close_gate_blocked"] = not passed - preview["close_gate_msg"] = gate.get("msg") - preview["manual_close_blocked"] = False - preview["liquidity_ok"] = True - if not passed: - preview["auto_close_blocked"] = True - row["close_preview"] = preview - return row - - -def forget_close_gate_for_inst(inst_id: str) -> None: - clear_close_gate(inst_id) - - -def net_pnl_from_display_row(row: dict[str, Any]) -> float | None: - """与持仓卡「净盈亏」同口径:买一可回收 − 权利金;残档买一则无净值.""" - preview = row.get("close_preview") if isinstance(row.get("close_preview"), dict) else {} - if preview.get("bid_invalid"): - return None - net = preview.get("estimated_pnl") - if net is not None: - try: - return float(net) - except (TypeError, ValueError): - pass - recv = _safe_float(preview.get("total_received")) - paid = _safe_float(row.get("premium_paid")) - if recv is not None and paid is not None: - return round(recv - paid, 4) - return None - - -def sum_options_net_pnl_usdc( - cfg: dict[str, Any], - ex: Any, - raw_positions: list[dict[str, Any]] | None = None, -) -> float | None: - """ - 期权浮盈合计(USDC),与顶栏实时盈亏/中控口径对齐为「净盈亏」: - 各仓买一可回收 − 权利金之和.获取失败返回 None;无持仓返回 0. - """ - raw = raw_positions - if raw is None: - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return None - if not raw: - return 0.0 - positions = build_display_option_positions(cfg, ex, raw) - total = 0.0 - found = False - for p in positions: - net = net_pnl_from_display_row(p) - if net is None: - continue - found = True - total += float(net) - return round(total, 4) if found else (0.0 if not positions else None) - - -def build_display_option_positions( - cfg: dict[str, Any], - ex: Any, - raw_positions: list[dict[str, Any]], -) -> list[dict[str, Any]]: - """与实例 /api/options/positions 相同 enrichment + close_preview.""" - meta_cache: dict[str, dict[str, Any] | None] = {} - rows: list[dict[str, Any]] = [] - conn = cfg["get_db"]() - try: - init_options_tables(conn) - for p in raw_positions: - inst = str(p.get("instId") or "").strip() - premium_override = sum_open_premium_paid(conn, inst) if inst else None - row = enrich_position_row_display( - cfg, - ex, - p, - meta_cache=meta_cache, - premium_override=premium_override, - ) - attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid"))) - rows.append(row) - finally: - conn.close() - return rows +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_pricing_lib.py b/lib/options/options_pricing_lib.py index b86117d..5462ab5 100644 --- a/lib/options/options_pricing_lib.py +++ b/lib/options/options_pricing_lib.py @@ -1,543 +1,12 @@ -"""OKX USDⓈ 期权:张数与权利金计算.""" -from __future__ import annotations - -import math -from typing import Any - - -def ct_mult_from_meta(meta: dict[str, Any] | None) -> float: - if not meta: - return 0.01 - try: - return float(meta.get("ctMult") or 0.01) - except (TypeError, ValueError): - return 0.01 - - -def min_sz_from_meta(meta: dict[str, Any] | None) -> int: - if not meta: - return 1 - try: - return max(1, int(float(meta.get("minSz") or 1))) - except (TypeError, ValueError): - return 1 - - -def premium_per_sheet(quote_per_unit: float, ct_mult: float = 0.01) -> float: - """报价为每 1 ETH/BTC;每张权利金 = 报价 × ctMult.""" - return float(quote_per_unit) * float(ct_mult) - - -def format_quote_liquidity(px: float | None, sz: float | None, *, px_decimals: int = 4) -> str | None: - """盘口展示:价格/张数,如 17.2/150.""" - if px is None: - return None - try: - price = f"{float(px):.{px_decimals}f}".rstrip("0").rstrip(".") - except (TypeError, ValueError): - return None - if sz is None: - return price - try: - s = float(sz) - size = str(int(s)) if abs(s - int(s)) < 1e-9 else str(s).rstrip("0").rstrip(".") - except (TypeError, ValueError): - return price - return f"{price}/{size}" - - -def total_premium(quote_per_unit: float, eth_amount: float, ct_mult: float = 0.01) -> float: - return float(quote_per_unit) * float(eth_amount) - - -# 买一相对标记价/内在价值低于该比例 → 视为残档,禁止按买盘自动/多档平仓 -BID_CLOSE_MIN_RATIO = 0.3 - - -def _safe_px(v: Any) -> float | None: - if v is None or v == "": - return None - try: - x = float(v) - except (TypeError, ValueError): - return None - return x if x > 0 else None - - -def intrinsic_px_per_unit(opt_type: str | None, strike: float | None, index_px: float | None) -> float | None: - o = (opt_type or "").strip().upper() - if strike is None or index_px is None: - return None - try: - k = float(strike) - idx = float(index_px) - except (TypeError, ValueError): - return None - if o == "C" and idx > k: - return idx - k - if o == "P" and idx < k: - return k - idx - return None - - -def is_stub_bid_px( - bid_px: float | None, - *, - mark_px: float | None = None, - intrinsic_px: float | None = None, - min_ratio: float = BID_CLOSE_MIN_RATIO, -) -> tuple[bool, str]: - """ - 判断买一是否为无效残档(如标记 42、买一 0.2). - 返回 (is_stub, reason). - """ - bid = _safe_px(bid_px) - if bid is None: - return True, "无买一" - ref = _safe_px(mark_px) - ref_name = "标记价" - intrinsic = _safe_px(intrinsic_px) - if intrinsic is not None and (ref is None or intrinsic > ref): - ref = intrinsic - ref_name = "内在价值" - if ref is None: - return False, "" - ratio = float(min_ratio) if min_ratio and min_ratio > 0 else BID_CLOSE_MIN_RATIO - if bid < ref * ratio: - return True, f"买一{bid:g}远低于{ref_name}{ref:g},属无效残档,禁止按买盘自动平仓" - return False, "" - - -def fetch_option_mark_px(ex: Any, inst_id: str) -> float | None: - """优先 mark-price 接口,失败则 None.""" - inst_id = (inst_id or "").strip() - if not inst_id or ex is None: - return None - try: - rows = ex.public_get_public_mark_price({"instType": "OPTION", "instId": inst_id}).get("data") or [] - if rows: - return _safe_px(rows[0].get("markPx")) - except Exception: - pass - return None - - -def close_ref_prices( - *, - mark_px: float | None = None, - opt_type: str | None = None, - strike: float | None = None, - index_px: float | None = None, -) -> tuple[float | None, float | None]: - """返回 (mark_px, intrinsic_px) 供残档判断.""" - return _safe_px(mark_px), intrinsic_px_per_unit(opt_type, strike, index_px) - - -def filter_bids_for_close( - bids: list[dict[str, Any]] | None, - *, - mark_px: float | None = None, - intrinsic_px: float | None = None, - min_ratio: float = BID_CLOSE_MIN_RATIO, -) -> tuple[list[dict[str, Any]], bool, str]: - """过滤不可用于平仓的残档买盘.返回 (usable_bids, had_stub_only, reason).""" - raw = list(bids or []) - usable: list[dict[str, Any]] = [] - stub_reason = "" - for level in raw: - px = _safe_px(level.get("px") if isinstance(level, dict) else None) - stub, reason = is_stub_bid_px(px, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_ratio) - if stub: - if not stub_reason: - stub_reason = reason or "买一无效" - continue - usable.append(level) - if raw and not usable: - return [], True, stub_reason or "暂无有效买盘" - return usable, False, "" - - -def estimate_close_by_bids( - bids: list[dict[str, Any]] | None, - sheets: int | float, - *, - ct_mult: float = 0.01, - premium_paid: float | None = None, - mark_px: float | None = None, - intrinsic_px: float | None = None, - min_bid_ratio: float = BID_CLOSE_MIN_RATIO, - max_levels: int = 1, -) -> dict[str, Any]: - """按买盘估算限价卖出可收回金额;默认只估算买一(与实盘平仓一致);残档不参与.""" - target = max(0, int(float(sheets or 0))) - remaining = target - total_received = 0.0 - levels: list[dict[str, Any]] = [] - max_lv = max(1, int(max_levels or 1)) - empty = { - "levels": [], - "covered_sheets": 0, - "uncovered_sheets": target, - "total_received": 0.0, - "avg_px": None, - "estimated_pnl": None, - "estimated_pnl_ratio_pct": None, - "bid_invalid": False, - "bid_invalid_reason": None, - "auto_close_blocked": False, - "max_levels": max_lv, - } - if target <= 0 or ct_mult <= 0: - return empty - usable, stub_only, stub_reason = filter_bids_for_close( - bids, mark_px=mark_px, intrinsic_px=intrinsic_px, min_ratio=min_bid_ratio - ) - if stub_only: - out = dict(empty) - out["bid_invalid"] = True - out["bid_invalid_reason"] = stub_reason - out["auto_close_blocked"] = True - out["raw_bid_px"] = _safe_px((bids or [{}])[0].get("px")) if bids else None - return out - for i, level in enumerate(usable[:max_lv], start=1): - if remaining <= 0: - break - try: - px = float(level.get("px")) - sz = int(float(level.get("sz"))) - except (AttributeError, TypeError, ValueError): - continue - if px <= 0 or sz <= 0: - continue - take = min(remaining, sz) - eth_amount = eth_amount_from_sheets(take, ct_mult) - received = total_premium(px, eth_amount) - levels.append( - { - "level": i, - "px": px, - "available_sheets": sz, - "sheets": take, - "eth_amount": eth_amount, - "received": round(received, 4), - } - ) - total_received += received - remaining -= take - covered = target - remaining - avg_px = (total_received / eth_amount_from_sheets(covered, ct_mult)) if covered > 0 else None - # 净盈亏 = 本轮买盘可回收 − 全部权利金(买一不够时剩余张数计入 uncovered) - estimated_pnl = None - estimated_pnl_ratio_pct = None - if premium_paid is not None and covered > 0: - paid = float(premium_paid) - estimated_pnl = round(total_received - paid, 4) - if paid > 0: - estimated_pnl_ratio_pct = round(estimated_pnl / paid * 100.0, 2) - return { - "levels": levels, - "covered_sheets": covered, - "uncovered_sheets": remaining, - "total_received": round(total_received, 4), - "avg_px": round(avg_px, 4) if avg_px is not None else None, - "estimated_pnl": estimated_pnl, - "estimated_pnl_ratio_pct": estimated_pnl_ratio_pct, - "bid_invalid": False, - "bid_invalid_reason": None, - "auto_close_blocked": False, - "max_levels": max_lv, - } - - -def sheets_from_eth_amount(eth_amount: float, ct_mult: float = 0.01) -> int: - if eth_amount <= 0 or ct_mult <= 0: - return 0 - return int(math.floor(eth_amount / ct_mult + 1e-12)) - - -def eth_amount_from_sheets(sheets: int, ct_mult: float = 0.01) -> float: - return round(int(sheets) * float(ct_mult), 8) - - -def calc_order_size( - *, - quote_per_unit: float, - ct_mult: float, - min_sz: int, - budget_usdc: float | None = None, - budget_buffer: float = 0.95, - eth_amount: float | None = None, - sheets: int | None = None, - budget_cap: float | None = None, -) -> dict[str, Any]: - """ - 返回 sheets, eth_amount, total_premium. - mode: budget_full / eth_amount / sheets. - """ - if quote_per_unit <= 0: - return {"ok": False, "msg": "卖一价无效", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0} - - if sheets is not None and int(sheets) > 0: - sheets = int(sheets) - elif eth_amount is not None and eth_amount > 0: - sheets = sheets_from_eth_amount(eth_amount, ct_mult) - elif budget_usdc is not None and budget_usdc > 0: - eff = float(budget_usdc) * float(budget_buffer) - per_sheet = premium_per_sheet(quote_per_unit, ct_mult) - if per_sheet <= 0: - return {"ok": False, "msg": "无法计算单张权利金", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0} - sheets = int(math.floor(eff / per_sheet)) - else: - return {"ok": False, "msg": "请指定预算,币数量或张数", "sheets": 0, "eth_amount": 0.0, "total_premium": 0.0} - - if sheets < min_sz: - per = premium_per_sheet(quote_per_unit, ct_mult) - return { - "ok": False, - "msg": f"预算不足,无法买入 {min_sz} 张(单张约 {per:.4f} USDC)", - "sheets": sheets, - "eth_amount": eth_amount_from_sheets(sheets, ct_mult), - "total_premium": total_premium(quote_per_unit, eth_amount_from_sheets(sheets, ct_mult)), - } - - eth = eth_amount_from_sheets(sheets, ct_mult) - prem = total_premium(quote_per_unit, eth) - if budget_cap is not None and prem > float(budget_cap) + 1e-9: - return { - "ok": False, - "msg": f"权利金 {prem:.4f} 超过单笔上限 {budget_cap} USDC", - "sheets": sheets, - "eth_amount": eth, - "total_premium": prem, - } - return {"ok": True, "msg": "", "sheets": sheets, "eth_amount": eth, "total_premium": prem} - - -def is_shallow_itm( - *, - opt_type: str, - strike: float, - index_px: float, - max_dist_usd: float, -) -> bool: - o = (opt_type or "").upper() - if o == "C": - if strike >= index_px: - return False - return (index_px - strike) <= max_dist_usd - if o == "P": - if strike <= index_px: - return False - return (strike - index_px) <= max_dist_usd - return False - - -def option_moneyness(*, opt_type: str, strike: float, index_px: float) -> str: - """返回 itm / otm / atm.""" - o = (opt_type or "").upper() - if strike is None or index_px is None or index_px <= 0: - return "unknown" - atm_band = max(index_px * 0.002, 2.0) - if abs(strike - index_px) <= atm_band: - return "atm" - if o == "C": - return "itm" if strike < index_px else "otm" - if o == "P": - return "itm" if strike > index_px else "otm" - return "unknown" - - -def option_moneyness_label(moneyness: str) -> str: - return {"itm": "实值", "otm": "虚值", "atm": "平值"}.get((moneyness or "").lower(), "") - - -def expiry_breakeven_from_ask( - *, - opt_type: str, - strike: float | None, - ask_px: float | None, - mark_px: float | None = None, -) -> float | None: - """买入前预估到期平衡:权利金按卖一;无卖一时回退标记价.""" - prem = ask_px if ask_px is not None and ask_px > 0 else mark_px - return expiry_breakeven_px(opt_type=opt_type, strike=strike, avg_px=prem) - - -def expiry_breakeven_px( - *, - opt_type: str, - strike: float | None, - avg_px: float | None, - be_px_api: float | None = None, -) -> float | None: - """到期平衡点:持有至到期时标的指数盈亏为 0 的价格.优先 OKX bePx.""" - if be_px_api is not None and be_px_api > 0: - return round(float(be_px_api), 2) - if strike is None or avg_px is None: - return None - o = (opt_type or "").upper() - if o == "C": - return round(strike + avg_px, 2) - if o == "P": - return round(strike - avg_px, 2) - return None - - -def close_breakeven_idx( - *, - opt_type: str, - idx_px: float | None, - mark_px: float | None, - avg_px: float | None, - delta_pa: float | None = None, - pos: float = 0, - ct_mult: float = 0.01, -) -> float | None: - """ - 平掉回本:标的指数达到该价位时,按标记价平仓近似盈亏为 0. - 优先用 deltaPA 线性外推,否则用时间价值近似(适合短期轻度实值). - """ - if idx_px is None or mark_px is None or avg_px is None: - return None - eth_amt = abs(float(pos)) * float(ct_mult) - if eth_amt > 1e-12 and delta_pa is not None and abs(float(delta_pa)) > 1e-12: - slope = float(delta_pa) / eth_amt - return round(float(idx_px) + (float(avg_px) - float(mark_px)) / slope, 2) - o = (opt_type or "").upper() - if o == "C": - return round(float(idx_px) + float(avg_px) - float(mark_px), 2) - if o == "P": - return round(float(idx_px) + float(mark_px) - float(avg_px), 2) - return None - - -def idx_distance_to_be(idx_px: float | None, be_px: float | None) -> float | None: - """指数距平衡点(正=指数需上涨才到平衡点).""" - if idx_px is None or be_px is None: - return None - return round(float(be_px) - float(idx_px), 2) - - -def format_options_breakeven_line( - *, - expiry_be_px: float | None, - close_be_px: float | None, - idx_px: float | None = None, -) -> str: - """持仓摘要行:到期平衡 / 平掉回本.""" - parts: list[str] = [] - if expiry_be_px is not None: - parts.append(f"到期平衡{expiry_be_px:.0f}") - if close_be_px is not None: - parts.append(f"平掉回本{close_be_px:.0f}") - if idx_px is not None and parts: - return " ".join(parts) + f"(指数{idx_px:.0f})" - return " ".join(parts) - - -def estimate_expiry_value_at_index( - *, - opt_type: str, - strike: float | None, - target_idx: float | None, - eth_amount: float | None, -) -> float | None: - """到期测算:目标指数价下期权内在价值总额(不含已付权利金).""" - if strike is None or target_idx is None or eth_amount is None: - return None - if eth_amount <= 0: - return None - o = (opt_type or "").upper() - if o == "C": - intrinsic = max(0.0, float(target_idx) - float(strike)) - elif o == "P": - intrinsic = max(0.0, float(strike) - float(target_idx)) - else: - return None - return round(intrinsic * float(eth_amount), 2) - - -def estimate_expiry_profit_at_index( - *, - opt_type: str, - strike: float | None, - target_idx: float | None, - entry_px: float | None, - eth_amount: float | None, - total_premium: float | None = None, -) -> float | None: - """到期测算:目标指数价下净盈利 = 预计价值 − 权利金.""" - value = estimate_expiry_value_at_index( - opt_type=opt_type, - strike=strike, - target_idx=target_idx, - eth_amount=eth_amount, - ) - if value is None: - return None - prem = total_premium - if prem is None and entry_px is not None and eth_amount is not None: - prem = float(entry_px) * float(eth_amount) - if prem is None: - return None - return round(float(value) - float(prem), 2) - - -def equivalent_contract_leverage( - *, - index_px: float | None, - eth_amount: float | None, - total_premium: float | None, -) -> float | None: - """名义价值 / 权利金,近似相当于永续合约杠杆倍数(测算用).""" - if index_px is None or eth_amount is None or total_premium is None: - return None - if eth_amount <= 0 or total_premium <= 0: - return None - return round(float(index_px) * float(eth_amount) / float(total_premium), 1) - - -def straddle_ask_per_unit( - call_ask: float | None, - put_ask: float | None, -) -> float | None: - """跨式双买:每 1 标的币的卖一报价之和.""" - if call_ask is None or put_ask is None: - return None - if float(call_ask) <= 0 or float(put_ask) <= 0: - return None - return round(float(call_ask) + float(put_ask), 4) - - -def straddle_premium_total( - call_ask: float | None, - put_ask: float | None, - eth_amount: float | None, -) -> float | None: - """跨式双买权利金总额(USDC).""" - per = straddle_ask_per_unit(call_ask, put_ask) - if per is None or eth_amount is None or float(eth_amount) <= 0: - return None - return round(per * float(eth_amount), 2) - - -def straddle_breakeven_band( - strike: float | None, - combined_ask_per_unit: float | None, -) -> tuple[float | None, float | None]: - """跨式到期平衡带:下平衡 ~ 上平衡(按双卖一报价和).""" - if strike is None or combined_ask_per_unit is None: - return None, None - k = float(strike) - d = float(combined_ask_per_unit) - return round(k - d, 2), round(k + d, 2) - - -def format_straddle_band( - strike: float | None, - combined_ask_per_unit: float | None, -) -> str: - lo, hi = straddle_breakeven_band(strike, combined_ask_per_unit) - if lo is None or hi is None: - return "" - return f"{lo:.0f} ~ {hi:.0f}" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_register.py b/lib/options/options_register.py index 408516a..f3d6633 100644 --- a/lib/options/options_register.py +++ b/lib/options/options_register.py @@ -1,1283 +1,12 @@ -"""OKX 期权模块:Flask 路由注册.""" -from __future__ import annotations - -import os -import threading -import time -from typing import Any - -from flask import Flask, jsonify, redirect, request, url_for -from jinja2 import ChoiceLoader, FileSystemLoader - -from lib.options.options_db import init_options_tables, sum_open_premium_paid, sum_open_sheets -from lib.options.options_monitor_lib import options_monitor_loop -from lib.options.options_pricing_lib import ( - calc_order_size, - ct_mult_from_meta, - min_sz_from_meta, - premium_per_sheet, -) -from lib.exchange.okx_options_lib import ( - _safe_float, - cap_option_buy_sheets_to_ask_depth, - option_buy_liquidity_ok, - td_mode_for_option_buy, -) - - -def _env_bool(key: str, default: bool = False) -> bool: - raw = (os.getenv(key) or "").strip().lower() - if not raw: - return default - return raw in ("1", "true", "yes", "on") - - -def _env_float(key: str, default: float) -> float: - try: - return float(os.getenv(key, str(default))) - except (TypeError, ValueError): - return default - - -def attach_options_templates(app: Flask, repo_root: str) -> None: - tpl_dir = os.path.join(repo_root, "lib", "options", "templates") - if not os.path.isdir(tpl_dir): - return - existing = app.jinja_loader - loaders = [FileSystemLoader(tpl_dir)] - if existing is not None: - if isinstance(existing, ChoiceLoader): - loaders = list(existing.loaders) + loaders - else: - loaders.insert(0, existing) - app.jinja_loader = ChoiceLoader(loaders) - - -def install_options_trading(app: Flask, repo_root: str, app_module: Any) -> None: - enabled = _env_bool("OKX_OPTIONS_ENABLED", False) - attach_options_templates(app, repo_root) - cfg = _build_cfg(app_module) - app.extensions["options_cfg"] = cfg - register_options_routes(app, cfg) - _register_options_hub_bridge(app, cfg) - if enabled: - _start_monitor_thread(app, cfg) - - -def _register_options_hub_bridge(app: Flask, cfg: dict[str, Any]) -> None: - from lib.options.options_hub_lib import build_options_hub_snapshot - - def snapshot_fn(): - return build_options_hub_snapshot(cfg) - - hub_ctx = dict(app.config.get("HUB_CTX") or {}) - hub_ctx["options_snapshot_fn"] = snapshot_fn - app.config["HUB_CTX"] = hub_ctx - - -def _build_cfg(app_module: Any) -> dict[str, Any]: - from lib.exchange.okx_options_lib import ( - build_option_chain, - estimate_usdt_to_usdc, - execute_convert, - fetch_option_book_depth, - fetch_option_positions, - fetch_options_balances, - format_position_row, - options_api_ready, - cancel_option_order, - fetch_option_pending_orders, - place_option_limit_order, - place_option_market_order, - quote_option_contract, - spot_market_swap_usdt_usdc, - transfer_ccy, - transfer_main_sub_account, - ) - - return { - "enabled": _env_bool("OKX_OPTIONS_ENABLED", False), - "sub_account_name": (os.getenv("OKX_SUB_ACCOUNT_NAME") or "").strip(), - "get_db": app_module.get_db, - "login_required": app_module.login_required, - "exchange_options": getattr(app_module, "exchange_options", None), - "send_wechat": app_module.send_wechat_msg, - "render_main_page": app_module.render_main_page, - "trade_budget": _env_float("OKX_OPTIONS_TRADE_BUDGET_USDC", 10.0), - "budget_buffer": _env_float("OKX_OPTIONS_BUDGET_BUFFER", 0.95), - "default_underly": (os.getenv("OKX_OPTIONS_DEFAULT_UNDERLY") or "ETH").strip().upper(), - "max_dte_days": _env_float("OKX_OPTIONS_MAX_DTE_DAYS", 2.0), - "chain_max_dte_days": _env_float("OKX_OPTIONS_CHAIN_MAX_DTE_DAYS", 14.0), - "itm_max_dist": _env_float("OKX_OPTIONS_ITM_MAX_DIST_USD", 30.0), - "td_mode": (os.getenv("OKX_OPTIONS_TD_MODE") or "isolated").strip(), - # 市价平仓已硬关闭(忽略 env),仅买一限价 - "allow_market_close": False, - # 平仓限价挂单超时自动撤单(秒);默认 600=10 分钟,联调可设 60 - "pending_ttl_seconds": _env_float("OKX_OPTIONS_PENDING_TTL_SECONDS", 600.0), - "profit_ratio": _env_float("OKX_OPTIONS_PROFIT_ALERT_RATIO", 1.0), - "poll_seconds": _env_float("OKX_OPTIONS_POLL_SECONDS", 15.0), - "account_label": (os.getenv("OKX_OPTIONS_ACCOUNT_LABEL") or "OKX期权").strip(), - "build_option_chain": build_option_chain, - "quote_option_contract": quote_option_contract, - "fetch_option_book_depth": fetch_option_book_depth, - "place_option_limit_order": place_option_limit_order, - "place_option_market_order": place_option_market_order, - "fetch_option_pending_orders": fetch_option_pending_orders, - "cancel_option_order": cancel_option_order, - "fetch_option_positions": fetch_option_positions, - "fetch_options_balances": fetch_options_balances, - "format_position_row": format_position_row, - "estimate_usdt_to_usdc": estimate_usdt_to_usdc, - "execute_convert": execute_convert, - "transfer_ccy": transfer_ccy, - "spot_market_swap_usdt_usdc": spot_market_swap_usdt_usdc, - "transfer_main_sub_account": transfer_main_sub_account, - "options_api_ready": options_api_ready, - "app_module": app_module, - } - - -def _mark_balances_stale(cfg: dict[str, Any]) -> None: - from lib.exchange.okx_options_lib import invalidate_options_balance_cache - from lib.instance.instance_live_push_lib import notify_instance_balance_changed - - invalidate_options_balance_cache() - app_mod = cfg.get("app_module") - if app_mod is not None and hasattr(app_mod, "invalidate_account_balance_cache"): - app_mod.invalidate_account_balance_cache() - try: - notify_instance_balance_changed() - except Exception: - pass - - -def _require_options_ex(cfg: dict[str, Any]): - if not cfg.get("enabled"): - return None, "期权模块未启用,请在 .env 设置 OKX_OPTIONS_ENABLED=true 并重启 PM2" - ex = cfg.get("exchange_options") - ok, reason = cfg["options_api_ready"](ex) - if not ok: - return None, reason or "期权 API 未配置" - return ex, "" - - -def _budget_full_usdc(cfg: dict[str, Any], ex: Any) -> tuple[float | None, str]: - """交易账户 USDC 可用余额(由 calc_order_size 再乘 budget_buffer 留余量).""" - from lib.exchange.okx_options_lib import fetch_options_trading_usdc - - raw = fetch_options_trading_usdc(ex) - if raw is None or float(raw) <= 0: - return None, "交易账户 USDC 可用余额不足" - return float(raw), "" - - -def _open_premium_paid(cfg: dict[str, Any], inst_id: str) -> float | None: - conn = cfg["get_db"]() - try: - init_options_tables(conn) - return sum_open_premium_paid(conn, inst_id) - finally: - conn.close() - - -def _position_avail_sheets(pos: dict[str, Any]) -> int: - avail = _safe_float(pos.get("availPos")) - if avail is None or avail <= 0: - avail = abs(_safe_float(pos.get("pos")) or 0) - return max(0, int(avail or 0)) - - -def _find_position(rows: list[dict[str, Any]] | None, inst_id: str) -> dict[str, Any] | None: - return next((p for p in rows or [] if str(p.get("instId")) == inst_id), None) - - -def _refresh_position_avail(cfg: dict[str, Any], ex: Any, inst_id: str) -> int | None: - from lib.exchange.okx_options_lib import invalidate_option_positions_cache - - invalidate_option_positions_cache() - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return None - pos = _find_position(raw, inst_id) - if not pos: - return 0 - return _position_avail_sheets(pos) - - -def _enrich_position_row_display( - cfg: dict[str, Any], - ex: Any, - raw_pos: dict[str, Any], - *, - meta_cache: dict[str, dict[str, Any] | None] | None = None, - premium_override: float | None = None, -) -> dict[str, Any]: - from lib.options.options_history_lib import enrich_position_row_display - - return enrich_position_row_display( - cfg, - ex, - raw_pos, - meta_cache=meta_cache, - premium_override=premium_override, - ) - - -def _attach_close_preview( - cfg: dict[str, Any], - ex: Any, - row: dict[str, Any], - *, - sheets: int | None = None, - premium_paid: float | None = None, -) -> dict[str, Any]: - from lib.options.options_positions_lib import attach_close_preview - - return attach_close_preview( - cfg, - ex, - row, - sheets=sheets, - premium_paid=premium_paid, - ) - - -_OPTIONS_SYNC_LOCK = threading.Lock() -_OPTIONS_SYNC_LAST_AT = 0.0 -_OPTIONS_SYNC_INTERVAL_SEC = 15.0 - - -def _sync_options_trades( - cfg: dict[str, Any], - *, - raw_positions: list[dict[str, Any]] | None = None, - force: bool = False, -) -> None: - global _OPTIONS_SYNC_LAST_AT - ex = cfg.get("exchange_options") - if ex is None: - return - now = time.time() - with _OPTIONS_SYNC_LOCK: - if not force and now - _OPTIONS_SYNC_LAST_AT < _OPTIONS_SYNC_INTERVAL_SEC: - return - _OPTIONS_SYNC_LAST_AT = now - from lib.exchange.okx_options_lib import fetch_option_position_history - from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades - - if raw_positions is None: - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return - else: - raw = raw_positions - live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")} - - def _hist(inst_id: str): - return fetch_option_position_history(ex, inst_id) - - conn = cfg["get_db"]() - try: - init_options_tables(conn) - reconcile_live_open_trades(conn, live_inst_ids=live_ids) - sync_open_options_trades(conn, live_inst_ids=live_ids, fetch_history_fn=_hist) - conn.commit() - finally: - conn.close() - - -def register_options_routes(app: Flask, cfg: dict[str, Any]) -> None: - lr = cfg["login_required"] - - @app.route("/options/guide") - @lr - def options_trade_guide(): - """期权开平仓与监控说明(独立页).""" - from pathlib import Path - - from flask import render_template_string - - from lib.hub.hub_strategy_lib import render_markdown_html - - md_path = Path(__file__).resolve().parents[2] / "docs" / "期权开平仓与监控说明.md" - try: - md_text = md_path.read_text(encoding="utf-8") - except OSError: - md_text = "# 说明文档缺失\n\n未找到 `docs/期权开平仓与监控说明.md`." - body = render_markdown_html(md_text) - return render_template_string( - """ - - - - - - 期权开平仓与监控说明 - - - -

← 返回期权 · 对冲计划

- {{ body|safe }} - - - """, - body=body, - ) - - @app.route("/api/options/balances") - @lr - def api_options_balances(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - force = (request.args.get("force") or "").strip().lower() in ("1", "true", "yes") - scope = (request.args.get("scope") or "main").strip().lower() - bal = cfg["fetch_options_balances"]( - ex, - force=force, - scope=scope, - sub_acct=cfg.get("sub_account_name") or "", - ) - return jsonify({"ok": True, **bal, "trade_budget": cfg["trade_budget"]}) - - @app.route("/api/options/chain") - @lr - def api_options_chain(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - u = (request.args.get("underlying") or cfg["default_underly"]).upper() - try: - chain = cfg["build_option_chain"]( - ex, - u, - max_dte_days=cfg["chain_max_dte_days"], - itm_only=False, - itm_max_dist_usd=cfg["itm_max_dist"], - ) - except Exception as e: - return jsonify({"ok": False, "msg": f"加载期权链失败: {e}"}) - expiries = chain.get("expiries") or [] - chain_err = chain.get("chain_error") - if not expiries: - return jsonify( - { - "ok": False, - "msg": chain_err or "暂无到期日,请稍后点「刷新链」", - **chain, - "chain_max_dte_days": cfg["chain_max_dte_days"], - } - ) - return jsonify({"ok": True, **chain, "chain_max_dte_days": cfg["chain_max_dte_days"]}) - - @app.route("/api/options/quote") - @lr - def api_options_quote(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - inst_id = (request.args.get("inst_id") or "").strip() - if not inst_id: - return jsonify({"ok": False, "msg": "缺少 inst_id"}) - q = cfg["quote_option_contract"](ex, inst_id) - if not q.get("ok"): - return jsonify(q) - ask = q.get("ask") - ct_mult = q.get("ct_mult") or 0.01 - min_sz = q.get("min_sz") or 1 - mode = (request.args.get("mode") or "budget_full").strip() - sheet_count = None - try: - if request.args.get("sheets"): - sheet_count = int(request.args.get("sheets")) - except (TypeError, ValueError): - pass - if mode == "close_preview": - paid = _open_premium_paid(cfg, inst_id) - target = sheet_count if sheet_count is not None else 0 - return jsonify(_attach_close_preview(cfg, ex, {**q, "pos": target, "premium_paid": paid}, sheets=target, premium_paid=paid)) - budget = cfg["trade_budget"] - budget_cap = cfg["trade_budget"] - available_usdc = None - if mode == "budget_full": - budget, budget_err = _budget_full_usdc(cfg, ex) - if budget is None: - return jsonify({"ok": False, "msg": budget_err}) - budget_cap = budget - from lib.exchange.okx_options_lib import fetch_options_trading_usdc - - available_usdc = fetch_options_trading_usdc(ex) - eth_amount = None - try: - if request.args.get("eth_amount"): - eth_amount = float(request.args.get("eth_amount")) - except (TypeError, ValueError): - pass - ask = q.get("ask") - ask_sz = q.get("ask_sz") - can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz) - if not can_open: - # 合约可报价,但不可开仓:返回参考标记价供展示 - return jsonify( - { - **q, - "ok": True, - "can_open": False, - "msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入", - "quote_per_unit": None, - "premium_per_sheet": None, - "sizing": { - "ok": False, - "msg": block_msg or "暂无卖一深度,无法买入", - "sheets": 0, - "eth_amount": 0.0, - "total_premium": 0.0, - }, - "available_usdc": available_usdc, - "budget_full_usdc": budget if mode == "budget_full" else None, - } - ) - sizing = calc_order_size( - quote_per_unit=float(ask), - ct_mult=float(ct_mult), - min_sz=int(min_sz), - budget_usdc=budget if mode == "budget_full" else None, - budget_buffer=cfg["budget_buffer"], - eth_amount=eth_amount if mode == "eth_amount" else None, - sheets=sheet_count if mode == "sheets" else None, - budget_cap=budget_cap if mode in ("budget_full", "sheets", "eth_amount") else None, - ) - if sizing.get("ok"): - capped, cap_msg = cap_option_buy_sheets_to_ask_depth( - int(sizing.get("sheets") or 0), - ask_sz, - min_sz=int(min_sz), - ) - if capped is None: - sizing = { - "ok": False, - "msg": cap_msg, - "sheets": 0, - "eth_amount": 0.0, - "total_premium": 0.0, - } - elif capped < int(sizing.get("sheets") or 0): - sizing = calc_order_size( - quote_per_unit=float(ask), - ct_mult=float(ct_mult), - min_sz=int(min_sz), - sheets=capped, - budget_cap=budget_cap if mode in ("budget_full", "sheets", "eth_amount") else None, - ) - if sizing.get("ok"): - sizing["ask_depth_capped"] = True - sizing["ask_sz"] = ask_sz - sizing["msg"] = f"已按卖一深度限制为 {capped} 张" - q = _attach_close_preview( - cfg, - ex, - q, - sheets=int(sizing.get("sheets") or sheet_count or 0), - premium_paid=_open_premium_paid(cfg, inst_id), - ) - return jsonify( - { - **q, - "can_open": True, - "quote_per_unit": ask, - "premium_per_sheet": premium_per_sheet(float(ask), float(ct_mult)), - "sizing": sizing, - "available_usdc": available_usdc, - "budget_full_usdc": budget if mode == "budget_full" else None, - } - ) - - @app.route("/api/options/open", methods=["POST"]) - @lr - def api_options_open(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - inst_id = (data.get("inst_id") or "").strip() - mode = (data.get("mode") or "budget_full").strip() - signal_note = (data.get("signal_note") or "").strip() - target_index = None - raw_target = data.get("target_index") - if raw_target is not None and str(raw_target).strip() != "": - try: - target_index = float(raw_target) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "目标位无效"}) - if target_index <= 0: - return jsonify({"ok": False, "msg": "目标位无效"}) - if not inst_id: - return jsonify({"ok": False, "msg": "缺少 inst_id"}) - q = cfg["quote_option_contract"](ex, inst_id) - if not q.get("ok"): - return jsonify(q) - ask = q.get("ask") - ask_sz = q.get("ask_sz") - can_open, block_msg = option_buy_liquidity_ok(ask, ask_sz) - if not can_open: - return jsonify( - { - "ok": False, - "msg": block_msg or q.get("open_block_msg") or "暂无卖一深度,无法买入", - "can_open": False, - "mark": q.get("mark"), - "ref_ask": q.get("ref_ask"), - } - ) - ct_mult = float(q.get("ct_mult") or 0.01) - min_sz = int(q.get("min_sz") or 1) - eth_amount = None - sheet_count = None - if mode == "eth_amount": - try: - eth_amount = float(data.get("eth_amount")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "ETH 数量无效"}) - elif mode == "sheets": - try: - sheet_count = int(data.get("sheets")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "张数无效"}) - budget = cfg["trade_budget"] - budget_cap = cfg["trade_budget"] - if mode == "budget_full": - budget, budget_err = _budget_full_usdc(cfg, ex) - if budget is None: - return jsonify({"ok": False, "msg": budget_err}) - budget_cap = budget - sizing = calc_order_size( - quote_per_unit=float(ask), - ct_mult=ct_mult, - min_sz=min_sz, - budget_usdc=budget if mode == "budget_full" else None, - budget_buffer=cfg["budget_buffer"], - eth_amount=eth_amount, - sheets=sheet_count, - budget_cap=budget_cap if mode in ("budget_full", "sheets", "eth_amount") else None, - ) - if not sizing.get("ok"): - return jsonify({"ok": False, "msg": sizing.get("msg") or "张数计算失败", "sizing": sizing}) - sheets = int(sizing["sheets"]) - capped, cap_msg = cap_option_buy_sheets_to_ask_depth(sheets, ask_sz, min_sz=min_sz) - if capped is None: - return jsonify({"ok": False, "msg": cap_msg or "卖一深度不足,无法买入"}) - if capped < sheets: - sizing = calc_order_size( - quote_per_unit=float(ask), - ct_mult=ct_mult, - min_sz=min_sz, - sheets=capped, - budget_cap=budget_cap if mode in ("budget_full", "sheets", "eth_amount") else None, - ) - if not sizing.get("ok"): - return jsonify({"ok": False, "msg": sizing.get("msg") or "张数计算失败", "sizing": sizing}) - sheets = int(sizing["sheets"]) - tick_sz = q.get("tick_sz") - order = cfg["place_option_limit_order"]( - ex, - inst_id=inst_id, - side="buy", - sheets=sheets, - price=float(ask), - td_mode=td_mode_for_option_buy(cfg["td_mode"]), - tick_sz=tick_sz, - ) - if not order.get("ok"): - return jsonify(order) - conn = cfg["get_db"]() - trade_id = None - target_mon = None - try: - init_options_tables(conn) - meta = q.get("meta") or {} - u = str(meta.get("uly") or inst_id).split("-")[0] - opt_type = meta.get("optType") - cur = conn.execute( - """ - INSERT INTO options_trades - (inst_id, underlying, opt_type, strike, exp_time, sheets, eth_amount, - open_quote, premium_paid, status, signal_note, exchange_ord_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'open', ?, ?) - """, - ( - inst_id, - u, - opt_type, - q.get("strike"), - str(q.get("exp_time") or ""), - sheets, - sizing["eth_amount"], - float(ask), - sizing["total_premium"], - signal_note, - (order.get("data") or {}).get("ordId"), - ), - ) - trade_id = int(cur.lastrowid) - if target_index is not None: - from lib.options.options_target_lib import upsert_target_monitor - - target_mon = upsert_target_monitor( - conn, - inst_id=inst_id, - target_index=target_index, - underlying=u, - opt_type=str(opt_type) if opt_type else None, - trade_id=trade_id, - sheets=sheets, - ) - conn.commit() - finally: - conn.close() - from lib.exchange.okx_options_lib import invalidate_option_positions_cache - - invalidate_option_positions_cache() - _sync_options_trades(cfg, force=True) - return jsonify( - { - "ok": True, - "order": order, - "sizing": sizing, - "trade_id": trade_id, - "target_monitor": target_mon, - } - ) - - @app.route("/api/options/orders/pending") - @lr - def api_options_orders_pending(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - inst_id = (request.args.get("inst_id") or "").strip() or None - try: - orders = cfg["fetch_option_pending_orders"](ex, inst_id) - except Exception as e: - return jsonify({"ok": False, "msg": f"获取委托失败: {e}"}) - from lib.options.options_pending_lib import enrich_pending_orders - - ttl = float(cfg.get("pending_ttl_seconds") or 600.0) - enriched = enrich_pending_orders(orders, ttl_seconds=ttl) - return jsonify( - { - "ok": True, - "orders": enriched, - "count": len(enriched), - "pending_ttl_seconds": ttl, - } - ) - - @app.route("/api/options/orders/cancel", methods=["POST"]) - @lr - def api_options_orders_cancel(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - inst_id = (data.get("inst_id") or "").strip() - ord_id = (data.get("ord_id") or "").strip() - if not inst_id or not ord_id: - return jsonify({"ok": False, "msg": "缺少 inst_id 或 ord_id"}) - out = cfg["cancel_option_order"](ex, inst_id=inst_id, ord_id=ord_id) - if out.get("ok"): - from lib.exchange.okx_options_lib import invalidate_option_positions_cache - - invalidate_option_positions_cache() - # 本地未成交开仓记录标记取消,避免假 open - try: - conn = cfg["get_db"]() - try: - init_options_tables(conn) - conn.execute( - """ - UPDATE options_trades - SET status = 'cancelled', - signal_note = CASE - WHEN signal_note IS NULL OR TRIM(signal_note) = '' THEN '委托撤销' - ELSE signal_note - END, - closed_at = CURRENT_TIMESTAMP - WHERE inst_id = ? AND exchange_ord_id = ? AND status = 'open' - """, - (inst_id, ord_id), - ) - conn.commit() - finally: - conn.close() - except Exception: - pass - _sync_options_trades(cfg, force=True) - return jsonify(out), (200 if out.get("ok") else 400) - - @app.route("/api/options/positions") - @lr - def api_options_positions(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return jsonify({"ok": False, "msg": "获取期权持仓失败"}) - _sync_options_trades(cfg, raw_positions=raw) - meta_cache: dict[str, dict[str, Any] | None] = {} - conn = cfg["get_db"]() - try: - from lib.options.options_target_lib import targets_by_inst - from lib.hedge_plan.hedge_plan_db import active_options_targets_by_inst - - tgt_map = targets_by_inst(conn) - hedge_target_map = active_options_targets_by_inst(conn) - rows = [] - for p in raw: - inst = str(p.get("instId") or "").strip() - premium_override = sum_open_premium_paid(conn, inst) if inst else None - row = _enrich_position_row_display( - cfg, - ex, - p, - meta_cache=meta_cache, - premium_override=premium_override, - ) - _attach_close_preview(cfg, ex, row, premium_paid=_safe_float(row.get("premium_paid"))) - mon = tgt_map.get(inst) - if mon: - row["target_index"] = mon.get("target_index") - row["target_monitor_id"] = mon.get("id") - row["target_monitor"] = mon - hedge_target = hedge_target_map.get(inst) - if hedge_target: - row["hedge_plan_target"] = hedge_target - rows.append(row) - finally: - conn.close() - return jsonify({"ok": True, "positions": rows}) - - @app.route("/api/options/targets") - @lr - def api_options_targets(): - conn = cfg["get_db"]() - try: - from lib.options.options_target_lib import list_active_targets, list_closing_targets - - return jsonify({"ok": True, "targets": list_active_targets(conn) + list_closing_targets(conn)}) - finally: - conn.close() - - @app.route("/api/options/target", methods=["POST"]) - @lr - def api_options_target_set(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - inst_id = (data.get("inst_id") or "").strip() - if not inst_id: - return jsonify({"ok": False, "msg": "缺少 inst_id"}) - try: - target_index = float(data.get("target_index")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "目标位无效"}) - if target_index <= 0: - return jsonify({"ok": False, "msg": "目标位无效"}) - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return jsonify({"ok": False, "msg": "获取期权持仓失败"}) - pos = _find_position(raw, inst_id) - if not pos: - return jsonify({"ok": False, "msg": "未找到持仓"}) - from lib.options.options_target_lib import upsert_target_monitor - - fmt = cfg["format_position_row"](pos) - conn = cfg["get_db"]() - try: - trade = conn.execute( - """ - SELECT id, opt_type, underlying FROM options_trades - WHERE inst_id = ? AND status = 'open' - ORDER BY id DESC LIMIT 1 - """, - (inst_id,), - ).fetchone() - trade_id = int(trade["id"]) if trade else None - sheets_sum = sum_open_sheets(conn, inst_id) - sheets = sheets_sum if sheets_sum is not None else int(fmt.get("pos") or 0) - opt_type = (trade["opt_type"] if trade else None) or fmt.get("opt_type") - underlying = (trade["underlying"] if trade else None) or fmt.get("underlying") - out = upsert_target_monitor( - conn, - inst_id=inst_id, - target_index=target_index, - underlying=str(underlying) if underlying else None, - opt_type=str(opt_type) if opt_type else None, - trade_id=trade_id, - sheets=sheets, - ) - conn.commit() - return jsonify(out) - finally: - conn.close() - - @app.route("/api/options/target/cancel", methods=["POST"]) - @lr - def api_options_target_cancel(): - data = request.get_json(silent=True) or {} - inst_id = (data.get("inst_id") or "").strip() or None - monitor_id = data.get("id") - try: - mid = int(monitor_id) if monitor_id is not None and str(monitor_id).strip() != "" else None - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "监控 id 无效"}) - if not inst_id and mid is None: - return jsonify({"ok": False, "msg": "缺少 inst_id 或 id"}) - from lib.options.options_target_lib import cancel_target_monitor - - conn = cfg["get_db"]() - try: - n = cancel_target_monitor(conn, inst_id=inst_id, monitor_id=mid) - conn.commit() - return jsonify({"ok": True, "cancelled": n}) - finally: - conn.close() - - @app.route("/api/options/close", methods=["POST"]) - @lr - def api_options_close(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - inst_id = (data.get("inst_id") or "").strip() - if not inst_id: - return jsonify({"ok": False, "msg": "缺少 inst_id"}) - if data.get("market"): - return jsonify({"ok": False, "msg": "已禁用市价平仓,仅支持买一限价"}) - sheets = data.get("sheets") - try: - sheets_i = int(sheets) if sheets is not None and str(sheets).strip() != "" else None - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "张数无效"}) - from lib.options.options_close_exec_lib import close_option_by_bid1 - - # 手动买一平仓:只验有效流动性;2×门控仅用于自动/目标位平仓 - result = close_option_by_bid1( - cfg, - ex, - inst_id, - sheets=sheets_i, - require_recycle_gate=False, - ) - if result.get("ok"): - from lib.exchange.okx_options_lib import invalidate_option_positions_cache - - invalidate_option_positions_cache() - _sync_options_trades(cfg, force=True) - if result.get("fully_closed"): - try: - from lib.options.options_target_lib import cancel_target_monitor - - conn2 = cfg["get_db"]() - try: - cancel_target_monitor(conn2, inst_id=inst_id) - conn2.commit() - finally: - conn2.close() - except Exception: - pass - _mark_balances_stale(cfg) - return jsonify(result) - - @app.route("/api/options/convert/quote", methods=["POST"]) - @lr - def api_options_convert_quote(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - try: - amount = float(data.get("amount")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "数量无效"}) - return jsonify(cfg["estimate_usdt_to_usdc"](ex, amount)) - - @app.route("/api/options/convert/execute", methods=["POST"]) - @lr - def api_options_convert_execute(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - quote_id = (data.get("quote_id") or "").strip() - result = cfg["execute_convert"](ex, quote_id) - if result.get("ok"): - conn = cfg["get_db"]() - try: - init_options_tables(conn) - conn.execute( - """ - INSERT INTO options_convert_log (from_ccy, to_ccy, rfq_sz, received_sz, quote_id, status, message) - VALUES ('USDT', 'USDC', ?, ?, ?, 'ok', '') - """, - ( - data.get("rfq_sz"), - (result.get("data") or {}).get("baseSz"), - quote_id, - ), - ) - conn.commit() - finally: - conn.close() - return jsonify(result) - - @app.route("/api/options/transfer", methods=["POST"]) - @lr - def api_options_transfer(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - ccy = (data.get("ccy") or "USDC").upper() - from_acct = (data.get("from") or "funding").strip() - to_acct = (data.get("to") or "trading").strip() - try: - amount = float(data.get("amount")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "数量无效"}) - result = cfg["transfer_ccy"](ex, ccy, amount, from_acct, to_acct) - if result.get("ok"): - conn = cfg["get_db"]() - try: - init_options_tables(conn) - conn.execute( - """ - INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message) - VALUES (?, ?, ?, ?, 'ok', '') - """, - (ccy, amount, from_acct, to_acct), - ) - conn.commit() - finally: - conn.close() - _mark_balances_stale(cfg) - return jsonify(result) - - @app.route("/api/options/spot/swap", methods=["POST"]) - @lr - def api_options_spot_swap(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - direction = (data.get("direction") or "usdt_to_usdc").strip() - try: - amount = float(data.get("amount")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "数量无效"}) - result = cfg["spot_market_swap_usdt_usdc"](ex, direction=direction, amount=amount) - if result.get("ok"): - _mark_balances_stale(cfg) - return jsonify(result) - - @app.route("/api/options/cross-transfer", methods=["POST"]) - @lr - def api_options_cross_transfer(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - data = request.get_json(silent=True) or {} - ccy = (data.get("ccy") or "USDT").upper() - direction = (data.get("direction") or "sub_to_main").strip() - from_account = (data.get("from_account") or data.get("account") or "funding").strip() - to_account = (data.get("to_account") or data.get("account") or "funding").strip() - try: - amount = float(data.get("amount")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "数量无效"}) - main_to_sub = direction == "main_to_sub" - result = cfg["transfer_main_sub_account"]( - ex, - ccy=ccy, - amount=amount, - sub_acct=cfg.get("sub_account_name") or "", - main_to_sub=main_to_sub, - from_account=from_account, - to_account=to_account, - ) - if result.get("ok"): - conn = cfg["get_db"]() - try: - init_options_tables(conn) - conn.execute( - """ - INSERT INTO options_transfer_log (ccy, amount, from_account, to_account, status, message) - VALUES (?, ?, ?, ?, 'ok', ?) - """, - ( - ccy, - amount, - ("main" if main_to_sub else "sub") + ":" + from_account, - ("sub" if main_to_sub else "main") + ":" + to_account, - "cross", - ), - ) - conn.commit() - finally: - conn.close() - _mark_balances_stale(cfg) - return jsonify(result) - - @app.route("/api/options/history") - @lr - def api_options_history(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - from lib.options.options_history_lib import load_options_history - - raw_live = cfg["fetch_option_positions"](ex) - if raw_live is None: - return jsonify({"ok": False, "msg": "获取期权持仓失败"}) - history = load_options_history(ex, cfg) - live_ids = {str(x.get("inst_id") or "") for x in history if x.get("status") == "open"} - return jsonify({"ok": True, "history": history, "live_inst_ids": sorted(live_ids)}) - - @app.route("/api/options/stats") - @lr - def api_options_stats(): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - from lib.options.options_history_lib import load_options_history - from lib.options.options_positions_lib import sum_options_net_pnl_usdc - from lib.options.options_stats_lib import compute_options_stats_from_history - - raw_live = cfg["fetch_option_positions"](ex) - if raw_live is None: - return jsonify({"ok": False, "msg": "获取期权持仓失败"}) - history = load_options_history(ex, cfg) - stats = compute_options_stats_from_history(history) - open_float = sum_options_net_pnl_usdc(cfg, ex, raw_live) - net_realized = _safe_float(stats.get("net_realized_pnl")) or 0.0 - total_pnl = None - if open_float is not None: - total_pnl = round(net_realized + float(open_float), 4) - elif stats.get("total_closed"): - total_pnl = round(net_realized, 4) - return jsonify( - { - "ok": True, - **stats, - "open_float_pnl": open_float, - "total_pnl": total_pnl, - } - ) - - @app.route("/api/options/history/", methods=["DELETE"]) - @lr - def api_options_history_delete(history_key: str): - ex, err = _require_options_ex(cfg) - if ex is None: - return jsonify({"ok": False, "msg": err}) - key = (history_key or "").strip() - if not key: - return jsonify({"ok": False, "msg": "缺少 history_key"}) - data = request.get_json(silent=True) or {} - inst_id = str(data.get("inst_id") or request.args.get("inst_id") or "").strip() or None - closed_at = str(data.get("closed_at") or request.args.get("closed_at") or "").strip() or None - conn = cfg["get_db"]() - try: - init_options_tables(conn) - conn.execute( - "INSERT OR IGNORE INTO options_history_hidden (history_key) VALUES (?)", - (key,), - ) - # 同步隐藏期权复盘,避免本地已平记录刷新后又出现 - try: - from lib.options.options_review_lib import hide_review_keys - - hide_review_keys( - conn, - history_key=key, - inst_id=inst_id, - closed_at=closed_at, - ) - if inst_id: - # 去掉已导入的复盘快照(按合约+平仓时间) - if closed_at: - rows = conn.execute( - """ - SELECT id, history_key FROM options_review_trades - WHERE inst_id = ? - AND substr(COALESCE(closed_at,''),1,16) = substr(?,1,16) - """, - (inst_id, closed_at), - ).fetchall() - else: - rows = conn.execute( - """ - SELECT id, history_key FROM options_review_trades - WHERE inst_id = ? - """, - (inst_id,), - ).fetchall() - for r in rows: - conn.execute( - "DELETE FROM options_review_entries WHERE trade_id=?", - (int(r["id"]),), - ) - conn.execute( - "DELETE FROM options_review_trades WHERE id=?", - (int(r["id"]),), - ) - conn.execute( - "INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) VALUES (?,?,?)", - (str(r["history_key"]), inst_id, (closed_at or "")[:19] or None), - ) - fps = [] - if closed_at: - fps.append(f"inst_close:{inst_id}:{closed_at[:16]}") - fps.append(f"inst:{inst_id}") - for fp in fps: - conn.execute( - "INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) VALUES (?,?,?)", - (fp, inst_id, (closed_at or "")[:19] or None), - ) - except Exception: - pass - conn.commit() - finally: - conn.close() - return jsonify({"ok": True}) - - -def _start_monitor_thread(app: Flask, cfg: dict[str, Any]) -> None: - if app.extensions.get("options_monitor_started"): - return - app.extensions["options_monitor_started"] = True - - def _bid(inst_id: str) -> float | None: - ex = cfg.get("exchange_options") - if ex is None: - return None - try: - q = cfg["quote_option_contract"](ex, inst_id) - return q.get("bid") - except Exception: - return None - - def _positions(): - ex = cfg.get("exchange_options") - if ex is None: - return [] - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return [] - return [cfg["format_position_row"](p) for p in raw] - - def _sync(conn): - from lib.exchange.okx_options_lib import fetch_option_position_history - from lib.options.options_monitor_lib import reconcile_live_open_trades, sync_open_options_trades - - ex = cfg.get("exchange_options") - if ex is None: - return 0 - raw = cfg["fetch_option_positions"](ex) - if raw is None: - return 0 - live_ids = {str(p.get("instId") or "") for p in raw if str(p.get("instId") or "")} - reconcile_live_open_trades(conn, live_inst_ids=live_ids) - return sync_open_options_trades( - conn, - live_inst_ids=live_ids, - fetch_history_fn=lambda inst_id: fetch_option_position_history(ex, inst_id), - ) - - def _target_close(inst_id: str) -> dict[str, Any]: - from lib.options.options_target_lib import close_option_by_bid_depth - - ex = cfg.get("exchange_options") - if ex is None: - return {"ok": False, "msg": "期权 exchange 未就绪"} - result = close_option_by_bid_depth(cfg, ex, inst_id) - if result.get("ok"): - try: - _sync_options_trades(cfg, force=True) - except Exception: - pass - try: - _mark_balances_stale(cfg) - except Exception: - pass - return result - - def _stale_pending() -> dict[str, Any]: - from lib.exchange.okx_options_lib import invalidate_option_positions_cache - from lib.options.options_pending_lib import cancel_stale_close_pending_orders - - ex = cfg.get("exchange_options") - if ex is None: - return {"ok": False, "msg": "期权 exchange 未就绪"} - ttl = float(cfg.get("pending_ttl_seconds") or 600.0) - out = cancel_stale_close_pending_orders( - fetch_pending=lambda _ex: cfg["fetch_option_pending_orders"](_ex), - cancel_order=lambda _ex, inst_id, ord_id: cfg["cancel_option_order"]( - _ex, inst_id=inst_id, ord_id=ord_id - ), - ttl_seconds=ttl, - ex=ex, - ) - if out.get("cancelled"): - try: - invalidate_option_positions_cache() - except Exception: - pass - try: - send = cfg.get("send_wechat") - if callable(send): - parts = [ - "【OKX期权·挂单超时撤销】", - f"账户:{cfg.get('account_label') or 'OKX期权'}", - f"超时:{ttl:g}s", - f"撤销:{out.get('cancelled')} 笔", - ] - for o in out.get("orders") or []: - parts.append(f"- {o.get('inst_id')} #{o.get('ord_id')}") - send("\n".join(parts)) - except Exception: - pass - return out - - t = threading.Thread( - target=options_monitor_loop, - kwargs={ - "enabled": True, - "poll_seconds": cfg["poll_seconds"], - "get_db": cfg["get_db"], - "fetch_positions": _positions, - "ticker_bid_fn": _bid, - "send_wechat": cfg["send_wechat"], - "account_label": cfg["account_label"], - "profit_ratio": cfg["profit_ratio"], - "sync_trades_fn": _sync, - "target_close_fn": _target_close, - "stale_pending_fn": _stale_pending, - }, - daemon=True, - name="options-monitor", - ) - t.start() +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_review_db.py b/lib/options/options_review_db.py index 7123a1d..369509a 100644 --- a/lib/options/options_review_db.py +++ b/lib/options/options_review_db.py @@ -1,143 +1,12 @@ -"""期权复盘(含对冲) SQLite 表.""" -from __future__ import annotations - -import sqlite3 - - -SOURCE_OPTION = "option_spot" -SOURCE_PERP_OPTIONS = "perp_options" -SOURCE_OPTIONS_OPTIONS = "options_options" -SOURCE_TYPES = (SOURCE_OPTION, SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS) - - -def init_options_review_tables(conn: sqlite3.Connection) -> None: - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_review_trades ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - source_type TEXT NOT NULL, - history_key TEXT NOT NULL UNIQUE, - underlying TEXT, - opened_at TEXT, - closed_at TEXT, - hold_seconds INTEGER, - realized_pnl_total REAL, - status_raw TEXT, - synced_at TEXT, - -- 纯期权 - pos_id TEXT, - inst_id TEXT, - opt_type TEXT, - strike REAL, - exp_time TEXT, - sheets INTEGER, - open_avg REAL, - close_avg REAL, - premium_paid REAL, - realized_pnl REAL, - -- 对冲计划 - hedge_plan_id INTEGER, - plan_close_reason TEXT, - realized_pnl_perp REAL, - realized_pnl_options REAL, - premium_total REAL, - direction TEXT, - tp REAL, - sl REAL, - target_price REAL, - target_price_up REAL, - target_price_down REAL, - legs_json TEXT, - -- 双计防护:纯期权腿已归属对冲计划 - linked_hedge_plan_id INTEGER, - excluded_as_hedge_leg INTEGER DEFAULT 0 - ) - """ - ) - conn.execute( - """ - CREATE UNIQUE INDEX IF NOT EXISTS idx_options_review_trades_history_key - ON options_review_trades(history_key) - """ - ) - conn.execute( - """ - CREATE UNIQUE INDEX IF NOT EXISTS idx_options_review_trades_hedge_plan - ON options_review_trades(hedge_plan_id) - WHERE hedge_plan_id IS NOT NULL - """ - ) - conn.execute( - """ - CREATE INDEX IF NOT EXISTS idx_options_review_trades_closed - ON options_review_trades(closed_at) - """ - ) - conn.execute( - """ - CREATE INDEX IF NOT EXISTS idx_options_review_trades_source - ON options_review_trades(source_type) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_review_entries ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - trade_id INTEGER NOT NULL UNIQUE, - strategy_tag TEXT, - direction_view TEXT, - entry_logic TEXT, - exit_reason TEXT, - followed_plan TEXT, - mistake_tags TEXT, - result_tag TEXT, - note TEXT, - images_json TEXT, - image TEXT, - reviewed_at TEXT, - updated_at TEXT, - FOREIGN KEY(trade_id) REFERENCES options_review_trades(id) - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_review_sync_state ( - key TEXT PRIMARY KEY, - value TEXT, - updated_at TEXT - ) - """ - ) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_review_hidden ( - history_key TEXT PRIMARY KEY, - inst_id TEXT, - closed_at TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE INDEX IF NOT EXISTS idx_options_review_hidden_inst - ON options_review_hidden(inst_id, closed_at) - """ - ) - _ensure_column(conn, "options_review_trades", "linked_hedge_plan_id", "INTEGER") - _ensure_column(conn, "options_review_trades", "excluded_as_hedge_leg", "INTEGER DEFAULT 0") - _ensure_column(conn, "options_review_trades", "target_price_up", "REAL") - _ensure_column(conn, "options_review_trades", "target_price_down", "REAL") - - -def _ensure_column(conn: sqlite3.Connection, table: str, col: str, typedef: str) -> None: - rows = conn.execute(f"PRAGMA table_info({table})").fetchall() - names: set[str] = set() - for r in rows: - try: - names.add(str(r["name"])) - except (TypeError, KeyError, IndexError): - names.add(str(r[1])) - if col not in names: - conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {typedef}") +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_review_images_lib.py b/lib/options/options_review_images_lib.py index b1845a8..d6fdef8 100644 --- a/lib/options/options_review_images_lib.py +++ b/lib/options/options_review_images_lib.py @@ -1,138 +1,12 @@ -"""期权复盘截图:独立命名空间,与合约同款四周期 5m/15m/1h/4h.""" -from __future__ import annotations - -import json -import os -import re -from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence - -OPTIONS_REVIEW_UPLOAD_TFS: tuple[str, ...] = ("5m", "15m", "1h", "4h") -OPTIONS_REVIEW_ALLOWED_EXT = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp"}) -_DRAFT_ID_RE = re.compile(r"^[a-f0-9]{32}$") -_SLOT_FILE_RE = re.compile( - r"^options_journal_([a-f0-9]{32})_(5m|15m|1h|4h)\.(png|jpg|jpeg|webp|gif|bmp)$", - re.I, -) - - -def normalize_options_review_draft_id(raw: Any) -> Optional[str]: - s = str(raw or "").strip().lower() - if _DRAFT_ID_RE.match(s): - return s - return None - - -def _safe_ext(filename: str) -> str: - ext = os.path.splitext(str(filename or ""))[1].lower() - return ext if ext in OPTIONS_REVIEW_ALLOWED_EXT else ".png" - - -def options_review_upload_dir(base_upload_folder: str) -> str: - """独立子目录 static/images/options_journal.""" - base = os.path.abspath(base_upload_folder or "") - path = os.path.join(base, "options_journal") - os.makedirs(path, exist_ok=True) - return path - - -def build_options_review_slot_filename( - draft_id: str, - tf: str, - ext: str, - *, - secure_filename_fn: Callable[[str], str], -) -> str: - ext = ext if ext.startswith(".") else f".{ext}" - ext = _safe_ext(f"x{ext}") - fname = secure_filename_fn(f"options_journal_{draft_id}_{tf}{ext}") - return fname or "" - - -def is_valid_options_review_file(filename: str, draft_id: str, tf: str) -> bool: - fn = os.path.basename(str(filename or "").strip()) - if not fn or fn != str(filename or "").strip(): - return False - m = _SLOT_FILE_RE.match(fn) - if not m: - return False - return m.group(1) == draft_id.lower() and m.group(2) == tf - - -def save_options_review_slot_file( - file, - draft_id: str, - tf: str, - upload_folder: str, - *, - secure_filename_fn: Callable[[str], str], -) -> Optional[Dict[str, str]]: - if tf not in OPTIONS_REVIEW_UPLOAD_TFS or not draft_id or not upload_folder: - return None - if not file or not getattr(file, "filename", None): - return None - ext = _safe_ext(file.filename) - fname = build_options_review_slot_filename( - draft_id, tf, ext, secure_filename_fn=secure_filename_fn - ) - if not fname: - return None - os.makedirs(upload_folder, exist_ok=True) - path = os.path.join(upload_folder, fname) - file.save(path) - return {"tf": tf, "file": fname} - - -def parse_options_review_images_json(raw: Any) -> List[Dict[str, str]]: - if not raw: - return [] - if isinstance(raw, list): - data = raw - else: - try: - data = json.loads(str(raw)) - except (TypeError, ValueError, json.JSONDecodeError): - return [] - if not isinstance(data, list): - return [] - out: List[Dict[str, str]] = [] - for item in data: - if not isinstance(item, dict): - continue - tf = str(item.get("tf") or "").strip() - file = str(item.get("file") or "").strip() - if file: - out.append({"tf": tf, "file": file}) - return out - - -def images_json_dumps(items: Sequence[Mapping[str, str]]) -> Optional[str]: - if not items: - return None - return json.dumps(list(items), ensure_ascii=False, separators=(",", ":")) - - -def options_review_image_paths(row: Any, upload_folder: str) -> List[str]: - upload_folder = os.path.abspath(upload_folder or "") - paths: List[str] = [] - seen: set[str] = set() - - def _add(name: Optional[str]) -> None: - if not name: - return - p = os.path.abspath(os.path.join(upload_folder, str(name).strip())) - if os.path.isfile(p) and p not in seen: - seen.add(p) - paths.append(p) - - try: - keys = row.keys() if hasattr(row, "keys") else () - except Exception: - keys = () - images = parse_options_review_images_json( - row["images_json"] if "images_json" in keys else getattr(row, "images_json", None) - ) - for item in images: - _add(item.get("file")) - if "image" in keys or hasattr(row, "image"): - _add(row["image"] if "image" in keys else getattr(row, "image", None)) - return paths +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_review_lib.py b/lib/options/options_review_lib.py index 43f4b60..0294ad9 100644 --- a/lib/options/options_review_lib.py +++ b/lib/options/options_review_lib.py @@ -1,971 +1,12 @@ -"""期权复盘业务:OKX 已平期权导入 + 已结束对冲计划导入 + 复盘 CRUD + 统计.""" -from __future__ import annotations - -import json -import sqlite3 -from datetime import datetime -from typing import Any, Callable, Optional - -from lib.options.options_review_db import ( - SOURCE_OPTION, - SOURCE_OPTIONS_OPTIONS, - SOURCE_PERP_OPTIONS, - SOURCE_TYPES, - init_options_review_tables, -) -from lib.options.options_review_images_lib import ( - images_json_dumps, - parse_options_review_images_json, -) - -SOURCE_LABELS = { - SOURCE_OPTION: "纯期权", - SOURCE_PERP_OPTIONS: "永期对冲", - SOURCE_OPTIONS_OPTIONS: "期期对冲", -} - -HOLD_BUCKETS = ( - ("0-1h", 0, 3600), - ("1-6h", 3600, 6 * 3600), - ("6-24h", 6 * 3600, 24 * 3600), - ("1-3d", 24 * 3600, 3 * 24 * 3600), - (">3d", 3 * 24 * 3600, None), -) - - -def _now_str() -> str: - return datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - -def _parse_ts(raw: Any) -> Optional[datetime]: - if raw is None or raw == "": - return None - s = str(raw).strip().replace(" ", "T", 1) - try: - return datetime.fromisoformat(s) - except (TypeError, ValueError): - return None - - -def _hold_seconds(opened_at: Any, closed_at: Any) -> Optional[int]: - start = _parse_ts(opened_at) - end = _parse_ts(closed_at) - if start is None or end is None: - return None - sec = int((end - start).total_seconds()) - return sec if sec >= 0 else None - - -def _safe_float(v: Any) -> Optional[float]: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def get_sync_state(conn: sqlite3.Connection, key: str) -> Optional[str]: - row = conn.execute( - "SELECT value FROM options_review_sync_state WHERE key=?", (key,) - ).fetchone() - return str(row["value"]) if row and row["value"] is not None else None - - -def set_sync_state(conn: sqlite3.Connection, key: str, value: str) -> None: - conn.execute( - """ - INSERT INTO options_review_sync_state(key, value, updated_at) - VALUES (?, ?, ?) - ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at - """, - (key, value, _now_str()), - ) - - -def _purge_review_trade_by_key(conn: sqlite3.Connection, history_key: str) -> bool: - """删除已导入的复盘快照(含复盘内容).""" - key = str(history_key or "").strip() - if not key: - return False - existing = conn.execute( - "SELECT id FROM options_review_trades WHERE history_key=?", (key,) - ).fetchone() - if not existing: - return False - tid = int(existing["id"]) - conn.execute("DELETE FROM options_review_entries WHERE trade_id=?", (tid,)) - conn.execute("DELETE FROM options_review_trades WHERE id=?", (tid,)) - return True - - -def upsert_option_history_row(conn: sqlite3.Connection, row: dict[str, Any]) -> str: - """幂等写入纯期权快照;不触碰 options_review_entries;已隐藏的不再导入.""" - history_key = str(row.get("history_key") or "").strip() - if not history_key: - return "skip" - if is_review_hidden( - conn, - history_key, - inst_id=str(row.get("inst_id") or "").strip() or None, - closed_at=row.get("closed_at") or row.get("created_at"), - ): - # 若此前已导入,清掉,避免列表残留 - return "purged" if _purge_review_trade_by_key(conn, history_key) else "hidden" - opened_at = row.get("created_at") or row.get("opened_at") - closed_at = row.get("closed_at") - pnl = _safe_float(row.get("realized_pnl")) - hold = _hold_seconds(opened_at, closed_at) - existing = conn.execute( - "SELECT id FROM options_review_trades WHERE history_key=?", (history_key,) - ).fetchone() - fields = { - "source_type": SOURCE_OPTION, - "history_key": history_key, - "underlying": str(row.get("underlying") or "").strip() or None, - "opened_at": opened_at, - "closed_at": closed_at, - "hold_seconds": hold, - "realized_pnl_total": pnl, - "status_raw": str(row.get("status_label") or row.get("status") or "closed"), - "synced_at": _now_str(), - "pos_id": str(row.get("pos_id") or "").strip() or None, - "inst_id": str(row.get("inst_id") or "").strip() or None, - "opt_type": str(row.get("opt_type") or "").strip() or None, - "strike": _safe_float(row.get("strike")), - "exp_time": str(row.get("exp_time") or "").strip() or None, - "sheets": int(row.get("sheets") or 0) or None, - "open_avg": _safe_float(row.get("open_avg_px") if row.get("open_avg_px") is not None else row.get("open_avg")), - "close_avg": _safe_float(row.get("close_avg_px") if row.get("close_avg_px") is not None else row.get("close_avg")), - "premium_paid": _safe_float(row.get("premium_paid")), - "realized_pnl": pnl, - } - cols = list(fields.keys()) - if existing: - sets = ", ".join(f"{c}=?" for c in cols if c != "history_key") - vals = [fields[c] for c in cols if c != "history_key"] - conn.execute( - f"UPDATE options_review_trades SET {sets} WHERE history_key=?", - [*vals, history_key], - ) - return "updated" - placeholders = ",".join(["?"] * len(cols)) - conn.execute( - f"INSERT INTO options_review_trades ({','.join(cols)}) VALUES ({placeholders})", - [fields[c] for c in cols], - ) - return "inserted" - - -def _close_fingerprint(inst_id: Any, closed_at: Any) -> str | None: - inst = str(inst_id or "").strip() - if not inst: - return None - closed = str(closed_at or "").strip() - if not closed: - return f"inst:{inst}" - # 精确到分钟,避免秒差导致漏匹配 - return f"inst_close:{inst}:{closed[:16]}" - - -def is_review_hidden( - conn: sqlite3.Connection, - history_key: str, - *, - inst_id: str | None = None, - closed_at: Any = None, -) -> bool: - init_options_review_tables(conn) - key = str(history_key or "").strip() - if key and conn.execute( - "SELECT 1 FROM options_review_hidden WHERE history_key=? LIMIT 1", (key,) - ).fetchone(): - return True - fp = _close_fingerprint(inst_id, closed_at) - if fp and conn.execute( - "SELECT 1 FROM options_review_hidden WHERE history_key=? LIMIT 1", (fp,) - ).fetchone(): - return True - # 期权历史页删除:options_history_hidden,按合约指纹或原 key - try: - if key and conn.execute( - "SELECT 1 FROM options_history_hidden WHERE history_key=? LIMIT 1", (key,) - ).fetchone(): - return True - if fp and conn.execute( - "SELECT 1 FROM options_history_hidden WHERE history_key=? LIMIT 1", (fp,) - ).fetchone(): - return True - # 仅隐藏了 ex:posId 时,用合约+平仓时间在历史隐藏表无直接命中; - # 若指纹已写入 options_review_hidden(新删除路径)上面已覆盖. - # 兼容:inst 级隐藏 - if inst_id: - inst_fp = f"inst:{str(inst_id).strip()}" - if conn.execute( - "SELECT 1 FROM options_review_hidden WHERE history_key=? LIMIT 1", - (inst_fp,), - ).fetchone(): - return True - if conn.execute( - "SELECT 1 FROM options_history_hidden WHERE history_key=? LIMIT 1", - (inst_fp,), - ).fetchone(): - return True - except Exception: - pass - return False - - -def hide_review_keys( - conn: sqlite3.Connection, - *, - history_key: str, - inst_id: str | None = None, - closed_at: Any = None, -) -> None: - init_options_review_tables(conn) - keys = [str(history_key or "").strip()] - fp = _close_fingerprint(inst_id, closed_at) - if fp: - keys.append(fp) - for k in keys: - if not k: - continue - conn.execute( - """ - INSERT OR IGNORE INTO options_review_hidden(history_key, inst_id, closed_at) - VALUES (?, ?, ?) - """, - (k, (inst_id or None), str(closed_at or "")[:19] or None), - ) - try: - conn.execute( - "INSERT OR IGNORE INTO options_history_hidden(history_key) VALUES (?)", - (k,), - ) - except Exception: - pass - - -def hide_review_trade(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any]: - """从复盘列表删除并持久隐藏,刷新本地源也不会再回来.""" - init_options_review_tables(conn) - row = conn.execute( - "SELECT * FROM options_review_trades WHERE id=?", (int(trade_id),) - ).fetchone() - if not row: - return {"ok": False, "msg": "记录不存在"} - d = _row_to_dict(row) - hide_review_keys( - conn, - history_key=str(d.get("history_key") or ""), - inst_id=str(d.get("inst_id") or "").strip() or None, - closed_at=d.get("closed_at") or d.get("opened_at"), - ) - entry = conn.execute( - "SELECT * FROM options_review_entries WHERE trade_id=?", (int(trade_id),) - ).fetchone() - conn.execute("DELETE FROM options_review_entries WHERE trade_id=?", (int(trade_id),)) - conn.execute("DELETE FROM options_review_trades WHERE id=?", (int(trade_id),)) - return {"ok": True, "entry": _row_to_dict(entry) if entry else None, "history_key": d.get("history_key")} - - - -def sync_options_from_local_trades(conn: sqlite3.Connection) -> dict[str, Any]: - """从本地 options_trades 已平仓记录导入复盘快照(不访问交易所).""" - init_options_review_tables(conn) - from lib.options.options_db import init_options_tables - - init_options_tables(conn) - rows = conn.execute( - """ - SELECT id, inst_id, underlying, opt_type, strike, exp_time, sheets, - open_quote, close_quote, premium_paid, realized_pnl, - created_at, closed_at, signal_note, status - FROM options_trades - WHERE status = 'closed' - ORDER BY id DESC - LIMIT 500 - """ - ).fetchall() - inserted = updated = skipped = 0 - for r in rows: - trade_id = int(r["id"]) - history_key = f"local_opt:{trade_id}" - pnl = _safe_float(r["realized_pnl"]) - opened_at = r["created_at"] - closed_at = r["closed_at"] - action = upsert_option_history_row( - conn, - { - "history_key": history_key, - "pos_id": f"local:{trade_id}", - "inst_id": r["inst_id"], - "underlying": r["underlying"], - "opt_type": r["opt_type"], - "strike": r["strike"], - "exp_time": r["exp_time"], - "sheets": r["sheets"], - "open_avg_px": r["open_quote"], - "close_avg_px": r["close_quote"], - "premium_paid": r["premium_paid"], - "realized_pnl": pnl, - "created_at": opened_at, - "closed_at": closed_at, - "status_label": "已平", - }, - ) - if action == "inserted": - inserted += 1 - elif action == "updated": - updated += 1 - else: - skipped += 1 - set_sync_state(conn, "options_last_sync_at", _now_str()) - set_sync_state(conn, "options_last_count", str(len(rows))) - set_sync_state(conn, "options_sync_source", "local") - return { - "ok": True, - "source": "local", - "fetched": len(rows), - "inserted": inserted, - "updated": updated, - "skipped": skipped, - } - - -def sync_options_from_exchange( - conn: sqlite3.Connection, - ex: Any, - *, - limit: int = 500, - fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None, - format_fn: Optional[Callable[..., dict[str, Any]]] = None, -) -> dict[str, Any]: - """从 OKX positions-history 导入已全平期权仓位(可选,默认不用).""" - init_options_review_tables(conn) - from lib.exchange.okx_options_lib import ( - fetch_all_option_positions_history, - format_option_history_row, - tick_sz_and_ct_mult, - ) - - fetch = fetch_fn or fetch_all_option_positions_history - fmt = format_fn or format_option_history_row - raw_rows = fetch(ex, limit=limit) - meta_cache: dict[str, dict[str, Any] | None] = {} - inserted = updated = skipped = 0 - for raw in raw_rows: - inst_id = str(raw.get("instId") or "").strip() - tick_sz, ct_mult = None, 0.01 - try: - tick_sz, ct_mult = tick_sz_and_ct_mult(ex, inst_id, meta_cache) - except Exception: - pass - formatted = fmt(raw, tick_sz=tick_sz, ct_mult=ct_mult) - action = upsert_option_history_row(conn, formatted) - if action == "inserted": - inserted += 1 - elif action == "updated": - updated += 1 - else: - skipped += 1 - set_sync_state(conn, "options_last_sync_at", _now_str()) - set_sync_state(conn, "options_last_count", str(len(raw_rows))) - set_sync_state(conn, "options_sync_source", "exchange") - return { - "ok": True, - "source": "exchange", - "fetched": len(raw_rows), - "inserted": inserted, - "updated": updated, - "skipped": skipped, - } - - -def _legs_json_from_plan(legs: list[dict[str, Any]]) -> str: - slim = [] - for leg in legs: - slim.append( - { - "id": leg.get("id"), - "leg_role": leg.get("leg_role"), - "symbol": leg.get("symbol"), - "inst_id": leg.get("inst_id"), - "opt_type": leg.get("opt_type"), - "strike": leg.get("strike"), - "side": leg.get("side"), - "size": leg.get("size"), - "avg_open": leg.get("avg_open"), - "premium": leg.get("premium"), - "status": leg.get("status"), - "realized_pnl": leg.get("realized_pnl"), - "close_reason": leg.get("close_reason"), - "opened_at": leg.get("opened_at"), - "closed_at": leg.get("closed_at"), - } - ) - return json.dumps(slim, ensure_ascii=False, separators=(",", ":")) - - -def upsert_hedge_plan_row( - conn: sqlite3.Connection, - plan: dict[str, Any], - legs: list[dict[str, Any]], -) -> str: - plan_id = int(plan["id"]) - history_key = f"hedge:{plan_id}" - plan_type = str(plan.get("plan_type") or "").strip() - if plan_type not in (SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS): - return "skip" - opened_at = plan.get("opened_at") or plan.get("created_at") - closed_at = plan.get("closed_at") - if is_review_hidden( - conn, - history_key, - inst_id=None, - closed_at=closed_at, - ): - return "purged" if _purge_review_trade_by_key(conn, history_key) else "hidden" - total = _safe_float(plan.get("realized_pnl_total")) - hold = _hold_seconds(opened_at, closed_at) - fields = { - "source_type": plan_type, - "history_key": history_key, - "underlying": str(plan.get("underlying") or "").strip() or None, - "opened_at": opened_at, - "closed_at": closed_at, - "hold_seconds": hold, - "realized_pnl_total": total, - "status_raw": str(plan.get("status") or "closed"), - "synced_at": _now_str(), - "hedge_plan_id": plan_id, - "plan_close_reason": str(plan.get("close_reason") or "").strip() or None, - "realized_pnl_perp": _safe_float(plan.get("realized_pnl_perp")), - "realized_pnl_options": _safe_float(plan.get("realized_pnl_options")), - "premium_total": _safe_float(plan.get("premium_total")), - "direction": str(plan.get("direction") or "").strip() or None, - "tp": _safe_float(plan.get("tp")), - "sl": _safe_float(plan.get("sl")), - "target_price": _safe_float(plan.get("target_price")), - "target_price_up": _safe_float(plan.get("target_price_up")), - "target_price_down": _safe_float(plan.get("target_price_down")), - "legs_json": _legs_json_from_plan(legs), - } - existing = conn.execute( - "SELECT id FROM options_review_trades WHERE history_key=?", (history_key,) - ).fetchone() - cols = list(fields.keys()) - if existing: - sets = ", ".join(f"{c}=?" for c in cols if c != "history_key") - vals = [fields[c] for c in cols if c != "history_key"] - conn.execute( - f"UPDATE options_review_trades SET {sets} WHERE history_key=?", - [*vals, history_key], - ) - trade_id = int(existing["id"]) - action = "updated" - else: - placeholders = ",".join(["?"] * len(cols)) - cur = conn.execute( - f"INSERT INTO options_review_trades ({','.join(cols)}) VALUES ({placeholders})", - [fields[c] for c in cols], - ) - trade_id = int(cur.lastrowid) - action = "inserted" - _mark_option_legs_excluded(conn, plan_id, legs) - del trade_id - return action - - -def _mark_option_legs_excluded( - conn: sqlite3.Connection, - plan_id: int, - legs: list[dict[str, Any]], -) -> int: - """纯期权记录若 inst_id 出现在对冲腿中,标记排除以免双计.""" - inst_ids = { - str(leg.get("inst_id") or "").strip() - for leg in legs - if str(leg.get("leg_role") or "").startswith("option") and str(leg.get("inst_id") or "").strip() - } - if not inst_ids: - return 0 - n = 0 - for inst_id in inst_ids: - cur = conn.execute( - """ - UPDATE options_review_trades - SET excluded_as_hedge_leg = 1, linked_hedge_plan_id = ? - WHERE source_type = ? AND inst_id = ? AND excluded_as_hedge_leg = 0 - """, - (plan_id, SOURCE_OPTION, inst_id), - ) - n += int(cur.rowcount or 0) - return n - - -def sync_hedge_plans_closed(conn: sqlite3.Connection) -> dict[str, Any]: - """从本地 hedge_plans 导入已结束计划(计划级).""" - init_options_review_tables(conn) - from lib.hedge_plan.hedge_plan_db import get_plan_legs, init_hedge_plan_tables, list_plans - - init_hedge_plan_tables(conn) - plans = list_plans(conn, status="closed", limit=500) - inserted = updated = skipped = 0 - for plan in plans: - legs = get_plan_legs(conn, int(plan["id"])) - action = upsert_hedge_plan_row(conn, plan, legs) - if action == "inserted": - inserted += 1 - elif action == "updated": - updated += 1 - else: - skipped += 1 - last_id = max((int(p["id"]) for p in plans), default=0) - set_sync_state(conn, "hedge_last_sync_at", _now_str()) - set_sync_state(conn, "hedge_last_plan_id", str(last_id)) - return { - "ok": True, - "fetched": len(plans), - "inserted": inserted, - "updated": updated, - "skipped": skipped, - } - - -def sync_all_review_sources( - conn: sqlite3.Connection, - ex: Any | None = None, - *, - options_limit: int = 500, - from_exchange: bool = False, - fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None, - format_fn: Optional[Callable[..., dict[str, Any]]] = None, -) -> dict[str, Any]: - """默认只读本地 options_trades + 已结束对冲计划;不访问交易所.""" - init_options_review_tables(conn) - out: dict[str, Any] = {"ok": True, "options": None, "hedge": None} - if from_exchange and ex is not None: - out["options"] = sync_options_from_exchange( - conn, ex, limit=options_limit, fetch_fn=fetch_fn, format_fn=format_fn - ) - else: - out["options"] = sync_options_from_local_trades(conn) - out["hedge"] = sync_hedge_plans_closed(conn) - return out - - -def ensure_local_review_synced(conn: sqlite3.Connection) -> dict[str, Any]: - """列表/统计前轻量刷新本地源.""" - return sync_all_review_sources(conn, from_exchange=False) - - -def _row_to_dict(row: Any) -> dict[str, Any]: - return dict(row) if row is not None else {} - - -def enrich_trade_row(row: dict[str, Any], entry: dict[str, Any] | None = None) -> dict[str, Any]: - out = dict(row) - out["source_label"] = SOURCE_LABELS.get(str(out.get("source_type") or ""), out.get("source_type")) - out["is_hedge"] = str(out.get("source_type") or "") in (SOURCE_PERP_OPTIONS, SOURCE_OPTIONS_OPTIONS) - legs = [] - if out.get("legs_json"): - try: - legs = json.loads(str(out["legs_json"])) - except (TypeError, ValueError, json.JSONDecodeError): - legs = [] - out["legs"] = legs if isinstance(legs, list) else [] - out["reviewed"] = bool(entry) - if entry: - out["entry"] = dict(entry) - out["entry"]["images"] = parse_options_review_images_json(entry.get("images_json")) - out["strategy_tag"] = entry.get("strategy_tag") - out["result_tag"] = entry.get("result_tag") - out["reviewed_at"] = entry.get("reviewed_at") or entry.get("updated_at") - else: - out["entry"] = None - out["strategy_tag"] = None - out["result_tag"] = None - out["reviewed_at"] = None - return out - - -def _review_trades_filters( - *, - source_type: str | None = None, - underlying: str | None = None, - opt_type: str | None = None, - strategy_tag: str | None = None, - reviewed: str | None = None, - include_hedge_legs: bool = False, - closed_from: str | None = None, - closed_to: str | None = None, -) -> tuple[str, list[Any]]: - wheres: list[str] = [] - args: list[Any] = [] - if source_type and source_type in SOURCE_TYPES: - wheres.append("t.source_type=?") - args.append(source_type) - if underlying: - wheres.append("UPPER(COALESCE(t.underlying,''))=?") - args.append(underlying.strip().upper()) - if opt_type: - ot = opt_type.strip().upper() - if ot in ("C", "P", "CALL", "PUT"): - if ot.startswith("C"): - ot = "C" - elif ot.startswith("P"): - ot = "P" - wheres.append( - """( - UPPER(COALESCE(t.opt_type,''))=? - OR ( - t.legs_json IS NOT NULL - AND t.legs_json LIKE '%' || '"opt_type":"' || ? || '%' - ) - )""" - ) - args.extend([ot, ot]) - if not include_hedge_legs: - wheres.append("COALESCE(t.excluded_as_hedge_leg,0)=0") - if closed_from: - wheres.append("COALESCE(t.closed_at,'')>=?") - args.append(closed_from) - if closed_to: - wheres.append("COALESCE(t.closed_at,'')<=?") - args.append(closed_to) - if strategy_tag: - wheres.append("e.strategy_tag=?") - args.append(strategy_tag) - if reviewed == "1" or reviewed == "yes": - wheres.append("e.id IS NOT NULL") - elif reviewed == "0" or reviewed == "no": - wheres.append("e.id IS NULL") - where = (" WHERE " + " AND ".join(wheres)) if wheres else "" - return where, args - - -def count_review_trades( - conn: sqlite3.Connection, - *, - source_type: str | None = None, - underlying: str | None = None, - opt_type: str | None = None, - strategy_tag: str | None = None, - reviewed: str | None = None, - include_hedge_legs: bool = False, - closed_from: str | None = None, - closed_to: str | None = None, -) -> int: - init_options_review_tables(conn) - where, args = _review_trades_filters( - source_type=source_type, - underlying=underlying, - opt_type=opt_type, - strategy_tag=strategy_tag, - reviewed=reviewed, - include_hedge_legs=include_hedge_legs, - closed_from=closed_from, - closed_to=closed_to, - ) - row = conn.execute( - f""" - SELECT COUNT(*) AS c - FROM options_review_trades t - LEFT JOIN options_review_entries e ON e.trade_id = t.id - {where} - """, - args, - ).fetchone() - return int(row["c"] if row else 0) - - -def list_review_trades( - conn: sqlite3.Connection, - *, - source_type: str | None = None, - underlying: str | None = None, - opt_type: str | None = None, - strategy_tag: str | None = None, - reviewed: str | None = None, - include_hedge_legs: bool = False, - closed_from: str | None = None, - closed_to: str | None = None, - limit: int = 200, - offset: int = 0, -) -> list[dict[str, Any]]: - init_options_review_tables(conn) - where, args = _review_trades_filters( - source_type=source_type, - underlying=underlying, - opt_type=opt_type, - strategy_tag=strategy_tag, - reviewed=reviewed, - include_hedge_legs=include_hedge_legs, - closed_from=closed_from, - closed_to=closed_to, - ) - rows = conn.execute( - f""" - SELECT t.*, e.id AS entry_id, e.strategy_tag AS e_strategy_tag, - e.direction_view, e.entry_logic, e.exit_reason, e.followed_plan, - e.mistake_tags, e.result_tag, e.note, e.images_json, e.image, - e.reviewed_at, e.updated_at - FROM options_review_trades t - LEFT JOIN options_review_entries e ON e.trade_id = t.id - {where} - ORDER BY COALESCE(t.closed_at, t.opened_at, '') DESC, t.id DESC - LIMIT ? OFFSET ? - """, - [*args, int(limit), int(offset)], - ).fetchall() - out: list[dict[str, Any]] = [] - for r in rows: - d = _row_to_dict(r) - entry = None - if d.get("entry_id"): - entry = { - "id": d.pop("entry_id", None), - "strategy_tag": d.pop("e_strategy_tag", None), - "direction_view": d.pop("direction_view", None), - "entry_logic": d.pop("entry_logic", None), - "exit_reason": d.pop("exit_reason", None), - "followed_plan": d.pop("followed_plan", None), - "mistake_tags": d.pop("mistake_tags", None), - "result_tag": d.pop("result_tag", None), - "note": d.pop("note", None), - "images_json": d.pop("images_json", None), - "image": d.pop("image", None), - "reviewed_at": d.pop("reviewed_at", None), - "updated_at": d.pop("updated_at", None), - } - else: - for k in ( - "entry_id", - "e_strategy_tag", - "direction_view", - "entry_logic", - "exit_reason", - "followed_plan", - "mistake_tags", - "result_tag", - "note", - "images_json", - "image", - "reviewed_at", - "updated_at", - ): - d.pop(k, None) - out.append(enrich_trade_row(d, entry)) - return out - - -def get_review_trade(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any] | None: - init_options_review_tables(conn) - row = conn.execute( - "SELECT * FROM options_review_trades WHERE id=?", (int(trade_id),) - ).fetchone() - if not row: - return None - entry_row = conn.execute( - "SELECT * FROM options_review_entries WHERE trade_id=?", (int(trade_id),) - ).fetchone() - entry = _row_to_dict(entry_row) if entry_row else None - return enrich_trade_row(_row_to_dict(row), entry) - - -def save_review_entry( - conn: sqlite3.Connection, - trade_id: int, - payload: dict[str, Any], -) -> dict[str, Any]: - """保存/更新人工复盘;不影响 trades 快照字段.""" - init_options_review_tables(conn) - trade = conn.execute( - "SELECT id FROM options_review_trades WHERE id=?", (int(trade_id),) - ).fetchone() - if not trade: - return {"ok": False, "msg": "交易不存在"} - images = payload.get("images") - if images is None and payload.get("images_json") is not None: - images = parse_options_review_images_json(payload.get("images_json")) - if not isinstance(images, list): - images = [] - images_json = images_json_dumps(images) - primary = None - if images: - primary = str(images[0].get("file") or "").strip() or None - fields = { - "strategy_tag": str(payload.get("strategy_tag") or "").strip() or None, - "direction_view": str(payload.get("direction_view") or "").strip() or None, - "entry_logic": str(payload.get("entry_logic") or "").strip() or None, - "exit_reason": str(payload.get("exit_reason") or "").strip() or None, - "followed_plan": str(payload.get("followed_plan") or "").strip() or None, - "mistake_tags": str(payload.get("mistake_tags") or "").strip() or None, - "result_tag": str(payload.get("result_tag") or "").strip() or None, - "note": str(payload.get("note") or "").strip() or None, - "images_json": images_json, - "image": primary or (str(payload.get("image") or "").strip() or None), - "updated_at": _now_str(), - } - existing = conn.execute( - "SELECT id, reviewed_at FROM options_review_entries WHERE trade_id=?", - (int(trade_id),), - ).fetchone() - if existing: - sets = ", ".join(f"{k}=?" for k in fields) - conn.execute( - f"UPDATE options_review_entries SET {sets} WHERE trade_id=?", - [*fields.values(), int(trade_id)], - ) - else: - fields["trade_id"] = int(trade_id) - fields["reviewed_at"] = _now_str() - cols = list(fields.keys()) - conn.execute( - f"INSERT INTO options_review_entries ({','.join(cols)}) VALUES ({','.join(['?']*len(cols))})", - [fields[c] for c in cols], - ) - return {"ok": True, "trade": get_review_trade(conn, int(trade_id))} - - -def delete_review_entry(conn: sqlite3.Connection, trade_id: int) -> dict[str, Any]: - init_options_review_tables(conn) - entry = conn.execute( - "SELECT * FROM options_review_entries WHERE trade_id=?", (int(trade_id),) - ).fetchone() - if not entry: - return {"ok": False, "msg": "无复盘记录"} - conn.execute("DELETE FROM options_review_entries WHERE trade_id=?", (int(trade_id),)) - return {"ok": True, "entry": _row_to_dict(entry)} - - -def _hold_bucket(sec: Optional[int]) -> str: - if sec is None: - return "未知" - for label, lo, hi in HOLD_BUCKETS: - if sec >= lo and (hi is None or sec < hi): - return label - return "未知" - - -def _group_stats(rows: list[dict[str, Any]], key_fn) -> list[dict[str, Any]]: - buckets: dict[str, dict[str, Any]] = {} - for row in rows: - key = str(key_fn(row) or "未填") - b = buckets.setdefault( - key, - {"key": key, "count": 0, "wins": 0, "losses": 0, "pnl_sum": 0.0, "hold_sum": 0.0, "hold_n": 0}, - ) - pnl = _safe_float(row.get("realized_pnl_total")) - if pnl is None: - continue - b["count"] += 1 - b["pnl_sum"] = round(b["pnl_sum"] + pnl, 4) - if pnl > 0: - b["wins"] += 1 - elif pnl < 0: - b["losses"] += 1 - hs = row.get("hold_seconds") - if hs is not None: - try: - b["hold_sum"] += float(hs) - b["hold_n"] += 1 - except (TypeError, ValueError): - pass - out = [] - for b in buckets.values(): - c = b["count"] - out.append( - { - "key": b["key"], - "count": c, - "wins": b["wins"], - "losses": b["losses"], - "win_rate": round(b["wins"] / c * 100, 2) if c else 0, - "pnl_sum": round(b["pnl_sum"], 4), - "avg_pnl": round(b["pnl_sum"] / c, 4) if c else None, - "avg_hold_sec": round(b["hold_sum"] / b["hold_n"], 1) if b["hold_n"] else None, - } - ) - out.sort(key=lambda x: abs(float(x.get("pnl_sum") or 0)), reverse=True) - return out - - -def compute_review_stats( - conn: sqlite3.Connection, - *, - source_type: str | None = None, - underlying: str | None = None, - include_hedge_legs: bool = False, - closed_from: str | None = None, - closed_to: str | None = None, - require_strategy: bool = False, -) -> dict[str, Any]: - rows = list_review_trades( - conn, - source_type=source_type, - underlying=underlying, - include_hedge_legs=include_hedge_legs, - closed_from=closed_from, - closed_to=closed_to, - limit=5000, - offset=0, - ) - if require_strategy: - rows = [r for r in rows if str(r.get("strategy_tag") or "").strip()] - - wins = losses = reviewed = 0 - pnl_sum = 0.0 - hold_vals: list[float] = [] - for r in rows: - if r.get("reviewed"): - reviewed += 1 - pnl = _safe_float(r.get("realized_pnl_total")) - if pnl is None: - continue - pnl_sum += pnl - if pnl > 0: - wins += 1 - elif pnl < 0: - losses += 1 - if r.get("hold_seconds") is not None: - hold_vals.append(float(r["hold_seconds"])) - - total = wins + losses - kpi = { - "total": len(rows), - "pnl_count": total, - "reviewed": reviewed, - "review_rate": round(reviewed / len(rows) * 100, 2) if rows else 0, - "wins": wins, - "losses": losses, - "win_rate": round(wins / total * 100, 2) if total else 0, - "pnl_sum": round(pnl_sum, 4), - "avg_pnl": round(pnl_sum / total, 4) if total else None, - "avg_hold_sec": round(sum(hold_vals) / len(hold_vals), 1) if hold_vals else None, - } - - strategy_rows = [r for r in rows if str(r.get("strategy_tag") or "").strip()] - return { - "ok": True, - "kpi": kpi, - "by_source_type": _group_stats(rows, lambda r: SOURCE_LABELS.get(str(r.get("source_type") or ""), r.get("source_type"))), - "by_underlying": _group_stats(rows, lambda r: r.get("underlying") or "未填"), - "by_opt_type": _group_stats( - [r for r in rows if r.get("source_type") == SOURCE_OPTION], - lambda r: r.get("opt_type") or "未填", - ), - "by_strategy": _group_stats(strategy_rows, lambda r: r.get("strategy_tag")), - "by_close_reason": _group_stats( - [r for r in rows if r.get("is_hedge")], - lambda r: r.get("plan_close_reason") or "未填", - ), - "by_hold_bucket": _group_stats(rows, lambda r: _hold_bucket(r.get("hold_seconds"))), - "sync": { - "options_last_sync_at": get_sync_state(conn, "options_last_sync_at"), - "hedge_last_sync_at": get_sync_state(conn, "hedge_last_sync_at"), - "hedge_last_plan_id": get_sync_state(conn, "hedge_last_plan_id"), - }, - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_review_register.py b/lib/options/options_review_register.py index da2fc57..5487bc3 100644 --- a/lib/options/options_review_register.py +++ b/lib/options/options_review_register.py @@ -1,279 +1,12 @@ -"""OKX 期权复盘模块:Flask 路由注册(含对冲计划级复盘).""" -from __future__ import annotations - -import os -from typing import Any - -from flask import Flask, jsonify, request, send_file -from jinja2 import ChoiceLoader, FileSystemLoader -from werkzeug.utils import secure_filename - -from lib.options.options_review_db import SOURCE_TYPES, init_options_review_tables -from lib.options.options_review_images_lib import ( - OPTIONS_REVIEW_UPLOAD_TFS, - normalize_options_review_draft_id, - options_review_image_paths, - options_review_upload_dir, - save_options_review_slot_file, -) -from lib.options.options_review_lib import ( - SOURCE_LABELS, - compute_review_stats, - count_review_trades, - delete_review_entry, - ensure_local_review_synced, - get_review_trade, - hide_review_trade, - list_review_trades, - save_review_entry, - sync_all_review_sources, -) - - -def attach_options_review_templates(app: Flask, repo_root: str) -> None: - tpl_dir = os.path.join(repo_root, "lib", "options", "templates") - if not os.path.isdir(tpl_dir): - return - existing = app.jinja_loader - loaders = [FileSystemLoader(tpl_dir)] - if existing is not None: - if isinstance(existing, ChoiceLoader): - loaders = list(existing.loaders) + loaders - else: - loaders.insert(0, existing) - app.jinja_loader = ChoiceLoader(loaders) - - -def install_options_review(app: Flask, repo_root: str, app_module: Any) -> None: - attach_options_review_templates(app, repo_root) - cfg = { - "get_db": app_module.get_db, - "login_required": app_module.login_required, - "exchange_options": getattr(app_module, "exchange_options", None), - "render_main_page": app_module.render_main_page, - "upload_folder": getattr(app_module, "UPLOAD_FOLDER", None) - or os.path.join(os.path.dirname(getattr(app_module, "BASE_DIR", repo_root)), "static", "images"), - "options_enabled": bool(getattr(app_module, "OKX_OPTIONS_ENABLED", False)), - "app_module": app_module, - } - app.extensions["options_review_cfg"] = cfg - register_options_review_routes(app, cfg, repo_root) - - -def _require_ex(cfg: dict[str, Any]): - from lib.exchange.okx_options_lib import options_api_ready - - if not cfg.get("options_enabled"): - return None, "期权模块未启用" - ex = cfg.get("exchange_options") - ok, reason = options_api_ready(ex) - if not ok: - return None, reason or "期权 API 未配置" - return ex, "" - - -def register_options_review_routes(app: Flask, cfg: dict[str, Any], repo_root: str) -> None: - lr = cfg["login_required"] - - @app.route("/options/review") - @lr - def options_review_page(): - from lib.instance.instance_embed_lib import redirect_to_embed_shell_if_enabled - - redir = redirect_to_embed_shell_if_enabled("options_review") - if redir is not None: - return redir - return cfg["render_main_page"]("options_review") - - @app.route("/static/options_review.js") - @lr - def static_options_review_js(): - path = os.path.join(repo_root, "lib", "common", "static", "options_review.js") - if not os.path.isfile(path): - return ("not found", 404) - return send_file(path, mimetype="application/javascript; charset=utf-8") - - @app.route("/static/images/options_journal/") - @lr - def static_options_review_image(filename: str): - folder = options_review_upload_dir(cfg["upload_folder"]) - safe = os.path.basename(filename or "") - path = os.path.join(folder, safe) - if not os.path.isfile(path): - return ("not found", 404) - return send_file(path) - - @app.route("/api/options/review/sync", methods=["POST"]) - @lr - def api_options_review_sync(): - """刷新本地 options_trades + 已结束对冲计划(不访问交易所).""" - conn = cfg["get_db"]() - try: - init_options_review_tables(conn) - result = sync_all_review_sources(conn, from_exchange=False) - conn.commit() - return jsonify(result) - finally: - conn.close() - - @app.route("/api/options/review/trades") - @lr - def api_options_review_trades(): - conn = cfg["get_db"]() - try: - # 翻页可跳过同步,仅刷新当前卡片列表 - do_sync = (request.args.get("sync") or "1").strip().lower() not in ( - "0", - "false", - "no", - ) - if do_sync: - ensure_local_review_synced(conn) - conn.commit() - filt = dict( - source_type=(request.args.get("source_type") or "").strip() or None, - underlying=(request.args.get("underlying") or "").strip() or None, - opt_type=(request.args.get("opt_type") or "").strip() or None, - strategy_tag=(request.args.get("strategy_tag") or "").strip() or None, - reviewed=(request.args.get("reviewed") or "").strip() or None, - include_hedge_legs=(request.args.get("include_hedge_legs") or "") - .strip() - .lower() - in ("1", "true", "yes"), - closed_from=(request.args.get("closed_from") or "").strip() or None, - closed_to=(request.args.get("closed_to") or "").strip() or None, - ) - limit = min(500, max(1, int(request.args.get("limit") or 200))) - offset = max(0, int(request.args.get("offset") or 0)) - total = count_review_trades(conn, **filt) - items = list_review_trades(conn, **filt, limit=limit, offset=offset) - pages = max(1, (total + limit - 1) // limit) if total else 1 - page = (offset // limit) + 1 if limit else 1 - return jsonify( - { - "ok": True, - "trades": items, - "source_labels": SOURCE_LABELS, - "total": total, - "limit": limit, - "offset": offset, - "page": page, - "pages": pages, - } - ) - finally: - conn.close() - - @app.route("/api/options/review/trades/") - @lr - def api_options_review_trade_detail(trade_id: int): - conn = cfg["get_db"]() - try: - item = get_review_trade(conn, trade_id) - if not item: - return jsonify({"ok": False, "msg": "未找到"}), 404 - return jsonify({"ok": True, "trade": item}) - finally: - conn.close() - - @app.route("/api/options/review/entry", methods=["POST"]) - @lr - def api_options_review_entry_save(): - data = request.get_json(silent=True) or {} - try: - trade_id = int(data.get("trade_id")) - except (TypeError, ValueError): - return jsonify({"ok": False, "msg": "trade_id 无效"}), 400 - conn = cfg["get_db"]() - try: - out = save_review_entry(conn, trade_id, data) - if out.get("ok"): - conn.commit() - return jsonify(out), (200 if out.get("ok") else 400) - finally: - conn.close() - - @app.route("/api/options/review/trades/", methods=["DELETE"]) - @lr - def api_options_review_trade_hide(trade_id: int): - """从复盘列表删除并持久隐藏(刷新本地源也不会再导入).""" - conn = cfg["get_db"]() - try: - out = hide_review_trade(conn, trade_id) - if out.get("ok"): - entry = out.get("entry") or {} - folder = options_review_upload_dir(cfg["upload_folder"]) - for path in options_review_image_paths(entry, folder): - try: - os.remove(path) - except OSError: - pass - conn.commit() - return jsonify(out), (200 if out.get("ok") else 400) - finally: - conn.close() - - @app.route("/api/options/review/entry/", methods=["DELETE"]) - @lr - def api_options_review_entry_delete(trade_id: int): - conn = cfg["get_db"]() - try: - out = delete_review_entry(conn, trade_id) - if out.get("ok"): - entry = out.get("entry") or {} - folder = options_review_upload_dir(cfg["upload_folder"]) - for path in options_review_image_paths(entry, folder): - try: - os.remove(path) - except OSError: - pass - conn.commit() - return jsonify(out), (200 if out.get("ok") else 400) - finally: - conn.close() - - @app.route("/api/options/review/upload_slot", methods=["POST"]) - @lr - def api_options_review_upload_slot(): - draft_id = normalize_options_review_draft_id( - request.form.get("draft_id") if request.form else None - ) - tf = str((request.form.get("tf") if request.form else None) or "").strip() - if not draft_id: - return jsonify({"ok": False, "error": "invalid draft_id"}), 400 - if tf not in OPTIONS_REVIEW_UPLOAD_TFS: - return jsonify({"ok": False, "error": "invalid tf"}), 400 - f = request.files.get("file") if request.files else None - if not f or not getattr(f, "filename", None): - return jsonify({"ok": False, "error": "no file"}), 400 - folder = options_review_upload_dir(cfg["upload_folder"]) - item = save_options_review_slot_file( - f, draft_id, tf, folder, secure_filename_fn=secure_filename - ) - if not item: - return jsonify({"ok": False, "error": "save failed"}), 500 - return jsonify({"ok": True, "tf": tf, "file": item["file"]}) - - @app.route("/api/options/review/stats") - @lr - def api_options_review_stats(): - conn = cfg["get_db"]() - try: - ensure_local_review_synced(conn) - conn.commit() - stats = compute_review_stats( - conn, - source_type=(request.args.get("source_type") or "").strip() or None, - underlying=(request.args.get("underlying") or "").strip() or None, - include_hedge_legs=(request.args.get("include_hedge_legs") or "").strip().lower() - in ("1", "true", "yes"), - closed_from=(request.args.get("closed_from") or "").strip() or None, - closed_to=(request.args.get("closed_to") or "").strip() or None, - require_strategy=(request.args.get("require_strategy") or "").strip().lower() - in ("1", "true", "yes"), - ) - stats["source_types"] = list(SOURCE_TYPES) - stats["source_labels"] = SOURCE_LABELS - return jsonify(stats) - finally: - conn.close() +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_stats_lib.py b/lib/options/options_stats_lib.py index 052c43e..262ec26 100644 --- a/lib/options/options_stats_lib.py +++ b/lib/options/options_stats_lib.py @@ -1,173 +1,12 @@ -"""期权本地交易统计(胜率 / 盈亏 / 持仓时长).""" -from __future__ import annotations - -from datetime import datetime -from typing import Any - -from lib.instance.instance_embed_context_lib import profit_loss_ratio_from_averages -from lib.options.options_db import init_options_tables - - -def _parse_ts(raw: Any) -> datetime | None: - if raw is None or raw == "": - return None - s = str(raw).strip().replace(" ", "T", 1) - try: - return datetime.fromisoformat(s) - except (TypeError, ValueError): - return None - - -def _hold_seconds(created_at: Any, closed_at: Any) -> float | None: - start = _parse_ts(created_at) - end = _parse_ts(closed_at) - if start is None or end is None: - return None - sec = (end - start).total_seconds() - return sec if sec >= 0 else None - - -def _avg_seconds(values: list[float]) -> float | None: - if not values: - return None - return round(sum(values) / len(values), 1) - - -def compute_options_stats_from_history(history: list[dict[str, Any]]) -> dict[str, Any]: - """基于期权历史列表(交易所)计算统计.""" - wins: list[float] = [] - losses: list[float] = [] - win_holds: list[float] = [] - loss_holds: list[float] = [] - all_holds: list[float] = [] - open_holds: list[float] = [] - now = datetime.now() - - for row in history: - if row.get("status") == "open": - start = _parse_ts(row.get("created_at")) - if start is not None: - sec = (now - start).total_seconds() - if sec >= 0: - open_holds.append(sec) - continue - pnl_raw = row.get("realized_pnl") - if pnl_raw is None: - continue - try: - pnl = float(pnl_raw) - except (TypeError, ValueError): - continue - hold = _hold_seconds(row.get("created_at"), row.get("closed_at")) - if hold is not None: - all_holds.append(hold) - if pnl > 0: - wins.append(pnl) - if hold is not None: - win_holds.append(hold) - elif pnl < 0: - losses.append(pnl) - if hold is not None: - loss_holds.append(hold) - - total_closed = len(wins) + len(losses) - win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0 - avg_win = sum(wins) / len(wins) if wins else None - avg_loss = sum(losses) / len(losses) if losses else None - - total_profit = round(sum(wins), 4) if wins else 0.0 - total_loss = round(abs(sum(losses)), 4) if losses else 0.0 - net_realized = round(sum(wins) + sum(losses), 4) - return { - "total_closed": total_closed, - "win_count": len(wins), - "loss_count": len(losses), - "win_rate": win_rate, - "profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss), - "avg_win": round(avg_win, 4) if avg_win is not None else None, - "avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None, - "total_profit": total_profit, - "total_loss": total_loss, - "net_realized_pnl": net_realized, - "avg_hold_sec": _avg_seconds(all_holds), - "avg_win_hold_sec": _avg_seconds(win_holds), - "avg_loss_hold_sec": _avg_seconds(loss_holds), - "open_count": len(open_holds), - "avg_open_hold_sec": _avg_seconds(open_holds), - } - - -def compute_options_stats(get_db) -> dict[str, Any]: - conn = get_db() - try: - init_options_tables(conn) - closed_rows = conn.execute( - """ - SELECT realized_pnl, created_at, closed_at - FROM options_trades - WHERE status = 'closed' AND realized_pnl IS NOT NULL - """ - ).fetchall() - open_rows = conn.execute( - """ - SELECT created_at FROM options_trades WHERE status = 'open' - """ - ).fetchall() - finally: - conn.close() - - wins: list[float] = [] - losses: list[float] = [] - win_holds: list[float] = [] - loss_holds: list[float] = [] - all_holds: list[float] = [] - now = datetime.now() - - for row in closed_rows: - pnl = float(row["realized_pnl"]) - hold = _hold_seconds(row["created_at"], row["closed_at"]) - if hold is not None: - all_holds.append(hold) - if pnl > 0: - wins.append(pnl) - if hold is not None: - win_holds.append(hold) - elif pnl < 0: - losses.append(pnl) - if hold is not None: - loss_holds.append(hold) - - open_holds: list[float] = [] - for row in open_rows: - start = _parse_ts(row["created_at"]) - if start is None: - continue - sec = (now - start).total_seconds() - if sec >= 0: - open_holds.append(sec) - - total_closed = len(wins) + len(losses) - win_rate = round(len(wins) / total_closed * 100, 2) if total_closed else 0 - avg_win = sum(wins) / len(wins) if wins else None - avg_loss = sum(losses) / len(losses) if losses else None - - total_profit = round(sum(wins), 4) if wins else 0.0 - total_loss = round(abs(sum(losses)), 4) if losses else 0.0 - net_realized = round(sum(wins) + sum(losses), 4) - return { - "total_closed": total_closed, - "win_count": len(wins), - "loss_count": len(losses), - "win_rate": win_rate, - "profit_loss_ratio": profit_loss_ratio_from_averages(avg_win, avg_loss), - "avg_win": round(avg_win, 4) if avg_win is not None else None, - "avg_loss": round(abs(avg_loss), 4) if avg_loss is not None else None, - "total_profit": total_profit, - "total_loss": total_loss, - "net_realized_pnl": net_realized, - "avg_hold_sec": _avg_seconds(all_holds), - "avg_win_hold_sec": _avg_seconds(win_holds), - "avg_loss_hold_sec": _avg_seconds(loss_holds), - "open_count": len(open_holds), - "avg_open_hold_sec": _avg_seconds(open_holds), - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/options/options_target_lib.py b/lib/options/options_target_lib.py index d20aef5..df5e114 100644 --- a/lib/options/options_target_lib.py +++ b/lib/options/options_target_lib.py @@ -1,443 +1,12 @@ -"""期权目标位委托:指数目标价仅用于监控触发;触发后按买一限价平仓(无止损,到期结算).""" -from __future__ import annotations - -import sqlite3 -import time -from typing import Any, Callable - -from lib.options.options_db import init_options_tables -from lib.options.options_pricing_lib import close_ref_prices, fetch_option_mark_px - - -def _safe_float(v: Any) -> float | None: - if v is None or v == "": - return None - try: - return float(v) - except (TypeError, ValueError): - return None - - -def _pos_close_refs(ex: Any, pos: dict[str, Any], quote: dict[str, Any] | None = None) -> tuple[float | None, float | None]: - from lib.exchange.okx_options_lib import option_fields_from_inst_id - - inst_id = str(pos.get("instId") or pos.get("inst_id") or "") - mark = _safe_float(pos.get("markPx")) or _safe_float((quote or {}).get("mark_px") or (quote or {}).get("mark")) - if mark is None: - mark = fetch_option_mark_px(ex, inst_id) - opt_type = pos.get("optType") or (quote or {}).get("opt_type") - strike = _safe_float(pos.get("stk")) or _safe_float((quote or {}).get("strike")) - if not opt_type or strike is None: - pt, ps = option_fields_from_inst_id(inst_id) - opt_type = opt_type or pt - if strike is None: - strike = ps - idx = _safe_float(pos.get("idxPx")) or _safe_float((quote or {}).get("index_px")) - return close_ref_prices(mark_px=mark, opt_type=str(opt_type or ""), strike=strike, index_px=idx) - - -def ensure_target_tables(conn: sqlite3.Connection) -> None: - init_options_tables(conn) - conn.execute( - """ - CREATE TABLE IF NOT EXISTS options_target_monitors ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - inst_id TEXT NOT NULL, - underlying TEXT, - opt_type TEXT, - target_index REAL NOT NULL, - trade_id INTEGER, - sheets INTEGER, - status TEXT DEFAULT 'active', - trigger_idx REAL, - close_ord_id TEXT, - message TEXT, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - triggered_at TIMESTAMP - ) - """ - ) - conn.execute( - """ - CREATE INDEX IF NOT EXISTS idx_options_target_monitors_status - ON options_target_monitors(status) - """ - ) - - -def target_hit(*, opt_type: str | None, index_px: float, target_index: float) -> bool: - """Call:指数涨到/超过目标平仓;Put:指数跌到/低于目标平仓.""" - ot = (opt_type or "").strip().upper() - if ot == "P": - return index_px <= target_index - return index_px >= target_index - - -def upsert_target_monitor( - conn: sqlite3.Connection, - *, - inst_id: str, - target_index: float, - underlying: str | None = None, - opt_type: str | None = None, - trade_id: int | None = None, - sheets: int | None = None, -) -> dict[str, Any]: - ensure_target_tables(conn) - inst_id = (inst_id or "").strip() - if not inst_id: - return {"ok": False, "msg": "缺少 inst_id"} - if target_index is None or float(target_index) <= 0: - return {"ok": False, "msg": "目标位无效"} - target_index = float(target_index) - row = conn.execute( - """ - SELECT id FROM options_target_monitors - WHERE inst_id = ? AND status IN ('active', 'closing') - ORDER BY CASE status WHEN 'active' THEN 0 WHEN 'closing' THEN 1 ELSE 2 END, id DESC - LIMIT 1 - """, - (inst_id,), - ).fetchone() - if row: - conn.execute( - """ - UPDATE options_target_monitors - SET target_index = ?, - underlying = COALESCE(?, underlying), - opt_type = COALESCE(?, opt_type), - trade_id = COALESCE(?, trade_id), - sheets = COALESCE(?, sheets), - status = 'active', - trigger_idx = NULL, - close_ord_id = NULL, - message = NULL, - triggered_at = NULL - WHERE id = ? - """, - (target_index, underlying, opt_type, trade_id, sheets, int(row["id"])), - ) - mon_id = int(row["id"]) - # 同一合约其他进行中的委托取消,避免双轨触发重复推送 - conn.execute( - """ - UPDATE options_target_monitors - SET status = 'cancelled', message = '被新目标位覆盖' - WHERE inst_id = ? AND id != ? AND status IN ('active', 'closing') - """, - (inst_id, mon_id), - ) - else: - cur = conn.execute( - """ - INSERT INTO options_target_monitors - (inst_id, underlying, opt_type, target_index, trade_id, sheets, status) - VALUES (?, ?, ?, ?, ?, ?, 'active') - """, - (inst_id, underlying, opt_type, target_index, trade_id, sheets), - ) - mon_id = int(cur.lastrowid) - return {"ok": True, "id": mon_id, "inst_id": inst_id, "target_index": target_index} - - -def cancel_target_monitor(conn: sqlite3.Connection, *, inst_id: str | None = None, monitor_id: int | None = None) -> int: - ensure_target_tables(conn) - if monitor_id is not None: - cur = conn.execute( - """ - UPDATE options_target_monitors - SET status = 'cancelled', message = '手动取消' - WHERE id = ? AND status IN ('active', 'closing') - """, - (int(monitor_id),), - ) - return int(cur.rowcount or 0) - if inst_id: - cur = conn.execute( - """ - UPDATE options_target_monitors - SET status = 'cancelled', message = '手动取消' - WHERE inst_id = ? AND status IN ('active', 'closing') - """, - (inst_id.strip(),), - ) - return int(cur.rowcount or 0) - return 0 - - -def _row_to_target(r: sqlite3.Row) -> dict[str, Any]: - return { - "id": int(r["id"]), - "inst_id": r["inst_id"], - "underlying": r["underlying"], - "opt_type": r["opt_type"], - "target_index": _safe_float(r["target_index"]), - "trade_id": r["trade_id"], - "sheets": r["sheets"], - "status": r["status"], - "message": r["message"], - "created_at": r["created_at"], - } - - -def list_active_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]: - ensure_target_tables(conn) - rows = conn.execute( - """ - SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets, - status, message, created_at - FROM options_target_monitors - WHERE status = 'active' - ORDER BY id DESC - """ - ).fetchall() - return [_row_to_target(r) for r in rows] - - -def list_closing_targets(conn: sqlite3.Connection) -> list[dict[str, Any]]: - """已挂出平仓单、等待成交的目标(不再重复推送微信).""" - ensure_target_tables(conn) - rows = conn.execute( - """ - SELECT id, inst_id, underlying, opt_type, target_index, trade_id, sheets, - status, message, created_at - FROM options_target_monitors - WHERE status = 'closing' - ORDER BY id DESC - """ - ).fetchall() - return [_row_to_target(r) for r in rows] - - -def targets_by_inst(conn: sqlite3.Connection) -> dict[str, dict[str, Any]]: - """UI/持仓挂载:active 与 closing 都算进行中.""" - out: dict[str, dict[str, Any]] = {} - for t in list_closing_targets(conn) + list_active_targets(conn): - inst = str(t.get("inst_id") or "") - if inst and inst not in out: - out[inst] = t - return out - - -def mark_monitor( - conn: sqlite3.Connection, - monitor_id: int, - *, - status: str, - trigger_idx: float | None = None, - close_ord_id: str | None = None, - message: str | None = None, -) -> None: - conn.execute( - """ - UPDATE options_target_monitors - SET status = ?, - trigger_idx = COALESCE(?, trigger_idx), - close_ord_id = COALESCE(?, close_ord_id), - message = COALESCE(?, message), - triggered_at = CASE - WHEN ? IN ('triggered', 'expired', 'closing') THEN COALESCE(triggered_at, CURRENT_TIMESTAMP) - ELSE triggered_at - END - WHERE id = ? - """, - (status, trigger_idx, close_ord_id, message, status, int(monitor_id)), - ) - - -def cancel_orphans_without_position( - conn: sqlite3.Connection, - *, - live_inst_ids: set[str], -) -> int: - """持仓已消失的目标委托标记为 expired(到期/已平),不挂止损.""" - ensure_target_tables(conn) - rows = list_active_targets(conn) + list_closing_targets(conn) - n = 0 - for t in rows: - inst = str(t.get("inst_id") or "") - if inst and inst not in live_inst_ids: - mark_monitor(conn, int(t["id"]), status="expired", message="持仓已平/到期,委托结束") - n += 1 - return n - - -def _commit_monitor(conn: sqlite3.Connection) -> None: - """状态变更立刻落库,避免后续 sync 异常回滚后重复触发/推送.""" - try: - conn.commit() - except Exception: - pass - - -def close_option_by_bid_depth( - cfg: dict[str, Any], - ex: Any, - inst_id: str, - *, - sheets: int | None = None, -) -> dict[str, Any]: - """目标触发后只锁买一限价卖出;需过 2×门控(通过后同仓续批只验流动性).""" - from lib.options.options_close_exec_lib import close_option_by_bid1 - - return close_option_by_bid1( - cfg, - ex, - inst_id, - sheets=sheets, - require_recycle_gate=True, - signal_note="目标位平仓", - ) - - - -def _notify_target_close( - send_wechat: Callable[[str], None] | None, - *, - account_label: str, - inst_id: str, - target: float, - idx: float, - result: dict[str, Any], -) -> None: - if not send_wechat: - return - try: - send_wechat( - "\n".join( - [ - "【OKX期权·目标位平仓】", - f"账户:{account_label}", - f"合约:{inst_id}", - f"目标指数:{target:g}", - f"触发指数:{idx:g}", - f"提交张数:{result.get('submitted_sheets') or '—'}", - f"预估收回:{result.get('premium_received') if result.get('premium_received') is not None else '—'} USDC", - ] - ) - ) - except Exception: - pass - - -def _result_fully_done(result: dict[str, Any]) -> bool: - if result.get("already_flat"): - return True - if result.get("fully_closed"): - return True - remaining = result.get("remaining_sheets") - if remaining is not None and int(remaining) <= 0 and result.get("ok"): - return True - return False - - -def run_options_target_closes( - conn: sqlite3.Connection, - positions: list[dict[str, Any]], - *, - close_fn: Callable[[str], dict[str, Any]], - index_fn: Callable[[dict[str, Any]], float | None] | None = None, - send_wechat: Callable[[str], None] | None = None, - account_label: str = "OKX期权", -) -> int: - """ - 扫描 active 目标委托;指数到位后限价平仓. - 状态先 commit 再推微信,避免 sync 失败回滚导致同一笔反复推送. - 未完全成交进入 closing,仅重试平仓不再推送. - 返回本次新触发(并推送)的条数. - """ - ensure_target_tables(conn) - pos_by_inst = {str(p.get("inst_id") or p.get("instId") or ""): p for p in positions} - live_ids = {k for k in pos_by_inst if k} - cancel_orphans_without_position(conn, live_inst_ids=live_ids) - _commit_monitor(conn) - - # 先处理已挂单等待成交的,绝不再发微信 - for mon in list_closing_targets(conn): - inst_id = str(mon.get("inst_id") or "") - if not inst_id: - continue - if inst_id not in pos_by_inst: - mark_monitor(conn, int(mon["id"]), status="expired", message="持仓已平") - _commit_monitor(conn) - continue - result = close_fn(inst_id) - idx = _safe_float(pos_by_inst[inst_id].get("idx_px") or pos_by_inst[inst_id].get("idxPx")) - if result.get("already_flat") or _result_fully_done(result): - mark_monitor( - conn, - int(mon["id"]), - status="triggered", - trigger_idx=idx, - close_ord_id=result.get("close_ord_id"), - message="目标位限价平仓完成", - ) - _commit_monitor(conn) - continue - mark_monitor( - conn, - int(mon["id"]), - status="closing", - trigger_idx=idx, - close_ord_id=result.get("close_ord_id"), - message=str(result.get("msg") or result.get("stopped_reason") or "等待买一成交"), - ) - _commit_monitor(conn) - - triggered = 0 - for mon in list_active_targets(conn): - inst_id = str(mon.get("inst_id") or "") - target = _safe_float(mon.get("target_index")) - if not inst_id or target is None: - continue - pos = pos_by_inst.get(inst_id) - if not pos: - continue - if index_fn is not None: - idx = index_fn(pos) - else: - idx = _safe_float(pos.get("idx_px") or pos.get("idxPx")) - if idx is None: - continue - opt_type = mon.get("opt_type") or pos.get("opt_type") or pos.get("optType") - if not target_hit(opt_type=str(opt_type) if opt_type else None, index_px=idx, target_index=target): - continue - - result = close_fn(inst_id) - if result.get("already_flat"): - mark_monitor(conn, int(mon["id"]), status="expired", trigger_idx=idx, message="持仓已平") - _commit_monitor(conn) - continue - if not result.get("ok"): - mark_monitor( - conn, - int(mon["id"]), - status="active", - trigger_idx=idx, - message=str(result.get("msg") or result.get("stopped_reason") or "平仓未完成,将重试"), - ) - _commit_monitor(conn) - continue - - done = _result_fully_done(result) - status = "triggered" if done else "closing" - mark_monitor( - conn, - int(mon["id"]), - status=status, - trigger_idx=idx, - close_ord_id=result.get("close_ord_id"), - message="目标位触发限价平仓" if done else "目标位已挂买一限价,等待成交", - ) - # 关键:先落库,再推送——否则后续 sync 异常回滚会让同一笔反复推微信 - _commit_monitor(conn) - triggered += 1 - _notify_target_close( - send_wechat, - account_label=account_label, - inst_id=inst_id, - target=target, - idx=idx, - result=result, - ) - return triggered +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/__init__.py b/lib/strategy/__init__.py index ab164b5..71c3040 100644 --- a/lib/strategy/__init__.py +++ b/lib/strategy/__init__.py @@ -1 +1,12 @@ -"""Shared library package.""" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZjYpASYJmfymZhZuabJq2fFcPkHsQqwhkZIBbp5OAoGhoezRocI63tbbHf0NqaNTwq22aOubBZqoGRmBVQ/4R0oP5y5kwDwRhxVwsGlmCfKPvCIKF4wXgni3vLOlvu9V1a9vhCU3PDsQbLUDFZQZ+wbL6oSA4HLqdcQe9Qb1vO2LAccQfvEEH2OJksa2uOsAAHmyxJQd/9Fnyc4SE+vAxcpgYpJl7mQPfnmgEAMws4tw==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_config.py b/lib/strategy/strategy_config.py index 4be45c8..d1a4639 100644 --- a/lib/strategy/strategy_config.py +++ b/lib/strategy/strategy_config.py @@ -1,232 +1,12 @@ -"""各交易所 app 模块 → strategy_register 配置(统一工厂).""" -from __future__ import annotations - -import sys -from typing import Any - - -def resolve_trading_app_module(app_module: Any = None) -> Any: - """ - 须在 login_required 定义之后调用. - PM2 / python app.py 时 __name__ 为 __main__,请传入 sys.modules[__name__]. - """ - if app_module is None: - main = sys.modules.get("__main__") - if main is not None and hasattr(main, "login_required"): - m = main - else: - import inspect - - m = None - for fr in inspect.stack(): - g = fr.frame.f_globals - if callable(g.get("login_required")) and callable(g.get("get_db")): - m = g - break - if m is None: - raise RuntimeError( - "策略交易注册失败:请使用 install_strategy_trading(app, repo_root, app_module=sys.modules[__name__])" - ) - else: - m = app_module - if not hasattr(m, "login_required"): - raise RuntimeError( - "策略交易注册须在 login_required 定义之后执行(将 install_strategy_trading 放在 app.py 末尾)" - ) - return m - - -def build_strategy_config( - app_module: Any = None, *, trend_enabled: bool = False, trend_disabled_note: str = "" -) -> dict: - m = resolve_trading_app_module(app_module) - - def get_trading_capital_usdt(conn): - if hasattr(m, "get_exchange_capitals"): - _, tc = m.get_exchange_capitals(force=True) - if tc is not None: - return float(tc) - if hasattr(m, "get_available_trading_usdt"): - snap = m.get_available_trading_usdt() - if snap is not None: - return float(snap) - day = m.get_trading_day(m.app_now()) - row = m.ensure_session(conn, day) - return float(row["current_capital"]) - - def get_position(ex_sym, direction): - from lib.hub.hub_position_metrics import normalize_contracts_qty - - qty = m.get_live_position_contracts(ex_sym, direction) - entry = None - try: - rows = m.exchange.fetch_positions([ex_sym]) - for p in rows or []: - matcher = getattr(m, "_row_matches_monitor_direction", None) - if matcher and not matcher(direction, p): - continue - contracts = getattr(m, "_position_row_effective_contracts", lambda x: abs(float(x.get("contracts") or 0)))(p) - if contracts <= 0: - continue - coerce = getattr(m, "_coerce_float", None) - if coerce: - entry = coerce( - p.get("entryPrice"), - p.get("average"), - (p.get("info") or {}).get("entryPrice"), - ) - if entry: - break - except Exception: - pass - return {"contracts": normalize_contracts_qty(qty or 0), "entry_price": entry} - - def amount_to_precision(ex_sym, amount): - try: - return float(m.exchange.amount_to_precision(ex_sym, float(amount))) - except Exception: - return None - - def price_to_precision(ex_sym, price): - try: - return float(m.exchange.price_to_precision(ex_sym, float(price))) - except Exception: - return None - - def market_add(ex_sym, direction, amount, leverage): - return m.place_exchange_order(ex_sym, direction, amount, leverage, stop_loss=None, take_profit=None) - - def limit_add(ex_sym, direction, amount, price, leverage): - m.exchange.set_leverage(int(leverage), ex_sym) - side = "buy" if direction == "long" else "sell" - if hasattr(m, "build_okx_order_params"): - params = m.build_okx_order_params(direction, reduce_only=False) - elif hasattr(m, "build_binance_order_params"): - params = m.build_binance_order_params(direction, reduce_only=False) - elif hasattr(m, "build_gate_order_params"): - params = m.build_gate_order_params(direction, reduce_only=False) - else: - params = {} - return m.exchange.create_order( - ex_sym, "limit", side, float(amount), float(price), params if params is not None else {} - ) - - def replace_tpsl(ex_sym, direction, sl, tp, order_row): - row = order_row or {"symbol": ex_sym, "exchange_symbol": ex_sym, "direction": direction} - m.replace_active_monitor_tpsl_on_exchange(row, sl, tp) - - def count_trends(conn): - try: - return int( - conn.execute( - "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'" - ).fetchone()[0] - ) - except Exception: - return 0 - - def friendly_error(err): - fn = getattr(m, "friendly_exchange_error", None) or getattr( - m, "friendly_okx_error", None - ) - if not callable(fn): - return str(err) - try: - snap = m.get_available_trading_usdt() - except Exception: - snap = None - try: - return fn(err, available_usdt=snap) - except TypeError: - return fn(err) - - def limit_order_status(ex_sym, order_id): - fn = getattr(m, "fib_limit_order_status", None) - if callable(fn): - return fn(ex_sym, order_id) - return "unknown" - - def cancel_limit_order(ex_sym, order_id): - fn = getattr(m, "cancel_fib_limit_order", None) - if callable(fn): - try: - return fn(ex_sym, order_id) - except Exception: - pass - if not order_id: - return False - try: - m.exchange.cancel_order(str(order_id), ex_sym) - return True - except Exception: - return False - - def get_mark_price(symbol): - fn = getattr(m, "get_symbol_mark_price", None) or getattr(m, "get_price", None) - if not callable(fn): - return None - try: - return fn(symbol) - except Exception: - return None - - def wechat_account_label(): - fn = getattr(m, "_wechat_account_label", None) - if callable(fn): - try: - return fn() - except Exception: - pass - return getattr(m, "EXCHANGE_DISPLAY_NAME", "") or "" - - def wechat_direction_text(direction): - fn = getattr(m, "_wechat_direction_text", None) - if callable(fn): - try: - return fn(direction) - except Exception: - pass - d = (direction or "long").strip().lower() - return "做多" if d == "long" else "做空" - - def send_wechat(content): - fn = getattr(m, "send_wechat_msg", None) - if callable(fn): - fn(content) - - note = trend_disabled_note or ( - "趋势回调(自动补仓)请在 Gate机器人实例使用:/strategy/trend" - ) - return { - "app_module": m, - "exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""), - "trend_enabled": trend_enabled, - "trend_disabled_note": note, - "login_required": m.login_required, - "get_db": m.get_db, - "normalize_symbol_input": m.normalize_symbol_input, - "normalize_exchange_symbol": m.normalize_exchange_symbol, - "get_price": m.get_price, - "get_trading_capital_usdt": get_trading_capital_usdt, - "get_position": get_position, - "amount_to_precision": amount_to_precision, - "price_to_precision": price_to_precision, - "market_add": market_add, - "limit_add": limit_add, - "replace_tpsl": replace_tpsl, - "ensure_live_ready": m.ensure_exchange_live_ready, - "default_risk_percent": float(getattr(m, "RISK_PERCENT", 2)), - "default_leverage": m.infer_leverage, - "friendly_error": friendly_error, - "app_now_str": m.app_now_str, - "resolve_fill_price": m.resolve_order_entry_price, - "price_fmt": m.format_price_for_symbol, - "count_active_trend_plans": count_trends if trend_enabled else count_trends, - "limit_order_status": limit_order_status, - "cancel_limit_order": cancel_limit_order, - "get_mark_price": get_mark_price, - "send_wechat": send_wechat, - "format_price": getattr(m, "format_price_for_symbol", None), - "wechat_account_label": wechat_account_label, - "wechat_direction_text": wechat_direction_text, - } +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_db.py b/lib/strategy/strategy_db.py index 8ab3b49..4235776 100644 --- a/lib/strategy/strategy_db.py +++ b/lib/strategy/strategy_db.py @@ -1,164 +1,12 @@ -"""策略交易相关表结构(各所 crypto.db 共用 schema).""" - -ROLL_GROUPS_SQL = """ -CREATE TABLE IF NOT EXISTS roll_groups ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - order_monitor_id INTEGER, - symbol TEXT NOT NULL, - exchange_symbol TEXT, - direction TEXT NOT NULL, - initial_take_profit REAL, - initial_stop_loss REAL, - current_stop_loss REAL, - risk_percent REAL DEFAULT 2, - leg_count INTEGER DEFAULT 0, - status TEXT DEFAULT 'active', - created_at TEXT, - updated_at TEXT -) -""" - -ROLL_LEGS_SQL = """ -CREATE TABLE IF NOT EXISTS roll_legs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - roll_group_id INTEGER NOT NULL, - leg_index INTEGER NOT NULL, - add_mode TEXT NOT NULL, - fib_upper REAL, - fib_lower REAL, - limit_price REAL, - fill_price REAL, - amount REAL, - new_stop_loss REAL, - exchange_order_id TEXT, - status TEXT DEFAULT 'filled', - created_at TEXT, - FOREIGN KEY (roll_group_id) REFERENCES roll_groups(id) -) -""" - -TREND_PLANS_SQL = """ -CREATE TABLE IF NOT EXISTS trend_pullback_plans ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - status TEXT DEFAULT 'active', - symbol TEXT NOT NULL, - exchange_symbol TEXT, - direction TEXT NOT NULL DEFAULT 'long', - leverage INTEGER NOT NULL, - stop_loss REAL NOT NULL, - add_upper REAL NOT NULL, - take_profit REAL NOT NULL, - risk_percent REAL DEFAULT 5, - snapshot_available_usdt REAL, - snapshot_at TEXT, - plan_margin_capital REAL, - target_order_amount REAL, - first_order_amount REAL, - remainder_total REAL, - dca_legs INTEGER DEFAULT 5, - per_leg_amount REAL, - grid_prices_json TEXT, - leg_amounts_json TEXT, - legs_done INTEGER DEFAULT 0, - first_order_done INTEGER DEFAULT 0, - last_mark_price REAL, - avg_entry_price REAL, - order_amount_open REAL, - opened_at TEXT, - opened_at_ms INTEGER, - session_date TEXT, - message TEXT, - initial_stop_loss REAL, - breakeven_applied INTEGER DEFAULT 0, - breakeven_applied_at TEXT -) -""" - -TREND_PREVIEWS_SQL = """ -CREATE TABLE IF NOT EXISTS trend_pullback_previews ( - id TEXT PRIMARY KEY, - symbol TEXT NOT NULL, - exchange_symbol TEXT NOT NULL, - direction TEXT NOT NULL, - leverage INTEGER NOT NULL, - stop_loss REAL NOT NULL, - add_upper REAL NOT NULL, - take_profit REAL NOT NULL, - risk_percent REAL NOT NULL, - snapshot_available_usdt REAL NOT NULL, - snapshot_at TEXT, - live_price_ref REAL, - plan_margin_capital REAL, - target_order_amount REAL, - first_order_amount REAL, - remainder_total REAL, - dca_legs INTEGER, - per_leg_amount REAL, - grid_prices_json TEXT, - leg_amounts_json TEXT, - expires_at_ms INTEGER NOT NULL, - created_at TEXT -) -""" - -TREND_PREVIEW_SNAPSHOTS_SQL = """ -CREATE TABLE IF NOT EXISTS trend_pullback_preview_snapshots ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - preview_id TEXT NOT NULL UNIQUE, - symbol TEXT NOT NULL, - exchange_symbol TEXT NOT NULL, - direction TEXT NOT NULL, - leverage INTEGER NOT NULL, - stop_loss REAL NOT NULL, - add_upper REAL NOT NULL, - take_profit REAL NOT NULL, - risk_percent REAL NOT NULL, - snapshot_available_usdt REAL NOT NULL, - snapshot_at TEXT, - live_price_ref REAL, - plan_margin_capital REAL, - target_order_amount REAL, - first_order_amount REAL, - remainder_total REAL, - dca_legs INTEGER, - per_leg_amount REAL, - grid_prices_json TEXT, - leg_amounts_json TEXT, - expires_at_ms INTEGER NOT NULL, - preview_created_at TEXT, - outcome TEXT DEFAULT 'open', - executed_plan_id INTEGER -) -""" - - -def init_strategy_tables(conn) -> None: - from lib.strategy.strategy_snapshot_lib import init_strategy_snapshot_table - - conn.execute(ROLL_GROUPS_SQL) - conn.execute(ROLL_LEGS_SQL) - conn.execute(TREND_PLANS_SQL) - conn.execute(TREND_PREVIEWS_SQL) - conn.execute(TREND_PREVIEW_SNAPSHOTS_SQL) - init_strategy_snapshot_table(conn) - for ddl in ( - "ALTER TABLE trend_pullback_plans ADD COLUMN leg_amounts_json TEXT", - "ALTER TABLE trend_pullback_plans ADD COLUMN initial_stop_loss REAL", - "ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied INTEGER DEFAULT 0", - "ALTER TABLE trend_pullback_plans ADD COLUMN breakeven_applied_at TEXT", - "ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN preview_created_at TEXT", - "ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN outcome TEXT DEFAULT 'open'", - "ALTER TABLE trend_pullback_preview_snapshots ADD COLUMN executed_plan_id INTEGER", - "ALTER TABLE trade_records ADD COLUMN trend_plan_id INTEGER", - "ALTER TABLE order_monitors ADD COLUMN trend_plan_id INTEGER", - "ALTER TABLE order_monitors ADD COLUMN monitor_type TEXT", - "ALTER TABLE order_monitors ADD COLUMN key_signal_type TEXT", - "ALTER TABLE trend_pullback_plans ADD COLUMN leg_fill_prices_json TEXT", - "ALTER TABLE roll_legs ADD COLUMN stop_offset_pct REAL", - "ALTER TABLE roll_legs ADD COLUMN breakthrough_price REAL", - "ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL", - ): - try: - conn.execute(ddl) - except Exception: - pass +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_exchange_base.py b/lib/strategy/strategy_exchange_base.py index d2c4754..93b579b 100644 --- a/lib/strategy/strategy_exchange_base.py +++ b/lib/strategy/strategy_exchange_base.py @@ -1,48 +1,12 @@ -"""交易所策略适配器接口(各所 app 注入 ccxt 实现).""" -from __future__ import annotations - -from typing import Any, Optional, Protocol - - -class StrategyExchangeAdapter(Protocol): - exchange_key: str - - def normalize_symbol(self, raw: str) -> str: ... - - def normalize_exchange_symbol(self, symbol: str) -> str: ... - - def get_mark_price(self, symbol: str) -> Optional[float]: ... - - def get_position(self, exchange_symbol: str, direction: str) -> dict[str, Any]: - """返回 {contracts, entry_price, leverage?}.""" - ... - - def amount_to_precision(self, exchange_symbol: str, amount: float) -> Optional[float]: ... - - def price_to_precision(self, exchange_symbol: str, price: float) -> Optional[float]: ... - - def market_add( - self, exchange_symbol: str, direction: str, amount: float, leverage: int - ) -> dict[str, Any]: ... - - def limit_add( - self, - exchange_symbol: str, - direction: str, - amount: float, - price: float, - leverage: int, - ) -> dict[str, Any]: ... - - def cancel_order(self, exchange_symbol: str, order_id: str) -> None: ... - - def replace_position_tpsl( - self, - exchange_symbol: str, - direction: str, - stop_loss: float, - take_profit: float, - order_monitor_row: Any = None, - ) -> None: ... - - def ensure_live_ready(self) -> tuple[bool, str]: ... +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNp9lllsG9cVhjk0N3GRSJEcSrZoK4qcOI5jkdRwXzSkhkOJo5FmxBkOtxFFcZHEEUl4kdfECOI8BC3QBSjaAk1e+tCX9MEunDaFW/mpLVo0QZM0DVCkdVDDgCG4clu06FOBnuvajFLYef4w9/7nP/85d/7QXAs3A41GLRQm6v5gjah5fI0LnvAqbiY8DS7ca+pCGGFomiP1wJo/t8q1Kt6BWDtuXu165bXuDNE8GsZ8qtrdkOzVTd9Y664JRDA0mnCUCcLz6tfe+ug/f799/8dXf/3v2z/85lfeeeeN6/c+fvu3f9558/Xd74x9/M9rt392N1Vw0tRrH37j0+u/dArqBZv1k9/84JOffLBw8E+eBhEea7zr0xSMJmeRlc1lai54a/qMMbnrPekk5apRFB90fIwxZdIPCqX/6W+C/pAnXGsSHvj+or9Zz/4V6tOE6n69v1OvNYVpLCpE5SY3KtS4meu1UBwbUW1pvT/3hD25Ra4rJD6LtWe0IjcpbF+L99Z0BmxCdepurB7T63PnuFduBBJewai9wh27sdqd5nndJHbss9XotIzpHbnZa7V2onNIGNIuc2MCey3W29BhmFs1B99H9Hij5q7sHg0uQM2qHLs9JvGCSVuZXNY6nDKzt0rP2rubYlbP8ZJOpVRF914ko3HeqVOY1ZGVpNq++upNgvCCP+eaa6Fv+6B/lfB6/c3A1FvNUc9ObLZiOUDHoH4mDP11RYC/VwB/zpY3PLMSzmzJmsFkaWucz67YLPPh3/3j969fvfrhv3700R9fe/VbXw2VU6TNVlZkU4lLGq0taxrD4wKXnwF9ok0U3Y1Z1q4hecmtVFidsWDiseiBYpZn04s3J01VcqBY4Q6plMz8QD5f7EG+ftGCfMWJUXfDSWso6UByO6m2iCUJ25YFJ15xjRhNdrFQMC4tJzJjMmcSdmeGx4r5qkGK+j/PZwD6uwr1n2qEvIwC9ad6RNjv6gJ/r2X3Q34zz4E/qQT4w3QQbwO/dVkyUgNaCz/Lx7gldUWzImLrs6RZXy7EwL/MNpy/IAad2QKpW16cXNAVcsaqeEGroUgRPyiVXEOFgephLCEnJZNQ1PGRWWagVBW/175MUZayJAyEqwNFrTVWju7Lnw/y54f5OR/2N6zToB9vw3zZwsBdE8BTfsgn/fB+Be6/3z6gWWgV1ZlKgnIVeSwA+XVNwveLR1o2a8GWO+CeJEsVdZnTbGN8yiqnXDoVqchaernttHK63I6iecQVfL6qkyp3NjCZFvUkVzJ9N4hV9dYIw9JLznNDbCjnfTQfcD4RAX9DoO/5WiiUWQf9XKPuYyPpYeCffv1vH3z/7fRoyViw4qoym1lyLg+RrvmOI1ssaeYdBXGYZwS1XjcychzvQH3ZQHoL6idPw/n203A+Hgr7m1Y/qr8H/KdhTkNGSNnFU9ygCdX3K9Tf+PoC+TT/VzhbfN6YtlgNkrTXFEqDSRN1r7ZSyatsUhDth0f5WId8+MH/CaLpJbvo/u7j+5mIKwr5yU5Bf9KXkL4NxDcRjwB/97imbLINLbRIcztj53XFpEVaeQn1ZxzOX1r0si7JiumSLy+o5PxQTnUqr52jKnZGLFZtJbZts7ScFKY3MFzZ8sBno805SR/t+xv2t0BfEO6/HCbWMnGUz/DjfI6u0wToZ85Avu3PofxG+/ntAd89RzEVnb5lK0YddrkkWyuFoQnIF34Mzj9yeJ3CRZZTH3rZLZq1Q9XKtl7IOHnX4RTO5yoJmF8S50sV3JQ33d8yaBYrtHp5+WbQvELr4/v0rX6Jvto+fVNI3wnEh/vztYHmbwv83+mai0xVl9xQcdgxlO9n4fzF4SDJCDb9ytiZMf1AUcrpzuZUlE0ksyLPGCG/vIorTe97f5qQPz/U93zIE7L3UH9mHueHXc8SqH/nUf/OoP5dQfw44qMoX53+fCXg/l2PxVyUqago8xf7+2d61PG0/WOwmHlWUfO5mzFjKo9le5z2ludS1ciXAvv0rX6JvrV9+pL9fD3MfxD4TqdcKKm4cqLsEiTc3Z9v99PmW60pSaJByWcwPfPi/Y7LKc0rQyVqeK9L5Ybk3rzduCxTO0cf8kjZZDQs5OlYpoDTLdg/5cDn/W02QH8T9F8i6kRaQfp6j/WxPiaE3odxNB8nkf55xFnEl54wv+NpDTXA89g6mR82o/dXYdP44GHE08BHR57mL47ZhIKka2cGM2aqbMr3WBteeH/cmpwvqsJlY9Ewl4T9lMgH9r3faH/64f5nIJ94F/ynN8B/ehO4zcts9fOH9j/eBr53misOJ11ix8DTlYSLFwW3Bul/AfQdnlNYWp1c1rHjc7y5uLKimeRE0sozh1TmrJXCohZ+iY9lllXc2GgA96D+TMP9tAflfw36PxdjjsJ+sU+j+fM+5u545izkg6qDv0wc8XHgbCI1hfZHEO3301/kJ/6fX0H8wtO/v/wE7nvi+VOIH0P6RpA+Z1//K0g/wUz09U/165tA/GJ//7zYn28S+dtB/l4Cf9k1+0nEPYi7EG/1OYX874H/7l7mGcT9iJ9DvI54FvELqH9Z1L+L0L/sQegvjfY3E0P5s0L+bFG6jfg86u/Mnc2IUuIH+Fb71kYL8q1yCBUl9iCaUBzpnCHPC5qIsjcTVQplk4XVpCymIaal2CJo/p9F++cE8ucF5M8F5M9l5A+O6neh+o+g+o+g+h2o/k1UP9HfvzGkfx30I39ptD9ija1VXzRRm/ErgR4Ravl8081ePBDaeD8Yc6H5Os5u/iWhxPiCWonshqLr+QWnleZ7URq9b3fqKbfDkF+h0f6g0f911o/q933h/yzrR/PXg/zf29LIea21wA82wT+8DeevNjrhlwLnA/8FG6EyNQ==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_exchange_binance.py b/lib/strategy/strategy_exchange_binance.py index 4951aca..29041ca 100644 --- a/lib/strategy/strategy_exchange_binance.py +++ b/lib/strategy/strategy_exchange_binance.py @@ -1,4 +1,12 @@ -"""Binance USDT-M 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config).""" -from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter - -__all__ = ["StrategyExchangeAdapter"] +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJuYJRSYWCZKKZnYpASYJmfymbBaMKRymOVbJZkGpwUnJJlyGCTbOmR4BhqkGJSHcPNHM4W7GJoVJMrqSd68WJPU/d8l1sb1jo2zVx0/ePEu/0HWxZ+u3TvQPuCVZdebb33cFvP1v0+F6bvcBRzcAsUCIzzsWIM4w7gZfaP8onlD8wQchAPdGOM90xj4ooKimSWFb8Dcp986r6ybF83JscgNl8Fj0CeyLg4FpWAUAeBQG/3dKB8hW/Cbvu0bPaoELtse+E43ig5L1HxUIdQZl9xN0GBCMFgZlm7WMdw7pBItkCroGDxWCfrxFSTd+bW2RGRnLZpZ8zEEoH+d0sD+v8gI9D/1VypJmKBbs4WATFxAjypisHi/KLhnhmti9tb+u9fW/L4Tnd/y8HejGAxX09HLtsQrgheAT6vNEeBAFfbCJ6gPHEfpwhRhmDfHHEH7xBB9jiZTEYZpoCIKJaADOZY7sAY9lg5T4n9Nhyx3AE+kQwxFsDwF08Hhn+6qXWCXToAe7J/fw==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_exchange_gate.py b/lib/strategy/strategy_exchange_gate.py index 67ea792..1f84c7c 100644 --- a/lib/strategy/strategy_exchange_gate.py +++ b/lib/strategy/strategy_exchange_gate.py @@ -1,9 +1,12 @@ -""" -Gate.io USDT 永续 — 策略交易交易所侧能力. - -实现方式:各 Gate 实例 app 通过 strategy_config.build_strategy_config(app_module) 注入 -ccxt 下单,精度,换 TP/SL;本文件为文档与类型锚点,避免在各 app 重复实现滚仓公式. -""" -from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter - -__all__ = ["StrategyExchangeAdapter"] +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNoBMwLM/dZlYjllNmRkYTg5NGM1N2E0ZzAyZHgwOWEWLjQwZFA5b2UGOAE0CGUMOmM2YjVTYlNkajEAPGM5SGBBMTFkNDNyBBZcS18LRDRrfWBD04fZ096dEoa4pBmGyKLTpcHQg5GD+qqBjMCC+avfvePRicbc5o3X7qMeM2uAmqrX6oTfo9yHheoMgfTlGH5VF1AXhJr71IzvGFFJEUXdtKqMi74VFhZLBEIBAxhnWlsNU14GGgdFWwhcb0oVF1VAVQNNZlYKDF8MUUwFEUhmWQxRQg1RTBDU15DVvMRvV1dIEBTdje6HtPAag9bf3YOST9O6wxQxYB03dAvf/cnSoreAj4/R3djf87GCxcLcgbqEhIyEqu7Zpv7fsoBNjLWL1eG53KnNh6nhFgUUERjQs+7Qk+zRy67V6ojWgvuBj6fV4ZjciepMM4w2ZGRhETjudGZDE1URVVUdfUhaCQRaU1UlUFhFEQdLFzRkZGF2EDC5Mmg+UApTbTtiE1UIBxpHRBZVTVACGxcWQhYFFV1eTTxQTwJcBF5VAWdSWBIARjYwZDTjMjo9WAlaOzvIOEsxYzU3EzFlMDKRWTA5YSYOaGUXUUtGOQZcDkMKOCVdSl8XWkc90vOA19+C1q/mgY+C1cCNZVYXG0kRWTsJDlZQQAxHaBRHAEJuCFFSZRIRRlVEAVNAaRYWSwRCAQMYZ1xMAF1WD1MAb1UFTFUXERzuPAwJW11ACQcHZDZkZBI+OTRjMTdtMm8yiafaEQ==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_exchange_okx.py b/lib/strategy/strategy_exchange_okx.py index 5ea0963..f972006 100644 --- a/lib/strategy/strategy_exchange_okx.py +++ b/lib/strategy/strategy_exchange_okx.py @@ -1,4 +1,12 @@ -"""OKX 永续 — 策略交易交易所适配(见 strategy_config.build_strategy_config).""" -from lib.strategy.strategy_exchange_base import StrategyExchangeAdapter - -__all__ = ["StrategyExchangeAdapter"] +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJuYJRSYWCZKKZnYpASYJmfymbBaMKRymOVbJZkGpwUnJJlyGCTbOmR4JhkkGJSVmfrdP9qX/P9MxK3t3wXvXBmUdPS6Q1NU+7//Hahb0rTmlWX3zy68HRL78M3AhcnPxJ1EXVjcQlndsyP4uMOig33CvOJ4WRLE3MSYxWNjfO1CYvgD+IJl/a6CLQ/Vdb0VXG6rzuLWAyTv6IMU0BEFEtAGWuoqEu4mJcRUL5K1mSfcVoWI2+UVaadO29gtJSAoyuDA0O4t5e3izeDYGCwp3WgYxgvczhTsDUbg3eMWyLQf++S8nNZQ2LS7M6YuJmmmFi6JwD9f5gF6P8qPvsEFxY3sZwwxmDXcFvFQPdogWjPzOYvnQ2995q+77/bf/XC5BsZbE7eIj7xaTxcUbEu3G5pDmLs3pZRvGy23r5uTI5BbL6Wzm5ikS4xbDLZgR7sMRHRTOxpXLEyXoKOj23iIyJYHTlDeVItDCyFkk1MDBJMTE3yEwBT5X1P' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_records_register.py b/lib/strategy/strategy_records_register.py index ed2b45a..8507478 100644 --- a/lib/strategy/strategy_records_register.py +++ b/lib/strategy/strategy_records_register.py @@ -1,72 +1,12 @@ -"""策略交易记录页:已结束趋势 / 顺势加仓快照(三所统一).""" -from __future__ import annotations - -import json -from typing import Any - -from flask import flash, redirect, url_for - -from lib.strategy.strategy_snapshot_lib import ( - STRATEGY_SNAPSHOTS_MAX_ROWS, - dedupe_strategy_snapshots, - list_strategy_snapshots_split, -) - - -def load_strategy_records_page( - conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS -) -> dict[str, Any]: - try: - if dedupe_strategy_snapshots(conn): - conn.commit() - except Exception: - pass - trend, roll, symbols = list_strategy_snapshots_split(conn, limit=limit) - return { - "strategy_trend_records": trend, - "strategy_roll_records": roll, - "strategy_record_symbols": symbols, - "strategy_records_limit": limit, - "strategy_snapshots": trend + roll, - } - - -def register_strategy_records(app, cfg: dict[str, Any]) -> None: - login_required = cfg["login_required"] - get_db = cfg["get_db"] - - def _lr(f): - return login_required(f) - - @_lr - @app.route("/strategy/records") - def strategy_records_page(): - m = cfg.get("app_module") - fn = getattr(m, "render_main_page", None) - if not callable(fn): - flash("render_main_page 未配置") - return redirect(url_for("strategy_trading_page")) - return fn("strategy_records") - - @_lr - @app.route("/strategy/records/") - def strategy_records_detail(snap_id: int): - conn = get_db() - row = conn.execute( - "SELECT * FROM strategy_trade_snapshots WHERE id=?", - (int(snap_id),), - ).fetchone() - conn.close() - if not row: - flash("未找到该策略快照") - return redirect(url_for("strategy_records_page")) - try: - snap = json.loads(row["snapshot_json"] or "{}") - except Exception: - snap = {} - dca = snap.get("dca_levels") or [] - flash( - f"快照 #{snap_id} {row['strategy_type']} {row['symbol']} " - f"{row['result_label']} · 补仓档 {len(dca)} 项(详情见列表页)" - ) - return redirect(url_for("strategy_records_page")) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_register.py b/lib/strategy/strategy_register.py index 00575fa..93289df 100644 --- a/lib/strategy/strategy_register.py +++ b/lib/strategy/strategy_register.py @@ -1,654 +1,12 @@ -"""策略交易:Flask 路由注册(顺势加仓 + 趋势回调页).逻辑在 strategy_*_lib.""" -from __future__ import annotations - -from lib.paths import strategy_templates_dir - -import html as html_module -import os -import re -from typing import Any, Optional - -from flask import Flask, flash, jsonify, redirect, render_template, request, url_for -from jinja2 import ChoiceLoader, FileSystemLoader - -from lib.strategy.strategy_db import init_strategy_tables -from lib.strategy.strategy_roll_lib import BREAKOUT_MODE, FIB_MODES, MARKET_MODE, preview_roll -from lib.strategy.strategy_roll_monitor_lib import ( - cancel_roll_pending_leg, - count_filled_roll_legs, - count_pending_roll_legs, - sync_roll_after_external_close, -) - - -def _dedupe_strategy_snapshots_on_startup(cfg: dict[str, Any]) -> None: - """启动时清理历史重复快照(同计划同结果仅保留最新一条).""" - get_db = cfg.get("get_db") - if not callable(get_db): - return - try: - from lib.strategy.strategy_snapshot_lib import dedupe_strategy_snapshots - - conn = get_db() - try: - removed = dedupe_strategy_snapshots(conn) - if removed: - conn.commit() - print( - f"[strategy] deduped {removed} duplicate strategy_trade_snapshots", - flush=True, - ) - finally: - conn.close() - except Exception as e: - print(f"[strategy] snapshot dedupe skipped: {e}", flush=True) - - -def install_strategy_trading(app: Flask, repo_root: str, app_module: Any = None, **build_kw) -> None: - """在 app.py 末尾调用(login_required 已定义后).仅注册 POST API;页面由各 app 的 render_main_page 渲染.""" - from lib.strategy.strategy_config import build_strategy_config - - build_kw.pop("render_trend_page", None) - attach_strategy_templates(app, repo_root) - cfg = build_strategy_config(app_module, **build_kw) - register_strategy_trading(app, cfg) - from lib.strategy.strategy_records_register import register_strategy_records - - register_strategy_records(app, cfg) - app.extensions["strategy_roll_cfg"] = cfg - _dedupe_strategy_snapshots_on_startup(cfg) - - -def attach_strategy_templates(app: Flask, repo_root: str) -> None: - strat_dir = strategy_templates_dir(repo_root) - if not os.path.isdir(strat_dir): - return - existing = app.jinja_loader - loaders = [FileSystemLoader(strat_dir)] - if existing is not None: - if isinstance(existing, ChoiceLoader): - loaders = list(existing.loaders) + loaders - else: - loaders.insert(0, existing) - app.jinja_loader = ChoiceLoader(loaders) - - -def register_strategy_trading(app: Flask, cfg: dict[str, Any]) -> None: - """cfg 由各市面 app 注入回调(仅 API / DB 差异).""" - - login_required = cfg["login_required"] - - def _lr(f): - return login_required(f) - - @_lr - @app.route("/strategy/roll/preview", methods=["POST"]) - def strategy_roll_preview(): - data = request.get_json(silent=True) or request.form - err = _roll_preview_response(cfg, data, json_mode=request.is_json) - if request.is_json: - return jsonify(err) - if err.get("ok"): - p = err["preview"] - flash( - f"预览:约 {p.get('add_amount_display', '-')} 张," - f"合并均价 {p.get('avg_entry_after', '-')}," - f"打到止损约 {p.get('loss_at_sl_usdt', '-')}U" - ) - else: - flash(err.get("msg") or "预览失败") - return redirect(url_for("strategy_trading_page")) - - @_lr - @app.route("/strategy/roll/execute", methods=["POST"]) - def strategy_roll_execute(): - data = request.form - try: - ok, msg = _roll_execute(cfg, data) - except Exception as e: - fe = cfg.get("friendly_error") - msg = fe(e) if callable(fe) else str(e) - ok = False - flash(msg) - return redirect(url_for("strategy_trading_page")) - - @_lr - @app.route("/strategy/roll/cancel/", methods=["POST"]) - def strategy_roll_cancel_leg(leg_id: int): - conn = cfg["get_db"]() - try: - init_strategy_tables(conn) - ok, msg = cancel_roll_pending_leg(cfg, conn, leg_id) - finally: - conn.close() - if request.is_json: - return jsonify({"ok": ok, "msg": msg}) - flash(msg) - return redirect(url_for("strategy_trading_page")) - - @_lr - @app.route("/strategy/roll/docs") - def strategy_roll_docs(): - path = _resolve_roll_doc_path() - if not path: - flash("滚仓说明文档不存在") - return redirect(url_for("strategy_trading_page")) - with open(path, encoding="utf-8") as f: - raw = f.read() - return render_template( - "strategy_roll_docs.html", - doc_html=_roll_doc_markdown_to_html(raw), - exchange_display=cfg.get("exchange_display") or "", - ) - - -def _resolve_roll_doc_path() -> str | None: - """滚仓说明 md:优先包内,否则仓库根目录(顺势加仓滚仓说明.md).""" - here = os.path.dirname(os.path.abspath(__file__)) - name = "顺势加仓滚仓说明.md" - for path in ( - os.path.join(here, name), - os.path.normpath(os.path.join(here, "..", "..", name)), - ): - if os.path.isfile(path): - return path - return None - - -def _roll_doc_markdown_to_html(text: str) -> str: - """轻量 Markdown → HTML(仅供滚仓说明页).""" - lines = text.splitlines() - out: list[str] = [] - i = 0 - in_code = False - code_buf: list[str] = [] - - def flush_code() -> None: - nonlocal code_buf - if code_buf: - out.append( - "
"
-                + html_module.escape("\n".join(code_buf))
-                + "
" - ) - code_buf = [] - - def inline_md(s: str) -> str: - s = html_module.escape(s) - s = re.sub(r"`([^`]+)`", r"\1", s) - s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s) - return s - - while i < len(lines): - line = lines[i] - if line.strip().startswith("```"): - if in_code: - in_code = False - flush_code() - else: - in_code = True - i += 1 - continue - if in_code: - code_buf.append(line) - i += 1 - continue - if line.startswith("# "): - out.append(f"

{inline_md(line[2:].strip())}

") - elif line.startswith("## "): - out.append(f"

{inline_md(line[3:].strip())}

") - elif line.startswith("### "): - out.append(f"

{inline_md(line[4:].strip())}

") - elif line.strip() == "---": - out.append("
") - elif line.startswith("|") and "|" in line[1:]: - rows: list[str] = [] - while i < len(lines) and lines[i].startswith("|"): - rows.append(lines[i]) - i += 1 - if len(rows) >= 2 and re.match(r"^\|[\s\-:|]+\|$", rows[1].strip()): - out.append("") - hdr = [c.strip() for c in rows[0].strip("|").split("|")] - out.append("" + "".join(f"" for c in hdr) + "") - for row in rows[2:]: - cells = [c.strip() for c in row.strip("|").split("|")] - out.append("" + "".join(f"" for c in cells) + "") - out.append("
{inline_md(c)}
{inline_md(c)}
") - continue - elif re.match(r"^[-*]\s+", line): - out.append("
    ") - while i < len(lines) and re.match(r"^[-*]\s+", lines[i]): - item = re.sub(r"^[-*]\s+", "", lines[i]) - out.append(f"
  • {inline_md(item)}
  • ") - i += 1 - out.append("
") - continue - elif line.strip(): - out.append(f"

{inline_md(line.strip())}

") - i += 1 - flush_code() - return "\n".join(out) - - -def _row_to_dict(row) -> dict: - if row is None: - return {} - try: - return dict(row) - except Exception: - return {} - - -def _count_active_trends(conn, cfg: dict) -> int: - fn = cfg.get("count_active_trend_plans") - if callable(fn): - return int(fn(conn) or 0) - try: - return int( - conn.execute( - "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'" - ).fetchone()[0] - ) - except Exception: - return 0 - - -def _risk_from_monitor(mon: dict, cfg: dict) -> tuple[Optional[float], Optional[str]]: - try: - rp = float(mon.get("risk_percent") or cfg.get("default_risk_percent", 2)) - except (TypeError, ValueError): - return None, "监控单风险%无效" - if rp <= 0: - return None, "监控单风险%须大于0" - return rp, None - - -def _contract_size(cfg: dict, ex_sym: str) -> float: - get_cs = cfg.get("get_contract_size") - if callable(get_cs): - try: - return float(get_cs(ex_sym) or 1.0) - except Exception: - pass - return 1.0 - - -def _roll_context(cfg: dict, data: dict) -> tuple[Optional[dict], Optional[str]]: - m = cfg.get("app_module") - if m is not None: - try: - from lib.trade.position_sizing_lib import OPEN_SOURCE_ROLL, assert_open_source_allowed - - mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk" - ok_src, src_msg = assert_open_source_allowed(mode, OPEN_SOURCE_ROLL) - if not ok_src: - return None, src_msg - except Exception: - pass - get_db = cfg["get_db"] - symbol = cfg["normalize_symbol_input"](data.get("symbol") or "") - if not symbol: - return None, "请选择或填写币种" - direction = (data.get("direction") or "long").strip().lower() - validate_fn = getattr(m, "validate_trade_policy_open", None) if m is not None else None - if callable(validate_fn): - ok_pol, pol_msg = validate_fn(symbol, direction) - if not ok_pol: - return None, pol_msg - ex_sym = cfg["normalize_exchange_symbol"](symbol) - conn = get_db() - init_strategy_tables(conn) - if _count_active_trends(conn, cfg) > 0: - conn.close() - return None, "存在运行中的趋势回调计划,请先结束后再滚仓" - mon = _get_active_monitor(conn, cfg, symbol, direction) - if not mon: - conn.close() - return None, "未找到该币种同向的下单监控持仓,请先在「实盘下单」开仓" - rg, legs_done, pending, roll_is_new = _get_or_create_roll_group_meta(conn, mon) - if pending > 0: - conn.close() - return None, "已有监控中的滚仓腿,请等待成交/失效或先删除后再提交" - conn_cap = get_db() - try: - capital = float(cfg["get_trading_capital_usdt"](conn_cap)) - finally: - conn_cap.close() - risk_pct, risk_err = _risk_from_monitor(mon, cfg) - if risk_err: - conn.close() - return None, risk_err - pos = cfg["get_position"](ex_sym, direction) - qty = float(pos.get("contracts") or 0) - if qty <= 0: - conn.close() - return None, "交易所无该方向持仓,无法滚仓" - entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0) - if entry <= 0: - conn.close() - return None, "无法获取持仓均价" - mark_fn = cfg.get("get_mark_price") or cfg.get("get_price") - mark = mark_fn(symbol) if callable(mark_fn) else cfg["get_price"](symbol) - ctx = { - "conn": conn, - "mon": mon, - "rg": rg, - "legs_done": legs_done, - "symbol": symbol, - "direction": direction, - "ex_sym": ex_sym, - "qty": qty, - "entry": entry, - "mark": float(mark) if mark else None, - "capital": capital, - "risk_pct": float(risk_pct), - "tp0": float(mon.get("take_profit") or rg.get("initial_take_profit") or 0), - "contract_size": _contract_size(cfg, ex_sym), - } - return ctx, None - - -def _parse_roll_form(data: dict, ctx: dict) -> tuple[Optional[dict], Optional[str]]: - add_mode = (data.get("add_mode") or MARKET_MODE).strip().lower() - raw_sl = data.get("new_stop_loss") or data.get("sl") - if raw_sl in (None, ""): - return None, "请填写新止损价" - try: - new_sl = float(raw_sl) - except (TypeError, ValueError): - return None, "止损价格式错误" - if new_sl <= 0: - return None, "止损价须大于0" - fib_u = fib_l = bp = None - try: - if data.get("fib_upper") not in (None, ""): - fib_u = float(data.get("fib_upper")) - if data.get("fib_lower") not in (None, ""): - fib_l = float(data.get("fib_lower")) - if data.get("breakthrough_price") not in (None, ""): - bp = float(data.get("breakthrough_price")) - except (TypeError, ValueError): - return None, "价格参数格式错误" - - add_price = ctx.get("mark") - if add_mode == MARKET_MODE: - if add_price is None or add_price <= 0: - return None, "无法获取市价快照" - elif add_mode in FIB_MODES: - if fib_u is None or fib_l is None: - return None, "斐波须填写上沿 H 与下沿 L" - elif add_mode == BREAKOUT_MODE: - if bp is None: - return None, "突破加仓须填写突破价" - add_price = ctx.get("mark") - else: - return None, "加仓方式无效" - - return { - "add_mode": add_mode, - "new_stop_loss": new_sl, - "fib_upper": fib_u, - "fib_lower": fib_l, - "breakthrough_price": bp, - "add_price": add_price, - }, None - - -def _roll_preview_response(cfg: dict, data: dict, json_mode: bool = False) -> dict: - ctx, err = _roll_context(cfg, data) - if err: - return {"ok": False, "msg": err} - parsed, perr = _parse_roll_form(data, ctx) - if perr: - ctx["conn"].close() - return {"ok": False, "msg": perr} - conn = ctx["conn"] - try: - preview, perr2 = preview_roll( - direction=ctx["direction"], - symbol=ctx["symbol"], - qty_existing=ctx["qty"], - entry_existing=ctx["entry"], - initial_take_profit=ctx["tp0"], - add_mode=parsed["add_mode"], - new_stop_loss=parsed["new_stop_loss"], - risk_percent=ctx["risk_pct"], - capital_base_usdt=ctx["capital"], - add_price=parsed["add_price"], - fib_upper=parsed["fib_upper"], - fib_lower=parsed["fib_lower"], - breakthrough_price=parsed["breakthrough_price"], - legs_done=ctx["legs_done"], - contract_size=ctx["contract_size"], - ) - finally: - conn.close() - if perr2: - return {"ok": False, "msg": perr2} - amt_raw = float(preview["add_amount_raw"]) - amt_p = cfg["amount_to_precision"](ctx["ex_sym"], amt_raw) - preview["add_amount_display"] = amt_p if amt_p is not None else amt_raw - preview["risk_display"] = f"{ctx['risk_pct']:g}%≈{ctx['capital'] * ctx['risk_pct'] / 100:.2f}U" - price_fmt = cfg.get("price_fmt") - if callable(price_fmt): - preview["add_price_display"] = price_fmt(ctx["symbol"], preview["add_price"]) - preview["new_sl_display"] = price_fmt(ctx["symbol"], preview["new_stop_loss"]) - preview["tp_display"] = price_fmt(ctx["symbol"], preview["initial_take_profit"]) - return {"ok": True, "preview": preview} - - -def _roll_execute(cfg: dict, data: dict) -> tuple[bool, str]: - get_db = cfg["get_db"] - conn = None - try: - ok_live, reason = cfg["ensure_live_ready"]() - if not ok_live: - return False, reason or "实盘未就绪" - prev = _roll_preview_response(cfg, data) - if not prev.get("ok"): - return False, prev.get("msg") or "预览失败" - preview = prev["preview"] - symbol = cfg["normalize_symbol_input"](data.get("symbol") or "") - direction = preview["direction"] - ex_sym = cfg["normalize_exchange_symbol"](symbol) - add_mode = preview["add_mode"] - new_sl = float(preview["new_stop_loss"]) - tp0 = float(preview["initial_take_profit"]) - lev_fn = cfg.get("default_leverage") - if not callable(lev_fn): - lev_fn = lambda _s: 5 - leverage = int(data.get("leverage") or 0) or int(lev_fn(symbol)) - conn = get_db() - init_strategy_tables(conn) - mon = _get_active_monitor(conn, cfg, symbol, direction) - if not mon: - return False, "监控单已不存在" - rg, legs_done, pending, roll_is_new = _get_or_create_roll_group_meta(conn, mon) - if pending > 0: - return False, "已有监控中的滚仓腿,请先删除或等待结束" - if add_mode == MARKET_MODE: - amount = cfg["amount_to_precision"](ex_sym, float(preview["add_amount_raw"])) - if amount is None or amount <= 0: - return False, "加仓张数低于交易所最小精度" - order = cfg["market_add"](ex_sym, direction, amount, leverage) - fill = float( - cfg.get("resolve_fill_price", lambda o, s, p: p)( - order, ex_sym, preview["add_price"] - ) - or preview["add_price"] - ) - oid = str(order.get("id") or "") if isinstance(order, dict) else "" - cfg["replace_tpsl"](ex_sym, direction, new_sl, tp0, mon) - conn.execute( - """INSERT INTO roll_legs ( - roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_price, - breakthrough_price, fill_price, amount, new_stop_loss, exchange_order_id, - status, created_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - rg["id"], - legs_done + 1, - preview["add_mode_label"], - preview.get("fib_upper"), - preview.get("fib_lower"), - None, - preview.get("breakthrough_price"), - fill, - amount, - new_sl, - oid, - "filled", - cfg["app_now_str"](), - ), - ) - conn.execute( - "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?", - (legs_done + 1, new_sl, cfg["app_now_str"](), rg["id"]), - ) - live_qty = float(mon.get("order_amount") or 0) + float(amount) - try: - from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty - - pos2 = cfg["get_position"](ex_sym, direction) or {} - q2 = normalize_contracts_qty(pos2.get("contracts") or 0) - if contracts_qty_is_open(q2): - live_qty = q2 - else: - live_qty = normalize_contracts_qty(live_qty) - except Exception: - from lib.hub.hub_position_metrics import normalize_contracts_qty - - live_qty = normalize_contracts_qty(live_qty) - conn.execute( - """UPDATE order_monitors SET stop_loss=?, order_amount=?, - breakeven_armed=0, breakeven_price=NULL WHERE id=?""", - (new_sl, live_qty, mon["id"]), - ) - conn.commit() - _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new) - return True, f"市价加仓第 {legs_done + 1} 腿已成交,止损已更新,止盈仍为首仓" - # 程序监控:斐波 / 突破 - limit_px = None - if add_mode in FIB_MODES: - px_fn = cfg.get("price_to_precision") - limit_px = float(preview["add_price"]) - if callable(px_fn): - limit_px = float(px_fn(ex_sym, limit_px) or limit_px) - mark_fn = cfg.get("get_mark_price") or cfg.get("get_price") - last_mark = mark_fn(symbol) if callable(mark_fn) else preview["add_price"] - conn.execute( - """INSERT INTO roll_legs ( - roll_group_id, leg_index, add_mode, fib_upper, fib_lower, limit_price, - breakthrough_price, new_stop_loss, last_mark_price, status, created_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?)""", - ( - rg["id"], - legs_done + 1, - preview["add_mode_label"], - preview.get("fib_upper"), - preview.get("fib_lower"), - limit_px, - preview.get("breakthrough_price"), - new_sl, - last_mark, - "pending", - cfg["app_now_str"](), - ), - ) - conn.commit() - _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, roll_is_new=roll_is_new) - return True, f"已提交{preview['add_mode_label']}监控,触价后将市价加仓并更新止损" - except Exception as e: - fe = cfg.get("friendly_error") - return False, fe(e) if callable(fe) else str(e) - finally: - if conn is not None: - try: - conn.close() - except Exception: - pass - - -def _maybe_notify_roll_started(cfg, rg, mon, symbol, direction, tp0, new_sl, *, roll_is_new: bool) -> None: - if not roll_is_new: - return - try: - from lib.strategy.strategy_wechat_notify import notify_roll_group_started - - notify_roll_group_started( - cfg, - group_id=int(rg["id"]), - symbol=symbol, - direction=direction, - order_monitor_id=int(mon["id"]), - initial_take_profit=tp0, - initial_stop_loss=float(mon.get("stop_loss") or new_sl), - ) - except Exception: - pass - - -def _get_active_monitor(conn, cfg: dict, symbol: str, direction: str) -> Optional[dict]: - row = conn.execute( - "SELECT * FROM order_monitors WHERE status='active' AND symbol=? AND direction=? ORDER BY id DESC LIMIT 1", - (symbol, direction), - ).fetchone() - return _row_to_dict(row) if row else None - - -def _get_or_create_roll_group_meta(conn, mon: dict) -> tuple[dict, int, int, bool]: - """返回 (roll_group, filled_legs, pending_legs, is_new_group).""" - row = conn.execute( - "SELECT * FROM roll_groups WHERE order_monitor_id=? AND status='active' ORDER BY id DESC LIMIT 1", - (mon["id"],), - ).fetchone() - if row: - d = _row_to_dict(row) - gid = int(d["id"]) - filled = count_filled_roll_legs(conn, gid) - pending = count_pending_roll_legs(conn, gid) - return d, filled, pending, False - now = mon.get("created_at") or "" - cur = conn.execute( - """INSERT INTO roll_groups ( - order_monitor_id, symbol, exchange_symbol, direction, - initial_take_profit, initial_stop_loss, current_stop_loss, - risk_percent, leg_count, status, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - mon["id"], - mon["symbol"], - mon.get("exchange_symbol"), - mon["direction"], - mon.get("take_profit"), - mon.get("stop_loss"), - mon.get("stop_loss"), - mon.get("risk_percent") or 2, - 0, - "active", - now, - now, - ), - ) - gid = int(cur.lastrowid) - return ( - { - "id": gid, - "leg_count": 0, - "initial_take_profit": mon.get("take_profit"), - "initial_stop_loss": mon.get("stop_loss"), - "symbol": mon.get("symbol"), - "direction": mon.get("direction"), - }, - 0, - 0, - True, - ) - - -def roll_sync_after_external_close(cfg: dict, conn, symbol: str, direction: str) -> dict: - """供 hub / del_order 调用的滚仓同步入口.""" - return sync_roll_after_external_close( - cfg, conn, symbol, direction, reason="手动平仓,滚仓监控已结束" - ) - +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_roll_lib.py b/lib/strategy/strategy_roll_lib.py index 1979e64..0338204 100644 --- a/lib/strategy/strategy_roll_lib.py +++ b/lib/strategy/strategy_roll_lib.py @@ -1,392 +1,12 @@ -"""顺势加仓(滚仓):纯计算.人工触发;止盈锁定首仓;程序监控触价市价成交.""" -from __future__ import annotations - -from typing import Any, Optional, Tuple - -from lib.key_monitor.fib_key_monitor_lib import calc_fib_plan, fib_invalidate_by_mark - -ROLL_MAX_LEGS_LONG = 3 -ROLL_MAX_LEGS_SHORT = 3 - -MARKET_MODE = "market" -FIB_MODES = frozenset({"fib_618", "fib_786"}) -BREAKOUT_MODE = "breakout" - -MODE_LABELS = { - MARKET_MODE: "市价加仓", - "fib_618": "斐波0.618", - "fib_786": "斐波0.786", - BREAKOUT_MODE: "突破加仓", -} - - -def fib_ratio_from_mode(mode: str) -> Optional[float]: - m = (mode or "").strip().lower() - if m in ("fib_618", "618", "0.618"): - return 0.618 - if m in ("fib_786", "786", "0.786"): - return 0.786 - return None - - -def mode_label(mode: str) -> str: - m = (mode or MARKET_MODE).strip().lower() - return MODE_LABELS.get(m, m) - - -def fib_limit_entry(direction: str, upper: float, lower: float, mode: str) -> Tuple[Optional[float], Optional[str]]: - """H/L 仅用于计算限价加仓价;多:下沿=止损侧;空:上沿=止损侧.""" - ratio = fib_ratio_from_mode(mode) - if ratio is None: - return None, "斐波档位无效" - h, l = float(upper), float(lower) - if h <= l: - return None, "上沿须大于下沿" - direction = (direction or "long").strip().lower() - if direction == "short": - plan = calc_fib_plan("short", h, l, ratio) - else: - plan = calc_fib_plan("long", h, l, ratio) - if not plan: - return None, "无法计算斐波限价" - entry, _sl, _tp = plan - return float(entry), None - - -def max_roll_legs(direction: str) -> int: - return ROLL_MAX_LEGS_LONG if (direction or "long").strip().lower() == "long" else ROLL_MAX_LEGS_SHORT - - -def avg_entry_after_add( - qty_existing: float, - entry_existing: float, - add_qty: float, - add_price: float, -) -> float: - q1 = float(qty_existing) - e1 = float(entry_existing) - q2 = float(add_qty) - e2 = float(add_price) - total = q1 + q2 - if total <= 0: - return 0.0 - return (q1 * e1 + q2 * e2) / total - - -def calc_risk_budget_usdt(capital_base_usdt: float, risk_percent: float) -> float: - return float(capital_base_usdt) * (float(risk_percent) / 100.0) - - -def solve_add_amount_for_total_risk( - direction: str, - qty_existing: float, - entry_existing: float, - add_price: float, - new_stop: float, - risk_budget_usdt: float, - contract_size: float = 1.0, -) -> Tuple[Optional[float], Optional[str]]: - """ - 合并持仓打到 new_stop 时总亏损 ≈ risk_budget(方案 C). - long: (avg - SL) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(E1-SL)) / (E2-SL) - short: (SL - avg) * (Q1+Q2) * cs = B => Q2 = (B/cs - Q1*(SL-E1)) / (SL-E2) - """ - try: - q1 = float(qty_existing) - e1 = float(entry_existing) - e2 = float(add_price) - sl = float(new_stop) - b = float(risk_budget_usdt) - cs = float(contract_size) if contract_size else 1.0 - except (TypeError, ValueError): - return None, "参数格式错误" - if q1 <= 0 or e1 <= 0 or e2 <= 0 or b <= 0 or cs <= 0: - return None, "持仓或风险预算无效" - direction = (direction or "long").strip().lower() - if direction == "short": - denom = sl - e2 - numer = b / cs - q1 * (sl - e1) - if denom <= 0: - return None, "做空:新止损须高于加仓价" - else: - denom = e2 - sl - numer = b / cs - q1 * (e1 - sl) - if denom <= 0: - return None, "做多:新止损须低于加仓价" - q2 = numer / denom - if q2 <= 0: - return None, "按当前新止损与风险预算,无需加仓或无法再加(已满足风险上限)" - return q2, None - - -def loss_at_stop_usdt( - direction: str, - avg: float, - qty: float, - stop: float, - contract_size: float = 1.0, -) -> float: - cs = float(contract_size or 1.0) - direction = (direction or "long").strip().lower() - if direction == "short": - return (float(stop) - float(avg)) * float(qty) * cs - return (float(avg) - float(stop)) * float(qty) * cs - - -def reward_at_tp_usdt( - direction: str, - avg: float, - take_profit: float, - qty: float, - contract_size: float = 1.0, -) -> float: - cs = float(contract_size or 1.0) - direction = (direction or "long").strip().lower() - if direction == "short": - gross = (float(avg) - float(take_profit)) * float(qty) * cs - else: - gross = (float(take_profit) - float(avg)) * float(qty) * cs - try: - from lib.trade.trade_fee_lib import net_pnl_after_fee - - net = net_pnl_after_fee(gross, avg, take_profit, qty, cs) - return float(net) if net is not None else gross - except Exception: - return gross - - -def roll_fib_trigger_crossed( - direction: str, - prev_mark: Optional[float], - mark: float, - limit_price: float, -) -> bool: - """斐波:多=向下穿越限价;空=向上穿越限价.""" - try: - m = float(mark) - lv = float(limit_price) - pm = float(prev_mark) if prev_mark is not None else None - except (TypeError, ValueError): - return False - direction = (direction or "long").strip().lower() - if direction == "long": - if pm is None: - return m <= lv - return pm > lv and m <= lv - if pm is None: - return m >= lv - return pm < lv and m >= lv - - -def roll_breakout_trigger_crossed( - direction: str, - prev_mark: Optional[float], - mark: float, - breakthrough_price: float, -) -> bool: - """突破:多=mark 在突破价之上;空=mark 在突破价之下. - - 提交时已校验 mark 在逆势侧(多低于突破价,空高于突破价),触价侧到达即成交. - 不再要求单 tick 内穿越,避免 mark 已破位但 last_mark 也落在突破价另一侧时永久漏触发. - """ - try: - m = float(mark) - bp = float(breakthrough_price) - except (TypeError, ValueError): - return False - direction = (direction or "long").strip().lower() - if direction == "long": - return m > bp - return m < bp - - -def roll_fib_invalidate(direction: str, mark: float, upper: float, lower: float) -> bool: - """斐波 pending 失效:止盈侧突破(多 mark>=H;空 mark<=L).""" - return fib_invalidate_by_mark(direction, mark, upper, lower) - - -def roll_breakout_invalidate(direction: str, mark: float, stop_loss: float) -> bool: - """突破 pending 失效:未到突破价先触达止损侧(多 mark<=S;空 mark>=S).""" - try: - m = float(mark) - sl = float(stop_loss) - except (TypeError, ValueError): - return False - direction = (direction or "long").strip().lower() - if direction == "long": - return m <= sl - return m >= sl - - -def validate_roll_geometry( - direction: str, - add_mode: str, - *, - new_stop_loss: float, - add_price: Optional[float] = None, - fib_upper: Optional[float] = None, - fib_lower: Optional[float] = None, - breakthrough_price: Optional[float] = None, - entry_existing: float = 0.0, - initial_take_profit: float = 0.0, - mark_price: Optional[float] = None, -) -> Optional[str]: - direction = (direction or "long").strip().lower() - mode = (add_mode or MARKET_MODE).strip().lower() - try: - sl = float(new_stop_loss) - tp = float(initial_take_profit) - e1 = float(entry_existing or 0) - except (TypeError, ValueError): - return "止损/止盈格式错误" - if sl <= 0 or tp <= 0: - return "止损与首仓止盈须大于0" - if direction == "long": - if e1 > 0 and tp <= e1: - return "做多:首仓止盈须高于当前持仓均价" - else: - if e1 > 0 and tp >= e1: - return "做空:首仓止盈须低于当前持仓均价" - - if mode == MARKET_MODE: - if add_price is None or float(add_price) <= 0: - return "市价加仓需要有效参考价" - entry_add = float(add_price) - elif mode in FIB_MODES: - if fib_upper is None or fib_lower is None: - return "斐波须填写上沿 H 与下沿 L" - entry_add, err = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode) - if err: - return err - if entry_add is None or entry_add <= 0: - return "无法计算斐波限价" - elif mode == BREAKOUT_MODE: - if breakthrough_price is None: - return "突破加仓须填写突破价" - try: - bp = float(breakthrough_price) - except (TypeError, ValueError): - return "突破价格式错误" - if bp <= 0: - return "突破价须大于0" - entry_add = bp - if direction == "long": - if sl >= bp: - return "做多:止损须低于突破价" - if mark_price is not None and float(mark_price) >= bp: - return "做多:当前价须低于突破价(等待向上突破)" - else: - if sl <= bp: - return "做空:止损须高于突破价" - if mark_price is not None and float(mark_price) <= bp: - return "做空:当前价须高于突破价(等待向下跌破)" - else: - return "加仓方式无效" - - if mode != BREAKOUT_MODE: - entry_add = float(entry_add) # type: ignore[arg-type] - if direction == "long": - if sl >= entry_add: - return "做多:新止损须低于加仓价" - else: - if sl <= entry_add: - return "做空:新止损须高于加仓价" - return None - - -def preview_roll( - *, - direction: str, - symbol: str, - qty_existing: float, - entry_existing: float, - initial_take_profit: float, - add_mode: str, - new_stop_loss: Optional[float] = None, - risk_percent: float, - capital_base_usdt: float, - add_price: Optional[float] = None, - fib_upper: Optional[float] = None, - fib_lower: Optional[float] = None, - breakthrough_price: Optional[float] = None, - legs_done: int = 0, - contract_size: float = 1.0, -) -> Tuple[Optional[dict[str, Any]], Optional[str]]: - direction = (direction or "long").strip().lower() - if legs_done >= max_roll_legs(direction): - return None, f"{'做多' if direction == 'long' else '做空'}滚仓已达 {max_roll_legs(direction)} 次上限" - mode = (add_mode or MARKET_MODE).strip().lower() - if new_stop_loss is None: - return None, "请填写新止损价" - try: - sl = float(new_stop_loss) - except (TypeError, ValueError): - return None, "止损价格式错误" - if sl <= 0: - return None, "止损须大于0" - - geom_err = validate_roll_geometry( - direction, - mode, - new_stop_loss=sl, - add_price=add_price, - fib_upper=fib_upper, - fib_lower=fib_lower, - breakthrough_price=breakthrough_price, - entry_existing=entry_existing, - initial_take_profit=initial_take_profit, - mark_price=add_price if mode == BREAKOUT_MODE else add_price, - ) - if geom_err: - return None, geom_err - - if mode == MARKET_MODE: - entry_add = float(add_price) # validated - elif mode in FIB_MODES: - entry_add, _ = fib_limit_entry(direction, float(fib_upper), float(fib_lower), mode) - entry_add = float(entry_add or 0) - else: - entry_add = float(breakthrough_price or 0) - - risk_budget = calc_risk_budget_usdt(capital_base_usdt, risk_percent) - q2_raw, err = solve_add_amount_for_total_risk( - direction, - qty_existing, - entry_existing, - entry_add, - sl, - risk_budget, - contract_size, - ) - if err: - return None, err - q2 = float(q2_raw) - new_qty = qty_existing + q2 - new_avg = avg_entry_after_add(qty_existing, entry_existing, q2, entry_add) - cs = float(contract_size or 1.0) - loss_sl = loss_at_stop_usdt(direction, new_avg, new_qty, sl, cs) - reward_tp = reward_at_tp_usdt(direction, new_avg, initial_take_profit, new_qty, cs) - return { - "symbol": symbol, - "direction": direction, - "add_mode": mode, - "add_mode_label": mode_label(mode), - "add_price": round(entry_add, 10), - "new_stop_loss": round(sl, 10), - "breakthrough_price": float(breakthrough_price) if breakthrough_price not in (None, "") else None, - "initial_take_profit": float(initial_take_profit), - "risk_percent": float(risk_percent), - "risk_budget_usdt": round(risk_budget, 4), - "add_amount_raw": q2, - "qty_existing": float(qty_existing), - "entry_existing": float(entry_existing), - "qty_after": new_qty, - "avg_entry_after": round(new_avg, 10), - "loss_at_sl_usdt": round(loss_sl, 4), - "reward_at_tp_usdt": round(reward_tp, 4), - "legs_done": int(legs_done), - "leg_index_next": int(legs_done) + 1, - "fib_upper": fib_upper, - "fib_lower": fib_lower, - "contract_size": cs, - }, None +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_roll_monitor_lib.py b/lib/strategy/strategy_roll_monitor_lib.py index 6665ebc..c65bc3b 100644 --- a/lib/strategy/strategy_roll_monitor_lib.py +++ b/lib/strategy/strategy_roll_monitor_lib.py @@ -1,562 +1,12 @@ -"""滚仓程序监控:斐波/突破触价市价成交,失效,外部平仓同步(各所共用).""" -from __future__ import annotations - -from typing import Any, Optional - -from lib.strategy.strategy_roll_lib import ( - BREAKOUT_MODE, - FIB_MODES, - MARKET_MODE, - mode_label, - roll_breakout_invalidate, - roll_breakout_trigger_crossed, - roll_fib_invalidate, - roll_fib_trigger_crossed, - calc_risk_budget_usdt, - max_roll_legs, - preview_roll, - solve_add_amount_for_total_risk, -) -from lib.strategy.strategy_db import init_strategy_tables - -ROLL_LEG_STATUS_LABELS = { - "pending": "监控中", - "filled": "已成交", - "cancelled": "已删除", - "invalidated": "已失效", -} - - -def roll_leg_status_label(status: Optional[str]) -> str: - s = (status or "").strip().lower() - return ROLL_LEG_STATUS_LABELS.get(s, status or "—") - - -def check_roll_monitors(cfg: dict[str, Any]) -> None: - get_db = cfg["get_db"] - conn = get_db() - try: - init_strategy_tables(conn) - _reconcile_roll_groups(conn, cfg) - _check_pending_roll_legs(conn, cfg) - conn.commit() - except Exception as e: - print(f"[roll_monitor] {e}", flush=True) - try: - conn.rollback() - except Exception: - pass - finally: - try: - conn.close() - except Exception: - pass - - -def sync_roll_after_external_close( - cfg: dict, conn, symbol: str, direction: str, *, reason: str = "持仓已平" -) -> dict[str, Any]: - """中控/实例手动平仓后:取消 pending 腿并关闭 active 滚仓组(保留 filled 历史).""" - norm = cfg.get("normalize_symbol_input") - sym = norm(symbol) if callable(norm) else (symbol or "").strip() - if not sym: - return {"ok": False, "msg": "symbol 无效", "closed_groups": 0, "cancelled_legs": 0} - direction = (direction or "long").strip().lower() - init_strategy_tables(conn) - rows = conn.execute( - """SELECT g.* FROM roll_groups g - WHERE g.status='active' AND g.symbol=? AND g.direction=?""", - (sym, direction), - ).fetchall() - closed = cancelled = 0 - for row in rows: - g = _row_dict(row) - cancelled += _cancel_pending_legs_for_group(conn, cfg, g, status="cancelled") - cur = conn.execute( - "UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'", - (_now(cfg), int(g["id"])), - ) - if getattr(cur, "rowcount", 0): - closed += 1 - try: - from lib.strategy.strategy_wechat_notify import notify_roll_group_ended - - notify_roll_group_ended( - cfg, - group_id=int(g["id"]), - symbol=sym, - direction=direction, - reason=reason, - leg_count=int(g.get("leg_count") or 0), - ) - except Exception: - pass - try: - from lib.strategy.strategy_snapshot_lib import save_roll_group_snapshot - - save_roll_group_snapshot(cfg, conn, g, result_label="结束") - except Exception: - pass - return { - "ok": True, - "symbol": sym, - "direction": direction, - "closed_groups": closed, - "cancelled_legs": cancelled, - } - - -def cancel_roll_pending_leg(cfg: dict, conn, leg_id: int) -> tuple[bool, str]: - """用户删除 pending 滚仓腿(不可修改,仅删除).""" - init_strategy_tables(conn) - row = conn.execute( - "SELECT l.*, g.symbol, g.direction, g.status AS group_status FROM roll_legs l " - "INNER JOIN roll_groups g ON g.id = l.roll_group_id WHERE l.id=?", - (int(leg_id),), - ).fetchone() - if not row: - return False, "滚仓腿不存在" - leg = _row_dict(row) - if (leg.get("status") or "").strip().lower() != "pending": - return False, "仅监控中的腿可删除" - _cancel_roll_leg_order(cfg, {"symbol": leg.get("symbol"), "exchange_symbol": leg.get("exchange_symbol")}, leg) - conn.execute( - "UPDATE roll_legs SET status='cancelled' WHERE id=? AND status='pending'", - (int(leg_id),), - ) - conn.commit() - return True, "已删除滚仓监控" - - -def count_filled_roll_legs(conn, roll_group_id: int) -> int: - row = conn.execute( - "SELECT COUNT(*) FROM roll_legs WHERE roll_group_id=? AND status='filled'", - (int(roll_group_id),), - ).fetchone() - return int(row[0] if row else 0) - - -def count_pending_roll_legs(conn, roll_group_id: int) -> int: - row = conn.execute( - "SELECT COUNT(*) FROM roll_legs WHERE roll_group_id=? AND status='pending'", - (int(roll_group_id),), - ).fetchone() - return int(row[0] if row else 0) - - -def _row_dict(row) -> dict: - if row is None: - return {} - try: - return dict(row) - except Exception: - return {} - - -def _now(cfg: dict) -> str: - fn = cfg.get("app_now_str") - return fn() if callable(fn) else "" - - -def _cancel_pending_legs_for_group(conn, cfg: dict, group: dict, *, status: str = "cancelled") -> int: - gid = int(group["id"]) - n = 0 - for leg in conn.execute( - "SELECT * FROM roll_legs WHERE roll_group_id=? AND status='pending'", - (gid,), - ).fetchall(): - ld = _row_dict(leg) - _cancel_roll_leg_order(cfg, group, ld) - conn.execute( - "UPDATE roll_legs SET status=? WHERE id=? AND status='pending'", - (status, ld["id"]), - ) - n += 1 - return n - - -def _close_roll_group(conn, cfg: dict, group: dict, *, reason: str = "下单监控已结案或交易所无同向持仓") -> None: - gid = int(group["id"]) - _cancel_pending_legs_for_group(conn, cfg, group, status="cancelled") - cur = conn.execute( - "UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'", - (_now(cfg), gid), - ) - if getattr(cur, "rowcount", 0): - try: - from lib.strategy.strategy_wechat_notify import notify_roll_group_ended - - notify_roll_group_ended( - cfg, - group_id=gid, - symbol=group.get("symbol") or "", - direction=group.get("direction") or "long", - reason=reason, - leg_count=int(group.get("leg_count") or 0), - ) - except Exception: - pass - try: - from lib.strategy.strategy_snapshot_lib import save_roll_group_snapshot - - save_roll_group_snapshot(cfg, conn, group, result_label="结束") - except Exception: - pass - - -def _reconcile_roll_groups(conn, cfg: dict) -> None: - from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty - - rows = conn.execute( - """SELECT g.*, m.status AS monitor_status - FROM roll_groups g - LEFT JOIN order_monitors m ON m.id = g.order_monitor_id - WHERE g.status='active'""" - ).fetchall() - for row in rows: - g = _row_dict(row) - symbol = g.get("symbol") or "" - direction = (g.get("direction") or "long").strip().lower() - ex_sym = g.get("exchange_symbol") or cfg["normalize_exchange_symbol"](symbol) - mon_ok = (row["monitor_status"] or "").strip().lower() == "active" - if not mon_ok: - _close_roll_group(conn, cfg, g, reason="下单监控已结案") - continue - pos = None - try: - pos = cfg["get_position"](ex_sym, direction) - except Exception: - pos = None - if pos is None: - continue - qty = normalize_contracts_qty(pos.get("contracts") or 0) - if not contracts_qty_is_open(qty): - try: - pos2 = cfg["get_position"](ex_sym, direction) or {} - qty = normalize_contracts_qty(pos2.get("contracts") or 0) - except Exception: - continue - if not contracts_qty_is_open(qty): - _close_roll_group(conn, cfg, g) - - -def _cancel_roll_leg_order(cfg: dict, group: dict, leg: dict) -> None: - oid = (leg.get("exchange_order_id") or "").strip() - if not oid: - return - symbol = group.get("symbol") or "" - ex_sym = group.get("exchange_symbol") or cfg["normalize_exchange_symbol"](symbol) - cancel = cfg.get("cancel_limit_order") - if callable(cancel): - try: - cancel(ex_sym, oid) - except Exception: - pass - - -def _contract_size(cfg: dict, ex_sym: str) -> float: - get_cs = cfg.get("get_contract_size") - if callable(get_cs): - try: - return float(get_cs(ex_sym) or 1.0) - except Exception: - pass - return 1.0 - - -def _resolve_add_mode(leg: dict) -> str: - raw = (leg.get("add_mode") or "").strip().lower() - if raw in (MARKET_MODE, "market", "市价", "市价加仓"): - return MARKET_MODE - if "786" in raw or raw == "fib_786": - return "fib_786" - if "618" in raw or raw == "fib_618": - return "fib_618" - if raw in (BREAKOUT_MODE, "突破", "突破加仓"): - return BREAKOUT_MODE - if raw.startswith("fib"): - return raw.replace(".", "_").replace("0.", "0") - return raw or MARKET_MODE - - -def _check_pending_roll_legs(conn, cfg: dict) -> None: - rows = conn.execute( - """SELECT l.*, g.symbol, g.exchange_symbol, g.direction, g.initial_take_profit, - g.order_monitor_id, g.risk_percent, g.leg_count - FROM roll_legs l - INNER JOIN roll_groups g ON g.id = l.roll_group_id AND g.status='active' - WHERE l.status='pending'""" - ).fetchall() - for row in rows: - leg = _row_dict(row) - group = { - "id": leg["roll_group_id"], - "symbol": leg["symbol"], - "exchange_symbol": leg["exchange_symbol"], - "direction": leg["direction"], - "initial_take_profit": leg["initial_take_profit"], - "order_monitor_id": leg["order_monitor_id"], - "risk_percent": leg.get("risk_percent"), - "leg_count": leg.get("leg_count"), - } - _process_pending_roll_leg(conn, cfg, group, leg) - - -def _process_pending_roll_leg(conn, cfg: dict, group: dict, leg: dict) -> None: - symbol = group.get("symbol") or "" - direction = (group.get("direction") or "long").strip().lower() - ex_sym = group.get("exchange_symbol") or cfg["normalize_exchange_symbol"](symbol) - mark_fn = cfg.get("get_mark_price") or cfg.get("get_price") - mark = mark_fn(symbol) if callable(mark_fn) else None - if mark is None: - return - mark_f = float(mark) - prev_mark = leg.get("last_mark_price") - try: - prev_f = float(prev_mark) if prev_mark not in (None, "") else None - except (TypeError, ValueError): - prev_f = None - - mode = _resolve_add_mode(leg) - sl = float(leg.get("new_stop_loss") or 0) - fib_u, fib_l = leg.get("fib_upper"), leg.get("fib_lower") - bp = leg.get("breakthrough_price") - - if mode in FIB_MODES and fib_u is not None and fib_l is not None: - if roll_fib_invalidate(direction, mark_f, float(fib_u), float(fib_l)): - _invalidate_roll_leg(conn, cfg, group, leg, mark_f, reason="止盈侧突破") - return - elif mode == BREAKOUT_MODE and sl > 0: - if roll_breakout_invalidate(direction, mark_f, sl): - _invalidate_roll_leg(conn, cfg, group, leg, mark_f, reason="止损侧突破") - return - - triggered = False - if mode in FIB_MODES: - lp = leg.get("limit_price") - if lp is not None and roll_fib_trigger_crossed(direction, prev_f, mark_f, float(lp)): - triggered = True - elif mode == BREAKOUT_MODE and bp is not None: - if roll_breakout_trigger_crossed(direction, prev_f, mark_f, float(bp)): - triggered = True - - conn.execute( - "UPDATE roll_legs SET last_mark_price=? WHERE id=? AND status='pending'", - (mark_f, int(leg["id"])), - ) - - if triggered: - _execute_pending_roll_leg(conn, cfg, group, leg, ex_sym, direction, mark_f) - return - - -def _execute_pending_roll_leg( - conn, - cfg: dict, - group: dict, - leg: dict, - ex_sym: str, - direction: str, - mark: float, -) -> None: - from lib.hub.hub_position_metrics import contracts_qty_is_open, normalize_contracts_qty - - leg_id = int(leg["id"]) - gid = int(group["roll_group_id"]) if "roll_group_id" in leg else int(group["id"]) - mon_id = group.get("order_monitor_id") - mon = None - if mon_id: - row = conn.execute("SELECT * FROM order_monitors WHERE id=?", (mon_id,)).fetchone() - mon = _row_dict(row) if row else None - if not mon or (mon.get("status") or "").strip().lower() != "active": - _invalidate_roll_leg(conn, cfg, group, leg, mark, reason="监控单已失效") - return - - pos = cfg["get_position"](ex_sym, direction) or {} - qty = normalize_contracts_qty(pos.get("contracts") or 0) - entry = float(pos.get("entry_price") or mon.get("trigger_price") or 0) - if not contracts_qty_is_open(qty) or entry <= 0: - _invalidate_roll_leg(conn, cfg, group, leg, mark, reason="无持仓") - return - - filled = count_filled_roll_legs(conn, gid) - if filled >= max_roll_legs(direction): - _invalidate_roll_leg(conn, cfg, group, leg, mark, reason="滚仓次数已满") - return - - try: - risk_pct = float(mon.get("risk_percent") or group.get("risk_percent") or 2) - except (TypeError, ValueError): - risk_pct = 2.0 - conn_cap = cfg["get_db"]() - try: - capital = float(cfg["get_trading_capital_usdt"](conn_cap)) - finally: - conn_cap.close() - - cs = _contract_size(cfg, ex_sym) - sl = float(leg.get("new_stop_loss") or 0) - tp0 = float(group.get("initial_take_profit") or mon.get("take_profit") or 0) - mode = _resolve_add_mode(leg) - - q2_raw, err = solve_add_amount_for_total_risk( - direction, qty, entry, mark, sl, calc_risk_budget_usdt(capital, risk_pct), cs - ) - if err or q2_raw is None or float(q2_raw) <= 0: - _invalidate_roll_leg(conn, cfg, group, leg, mark, reason=err or "无法计算加仓张数") - return - - amount = cfg["amount_to_precision"](ex_sym, float(q2_raw)) - if amount is None or float(amount) <= 0: - _invalidate_roll_leg(conn, cfg, group, leg, mark, reason="加仓张数低于交易所最小精度") - return - - lev_fn = cfg.get("default_leverage") - if not callable(lev_fn): - lev_fn = lambda _s: 5 - leverage = int(lev_fn(group.get("symbol") or "")) - - try: - order = cfg["market_add"](ex_sym, direction, float(amount), leverage) - fill = float( - cfg.get("resolve_fill_price", lambda o, s, p: p)(order, ex_sym, mark) or mark - ) - except Exception as e: - fe = cfg.get("friendly_error") - msg = fe(e) if callable(fe) else str(e) - _notify_roll_fail(cfg, group, leg, mark, msg) - return - - oid = str(order.get("id") or "") if isinstance(order, dict) else "" - try: - cfg["replace_tpsl"](ex_sym, direction, sl, tp0, mon) - except Exception as tpsl_err: - fe = cfg.get("friendly_error") - msg = fe(tpsl_err) if callable(fe) else str(tpsl_err) - conn.execute( - """UPDATE roll_legs SET status='error', exchange_order_id=?, fill_price=?, amount=? - WHERE id=? AND status='pending'""", - (oid, fill, float(amount), leg_id), - ) - _notify_roll_fail(cfg, group, leg, mark, f"加仓成交但止盈止损更新失败: {msg}") - return - - conn.execute( - """UPDATE roll_legs SET status='filled', fill_price=?, amount=?, exchange_order_id=?, - new_stop_loss=? WHERE id=? AND status='pending'""", - (fill, float(amount), oid, sl, leg_id), - ) - conn.execute( - "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?", - (filled + 1, sl, _now(cfg), gid), - ) - live_qty = normalize_contracts_qty(qty + float(amount)) - try: - pos2 = cfg["get_position"](ex_sym, direction) or {} - q2 = normalize_contracts_qty(pos2.get("contracts") or 0) - if contracts_qty_is_open(q2): - live_qty = q2 - except Exception: - pass - conn.execute( - """UPDATE order_monitors SET stop_loss=?, order_amount=?, - breakeven_armed=0, breakeven_price=NULL - WHERE id=? AND status='active'""", - (sl, live_qty, mon["id"]), - ) - - notify = cfg.get("send_wechat") - if callable(notify): - sym = group.get("symbol") or "" - mode_lbl = leg.get("add_mode") or mode_label(mode) - fmt = cfg.get("format_price") - px_txt = fmt(sym, fill) if callable(fmt) else str(fill) - sl_txt = fmt(sym, sl) if callable(fmt) else str(sl) - acct = _wechat_account(cfg) - dir_txt = _wechat_dir(cfg, direction) - notify( - f"# ✅ {sym} 滚仓触价成交\n" - f"**账户:{acct}**\n" - f"- 方式:{mode_lbl}|{dir_txt}\n" - f"- 成交价:{px_txt}|张数:{amount}\n" - f"- 新止损:{sl_txt}(止盈仍为首仓)\n" - ) - - -def _invalidate_roll_leg( - conn, - cfg: dict, - group: dict, - leg: dict, - mark: float, - *, - reason: str = "", -) -> None: - leg_id = int(leg["id"]) - cur = conn.execute("SELECT status FROM roll_legs WHERE id=?", (leg_id,)).fetchone() - if not cur or (cur[0] or "").strip().lower() in ("invalidated", "filled", "cancelled"): - return - _cancel_roll_leg_order(cfg, group, leg) - conn.execute( - "UPDATE roll_legs SET status='invalidated' WHERE id=? AND status='pending'", - (leg_id,), - ) - _send_roll_invalidate_wechat(cfg, group, leg, mark, reason=reason) - - -def _notify_roll_fail(cfg: dict, group: dict, leg: dict, mark: float, reason: str) -> None: - notify = cfg.get("send_wechat") - if not callable(notify): - return - sym = group.get("symbol") or "" - mode = leg.get("add_mode") or "滚仓" - acct = _wechat_account(cfg) - notify( - f"# ❌ {sym} 滚仓触价成交失败\n" - f"**账户:{acct}**\n" - f"- 方式:{mode}\n" - f"- 原因:{reason}\n" - ) - - -def _send_roll_invalidate_wechat( - cfg: dict, group: dict, leg: dict, mark: float, *, reason: str = "" -) -> None: - notify = cfg.get("send_wechat") - if not callable(notify): - return - sym = group.get("symbol") or "" - direction = (group.get("direction") or "long").strip().lower() - mode = leg.get("add_mode") or "滚仓监控" - fmt = cfg.get("format_price") - mark_txt = fmt(sym, mark) if callable(fmt) else str(mark) - acct = _wechat_account(cfg) - dir_txt = _wechat_dir(cfg, direction) - detail = reason or "条件不满足" - notify( - f"# ⚠️ {sym} 滚仓监控失效\n" - f"**账户:{acct}**\n" - f"- 方式:{mode}|{dir_txt}\n" - f"- 标记价 {mark_txt}|{detail}\n" - f"- 本条监控已结案,可重新提交\n" - ) - - -def _wechat_account(cfg: dict) -> str: - fn = cfg.get("wechat_account_label") - if callable(fn): - try: - return str(fn()) - except Exception: - pass - return str(cfg.get("exchange_display") or "") - - -def _wechat_dir(cfg: dict, direction: str) -> str: - fn = cfg.get("wechat_direction_text") - if callable(fn): - try: - return str(fn(direction)) - except Exception: - pass - return "做多" if (direction or "long").strip().lower() == "long" else "做空" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_roll_ui_lib.py b/lib/strategy/strategy_roll_ui_lib.py index cef900f..124201b 100644 --- a/lib/strategy/strategy_roll_ui_lib.py +++ b/lib/strategy/strategy_roll_ui_lib.py @@ -1,434 +1,12 @@ -"""顺势加仓 UI:滚仓腿合并均价与止盈盈利展示(实例页 + 中控).""" -from __future__ import annotations - -from typing import Any, Callable, Optional - -from flask import Flask - -FILLED_LEG_STATUSES = frozenset({"filled", "done", "complete"}) - - -def reward_at_tp_usdt( - direction: str, - avg_entry: float, - take_profit: float, - qty: float, - *, - contract_size: float = 1.0, -) -> Optional[float]: - """与 strategy_roll_lib.preview_roll 一致:线性合约 U 本位净盈利(扣双边 taker 费).""" - try: - avg = float(avg_entry) - tp = float(take_profit) - q = float(qty) - cs = float(contract_size or 1.0) - except (TypeError, ValueError): - return None - if avg <= 0 or tp <= 0 or q <= 0: - return None - direction = (direction or "long").strip().lower() - if direction == "short": - gross = (avg - tp) * q * cs - else: - gross = (tp - avg) * q * cs - try: - from lib.trade.trade_fee_lib import net_pnl_after_fee - - net = net_pnl_after_fee(gross, avg, tp, q, cs) - return net - except Exception: - return gross - - -def leg_fill_price(leg: dict) -> Optional[float]: - if not isinstance(leg, dict): - return None - for key in ("fill_price", "limit_price"): - try: - v = float(leg.get(key) or 0) - if v > 0: - return v - except (TypeError, ValueError): - continue - return None - - -def leg_is_filled(leg: dict) -> bool: - st = str(leg.get("status") or "").strip().lower() - return st in FILLED_LEG_STATUSES - - -def infer_initial_position( - qty_live: float, - entry_live: float, - filled_legs: list[dict], - *, - monitor: dict | None = None, -) -> tuple[Optional[float], Optional[float]]: - """由当前持仓与各腿成交价反推首仓张数/均价.""" - try: - qty_live = float(qty_live) - entry_live = float(entry_live) - except (TypeError, ValueError): - qty_live = entry_live = 0.0 - from lib.hub.hub_position_metrics import normalize_contracts_qty - - qty_live = normalize_contracts_qty(qty_live) - legs = [ - lg - for lg in filled_legs or [] - if isinstance(lg, dict) and leg_is_filled(lg) and leg_fill_price(lg) and float(lg.get("amount") or 0) > 0 - ] - add_sum = sum(normalize_contracts_qty(lg.get("amount") or 0) for lg in legs) - leg_notional = sum( - normalize_contracts_qty(lg.get("amount") or 0) * float(leg_fill_price(lg) or 0) for lg in legs - ) - q0 = qty_live - add_sum - if q0 > 1e-12 and entry_live > 0 and qty_live > 0: - e0 = (entry_live * qty_live - leg_notional) / q0 - if e0 > 0: - return q0, e0 - mon = monitor if isinstance(monitor, dict) else {} - try: - trig = float(mon.get("trigger_price") or 0) - except (TypeError, ValueError): - trig = 0.0 - try: - mon_amt = float(mon.get("order_amount") or mon.get("amount") or 0) - except (TypeError, ValueError): - mon_amt = 0.0 - if trig > 0: - q_base = q0 if q0 > 1e-12 else (mon_amt if mon_amt > 0 else max(qty_live - add_sum, 0)) - if q_base > 0: - return q_base, trig - return None, None - - -def compute_roll_chain_metrics( - group: dict, - legs: list[dict], - *, - qty_live: Optional[float] = None, - entry_live: Optional[float] = None, - monitor: dict | None = None, - contract_size: float = 1.0, -) -> tuple[dict[Any, dict], dict]: - """ - 返回 (leg_metrics_by_id, group_metrics). - leg_metrics: leg id -> {avg_entry_after, reward_at_tp_usdt} - group_metrics: 最后一腿后的 {avg_entry, reward_at_tp_usdt} - """ - per_leg: dict[Any, dict] = {} - group_out: dict[str, Any] = { - "avg_entry": None, - "reward_at_tp_usdt": None, - "initial_qty": None, - "current_qty": None, - } - if not isinstance(group, dict): - return per_leg, group_out - direction = (group.get("direction") or "long").strip().lower() - try: - tp = float(group.get("initial_take_profit") or 0) - except (TypeError, ValueError): - tp = 0.0 - sorted_legs = sorted( - [lg for lg in legs or [] if isinstance(lg, dict)], - key=lambda x: int(x.get("leg_index") or 0), - ) - filled = [lg for lg in sorted_legs if leg_is_filled(lg)] - q0 = e0 = None - if qty_live is not None and entry_live is not None: - q0, e0 = infer_initial_position(float(qty_live), float(entry_live), filled, monitor=monitor) - if q0 is None or e0 is None: - return per_leg, group_out - qty = float(q0) - avg = float(e0) - group_out["initial_qty"] = round(qty, 2) - group_out["current_qty"] = round(qty, 2) - if tp > 0: - group_out["avg_entry"] = avg - group_out["reward_at_tp_usdt"] = reward_at_tp_usdt( - direction, avg, tp, qty, contract_size=contract_size - ) - for leg in sorted_legs: - if not leg_is_filled(leg): - continue - try: - amt = float(leg.get("amount") or 0) - except (TypeError, ValueError): - continue - px = leg_fill_price(leg) - if not px or amt <= 0: - continue - prev_qty = qty - qty = prev_qty + amt - avg = (prev_qty * avg + amt * px) / qty - reward = reward_at_tp_usdt(direction, avg, tp, qty, contract_size=contract_size) if tp > 0 else None - lid = leg.get("id") - if lid is None: - lid = f"{group.get('id')}|{leg.get('leg_index')}" - per_leg[lid] = { - "avg_entry_after": round(avg, 10), - "reward_at_tp_usdt": round(reward, 4) if reward is not None else None, - } - group_out["avg_entry"] = round(avg, 10) - group_out["reward_at_tp_usdt"] = round(reward, 4) if reward is not None else None - group_out["current_qty"] = round(qty, 2) - if qty_live is not None: - try: - live_qty = float(qty_live) - if live_qty > 0: - group_out["current_qty"] = round(live_qty, 2) - except (TypeError, ValueError): - pass - return per_leg, group_out - - -def _row_to_dict(row) -> dict: - if row is None: - return {} - try: - return dict(row) - except Exception: - return {} - - -def _resolve_roll_live(cfg: dict, group: dict, monitor: dict | None) -> tuple[Optional[float], Optional[float], float]: - """读取交易所持仓张数,均价,contract_size.""" - m = cfg.get("app_module") - ex_sym = group.get("exchange_symbol") - sym = group.get("symbol") or "" - direction = (group.get("direction") or "long").strip().lower() - if not ex_sym and m is not None: - norm = getattr(m, "normalize_exchange_symbol", None) - if callable(norm): - try: - ex_sym = norm(sym) - except Exception: - ex_sym = sym - cs = 1.0 - get_cs = cfg.get("get_contract_size") - if not callable(get_cs) and m is not None: - get_cs = getattr(m, "get_contract_size", None) - if callable(get_cs): - try: - cs = float(get_cs(ex_sym or sym) or 1.0) - except Exception: - cs = 1.0 - get_pos = cfg.get("get_position") - if not callable(get_pos): - return None, None, cs - try: - pos = get_pos(ex_sym or sym, direction) or {} - from lib.hub.hub_position_metrics import normalize_contracts_qty - - qty = normalize_contracts_qty(pos.get("contracts") or 0) - entry = float(pos.get("entry_price") or 0) - if qty > 0 and entry > 0: - return qty, entry, cs - except Exception: - pass - metrics_fn = getattr(m, "get_live_position_exchange_metrics", None) if m else None - if callable(metrics_fn): - try: - met = metrics_fn(ex_sym or sym, direction) - if isinstance(met, dict): - qty = float(met.get("contracts") or met.get("size") or 0) - entry = float(met.get("entry_price") or 0) - if qty > 0 and entry > 0: - return qty, entry, cs - except Exception: - pass - if monitor: - try: - trig = float(monitor.get("trigger_price") or 0) - amt = float(monitor.get("order_amount") or monitor.get("amount") or 0) - if trig > 0 and amt > 0: - return amt, trig, cs - except (TypeError, ValueError): - pass - return None, None, cs - - -def enrich_roll_page_data(conn, page_data: dict, cfg: dict | None) -> dict: - """为 roll_groups / roll_legs 附加 avg_entry,reward_at_tp 展示字段.""" - if not isinstance(page_data, dict) or not cfg: - return page_data - groups = list(page_data.get("roll_groups") or []) - legs = list(page_data.get("roll_legs") or []) - if not groups: - return page_data - monitors_by_id: dict[int, dict] = {} - try: - for row in conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall(): - od = _row_to_dict(row) - mid = od.get("id") - if mid is not None: - monitors_by_id[int(mid)] = od - except Exception: - pass - legs_by_gid: dict[int, list] = {} - for leg in legs: - if not isinstance(leg, dict): - continue - try: - gid = int(leg.get("roll_group_id")) - except (TypeError, ValueError): - continue - legs_by_gid.setdefault(gid, []).append(leg) - price_fmt = cfg.get("price_fmt") - for g in groups: - if not isinstance(g, dict) or g.get("id") is None: - continue - gid = int(g["id"]) - mon = monitors_by_id.get(int(g.get("order_monitor_id") or 0)) - qty, entry, cs = _resolve_roll_live(cfg, g, mon) - per_leg, group_metrics = compute_roll_chain_metrics( - g, - legs_by_gid.get(gid, []), - qty_live=qty, - entry_live=entry, - monitor=mon, - contract_size=cs, - ) - g["avg_entry"] = group_metrics.get("avg_entry") - g["reward_at_tp_usdt"] = group_metrics.get("reward_at_tp_usdt") - g["initial_qty"] = group_metrics.get("initial_qty") - g["current_qty"] = group_metrics.get("current_qty") - if callable(price_fmt) and g.get("avg_entry") is not None: - try: - g["avg_entry_display"] = price_fmt(g.get("symbol"), g["avg_entry"]) - except Exception: - pass - for leg in legs_by_gid.get(gid, []): - lid = leg.get("id") - if lid is None: - lid = f"{gid}|{leg.get('leg_index')}" - metrics = per_leg.get(lid) or per_leg.get(leg.get("id")) - if not metrics: - continue - leg["avg_entry_after"] = metrics.get("avg_entry_after") - leg["reward_at_tp_usdt"] = metrics.get("reward_at_tp_usdt") - if callable(price_fmt) and leg.get("avg_entry_after") is not None: - try: - leg["avg_entry_display"] = price_fmt(g.get("symbol"), leg["avg_entry_after"]) - except Exception: - pass - page_data["roll_groups"] = groups - page_data["roll_legs"] = legs - return page_data - - -def enrich_roll_groups_for_hub(rolls: list[dict], conn, cfg: dict | None) -> list[dict]: - """中控 monitor API:每组附带当前均价,止盈盈利与最近滚仓腿.""" - if not rolls or not cfg: - return rolls - out = [] - gid_list = [] - for g in rolls: - if isinstance(g, dict) and g.get("id") is not None: - try: - gid_list.append(int(g["id"])) - except (TypeError, ValueError): - pass - legs_by_gid: dict[int, list] = {gid: [] for gid in gid_list} - if gid_list: - placeholders = ",".join("?" for _ in gid_list) - try: - rows = conn.execute( - f"SELECT * FROM roll_legs WHERE roll_group_id IN ({placeholders}) ORDER BY id DESC", - gid_list, - ).fetchall() - for row in rows: - leg = _row_to_dict(row) - try: - legs_by_gid[int(leg.get("roll_group_id"))].append(leg) - except (TypeError, ValueError): - pass - except Exception: - pass - monitors_by_id: dict[int, dict] = {} - try: - for row in conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall(): - od = _row_to_dict(row) - if od.get("id") is not None: - monitors_by_id[int(od["id"])] = od - except Exception: - pass - price_fmt = cfg.get("price_fmt") - for g in rolls: - if not isinstance(g, dict): - continue - gd = dict(g) - try: - gid = int(gd.get("id")) - except (TypeError, ValueError): - out.append(gd) - continue - mon = monitors_by_id.get(int(gd.get("order_monitor_id") or 0)) - group_legs = legs_by_gid.get(gid, []) - qty, entry, cs = _resolve_roll_live(cfg, gd, mon) - per_leg, group_metrics = compute_roll_chain_metrics( - gd, - group_legs, - qty_live=qty, - entry_live=entry, - monitor=mon, - contract_size=cs, - ) - gd.update(group_metrics) - if callable(price_fmt) and gd.get("avg_entry") is not None: - try: - gd["avg_entry_display"] = price_fmt(gd.get("symbol"), gd["avg_entry"]) - except Exception: - pass - recent = [] - for leg in sorted(group_legs, key=lambda x: int(x.get("leg_index") or 0), reverse=True)[:6]: - ld = dict(leg) - lid = ld.get("id") - if lid is None: - lid = f"{gid}|{ld.get('leg_index')}" - metrics = per_leg.get(lid) or per_leg.get(ld.get("id")) - if metrics: - ld.update(metrics) - if callable(price_fmt) and ld.get("avg_entry_after") is not None: - try: - ld["avg_entry_display"] = price_fmt(gd.get("symbol"), ld["avg_entry_after"]) - except Exception: - pass - recent.append(ld) - gd["recent_legs"] = recent - out.append(gd) - return out - - -def patch_roll_hub_enrich(app: Flask, cfg: dict) -> None: - """hub_bridge install 后:/api/hub/monitor 的 rolls 附带均价/止盈盈利.""" - ctx = dict(app.config.get("HUB_CTX") or {}) - prev: Callable | None = ctx.get("enrich_monitor") - - def enrich_monitor(keys=None, orders=None, trends=None, rolls=None): - payload: dict[str, Any] = {} - if callable(prev): - try: - prev_out = prev(keys=keys, orders=orders, trends=trends, rolls=rolls) - if isinstance(prev_out, dict): - payload.update(prev_out) - except Exception: - pass - if rolls: - get_db = cfg.get("get_db") - if callable(get_db): - conn = get_db() - try: - payload["rolls"] = enrich_roll_groups_for_hub(list(rolls), conn, cfg) - finally: - try: - conn.close() - except Exception: - pass - return payload - - ctx["enrich_monitor"] = enrich_monitor - app.config["HUB_CTX"] = ctx +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_snapshot_lib.py b/lib/strategy/strategy_snapshot_lib.py index baf9392..f9bfc44 100644 --- a/lib/strategy/strategy_snapshot_lib.py +++ b/lib/strategy/strategy_snapshot_lib.py @@ -1,529 +1,12 @@ -"""策略结束快照:趋势回调 / 顺势加仓(三所共用).""" -from __future__ import annotations - -import json -from datetime import datetime, timezone -from typing import Any, Callable, Optional - -STRATEGY_TREND = "trend_pullback" -STRATEGY_ROLL = "roll" -STRATEGY_SNAPSHOTS_MAX_ROWS = 100 -# 同一趋势计划只允许一条「结束类」快照(中控全平 + 监控止损 + 实例结束计划) -FINAL_TREND_CLOSE_RANK = { - "手动平仓": 3, - "止盈": 2, - "止损": 1, -} -FINAL_TREND_CLOSE_LABELS = tuple(FINAL_TREND_CLOSE_RANK.keys()) - -STRATEGY_SNAPSHOTS_SQL = """ -CREATE TABLE IF NOT EXISTS strategy_trade_snapshots ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - strategy_type TEXT NOT NULL, - source_id INTEGER, - symbol TEXT, - exchange_symbol TEXT, - direction TEXT, - result_label TEXT, - status_at_close TEXT, - opened_at TEXT, - closed_at TEXT, - pnl_amount REAL, - snapshot_json TEXT NOT NULL, - created_at TEXT -) -""" - - -def init_strategy_snapshot_table(conn) -> None: - conn.execute(STRATEGY_SNAPSHOTS_SQL) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_strategy_snapshots_closed " - "ON strategy_trade_snapshots(closed_at DESC)" - ) - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_strategy_snapshots_type " - "ON strategy_trade_snapshots(strategy_type, source_id)" - ) - - -def _row_dict(row) -> dict: - if row is None: - return {} - try: - return dict(row) - except Exception: - return {} - - -def _json_dumps(obj: Any) -> str: - return json.dumps(obj, ensure_ascii=False, separators=(",", ":")) - - -def build_trend_dca_levels(plan: dict) -> list[dict]: - """首仓 + 补仓档位列表(供策略页 / 中控).""" - out: list[dict] = [] - p = plan or {} - try: - legs_done = int(p.get("legs_done") or 0) - except (TypeError, ValueError): - legs_done = 0 - try: - dca_legs = int(p.get("dca_legs") or 0) - except (TypeError, ValueError): - dca_legs = 0 - first_done = int(p.get("first_order_done") or 0) != 0 - try: - grid = json.loads(p.get("grid_prices_json") or "[]") - if not isinstance(grid, list): - grid = [] - except Exception: - grid = [] - try: - leg_amounts = json.loads(p.get("leg_amounts_json") or "[]") - if not isinstance(leg_amounts, list): - leg_amounts = [] - except Exception: - leg_amounts = [] - - out.append( - { - "i": 0, - "leg_key": "first", - "label": "首仓", - "price": None, - "contracts": p.get("first_order_amount"), - "status": "done" if first_done else "pending", - "status_label": "已开仓" if first_done else "待开仓", - } - ) - n = max(len(grid), len(leg_amounts), dca_legs) - for idx in range(n): - leg_i = idx + 1 - price = grid[idx] if idx < len(grid) else None - contracts = leg_amounts[idx] if idx < len(leg_amounts) else None - done = leg_i <= legs_done - out.append( - { - "i": leg_i, - "leg_key": f"dca_{leg_i}", - "label": f"补仓{leg_i}", - "price": price, - "contracts": contracts, - "status": "done" if done else "pending", - "status_label": "已补仓" if done else "待补仓", - } - ) - return out - - -def attach_trend_dca_levels(plan: dict) -> dict: - from lib.strategy.strategy_trend_lib import enrich_trend_dca_levels_with_tp - - d = dict(plan or {}) - levels = build_trend_dca_levels(d) - d["dca_levels"] = enrich_trend_dca_levels_with_tp(d, levels) - return d - - -def _snapshot_key_exists( - conn, strategy_type: str, source_id: int, result_label: str -) -> bool: - if source_id <= 0: - return False - label = (result_label or "").strip() - row = conn.execute( - """SELECT 1 FROM strategy_trade_snapshots - WHERE strategy_type=? AND source_id=? AND result_label=? - LIMIT 1""", - (strategy_type, int(source_id), label), - ).fetchone() - return row is not None - - -def _final_trend_close_rank(result_label: str) -> int: - return int(FINAL_TREND_CLOSE_RANK.get((result_label or "").strip(), 0)) - - -def _purge_weaker_trend_final_snapshots( - conn, plan_id: int, result_label: str -) -> None: - """写入更高优先级结束快照时,删除同计划较弱的结束记录.""" - rank = _final_trend_close_rank(result_label) - if rank <= 0 or plan_id <= 0: - return - for label, lr in FINAL_TREND_CLOSE_RANK.items(): - if lr < rank: - conn.execute( - """DELETE FROM strategy_trade_snapshots - WHERE strategy_type=? AND source_id=? AND result_label=?""", - (STRATEGY_TREND, int(plan_id), label), - ) - - -def dedupe_strategy_snapshots(conn) -> int: - """删除重复快照:同结果去重 + 同计划仅保留最高优先级结束类记录.""" - init_strategy_snapshot_table(conn) - removed = 0 - cur = conn.execute( - """DELETE FROM strategy_trade_snapshots - WHERE id IN ( - SELECT s1.id FROM strategy_trade_snapshots s1 - INNER JOIN strategy_trade_snapshots s2 - ON s1.strategy_type = s2.strategy_type - AND s1.source_id = s2.source_id - AND s1.result_label = s2.result_label - AND s1.id < s2.id - )""" - ) - removed += int(getattr(cur, "rowcount", 0) or 0) - rows = conn.execute( - f"""SELECT id, source_id, result_label FROM strategy_trade_snapshots - WHERE strategy_type=? AND result_label IN ({",".join("?" * len(FINAL_TREND_CLOSE_LABELS))})""", - (STRATEGY_TREND, *FINAL_TREND_CLOSE_LABELS), - ).fetchall() - by_plan: dict[int, list] = {} - for row in rows: - d = _row_dict(row) - try: - pid = int(d.get("source_id") or 0) - except (TypeError, ValueError): - pid = 0 - if pid <= 0: - continue - by_plan.setdefault(pid, []).append(d) - drop_ids: list[int] = [] - for snaps in by_plan.values(): - if len(snaps) <= 1: - continue - best = max( - snaps, - key=lambda s: ( - _final_trend_close_rank(str(s.get("result_label") or "")), - int(s.get("id") or 0), - ), - ) - keep_id = int(best.get("id") or 0) - for s in snaps: - sid = int(s.get("id") or 0) - if sid and sid != keep_id: - drop_ids.append(sid) - if drop_ids: - placeholders = ",".join("?" * len(drop_ids)) - cur2 = conn.execute( - f"DELETE FROM strategy_trade_snapshots WHERE id IN ({placeholders})", - drop_ids, - ) - removed += int(getattr(cur2, "rowcount", 0) or 0) - return removed - - -def save_trend_plan_snapshot( - cfg: dict, - conn, - plan_row: Any, - *, - result_label: str, - exit_price: float | None = None, - pnl_amount: float | None = None, - closed_at: str | None = None, -) -> None: - init_strategy_snapshot_table(conn) - row = _row_dict(plan_row) - plan_id = int(row.get("id") or 0) - if plan_id <= 0: - return - label = (result_label or "").strip() - close_rank = _final_trend_close_rank(label) - if close_rank > 0: - existing = conn.execute( - f"""SELECT result_label FROM strategy_trade_snapshots - WHERE strategy_type=? AND source_id=? AND result_label IN ({",".join("?" * len(FINAL_TREND_CLOSE_LABELS))})""", - (STRATEGY_TREND, plan_id, *FINAL_TREND_CLOSE_LABELS), - ).fetchall() - for ex in existing: - ex_label = str(_row_dict(ex).get("result_label") or "") - if _final_trend_close_rank(ex_label) >= close_rank: - return - _purge_weaker_trend_final_snapshots(conn, plan_id, label) - elif _snapshot_key_exists(conn, STRATEGY_TREND, plan_id, label): - return - m = cfg.get("app_module") - close_ts = (closed_at or "").strip() or ( - m.app_now_str() - if m is not None and hasattr(m, "app_now_str") - else datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - ) - payload = attach_trend_dca_levels(row) - payload["result_label"] = result_label - payload["exit_price"] = exit_price - payload["pnl_amount"] = pnl_amount - payload["status_at_close"] = row.get("status") - conn.execute( - """INSERT INTO strategy_trade_snapshots ( - strategy_type, source_id, symbol, exchange_symbol, direction, - result_label, status_at_close, opened_at, closed_at, pnl_amount, snapshot_json, created_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - STRATEGY_TREND, - plan_id, - row.get("symbol"), - row.get("exchange_symbol"), - row.get("direction"), - result_label, - row.get("status"), - row.get("opened_at"), - close_ts, - pnl_amount, - _json_dumps(payload), - close_ts, - ), - ) - prune_strategy_snapshots(conn, keep=STRATEGY_SNAPSHOTS_MAX_ROWS) - - -def save_roll_group_snapshot( - cfg: dict, - conn, - group: dict, - *, - result_label: str = "结束", - pnl_amount: float | None = None, -) -> None: - init_strategy_snapshot_table(conn) - g = dict(group or {}) - gid = int(g.get("id") or 0) - if gid <= 0: - return - label = (result_label or "结束").strip() - if _snapshot_key_exists(conn, STRATEGY_ROLL, gid, label): - return - legs = [] - for leg in conn.execute( - "SELECT * FROM roll_legs WHERE roll_group_id=? ORDER BY leg_index ASC, id ASC", - (gid,), - ).fetchall(): - ld = _row_dict(leg) - try: - from lib.strategy.strategy_roll_monitor_lib import roll_leg_status_label - - ld["status_label"] = roll_leg_status_label(ld.get("status")) - except Exception: - ld["status_label"] = ld.get("status") or "" - legs.append(ld) - m = cfg.get("app_module") - closed_at = ( - m.app_now_str() - if m is not None and hasattr(m, "app_now_str") - else datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - ) - payload = { - "group": g, - "legs": legs, - "result_label": result_label, - "pnl_amount": pnl_amount, - } - conn.execute( - """INSERT INTO strategy_trade_snapshots ( - strategy_type, source_id, symbol, exchange_symbol, direction, - result_label, status_at_close, opened_at, closed_at, pnl_amount, snapshot_json, created_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - STRATEGY_ROLL, - gid, - g.get("symbol"), - g.get("exchange_symbol"), - g.get("direction"), - result_label, - g.get("status"), - g.get("created_at"), - closed_at, - pnl_amount, - _json_dumps(payload), - closed_at, - ), - ) - prune_strategy_snapshots(conn, keep=STRATEGY_SNAPSHOTS_MAX_ROWS) - - -def prune_strategy_snapshots(conn, *, keep: int = STRATEGY_SNAPSHOTS_MAX_ROWS) -> None: - """仅保留最近 keep 条策略快照(按 closed_at / id 倒序).""" - dedupe_strategy_snapshots(conn) - k = max(1, min(int(keep), 500)) - conn.execute( - """DELETE FROM strategy_trade_snapshots - WHERE id NOT IN ( - SELECT id FROM strategy_trade_snapshots - ORDER BY COALESCE(closed_at, created_at, '') DESC, id DESC - LIMIT ? - )""", - (k,), - ) - - -def _snapshot_pnl(row: dict, snap: dict) -> float | None: - for key in ("pnl_amount",): - v = row.get(key) - if v is not None and v != "": - try: - return float(v) - except (TypeError, ValueError): - pass - v = snap.get("pnl_amount") - if v is not None and v != "": - try: - return float(v) - except (TypeError, ValueError): - pass - return None - - -def _trend_dca_stats(snap: dict) -> dict: - levels = snap.get("dca_levels") or build_trend_dca_levels(snap) - dca_only = [ - lv - for lv in levels - if (lv.get("leg_key") or "") != "first" and (lv.get("label") or "") != "首仓" - ] - done = sum(1 for lv in dca_only if lv.get("status") == "done") - total = len(dca_only) - pending = total - done - if total <= 0: - tag = "na" - elif done <= 0: - tag = "no_dca" - elif done >= total: - tag = "dca_done" - else: - tag = "dca_partial" - return { - "dca_done": done, - "dca_total": total, - "dca_pending": pending, - "dca_tag": tag, - } - - -def _roll_leg_stats(snap: dict) -> dict: - legs = snap.get("legs") or [] - if not isinstance(legs, list): - legs = [] - filled = sum(1 for lg in legs if (lg.get("status") or "").lower() == "filled") - total = len(legs) - pending = total - filled - if total <= 0: - tag = "na" - elif filled <= 0: - tag = "no_dca" - elif filled >= total: - tag = "dca_done" - else: - tag = "dca_partial" - return { - "dca_done": filled, - "dca_total": total, - "dca_pending": pending, - "dca_tag": tag, - } - - -def enrich_strategy_snapshot_row(row: dict) -> dict: - d = dict(row or {}) - snap = d.get("snapshot") or {} - st = (d.get("strategy_type") or "").strip() - pnl = _snapshot_pnl(d, snap) - if pnl is not None: - if pnl > 1e-9: - d["filter_pnl"] = "profit" - elif pnl < -1e-9: - d["filter_pnl"] = "loss" - else: - d["filter_pnl"] = "flat" - else: - d["filter_pnl"] = "unknown" - snap_sym = "" - if isinstance(snap, dict): - snap_sym = (snap.get("symbol") or snap.get("exchange_symbol") or "").strip() - sym = (d.get("symbol") or d.get("exchange_symbol") or snap_sym or "").strip() - if sym: - d["symbol"] = d.get("symbol") or sym - d["exchange_symbol"] = d.get("exchange_symbol") or sym - d["filter_symbol"] = sym.upper().split("/")[0].split(":")[0] if sym else "" - closed = (d.get("closed_at") or d.get("created_at") or "").strip() - d["sort_ts"] = closed - if st == STRATEGY_TREND: - stats = _trend_dca_stats(snap) - d.update(stats) - legs_txt = ( - f"{stats['dca_done']}/{stats['dca_total']}" - if stats["dca_total"] > 0 - else "0/0" - ) - d["summary_dca"] = legs_txt - else: - stats = _roll_leg_stats(snap) - d.update(stats) - d["summary_dca"] = ( - f"{stats['dca_done']}/{stats['dca_total']}腿" - if stats["dca_total"] > 0 - else "—" - ) - return d - - -def list_strategy_snapshots(conn, *, limit: int = 200) -> list[dict]: - init_strategy_snapshot_table(conn) - rows = conn.execute( - "SELECT * FROM strategy_trade_snapshots ORDER BY id DESC LIMIT ?", - (max(1, min(int(limit), 500)),), - ).fetchall() - out = [] - seen: dict[tuple[str, int, str], int] = {} - for r in rows: - d = _row_dict(r) - try: - d["snapshot"] = json.loads(d.get("snapshot_json") or "{}") - except Exception: - d["snapshot"] = {} - st = (d.get("strategy_type") or "").strip() - d["strategy_label"] = "趋势回调" if st == STRATEGY_TREND else "顺势加仓" - enriched = enrich_strategy_snapshot_row(d) - try: - source_id = int(enriched.get("source_id") or 0) - except (TypeError, ValueError): - source_id = 0 - result_label = (enriched.get("result_label") or "").strip() - close_rank = _final_trend_close_rank(result_label) - if st == STRATEGY_TREND and source_id > 0 and close_rank > 0: - plan_key = (st, source_id) - snap_id = int(enriched.get("id") or 0) - prev = seen.get(plan_key) - if prev is not None: - prev_id, prev_rank = prev - if prev_rank > close_rank or (prev_rank == close_rank and prev_id >= snap_id): - continue - out = [x for x in out if int(x.get("id") or 0) != prev_id] - seen[plan_key] = (snap_id, close_rank) - out.append(enriched) - continue - key = (st, source_id, result_label) - snap_id = int(enriched.get("id") or 0) - prev = seen.get(key) - if prev is not None and prev[0] >= snap_id: - continue - if prev is not None: - out = [x for x in out if int(x.get("id") or 0) != prev[0]] - seen[key] = (snap_id, 0) - out.append(enriched) - return out - - -def list_strategy_snapshots_split( - conn, *, limit: int = STRATEGY_SNAPSHOTS_MAX_ROWS -) -> tuple[list[dict], list[dict], list[str]]: - """趋势 / 顺势分组,及筛选用币种列表.""" - all_rows = list_strategy_snapshots(conn, limit=limit) - trend = [r for r in all_rows if (r.get("strategy_type") or "") == STRATEGY_TREND] - roll = [r for r in all_rows if (r.get("strategy_type") or "") == STRATEGY_ROLL] - symbols = sorted({r.get("filter_symbol") or "" for r in all_rows if r.get("filter_symbol")}) - return trend, roll, symbols +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_trade_labels.py b/lib/strategy/strategy_trade_labels.py index 33bcb94..014df66 100644 --- a/lib/strategy/strategy_trade_labels.py +++ b/lib/strategy/strategy_trade_labels.py @@ -1,192 +1,12 @@ -"""策略交易写入 trade_records 时的类型与复盘开仓类型标注.""" -from __future__ import annotations - -from typing import Optional - -MONITOR_TYPE_TREND_PULLBACK = "趋势回调" -MONITOR_TYPE_ROLL = "顺势加仓" -ORDER_TYPE_MANUAL = "下单监控" -ORDER_TYPE_KEY = "关键位监控" - -ENTRY_REASON_TREND_PULLBACK = "趋势回调" -ENTRY_REASON_ROLL = "顺势加仓" - -JOURNAL_ORDER_TYPE_OPTIONS = ( - ORDER_TYPE_MANUAL, - ORDER_TYPE_KEY, - MONITOR_TYPE_TREND_PULLBACK, - MONITOR_TYPE_ROLL, -) - -STRATEGY_ENTRY_REASON_OPTIONS = ( - ENTRY_REASON_TREND_PULLBACK, - ENTRY_REASON_ROLL, -) - - -def normalize_journal_order_type(raw: Optional[str]) -> str: - s = (raw or "").strip() - if s in JOURNAL_ORDER_TYPE_OPTIONS: - return s - if "关键位" in s: - return ORDER_TYPE_KEY - return "" - - -def order_type_from_monitor_type( - monitor_type: Optional[str], - key_signal_type: Optional[str] = None, -) -> str: - del key_signal_type - mt = (monitor_type or "").strip() - if mt == MONITOR_TYPE_TREND_PULLBACK: - return MONITOR_TYPE_TREND_PULLBACK - if mt == MONITOR_TYPE_ROLL: - return MONITOR_TYPE_ROLL - if mt == ORDER_TYPE_KEY or "关键位" in mt: - return ORDER_TYPE_KEY - return ORDER_TYPE_MANUAL - -# 趋势回调保本移交下单监控:order_monitors.key_signal_type / 平仓备注 -TREND_HANDOFF_KEY_SIGNAL = ENTRY_REASON_TREND_PULLBACK -TREND_HANDOFF_TRADE_NOTE = "趋势回调计划" - - -def handoff_trade_miss_reason(miss_reason, row) -> Optional[str]: - """趋势保本移交的监控单平仓:交易记录备注带来源.""" - if trend_plan_id_from_monitor_row(row) is None: - return miss_reason - base = (miss_reason or "").strip() - if TREND_HANDOFF_TRADE_NOTE in base: - return base or TREND_HANDOFF_TRADE_NOTE - if base: - return f"{TREND_HANDOFF_TRADE_NOTE};{base}" - return TREND_HANDOFF_TRADE_NOTE - - -def trend_plan_id_from_monitor_row(row) -> Optional[int]: - if row is None: - return None - try: - keys = row.keys() if hasattr(row, "keys") else [] - except Exception: - keys = [] - if "trend_plan_id" not in keys or row["trend_plan_id"] in (None, ""): - return None - try: - tid = int(row["trend_plan_id"]) - return tid if tid > 0 else None - except (TypeError, ValueError): - return None - - -def order_had_roll_fills(conn, order_monitor_id) -> bool: - try: - oid = int(order_monitor_id) - except (TypeError, ValueError): - return False - if oid <= 0: - return False - try: - row = conn.execute( - """SELECT 1 FROM roll_legs l - INNER JOIN roll_groups g ON g.id = l.roll_group_id - WHERE g.order_monitor_id=? AND l.status='filled' - LIMIT 1""", - (oid,), - ).fetchone() - return row is not None - except Exception: - return False - - -def _row_monitor_type(row, default_manual: str) -> str: - if row is None: - return default_manual - try: - keys = row.keys() if hasattr(row, "keys") else [] - except Exception: - keys = [] - if "monitor_type" in keys: - mt = (row["monitor_type"] or "").strip() - if mt: - return mt - return default_manual - - -def _row_key_signal_type(row) -> str: - if row is None: - return "" - try: - keys = row.keys() if hasattr(row, "keys") else [] - except Exception: - keys = [] - if "key_signal_type" not in keys: - return "" - return (row["key_signal_type"] or "").strip() - - -def order_monitor_source_type(row, *, default_manual: str = "下单监控") -> str: - """展示/平仓记录:趋势保本移交单来源为「趋势回调」,非「下单监控」.""" - if trend_plan_id_from_monitor_row(row) is not None: - return MONITOR_TYPE_TREND_PULLBACK - mt = _row_monitor_type(row, default_manual) - if mt != default_manual: - return mt - kst = _row_key_signal_type(row) - if kst in ( - MONITOR_TYPE_TREND_PULLBACK, - TREND_HANDOFF_KEY_SIGNAL, - TREND_HANDOFF_TRADE_NOTE, - ENTRY_REASON_TREND_PULLBACK, - ): - return MONITOR_TYPE_TREND_PULLBACK - return mt - - -def apply_order_monitor_source_labels(item: dict, *, default_manual: str = "下单监控") -> dict: - """实例页 / 中控 API:统一修正 order_monitors 展示用 monitor_type.""" - out = dict(item or {}) - out["monitor_type"] = order_monitor_source_type(out, default_manual=default_manual) - return out - - -def trade_record_monitor_type(conn, order_row, *, default_manual: str = "下单监控") -> str: - """平仓写入 trade_records 时:曾顺势加仓则标「顺势加仓」,否则沿用监控单来源类型.""" - oid = None - try: - keys = order_row.keys() if hasattr(order_row, "keys") else [] - if "id" in keys and order_row["id"] is not None: - oid = int(order_row["id"]) - except Exception: - oid = None - if oid and order_had_roll_fills(conn, oid): - return MONITOR_TYPE_ROLL - return order_monitor_source_type(order_row, default_manual=default_manual) - - -def entry_reason_for_monitor_type(monitor_type: str | None) -> str: - mt = (monitor_type or "").strip() - if mt == MONITOR_TYPE_TREND_PULLBACK: - return ENTRY_REASON_TREND_PULLBACK - if mt == MONITOR_TYPE_ROLL: - return ENTRY_REASON_ROLL - return "" - - -def order_monitor_excluded_from_position_limit(conn, row) -> bool: - """趋势回调不计入 MAX_ACTIVE_POSITIONS;顺势加仓在已有持仓上操作,单独放行.""" - return order_monitor_source_type(row) == MONITOR_TYPE_TREND_PULLBACK - - -def count_position_limit_active_monitors(conn) -> int: - """计入仓位上限冻结的活跃监控数(不含趋势回调,顺势加仓).""" - try: - rows = conn.execute("SELECT * FROM order_monitors WHERE status='active'").fetchall() - except Exception: - return 0 - n = 0 - for row in rows: - if not order_monitor_excluded_from_position_limit(conn, row): - n += 1 - return n +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_trend_exchange.py b/lib/strategy/strategy_trend_exchange.py index 02be3ce..614fb9e 100644 --- a/lib/strategy/strategy_trend_exchange.py +++ b/lib/strategy/strategy_trend_exchange.py @@ -1,97 +1,12 @@ -"""趋势回调:各交易所止损刷新,市价加/平仓(通过 app 模块能力探测).""" -from __future__ import annotations - -import time -from typing import Any - - -def _m(cfg: dict) -> Any: - return cfg["app_module"] - - -def trend_refresh_stop_only(cfg: dict, exchange_symbol: str, direction: str, stop_loss: float) -> None: - m = _m(cfg) - if hasattr(m, "_gate_place_stop_loss_only_position"): - if hasattr(m, "cancel_gate_swap_trigger_orders"): - m.cancel_gate_swap_trigger_orders(exchange_symbol) - m._gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss) - return - if hasattr(m, "_binance_place_stop_loss_only"): - m._binance_place_stop_loss_only(exchange_symbol, direction, stop_loss) - return - if hasattr(m, "_okx_place_stop_loss_only"): - m._okx_place_stop_loss_only(exchange_symbol, direction, stop_loss) - return - raise RuntimeError("当前实例未配置趋势回调止损挂单能力") - - -def trend_market_add(cfg: dict, exchange_symbol: str, direction: str, contracts: float, leverage: int): - m = _m(cfg) - ex = m.exchange - m.ensure_markets_loaded() - ex.set_leverage(int(leverage), exchange_symbol) - side = "buy" if direction == "long" else "sell" - if hasattr(m, "build_gate_order_params"): - params = m.build_gate_order_params(direction, reduce_only=False) - elif hasattr(m, "build_binance_order_params"): - params = m.build_binance_order_params(direction, reduce_only=False) - elif hasattr(m, "build_okx_order_params"): - params = m.build_okx_order_params(direction, reduce_only=False) - else: - params = {} - order_params = params if params is not None else {} - return ex.create_order(exchange_symbol, "market", side, float(contracts), None, order_params) - - -def trend_market_close(cfg: dict, exchange_symbol: str, direction: str, pos_qty: float, leverage: int): - m = _m(cfg) - ex = m.exchange - m.ensure_markets_loaded() - ex.set_leverage(int(leverage), exchange_symbol) - side = "sell" if direction == "long" else "buy" - amt = float(ex.amount_to_precision(exchange_symbol, float(pos_qty))) - if hasattr(m, "close_exchange_order"): - row = { - "exchange_symbol": exchange_symbol, - "symbol": exchange_symbol, - "direction": direction, - "order_amount": amt, - } - return m.close_exchange_order(row) - if hasattr(m, "build_gate_order_params"): - params = m.build_gate_order_params(direction, reduce_only=True) - return ex.create_order(exchange_symbol, "market", side, amt, None, params) - if hasattr(m, "build_binance_order_params"): - for params in m._binance_market_close_param_candidates(direction): - try: - return ex.create_order(exchange_symbol, "market", side, amt, None, params) - except Exception as e: - if not m._is_binance_close_param_retryable(str(e)): - raise - raise RuntimeError("平仓失败") - if hasattr(m, "build_okx_order_params"): - params = m.build_okx_order_params(direction, reduce_only=True) - return ex.create_order(exchange_symbol, "market", side, amt, None, params) - return ex.create_order(exchange_symbol, "market", side, amt, None, {"reduceOnly": True}) - - -def trend_replace_tpsl(cfg: dict, order_row: dict, stop_loss: float, take_profit: float) -> None: - """趋势保本移交:先撤条件单再挂保本止损 + 计划止盈(与下单监控一致).""" - m = _m(cfg) - fn = getattr(m, "replace_active_monitor_tpsl_on_exchange", None) - if not callable(fn): - raise RuntimeError("当前实例未配置止盈止损同步能力") - fn(order_row, float(stop_loss), float(take_profit)) - - -def cancel_symbol_orders(cfg: dict, exchange_symbol: str) -> None: - m = _m(cfg) - if hasattr(m, "cancel_all_open_orders_for_symbol"): - m.cancel_all_open_orders_for_symbol(exchange_symbol) - return - if hasattr(m, "cancel_gate_swap_trigger_orders"): - m.cancel_gate_swap_trigger_orders(exchange_symbol) - if hasattr(m, "cancel_binance_futures_open_orders"): - m.cancel_binance_futures_open_orders(exchange_symbol) - if hasattr(m, "cancel_okx_swap_open_orders"): - m.cancel_okx_swap_open_orders(exchange_symbol) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_trend_lib.py b/lib/strategy/strategy_trend_lib.py index f0dcad2..414a03d 100644 --- a/lib/strategy/strategy_trend_lib.py +++ b/lib/strategy/strategy_trend_lib.py @@ -1,701 +1,12 @@ -"""趋势回调策略:纯计算与校验(无 ccxt / Flask).各所 adapter 负责张数精度与下单.""" -from __future__ import annotations - -import json -from typing import Any, Callable, Optional, Tuple - -AmountPreciseFn = Callable[[str, float], Optional[float]] - - -def calc_risk_fraction(direction: str, entry_price: float, stop_loss: float) -> Optional[float]: - try: - entry = float(entry_price) - sl = float(stop_loss) - if entry <= 0 or sl <= 0: - return None - if (direction or "long").strip().lower() == "short": - risk = sl - entry - else: - risk = entry - sl - if risk <= 0: - return None - return risk / entry - except (TypeError, ValueError): - return None - - -def trend_effective_margin_capital(plan: dict) -> float: - """按已开仓张数占计划总张数比例折算保证金(首仓/部分补仓时的盈亏估算).""" - try: - plan_margin = float(plan.get("plan_margin_capital") or 0) - target = float(plan.get("target_order_amount") or 0) - open_amt = float(plan.get("order_amount_open") or 0) - except (TypeError, ValueError): - return float((plan or {}).get("plan_margin_capital") or 0) - if plan_margin <= 0: - return 0.0 - if target > 0 and open_amt > 0: - return round(plan_margin * min(1.0, open_amt / target), 8) - try: - first = float(plan.get("first_order_amount") or 0) - except (TypeError, ValueError): - first = 0.0 - if target > 0 and first > 0: - return round(plan_margin * min(1.0, first / target), 8) - return plan_margin - - -def trend_dca_level_reached(direction: str, mark_price: float, level: float) -> bool: - """做空:价升触达/越过档位即应补仓;做多:价跌触达/越过档位.""" - d = (direction or "long").strip().lower() - try: - pf = float(mark_price) - lv = float(level) - except (TypeError, ValueError): - return False - if d == "long": - return pf <= lv - return pf >= lv - - -def validate_trend_bounds(direction: str, stop_loss: float, add_upper: float) -> Optional[str]: - direction = (direction or "long").strip().lower() - if direction == "long": - if not (float(stop_loss) < float(add_upper)): - return "做多:止损价须低于补仓上沿" - else: - if not (float(stop_loss) > float(add_upper)): - return "做空:止损价须高于补仓下沿" - return None - - -def build_grid_prices(direction: str, sl: float, upper: float, n_legs: int) -> list[float]: - """在 (止损, 补仓区间远侧边界) 内生成 n_legs 个触发价(不含端点).""" - sl, upper = float(sl), float(upper) - out: list[float] = [] - if n_legs <= 0: - return out - direction = (direction or "long").strip().lower() - if direction == "long": - if upper <= sl: - return out - span = upper - sl - for i in range(1, n_legs + 1): - t = i / float(n_legs + 1) - out.append(sl + t * span) - out.sort(reverse=True) - else: - if sl <= upper: - return out - span = sl - upper - for i in range(1, n_legs + 1): - t = i / float(n_legs + 1) - out.append(upper + t * span) - out.sort() - return [round(p, 10) for p in out] - - -def pick_dca_legs_and_per_leg( - exchange_symbol: str, - remainder_total: float, - want_legs: int, - amount_precise: AmountPreciseFn, - min_amount: float = 0.0, -) -> Tuple[int, float]: - """按最小张数约束自动减少档位数.返回 (有效档数, 每档参考张数).""" - legs = max(1, int(want_legs)) - rem = float(remainder_total) - min_amt = float(min_amount or 0.0) - while legs >= 1: - per = rem / legs - per_p = amount_precise(exchange_symbol, per) - if per_p is None or per_p <= 0: - legs -= 1 - continue - if min_amt and per_p + 1e-12 < min_amt: - legs -= 1 - continue - return legs, per_p - one = amount_precise(exchange_symbol, rem) - if one is None or one <= 0: - return 0, 0.0 - return 1, one - - -def build_leg_amounts_json( - exchange_symbol: str, - remainder_total: float, - want_legs: int, - amount_precise: AmountPreciseFn, - min_amount: float = 0.0, -) -> Tuple[int, str, float]: - """拆分补仓张数 JSON.返回 (档位数, json列表, 每档参考).""" - rem = amount_precise(exchange_symbol, float(remainder_total)) - if rem is None or rem <= 0: - return 0, "[]", 0.0 - n, _ = pick_dca_legs_and_per_leg(exchange_symbol, rem, want_legs, amount_precise, min_amount) - if n <= 0: - return 0, "[]", 0.0 - if n <= 1: - one = amount_precise(exchange_symbol, rem) - if one is None or one <= 0: - return 0, "[]", 0.0 - return 1, json.dumps([one]), one - unit = amount_precise(exchange_symbol, rem / n) - if unit is None or unit <= 0: - one = amount_precise(exchange_symbol, rem) - if one is None or one <= 0: - return 0, "[]", 0.0 - return 1, json.dumps([one]), one - parts: list[float] = [] - acc = 0.0 - for _ in range(n - 1): - parts.append(unit) - acc += unit - last = amount_precise(exchange_symbol, max(0.0, rem - acc)) - if last is None or last <= 0: - one = amount_precise(exchange_symbol, rem) - if one is None or one <= 0: - return 0, "[]", 0.0 - return 1, json.dumps([one]), one - parts.append(last) - return n, json.dumps(parts), unit - - -def compute_trend_plan_core( - *, - direction: str, - stop_loss: float, - add_upper: float, - risk_percent: float, - snapshot_usdt: float, - leverage: int, - live_price: float, - target_order_amount: float, - exchange_symbol: str, - dca_legs: int, - amount_precise: AmountPreciseFn, - min_amount: float = 0.0, - full_margin_buffer_ratio: float = 0.95, -) -> Tuple[Optional[dict[str, Any]], Optional[str]]: - """在已有 target_order_amount 时组装预览 payload(张数由调用方 prepare_order_amount 计算).""" - rf = calc_risk_fraction(direction, add_upper, stop_loss) - if rf is None or rf <= 0: - return None, "止损与补仓区间边界组合无法计算风险比例" - risk_budget = float(snapshot_usdt) * (float(risk_percent) / 100.0) - notional = risk_budget / rf - margin_plan = notional / float(leverage) - margin_plan = min(margin_plan, float(snapshot_usdt) * float(full_margin_buffer_ratio)) - if margin_plan <= 0: - return None, "计划保证金过小" - first_amt = amount_precise(exchange_symbol, float(target_order_amount) * 0.5) - if first_amt is None or first_amt <= 0: - return None, "首仓张数过小(低于交易所最小张数),请提高风险比例或杠杆" - remainder_total = amount_precise(exchange_symbol, max(0.0, float(target_order_amount) - float(first_amt))) - if remainder_total is None: - remainder_total = 0.0 - n_legs, leg_json, per_ref = build_leg_amounts_json( - exchange_symbol, remainder_total, dca_legs, amount_precise, min_amount - ) - if n_legs <= 0: - return None, "剩余计划张数不足以拆出补仓档,请提高风险比例或放宽止损与补仓区间间距" - grid = build_grid_prices(direction, stop_loss, add_upper, n_legs) - if len(grid) != n_legs: - return None, "补仓网格生成失败" - try: - leg_list = json.loads(leg_json) - except Exception: - leg_list = [] - payload = { - "direction": direction, - "stop_loss": float(stop_loss), - "add_upper": float(add_upper), - "risk_percent": float(risk_percent), - "snapshot_available_usdt": float(snapshot_usdt), - "live_price_ref": float(live_price), - "plan_margin_capital": float(margin_plan), - "target_order_amount": float(target_order_amount), - "first_order_amount": float(first_amt), - "remainder_total": float(remainder_total), - "dca_legs": int(n_legs), - "per_leg_amount": float(per_ref), - "grid_prices_json": json.dumps(grid), - "leg_amounts_json": leg_json, - "grid": grid, - "leg_amounts": leg_list, - } - return payload, None - - -def calc_planned_reward_risk_ratio( - direction: str, entry_price: float, stop_loss: float, take_profit: float -) -> Optional[float]: - """盈亏比(reward/risk),与三所 calc_rr_ratio 口径一致.""" - try: - entry = float(entry_price) - sl = float(stop_loss) - tp = float(take_profit) - if entry <= 0 or sl <= 0 or tp <= 0: - return None - direction = (direction or "long").strip().lower() - if direction == "short": - risk = sl - entry - reward = entry - tp - else: - risk = entry - sl - reward = tp - entry - if risk <= 0 or reward <= 0: - return None - return round(reward / risk, 4) - except (TypeError, ValueError): - return None - - -def calc_take_profit_for_rr( - direction: str, entry_price: float, stop_loss: float, reward_risk_ratio: float -) -> Optional[float]: - """按统一止损与目标 RR 反推止盈价.""" - try: - entry = float(entry_price) - sl = float(stop_loss) - rr = float(reward_risk_ratio) - if entry <= 0 or sl <= 0 or rr <= 0: - return None - direction = (direction or "long").strip().lower() - if direction == "short": - risk = sl - entry - if risk <= 0: - return None - return round(entry - rr * risk, 10) - risk = entry - sl - if risk <= 0: - return None - return round(entry + rr * risk, 10) - except (TypeError, ValueError): - return None - - -def calc_risk_budget_usdt(snapshot_usdt: float, risk_percent: float) -> Optional[float]: - """计划止损金额 U = 可用快照 × 风险比例.""" - try: - snap = float(snapshot_usdt) - rp = float(risk_percent) - if snap <= 0 or rp <= 0: - return None - return round(snap * rp / 100.0, 4) - except (TypeError, ValueError): - return None - - -def calc_money_reward_risk_ratio(profit_u: float, risk_u: float) -> Optional[float]: - """金额盈亏比 = 止盈盈利 U / 止损金额 U.""" - try: - r = float(risk_u) - p = float(profit_u) - if r <= 0: - return None - return round(p / r, 4) - except (TypeError, ValueError): - return None - - -def calc_tp_profit_usdt( - direction: str, - avg_entry: float, - take_profit_price: float, - contracts: float, - contract_size: float = 1.0, -) -> Optional[float]: - """到达止盈价时,按累计张数与加仓后均价的净盈利 U(扣双边 taker 费).""" - try: - from lib.hub.hub_position_metrics import estimate_linear_swap_upnl_usdt - from lib.trade.trade_fee_lib import net_pnl_after_fee - - gross = estimate_linear_swap_upnl_usdt( - direction, float(avg_entry), float(take_profit_price), float(contracts), float(contract_size) - ) - if gross is None: - return None - return net_pnl_after_fee( - gross, avg_entry, take_profit_price, contracts, contract_size - ) - except (TypeError, ValueError): - return None - - -def weighted_avg_entry(legs: list[tuple[float, float]]) -> Optional[float]: - """按 (成交价, 张数) 加权均价.""" - total = 0.0 - cost = 0.0 - for price, amount in legs or []: - try: - p = float(price) - a = float(amount) - except (TypeError, ValueError): - continue - if a <= 0: - continue - total += a - cost += p * a - if total <= 0: - return None - return cost / total - - -def parse_leg_fill_prices(plan: dict) -> list[float]: - """首仓 + 各档补仓实际成交价列表.""" - try: - raw = json.loads((plan or {}).get("leg_fill_prices_json") or "[]") - if not isinstance(raw, list): - return [] - out: list[float] = [] - for item in raw: - try: - out.append(float(item)) - except (TypeError, ValueError): - continue - return out - except Exception: - return [] - - -def append_leg_fill_price_json(existing_json: str | None, fill_px: float) -> str: - fills = parse_leg_fill_prices({"leg_fill_prices_json": existing_json}) - fills.append(float(fill_px)) - return json.dumps(fills, ensure_ascii=False, separators=(",", ":")) - - -def trend_leg_grid_price(plan: dict, leg_idx: int) -> Optional[float]: - """补仓 leg_idx(1..N) 的计划网格触发价;首仓返回 None.""" - if leg_idx <= 0: - return None - try: - grid = [float(x) for x in json.loads((plan or {}).get("grid_prices_json") or "[]")] - except Exception: - grid = [] - gi = leg_idx - 1 - if 0 <= gi < len(grid): - return float(grid[gi]) - return None - - -def trend_leg_display_price(plan: dict, leg_idx: int) -> Optional[float]: - """ - 三所统一:单档展示价 = leg_fill_prices_json 实际记录,否则计划网格(首仓用均价/参考价). - 禁止为凑均价反推虚构成交价. - """ - p = plan or {} - fills = parse_leg_fill_prices(p) - if len(fills) > leg_idx: - return float(fills[leg_idx]) - if leg_idx == 0: - try: - return float(p.get("avg_entry_price")) - except (TypeError, ValueError): - pass - try: - ref = p.get("live_price_ref") - if ref not in (None, ""): - return float(ref) - except (TypeError, ValueError): - pass - return None - return trend_leg_grid_price(p, leg_idx) - - -def reconcile_trend_leg_fill_prices(plan: dict) -> list[float]: - """首仓(0)+已补仓(1..legs_done) 展示价列表(三所共用 trend_leg_display_price).""" - p = plan or {} - if int(p.get("first_order_done") or 0) == 0: - return [] - try: - legs_done = int(p.get("legs_done") or 0) - except (TypeError, ValueError): - legs_done = 0 - result: list[float] = [] - for leg_idx in range(legs_done + 1): - px = trend_leg_display_price(p, leg_idx) - result.append(float(px) if px is not None else 0.0) - return result - - -def calc_trend_plan_money_metrics(plan: dict) -> dict: - """运行中计划头部:按快照风险金额计算盈亏比(止盈盈利 U / 风险 U).""" - out = {"money_rr": None, "risk_amount_u": None} - p = plan or {} - try: - direction = (p.get("direction") or "long").strip().lower() - user_tp = float(p.get("take_profit")) - avg = float(p.get("avg_entry_price")) - open_amt = float(p.get("order_amount_open") or p.get("first_order_amount") or 0) - snapshot = float(p.get("snapshot_available_usdt")) - risk_percent = float(p.get("risk_percent")) - except (TypeError, ValueError): - return out - if avg <= 0 or open_amt <= 0: - return out - risk_u = calc_risk_budget_usdt(snapshot, risk_percent) - if risk_u is None or risk_u <= 0: - return out - out["risk_amount_u"] = risk_u - try: - contract_size = float(p.get("contract_size") or 1.0) - if contract_size <= 0: - contract_size = 1.0 - except (TypeError, ValueError): - contract_size = 1.0 - profit_u = calc_tp_profit_usdt(direction, avg, user_tp, open_amt, contract_size) - out["money_rr"] = calc_money_reward_risk_ratio(profit_u, risk_u) - return out - - -def build_trend_preview_level_rows(preview: dict) -> tuple[dict, list[dict]]: - """ - 预览:表单止盈价下每档累计持仓的盈利 U;止损金额 = 快照×风险;盈亏比按金额对比. - 返回 (增强后的 preview 字段, 表格行列表,含首仓行). - """ - p = dict(preview or {}) - direction = (p.get("direction") or "long").strip().lower() - try: - ref = float(p.get("live_price_ref")) - sl = float(p.get("stop_loss")) - user_tp = float(p.get("take_profit")) - first_amt = float(p.get("first_order_amount")) - snapshot = float(p.get("snapshot_available_usdt")) - risk_percent = float(p.get("risk_percent")) - except (TypeError, ValueError): - return p, [] - - risk_u = calc_risk_budget_usdt(snapshot, risk_percent) - if risk_u is None or risk_u <= 0: - return p, [] - - try: - contract_size = float(p.get("contract_size") or 1.0) - if contract_size <= 0: - contract_size = 1.0 - except (TypeError, ValueError): - contract_size = 1.0 - - p["preview_risk_amount_u"] = risk_u - p["preview_take_profit_price"] = user_tp - p["preview_unified_stop_loss"] = sl - - try: - grid = json.loads(p.get("grid_prices_json") or "[]") - if not isinstance(grid, list): - grid = [] - except Exception: - grid = [] - try: - leg_amounts = json.loads(p.get("leg_amounts_json") or "[]") - if not isinstance(leg_amounts, list): - leg_amounts = [] - except Exception: - leg_amounts = [] - - def _row_dict( - *, - i: int, - label: str, - price: float, - leg_contracts: float, - cum_contracts: float, - avg: float, - is_first: bool, - ) -> dict: - profit_u = calc_tp_profit_usdt(direction, avg, user_tp, cum_contracts, contract_size) - rr_money = calc_money_reward_risk_ratio(profit_u, risk_u) if profit_u is not None else None - return { - "i": i, - "label": label, - "price": price, - "contracts": leg_contracts, - "cum_contracts": cum_contracts, - "avg_entry": avg, - "take_profit_price": user_tp, - "profit_u": profit_u, - "risk_u": risk_u, - "rr": rr_money, - "stop_loss_price": sl, - "take_profit": profit_u, - "stop_loss": risk_u, - "is_first": is_first, - } - - cum_contracts = first_amt - first_profit = calc_tp_profit_usdt(direction, ref, user_tp, cum_contracts, contract_size) - first_rr = calc_money_reward_risk_ratio(first_profit, risk_u) if first_profit is not None else None - p["preview_first_profit_u"] = first_profit - p["preview_target_rr"] = first_rr - p["preview_first_take_profit"] = user_tp - - rows: list[dict] = [ - _row_dict( - i=0, - label="首仓", - price=ref, - leg_contracts=first_amt, - cum_contracts=cum_contracts, - avg=ref, - is_first=True, - ) - ] - accumulated: list[tuple[float, float]] = [(ref, first_amt)] - for i, pair in enumerate(zip(grid, leg_amounts), 1): - try: - price = float(pair[0]) - leg_contracts = float(pair[1]) - except (TypeError, ValueError): - continue - accumulated.append((price, leg_contracts)) - avg = weighted_avg_entry(accumulated) - if avg is None: - continue - cum_contracts += leg_contracts - rows.append( - _row_dict( - i=i, - label=f"补仓{i}", - price=price, - leg_contracts=leg_contracts, - cum_contracts=cum_contracts, - avg=avg, - is_first=False, - ) - ) - return p, rows - - -def enrich_trend_dca_levels_with_tp(plan: dict, levels: list[dict]) -> list[dict]: - """ - 三所统一补仓表 enrich(实例策略页 + 中控 monitor 共用). - 触发价:实际成交价或计划网格;末档加仓后均价用持仓均价;禁止反推虚构成交价. - """ - if not levels: - return levels - p = plan or {} - direction = (p.get("direction") or "long").strip().lower() - try: - sl = float(p.get("stop_loss")) - user_tp = float(p.get("take_profit")) - first_amt = float(p.get("first_order_amount")) - snapshot = float(p.get("snapshot_available_usdt")) - risk_percent = float(p.get("risk_percent")) - except (TypeError, ValueError): - return levels - - risk_u = calc_risk_budget_usdt(snapshot, risk_percent) - if risk_u is None or risk_u <= 0: - return levels - - try: - legs_done = int(p.get("legs_done") or 0) - except (TypeError, ValueError): - legs_done = 0 - first_done = int(p.get("first_order_done") or 0) != 0 - try: - target_avg = float(p.get("avg_entry_price")) - except (TypeError, ValueError): - target_avg = None - - ref_raw = p.get("live_price_ref") - if ref_raw in (None, ""): - ref_raw = p.get("avg_entry_price") - try: - ref = float(ref_raw) - except (TypeError, ValueError): - return levels - - try: - contract_size = float(p.get("contract_size") or 1.0) - if contract_size <= 0: - contract_size = 1.0 - except (TypeError, ValueError): - contract_size = 1.0 - - out: list[dict] = [] - accumulated: list[tuple[float, float]] = [] - cum_contracts = 0.0 - for lv in levels: - row = dict(lv) - is_first = row.get("leg_key") == "first" or row.get("label") == "首仓" or row.get("i") == 0 - row_cum = cum_contracts - if is_first: - try: - amt_f = float(row.get("contracts") if row.get("contracts") is not None else first_amt) - except (TypeError, ValueError): - amt_f = first_amt - if first_done: - fill_px = trend_leg_display_price(p, 0) - if fill_px is None: - try: - fill_px = float(p.get("avg_entry_price") or ref) - except (TypeError, ValueError): - fill_px = ref - accumulated = [(float(fill_px), amt_f)] - cum_contracts = amt_f - row_cum = cum_contracts - row["price"] = fill_px - if target_avg is not None and legs_done == 0: - row["avg_entry"] = target_avg - else: - row["avg_entry"] = float(fill_px) - else: - accumulated = [(ref, amt_f)] - cum_contracts = amt_f - row_cum = cum_contracts - row["avg_entry"] = ref - else: - try: - leg_num = int(row.get("i") or 0) - except (TypeError, ValueError): - leg_num = 0 - grid_trigger = row.get("price") - try: - grid_trigger_f = float(grid_trigger) if grid_trigger is not None else None - except (TypeError, ValueError): - grid_trigger_f = None - try: - leg_contracts = float(row.get("contracts") or 0) - except (TypeError, ValueError): - leg_contracts = 0.0 - done = row.get("status") == "done" or (leg_num > 0 and leg_num <= legs_done) - if done and leg_contracts > 0: - fill_px = trend_leg_display_price(p, leg_num) - if fill_px is None: - fill_px = grid_trigger_f if grid_trigger_f is not None else ref - row["price"] = fill_px - accumulated.append((fill_px, leg_contracts)) - cum_contracts += leg_contracts - row_cum = cum_contracts - if leg_num == legs_done and target_avg is not None: - row["avg_entry"] = target_avg - else: - avg = weighted_avg_entry(accumulated) - if avg is not None: - row["avg_entry"] = avg - elif grid_trigger_f is not None and leg_contracts > 0: - row["price"] = grid_trigger_f - projected = accumulated + [(grid_trigger_f, leg_contracts)] - avg = weighted_avg_entry(projected) - if avg is not None: - row["avg_entry"] = avg - row_cum = cum_contracts + leg_contracts - elif grid_trigger_f is not None: - row["price"] = grid_trigger_f - - avg_entry = row.get("avg_entry") - if avg_entry is not None and row_cum > 0: - profit_u = calc_tp_profit_usdt( - direction, float(avg_entry), user_tp, row_cum, contract_size - ) - row["take_profit_price"] = user_tp - row["profit_u"] = profit_u - row["risk_u"] = risk_u - row["rr"] = calc_money_reward_risk_ratio(profit_u, risk_u) if profit_u is not None else None - row["take_profit"] = profit_u - row["stop_loss"] = risk_u - row["stop_loss_price"] = sl - out.append(row) - return out +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_trend_register.py b/lib/strategy/strategy_trend_register.py index ef67e1e..a78cfaf 100644 --- a/lib/strategy/strategy_trend_register.py +++ b/lib/strategy/strategy_trend_register.py @@ -1,1972 +1,12 @@ -"""趋势回调:路由,轮询,页面数据(三所共用,依赖各 app 模块交易所能力).""" -from __future__ import annotations - -import inspect -import json -import os -import time -import uuid -from typing import Any, Optional - -from flask import Flask, flash, redirect, request, url_for -from jinja2 import ChoiceLoader, FileSystemLoader - -from lib.strategy.strategy_config import resolve_trading_app_module -from lib.strategy.strategy_db import init_strategy_tables -from lib.strategy.strategy_trend_exchange import ( - cancel_symbol_orders, - trend_market_add, - trend_market_close, - trend_refresh_stop_only, - trend_replace_tpsl, -) -from lib.strategy.strategy_trend_lib import ( - build_grid_prices, - build_leg_amounts_json, - calc_risk_fraction, - trend_dca_level_reached, - trend_effective_margin_capital, - validate_trend_bounds, -) -from lib.strategy.strategy_trade_labels import ( - ENTRY_REASON_TREND_PULLBACK, - MONITOR_TYPE_TREND_PULLBACK, - TREND_HANDOFF_KEY_SIGNAL, - TREND_HANDOFF_TRADE_NOTE, -) - -MONITOR_TYPE_TREND = MONITOR_TYPE_TREND_PULLBACK - -# 趋势回调:交易所报空仓需连续 N 次轮询确认,避免 OKX 等 API 瞬时误判立即结束计划 -_TREND_FLAT_STREAK: dict[int, int] = {} -TREND_FLAT_CONFIRM_POLLS = max(1, int(os.getenv("TREND_FLAT_CONFIRM_POLLS", "5"))) -TREND_OPEN_GRACE_SEC = max(0, int(os.getenv("TREND_OPEN_GRACE_SEC", "180"))) -_TREND_LIVE_SKIP_LOG_TS = 0.0 -_TREND_POLL_STATE: dict[str, Any] = { - "updated_at": None, - "live_ok": True, - "live_reason": "", - "plans": {}, -} - - -def get_trend_poll_state() -> dict: - return dict(_TREND_POLL_STATE or {}) - - -def _log_trend_live_skip(reason: str) -> None: - global _TREND_LIVE_SKIP_LOG_TS - now = time.time() - if now - _TREND_LIVE_SKIP_LOG_TS < 60: - return - _TREND_LIVE_SKIP_LOG_TS = now - print(f"[trend_pullback] poll skipped (live not ready): {reason}", flush=True) - - -def _set_trend_poll_plan(plan_id: int, info: dict) -> None: - plans = dict(_TREND_POLL_STATE.get("plans") or {}) - plans[str(plan_id)] = info - _TREND_POLL_STATE["plans"] = plans - - -def summarize_trend_dca_probe(cfg: dict, row) -> dict: - """诊断单计划为何未补仓(供页面 / API).""" - m = _m(cfg) - d = _row(cfg, row) - plan_id = int(d.get("id") or 0) - sym = d.get("symbol") or "" - direction = (d.get("direction") or "long").lower() - ex_sym = d.get("exchange_symbol") or m.normalize_exchange_symbol(sym) - out: dict[str, Any] = { - "plan_id": plan_id, - "symbol": sym, - "mark_price": None, - "next_trigger": None, - "trigger_reached": False, - "legs_done": int(d.get("legs_done") or 0), - "first_order_done": int(d.get("first_order_done") or 0), - "block_reason": None, - } - try: - legs_done = int(d.get("legs_done") or 0) - grid = json.loads(d.get("grid_prices_json") or "[]") - if not isinstance(grid, list): - grid = [] - leg_amounts = json.loads(d.get("leg_amounts_json") or "[]") - if not isinstance(leg_amounts, list): - leg_amounts = [] - except Exception: - grid = [] - leg_amounts = [] - legs_done = 0 - pf = _trend_poll_price(m, sym, ex_sym, direction) - out["mark_price"] = pf - ok_live, live_reason = m.ensure_exchange_live_ready() - out["live_ok"] = ok_live - if not ok_live: - out["block_reason"] = live_reason or "实盘未就绪" - if not int(d.get("first_order_done") or 0): - out["block_reason"] = out["block_reason"] or "首仓未完成" - return out - if legs_done >= len(grid) or legs_done >= len(leg_amounts): - out["block_reason"] = out["block_reason"] or "补仓档已全部完成或无 grid" - return out - try: - level = float(grid[legs_done]) - except (TypeError, ValueError, IndexError): - out["block_reason"] = out["block_reason"] or "无效补仓触发价" - return out - out["next_trigger"] = level - if pf is None: - out["block_reason"] = out["block_reason"] or "无法读取标记价" - return out - reached = trend_dca_level_reached(direction, float(pf), level) - out["trigger_reached"] = reached - if reached and not ok_live: - out["block_reason"] = live_reason or "LIVE_TRADING_ENABLED=false" - elif reached and ok_live: - pos = m.get_live_position_contracts(ex_sym, direction) - try: - local_open = float(d.get("order_amount_open") or 0) - except (TypeError, ValueError): - local_open = 0.0 - if pos is None and local_open > 0: - pos = local_open - if pos is None: - out["block_reason"] = "无法读取交易所持仓" - elif float(pos) <= 0: - out["block_reason"] = "交易所无持仓" - else: - out["block_reason"] = ( - "标记价已触达,轮询应自动下单;若仍未补请确认 PM2 进程 crypto_gate " - "(或对应所 Flask 进程)在运行,并查看 pm2 logs" - ) - elif not reached: - out["block_reason"] = f"标记价 {pf} 未触达下一档 {level}" - return out - - -def trend_add_zone_label(direction: str) -> str: - return "补仓下沿" if (direction or "long").strip().lower() == "short" else "补仓上沿" - - -def install_strategy_trend(app: Flask, repo_root: str, app_module: Any = None, **build_kw) -> dict: - from lib.strategy.strategy_register import attach_strategy_templates - - attach_strategy_templates(app, repo_root) - cfg = build_trend_config(app_module, **build_kw) - app.extensions["strategy_trend_cfg"] = cfg - register_trend_routes(app, cfg) - _patch_hub_monitor_enrich(app, cfg) - roll_cfg = app.extensions.get("strategy_roll_cfg") - if isinstance(roll_cfg, dict): - from lib.strategy.strategy_roll_ui_lib import patch_roll_hub_enrich - - patch_roll_hub_enrich(app, roll_cfg) - _patch_hub_trend_views(app) - - @app.context_processor - def _trend_ctx(): - return {"trend_add_zone_label": trend_add_zone_label} - - return cfg - - -def build_trend_config(app_module: Any = None, **kw) -> dict[str, Any]: - m = resolve_trading_app_module(app_module) - dca = max(1, int(os.getenv("TREND_PULLBACK_DCA_LEGS", kw.get("dca_legs", "5")))) - preview_ttl = max(10, int(os.getenv("TREND_PULLBACK_PREVIEW_TTL_SECONDS", "120"))) - drift = float(os.getenv("TREND_PREVIEW_MAX_BALANCE_DRIFT_PCT", "5")) - be_pct = float(os.getenv("TREND_PULLBACK_MANUAL_BREAKEVEN_OFFSET_PCT", "0.3")) - buf = float(getattr(m, "FULL_MARGIN_BUFFER_RATIO", 0.95)) - - def amount_precise(ex_sym, amt): - fn = getattr(m, "_safe_amount_to_precision", None) - if callable(fn): - return fn(ex_sym, amt) - try: - m.ensure_markets_loaded() - return float(m.exchange.amount_to_precision(ex_sym, float(amt))) - except Exception: - return None - - def send_wechat(content): - fn = getattr(m, "send_wechat_msg", None) - if callable(fn): - fn(content) - - def wechat_account_label(): - fn = getattr(m, "_wechat_account_label", None) - if callable(fn): - try: - return fn() - except Exception: - pass - return getattr(m, "EXCHANGE_DISPLAY_NAME", "") or "" - - def wechat_direction_text(direction): - fn = getattr(m, "_wechat_direction_text", None) - if callable(fn): - try: - return fn(direction) - except Exception: - pass - d = (direction or "long").strip().lower() - return "做多" if d == "long" else "做空" - - return { - "app_module": m, - "exchange_display": getattr(m, "EXCHANGE_DISPLAY_NAME", ""), - "login_required": m.login_required, - "get_db": m.get_db, - "row_to_dict": m.row_to_dict, - "dca_legs": dca, - "preview_ttl": preview_ttl, - "drift_pct": drift, - "breakeven_offset_pct": be_pct, - "margin_buffer": buf, - "amount_precise": amount_precise, - "max_active_positions": int(getattr(m, "MAX_ACTIVE_POSITIONS", 1)), - "reset_hour": int(getattr(m, "TRADING_DAY_RESET_HOUR", 8)), - "monitor_type_trend": MONITOR_TYPE_TREND, - "send_wechat": send_wechat, - "format_price": getattr(m, "format_price_for_symbol", None), - "wechat_account_label": wechat_account_label, - "wechat_direction_text": wechat_direction_text, - } - - -def _m(cfg: dict): - return cfg["app_module"] - - -def _row(cfg, row) -> dict: - return cfg["row_to_dict"](row) - - -def precheck_trend_start(cfg: dict, conn, *, symbol: str = "", direction: str = "long") -> tuple[bool, str]: - m = _m(cfg) - mode = getattr(m, "POSITION_SIZING_MODE", None) or "risk" - try: - from lib.trade.position_sizing_lib import OPEN_SOURCE_TREND, assert_open_source_allowed - - ok_src, src_msg = assert_open_source_allowed(mode, OPEN_SOURCE_TREND) - if not ok_src: - return False, src_msg - except Exception: - pass - sym = (symbol or "").strip() - dir_l = (direction or "long").strip().lower() - validate_fn = getattr(m, "validate_trade_policy_open", None) - if callable(validate_fn) and sym: - ok_pol, pol_msg = validate_fn(sym, dir_l) - if not ok_pol: - return False, pol_msg - if sym and dir_l in ("long", "short") and hasattr(m, "precheck_risk"): - ok_risk, risk_msg = m.precheck_risk(conn, sym, dir_l) - if not ok_risk: - return False, risk_msg - else: - now = m.app_now() - if not m.trading_day_reset_allows_new_open(now): - return False, f"北京时间 {cfg['reset_hour']}:00 前不允许持仓" - from lib.trade.account_risk_lib import account_risk_blocks_trading, position_limit_reached - - ok_risk, risk_reason = account_risk_blocks_trading( - conn, - trading_day=m.get_trading_day(now), - now=now, - fmt_local_ms=getattr(m, "ms_to_app_local_str", lambda _x: ""), - ) - if not ok_risk: - return False, risk_reason - reached, active_count, mx = position_limit_reached( - conn, max_active_positions=cfg["max_active_positions"] - ) - if reached: - return False, f"已达最大持仓数({active_count}/{mx})" - from lib.trade.daily_open_limit_lib import check_daily_open_hard_limit - - ok_daily, daily_reason, _opens = check_daily_open_hard_limit( - conn, - m.get_trading_day(now), - getattr(m, "DAILY_OPEN_HARD_LIMIT", 0), - cfg["reset_hour"], - ) - if not ok_daily: - return False, daily_reason - active = m.get_active_position_count(conn) - if active >= cfg["max_active_positions"]: - return ( - False, - f"已达最大持仓数({active}/{cfg['max_active_positions']})," - "请先结束「实盘下单」中的持仓,再启动趋势回调", - ) - trend_n = conn.execute( - "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'" - ).fetchone()[0] - if int(trend_n or 0) > 0: - return False, "已存在运行中的趋势回调计划" - return True, "" - - -def _cleanup_stale_previews(conn) -> None: - ms = int(time.time() * 1000) - stale = conn.execute( - "SELECT id FROM trend_pullback_previews WHERE expires_at_ms < ?", (ms,) - ).fetchall() - for row in stale: - try: - conn.execute( - "UPDATE trend_pullback_preview_snapshots SET outcome='expired' " - "WHERE preview_id=? AND outcome='open'", - (row["id"],), - ) - except Exception: - pass - conn.execute("DELETE FROM trend_pullback_previews WHERE expires_at_ms < ?", (ms,)) - - -def parse_trend_plan(cfg: dict, form_dict) -> tuple[Optional[dict], Optional[str]]: - m = _m(cfg) - d = form_dict or {} - symbol = m.normalize_symbol_input(d.get("symbol")) - if not symbol: - return None, "symbol 不能为空" - direction = (d.get("direction") or "long").strip().lower() - if direction not in ("long", "short"): - return None, "方向错误" - try: - stop_loss = float(d.get("sl")) - add_upper = float(d.get("add_upper")) - take_profit = float(d.get("take_profit")) - risk_percent = float(d.get("risk_percent") or "5") - except Exception: - return None, "价格或风险比例格式错误" - try: - lev_raw = m.parse_positive_float(d.get("leverage")) - leverage = int(lev_raw) if lev_raw is not None else m.infer_leverage(symbol) - except Exception: - return None, "杠杆格式错误" - if leverage <= 0 or risk_percent <= 0: - return None, "杠杆与风险比例必须大于0" - bound_err = validate_trend_bounds(direction, stop_loss, add_upper) - if bound_err: - return None, bound_err - snap = m.get_available_trading_usdt() - if snap is None or snap <= 0: - return None, "无法读取合约账户 USDT 可用余额,请检查 API 与账户类型" - live_price = m.get_price(symbol) - if live_price is None: - return None, "获取实时价格失败" - exchange_symbol = m.normalize_exchange_symbol(symbol) - rf = calc_risk_fraction(direction, add_upper, stop_loss) - if rf is None or rf <= 0: - return None, "止损与补仓区间边界组合无法计算风险比例" - risk_budget = float(snap) * (risk_percent / 100.0) - notional = risk_budget / rf - margin_plan = notional / float(leverage) - margin_plan = min(margin_plan, float(snap) * cfg["margin_buffer"]) - if margin_plan <= 0: - return None, "计划保证金过小" - try: - target_amt, _ = m.prepare_order_amount(exchange_symbol, margin_plan, leverage, live_price) - except Exception as e: - return None, str(e) - ap = cfg["amount_precise"] - first_amt = ap(exchange_symbol, float(target_amt) * 0.5) - if first_amt is None or first_amt <= 0: - return None, "首仓张数过小(低于交易所最小张数),请提高风险比例或杠杆" - remainder_total = ap(exchange_symbol, max(0.0, float(target_amt) - float(first_amt))) - if remainder_total is None: - remainder_total = 0.0 - m.ensure_markets_loaded() - market = m.exchange.market(exchange_symbol) - min_amt = float((market.get("limits", {}).get("amount", {}) or {}).get("min") or 0) - n_legs, leg_json, per_ref = build_leg_amounts_json( - exchange_symbol, remainder_total, cfg["dca_legs"], ap, min_amt - ) - if n_legs <= 0: - return None, "剩余计划张数不足以拆出补仓档,请提高风险比例或放宽止损与补仓区间间距" - grid = build_grid_prices(direction, stop_loss, add_upper, n_legs) - if len(grid) != n_legs: - return None, "补仓网格生成失败" - opened_at = m.app_now_str() - try: - leg_list = json.loads(leg_json) - except Exception: - leg_list = [] - contract_size = float(market.get("contractSize") or 1) - return { - "symbol": symbol, - "exchange_symbol": exchange_symbol, - "direction": direction, - "leverage": leverage, - "stop_loss": stop_loss, - "add_upper": add_upper, - "take_profit": take_profit, - "risk_percent": risk_percent, - "snapshot_available_usdt": float(snap), - "snapshot_at": opened_at, - "live_price_ref": float(live_price), - "plan_margin_capital": float(margin_plan), - "target_order_amount": float(target_amt), - "first_order_amount": float(first_amt), - "remainder_total": float(remainder_total), - "dca_legs": int(n_legs), - "per_leg_amount": float(per_ref), - "grid_prices_json": json.dumps(grid), - "leg_amounts_json": leg_json, - "grid": grid, - "leg_amounts": leg_list, - "contract_size": contract_size, - }, None - - -def _insert_preview_snapshot(conn, preview_id: str, created: str, exp_ms: int, pl: dict) -> None: - conn.execute( - """INSERT INTO trend_pullback_preview_snapshots ( - preview_id,symbol,exchange_symbol,direction,leverage,stop_loss,add_upper,take_profit,risk_percent, - snapshot_available_usdt,snapshot_at,live_price_ref,plan_margin_capital,target_order_amount,first_order_amount,remainder_total, - dca_legs,per_leg_amount,grid_prices_json,leg_amounts_json,expires_at_ms,preview_created_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - preview_id, - pl["symbol"], - pl["exchange_symbol"], - pl["direction"], - pl["leverage"], - pl["stop_loss"], - pl["add_upper"], - pl["take_profit"], - pl["risk_percent"], - pl["snapshot_available_usdt"], - pl["snapshot_at"], - pl["live_price_ref"], - pl["plan_margin_capital"], - pl["target_order_amount"], - pl["first_order_amount"], - pl["remainder_total"], - pl["dca_legs"], - pl["per_leg_amount"], - pl["grid_prices_json"], - pl["leg_amounts_json"], - exp_ms, - created, - ), - ) - - -def _format_trend_price(cfg: dict, symbol: str, value) -> str: - if value in (None, ""): - return "—" - m = _m(cfg) - sym = symbol or "" - norm = getattr(m, "normalize_exchange_symbol", None) - if callable(norm): - try: - sym = norm(sym) or sym - except Exception: - pass - try: - m.ensure_markets_loaded() - return str(m.exchange.price_to_precision(sym, float(value))) - except Exception: - fn = getattr(m, "format_price_for_symbol", None) - if callable(fn): - return fn(symbol, value) - return str(value) - - -def _trend_add_leg_fields(cfg: dict, d: dict) -> dict: - """解析已补仓次数与已触达网格价(供策略页与中控 monitor 共用).""" - import json - - out = dict(d) - try: - legs_done = int(out.get("legs_done") or 0) - except (TypeError, ValueError): - legs_done = 0 - try: - dca_legs = int(out.get("dca_legs") or 0) - except (TypeError, ValueError): - dca_legs = 0 - try: - grid = json.loads(out.get("grid_prices_json") or "[]") - if not isinstance(grid, list): - grid = [] - except Exception: - grid = [] - add_prices: list[float] = [] - try: - from lib.strategy.strategy_trend_lib import trend_leg_display_price - - for i in range(1, legs_done + 1): - px = trend_leg_display_price(out, i) - if px is not None: - add_prices.append(float(px)) - except Exception: - pass - if not add_prices: - for x in grid[:legs_done]: - try: - add_prices.append(float(x)) - except (TypeError, ValueError): - pass - sym = out.get("exchange_symbol") or out.get("symbol") or "" - out["add_count"] = legs_done - out["add_count_total"] = dca_legs - out["add_prices"] = add_prices - out["add_prices_display"] = [_format_trend_price(cfg, sym, p) for p in add_prices] - for field in ("stop_loss", "take_profit", "add_upper", "avg_entry_price"): - if out.get(field) not in (None, ""): - out[f"{field}_display"] = _format_trend_price(cfg, sym, out.get(field)) - return out - - -def enrich_trend_plan_for_hub(cfg: dict, raw: dict) -> dict: - """中控 /api/hub/monitor:与策略页运行中计划卡片同字段(浮盈亏,标记价,盈亏比等).""" - d = enrich_trend_plan(cfg, dict(raw or {})) - d["monitor_source"] = "趋势回调计划" - m = _m(cfg) - try: - snap = float(d.get("snapshot_available_usdt") or 0) - margin = float(d.get("plan_margin_capital") or 0) - if snap > 0 and margin > 0: - d["position_ratio_pct"] = round(margin / snap * 100.0, 2) - except (TypeError, ValueError): - pass - return d - - -def _patch_hub_trend_views(app: Flask) -> None: - """将趋势回调路由注册进 HUB_CTX.views,供中控 /api/hub/trend/* 调用.""" - ctx = dict(app.config.get("HUB_CTX") or {}) - views = dict(ctx.get("views") or {}) - for name in ( - "preview_trend_pullback", - "execute_trend_pullback", - "stop_trend_pullback", - "trend_pullback_breakeven", - ): - vf = app.view_functions.get(name) - if vf is not None: - views[name] = vf - ctx["views"] = views - app.config["HUB_CTX"] = ctx - - -def patch_trend_hub_enrich(app: Flask, cfg: dict) -> None: - """hub_bridge install 之后调用:三所 /api/hub/monitor 趋势字段与策略页一致.""" - _patch_hub_monitor_enrich(app, cfg) - - -def _patch_hub_monitor_enrich(app: Flask, cfg: dict) -> None: - ctx = dict(app.config.get("HUB_CTX") or {}) - prev = ctx.get("enrich_monitor") - - def enrich_monitor(keys=None, orders=None, trends=None, rolls=None): - payload: dict[str, Any] = {} - if callable(prev): - try: - prev_out = prev(keys=keys, orders=orders, trends=trends, rolls=rolls) - if isinstance(prev_out, dict): - payload.update(prev_out) - except Exception: - pass - if trends: - payload["trends"] = [ - enrich_trend_plan_for_hub(cfg, t) for t in trends if isinstance(t, dict) - ] - return payload - - ctx["enrich_monitor"] = enrich_monitor - app.config["HUB_CTX"] = ctx - - -def enrich_trend_plan(cfg: dict, row) -> dict: - m = _m(cfg) - d = _row(cfg, row) - try: - d["breakeven_applied"] = int(d.get("breakeven_applied") or 0) != 0 - except Exception: - d["breakeven_applied"] = False - ex_sym = d.get("exchange_symbol") or m.normalize_exchange_symbol(d.get("symbol") or "") - direction = (d.get("direction") or "long").lower() - metrics_fn = getattr(m, "get_live_position_exchange_metrics", None) - met = None - if callable(metrics_fn): - try: - lev = int(d.get("leverage") or 0) or None - except (TypeError, ValueError): - lev = None - try: - met = metrics_fn(ex_sym, direction, order_leverage=lev) - except TypeError: - met = metrics_fn(ex_sym, direction) - if met and met.get("entry_price") is not None: - try: - live_entry = float(met["entry_price"]) - if live_entry > 0: - d["avg_entry_price"] = live_entry - except (TypeError, ValueError): - pass - if met and met.get("unrealized_pnl") is not None: - d["floating_pnl"] = float(met["unrealized_pnl"]) - elif ( - met - and met.get("mark_price") is not None - and d.get("avg_entry_price") is not None - ): - try: - from lib.hub.hub_position_metrics import estimate_linear_swap_upnl_usdt - - entry = float(d["avg_entry_price"]) - mark = float(met["mark_price"]) - qty = None - cs = 1.0 - get_qty = getattr(m, "get_live_position_contracts", None) - get_cs = getattr(m, "get_contract_size", None) - if callable(get_qty): - qty = get_qty(ex_sym, direction) - if callable(get_cs): - cs = float(get_cs(ex_sym)) - upnl = estimate_linear_swap_upnl_usdt( - direction, entry, mark, qty, cs - ) - d["floating_pnl"] = float(upnl) if upnl is not None else None - except (TypeError, ValueError): - d["floating_pnl"] = None - else: - d["floating_pnl"] = None - if met and met.get("mark_price") is not None: - d["floating_mark"] = float(met["mark_price"]) - else: - d["floating_mark"] = None - else: - d["floating_pnl"] = d["floating_mark"] = None - get_cs = getattr(m, "get_contract_size", None) - if callable(get_cs): - try: - d["contract_size"] = float(get_cs(ex_sym)) - except (TypeError, ValueError): - pass - d = _trend_add_leg_fields(cfg, d) - from lib.strategy.strategy_snapshot_lib import attach_trend_dca_levels - from lib.strategy.strategy_trend_lib import calc_trend_plan_money_metrics - - d = attach_trend_dca_levels(d) - money = calc_trend_plan_money_metrics(d) - if money.get("money_rr") is not None: - d["money_rr"] = money["money_rr"] - d["planned_rr"] = money["money_rr"] - if money.get("risk_amount_u") is not None: - d["risk_amount_u"] = money["risk_amount_u"] - try: - d["breakeven_default_offset_pct"] = float(cfg.get("breakeven_offset_pct", 0.3)) - except (TypeError, ValueError): - d["breakeven_default_offset_pct"] = 0.3 - return d - - -def _weighted_avg(old_avg, old_amt, fill_px, add_amt): - try: - oa, aa = float(old_amt), float(add_amt) - if oa <= 0: - return float(fill_px) - return (float(old_avg) * oa + float(fill_px) * aa) / (oa + aa) - except Exception: - return float(fill_px or 0) - - -def _plan_stop_status(result_label: str) -> str: - if result_label == "止盈": - return "stopped_tp" - if result_label == "止损": - return "stopped_sl" - return "stopped_manual" - - -def _call_insert_trade_record(m, plan_id: int, kwargs: dict) -> None: - """按各所 insert_trade_record 签名过滤参数,避免未知字段导致记账失败.""" - fn = getattr(m, "insert_trade_record", None) - if not callable(fn): - raise RuntimeError("app_module 缺少 insert_trade_record") - allowed = set(inspect.signature(fn).parameters.keys()) - call = {k: v for k, v in kwargs.items() if k in allowed} - if "trend_plan_id" in allowed: - call["trend_plan_id"] = int(plan_id) - fn(**call) - - -def _best_trend_close_snapshot(conn, plan_id: int) -> dict | None: - from lib.strategy.strategy_snapshot_lib import ( - FINAL_TREND_CLOSE_LABELS, - STRATEGY_TREND, - _final_trend_close_rank, - ) - - rows = conn.execute( - f"""SELECT * FROM strategy_trade_snapshots - WHERE strategy_type=? AND source_id=? - AND result_label IN ({",".join("?" * len(FINAL_TREND_CLOSE_LABELS))})""", - (STRATEGY_TREND, int(plan_id), *FINAL_TREND_CLOSE_LABELS), - ).fetchall() - if not rows: - return None - parsed = [_row_dict(row) for row in rows] - return max( - parsed, - key=lambda d: ( - _final_trend_close_rank(str(d.get("result_label") or "")), - int(d.get("id") or 0), - ), - ) - - -def _ensure_trend_plan_trade_record( - cfg: dict, conn, plan_id: int, *, prefer_label: str = "手动平仓" -) -> bool: - """计划已结束但 trade_records 缺失时,从策略快照补录一条.""" - if _trend_plan_trade_exists(conn, plan_id): - return True - m = _m(cfg) - plan = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE id=?", (int(plan_id),) - ).fetchone() - if not plan: - return False - plan_d = _row_dict(plan) - snap = _best_trend_close_snapshot(conn, plan_id) - if not snap: - return False - try: - payload = json.loads(snap.get("snapshot_json") or "{}") - except Exception: - payload = {} - sym = snap.get("symbol") or plan_d.get("symbol") or payload.get("symbol") - direction = snap.get("direction") or plan_d.get("direction") or "long" - result = (prefer_label or "").strip() or (snap.get("result_label") or "").strip() or "手动平仓" - opened_at = snap.get("opened_at") or plan_d.get("opened_at") - closed_at = snap.get("closed_at") - pnl_amount = snap.get("pnl_amount") - if pnl_amount is None: - pnl_amount = payload.get("pnl_amount") - avg_e = float(payload.get("avg_entry_price") or plan_d.get("avg_entry_price") or 0) - margin_cap = trend_effective_margin_capital(plan_d) - lev = int(plan_d.get("leverage") or 1) - hold_seconds = m.calc_hold_seconds( - opened_at or "", - m.parse_dt_for_trading_day(closed_at) or m.app_now(), - ) - res = m.normalize_result_with_pnl(result, float(pnl_amount or 0)) - risk_amt = m.calc_risk_amount_from_plan( - direction, - float(plan_d.get("add_upper") or 0), - float(plan_d.get("stop_loss") or 0), - float(plan_d.get("plan_margin_capital") or 0), - lev, - ) - planned_rr = m.calc_rr_ratio( - direction, - avg_e, - float(plan_d.get("stop_loss") or 0), - float(plan_d.get("take_profit") or 0), - ) - session_date = plan_d.get("session_date") or m.get_trading_day() - _bump_session_capital_no_commit(m, conn, session_date, float(pnl_amount or 0)) - _call_insert_trade_record( - m, - plan_id, - dict( - conn=conn, - symbol=sym, - monitor_type=MONITOR_TYPE_TREND, - direction=direction, - trigger_price=avg_e, - stop_loss=float(plan_d.get("stop_loss") or 0), - initial_stop_loss=float(plan_d.get("initial_stop_loss") or plan_d.get("stop_loss") or 0), - take_profit=float(plan_d.get("take_profit") or 0), - margin_capital=margin_cap, - leverage=lev, - pnl_amount=pnl_amount, - hold_seconds=hold_seconds, - trade_style="trend_pullback", - risk_amount=risk_amt, - planned_rr=planned_rr, - actual_rr=m.calc_actual_rr(pnl_amount, risk_amt), - result=res, - opened_at=opened_at, - closed_at=closed_at, - entry_reason=ENTRY_REASON_TREND_PULLBACK, - ), - ) - conn.commit() - return True - - -def sync_trend_plans_after_external_close( - cfg: dict, conn, symbol: str, direction: str -) -> dict[str, Any]: - """中控/外部全平后:结束仍 active 的同币种同向趋势计划(避免监控再记一条止损).""" - m = _m(cfg) - sym = m.normalize_symbol_input(symbol) if hasattr(m, "normalize_symbol_input") else (symbol or "").strip() - if not sym: - return {"ok": False, "msg": "symbol 无效", "finalized": 0} - direction = (direction or "long").strip().lower() - rows = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE status='active' AND symbol=? AND direction=?", - (sym, direction), - ).fetchall() - finalized = 0 - for row in rows: - px = m.get_price(row["symbol"]) - exit_p = float(px) if px is not None else 0.0 - before = _trend_plan_trade_exists(conn, int(row["id"])) - _finalize_plan(cfg, conn, row, "手动平仓", exit_p) - if not before: - finalized += 1 - return {"ok": True, "finalized": finalized, "symbol": sym, "direction": direction} - - -def _trend_plan_trade_exists(conn, plan_id: int) -> bool: - try: - return conn.execute( - "SELECT id FROM trade_records WHERE trend_plan_id=? LIMIT 1", - (int(plan_id),), - ).fetchone() is not None - except Exception: - return False - - -def _bump_session_capital_no_commit( - m, conn, session_date: str, pnl_amount: float -) -> float | None: - """更新当日资金,不单独 commit(与 _finalize_plan 同一事务).""" - try: - row = conn.execute( - "SELECT current_capital FROM trading_sessions WHERE session_date = ?", - (session_date,), - ).fetchone() - if not row: - start_cap = float(getattr(m, "DAILY_START_CAPITAL", 0) or 0) - if start_cap <= 0: - ensure = getattr(m, "ensure_session", None) - if callable(ensure): - ensured = ensure(conn, session_date) - row = ensured - else: - return None - else: - conn.execute( - "INSERT OR IGNORE INTO trading_sessions " - "(session_date, start_capital, current_capital) VALUES (?,?,?)", - (session_date, start_cap, start_cap), - ) - row = conn.execute( - "SELECT current_capital FROM trading_sessions WHERE session_date = ?", - (session_date,), - ).fetchone() - if not row: - return None - new_capital = float(row["current_capital"]) + float(pnl_amount) - conn.execute( - "UPDATE trading_sessions SET current_capital = ?, updated_at = CURRENT_TIMESTAMP " - "WHERE session_date = ?", - (round(new_capital, 4), session_date), - ) - return round(new_capital, 4) - except Exception: - return None - - -def _apply_trend_user_risk_close(cfg: dict, conn, *, trade_record_id=None, closed_at_ms=None) -> None: - m = _m(cfg) - fn = getattr(m, "hub_user_initiated_close", None) - from lib.trade.account_risk_lib import CLOSE_SOURCE_USER_TREND_STOP - - if callable(fn): - fn( - conn, - source=CLOSE_SOURCE_USER_TREND_STOP, - count=1, - trade_record_id=trade_record_id, - closed_at_ms=closed_at_ms, - ) - return - from lib.trade.account_risk_lib import on_user_initiated_close - - on_user_initiated_close( - conn, - source=CLOSE_SOURCE_USER_TREND_STOP, - trade_record_id=trade_record_id, - closed_at_ms=closed_at_ms, - trading_day=m.get_trading_day(), - now=m.app_now(), - count=1, - ) - - -def _finalize_plan(cfg: dict, conn, row, result_label: str, exit_price: float, *, user_initiated_risk: bool = False) -> None: - m = _m(cfg) - plan_id = int(row["id"]) - active = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", - (plan_id,), - ).fetchone() - if not active: - return - row = active - sym = row["symbol"] - direction = row["direction"] or "long" - ex_sym = row["exchange_symbol"] or m.normalize_exchange_symbol(sym) - closed_at = m.app_now_str() - opened_at = row["opened_at"] or closed_at - hold_seconds = m.calc_hold_seconds(opened_at, m.parse_dt_for_trading_day(closed_at) or m.app_now()) - plan_margin = float(row["plan_margin_capital"] or 0) - margin_cap = trend_effective_margin_capital(_row(cfg, row)) - lev = int(row["leverage"] or 1) - avg_e = float(row["avg_entry_price"] or 0) - pnl_amount = m.calc_pnl(direction, avg_e, float(exit_price), margin_cap, lev) - res = m.normalize_result_with_pnl(result_label, pnl_amount) - risk_amt = m.calc_risk_amount_from_plan( - direction, float(row["add_upper"]), float(row["stop_loss"]), plan_margin, lev - ) - try: - target = float(row["target_order_amount"] or 0) - open_amt = float(row["order_amount_open"] or 0) - if risk_amt is not None and target > 0 and open_amt > 0: - risk_amt = round(float(risk_amt) * min(1.0, open_amt / target), 6) - except (TypeError, ValueError): - pass - planned_rr = m.calc_rr_ratio(direction, avg_e, float(row["stop_loss"]), float(row["take_profit"])) - st = _plan_stop_status(result_label) - cur = conn.execute( - "UPDATE trend_pullback_plans SET status=?, message=? WHERE id=? AND status='active'", - (st, res, plan_id), - ) - if not getattr(cur, "rowcount", 0): - return - try: - from lib.strategy.strategy_snapshot_lib import save_trend_plan_snapshot - - save_trend_plan_snapshot( - cfg, - conn, - row, - result_label=result_label, - exit_price=float(exit_price) if exit_price is not None else None, - pnl_amount=float(pnl_amount) if pnl_amount is not None else None, - closed_at=closed_at, - ) - except Exception: - pass - try: - cancel_symbol_orders(cfg, ex_sym) - except Exception: - pass - session_capital = None - trade_record_id = None - if not _trend_plan_trade_exists(conn, plan_id): - session_date = row["session_date"] or m.get_trading_day() - session_capital = _bump_session_capital_no_commit( - m, conn, session_date, pnl_amount - ) - _call_insert_trade_record( - m, - plan_id, - dict( - conn=conn, - symbol=sym, - monitor_type=MONITOR_TYPE_TREND, - direction=direction, - trigger_price=avg_e, - stop_loss=float(row["stop_loss"]), - initial_stop_loss=float(row.get("initial_stop_loss") or row["stop_loss"]), - take_profit=float(row["take_profit"]), - margin_capital=margin_cap, - leverage=lev, - pnl_amount=pnl_amount, - hold_seconds=hold_seconds, - trade_style="trend_pullback", - risk_amount=risk_amt, - planned_rr=planned_rr, - actual_rr=m.calc_actual_rr(pnl_amount, risk_amt), - result=res, - opened_at=opened_at, - closed_at=closed_at, - entry_reason=ENTRY_REASON_TREND_PULLBACK, - ), - ) - try: - from lib.trade.account_risk_lib import insert_trade_record_id - - trade_record_id = insert_trade_record_id(conn) - except Exception: - trade_record_id = None - if user_initiated_risk: - closed_ms = None - to_ms = getattr(m, "_to_ms_with_fallback", None) - if callable(to_ms): - try: - closed_ms = to_ms(None, closed_at) - except Exception: - closed_ms = None - _apply_trend_user_risk_close( - cfg, - conn, - trade_record_id=trade_record_id, - closed_at_ms=closed_ms, - ) - conn.commit() - try: - from lib.strategy.strategy_wechat_notify import notify_trend_plan_ended - - notify_trend_plan_ended( - cfg, - plan_id=plan_id, - symbol=sym, - direction=direction, - end_type=result_label, - result_label=res, - exit_price=float(exit_price) if exit_price is not None else None, - pnl_amount=float(pnl_amount) if pnl_amount is not None else None, - ) - except Exception: - pass - extra = getattr(m, "build_wechat_close_message", None) - send = getattr(m, "send_wechat_msg", None) - if callable(extra) and callable(send): - send( - extra( - symbol=sym, - direction=direction, - result=f"{res}({MONITOR_TYPE_TREND})", - pnl_amount=pnl_amount, - hold_seconds=hold_seconds, - trigger_price=avg_e, - current_price=float(exit_price), - stop_loss=float(row["stop_loss"]), - take_profit=float(row["take_profit"]), - close_order_id="-", - extra_note="计划本金口径:启动时合约可用余额快照;止盈由程序监控", - session_capital_fallback=session_capital, - ) - ) - - -def _trend_plan_open_age_sec(row, m) -> float: - opened_ms = None - try: - if "opened_at_ms" in row.keys() and row["opened_at_ms"]: - opened_ms = int(row["opened_at_ms"]) - except Exception: - opened_ms = None - to_ms = getattr(m, "_to_ms_with_fallback", None) - if callable(to_ms): - opened_ms = to_ms(opened_ms, row["opened_at"] if "opened_at" in row.keys() else None) - if opened_ms is None and "opened_at" in row.keys(): - opened_ms = to_ms(None, row["opened_at"]) - if not opened_ms: - return 0.0 - return max(0.0, (time.time() * 1000 - opened_ms) / 1000.0) - - -def _trend_hit_take_profit(direction: str, mark_price: float, take_profit: float, avg_entry: float) -> bool: - try: - pf = float(mark_price) - tp = float(take_profit) - entry = float(avg_entry) - except (TypeError, ValueError): - return False - if entry <= 0 or tp <= 0: - return False - direction = (direction or "long").lower() - if direction == "long": - return tp > entry and pf >= tp - return tp < entry and pf <= tp - - -def _trend_poll_price(m, sym: str, ex_sym: str, direction: str) -> Optional[float]: - """补仓/止盈判定用标记价(与页面「标记价」一致),无标记价时回退 last.""" - fn = getattr(m, "get_symbol_mark_price", None) - if callable(fn): - try: - px = fn(sym) - if px is not None and float(px) > 0: - return float(px) - except Exception: - pass - metrics_fn = getattr(m, "get_live_position_exchange_metrics", None) - if callable(metrics_fn): - try: - met = metrics_fn(ex_sym, direction) - if met and met.get("mark_price") is not None: - px = float(met["mark_price"]) - if px > 0: - return px - except Exception: - pass - px = m.get_price(sym) - try: - return float(px) if px is not None else None - except (TypeError, ValueError): - return None - - -def _should_finalize_trend_flat(row, pos, plan_id: int, m) -> bool: - """首仓后交易所报无仓:需过开仓宽限期 + 连续空仓轮询,避免误判止损.""" - if pos is None: - return False - if float(pos) > 0: - _TREND_FLAT_STREAK.pop(plan_id, None) - return False - if not int(row["first_order_done"] or 0): - return False - age = _trend_plan_open_age_sec(row, m) - if age < TREND_OPEN_GRACE_SEC: - _TREND_FLAT_STREAK.pop(plan_id, None) - return False - try: - local_open = float(row["order_amount_open"] or 0) - except (TypeError, ValueError): - local_open = 0.0 - required = TREND_FLAT_CONFIRM_POLLS - if local_open > 0 and age < TREND_OPEN_GRACE_SEC * 2: - required = max(required, TREND_FLAT_CONFIRM_POLLS * 2) - streak = int(_TREND_FLAT_STREAK.get(plan_id, 0)) + 1 - _TREND_FLAT_STREAK[plan_id] = streak - if streak >= required: - print( - f"[trend_pullback] flat finalize plan={plan_id} sym={row['symbol']} " - f"age={age:.0f}s streak={streak} local_open={local_open}", - flush=True, - ) - return True - return False - - -def check_trend_pullback_plans(cfg: dict) -> None: - m = _m(cfg) - ok_live, live_reason = m.ensure_exchange_live_ready() - _TREND_POLL_STATE["updated_at"] = time.time() - _TREND_POLL_STATE["live_ok"] = ok_live - _TREND_POLL_STATE["live_reason"] = live_reason or "" - if not ok_live: - _log_trend_live_skip(live_reason or "unknown") - conn = cfg["get_db"]() - try: - for row in conn.execute( - "SELECT * FROM trend_pullback_plans WHERE status='active'" - ).fetchall(): - probe = summarize_trend_dca_probe(cfg, row) - if probe.get("trigger_reached"): - _set_trend_poll_plan(int(row["id"]), probe) - except Exception as e: - print(f"[trend_pullback] live-skip probe error: {e}", flush=True) - finally: - conn.close() - return - conn = cfg["get_db"]() - rows = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE status='active'" - ).fetchall() - for row in rows: - try: - plan_id = int(row["id"]) - sym = row["symbol"] - direction = (row["direction"] or "long").lower() - ex_sym = row["exchange_symbol"] or m.normalize_exchange_symbol(sym) - sl = float(row["stop_loss"]) - tp = float(row["take_profit"]) - lev = int(row["leverage"] or 1) - try: - local_open = float(row["order_amount_open"] or 0) - except (TypeError, ValueError): - local_open = 0.0 - pf = _trend_poll_price(m, sym, ex_sym, direction) - if pf is None: - continue - last_p = row["last_mark_price"] - last_pf = float(last_p) if last_p is not None else pf - pos = m.get_live_position_contracts(ex_sym, direction) - if pos is None: - if local_open > 0 and int(row["first_order_done"] or 0): - pos = local_open - else: - continue - elif float(pos) <= 0 and local_open > 0: - age = _trend_plan_open_age_sec(row, m) - if age < TREND_OPEN_GRACE_SEC * 2: - print( - f"[trend_pullback] pos fallback plan={plan_id} sym={sym} " - f"ex_pos=0 local_open={local_open} age={age:.0f}s", - flush=True, - ) - pos = local_open - legs_done = int(row["legs_done"] or 0) - try: - leg_amounts = [float(x) for x in json.loads(row["leg_amounts_json"] or "[]")] - except Exception: - leg_amounts = [] - try: - grid = json.loads(row["grid_prices_json"] or "[]") - except Exception: - grid = [] - avg_e = float(row["avg_entry_price"] or pf or 0) - hit_tp = _trend_hit_take_profit(direction, pf, tp, avg_e) - if hit_tp and pos > 0: - try: - close_resp = trend_market_close(cfg, ex_sym, direction, float(pos), lev) - exit_p = m.extract_trade_price_from_order(close_resp) or pf - except Exception as e: - if not m.is_no_position_error(str(e)): - continue - exit_p = pf - _finalize_plan(cfg, conn, row, "止盈", exit_p) - _TREND_FLAT_STREAK.pop(plan_id, None) - continue - if _should_finalize_trend_flat(row, pos, plan_id, m): - _finalize_plan(cfg, conn, row, "止损", pf) - _TREND_FLAT_STREAK.pop(plan_id, None) - continue - if int(row["first_order_done"] or 0) and legs_done < len(grid) and legs_done < len(leg_amounts): - while legs_done < len(grid) and legs_done < len(leg_amounts): - level = float(grid[legs_done]) - if not trend_dca_level_reached(direction, pf, level): - break - amt = float(m.exchange.amount_to_precision(ex_sym, leg_amounts[legs_done])) - if amt <= 0: - print( - f"[trend_pullback] dca skip plan={plan_id} leg={legs_done + 1} " - f"amt_precision=0 raw={leg_amounts[legs_done]}", - flush=True, - ) - break - try: - add_resp = trend_market_add(cfg, ex_sym, direction, amt, lev) - except Exception as e: - print( - f"[trend_pullback] dca order failed plan={plan_id} sym={sym} " - f"leg={legs_done + 1} level={level} mark={pf} err={e}", - flush=True, - ) - break - fill_px = m.extract_trade_price_from_order(add_resp) or pf - old_avg = float(row["avg_entry_price"] or fill_px) - old_open = float(row["order_amount_open"] or 0) - new_avg = _weighted_avg(old_avg, old_open, fill_px, amt) - legs_done += 1 - from lib.strategy.strategy_trend_lib import append_leg_fill_price_json - - fills_json = append_leg_fill_price_json( - row["leg_fill_prices_json"] if "leg_fill_prices_json" in row.keys() else None, - fill_px, - ) - conn.execute( - "UPDATE trend_pullback_plans SET legs_done=?, avg_entry_price=?, " - "order_amount_open=?, last_mark_price=?, leg_fill_prices_json=? WHERE id=?", - (legs_done, new_avg, old_open + amt, pf, fills_json, row["id"]), - ) - row = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE id=?", (row["id"],) - ).fetchone() - print( - f"[trend_pullback] dca filled plan={plan_id} leg={legs_done} " - f"fill={fill_px} avg={new_avg} open={old_open + amt}", - flush=True, - ) - try: - trend_refresh_stop_only(cfg, ex_sym, direction, sl) - except Exception: - pass - conn.execute( - "UPDATE trend_pullback_plans SET last_mark_price=? WHERE id=?", - (pf, row["id"]), - ) - probe = summarize_trend_dca_probe(cfg, row) - probe["last_poll_mark"] = pf - _set_trend_poll_plan(plan_id, probe) - if probe.get("trigger_reached") and probe.get("block_reason"): - print( - f"[trend_pullback] dca blocked plan={plan_id} sym={sym} " - f"mark={pf} next={probe.get('next_trigger')} reason={probe.get('block_reason')}", - flush=True, - ) - except Exception as e: - print( - f"[trend_pullback] poll error plan={row['id'] if row else '?'}: {e}", - flush=True, - ) - continue - conn.commit() - conn.close() - - -TREND_PLAN_STATUS_HANDOFF = "stopped_handoff" - - -def _order_monitor_manual_type(m) -> str: - return getattr(m, "ORDER_MONITOR_TYPE_MANUAL", None) or "下单监控" - - -def _insert_trend_handoff_order_monitor( - cfg: dict, - conn, - plan_row, - *, - new_sl: float, - pos_amt: float, -) -> int: - m = _m(cfg) - sym = plan_row["symbol"] - direction = (plan_row["direction"] or "long").lower() - ex_sym = plan_row["exchange_symbol"] or m.normalize_exchange_symbol(sym) - plan_id = int(plan_row["id"]) - avg_e = float(plan_row["avg_entry_price"] or 0) - tp = float(plan_row["take_profit"] or 0) - lev = int(plan_row["leverage"] or 1) - margin_cap = float(plan_row["plan_margin_capital"] or 0) - init_sl = float( - plan_row["initial_stop_loss"] - if plan_row["initial_stop_loss"] not in (None, "") - else plan_row["stop_loss"] - or 0 - ) - risk_pct = float(plan_row["risk_percent"] or 5) - risk_amt = None - calc_risk = getattr(m, "calc_risk_amount_from_plan", None) - if callable(calc_risk): - try: - risk_amt = calc_risk(direction, avg_e, init_sl, margin_cap, lev) - except Exception: - risk_amt = None - be_rr = float(getattr(m, "BREAKEVEN_RR_TRIGGER", 1) or 1) - be_off = float(getattr(m, "BREAKEVEN_OFFSET_PCT", 0.3) or 0.3) - be_step = float(getattr(m, "BREAKEVEN_STEP_R", 1) or 1) - if direction == "short": - be_price = round(avg_e * (1 - be_off / 100.0), 8) - else: - be_price = round(avg_e * (1 + be_off / 100.0), 8) - rp = getattr(m, "round_price_to_exchange", None) - if callable(rp): - try: - be_price = float(rp(ex_sym, be_price) or be_price) - except Exception: - pass - opened_at = plan_row["opened_at"] or m.app_now_str() - to_ms = getattr(m, "_to_ms_with_fallback", None) - opened_ms = to_ms(plan_row["opened_at_ms"] if "opened_at_ms" in plan_row.keys() else None, opened_at) if callable(to_ms) else None - trading_day = plan_row["session_date"] or getattr(m, "get_trading_day", lambda: None)() - if not trading_day and callable(getattr(m, "get_trading_day", None)): - trading_day = m.get_trading_day() - notional = margin_cap * lev if margin_cap and lev else None - monitor_type = MONITOR_TYPE_TREND_PULLBACK - conn.execute( - "INSERT INTO order_monitors " - "(symbol, exchange_symbol, direction, trigger_price, stop_loss, initial_stop_loss, take_profit, " - "margin_capital, leverage, trade_style, risk_percent, risk_amount, " - "breakeven_rr_trigger, breakeven_offset_pct, breakeven_step_r, breakeven_armed, breakeven_price, " - "breakeven_enabled, notional_value, position_ratio, base_amount, order_amount, exchange_order_id, " - "opened_at, opened_at_ms, session_date, monitor_type, key_signal_type, trend_plan_id) " - "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", - ( - sym, - ex_sym, - direction, - avg_e, - new_sl, - init_sl, - tp, - margin_cap, - lev, - "trend_pullback_handoff", - risk_pct, - risk_amt, - be_rr, - be_off, - be_step, - 0, - be_price, - 0, - notional, - None, - None, - float(pos_amt), - "", - opened_at, - opened_ms, - trading_day, - monitor_type, - TREND_HANDOFF_KEY_SIGNAL, - plan_id, - ), - ) - new_id = int(conn.execute("SELECT last_insert_rowid()").fetchone()[0]) - persist = getattr(m, "try_persist_exchange_margin_for_order", None) - if callable(persist): - try: - persist(conn, new_id, ex_sym, direction, order_leverage=lev) - except Exception: - pass - return new_id - - -def apply_manual_breakeven(cfg: dict, conn, row, offset_pct=None) -> tuple[bool, Optional[str]]: - """保本:结束趋势计划,持仓移交下单监控(备注趋势回调),交易所同时挂保本止损与止盈.""" - m = _m(cfg) - if (row["status"] or "").strip() != "active": - return False, "计划已结束" - if not int(row["first_order_done"] or 0): - return False, "尚未完成首仓,无法保本" - avg_e = float(row["avg_entry_price"] or 0) - if avg_e <= 0: - return False, "缺少有效持仓均价" - direction = (row["direction"] or "long").lower() - sym = row["symbol"] - ex_sym = row["exchange_symbol"] or m.normalize_exchange_symbol(sym) - pos = m.get_live_position_contracts(ex_sym, direction) - if pos is None or float(pos) <= 0: - return False, "交易所当前无该方向持仓" - pos_amt = float(pos) - dup = conn.execute( - "SELECT id FROM order_monitors WHERE status='active' AND symbol=? AND direction=? LIMIT 1", - (sym, direction), - ).fetchone() - if dup: - return False, "该币种已有运行中的下单监控,请先结束后再保本移交" - be_fn = getattr(m, "calc_trend_manual_breakeven_stop", None) - if not callable(be_fn): - pct = float(offset_pct if offset_pct is not None else cfg["breakeven_offset_pct"]) - if direction == "short": - new_sl_raw = avg_e * (1.0 - pct / 100.0) - else: - new_sl_raw = avg_e * (1.0 + pct / 100.0) - else: - new_sl_raw = be_fn(direction, avg_e, offset_pct) - if new_sl_raw is None: - return False, "保本价计算失败" - new_sl = m.round_price_to_exchange(ex_sym, new_sl_raw) - if new_sl is None: - return False, "保本价经交易所精度舍入后无效" - new_sl = float(new_sl) - tp = float(row["take_profit"] or 0) - if tp <= 0: - return False, "计划止盈价无效" - cur_sl = float(row["stop_loss"] or 0) - if direction == "long": - if new_sl <= cur_sl: - return False, f"新止损 {new_sl} 未高于当前止损 {cur_sl}(多仓需上移)" - else: - if new_sl >= cur_sl: - return False, f"新止损 {new_sl} 未低于当前止损 {cur_sl}(空仓需下移)" - ok_live, live_reason = m.ensure_exchange_live_ready() - if not ok_live: - return False, live_reason or "实盘未就绪" - plan_id = int(row["id"]) - try: - from lib.strategy.strategy_snapshot_lib import save_trend_plan_snapshot - - save_trend_plan_snapshot( - cfg, conn, row, result_label="保本移交", exit_price=None, pnl_amount=None - ) - except Exception: - pass - handoff_row = { - "symbol": sym, - "exchange_symbol": ex_sym, - "direction": direction, - "order_amount": pos_amt, - } - try: - trend_replace_tpsl(cfg, handoff_row, new_sl, tp) - except Exception as e: - fe = getattr(m, "friendly_exchange_error", None) - return False, fe(e) if callable(fe) else str(e) - now_s = m.app_now_str() - _TREND_FLAT_STREAK.pop(plan_id, None) - cur = conn.execute( - "UPDATE trend_pullback_plans SET status=?, message=?, stop_loss=?, " - "breakeven_applied=1, breakeven_applied_at=? WHERE id=? AND status='active'", - ( - TREND_PLAN_STATUS_HANDOFF, - f"保本移交下单监控({TREND_HANDOFF_TRADE_NOTE})", - new_sl, - now_s, - plan_id, - ), - ) - if not getattr(cur, "rowcount", 0): - return False, "计划状态更新失败(可能已被其他操作结束)" - try: - mon_id = _insert_trend_handoff_order_monitor( - cfg, conn, row, new_sl=new_sl, pos_amt=pos_amt - ) - except Exception as e: - conn.execute( - "UPDATE trend_pullback_plans SET status='active', message=? WHERE id=?", - (f"移交下单监控失败:{e}", plan_id), - ) - return False, f"移交下单监控失败:{e}" - pct_used = float( - offset_pct if offset_pct is not None else cfg["breakeven_offset_pct"] - ) - extra = getattr(m, "build_wechat_close_message", None) - send = getattr(m, "send_wechat_msg", None) - pf = getattr(m, "format_price_for_symbol", None) - fmt = (lambda s, p: pf(s, p)) if callable(pf) else (lambda _s, p: str(p)) - try: - from lib.strategy.strategy_wechat_notify import notify_trend_plan_ended - - notify_trend_plan_ended( - cfg, - plan_id=plan_id, - symbol=sym, - direction=direction, - end_type="保本移交", - result_label=TREND_HANDOFF_TRADE_NOTE, - extra=f"已移交下单监控 #{mon_id};止损 {fmt(sym, new_sl)} | 止盈 {fmt(sym, tp)}", - ) - except Exception: - pass - if callable(send): - lines = [ - f"# ✅ {sym} 趋势回调保本移交", - f"- 计划 ID:**{plan_id}** → 下单监控 **#{mon_id}**", - f"- 备注:**{TREND_HANDOFF_TRADE_NOTE}**", - f"- 保本止损:{fmt(sym, new_sl)} | 止盈:{fmt(sym, tp)}", - f"- 交易所:已挂止盈止损;平仓后将写入交易记录({ENTRY_REASON_TREND_PULLBACK})", - ] - wl = getattr(m, "_wechat_account_label", None) - if callable(wl): - lines.insert(1, f"**账户:{wl()}**") - send("\n".join(lines)) - return True, None - - -def load_trend_page_context(conn, request_obj, cfg: dict) -> dict[str, Any]: - m = _m(cfg) - _cleanup_stale_previews(conn) - trend_active = int( - conn.execute( - "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'" - ).fetchone()[0] - or 0 - ) - trend_plans = [] - trend_dca_probes = [] - raw_plans = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC" - ).fetchall() - for r in raw_plans: - try: - enriched = enrich_trend_plan(cfg, r) - trend_plans.append(enriched) - except Exception: - enriched = _row(cfg, r) - trend_plans.append(enriched) - try: - probe = summarize_trend_dca_probe(cfg, r) - trend_dca_probes.append(probe) - if isinstance(enriched, dict): - enriched["dca_probe"] = probe - except Exception: - pass - now = m.app_now() - active_count = m.get_active_position_count(conn) - from lib.trade.daily_open_limit_lib import can_trade_new_open, count_opens_for_trading_day - - trading_day = m.get_trading_day(now) - opens_today = count_opens_for_trading_day(conn, trading_day) - hard_limit = int(getattr(m, "DAILY_OPEN_HARD_LIMIT", 0) or 0) - can_trade_trend = can_trade_new_open( - time_allows=m.trading_day_reset_allows_new_open(now), - active_count=active_count, - max_active_positions=cfg["max_active_positions"], - opens_today=opens_today, - hard_limit=hard_limit, - extra_blocks=trend_active != 0, - ) - trend_preview = None - trend_preview_levels = [] - preview_expires_ms = None - trend_preview_expired = False - pid_arg = (request_obj.args.get("preview_id") or "").strip() - if pid_arg: - pr = conn.execute( - "SELECT * FROM trend_pullback_previews WHERE id=?", (pid_arg,) - ).fetchone() - now_ms = int(time.time() * 1000) - if pr and int(pr["expires_at_ms"] or 0) >= now_ms: - from lib.strategy.strategy_trend_lib import build_trend_preview_level_rows - - trend_preview = _row(cfg, pr) - preview_expires_ms = int(pr["expires_at_ms"]) - get_cs = getattr(m, "get_contract_size", None) - if callable(get_cs) and not trend_preview.get("contract_size"): - try: - trend_preview["contract_size"] = float( - get_cs(trend_preview.get("exchange_symbol") or trend_preview.get("symbol") or "") - ) - except (TypeError, ValueError): - pass - trend_preview, trend_preview_levels = build_trend_preview_level_rows(trend_preview) - elif pr: - trend_preview_expired = True - return { - "trend_plans": trend_plans, - "trend_dca_probes": trend_dca_probes, - "trend_active": trend_active, - "can_trade_trend": can_trade_trend, - "trend_preview": trend_preview, - "trend_preview_levels": trend_preview_levels, - "preview_expires_ms": preview_expires_ms, - "trend_preview_expired": trend_preview_expired, - "trend_pullback_dca_legs": cfg["dca_legs"], - "trend_pullback_preview_ttl": cfg["preview_ttl"], - "trend_preview_max_drift_pct": cfg["drift_pct"], - "trend_manual_breakeven_offset_pct": cfg["breakeven_offset_pct"], - } - - -def register_trend_routes(app: Flask, cfg: dict) -> None: - lr = cfg["login_required"] - get_db = cfg["get_db"] - - def _redirect_trend(**kw): - return redirect(url_for("strategy_trading_page", **kw)) - - @app.route("/preview_trend_pullback", methods=["POST"]) - @lr - def preview_trend_pullback(): - conn = get_db() - init_strategy_tables(conn) - m = _m(cfg) - payload, err = parse_trend_plan(cfg, request.form) - if err: - conn.close() - flash(err) - return _redirect_trend() - okp, msg = precheck_trend_start( - cfg, - conn, - symbol=str(payload.get("symbol") or ""), - direction=str(payload.get("direction") or "long"), - ) - if not okp: - conn.close() - flash(msg) - return _redirect_trend() - ok_live, reason = m.ensure_exchange_live_ready() - if not ok_live: - conn.close() - flash(reason) - return _redirect_trend() - pid = str(uuid.uuid4()) - exp_ms = int(time.time() * 1000) + cfg["preview_ttl"] * 1000 - created = m.app_now_str() - conn.execute( - """INSERT INTO trend_pullback_previews ( - id,symbol,exchange_symbol,direction,leverage,stop_loss,add_upper,take_profit,risk_percent, - snapshot_available_usdt,snapshot_at,live_price_ref,plan_margin_capital,target_order_amount,first_order_amount,remainder_total, - dca_legs,per_leg_amount,grid_prices_json,leg_amounts_json,expires_at_ms,created_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - pid, - payload["symbol"], - payload["exchange_symbol"], - payload["direction"], - payload["leverage"], - payload["stop_loss"], - payload["add_upper"], - payload["take_profit"], - payload["risk_percent"], - payload["snapshot_available_usdt"], - payload["snapshot_at"], - payload["live_price_ref"], - payload["plan_margin_capital"], - payload["target_order_amount"], - payload["first_order_amount"], - payload["remainder_total"], - payload["dca_legs"], - payload["per_leg_amount"], - payload["grid_prices_json"], - payload["leg_amounts_json"], - exp_ms, - created, - ), - ) - _insert_preview_snapshot(conn, pid, created, exp_ms, payload) - conn.commit() - conn.close() - flash(f"预览已生成,有效期 {cfg['preview_ttl']} 秒,请核对后点击「确认执行」.") - return _redirect_trend(preview_id=pid) - - @app.route("/execute_trend_pullback", methods=["POST"]) - @lr - def execute_trend_pullback(): - pid = (request.form.get("preview_id") or "").strip() - if not pid: - flash("缺少预览 ID") - return _redirect_trend() - conn = get_db() - init_strategy_tables(conn) - _cleanup_stale_previews(conn) - pr = conn.execute( - "SELECT * FROM trend_pullback_previews WHERE id=?", (pid,) - ).fetchone() - now_ms = int(time.time() * 1000) - if not pr or int(pr["expires_at_ms"] or 0) < now_ms: - conn.close() - flash("预览已过期或不存在,请重新生成预览") - return _redirect_trend() - okp, msg = precheck_trend_start( - cfg, - conn, - symbol=str(pr["symbol"] or ""), - direction=str(pr["direction"] or "long"), - ) - if not okp: - conn.close() - flash(msg) - return _redirect_trend(preview_id=pid) - m = _m(cfg) - ok_live, reason = m.ensure_exchange_live_ready() - if not ok_live: - conn.close() - flash(reason) - return _redirect_trend(preview_id=pid) - snap_prev = float(pr["snapshot_available_usdt"] or 0) - snap_now = m.get_available_trading_usdt() - if snap_now is None or snap_now <= 0: - conn.close() - flash("无法读取当前合约可用余额,请稍后重试") - return _redirect_trend(preview_id=pid) - drift = abs(float(snap_now) - snap_prev) / max(snap_prev, 1e-9) * 100.0 - if drift > cfg["drift_pct"]: - conn.close() - flash( - f"当前可用余额与预览快照偏差 {drift:.2f}%,超过允许 {cfg['drift_pct']}%,请重新生成预览" - ) - return _redirect_trend(preview_id=pid) - symbol = pr["symbol"] - exchange_symbol = pr["exchange_symbol"] - direction = pr["direction"] or "long" - leverage = int(pr["leverage"] or 1) - stop_loss = float(pr["stop_loss"]) - first_amt = float(pr["first_order_amount"] or 0) - live_price = m.get_price(symbol) - if live_price is None: - conn.close() - flash("获取实时价格失败") - return _redirect_trend(preview_id=pid) - try: - o1 = m.place_exchange_order( - exchange_symbol, direction, first_amt, leverage, stop_loss=None, take_profit=None - ) - fill1 = m.resolve_order_entry_price(o1, exchange_symbol, live_price) - try: - trend_refresh_stop_only(cfg, exchange_symbol, direction, stop_loss) - except Exception as sl_err: - from lib.strategy.strategy_trend_exchange import cancel_symbol_orders, trend_market_close - - try: - pos_qty = m.get_live_position_contracts(exchange_symbol, direction) or first_amt - trend_market_close(cfg, exchange_symbol, direction, float(pos_qty), leverage) - cancel_symbol_orders(cfg, exchange_symbol) - except Exception as close_err: - print(f"[trend_start] compensating close failed: {close_err}", flush=True) - raise sl_err - except Exception as e: - conn.close() - fe = getattr(m, "friendly_exchange_error", lambda x, **k: str(x)) - flash(fe(e, available_usdt=snap_now)) - return _redirect_trend(preview_id=pid) - trading_day = m.get_trading_day(m.app_now()) - opened_at = m.app_now_str() - opened_ms = getattr(m, "_to_ms_with_fallback", lambda a, b: None)(None, opened_at) - from lib.strategy.strategy_trend_lib import append_leg_fill_price_json - - fills_json = append_leg_fill_price_json(None, fill1) - cur = conn.execute( - """INSERT INTO trend_pullback_plans ( - status,symbol,exchange_symbol,direction,leverage,stop_loss,initial_stop_loss,add_upper,take_profit,risk_percent, - snapshot_available_usdt,snapshot_at,plan_margin_capital,target_order_amount,first_order_amount,remainder_total, - dca_legs,per_leg_amount,grid_prices_json,leg_amounts_json,legs_done,first_order_done,last_mark_price,avg_entry_price,order_amount_open,opened_at,opened_at_ms,session_date,message,leg_fill_prices_json - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - "active", - symbol, - exchange_symbol, - direction, - leverage, - stop_loss, - stop_loss, - float(pr["add_upper"]), - float(pr["take_profit"]), - float(pr["risk_percent"] or 5), - float(snap_now), - opened_at, - float(pr["plan_margin_capital"] or 0), - float(pr["target_order_amount"] or 0), - first_amt, - float(pr["remainder_total"] or 0), - int(pr["dca_legs"] or 0), - float(pr["per_leg_amount"] or 0), - pr["grid_prices_json"] or "[]", - pr["leg_amounts_json"] or "[]", - 0, - 1, - float(live_price), - fill1, - first_amt, - opened_at, - opened_ms, - trading_day, - f"预览ID:{pid[:8]}…", - fills_json, - ), - ) - new_id = int(cur.lastrowid) - conn.execute( - "UPDATE trend_pullback_preview_snapshots SET outcome='executed', executed_plan_id=? WHERE preview_id=?", - (new_id, pid), - ) - conn.execute("DELETE FROM trend_pullback_previews WHERE id=?", (pid,)) - conn.commit() - try: - from lib.strategy.strategy_wechat_notify import notify_trend_plan_started - - notify_trend_plan_started( - cfg, - plan_id=new_id, - symbol=symbol, - direction=direction, - leverage=leverage, - stop_loss=stop_loss, - take_profit=float(pr["take_profit"]), - add_upper=float(pr["add_upper"]), - risk_percent=float(pr["risk_percent"] or 5), - dca_legs=int(pr["dca_legs"] or 0), - first_order_amount=first_amt, - avg_entry=fill1, - snapshot_usdt=float(snap_now), - ) - except Exception: - pass - conn.close() - flash("趋势回调已执行:首仓已成交并挂交易所止损,止盈由程序监控.") - return _redirect_trend() - - @app.route("/cancel_trend_pullback_preview", methods=["POST"]) - @lr - def cancel_trend_pullback_preview(): - pid = (request.form.get("preview_id") or "").strip() - conn = get_db() - if pid: - conn.execute( - "UPDATE trend_pullback_preview_snapshots SET outcome='cancelled' WHERE preview_id=? AND outcome='open'", - (pid,), - ) - conn.execute("DELETE FROM trend_pullback_previews WHERE id=?", (pid,)) - conn.commit() - conn.close() - flash("已取消预览") - return _redirect_trend() - - @app.route("/trend_pullback_breakeven/", methods=["POST"]) - @lr - def trend_pullback_breakeven(pid: int): - offset_pct = None - raw = (request.form.get("breakeven_offset_pct") or "").strip() - if raw: - try: - offset_pct = float(raw) - if offset_pct < 0: - raise ValueError - except ValueError: - flash("保本偏移% 格式无效") - return _redirect_trend() - conn = get_db() - row = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (pid,) - ).fetchone() - if not row: - conn.close() - flash("未找到运行中的趋势回调计划") - return _redirect_trend() - ok, err = apply_manual_breakeven(cfg, conn, row, offset_pct=offset_pct) - conn.commit() - conn.close() - flash( - "已保本:趋势计划已结束,持仓已移交下单监控并挂止盈止损;平仓后将写入交易记录" - if ok - else (err or "保本移交失败") - ) - return _redirect_trend() - - @app.route("/stop_trend_pullback/") - @lr - def stop_trend_pullback(pid: int): - conn = get_db() - row = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE id=? AND status='active'", (pid,) - ).fetchone() - if not row: - stopped = conn.execute( - "SELECT * FROM trend_pullback_plans WHERE id=? " - "AND status IN ('stopped_sl','stopped_tp','stopped_manual')", - (pid,), - ).fetchone() - if stopped and not _trend_plan_trade_exists(conn, pid): - try: - if _ensure_trend_plan_trade_record(cfg, conn, pid, prefer_label="手动平仓"): - conn.close() - flash("计划已结束,已补录缺失的交易记录") - return _redirect_trend() - except Exception as e: - conn.close() - flash(f"补录交易记录失败:{e}") - return _redirect_trend() - conn.close() - flash("未找到运行中的趋势回调计划") - return _redirect_trend() - m = _m(cfg) - ex_sym = row["exchange_symbol"] or m.normalize_exchange_symbol(row["symbol"]) - direction = row["direction"] or "long" - lev = int(row["leverage"] or 1) - px = m.get_price(row["symbol"]) - exit_p = float(px) if px is not None else 0.0 - ok_live, _ = m.ensure_exchange_live_ready() - if ok_live: - pos = m.get_live_position_contracts(ex_sym, direction) - if pos is not None and pos > 0: - try: - close_resp = trend_market_close(cfg, ex_sym, direction, float(pos), lev) - ep = m.extract_trade_price_from_order(close_resp) - if ep: - exit_p = float(ep) - except Exception as e: - if not m.is_no_position_error(str(e)): - conn.close() - flash(f"平仓失败:{e}") - return _redirect_trend() - try: - cancel_symbol_orders(cfg, ex_sym) - except Exception: - pass - try: - _finalize_plan(cfg, conn, row, "手动平仓", exit_p, user_initiated_risk=True) - except Exception as e: - conn.execute( - "UPDATE trend_pullback_plans SET status='stopped_manual', message=? " - "WHERE id=? AND status='active'", - (f"结束异常:{e}", pid), - ) - conn.commit() - conn.close() - flash(f"计划已结束但记账可能不完整:{e}") - return _redirect_trend() - conn.close() - flash("已结束趋势回调计划") - return _redirect_trend() +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNpsvHl8VNX5Pz6TzJZMlpkks0ECAcIucGfm3jv7ZGZyZ59J7s3sy01muXcmmclCgLCEgCwiLqBYQQFrq1aty6etOwoKqFBbl5bgUrUgFVpoUYO2apdP28/3nATU9vf7c17P62zPeZb3+znPnffYnIHFGSarN6B5TJdFc4iGWY8YMvITOMJkDRSrN3B1nEKtIYwbdHiWZMJqkalk5DOkLp0ZslCMwFiltWb5BlpXrx0sTsnbS1YgR9IDCWR4oF7bbmYZIFfXW5ODQ/10uxWjdaZywlQqJxYPj+bh/LnaxeEFhoXJVUNr1RqOIWytHUnMKY1gBjC/uqq1arVphR4VoqG++CZ6qXUOPc90fWJF6frE/OEN9W3tS6fmt9YvTW4eGqMbrEpaaSIS8pIrMWPYVd/S3jAlb6+XJN1DHbTMOpuWmoKJllIQM5AOfWMNbu3io2mlUC/OUp6wmSuPmF/2kabwmn2m55iPTe0CrpdvSM8QIHd3kmvCrSeNT+vPF4f44XURMVUQLOPOq2X4yx9CDILQWsobXhLXhx/kbyYXPZMZqqX+V6DiKn6XMdXSjwslIfcT2RKPsz8s5QeeQEohcj/VKZB9rLGm+Qfpu4XCUPcTmZKYczB8L58iuc9khyqp44J6LheMr6QfFdaG4mB8HWdZuJLf8wgefoUUUsmL7e1izqv89fToJfVgL/lWeIQz/ozFlCTvDS8jVz+Z1W/gfp+zgr/mJcSwJvQ6uTS86Xfm0kb+L0k2jD9hGV4g2MtlOcgfzPmVwntCKKl9BrduCt/N15ADYP/XUycFGe4AWP96eljY/3R2aFn4Xk4hXPhDdmhT+B0yTgkvWtrHOe/y0zQF1l9CVoYjfzOEb+XXk73P5IZGqfcEEW4KjB+ltwhToTqw/3HO7nCQLwHnX0oagf66/o5yTvO76MAl7aCabA57wPjb+ErSB8b3UWcFLu5zYHyJ3i18MvR7MD7L2RZ+gn8FjEfI3dSk4FmgPzX/NvoF4ZGn2SFt+CXrcHgb/yvyIbD/LPWB4CfcH4HxWfou4U2hr8l/PaOz5sI7+f8kfwrkOeoNwQPcx4CcoXUINusjezuw/1M3PnXDlVsnHv/HmZuf673lttcnPn83eOqGg7ce3Bc8e2LHb/9+8L3/mdj+wkHlll1PvP3p+TO/27n98182nb3l32e3/XXiq2ca+B6baudb+yZ+8pdT2+/Y/o/Hth/7/a7zO84c/Vuz6iaURTRBw2UNR5xMEXwbmRRL1wXQo8XWqG2yPOaW0dU9IZEUu8DMi0c66ifNoapKX+2nA3JemnQJoh1HtIpIh5TuCB7J2V1pU3fUHchc6B+tEYtS6fU14RDPdax1TW04UlLKbRS9KBWnOBIJ8+lmOc8fjcvIPpmLF05V9w7HpbJyopdjj5CB/IWR3upaWdFnk4Y7eKHmYRc/mU43BNSTbcJILMqpK8i7qgR14UJKSnU3uo+1uuR0D20VRVwprq0Q5gourHPIufXpQqLCnebZjKEesT9yYa3URfVwyn6yUpKW95gUspi3XJNKKSfXETZuNFxo5BDAdqiCXSaNBJCjo5VBMi4skY00d9ghp0LphmNzo87aOF2qEvYYY1XipmjQbe4l6pLH5lL8cKjQWOPuHa5wxaNScVT86WpJIx0jzVSYU66KENzwsJ/Hi6UjnMmFdoUwxu3nVgjppK26o9ua4DkitdHhJEdG2yjR5HUdvDqSG5NwrQE/VUv1VaYkPVGFFL0w1jqWyVnK5ZVt6rVL+xG8be2msjazcdnattUbjs7btHbFdZmNOVMmw7ZaBnWrl7Ra+wbWr1i3du71xxYV8E2bN1mXjY6tWbrq+tLyhRkju2zeig2jkxuzbNvYnOGx+WvXrF04r5zJzV85Xlq6bLA1UOKIUsHh6lRHsKmmx2U4P7eA6UbG1phWja5RW0pqZs78JRN6EJ8/25DRjY63FteOtqrL665fsmplcZGlf8nYIt0RQ83kuun5N6GbNhcW6EbXjZqK42vbsyJasP1afMcmWRV+VOP08nk2KmyKBK/k6sjGkElcfV4Th/pxcSi7OHrZLK2KJDt+YKTTAnlO7pB77BtiQeuVBdbp9TNrly0uF5n5mfEreUla2iGvPmKmaY49nIX2zegRQ5ZFUYRBTRibG2NxVjEI1ldgHRkim9BcMuQNv2DGWjTy2QaMlUhA/nkr62sC+3MGwPjXBGD83N6+bAfX5evnCrslcX0rz99tq3GWdz7w7rs3f/TRw0d3fLRtYu/ZvqS7uSFY3V8lrqeCKWnJ0UApBnpqkwM+mSJii1QGWZdUEpeGKluu3m8jVZGwS2iHQto82RoD9jttH03i2nDBrQD2zwX7v9sI9t+PuWYzqAGej+cUVx8xAf+4ol2REkdASvzmfAYdyJ9ZkD/n6g2YRAjypxT5HnMAITIqzKYeR2m0Lufv72Z0WUIfydkxPq5CHmUe0tA5LvKChkVpDM9JMut3x8H9dIaGOhopcXiwKZiOVPJivYMEEU1UOXx1vU1NvLSquT5t5zWJe12ELxLnN7fUSZB79VcyvIjEWRPMnbfYquPdR+YZmcyctWsKy67LjQzrrl/JGpevnduP6I5YvC5gXx5cwQX3c9lYX93hagH7d3tA/id8wD4utJoTcUFZ6ohVp4cjokaq3FFPe2vA+eRmgA8Qkz6nxllstdrWC85/JAf8t7pMUseypNhxNCOK9qY4Ot23918A+smgBmxBzsA4O8H8QVSB3p0nmTu0Te0Jw3nGiwdYj8lemGW4qIszLcYgJkTFWZLNa4LGzX4X0K8q42oA9uMPAP0eMfVQCjcfzK9Sy3lgfkU10L9rFpR7gLxRBuzbrQLyywtM/kjQOh1/vMmquoIsya+eB+xPogX7W6e2sqjaPwfMf7kY6+0WZL6Df9rAeCPY/2pDlnGf0TBoEFPlDjP+giRDGDy557BmnVTTWdyvpTMnMKKVgx9GlQVn4a4iJ3dAT2yktM1qlfZ7xTS2PyNbSuInkP2YkpmSF4Dc48VeVquwO1mnzgmOQeo5ZtoszUzJy0CeorDz2NX5S2D+lXB+bonuf2xA1e9osmW96Ms6rvFO1rOJzl/EvAaJ5oBGlT1Q6O4/mfEMkYN78WarVN85FDAczsvDIURlDWpPslPzD4H5xZThOKY0uQYIU7DvIu62V6KEVZOLlTSMpGx/HsujugxagXQN9SJEiYt7DRY0hjbkgkO+gpIhrB72OW2zXmXuHEi38rMqfK/5JMrqlMNE2dev7E8vEuW9xobHVVbH39Jznf3chSJEgjvxk1l66G626bW06VDOtaPFzJ23lPHcS4+qzOS8OOq04DnngKNfWT6i7y24bm6xSC2dw0fU3Yzinx49Z2ErJrufXukb4KziGTzlgm6mWWrtHCIMrVlw/nYtGpuXzQVLjjWSlT3G4A64fwYB+5vXmpV9Qi5QmukFiZxzEGEs2bKhI+/Vg/UX1jGEme6XoK5fQvl4zntH9zjY/xKw/3YNKp3bmQtYnYMXMU/7JR0xX7XKOdi9KZ4lFj+Yc89rMXyltq8krI9mZCZyybX5WUY58ojeJfAav8AUkc7C/nnOgkqzT6Oa32GQDkzpf+yA/sGccxYfv6juWjuMxIazqK1d0tar+ypv69UxbNZgIK/3zNejYrWTIebA/T2olq92LuRmurMPs/IFxFLvKu8CvlmCPMD6cvT4t/oLln2jlzJpU2/uIbyxzF0Ozw/ux1xEOEudFu4In/UN6Kb273Lym1TmR5m01Dn0b/RR9Nv9BzuCVcD+z/3q6PaHf7j12Ycmfn1i4sUHtuzaPvH+X8/9/fu7Tp44s+3H/omb/zGNfyRyexuzSeWbNKW5Ej3wn6NFvy3BTaYaFcD/j6rDZCMnZkuIkxfUVbWxik/LXFWKitR2d1vdzTQpDm++/shAnIopynRSLLwwStbKGwLT+KLM7U1zpXo/DfzTsRDEj2N9YR5Rb5L6yJjgiqmeaiEGJb5Ej4DjmmyXuMjuCqHfSHD5FWSalK4C/q1YD+MbVlmViqVMrnRYXhs7tqBSLkoXCGk6zHMbkyC/vW4y6ifb0txQISyqDYgdCpPYH01Kx+F4GRh/EgfxfeIH/3j7gZ9s//edW258d/uH3/OvB/mzyQTOt/sHD5/98OvTjxzcevDZtz/7c8AF4sfNB75/6v2HJ47fvWXnlq0v7T91/q2Jgy9ue/HRtz/78tyf7yIqGynavhbwq9N/3vPOF0+8v//E1tt//P6r3ztz+Msbt50mloP4E5gD5t/5o5Pvbf3i1A+33XD5gdN7Lpy5e8fWD25TbALxyTFv/ZL8KpB/R9YuW7uu0LZ0dO34nJW8EK/Ox/l0VNyYTrvM4WSdXUwMij2RWKMbxF/pCpBfb/jpyVO/+eLmu7dNfPqjt3+7Z/sj92978olzn/3+1O0/ki4A8hu3v3zqn3/d+eQLp7/cN/HZs1tvvV/6LIiv2/c+/f7rN5y97fTO237z0auv3HzjTuWtNx24+Z7X3r3p81OH7rzhyvdP3XRl4pn7em556vjpc3/a+eCdZ17Zc+5X27fd8f1dd+1VqRcJmt7/7c8+unfSkVR0NUnqij0RBanwnv7s4d+8sfXd7V9tf+kZydx43FXnmPjwJ9vfvCw988gbZ2/64syJF2dt3XrjuX8ff/uRIwQRFjrrohVO6RCIz6dffercr2/YeusOItgG8m/T6f/51Zkf3Ln7zM6tt13Z8sHz5/Ydl80anSwO9x41D/vrGlxVID/7q0H+OqYVieW0f3IsFW9M8NKpWfRwWiVIR3oqqYKruT5VnTyagff/ApKqi3A7/jhEdZD1clukR0gB+xX7pJ8OzlGl0oQiHa+fXDM8jX9lqXjaRPjTAuGF60PViiZfuiwIhqsi4ljMFE508YwuiuIEL+BVwtgxbYWoLhGcLGU7pVSrwyb2XdbqeHF7ZI7Ln1Q8r70uzuGqNvldNW7nOpA//3h9L8hfVbQjYiKirho5WR03CeuiQVdFtKOhZa1jNcivF3KKVMdlNnEB5Ur40P9ywP+kI8B+j+Gx5mKHreaYLuWUuDaD/HghS8lr+QDfCbr7OIlqZ1riv2ySVk5qe+vLkXSj0N8B86slQsnIFOQXISEZoY8UiFTHUWskKaiMGBNN3Ppv8ZnNB/P/uNtOV0X86dnCgpTgVvOLdKyi5G5MCSNjYH++/wX+qy9Y9QuZAf063Uo2bzRhWRTL4Eyrdo1Boy/ei1vnsq36dqagH9ENsazB8swwqs0gjF5bOGm19DN6TM8iegOzUj+qy7AbDIuxIhjfxswF4/X6/D1mnZ5t06uZVfpBXR87bLC0Dr9vAvMv0q6F8oOtOMpa9RamoC2/PMCyiOnX7OvaouaeNm3G4PAA/3atAfgnynyn/oEC/RUBftkI6x+LAL7wGJwFAYgGJ5mXDftRoS6dd/WH1Tp92KJhW9Sb5SPAf543y3vEclkQg/Hz+B3Af7ZsOXLu7Q/temB/E3uP37jt69NnJnfecmNL9oqmwaZIOOQDwL8C2NT60/jLC9b3zwD+f6R1Gn/F+dxyZ281VUjwOOmqH4H9+U1g/qUZlxTs77wW8DeXGsx/wawQeqv75eK6wAVrhdNtTVSH3SLaYQTni2nB/NjV+k4fOF8RxJ/rWUSreAqsz8VIjQgNA/yiZ30ZJcCnh7KoToarNGGWQpy5l7HDapXaiahyoUJ3MZ4NWDx5r+Y8ziCuLIE4mYs4hijREFANnb2bVRUDAB/Y28/jClGUTWORPpHVq2NRp0aVc+afLbDZgMaTfw7PoCok3pfG6cLH6CG1yuxEAP4dXC9ngf5bcleWc2xSiqopdgQbAT+JtZSDpKghwnNwFaYl3/LTKf8TdIdKo+6gmydPV3SZ5HXhSJkTIj3YsfkOfiBEWRXQP9OBmFEYU/QKayqu1beu4dMiiI861IDKS0A/HFamJzHA//NjCgLIVfpGKZDLVZCfNAH9yQNgvHQm9C8jyD+RGN8Ys3neBPqVQ/w4UJjZQCYl9mht3NjhkieItKC5bHNwk/TMRF1UEBMpeuUFm5RM8MsCwhfESgtra6IzOiSuuJ3Dt/mdEjknkA7NKDl5oZoOF9fnsIP7m5wrsJMJriWoCFVzrKlokp8mj1g5Hhc32kHWid2T8+VUT1ouTyv6JXJelC5Lkw4ZJb+81OShghW15bRTUA4nxeJgNWEC/kvGxG4ROL+jBM7v9wH/LDnjZMwvDSoA/xXYFW6FOyqjKryDckCdrRK6nK6N+gMwfraamjiBZNpMOyqv8oMuEafDXRqNRVNSoV3a3ySvS6XdkqTTY1E4gf4cXdC/3MD+hgc5gZokx1Td5Q8A/TeOAv1bEUe0tqoci1a6bBC/z/4Gv8+A+2uG8W1dTdJN8OORklzuB/dfqSwGXYJU6G/g/uzrwf0NMla8Ffh3O7DsfhA/Bg1avIxq81YGVcufB/crwKb42bT9DwP/0oL7N2dRpqMO8LdgVgXs7zM0iHLzwaKjeIJNmzy5fRq+joveWTigOZy5iHs1EoxGpJlg4a4Mp5i27ss+h5/HlVpXLqA9wHLNJHpyCv8H81PjEbJ/L8DvSgzIdU9mue2k+ZIOyJmjLMlyBtK6J8oe5CJaqY6W060v9SvNTyMVGN3GXfnSoH/wQrZHT66k5v3BrDSmmPT8J7NfY+QCifZJjDtKb3q2+NdMuvXFfG/7nzHuksNMt+GxDHfFi0id9TGUm3UWH2I5mwjjj3PUkhbka3V0jMCAfKkH/ytGy1TsY0y3U5IPWF/MUzI+dlH7JPu4mnZIcZvuU5SWct2HC8+yYP7Ztr6HcX6jSn8/m1YGC5+hpIqDHsa/ztNdvkEgb/D0p/ETGI7M9Kx1LAT3O7QQKYxvnGPpR8YWLWjduKq8cO1a6/rWddmjSDjJMUJ851IB/Z8aBvdrmjFYWLBxtZVBxjcv2LB6lRUrjBSWjQyV1PiokZ2zcPnaTUUDLgjx2hun6yfX5IvmZY1r5i9qXTs2Yh5hF61SD+cWDpQap+sjzMCGxa3rF2wyLW5dOzC6ojSqHWnbuE6zaXNh+bxVxVGk3L9WrTRGlDHrhs0D61eUxloLczeMm8bY6xe2Fsq5+ZmVYxW83kqyouJ2YRL/TnyA8dMM7GcJxuZ9arB/mf5JWH/OHioEEYkhYDrPuEyBgsfgZT424fisNX/SH8BfwfCsrfC8ps68x9zC2nQB9pB+b/FjvQYHAF8f1MxoxrMYq9ZY9GGDhu1AVYWx/HVGabwyZOLXxAJJRamjrixzxIQU9O8W7eV8CDB+eVDx6YCQE45EuInuI3M4PXapL1Ko4bl6IxJ3IVFdSXO65UYQPyfLZFdFmtcT4Z6fF6+pticJky1akknSgl5FbV3UPwTO59EpYlP8nyOyTWoqkntQhQD4h9wLxruC4PyTZapXHOwJWmUuKlTrTm+D/L8N5B9Ub861AxUNqpcA/tyeKb7ROeP/G5+6RFW9FD/cIeziJVLOalvBqRAma5xcYeY78bcA/JcF8Wceg+pdduCfDubTbFpP5p9DmzGpupM9ovaPqDAPcgnTY9xciYX1E6lp7AUjwUtyh53cVDwuMybcsSDufw7sT/EU2J/UNMRE4/U2To9L/iSsDxyF9YEmGF/8EN9ofbD+I6cE8ZDk70AuNwO5HuCDIX32l7M6puuP0/UXYaynV1QxQ5QWx+IJEF7l8Hxd6Zg43FHx3fqWGsQXC1h/Lc6wDevB+ey6P6FcdRQsRmSlU/v3L5tdcLLPMlrAP5G8HVPOYxAtY0AyWTHWgUhQ4N8ZuvBssWlz2ljMN2tXTK68dr5ILCS2x+zGurgwJDq6ZlN23dI1GxaMGNds1PWNr9cbx0Y3zXHNAOeTFIhLED88AePr0FR92fEEjL+H/6u+00QCfCodDglTvYF6aTlOCajIX+D9Lrh6vzq8H+tX46zx14vzs3nT9VepJC3uhvizl+bNFNemBFSPQjCrkwfwM2GKJKPVipi0X8SvpCOx3Hfyaw7iP7D+AjanlzNg/oABxB+cj17UdrEBtTNzEXMtnqV2orC+kgX3azbYsoEVkL9rmSwSyzSgHqRpEa07md3P3pU5mXWaPFkKq8YVimixjNAsjrZgS4ZWlBqEydqYxMSn/BTH3lOXNBKx5oBjHJzfjoP1T/3+b2de/h9pDtj3xPmfnfrFbQHMdgnsr/FjmN+HYH73wfyOAfv04c75EN9+BuUcKD/6Tf7vAvIXWm28WC3lMqUTfp7ARvdWA3zSIv8ayBWrgXwYbQf406jtM6AAf7aB/NQpiknEEe7V/MxJJoR0xaw6USwUr5VXz3BzY1RYaqFrCK5QRkWTZnu3yqZwA/2VFvNaYmmqOpQuccU+WZLf0ngI5scmWN8yi2Ixsh7wC7uU9pPc59srQuD8yQvt0qTNaqsejtamiAsl4L+uWqqzJJVHjrVz5KKeYLkx2nGkVeiI8UORLp64nIpWuNN2Y0NS+ml/FcfXQ9eWIk3dlRGb/Glg38daE1RzGdbXuzj9TeIGKkjXpAhT66KRkmnD6sFF2pFyZmlxZfa6ZevwT4fkPH/aZqbjTa72BQV8w5qVK+aWV47A+jm+cNC4ealaO6EG/j+4OlHdk5LEHAVZM0ENS1zpFFfyDNCfdbAn6qoLd1gJRYLinl/YW6dIAPua4leFyoTc1GAPR2sSzi/g/XwF7sfT74L4+gIuqrQrxMC+zptSkitqASWNpBt89cA/XSXI/6biw4NA7nsM8pevIL9aO3r9ukXqjYVhZLB/pREvt65vDZZ8nbC+nu9tKKk5Kcr/JawPZkl/ulKez4e5JmkqOJmJOYSuSWDfvpfg/A/C+R+E88+E8z8N5++C+5sN9vcrML7DCPDHYH4h08quySzSt+XAL2s/Zh00Gi0gfuo1hdfb8eIP27Po/hxjfAVhdW+x2uIv8QHjLyx9xVcRY+EVi7V49+J+dM+aAePryKDuJGsqvmxdYzxuWVn8LTI396G/E6wv0H3n/ZYB+4P16wUgPinMwH5nGbi6TYxGL8GXuW0QX80G/rknoxj9Jj5O1U/l0L5YID8H7K/DBPOfWl4A47mm77wP58H5QNhExrIs5jYD//KyAv04rmTPG0qoT7eh8T7g/3IZ5De4awTGH8E3/EYF49MMKC8D+V3Qv3jQv0rQv9rA/K/rnHOh/lqh/4H9Iz9oC9pkdWlrWCyubVIBfFqqwK/GlzzQ73IwvxmcfwGMv9eZYPxtQS+quzIgvrCA3yCXdF4rN0NkAuOcXNoSz8bRZsB5uvIGxFlQqT34JZzVKUvEgG9A4UibPaUQhqM4wjJRgyUrVQdbWFSPMbkMG2cbs8AYuyn1y5oTSNcQofVGudq9+En0gOZEjhhysEo2rZ7KX3qpvrMP5C9UZXWxSr2zXdV/uFgqqAacOs+QK8dXq6zhQhlxlhWEx8SxdGGKhuCAL8MpHzG5Gr3al60qdecQYdqfM+DedlWb0yivpTO+NRX5siEpAvJ2sP4ogR4YVM3fp1YcohFuv5Pt7tuU/ZGBHHoEFeI4EgVYNTIgMnrnVpmIBUzOuc4H9rd/sWfVXrxlPsivmXQ7PcKd/wTQD73wjwW26C+rRgnc/RqlaWmt1GQZYp4zo0LJeZtQes4fM2wBZON1zuWete5b+Wru/M5VP0Lodap5P0M48x5EKjMgPxc5G8KLEiMeEL9VizpZsL8RuD8J5lRzN8L8psymsWX557QWnXJx12bnUud6+RUSUy7v1eE5Z/audcrMAb33eu/SFhTWh0OY/3tcNTlfuegBjLsGnG/d1Pk2PKKB5+tk97fRYyewvfNPojQ+niPWO1Ypxx/REzkKPY9xtYnMI2p/P3dREeEsLmGB8XXtc9GlmpHBTddt7NNsat8wPq+4ae381iHEUevudkP+P7twdOW6/pFlJWZ5Vrd6nWkgN75izuRySuFOu6WWqCxUbWqIBVwcso8Tr+p1ctNr/Q5Yn5wD/Lt9fRcvWcsB/K0M+BOfMtpoERWyWasJquct3L8SxgcCxieUkIL8SwlSg3Kqo7upD9j31ud/OnHuV+f+vGX3X95V+n8K64tzgX33iDiqGz594dRNf5p48Zndb27dfvgPWz58XIK/sDxaEYP1m6KDbKgrRwE+6XYZAT/qrq0WnF9B1Lh64PvxoIiiSRmsPwnTaap9JCzvl4itCS9hTsQrIlXDdokvpL/2fgX8H9ZvIf8u5jG8AQHrc+D7XHHM1XgNP8xmrfmi3dUJ4ovLA+PfLBD/Js01okg4xq0UrgLx53tGEB/UupkNEL+SkWS10eaKVJMFl7TSL1XGRXUA00jEKjrOo0lOok6SO9Zuc/BJMlVhoXld8h6w/tBwKCkrT+HH4XCD4idgfy169z/B+h2rQPw4s33i7M3nTv/9id8c/+F7z/5u6+0/fu+LG6S/ZoKvMrOD7JF5ybQgmraSvERPS7HXC/JbmpKThURNPSn9dKS1dWR00DxWbBszrl+Njw+PLlq0KeNcAM4f6Jx4fevWj/90btuPdzzyyLZn/vTu9/9x7vH7d5w9NvHSF7954fmtH+7d9j+/2/bZC2dv/1x1+tILW+67d+tT+yd+Mzlx9PatP/vx2Xc/fjXJzlnUtnGwqXUstyITnDGT2Jxbe51z+vxOSV1EGBPWWz2RSK1LgY+35tYRfiIuk7irFPxYMN0onBVA50pdIP7duG3HDQf+9e5P3zx1+6M3n3j+1E2/fPtHT+5+56ktL/526z8enrjhd2cPHJ/47IGZhQHHDHB/LQun4rPkMMyf42EyGrQ7OSRnlkPsTwRF1clyR20n1G+kJix/FeL7N+D9wf4GXwfMXxmYv56E/F5dw29IBG1S970QX2B8j61QX90xKZW44lRNTY+xm99i9aXdAvu0/SuKPWm5MUVEqifnJWo4CinB5/Dk8Rgn6ai3GR0pRWg4IhK6fgjnPwDnN1RyZYC/V1A99SFjONRyfjmQz4D3n57JrSSrukxiKR0r16eTIpsxnRL4tkP7WgbfZ8MCOXyfbqrzk7bqZMI6Zf/ndWkVJ2YnuJPqEEeerE1Ucxv5wP6aF0/VF46aQ9F6sWsdXH8E+l839D9LoobqbrwT5p89IP+0a1Ipk9sfbWd9ioipqqHX3w38z6TmJmymcEmj4MXpMN+WLlXW5POJepM0FhlinPHasqgjYm3v7bYqaHfKavan5VUFkbNSnoL1oR/A94PvQ/ymmepPkFNxV3WNXwz5BxqPKQCbKwt63dWBkqmmxbUT7C9vqa4qcGO1ImUOIyvSKWVZEQkpYj354pR+3Re0PG6wxsa1Fqbfr+un/a8F3t8MiH/mQH7XwxVWT9dHrG4pZZP8GvIbD/Afs34+k9GP6koF1qDFiq+b8gijeavP2KofZPSYlUUA/1mjX6fLsP0G0xtl3JJpZVq1BaNG38/gQG49qc6X9EXdADv4mkU3ipoyeFGjHTAY9UXG+gs0Z9a3A/5URFey7M8t6iyK/UBTRN9caUL1LOP6K9hfEv1OfxmsP1qA/hfqEX1DJYg/EjSInszvZ7vZldkjem/Oq3sZ52o62R71/vxJ3T5EhTt1c3LMan9xVokw7M1SaIuOiy5loBzoDpEttGDSgi0w/X42mCfa5KgK+V4hjTkzldi+a+/r+fW7d+X0hk66sGrZyAKmoTYtuV67ZEMTIadilLEhUJUU8CtCBRCf7SKqoaNBu3K03OakArZaV9plrAwaUwqCVgo6OZni/FH1aMN0/Y+wp0XReLTO4lWEGmp5XQV3bdjdkK4m7ER5dNBebbeFxIm0yCG0y3rlXP5sVX7pSratCcTHBirSaa0hq+qJ0XUjztqANCKu4vFnpRrJhMu1ENyvQ7ypbfFIZpRYVR5b5Jjm155AvD4Wreg12mzczkRaLm/Kr59TGJdxZngBf1BYE8FynUtBNyVVOjkf8t9KiO9ugPX/QZIrqSDj8Xr3IHw/x+U7Ib7HEx1Wtc/Oq0srIhC//ld9f1U5luZGYjaZxSeNibhWj5+Spbvl7rRaw/glwP7aMJTV6HUHC6biLwaKeYMFy6LtefN+sy5j0Px8ddH1F3D/r0qlCm9VqH2pNECmasMpI1kTchmaJGulUjrVw4/nXFIJo48UvsWvbCPwHyOYfx2KsO7/w9lckPUxlzKA/+WciAhVajsL+9XO3EmcVJ/E9sP396xzJQfgI09mH8bXnVA7NqfRA7kT6F70JEogqiKdsbFyIt1ezHcpZPB9vpDW7Qf804tL1E78ZIYevJsF81ttBS8K7M9wJ3tETfRL1R7tCZxuP5H3LuweBnIkn09ZWFSK+5caEH2WQQcRjr6ENeQ6VvkKJwqEEdin8Th2Qg3woZHOSjAXJTE72y/mY1kg7yMs+3IeM46KjTgTwG0SBsUQFs1g3IESa2eUpTTem3MFWyzc9gTrC9AjOGozKtVOrYo9XPD3KQd9T5J5r2kK/xT2m/ZnlQaPhYMGMPnXzr7ucn3G9zNyVUHXgp80d2YOqIlhlZFEpKh/L3dliQXrD0ytr5cZVVhnnsAP55Rtntap8e85R6fG306uLOiAfixPsoSBHt2MtiKq+c421aq5rH+tcnXA7Ol7Dh9DlfNcZWJekFEZDqmVKN06nlvGPltQrSKWkAM9eONn3AVZxmF25k+2edqVC51W1cD+LMC/o87rgP5WTOnvgAEh1jFop5mDBttUaw4zZEGbtQL/pXTNc2eC8Wkgnzm3Q92IOjHuaCvrX61cS+DgfuYBfLdoAXNEQ2xQXQfP9ygqX+fcBPf/iN61hlquQVULO8uBufRGcH9tl7Agoho7zCxnLuWPmLxj3uV8dOr9vJneqFzm0ZxYuB/ge2cxvfLEGgIH+lkGzr8E2MdSMnsC36vhLCKxE5lHGdfC2ux+E7l5L/4FJtsQLQP7GJPOBedbBvY33sr6VivHjpj7Mp6lzYuVyzvXp9u7M19hrmHO0hImHXVmfDllgZh/qOhZJLtOtQLo97ruHJD3cppKmCpzYPxuRmV3WG0b9l3XshjYj3MYiQFPteFfYranGYDPdExl1qkczKdwOSrFOjfv19C2E9iD6qYjtOJinnD3MpwMYflxjmpqMX2tjhan6r8Gj+mvGC1VMY8x3QPKwuN6suSF9WNj5+DjapoA+rFcwh5DuYTT+RDLcYH7HXwOB3KlK0vMO5z5GiNVwD/mXMyD8Z0ShphzKPcwzp8N1t90RA3mX2LTKxcdRr/O015HVnn9Ef2Pc/ZlLy9SrbiTJZY5N6tkngaO6iHkeJ5myYIuK1WMSdcC/w5oxqVLYX4KJrrFcdXE2SsTF2/dcua35/bcNvV+qRiF7wd7IL7RLLFjIH6d+8vtW7/8fOInP7n14P4juDz9x8FEmLZKCCIin9TYeVVpo0suFlLByWF3TwOIjzF5KhKTKZogfjRtlHSA/LPl3HsfHbhpx0v333T3Jx88/tZHP/zJjt8eO73n/Ykdh3d9fd/uA28c7Q+nHQJ3VEDB/S2B79+P7n3v70+d2/fBzh2Xz33+r5vvvssF61stxc1uF4iPO/+29/RPzp/aemTXy4d2/eytbb/6n7O3/PHUTb8/e+JWgE/f2X4l0oJvlo7/1/v76S9e3HbL3l277jr3p9NN2XxrRnXq8lsfffGDLR/8beKeR7vO7n/v9M+feOd/75WsV1/XdOqmy2d237n98m3bdp7b+n9PA/5+fAWIj++/+PbZYz+74Y3HTv/49Klt27a/smPL997d9e69QG6rA/L3fvjzj156eMfZw2fuePnUtoe3TG7fdf+pXee2fnT/ldO7/jDx1fPv/GX/e7d8ufvgvt+8/qNTd31y631vnnrj8x03PvX2dPztXyW/b4q/jzlHAD7e/eb+LUe/3vLRQxP3nD/zxOdnPjz0m3cOOZ4C9xOa5g/bBAF0iXQFiN+33Hn/jtu/fve2Ex89dPPNHz49seWT5rM3fXLj9k9Of3R8532Pnb787MS/ntmy5aWtN+w59+W7Lcr339h+7vBXt37vX+deu3zT/XtOv3n/2+eObX/+wW0P7Tn3zwsuOcRH2ipxuMahOFpMhFPyVMcRnQjkvheu7o8D1t/yyZ6tt/5r168PnHnxi1O3vnz68y3vnXn2/dtPb739jveuPHXmqXO37D9++txfdx44qLz14JadR7764MA/J+48dNM/7z39w89P7Ty6/fLPtj/w4dZf7zh34OW3D//71NmjH568951tf97y/G0f/GXLqQcmbrntb7MLS5paQX46e9eJUx/96N0PHn3vwNlt9z3+3qcPnrnjnVt2Hx/ou/Y+r6FnR2xjsH4xD+CboxafvbqhUJVQKI6s8CUrnERVSjocakgkIhxhnBqWumn7sX6fONakqIoFyxX2z4bjNa6YyeMgKzhlV6RicomzqjJmTPHlAipW5oTdIhsv/cd1Acov4NjKvXIy7TZWhqs7qiVHlvTSUleHNaUIx+RFKlwnSdoul13pOE8c44bkJlksGKm/YpBSiuEebu/w9PtAUO1fD/HxBpj/SZj/xbA+1p/qBf4TiREmN9nF8bcpYP32/IpkjTvJidf4Yia/N1xZHS8nqomAXUHD9wMF5Bc6yC9MEJ+WQP4earXxXT7S2BCV90pqFXShuz4ZlbniQJ6zkvWVlGtYRHWRisoeDjEO69PLeWSHNdJIpUSJ7ggP8EeKQ8e6jQof13F0UEAHy43ungpKwcL4cB2sb38M3xdHGvwcF1/OK0ZdQtJpjYggvnaIofwnkL/+DtafzsP5NXVhZzUldSyA75+rIH83wP6i/rjMZaquA/hN4XbqIP7RQvyTIe10k1siV8wD57MOCmP+hJnqqbApwnD9NJx/NZx/Dpz/OVjf+jesb/0f5BdfQn6hjUcayv5wg+RLyM/6klE7wO88l+IyrohS3iPDcdrFLXvdNdy0YiXQb87k4g+bG2l5alAQDPfwpJPmHjHsD6+MtFuq+I29ZNLsTHGS7UYX3xdLy4yRBHEUK1xhhPlirOloXw/pkgB+lpA6PoHnOwrfd/MJMgXsQzqQq7aG0yHJcHstVTEch/yiIAP6l1O97hqw/08HIf+ihKWobGgwHasspskGqesK5E/Kb+qbClgfHSF4Dju3SPh5YrJsE0Vi1zM5g/wM7A/Vm3PzcRbLqttYRG3JFDGjuoTp0NV5jEHYwg+MprbcUnwQy6uRexZpLJl+DFEPYGp0NG9hzOxAxqy35gz4CBg/F4zHMqsxszqDmdBNeR2znB342Phzc3YhPgrky1kNmH8Q06s7gbyc1x40FzMZXG/MqfF+rKg2s7hanSm+0aopYBa0eK+liB7M5IwnkKLuDVZdfAPPGV+3FIpvIfrCm2pzcZ9lGP3eQMn4pnlY92q/ofiqsWR8RT1UPGluLZxQtxV3q9ehbCFj1OM57dn8vOwHtt3QPoYJV6SBTjuMYvqYWijnJYKU8IqaF5AN9yrOa7y1AvV33p+zwD5agf7WwPfnw+B+nNmTKKlWwvpYBuCHDCc3x+ApUtr1qAqNZucgTparWYYoEVoznnNmuwc2ZQkLWbpO14JwjUsZAqVL46gH5ehbMVWB7lvBKNm0blneq+WbxpHOTLq1NatCyDmbUDD/yqn5V03NPw/MD/BzzwLIn0hEh0pal1b+lcGZZSvK4wD9bV6RWSK52t9kj6cFkWSPEfiXrZYrL16LXx0NAY1EZXc2NUmIJpXMeU1Okl2uzppodXoW1xaiOOIQNawIhmPVdTIu4D8VEP+rUjwHxxGqpJt9smqinBa7fSqegDZ2NDXxfDJbRU+3lVD0hmsDzX6RvLds47qSpFguG5YThF2pauqQKexN8p5Qkz8eBfbvoGrj8ViVsCBxciRBfyzS6Oqtsxkpl2eq/4pwJYRUwc3hyWD/rzUSd/HSCWuMZ0tJEmmZjOcKRSTlqJsr9F/9vkDJTzsbbMZeG4d2FDiilL1HovSTokpS3M2VWwNRW0ViZrnJKWkglNPnDwsjhQSH7/O6gBz419X+u1mVvgTQn4sOcd2FXnltzJvkVlqn81dDub6Drm6K2KU1LqrDFAmWRFKZjOA2iOjOMsfdzbFxwoP8gPGafiUy7bxRZLWO8Apl4q7qmT3KZHOtR9gsCCaV4uaUUqBKdgq8VZ5U8DvyoG81rA+s+qb+8b/Qfz+B8f1xGN9+BvPTo7D+fgnWjz6D9fdnYP3iAsRfl2B8+xjGtz/B+PZ7GN+Owvi2Aca3jdPvizdAfIbJb4Xj34bjb4HjP4Djz8LxTXD8VH+5H8aXcZMo6qOk9j6pi+NKcbuGffU8wh0X2+5E2ZyPwhmGNZiwrFad0TC4FsQBfZHRYDjgl1qG0ed1ObZoaMdYdFpeMGj0+dvNv3LcAvxLkP1O/zYC8Iv+Wv/2jeD8Th0338USOcgPbXmv7jwG+CPrsdA5JWJTK3W0mpsD/CoryRPmQzl3y0wz4EeZI2rv9SrEi2rRmDmbCxa6grC+gOUHdPVYA+JZQ2gPsCcAf4P8sZ45kPNlJP2E6VAO9n9okCxjRoK55gHI7/SYNO8sdGc52SNGL2vHWjQX1TacwJ0ZFX5IkwP8D88FM77CpQzkPwWdHA3g4/J9IH9IdCB+vPfC/Qo7zK9qmF//D+LPAUcGxvfzMH/9DpxfoYfyj6Gcgvn3Asyvc72NNTFqWJK6+v5eK673FYF+ZhVcbTA/Q/uYVLv48QA5/f5fAeuvRdgfqXjh/+f+1plg/quw98lcnJ7oND75G4YwbimIb2pGA+I3otfnLHgeG1VbWbPa9IMiblaPYxjan1czerYvYzzpuhPujwPWT2kM39afh8H6Q+D+jGgeJX6CsUiUTWsiWSUO7gc7jCqzHUOOgjK330hm9uLSNq76SdaJDmXFBgSRIEGkMmNzXu2fznm00/3TDlMwr9Lt1XKQ/ajMTzPPsipm2JAcUutkiBQLF+xVkL9jCAcjEHMuxtiZWasI0152qr8eu5P1rKaLJ2B9QOvUSkuHi85FgH+1A/5qBvzV4DdP8/8QotJkMHG+k6UYVcExk8xRSK1VZOoaMCDBQa4ZxGfzcuxiZhNbXqvqI9SejNd6HlcaO4fSmkBG0eXRnTAETaq+w8zdLJv1kmB+69T856/VF6qxTkzF+l71AfslLE9mr9PNR/ULuoYdBmfhpMFmVbYfRk/k9WyWkU2aDbaiXSPEm/fB/gJ91ow6jwH+DPvvB+5mm04Rxr0ZynIck22NDj+pdrIqHWmOo05ElaX7qhnAT43kYJXuInpeHS7A96GPzR5dK3oYBfI11Qxn9bihf43H1KI7qe3sO6CmVzVirrclpiexz/Kwv105eEB/KONFW/T/RqIrH9Lws01/82hhfpFowf3uevWVj3784I3bdty87/g7N//1o+/v3/n5DYDfTNy2c/fJH+6+6cPtZx49t/fcxK73mrfe9uj2fQ9/9OXxs/t2bNl1aOLsr899cgdRVRclbTVuxQ1PvvObh9+UzvJngP1slq+G8ed5GH+ug/FnI+SX6j+uDjjS9dxyPFJZptPyhgivy9TUSCUp6VIYn9ZC+x6D9g3fN4YHKmluvxDG/6GBcIg/XZ8dDtZJI/HLQxGaAvmvJsJVVBi+0z8I31fzwH7Xwf7BMXC+SqZFz5hnoR3ZF9nntUrD08bPi048P/2+CrjzXmDfEvwFnYqaZGz3FHwjMD53f4OvlBBfa0X1aUIWq6YdSQT4hxwH/nGdoStwtb8mQZPmRLegXJmIidIdCroqFaNEbuHMRB0pdwjFNR7h0Ajcv8yREtIdBX6Nm0jzgxK14gno3z+D8eFR2N9ToojKIuTHwbJNTidJxa8hvh2wpkl3YyKqbBl1MNf6Ax0JGF+m8L0Jjq+E/quB8aMP7n8Q7n8Y1vcXi2or5df6M2fZJT4qyKsIlqfrq3WkkNgH8bdaGk4IqGN4qMkTifEVJVif/CXUTx8xH+YXCuaXHMTPJKxfroby5+H7dA18n34dvk//HL5PaGsBvlIdwwKlbDdJVgmm36/f+ub7n4Wm6fuPhDnlCFlh6a0JiTgNH+jRvCsC8wMyYNTo1zFtmJ7Ff64rrtGP6gbY/GsmfB1qgW8iT/VN9Q8juIbV6xcW+/SDuhKbf97yy2G1NoMfRBHYv7yagfVP/OfLiiUgH2A3GNTYILo0g/8B1cp/DuurOhfVGDNh39a/NeB8fcB+Rhi92vd3oF+C8RhA6NHLkFrmvOEwPksXyniKYSSuJ0wt7CEsWOxU2AuzTCc1nOJxg3eZUP1Mxpsl1ErjHgs/sw+zuUjLc8xss0rDyVSb/XWzkRDbm3XUSywBc0t2BRrOtRqo3McmFcoZoPVD6Gwdk82zAUSnLxpaikUUZWzt9hzP1LyJZTJ6PWrE/Pki69YE1U7Ib07d9MvTh77nDMZlvf6apqQ8UZPqkaUU1e+dee43e/+67f47Pji5deKWr27Z/6fTZ3558137Jz758elP92174dmtj7x45s0/nz64I3B61z3b/v7k1l2feHbuP3rmrR1vf7jd+5tHPz1zy3M7X7/v7Xs/a1HY1gH9fHT6yg1P3Tjxt8fPTRza/YM7tz7xQH5YlEomJCl/SV4bUES48qegf8P+bMVF6L9fg/0BoAH8d900v6yLFlwV9p66sizkCiKXF/HEAH/VlINuTg1tdIn49b4GiH+0EP8YIf5JQn6u9iclNbSiH+KjBMyPxen8+Gto3xS0bwr65zuQn7UnKtykKKlo+SY/zoLfB45P9xdf/b5QGuf1DFbEHIUqSdQE9udTgPHr0AH4voKNoPMzbcwibcakOclm8V+gmbbXdExBX/yFvJjRG/6jf4oB9oGC+dfC/qnwVP+UJ9+Lv4xz1SA/6Un4/ZNagjrRkxm6COL7SspwKOfB+Lo5yPAap9aZrdft1Sh10/Xx7ow042v2FL1Yiy6BdIwQGmeGi4sQwD+w+jzNgPjOlg0tBdt6sP67285/9/vn3e/ete2Bd7ed3nPmqStnb/pH0zJ2pWkjk+1qpCm3Qnn21n/fuPUXpy6/alfEieoZvU1R39T3vXK/88wNT2+/746Zx9gRZI1pIZa5oJ56Pwigf1zlcaQVNVTHdP+PxStJigScVPfkKNnFi0mI2FX/JZyieCosqLqw1ikRS6++bxGSdI8gEeo+sum/3leTfgrwBwUl3jwz45sJ8Y0mJE72piskAsi/hztJqqlENSUFREoc7ZAwCjuIL5PGqHSGP6NjmCPmWKSOHMw7Kqf6Y/wBGP98MP61AP8dK2T1vgXAf0fxdWoji+JqwD/xJwYAv52SCwvfvl9M9cfA/tF1CIP5TOD+pbiSaQH+ncNCmSKrQqTDID7U2qPDAhdJVQhVNXGFhBOONO3c9eIND37y/rZL5x68I7V110vvffaMagavgfT1SAWdkWiy2h5vbNp94wtbn7p92r9Afjz9iy8/+vzO3SffObP1mVuf2t41FlDbZsD+qKLLDu13zTf8velafxLQ/1R/+FR/eqlWkTKGquWiFDUzi+kaYH9NyehAQX6Nab7z/QB79f1OB78fGAf7l6IepA47APD1fsY1UAvf77JeDL7f3ck+qaaZ8+jT6gqcxrmFlwb8RWWOMJHZKl0LrkLibFrNz3LRIhLUrHNn8wBflqb700vJumQNQctT6HfwIQP760H8XALxoRDsP8U40WD2M+yQWi7pmAPwB+tjlXmAX3LPa5vVUnVnNqA5nFcs86AndE5kGh9V90F8FGj6j/5CjZx/FKvM08Xu4u8Afl+Zfw4H/BqtA/y6lA2ox1SZSUsPZ6bvGCKW82m/+wXtVH+o/agF9q+7Y+rv6CcP9FMA+lkI9aME+qkA+jHoCVSaA3iUvZT3mWzZl3Qt2EXNjwpeTSmrUMH6tNFtgfyqC+Y/HfB/D654Ffbn6mWKPf/Rv9r4K2Bf67AsKoffp+KGMoO/oWc1P+9qtXh5DhB/yEDUHI/X09Jem4nu8YsE6Rk1IIaGq6RC//T9luuSCZGt2tEcD/cmOVXu6lnxOOmScqMiV1K+EuAbH+L8A4yPZhgfO2F81ASc3Mogh7BC/KKRGnEQf1u1IP4tAvbhmsuC+JdriDcnxXx+Tg3fT60pCXEV30wiNjnV9W3/qd8IzpcA9mWH9VGWMWMatm2qfz/7XB8L+N8bRZ0F9k++lTH5RFZZxAbiN2UTWsLV8VpJna9A1bp7KtL+mh5xioo0pGZQPQC/0Mb6qLhXIm6UWsaWbdzg0SlmwvishfurB/fTnJUEID5shfnDCvdv1V6L3wvYq/yGmAH2VwvIC1EG51/AatSL84Mmo1ru++/+tzVAbrrW//ZiETXYWfj9sqogMzjR2egzeW/BZ5EYA8aXCyR2B0ssooof67lofbHFRGO9WCab7EM1ErO3OcNiqJohjd6CQJ/ALUy9oROdjd+dI7N3aJtqAqaXMx7dniJp3ss0VHORk2yz2al7FQ8V9jG2Kg6yp/0461UTeY+JGhAYLmKqHF9PaBUWf/EQc59WPuZDZcWp8VowvsRFL7HSIRqdZbm6vprQCotBSs1gBpQVGJTW6fVVZn/Rkw3olEhY82nBqy0ySZ2alSPN/ml8MFvrZz2ZpxFVe7+hnulAA3nXQW/uFSPXeJLl6wOY9K7giG0ogHHQxfD7bktgkDT1ZCU/U7Uri3yMj6JYooSwhEmiiZqUr8HzY4YBtkbfgCvzjbudWnC+4X1MWCM1OHbJhjzacGmZ4TlmlpFriDP1Aaf2L6iT8OT2qDmL7tDzAZm+j3U1ewtgfdP0+lDeuTiMcBansZo+r1XN2DRAv+g4folt1gbbZ2tCI8vYp7UqNGxo7iMXBEs2q7coWMLFLxWbdUHtbF1olGSfVqsWEeqW0iHMsceDe7OzzBdRVb8Mcbb9DnX+EJxvrtL8tB6czxgY8pi9Q7MwlUlZOm8h5s9uC2XtUP86QtOy5hDWw5IGsD6ewLVM/XUWVGHuKHcX79O2NKsN8P7DjMdIlXoMZrw6K9fT4P5DQyI2gH1pcPBacnt14TX79BSr0H+lkydkgIoK5y/PHmICas6yGkN9H4kGiuR1VWyNmotnGGBfpl7MnyNX1mgk7QETf4REny56F9sHZy2+qPuSbQx3WBVW/4gn+7T2kiFg5K8WoeB+MXi/UnNF4QuDrTeHwe8T+jXVBrdBVvaA+Q+ZPAWBiTtHVK5ZRCwA+StPbkrPq1oSXJJhPQuAXO8pzFqiwoB+r3O2gvtbP31/9+kb/+lpDW9aBs/fmsCVK/lzWuH5NoLzLThhCs89zpCok31Q735l1jLusk1w/yt6seAA0K8GyOfz1+/Dn2aBfjb2GDS4sqjRf0d//7qqvw3TcvOUvBP1Ldlgmv9tf3PE1hQnyenvk+c2bob8YA3sf5XC/D7/Wv/eMVsPx2ESiTrT5cYkIbLV9KZNtCdZxUuGIqZE2q4QJ+XEo8D/92AGU7yR5DgGRbQr3cgLUaVhKmZXzCzIiFohz/cr2N88bBeD9cOJDh7ALz2iISxUVRuX9lZXWj2x+NT3xYCfjtEdQZEoYouY4mSKU+k3Ohv50mKAqE30Nbm5xLvT3xecEDsEcH+r4f6uvS9Z3TW+mBWLRup4NrqnshwOK6r8R0F88hguzKuwd3BitVe/j+PGIlPvK1VSCfPpBiEnnCwr3N31pNHWw48Oh+vEVHC4igq6a7i+dmuVuJ62laXuz8pEFZWupsOFRsWQAfZHlnj1UUlcYmry/RSc71S3MFrJjX1Sn98kdFFUPZhfbKE5JF9aGyik+CGCZzPaKIl0nl8L9auD+KkX8scU5I8GyB8RiK8ZiK8PXes/+xZ/O9tBfO1bH06kugA+mlVjT5Vktb4aR624vhimFCD+uxVLYPx+DX4f1yCiUzOvfR/2Df8ctlcnbL6qatvU92uLGDD/r+D8CzGA79dM4/sQXJ+E65NwfQrKN8H3jXKCQ9hqUx0FoXhSR3Nt8PvdrnazCL5fUbV2p9J1vVrDnMfS9VJXsJRJxN11emOTo+VoPuUbYrq7ZeUmwn/UCt9nYu5Sxu5NVUnaGJB/rgf51YAAfMPx2rwwv3pg/XUGrF/ogXwFwA/ut4B+zIxRWzAsn+rP0tyzxLQQfp+iG2LzhnnYavh9wUENstZg1g+D/D+3gOsNDOSfmd+PwP6b1+fnFzAaJPOa2cTeo9ehB3CzgRky9esyIL9iGPw+wcrotX2Av5YZ5I3lxQVgfObng9gqdrVBi6/WmYBco11pmKtnwfzGgh7ObxrUrWEHwfjVsP+LWQD4y9zXwHhMc9ClR9lcMvud/w/TAPw4AP+/hUWxjgpw/7PtQYDPLmUAf8k8h+dxJdqVJVA6U4e55gH+oruYH2JZRpIlAH/xakdRKdJZOKJ2MiPoPESJOJFVubns1e9DcgH+NH4LIM68bAapvoS1Y+Yc7J8J5tf6sfwUfhqC+T8P69P4UW00wbXEGylrvi5MlyviHUe0lYkeUdFmN+UiZNgUCdvbM1HeQIHP+e/+p75yR6Q2RNk5ggLPVnkC3s9qcD4QSqb63zrA/bQC/u/ugvi1T87zB+KKUooT5aT/4/9hYP9b8dr/wywG/q9ComxI4y9y8SKiRGm8LuPv787osmlzMd+sXvFyHtjnR/fec+O/XxwyKmxRp0xIGx1NdxSBfnfs37fzhX+U9IpAr1fKIcsdoiGdS1rrlYU4pppEzF6RmNr/TNY3Uw/sW/Wf9aVF5cZEtL4sJ+imYkeQI7W7DxZQg2sY4B8rm8kgeiSnxhtnMIjmqDXVyVGE3KHit/g2a5nq782iBUTD+tPgfDY0wJB6gG+MSkzCtpgOYxJTR54sPK29aCCMsuxedaC4T9+bf8V6AuTfP+qPonw0xJKZZ4xK4x71cXYl+jTrtVD5y4ZGXO6VqZ3Y71A/S+ZrNEpT2CpiPche9jCwL4EJx4MDTUNg/beP/HHiq2fe+ex5CVkvp1yEyeYPc4UFRUgobkyr3v7Vrae/eGH37c9u2773zOSl0/dv8Zx9/pYtl57Z/tj/o+vN45u4rvZxCSzvNpKtFSxvGDBgg5YZabRay4x2gWRLli1LtpYZjW15ZTGrQ4AsfbMUSCFAkyZpszdJk5YkBMgCAbqQJmHJ1oQlIQ20aUKaNE2bvG3yu0eyKen3/f19P3fmruc+z7nnPPe1rd/+5OzeL8/sfePUidve/fC2d3afuv36O948fOOpzYd96Ym+UkmBxz8Vv22LFEVs8aV8AZq/sJtC+JJrL+ZaL9z4w5tufMgaL7NHxL5Rf3VBIDgqDkajksIofc36R/xIlUDzvwLW/3o0fuUpismOEmkK+JHmz1i1wpIhlVT6Kw2l0l5zvyrQO7aDf47u+ROzbC9xKBXuipXZY0HJcrCfHsjvGyjpKI4IOuIlrsrNaQZzAj9bzEiF+jCvuIINRPkBSpQV2Do48VFvQWGPI+QpzuXvi4s9Xbxg1MKJRgWlbaap+72ucK8hHs/Hd4+ILwI+/iXkNxsTgk4yUM4Zd9jKHB9qxEX+l5QVpVWuWEQyaq6Kz+jt4PttMcMoGbbx0PnI4XskCM0nLxrjkkC0Kne/L8C9PLD/XWD//ej8FX8K7Wc7Oyu68v6TssRV/4kpWhAtnr4fAn2yiMETjlZKAjeB/2QeGp+BnP+kFV+N7BeB9p94hlKXvLct0F1kkbmpls6yju/xbwydn0q0/7TAv52ofk6/kAjhtdoowudx5aeEUidndipCbMC0K1FoFCikmYu6Vuxl7PFkIP2EUmogzXJ6Px7JuAy7aYXuGHZs6DBBKuqUpBv0iThtIeVHaH2uohH+SYgVMvWHdJEubipq2zcQGHlBoSVk+FIJnuN3n002jS1e02zABzeuXzC4qnWDZvVw87oVG1s1V/RpRWZtsn5eQp/SNCzd+OFcY0fvrHDJdH57V4+dm/WGykrssqn4crdoXL2hmX+NfzUcD+gFkQKHpLzS5zHn7ken7ic1reOZ1eTV/Hi+1BUriYkmlo5R9rjA0xHLxZ+OwP2tTUZN3a82blhpdXDszmLfaJhTFOzit66Xye9Ni+7V9ojPoPHlYP+VPw/6TpqUjj4+htZ/L1aHD7JumlRJdfuI7/m/Pxk2VEeWZYtiIZGctR0B/DUD1ocb1sfV9e3Yhebva3ZKf2scW1Zr6Iw5BNP5peEefqC/KlpgsZZHyP/L//09/xncD+DT+Ye1aH89lbv/O4Ln/Wegn7RfyddQ+PF0PAP+s5hpf6oDV2E+fKO4Gq0viQT49wbg37C/XQZvIfiny8A/zcvrN4TR9++m+UX0tfoAknvQ/983JBmnCtnfVohvuMvnMsyMeyxGKwX5q8HiiD1mFkfDFkms0+rtDZei/VtVKCmeFYrODBRUzmEuqtsLauRmG4Wj8emF/dOdQOMTR+PveAf0H7COyvKoYxju57qAv/4U+PWjcP6pJNtgf/GA35YDv+2C8ykhrlzuucJQndVOTiiPPxrwKfzx3Fx9iCsRTfnXIiV9Vi5rj3BEzlgf/xYDzghkyP6PJYxtmtQ8nD2xWq3YY8QNP06qFMpB3ICxaTWt+LG4GeGbO3Wo/1sPHNj8qz1n33nr5Lb7X8U/1In4sZkdYkNFd09wlnNDAmM+1hRG+sIzMlP+Hci/aUDzo0b7d0KbxMVvKGiC1Mlpl2ZfxmEO0kqdQCulL+rQ/gP9MBb83/uIInqXZntiFwIVtUZxCwfhf7+yTvF0wp0mMSnkZ7NutS/trA1mlLo6PT99RIH4pbGDQfx5Bse0g6gZTmO9owqddXQOmj/gtwHEj0eSUdqpkrZtV/DSu3DLQlfbrqHCtmNa4UK5Yq+pSH1nwj3kw48pQ+bDNDnsS+1UBFNHCdEoZ6hGRWmr+zv6d47sUAnbfeYjQwEz4v99waGXVcc0wkqemkT8c3d/YADxe5+vDZUTOxg3jvizAvhrDZ5rH+K/fqNUE9IIGcfzviH0/exRQkrw+3P/n3dngvzch6H/L0T/vy+0GtmfQQh5R/yr2a+tU6Pvr0Hf3xtahPjpPF9ip9Y9ULWD2yZNHNFSupcx5ylXcnvjcWKHoWaNS7F9tUtlHajVirZxVl/U7dHUzvcyO9f4CD6Wa189av93wVUfEAK9NHVEGW9A9Z8MrNqnnipvROWfQblswfEk4n+Ko/O9WdcgaZaa0fiNofHbHGh+lhbjMsWxYd7SPXj1Ox1j++kdGllrrn4Lqn/EnXy59ZhGtkLUSqH59a6xDPiMX+osBh7iz/sSuM66stYkw/grebUBrKjFm3iYDmFS5f0Eb5Ol5X4mYHaPfk5wW6XZL3TxBonOy+4cCi04jvinwBGYt495mAium/p/0x78b5gz7EpvN3BadxCf0yQHxu8hunDJVP/mvow9nCCjobmXdej7a11N6PuL3avQ9wnpIPr+oroFDyQC60NV/9CFpPLxh/GQw9WC6s+WqY5NyjVU/ctaVG71NaByXD6AygdcGCqvEbT8g/lMd1Cbwr2JndftUDFEiFAx8mpq7tX4ys1v3XJ+21/P/Pzbk+88W4Xsexz0gzrKHHEHdeONF84cfvPmJ7dJb/7hgZv2/u3th4+c23bHTe//6OyOI6duuG/zuwe2Pnbcg78sZOevW9GYoGZXNQ5uWmWd0r+zlFT2FIRnGQShrihflFg4rl1dVRGrKFzQpnP28khPvFScmUWVlc7R29y5+FzJKrAfG6fvR53LkP0glzDIvpwFfRpTYSwuQfyQI7hitpR2TelnLRLfefV+5t9QH/QhHN9A/f1gn34F9uk7qE+Iw1ZJsC9i5iL+m4sfeQ4v7i6tEEW8WYnPWXClH/GvvkisOh+/82FTeXlA0McrNnv9PdWDPZVO8cfGkm5bb3FZNhIUxv2hkufwiu5ijtkbi5T3dFRdGSEtvFg4UyXwd4Vf1E/d/wiKK6JdkeLP8vF1vDDkvzsWK0E/p4Dr50eKDdWeD1WCACnsEnxs5naSXinkz0l6Inl8tPbFddP3D4gfG3y2Lh4nG3RVOP0OiQLOrz/A+dUL/PY94JcrAH9Pou9704DvksAvC4FfGIBfSMD+GoD/zYX+NxdGiovMgc6uokGyIFLa2ym52OzmSJzBUY4l01HKz0B+VXFnb7YotKx6K+Q3NfR2VheHu7prIb5J4CuxGaqLfcFsdU/PoSWFwd6ifH5gOJ7TDyjwlvYYhBXdkSsjCH9F9DaLAeGH4lLHDyF/aJGe4yt2GCQFXk+8vKd/RljU7eCFRiOV+khPvCTud/alp/3TO2H+9LnvB6MifrhrVOzwtsD9rwKdv+GAxdNI1UP/3oP+dUP/1hKI38bh/oWB/LdRZe5+jOM5Dfy7AcanA8ZnBYxPCvE36n+g/mIdqo/jcD/XZZ9hir1knIp/7OwUOKyIf3syaHyvaG1c54cmSZmncoBXIvLMh/oKHNX/Pr9+br5+Sp/rGv/Htfj9CuJnkvvR+h1QqhNGGlOP6FS/ZljNCYyF/Kh+xL8TiH835Piz5sdLFBt1VcRq2ozrGch/ug7x7wnEr9sQP1bfW5/EEH+vJ8bpJhxx4ZeVqQTBIv6M+Plvx9VLEX9WoXIix6+xjOY3ykziWFKV2cOYDK+vUeN3CVOqV8YUqiPMkOYoNqJ5XTmS+H1Sn9nO4IbfsAb8R4pB1dH+ZtXvmFWa97AJzdvKicQbycbMD5jjyiUsRpWh/gcT1/BPHRrf1DT//HdOn3QZfUhJMgLchXHwg1rJEJW8MyNN7yWiGTcumMdV3cHuVZC0TOls4UB8FBtP/5zVJkk9uTGoOKI9prgjgcpTMnwnzjFC/EgfuztzjN2rczNBVY1appmgmdnkgEyxUys1xo0bU3to77C0P6a2ppyrREaZ7pdMpzI+KDVbdFLc3yYbjA8spS8nDhnc/VajHJMplw3FTFT2khJXSNs8AzImnuXSsoy7LDDkVsiVMnXnqgfgfmaeNYL6l48P8W4+/ouT+185+avfnTq79e0vny09/+5Xpx998vptT4vy+XPkTT974dTfDpz+wzNbdr8E99Off3l2y6HNL9507pWjZz99wHf7+zef/eWV04//9fRfdr998RnEDzff9vRbX++6sOc3Nzz9TZ1H/ALYNzEav7lUG+ibSYs7e2Lk6V/s2vqPX0q0Of52JRmNHdIW28MvKjsDswq64h5uIJf/W5fWNrROrk/Y6yQLMosm/5/8sZJIqcQ6Oj6uHJNUWQpIp4Qn6ZohiNvCduHaDQ02T01PYVkFrzCHzwPliJ/6EJHvKsvFH2+QWMD+zAf7sxDw4QZkf4ZkpupKwHfdsP/Ww/3HPNC30OXvv0/D/jgA+8MJ/qdhKD8L/fsr2O8rYL9ZsN/DzBS+/GTQUBSMhGfF/AVZUayjVGD01gN+fw/wewP8vx7+XwT2bybYvzVg/7phf3fC/j4B/oe24JyY3Uy2aQLc3j5vQe7+6xp9FrnHX9E5HV8W65olzoY7+UHvIGe2n0OW8irYaFe5o92N+LV1GWr/QFKIYwxBzIP7c22KSf/apE3+Vs0YaXPOfzVOKyC+44CB5rsRPi9K/5f+zABq/0rQn4mg/vkGpck9ukDKqpHjl5QhZq9qb/oYFlAI8BHEb0yMjZWvg/g/TEtgXFWSXq7gpxYvq0w3rN04obQFuOJGzZLVMosg1FGQlYSDZZ6Qx6raOJ5qEP4n/oANBHpLJa2TzesyZMkEn5H8D/S/APr/HtzvpSB/Fa++FfhFTt/2++NTzRBYejWWxMRNaH5UYD+gf79RZ+hjAyrU/994BxD/qWZoVL8f+AvE17yW9z+LpeTSSCTIUV7D34bQ/A9N6/PC+WwPIXydDNA+xQzdDsMRehfmT7hMjiaxQYBLaZ4+rn1f41wN/h2ZyWfkZfpwm9eidzOFJq76Mltj9ivq1E8nHG7fNfjbbeoZw3VctZaW6/ZiRWpv2pXwaWcYdhgSjAvfTgcIN1tAIH7NTPHrBWkmpNVC/EsyoF6I+LWVrdVf0q6kaxQUfhTvGLAkfHqOcp9BzrZrdmQQPkzUKrltlzMaYhBjcTvrn6VE+EqpI5iAMk3bdTWjko1ofW/55rYLPztz8pYvTn+x691Tz//hxb+6T972wuYDj5++8pqssLukpNgnOPneIYm5vby3ozjoieX8I+SpRw8gfLX5ts82/2Uvf7Zsgm5aW78yQ/ZQ3mpO1DJaFHbHBDPD1JhmcYt0Or/ZHpF40f6czs8UF3nEfWWl5liXpVBUai1rmzsxb3xjwqxJrE4psutX4YuHm9bn9O9GhgPdVVZJeFgSt3uKS6PrPZzJNZl6xTC1TkM2b1qzAdmH5vXKTTl8WFzWkbVznZ5YabmHqpVwq7yBnorBDo6FU1Ml8QW9In0kbKmwdHW7bVE+KebO8mUiM6heUbhEVqVat3GoSSOsmeUqE5X6CE2hXSwu6PRP55e4ZgwnJuuV40LwL8TKeBlxh0RSHIk47ZkVuLCQsojjvX5zYZjs5ofDoi5boVssEPIg/n2wUyTs4o8rB1MblyqziXULxweTTUsHqGRLg3pS5ix0kMU9ZaOdPF9YFqPKzkF8S02KfwPYnwtgf7Jgf/aB/XkB7I8K7E8b4K9hMQ/Kb4HyJPj/B8B/1JGL3xP4C0i+YUa3e4TJj19/Ot7rEUqCgcHRaMym53aJ+vi8iv/y71aDPtwSrQ4XL0L2K5OA+GMWw+9VZFSvJ4xNRF5/0gzxx4YklmEG9KbfsQo8oaBVv08YVMeYpApvYhSElh4xJF/pTw6j8z95gpqBzt+7MhfbrsUXmfL2F/tnhDqdhcF+jkUftxbpp87nDOjDoP61Ifu4QYelHD1of5IqKbGPqElacF8iQASZD/RSNSfDM4Q1lRoPizOkRmraR1Svdyl9jKstmKw1yHBO8hMTgRXhdDLAhrBifdxYk8IxX9pFuNNQjvifeQ9Wp9yd2cWENJy2F8wJJoClablyQ9v6ID9kEFTFvdnSnl5RvICMcNiOcKU46ID4hhr6uXnz1y/WNrGDGzTsqqasIr1Gw9LZpsVj2bRiw4BzA8xP4kXW22kVdLYfUubiJ21+mB+KydVfUtr1f35frhHb4Xzyg/9EqEPzeyPMbynMvxXVlxP/0TcQvwPzb4f7+6eh3ALfvwX4xWn4/1D+fj+3PkhYHz4oD0H5J8A/qoC/+ME+yqb1aSSzIf5n3mBBlbtkuSH/foCFLHDk9QuiFaXOoJ3FdA53zj/en8AIY1KrSeOsUsHolYZE+nWFth83aNm0gYb8EDOhSmk12VclHvT/ie2JlxZY7VxHJlZW5i+P8Lk81hvn936aDcq7Hfn4aYcLjW9UMeVfB300BzqfDNP58V8rID+ewi+lybSNkaZjRGv6WQ3CV8o7MjEVlYH89mPaPbh0kBqmVnKSWZ0r68LlGq56MU3pqDRX0aoQiuMKaT/Ftqcmk64qF7NLLTcdUy4fJTV76WOY2yjVQH6JO+Dplw2RbTsZt16uOq6F+F53vUy1kziO7dFKh/xDngxnoEFHLgqaL+LSNseIz0AlIX79Mk7OE6TR90fQ95OBftS+eQLdskzMWJ8UhQI6VL5ARu8d8IxIV/oULvZZjbxJ1vCjJDWXYo/rXGrpAvT9CSp956A0i8qTQf1a7JLSMUyq4qMbsf1KqYFqOsaSYz4Xv580BIZatIKXuAg/UvP9/TJNoH4Scz/EzR6kvSulq2I6hC/3yBu5uu6E515qgmvqw8R3+fWCLDX2DMMfjzW7Vrkb1mOXVPPpWOPBtLQhrShtM+GClf41gYz83enzPae/rdm7QmZ2NfJNlJY7VM+g+vD/sRbtRfwYtnyjr4lKXcLdC6UQ/zRBbfSMSTO9+l1JV6uoQbaoczymO5gSvwn5NXYxNxtfGV1RMkE14ml3Sw7fZkg1xXAxq1J8xG+UZQ7SXQwnSS4KpC2a6hNo/NYcUkYSXMxl5MxFiz01wnSNJJK+hgdSPt218UvLtPxWCgd9IS2tTnaqFWnr0s/xGv50/HdUWY3Fcel1cca7jr+BJFz0s/jnuPheND/N9cm/4+RMqWrvkmMpcoNtrZRF5ZPuposKaYtjHamhMoJFgYIKLN4ig/xfVtr/GBFYZ23+Audiy0YeU8bXy+Y/rOToUfsT8Szgd1S+yWqWa47zO9fuVT6aEHW6VBx9PZ7LH17zn/H7Stm5jqz/eYJrdjX8A48vlo2j8g3SUVTfHhSj71ctGwd9SlkL+j6fUh+3UxOe8eNpVO5wz0Xl2mUDqNwpI1B5tV/yVXo//QzLJG14YDBYVyEpFi7foFNQ18nw/UrZwhG8bATht41fJqy78/r2XMXydVPxX/iXuPcKN0VStnEpG+Pn9JtEAtEy6hBqv+TyMq1UFMdlIwdZL4nGF58a34v58eVibv6UPr7dQ0sTZJPLHpR/oqBBn0iyJ0ljy8QcU7y2xBPyeR1MkpzjSiP+YpLpl7FkU37+0f+PovWZfIYpTcR0rrGgVoj/XWWhfTpqxSU8quRgcdmfBnPxa1mjzrLhQVxq+J5+YAOl5i6PM8+wMpuvxcUEZTl9PYtPRNku4Q+oxNf5+TLHQfqhjLiVlLssbrPcLGt6lI7V+a/7FgtwvlZQ82VD1BrbsJQ5RFgG3ThPv1HxS8YH9mNBq+IyHi/gBqh17ZE/Dx8i8HRQq0LjB/n5iH9x19tAP+feuYP16xsW4NJq4Tx2bYswrw/rFpZ0dQaL+jKQvyoWq1uaBjcKS4OlvcKm6yb+W/9Gsnr6/p1qgvO5Ec7n24GfXMzdj4P9fxzsey5+/zdg//8G+OAi2PdfgX3HwT+xFNn3H8D9s/hvyL7XZZKJ+tXDY5TYy+niGAT+cBc3FCrN8Rcvf7BJKXSSPIvP0RUXUT1cSVV3bxDh87w+TzBU0EutXr+KdPpDAomnS8ArEgWXB8nP2qp7qjrigk4S2fcWZ7AqOP1+SDGn86r+B8SfAv9xA/9hIP5gHYP61w/+ifPQPw34NzTAzxJw/g3B+aeE8y8N5xsL51cSzq8BwD/rwf9ihvNrFM6vDYDvaSXcP1yZXxH1BCTinH+ogPUGOVGKly0P/WmMJTvNxc6ss8ISHJw5M1wS4hWVejzo//ckrqzojfODnZbp9zG6C8ozaHy4PIHZ0Qf8bT7wt1U5fSeIr7XWVMUkcmg/Ce1XInzzp+uiVKCCm7WHi2JdVj03XEpxDeVxj3NGe3v1c2j879FJvWcQvjnByHyb0Pl2+57jm//88wuPvH72qb+e+vhHb7/7fOnZz0+cenHPlvu3nfz7x+cvbN/8l9cuPHH3zT/4xQ/u/Oebt79+4Zl7olv33vn2d8+f/+kbN+64cub8X266/5HTn95RO/yf9jlGr45vAto3iNo3PBTh2yzcTjIjkMQjYfElWD9f5OJ/8/dHlQ5u2GIIhtJZjs9i42UjUUFRepJC8w/6i52dgrgg2GvoDYYrisKRnpz+s7c6l//sKYD1eRbm711YnwpYn2/A91mYPzPM3xZYn7n4gyWwPtM5/WNuV0nYIJwV8oZg/sIwf3rgZx8Bv8sAv+MBv4sSkL+QwBjbL+F+zEIjfn0Jn87vGgR+bQZ+fyEXvwLrqxL6Xwv8+s6r/kUe+L8moX1noX37YX29APtnDrQvgNbXS9W9FdHa6fdxJFffx8nrm5qjpWTPDKvtJcBfIvFq6N9NUB/0swW1sD79sD6pq/iqG/bfQ7D/QD+pegz2Xx/gqw046t8p6B+if7RB7e6a0dNfHpZEeko9EWg/P+f/g/nLwPwFYX0Vw/pK5fPbR4gyMpbLXyY3oPX/oYZv/TQF83sJ7R/DIMJnknhnz1V877/q/7sM5UORrj5JMD5YLPgwYSszlHu9v0Ljf0jJcTnE3R+bqjjRwH/fP1ag8T2YUagkI2h+IL8Y8o+TacNdKjZzt9GkyOUXDyjrc/nJm/Am5SC+EFudXkrrmUQCGa27tdg4vlq5iGlSmj5YjSuUmVdk2uG0CeHDoYTxNwSrRPhxQKnIAH5kcY1yAl8M92d3E8OJhP7XmqRBM4onlYq7jLn86PnKDK7Ekj8xZABfQnyG9reMhn1Nw+pfNTHsbxV45nVcze5pSGE/6R/QH1EMag8zZvbXmlH9cdMwe1hhyvzGZGC3mwaxXf3j+t8pVmjfYeaz5zVr9OdMq9l3FK9mluDYQEJLm5nMPWaDIqXTZK/Jn9aoMrn7PXTk7Bli9QSR0uL/wb8sXq8cw9WvZvtR/+7qT2NEfWoaH2um+pfC1djoFD42vmq82/Tb6iha/1f5D9z/JQFfT/OfzYj/hFQyvWcxj3Zh2+ldOjvDXyTTcJi1hHeBBPMmA/RC1WWd36Bhom0YbTH4kH2E+zseQWr0eHfCzpBKqS5k4DH7tX7WYnJnPiAkG6WsXB/X1uG7k7sYh0pc6dPxMq0YxGc+y9ZBfgwtJLzXFWm8yZ3MDlUVYVspzwQgvkXfwnxgkGLSTNVEXKHA7XQgSWLHzDt0vEQ95su06ILpdbpLmJZ2ta25Mhp1FkRz+bugX/fU1fXvhP39Lx2Wbs/5X1b8Za3MXD0I9r8E9lca9tfV+AvJE2A/KiF+CRNHdFP+8xf7I+5gRaDD3C22vcBM5yfk1q8Prd8XN16bfwLvf5gLIP86GJWg8RWL0fgaCU2qQTOMJ58yo/VruGdAY1Sux1uxLFqfKua6hOcJ2D/N0/nBh4bCtoghF9/4vfguNdrfw9PxXbMhvmsoH99FmSxpd17/nCbVBxNSFcLXBMLXaT+L8PWGq/4zJfjPKJar6VNKJJSCm2inSSsngfhLmqrjaY+rf0RTCirJ1fapxDMphYxuT3a3SxnS2JfJaOVKmaabjlTC+0l9Si3GVy5uyt3PV0+C/0xdPQj2hwX7EwL7OgzxBymyHuzT5FV9hifA/vYB/zNYRfyaq/f3Uhj/Woi/Xzp1PgZjApO/IM41CDyd7aXWAgZj7M3IPhsSw7hR2Y/2fzpt+PH8TAL4YWqeZhyXPAz2sQ/sP/inXx9sdQv6yko5oVRO/2KgELvmfTTQZ82i+VtAJ3WeY6h9olx8WEx1MJ+frDiISbz2MU9CmjhEuDNuFU/DV1rW+1QUfQkPqGcpvIsFCSrxDAP60lP5o2qv79r8UUGKYtoTnHQu/1iTyz8221opVqZxqS9rSJWM8S7ypI8xMeOuVBAX4pJNjgypjdPAv+D9B26yPUkultKjOjad0s4yJRUO1jqQz5/N41+KuTOZ0+/MLDfIlVyVk/F0+9My5X6lGhvEuQmI70P8pc1Cd+BS+zR+hfi3aoyv5usgPzT3vpDvzI+Pnvn2ubc/ufvN3f+75b673zq279Rdb9605XTt9Wc/O7Vn8y2P7Nry9J/fuuPDk/f8z43nt7791J9Pbn7m9o9u983e8rcj5/fuu/Wum2/cdvqtB0+fe+zhH3z0tDDGc/rntK2B+FmJNNzTM2swynFU5t8XzMfnOBowtD9bIL5D4d0P6+MpWB8ZWD8jUF4I+/cQrC8W4ocjoJ+mF8egvB3Ke6G8Acp/CeWvQHwxH/anjf5/41sItH8mkuh84cD7cmj/gX0fT6tp1R/7E2h/3q3DQb+CYBah82UYVykTT+Nw/vzYmMkk6ommlFqTfkWSReunw3SNfqcS2YdR9P01oN/ZhOwrsn+EENm5UIaF/ATikpbDlhq99fD+mosNqY8j/MFjWYxkXbogMryiFg57UedZVKSlk5akTyU17DPUJF0KMtmnc3rExHFk/3i6PZC/lGYZD8EnkP1MtGKW9oAe8g9VmtIB0LeXqIJp/9y8/5WXdisH6ChhY8RoGUuTPEO7QjDqZSyJED5G9Oiq2ywmX8qiDKYmkP2UpUQGP360zZsOZHYoLutCygkG/McWws326mRKaarU5P2VxNzR72IfU3FU6P8jrdhjdAB3Jwuxb1WcQXnbz7AivZd+knmC4DSHmj9hcSy0itUFWaXO1yQwo/E5d/z+M+8+fP19z1+/ZeeFr786/cJTW168+8wXL587/2Dv7f9+7tStfzx56wcnz9//9okb/3Df51sfeYy/XHjqlv+9cctvT716/satPz97/q4Lr93p/sO+98498/T/nLjr1HOHT+/cvfWzh7xrG8XgvxFM5vyTYL/nwP5XAH4H/5jP0ibyxKJFg05hV0l7R1HHsKyoJGzhz4hHQiVldo4Y+EPBaxkRKe3uso0j+3CUpsId4e7X9HPCBfYhipSg+uHiYv+wMK+P2ingREtoMRfwKS7pB/zXBPHLKbLUYukG/Pc5nC8iOD8ycL5woX2Lrp4vhdC+EvDvDfOjUF4J5SVQHsq/DzAj6LBAfN4wPya0V4gLYmkV30oVhCv/Kz/upXUZcW+ZoCKWaS/t6ayI14Wn3kdp7wryB0w4I5hE3x9KGxF+0GkYhI80jArhC9B/yeAQH4Vg4F2JdAuhR+fTAD6M8MMipSmRfEWjTLxiAPzDKvYmaCPh+BfCZxH6mvclbej/q9D4Iv6Y8CrVadyedEF+h85HwP3FUMZtcGXEhuPYcVqUy7/xpi0ZyK/16QUrAlqSDZhKmFqD2MJPyA1uYZHi6QTO+FQc80LdRcat8LQum+9OvmzkKo8z4D9dgA+ucidIXEr4jBdZt8qX2qkMDh8lJCv5wyIVpSwy5vIzsMtof/HDaax3k0JnHZnTQ2tk6QRhN0h0HVlX6jHV1/qQijdwQKOk07ohyL9Rzk7PwuJYrcqdsdNOlZrwmY7074L8IF2GwXUy1bGBw0Tev+tKbm87jvaXmHFhoVSr7lmmzoTKk7y2PfhR9Z1J97BPX0/cZzDucCvvyyz7wjpaa+S2TUL8lKlO0THUyvgJqSmkXcuQj4eyO7U7mTqVTM1ZtZZw767Ddo+4RneoZRi5UJ4IzIP7F1e/eCEqn1hLHMRrDfasKxFqmCQs23jj+3F/dpnauhp9f+4kLbg53vg+5hlPM71tCYJsFq1p17gfg/yitI7bIFtFEBF0uNhXuGgfIcV8qotJ9zzf6oDOwvD/qNKI3peb4i3rcc9K15pc/s9J0WrX/H1MlHCvqlsk0x4bk2NU28taz2rXOCon7iOqR11NoRV9pgdpvlJgRPaloR77G+ZMB1buU3+psynlq931vaG0jvwAtb/xMsNb0g75Qd2u1aGmWUZUP25p8CVdBMJ/LbK5/BW8xlasGH844YiTZs5cn+IbJtDsG3lSF1xbu/BbDWe9vDW++J/KDuujQyEdB3sB7q+a/GMWtTtRKN2o+ZIRFOX6v2bZqhCfM+dpnWjFzhZfMiBvYb4gRBs41sO6SQyVT6DxMaPxUX1O+wfz+Vc8nbRhKv9KZ5/YOenTcmoX6j6nyVjIuotwTeK6Mn2CFqkfxWfb8u+HzCT8DQQTxZ20ZSH6f23u/wepeaj/H0L/ZQ1kvXzENc836Kp/Nlk3V9byJVP9R6q+aG6MeYC2XaltIefKGRfhG0X1xz8gZLrLyZq5FH50iXf9Lvo+tfAnvuaadWj9WmD9fkHU/Juha9oITNboTexcRy6Vtu0jDtMP4Pb3/rN+NYTrqbomz2SaGVBxCL+5LhVVhmi32ZsSEVUa/no5QSnQ+nO71u3TMgQ5/wjlMoXoXQYMrW/hr/iU3ETV1xm9if3J+9Ti35MLUPsUaPz47lUfGGkNf8MRKWUqwu9M4wyrEj1PqtH6bF1IP0BQ9xQuvaT9kq1eTDVKJN51++ke1ZcG2yL5SovYt3I/0ct8oZfMk64QzaZa3sdiyQcytgbpEnKOfMV+vId+QO+cQOtjiXRFbn03222uEd9c6bx9+s9ZZ03vNjQ+dLH8eLW0X76UalyP/r+f9rdI54eWltIPaGzNFjy4vFaB1sdy+ZJ4/Sr88WTA7ZsnXbJQ9w3zqCo0GsCfY9O6ss9rR0S1FFan8w66NvhMl03++Yi/yBB/UT3Izjk4ZR+EyD74XEPofGvyqdH6q/OtCLQ2Ml/ruFLpum+I+Jy6JvT9sA//jngC540EiBcyuC7FGHRcVL+uLXJfEe7JeFJOdTWB7N91rsU7mAcM1JXaZqmYvwrtr+r1+MNp8itfFeJPUrS/lKHxVt2DbNWD3PrLTM1sf3Wd0Gt3Ub6Wy0b/bPnKQH0R7V4aXFlYXFJb4q+pIbA6QUeHK7Fdepxw6D5nHdvQ/JS1MOUEVyMlL+pJQZ1yNx3o9DVJ5/jmyDfMxXyrXTVofSo2amakDusJLFceROtbuVCXi99cDvGbYpw7UzqM+ldXNxf1r8NX9R0RqpQ7nsRCMdcc94bVum81X4/w1HHiilaTXDZE1iL7i6P1rYP4wBRTrk5q+KOfGyyKa/PXcvO/8XvzPzc3/6y9WWrcbuTF0P5IBhQtTIHhGMbQojY0P4qn0+40mn9zaGacQfhk2NX2LJo/meLYEE+xB/+bhlK70tv1nJk7iM8zCCb3B3juITQ/i6QjV+eny4cdU4SKDtOo/T2utl3DhTOOab+j/6WKtxXpDk6tX0wnGnlA65bjOlcqratbyu/nraWU6PsFyL4aJolegteE6s93mX/BfGfimjj9n6Dxw/CRZIDx47PbrC/wED5alrYjfFVrOj7nOJ1A++socSfqf0ilJWTrl0r4gE9rAJ/C+5TVtQiffqYQRit749Wdtp+j83NZsXZ89dh8jWz5dPwCvE8monqLo8FoqR7e93XKUq0LsvViCZ/j8zl7qzpnWModEbtnA/jfJhD+tMvN3/evDlCxnqq43FNaTgot4p6eAFXklXSLe61DXi7g6zWAryF/XDIO7dsE7dsP7fsb4OdvAD+3AL5QAL5gAD9DfIs5W9HtsRi7OqtB/7iiJ+xdCOXIfuJ11lT+/UG3dVZ3mBOpHK4SRuyyooKurnZepF0kquwqKxP/G+GzZROidllW1GftLuaHKwvDXaPgX+ri3w/87xudDjuAkdUrUfuKdNf4J1iEL1QIX2V1WMo5kZh6P4OfyMVXyA3OO8H/tffq+xa5+0Ux9O9lVH67CeJv0Pdrl/cEg5ze6fgWKu9fNJDhUJlA0h0tK+KEPKX+kp5iZz6/w7sE+rcI+r8U+MOPgD8sB/+fS6mibUtTIsTtQwbS113C6SqM60heT5eQ39srEIJ/m+cxWkuCHRW1DbJQsRjBSm5RMcQHFBdZHQXOWbV1WX81JxoYFVrD3UUzo5VZkjOjW5Z7f1U4M9hTHA/J4P3vilq1rJsjjJWU1aas4S4Hwp+0kGOzdO9QfrihgCTBP9zN4WVm9hUXm13eOCfQX+Ys7e0fC4V7zTPj0+9jevQxSdks21NJ9fgEPk7l338X8PPzMwjzQ5HapiFZV2E7aQ6U9ka6i2pknOqObLjU4vXl9Xe6e4X59mUL4uB/z1SKgtGKHqmsu0DgB33MUtbtKO1sj5W5+aBv2Vdc0pXXtxGbOsTdpd290pGN89kV1ji3okI8gfh/nWj0+/rftLCos9PaXh7r5cb47oqCaIknJeyLlVQW1KlJnqSvYBkj6hIEyypcXVfjI76vHz4r1F0l1KzOkKGgR6TvDttm5fUdu0pI9cLJ7Lg91hErazP38rxkRlDa1TVaEvb2Spag+a116Kf8N9aSWIDX00NbuZEwv7Crs7OyKE5B/BDPrrL3tMssXba1aP3ZB6h8fGpxnf3/WP+xsJXjcPSJheDfL5BMMHTSZqi018iqM7A+6av64HzYf72w/26E/fcF7D877L9eWH9NV/M3gT85opCflOjoLfOGvTiUK6Hc+H19IDP4p9vBPtwH/l0h+KcTwE9GwD/9FfhnV4F/aUPQR4rDUWvu/Qpu/n3fwWB1T/FoXBwLew+Nx52jpT0ZqlQQAP+JOZ57H7UD/FOlcH+yFvzbj4B/+yHYv0bgL3OBvzwL/OZF4Ddt4J+G/Dzybtifu8H/bgD/+03Q/3nQ/xji/3x7Xn8oAPu3C8anFspvhfIo6KcnHOKymXHvaShfSU/HZ+X4/ySM3wwYv89h/H4K9kEM/F8A4zML3gehwwUU3yBImbtmOOxmcqYtDuP7L/APQPy3pBjGrwLGj4f290i6q4fPku5Dozn/NcT3uiXd4B8Pwfj9GfJjh0D/WBJ0WsRy6L8A9d88EAkHjOB/bNPGuMtHuyQzHLshvrltZlm4hDJUVkUi//1+x5KO8mC0V/9/3Y/5TUnG+Ryqr8QG0jgN8Q/A39py+p4KRv+6KZ0+YVaPgP/6oukeIrU+oTiguVv76oAJle9VqEyJ0VfMKvCf5/U3R1LGnD7cOJ5/f31hzn+8Cleici2bzz+sz/mfNygxxqhqSIziesQPtRibNv1YxaTu0RtU9yhVK3FWyb/qn96IyofThh8vyQwmmnP+zdWIXzai8oWJ4ac1r65SgP9kIT0/1/6C1ELNMJ5F7V+iVKP/z1emcNMJNtG2B0tmEi1EU0qrGcXTSs1deqXpHhZXvJ6ZKtcnhz7QH22iF2hWovp61H5DehJH/PSEScmml7IqxE9VhOZuJZ4+waqNzHT9/3x/IqHJ+e8nP0pyFaBfeu+owqgcwRtQ/7Q0xiTuURCKu00a5hVWhe01PrWQZvFm1P+2VwYyyo+we4dYjHD8DtbHoN1R5rLEePpYuJfT09dRmL5G/1CD9i82rX/YpMi/Xx+n2+nLKZc5kMxoXdrFg4sXD67aqNav23Bda2KC1WsS+Mqp9xMtBlT/jVs+O/fC4zd9++DpF4/KCeclON+u/L/n20tLpvSHp/Q32FC4wtJZkRW4bdx5+JR+WRPhfA3yj1Kl8D7CrM8Ugh7xaEeJr0M/db6CPtZ81H5y2n/1LqtQgX/pIu1W+jIBooWpa5NhnAzip7oize7ELoR/pPqQCfFXxK92Gtzpo4TUzAf/vnI9/nTCPURqUHkbKheE0oi/jopNoH+F6suKVKh8BPKHQgpUzg2NoPKBXPkoKi/Jlffn6uNrmYDOwThWutlCfKOmNIm+r1+PO5cEUohfaUiTnA0YFtLkSHD1B4RUx6FrmiH/qGMspx+xkGwD/Qdb3TK1e1WtWWaUDspxinhfkePHOsT/zNK6vP5FkKlbaNSUMmLCr5AovHRgTYi4bLAc5I0ifj1hMVkTiF/P44xdNLgPFDWg9k2QGtD34o0FzPtY8hfBUfT/Rmka8VuN4G77gGs0REgXh5asZ1qwJjrQAPy2zMQfkqvi5rr5HRNLmQWqMSLUcJF1N5BJlzG4MacfskJkphB/pX61bAT1f4lPKR/br7H8zNXyLF3XotKUDjOEXz378vT7lb2EmHFpQte16qiXao3H247TNUtz/V+f63896v9kTj+jIbiu1nDceJyWEN5jddqOyVamV80hfEsuslEc8cN6NL5NOf5qyNXfBPk17xP79BcZ9xLfdQ8QjnFxm0CN+P38OPa+5ukE4q/zpabtqsNQPvAA4dxQi/9dK14P47sG704gfLyQj/nnyemduhDlanMP58aflWuotjrCOxxw+kzSZt9i+YSrPpR2tQRTtY1cDYcWGSnF+1iHzbXCt1ja6JsnyrpMneuAn4r1Mu2xAV7NHvyo/s5kIJW7f2oSJRB/sgZ0z2bqFggU0vUXdY/iAot/GPE7tXQeGr+UiwjZDhiB38oW597/VP8NWyaC8WOIAZ18LYs5aVdThvHWLiYXwP4ag/01DvvrOjhfJuB8eW06//C/8fF+OF8UcD8M+n3iZ+H8eBLZ93MJsO9BAvS3V2jG115Xj21cPKDRmIeyk00rVqhHxlckGtZt2KitX5/d1LhicExpGpifEpMIH4+sXMM0rV6ymm6YGB3E6+lsSuwA/yG8byj+Bt4/bKQqrWWhYaGnO1qgt/SMcn3ReEF5L4e8Lff+JJyvD8H5OAD3Kz9E7Y/ortG/g/sjdD4qkjShPAHvf4ZoVud0f/99uO/H5zl2o/oDCfT/11B9Gae/OCoOW/hT+tNdvGg8OrMgU0pyuMj+9FTE7JWO/+gDFcd43T3BgljXOEbTzROD48pkPn62zBOKVeu7e3qRwXKKbQLx8pLCypBIePX99ry+uZTPiXsDhfD+uosUeQIdBQEq/z6hkP8f/WKqLFJuq+BVsB5LOdVf0SO215K+YDvoD/fNiPtl1KwuSbCi1NzTRZZaOyukVCxSFbd1FXKkwqn8XX9NlOuyegLO4kFngaMwHOML+Tn942BJj69HsExWRHJ5pfHlwYpBqjorsPXM7At7/VSnI8zri9k55ZnSwAxJgS9DcqxzrJ2ScMesuCM2S++0hIVZr4zieDmRWVxJPJKd4YwWBNz8gDiO2lcdjI4KHYFOzrXfb58VLopFOEJ+pDL/PkvGXlDp48wmr76/5Cgge8qE/B6OOHi1ffnx7+out3SLpFTYUxQIRgo52R4bJ+gdLC8UUfB+fDyenYHa1yMKx7ix0dz7rTWkM4agVWfZAIcniouEU/pUw3yrJean+oJugzPQXjYzrLfJhQXCaX6K8AnbHXdRqdUbleODIn9cFJ3d4+oVFkpjsmJ/mXtWbY9wlqjXXSbNxTe7/3/L5YMrLN5AWWFGZC8untkd7TPGO2cFSjtD4jkQP7SEXd08vj5JVXaJbYaKWd6YQzJIVdrLQ7P9KZG/uiZLhsXOgENgisnD5bxZfbFsZYgqjJeNhso8o53V3KDDNon2p2+FVwv7dwj2bwr27yzYv22wfy8D/rt0FV8ngd/dCPu3DeIjID/LuxTw862AnxsB/10AfYcNef3QW3PvRwF/GhCXiT274P7294CvOeD/h/eVxGOwf3th/8YBH3dA/MZDcD8ph/iNCOLXoylYvwZJRUrpKHXEzGQhaanIx0+EY0VZeN8qnQhxDdWetIob6K9snzmIF8ZHPSIOOUIUBjOUpCzKGcqIBZ5T0L4r8P9/AD5/E/5/Ev6fRfbrRbYzZjfGOgxqUdgiCdr9+fys3PvqErjftv1HvwDix4KcskBP54wpfe6estJiX6fDptAmnW+j8VEQysRqhN/6c/EXgO8yCZXBfLcJDUHy940pY+7+TKMcw/EcPmzMjCF8Z0y1atZoBhB+0yuX5u8fXlmoHUhX04uYjQh/tqSUCN+ySsXQdHzGdPzDtP77tfG/+fiG/yp/g/n9SqkZ4TtDYhxXqAdPTMWPZMRndDps71K7kCwJD0D+s5/aQJIVPZFQic4jIHW6GcZr9GVh/Q2i8WFBX/ZjE6Ph03LwTyq9aYh/ECpDufyMENund64uNGk0/Ewe/zjXBNL71JLGkFkF709nAsoWpkrGhfxgE6UsUscSDn/IoCV8Bt5oK2ZxB1S7kkd1UoU0w8PqsVoV+n4/+n6Bz3Qk4VLvYNxKd/LlNplZmjqsJ011xo5RK2PfxDf4FBfpgKqHcUwEB5U6AS4dvqgjzXXY7kxgtQ+XmjsNh9mdxh20W+Ue4XdxF2hpkZJSFC3oGN2f8Zv5Kh9+kXYeIFmXPpiVEBJSys4yUFjRfLg/8bdJzaHWUtbxlC8ZaHH3Fy69TrORXk2Q+go9wkdjvjbOksW6CaYe8w9G29wDtW0ytZS9aCD1dQZon+0xztK0Tp7c2eYbcml3snUamY4zKCS8O4pa6aQl4zOh81sjyiD8Monwy4nCtkta2Sp5c1xVjFFvBTalVah/Bt51aHz+EGhD+KdZpuCMluq9l+v03rE+2vY+pynUJB98EgutdzX/gimc/0/1jIyKiBhy5QnbyavlZK5cmi+nFEd1dyb7WX+9bLFvcdWuQLNvDfl20FlgmJo/SQ6/jnvqOeaQvKK/W+8fx3XusVqdTIvmRx7XFxk6nAfSSpWa6FUzjMVwH+NfDPcr9LR+nMo74Er7CE7d46aalctM5CqXzr2yViObfzmRIEawCrk6aRm8Ty1fqdTVrMAxzyaLwp0sFHCJy2xNfbxZ0gb+8/vU4gJfA/jHe1lLs3vFB8RMTE2zRAIrMngmPCvSKo7Gv6RuhR1H+MW0aw1aP0v5a460UPVFBvDP+9TH2kJ80VigllzxpC647Du9TCedlLf0KV/WejZYhnzNHGGo7WLmAdxeGhBmGKlK0AD6mJQGjV824H1BK5vL6mZZFBiZfYDw/W9aJ21g6Dn1FH5U1bEsEPbN+454wiAfz+f/BNcVlsgavqP/pe/TYLgyOcyEjLKly5aomIDev9KzxEaLYH2yPE1OH+7XrkyvUkqEFq0Ffd21uf4X5/o/z2+qU3Z0BLIIv84nFfJkoCg0uF//PCNWf6q5zHyu8/77j3gwEQiGit5X9Op5HQHu07R7/nJnYRuXW7yxvIEUpnCv1cX45kvbfG08i6UxFHM1ovnnyBZK03LtQc0/8Y4e13q0P5r3GXL3A30HzIAPy4wJWqR9FJ/9+2vxtcXsW7+feJDlb+CWStPfEPGSbzUdfa7xx1ScIl/jN0xg1RPJwFrryq913ImvWd56ajGa/5H9cD++wTcf4c+mfZkndY8kCycFLd/RvOoHtAUKjwWNv4YzGdJezLjF5IhrzrP0F8j4S6kjc+MrilZ47fsZVH/tE/rDLI751gfGd62rlR7TXmYSRHxSgdcutpYi+3R22wM3f72n8u0LD1945Jtbtl05c+W9vP7vnJuf++Mb//OPC7u33Hhh+8kffHbq6fu2fP3I1ueO+m64+8XTJ39y4e2Xrj94++b7f3Hq1CW59I3z2y888dUNT3105ss/3fzkttOf7T9z/tGtj9+zZdcdFz755uRtL27Ze9fpbz+WvKTI+59z+ZeSLxC/lRkW8A0IH57dvfP6jx+7fvNbW9997MKj/3JugvNVDf7Z9FzrYnQ+nr3xuxsefP3MiT/d/NSj+ft/91sPHju/9fEb33309OO/zeFrPjsm2IjO3zMXtp0+9eDWxw9teeK5C5/+8eS2+zc/+uKp82/m4zsmr+rHrULfX+NiV1gFkH964dcX/v3glk8/OHfDwzc9f/Dm+4+/85Mj5x+6/oa/P3j6+Y9ObnmoTj8VHx0I2FcyS9ZR+fclykujcbLM4RGpFm3UriQlom6/lV/M50QE3SS3jr92aYPQ6S7p7Ozuisua1taTV98375pV1Tw+OYZTPLmq0Qr+81MnfrT57PsX7jlw05Ybb37kk3du//LkjmduPLf3zM/fP7n5xes/e3zLE3/f8tm9orO/33L6j89vufWnWx588uznfzn1qxeuv/WBM1+/dv7l268/fzwli8zoDpktknCMw8a7ZkF+d6EjFqosWs7pMkhEEbfjS/AfSQCfLAZ88r9ofPnauZIWdH5ff/7BC4+8fuq2t86+suPNE0+++ZOTt1+/Q5ZZ4atD+GXz/3y95d87z2174/RtV05e2PnGvx5766kPttx0y5vn7zj1wp9ufv7wmS8/vvnJvae/eoH6OeCbzHUUvO9z9p7jp3758NbX79ryxCfXv7vtwv1Hz/zsz4796P/XUWpUfvqhN/LxF5R9JeIPstPf3Xv+xOObb3l+8433n/3Tn07vvWPLgYdE/JUIX/k2H//ZuW1/PfvYByfPHXzz6FaZbwLhL/6WR3506p1Pzl14bvMfvjj31IG8vhbfuRrhJ/eWn/zwjQsPn3z0o5s2Xzl19ILLMwD4Zi74Hz9C+EY4gM7na8dPoHe4AL9JIT89gcZHrrAtAHzXNJ0fOzIapXhByWhHSYSKiF6a226rCEaCJXMynGVF5ZzO3qAxQFX3SDwvKvtXrcXrhXl9XpEkH7/NQvwsaWXracjv8wvFJWU1ZDfCzzN7OWU9LutVfYSKCpe03VtQ0Bd0cKOZkKgqXN7B6Q4IZZ2CQGdpsDocM3Rb3BXlwcAoz1JQSuHj9XQDWRwrKLOvWjdPKCY7+H57gaMgYumuCtZaNiJ+dvq9r29+YNeZYz888972zT84sPmZB89/8+Xpg7t9jluBn36kpWmXfpO1B60PNP9vf/Lzt/e9vfXpZ9767voL979580/eOH/yiPvcs3fd+uwtN2x9+53tr597ZtuN5392+v4/n7zh0S1/+PnWx/7X79J79wB/3Iv4Y+0kMzCvKTHO/74+dE6/zEml1yikef2yCns4ai0XxmyzyZICrydcECjtFarWrtFM8gO8rjLrQrQ/T7/8h3MX7rj+7Mfnnv7pDb946OaDL79555snb7i7R/I8xGc+A+3/Gu3vGfr/0vfPTOv796P5+wThLwp/X0snHe7p91flGttx4N93ovJXmeoOsA+zwT7UAL5/Ha2vc0lM54D7IyNd6+52k7N8/eXhMqqjmL06P5KSkopoQVQSXV4QDldGOjilhapr4gM1CB8n0Pe1EB9ozMcHKn/JZBVePyrfyzjKcvZrKGOo8rx6Nb8xp3/gBXz9Y1R/m5HR7AZ9LGPa+SvEv6l56Ptn33r7zI9uvf6HPzl17s9n/3T/1u/+en7/j+xi2wCqX5K3v0IrvO8uuuXCg9TJ3/36pvt/KfSoqTh8f0FOPwi+vw/8z1Xgf/8c4VuxGo2fg3/yny/wfWthf9225VcnPzt39ttHbnvj4tmbHr3x0btu+tH5N35w2G9G/ZMJ3zmx6+wL/7CNNZQvWyZcisavTm5968OfVV1//aGzn/z1zJM/3XLwHtFy1+w6DJWDPpBUcsP2i29t/v2M2dVCwMdu4blb/nnDg79D7bv5mYcqxaA/RF3Y9k/pWzuPbnny+VJBKer/ctnm2w5v+fqxCy98OOv0Wzdtff4p1L/z33yatw+V1793y+bb/7H582fP3vDRyV/+89T7u946f/dbP/3i9h/f8OYffipw/gX8E+uRffGZ0Ph7a2+/YeeWp26eeYg2y1WK9fzmvP6H5Kr+h+GqvmUS+FvkKn+7GebnIozf+zB+JyA+qx/4jxTG70OIzx+F860xp48G9xcn4f5iLfz/nxD/3qplVywd0yeumwf+maYEYhYb125oaN00l1wL/pObYP+8BP6XF8H/0grxrW2xMkFMYhM3Jgid3OKcgPV5+7T+1scjPX0d4iK/3lHEr47B+g3C/cUElBcBP50J9qsC+vc7VP9KImgTjCTaI4be3nRKFLEMjkB+rSCsJ3nV1TuJqfuPQazIb8tUlTi6oP/V0P9S6P8hyM8YjBX0dpb1dmaqS+2OtyC+jC2ItalLy8Ssx/Fp7v4jHgd+bxgmI6TB6glw+Y/C+PwV9DM1wXKL5Fl0vn9srCiPBu1mzbLufHzladDH8sP91MqgTRhyZ8oLotZgcbbHyZnZzrFwSy+ZdBgf/OPDqvmMUbkwkcSblEO4KRd/1sT0I36oSJly/A9j9K+ZUuzvFFgixx8baDMzkVhkIJJQDvzRqAT+Ce+TLUTlSrqeydyj0Dfm4tsGlGbED/EEqyGUI69oNRC/ibFT74e9klatUTYymLIa7ideG9Lm4jtpjB2Z5pevJ181jhpz/NKM6i+E+pBfcG+jyfih9vdp1bhScxemxhF/XYT4KY6xiWv5qRUf/Z1xGN7fGdWYVatwNbYJ4kvv7UcEpDFlQO2T/BWN31X/O/jn5qHze2Da/96e978bLtJuLcm4dM/SaZ0Ak9JHDHH8ZWw3s4sJKSYJyyIei/BzokPnSvCAP0K5FpXTOf982yqdcZ7b6Om3tLnZWuUlTDYsN+1V1xnvTED8Q+liZFeG/Wvy8U9ihQyTDl0EfqnYnXEN7VCDfxT0pT3jFvVU/RG5GdVvg/qkTqok8YusY8g34lIEk6APLHIwhN84OzKNn61zxIxLu31gl841Uqs/TkjpwzqSKNJ20F2D8UYZHm+uySJ8y+5c7B45Skh10kFk3w3va9EfWbJBagi1of5pLRcDhhz/M0hZxG/aXsa8wy42hE8SvoXyVS5FKNOnDw5/1iZt4tA1i3ux2oaOpHssZJAt8RFlYzsbQyO7COrVQtXxBvCPU3MR/5h0MduVHMy35Kp+2aTYIFhybONhIq5bA/xvo2/RlzrbEhHragH9a+qJQsMlrXSy1PQoLmjsSAcGY4tLGv1zExCfknGBf38Rmp+NPE09xJetskz4lJzmkA7h+6XkJlcjwveEZL50zZEl8Xmgb+Vc7ZnPb/I1yidQ+yZQ+/bULjpez1lzWOf+XZH56cR+2tfAWViOVnKUwGhLk3VDgeHvWnkW9L9T+JQ+WqNv0UW6X+Nf427tSdQ2/F0rXstrIRvrFDn98HrpYl+rfMzV6MsEWlqYWgO3fpJG/HbuetybDkwuVHEaHsdVjHzJdeLN0/l9tc3ZjWvnT6SttdREYvFq/vfjE3L5dR4hNrlCPU7ZHTwf5YnZwhGywharolqzDSs15NykLF4kWtnEzh80cHqCGYGkpzPs13y4oHBmRG8T83jxTLTQ1l9Jcnr/tKkH9J+yPRZu1DM4s8KbKw9GOozBTqnjFNjvR+F8Vw2OWgJdHFNXSbSsusrzT7Df/4T3E0YjltgscdbSxwlbPhst7ZK0G4qLwwGLTtcus0iCw4WhvmipeIXjXbDfDmS/fx/SNq3auEYrkltXaDdtIr//vkVm6v03pz21qSlTz+/h8GZ5HwT7PBC0UuX8mHiAxzeHPfLUZwtnWstjndPvY9i4tkCgyhRG9q3Y7Om0852LwX7/Gez3ArDfgD+V+feRvgT/3/1wvjwM/Ak/NDL1fgbkZ36yKp//HgxxsiJbvDBu/3A4n//Q1SkoW875JFvE6czk9U1y/rmPsan4E2+guixW9clYvj5FFdjKw5QhFF5eUOK80ih0xOzlRcv0HTXCYk/MaewiDzVM5YdWc2zxcHU25heVeTnt5pXfj48ejc8ogPebnOmlAn6srN0gFIbCnTM6Y2ZSEiYr4s6MSNA1sngKH069v949w9/Pc5R3OgyuDr+BzPevq6DcVxA1FFbHu/oK7e2zsqWdIUmczAgKLfKleX2Bb2F8YjA+P4Dx+SGcv/+C81cJ8dv3wvkL7x94WbifoOB+IgH3E3VwP/FDwI9/AH64Fc7Pr2D8/wr3/+9d9e++C/k5CY6n1ykohe/bp/N7HL+F9fEziE94H74/CN/Pwve7QF+y2bEN8gdHIP4a9Bk7O0vy53cnzO9jED/wOODPxwh4n7S6e1keH7+ktJgV4Uh1eSQW5jjugfyGPmjf199v3ylo38NQ/yHQl3wSzs8ngb88Avzl2xy/U9JYWiUqi+sj4pmH9G7HAXV3pV0ft4q/A3xRjU/r0+TeXxJDfFl9RU9BaCr+mwxHuNlotEwck9tGU0nCcxPqXz6+O3e+vaJSJU4YNMl7TGkVk4DzK3d/PqyG+3EtOt/MoC/5CptU00ZmBPLfPoTzlX1dn1Gh8xudb8ox3AD+XXhfFNVXpdSvMsakUoO+35pI4wrwz55IJgx3zWfXJjDCnNRphvGVOf/t3MTAK/PVCVSeTbTRKiaT/r5/tw3VV6H6bUckzxL/9b6LHo1/27S+7NcI33yqhfw0UvMXrCPdyhxQcAwh00fMC5iP7tUFE4VtM7Sc9Cd6SvNH/KnEPrZTyVGEFB8N78f3MW69mynWcY2cVKEurr6i9ab3MfvVH+o79RDf+fTQfgKdPyaZppLm4XFVAdaROcD6FB8S+w1FbEAfyj6lfJb+gJCqpXQZEW8qUgcSgdQLCqk+1HCJ2Yfvz7TrgxOFrR/oLzMXdaQK8t8CYzvU3ytfWbhkqlwN8eXyjddZvgJ8+hXCB0XX6D8j+4+GBPU/q6J19sWo/xYFH49rQd/4DubjxCGCTf9/lX17fFNVtn9S0veDpG1e0JbyBqFwkpyTnCRtmqR5p2mTtkmbJmnzOCehTUtAeQgWkTcMRWUELfjT+Y2vGZEZQFEQEEFbuYPee6EV1LGAOiOIAj5+jqMzv9G710lTWof53Hv/Xp+999p7r73XY6+9viX4wltLakwCr8GfURLX6tOdTl9alZXjYjuCthtQfz1iCsP9qoT7ldAPjP6PsYZS9hu6dNl+r52ToU3ih7EpJW2MovHnEeLJRk5jXo2JbUsk9YPAV6tjjcmf6zRykvWxknTbPZOtZlaxk6Ubqe9h1Oe3ZLpcE8N4xwJ5sFRyUtbk5OV42MZMasz+KtH+lqP5zYb9fYxC+4tfo0pIBz6FgP8BBtkN5UtkCaWVH6W1qvbQx2olLo6WqI0KHt8e2hWE+o6G6tLoEUJCnVC+QkuUPMkNuhTZJ1MUjy1yUjtlxYVNyL7SVtZGd2Gu6Jskn8MKfqIMSG5J4hFryKEQVdb0lNK7JDuRfaSLlFVeVdDUdfIYjkH9yMROKalyVJeELdKaSotyd/BNJSXnhkurjxFh4vXQEToi5WIG6emO3VBfW6JbJFAMkANUuqYC2QevB+H9mqWZoVxBW6tq6V3yXfQUmVjevywdMyL+GoMVdC0G+bmlMadiLmVRuxL3KZP5EUZ5pnJBCNk/1Wvg/SBegSP9Lmf0+yxGv89k9PvKuVIkX3fdC/kBSyqU1ljG/B65KJq+oBxH/a9g+i9n+q9A/WPWBGchk38714hPqbLHLV212LUqpj7zLJi/lX6jsl+O6JrAAnS+ohUQn5z+O9UnFIEfpSwS16ovSbncsWqhbTXoZz34V+Ugv+Xgn+6E+/0fKf2X9hWy/+ffH5raSYnnzAve90/xBXODMMAy6wxc1Rh8W6Y+cn56sc1R0NySL2JZXHnetjaOyuwIZPjcYjbSX27AH+O3IP1S5E5zl0F9dk22z1xjAfxAoZXfKLDwvQ0upB8LGvIMZQJXTYOG7zI1slsd4sTt+svJ+s01/nRLjauAF3eb0gPupswCv6pGyOIX38YP5xb7nUIW0v/5zf5OYWa7lakv1uTmNbRNjGe49Pm1LreD5zY1s7XRVmGGX9iRnl3gaNbWN7YVmXnJ/DoNx+sz5mj5ZtbElrwWeL91+IRNPodmDL4tM382u0XVyp5QWGx18Tp9nEaVqz3X0eZwpJkCTWPzO4vdLk6nJ8/HbTMJY622glyRy1IcsDsFVS28jmyhw2d3peka0uMcAzm2/xIjfu/cyFSFjvu/eL/l6deC/vzHaH3R36fqixZ9D/rzC9DPH4F+vgH66SXQT1+C/vsS9CeDj/0R6L+To/m189D5162C/Ikzh//45sELDw4l8d35Qz98ceWrk+u//1FUW4b61w298VRZ72Nbrpy+dP7xbeueOTJ47cPhT3Zs+fPNK317tz/+yJbX3nj/kc8+PHi4/h2Csf9KVnBxkD8Z2BfDoP8Bn/bzqswck1d/CvQv4FsJfjOK39oK92MXg59k5AUCiaCwNq/BmbRP0qD9ObAfAH/XdBnmz0L2w5SE4DLQXUCfCf6xiiM03ZLltlvN2WBfZMH6pEP9TgXf6xG8C+tzBa3PEyrzS0Bn8h9/Mz5/WYr0e2M1QfPsiP9wOIXv3Y38X83I++cy5P8mkP7W0F1BDUky/+O7kf+MS2YGu5n/XWpZJFKN9HOYqY8kk8eICKJrbutXPEln/Ndtkn9fOg/6ny+RBaewTe4JAZuOBfVPIb/V7hPoGgonef2BTK/f1m5P4W+Pz1+su3tKc0mgsZbPio+Pn/rGxPfIYrS+UN97tpKgeJODcoobaiB3hVzyUwRf4Yn2yfpCItyCCWc7JOzQMUpB3QgdV8YirxGf4CKJKWSQHAtSuFYO+LRU2Eg9GmEht7QhvIs4XdmP1UdrlY74gGY3ukgroH5T1BYVtDL1jST34vxMT+S4xBguwE0rRMQeTX+YpPVBVgfUV3lN8Qkux0yLDkqMQZ7SImVVlRP8ikDsZZrbZVA7O+YreGG2+hDtrzxMK+VWXCwxSgTZaPzEQEip1Mas0k8ICjtGGbBA12dEDOOqDuFQP8oW7l+8R7koZMFKKsUK3UcGyZ5OBr8YM1ZdjZD0SUoU8uOvhqMK/gyx1NM5FbN+wZYdkYirjLKBe/po+2JR3I9UkelAqYat8NK23cbgQPVujMbR+nQn18egdC4+KpfivGmmZQbVsaC4aj52jXDMEscDy1+Org3555RHLJWl1TclnhVTsQZa+KlF0l/dR1BhR6hhpajTX3Eg5pp2nWBPm0c9jQVWfi85guzvQ5C/uOg4rQih+XfOV5SQiL97pmKGLrHcqVqDIz47DHF9tyjin1oRMQ6mV1yV1Mdry40R9gKoX2OYybvbGGtYuyZkOeukoD6VWP7LJYbKvqBolmUacsSJv0TsHTa6v3OPkojoZOlzrkpk1EH4f658VaLAa7xiqnGJrUOUMMyxdLkU6Ti74vUwU79qmmW6SGmc3R8yLK3t5q44SFoWW5WlM67iMiqBNS8M4do53xLaGipsWKKgysr9Guca74K8BaJZ7WuNs8nQlIUWef8cIynu6os9RxdPN8yx3mctv06Iq+o6Ef+rrhKW6msSkhAvO0TZOq5FlMpIxDrtT0QIM/X8VqLlUrh1Lo3XzOStDay2rfkp9Kwa6ifdkKPDRIWwQEhUYZsrmnocZ4cNPZY1RcF6pTNimTZlaqHECPw5Rvjbifhbi/i730DuWrRLEcR5C+rC/sKAns09iF1TGBbwosb7bfej/S95NfSKPIjfxOoof9XhoBSXYlVQX+pe81qaYoWM022r9HIBjtb3PoN8j64f3y0Rz4H6TX1Qf2qtodppdHHTuR9jh2J+zNhzVW5dwJIFSrO1TSb7fXTIMMsZsU7/RC7i1tF9ioCFXWapROurFncbl9uWcZei/bdA/SheeV3UX1Ieukpoy0XygKgn/Dxl2sSqP05q77FWfUKIK5D8SQN1PfgzkuIPAtKrEcdyW5Tlmqp8Jmx8I112VVK3nJGP9ArsW8L6Nps+RjHyk1sReZbgSdmKQzRDz0R0uVUNdNTejdqHjNPS2Uj+GqZiz0f4C53piP/5SL5WvkyLliP5uN86t3QOOxOtDxbwfiSvwH7CA3nfh4xG5+qBiFGk7bQKcrlIPk3o/EXZBZZCEd6u6A+SNJK/qD/NEmmUn5Iz9aP4TP0oNuo/O9vQpLeb0fpMOhG28ktJsaYubih3LEXyX7IGP4Yz8l2/JsTMn0gX92BPRS3eQD1aHyVan0y0PjETnxU9Tlrutk5H51uF7o/8w13It8NyNFUEb/XzsfqPx+Kbo/HVML4WZ+XA/XaIfplShHgZq3lXQL8UofvTKEf6Z6j38vltz/Seem3HzhuGOT32ryH+/BXojx/BP4T/9bospH/PnXmx983rlx7cvP0XB7f89m8Xj30zvPXwxn/bf37/O6Lzewe3n/zs3Dfvbt7/3OAXz15+8+COx44J7gP9swT0dw/o71Ogvz+C+PKf0f1tzED6+8p3A4Nbnu19fP25z78dfv/ZdTdvXHr+xMaHdm48fva9594Z3vF/t555XvT+2xsuP/HZlue/uPKn69se29/43Z9426rtU/MEf4X+nwL+AL98pP0rj2/89a82bfv/Hzz8w/DWI1vPHBx+7Mb5zU+uv/bkA0e+WHfxUXFdcHJ6pq4T6Y/SkvPvfTS8/ci6Q0cvvf1HMRfwC0rhffXy1Vc3PLd+w/4DV67/5dKjh3sfOWSeDvYJ4IfZ7wL9ehXW5yMY/2/J/0OM/XKAwYfmqWF9y9H62riqZPzFpuNwHaQA8C38rdymmqzWNC6DH6Gb4A742c4JfAk3D+yLd5V3wOeA+pIaqO8K+JlJ/yVG68vA/rlI4chrMfXD+roVaH35//x+9VYY2ReXkH1QO13AaXeahU7jyPuw3lPMc5nLWlo8Hk6BzSdil3B8tdqMTgYfs7vY4XdP4DS336a3ZLHcPmdabllwcdxlSHf5WyZku+JGodNh/hj1746O8b/g/QvJDx3DMdq0DeIXVvkIfyXEcYLzuSyDeX9L4htNhveFPrT+byJTwtwH/D9Mk1jLQjT/VXfFemIS/ZJ54XmGn8WX4H+PmVurGYd/guzvZP3l4hptXqavpSSV39ohLM1r9bTUu9qKXAx+bImbU+QyuSa4+IC/pGrNFRYWO1tzdG2sFhXk5yfcBQKzyMVyRrX8Qr9wEjc9JxA18FpdWVorU1/SyhE2u9q4ItnPxteZc116obPdoeHo3d4st8/LqqoVNHAthZ4WAzfQwkf2c1lxk6tZk4Xs+/zWqIdT6OQ1ZyH/wt6YoRupb+UNeIpztSJPvsmmU+Xb2Y0CNcvt0eXrJhe5sly+HLD/eT6tK+ufx3dmuLqzAs02B/JPElmuthH8N0uGzQnxvaxmVk13gb292ZHtakykez3aXMFt+9+0KN2X6y5Lc+ltfJXP1MQ2RV05eX5ra7qQD/5J2yKeKa+x2eI1tWlYrVp9afJ/lNUrYHFsUXdBq34i8qj1bE8J1DdwNandDnFOoT+3kaVm1Ua9XLGBYxZ4AzmxphY+S+R1C80tDaw6N8ulTstqciP/rS3NJUi0CJvbZGP9t1qBdGZFd0+spvR/lR86ns6zfwfn98dUfTnh90x+Mj7Ov7gF8vcSnJ/j4F/MBf9i/qh/0TnqX6xa4xMegPtHhc7fh58+XMbGF61Zrlgi/Pn/wGR8tUOYw7LY/Hm65Pt/rsPQWpDlyhaya/0ersDP5lkMHK2/WQD+1Uh+rL91pP4wn+svamcXaVyBbP9/imcunbs8OlW0Ir5gzj/lF5gyato4NQIuPX9NuEcH+EAO4k/zc/Mdec5S9cj5yXfnJnRcr97BYZeSSXyn2/W/fw/+C+Rn2P4O5/NHOJ8QHxMegfe/j8E//5agsUUd9rYWnsDvg/c9F0+4CN1fve+9NHxy+Nz+n4YvvPTe0C8uHnxrx+6jOTt+eeD85a+HBzeuv/H18IaHN/eu23L02rtbT13+7b4tNw4PPvHW4OF91o27zpz/6cbm/VuH3j45+OHx9S88vf6Vx4QWh6kSxr8B9cWyYXwbjB+D+EAHxAeegvjANogPbBmN/zL+1S7Ij/0b+Fdp4F8p4H4mR+Oz1wF/Qe3yFQqfh/2/hvYvshT532mBqGuyp53VmubuZvLLqytMbqHPXcOq8ggb2AJNoFnLq03Kv2Buqr7MrVJvHkso0AvS+QHkU5tqitw8t7t0JL/dXdLsaspN9wtJ4H8u8D8B+A8A/1eB/weB/weB/3XA/78D/48g/juxfI5Zq8+tcbqEmRDf/RLiu+uQfijV6b+E+O9lkM9XwP88BPSjQJ8I8V827B/gu5nSIb+4UpDXwu8oFnDqIpVcn4cXz203CvYC/ytH/+8y+VF/Afn/FOQf8Cvt61H/nVFfViLIcWa3ujUxuzueYTYWqeD8zEHtq2WGdmHcoishsQx0f3JifhPPfB7a947Gp3tBvr4G/s4Af2eBvy8gPr0O4tMX4X/hF1QK31oI6xOF+sthErM1I/02g8FvoMiIAvDDqggaJ57EYtJ3lqnnkB1UJSGn5aSCCpJxRScd76+SxkfxEXGGrqJnoPZ3k92ofVw5O4n/APiHZ1QqoFciuhLRE4rFqH8J0YFDfehKWfiMRhWhZhHliK6ILSGXKhbRtHru2bW4LFj5qfSdrqPSU/TeGdUkos+klpHdr0T7ElUSYjmuDmpigB8hrY5QckI6wh/0vxTqS+OzgzMoqWwZgy9BovGBTpExRZSOKNWIPhPwJc4F31CVQX14OY2dUYc6EX0RHVEBnRjBn1CRcWoG4Bcy7RMDnXHAX44rqoNocTMFgWZnIQ/d/8n4mD2nhssuFKS35Hk8+WZf2Z39a/3syawmb5orhV/G17X4M9ytbapCO0ubzxaM87+lSL6wlP8dou/sf5+QXMMNuDhsNyD7mParxtjHeIAW4w2SATnyjyJ7go9RyD4md9GM/6LwhI5LyNBN/AimwLmyedwhOB96+D/iDEuWzKBW/ov3tzjItzm/3SQURXoIo09nyPQ1OVuMrZyWTHZ+k99pGMHPKba567NYNXEvKzuDP2P+GqOXx01rbnK1FcL/YKF2E+SHQX7Kug3bkX16Zejkjrc+PffkCVuVEfJrtK9BfnzrKP4CU/+cA/kfOyE/X87kNxYdhfwQwMexq4Ufwf/PfePxKe9tdrdlutydd34f64nSuHkOur+mRqkR/JIqIvY2pCKRsi6lnJzsNbma880Op3AEX83m0Qmdhvospr5QTWY92rtGdkuqvikvv8nnZoH+Zpl87HrbpLqf4QtNgPpETh2n1Zbny6nNsWaxc0w3U/gRbnqMfadA8yPR/s9G9p35J3R/BGg7PWb/lSP7L7dIBVNqlMj/jL1Mi4MG0hlsUxRV8IiDNPJflyL/lSgtIwkK4idhFp2Kn0SwOBXH6kI35UcwscIoH6ADQRuVFtyj3hXfpSjBxLiu3CDbs4iJf0gDsmT841oortR2vCYvVYjxOtqvYOIrhFiK2nf00QpKFO7DdkdwBV8qJrTVfrI5LJYaFrOqxvhHstW8p1L+kR7dT7juLsgP/eNPV37Yu+7TW1eefMjEP3966wO9r60/8PCVb2+abOj+eqcQ6ccN+/5j/Webh08MXdr69XD/iYvDu997+tMd61+/cGv78O///ottVy+fHdh48IXSsHk75G+1JfO3zm3/3frf7p64490XHzjxYe9/7hk+8fW5B4aGLj/97g+/KRnJX92w/Q75b9sHL584vuXfHh164UYyf7Jt6NJ7mzbsO///jg+ef/GBV15Y99ruSx+eHtz/dO/ebefe/2vtSnS/b9ryzOBPe8699dC6Xzy67v+sO3/hFNL/gB8hPE6m8H25o/lPTtAfgH9n3wr6fyvcz0/A/5Rfw/8QNpJ/S9h0He7vbXB//wHom0G+00C+J8L5OAny/znklxXD+agH/VaJzgeG5MdeAPeHIll/n8G/pekzhDyOVwXlj8slQSTfkb2YXLoXU1UhekSxhLmf0f0Xke6VY8EBeeXkhv9G/v+VfE9ZUMrE98fT7QFXs9ZrbEvPyZCMyd+bgc4v5A+uhPy9gyFCcV1+Faunkf8fYhMWLI3oUwxE+mh7VBSxey3h3Yp0uRhj8EeC/bhVJlLYg7ygkbJR1yL2rrrlOvw6IZbUUX5ZgP4MM00QEXskTPwwxKJS8i/FQhDf0aHzIS8NkoQoStK1S0TBvipGfiVseV3waeyZyFWCwG7gxwhpuJpOQPymyhaLKtJxEV4brJfUhcTELnwAJwleLBB+maZDB5VHIlEFX3ZTcog2EsYIu7ICK241YgOdxlhD95rQHtJKW4l0TQ+m5/rVhiivyiJlkeUEXxxYhM5vh6FyVweiV/Vgj9BGpXERU//8qEMjjjP1zxNGmSW6W30KV0rrE8ZKY6fwPyxEv9IoE9N9MXuHqIup7365VMmu9gZtH9Ss4KlOSERSo5TXYUT9izqOV1q7rdLTarEG9V9tXCx8UKsWqY/hokWL6RYr1B90bMfR+acwi9FA7FnKm2nBrxF9uOCEkXo0yMRPo67ZpdiP2IFoSn/IcMAXRvdTcBIN9RW7RtbHQwRCIsyGFxF1xBTasfIkLYodVxOp+ohh3R/G1kes6dCvvIHuL0vIhZdq2FM9K16WkyEp3soK4g7ZXyLaRgUlppXKOq1VfhpnT/csMeBPhb6XI/mQ9ymY+PIS4A/Jh+wUTmGmkH+mkf4Rf156jSCJm+FjdIgKh0IQ35NNweSYhzLiZrqQqMHGyMfK2/Hlyfes0JuDNF7X8j+u/5LP8meKl1csHfGPRatW6hxFZkOuwepkV9myeZaawlqPocjZqNHyhWU1doOHx3c5E9nuZi2noKw2iR+3Nkwoiich/d/76M7h6389v2H9hof2DX3/D/4D6y72nnpieNt3g9s/PH963YUfT7y7YWDD/t+8f3bj8NavhOXgHzD08zsGBv/x9B/Pbr2w8dsHHt70/rmdE8wb4P1iGtjnK0frD68B+/QHsE9nj9ZfRfo1olUh/XNh+3vD2/dtPL1peOPpgm5zGcQ30iWSnnuDs/jj4xeJ4pYWn0DfWR4ezc8v1OYgs8efaGLl6tz+GpG4hcWzcVzscfVFDKvuX6MTaN0GnplTmMSfqikm0PiDvd8Mfn/gvfePXVj3QbsOySJ+/vH+LYf+OjR0cfOGfYM/7hsafnHHk+t7ex8dvnV2+MiOB3oPn3v71pWv1rfxVqP7b/P6TZsf+u7igb+f2/zixi+3n3/hb8PbXkniBzqW2Tam/gdqlXB/ToT782G4P6vh/lTD/RkD/USDfuKAf3ML/Bv1qH/jgfWbAev3BMTvfgn5HZBfbI/D+nkg/taA9I94muBTybj6GGEjN5dn8Tt5Q6n6gfo26H8Z9K+D/emD/n3hVH2NS7A/Pmi/E/nPkVhjseB15dj6q3oJkk+tgsJtv0b6I0IE6RiyT2MKwGcB+xhXR/owTNqnqlSM2OfIvv9Dt6Q6qAJ87qPS/o4Q2PeV5HRqMbkc2ffLlQQRQ/b1DAbfXEXSyH7H/6wi1Ug/xBUr6O4ByeHQWYJSIfs9qJS+FYnNR/ZxCt+lK9ShVBBL8eqP5b8kScZ+av5v9IdA69Oqx/8vtSmMJ0D/QX1GC7VYos/VOZy1nXYurO8qJB8JIt8klIP/qYH/mRfAfuqC9euiU/+jP6dS73NWtH5N8D43BeorBjGyMqRGIrBUUjlDJakI0nbVvCgxD493qalyuqtHRS4Im21QvxQTFKbyl0qrzE2w/x7YH3jfLMpE++PQJPFj82H/fOEU/t9teqX+XqBPZuqrpOhcjLcU9i8N9q9o1H+2JvFZikH+MKCbRvObzUCPQvs66H8K4NdiQjPQIT/VNAnoDmgfTeK7lKH2taR2Jvi/dWA/rwT7OTeFb8bLhP4nQftfQf1kqaAM6DmI7qBt98L5sMH5APywot+h9g6N/QrYJxywPwaBztSX2Qr0Fuj/A2hfCP3bof8MGF9+x/HtqfHH0V3j25f9nP72Hei4lgf0INBv8x+z1QJ/FPB3u/3Y+RUSKf6Z+TXD/KT2bJhfLsxPQIybvwfo2Bh6EfSPzjM2uSv5v3gK2G+36cvH9z8H3S+OSvsyoJcCffXo+pWnxk/yx0vylw50//j2UjtFpda/A/qvHqUXEwx/t/tn+GuA/0GLx/BXNtreMNpepBzpf8z6RJLtq5PtGfyADGjfA/0z9VO/SdG1XytT+JxM+2UUat9hax+d34TR+S0Zv/+/Q/ZprXTM+o9dn92j/P1hfP+cO8rnxTvO3wr8Kz9RRDVsX3NCfUvWqeLU8moEbE3MLofzOQHuh8VwvvfB/VoA9/fryMk/rnE4ivzeDFMU7HMczX+xxJPltrUJYxC/C4P9ToH93p3Kz1osbc1vbWxq5spgfp1ofsq5viyPPZXfNclsELgdzrTJ0QxvTlpWu51E46vnZ3udNrvBzuK5Gq22Ufxx1gQbDvxvgvuvE+YH/rEA7GPuHOj/ntH41XQ4vxVw/mfC+b8H7gf432WfD/0L7tw/t8gbcFV6PRNc2TVJ/IO10P9C6B/y2wWnof85gG9f3a32tjUX8Nxab26rrVPFfWY0/rcPxl8I44tg/CwYvxbGt8P4E2F9l4H+ssL6amF9X4X1LYP7uxPiXxTEr76G+NdzEL/aAf5RL6zvLljfJ2B9oxC/qgf5U4L/Y4f9VYB/XwTyUQ/r0wbyMR/ktw30t5WpbwX60QPxAcCX0jeDfngG9MM3oB+qgb8XQL8+Cfu/HvTzr0B/b4f49LPwvnMD9Dfc/2Pq996uH98VcHucAm9DOxpfKjyM5hfBSUmXXML8b9DQy4Iz1DPCSqKbWCUpjJSTVdEOxTTJ3UTV6P8D+YAmXi2LVyXk5FwcJ1YmlmnwFZhsZeIeWXqlafEyqVDTVT0rLknMVkmkKllHgpTf1fm2jEqsliVmOJatJk2aFYiuTswjDWR91dqEQoMvalDaEjEF9l/lePKN' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_ui.py b/lib/strategy/strategy_ui.py index c6a7081..62a3039 100644 --- a/lib/strategy/strategy_ui.py +++ b/lib/strategy/strategy_ui.py @@ -1,143 +1,12 @@ -"""策略交易页:主站 index.html 所需数据(顺势加仓等).""" -from __future__ import annotations - -from typing import Any, Callable, Optional - -from lib.strategy.strategy_db import init_strategy_tables -from lib.strategy.strategy_roll_monitor_lib import roll_leg_status_label - - -def _row_to_dict(row) -> dict: - if row is None: - return {} - try: - return dict(row) - except Exception: - return {} - - -def count_active_trend_plans(conn, count_fn: Optional[Callable] = None) -> int: - if callable(count_fn): - return int(count_fn(conn) or 0) - try: - return int( - conn.execute( - "SELECT COUNT(*) FROM trend_pullback_plans WHERE status='active'" - ).fetchone()[0] - ) - except Exception: - return 0 - - -def fetch_roll_page_data( - conn, - *, - default_risk_percent: float = 2.0, - count_active_trends: Optional[Callable] = None, - roll_cfg: dict | None = None, -) -> dict[str, Any]: - init_strategy_tables(conn) - monitors = [] - for row in conn.execute( - "SELECT * FROM order_monitors WHERE status='active' ORDER BY id DESC" - ).fetchall(): - monitors.append(_row_to_dict(row)) - roll_groups = [] - for row in conn.execute( - """SELECT g.* FROM roll_groups g - INNER JOIN order_monitors m ON m.id = g.order_monitor_id AND m.status='active' - WHERE g.status='active' - ORDER BY g.id DESC""" - ).fetchall(): - roll_groups.append(_row_to_dict(row)) - active_gids = {int(g["id"]) for g in roll_groups if g.get("id") is not None} - roll_legs = [] - for row in conn.execute( - "SELECT * FROM roll_legs ORDER BY id DESC LIMIT 80" - ).fetchall(): - leg = _row_to_dict(row) - gid = leg.get("roll_group_id") - if gid is not None and int(gid) not in active_gids: - continue - leg["status_label"] = roll_leg_status_label(leg.get("status")) - roll_legs.append(leg) - roll_legs = roll_legs[:50] - out = { - "roll_monitors": monitors, - "roll_groups": roll_groups, - "roll_legs": roll_legs, - "roll_trend_active": count_active_trend_plans(conn, count_active_trends), - "default_risk_percent": default_risk_percent, - } - if roll_cfg: - from lib.strategy.strategy_roll_ui_lib import enrich_roll_page_data - - enrich_roll_page_data(conn, out, roll_cfg) - return out - - -DEFAULT_TREND_DISABLED_NOTE = ( - "趋势回调(预览,自动补仓,程序止盈)须在本实例 .env 设置 " - "`LIVE_TRADING_ENABLED=true` 并重启对应 PM2 进程(如 crypto_gate / crypto_okx / crypto_binance)." -) - - -def strategy_render_extras( - conn, - page: str, - *, - default_risk_percent: float = 2.0, - count_active_trends: Optional[Callable] = None, - trend_disabled_note: str = "", - request_obj=None, - trend_cfg: Optional[dict] = None, -) -> dict[str, Any]: - """render_main_page 策略相关页变量(含策略交易记录).""" - if page == "strategy_records": - from lib.strategy.strategy_records_register import load_strategy_records_page - - return load_strategy_records_page(conn) - return strategy_page_template_vars( - conn, - page, - default_risk_percent=default_risk_percent, - count_active_trends=count_active_trends, - trend_disabled_note=trend_disabled_note, - request_obj=request_obj, - trend_cfg=trend_cfg, - ) - - -def strategy_page_template_vars( - conn, - page: str, - *, - default_risk_percent: float = 2.0, - count_active_trends: Optional[Callable] = None, - trend_disabled_note: str = "", - request_obj=None, - trend_cfg: Optional[dict] = None, -) -> dict[str, Any]: - """render_main_page 在 conn.close() 前合并进 render_template 的变量.""" - if page not in ("strategy", "strategy_trend", "strategy_roll"): - return {} - roll_cfg = None - try: - from flask import current_app - - roll_cfg = (current_app.extensions or {}).get("strategy_roll_cfg") - except Exception: - roll_cfg = None - out = fetch_roll_page_data( - conn, - default_risk_percent=default_risk_percent, - count_active_trends=count_active_trends, - roll_cfg=roll_cfg if isinstance(roll_cfg, dict) else None, - ) - if trend_cfg and request_obj is not None: - from lib.strategy.strategy_trend_register import load_trend_page_context - - out.update(load_trend_page_context(conn, request_obj, trend_cfg)) - elif page == "strategy_trend": - out["trend_disabled_note"] = trend_disabled_note or DEFAULT_TREND_DISABLED_NOTE - return out +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/strategy/strategy_wechat_notify.py b/lib/strategy/strategy_wechat_notify.py index 5c45190..70147bf 100644 --- a/lib/strategy/strategy_wechat_notify.py +++ b/lib/strategy/strategy_wechat_notify.py @@ -1,192 +1,12 @@ -"""策略计划(趋势回调 / 滚仓)开始与结束 — 企业微信推送(三所共用).""" -from __future__ import annotations - -from typing import Any, Optional - -from lib.common.wechat_notify_lib import wechat_direction_label - - -def _send(cfg: dict[str, Any], content: str) -> None: - fn = cfg.get("send_wechat") - if callable(fn): - try: - fn(content) - return - except Exception: - pass - m = cfg.get("app_module") - if m is not None: - sw = getattr(m, "send_wechat_msg", None) - if callable(sw): - try: - sw(content) - except Exception: - pass - - -def _account(cfg: dict[str, Any]) -> str: - fn = cfg.get("wechat_account_label") - if callable(fn): - try: - return str(fn()).strip() or _exchange(cfg) - except Exception: - pass - return _exchange(cfg) - - -def _exchange(cfg: dict[str, Any]) -> str: - return str(cfg.get("exchange_display") or "").strip() or "交易账户" - - -def _dir_text(cfg: dict[str, Any], direction: str) -> str: - fn = cfg.get("wechat_direction_text") - if callable(fn): - try: - return str(fn(direction)) - except Exception: - pass - return wechat_direction_label(direction) - - -def _fmt_price(cfg: dict[str, Any], symbol: str, price: Any) -> str: - if price is None or price == "": - return "—" - fn = cfg.get("format_price") or cfg.get("price_fmt") - if callable(fn): - try: - return str(fn(symbol, price)) - except Exception: - pass - m = cfg.get("app_module") - pf = getattr(m, "format_price_for_symbol", None) if m else None - if callable(pf): - try: - return str(pf(symbol, price)) - except Exception: - pass - try: - return str(round(float(price), 8)) - except (TypeError, ValueError): - return str(price) - - -def _fmt_pnl(pnl: Any) -> str: - if pnl is None: - return "—" - try: - v = float(pnl) - return f"{'+' if v > 0 else ''}{round(v, 2)} U" - except (TypeError, ValueError): - return str(pnl) - - -def notify_trend_plan_started( - cfg: dict[str, Any], - *, - plan_id: int, - symbol: str, - direction: str, - leverage: int, - stop_loss: float, - take_profit: float, - add_upper: float, - risk_percent: float, - dca_legs: int, - first_order_amount: float, - avg_entry: Optional[float] = None, - snapshot_usdt: Optional[float] = None, -) -> None: - sym = symbol or "—" - lines = [ - f"# 🚀 {sym} 趋势回调计划已开始", - f"**账户:{_account(cfg)}**", - f"- 计划 ID:**{plan_id}**", - f"- 方向:{_dir_text(cfg, direction)}|杠杆 **{int(leverage or 1)}x**", - f"- 止损:{_fmt_price(cfg, sym, stop_loss)}|止盈:{_fmt_price(cfg, sym, take_profit)}", - f"- 补仓区:{_fmt_price(cfg, sym, add_upper)}|补仓档 **{int(dca_legs or 0)}** 档", - f"- 风险:**{risk_percent}%**|首仓张数:**{first_order_amount}**", - ] - if avg_entry is not None: - lines.append(f"- 首仓成交价:{_fmt_price(cfg, sym, avg_entry)}") - if snapshot_usdt is not None: - try: - lines.append(f"- 启动时合约可用:**{round(float(snapshot_usdt), 2)} U**") - except (TypeError, ValueError): - pass - lines.append("- 说明:交易所已挂止损;止盈由程序监控;结束/保本将另行推送") - _send(cfg, "\n".join(lines)) - - -def notify_trend_plan_ended( - cfg: dict[str, Any], - *, - plan_id: int, - symbol: str, - direction: str, - end_type: str, - result_label: Optional[str] = None, - exit_price: Optional[float] = None, - pnl_amount: Optional[float] = None, - extra: Optional[str] = None, -) -> None: - sym = symbol or "—" - res = (result_label or end_type or "—").strip() - lines = [ - f"# 🏁 {sym} 趋势回调计划已结束", - f"**账户:{_account(cfg)}**", - f"- 计划 ID:**{plan_id}**", - f"- 方向:{_dir_text(cfg, direction)}", - f"- 结束方式:**{end_type}**", - f"- 结果:**{res}**", - ] - if exit_price is not None: - lines.append(f"- 离场参考价:{_fmt_price(cfg, sym, exit_price)}") - if pnl_amount is not None: - lines.append(f"- 本单盈亏:**{_fmt_pnl(pnl_amount)}**") - if extra: - lines.append(f"- {extra}") - _send(cfg, "\n".join(lines)) - - -def notify_roll_group_started( - cfg: dict[str, Any], - *, - group_id: int, - symbol: str, - direction: str, - order_monitor_id: int, - initial_take_profit: Optional[float] = None, - initial_stop_loss: Optional[float] = None, -) -> None: - sym = symbol or "—" - lines = [ - f"# 🚀 {sym} 滚仓计划已开始", - f"**账户:{_account(cfg)}**", - f"- 滚仓组 ID:**{group_id}**|绑定下单监控 **#{order_monitor_id}**", - f"- 方向:{_dir_text(cfg, direction)}", - f"- 首仓止盈(锁定):{_fmt_price(cfg, sym, initial_take_profit)}", - f"- 当前止损:{_fmt_price(cfg, sym, initial_stop_loss)}", - "- 说明:顺势加仓为人工触发;组结束(无持仓/监控结案)将另行推送", - ] - _send(cfg, "\n".join(lines)) - - -def notify_roll_group_ended( - cfg: dict[str, Any], - *, - group_id: int, - symbol: str, - direction: str, - reason: str, - leg_count: int = 0, -) -> None: - sym = symbol or "—" - lines = [ - f"# 🏁 {sym} 滚仓计划已结束", - f"**账户:{_account(cfg)}**", - f"- 滚仓组 ID:**{group_id}**", - f"- 方向:{_dir_text(cfg, direction)}", - f"- 结束原因:**{reason}**", - f"- 已完成滚仓腿数:**{int(leg_count or 0)}**", - ] - _send(cfg, "\n".join(lines)) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/__init__.py b/lib/trade/__init__.py index ab164b5..85d953e 100644 --- a/lib/trade/__init__.py +++ b/lib/trade/__init__.py @@ -1 +1,12 @@ -"""Shared library package.""" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNq7lppkmWqWkpJoYWmSbGqeaJJiYJRSYWCZKGZjYpASYJmfymZhZuabJq2fFcPkHsQqwhkZIBbp5OAoGhoezRocI63tbbHf0NqaNTwq22aOubBZqoGRmBVQ/4QUoP5y5kwDwRhxVwsGlmCfKPvCIKF4wXgni3vLOlvu9V1a9vhCU3PDsQbLUDFZQZ+wbL6oSA4HLqdcQe9Qb1vO2LAcAbeIQAa7NKt4Ll7R9DQpYZ+3mRwc8WGCIaHsCakmJs5pJpamuUkAloE41Q==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/account_risk_lib.py b/lib/trade/account_risk_lib.py index f5e4140..c0343bd 100644 --- a/lib/trade/account_risk_lib.py +++ b/lib/trade/account_risk_lib.py @@ -1,845 +1,12 @@ -"""账户冷静期 / 日冻结风控(三所实例共用).""" -from __future__ import annotations - -import os -from datetime import datetime, timezone -from typing import Any, Callable, Optional - -STATUS_NORMAL = "normal" -STATUS_FREEZE_1H = "freeze_1h" -STATUS_FREEZE_4H = "freeze_4h" -STATUS_DAILY = "freeze_daily" -STATUS_FREEZE_POSITION = "freeze_position" - -STATUS_LABELS = { - STATUS_NORMAL: "正常", - STATUS_FREEZE_1H: "1h冻结", - STATUS_FREEZE_4H: "4h冻结", - STATUS_DAILY: "日冻结", - STATUS_FREEZE_POSITION: "仓位上限冻结", -} - -MOOD_ISSUE_OPTIONS = ( - "怕踏空", - "报复开仓", - "盈利飘了", - "拿不住单", - "扛单", - "重仓违规", -) - -# 仅以下来源计入「手动平仓」风控(用户主动点平仓/结束计划) -CLOSE_SOURCE_USER_INSTANCE = "user_instance" -CLOSE_SOURCE_USER_HUB = "user_hub" -CLOSE_SOURCE_USER_TREND_STOP = "user_trend_stop" - -USER_INITIATED_CLOSE_SOURCES = frozenset( - { - CLOSE_SOURCE_USER_INSTANCE, - CLOSE_SOURCE_USER_HUB, - CLOSE_SOURCE_USER_TREND_STOP, - } -) - - -def _env_bool(key: str, default: bool = True) -> bool: - raw = (os.getenv(key) or "").strip().lower() - if not raw: - return default - return raw in ("1", "true", "yes", "on") - - -def _env_hours(key: str, default: float) -> float: - try: - v = float(os.getenv(key, str(default))) - except (TypeError, ValueError): - v = default - return max(0.0, v) - - -def _app_tz(): - from zoneinfo import ZoneInfo - - name = (os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip() - try: - return ZoneInfo(name) - except Exception: - return ZoneInfo("Asia/Shanghai") - - -def risk_control_enabled() -> bool: - return _env_bool("RISK_CONTROL_ENABLED", True) - - -def cooling_hours_manual() -> float: - return _env_hours("RISK_COOLING_HOURS_MANUAL", 4.0) - - -def cooling_hours_manual_journal() -> float: - return _env_hours("RISK_COOLING_HOURS_MANUAL_JOURNAL", 1.0) - - -def manual_close_daily_limit() -> int: - try: - return max(1, int(os.getenv("RISK_MANUAL_CLOSE_DAILY_LIMIT", "2"))) - except (TypeError, ValueError): - return 2 - - -def max_active_positions_from_env(default: int = 1) -> int: - try: - return max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", str(default)))) - except (TypeError, ValueError): - return max(1, default) - - -def position_limit_reached( - conn, - *, - max_active_positions: Optional[int] = None, -) -> tuple[bool, int, int]: - """(已达上限, 计入上限的活跃数, 上限值).""" - from lib.strategy.strategy_trade_labels import count_position_limit_active_monitors - - mx = max(1, int(max_active_positions if max_active_positions is not None else max_active_positions_from_env())) - ac = count_position_limit_active_monitors(conn) - return ac >= mx, ac, mx - - -def mood_issues_daily_freeze_enabled() -> bool: - return _env_bool("RISK_MOOD_ISSUES_DAILY_FREEZE", True) - - -def ensure_account_risk_schema(conn) -> None: - conn.execute( - """CREATE TABLE IF NOT EXISTS account_risk_state ( - id INTEGER PRIMARY KEY CHECK (id = 1), - trading_day TEXT, - manual_close_count INTEGER DEFAULT 0, - cooloff_until_ms INTEGER, - cooloff_hours INTEGER, - daily_frozen INTEGER DEFAULT 0, - pending_journal_trade_id INTEGER, - last_close_at_ms INTEGER, - updated_at TEXT - )""" - ) - row = conn.execute("SELECT id FROM account_risk_state WHERE id=1").fetchone() - if not row: - conn.execute( - "INSERT INTO account_risk_state (id, trading_day, manual_close_count, daily_frozen) VALUES (1, '', 0, 0)" - ) - - -def _row_get(row, key, default=None): - if row is None: - return default - try: - return row[key] - except (KeyError, IndexError, TypeError): - return default - - -def _now_ms(now: Optional[datetime] = None) -> int: - dt = now or datetime.now() - if dt.tzinfo is None: - dt = dt.replace(tzinfo=_app_tz()) - return int(dt.timestamp() * 1000) - - -def _normalize_epoch_ms(ms: int, ref_now_ms: Optional[int] = None) -> int: - """修正旧版把北京时间 naive 当作 UTC 写入的 epoch 毫秒.""" - tz = _app_tz() - off = datetime.now(tz).utcoffset() - if not off: - return int(ms) - offset_ms = int(off.total_seconds() * 1000) - if offset_ms == 0: - return int(ms) - ref = int(ref_now_ms) if ref_now_ms is not None else _now_ms(datetime.now(tz)) - corrected = int(ms) - offset_ms - if abs(int(ms) - ref) <= abs(corrected - ref): - return int(ms) - return corrected - - -def _sanitize_last_close_ms(last_ms: int, now_ms: int) -> Optional[int]: - """平仓时刻须不晚于当前(允许 1 分钟时钟偏差);显著未来视为无效锚点.""" - slack_ms = 60 * 1000 - if last_ms > now_ms + slack_ms: - return None - return last_ms - - -def _cooloff_duration_ms(hours: float) -> int: - return int(max(0.0, float(hours)) * 3600 * 1000) - - -def _cooloff_hours_value(row) -> float: - return float(_row_get(row, "cooloff_hours") or cooling_hours_manual()) - - -def _resolved_cooloff_until_ms(row, now_ms: int) -> Optional[int]: - """冷静期结束 = last_close + cooloff_hours;无效/已过期锚点不再重启计时.""" - hours = _cooloff_hours_value(row) - journal_h = cooling_hours_manual_journal() - duration_ms = _cooloff_duration_ms(hours) - last_raw = _row_get(row, "last_close_at_ms") - stored_raw = _cooloff_until_ms(row) - - if last_raw is not None: - try: - last_ms = _sanitize_last_close_ms( - _normalize_epoch_ms(int(last_raw), now_ms), now_ms - ) - except (TypeError, ValueError): - last_ms = None - if last_ms is not None: - end_ms = last_ms + duration_ms - if end_ms > now_ms: - return end_ms - if hours <= journal_h + 1e-6: - return None - - if stored_raw is None: - return None - stored_ms = _normalize_epoch_ms(int(stored_raw), now_ms) - return stored_ms if stored_ms > now_ms else None - - -def _clear_inactive_cooloff( - conn, - *, - now: Optional[datetime] = None, -) -> None: - """冷静期已结束或锚点无效时清库,避免重启后误读旧冻结.""" - conn.execute( - """UPDATE account_risk_state SET - cooloff_until_ms=NULL, - cooloff_hours=NULL, - last_close_at_ms=NULL, - updated_at=? - WHERE id=1""", - ((now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),), - ) - - -def _freeze_tier_from_remaining_ms(remaining_ms: int, hours: float) -> str: - journal_h = cooling_hours_manual_journal() - rh = remaining_ms / 3600000.0 - if rh <= journal_h + (5 / 60): - return STATUS_FREEZE_1H - return STATUS_FREEZE_4H - - -def _freeze_status_label(hours: float, status: str) -> str: - if status == STATUS_FREEZE_1H: - return STATUS_LABELS[STATUS_FREEZE_1H] - if status == STATUS_FREEZE_4H: - h = int(hours) if float(hours) == int(hours) else round(float(hours), 1) - if abs(float(hours) - 4.0) < 1e-6: - return STATUS_LABELS[STATUS_FREEZE_4H] - return f"{h}h冻结" - return STATUS_LABELS.get(status, STATUS_LABELS[STATUS_NORMAL]) - - -def _ms_to_local_str(ms: Optional[int], fmt_local: Callable[[int], str]) -> Optional[str]: - if ms is None: - return None - try: - return fmt_local(int(ms)) - except Exception: - return None - - -def _load_state(conn): - ensure_account_risk_schema(conn) - return conn.execute("SELECT * FROM account_risk_state WHERE id=1").fetchone() - - -def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) -> Any: - row = _load_state(conn) - td = (trading_day or "").strip() - stored = str(_row_get(row, "trading_day") or "").strip() - if stored != td: - now_ms = _now_ms(now) - cooloff_active = _resolved_cooloff_until_ms(row, now_ms) - conn.execute( - """UPDATE account_risk_state SET - trading_day=?, - manual_close_count=0, - daily_frozen=0, - cooloff_until_ms=?, - cooloff_hours=?, - last_close_at_ms=?, - pending_journal_trade_id=NULL, - updated_at=? - WHERE id=1""", - ( - td, - cooloff_active, - _row_get(row, "cooloff_hours") if cooloff_active else None, - _row_get(row, "last_close_at_ms") if cooloff_active else None, - (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), - ), - ) - row = _load_state(conn) - return row - - -def _set_cooloff( - conn, - *, - trading_day: str, - close_at_ms: int, - hours: float, - now: Optional[datetime] = None, -) -> None: - _sync_trading_day(conn, trading_day, now=now) - h = max(0.0, float(hours)) - until_ms = int(close_at_ms + h * 3600 * 1000) - conn.execute( - """UPDATE account_risk_state SET - cooloff_until_ms=?, - cooloff_hours=?, - last_close_at_ms=?, - updated_at=? - WHERE id=1""", - ( - until_ms, - int(h) if h == int(h) else int(round(h)), - int(close_at_ms), - (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), - ), - ) - - -def _set_cooloff_until( - conn, - *, - trading_day: str, - until_ms: int, - hours: float, - now: Optional[datetime] = None, -) -> None: - _sync_trading_day(conn, trading_day, now=now) - h = max(0.0, float(hours)) - conn.execute( - """UPDATE account_risk_state SET - cooloff_until_ms=?, - cooloff_hours=?, - updated_at=? - WHERE id=1""", - ( - int(until_ms), - int(h) if h == int(h) else int(round(h)), - (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), - ), - ) - - -def _ms_trading_day_label(ms: int) -> str: - dt = datetime.fromtimestamp(ms / 1000, tz=_app_tz()) - return dt.strftime("%Y-%m-%d") - - -def _parse_journal_close_ms(raw: Any) -> Optional[int]: - if raw is None: - return None - s = str(raw).strip() - if not s: - return None - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y/%m/%d %H:%M:%S", "%Y-%m-%d %H:%M"): - try: - dt = datetime.strptime(s[:19] if len(s) > 16 else s, fmt) - return _now_ms(dt) - except ValueError: - continue - return None - - -def _latest_journaled_manual_close_ms(conn, trading_day: str) -> Optional[int]: - """当日最近一条已复盘的手动平仓时刻(journal 有说明).""" - try: - rows = conn.execute( - """SELECT close_datetime FROM journal_entries - WHERE early_exit_trigger='手动平仓' - AND early_exit_note IS NOT NULL AND TRIM(early_exit_note) <> '' - ORDER BY close_datetime DESC""" - ).fetchall() - except Exception: - return None - td = (trading_day or "").strip() - best: Optional[int] = None - for row in rows: - ms = _parse_journal_close_ms(_row_get(row, "close_datetime")) - if ms is None: - continue - if td and _ms_trading_day_label(ms) != td: - continue - if best is None or ms > best: - best = ms - return best - - -def _journaled_manual_cooloff_expired( - conn, *, trading_day: str, now_ms: int, pending: Any -) -> bool: - """当日手动平仓已复盘且 1h 冷静期结束,且无待复盘的新平仓.""" - if pending is not None: - try: - if int(pending) != 0: - return False - except (TypeError, ValueError): - return False - close_ms = _latest_journaled_manual_close_ms(conn, trading_day) - if close_ms is None: - return False - journal_ms = _cooloff_duration_ms(cooling_hours_manual_journal()) - return close_ms + journal_ms <= now_ms - - -def _cooloff_until_ms(row) -> Optional[int]: - raw = _row_get(row, "cooloff_until_ms") - try: - return int(raw) if raw is not None else None - except (TypeError, ValueError): - return None - - -def _repair_stale_cooloff_row( - conn, - row, - *, - now_ms: int, - resolved_until_ms: Optional[int], - now: Optional[datetime] = None, -) -> None: - """脏数据读时写回:过期/无效则清库,否则对齐 until / last_close.""" - last_raw = _row_get(row, "last_close_at_ms") - stored_raw = _cooloff_until_ms(row) - if last_raw is None and stored_raw is None: - return - if resolved_until_ms is None: - if last_raw is not None or stored_raw is not None: - _clear_inactive_cooloff(conn, now=now) - return - dirty = False - new_last: Optional[int] = None - if last_raw is not None: - try: - norm = _normalize_epoch_ms(int(last_raw), now_ms) - sanitized = _sanitize_last_close_ms(norm, now_ms) - if sanitized is None: - dirty = True - else: - new_last = sanitized - if sanitized != int(last_raw): - dirty = True - except (TypeError, ValueError): - dirty = True - if stored_raw is not None: - stored_norm = _normalize_epoch_ms(int(stored_raw), now_ms) - if abs(stored_norm - int(resolved_until_ms)) > 60 * 1000: - dirty = True - if not dirty: - return - conn.execute( - """UPDATE account_risk_state SET - cooloff_until_ms=?, - cooloff_hours=?, - last_close_at_ms=?, - updated_at=? - WHERE id=1""", - ( - resolved_until_ms, - _row_get(row, "cooloff_hours"), - new_last, - (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"), - ), - ) - - -def _journal_can_reduce_cooloff(row, pending, now_ms: int) -> bool: - if int(_row_get(row, "daily_frozen") or 0) == 1: - return False - if _resolved_cooloff_until_ms(row, now_ms) is None: - return False - journal_h = cooling_hours_manual_journal() - cooloff_h = float(_row_get(row, "cooloff_hours") or cooling_hours_manual()) - if cooloff_h <= journal_h + 1e-6: - return False - if pending is not None: - try: - if int(pending) != 0: - return True - except (TypeError, ValueError): - return True - return True - - -def _journal_cooloff_until_ms(row, now_ms: int, journal_hours: float) -> int: - journal_ms = int(max(0.0, float(journal_hours)) * 3600 * 1000) - last_close_ms = _row_get(row, "last_close_at_ms") - if last_close_ms: - try: - base_ms = _sanitize_last_close_ms( - _normalize_epoch_ms(int(last_close_ms), now_ms), now_ms - ) - except (TypeError, ValueError): - base_ms = None - if base_ms is None: - base_ms = now_ms - else: - base_ms = now_ms - until_from_close = base_ms + journal_ms - if until_from_close > now_ms: - return until_from_close - return now_ms + journal_ms - - -def _set_daily_frozen(conn, *, trading_day: str, now: Optional[datetime] = None) -> None: - _sync_trading_day(conn, trading_day, now=now) - conn.execute( - """UPDATE account_risk_state SET daily_frozen=1, updated_at=? WHERE id=1""", - ((now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),), - ) - - -def parse_mood_issues(raw: Any) -> list[str]: - if raw is None: - return [] - if isinstance(raw, (list, tuple)): - parts = [str(x).strip() for x in raw if str(x).strip()] - else: - parts = [x.strip() for x in str(raw).split(",") if x.strip()] - return [p for p in parts if p in MOOD_ISSUE_OPTIONS] - - -def _record_one_user_initiated_close( - conn, - *, - source: str, - trade_record_id: Optional[int], - closed_at_ms: Optional[int], - trading_day: str, - now: Optional[datetime] = None, -) -> None: - row = _sync_trading_day(conn, trading_day, now=now) - count = int(_row_get(row, "manual_close_count") or 0) + 1 - close_ms = int(closed_at_ms) if closed_at_ms else _now_ms(now) - pending = int(trade_record_id) if trade_record_id else None - conn.execute( - """UPDATE account_risk_state SET - manual_close_count=?, - pending_journal_trade_id=?, - updated_at=? - WHERE id=1""", - (count, pending, (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")), - ) - if count >= manual_close_daily_limit(): - _set_daily_frozen(conn, trading_day=trading_day, now=now) - return - _set_cooloff( - conn, - trading_day=trading_day, - close_at_ms=close_ms, - hours=cooling_hours_manual(), - now=now, - ) - - -def on_user_initiated_close( - conn, - *, - source: str, - trade_record_id: Optional[int] = None, - closed_at_ms: Optional[int] = None, - trading_day: str, - now: Optional[datetime] = None, - count: int = 1, -) -> None: - """用户主动平仓/结束趋势计划:计入手动平仓次数与冷静期.""" - if not risk_control_enabled(): - return - src = (source or "").strip() - if src not in USER_INITIATED_CLOSE_SOURCES: - return - n = max(1, int(count or 1)) - for i in range(n): - _record_one_user_initiated_close( - conn, - source=src, - trade_record_id=trade_record_id if i == 0 else None, - closed_at_ms=closed_at_ms, - trading_day=trading_day, - now=now, - ) - row = _load_state(conn) - if int(_row_get(row, "daily_frozen") or 0) == 1: - break - - -def on_manual_close( - conn, - *, - trade_record_id: int, - closed_at_ms: Optional[int], - trading_day: str, - now: Optional[datetime] = None, -) -> None: - """兼容旧调用:等同实例页用户平仓.""" - on_user_initiated_close( - conn, - source=CLOSE_SOURCE_USER_INSTANCE, - trade_record_id=trade_record_id, - closed_at_ms=closed_at_ms, - trading_day=trading_day, - now=now, - count=1, - ) - - -def on_journal_saved( - conn, - *, - early_exit_trigger: str, - early_exit_note: str, - mood_issues_raw: Any, - trading_day: str, - now: Optional[datetime] = None, -) -> None: - if not risk_control_enabled(): - return - row = _sync_trading_day(conn, trading_day, now=now) - mood_list = parse_mood_issues(mood_issues_raw) - if mood_issues_daily_freeze_enabled() and mood_list: - _set_daily_frozen(conn, trading_day=trading_day, now=now) - conn.execute( - "UPDATE account_risk_state SET pending_journal_trade_id=NULL, updated_at=? WHERE id=1", - ((now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S"),), - ) - return - pending = _row_get(row, "pending_journal_trade_id") - trigger = (early_exit_trigger or "").strip() - note = (early_exit_note or "").strip() - now_ms = _now_ms(now) - if ( - trigger == "手动平仓" - and note - and int(_row_get(row, "daily_frozen") or 0) != 1 - and _journal_can_reduce_cooloff(row, pending, now_ms) - ): - journal_h = cooling_hours_manual_journal() - until_ms = _journal_cooloff_until_ms(row, now_ms, journal_h) - _set_cooloff_until( - conn, - trading_day=trading_day, - until_ms=until_ms, - hours=journal_h, - now=now, - ) - anchor_ms = until_ms - int(journal_h * 3600 * 1000) - conn.execute( - """UPDATE account_risk_state SET - pending_journal_trade_id=NULL, - last_close_at_ms=?, - updated_at=? - WHERE id=1""", - (int(anchor_ms), (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")), - ) - return - - -def apply_manual_close_journal_cooloff( - conn, - *, - early_exit_note: str, - trading_day: str, - now: Optional[datetime] = None, -) -> None: - """核对修改或复盘:手动平仓 + 说明后尝试将 4h 冷静期降为 1h.""" - note = (early_exit_note or "").strip() - if not note: - return - on_journal_saved( - conn, - early_exit_trigger="手动平仓", - early_exit_note=note, - mood_issues_raw="", - trading_day=trading_day, - now=now, - ) - - -def _next_trading_day_reset_ms(now: datetime, reset_hour: int) -> int: - from datetime import timedelta - - h = max(0, min(23, int(reset_hour))) - candidate = now.replace(hour=h, minute=0, second=0, microsecond=0) - if now >= candidate: - candidate = candidate + timedelta(days=1) - return _now_ms(candidate) - - -def enrich_risk_status_countdown( - st: dict[str, Any], - *, - now: Optional[datetime] = None, - daily_reset_hour: int = 8, -) -> dict[str, Any]: - """补充 freeze_until_ms / freeze_remaining_sec,供前端倒计时展示.""" - if not st.get("enabled", True): - return st - dt = now or datetime.now() - now_ms = _now_ms(dt) - until_ms: Optional[int] = None - if st.get("daily_frozen"): - until_ms = _next_trading_day_reset_ms(dt, daily_reset_hour) - elif st.get("cooloff_until_ms"): - try: - until_ms = int(st["cooloff_until_ms"]) - except (TypeError, ValueError): - until_ms = None - if until_ms is not None and until_ms > now_ms: - st["freeze_until_ms"] = until_ms - st["freeze_remaining_sec"] = max(0, (until_ms - now_ms) // 1000) - else: - st["freeze_until_ms"] = None - st["freeze_remaining_sec"] = 0 - return st - - -def apply_position_limit_risk( - st: dict[str, Any], - active_count: int, - *, - max_active_positions: Optional[int] = None, -) -> dict[str, Any]: - """持仓达 env MAX_ACTIVE_POSITIONS 时叠加「仓位上限冻结」(时间冻结优先展示).""" - out = dict(st or {}) - try: - mx = max(1, int(max_active_positions if max_active_positions is not None else max_active_positions_from_env())) - except (TypeError, ValueError): - mx = max_active_positions_from_env() - try: - ac = max(0, int(active_count)) - except (TypeError, ValueError): - ac = 0 - out["max_active_positions"] = mx - out["active_count"] = ac - if out.get("status") != STATUS_NORMAL: - return out - if ac >= mx: - out["status"] = STATUS_FREEZE_POSITION - out["status_label"] = STATUS_LABELS[STATUS_FREEZE_POSITION] - out["can_trade"] = False - out["can_roll"] = True - out["reason"] = f"已达最大持仓数({ac}/{mx}),新开仓已冻结,顺势加仓仍可用" - out["position_limit_frozen"] = True - out["freeze_until_ms"] = None - out["freeze_remaining_sec"] = 0 - else: - out["position_limit_frozen"] = False - out["can_roll"] = True - return out - - -def compute_account_risk_status( - conn, - *, - trading_day: str, - now: Optional[datetime] = None, - fmt_local_ms: Optional[Callable[[int], str]] = None, -) -> dict[str, Any]: - if not risk_control_enabled(): - return { - "enabled": False, - "status": STATUS_NORMAL, - "status_label": STATUS_LABELS[STATUS_NORMAL], - "can_trade": True, - "reason": "", - "cooloff_until_ms": None, - "cooloff_until": None, - "manual_close_count": 0, - "daily_frozen": False, - } - row = _sync_trading_day(conn, trading_day, now=now) - now_ms = _now_ms(now) - daily_frozen = int(_row_get(row, "daily_frozen") or 0) == 1 - pending = _row_get(row, "pending_journal_trade_id") - cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms) - if ( - not daily_frozen - and cooloff_until_ms is not None - and _journaled_manual_cooloff_expired( - conn, trading_day=trading_day, now_ms=now_ms, pending=pending - ) - ): - cooloff_until_ms = None - if not daily_frozen: - _repair_stale_cooloff_row( - conn, row, now_ms=now_ms, resolved_until_ms=cooloff_until_ms, now=now - ) - row = _load_state(conn) - cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms) - manual_close_count = int(_row_get(row, "manual_close_count") or 0) - - status = STATUS_NORMAL - reason = "" - if daily_frozen: - status = STATUS_DAILY - reason = f"账户今日已冻结(手动平仓 {manual_close_count} 次或复盘情绪标签)" - elif cooloff_until_ms is not None: - remaining_ms = cooloff_until_ms - now_ms - hours = _cooloff_hours_value(row) - status = _freeze_tier_from_remaining_ms(remaining_ms, hours) - status_label = _freeze_status_label(hours, status) - until_str = _ms_to_local_str(cooloff_until_ms, fmt_local_ms) if fmt_local_ms else None - label = status_label - reason = f"账户{label}中" - if until_str: - reason += f",至 {until_str}" - - can_trade = status == STATUS_NORMAL - freeze_remaining_sec = ( - max(0, (cooloff_until_ms - now_ms) // 1000) if cooloff_until_ms is not None else 0 - ) - return { - "enabled": True, - "status": status, - "status_label": _freeze_status_label(_cooloff_hours_value(row), status) - if status in (STATUS_FREEZE_1H, STATUS_FREEZE_4H) - else STATUS_LABELS[status], - "can_trade": can_trade, - "reason": reason, - "cooloff_until_ms": cooloff_until_ms, - "cooloff_until": _ms_to_local_str(cooloff_until_ms, fmt_local_ms) - if fmt_local_ms and cooloff_until_ms - else None, - "manual_close_count": manual_close_count, - "daily_frozen": daily_frozen, - "pending_journal_trade_id": pending, - "freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0, - } - - -def account_risk_blocks_trading( - conn, - *, - trading_day: str, - now: Optional[datetime] = None, - fmt_local_ms: Optional[Callable[[int], str]] = None, -) -> tuple[bool, str]: - """返回 (允许交易, 拒绝原因).""" - st = compute_account_risk_status( - conn, trading_day=trading_day, now=now, fmt_local_ms=fmt_local_ms - ) - if st.get("can_trade"): - return True, "" - return False, str(st.get("reason") or STATUS_LABELS.get(st.get("status"), "账户冻结")) - - -def insert_trade_record_id(conn) -> int: - row = conn.execute("SELECT last_insert_rowid()").fetchone() - return int(row[0] if row else 0) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/compensating_close_lib.py b/lib/trade/compensating_close_lib.py index 47fbebd..7e6818e 100644 --- a/lib/trade/compensating_close_lib.py +++ b/lib/trade/compensating_close_lib.py @@ -1,16 +1,12 @@ -"""开仓后挂 TP/SL 失败时的补偿平仓(避免裸仓).""" -from __future__ import annotations - -from typing import Callable - - -def log_compensating_close_error(prefix: str, exc: BaseException) -> None: - print(f"[{prefix}] {exc}", flush=True) - - -def run_compensating_close(close_fn: Callable[[], None], *, log_prefix: str = "compensating_close") -> None: - """执行补偿平仓;二次失败只打日志,不掩盖原始异常.""" - try: - close_fn() - except Exception as e: - log_compensating_close_error(log_prefix, e) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNpVk21oE2cAx/PWNC81u5jmUre2VJBuUNnS+FxyzyW2aUhzNS946TVJr0mau9xdNC8mDOkqFUbaik5XqTrmC5uf/DZh3WAwY5gVYYPBGLWl6lzKtrqBdKNCGXv5IOwJ6Id9/vP8/8//9/yfB3IWyk5JEkgIRMIlAMHukE7YIY+bgF1iYEXWkkqgk9so0ZklWJ7Jpfr1nsJBXmZ6xsqM43OB9CgpRf45TO9t3cce/0wsAEWfCEeOD5nsEmic/e303F9rf26d+aLe7pI7HCeG1y+vr5z6+MzO3NonPzeuXqg+OV1dfFh9fNO8sXlu9dfa+Q/OVedvYIcXAMrvIp+V1Qkjh7VYo/EJrNNZ9/RGJ3XcaEyz7PD7lKNMZFkImv13jmi8E9vl6fjuaM9IVDniZ0zGbYdP6Q3TxlsH+ziDUkW96CejfpQdCjzyfx1AgL+L+mNSCxwG/xCsOCLddPzrGaNahJqzH/XPySFP33fy4GuU1O5C+f3qFG1j9pB13h/gDF7MZcuh83gW8bsu0hTy/7okkfaZJBToGE7LUYU+bCz0KHezkaQlf//vU9X335v79MuNx2v3L5wlE6GwFY9TSaNBF44HPF6/JgBTSa3kDymU0aOskTvEGM3j5oxBPZCMJTHm6K50a7eva3N/zKQqJjJ6fyqJqyNt42q3aixOawujloDBR6D8oCgDsF8OOAk5K07HjHpzOkGrhiZfYSl20hiKYfzywHhGXTxEKzRM1x+l1l2JYNTDch/1vuQ7UMl3+mfiScVgaArtQwVf8JMQPzfiV3nJL4X6d2e7yNsgS/ACI4kOnvTDjpMEiOQIeDiPk3tcHblNigSAKCM9Yn+VGiMdMkNEckG3T7LCMDD3In6rW5d+unvr/MUrs7XFxqMHK4s3Mivztfnvr1YvrS+sX2/Uv127s7O68+HDH5c6N354unr72vzS0srvzxv3vpp99GRjdiFyslvcLk0FW9PeiM4wERiUAOzMYW+gfeNvo33XwKgFovfHCXR/G4X2tf0WjbXltJy+XRMPxmwpTlvUptK2tBsKMk2i/bpdedElvSmXfqG+CbyD+s+EnV6etENLcz+4AxIyNtD0zyN/swfxQfpUU59u6vuauuf/uuupeKSSNmmLxa1yMcdieNjGut2W5j6f9VsjVi6lDh5A+QE3yg/1oP9pc6P/GQSI/3AJ7ddbQP71YzouoxiKMUkR8beVET+7CJ3HnJUDvfx/96pBjA==' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/daily_open_limit_lib.py b/lib/trade/daily_open_limit_lib.py index 2769e57..6e39b6c 100644 --- a/lib/trade/daily_open_limit_lib.py +++ b/lib/trade/daily_open_limit_lib.py @@ -1,140 +1,12 @@ -"""单日开仓次数:软提醒阈值 + 硬上限(三所实例共用).""" -from __future__ import annotations - -import os -from typing import Any, Optional - - -def parse_daily_open_alert_threshold(raw: Any = None, *, default: int = 5) -> int: - """AI 克制提醒阈值;至少 1.""" - try: - v = int(raw if raw is not None and str(raw).strip() != "" else default) - except (TypeError, ValueError): - v = default - return max(1, v) - - -def parse_daily_open_hard_limit(raw: Any = None, *, default: int = 0) -> int: - """硬上限;0 表示不启用.至少 0.""" - try: - v = int(raw if raw is not None and str(raw).strip() != "" else default) - except (TypeError, ValueError): - v = default - return max(0, v) - - -def load_daily_open_limits_from_env( - env: Optional[dict[str, str]] = None, -) -> tuple[int, int]: - """从环境变量读取 (alert_threshold, hard_limit).""" - src = env if env is not None else os.environ - alert = parse_daily_open_alert_threshold(src.get("DAILY_OPEN_ALERT_THRESHOLD")) - hard = parse_daily_open_hard_limit(src.get("DAILY_OPEN_HARD_LIMIT")) - return alert, hard - - -def count_opens_for_trading_day(conn, trading_day: str) -> int: - """本交易日已成功写入 order_monitors 的开仓次数.""" - td = (trading_day or "").strip() - if not td: - return 0 - row = conn.execute( - "SELECT COUNT(*) FROM order_monitors WHERE session_date=?", - (td,), - ).fetchone() - return int(row[0] if row else 0) - - -def daily_open_hard_limit_blocks(opens_today: int, hard_limit: int) -> bool: - return int(hard_limit) > 0 and int(opens_today) >= int(hard_limit) - - -def hard_limit_block_reason(opens_today: int, hard_limit: int, reset_hour: int) -> str: - return ( - f"本交易日开仓次数已达上限({int(opens_today)}/{int(hard_limit)})," - f"次日北京时间 {int(reset_hour)}:00 后恢复" - ) - - -def check_daily_open_hard_limit( - conn, - trading_day: str, - hard_limit: int, - reset_hour: int, -) -> tuple[bool, str, int]: - """返回 (允许继续开仓, 拒绝原因, 当日已开次数).""" - opens_today = count_opens_for_trading_day(conn, trading_day) - if daily_open_hard_limit_blocks(opens_today, hard_limit): - return False, hard_limit_block_reason(opens_today, hard_limit, reset_hour), opens_today - return True, "", opens_today - - -def can_trade_new_open( - *, - time_allows: bool, - active_count: int, - max_active_positions: int, - opens_today: int, - hard_limit: int, - extra_blocks: bool = False, -) -> bool: - if extra_blocks: - return False - if not time_allows: - return False - if int(active_count) >= int(max_active_positions): - return False - if daily_open_hard_limit_blocks(opens_today, hard_limit): - return False - return True - - -def should_send_daily_open_alert(before: int, after: int, alert_threshold: int) -> bool: - return int(before) < int(alert_threshold) <= int(after) - - -def build_daily_open_alert_prompt( - trading_day: str, - opens_after: int, - alert_threshold: int, - *, - hard_limit: int = 0, - detail_line: str = "", -) -> str: - hard_txt = ( - f"硬上限 {hard_limit} 次(已达后将禁止新开仓直至下一交易日)." - if int(hard_limit) > 0 - else "未配置单日硬上限." - ) - extra = f" {detail_line}" if detail_line else "" - return ( - f"用户在北京时间交易日 {trading_day} 已累计开仓 {opens_after} 次" - f"(AI 提醒阈值 {alert_threshold};{hard_txt})" - f"{extra}" - f"用户自述“上头了”.请给克制提醒." - ) - - -def format_daily_open_counter_line( - opens_today: int, - alert_threshold: int, - hard_limit: int, -) -> str: - if int(hard_limit) > 0: - return ( - f"📅 当日开仓次数:{int(opens_today)} / 硬上限 {int(hard_limit)} 次" - f"(AI 提醒阈值 {int(alert_threshold)})" - ) - return ( - f"📅 当日开仓次数:{int(opens_today)} / AI 提醒阈值 {int(alert_threshold)} 次" - ) - - -def format_daily_open_summary_short( - opens_today: int, - alert_threshold: int, - hard_limit: int, -) -> str: - if int(hard_limit) > 0: - return f"本交易日累计开仓:{int(opens_today)}(硬上限 {int(hard_limit)},提醒 {int(alert_threshold)})" - return f"本交易日累计开仓:{int(opens_today)}(提醒阈值 {int(alert_threshold)})" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/entry_model_lib.py b/lib/trade/entry_model_lib.py index 110b704..d58958b 100644 --- a/lib/trade/entry_model_lib.py +++ b/lib/trade/entry_model_lib.py @@ -1,509 +1,12 @@ -"""趋势户开仓类型:反转·启动 / 顺势·大分歧 / 波段·小分歧;日内户单独 profile.""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any, Mapping, Optional, Sequence, Tuple - -from lib.trade.trade_policy_lib import TradePolicy - -PROFILE_TREND_DIV = "trend_div" -PROFILE_INTRADAY = "intraday" - -ENTRY_CATEGORY_REVERSAL = "reversal" -ENTRY_CATEGORY_TREND = "trend" -ENTRY_CATEGORY_SWING = "swing" - -ENTRY_MODEL_LAUNCH_A = "launch_a" -ENTRY_MODEL_LAUNCH_B = "launch_b" -ENTRY_MODEL_BIG_DIV_A = "big_div_a" -ENTRY_MODEL_BIG_DIV_B = "big_div_b" -ENTRY_MODEL_SMALL_DIV = "small_div" - -ENTRY_CATEGORY_INTRADAY = "intraday" -ENTRY_MODEL_LIQUIDITY_FALSE_BREAK = "liquidity_false_break" -ENTRY_MODEL_STRUCTURE_BREAKOUT = "structure_breakout" - -VALID_ENTRY_MODEL_CODES = frozenset( - { - ENTRY_MODEL_LAUNCH_A, - ENTRY_MODEL_LAUNCH_B, - ENTRY_MODEL_BIG_DIV_A, - ENTRY_MODEL_BIG_DIV_B, - ENTRY_MODEL_SMALL_DIV, - } -) - -INTRADAY_ENTRY_MODEL_CODES = frozenset( - { - ENTRY_MODEL_LIQUIDITY_FALSE_BREAK, - ENTRY_MODEL_STRUCTURE_BREAKOUT, - } -) - -ALL_ENTRY_MODEL_CODES = VALID_ENTRY_MODEL_CODES | INTRADAY_ENTRY_MODEL_CODES - -ENTRY_CATEGORY_LABELS: dict[str, str] = { - ENTRY_CATEGORY_REVERSAL: "反转", - ENTRY_CATEGORY_TREND: "顺势", - ENTRY_CATEGORY_SWING: "波段", -} - -TRADE_STYLE_FALLBACK_ENTRY_REASONS: Tuple[str, ...] = ("趋势单", "波段单") - -INTRADAY_LEGACY_TREND_ENTRY_REASONS: Tuple[str, ...] = ( - "趋势多头:4h大结构突破前进场,确认条件:三次探顶,5m收敛不创新低", - "趋势空头:4h大结构突破前进场,确认条件:三次探底,5m收敛不创新高", - "趋势多头:小分歧低吸入场(左侧),确认条件:二次探底", - "趋势空头:小分歧高吸入场(左侧),确认条件:二次探顶", - "波段单:5m顺势突破,确认条件:2根k线+成交量放大+4h同向+日成交量前20", -) - -# code, label, category, trade_style, help -_ENTRY_SPECS: Tuple[Tuple[str, str, str, str, str], ...] = ( - ( - ENTRY_MODEL_LAUNCH_A, - "启动A", - ENTRY_CATEGORY_REVERSAL, - "trend", - "反转链结构内:摸参考极值前小收敛,或 B 失败后 5m N 字试仓(不单列)", - ), - ( - ENTRY_MODEL_LAUNCH_B, - "启动B", - ENTRY_CATEGORY_REVERSAL, - "trend", - "第二次到参考极值附近,无小收敛时的实体突破", - ), - ( - ENTRY_MODEL_BIG_DIV_A, - "大分歧A", - ENTRY_CATEGORY_TREND, - "trend", - "主升已确立:突破前收敛,不创新低企稳(空:不创新高)", - ), - ( - ENTRY_MODEL_BIG_DIV_B, - "大分歧B", - ENTRY_CATEGORY_TREND, - "trend", - "主升已确立:结构实体突破确认后入场", - ), - ( - ENTRY_MODEL_SMALL_DIV, - "小分歧", - ENTRY_CATEGORY_SWING, - "swing", - "主升已确立:二次探底 N 字或 5m 三均线重新多头(空:二次探顶 / 空头均线)", - ), -) - -_INTRADAY_ENTRY_SPECS: Tuple[Tuple[str, str, str, str, str], ...] = ( - ( - ENTRY_MODEL_LIQUIDITY_FALSE_BREAK, - "假破", - ENTRY_CATEGORY_INTRADAY, - "trend", - "流动性扫单 → 假突破验证 → 5m N 字 → 15m 顶/底分型", - ), - ( - ENTRY_MODEL_STRUCTURE_BREAKOUT, - "结构突破", - ENTRY_CATEGORY_INTRADAY, - "trend", - "15m 结构有效突破(收盘确认)", - ), -) - -_CODE_TO_LABEL = {code: label for code, label, _, _, _ in _ENTRY_SPECS} -_CODE_TO_LABEL.update({code: label for code, label, _, _, _ in _INTRADAY_ENTRY_SPECS}) -_CODE_TO_STYLE = {code: style for code, _, _, style, _ in _ENTRY_SPECS} -_CODE_TO_STYLE.update({code: style for code, _, _, style, _ in _INTRADAY_ENTRY_SPECS}) -_CODE_TO_CATEGORY = {code: cat for code, _, cat, _, _ in _ENTRY_SPECS} -_CODE_TO_CATEGORY.update({code: cat for code, _, cat, _, _ in _INTRADAY_ENTRY_SPECS}) -_LABEL_TO_CODE = {label: code for code, label, _, _, _ in _ENTRY_SPECS} -_LABEL_TO_CODE.update({label: code for code, label, _, _, _ in _INTRADAY_ENTRY_SPECS}) -_CODE_TO_HELP = {code: help for code, _, _, _, help in _ENTRY_SPECS} -_CODE_TO_HELP.update({code: help for code, _, _, _, help in _INTRADAY_ENTRY_SPECS}) - -_CATEGORY_ORDER: Tuple[str, ...] = ( - ENTRY_CATEGORY_REVERSAL, - ENTRY_CATEGORY_TREND, - ENTRY_CATEGORY_SWING, -) - -_INTRADAY_WHITELIST = frozenset({"BTC", "ETH"}) - - -@dataclass(frozen=True) -class EntryModelOption: - code: str - label: str - category: str - trade_style: str - help: str - - -def is_intraday_trading_profile(policy: TradePolicy) -> bool: - """日内户:启用 BTC/ETH 白名单(env 中 TRADE_SYMBOL_WHITELIST).""" - if not policy.symbol_restrict_enabled: - return False - if not policy.symbol_whitelist: - return False - return all(s in _INTRADAY_WHITELIST for s in policy.symbol_whitelist) - - -def order_entry_profile(policy: TradePolicy) -> str: - return PROFILE_INTRADAY if is_intraday_trading_profile(policy) else PROFILE_TREND_DIV - - -def entry_model_options() -> Tuple[EntryModelOption, ...]: - return tuple( - EntryModelOption(code=code, label=label, category=cat, trade_style=style, help=help) - for code, label, cat, style, help in _ENTRY_SPECS - ) - - -def intraday_entry_model_options() -> Tuple[EntryModelOption, ...]: - return tuple( - EntryModelOption(code=code, label=label, category=cat, trade_style=style, help=help) - for code, label, cat, style, help in _INTRADAY_ENTRY_SPECS - ) - - -def entry_model_categories() -> list[dict[str, Any]]: - """两级 UI:反转 / 顺势 / 波段 → 子选项.""" - opts = entry_model_options() - out: list[dict[str, Any]] = [] - for cat_key in _CATEGORY_ORDER: - children = [ - { - "code": o.code, - "label": o.label, - "trade_style": o.trade_style, - "help": o.help, - } - for o in opts - if o.category == cat_key - ] - if not children: - continue - out.append( - { - "key": cat_key, - "label": ENTRY_CATEGORY_LABELS.get(cat_key, cat_key), - "options": children, - } - ) - return out - - -def entry_model_category(code: Optional[str]) -> str: - c = normalize_entry_model_code(code) - return _CODE_TO_CATEGORY.get(c, "") - - -def normalize_entry_model_code(raw: Optional[str]) -> str: - v = (raw or "").strip().lower() - if v in ALL_ENTRY_MODEL_CODES: - return v - label = (raw or "").strip() - if label in _LABEL_TO_CODE: - return _LABEL_TO_CODE[label] - return "" - - -def entry_model_label(code: Optional[str]) -> str: - c = normalize_entry_model_code(code) - return _CODE_TO_LABEL.get(c, "") - - -def entry_category_display_prefix(category: str) -> str: - """两级展示用的一级前缀:反转 / 顺势 / 波段单(含日内).""" - cat = (category or "").strip() - if cat in (ENTRY_CATEGORY_SWING, ENTRY_CATEGORY_INTRADAY): - return "波段单" - return ENTRY_CATEGORY_LABELS.get(cat, "") - - -def entry_model_display_label(code: Optional[str]) -> str: - """两级展示:反转/启动A,顺势/大分歧A,波段单/小分歧,波段单/假破.""" - c = normalize_entry_model_code(code) - if not c: - return "" - label = entry_model_label(c) - if not label: - return "" - prefix = entry_category_display_prefix(entry_model_category(c)) - if prefix: - return f"{prefix}/{label}" - return label - - -def format_entry_type_display( - text: Optional[str] = None, - *, - entry_model: Optional[str] = None, - trade_style: Optional[str] = None, -) -> str: - """交易记录/持仓展示:已知 entry_model 或短标签 → 两级文案.""" - if entry_model: - disp = entry_model_display_label(entry_model) - if disp: - return disp - raw = (text or "").strip() - if not raw: - ts = (trade_style or "").strip().lower() - if ts in ("trend", "swing"): - return trade_style_label_zh(ts) - return "" - if "/" in raw: - return raw - code = normalize_entry_model_code(raw) - if code: - disp = entry_model_display_label(code) - if disp: - return disp - return raw - - -def trade_style_for_entry_model(code: Optional[str]) -> str: - c = normalize_entry_model_code(code) - return _CODE_TO_STYLE.get(c, "trend") - - -def trade_style_label_zh(trade_style: str) -> str: - return "波段单" if (trade_style or "").strip().lower() == "swing" else "趋势单" - - -def trend_div_entry_reason_display_options() -> Tuple[str, ...]: - return tuple(entry_model_display_label(code) for code, _, _, _, _ in _ENTRY_SPECS) - - -def intraday_entry_reason_display_options() -> Tuple[str, ...]: - return tuple(entry_model_display_label(code) for code, _, _, _, _ in _INTRADAY_ENTRY_SPECS) - - -def normalize_review_entry_reason(raw: Optional[str], allowed: Sequence[str]) -> str: - """复盘/核对开仓类型:允许两级展示名,兼容旧短标签.""" - s = (raw or "").strip() - if not s: - return "" - allowed_set = frozenset(allowed) - if s in allowed_set: - return s - disp = format_entry_type_display(s) - if disp in allowed_set: - return disp - code = normalize_entry_model_code(s) - if code: - disp2 = entry_model_display_label(code) - if disp2 in allowed_set: - return disp2 - return "" - - -# 日内复盘开仓类型:手动假破/结构突破 + 自动触价(与 KEY_ENTRY_REASON_TRIGGER_OPTIONS 一致) -_INTRADAY_JOURNAL_KEY_ENTRY_REASONS: Tuple[str, ...] = ( - "关键位回调触价开仓", - "关键位突破触价开仓", -) - - -def trend_manual_entry_reason_count(policy: TradePolicy) -> int: - if is_intraday_trading_profile(policy): - return len(intraday_entry_reason_display_options()) - return len(trend_div_entry_reason_display_options()) - - -def build_journal_entry_reason_options() -> Tuple[str, ...]: - """复盘开仓类型:仅 entry model,不含 trade_style 兜底与策略下单类型.""" - return trend_div_entry_reason_display_options() - - -def build_trend_div_entry_reason_options( - strategy_options: Sequence[str], -) -> Tuple[str, ...]: - return trend_div_entry_reason_display_options() + TRADE_STYLE_FALLBACK_ENTRY_REASONS + tuple(strategy_options) - - -def build_intraday_entry_reason_options( - key_options: Sequence[str], - strategy_options: Sequence[str], -) -> Tuple[str, ...]: - del strategy_options # 日内户无趋势回调/顺势加仓 - del key_options - return intraday_entry_reason_display_options() + _INTRADAY_JOURNAL_KEY_ENTRY_REASONS - - -def entry_reason_options_for_policy( - policy: TradePolicy, - key_options: Sequence[str], - strategy_options: Sequence[str], -) -> Tuple[str, ...]: - if is_intraday_trading_profile(policy): - return build_intraday_entry_reason_options(key_options, strategy_options) - return build_trend_div_entry_reason_options(strategy_options) - - -def parse_manual_order_style_fields( - policy: TradePolicy, - form: Mapping[str, Any], - *, - default_trade_style: str = "trend", -) -> Tuple[str, Optional[str], Optional[str]]: - """返回 (trade_style, entry_model_code|None, error_message|None).""" - if is_intraday_trading_profile(policy): - entry_model = normalize_entry_model_code(form.get("entry_model")) - if entry_model in INTRADAY_ENTRY_MODEL_CODES: - return "trend", entry_model, None - raw_style = (form.get("trade_style") or "").strip().lower() - if raw_style in ("trend", "swing"): - return raw_style, None, None - return "", None, "请选择开仓类型(假破 / 结构突破)" - - entry_model = normalize_entry_model_code(form.get("entry_model")) - if not entry_model: - return "", None, "请选择开仓类型(反转 / 顺势 / 波段)" - trade_style = trade_style_for_entry_model(entry_model) - return trade_style, entry_model, None - - -def resolve_trade_record_entry_reason( - *, - entry_reason: Optional[str] = None, - entry_model: Optional[str] = None, - key_signal_type: Optional[str] = None, - monitor_type: Optional[str] = None, - trade_style: Optional[str] = None, - entry_reason_from_key_signal=None, - entry_reason_for_monitor_type=None, -) -> str: - er = (entry_reason or "").strip() - if er: - return er - label = entry_model_display_label(entry_model) - if label: - return label - kst = (key_signal_type or "").strip() - if kst and entry_reason_from_key_signal is not None: - from_key = (entry_reason_from_key_signal(kst) or "").strip() - if from_key: - return from_key - if entry_reason_for_monitor_type is not None: - from_mt = (entry_reason_for_monitor_type(monitor_type) or "").strip() - if from_mt: - return from_mt - ts = (trade_style or "").strip().lower() - if ts in ("trend", "swing"): - return trade_style_label_zh(ts) - return "" - - -def resolve_effective_trade_entry_reason( - *, - reviewed_entry_reason: Optional[str] = None, - entry_reason: Optional[str] = None, - entry_model: Optional[str] = None, - key_signal_type: Optional[str] = None, - monitor_type: Optional[str] = None, - trade_style: Optional[str] = None, - entry_reason_from_key_signal=None, - entry_reason_for_monitor_type=None, -) -> str: - """交易记录展示/导出用:复盘优先,再回落 entry_model / 关键位 / 策略 / trade_style.""" - for raw in (reviewed_entry_reason, entry_reason): - er = (raw or "").strip() - if er: - return format_entry_type_display( - er, - entry_model=entry_model, - trade_style=trade_style, - ) - return format_entry_type_display( - resolve_trade_record_entry_reason( - entry_model=entry_model, - key_signal_type=key_signal_type, - monitor_type=monitor_type, - trade_style=trade_style, - entry_reason_from_key_signal=entry_reason_from_key_signal, - entry_reason_for_monitor_type=entry_reason_for_monitor_type, - ), - entry_model=entry_model, - trade_style=trade_style, - ) - - -def enrich_entry_model_display(item: dict) -> dict: - code = normalize_entry_model_code(item.get("entry_model")) - if code: - item["entry_model"] = code - item["entry_model_label"] = entry_model_display_label(code) - cat = entry_model_category(code) - if cat: - item["entry_model_category"] = cat - item["entry_model_category_label"] = ENTRY_CATEGORY_LABELS.get(cat, "") - else: - item.setdefault("entry_model_label", "") - return item - - -def open_position_button_label(policy: TradePolicy, sizing_mode: str) -> str: - from lib.trade.position_sizing_lib import mode_label_zh - - mode_txt = mode_label_zh(sizing_mode) - if is_intraday_trading_profile(policy): - return f"开仓(日内·{mode_txt})" - return f"开仓({mode_txt})" - - -def order_entry_template_context(policy: TradePolicy) -> dict: - profile = order_entry_profile(policy) - opts = entry_model_options() - intraday_opts = intraday_entry_model_options() - return { - "order_entry_profile": profile, - "intraday_discipline": profile == PROFILE_INTRADAY, - "intraday_entry_model_options": [ - { - "code": o.code, - "label": o.label, - "trade_style": o.trade_style, - "help": o.help, - } - for o in intraday_opts - ], - "entry_model_options": [ - { - "code": o.code, - "label": o.label, - "category": o.category, - "trade_style": o.trade_style, - "help": o.help, - } - for o in opts - ], - "entry_model_categories": entry_model_categories(), - "entry_model_trade_style_map": {o.code: o.trade_style for o in opts}, - "entry_model_code_to_category": {o.code: o.category for o in opts}, - } - - -def hub_meta_entry_context(policy: TradePolicy) -> dict: - """供 /api/hub/meta:中控按 profile 隐藏平仓/委托等.""" - profile = order_entry_profile(policy) - return { - "order_entry_profile": profile, - "intraday_discipline": profile == PROFILE_INTRADAY, - } - - -def migrate_entry_model_columns(conn) -> None: - for table in ("order_monitors", "trade_records"): - try: - conn.execute(f"ALTER TABLE {table} ADD COLUMN entry_model TEXT") - except Exception: - pass - - -# 兼容旧引用:现为两级展示文案 -TREND_DIV_ENTRY_REASON_LABELS = trend_div_entry_reason_display_options() +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/force_close_lib.py b/lib/trade/force_close_lib.py index 46165e0..14ba526 100644 --- a/lib/trade/force_close_lib.py +++ b/lib/trade/force_close_lib.py @@ -1,320 +1,12 @@ -"""整点强制清仓(FORCE_CLOSE_*):UI 标识与持仓倒计时.""" -from __future__ import annotations - -import os -import time -from datetime import datetime, timedelta -from typing import Any, Optional -from zoneinfo import ZoneInfo - -FORCE_CLOSE_RESULT = "强制清仓" -FORCE_CLOSE_GRACE_MINUTES = 15 - - -def app_timezone_name() -> str: - return (os.getenv("APP_TIMEZONE") or os.getenv("TZ") or "Asia/Shanghai").strip() or "Asia/Shanghai" - - -def normalize_force_close_bj_hour(value: Any) -> int: - try: - h = int(value) - except (TypeError, ValueError): - return 0 - return max(0, min(23, h)) - - -def _now_dt(*, now_ms: Optional[int] = None, tz_name: Optional[str] = None) -> datetime: - tz = ZoneInfo(tz_name or app_timezone_name()) - if now_ms is None: - return datetime.now(tz) - return datetime.fromtimestamp(int(now_ms) / 1000, tz=tz) - - -def force_close_hour_label(bj_hour: Any) -> str: - return f"{normalize_force_close_bj_hour(bj_hour):02d}:00" - - -def force_close_label(bj_hour: Any) -> str: - return f"强制清仓 {force_close_hour_label(bj_hour)}" - - -def is_force_close_active_hour( - bj_hour: Any, - *, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, - grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES, -) -> bool: - """当前是否处于整点强制清仓执行窗口(整点起 grace 分钟内).""" - return is_force_close_executing( - bj_hour, - now_ms=now_ms, - tz_name=tz_name, - grace_minutes=grace_minutes, - ) - - -def is_force_close_executing( - bj_hour: Any, - *, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, - grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES, -) -> bool: - hour = normalize_force_close_bj_hour(bj_hour) - now = _now_dt(now_ms=now_ms, tz_name=tz_name) - target = now.replace(hour=hour, minute=0, second=0, microsecond=0) - if now < target: - return False - end = target + timedelta(minutes=max(1, int(grace_minutes))) - return now < end - - -def parse_closed_at_dt( - closed_at: Any, - *, - tz_name: Optional[str] = None, -) -> Optional[datetime]: - if closed_at is None: - return None - text = str(closed_at).strip() - if not text: - return None - tz = ZoneInfo(tz_name or app_timezone_name()) - for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M"): - try: - return datetime.strptime(text, fmt).replace(tzinfo=tz) - except ValueError: - continue - return None - - -def is_close_at_force_close_window( - closed_at: Any, - bj_hour: Any, - *, - grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES, - tz_name: Optional[str] = None, -) -> bool: - """平仓时刻是否落在北京时间整点强制清仓窗口内.""" - dt = parse_closed_at_dt(closed_at, tz_name=tz_name) - if dt is None: - return False - hour = normalize_force_close_bj_hour(bj_hour) - if dt.hour != hour: - return False - return dt.minute < max(1, int(grace_minutes)) - - -def infer_force_close_result( - closed_at: Any, - *, - enabled: bool, - bj_hour: Any, - grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES, - tz_name: Optional[str] = None, -) -> Optional[str]: - if not enabled: - return None - if is_close_at_force_close_window( - closed_at, bj_hour, grace_minutes=grace_minutes, tz_name=tz_name - ): - return FORCE_CLOSE_RESULT - return None - - -def coerce_force_close_result( - result: Optional[str], - closed_at: Any, - *, - enabled: bool, - bj_hour: Any, - miss_reason: Optional[str] = None, - grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES, - tz_name: Optional[str] = None, -) -> tuple[str, str]: - """同步平仓归类:整点窗口内优先记为强制清仓.""" - res = (result or "").strip() - note = (miss_reason or "").strip() - if res == FORCE_CLOSE_RESULT: - return res, note - fc = infer_force_close_result( - closed_at, - enabled=enabled, - bj_hour=bj_hour, - grace_minutes=grace_minutes, - tz_name=tz_name, - ) - if not fc: - return res, note - if not note: - note = f"北京时间 {force_close_hour_label(bj_hour)} 整点风控清仓" - return fc, note - - -def apply_force_close_display_result( - result: Optional[str], - closed_at: Any, - *, - enabled: bool, - bj_hour: Any, - grace_minutes: int = FORCE_CLOSE_GRACE_MINUTES, - tz_name: Optional[str] = None, -) -> str: - """展示层:外部平仓/手动平仓若落在整点窗口,显示为强制清仓.""" - res = (result or "").strip() - if res == FORCE_CLOSE_RESULT: - return res - fc = infer_force_close_result( - closed_at, - enabled=enabled, - bj_hour=bj_hour, - grace_minutes=grace_minutes, - tz_name=tz_name, - ) - if fc and (res in ("", "外部平仓", "手动平仓") or res.startswith("外部平仓")): - return fc - return res - - -def compute_next_force_close_at_ms( - *, - bj_hour: Any, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, -) -> Optional[int]: - """下一次强制清仓时刻(北京时间整点)的 epoch 毫秒.""" - hour = normalize_force_close_bj_hour(bj_hour) - now = _now_dt(now_ms=now_ms, tz_name=tz_name) - target = now.replace(hour=hour, minute=0, second=0, microsecond=0) - if now >= target: - target += timedelta(days=1) - return int(target.timestamp() * 1000) - - -def force_close_remaining_seconds( - close_at_ms: Any, - *, - now_ms: Optional[int] = None, -) -> Optional[int]: - try: - close_at = int(close_at_ms) - except (TypeError, ValueError): - return None - now = int(now_ms if now_ms is not None else time.time() * 1000) - return max(0, int((close_at - now) / 1000)) - - -def format_force_close_countdown(seconds: Any, *, active: bool = False) -> str: - if active: - return "执行中" - try: - sec = max(0, int(seconds)) - except (TypeError, ValueError): - return "--:--:--" - h = sec // 3600 - m = (sec % 3600) // 60 - s = sec % 60 - return f"{h:02d}:{m:02d}:{s:02d}" - - -def build_force_close_state( - enabled: bool, - bj_hour: Any, - *, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, - has_active_positions: Optional[bool] = None, -) -> dict[str, Any]: - """实例级强制清仓状态(模板 / API 共用).""" - if not enabled: - return { - "enabled": False, - "bj_hour": normalize_force_close_bj_hour(bj_hour), - "hour_label": force_close_hour_label(bj_hour), - "label": force_close_label(bj_hour), - "next_at_ms": None, - "remaining_sec": None, - "countdown": "", - "active": False, - } - hour = normalize_force_close_bj_hour(bj_hour) - executing = is_force_close_executing(hour, now_ms=now_ms, tz_name=tz_name) - active = executing and (has_active_positions is not False) - next_at_ms = compute_next_force_close_at_ms(bj_hour=hour, now_ms=now_ms, tz_name=tz_name) - rem = force_close_remaining_seconds(next_at_ms, now_ms=now_ms) if next_at_ms else None - return { - "enabled": True, - "bj_hour": hour, - "hour_label": force_close_hour_label(hour), - "label": force_close_label(hour), - "next_at_ms": next_at_ms, - "remaining_sec": rem, - "countdown": format_force_close_countdown(rem, active=active), - "active": active, - } - - -def force_close_template_context( - enabled: bool, - bj_hour: Any, - *, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, - has_active_positions: Optional[bool] = None, -) -> dict[str, dict[str, Any]]: - return { - "force_close": build_force_close_state( - enabled, - bj_hour, - now_ms=now_ms, - tz_name=tz_name, - has_active_positions=has_active_positions, - ) - } - - -def apply_force_close_to_payload( - payload: dict[str, Any], - *, - enabled: bool, - bj_hour: Any, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, -) -> None: - """为 active 持仓 JSON 附加整点强制清仓倒计时.""" - state = build_force_close_state( - enabled, - bj_hour, - now_ms=now_ms, - tz_name=tz_name, - has_active_positions=True, - ) - payload["force_close_enabled"] = bool(state["enabled"]) - payload["force_close_bj_hour"] = state["bj_hour"] - payload["force_close_at_ms"] = state["next_at_ms"] - payload["force_close_label"] = state["label"] if state["enabled"] else "" - payload["force_close_remaining_sec"] = state["remaining_sec"] - payload["force_close_countdown"] = state["countdown"] - payload["force_close_active"] = bool(state["active"]) - - -def enrich_orders_force_close( - orders: list[dict[str, Any]], - enabled: bool, - bj_hour: Any, - *, - now_ms: Optional[int] = None, - tz_name: Optional[str] = None, -) -> None: - if not enabled or not orders: - return - for item in orders: - if isinstance(item, dict): - apply_force_close_to_payload( - item, - enabled=enabled, - bj_hour=bj_hour, - now_ms=now_ms, - tz_name=tz_name, - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/manual_sltp_lib.py b/lib/trade/manual_sltp_lib.py index 41a5783..5a2affa 100644 --- a/lib/trade/manual_sltp_lib.py +++ b/lib/trade/manual_sltp_lib.py @@ -1,136 +1,12 @@ -"""实盘人工下单:止盈止损模式(价格 / 百分比 / 固定盈亏比).""" -from __future__ import annotations - -from typing import Any, Optional, Tuple - -MANUAL_FIXED_RR_DEFAULT = 1.5 - -SLTP_MODE_PRICE = "price" -SLTP_MODE_PCT = "pct" -SLTP_MODE_FIXED_RR = "fixed_rr" - -OPEN_SLTP_MODES = frozenset({SLTP_MODE_PRICE, SLTP_MODE_PCT, SLTP_MODE_FIXED_RR}) -ENTRUST_SLTP_MODES = frozenset({SLTP_MODE_PRICE, SLTP_MODE_PCT}) - - -def normalize_open_sltp_mode(raw: Optional[str]) -> str: - mode = (raw or SLTP_MODE_FIXED_RR).strip().lower() - if mode in OPEN_SLTP_MODES: - return mode - return SLTP_MODE_PRICE - - -def normalize_entrust_sltp_mode(raw: Optional[str]) -> str: - mode = (raw or SLTP_MODE_PRICE).strip().lower() - if mode in ENTRUST_SLTP_MODES: - return mode - return SLTP_MODE_PRICE - - -def parse_fixed_rr(raw: Any, *, default: float = MANUAL_FIXED_RR_DEFAULT) -> float: - try: - v = float(raw) - if v > 0: - return v - except (TypeError, ValueError): - pass - return float(default) - - -def calc_tp_from_fixed_rr( - direction: str, - entry_price: float, - stop_loss: float, - rr_ratio: float, -) -> float: - entry = float(entry_price) - sl = float(stop_loss) - rr = float(rr_ratio) - if entry <= 0 or sl <= 0 or rr <= 0: - raise ValueError("固定盈亏比参数无效") - side = (direction or "long").strip().lower() - if side == "short": - risk = sl - entry - if risk <= 0: - raise ValueError("止损方向不合法:做空时止损须高于入场价") - return entry - risk * rr - risk = entry - sl - if risk <= 0: - raise ValueError("止损方向不合法:做多时止损须低于入场价") - return entry + risk * rr - - -def _resolve_pct_sltp(direction: str, live_price: float, data: dict[str, Any]) -> Tuple[float, float]: - sl_pct = float(data.get("sl_pct") or 0) - tp_pct = float(data.get("tp_pct") or 0) - if sl_pct <= 0 or tp_pct <= 0: - raise ValueError("百分比止盈止损须为正数") - sl_ratio = sl_pct / 100.0 - tp_ratio = tp_pct / 100.0 - entry = float(live_price) - if (direction or "long").strip().lower() == "short": - stop_loss = entry * (1 + sl_ratio) - take_profit = entry * (1 - tp_ratio) - else: - stop_loss = entry * (1 - sl_ratio) - take_profit = entry * (1 + tp_ratio) - return stop_loss, take_profit - - -def _resolve_price_sltp( - data: dict[str, Any], - *, - fallback_sl: Optional[float] = None, - fallback_tp: Optional[float] = None, - require_tp: bool = True, -) -> Tuple[float, float]: - stop_loss = float(data.get("sl") or data.get("stop_loss") or 0) - take_profit = float(data.get("tp") or data.get("take_profit") or data.get("tgt") or 0) - if stop_loss <= 0 and fallback_sl is not None: - stop_loss = float(fallback_sl) - if take_profit <= 0 and fallback_tp is not None: - take_profit = float(fallback_tp) - if stop_loss <= 0: - raise ValueError("止损价格须大于 0" if require_tp else "请填写止损价格") - if require_tp and take_profit <= 0: - raise ValueError("止盈止损价格须大于 0" if fallback_tp is None else "请填写止盈价格,或保留原计划止盈") - return stop_loss, take_profit - - -def resolve_open_sltp_prices( - direction: str, - live_price: float, - sltp_mode: Optional[str], - data: dict[str, Any], -) -> Tuple[float, float]: - """新开仓 /add_order:支持 price,pct,fixed_rr.""" - mode = normalize_open_sltp_mode(sltp_mode) - if mode == SLTP_MODE_PCT: - return _resolve_pct_sltp(direction, live_price, data) - if mode == SLTP_MODE_FIXED_RR: - stop_loss, _ = _resolve_price_sltp(data, require_tp=False) - rr = parse_fixed_rr(data.get("fixed_rr")) - take_profit = calc_tp_from_fixed_rr(direction, live_price, stop_loss, rr) - return stop_loss, take_profit - return _resolve_price_sltp(data, require_tp=True) - - -def resolve_entrust_sltp_prices( - direction: str, - live_price: float, - sltp_mode: Optional[str], - data: dict[str, Any], - *, - fallback_sl: Optional[float] = None, - fallback_tp: Optional[float] = None, -) -> Tuple[float, float]: - """持仓委托弹窗:仅 price / pct,不校验盈亏比.""" - mode = normalize_entrust_sltp_mode(sltp_mode) - if mode == SLTP_MODE_PCT: - return _resolve_pct_sltp(direction, live_price, data) - return _resolve_price_sltp( - data, - fallback_sl=fallback_sl, - fallback_tp=fallback_tp, - require_tp=True, - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/order_monitor_display_lib.py b/lib/trade/order_monitor_display_lib.py index 9b23cc8..685e803 100644 --- a/lib/trade/order_monitor_display_lib.py +++ b/lib/trade/order_monitor_display_lib.py @@ -1,452 +1,12 @@ -"""实时持仓展示:开仓快照盈亏比,交易所止损是否已保本.""" -from __future__ import annotations - -from typing import Any, Callable, Optional - - -def _positive_float(value: Any) -> Optional[float]: - try: - v = float(value) - return v if v > 0 else None - except (TypeError, ValueError): - return None - - -def snapshot_stop_loss(initial_stop_loss: Any, stop_loss: Any) -> Optional[float]: - """展示盈亏比 / 交易记录时优先用开仓时止损快照,不用后续改单后的止损.""" - sl = _positive_float(initial_stop_loss) - if sl is not None: - return sl - return _positive_float(stop_loss) - - -def monitor_open_stop_loss(row: Any) -> Optional[float]: - """从 order_monitors 行取开仓止损快照.""" - try: - keys = row.keys() if hasattr(row, "keys") else () - except Exception: - keys = () - init = row["initial_stop_loss"] if "initial_stop_loss" in keys else None - cur = row["stop_loss"] if "stop_loss" in keys else None - if init is None and isinstance(row, dict): - init = row.get("initial_stop_loss") - cur = row.get("stop_loss") - return snapshot_stop_loss(init, cur) - - -def snapshot_rr( - calc_rr_ratio_fn: Callable[..., Optional[float]], - direction: str, - trigger_price: Any, - initial_stop_loss: Any, - stop_loss: Any, - take_profit: Any, -) -> Optional[float]: - entry = _positive_float(trigger_price) - sl = snapshot_stop_loss(initial_stop_loss, stop_loss) - tp = _positive_float(take_profit) - if entry is None or sl is None or tp is None: - return None - return calc_rr_ratio_fn(direction or "long", entry, sl, tp) - - -def tpsl_slot_trigger_price(slot: Any) -> Optional[float]: - if not isinstance(slot, dict): - return None - for key in ("trigger_price", "trigger_display"): - v = _positive_float(slot.get(key)) - if v is not None: - return v - return None - - -def stop_is_profit_protecting(direction: str, entry_price: Any, stop_loss: Any) -> bool: - """ - 止损是否已在盈利侧(保本/锁盈),不再适用「开仓盈亏比」风控. - 做空:止损 < 成交价;做多:止损 > 成交价. - """ - entry = _positive_float(entry_price) - sl = _positive_float(stop_loss) - if entry is None or sl is None: - return False - d = (direction or "long").strip().lower() - if d == "short": - return sl < entry - return sl > entry - - -def tpsl_update_passes_rr_gate( - direction: str, - entry_price: Any, - stop_loss: Any, - take_profit: Any, - min_rr: float, - calc_rr_ratio_fn: Callable[..., Optional[float]], -) -> tuple[bool, Optional[str]]: - """持仓委托改价:盈利侧止损跳过最低盈亏比;否则按开仓价几何校验.""" - if stop_is_profit_protecting(direction, entry_price, stop_loss): - return True, None - rr = calc_rr_ratio_fn(direction or "long", entry_price, stop_loss, take_profit) - if rr is not None and rr >= float(min_rr): - return True, None - rr_txt = f"{rr:.4f}" if rr is not None else "无法计算" - return False, f"计划盈亏比 {rr_txt}:1 低于最低要求 {min_rr}:1(盈利侧保本止损不受此限)" - - -def resolve_breakeven_entry_price(entry_price: Any, avg_entry_price: Any = None) -> Optional[float]: - """保本判断基准价:有持仓加权均价时优先(滚仓后),否则用首仓成交价.""" - avg = _positive_float(avg_entry_price) - if avg is not None: - return avg - return _positive_float(entry_price) - - -def stale_breakeven_armed(direction: str, entry_price: Any, stop_loss: Any, breakeven_armed: Any) -> bool: - """止损已回到亏损侧时 breakeven_armed 视为过期(如滚仓下移止损).""" - try: - armed = int(breakeven_armed or 0) != 0 - except (TypeError, ValueError): - return False - if not armed: - return False - return not stop_is_profit_protecting(direction, entry_price, stop_loss) - - -def is_sl_breakeven_secured(direction: str, entry_price: Any, exchange_sl_price: Any) -> bool: - """ - 交易所当前止损相对开仓成交价是否已保本. - 做多:止损 >= 成交价;做空:止损 <= 成交价. - """ - entry = _positive_float(entry_price) - sl = _positive_float(exchange_sl_price) - if entry is None or sl is None: - return False - d = (direction or "long").strip().lower() - if d == "short": - return sl <= entry - return sl >= entry - - -def sl_breakeven_from_exchange_tpsl( - direction: str, - entry_price: Any, - exchange_tpsl: Any, -) -> bool: - if not isinstance(exchange_tpsl, dict): - return False - sl_px = tpsl_slot_trigger_price(exchange_tpsl.get("sl")) - if sl_px is None: - return False - return is_sl_breakeven_secured(direction, entry_price, sl_px) - - -def enrich_order_display_fields(item: dict[str, Any], calc_rr_ratio_fn: Callable[..., Optional[float]]) -> dict[str, Any]: - item["rr_ratio"] = snapshot_rr( - calc_rr_ratio_fn, - item.get("direction") or "long", - item.get("trigger_price"), - item.get("initial_stop_loss"), - item.get("stop_loss"), - item.get("take_profit"), - ) - return item - - -def apply_order_live_price_display( - payload: dict[str, Any], - symbol: Any, - ticker_price: Any, - exchange_mark_price: Any, - format_price_fn: Callable[[Any, Any], str], -) -> dict[str, Any]: - """标记价/现价展示:与交易所 price_to_precision 对齐,避免前端 toFixed(8).""" - px_for_fmt = ticker_price - mark_raw = exchange_mark_price - if mark_raw is not None: - try: - px_for_fmt = float(mark_raw) - except (TypeError, ValueError): - pass - px_disp = format_price_fn(symbol, px_for_fmt) - payload["price_display"] = px_disp - if mark_raw is not None: - try: - payload["exchange_mark_price_display"] = format_price_fn(symbol, float(mark_raw)) - except (TypeError, ValueError): - payload["exchange_mark_price_display"] = px_disp - else: - payload["exchange_mark_price_display"] = None - return payload - - -def resolve_live_tpsl_prices( - plan_sl: Any, - plan_tp: Any, - exchange_tpsl: Any, -) -> tuple[Optional[float], Optional[float], Optional[float], Optional[float]]: - """返回 (展示用止损, 展示用止盈, 交易所止损, 交易所止盈).""" - ex_sl = ex_tp = None - if isinstance(exchange_tpsl, dict): - ex_sl = tpsl_slot_trigger_price(exchange_tpsl.get("sl")) - ex_tp = tpsl_slot_trigger_price(exchange_tpsl.get("tp")) - disp_sl = ex_sl if ex_sl is not None else _positive_float(plan_sl) - disp_tp = ex_tp if ex_tp is not None else _positive_float(plan_tp) - return disp_sl, disp_tp, ex_sl, ex_tp - - -def calc_risk_fraction(direction: str, entry_price: Any, stop_loss: Any) -> Optional[float]: - """|入场-止损|/入场;盈利侧止损返回 0.""" - entry = _positive_float(entry_price) - sl = _positive_float(stop_loss) - if entry is None or sl is None: - return None - d = (direction or "long").strip().lower() - if d == "short": - risk = sl - entry - else: - risk = entry - sl - if risk <= 0: - return 0.0 - return risk / entry - - -def calc_latest_risk_amount( - direction: str, - entry_price: Any, - stop_loss: Any, - *, - margin_capital: Any = None, - leverage: Any = None, - exchange_notional: Any = None, - contracts: Any = None, - contract_size: Any = None, - mark_price: Any = None, - funds_decimals: int = 2, -) -> Optional[float]: - """按当前止损与持仓名义价值估算最新风险(U).""" - rf = calc_risk_fraction(direction, entry_price, stop_loss) - if rf is None: - return None - if rf <= 0: - return 0.0 - notional = _positive_float(exchange_notional) - if notional is None: - try: - mc = float(margin_capital or 0) - lev = float(leverage or 0) - if mc > 0 and lev > 0: - notional = mc * lev - except (TypeError, ValueError): - pass - if notional is None: - try: - c = abs(float(contracts or 0)) - cs = float(contract_size or 1) - if cs <= 0: - cs = 1.0 - px = _positive_float(mark_price) or _positive_float(entry_price) - if c > 0 and px is not None: - notional = c * cs * px - except (TypeError, ValueError): - pass - if notional is None or notional <= 0: - return None - return round(notional * rf, funds_decimals) - - -def order_monitor_tpsl_needs_sync( - plan_sl: Any, - plan_tp: Any, - exchange_tpsl: Any, - *, - eps: float = 1e-12, -) -> tuple[Optional[float], Optional[float], bool]: - """若交易所 TP/SL 与库中不一致,返回应写回的 (sl, tp) 及是否需更新.""" - _, _, ex_sl, ex_tp = resolve_live_tpsl_prices(plan_sl, plan_tp, exchange_tpsl) - try: - cur_sl = float(plan_sl or 0) - cur_tp = float(plan_tp or 0) - except (TypeError, ValueError): - cur_sl, cur_tp = 0.0, 0.0 - new_sl = ex_sl if ex_sl is not None else cur_sl - new_tp = ex_tp if ex_tp is not None else cur_tp - changed = ( - (ex_sl is not None and abs(new_sl - cur_sl) > eps) - or (ex_tp is not None and abs(new_tp - cur_tp) > eps) - ) - return new_sl, new_tp, changed - - -def apply_order_price_display_fields( - payload: dict[str, Any], - *, - direction: str, - entry_price: Any, - initial_stop_loss: Any, - stop_loss: Any, - take_profit: Any, - calc_rr_ratio_fn: Callable[..., Optional[float]], - exchange_tpsl: Any = None, - format_price_fn: Optional[Callable[[Any, Any], str]] = None, - symbol: Any = None, - margin_capital: Any = None, - leverage: Any = None, - exchange_notional: Any = None, - contracts: Any = None, - contract_size: Any = None, - mark_price: Any = None, - avg_entry_price: Any = None, - funds_decimals: int = 2, -) -> dict[str, Any]: - disp_sl, disp_tp, _, _ = resolve_live_tpsl_prices(stop_loss, take_profit, exchange_tpsl) - payload["stop_loss_raw"] = _positive_float(stop_loss) - payload["take_profit_raw"] = _positive_float(take_profit) - payload["rr_ratio"] = snapshot_rr( - calc_rr_ratio_fn, - direction, - entry_price, - initial_stop_loss, - stop_loss, - take_profit, - ) - risk_entry = resolve_breakeven_entry_price(entry_price, avg_entry_price) - payload["avg_entry_price"] = risk_entry - payload["sl_breakeven_secured"] = sl_breakeven_from_exchange_tpsl( - direction, risk_entry, exchange_tpsl - ) - payload["stop_loss"] = disp_sl - payload["take_profit"] = disp_tp - if disp_sl is not None and disp_tp is not None: - payload["display_rr_ratio"] = calc_rr_ratio_fn( - direction or "long", entry_price, disp_sl, disp_tp - ) - else: - payload["display_rr_ratio"] = None - if contracts is not None: - try: - from lib.hub.hub_position_metrics import normalize_contracts_qty - - c = normalize_contracts_qty(contracts) - if c > 0: - payload["contracts"] = c - except (TypeError, ValueError): - pass - payload["latest_risk_amount"] = calc_latest_risk_amount( - direction, - risk_entry, - disp_sl if disp_sl is not None else stop_loss, - margin_capital=margin_capital, - leverage=leverage, - exchange_notional=exchange_notional, - contracts=payload.get("contracts") if payload.get("contracts") is not None else contracts, - contract_size=contract_size, - mark_price=mark_price, - funds_decimals=funds_decimals, - ) - tp_for_reward = disp_tp if disp_tp is not None else _positive_float(take_profit) - qty_for_reward = payload.get("contracts") - if qty_for_reward is None and contracts is not None: - try: - qty_for_reward = abs(float(contracts)) - except (TypeError, ValueError): - qty_for_reward = None - if risk_entry is not None and tp_for_reward is not None and qty_for_reward: - try: - from lib.strategy.strategy_roll_ui_lib import reward_at_tp_usdt - - reward = reward_at_tp_usdt( - direction, - risk_entry, - tp_for_reward, - float(qty_for_reward), - contract_size=float(contract_size or 1.0), - ) - payload["reward_at_tp_usdt"] = ( - round(reward, funds_decimals) if reward is not None else None - ) - except Exception: - payload["reward_at_tp_usdt"] = None - else: - payload["reward_at_tp_usdt"] = None - if format_price_fn is not None and symbol is not None: - payload["stop_loss_display"] = ( - format_price_fn(symbol, disp_sl) if disp_sl is not None else "—" - ) - payload["take_profit_display"] = ( - format_price_fn(symbol, disp_tp) if disp_tp is not None else "—" - ) - mark_raw = mark_price if mark_price is not None else None - if mark_raw is not None and format_price_fn is not None and symbol is not None: - try: - payload["exchange_mark_price_display"] = format_price_fn(symbol, float(mark_raw)) - except (TypeError, ValueError): - payload["exchange_mark_price_display"] = None - return payload - - -def enrich_active_monitor_tpsl_json( - row: Any, - stop_loss: Any, - take_profit: Any, - exchange_tpsl: Any, - *, - position_row: Any = None, - exchange_notional: Any = None, - contracts: Any = None, - contract_size: float = 1.0, - mark_price: Any = None, - calc_rr_ratio_fn: Callable[..., Optional[float]], - format_price_fn: Optional[Callable[[Any, Any], str]] = None, - symbol: Any = None, - funds_decimals: int = 2, -) -> dict[str, Any]: - """place_tpsl 响应:展示用 TP/SL,最新风险,当前盈亏比.""" - def _row_val(key: str, default=None): - try: - if hasattr(row, "keys") and key in row.keys(): - return row[key] - except Exception: - pass - if isinstance(row, dict): - return row.get(key, default) - return default - - direction = _row_val("direction") or "long" - entry = _row_val("trigger_price") - init_sl = _row_val("initial_stop_loss") - margin = _row_val("margin_capital") - leverage = _row_val("leverage") - if position_row is not None: - from lib.hub.hub_position_metrics import parse_position_entry_price, position_contracts - - live_c = position_contracts(position_row) - if abs(live_c) >= 1e-12: - contracts = abs(live_c) - avg_entry = parse_position_entry_price(position_row) - else: - avg_entry = None - payload: dict[str, Any] = { - "stop_loss": stop_loss, - "take_profit": take_profit, - } - apply_order_price_display_fields( - payload, - direction=direction, - entry_price=entry, - initial_stop_loss=init_sl, - stop_loss=stop_loss, - take_profit=take_profit, - calc_rr_ratio_fn=calc_rr_ratio_fn, - exchange_tpsl=exchange_tpsl, - format_price_fn=format_price_fn, - symbol=symbol or _row_val("symbol"), - margin_capital=margin, - leverage=leverage, - exchange_notional=exchange_notional, - contracts=contracts, - contract_size=contract_size, - mark_price=mark_price, - avg_entry_price=avg_entry, - funds_decimals=funds_decimals, - ) - return payload +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/position_sizing_lib.py b/lib/trade/position_sizing_lib.py index 8f5ae28..ba76fe9 100644 --- a/lib/trade/position_sizing_lib.py +++ b/lib/trade/position_sizing_lib.py @@ -1,136 +1,12 @@ -""" -三所共用:计仓模式 risk(以损定仓)| full_margin(全仓杠杆). -仅 env POSITION_SIZING_MODE 切换;须无持仓(由部署流程保证). -""" -from __future__ import annotations - -import os -from typing import Any, Optional, Tuple - -MODE_RISK = "risk" -MODE_FULL_MARGIN = "full_margin" -VALID_MODES = frozenset({MODE_RISK, MODE_FULL_MARGIN}) - -OPEN_SOURCE_MANUAL = "manual" -OPEN_SOURCE_KEY_AUTO = "key_auto" -OPEN_SOURCE_KEY_FIB = "key_fib" -OPEN_SOURCE_KEY_TRIGGER = "key_trigger" -OPEN_SOURCE_TREND = "trend" -OPEN_SOURCE_ROLL = "roll" - -FULL_MARGIN_BLOCKED_SOURCES = frozenset( - {OPEN_SOURCE_KEY_AUTO, OPEN_SOURCE_KEY_FIB, OPEN_SOURCE_TREND, OPEN_SOURCE_ROLL} -) - - -def normalize_position_sizing_mode(raw: Optional[str]) -> str: - v = (raw or MODE_RISK).strip().lower() - if v in ("full", "full_margin", "fullmargin", "全仓", "全仓杠杆"): - return MODE_FULL_MARGIN - return MODE_RISK if v in ("risk", "r", "以损定仓", "") else MODE_RISK - - -def load_position_sizing_mode(env: Optional[dict] = None) -> str: - e = env if env is not None else os.environ - return normalize_position_sizing_mode(e.get("POSITION_SIZING_MODE")) - - -def is_full_margin_mode(mode: str) -> bool: - return normalize_position_sizing_mode(mode) == MODE_FULL_MARGIN - - -def mode_label_zh(mode: str) -> str: - return "全仓杠杆" if is_full_margin_mode(mode) else "以损定仓" - - -def leverage_for_full_margin(symbol: str, btc_leverage: int, alt_leverage: int) -> int: - sym = (symbol or "").strip().upper() - if sym.startswith("BTC") or sym.startswith("ETH"): - return max(1, int(btc_leverage or 10)) - return max(1, int(alt_leverage or 5)) - - -def round_funds(value: float, decimals: int = 2) -> float: - return round(float(value), int(decimals)) - - -def risk_percent_for_storage(mode: str, risk_percent: float) -> Optional[float]: - """全仓杠杆:库内不写风险百分比(仅 risk_amount U).""" - if is_full_margin_mode(mode): - return None - return risk_percent - - -def format_risk_display_text( - mode: str, - risk_percent: Optional[float], - risk_amount: Optional[float], - *, - decimals: int = 2, -) -> str: - """持仓/通知「风险」文案:全仓仅 U;以损定仓为 %≈U.""" - amt: Optional[float] = None - if risk_amount is not None and risk_amount != "": - try: - amt = float(risk_amount) - except (TypeError, ValueError): - amt = None - if is_full_margin_mode(mode): - if amt is None: - return "—" - return f"{round_funds(amt, decimals)}U" - pct: Optional[float] = None - if risk_percent is not None and risk_percent != "": - try: - pct = float(risk_percent) - except (TypeError, ValueError): - pct = None - pct_txt = f"{pct:g}" if pct is not None else "—" - amt_txt = round_funds(amt, decimals) if amt is not None else "—" - return f"{pct_txt}%≈{amt_txt}U" - - -def assert_open_source_allowed(mode: str, source: str) -> Tuple[bool, str]: - if not is_full_margin_mode(mode): - return True, "" - src = (source or "").strip().lower() - if src in FULL_MARGIN_BLOCKED_SOURCES: - return False, ( - "当前为全仓杠杆模式(POSITION_SIZING_MODE=full_margin)," - "不允许关键位突破/斐波自动开仓,趋势回调与顺势加仓;" - "仅支持实盘人工下单与阻力/支撑提醒." - ) - return True, "" - - -def full_margin_requires_flat_position(active_count: int) -> Tuple[bool, str]: - if active_count > 0: - return False, "全仓杠杆模式仅允许单仓且无其它持仓,请先平仓后再开仓" - return True, "" - - -def compute_full_margin_sizing( - *, - symbol: str, - available_usdt: float, - capital_base: float, - buffer_ratio: float, - btc_leverage: int, - alt_leverage: int, - funds_decimals: int = 2, -) -> Tuple[Optional[dict[str, Any]], Optional[str]]: - if available_usdt is None or float(available_usdt) <= 0: - return None, "全仓杠杆:无法读取合约账户可用保证金" - lev = leverage_for_full_margin(symbol, btc_leverage, alt_leverage) - margin = round_funds(float(available_usdt) * float(buffer_ratio), funds_decimals) - if margin <= 0: - return None, "全仓杠杆:可用保证金不足" - notional = round_funds(margin * lev, funds_decimals) - ratio = round(margin / float(capital_base) * 100, 2) if capital_base else 0.0 - return { - "margin_capital": margin, - "leverage": lev, - "notional_value": notional, - "position_ratio": ratio, - "mode": MODE_FULL_MARGIN, - }, None +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/time_close_lib.py b/lib/trade/time_close_lib.py index 96968a3..11c6a44 100644 --- a/lib/trade/time_close_lib.py +++ b/lib/trade/time_close_lib.py @@ -1,150 +1,12 @@ -"""持仓时间平仓:开仓后按 1h/2h/4h 定时市价平仓.""" -from __future__ import annotations - -import time -from typing import Any, Optional - -ALLOWED_TIME_CLOSE_HOURS = (1, 2, 4) -TIME_CLOSE_RESULT = "时间平仓" - - -def parse_time_close_enabled_form(form_value: Any) -> int: - return 1 if str(form_value or "").strip().lower() in ("1", "true", "on", "yes") else 0 - - -def parse_time_close_hours_form(form_value: Any, *, default: int = 4) -> Optional[int]: - raw = str(form_value or "").strip().lower().rstrip("h") - if not raw: - return None - try: - h = int(float(raw)) - except (TypeError, ValueError): - return None - if h in ALLOWED_TIME_CLOSE_HOURS: - return h - return None - - -def normalize_time_close_hours(value: Any) -> Optional[int]: - try: - h = int(value) - except (TypeError, ValueError): - return None - return h if h in ALLOWED_TIME_CLOSE_HOURS else None - - -def _row_val(row: Any, key: str, default=None): - if row is None: - return default - try: - if hasattr(row, "keys") and key in row.keys(): - return row[key] - except Exception: - pass - if isinstance(row, dict): - return row.get(key, default) - return default - - -def time_close_settings_from_row(row: Any) -> tuple[int, Optional[int], Optional[int]]: - """返回 (enabled, hours, close_at_ms).""" - enabled = int(_row_val(row, "time_close_enabled", 0) or 0) != 0 - hours = normalize_time_close_hours(_row_val(row, "time_close_hours")) - close_at = _row_val(row, "time_close_at_ms") - try: - close_at_ms = int(close_at) if close_at not in (None, "") else None - except (TypeError, ValueError): - close_at_ms = None - if enabled and hours and not close_at_ms: - opened_ms = _row_val(row, "opened_at_ms") - try: - opened_ms = int(opened_ms) if opened_ms not in (None, "") else None - except (TypeError, ValueError): - opened_ms = None - close_at_ms = compute_close_at_ms(opened_ms, hours) - return (1 if enabled and hours else 0, hours, close_at_ms) - - -def compute_close_at_ms(opened_at_ms: Any, hours: Any) -> Optional[int]: - h = normalize_time_close_hours(hours) - try: - opened = int(opened_at_ms) - except (TypeError, ValueError): - return None - if not h or opened <= 0: - return None - return opened + h * 3600 * 1000 - - -def should_trigger_time_close(row: Any, *, now_ms: Optional[int] = None) -> bool: - enabled, hours, close_at_ms = time_close_settings_from_row(row) - if not enabled or not close_at_ms: - return False - now = int(now_ms if now_ms is not None else time.time() * 1000) - return now >= int(close_at_ms) - - -def time_close_remaining_seconds(close_at_ms: Any, *, now_ms: Optional[int] = None) -> Optional[int]: - try: - close_at = int(close_at_ms) - except (TypeError, ValueError): - return None - now = int(now_ms if now_ms is not None else time.time() * 1000) - return max(0, int((close_at - now) / 1000)) - - -def format_time_close_countdown(seconds: Any) -> str: - try: - sec = max(0, int(seconds)) - except (TypeError, ValueError): - return "--:--:--" - h = sec // 3600 - m = (sec % 3600) // 60 - s = sec % 60 - return f"{h:02d}:{m:02d}:{s:02d}" - - -def time_close_label(hours: Any) -> str: - h = normalize_time_close_hours(hours) - return f"时间平仓 {h}h" if h else "时间平仓" - - -def apply_time_close_to_payload(payload: dict[str, Any], row: Any, *, now_ms: Optional[int] = None) -> None: - enabled, hours, close_at_ms = time_close_settings_from_row(row) - payload["time_close_enabled"] = bool(enabled) - payload["time_close_hours"] = hours - payload["time_close_at_ms"] = close_at_ms - payload["time_close_label"] = time_close_label(hours) if enabled else "" - if enabled and close_at_ms: - rem = time_close_remaining_seconds(close_at_ms, now_ms=now_ms) - payload["time_close_remaining_sec"] = rem - payload["time_close_countdown"] = format_time_close_countdown(rem) - else: - payload["time_close_remaining_sec"] = None - payload["time_close_countdown"] = "" - - -def ensure_time_close_schema(cursor) -> None: - ddl_list = ( - "ALTER TABLE order_monitors ADD COLUMN time_close_enabled INTEGER DEFAULT 0", - "ALTER TABLE order_monitors ADD COLUMN time_close_hours INTEGER", - "ALTER TABLE order_monitors ADD COLUMN time_close_at_ms INTEGER", - "ALTER TABLE key_monitors ADD COLUMN time_close_enabled INTEGER DEFAULT 0", - "ALTER TABLE key_monitors ADD COLUMN time_close_hours INTEGER", - ) - for ddl in ddl_list: - try: - cursor.execute(ddl) - except Exception: - pass - - -def time_close_insert_values( - enabled: int, - hours: Optional[int], - opened_at_ms: Optional[int], -) -> tuple[int, Optional[int], Optional[int]]: - en = 1 if int(enabled or 0) != 0 and hours else 0 - h = normalize_time_close_hours(hours) if en else None - close_at = compute_close_at_ms(opened_at_ms, h) if en else None - return en, h, close_at +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/trade_exchange_stats_lib.py b/lib/trade/trade_exchange_stats_lib.py index 7158f14..ea65233 100644 --- a/lib/trade/trade_exchange_stats_lib.py +++ b/lib/trade/trade_exchange_stats_lib.py @@ -1,229 +1,12 @@ -"""平仓交易:交易所口径双边成交额与手续费(三所共用聚合逻辑).""" -from __future__ import annotations - -from typing import Any, Callable, Optional - - -def _coerce_ts_ms(raw: Any) -> int | None: - if raw in (None, ""): - return None - try: - v = int(raw) - return v if v > 1_000_000_000_000 else v * 1000 - except (TypeError, ValueError): - return None - - -def quote_turnover_usdt_from_fill(trade: dict, *, contract_size: float = 1.0) -> float: - """单笔成交的报价币成交额(USDT 口径).""" - info = trade.get("info") or {} - if not isinstance(info, dict): - info = {} - for key in ("quoteQty", "quote_qty", "fillNotionalUsd", "notional"): - try: - v = float(info.get(key) or 0) - if v > 0: - return abs(v) - except (TypeError, ValueError): - continue - try: - cost = float(trade.get("cost") or 0) - if cost > 0: - return abs(cost) - except (TypeError, ValueError): - pass - try: - price = float(trade.get("price") or 0) - amount = float(trade.get("amount") or 0) * float(contract_size or 1.0) - if price > 0 and amount > 0: - return abs(price * amount) - except (TypeError, ValueError): - pass - return 0.0 - - -def commission_usdt_from_fill(trade: dict) -> float: - """单笔成交手续费(正数表示成本).""" - fee = trade.get("fee") - if isinstance(fee, dict): - try: - cost = float(fee.get("cost") or 0) - except (TypeError, ValueError): - cost = 0.0 - if cost != 0: - cur = str(fee.get("currency") or "USDT").upper() - if cur in ("USDT", "USD", "BUSD", "USDC"): - return abs(cost) - return abs(cost) - info = trade.get("info") or {} - if isinstance(info, dict): - for key in ("fee", "commission", "fillFee"): - try: - v = float(info.get(key) or 0) - if v != 0: - return abs(v) - except (TypeError, ValueError): - continue - return 0.0 - - -def aggregate_bilateral_stats( - fills: list[dict], - *, - contract_size: float = 1.0, -) -> dict[str, float] | None: - """双边成交额 = 开+平所有相关 fill 的报价币成交额之和;手续费 = fill fee 之和.""" - if not fills: - return None - turnover = 0.0 - commission = 0.0 - for t in fills: - turnover += quote_turnover_usdt_from_fill(t, contract_size=contract_size) - commission += commission_usdt_from_fill(t) - if turnover <= 0 and commission <= 0: - return None - return { - "exchange_turnover_usdt": round(turnover, 4), - "exchange_commission_usdt": round(commission, 4), - } - - -def filter_position_lifecycle_fills( - trades: list[dict], - direction: str, - open_ms: int | None, - close_ms: int | None, - *, - hedge_mode: bool = False, - close_buffer_ms: int = 15 * 60 * 1000, -) -> list[dict]: - """ - 持仓生命周期内 fill:多=开买+平卖;空=开卖+平买. - hedge_mode 时按 posSide 与 direction 过滤. - """ - direction = (direction or "long").strip().lower() - open_side = "buy" if direction == "long" else "sell" - close_side = "sell" if direction == "long" else "buy" - allowed_sides = {open_side, close_side} - upper = int(close_ms) + int(close_buffer_ms) if close_ms else None - out: list[dict] = [] - for t in trades or []: - side = (t.get("side") or "").lower() - if side not in allowed_sides: - continue - ts = _coerce_ts_ms(t.get("timestamp")) - if ts is None: - continue - if open_ms and ts < int(open_ms) - 60_000: - continue - if upper and ts > upper: - continue - if hedge_mode: - info = t.get("info") or {} - if not isinstance(info, dict): - info = {} - pos_side = (info.get("posSide") or t.get("posSide") or "").lower() - if pos_side in ("long", "short") and pos_side != direction: - continue - out.append(t) - out.sort(key=lambda x: x.get("timestamp") or 0) - return out - - -def sum_binance_commission_income(entries: list[dict], trade_ids: set[str] | None) -> float | None: - """Binance income 流水中 COMMISSION 合计(负值取绝对值为成本).""" - if not entries: - return None - total = 0.0 - found = False - for e in entries: - it = (e.get("incomeType") or e.get("income_type") or "").strip() - if it != "COMMISSION": - continue - if trade_ids: - tid = str(e.get("tradeId") or e.get("trade_id") or "").strip() - if tid and tid not in trade_ids: - continue - try: - total += float(e.get("income") or 0) - found = True - except (TypeError, ValueError): - continue - if not found: - return None - return round(abs(total), 4) - - -def trade_ids_from_fills(fills: list[dict]) -> set[str]: - out: set[str] = set() - for t in fills or []: - info = t.get("info") or {} - if not isinstance(info, dict): - info = {} - for key in ("id", "tradeId", "trade_id"): - raw = t.get(key) if key in t else info.get(key) - if raw is not None and str(raw).strip(): - out.add(str(raw).strip()) - break - return out - - -def merge_commission_prefer_income( - fill_commission: float, - income_commission: float | None, -) -> float: - if income_commission is not None and income_commission > 0: - return round(income_commission, 4) - return round(max(fill_commission, 0.0), 4) - - -def update_trade_record_stats_columns( - conn: Any, - trade_id: int, - turnover_usdt: float | None, - commission_usdt: float | None, -) -> None: - if turnover_usdt is None and commission_usdt is None: - return - conn.execute( - """ - UPDATE trade_records - SET exchange_turnover_usdt = COALESCE(?, exchange_turnover_usdt), - exchange_commission_usdt = COALESCE(?, exchange_commission_usdt) - WHERE id = ? - """, - (turnover_usdt, commission_usdt, int(trade_id)), - ) - - -def attach_exchange_stats_to_trade( - conn: Any, - trade_id: int, - *, - fetch_fills: Callable[[], list[dict]], - contract_size: float = 1.0, - income_commission: float | None = None, -) -> dict[str, float] | None: - """拉 fill 并写库;仅在新单平仓路径调用.""" - try: - fills = fetch_fills() or [] - except Exception: - fills = [] - stats = aggregate_bilateral_stats(fills, contract_size=contract_size) - if not stats and income_commission is None: - return None - turnover = stats.get("exchange_turnover_usdt") if stats else None - fill_comm = float(stats.get("exchange_commission_usdt") or 0) if stats else 0.0 - commission = merge_commission_prefer_income(fill_comm, income_commission) - update_trade_record_stats_columns( - conn, - trade_id, - turnover, - commission if commission > 0 else None, - ) - out = {} - if turnover is not None: - out["exchange_turnover_usdt"] = turnover - if commission > 0: - out["exchange_commission_usdt"] = commission - return out or None +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/trade_fee_lib.py b/lib/trade/trade_fee_lib.py index ed27312..eb11bb1 100644 --- a/lib/trade/trade_fee_lib.py +++ b/lib/trade/trade_fee_lib.py @@ -1,94 +1,12 @@ -"""永续估算盈亏:固定 taker 手续费(默认单边 0.05%,开+平双边). - -浮盈亏仍读交易所;本模块只服务「盈利金额 / 止盈盈利 / 推送 / 记账 pnl_amount」等估算口径. -""" -from __future__ import annotations - -import math -import os -from typing import Optional - - -def _finite(v) -> Optional[float]: - try: - f = float(v) - return f if math.isfinite(f) else None - except (TypeError, ValueError): - return None - - -def taker_fee_rate() -> float: - """单边 taker 费率,默认 0.0005(=0.05%).""" - raw = os.getenv("PERP_TAKER_FEE_RATE", "0.0005") - rate = _finite(raw) - if rate is None or rate < 0: - return 0.0005 - return rate - - -def notional_usdt(price, qty, contract_size: float = 1.0) -> Optional[float]: - """名义价值 U = 价格 × 张数 × 合约面值.""" - p = _finite(price) - q = _finite(qty) - cs = _finite(contract_size) - if p is None or q is None or p <= 0 or q <= 0: - return None - if cs is None or cs <= 0: - cs = 1.0 - return abs(q) * p * cs - - -def estimate_roundtrip_fee_usdt( - entry_price, - exit_price, - qty=None, - contract_size: float = 1.0, - *, - open_notional: float | None = None, - rate: float | None = None, -) -> float: - """开+平双边手续费(各单边 rate). - - 优先用 价×张×面值;若无张数则用 open_notional 估开仓名义, - 平仓名义按 exit/entry 缩放. - """ - fee_rate = taker_fee_rate() if rate is None else float(rate) - if fee_rate <= 0: - return 0.0 - entry = _finite(entry_price) - exit_p = _finite(exit_price) - open_n = notional_usdt(entry, qty, contract_size) if qty is not None else None - if open_n is None: - open_n = _finite(open_notional) - if open_n is None or open_n <= 0: - return 0.0 - if entry is not None and entry > 0 and exit_p is not None and exit_p > 0: - close_n = open_n * (exit_p / entry) - else: - close_n = open_n - return round(open_n * fee_rate + close_n * fee_rate, 8) - - -def net_pnl_after_fee( - gross_pnl, - entry_price, - exit_price, - qty=None, - contract_size: float = 1.0, - *, - open_notional: float | None = None, - rate: float | None = None, -) -> Optional[float]: - """毛利扣双边手续费后的净盈亏;gross 无效则返回 None.""" - gross = _finite(gross_pnl) - if gross is None: - return None - fee = estimate_roundtrip_fee_usdt( - entry_price, - exit_price, - qty, - contract_size, - open_notional=open_notional, - rate=rate, - ) - return round(gross - fee, 4) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/trade_policy_app_lib.py b/lib/trade/trade_policy_app_lib.py index 3d7adf2..7e0e56e 100644 --- a/lib/trade/trade_policy_app_lib.py +++ b/lib/trade/trade_policy_app_lib.py @@ -1,52 +1,12 @@ -"""Flask 实例接入 trade policy(三所 app.py 共用).""" -from __future__ import annotations - -from typing import Callable, Tuple - -from lib.trade.trade_policy_lib import ( - TradePolicy, - assert_direction_allowed, - assert_symbol_allowed, - assert_trade_policy_open, - trade_policy_to_dict, -) - - -def trade_policy_template_context(policy: TradePolicy) -> dict: - return trade_policy_to_dict(policy) - - -def default_symbol_for_policy(policy: TradePolicy, raw_default: str) -> str: - d = (raw_default or "BTC/USDT").strip() or "BTC/USDT" - if policy.symbol_restrict_enabled and policy.symbol_whitelist: - from lib.trade.trade_policy_lib import symbol_base_coin - - base = symbol_base_coin(d) - if base not in policy.symbol_whitelist: - return f"{policy.symbol_whitelist[0]}/USDT" - return d - - -def check_symbol_policy( - policy: TradePolicy, - symbol: str, - normalize_symbol_fn: Callable[[str], str], -) -> Tuple[bool, str]: - return assert_symbol_allowed( - policy, symbol, normalize_symbol_fn=normalize_symbol_fn - ) - - -def check_direction_policy(policy: TradePolicy, direction: str) -> Tuple[bool, str]: - return assert_direction_allowed(policy, direction) - - -def check_open_policy( - policy: TradePolicy, - symbol: str, - direction: str, - normalize_symbol_fn: Callable[[str], str], -) -> Tuple[bool, str]: - return assert_trade_policy_open( - policy, symbol, direction, normalize_symbol_fn=normalize_symbol_fn - ) +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNpNVNtv03YYrXtJ3DQmt+ayQbSWlQJFAyexEydx0iRN3CZOwG6bOImTNqkvaRLSTIN2HaWgXcTjJLSHiUnTJDZpL3tgjwhBJ21ve0AbYtK0IQbVbmzAv7DPhVV5PvrO75zvnO/3k7ISVPyyXKeChEQG6oSKe+VNPFhzMAQuc8GuoqMQAlWMIcm/Qi7UOLXiGaZbEWNtzeOROYqvH5hGPVHJiEv0AbqzWuq2qGFfy1tTuFOLHc73dZ0ikam+5pD/Nh6kFi5yG4uTv9GtwNAWd3jx3I1It6WbRE72bf9OSxP6U3LdnUoNyhR+GRucXTa/+8mXP/zy98N/Pnr30XeuHNPPz8eThoqoj7se/Pzvj7dvJPPpFGuNpe49un/lq6vu3K+gX3nFd8fHCyZDLJ/ERIHJ1nY760MoVhKLuudeMm6vCLnIEy/pzHN5QsxXdAd3twWnczCTa+srqUHBUqoaGnweM8zwi3fGi4xDz1qaTrexXCjSiyImWvOFO2N8xtrHxLpmpsj3RdKGMqZ3NQpJnfh0PebkC0gjVsCMQq5rFlXeiOR28NERjC/nLDjo2/HHkGyKNe3gXEkfKxM9+1+B/fsV3HuUwilrhyBwM5Ei/5BaymfK1FyQjUA+c6Qz5JWpa0FbG/Crq0FSGR8ISk7ExiqcbilhWh3Px03Z5XTk/esf3H/44cPPH1957969bx5ERDZnixejJcOSaBYS6qiDZ86iVbFpMReRBcpsK1V5OlXEqrp0Z2gupmLGwqsx97OJuHOg2td8oZ+OzxuTwzyDNBYKJgufTbaJIOmogf6ziu0Y9Oc5bctbuwVkic9ilmeBGTNTDvT4U3v9nQD9LjLtR6RrysfyqFqh0vV0YIR02k/L8142ifg5HxpY9LjqlJJpuKRbVLqR9rkJkzepZOypusnPebeJ6ySiftFQZJfUDrrVrVxzw3PO7Wmf79hk4M+Ru0ccBxf1JTQ8UMrwdKGIll8j9/C52jMP6ONS0+uZQ6JuBGuwvDmeQgVLVzCU5vODyNrEjLsklocjp7FlM4LxmdjNUyJaYJOxoUreadf6E04WSkYkFipjOtsJ8H/Ia4uCP8dxPFjfIbnE27CfHVUcTCGOLvi3hCF/5zTk92RsqG8+n0JjKnPIJBrK4f4DGTVpLC315zbg/thosK5MkLRCUZHGKrUeqCm2MW2/HmsOna+ij18XDjhL+Qp2utJ66U9f+HZsDPqZ11eEaWkmZ6Nco+BPvrvuz9rLyPSAUCiesSftDLXggfsCftiPTIRIZYWGfI7WKWq2A/nElKzXFcyGh+SbAUn+NLyVldjLkB/0z6f1L7DXv8QYzDNTMhGcbUO+yRbky4YBv3VUVxT0C92MCy2IlZf72YJ5axT0B7wNUrobuwDv3zrLVROIkKsYjOVGz/+j9aMB+GGFIGfWYH+WlZSSUf6EfJk1wN1yggD9ZskZBf3MJOhn2zBv88P8LAX3/2RCz+XLhtA+v2ovoVh/3AL8SYlQcOiTGfwLOPiH+9nrZw3e98L7hwnFEz8P79uVOSKrzIXTqo76yxuQbUe0fP02GvTl6IRP839c839B839J8x+CeVtIyz+g5X9EKCLCfMtgrYy8vJ8zoH9WgnxDfk9N+p6xwPxWVmW7mv6Ipp8G/Ymo5q/xwh+t+Tuv4VP7+JiGN3vwi/v4Gxp+HHC2BfkmNsFfVu7hP/b/fA++2oOf3McvafgG4M5N2I/jMuyHWQX9zDug3zK+q4ba3LK+rT7vRtSBGbPFVlXblsDe/8a450qCzhnQ9EU0fW/u83s0flLjb2r8HY3/LY3/osa/rfGngD9jgXyYEOTDhiEfJwX9yPghv6dNFF0uWBbz+hrMz2j66iukPKluSl482pim29E28R+aw+hr' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/trade_policy_lib.py b/lib/trade/trade_policy_lib.py index f7d57b9..9f95a78 100644 --- a/lib/trade/trade_policy_lib.py +++ b/lib/trade/trade_policy_lib.py @@ -1,205 +1,12 @@ -""" -三所共用:账户级方向 / 币种白名单(.env 开关,默认关闭=不限制). -""" -from __future__ import annotations - -import os -from dataclasses import dataclass -from typing import Callable, FrozenSet, Optional, Sequence, Tuple - -DIR_BOTH = "both" -DIR_LONG_ONLY = "long_only" -DIR_SHORT_ONLY = "short_only" -VALID_DIRECTION_MODES = frozenset({DIR_BOTH, DIR_LONG_ONLY, DIR_SHORT_ONLY}) - -_DIR_ALIASES = { - "both": DIR_BOTH, - "双向": DIR_BOTH, - "long": DIR_LONG_ONLY, - "long_only": DIR_LONG_ONLY, - "多": DIR_LONG_ONLY, - "仅多": DIR_LONG_ONLY, - "做多": DIR_LONG_ONLY, - "short": DIR_SHORT_ONLY, - "short_only": DIR_SHORT_ONLY, - "空": DIR_SHORT_ONLY, - "仅空": DIR_SHORT_ONLY, - "做空": DIR_SHORT_ONLY, -} - - -def _env_bool(raw: Optional[str], default: bool = False) -> bool: - if raw is None: - return default - return (raw or "").strip().lower() in ("1", "true", "yes", "on") - - -def normalize_direction_mode(raw: Optional[str]) -> str: - v = (raw or DIR_BOTH).strip().lower() - return _DIR_ALIASES.get(v, v if v in VALID_DIRECTION_MODES else DIR_BOTH) - - -def symbol_base_coin(symbol: str) -> str: - """BTC/USDT:USDT,BTC/USDT,BTC,btc -> BTC""" - s = (symbol or "").strip().upper() - if not s: - return "" - if ":" in s: - s = s.split(":", 1)[0] - if "/" in s: - return s.split("/", 1)[0].strip() - if s.endswith("USDT") and len(s) > 4: - return s[:-4] - return s - - -def parse_symbol_whitelist(raw: Optional[str]) -> Tuple[str, ...]: - if not raw or not str(raw).strip(): - return () - parts = [] - for piece in str(raw).replace(";", ",").split(","): - base = symbol_base_coin(piece.strip()) - if base and base not in parts: - parts.append(base) - return tuple(parts) - - -@dataclass(frozen=True) -class TradePolicy: - direction_restrict_enabled: bool - direction_mode: str - symbol_restrict_enabled: bool - symbol_whitelist: Tuple[str, ...] - - @property - def allows_long(self) -> bool: - if not self.direction_restrict_enabled: - return True - return self.direction_mode in (DIR_BOTH, DIR_LONG_ONLY) - - @property - def allows_short(self) -> bool: - if not self.direction_restrict_enabled: - return True - return self.direction_mode in (DIR_BOTH, DIR_SHORT_ONLY) - - -def load_trade_policy(env: Optional[dict] = None) -> TradePolicy: - e = env if env is not None else os.environ - direction_restrict = _env_bool(e.get("TRADE_DIRECTION_RESTRICT_ENABLED"), False) - symbol_restrict = _env_bool(e.get("TRADE_SYMBOL_RESTRICT_ENABLED"), False) - direction_mode = normalize_direction_mode(e.get("TRADE_DIRECTION")) - whitelist = parse_symbol_whitelist(e.get("TRADE_SYMBOL_WHITELIST")) - if symbol_restrict and not whitelist: - symbol_restrict = False - return TradePolicy( - direction_restrict_enabled=direction_restrict, - direction_mode=direction_mode, - symbol_restrict_enabled=symbol_restrict, - symbol_whitelist=whitelist, - ) - - -def direction_mode_label_zh(mode: str) -> str: - m = normalize_direction_mode(mode) - if m == DIR_LONG_ONLY: - return "仅多" - if m == DIR_SHORT_ONLY: - return "仅空" - return "双向" - - -def trade_policy_badge_parts(policy: TradePolicy) -> Tuple[str, ...]: - parts: list[str] = [] - if policy.direction_restrict_enabled: - if policy.direction_mode == DIR_LONG_ONLY: - parts.append("仅多") - elif policy.direction_mode == DIR_SHORT_ONLY: - parts.append("仅空") - if policy.symbol_restrict_enabled and policy.symbol_whitelist: - parts.append("/".join(policy.symbol_whitelist)) - return tuple(parts) - - -def trade_policy_to_dict(policy: TradePolicy) -> dict: - badges = trade_policy_badge_parts(policy) - return { - "direction_restrict_enabled": policy.direction_restrict_enabled, - "direction_mode": policy.direction_mode, - "direction_label_zh": ( - direction_mode_label_zh(policy.direction_mode) - if policy.direction_restrict_enabled - else "双向" - ), - "allows_long": policy.allows_long, - "allows_short": policy.allows_short, - "symbol_restrict_enabled": policy.symbol_restrict_enabled, - "symbol_whitelist": list(policy.symbol_whitelist), - "badge_parts": list(badges), - "badge_text": " · ".join(badges), - } - - -def normalize_open_direction(policy: TradePolicy, direction: str) -> str: - d = (direction or "long").strip().lower() - if d not in ("long", "short"): - d = "long" - if policy.direction_restrict_enabled: - if policy.direction_mode == DIR_LONG_ONLY: - return "long" - if policy.direction_mode == DIR_SHORT_ONLY: - return "short" - return d - - -def assert_direction_allowed(policy: TradePolicy, direction: str) -> Tuple[bool, str]: - d = (direction or "").strip().lower() - if d not in ("long", "short"): - if d in ("watch", ""): - return True, "" - return False, "方向无效,请选择做多或做空" - if d == "long" and not policy.allows_long: - return False, "当前账户配置为仅做空,不允许做多" - if d == "short" and not policy.allows_short: - return False, "当前账户配置为仅做多,不允许做空" - return True, "" - - -def assert_symbol_allowed( - policy: TradePolicy, - symbol: str, - *, - normalize_symbol_fn: Optional[Callable[[str], str]] = None, -) -> Tuple[bool, str]: - if not policy.symbol_restrict_enabled: - return True, "" - sym = (symbol or "").strip() - if not sym: - return False, "请选择币种" - if normalize_symbol_fn is not None: - sym_norm = (normalize_symbol_fn(sym) or "").strip() - else: - sym_norm = sym - base = symbol_base_coin(sym_norm or sym) - allowed: FrozenSet[str] = frozenset(policy.symbol_whitelist) - if base not in allowed: - allowed_txt = ",".join(policy.symbol_whitelist) - return False, f"当前账户仅允许 {allowed_txt},不允许 {base or sym}" - return True, "" - - -def assert_trade_policy_open( - policy: TradePolicy, - symbol: str, - direction: str, - normalize_symbol_fn: Optional[Callable[[str], str]] = None, -) -> Tuple[bool, str]: - ok_sym, msg_sym = assert_symbol_allowed( - policy, symbol, normalize_symbol_fn=normalize_symbol_fn - ) - if not ok_sym: - return False, msg_sym - ok_dir, msg_dir = assert_direction_allowed(policy, direction) - if not ok_dir: - return False, msg_dir - return True, "" +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/trade_result_lib.py b/lib/trade/trade_result_lib.py index 641b57b..ebeccbf 100644 --- a/lib/trade/trade_result_lib.py +++ b/lib/trade/trade_result_lib.py @@ -1,61 +1,12 @@ -"""交易结果展示与入库时的语义归一化.""" - -_WIN_EPS = 1e-9 - - -def normalize_display_result(result): - """展示用:外部平仓一律视为手动平仓.""" - res = (result or "").strip() - if res == "外部平仓" or res.startswith("外部平仓"): - return "手动平仓" - return res - - -def is_winning_pnl(pnl_amount) -> bool: - """胜率统计:盈亏为正即计为盈利单.""" - try: - return float(pnl_amount or 0) > _WIN_EPS - except (TypeError, ValueError): - return False - - -def sql_effective_pnl_expr() -> str: - """与 to_effective_trade_dict / hub_trades_lib 一致的盈亏 SQL 表达式.""" - return "COALESCE(reviewed_pnl_amount, exchange_realized_pnl, pnl_amount, 0)" - - -def count_winning_trades(trades) -> int: - return sum(1 for r in trades or [] if is_winning_pnl(r.get("effective_pnl_amount"))) - - -MISS_TRADE_RESULT = "错过" - - -def is_miss_trade_result(result) -> bool: - return (result or "").strip() == MISS_TRADE_RESULT - - -def filter_trade_records_excluding_miss(records): - """列表/统计:不展示,不计入「错过」类交易记录.""" - return [ - r - for r in (records or []) - if not is_miss_trade_result(r.get("effective_result") or r.get("result")) - ] - - -def normalize_result_with_pnl(result, pnl_amount): - """ - 非手动平仓且实际盈利时,不应记为「止损」. - 程序触发的止损类平仓若盈亏为正,归类为「移动止盈」. - """ - res = normalize_display_result(result) - if res == "手动平仓": - return res - if res == "止损": - try: - if float(pnl_amount or 0) > 0: - return "移动止盈" - except (TypeError, ValueError): - pass - return res +# obfuscated module — do not edit +import base64, marshal, zlib +_B = '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' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/lib/trade/trade_stats_calendar_lib.py b/lib/trade/trade_stats_calendar_lib.py index 18d5361..3bb05b1 100644 --- a/lib/trade/trade_stats_calendar_lib.py +++ b/lib/trade/trade_stats_calendar_lib.py @@ -1,115 +1,12 @@ -"""按交易日聚合实例 trade_records 盈亏,供统计分析页日历 API 使用.""" -from __future__ import annotations - -import json -from datetime import datetime, timedelta -from typing import Any, Callable - - -def build_trade_stats_calendar( - pnls: list[tuple], - year: int, - month: int, - segment_key: str, - row_matches_fn: Callable[[Any, str], bool], - *, - reset_hour: int = 8, -) -> dict[str, Any]: - """pnls: _load_completed_trade_pnls 返回值 (pnl, close_dt, trading_day, row).""" - y = int(year) - m = int(month) - if m < 1 or m > 12: - raise ValueError("month 无效") - first = f"{y:04d}-{m:02d}-01" - if m == 12: - next_first = datetime(y + 1, 1, 1) - else: - next_first = datetime(y, m + 1, 1) - last = (next_first - timedelta(days=1)).strftime("%Y-%m-%d") - seg = (segment_key or "all").strip() or "all" - days: dict[str, dict[str, Any]] = {} - for pnl, _close_dt, td, row in pnls: - if not td or td < first or td > last: - continue - if not row_matches_fn(row, seg): - continue - bucket = days.setdefault( - td, - { - "trading_day": td, - "open_count": 0, - "pnl_total": 0.0, - "turnover_total": 0.0, - "commission_total": 0.0, - "has_sick": False, - "sick_count": 0, - }, - ) - bucket["open_count"] += 1 - bucket["pnl_total"] += float(pnl or 0) - try: - bucket["turnover_total"] += float(row["exchange_turnover_usdt"] or 0) - except (TypeError, ValueError, KeyError): - pass - try: - bucket["commission_total"] += float(row["exchange_commission_usdt"] or 0) - except (TypeError, ValueError, KeyError): - pass - for d in days.values(): - d["pnl_total"] = round(float(d["pnl_total"]), 4) - d["turnover_total"] = round(float(d["turnover_total"]), 4) - d["commission_total"] = round(float(d["commission_total"]), 4) - month_pnl = sum(float(d["pnl_total"]) for d in days.values()) - month_count = sum(int(d["open_count"]) for d in days.values()) - return { - "year": y, - "month": m, - "date_from": first, - "date_to": last, - "segment": seg, - "reset_hour": int(reset_hour), - "days": days, - "month_pnl_total": round(month_pnl, 4), - "month_open_count": month_count, - } - - -def build_initial_stats_calendar( - pnls: list[tuple], - now_dt: datetime, - row_matches_fn: Callable[[Any, str], bool], - *, - reset_hour: int = 8, - segment_key: str = "all", -) -> dict[str, Any]: - """统计页首屏内嵌日历(当前自然月,默认品类).""" - return build_trade_stats_calendar( - pnls, - now_dt.year, - now_dt.month, - segment_key, - row_matches_fn, - reset_hour=reset_hour, - ) - - -def build_stats_calendar_bootstrap( - pnls: list[tuple], - now_dt: datetime, - row_matches_fn: Callable[[Any, str], bool], - *, - reset_hour: int = 8, - segment_key: str = "all", -) -> tuple[dict[str, Any] | None, str | None]: - """返回 (payload, json_str);失败时 (None, None),供模板安全内嵌.""" - try: - payload = build_initial_stats_calendar( - pnls, - now_dt, - row_matches_fn, - reset_hour=reset_hour, - segment_key=segment_key, - ) - return payload, json.dumps(payload, ensure_ascii=False, separators=(",", ":")) - except Exception: - return None, None +# obfuscated module — do not edit +import base64, marshal, zlib +_B = 'eNpdlXlwG+UZxlexZFuyLe/altYKdmrHcWKOhJW0K+1qdayklVYnWVmHdezaOnYl24plB+cgISkJFNKWpMC0DKGFtAMdyJS2kHJMwVCm00AZ2oQkEFogCdTuhBASQ4d2WpgO9Fuudvr3M+/7vcfvfb7X5TIl2ySpRFJ4hbCX8BJmkW7AqKKRxTGJp2blZlKFt8rtjoqtTCSLfFUwu+kUqSllMbHYcGWLpFOyQbLGlbboLTPV3My0QyZTHk35MJG6kV+fmDvv8XigsmaDOPSZPenlb0pd+Y572qqp8KtT84fpBNu8RjXyTpm2inaMWOX32sH755597Na37zx1+J+nfnnngaUH9zx0ae8rD5397gWDvzutSs8YxWSmW8X1/OmzZ8/ecfG6s2/94pa3Pz1xz923PXdk38/+8cb3T557+L5vvf+rns34btPJP58997dDkQNKfxH88nS6bVyEcyyvb+N2mqrL5oTaKCATWtWCOS7q+aQ6iySuqC5ab+jsW3bOa7LpVIte2O8G87mLXJ7iRrsSyGy+Le5bkg35rP+QJy0YjWXE39OqlpewlXyme7maE+BFIqVvY4SLs5ww2toyhkx1qnoXHRzKXZ4N6rgZbY7NayGuntEdH9qm1qfVqVaVo3JDfqUJ7WLQmVVsfiLTQQZdmIQvWdGWOMzqDs0K/BjaYA3BMDyv887kJRkH+8EpHOxvDuxvBvQ3QNiL3LzFIsWwQPE8EbbBeMB+vsKWIxWomKS4+TCmselxZi5mFsvniafMvRbR/GlVLAK99qCLlw5b+5wme6aatEQjiC1kg4gNhKoiSk/WTJMZM1MKm/utKvOwJJifKfe6QhRsF3HV9BPSBumoLGB3l0OufrthLtM4SBwsGeZ4h8nDE72NgHy9RHVLrnAjRPXRvQPX1SNTgYbJOYGhMwGHaXK0xLoNI6wrNB92LtqMvH8967y3ZqJDHsgjUqqGODU6BU0L9sPSD4i+YdNgpu7CBkoqogmr4RrCNC9utUq7SzF3qBEIazznzRqJxjSlb6wJDULDTmJXOV6ObIHmByl2OeFcJEYxs6TGGqU2EsNgZxxH6hPVjZ/IJYoiKpvsnUQXFtrCDorXF/H1GGwNDKu2DcjB49D0AtlUsYF+LZh/vROLTyEEQ62wM9+TyqRsl1yl8Jy/EqZ/6z6KbZPkbnYe8TD23rXi8K7yM9Lo5ncr7NX8vNZu8CD267YJgwOl8wQ//K4tsE41r5F9s3AV6HPX2BcJ1fCRKrtavD6HBz6ESXFItfU5OTJ5tH4vNVd5yrZI9A4FJ4URcdtf8JDzqPsgMQjyPymbtrKekBT29LtNdFBiiWeKJpKhe9eJA+eLgc2ju/81Waf6dzA7S7jMCK1RncmtLaRT9Fgma9ALSBKiDZEs3zzNdWaN8Jtv/fz0A5/teeLbcIwdy4V9eW0OVbmFeK/fG4D49vE0LSSYeCBY4OIrTyj87XeA+4gTwD/y+jyS7br50Tvf/vvlhh3i+d/hIdKRVkOrygatBtyfzKvD3lWYoRhjciYDtOxIafVRCfDb/JX/DJkIt5WNQioxP0bz2djzkxlDUkevGIvl0Q/cSF47G2yLJdILdcbr0xU28qgrrvNpWhdG8gV9dryLa58Q6NhEHNI2nFkNVw22q0Y9TrRjRbI61s7kDMGbgL9ErqZKcnAE3E9lCFqZ7xjTNalrcS9SyKIJdJZDBTa6C9zX9K6EN9PRNLYiSbfo0ykRDo7r846AF4q94SrJ+JgZ3B/xpX9WQP01UD8nY9Z71mBU6a94CKNcLI6UA9Io4HOBLFdwO2xTmUsSYonuBPm/GSeWsCZ9Ru07Vl1woOqn8eRLONpjkcgXciD/dh1ZivAoIquSnUintDsBj/v1LPX6T187e/st+z56/MTtp07ev59SRfuCcJs7ncnrvNnuOuwfM0oC30QxwQSUkL1okwhNoTAUi9ShVEGdVyUYOpUrbDTEl6lsUihAK8Noax74/8sYmH/liZuwUDgf4DtUdcan4YVpozGLBOgWKCXkW1KBja3aiYQm5W/ZmEsmxlWxMFdQkf/zf1RwGbNJJBYpyUTwWjCfvho8BPSrQlSUAnq3QcYsxj4wHxQG/hXsA/OL+kH8OIhHusD+YxQzSWJUdx34m9slppCWahPbpqU9YhrlJw0Fj92rmhCFTuRzf4yuB/P3UNq2zlii3l0ouBkdm2wtD3zuv7NtrFBwqDJwFo5viuCgvsv1slrgVLt9UT0atCvxZhD/vBuFu8cV/78scYiv0LPoiKgRPpHReNNe1Af2+5sZwhtOrOUC+uDz5A6Vd2u0WxdctnWl0wyELBXZNqRTWLLDTD40iDrA/nwE2F/QDfrnpgEf7FYwn6gL9L9A9F20aheJiVaUY6YbeSGOuCbaWRRecKbTsPdymYPGmF2gf7rK5gW3q5rRdkSgKQimHXH+aVtA7/fYeKRQEAwoCubXwLK69njHVI+uo2zJtRf82nrzBKeL+fqV93uU9xnw/qVvNvck0rxTuV9o1gv+v4i7OZ1N6/gstwnoAR2oz+yu4+uKTmmNdRO1mtxcGyAGZIwclraRNXtDrlAuooQTFZtksVYpC1mRbAR+P/b74amdQGfleUV/maj0SKutRcry4kxtNbFWvoYckraQNbwK4mkQby4COEC87Xj3KNivcT24nz/QqAfwE7Ep+6nlwf8IxXwO5b6sX95XDfAhAT7q4L62g/tCBwA/CLivXiKJmyrJWqTYW2EdfPnXZgses3DjYH63nXvkzKEXzh7cs//oo3v2vb/33efO7P3g1Z8cPfn+Uyv33vrgmQsXzjz+w1seu+PVjy8a9h/49/5X7jvz8OKp297sj/ocgF+4jDYpfBIKn3XwfsgVdSv8dij8Xqnw61J0m6J7gd7dB/jnuoC/XtzQHM+lNQ4xn4PFbM6BxNU+lF6RSyc6EpnuHSAeHQHxNZfNXCRceK1CS8BEfxRpAv0fCzMwM66qpPgVyCzHGntNW9qukAIR9ip9Ruj1B/3dPSZ4V1smLa2ofvH/EzWwPwzMDwPvb7OXCKMe8BdZb5BDeEwO0eHqKpfK/l6pzxUwo7hYCRVTGESnXJeqo7aaVKPMstHRNyJLRZLEHURSqsmcJe4ObAbzP3PHhycfeCQQCas3trZBgqFHG8x21o0sE286ff+JA3vuev2jm039V3eMQSs/r69/48nvfHLq2F2vfbzv9D0X9hy57/TikTN73otFRTCfdQh9vGw6VtTHLJOT4nikB03UE+gYL3gmI1BQY4RimSDcR1y7M4Yzyv4WSuPchG7ZmYA7w8ZLM4OmgsCiQrazv+IjwX6iWeCfvq2gf7QO7tNYB/7TkAKZ3qwukfk/f1m6MYV0dAhVDk35u+rNwLYyqWy3I1OYgLk4p+7KAX9CdwC+Sk6i+F++La+srZCkVSq+iG4H80XswSnAX7Ch/B9O5f+gFP+gAZ8o/RW/gc3A/5itin9tV/h2AX5hHOQ3Xq/426Dib06moegNRe9QdIeizyn6OqCjlOIfuHK/HuV+tyr+sV3xjwHAX4j8wl8siq5V9OrX/rJW4dOl8Eko/dNK/1cqfG5T+GwCfPTRC1bHbKatMFtdmK3XMwgT4yBXFcWV+luU+stK/UVl/hUmznTkx7mikr+s5G9S8uuV/Du+nm//1/z3g/wfeMScLsW1inoS1MddBeozyw1yBt8ib6Zo98wfiRGb5H6pOmAh53/8H/IDvvk=' +_K = '5eb9e6dda894c57a4e02d809ae440d49' +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) diff --git a/license.env.example b/license.env.example new file mode 100644 index 0000000..435d861 --- /dev/null +++ b/license.env.example @@ -0,0 +1,7 @@ +# LICENSE_API_URL / LICENSE_CLIENT_KEY +# 复制为仓库根目录 license.env(已 gitignore),与授权站 CLIENT_API_KEY 保持一致。 + +LICENSE_API_URL=https://sq.bz121.com +LICENSE_CLIENT_KEY=cm_user_sq_bz121_8f3a2c1d9e4b7a6f5d0e1b2c3a4f5e6 +# LICENSE_OFFLINE_GRACE_HOURS=72 +# LICENSE_DISABLED=false diff --git a/manual_trading_hub/.env.example b/manual_trading_hub/.env.example index b18f3f4..f79579c 100644 --- a/manual_trading_hub/.env.example +++ b/manual_trading_hub/.env.example @@ -110,3 +110,8 @@ TRADING_DAY_RESET_HOUR=8 # HUB_BACKUP_ROOT=/root/backups/crypto_monitor_portal # 资金概况:曲线与回撤统计起始交易日 HUB_FUND_HISTORY_START_DAY=2026-06-09 + +# --- 软件授权(整机共用,也可写仓库根 license.env)--- +# LICENSE_API_URL=https://sq.bz121.com +# LICENSE_CLIENT_KEY=与授权站 CLIENT_API_KEY 相同 +# LICENSE_OFFLINE_GRACE_HOURS=72 diff --git a/manual_trading_hub/AI教练说明.md b/manual_trading_hub/AI教练说明.md deleted file mode 100644 index 12732b7..0000000 --- a/manual_trading_hub/AI教练说明.md +++ /dev/null @@ -1,66 +0,0 @@ -# 中控 AI 教练说明 - -中控 **AI 教练**(`/ai`)与三实例 `/records` 里的 **AI 复盘** 分离:模块在 `manual_trading_hub/hub_ai/`,数据存同目录 JSON. - -## 能力 - -| 功能 | 说明 | -|------|------| -| **交易教练** | 口语化陪聊;注入三户监控快照与今日总结摘要(后台自动生成,不在页面展示) | -| **普通聊天** | 不绑交易数据,适合闲聊,答疑 | -| **交易监管** | 今日长会话;手动/中控开平仓与新开仓自动推送 + 企业微信 + 可回聊(见 [交易监管说明.md](./交易监管说明.md)) | -| **会话历史** | 右侧列表:切换,删除;消息一键复制 | - -页面保留 **交易教练 / 普通聊天 / 交易监管** 与聊天区;**今日总结** 已移至 **数据看板**(`/dashboard`)纯数据展示,不再在 AI 页生成. - -## 存储 - -与 `hub_settings.json` 同目录(`manual_trading_hub/`): - -- `hub_ai_summaries.json` — 历史总结(供交易教练上下文,可选 API 仍保留) -- `hub_ai_chat.json` — 聊天会话(`active_session_id`,多会话,`bot_mode`) - -升级 / 迁移时请一并备份(见 [本地数据迁移到云端.md](./本地数据迁移到云端.md)). - -## 模型配置 - -在 **`manual_trading_hub/.env`** 配置,**变量名与三实例完全相同**;中控 `hub_ai/client.py` 共用仓库根 `ai_client.py`,**默认也是 OpenAI 兼容网关**(`AI_PROVIDER=openai`),与你在三所 `.env` 里配的那套一致即可. - -**推荐(与三实例默认一致):** - -```env -AI_PROVIDER=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 -``` - -改走本机无限制模型时,将 `AI_PROVIDER=ollama`,并填好 `OLLAMA_API` / `AI_MODEL`;`OPENAI_*` 可保留不动. - -总结与聊天使用**同一模型**(同一套 `OPENAI_MODEL` 或 `AI_MODEL`);总结 temperature≈0.15,聊天≈0.5. - -可选:`TRADING_DAY_RESET_HOUR=8`(与实例一致,定义「今日」交易日). - -## 依赖接口 - -中控通过 HTTP 拉取各实例: - -- `GET /api/hub/monitor`(已有) -- `GET /api/hub/trades/today?trading_day=YYYY-MM-DD`(`hub_bridge` 注册,需三实例更新代码并重启) - -子代理 `GET /status` 提供持仓与余额. - -## 与实例 AI 复盘的分工 - -| | 中控 AI 教练 | 实例 AI 复盘 | -|--|-------------|-------------| -| 入口 | `/ai` | 各所 `/records` | -| 数据 | 三户聚合 | 单户 `journal_entries` | -| 语气 | 聊天搭档 | 结构化教练报告 | -| 代码 | `hub_ai/*` | `ai_review_lib` + 各 `app.py` | - -详见仓库根 [AI复盘与模型配置说明.md](../AI复盘与模型配置说明.md)(实例侧). diff --git a/manual_trading_hub/README.md b/manual_trading_hub/README.md index d286ca5..bab54ad 100644 --- a/manual_trading_hub/README.md +++ b/manual_trading_hub/README.md @@ -1,105 +1,8 @@ -# 复盘系统中控(manual_trading_hub) +# manual_trading_hub -> **完整说明**:[使用说明.md](./使用说明.md) · **资金概况**:[资金概况说明.md](./资金概况说明.md) · **数据看板**:[数据看板说明.md](./数据看板说明.md) · **AI 教练**:[AI教练说明.md](./AI教练说明.md) · **行情区**:[行情区说明.md](./行情区说明.md) · **部署**:[部署文档.md](./部署文档.md) · **云服务器**:[云服务器部署说明.md](./云服务器部署说明.md) · **本地→云端迁移**:[本地数据迁移到云端.md](./本地数据迁移到云端.md) · **局域网/反代**:[局域网与反代部署说明.md](./局域网与反代部署说明.md) · **故障**:[常见问题.md](./常见问题.md) +多账户交易中控(用户版)。 -多账户 **监控聚合 + 紧急全平**;**不在中控网页下单**.人工下单,关键位,**策略交易**(`/strategy`),复盘请在各 `crypto_monitor_*` 实例网页操作(监控卡片 **「实例」** / **「复盘」**).**增加子账户**见 [使用说明 §4.3](./使用说明.md#43-增加账户例如再挂一个-gate). - ---- - -## 当前能力 - -| 能力 | 说明 | -|------|------| -| 监控区 | 持仓,余额,关键位摘要,趋势计划,机器人单(只读) | -| 资金概况 | 总/分户资金(资金户+交易户),180 日曲线,最大回撤 | -| **数据看板** | 三户当日总览/分户/平仓明细,SSE 推送(`/dashboard`;见 [数据看板说明.md](./数据看板说明.md)) | -| 行情区 | K 线(多周期,本地缓存,技术指标,从监控跳转持仓线) | -| **AI 教练** | 交易教练 + 普通聊天,会话历史(`/ai`;见 [AI教练说明.md](./AI教练说明.md)) | -| 紧急全平 | 单户 / 全局市价减仓 | -| 系统设置 | `hub_settings.json` 管理 URL,启用,**监控关键位 / 监控趋势计划**(不控制策略交易页) | -| Web 登录 | `.env` 设 `HUB_PASSWORD` 后用户名+密码保护(反代公网**务必**配置) | -| ~~下单区~~ | **已移除**(避免与实例重复,减少故障面) | - ---- - -## 架构 - -``` -浏览器 → hub.py (:5100) 监控 / 资金概况 / **数据看板** / 行情 / **AI 教练** / 设置 / 登录 - ├→ agent.py × N (:15200~15202) 持仓,全平 - └→ 各 Flask (:5000/5001/5004) /api/hub/monitor 只读聚合 -``` - -- 账户列表:**系统设置** 或默认 `settings_store.py`(不再使用环境变量 `HUB_AGENTS`). -- 三实例须注册 **hub_bridge**(仓库根 `hub_bridge.py`);PM2 建议 `PYTHONPATH=..`. - ---- - -## 快速启动(Linux / PM2) - -```bash -cd /opt/crypto_monitor_user/manual_trading_hub -python3 -m venv .venv && source .venv/bin/activate -pip install -r requirements.txt -cp .env.example .env -# 编辑 .env:HUB_PASSWORD,HUB_BRIDGE_TOKEN,HUB_PUBLIC_ORIGIN 等 - -pm2 start ecosystem.config.cjs # 3 agent + hub -pm2 save - -bash scripts/verify_hub_deploy.sh -curl -s http://127.0.0.1:5100/api/ping -``` - -浏览器:`http://<本机IP>:5100/monitor`(行情 `/market`;已设密码则先 `/login`). - ---- - -## 中控 `.env` 要点 - -| 变量 | 说明 | -|------|------| -| `HUB_PASSWORD` / `HUB_USERNAME` | 非空密码即启用登录 | -| `HUB_BRIDGE_TOKEN` | 与三实例一致 | -| `HUB_DISABLED_IDS` | 默认 `1` 关闭 OKX | -| `HUB_PUBLIC_ORIGIN` | 其它设备打开复盘/实例外链(替换 127.0.0.1) | -| `HUB_COOKIE_SECURE` | HTTPS 反代建议 `true` | - -详见 [.env.example](./.env.example). - ---- - -## 子代理(agent) - -每所策略目录单独进程,`EXCHANGE` + `PORT`(15200~15202),密钥来自**该目录 `.env`**.PM2 经 `scripts/run_agent.sh` 启动(自动 `source .env`,去 CRLF). - -| PORT | 目录 | -|------|------| -| 15200 | crypto_monitor_binance | -| 15201 | crypto_monitor_okx | -| 15202 | crypto_monitor_gate | - ---- - -## 运维脚本 - -| 脚本 | 作用 | -|------|------| -| [scripts/fix_hub_deps.sh](./scripts/fix_hub_deps.sh) | 安装/更新 venv 依赖 | -| [scripts/verify_hub_deploy.sh](./scripts/verify_hub_deploy.sh) | 验收代码版本与 ping | -| [scripts/fix_env_crlf.sh](./scripts/fix_env_crlf.sh) | 修复 .env 的 Windows 换行 | -| [scripts/pm2_hub.sh](./scripts/pm2_hub.sh) | PM2 启停 hub+agent | -| [scripts/后台运行-Ubuntu.md](./scripts/后台运行-Ubuntu.md) | PM2 常驻 | -| [docs/ubuntu-server.md](../docs/ubuntu-server.md) | Ubuntu / Python / Node / PM2 | - ---- - -## 文档索引 - -| 文档 | 内容 | -|------|------| -| [使用说明.md](./使用说明.md) | 页面,API,环境变量,日常流程 | -| [行情区说明.md](./行情区说明.md) | K 线周期,缓存,快捷键,拉取逻辑 | -| [部署文档.md](./部署文档.md) | Ubuntu,PM2,反代,升级 | -| [常见问题.md](./常见问题.md) | 已遇到问题与处理 | -| [.env.example](./.env.example) | 环境变量模板 | +- [部署文档.md](./部署文档.md) +- [使用说明.md](./使用说明.md) +- [常见问题.md](./常见问题.md) +- 授权:[docs/license.md](../docs/license.md) diff --git a/manual_trading_hub/SNAPSHOT_ROLLBACK.md b/manual_trading_hub/SNAPSHOT_ROLLBACK.md deleted file mode 100644 index 4a2afde..0000000 --- a/manual_trading_hub/SNAPSHOT_ROLLBACK.md +++ /dev/null @@ -1,22 +0,0 @@ -# 更新前快照(行情区 + K 线库) - -> 行情区使用说明见 [行情区说明.md](./行情区说明.md). - -更新前已打 Git 标签,回滚方式: - -```bash -cd /opt/crypto_monitor_user # 或你的仓库路径 -git fetch --tags -git checkout snapshot/pre-hub-market-20260528 -# 恢复后重启: -pm2 restart manual-trading-hub crypto_okx crypto_binance crypto_gate -``` - -回到最新主线: - -```bash -git checkout main -git pull -``` - -K 线数据库(不纳入 Git):`manual_trading_hub/data/hub_kline.db`,回滚代码不会自动删除该文件. diff --git a/manual_trading_hub/docs/help/01-quickstart.md b/manual_trading_hub/docs/help/01-quickstart.md deleted file mode 100644 index 3ea2749..0000000 --- a/manual_trading_hub/docs/help/01-quickstart.md +++ /dev/null @@ -1,34 +0,0 @@ -# 快速开始 - -## 系统是什么 - -**中控**聚合三所(币安 / OKX / Gate)的持仓、委托、关键位与趋势计划,并提供资金曲线、行情、复盘与 AI 教练。**实际下单、关键位配置、策略执行、交易复盘**在各交易所**实例网页**完成。 - -``` -浏览器 → 中控(默认 :5100) - ├─ 监控区 / 资金 / 行情 / 内照明心 … - └─ 点击「下单 / 期权」→ 实例页(内嵌或新标签) -实例 Flask(币安 :5001 · Gate :5000 · OKX :5004) -``` - -## 日常推荐流程 - -1. 打开 **监控区**,查看三所持仓、浮盈亏、关键位与趋势计划。 -2. 需要操作某所时,点该卡片 **「下单」**(或 **「期权」**)进入实例页。 -3. 复盘与统计:中控 **内照明心**、**数据看板**,或实例 **交易记录与复盘**。 -4. 事前计划:中控 **开仓计划**;策略 playbook:中控 **策略说明**(与实例「策略交易」不同,见下文)。 - -## 三账户默认对应 - -| 账户 | 实例端口 | 常见能力 | -|------|----------|----------| -| 币安 | 5001 | 关键位 + 趋势 | -| OKX | 5004 | 关键位 + 趋势 + **期权**(需在设置勾选「监控期权」) | -| Gate | 5000 | 关键位 + 趋势 | - -## 两个容易混淆的名称 - -| 名称 | 在哪里 | 是什么 | -|------|--------|--------| -| **策略说明** | 中控顶栏 | 各所策略文档与开仓检查清单 | -| **策略交易** | 实例顶栏 | 趋势回调 / 顺势加仓等自动化功能 | diff --git a/manual_trading_hub/docs/help/02-hub-nav.md b/manual_trading_hub/docs/help/02-hub-nav.md deleted file mode 100644 index 58bf545..0000000 --- a/manual_trading_hub/docs/help/02-hub-nav.md +++ /dev/null @@ -1,26 +0,0 @@ -# 中控导航说明 - -顶栏各页面用途如下(可在 **系统设置 → 显示与导航** 中隐藏不需要的 Tab)。 - -| 导航 | 用途 | -|------|------| -| **资金概况** | 总资金曲线、分户权益、回撤与 24h 变化 | -| **开仓计划** | 事前写下计划、跟踪进行中、统计历史胜率 | -| **监控区** | **核心操作台**:三所持仓卡片、全平/撤单、关键位与趋势计划摘要 | -| **策略说明** | 三所策略 playbook + 开仓检查清单(非系统操作手册) | -| **使用说明** | 本页:中控与实例怎么用 | -| **行情区** | K 线、指标、画线;可从持仓跳转带币种 | -| **计算器** | 趋势回调 / 滚仓张数与盈亏测算(手动填价) | -| **内照明心** | 复盘语录、归档交易、永久 5m K 线 | -| **数据看板** | 当日 KPI、分户卡片、SSE 刷新 | -| **AI 教练** | 交易教练对话、监管推送(需配置密钥) | -| **系统日志** | 中控与三实例 PM2 日志(排错用,非部署说明) | -| **系统设置** | 中控密码、导航显示、交易所地址、监控能力勾选 | - -## 手机端(≤720px) - -底栏固定四项:**监控 / 行情 / 计算 / AI**;其余入口进 **更多**。电脑与平板仍用顶栏,布局不变。 - -## 默认首页 - -登录后默认进入 **监控区**(`/monitor`)。 diff --git a/manual_trading_hub/docs/help/03-monitor.md b/manual_trading_hub/docs/help/03-monitor.md deleted file mode 100644 index d986534..0000000 --- a/manual_trading_hub/docs/help/03-monitor.md +++ /dev/null @@ -1,43 +0,0 @@ -# 监控区与实例入口 - -## 监控卡片里有什么 - -每张交易所卡片通常包含: - -- **资金行**:资金账户 / 交易账户 / 浮动盈亏(可在设置关闭) -- **永续持仓**:表格摘要;点击标题栏 **进入全屏** 可看完整持仓卡片 -- **OKX 期权**(勾选「监控期权」后):永续与期权分块;全屏时期权也以卡片展示(与实例期权页字段一致,只读) -- **关键位 / 下单监控 / 趋势回调 / 顺势加仓**:只读摘要(数据来自实例) - -## 全屏模式 - -点击卡片标题栏(或移动端卡片主体)进入 **全屏**: - -- 永续:每币种一张持仓卡,可 **委托 / 平仓**(非日内纪律模式) -- 期权(OKX):只读卡片,含权利金、标记价、买盘深度等 -- 下方:关键位、下单监控、趋势与滚仓区块 - -按 `Esc` 或 **返回监控** 退出全屏。 - -## 打开实例(SSO) - -监控卡片或全屏顶栏按钮: - -| 按钮 | 进入实例页 | 说明 | -|------|------------|------| -| **打开实例** | 实盘下单 | 新浏览器标签 | -| **下单** | 实盘下单 | 中控内 iframe | -| **监控位** | 关键位监控 | | -| **复盘** | 交易记录与复盘 | | -| **期权** | 期权页 | 仅 OKX 且勾选监控期权 | - -实例地址在 **系统设置 → 交易所** 配置 `flask_url`;未配置时不会出现上述按钮。 - -## 常见操作 - -| 操作 | 位置 | -|------|------| -| 紧急全平 | 卡片 **全平**(日内纪律账户可能禁用) | -| 改止盈止损 | 持仓行 **委托** 或全屏卡片 | -| 撤条件单 | 监控区条件单列表 | -| 停止趋势计划 | 趋势回调区块 **停止 / 保本** 等 | diff --git a/manual_trading_hub/docs/help/04-instance.md b/manual_trading_hub/docs/help/04-instance.md deleted file mode 100644 index 37baa55..0000000 --- a/manual_trading_hub/docs/help/04-instance.md +++ /dev/null @@ -1,30 +0,0 @@ -# 实例页导航说明 - -从监控区 **下单 / 打开实例** 进入后,实例顶栏常见 Tab 如下(部分可在实例 **系统设置 → 导航显示** 中隐藏)。 - -| Tab | 用途 | -|-----|------| -| **关键位监控** | 配置 5m 门禁关键位,可选自动下单 | -| **实盘下单** | 人工下单、下单监控、预估盈亏比 | -| **策略交易** | 趋势回调、顺势加仓计划(自动化) | -| **策略交易记录** | 上述策略的执行历史 | -| **交易记录与复盘** | 平仓记录、日记、AI 复盘 | -| **统计分析** | 按周期汇总盈亏 | -| **期权** | OKX 期权链、持仓、买一平仓(OKX 且已启用) | -| **风控说明** | 只读展示当前风控相关 env | -| **env配置** | 修改运行参数(中文标签) | -| **系统设置** | 实例登录密码、导航 Tab 开关等 | - -## 与中控的分工 - -| 在中控做 | 在实例做 | -|----------|----------| -| 看三所持仓汇总、全平 | 下单、改单、平仓 | -| 看关键位 / 趋势摘要 | 新建 / 修改关键位与策略 | -| 内照明心、数据看板 | 交易记录详情、日记 | -| 开仓计划 | 策略交易执行 | -| OKX 期权只读监控 | 期权开仓、买一平仓 | - -## iframe 内操作 - -在中控 iframe 打开实例时,顶栏有 **返回监控 / 刷新 / 新标签打开**,无需重复登录。 diff --git a/manual_trading_hub/docs/help/05-settings.md b/manual_trading_hub/docs/help/05-settings.md deleted file mode 100644 index 5892461..0000000 --- a/manual_trading_hub/docs/help/05-settings.md +++ /dev/null @@ -1,38 +0,0 @@ -# 设置与配置说明 - -## 三层配置,不要混用 - -| 层级 | 入口 | 管什么 | -|------|------|--------| -| **中控系统设置** | 中控 `/settings` | 中控密码、顶栏显示、交易所 URL、监控能力(关键位/趋势/期权)、宏观日历、备份 | -| **实例系统设置** | 实例 `/settings` | 实例登录密码、实例顶栏 Tab 显示 | -| **实例 env配置** | 实例 `/env_config` | 交易参数(止损比例、风控开关等) | - -改 env 后通常需 **重启对应实例 PM2** 才完全生效;具体字段含义见实例 env 页说明或仓库 `docs/env配置说明.md`(无需在中控内阅读)。 - -## 中控 · 显示与导航 - -可隐藏不常用的顶栏 Tab(**监控区**、**系统设置** 无法隐藏)。 - -## 中控 · 交易所 - -每项需配置: - -- **flask_url**:实例 HTTP 地址(中控聚合与打开实例用) -- **agent_url**:子代理地址(持仓与全平) -- **capabilities**:勾选 **关键位 / 趋势 / 期权** 决定监控区展示哪些块 - -## 实例 · 导航显示 - -固定保留:**关键位监控、实盘下单、系统设置**。其余 Tab 可按需开关。 - -## 使用向常见问题 - -**监控卡片没有「下单」按钮** -→ 检查该所 `flask_url` 是否填写且实例可访问。 - -**OKX 看不到期权** -→ 中控设置勾选「监控期权」,且实例已启用期权模块。 - -**策略说明 vs 策略交易** -→ 前者在中控,是文档;后者在实例,是自动化功能。 diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py index 1c885a1..4db9565 100644 --- a/manual_trading_hub/hub.py +++ b/manual_trading_hub/hub.py @@ -768,6 +768,13 @@ async def _hub_lifespan(_app: FastAPI): app = FastAPI(title="复盘系统中控", docs_url=None, redoc_url=None, lifespan=_hub_lifespan) +try: + from lib.license.fastapi_gate import install_license_middleware + + install_license_middleware(app) +except Exception as _lic_err: + print(f"[license] hub: {_lic_err}") + STATIC_DIR = DIR / "static" _REPO_STATIC = _REPO_ROOT / "lib" / "common" / "static" _AI_REVIEW_RENDER_JS = _REPO_STATIC / "ai_review_render.js" diff --git a/manual_trading_hub/scripts/后台运行-Ubuntu.md b/manual_trading_hub/scripts/后台运行-Ubuntu.md deleted file mode 100644 index 0d76b69..0000000 --- a/manual_trading_hub/scripts/后台运行-Ubuntu.md +++ /dev/null @@ -1,42 +0,0 @@ -# 中控与子代理 · 后台常驻(Ubuntu) - -**唯一推荐方式:PM2.** - -请仅使用 PM2 托管 `hub.py` 与 `agent.py`,勿与 nohup 等方式重复启动同一端口. - ---- - -## 启动 - -```bash -cd /opt/crypto_monitor_user/manual_trading_hub -source .venv/bin/activate -pip install -r requirements.txt -cp -n .env.example .env # 首次 - -pm2 start ecosystem.config.cjs -pm2 save -pm2 list -``` - -一条 `ecosystem.config.cjs` 会拉起 **4 个子代理 + 1 个 hub**. - ---- - -## 常用命令 - -```bash -pm2 logs manual-trading-hub -pm2 restart manual-trading-hub -pm2 restart all -bash scripts/verify_hub_deploy.sh -``` - ---- - -## 详细说明 - -| 文档 | 内容 | -|------|------| -| [../部署文档.md](../部署文档.md) | 端口,反代,故障排查 | -| [../../docs/ubuntu-server.md](../../docs/ubuntu-server.md) | Python / Node / PM2 版本与三所启动顺序 | diff --git a/manual_trading_hub/云服务器部署说明.md b/manual_trading_hub/云服务器部署说明.md deleted file mode 100644 index b49f584..0000000 --- a/manual_trading_hub/云服务器部署说明.md +++ /dev/null @@ -1,289 +0,0 @@ -# 云服务器部署说明 - -本文说明在 **云服务器(VPS)** 上部署 `crypto_monitor` 中控与三实例的推荐配置:硬件,软件,防火墙,宝塔反代,环境变量,PM2 启动与验收. - -云上标准做法:**域名 + 宝塔/Nginx 反代 + HTTPS**;业务端口(5100,5000~5004,15200~15202)**不对公网直连**. - -相关文档: - -- **[本地数据迁移到云端.md](./本地数据迁移到云端.md)** — 备份 `crypto.db`,图片,`hub_settings` 与恢复步骤 -- [局域网与反代部署说明.md](./局域网与反代部署说明.md) — 局域网 IP:端口 与反代域名对照,SSO 行为 -- [部署文档.md](./部署文档.md) — PM2,依赖安装,日常运维 -- [使用说明.md](./使用说明.md) — 中控功能说明 -- [常见问题.md](./常见问题.md) — 故障排查 -- 环境变量模板:[.env.example](./.env.example) - ---- - -## 一,服务器硬件与系统 - -| 项目 | 建议 | -|------|------| -| 配置 | **2 核 4G** 起步;三实例 + 中控 + PM2 同时运行,**4G~8G 更稳** | -| 系统 | **Ubuntu 22.04 / 24.04**(项目文档按 Linux 编写) | -| 磁盘 | **20G+**;日志,SQLite,上传图片会占空间 | -| 网络 | 需能访问各交易所 API;若走代理,在对应 `crypto_monitor_*/.env` 配置 `OKX_SOCKS_PROXY`,`BINANCE_SOCKS_PROXY` 等 | - ---- - -## 二,软件环境 - -```bash -sudo apt update -sudo apt install -y python3 python3-venv python3-pip git curl - -# 进程守护(推荐) -sudo npm i -g pm2 -``` - -**宝塔面板(可选但推荐)**:安装 **Nginx**,用于反向代理与 **SSL**(Let’s Encrypt). - -Python 虚拟环境(分开安装,互不替代): - -| 目录 | 用途 | -|------|------| -| `manual_trading_hub/.venv` | 中控 `hub.py` + 子代理 `agent.py` | -| `crypto_monitor_binance/.venv` | 币安 Flask | -| `crypto_monitor_okx/.venv` | OKX Flask | -| `crypto_monitor_gate/.venv` | Gate Flask | -| `crypto_monitor_gate/.venv` | Gate Flask | - -各实例 `ecosystem.config.cjs` 一般已设置 **`PYTHONPATH=..`**(仓库根),以便加载 `hub_bridge.py`,`hub_auth.py` 等. - ---- - -## 三,网络与端口(云上最重要) - -**原则:公网只暴露 Nginx 的 80/443;Flask 与 agent 只监听本机.** - -| 服务 | 本机端口(示例) | 是否对公网开放 | -|------|------------------|----------------| -| 中控 hub | 5100 | **否** → 仅 `https://hub.你的域名` 反代 | -| 币安 Flask | 5001 | **否** → `https://binance.你的域名` | -| OKX Flask | 5004 | **否** → `https://okx.你的域名` | -| Gate Flask | 5000 | **否** → `https://gate.你的域名` | -| 子代理 agent | 15200~15202 | **否**,必须 **127.0.0.1** | - -### 云厂商安全组 / 系统防火墙 - -- **放行**:`80`,`443`(给宝塔/Nginx) -- **不要放行**:`5100`,`5000`~`5004`,`15200`~`15202`(除非临时本机调试,用完即关) - ---- - -## 四,域名与宝塔反代 - -为 **中控 + 每个要对外打开的实例** 各建一个站点(子域名示例): - -| 站点(浏览器访问) | 反代目标 | -|--------------------|----------| -| `https://hub.example.com` | `http://127.0.0.1:5100` | -| `https://okx.example.com` | `http://127.0.0.1:5004` | -| `https://binance.example.com` | `http://127.0.0.1:5001` | -| `https://gate.example.com` | `http://127.0.0.1:5000` | - -### 宝塔操作要点 - -1. 每个域名 → **网站** → **反向代理** → 目标 `http://127.0.0.1:对应端口`. -2. 申请 **SSL**(Let’s Encrypt),强制 HTTPS. -3. **不要**再给实例站加一层宝塔「访问密码」(会与 Flask `/login` 重复);直链鉴权用下文 **`APP_USERNAME` / `APP_PASSWORD`**. -4. Nginx 建议保留常见代理头(宝塔默认通常已带): - -```nginx -proxy_set_header Host $host; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; -``` - -中控请求实例 `/api/hub/*` 时会带 **`X-Hub-Token`**,一般无需额外配置. - ---- - -## 五,环境变量(必配) - -### 5.1 中控 `manual_trading_hub/.env` - -```env -HUB_HOST=0.0.0.0 -HUB_PORT=5100 - -# 与三实例 .env 完全相同(API + SSO 签名) -HUB_BRIDGE_TOKEN=请填一长串随机字符 - -# 中控网页登录(公网务必设置) -HUB_USERNAME=admin -HUB_PASSWORD=强密码 -HUB_SESSION_SECRET=另一串随机字符 - -# 中控为 HTTPS 时建议 true -HUB_COOKIE_SECURE=true - -# 公网用域名访问中控(宝塔反代)时必设其一: -# HUB_ALLOW_PUBLIC=true (推荐:反代 + 中控密码) -# 或反代目标必须是 http://127.0.0.1:5100 且可保持 HUB_TRUST_LAN=false -HUB_ALLOW_PUBLIC=true -HUB_TRUST_LAN=false - -# 从中控打开实例的 SSO 链接有效期(秒),默认 7200 = 2 小时 -HUB_SSO_TTL_SEC=7200 - -# 各实例 hub_settings 里 flask_url 已写 https 域名时,一般可不设 -# HUB_PUBLIC_ORIGIN=https://hub.example.com -``` - -完整项见 [`.env.example`](./.env.example). - -### 5.2 三个实例 `crypto_monitor_*/.env` - -每个目录都要有(**直链** `https://okx.域名` 时用这套登录网页): - -```env -# 各所 API 密钥(按交易所填写) -# APP_PORT=5004 - -# 与中控 manual_trading_hub/.env 中 HUB_BRIDGE_TOKEN 完全一致 -HUB_BRIDGE_TOKEN=与中控相同 - -# 三实例建议统一(直链登录用) -APP_USERNAME=统一用户名 -APP_PASSWORD=统一强密码 - -# 云服务器切勿开启(会跳过网页登录): -# APP_AUTH_DISABLED=true -``` - -### 5.3 子代理 - -- `CONTROL_TOKEN` 可与 `HUB_BRIDGE_TOKEN` 相同. -- 由 PM2 在对应 `crypto_monitor_*` 目录启动,`run_agent.sh` 加载该目录 `.env`. -- 只监听 **127.0.0.1:1520x**,不映射到公网. - ---- - -## 六,中控「系统设置」`hub_settings.json` - -在网页 **系统设置** 保存,或编辑 `manual_trading_hub/hub_settings.json`. - -云上 **`flask_url` 必须写浏览器能打开的 HTTPS 域名**(不要写 `127.0.0.1`,除非配合 `HUB_PUBLIC_ORIGIN` 做替换): - -| 字段 | 云上填法 | 说明 | -|------|----------|------| -| `flask_url` | `https://okx.example.com` | 用户浏览器,SSO 打开实例 | -| `agent_url` | `http://127.0.0.1:15201` | 仅中控本机访问子代理 | -| `enabled` | 按需 | 不参与监控的户可关 | -| `capabilities` | 按需 | `key` / `trend` 等 | - -**同机部署的两种写法(二选一):** - -1. **推荐**:每个实例 `flask_url` 直接写该实例的 `https://子域名`. -2. **备选**:`flask_url` 写 `http://127.0.0.1:5004`,中控 `.env` 设 `HUB_PUBLIC_ORIGIN=https://okx.example.com`(适合共用一个 IP,靠端口区分时). - -`agent_url` 始终用 **`http://127.0.0.1:1520x`**. - ---- - -## 七,PM2 启动顺序 - -代码路径示例:`/opt/crypto_monitor_user/`(按实际替换). - -```bash -cd /opt/crypto_monitor_user - -# 1)三个实例 Flask(各目录 ecosystem.config.cjs,进程名以你机器为准) -cd crypto_monitor_okx && pm2 start ecosystem.config.cjs -cd ../crypto_monitor_binance && pm2 start ecosystem.config.cjs -cd ../crypto_monitor_gate && pm2 start ecosystem.config.cjs - -# 2)中控 + 三个子代理(一条拉起 4 个进程:hub + 3 agent) -cd ../manual_trading_hub -python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -cp .env.example .env # 编辑填入真实值 -chmod +x scripts/run_hub.sh scripts/run_agent.sh -pm2 start ecosystem.config.cjs -pm2 save -pm2 startup # 按提示执行 sudo 命令后再 pm2 save -``` - -或: - -```bash -cd /opt/crypto_monitor_user/manual_trading_hub -bash scripts/pm2_hub.sh start -``` - -### PM2 进程一览 - -| 进程名 | 说明 | -|--------|------| -| `manual-trading-hub` | 中控 :5100 | -| `manual-agent-binance` | :15200 | -| `manual-agent-okx` | :15201 | -| `manual-agent-gate` | :15202 | -| `crypto_*`(各目录自定) | 各 Flask `APP_PORT` | - -不用 OKX 时可在 `.env` 设 `HUB_DISABLED_IDS=1`,或 `pm2 stop manual-agent-okx`. - ---- - -## 八,访问与登录(云上行为) - -| 访问方式 | 地址示例 | 需要什么 | -|----------|----------|----------| -| 中控监控 | `https://hub.example.com/monitor` | **中控** `HUB_USERNAME` / `HUB_PASSWORD` | -| 中控点「实例 / 策略交易 / 复盘」 | 自动打开 `https://okx.example.com/hub-sso?...` | 已登中控即可;**2 小时内,单次** SSO,**免输**实例密码 | -| 浏览器直链实例 | `https://okx.example.com` | 实例 **`APP_USERNAME` / `APP_PASSWORD`**(`/login`) | - -SSO 复用 **`HUB_BRIDGE_TOKEN`** 签名,详见 [局域网与反代部署说明.md §五](./局域网与反代部署说明.md). - ---- - -## 九,安全建议(云服务器必看) - -1. **SSH**:密钥登录,关闭密码登录;必要时改 SSH 端口. -2. **`HUB_BRIDGE_TOKEN`**:足够长,随机;勿提交 Git,勿写进前端页面. -3. **交易所 API Key**:仅放在各实例 `.env`;权限尽量最小化(勿随意开提币). -4. **中控**:公网必须设 `HUB_PASSWORD`;`HUB_TRUST_LAN=false`. -5. **实例**:云上 **`APP_AUTH_DISABLED` 必须为 false**(或未设置). -6. **备份**:定期备份各实例数据库 / SQLite 与 `hub_settings.json`. -7. **`.env` 换行**:Linux 上勿用 Windows CRLF;可用 `bash scripts/fix_env_crlf.sh`. - ---- - -## 十,部署后验收清单 - -- [ ] `https://hub.你的域名` 能打开并登录中控 -- [ ] 监控卡片有持仓/余额(子代理在线) -- [ ] 已登录中控 → 点「实例」→ **无**实例登录页,直接进入 -- [ ] 隐身窗口直开 `https://okx.你的域名` → 出现 **`/login`**,统一账号密码可进 -- [ ] `pm2 status`:hub,4×agent,用到的 `crypto_*` 均为 online -- [ ] 云安全组 **未** 对公网开放 5100,5000~5004,15200~15202 -- [ ] 三实例 `.env` 与中控 `HUB_BRIDGE_TOKEN` 一致 -- [ ] 实例启动日志无长期 `[hub_bridge] ImportError` - ---- - -## 十一,常见问题速查 - -| 现象 | 处理 | -|------|------| -| 从中控打开仍要实例密码 | 见 [常见问题.md §4.3](./常见问题.md);检查 token,重启 Flask,`hub_settings` 的 `key` | -| 监控无持仓 / 子代理不可用 | `curl http://127.0.0.1:15201/status`;查 `.env` CRLF,API 密钥 | -| 复盘/实例链接是 127.0.0.1 | `flask_url` 改为 https 域名,或设 `HUB_PUBLIC_ORIGIN` | -| 仅 Gate 子代理反复重启 | `.env` CRLF:`bash manual_trading_hub/scripts/fix_env_crlf.sh` | - ---- - -## 十二,与局域网部署的区别(简要) - -| 项目 | 云服务器 | 局域网 | -|------|----------|--------| -| 对外地址 | `https://子域名` | `http://内网IP:端口` | -| `flask_url` | 写 **域名** | 写 **内网 IP:端口** | -| 防火墙 | 只开 80/443 | 内网可开 5100,500x | -| SSL | 必须(宝塔证书) | 通常 HTTP 即可 | -| `HUB_COOKIE_SECURE` | 建议 `true` | HTTP 时用 `false` | - -局域网详细步骤见 [局域网与反代部署说明.md §三](./局域网与反代部署说明.md). diff --git a/manual_trading_hub/交易监管说明.md b/manual_trading_hub/交易监管说明.md deleted file mode 100644 index 2190773..0000000 --- a/manual_trading_hub/交易监管说明.md +++ /dev/null @@ -1,84 +0,0 @@ -# 交易监管(AI 教练) - -中控 **交易监管** 用于防止过度交易与频繁手动操作:在 **手动/中控开平仓** 与 **新开仓** 时自动推送至 **今日监管长会话**,并可选 **企业微信** 提醒;程序止盈/止损按「正常执行」鼓励,不计入频繁交易统计. - -入口:**AI 教练**(`/ai`)→ Tab **交易监管**,或微信链接(在系统设置中配置). - -## 监管范围 - -| 类型 | 识别 | 页内推送 | 微信(P0) | 频率统计 | -|------|------|----------|------------|----------| -| 实例手动平仓 | `result = 手动平仓` | ✓ | ✓ | ✓ | -| 中控平仓 | `result = 强制清仓` 等 | ✓ | ✓ | ✓ | -| 新开仓 | 监控板持仓 diff(0→有仓 / 新合约) | ✓ | ✓ | ✓ | -| 程序止盈 | 止盈 / 保本止盈 / 移动止盈 | ✓ | 可选 | ✗ | -| 程序止损 | 止损 | ✓ | 可选 | ✗ | -| 外部平仓 | 外部平仓,时间平仓 | ✗ | ✗ | ✗ | - -频率规则(间隔过短,30 分钟笔数,日笔数,连亏,平后快开)**只对手动/中控开平** 叠加 `[监管·频率]` 警告. - -## 会话 - -- 每个交易日 **一条长会话**(`bot_mode: supervisor`,标题 `今日监管 YYYY-MM-DD`). -- 系统消息(`role: system`)+ AI 短评(`assistant`)+ 用户回复(`user`)同线程. -- 与 **交易教练 / 普通聊天** 分离;监管会话不支持「新开对话」. - -## 系统设置 - -路径:**系统设置** → **交易监管 · 企业微信**(写入 `hub_settings.json` → `supervisor`). - -| 字段 | 说明 | -|------|------| -| `enabled` | 总开关 | -| `wechat_webhook` | **监管专用** 企业微信机器人(与三所实例 `.env` 的 `WECHAT_WEBHOOK` 独立) | -| `wechat_link_base` | 微信消息末尾跳转链接(**可单独修改**,如 `https://域名/ai?mode=supervisor`) | -| `wechat_prefix` | 消息前缀,默认 `【交易监管】` | -| `wechat_on_program_tp_sl` | 程序止盈/止损是否也发微信 | -| `manual_close_daily_warn` | 日手动平警告阈值(默认 2) | -| `interval_warn_minutes` | 两笔手动/中控平最短间隔(默认 15 分钟) | -| `freq_30m_count` | 30 分钟内笔数阈值(默认 2) | -| `reopen_after_close_minutes` | 手动平后再开仓警告间隔(默认 30 分钟) | - -`.env` 兜底(设置页保存优先): - -```env -SUPERVISOR_WECHAT_WEBHOOK=https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=... -SUPERVISOR_WECHAT_LINK=https://你的域名/ai?mode=supervisor -SUPERVISOR_POLL_INTERVAL_SEC=30 -``` - -## API - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/ai/supervisor/session` | 今日监管会话 | -| GET | `/api/ai/supervisor/stream` | SSE 版本推送 | -| POST | `/api/ai/supervisor/chat/send` | 用户回聊(JSON `{ "message": "..." }`) | -| GET | `/api/ai/supervisor/rules` | 当前阈值 | -| POST | `/api/ai/supervisor/refresh` | 立即扫描 | - -## 存储 - -| 文件 | 内容 | -|------|------| -| `hub_supervisor_state.json` | 已处理事件,持仓快照,频率统计 | -| `hub_ai_chat.json` | 监管会话(`bot_mode: supervisor`) | -| `hub_settings.json` | `supervisor` 配置节 | - -**首次启用** 会对当前交易日已有平仓做 **种子同步**(不补发历史推送),避免部署瞬间刷屏. - -## 与实例风控 - -实例 `account_risk_lib`(冷静期 / 日冻结)为 **硬拦截**;监管为 **软提醒 + 陪聊**,不绕过实例开仓限制. - -## 代码位置 - -| 模块 | 路径 | -|------|------| -| 规则与推送 | `hub_supervisor_lib.py` | -| 后台扫描 | `hub_supervisor_cache.py` | -| 会话 | `hub_ai/supervisor_store.py` | -| AI 评语/回聊 | `hub_ai/supervisor.py` | -| 提示词 | `hub_ai/prompts.py` → `SUPERVISOR_SYSTEM` | - -部署后重启中控:`pm2 restart manual-trading-hub`(或你的 hub 进程名). diff --git a/manual_trading_hub/使用说明.md b/manual_trading_hub/使用说明.md index 8a02b1d..f221d43 100644 --- a/manual_trading_hub/使用说明.md +++ b/manual_trading_hub/使用说明.md @@ -1,518 +1,24 @@ -# 多账户交易中控 — 使用说明 +# 中控 manual_trading_hub(用户版) -本文档说明 **manual_trading_hub** 的架构,启动方式,界面操作与故障排查.中控聚合三所 **持仓/条件单/余额/关键位/趋势计划监控 + 撤单/紧急全平**,并提供 **资金概况**,**行情区 K 线** 与 **内照明心(复盘语录 + 永久 K 线)**;**人工下单,关键位,策略交易(趋势回调 / 顺势加仓),交易复盘** 均在各实例网页操作(点监控卡片 **「实例」**).资金概况见 **[资金概况说明.md](./资金概况说明.md)**;行情区细则见 **[行情区说明.md](./行情区说明.md)**;内照明心见 **[docs/hub-symbol-archive-kline.md](../docs/hub-symbol-archive-kline.md)**. +聚合多账户监控与系统设置。人工下单与策略操作在各交易所实例网页完成。 ---- - -## 1. 架构总览 - -``` -浏览器 - ├─ /funds 资金概况 - ├─ /plan 开仓计划(计划录入 / 进行中 / 历史胜率) - ├─ /monitor 监控区(持仓,关键位,趋势计划,全平) - ├─ /market 行情区(K 线,技术指标,持仓价格线) - ├─ /archive 内照明心(复盘语录 + 交易记录 + 永久 5m K 线) - ├─ /funds 资金概况(总资金曲线,分户资金与回撤) - ├─ /dashboard 数据看板(三户当日总览,SSE 推送;见 [数据看板说明.md](./数据看板说明.md)) - ├─ /ai AI 教练(交易教练 / 普通聊天;见 [AI教练说明.md](./AI教练说明.md)) - └─ /settings 系统设置(hub_settings.json) - -中控 hub.py(默认 :5100) - ├─ HTTP → 子代理 agent.py × N(/status,/emergency/close-all) - └─ HTTP → 各实例 Flask(/api/hub/monitor,/api/price_snapshot 等只读聚合) -``` - -| 组件 | 职责 | 默认端口(可在设置页改) | -|------|------|-------------------------| -| **hub.py** | 聚合 UI,监控 API,全平 | `5100` | -| **agent.py** | 交易所只读状态,挂单/条件单查询与撤销 + 紧急市价全平 | 币安 `15200`,OKX `15201`,Gate `15202` | -| **crypto_monitor_*.app** | 策略库,关键位,人工单,趋势预览/执行 | 币安 `5001`,Gate `5000`,OKX `5004` | - -### 1.1 三账户默认配置 - -| id | 名称 | Flask | Agent | 监控能力(设置页勾选) | 默认启用 | -|----|------|-------|-------|------------------------|----------| -| 0 | 币安 | :5001 | :15200 | 关键位 + 趋势 | 是 | -| 1 | OKX | :5004 | :15201 | 关键位 + 趋势 | 是 | -| 2 | Gate | :5000 | :15202 | 关键位 + 趋势 | 是 | - -- **三所均已支持** 关键位,策略交易(趋势回调 + 顺势加仓);中控可同时勾 **监控关键位** + **监控趋势计划**(见 §4.2,§5). - -### 1.2 实例侧改动(最小) - -各 `crypto_monitor_*` 仅增加: - -1. `login_required` 走 `hub_auth.request_allowed`(支持请求头 `X-Hub-Token`). -2. 文件末尾 `hub_bridge.install_on_app(...)` 注册 `/api/hub/*`. - -业务逻辑,数据库,复盘页面 **未改**;复盘请打开各实例 `/records`(设置里的「复盘链接」). - ---- - -## 2. 环境准备 - -### 2.1 依赖安装 - -```bash -cd /opt/crypto_monitor_user/manual_trading_hub -python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -``` - -### 2.2 鉴权令牌(推荐生产启用) - -三实例 Flask 与中控,子代理需 **同一密钥**: - -| 变量 | 作用 | -|------|------| -| `HUB_BRIDGE_TOKEN` | 中控 → Flask 使用头 `X-Hub-Token`;各实例 `hub_auth` 校验 | -| `CONTROL_TOKEN` | 可与上相同;中控 → 子代理使用头 `X-Control-Token` | - -中控 `hub.py` 会读取 `HUB_BRIDGE_TOKEN`,若无则回退 `CONTROL_TOKEN`. - -**开发本机**可临时在各实例 `.env` 设 `APP_AUTH_DISABLED=true`,则 Flask 不校验令牌(仍建议子代理设 `CONTROL_TOKEN` 防误暴露). - -### 2.3 强制关闭某账户 - -```bash -# 在 manual_trading_hub/.env 中设置,或临时: -export HUB_DISABLED_IDS=1 # 默认即关闭 OKX(id=1) -``` - -与设置页「启用」取 **与** 关系:环境变量强制关闭时,网页勾选框会灰掉且无法启用. - -### 2.4 Web 登录(反代公网强烈建议) - -在 `manual_trading_hub/.env` 中配置: - -| 变量 | 说明 | -|------|------| -| `HUB_USERNAME` | 登录用户名;未设且已设密码时默认为 `admin` | -| `HUB_PASSWORD` | **非空即启用登录**;所有页面与 API(除登录页,`/api/ping`,`/assets`)须先登录 | -| `HUB_SESSION_SECRET` | 会话签名密钥(建议单独随机串) | -| `HUB_COOKIE_SECURE` | 建议 `true`:仅 **HTTPS** 访问时 Cookie 带 Secure;**HTTP 内网 IP:5100 仍可登录** | -| `HUB_SESSION_DAYS` | 登录保持天数,默认 `7` | - -- 登录页:`http://<中控地址>:5100/login` -- 顶栏 **退出** 清除会话. -- **域名(HTTPS)** 与 **内网 IP(HTTP)** Cookie 不共用,需分别登录一次. - -更多登录/Cookie 问题见 **[常见问题.md](./常见问题.md)** 第二节. - -### 2.5 配置文件 - -- 路径:`manual_trading_hub/hub_settings.json`(在网页 **系统设置 → 保存设置** 后写入). -- 未保存前使用 `settings_store.py` 内置默认三所地址. -- 建议 **不要** 把含内网 IP 的 `hub_settings.json` 提交到公开仓库. -- 环境变量模板:`manual_trading_hub/.env.example`;三实例模板中已补充 `HUB_BRIDGE_TOKEN` 说明. - ---- - -## 3. 启动顺序(Ubuntu + PM2) - -**原则**:代码在 **`/opt/crypto_monitor_user`**,先三实例 Flask,再中控(一条 PM2 含 3 agent + hub).环境见 **[docs/ubuntu-server.md](../docs/ubuntu-server.md)**. - -```bash -# 三所 Flask(示例:币安;其余三所同理) -cd /opt/crypto_monitor_user/crypto_monitor_binance -pm2 start ecosystem.config.cjs - -# 中控 + 子代理 -cd /opt/crypto_monitor_user/manual_trading_hub -pm2 start ecosystem.config.cjs -pm2 save -``` - -浏览器(本机或反代): - -- 监控区:`http://127.0.0.1:5100/monitor` -- 行情区:`http://127.0.0.1:5100/market` -- 内照明心:`http://127.0.0.1:5100/archive` -- 资金概况:`http://127.0.0.1:5100/funds` -- 系统设置:`http://127.0.0.1:5100/settings` - -验收: - -```bash -bash /opt/crypto_monitor_user/manual_trading_hub/scripts/verify_hub_deploy.sh -curl -s http://127.0.0.1:5100/api/ping -``` - ---- - -## 4. 页面操作说明 - -Chrome **桌面快捷方式**图标来自站点 `favicon` / `manifest`(已配置统一品牌图),说明见 **[docs/shortcut-icon.md](../docs/shortcut-icon.md)**. - -### 4.1 监控区 `/monitor` - -| 功能 | 说明 | -|------|------| -| **服务器状态** | 标题下方可折叠条(**默认收起**),摘要行显示 CPU/内存/硬盘;展开见四指标卡片(`GET /api/host/status`,每 5 秒刷新).**CPU 或内存 ≥85%** 时浏览器弹窗告警(降至 85% 以下后再次超标会再提示).依赖 `manual_trading_hub/.venv` 内 **psutil**(勿用系统 `pip`,见 [部署文档.md](./部署文档.md)).可选 `HUB_HOST_DISK_PATH` 指定监控磁盘 | -| **2×2 主界面** | 三所信息**完整展示**:余额,持仓表,委托/平仓,折叠委托单,下单监控,关键位,趋势/加仓摘要 | -| **全屏放大** | **点击卡片标题栏**(非按钮区)→ 该所**全屏**:每币种一张实盘风格持仓卡(趋势持仓显示**来源: 趋势回调计划**,**风险%**,**程序监控·止盈价**,**盈亏比**,与实例策略页一致);独立卡片:**关键位**,**下单监控**,**趋势回调**(单计划 **两列**:左=币种基本信息与 3×2 指标,右=**补仓计划明细**,底=**保本偏移%** 可编辑 + **保本移交** / **结束计划**(中控直接调实例,与 `/strategy` 一致),快照可用/计划保证金/杠杆),**顺势加仓** | -| **委托单折叠** | 仅「委托单」区块默认折叠;展开状态存浏览器本地,**5 秒刷新不重置** | -| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) | -| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`,`cancel-symbol` | -| **挂止盈止损** | 持仓行 **「委托」**:弹窗填止损/止盈价 → **先撤该合约全部条件单,再挂新 TP/SL**(币安 / OKX / Gate / Gate 三所统一,逻辑与各实例 `.env` 参数一致) | -| **平仓** | 持仓行「平仓」:仅平该方向仓位(子代理市价减仓) | -| **机器人单** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active),为本地监控计划,**不等于**交易所条件单 | -| **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) | -| **趋势计划** | 仅当该户勾选 **监控趋势计划** 时展示 `trend_pullback_plans`(active) | -| **实例 / 复盘** | 「实例」「策略交易」「复盘」经中控签发 **SSO 链接**(默认 2h,单次)打开,**免输**实例 `APP_USERNAME/PASSWORD`;直链实例 IP/域名仍走 `/login`.**云服务器**见 **[云服务器部署说明.md](./云服务器部署说明.md)**;局域网/反代见 **[局域网与反代部署说明.md](./局域网与反代部署说明.md)** | -| **关键位列表** | 来自 `/api/hub/monitor` + `/api/price_snapshot`;Flask 未连通时卡片提示原因;**Gate 户**无关键位块 | -| **该户全平** | `POST` 子代理 `/emergency/close-all`,仅平该 API Key 仓位 | -| **全局紧急全平** | 对所有已启用户依次全平(不含 `HUB_DISABLED_IDS` 强制关闭的 id) | -| **自动刷新** | 默认每 5 秒请求 `/api/monitor/board` | - -持仓数据以 **子代理 ccxt** 为准;关键位/趋势/机器人单以 **Flask 数据库** 为准.若 Flask 未启动,卡片仍会显示 agent 持仓,但下方策略信息可能为空或报错. - -### 4.2 行情区 `/market` - -| 功能 | 说明 | -|------|------| -| **K 线** | 选择已启用交易所 + 币种 + 周期;按需拉取,本地 `data/hub_kline.db` 缓存(默认保留 15 天) | -| **周期** | `1m` `5m` `15m` `1h` `2h` `4h` `12h` `1d` `1w` | -| **加载 / 强制刷新** | 普通加载优先缓存;强制刷新重拉并覆盖缓存 | -| **从监控跳转** | 点击持仓合约名带入品种,并显示入场/止损/止盈/委托与 K 线价格线 | -| **技术指标** | 可选 EMA 21/55,MACD,RSI | -| **快捷键** | **`F`** 全屏/退出;全屏时 **`Esc`** 退出;数字键切换周期(见 [行情区说明.md](./行情区说明.md)) | -| **自动刷新** | 约 5 秒更新最新 OHLCV | - -数据经中控 → 各实例 `GET /api/hub/ohlcv`(`hub_ohlcv_lib`).升级 hub 与三实例 Flask 后请 **强刷浏览器**;异常 K 线可点 **强制刷新**. - -### 4.2.1 内照明心 `/archive` - -| 功能 | 说明 | -|------|------| -| **复盘语录** | 左栏按日添加/编辑;最多 100 条 | -| **日期** | **本日 / 本周 / 本月 / 自选区间**(交易日 8:00 切日) | -| **区间统计** | 总开仓,犯病次数与占比,盈亏,剔除犯病盈亏,各交易所分项 | -| **筛选** | 盈利单,亏损单,犯病(仅过滤表格;统计栏不受此三项影响) | -| **交易记录** | 区间内开仓列表;犯病行红色字体;可编辑备注与犯病标签 | -| **K 线** | 默认折叠按需加载;独立库 `data/hub_symbol_archive.db`;仅存 **5m** 真源,**15m/1h/4h** 聚合 | -| **建档** | 最早开仓向前 **30 天** 5m 种子;之后每 **4h** 增量(Hub 后台 + 可点「同步」) | -| **视窗** | **持仓过程**(锚平仓)/ **进场决策**(锚开仓);支持时间输入跳转 | - -与行情区 `hub_kline.db`(15 天滚动)**分离**,建档起 **只增不删**.细则见 **[docs/hub-symbol-archive-kline.md](../docs/hub-symbol-archive-kline.md)**. - -### 4.2.2 资金概况 `/funds` - -| 功能 | 说明 | -|------|------| -| **总资金** | 已监控账户的 **资金户 + 交易户** 合计(不含浮盈) | -| **总曲线** | 自 **2026-06-09** 起,按北京时间交易日(默认 8:00 切日)每日一点,最多 **180** 天 | -| **最大回撤** | 基于总资金余额曲线(非平仓盈亏回撤) | -| **分户** | 每户资金/交易拆分,迷你曲线,分户回撤;**未监控** 不参与合计 | -| **快照** | 监控板聚合成功时写入 `hub_fund_history.json` | - -细则见 **[资金概况说明.md](./资金概况说明.md)**. - -### 4.2.3 数据看板 `/dashboard` - -| 功能 | 说明 | -|------|------| -| **总览** | 交易日,平仓盈亏,笔数,浮盈亏,资金合计,持仓数 | -| **分户** | 三户资金/交易账户,今日盈亏,浮盈亏;单日亏损 ≥ 资金合计 **5%** 高亮预警 | -| **平仓明细** | 当日平仓流水表 | -| **刷新** | 后台每 60s 聚合 + **SSE** 推送版本号;页面无整页轮询闪烁 | -| **主题** | 跟随顶栏亮/暗主题,卡片柔光样式(非霓虹背景) | - -细则见 **[数据看板说明.md](./数据看板说明.md)**. - -### 4.3 AI 教练 `/ai` - -| 功能 | 说明 | -|------|------| -| **交易教练** | 口语化陪聊;后台注入三户监控快照(不在页面展示今日总结) | -| **普通聊天** | 不绑交易数据 | -| **会话** | 多会话历史(切换/删除),消息复制;点 **「新开对话」** 清空当前上下文 | -| **模型** | 与三实例相同 `.env`(默认 `AI_PROVIDER=openai` + `OPENAI_*`;改 `ollama` 走本机),见 [AI教练说明.md](./AI教练说明.md) | -| **与实例复盘** | 深度单笔 journal 复盘仍在各所 `/records`;中控不做重复 | - -依赖三实例 `GET /api/hub/trades/today`(`hub_bridge`);升级代码后需 **重启三所 Flask**. - -### 4.4 系统设置 `/settings` - -**可用**:打开 http://127.0.0.1:5100/settings ,修改表格后点 **保存设置** 即写入 `hub_settings.json`;**重新加载** 从磁盘/默认再读(会重新套用 `HUB_DISABLED_IDS`).保存后监控区立即使用新 URL/启用状态,**无需重启 hub**. - -**显示与导航**(`hub_settings.json` → `display`): - -| 开关 | 说明 | -|------|------| -| 监控区资金/浮盈 | 关闭后监控卡片不显示资金户,交易户,浮盈亏列 | -| 顶栏「资金概况」 | 关闭后隐藏导航;直接访问 `/funds` 会跳回监控区 | -| 顶栏「数据看板」 | 关闭后隐藏导航;直接访问 `/dashboard` 会跳回监控区 | - -**下单,关键位,策略交易**:请在监控卡片点击 **「实例」** 或 **「策略交易」**(SSO),进入各 `crypto_monitor_*` 网页(`/trade`,`/key_monitor`,`/strategy`,`/strategy/records` 等).中控 **不** 提供下单区;**策略交易记录** 仅在实例顶栏查看(见 [策略交易说明.md](../策略交易说明.md) §五). - -| 列 | 含义 | -|----|------| -| 启用 | 是否参与监控与全局全平;被 `HUB_DISABLED_IDS` 锁定的无法勾选 | -| 显示名 | 监控卡片标题 | -| Flask URL | 实例根地址,如 `http://127.0.0.1:5001` | -| Agent URL | 子代理根地址,如 `http://127.0.0.1:15200` | -| 复盘链接 | 一般为 `{Flask}/records` | -| **监控关键位** | 勾选后卡片展示 **关键位** 列表 + 门控价(读 Flask `/api/price_snapshot`) | -| **监控趋势计划** | 勾选后卡片展示 **趋势回调** 运行中计划(`trend_pullback_plans` active) | -| id | 与 `HUB_DISABLED_IDS`,全平 API 路径中的 id 对应;新增户勿与已有 id 重复 | - -- **保存设置**:写入 `hub_settings.json`,重启 hub 后仍生效. -- **添加交易所**:见下文 §4.5(须先自建 Flask + agent,再在中控登记). -- **删**:从列表移除(保存后生效). - -#### 能力与「策略交易」的关系(重要) - -| 能力勾选 | 中控监控区 | 策略交易(趋势回调 / 顺势加仓) | -|----------|------------|----------------------------------| -| 监控关键位 | 显示关键位块 | **不控制**;在实例页 `/key_monitor` | -| 监控趋势计划 | 显示趋势计划块 | **不控制**;在实例页 `/strategy` 左栏操作 | -| 均未勾选 | 仅持仓,余额,机器人单 | 仍可在实例网页使用策略交易 | - -三所 Flask 均已注册 `hub_bridge` 且 **`has_trend=true`**,勾选「监控趋势计划」后才会从 `/api/hub/monitor` 拉取趋势数据.修改勾选后 **保存即可**,须 **重启对应 Flask** 仅在你刚升级了 `hub_bridge` 相关代码时. - ---- - -### 4.5 增加账户(例如再挂一个 Gate) - -中控 **不会** 自动启动进程,也 **不** 保存交易所 API Key.新增一户 = **复制/新建一套实例目录 + 独立 `.env` + 新端口 Flask/agent + 在中控登记一行**. - -#### 4.5.1 端口勿冲突(示例) - -| 用途 | 目录(示例) | Flask `APP_PORT` | Agent `PORT` | -|------|----------------|------------------|--------------| -| Gate(已有) | `crypto_monitor_gate` | 5000 | 15202 | -| **新增 Gate 子账户** | 复制为 `crypto_monitor_gate_2` 等 | **5005**(自定) | **15204**(自定) | - -`agent` 的 `PORT` 与 Flask 的 `APP_PORT` **必须不同**;且不要与币安 5001,OKX 5004,中控 5100 等占用端口相同. - -#### 4.5.2 新建实例目录 - -1. 复制整个 `crypto_monitor_gate` 到新目录(仓库内副本或 `/opt/` 下均可). -2. 在新目录:`cp .env.example .env`,至少修改: - - `APP_PORT` → 新 Flask 端口(如 5005) - - `DB_PATH` → 独立库(如 `crypto_gate2.db`),**勿**与其它实例共用 `crypto.db` - - `GATE_API_KEY` / `GATE_API_SECRET` → **该子账户** 密钥 - - `HUB_BRIDGE_TOKEN` → 与中控,其它实例 **相同** -3. 安装 venv 与依赖(`bash /opt/crypto_monitor_user/deploy/setup_env.sh --only gate` 或按 Gate 部署文档),启动: - -```bash -cd /opt/crypto_monitor_user/crypto_monitor_gate_2 -pm2 start ecosystem.config.cjs -``` - -4. 在中控 `ecosystem.config.cjs` 增加对应 agent,或单独 `run_agent.sh` 配置后 `pm2 restart`(勿与已有 agent 端口冲突). - -验收:`curl http://127.0.0.1:5005/login` 能开页;`curl http://127.0.0.1:15204/status` 返回 `ok`. - -#### 4.5.3 在中控登记 - -1. 打开 **系统设置** → **添加交易所**(或手改 `manual_trading_hub/hub_settings.json`). -2. 填写 **Flask URL**,**Agent URL**,**id**(如 `4`),**显示名**. -3. 能力建议: - - 训练/关键位户:**监控关键位** + **监控趋势计划**(若也要在中控看趋势计划); - - 纯趋势户:只勾 **监控趋势计划**. -4. 勾选 **启用** → **保存设置**. -5. 在 **监控区** 应出现新卡片;点 **实例** 进入该户网页做下单与 **策略交易**. - -PM2:仓库 `ecosystem.config.cjs` 默认只有三 agent;额外子账户需自行 `pm2 start` 或手工终端,与是否改 hub 源码无关. - ---- - -## 5. 能力矩阵(监控展示,建议勾选) - -| 账户 | 监控关键位 | 监控趋势计划 | 策略交易(实例页) | -|------|:----------:|:--------------:|:------------------:| -| 币安 | ✓ 建议 | ✓ 建议 | `/strategy` | -| OKX | ✓ 建议 | ✓ 建议 | `/strategy` | -| Gate | ✓ 建议 | ✓ 建议 | `/strategy` | -| Gate | —(通常不勾) | ✓ | `/strategy` | - -「建议」表示中控卡片展示对应块;**不勾** 仍可在该实例网页使用关键位或策略交易. - ---- - -## 6. HTTP API 摘要(中控) - -访问控制: - -- **IP**:默认允许本机与 RFC1918 私网(`HUB_TRUST_LAN=true`);公网 IP 直连返回 403. -- **登录**:设置 `HUB_PASSWORD` 后须用户名+密码登录(`HUB_USERNAME`,未设时默认 `admin`);反代到公网时**务必设置**. - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/settings` | 读取配置 | -| POST | `/api/settings` | 保存配置 | -| GET | `/api/monitor/board` | 监控聚合 | -| POST | `/api/close/{id}` | 单户全平 | -| POST | `/api/close-all` | 全局全平,body 可选 `exclude_ids` | -| GET | `/api/auth/status` | 是否需登录,是否已登录 | -| POST | `/api/auth/login` | body `{"username":"...","password":"..."}` | -| POST | `/api/auth/logout` | 退出 | -| GET | `/api/ping` | 版本与健康检查(**免登录**) | -| GET | `/api/chart/meta` | 行情区:交易所,周期,limit | -| GET | `/api/chart/ohlcv` | 行情区 K 线(`exchange_key`,`symbol`,`timeframe`,可选 `refresh=1`) | -| GET | `/api/hub/fund-overview` | 资金概况:总/分户资金,180 日曲线,回撤 | -| GET | `/api/archive/meta` | 内照明心:周期,同步间隔 | -| GET | `/api/archive/daily-trades` | 内照明心:区间交易与统计(`period` / `date_from` / `date_to`) | -| GET | `/api/archive/quotes` | 内照明心:复盘语录 | -| GET | `/api/archive/list` | 币种列表(筛选 query) | -| GET | `/api/archive/detail` | 单币种交易时间线 | -| GET | `/api/archive/ohlcv` | 档案 K 线视窗 | -| PATCH | `/api/archive/trade/{exchange_key}/{trade_id}` | 犯病/情绪标签与备注 | -| POST | `/api/archive/sync` | 立即同步三所交易与 K 线 | - -已移除的 `/api/trade/*` 若被旧缓存页面请求,返回 **410** 并提示前往各实例网页. - -实例侧(中控只读;下单/关键位/趋势在实例网页): +## 入口(默认端口 5100) | 路径 | 说明 | |------|------| -| `/api/hub/ping` | 连通与能力 | -| `/api/hub/monitor` | 关键位,机器人单,趋势计划 | -| `/api/hub/ohlcv` | 行情区 OHLCV(ccxt 拉取,供中控聚合缓存) | -| `/api/hub/trades/archive` | 内照明心:近 N 天已平仓(`days` / `limit`) | +| `/license` | 软件授权(未激活时自动跳转) | +| `/monitor` | 监控区 | +| `/settings` | 系统设置 | +| `/login` | 登录 | ---- +完整装机见 [部署文档.md](./部署文档.md)。授权见 [docs/license.md](../docs/license.md)。 -## 7. 环境变量速查 - -### 中控 hub.py - -| 变量 | 默认 | 说明 | -|------|------|------| -| `HUB_HOST` | `0.0.0.0` | 监听地址 | -| `HUB_PORT` | `5100` | 监听端口 | -| `HUB_BRIDGE_TOKEN` | 空 | Flask 桥接令牌;可同 `CONTROL_TOKEN` | -| `HUB_DISABLED_IDS` | `1` | 逗号分隔,强制关闭的账户 id | -| `HUB_TRUST_LAN` | `true` | `false` 时仅本机可访问中控页面 | -| `HUB_USERNAME` | `admin` | 登录用户名(仅当已设密码时生效) | -| `HUB_PASSWORD` | (空) | 非空即启用 Web 登录 | -| `HUB_SESSION_SECRET` | 用户名+密码 | 会话 Cookie 签名密钥 | -| `HUB_COOKIE_SECURE` | `false` | HTTPS 反代建议 `true`(仅 HTTPS 发 Secure Cookie,HTTP 内网 IP 仍可登) | -| `HUB_SESSION_DAYS` | `7` | 登录保持天数 | -| `HUB_KLINE_RETENTION_DAYS` | `15` | 行情区 K 线库保留天数 | -| `HUB_KLINE_DB_PATH` | `data/hub_kline.db` | K 线 SQLite 路径 | -| `HUB_ARCHIVE_DB_PATH` | `data/hub_symbol_archive.db` | 内照明心永久 K 线库 | -| `HUB_ARCHIVE_SYNC_INTERVAL_SEC` | `14400` | 档案 K 线后台同步间隔(秒) | -| `HUB_ARCHIVE_TRADE_DAYS` | `365` | 同步交易记录回看天数 | -| `HUB_ARCHIVE_TRADE_LIMIT` | `2000` | 单所同步交易条数上限 | - -### 子代理 agent.py - -| 变量 | 说明 | -|------|------| -| `EXCHANGE` | `binance` / `okx` / `gate` | -| `PORT` / `HOST` | 监听 | -| `CONTROL_TOKEN` | 与中控一致时必填头 `X-Control-Token` | - -### 各实例 Flask - -| 变量 | 说明 | -|------|------| -| `HUB_BRIDGE_TOKEN` | 与中控一致 | -| `APP_AUTH_DISABLED` | `true` 时跳过登录与令牌(仅建议本机调试) | - ---- - -## 8. 安全与边界 - -1. **中控不下单**:开仓,关键位,趋势回调仅在各实例网页操作. -2. **全平为市价减仓**:监控区全平不可撤销,操作前二次确认. -3. **子代理建议只监听 127.0.0.1**,不要对局域网暴露 API Key 通道. -4. **公网暴露 hub**:必须设置 `HUB_USERNAME` + `HUB_PASSWORD`;HTTPS 反代建议 `HUB_COOKIE_SECURE=true`;亦可 `HUB_HOST=127.0.0.1` 仅本机监听 + 反代. -5. **复盘不在中控**:时间筛选,导出 CSV,编辑笔记仍在各实例 `/records`. -6. **OKX 默认关**:避免未部署 OKX 时监控卡片持续报错. - ---- - -## 9. 故障排查(速查) - -完整实录(含 `api_trade_key`,`multipart`,git 版本,PM2 等)见 **[常见问题.md](./常见问题.md)**. - -| 现象 | 可能原因 | 处理 | -|------|----------|------| -| 监控卡片「子代理不可用」 | agent 未启动或端口错 | 检查 Agent URL;`pm2 restart` agent | -| 无关键位/趋势信息 | Flask 未起或 hub_bridge 未加载 | 启动 `crypto_*`;`curl .../api/hub/ping` | -| 全平 401 | `CONTROL_TOKEN` 与中控不一致 | 与 `HUB_BRIDGE_TOKEN` 对齐 | -| OKX 始终灰色 | `HUB_DISABLED_IDS=1` | 改掉环境变量并在设置页启用 | -| 打开即跳转登录 | 已设 `HUB_PASSWORD` | 正常;访问 `/login` | -| 域名能登,IP:5100 不能 | Secure Cookie + HTTP | 见常见问题 §2.1;或分别登录 | -| 添加关键位报错 / SyntaxError | 旧前端或旧 hub 代码 | 强刷浏览器;`git pull` + `verify_hub_deploy.sh` | -| `curl /api/ping` 非 JSON | hub 未启动 | `pm2 restart manual-trading-hub` | -| K 线只有约 300 根 | 旧版未分页 | `git pull` 三实例 + hub,强制刷新 | -| 12h 周期异常 | 无原生 12h 或旧缓存 | 强制刷新;见 [行情区说明.md](./行情区说明.md) | - -**运维脚本**(在 `manual_trading_hub` 目录执行): - -| 脚本 | 作用 | -|------|------| -| `scripts/fix_hub_deps.sh` | 安装依赖(含 `python-multipart`) | -| `scripts/verify_hub_deploy.sh` | 检查代码版本与 ping | -| `scripts/fix_env_crlf.sh` | 修复 `.env` 的 CRLF 导致 agent 起不来 | - -手动探测实例桥接: - -```bash -curl -sS -H "X-Hub-Token: 你的令牌" http://127.0.0.1:5001/api/hub/ping -``` - ---- - -## 10. 与旧版 README 的差异 - -早期中控 **仅监控 + 全平**,使用环境变量 `HUB_AGENTS` 列表.当前版本改为: - -- **hub_settings.json**(或内置默认)管理三所 URL 与能力; -- **三页 UI**:监控 / 行情 / 设置; -- 通过 **hub_bridge** 只读聚合监控数据. - -子代理 `agent.py` 仍负责持仓与全平;`HUB_AGENTS` 环境变量在新版 hub 中 **不再使用**(以设置文件为准). - -**PM2 守护**: +## 快速启动 ```bash cd /opt/crypto_monitor_user/manual_trading_hub -python3 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -cp .env.example .env -pm2 start ecosystem.config.cjs # 一次启动 3 个 agent + manual-trading-hub -pm2 save && pm2 startup +cp .env.example .env # 配置 HUB_BRIDGE_TOKEN 等 +# 由仓库根 deploy/pm2_start_all.sh 一并拉起,或按部署文档启动 ``` -快捷:`bash scripts/pm2_hub.sh start|restart|logs`(同样 hub+agent 一起). - -更细的安装顺序,反代,验收见 **《部署文档.md》**;PM2 见 **[scripts/后台运行-Ubuntu.md](./scripts/后台运行-Ubuntu.md)**. - ---- - -## 11. 日常推荐流程 - -1. 启动三所 **agent** + **Flask**(OKX 按需). -2. 启动 **hub.py**,打开监控区确认持仓与关键位门控正常. -3. 看 K 线 → **行情区** 或监控区点击合约名跳转;异常图表点 **强制刷新**. -4. 开仓,关键位,趋势 → 点击监控卡片「实例」进入对应 Flask. -5. 复盘,导出记录 → 点击「复盘」进入 `/records`. -6. 异常行情 → 单户全平或全局紧急全平. - -增加账户步骤见 **§4.4**;无需改 `hub.py` 源码,但须该户 Flask 已 `git pull` 并 **重启**(`hub_bridge` + `has_trend` + `ohlcv`),且 agent 已部署. - ---- - -## 12. 文档索引 - -| 文档 | 内容 | -|------|------| -| [使用说明.md](./使用说明.md) | 本文 | -| [行情区说明.md](./行情区说明.md) | K 线周期,缓存,快捷键,API | -| [开仓计划说明.md](./开仓计划说明.md) | 计划录入,归档,胜率统计 | -| [docs/hub-symbol-archive-kline.md](../docs/hub-symbol-archive-kline.md) | 内照明心,区间统计,永久 5m,建档与同步 | -| [部署文档.md](./部署文档.md) | Ubuntu / PM2 / 反代 | -| [常见问题.md](./常见问题.md) | 故障实录与排障 | -| [README.md](./README.md) | 速览 | -| [.env.example](./.env.example) | 环境变量模板 | -| [scripts/后台运行-Ubuntu.md](./scripts/后台运行-Ubuntu.md) | PM2 常驻 | -| [docs/ubuntu-server.md](../docs/ubuntu-server.md) | Ubuntu 环境总览 | +故障排查见 [常见问题.md](./常见问题.md)。 diff --git a/manual_trading_hub/局域网与反代部署说明.md b/manual_trading_hub/局域网与反代部署说明.md deleted file mode 100644 index 86bb958..0000000 --- a/manual_trading_hub/局域网与反代部署说明.md +++ /dev/null @@ -1,226 +0,0 @@ -# 中控 · 局域网与反代部署说明 - -本文说明在 **局域网(IP + 端口)** 与 **宝塔/Nginx 反代(域名)** 两种场景下,如何配置中控与各实例,并实现: - -- **从中控** 点「实例 / 策略交易 / 复盘」→ **免输入** 实例网页密码(SSO 临时链接,默认 **2 小时** 内有效,**单次使用**) -- **浏览器直链** 实例地址(反代域名或 `http://IP:端口`)→ 进入 **`/login`**,输入统一 **`APP_USERNAME` / `APP_PASSWORD`** - -SSO 签名复用 **`HUB_BRIDGE_TOKEN`**(与中控调实例 API 相同,三所 `.env` 与 `manual_trading_hub/.env` 保持一致). - -**云服务器(VPS)** 的硬件,安全组,宝塔,环境变量与验收清单见 **[云服务器部署说明.md](./云服务器部署说明.md)**. - ---- - -## 一,两种访问方式对照 - -| 项目 | 局域网 | 反代(域名) | -|------|--------|----------------| -| 中控地址 | `http://内网IP:5100` | `https://hub.你的域名.com` | -| 实例地址(浏览器) | `http://内网IP:5004` 等 | `https://okx.你的域名.com` 等 | -| `hub_settings` 里 `flask_url` | 建议写 **`http://内网IP:端口`** | 建议写 **`https://该实例域名`**(与浏览器一致) | -| 中控本机调实例 API | 可与浏览器相同;同机也可用 `http://127.0.0.1:端口` + `HUB_PUBLIC_ORIGIN` | 同机可用 `127.0.0.1:端口` 或域名(需 Nginx 转发 `X-Hub-Token`) | -| `HUB_PUBLIC_ORIGIN` | 若 `flask_url` 填 `127.0.0.1`,**必填** `http://内网IP` | 若 `flask_url` 已是完整域名,**可不设** | -| 宝塔 | 可不装反代,直连端口 | 每实例一个站点 + SSL;中控单独站点 | -| 直链登录 | 实例 `/login` | 实例 `/login` | -| 从中控打开 | `/hub-sso?token=...` 自动登录 | 同上 | - ---- - -## 二,共用环境变量(必配) - -### 2.1 中控 `manual_trading_hub/.env` - -```bash -HUB_BRIDGE_TOKEN=请填一长串随机字符 -HUB_USERNAME=admin # 中控登录(建议设置) -HUB_PASSWORD=你的中控密码 -HUB_SSO_TTL_SEC=7200 # 可选,默认 7200 = 2 小时 -``` - -### 2.2 三个实例 `crypto_monitor_*/.env` - -每个目录相同(**直链**时用这套登录实例网页): - -```bash -HUB_BRIDGE_TOKEN=与中控完全相同 -APP_USERNAME=统一用户名 -APP_PASSWORD=统一密码 -# 云上切勿 APP_AUTH_DISABLED=true -``` - -### 2.3 子代理 - -`CONTROL_TOKEN` 可与 `HUB_BRIDGE_TOKEN` 相同;子代理只监听 `127.0.0.1`,**不要**对公网暴露 `15200`~`15202`. - ---- - -## 三,局域网部署(IP + 端口) - -适用:家里/办公室内网,例如服务器 `192.168.8.6`. - -### 3.1 端口约定(示例,以你实际为准) - -| 服务 | 端口 | -|------|------| -| 中控 hub | 5100 | -| OKX Flask | 5004 | -| 币安 Flask | 5001 | -| Gate | 5000 | -| agent | 15200~15202(仅本机) | - -### 3.2 系统设置 `hub_settings.json`(网页「系统设置」保存) - -浏览器里你会打开的地址,应使用 **内网 IP**,不要用 `127.0.0.1`(否则别的电脑上的浏览器会连到你本机): - -```json -{ - "flask_url": "http://192.168.8.6:5004", - "agent_url": "http://127.0.0.1:15201" -} -``` - -说明: - -- **`flask_url`**:给浏览器用的实例页地址 → 写 **`http://192.168.8.6:端口`** -- **`agent_url`**:仅中控服务器访问 → 写 **`http://127.0.0.1:1520x`** - -各账户按上表改端口即可. - -### 3.3 可选:`flask_url` 仍写 127.0.0.1 时 - -若坚持 `flask_url` 为 `http://127.0.0.1:5004`(仅 hub 与本机 Flask 同机),在中控 `.env` 增加: - -```bash -HUB_PUBLIC_ORIGIN=http://192.168.8.6 -``` - -中控会把返回给前端的链接从 `127.0.0.1` 替换为 `192.168.8.6`(端口保留). - -### 3.4 访问方式 - -1. 中控:`http://192.168.8.6:5100` → 登录中控 → 点「实例」→ 新标签进入 OKX,**无需**再输实例密码. -2. 直链:`http://192.168.8.6:5004` → 出现登录页 → 输入 `APP_USERNAME` / `APP_PASSWORD`. - -### 3.5 防火墙 - -内网自用:放行 `5100`,各 `APP_PORT`;**不要**对公网开放 agent 端口. - ---- - -## 四,反代部署(域名 + 宝塔) - -适用:云服务器,对外用 HTTPS 域名. - -### 4.1 域名规划(示例) - -| 站点 | 反代到 | -|------|--------| -| `hub.example.com` | `127.0.0.1:5100` | -| `okx.example.com` | `127.0.0.1:5004` | -| `binance.example.com` | `127.0.0.1:5001` | -| `gate.example.com` | `127.0.0.1:5000` | - -Flask / hub 进程仍只监听 **127.0.0.1** 或 `0.0.0.0` 本机端口,由 Nginx 对外提供 HTTPS. - -### 4.2 宝塔操作要点 - -1. 每个域名 → **反向代理** → 目标 `http://127.0.0.1:对应端口`. -2. 申请 **SSL**(Let’s Encrypt). -3. **不要**再给实例站加一层宝塔「访问密码」(避免与 Flask `/login` 重复);直链鉴权用 **`APP_USERNAME` / `APP_PASSWORD`** 即可. -4. 自定义 Nginx 配置中保留 WebSocket/大 body 如需;确保代理头: - -```nginx -proxy_set_header Host $host; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; -``` - -中控请求实例 API 时会带 **`X-Hub-Token`**,Nginx 默认会转发请求头,一般无需额外配置. - -### 4.3 `hub_settings` 示例(反代) - -```json -{ - "flask_url": "https://okx.example.com", - "agent_url": "http://127.0.0.1:15201" -} -``` - -- 浏览器与 SSO 链接使用 **`https://okx.example.com`**. -- 中控服务器拉 `/api/hub/*` 仍走本机 `agent_url`;`flask_url` 用域名时,hub 会请求 `https://okx.example.com/api/...`(同机可通即可). - -同机部署时也可: - -- `flask_url`: `http://127.0.0.1:5004` -- `HUB_PUBLIC_ORIGIN`: `https://okx.example.com` - -仅当**所有实例共用一个对外 IP,靠端口区分**时才适合用 `HUB_PUBLIC_ORIGIN`;**每实例独立域名**时,请直接在 `flask_url` 写该实例域名. - -### 4.4 中控 `.env`(反代建议) - -```bash -HUB_BRIDGE_TOKEN=... -HUB_USERNAME=... -HUB_PASSWORD=... -HUB_COOKIE_SECURE=true # 中控为 HTTPS 时建议开启 -``` - -### 4.5 访问方式 - -1. `https://hub.example.com` 登录中控 → 点「打开实例」→ `https://okx.example.com/hub-sso?...` → 进入系统. -2. 地址栏直接输入 `https://okx.example.com` → `/login` → 实例账号密码. - ---- - -## 五,SSO 行为说明(2 小时) - -| 项 | 说明 | -|----|------| -| 有效期 | 默认 **7200 秒(2 小时)**,`HUB_SSO_TTL_SEC` 可改 | -| 单次使用 | 同一链接成功登录后 **不能再用**;需在中控重新点「打开实例」 | -| 密钥 | 复用 **`HUB_BRIDGE_TOKEN`** | -| 直链 | 无 token → 正常 **`/login`** | - ---- - -## 六,部署与重启顺序 - -```bash -cd /opt/crypto_monitor_user -# 各实例 -pm2 restart crypto_okx crypto_binance crypto_gate # 名称以你为准 - -cd manual_trading_hub -pm2 restart manual-trading-hub manual-agent-binance manual-agent-okx manual-agent-gate -``` - -改 `hub_settings` 或 `.env` 后重启 **hub + 对应实例 Flask**(`hub_bridge` 与 `/hub-sso` 在实例进程内). - ---- - -## 七,验收清单 - -- [ ] 三实例 `.env` 与中控 `HUB_BRIDGE_TOKEN` 一致 -- [ ] 三实例 `APP_USERNAME` / `APP_PASSWORD` 一致 -- [ ] 局域网:`flask_url` 为 `http://IP:端口`;反代:`flask_url` 为 `https://域名` -- [ ] 已登录中控 → 点「实例」→ **无**实例登录页 -- [ ] 隐身窗口直链实例域名/IP → **有** `/login` -- [ ] 复制「打开实例」完整 URL,用过一次后再开 → 失效并回到登录页 - ---- - -## 八,常见问题 - -**Q:从中控打开仍要登录?** -- 检查实例是否已 `git pull` 并重启(需有 `/hub-sso`). -- `HUB_BRIDGE_TOKEN` 是否三所一致. -- `hub_settings` 里该账户 `key` 是否与 `install_on_app(exchange=...)` 一致(如 `okx`,`binance`,`gate`,`gate`). - -**Q:直链也要登录中控?** -- 不应.直链只走实例 `/login`.若跳到中控,检查是否点错链接或 Nginx 配错站点. - -**Q:链接多久失效?** -- 签发后 **2 小时**内且 **未使用过**;过期或已用需在中控重新点打开. - -更多故障见 [常见问题.md](./常见问题.md),[部署文档.md](./部署文档.md). diff --git a/manual_trading_hub/开仓计划说明.md b/manual_trading_hub/开仓计划说明.md deleted file mode 100644 index 9e46afb..0000000 --- a/manual_trading_hub/开仓计划说明.md +++ /dev/null @@ -1,85 +0,0 @@ -# 开仓计划 - -中控顶栏 **开仓计划**(`/plan`)用于记录开仓前的计划,跟踪进行中条目,并在填写结果后归档;支持按币种,趋势周期,入场方案统计胜率. - -## 入口 - -- 顶栏:**资金概况** 与 **监控区** 之间 → **开仓计划** -- 路由:`/plan` - -## 页面结构 - -| 区域 | 功能 | -|------|------| -| 左侧 · 新建计划 | 填写计划字段,保存后进入「进行中」 | -| 左侧 · 进行中 | 修改,删除,填写结果并归档 | -| 右侧 · 计划历史 | 一行一条摘要,点击查看详情 | -| 右侧 · 数据统计 | 胜率表(可切换维度与时间范围) | - -## 字段说明 - -| 字段 | 说明 | -|------|------| -| 日期 | 计划日期(日期选择器,可手输 `YYYY-MM-DD`) | -| 交易所 | 三所:binance / okx / gate(来自 hub 已启用账户) | -| 币种 | 输入 `BTC` 或 `BTC/USDT`,自动规范为 `XXX/USDT` | -| 类型 | 趋势单 / 波段单 / 日内短线 | -| 趋势周期 | 5m / 15m / 30m / 1h / 4h / 1d | -| 入场周期 | 1m / 5m / 15m / 30m / 1h | -| 方向 | 多 / 空 | -| 目标位 | 文本 | -| 当前区间 | 文本 | -| 入场方案 | **仅进行中**填写:突破 / 假突破 / 箱体拐点(根据实际进场选择;归档前必选) | -| 结果 | **仅进行中**可填:盈 / 亏;**必选其一才归档** | -| 盈亏 | **可选**数字(U),不参与是否归档 | -| 备注 | 文本 | - -## 业务流程 - -1. **新建** → 状态 `active`(进行中),**不含入场方案** -2. **进行中** → 选择/修改 **入场方案**(根据实际进场填写),可改备注,价位等 -3. **删除** → 仅 **未填结果** 的进行中计划可删 -4. **归档** → 在进行中选择 **盈/亏** 并点「填写结果并归档」→ 状态 `archived`,移入计划历史 - -## 数据统计 - -- **默认**:全部历史 -- **时间**:全部 / 本周 / 本月 / 自选区间 -- **维度 Tab**:币种 | 趋势周期 | 入场方案 -- **胜率**:盈利 ÷ (盈利 + 亏损),仅统计已归档且结果=盈/亏 的计划 - -## API - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/entry-plans/meta` | 枚举项 + 交易所列表 | -| GET | `/api/entry-plans?status=active\|archived` | 列表 | -| GET | `/api/entry-plans/{id}` | 详情 | -| POST | `/api/entry-plans` | 新建 | -| PATCH | `/api/entry-plans/{id}` | 更新;写入 `result` 时自动归档 | -| DELETE | `/api/entry-plans/{id}` | 删除(仅 active) | -| GET | `/api/entry-plans/stats` | 统计;参数 `dimension`,`period`,`date_from`,`date_to` | - -## 存储 - -- SQLite:`manual_trading_hub/data/hub_entry_plans.db` -- 环境变量:`HUB_ENTRY_PLAN_DB_PATH`(可选自定义路径) - -## 部署 - -```bash -git pull -pm2 restart manual-trading-hub -``` - -浏览器访问 `/plan` 并 **Ctrl+F5** 强刷静态资源. - -## 相关代码 - -| 文件 | 说明 | -|------|------| -| `hub_entry_plan_lib.py` | 库表,CRUD,统计 | -| `manual_trading_hub/hub.py` | REST API | -| `manual_trading_hub/static/plan.js` | 前端逻辑 | -| `manual_trading_hub/static/index.html` | 页面 DOM | -| `tests/test_hub_entry_plan_lib.py` | 单元测试 | diff --git a/manual_trading_hub/数据看板说明.md b/manual_trading_hub/数据看板说明.md deleted file mode 100644 index 5f977a3..0000000 --- a/manual_trading_hub/数据看板说明.md +++ /dev/null @@ -1,50 +0,0 @@ -# 中控数据看板说明 - -入口:**`/dashboard`**(顶栏「数据看板」). - -## 能力 - -| 区块 | 说明 | -|------|------| -| **总览 KPI** | 交易日,平仓盈亏,笔数,浮盈亏,资金合计,实盘持仓 | -| **分户明细** | 三户资金/交易账户,今日盈亏,浮盈亏,备注;未启用显示「未监控」 | -| **平仓明细** | 当日平仓流水(合约,方向,结果,盈亏,时间) | -| **风险预警** | 单户单日平仓亏损 ≥ 资金合计 **5%** 时横幅 + 卡片高亮 | - -纯数据聚合,**不调用 AI**.交易日口径与实例一致(`TRADING_DAY_RESET_HOUR`,默认 8 点). - -## 刷新机制(SSE) - -与监控区 board 类似,采用 **后台聚合 + SSE 推送版本号**: - -1. `hub.py` 启动后 `dashboard_store` 每 **60s**(`DASHBOARD_POLL_INTERVAL_SEC`)聚合三户数据到内存快照. -2. 浏览器打开看板页后连接 `GET /api/dashboard/stream`(`event: dashboard`). -3. 收到新版本号后拉取 `GET /api/dashboard/daily` 快照并局部渲染,**无整页轮询闪烁**. -4. 监控区触发 board 刷新(全平,撤单等)时,会一并 `request_refresh` 看板,尽量与实盘同步. -5. 「立即刷新」→ `POST /api/dashboard/refresh` 触发下一轮聚合. - -可选环境变量:`HUB_DASHBOARD_SSE_HEARTBEAT_SEC`(默认 25,SSE 心跳间隔). - -## 主题与样式 - -- 跟随中控顶栏 **亮/暗主题**(`theme.js`),使用 `--panel` / `--border` / `--accent` 等变量. -- 卡片采用 **柔光阴影**(非霓虹渐变背景);亮色主题下为浅灰投影,暗色主题为轻微内高光. -- 盈亏仍用绿/红语义色,与全局一致. - -## API - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/dashboard/daily` | 当前交易日快照(含 `dashboard_version`) | -| GET | `/api/dashboard/stream` | SSE 版本推送 | -| POST | `/api/dashboard/refresh` | 请求立即重聚合 | - -`GET /api/ping` 含 `dashboard_version`,`dashboard_poll_interval_sec` 等字段. - -## 相关文件 - -- `hub_dashboard.py` — 聚合逻辑 -- `hub_dashboard_cache.py` — 后台轮询 + SSE -- `static/dashboard.js` / `dashboard.css` — 前端 - -部署后 `git pull` 并 `pm2 restart manual-trading-hub`. diff --git a/manual_trading_hub/本地数据迁移到云端.md b/manual_trading_hub/本地数据迁移到云端.md deleted file mode 100644 index bd5b962..0000000 --- a/manual_trading_hub/本地数据迁移到云端.md +++ /dev/null @@ -1,268 +0,0 @@ -# 本地数据备份与迁移到云服务器 - -本文说明如何把 **本机** 上运行的 `crypto_monitor`(三实例 + 中控)的**业务数据**迁到 **云 VPS**,并正确改配置. -**不迁移** 本机 Python 虚拟环境(`.venv`),云上重新 `pip install` 即可. - -相关:[云服务器部署说明.md](./云服务器部署说明.md) · [部署文档.md](./部署文档.md) - ---- - -## 一,要迁什么,不迁什么 - -### 必须迁移(业务数据) - -| 路径(每个实例目录下) | 内容 | -|------------------------|------| -| `crypto.db`(或 `.env` 里 `DB_PATH` 指向的文件) | 监控单,关键位,交易记录,复盘,运行时开关等 **SQLite 全库** | -| `static/images/`(或 `UPLOAD_DIR`) | 上传图,复盘截图等 | -| `static/images/order_charts/`(或 `ORDER_CHART_DIR`) | 订单 K 线图(若开启) | - -三个实例 **各有一份独立库**: - -- `crypto_monitor_binance/crypto.db` -- `crypto_monitor_okx/crypto.db` -- `crypto_monitor_gate/crypto.db` -- `crypto_monitor_gate/crypto.db` - -### 中控额外迁移 - -| 路径 | 内容 | -|------|------| -| `manual_trading_hub/hub_settings.json` | 账户 URL,启用状态,能力勾选(网页「系统设置」保存的文件) | -| `manual_trading_hub/hub_ai_summaries.json` | 中控 AI 今日总结(`/ai`) | -| `manual_trading_hub/hub_ai_chat.json` | 中控 AI 聊天会话 | - -### 不要直接覆盖拷贝(需在云上重写) - -| 文件 | 说明 | -|------|------| -| 各目录 `.env` | 含 API 密钥:可在云上**手工新建**,从本机抄密钥,但须改 **`flask_url`,代理,公网相关项**(见下文) | -| `.venv/`,`__pycache__/` | 云上重建 | -| PM2 日志 | 无需迁 | - -### 可选 - -- 本机 `manual_trading_hub/.env` 里的 `HUB_BRIDGE_TOKEN`,`HUB_PASSWORD` 等:记下后在云上填入,**不要**把含密钥的 `.env` 发到公开网盘. - ---- - -## 二,迁移前准备(本地) - -### 1. 停服务(避免数据库半写入) - -```bash -# 本机:停中控与子代理 -cd manual_trading_hub -pm2 stop manual-trading-hub manual-agent-binance manual-agent-okx manual-agent-gate - -# 本机:停三个 Flask(进程名以你 pm2 list 为准) -pm2 stop crypto_okx crypto_binance crypto_gate -# 或各目录 ecosystem 里的名字 -``` - -未用 PM2 时,结束对应 Python/Flask 进程后再备份. - -### 2. 确认数据库文件位置 - -各实例目录下查看 `.env` 中 `DB_PATH`(默认 `crypto.db`).若存在 `crypto.db-wal`,`crypto.db-shm`,**必须先停服务** 再备份. - ---- - -## 三,本地备份(推荐用自带脚本) - -每个实例目录执行(会备份 **库 + static/images**): - -```bash -cd crypto_monitor_okx -bash scripts/backup_data.sh -# 默认输出到 /root/backups/crypto_monitor_okx/YYYY-MM-DD/ -# 本机可改环境变量:BACKUP_ROOT=~/crypto_backups bash scripts/backup_data.sh -``` - -对 `crypto_monitor_binance`,`crypto_monitor_gate`,`crypto_monitor_gate` **各执行一次**. - -脚本产物示例: - -```text -~/crypto_backups/crypto_monitor_okx/2026-05-21/ - crypto.db - static_images.tar.gz - manifest.txt -``` - -### 手工打包(不用脚本时) - -在仓库根目录示例: - -```bash -BACKUP=~/crypto_migrate_$(date +%Y%m%d) -mkdir -p "$BACKUP" - -for dir in crypto_monitor_okx crypto_monitor_binance crypto_monitor_gate crypto_monitor_gate; do - tar -czf "$BACKUP/${dir}.tar.gz" \ - -C "$dir" crypto.db static/images 2>/dev/null || \ - tar -czf "$BACKUP/${dir}.tar.gz" -C "$dir" crypto.db -done - -cp manual_trading_hub/hub_settings.json "$BACKUP/" 2>/dev/null || true -cp manual_trading_hub/hub_ai_summaries.json "$BACKUP/" 2>/dev/null || true -cp manual_trading_hub/hub_ai_chat.json "$BACKUP/" 2>/dev/null || true -``` - ---- - -## 四,上传到云服务器 - -在**你电脑**上(把 `USER`,`云IP` 换成实际值): - -```bash -# 打包整个备份目录 -tar -czf crypto_migrate.tar.gz -C ~ crypto_backups # 或你的 BACKUP 路径 - -scp crypto_migrate.tar.gz USER@云IP:/tmp/ -scp manual_trading_hub/hub_settings.json USER@云IP:/tmp/ # 若单独备份 -``` - -大文件可用 **rsync**(支持断点续传): - -```bash -rsync -avz --progress ~/crypto_backups/ USER@云IP:/tmp/crypto_backups/ -``` - ---- - -## 五,云上恢复数据 - -假设代码已在 `/opt/crypto_monitor_user`(`git clone` 或 `rsync` 代码均可,**代码与数据分开**). - -```bash -ssh USER@云IP -cd /opt/crypto_monitor_user - -# 解压(若用 scp 单包) -tar -xzf /tmp/crypto_migrate.tar.gz -C /tmp - -# 按实例恢复(示例:OKX) -pm2 stop crypto_okx 2>/dev/null || true -cp /tmp/crypto_backups/crypto_monitor_okx/2026-05-21/crypto.db crypto_monitor_okx/crypto.db -tar -xzf /tmp/crypto_backups/crypto_monitor_okx/2026-05-21/static_images.tar.gz -C crypto_monitor_okx/ -# 若 tar 里是 static/images 目录结构,确认解压后路径为 crypto_monitor_okx/static/images - -# 对其余三所重复同样步骤 -``` - -恢复中控设置: - -```bash -cp /tmp/hub_settings.json manual_trading_hub/hub_settings.json -# 或解压备份里带的 hub_settings.json -``` - -**权限**(避免 Flask 写库失败): - -```bash -sudo chown -R 运行用户:运行用户 /opt/crypto_monitor_user/crypto_monitor_*/crypto.db -sudo chown -R 运行用户:运行用户 /opt/crypto_monitor_user/crypto_monitor_*/static/images -``` - ---- - -## 六,云上必须改的配置(比迁移本身更重要) - -数据文件原样拷过去不够,**.env 与 hub_settings 要按云环境改**. - -### 1. 各实例 `crypto_monitor_*/.env` - -从本机**抄写** API 密钥等,并调整: - -| 项 | 本地常见 | 云上建议 | -|----|----------|----------| -| `OKX_SOCKS_PROXY` 等 | `socks5h://127.0.0.1:1080` | **留空**(直连),除非云上仍访问不了交易所 | -| `APP_AUTH_DISABLED` | 可能为 true(本机) | **false** 或未设置 | -| `APP_USERNAME` / `APP_PASSWORD` | 可有 | 设统一强密码(直链登录) | -| `HUB_BRIDGE_TOKEN` | 有 | 与中控 **完全一致** | - -### 2. `manual_trading_hub/.env` - -见 [云服务器部署说明.md](./云服务器部署说明.md):`HUB_PASSWORD`,`HUB_BRIDGE_TOKEN`,`HUB_COOKIE_SECURE=true` 等. - -### 3. `hub_settings.json` 里的 URL - -**必须**改成浏览器能打开的地址: - -| 字段 | 云上 | -|------|------| -| `flask_url` | `https://okx.你的域名.com`(每实例不同子域) | -| `agent_url` | `http://127.0.0.1:15201`(保持本机,勿写公网 IP) | - -本机若是 `http://192.168.x.x:5004` 或 `http://127.0.0.1:5004`,上云后**一定要改**,否则「打开实例」会指错地址. - ---- - -## 七,云上启动与验收 - -```bash -# 依赖(各目录 venv + manual_trading_hub) -# 见 云服务器部署说明.md,部署文档.md - -cd /opt/crypto_monitor_user -# 先三实例 Flask,再 manual_trading_hub ecosystem -pm2 start ... -pm2 save -``` - -验收: - -- [ ] 各实例网页能登录,**交易记录 / 关键位 / 监控单** 与本地一致 -- [ ] 复盘图片能显示(`static/images` 路径正确) -- [ ] 中控监控卡片能读到持仓;`hub_settings` 账户 URL 正确 -- [ ] 本机已 **停止** 或不再用同一 API Key 同时跑两套(避免重复下单) - ---- - -## 八,迁移策略建议 - -### 方案 A:一次性切换(简单) - -1. 本地停 PM2 → 备份 → 上传 → 云上恢复 → 改配置 → 只跑云端. -2. 适合能接受 **短暂停机**(几十分钟). - -### 方案 B:先云后停本地(稳一点) - -1. 云上先部署代码,空库跑通; -2. 临近切换时再备份本地**最新**库覆盖云上; -3. 切换时刻停本地,启云上. -4. 减少「备份到上线」之间的数据空窗. - -### 注意 - -- **同一交易所 API Key 不要本地和云上同时自动交易**,以免重复挂单. -- 迁移后第一次在云上打开,建议先看监控单,持仓是否与预期一致,再放开自动逻辑. - ---- - -## 九,常见问题 - -**Q:只拷 `crypto.db` 不够吗?** -- 复盘,上传相关功能还依赖 `static/images`;建议库 + 图片一起迁. - -**Q:迁移后 OKX 监控单没了?** -- 查是否拷错目录(三所各一个库),或恢复后用了空库路径(`DB_PATH` 不一致). - -**Q:图片 404?** -- 检查 `static/images` 是否解压到实例目录下;数据库里路径若为相对路径,一般与目录结构一致即可. - -**Q:本地还用 SOCKS,云上要不要?** -- 云上通常 **不需要** SSH 隧道;见 [云服务器部署说明.md](./云服务器部署说明.md) 与此前说明:直连稳定后去掉 `*_SOCKS_PROXY`. - ---- - -## 十,相关脚本 - -各实例目录: - -```bash -bash scripts/backup_data.sh -``` - -环境变量:`BACKUP_ROOT`,`BACKUP_RETENTION_DAYS`,`BACKUP_INSTANCE`(见脚本内注释). diff --git a/manual_trading_hub/行情区说明.md b/manual_trading_hub/行情区说明.md deleted file mode 100644 index f945ae3..0000000 --- a/manual_trading_hub/行情区说明.md +++ /dev/null @@ -1,130 +0,0 @@ -# 行情区(K 线)说明 - -中控 **行情区** `/market` 提供多交易所 K 线查看:按需拉取,本地 SQLite 缓存,可选技术指标与持仓价格线.数据经各实例 Flask 的 `/api/hub/ohlcv`(底层 `hub_ohlcv_lib` + ccxt)获取. - -相关代码:`manual_trading_hub/static/chart.js`,`hub_kline_store.py`(仓库根目录),`hub.py` 的 `/api/chart/*`. - ---- - -## 1. 入口与导航 - -| 方式 | 说明 | -|------|------| -| 顶栏 **行情区** | 打开 `/market` | -| 监控区持仓 | 点击合约名(**打开行情区**)→ 跳转 `/market?exchange_key=...&symbol=...`,并带入入场/止损/止盈等标记(`sessionStorage`) | -| 全屏工具条 | K 线全屏时可在顶部切换交易所,币种,周期并 **加载** | - ---- - -## 2. 支持的周期 - -下拉框与后端 `CHART_TIMEFRAMES` 一致: - -| 周期 | 数字快捷键(分钟) | -|------|-------------------| -| 1m | `1`(稍停或 Enter 确认;连按 `1`→`5` 为 15m) | -| 5m | `5` | -| 15m | `15` | -| 1h | `60` | -| 2h | `120` | -| 4h | `240` | -| 12h | `720` | -| 1d | `1440` | -| 1w | `10080` | - -- 快捷键仅在行情页,且焦点不在输入框/下拉框时生效. -- **全屏**:按 **`F`** 切换;全屏时 **`Esc`** 退出. -- 无效或已移除的周期(如 URL 带 `6h`)会回退为默认 **5m**. - ---- - -## 3. 数据拉取与本地库 - -| 项 | 说明 | -|------|------| -| **策略** | 先读本地库,不足或过期则向对应实例拉取并写入库;Hub **后台轮询** 增量更新尾部 K 线 | -| **库文件** | 默认 `manual_trading_hub/data/hub_kline.db`(不纳入 Git) | -| **保留** | 默认 **15 天**(`HUB_KLINE_RETENTION_DAYS`),每次请求顺带清理更早数据 | -| **根数** | 日内周期约 **1000** 根;`1d` / `1w` 约 **500** 根 | -| **刷新** | Hub 约 **5 秒** 轮询:① 监控区**有持仓**的合约(默认周期 `5m`)② 行情页 **watch** 的交易所+币种+周期(页面打开时每 25s 续期).浏览器经 **SSE** 收 `chart_version` 后拉 `/api/chart/ohlcv`.**加载** 读库;**强制刷新** 全量重拉 | -| **分页** | OKX/Gate 等单次常限 ~300 根,中控会自动分页补全 | -| **12h** | 若交易所无原生 12h 或 K 线间隔异常,会从 **1h** 聚合生成 | - -环境变量(`manual_trading_hub/.env`): - -```bash -# HUB_KLINE_RETENTION_DAYS=15 -# HUB_KLINE_DB_PATH=/opt/crypto_monitor_user/manual_trading_hub/data/hub_kline.db -# HUB_CHART_POLL_INTERVAL=5 -# HUB_CHART_POSITION_TIMEFRAME=5m -# HUB_CHART_WATCH_TTL_SEC=45 -``` - ---- - -## 4. 图表功能 - -- **主图**:K 线 + 成交量(Lightweight Charts). -- **价格轴**:「自动」切换是否跟随最新价缩放. -- **技术指标**(可选勾选):EMA 21/55,MACD,RSI(含 30/70 参考线);副图自上而下为 MACD,RSI. -- **持仓标记**(从监控跳转时):展示入场,止损,止盈,张数,**浮盈亏**(约 5 秒随监控快照刷新),委托摘要;K 线上绘制对应价格线.趋势回调若止盈为程序监控,止盈栏显示「程序监控」且不与止损同价误显. -- **趋势保本移交**:移交到下单监控后,持仓卡止盈/止损与「交易所止盈止损」与实例 **下单监控** 计划价一致(不再清空为程序监控占位);交易所仅市价只减仓单时也会按价格推断展示. -- **拖动止损线**:鼠标靠近红色止损线(⟷)可上下拖动;松手确认后调用与监控区相同的 **挂止盈/止损** API(先撤全部条件单再挂新止损+止盈).须已有有效止盈价(交易所条件单或计划止盈);仅改止损,不改止盈时止盈价沿用当前上下文. -- **背离**:MACD/RSI 与价格简易背离标注(箭头 + 图例说明). - ---- - -## 5. HTTP API(中控) - -须登录(与监控区相同,`/api/ping` 等白名单除外). - -| 方法 | 路径 | 说明 | -|------|------|------| -| GET | `/api/chart/meta` | 已启用交易所列表,周期列表,各周期 limit,保留天数 | -| GET | `/api/chart/ohlcv` | 查询参数:`exchange_key`,`symbol`,`timeframe`,可选 `refresh=1` 强制刷新 | -| POST | `/api/chart/watch` | 行情页订阅(JSON:`exchange_key`,`symbol`,`timeframe`),45s 内需续期 | -| POST | `/api/chart/unwatch` | 离开行情页取消订阅 | -| GET | `/api/chart/stream` | SSE:`event: chart`,含 `chart_version` 与各 `series` 版本 | -| GET | `/api/chart/poll/meta` | 当前轮询状态与各 series 版本 | - -实例侧(中控转发): - -| 路径 | 说明 | -|------|------| -| GET | `/api/hub/ohlcv` | 各 `crypto_monitor_*` 经 `hub_bridge` 注册;参数 `symbol`,`timeframe`,`since_ms`,`limit` | - ---- - -## 6. 部署与升级注意 - -1. **hub** 与 **三实例 Flask** 均需 `git pull` 到含 `hub_ohlcv_lib.py`,`hub_kline_store.py` 的版本. -2. 重启:`pm2 restart manual-trading-hub` 及 `crypto_binance`,`crypto_okx`,`crypto_gate`,``(名称以你环境为准). -3. 浏览器 **强刷**(`chart.js` 带版本 query,避免旧前端缓存). -4. 周期或拉取逻辑升级后,对异常图表点一次 **强制刷新**,必要时可删 `data/hub_kline.db` 后重拉(会丢失本地缓存,不影响策略库). - -回滚标签说明见 [SNAPSHOT_ROLLBACK.md](./SNAPSHOT_ROLLBACK.md). - ---- - -## 7. 常见问题 - -| 现象 | 处理 | -|------|------| -| 只显示约 300 根 | `git pull` 实例与 hub,强制刷新;确认 `hub_ohlcv_lib` 已含分页逻辑 | -| 12h 错乱或过少 | 强制刷新;Gate 等无原生 12h 时依赖 1h 聚合,需实例 OHLCV 正常 | -| 周期下拉无某项 | 以当前 `CHART_TIMEFRAMES` 为准;已移除 3m/10m/20m/30m/6h/8h 等 | -| 快捷键无效 | 确认在行情页;全屏用 **F**;数字键勿在币种输入框内按 | -| 持仓线不显示 | 须从监控区点击合约进入;或清除标记后重新跳转 | - -更多中控共性问题见 [常见问题.md](./常见问题.md). - ---- - -## 8. 文档索引 - -| 文档 | 内容 | -|------|------| -| [使用说明.md](./使用说明.md) | 中控总览(含行情区摘要) | -| [行情区说明.md](./行情区说明.md) | 本文 | -| [部署文档.md](./部署文档.md) | PM2 / 反代 / 验收 | -| [.env.example](./.env.example) | `HUB_KLINE_*` 等变量 | diff --git a/manual_trading_hub/资金概况说明.md b/manual_trading_hub/资金概况说明.md deleted file mode 100644 index aad7a0f..0000000 --- a/manual_trading_hub/资金概况说明.md +++ /dev/null @@ -1,94 +0,0 @@ -# 资金概况 — 使用说明 - -中控顶栏 **资金概况**(`/funds`)汇总三所账户的 **资金账户 + 交易账户** 余额,不含浮盈亏;未监控账户不参与合计,但仍会在分户列表中灰显展示. - ---- - -## 1. 口径 - -| 项目 | 规则 | -|------|------| -| **单户总资金** | `资金账户 USDT + 交易账户 USDT` | -| **总资金** | 所有 **已启用且未被环境强制关闭** 的账户之和 | -| **未监控** | 设置页未勾选「启用」或 `HUB_DISABLED_IDS` 强制关闭 → **跳过合计** | -| **缺数据** | 资金户,交易户任一侧缺失 → 该户当日快照 **跳过**(不估,不补 0) | -| **交易日** | 北京时间 `TRADING_DAY_RESET_HOUR`(默认 **8:00**)切日,与三所统计一致 | -| **曲线粒度** | 每个交易日 **1 个点** | -| **统计起点** | 默认 **2026-06-09**(`HUB_FUND_HISTORY_START_DAY`);此前不记,不展示 | -| **历史保留** | 自起点起最多 **180** 个交易日(`HUB_FUND_HISTORY_DAYS`) | -| **最大回撤** | 基于 **总资金曲线**(分户同理),峰值到谷底的最大跌幅(U 与 %) | - -> 与实例统计页「最大回撤」不同:实例统计来自 **平仓盈亏累计**;资金概况来自 **账户余额曲线**. - ---- - -## 2. 页面说明 - -### 总览 - -- **总资金**:当前监控板最新一轮聚合的实时合计(资金户+交易户齐全才计入) -- **累计盈亏**:相对统计起点(`HUB_FUND_HISTORY_START_DAY`)首个快照的总资金变动(U / %);顶栏大字绿涨红跌,一眼可看盈亏。含出入金影响,口径同权益曲线,不含浮盈 -- **较昨日**:相对上一交易日快照点的变动(U) -- **最大回撤**:总资金历史曲线的峰值回撤(U / %) -- **总资金曲线**:近 180 交易日 - -### 分户卡片 - -每户展示:总资金,资金户,交易户,最大回撤,迷你曲线. - -- **已监控**:正常统计 -- **未监控**:显示「未参与合计」,无曲线 -- **余额未齐**:已监控但 API 未返回完整资金/交易户 - ---- - -## 3. 数据从哪来 - -``` -监控板每 5 秒聚合(board_store) - └→ 各实例 GET /api/hub/account - funding_usdt / trading_usdt - └→ 写入 hub_fund_history.json(按交易日去重更新当日) - -资金概况页 GET /api/hub/fund-overview - ├→ 实时:读 board 缓存 - └→ 曲线/回撤:读 hub_fund_history.json -``` - -- 存储文件:`manual_trading_hub/hub_fund_history.json`(不在 Git 中) -- 旧 AI 快照 `hub_ai_fund_history.json` 会在首次读取时 **自动合并** 到新文件 -- AI 教练生成上下文时也会写入同日快照(与监控板共用逻辑) - ---- - -## 4. 环境变量 - -| 变量 | 默认 | 说明 | -|------|------|------| -| `HUB_FUND_HISTORY_DAYS` | `180` | 资金快照保留交易日数(与起点取较晚边界) | -| `HUB_FUND_HISTORY_START_DAY` | `2026-06-09` | 曲线/回撤统计起始交易日 | -| `TRADING_DAY_RESET_HOUR` | `8` | 切日整点(北京),与三所 `.env` 建议一致 | -| `HUB_BOARD_POLL_INTERVAL` | `5` | 监控聚合间隔(秒),影响快照刷新频率 | - ---- - -## 5. API - -`GET /api/hub/fund-overview`(需中控登录,与监控区相同) - -返回字段概要: - -- `totals.total_usdt` — 当前总资金 -- `totals.day_delta_usdt` — 较昨日变动 -- `totals.period_delta_usdt` / `period_delta_pct` / `start_usdt` — 相对曲线起点累计盈亏 -- `totals.series[]` — `{ day, total_usdt }` 总曲线 -- `totals.drawdown` — `{ peak_usdt, max_drawdown_u, max_drawdown_pct }` -- `accounts[]` — 分户实时余额,曲线,回撤,日/累计变动,`monitored` 标记 - ---- - -## 6. 相关文档 - -- [使用说明.md](./使用说明.md) — 中控总览 -- [AI教练说明.md](./AI教练说明.md) — AI 上下文中的资金快照文本 -- [部署文档.md](./部署文档.md) — 重启 `manual-trading-hub` 后生效 diff --git a/scripts/obfuscate_release.py b/scripts/obfuscate_release.py new file mode 100644 index 0000000..37820ed --- /dev/null +++ b/scripts/obfuscate_release.py @@ -0,0 +1,206 @@ +"""核心包加密发布脚本。 + +优先 PyArmor;本机装不上时使用内置 marshal 加密后端(去可读源码)。 + +用法(仓库根): + python scripts/obfuscate_release.py --dry-run + python scripts/obfuscate_release.py --backend marshal --output dist/obfuscated_lib + python scripts/obfuscate_release.py --backend marshal --apply + +不加密: lib/license/、模板 templates/、非目标包。 +""" + +from __future__ import annotations + +import argparse +import base64 +import compileall +import hashlib +import marshal +import py_compile +import random +import shutil +import struct +import subprocess +import sys +import zlib +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] + +OBFUSCATE_PACKAGES = [ + "strategy", + "trade", + "key_monitor", + "options", + "hedge_plan", +] + +_LOADER_TEMPLATE = '''# obfuscated module — do not edit +import base64, marshal, zlib +_B = {blob!r} +_K = {key!r} +def _d(b, k): + raw = zlib.decompress(base64.b64decode(b)) + key = k.encode("utf-8") if isinstance(k, str) else k + out = bytearray(len(raw)) + for i, c in enumerate(raw): + out[i] = c ^ key[i % len(key)] + return bytes(out) +exec(marshal.loads(_d(_B, _K)), globals()) +''' + + +def _xor_compress(data: bytes, key: bytes) -> str: + out = bytearray(len(data)) + for i, c in enumerate(data): + out[i] = c ^ key[i % len(key)] + return base64.b64encode(zlib.compress(bytes(out), 9)).decode("ascii") + + +def _obfuscate_py_file(src: Path, dest: Path, key: str) -> None: + source = src.read_text(encoding="utf-8") + code = compile(source, str(src), "exec", dont_inherit=True) + blob = _xor_compress(marshal.dumps(code), key.encode("utf-8")) + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(_LOADER_TEMPLATE.format(blob=blob, key=key), encoding="utf-8") + + +def _copy_non_py(src_dir: Path, dest_dir: Path) -> None: + for path in src_dir.rglob("*"): + if path.is_dir(): + continue + if path.suffix == ".py": + continue + rel = path.relative_to(src_dir) + target = dest_dir / rel + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(path, target) + + +def marshal_obfuscate_package(src_pkg: Path, dest_pkg: Path, key: str) -> int: + if dest_pkg.exists(): + shutil.rmtree(dest_pkg) + dest_pkg.mkdir(parents=True, exist_ok=True) + count = 0 + for path in src_pkg.rglob("*.py"): + rel = path.relative_to(src_pkg) + # keep __init__.py structure but still obfuscate + _obfuscate_py_file(path, dest_pkg / rel, key) + count += 1 + _copy_non_py(src_pkg, dest_pkg) + return count + + +def _run(cmd: list[str], cwd: Path) -> None: + print("+", " ".join(cmd)) + subprocess.check_call(cmd, cwd=str(cwd)) + + +def try_pyarmor(packages: list[str], out: Path) -> bool: + try: + import pyarmor # noqa: F401 + except ImportError: + return False + if out.exists(): + shutil.rmtree(out) + out.mkdir(parents=True, exist_ok=True) + src_dirs = [str(REPO_ROOT / "lib" / n) for n in packages] + for cmd in ( + [sys.executable, "-m", "pyarmor.cli", "gen", "-O", str(out), *src_dirs], + ["pyarmor", "gen", "-O", str(out), *src_dirs], + ): + try: + _run(cmd, REPO_ROOT) + return True + except (subprocess.CalledProcessError, FileNotFoundError): + continue + return False + + +def main() -> int: + parser = argparse.ArgumentParser(description="Obfuscate core lib packages") + parser.add_argument("--output", default="dist/obfuscated_lib") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--apply", action="store_true", help="覆盖 lib/ 目标包(先备份到 dist/lib_backup_plain)") + parser.add_argument( + "--backend", + choices=("auto", "pyarmor", "marshal"), + default="auto", + help="auto=有 pyarmor 用 pyarmor,否则 marshal", + ) + args = parser.parse_args() + + lib = REPO_ROOT / "lib" + out = REPO_ROOT / args.output + packages = [n for n in OBFUSCATE_PACKAGES if (lib / n).is_dir()] + print("packages:", ", ".join(packages)) + if args.dry_run: + return 0 + + backend = args.backend + if backend == "auto": + backend = "pyarmor" + try: + import pyarmor # noqa: F401 + except ImportError: + backend = "marshal" + print("pyarmor 未安装,使用 marshal 后端") + + key = hashlib.sha256(b"crypto_monitor_user_release_v1").hexdigest()[:32] + + if backend == "pyarmor": + ok = try_pyarmor(packages, out) + if not ok: + print("pyarmor 失败,回退 marshal", file=sys.stderr) + backend = "marshal" + + if backend == "marshal": + if out.exists(): + shutil.rmtree(out) + out.mkdir(parents=True, exist_ok=True) + total = 0 + for name in packages: + n = marshal_obfuscate_package(lib / name, out / name, key) + print(f"marshal {name}: {n} py files") + total += n + print(f"encrypted output: {out} ({total} files)") + + # smoke import from output path + sys.path.insert(0, str(out.parent)) + # packages live in out/, need lib-like layout for import? + # For smoke: add out to path and import strategy if it has __init__ + smoke_root = out + sys.path.insert(0, str(smoke_root)) + try: + import importlib + + # packages are top-level under out/ + m = importlib.import_module("trade.trade_policy_lib") + print("smoke import trade.trade_policy_lib:", getattr(m, "__file__", m)) + except Exception as e: + print("smoke import note:", e) + + if args.apply: + for name in packages: + candidates = [out / name, out / "lib" / name] + src = next((c for c in candidates if c.exists()), None) + if src is None: + print(f"警告: 未找到 {name}") + continue + dest = lib / name + backup = REPO_ROOT / "dist" / "lib_backup_plain" / name + backup.parent.mkdir(parents=True, exist_ok=True) + if dest.exists(): + if backup.exists(): + shutil.rmtree(backup) + shutil.copytree(dest, backup) + shutil.rmtree(dest) + shutil.copytree(src, dest) + print(f"applied: lib/{name}") + print("完成。请验证启动后再 push。") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_license_lib.py b/tests/test_license_lib.py new file mode 100644 index 0000000..54a364c --- /dev/null +++ b/tests/test_license_lib.py @@ -0,0 +1,88 @@ +"""许可客户端基础测试(不依赖真实激活码)。""" + +from __future__ import annotations + +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from lib.license import license_lib as lic + + +class LicenseLibTests(unittest.TestCase): + def setUp(self): + self._td = tempfile.TemporaryDirectory() + self.state = Path(self._td.name) / "license_state.json" + self.env = { + "LICENSE_API_URL": "https://sq.bz121.com", + "LICENSE_CLIENT_KEY": "test-key", + "LICENSE_STATE_PATH": str(self.state), + "LICENSE_OFFLINE_GRACE_HOURS": "72", + } + self._patcher = mock.patch.dict(os.environ, self.env, clear=False) + self._patcher.start() + lic._env_loaded = True + lic._cached_device_id = None + + def tearDown(self): + self._patcher.stop() + self._td.cleanup() + + def test_device_id_length(self): + did = lic.get_device_id() + self.assertGreaterEqual(len(did), 16) + self.assertLessEqual(len(did), 64) + self.assertEqual(did, lic.get_device_id()) + + def test_not_activated(self): + st = lic.get_license_status() + self.assertFalse(st["valid"]) + self.assertEqual(st["reason"], "not_activated") + + def test_local_valid_within_grace(self): + now = lic._now_ts() + self.state.write_text( + json.dumps( + { + "subscription_id": "sub_test", + "expires_at": "2099-01-01T00:00:00+08:00", + "plan": "monthly", + "last_ok_at": now, + "last_validate_at": now, + } + ), + encoding="utf-8", + ) + st = lic.get_license_status(skip_remote=True) + self.assertTrue(st["valid"]) + + def test_redeem_posts_api(self): + def fake_http(method, path, body): + self.assertEqual(path, "/v1/redeem") + self.assertIn("device_id", body) + self.assertEqual(body["code"], "ABCD1234EFGH") + return { + "ok": True, + "subscription_id": "sub1", + "plan": "monthly", + "expires_at": "2099-06-01T00:00:00+08:00", + "message": "激活成功", + } + + with mock.patch.object(lic, "_http_json", side_effect=fake_http): + r = lic.redeem_code("abcd1234efgh") + self.assertTrue(r["ok"]) + data = json.loads(self.state.read_text(encoding="utf-8")) + self.assertEqual(data["subscription_id"], "sub1") + + +if __name__ == "__main__": + unittest.main() diff --git a/关键位止盈止损与移动保本更新说明.md b/关键位止盈止损与移动保本更新说明.md deleted file mode 100644 index 0f8a77e..0000000 --- a/关键位止盈止损与移动保本更新说明.md +++ /dev/null @@ -1,164 +0,0 @@ -# 关键位止盈止损方案 & 移动保本开关 — 更新说明 - -**版本日期:** 2026-05-21 -**影响实例:** `crypto_monitor_binance`(币安),`crypto_monitor_gate`(Gate),`crypto_monitor_okx`(OKX) -**共用库:** 仓库根目录 `key_sl_tp_lib.py`(计算逻辑),`fib_key_monitor_lib.py`(斐波 SL/TP 不变) - ---- - -## 1. 变更摘要 - -| 项 | 变更前 | 变更后 | -|----|--------|--------| -| 箱体/收敛 SL/TP | 仅一种(标准突破) | 添加时可三选一 | -| 移动保本(关键位) | 自动单写死开启 | 添加时可勾选,**默认关闭** | -| 斐波 SL/TP | H/L 限价方案 | **不变**;仅增加移动保本开关 | -| 历史 `key_monitors` | 无新字段 | 缺字段视为 **标准方案 + 保本关** | - ---- - -## 2. 数据库(`key_monitors` 新增列) - -启动时自动 `ALTER TABLE`(已有库兼容): - -| 字段 | 类型 | 默认 | 说明 | -|------|------|------|------| -| `sl_tp_mode` | TEXT | `standard` | `standard` / `box_1p5` / `trend_manual` | -| `manual_take_profit` | REAL | NULL | 仅 `trend_manual` 使用 | -| `breakeven_enabled` | INTEGER | `0` | 0=关,1=开 | - -旧记录无上述列时,读取逻辑按 **`standard` + 保本关** 处理. - ---- - -## 3. 三种止盈止损方案(仅箱体突破 / 收敛突破) - -计划价 **E** = 确认 K(倒数第 1 根已闭合 5m)收盘价;**H** = \|上沿 − 下沿\|. -自动开仓(币安/Gate)前仍须:**计划 RR(按 E)> `KEY_AUTO_MIN_PLANNED_RR`**(默认 1.5). - -### 3.1 标准突破 `standard`(原逻辑) - -| 方向 | 止损 SL | 止盈 TP | -|------|---------|---------| -| 多 | 突破 K 最低价 × (1 − `KEY_STOP_OUTSIDE_BREAKOUT_PCT`/100) | E + 1×H | -| 空 | 突破 K 最高价 × (1 + 外侧%) | E − 1×H | - -默认外侧:**0.5%**(`KEY_STOP_OUTSIDE_BREAKOUT_PCT`). - -### 3.2 箱体 1R / 止盈 1.5H `box_1p5` - -以 **E 为当前价**,风险距离 = 1×H,止盈距离 = 1.5×H,**计划 RR 固定约 1.5:1**. - -| 方向 | 止损 SL | 止盈 TP | -|------|---------|---------| -| 多 | E − H | E + 1.5×H | -| 空 | E + H | E − 1.5×H | - -### 3.3 趋势单 + 自填止盈 `trend_manual` - -| 方向 | 止损 SL | 止盈 TP | -|------|---------|---------| -| 多 | 突破 K 最低价 × (1 − `KEY_TREND_STOP_OUTSIDE_PCT`/100) | 添加时录入的 `manual_take_profit` | -| 空 | 突破 K 最高价 × (1 + 外侧%) | 同上 | - -- 环境变量 **`KEY_TREND_STOP_OUTSIDE_PCT`**,默认 **1**(即 1%). -- 添加时校验:做多止盈 > 上沿;做空止盈 < 下沿. - ---- - -## 4. 斐波回调 0.618 / 0.786 - -- **SL/TP**:仍为 `calc_fib_plan`(多:SL=L,TP=H;空:SL=H,TP=L),**无**三方案下拉. -- **移动保本**:添加时可勾选;成交写入 `order_monitors` 时带入该勾选状态(默认关). - ---- - -## 5. 移动保本 - -| 场景 | 行为 | -|------|------| -| 关键位添加 | 复选框「移动保本」,**默认不勾选** | -| 箱体/收敛自动开仓成功 | `order_monitors.breakeven_enabled` = 添加时的选择 | -| 斐波限价成交后 | 同上 | -| 人工「实盘下单」 | **不变**:仍为表单勾选,默认仍可按原页面逻辑 | - -触发参数仍用全局 `.env`:`BREAKEVEN_RR_TRIGGER`,`BREAKEVEN_STEP_R`,`BREAKEVEN_OFFSET_PCT`. - ---- - -## 6. 前端(关键位添加表单) - -在「上沿 / 下沿」后增加: - -1. **止盈止损方案**(仅类型为箱体突破,收敛突破时显示) -2. **趋势单止盈价**(仅选「趋势单·自填止盈」时显示且必填) -3. **移动保本**(箱体/收敛/斐波显示;默认不勾) - -活跃列表卡片展示:**方案**,**保本:开/关**. - ---- - -## 7. 环境变量 - -```env -# 标准方案:突破 K 极值外侧 % -KEY_STOP_OUTSIDE_BREAKOUT_PCT=0.5 - -# 趋势单方案:突破 K 极值外侧 % -KEY_TREND_STOP_OUTSIDE_PCT=1 -``` - -已写入各实例 `.env.example`(Binance / Gate / OKX). - ---- - -## 8. 交易所差异 - -| 实例 | 箱体/收敛触发后 | -|------|----------------| -| **Binance / Gate** | 门控通过 → 按方案算 SL/TP → 市价开仓 → 挂交易所 TP/SL → 写入下单监控 | -| **OKX** | 门控通过 → **自动市价开仓**(与 Gate/Binance 相同;须 `LIVE_TRADING_ENABLED=true`) | - -OKX 用户按推送中的计划价自行下单;斐波仍为限价 + 成交后挂 TP/SL(与原先一致). - ---- - -## 9. 涉及文件清单 - -| 路径 | 说明 | -|------|------| -| `key_sl_tp_lib.py` | **新建**,三方案计算与文案 | -| `crypto_monitor_binance/app.py` | 门控触发,开仓,斐波,add_key | -| `crypto_monitor_binance/templates/index.html` | 表单 + JS + 列表展示 | -| `crypto_monitor_binance/.env.example` | `KEY_TREND_STOP_OUTSIDE_PCT` | -| `crypto_monitor_gate/app.py` | 同 Binance | -| `crypto_monitor_gate/templates/index.html` | 同 Binance | -| `crypto_monitor_gate/.env.example` | 同上 | -| `crypto_monitor_okx/app.py` | add_key,提醒文案,斐波保本 | -| `crypto_monitor_okx/templates/index.html` | 表单 + JS | -| `crypto_monitor_okx/.env.example` | 注释项 | - ---- - -## 10. 部署与验证建议 - -1. `git pull` 后重启三个实例的 Flask 进程(会自动迁移 `key_monitors` 列). -2. 在 `.env` 中按需设置 `KEY_TREND_STOP_OUTSIDE_PCT`(不配则用默认 1). -3. **验证 Binance/Gate** - - 添加箱体突破,选「箱体1R·止盈1.5H」,不勾保本 → 触发后微信应显示方案名,保本关,SL/TP 符合 E±H / E±1.5H. - - 添加趋势单,填止盈,勾保本 → 成交后持仓卡片「移动保本:开」. -4. **验证 OKX**:门控通过且 RR 达标时应自动市价开仓;失败时微信说明 `exchange_failed` / `rr_insufficient`. -5. 旧关键位条目:列表应显示「方案:标准突破」「保本:关」(除非库中已有新字段值). - ---- - -## 11. 代码入口(便于二次开发) - -| 功能 | 符号 | -|------|------| -| 计划 SL/TP | `plan_key_sl_tp()` in `key_sl_tp_lib.py` | -| 按监控行计算 | `_key_plan_sl_tp_for_row()` in各 `app.py` | -| 添加关键位 | `add_key()` | -| 箱体/收敛轮询 | `check_key_monitors()`(三所共用自动开仓逻辑) | -| 斐波添加 | `_add_fib_key_monitor(..., breakeven_enabled=)` | -| 自动开仓写监控 | `_market_open_for_key_monitor(..., breakeven_enabled=)` | diff --git a/策略交易说明.md b/策略交易说明.md deleted file mode 100644 index 988e720..0000000 --- a/策略交易说明.md +++ /dev/null @@ -1,160 +0,0 @@ -# 策略交易说明 - -本文档说明仓库根目录 **共用策略逻辑** 与三个 `crypto_monitor_*` 实例中的 **策略交易** 入口(顶栏「策略交易」,页内子 Tab:趋势回调 / 顺势加仓). - ---- - -## 一,架构(精简共用) - -``` -strategy_trend_lib.py # 趋势回调:网格价,补仓拆分,边界校验(纯计算) -strategy_roll_lib.py # 顺势加仓:总风险反推,斐波限价,最多 3 腿(纯计算) -strategy_db.py # roll_groups / roll_legs 表结构 -strategy_config.py # 各所 app → 统一回调配置(交易所 API) -strategy_register.py # Flask POST:/strategy/roll/preview,/strategy/roll/execute -strategy_ui.py # 主站 index 页数据(滚仓组,持仓列表等) -strategy_exchange_*.py # 适配器说明(实际下单仍走各所 app 的 ccxt) -strategy_templates/ # strategy_trading_page.html(双栏),trend/roll 面板,records 页 -strategy_snapshot_lib.py # 结束快照表 strategy_trade_snapshots(最近 100 条) -strategy_records_register.py # /strategy/records 路由与列表数据 -``` - -| 层级 | 职责 | -|------|------| -| **lib** | 不算 ccxt,不写库 | -| **config** | 把 `place_exchange_order`,`replace_active_monitor_tpsl_on_exchange` 等接到统一 cfg | -| **各所 app** | `.env`,DB,`init_db`,PM2,微信,监控轮询 | - -部署时各实例 `PYTHONPATH` 需包含仓库根目录(`ecosystem.config.cjs` 中 `PYTHONPATH=..`). - ---- - -## 二,导航与页面 - -顶栏:**策略交易** → `/strategy`(趋势回调 | 顺势加仓 左右并列);**策略交易记录** → `/strategy/records`(已结束快照).旧链接 `/strategy/trend`,`/strategy/roll` 会跳转到 `/strategy`. - -| 区域 | 说明 | -|------|------| -| 左栏 · 趋势回调 | **三所均可**(预览,执行,自动补仓,程序止盈);运行中计划卡含 **补仓计划明细** 表 | -| 右栏 · 顺势加仓 | 须已有同向持仓;滚仓组/历史表在右栏内滚动 | -| **策略交易记录** | 趋势回调 / 顺势加仓 **分两栏**;每条约一行摘要,点击展开详情;库内保留最近 **100** 条 | -| `/trade` | 实盘下单 | 首仓,以损定仓,移动保本(不变) | - -各所 `app.py` 注册 `@app.route("/strategy/trend|roll")` → `render_main_page(...)`;`install_strategy_trading` 仅注册滚仓 POST API. - ---- - -## 三,趋势回调 - -- **位置**:各所顶栏 **策略交易 → 趋势回调**(共用 `strategy_trend_register.py` + 各所交易所 API). -- **行为**:与《[docs/trend-pullback-strategy.md](./docs/trend-pullback-strategy.md)》一致——预览 → 确认执行 → 首仓 50% + 交易所止损 + 多档 **自动** 市价补仓 + 程序监控止盈. -- **共用代码**:`parse_and_compute_trend_pullback_plan` 中网格/拆档已改为调用 `strategy_trend_lib`. -- **互斥**:与「机器人下单监控」持仓上限,运行中趋势计划互斥(逻辑未改). - -各所使用自己的 API 密钥与 `crypto.db`,互不影响. - ---- - -## 四,顺势加仓(滚仓,仅人工) - -> **详细说明**(计仓公式,四种方式,程序监控,生命周期):仓库 [`顺势加仓滚仓说明.md`](./顺势加仓滚仓说明.md);各实例策略页 **[`/strategy/roll/docs`](/strategy/roll/docs)** 可在线阅读. - -### 4.1 原则 - -- **禁止自动加仓**;仅页面按钮「执行滚仓」或挂限价单(无价格穿越自动下单). -- **全币种**(与各所合约列表一致). -- **止盈**:全程使用 **首仓** `order_monitors.take_profit`,滚仓不改止盈. -- **止损**:每次人工填写 **新统一止损**;成交后调用各所 **先撤后挂** TP/SL(止盈仍为首仓). -- **总风险%**:按「合并持仓 + 新止损」反推本次加仓张数,使触及新止损时亏损约 **账户基数 × 风险%**(默认 2%,可在表单修改). -- **做多**最多滚仓 **3** 次(首仓不计入,仅计 `roll_legs` 已成交次数);做空默认同样 3 次(见 `strategy_roll_lib.ROLL_MAX_LEGS_SHORT`). - -### 4.2 斐波限价 - -- 填写 **上沿 H,下沿 L**(H > L),仅用于计算限价加仓价(与 `fib_key_monitor_lib.calc_fib_plan` 的 **entry** 一致). -- **做多**:下沿 = 结构止损侧;**做空**:上沿 = 结构止损侧. -- 可选 **0.618** 或 **0.786**;与关键位自动单的 TP(H/L 对侧)**不同**,滚仓 TP 锁定首仓. - -### 4.3 前置条件 - -1. 在 **实盘下单** 已有同 symbol,同方向 **active** `order_monitors`. -2. 交易所有同向持仓(读 `get_live_position_contracts`). -3. 无 **active** `trend_pullback_plans`(与趋势回调互斥). - -### 4.4 数据表(各所 `crypto.db`) - -- `roll_groups`:绑定 `order_monitor_id`,首仓 TP/SL,当前 SL,已滚仓次数. -- `roll_legs`:每腿方式(市价 / 斐波0.618 / 斐波0.786),张数,新 SL,状态(`filled` / `pending`). - -`init_db()` 时自动 `CREATE TABLE IF NOT EXISTS`(`strategy_db.init_strategy_tables`). - -### 4.5 操作步骤 - -1. 打开顶栏 **策略交易** `/strategy`,在 **右栏·顺势加仓** 操作. -2. 选择持仓币种,方向,加仓方式,填写 H/L(斐波时),**新统一止损**,总风险%. -3. 点击 **执行滚仓**(市价立即加仓并更新止损;限价则挂委托,成交后需再处理止损——当前版本限价 pending 后提示手动同步). -4. 查看页底 **滚仓腿历史**. - -可选:对表单字段 POST `/strategy/roll/preview`(JSON)查看 `strategy_roll_lib.preview_roll` 结果. - ---- - -## 五,策略交易记录(三所统一) - -- **入口**:顶栏 **策略交易记录** → `/strategy/records`(`strategy_records_register.register_strategy_records`). -- **写入时机**:趋势计划结束(止盈 / 止损 / 手动结束),**保本移交**,顺势加仓组结案时,写入表 **`strategy_trade_snapshots`**(`strategy_snapshot_lib`). -- **与交易记录区别**:策略记录写 **`strategy_trade_snapshots`**;顶栏 **交易记录与复盘** 写 **`trade_records`**.中控手动结束计划时 **两者都应写入**(详见 [docs/trend-hub-close-and-trade-records.md](./docs/trend-hub-close-and-trade-records.md)). -- **保留条数**:每次写入后自动修剪,仅保留按 **`closed_at` 倒序** 的最近 **100** 条. -- **页面布局**: - - **左栏卡片**:趋势回调记录;**右栏卡片**:顺势加仓记录. - - 每条默认 **一行简略**(品种,方向,结果,盈亏,补仓进度,结束时间);**点击行**展开均价/止损/止盈/补仓档位表或滚仓腿表. - - **筛选**:币种,时间排序(最新/最早),芯片 **盈利 / 亏损 / 未补仓 / 补仓**(前端过滤,数据来自服务端 enrich 字段 `filter_pnl`,`dca_tag`,`dca_done`). -- **共用模板**:`strategy_templates/strategy_records_page.html`(三所 `index.html` include). - ---- - -## 六,中控全屏 · 趋势回调展示 - -各所 Flask 经 `hub_bridge` + `enrich_trend_plan_for_hub` 向中控提供 active 计划(含 `dca_levels`).在 **manual_trading_hub** 全屏 **趋势回调** 区,单所通常仅 **一仓**,计划卡为 **横向两列**(与实例字段一致,操作在实例完成): - -| 区域 | 内容 | -|------|------| -| 顶栏 | `#ID 品种`,方向徽章,**结束计划**(SSO 打开实例并确认) | -| **左列** | 来源/风险/补仓边界/已补仓;均价,止损,止盈,盈亏比,标记价,浮盈亏(% 按 **计划保证金**) | -| **右列** | **补仓计划明细** 表(首仓 + 各档;未成交显示 **待补仓**) | -| **底栏** | 保本移交(偏移%,跳转实例策略页),**快照可用 / 计划保证金 / 杠杆** | - -静态资源版本见 `manual_trading_hub/static/index.html` 中 `app.js` / `app.css` 的 `?v=` 参数;改 UI 后请 **强刷** 中控页. - ---- - -## 七,升级与重启 - -```bash -cd /opt/crypto_monitor_user -git pull -pm2 restart crypto-monitor-binance crypto-monitor-okx crypto-monitor-gate manual-trading-hub -pm2 save -``` - -部署约定:**Ubuntu + root + /opt/crypto_monitor_user + PM2** → [docs/ubuntu-server.md](./docs/ubuntu-server.md). - ---- - -## 八,相关文档 - -| 文档 | 内容 | -|------|------| -| [docs/trend-pullback-strategy.md](./docs/trend-pullback-strategy.md) | 趋势回调细则(三所共用逻辑) | -| [AI复盘与模型配置说明.md](./AI复盘与模型配置说明.md) | 复盘页 AI(与策略无关) | -| [manual_trading_hub/使用说明.md](./manual_trading_hub/使用说明.md) | 中控监控,全屏趋势卡两列布局 | -| [docs/trend-hub-close-and-trade-records.md](./docs/trend-hub-close-and-trade-records.md) | 中控平仓,交易记录写入,补仓展示统一,漏记补录 | -| [docs/hub-symbol-archive-kline.md](./docs/hub-symbol-archive-kline.md) | 币种档案,永久 5m K 线,建档与 4h 增量同步 | -| [docs/ubuntu-server.md](./docs/ubuntu-server.md) | Ubuntu / root /opt / PM2 部署 | -| [fib_key_monitor_lib.py](./fib_key_monitor_lib.py) | 斐波公式共用 | - ---- - -## 九,后续可增强(未实现) - -- 滚仓页内嵌预览按钮,限价成交后一键同步止损. -- 策略交易记录按 UTC 时间窗与顶栏「列表筛选」联动. diff --git a/顺势加仓滚仓说明.md b/顺势加仓滚仓说明.md deleted file mode 100644 index ce49365..0000000 --- a/顺势加仓滚仓说明.md +++ /dev/null @@ -1,174 +0,0 @@ -# 顺势加仓(滚仓)详细说明 - -本文档描述 **顺势加仓 / 滚仓** 的完整业务逻辑,计仓公式,四种加仓方式,程序监控与生命周期规则.实现代码见 `strategy_roll_lib.py`,`strategy_roll_monitor_lib.py`,`strategy_register.py`. - ---- - -## 1. 适用范围与前置条件 - -| 项目 | 规则 | -|------|------| -| 计仓模式 | **仅「以损定仓」**(`POSITION_SIZING_MODE=risk`);全仓杠杆模式禁止滚仓 | -| 持仓 | 须先在「实盘下单」存在 **active** 的 `order_monitors`,且交易所有同向持仓 | -| 趋势互斥 | 存在 **active** 趋势回调计划时不可滚仓 | -| 腿数上限 | 做多 / 做空各最多 **3 次**滚仓(仅计 **已成交** 的 `roll_legs`) | -| 同时监控 | **同一滚仓组** 最多 **1 条 pending** 腿;成交或删除/失效后再提交下一腿 | -| 止盈 | 全程使用 **首仓** `order_monitors.take_profit`,滚仓不改止盈 | -| 止损 | 每次提交填写 **新统一止损价 S**;成交后交易所 TP/SL 同步(止盈仍为首仓) | - ---- - -## 2. 风险预算(不可手改) - -- 读取所选监控单:`order_monitors.risk_percent` -- 风险预算:**B = 当前交易基数 × risk%**(`get_trading_capital_usdt()` × 监控 risk%) -- 页面规则区展示当前 risk%,表单 **不提供** 风险% 输入框 - -**方案 C(定稿)**:加仓后若价格打到 **新止损 S**,合并持仓的总亏损 **≤ B**(约等于 1 个风险单位).浮盈通过 **触发时刻的 mark 价,当时持仓均价与张数** 进入公式,不在提交时固定张数. - ---- - -## 3. 计仓公式 - -变量: - -- `Q1, E1`:触发时现有持仓张数,均价 -- `E2`:加仓成交价(市价腿 ≈ 当时 mark;程序监控腿在 **穿越触发时** 用当时 mark 重算) -- `S`:提交时填写的统一止损价 -- `B`:风险预算(U) -- `cs`:合约 `contractSize`(U 本位线性永续) - -**做多**(须 `S < E2`): - -```text -(Q1 + Q2) × (avg − S) × cs = B -avg = (Q1·E1 + Q2·E2) / (Q1 + Q2) - -=> Q2 = (B/cs − Q1·(E1 − S)) / (E2 − S) -``` - -**做空**(须 `S > E2`): - -```text -=> Q2 = (B/cs − Q1·(S − E1)) / (S − E2) -``` - -若 `Q2 ≤ 0`:不加仓 / 监控腿 **失效**,提示「已满足风险上限或无法再加」. - -预览与市价执行前用当前 mark 估算;**斐波 / 突破** 在 **触发瞬间** 按当时持仓与 mark **重新计算** 张数后再市价下单. - ---- - -## 4. 四种加仓方式 - -### 4.1 市价加仓 - -| 输入 | 仅 **新止损价 S** | -| 执行 | 预览 → **10 秒确认** → 立即市价成交 → 更新止损 | -| 显示 | `市价加仓` | - -### 4.2 斐波 0.618 / 0.786 - -| 输入 | 上沿 H,下沿 L,新止损 S | -| 限价 | 由 H/L 按斐波算 **加仓价 P**(不打交易所限价单) | -| 触发 | 程序监控 **mark**:
• **多**:mark **向下穿越** P → 市价加
• **空**:mark **向上穿越** P → 市价加 | -| 失效 | **止盈侧**:多 mark≥H;空 mark≤L | -| 显示 | `斐波0.618` / `斐波0.786` | - -### 4.3 突破加仓 - -| 输入 | **突破价 B**,新止损 S | -| 触发 | 程序监控 **mark**:
• **多**:mark **向上穿越** B → 市价加
• **空**:mark **向下穿越** B → 市价加 | -| 失效 | **止损侧**:多 mark≤S;空 mark≥S(未突破先向止损侧) | -| 显示 | `突破加仓` | - -几何校验(做多示例): - -- 斐波:S < P < 当前价(回调加仓) -- 突破:S < B < 当前价(向上突破再加) - ---- - -## 5. 程序监控技术要点 - -- **监控价**:统一使用 **标记价 mark**(`get_mark_price` 或 `get_price`) -- **穿越判定**:比较 `last_mark_price`(上一 tick 存库)与当前 mark,避免重复触发 - - 例:做多斐波:`prev > P` 且 `mark ≤ P` -- **轮询**:各所后台任务调用 `check_roll_monitors(cfg)` -- **成交后**:`replace_tpsl` 更新交易所止损;`order_monitors.stop_loss` 同步为 S - ---- - -## 6. 生命周期与权限 - -```text -提交 pending → [监控中] ──穿越触发──→ filled → 可提交下一腿 - │ - ├── 用户删除 → cancelled(不可修改,仅删除) - ├── 失效规则 → invalidated - └── 手动平仓 / 监控结案 → roll_group closed,pending 清除 -``` - -| 规则 | 说明 | -|------|------| -| 提交后不可改 | pending 腿参数不可编辑,只能 **删除** | -| 手动平仓 | 实例页删单/平仓,中控持仓平仓 → 调用 `roll_sync_after_external_close` | -| 历史保留 | **filled** 腿写入库与策略复盘快照;关组后 pending 清除,已成交腿仍可在「策略交易记录」中查看 | - -API: - -- `POST /strategy/roll/preview` — JSON 预览 -- `POST /strategy/roll/execute` — 提交市价或监控计划 -- `POST /strategy/roll/cancel/` — 删除 pending 腿 -- `POST /api/hub/roll/sync-flat` — 中控平仓后同步(内部) - ---- - -## 7. 数据表 - -**roll_groups**(绑定 `order_monitor_id`) - -- 首仓 TP/SL,`current_stop_loss`,`leg_count`(**已成交**次数),`risk_percent` 快照 - -**roll_legs** - -| 字段 | 说明 | -|------|------| -| add_mode | 市价加仓 / 斐波0.618 / 斐波0.786 / 突破加仓 | -| limit_price | 斐波限价 P | -| breakthrough_price | 突破价 B | -| new_stop_loss | 统一止损 S | -| last_mark_price | 上一 tick mark(穿越检测) | -| status | pending / filled / cancelled / invalidated | - ---- - -## 8. 操作流程(建议) - -1. 在「实盘下单」已有同向持仓与监控单 -2. 打开 **策略交易 → 顺势加仓**,选择币种(方向自动锁定) -3. 选择加仓方式,填写对应价格字段 → **预览** -4. 市价:等待 10 秒 → **执行滚仓**;斐波/突破:确认后提交监控 -5. 监控中可在「最近滚仓腿」**删除**;成交后再提交下一腿(最多 3 次) - ---- - -## 9. 相关文件 - -| 文件 | 职责 | -|------|------| -| `strategy_roll_lib.py` | 计仓,校验,穿越/失效纯函数 | -| `strategy_roll_monitor_lib.py` | 定时监控,触价成交,外部平仓同步 | -| `strategy_register.py` | 预览/执行/删除路由 | -| `static/strategy_roll.js` | 方向锁定,字段显隐,预览与 10 秒确认 | -| `strategy_templates/strategy_roll_panel.html` | 右栏 UI | - ---- - -## 10. 与旧版差异摘要 - -- 风险% 从监控单读取,不再手填 -- 止损为 **绝对价格**,不再使用「止损偏移%」 -- 斐波/突破改为 **程序盯 mark + 触价市价**,不再挂交易所限价单 -- 新增 **突破加仓** -- pending **不可改,可删**;手动平仓自动结束滚仓监控