Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 40be3a5ab7 | |||
| 4ccfb838f6 | |||
| 58e9c8f85e | |||
| eb0eddbc9d | |||
| c5f40cba2b | |||
| a7216428ab | |||
| 77f66bf200 | |||
| 488b931959 | |||
| b89cba3b6e | |||
| e7f8e9201e | |||
| 301a464f29 | |||
| a4be294c06 |
@@ -158,6 +158,8 @@ RISK_CONTROL_ENABLED=true
|
||||
RISK_COOLING_HOURS_MANUAL=4
|
||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||
# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用
|
||||
RISK_DAILY_LOSS_LIMIT=2
|
||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||
|
||||
# 资金与仓位刷新周期(秒)
|
||||
|
||||
@@ -2751,6 +2751,17 @@ def insert_trade_record(
|
||||
opened_at_ms=open_ts_ms,
|
||||
closed_at_ms=close_ts_ms,
|
||||
)
|
||||
try:
|
||||
from lib.trade.account_risk_lib import on_closed_trade_pnl
|
||||
|
||||
close_dt = parse_dt_for_trading_day(close_ts)
|
||||
on_closed_trade_pnl(
|
||||
conn,
|
||||
pnl_amount=pnl_amount,
|
||||
trading_day=get_trading_day(close_dt),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return tid
|
||||
|
||||
|
||||
|
||||
@@ -160,6 +160,8 @@ RISK_CONTROL_ENABLED=true
|
||||
RISK_COOLING_HOURS_MANUAL=4
|
||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||
# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用
|
||||
RISK_DAILY_LOSS_LIMIT=2
|
||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||
|
||||
# 资金与仓位刷新周期(秒)
|
||||
|
||||
@@ -2445,6 +2445,17 @@ def insert_trade_record(
|
||||
sync_trade_records_from_exchange(conn, force=False)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from lib.trade.account_risk_lib import on_closed_trade_pnl
|
||||
|
||||
close_dt = parse_dt_for_trading_day(close_ts)
|
||||
on_closed_trade_pnl(
|
||||
conn,
|
||||
pnl_amount=pnl_amount,
|
||||
trading_day=get_trading_day(close_dt),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return tid
|
||||
|
||||
|
||||
|
||||
@@ -219,6 +219,8 @@ RISK_CONTROL_ENABLED=true
|
||||
RISK_COOLING_HOURS_MANUAL=4
|
||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||
# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用
|
||||
RISK_DAILY_LOSS_LIMIT=2
|
||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||
|
||||
# 资金与仓位刷新周期(秒)
|
||||
|
||||
@@ -2364,6 +2364,17 @@ def insert_trade_record(
|
||||
sync_trade_records_from_exchange(conn, force=False)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from lib.trade.account_risk_lib import on_closed_trade_pnl
|
||||
|
||||
close_dt = parse_dt_for_trading_day(close_ts)
|
||||
on_closed_trade_pnl(
|
||||
conn,
|
||||
pnl_amount=pnl_amount,
|
||||
trading_day=get_trading_day(close_dt),
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return tid
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
# 标的时段振幅统计 — 开发方案
|
||||
|
||||
> 状态:**方案冻结**(按本文实现;改需求先改本文).
|
||||
> 范围:**中控**新增只读统计工具;不改开平仓、不接 AI 教练(首版).
|
||||
> 数据源:**仅 OKX**.
|
||||
> 相关:[交易执行手册-期权与Gate.md](./交易执行手册-期权与Gate.md)(16:00 会话窗纪律) · [振幅统计说明.md](./振幅统计说明.md)(实现后用户说明)
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
在中控提供 **自定义时段、固定 16:00 收窗** 的历史振幅档案:
|
||||
|
||||
- **标的下拉**:`ETH` / `BTC`(默认 ETH)
|
||||
- 按整点起点 + **终点固定北京时间 16:00** 切出每日统计窗
|
||||
- 回溯周期可选(1 月 / 2 月 / 3 月 / 半年 / 1 年 / 自定义)
|
||||
- 日表明细分页展示;下方为汇总统计
|
||||
- 每次有效计算可写入 **历史**;支持 **下载**(明细 + 统计摘要)
|
||||
|
||||
定位:服务一天期期权开仓前的「空间」判断(已实现波动点数档案),**不算 IV / 权利金 / Greeks**.
|
||||
|
||||
---
|
||||
|
||||
## 2. 不做(首版外)
|
||||
|
||||
- 币安 / Gate 等非 OKX 价源
|
||||
- 百分比振幅列(可后加「参考 %」,不进必须统计)
|
||||
- 未完成窗(当天尚未到 16:00)计入样本
|
||||
- 自动推送企业微信 / 注入交易教练
|
||||
- 中控代下单或改期权仓
|
||||
|
||||
---
|
||||
|
||||
## 3. 时间与样本规则
|
||||
|
||||
### 3.1 时区与终点
|
||||
|
||||
- 时区:**Asia/Shanghai(北京时间)**
|
||||
- **到期/收窗时刻固定 `16:00`**,不可改
|
||||
- 起点时刻:**仅整点** `00:00`~`23:00`(下拉选择)
|
||||
|
||||
### 3.2 跨天切窗(结算日 D)
|
||||
|
||||
对每个结算日 **D**(窗终点 = `D 日 16:00`):
|
||||
|
||||
| 起点整点 T | 窗起点 | 窗终点 |
|
||||
|------------|--------|--------|
|
||||
| `T >= 16:00` | **D-1 日 T:00** | D 日 16:00 |
|
||||
| `T < 16:00` | **D 日 T:00** | D 日 16:00 |
|
||||
|
||||
示例:
|
||||
|
||||
| 用户选择 | 某一结算日 D 的实际窗 |
|
||||
|----------|------------------------|
|
||||
| 16:00 → 16:00 | D-1 16:00 → D 16:00 |
|
||||
| 22:00 → 16:00 | D-1 22:00 → D 16:00 |
|
||||
| 08:00 → 16:00 | D 08:00 → D 16:00 |
|
||||
|
||||
### 3.3 回溯周期
|
||||
|
||||
| 选项 | 含义(完整收窗个数,约) |
|
||||
|------|------------------------|
|
||||
| 1 个月 | 约 30 个结算日 |
|
||||
| 2 个月 | 约 60 个结算日(默认推荐) |
|
||||
| 3 个月 | 约 90 个结算日 |
|
||||
| 半年 | 约 180 个结算日 |
|
||||
| 1 年 | 约 365 个结算日 |
|
||||
| 自定义 | 用户输入天数 N(`7`~`400`,可配置上下限) |
|
||||
|
||||
说明:
|
||||
|
||||
- 「月」按 **日历回溯 + 完整 16:00 收窗** 计数,不足整天的末日不入样
|
||||
- 仅纳入 **已结束** 的窗(`now >= D 16:00`);进行中的今天不入样
|
||||
|
||||
### 3.4 标的与价源(OKX)
|
||||
|
||||
| UI 下拉 | 价源(优先) | 降级(仅指数失败时) |
|
||||
|---------|------------|---------------------|
|
||||
| **ETH** | OKX **ETH-USD 指数** | OKX `ETH/USDT` 永续标记 |
|
||||
| **BTC** | OKX **BTC-USD 指数** | OKX `BTC/USDT` 永续标记 |
|
||||
|
||||
约束:
|
||||
|
||||
- **交易所固定 OKX**,UI 不提供其它所
|
||||
- 具体指数/合约符号以实现时 OKX 接口与 `hub_ohlcv` 对齐为准;结果与下载须标注 `exchange=okx` + 实际价源
|
||||
- K 线粒度:**1H**(与整点起止对齐,优先);同一作业内不得混用粒度.若后续要更细高低点,可升 5m/1m(P2)
|
||||
|
||||
---
|
||||
|
||||
## 4. 指标口径(点数,非百分比)
|
||||
|
||||
全部为 **绝对价格点数**(标的报价差;BTC/ETH 各自用自身价格刻度).
|
||||
|
||||
设窗内:
|
||||
|
||||
- `O` = 起点时刻价(或起点分钟 K 的 open)
|
||||
- `H` = 窗内最高
|
||||
- `L` = 窗内最低
|
||||
- `C` = 终点 16:00 价(或该分钟 close)
|
||||
|
||||
| 字段 | 算法 | 例(O=2000,H=2500,L=1800) |
|
||||
|------|------|---------------------------|
|
||||
| 开盘价 | `O` | 2000 |
|
||||
| 最高价 | `H` | 2500 |
|
||||
| 最低价 | `L` | 1800 |
|
||||
| 收盘/窗末价 | `C` | (另算) |
|
||||
| 开→高距离 | `H − O` | **500** |
|
||||
| 开→低距离 | `O − L` | **200** |
|
||||
| **振幅** | `(H−O)+(O−L)` = **`H−L`** | **700** |
|
||||
| 涨跌值 | `C − O`(可正负) | 可选列,首版建议保留 |
|
||||
|
||||
**必须统计(汇总层):**
|
||||
|
||||
- **最大振幅**(值 + 对应结算日)
|
||||
- **开→高距离**:最大、均值(建议)
|
||||
- **开→低距离**:最大、均值(建议)
|
||||
|
||||
可选汇总(首版建议带上,成本低):
|
||||
|
||||
- 振幅均值 / 中位数
|
||||
- 上涨窗占比(`C>O`)、下跌窗占比
|
||||
- 振幅 ≥ 用户阈值 X 点数的天数(X 可填,默认空=不算)
|
||||
|
||||
---
|
||||
|
||||
## 5. 界面(中控)
|
||||
|
||||
### 5.1 入口
|
||||
|
||||
- 顶栏新增导航项:**「振幅统计」**或 **「期权统计」**(最终文案实现时定一处;设置里可隐藏)
|
||||
- 手机端进「更多」
|
||||
|
||||
### 5.2 Tab
|
||||
|
||||
| Tab | 作用 |
|
||||
|-----|------|
|
||||
| **统计** | 配参数 → 计算 → 看日表+汇总 → 下载 / 存历史 |
|
||||
| **历史** | 过往作业列表;打开复看;再下载 |
|
||||
|
||||
### 5.3 「统计」页布局
|
||||
|
||||
1. **参数区**
|
||||
- **标的**:下拉 `ETH` / `BTC`(默认 ETH)
|
||||
- 数据源:只读展示 `OKX`
|
||||
- 起点整点:下拉 `00`~`23`(默认 `16`)
|
||||
- 终点:固定展示 `16:00`(不可改)
|
||||
- 周期:单选 `1月 / 2月 / 3月 / 半年 / 1年 / 自定义`
|
||||
- 自定义天数:仅自定义时显示
|
||||
- 按钮:`计算` · `保存到历史` · `下载`
|
||||
2. **日表明细**(分页,如每页 20 行;排序默认结算日倒序)
|
||||
3. **下方汇总区**(本次全样本,不是当前页)
|
||||
|
||||
### 5.4 「历史」页
|
||||
|
||||
每条记录至少:
|
||||
|
||||
- 创建时间、**标的**、起点整点、周期/天数、价源(OKX+指数/标记)、样本数
|
||||
- 最大振幅(+日期)
|
||||
- 操作:查看 / 下载 / 删除
|
||||
|
||||
**写入规则(建议):** 用户点击 **「保存到历史」** 才入库;仅点「计算」不自动灌历史(避免误点刷屏).若产品坚持「输入一次就算进历史」,可改为计算成功自动写入——实现前在本文改为冻结口径.
|
||||
|
||||
> 当前方案冻结倾向:**显式「保存到历史」**.
|
||||
|
||||
---
|
||||
|
||||
## 6. 下载
|
||||
|
||||
格式:优先 **CSV**(UTF-8 BOM,Excel 可开);或单文件双段.
|
||||
|
||||
必须包含:
|
||||
|
||||
1. **日表明细**(本次全部结算日,非当前页)
|
||||
2. **统计摘要**:标的、交易所 OKX、价源、最大振幅(+日)、开→高最大/均值、开→低最大/均值、样本数、起点整点、终点 16:00、周期、生成时间
|
||||
|
||||
文件名示例:`okx_eth_amp_22to16_60d_20260723.csv` / `okx_btc_amp_16to16_90d_20260723.csv`
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据与实现要点
|
||||
|
||||
### 7.1 复用
|
||||
|
||||
- 优先复用中控 `hub_ohlcv` / `hub_kline_store`,按 `exchange_key=okx` + 标的对应指数/合约拉齐历史 K 线并本地缓存
|
||||
- 首次 1 年 × 1m 数据量较大:计算前检查缓存覆盖;缺口再增量拉取;UI 显示进度/耗时提示
|
||||
- BTC / ETH 缓存键分离
|
||||
|
||||
### 7.2 后端模块(建议)
|
||||
|
||||
| 路径 | 职责 |
|
||||
|------|------|
|
||||
| `lib/hub/amp_stats_lib.py` | 标的映射、切窗、算日行、汇总 |
|
||||
| `manual_trading_hub/` 路由 + 静态页 | UI / API |
|
||||
| `manual_trading_hub/amp_stats_history.json`(或 sqlite) | 历史作业 |
|
||||
|
||||
### 7.3 API 草稿
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| `POST` | `/api/amp-stats/compute` | body: `symbol`(eth\|btc), start_hour, period\|days → 日表+汇总 |
|
||||
| `GET` | `/api/amp-stats/history` | 历史列表(可按 symbol 筛选) |
|
||||
| `POST` | `/api/amp-stats/history` | 保存当前结果 |
|
||||
| `GET` | `/api/amp-stats/history/{id}` | 详情 |
|
||||
| `DELETE` | `/api/amp-stats/history/{id}` | 删除 |
|
||||
| `GET` | `/api/amp-stats/export` | query 或 history id → 文件下载 |
|
||||
|
||||
### 7.4 性能
|
||||
|
||||
- 2 个月 × 1m:可接受同步(数十秒级需有 loading)
|
||||
- 1 年:建议异步任务或分块拉齐后再算;首版可限制「自定义 > 180 天」需确认二次点击
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收清单
|
||||
|
||||
- [ ] 标的下拉 ETH / BTC 可切换;数据源固定 OKX
|
||||
- [ ] 起点仅整点;终点 UI 固定 16:00
|
||||
- [ ] `22→16` / `16→16` / `08→16` 跨天规则与 §3.2 一致
|
||||
- [ ] 周期六档 + 自定义天数生效;默认 2 个月
|
||||
- [ ] 日表含:开高低收、开→高、开→低、振幅(点数)、涨跌值
|
||||
- [ ] 例:O=2000,H=2500,L=1800 → 开→高 500、开→低 200、振幅 700
|
||||
- [ ] 汇总含最大振幅(+日)、开→高/开→低统计
|
||||
- [ ] 分页只影响展示;汇总与下载用全样本
|
||||
- [ ] 未到 16:00 的当日不入样
|
||||
- [ ] 保存历史含标的字段 / 回看 / 删除
|
||||
- [ ] 下载含明细 + 统计摘要(含标的与 OKX)
|
||||
- [ ] 电脑与手机均可完成计算与下载(手机下载走系统分享/保存即可)
|
||||
|
||||
---
|
||||
|
||||
## 9. 分期
|
||||
|
||||
| 阶段 | 内容 |
|
||||
|------|------|
|
||||
| **P0** | 统计 Tab:标的下拉(ETH/BTC) + 参数 + 计算 + 日表分页 + 汇总 + 下载(不经历史) |
|
||||
| **P1** | 历史 Tab:保存 / 列表 / 回看 / 再下载 / 删除 |
|
||||
| **P2** | 缓存加速、长周期异步、振幅阈值天数、可选 % 参考列 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 待冻结(实现前确认)
|
||||
|
||||
| # | 问题 | 当前倾向 |
|
||||
|---|------|----------|
|
||||
| 1 | 历史写入:自动 vs 点保存 | **点保存** |
|
||||
| 2 | 下载 CSV vs Excel | **CSV** |
|
||||
| 3 | 价源 | **OKX 指数优先**(ETH-USD / BTC-USD);失败再降级永续标记 |
|
||||
| 4 | K 线 1m vs 5m vs 1H | **1H**(整点窗) |
|
||||
| 5 | 导航文案 | **「振幅统计」** |
|
||||
|
||||
**已冻结(开工口径):** 点保存进历史 · CSV · OKX 指数优先 · **1H K 线**(整点对齐,降低拉取量;与整点窗一致) · 导航「振幅统计」.
|
||||
|
||||
确认后将本文状态改为 **方案冻结**,再开工实现.
|
||||
|
||||
---
|
||||
|
||||
## 11. 修订记录
|
||||
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-23 | 初稿:中控 ETH 时段振幅统计;点数口径;周期档位;16:00 固定收窗;历史+下载 |
|
||||
| 2026-07-23 | 支持 BTC/ETH 下拉;数据源固定 OKX 指数(可降级永续标记);模块/API 改名为 amp-stats |
|
||||
@@ -41,6 +41,7 @@
|
||||
|------|------|
|
||||
| 第 1 次用户主动平仓 | 默认 **4h** 冷静期 |
|
||||
| 第 2 次用户主动平仓(同一交易日) | **日冻结** |
|
||||
| 平仓亏损达 `RISK_DAILY_LOSS_LIMIT` 次(同一交易日) | **日冻结**(默认 2 次;`0`=不启用) |
|
||||
| 复盘勾选任意情绪标签 | **日冻结** |
|
||||
| 复盘:离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**(须处于 4h 档冷静期中) |
|
||||
|
||||
@@ -77,11 +78,15 @@ RISK_CONTROL_ENABLED=true
|
||||
RISK_COOLING_HOURS_MANUAL=4
|
||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||
RISK_DAILY_LOSS_LIMIT=2
|
||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||
TRADING_DAY_RESET_HOUR=8
|
||||
APP_TIMEZONE=Asia/Shanghai
|
||||
```
|
||||
|
||||
- `RISK_DAILY_LOSS_LIMIT`:任意已平仓交易若盈亏 < 0 计 1 次(含止损/止盈后仍亏损等);达上限当日冻结开仓;`0` 表示不因亏损次数冻结.
|
||||
- `RISK_MANUAL_CLOSE_DAILY_LIMIT`:仅计**用户主动平仓**次数(与亏损次数独立).
|
||||
|
||||
`RISK_COOLING_HOURS_EXTERNAL` 已废弃(外部平仓不再触发风控).
|
||||
|
||||
## API 与 `risk_status` 字段
|
||||
@@ -102,6 +107,7 @@ APP_TIMEZONE=Asia/Shanghai
|
||||
| `can_trade` | 是否允许新开仓(仅风控维度) |
|
||||
| `reason` | 悬停提示文案 |
|
||||
| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env` 中 `MAX_ACTIVE_POSITIONS` |
|
||||
| `daily_loss_count` / `daily_loss_limit` | 当日亏损笔数与上限(`0` 上限表示未启用) |
|
||||
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
|
||||
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
||||
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
|
||||
@@ -123,7 +129,7 @@ APP_TIMEZONE=Asia/Shanghai
|
||||
|
||||
## 相关代码
|
||||
|
||||
- `account_risk_lib.py` — 状态机,`enrich_risk_status_countdown`,`apply_position_limit_risk`,`on_user_initiated_close`
|
||||
- `account_risk_lib.py` — 状态机,`enrich_risk_status_countdown`,`apply_position_limit_risk`,`on_user_initiated_close`,`on_closed_trade_pnl`
|
||||
- `hub_bridge.py` — `/api/hub/account-risk/user-close`
|
||||
- `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close
|
||||
- `strategy_trend_register.py` — `stop_trend_pullback` 结束计划时登记风控
|
||||
|
||||
@@ -131,6 +131,7 @@ AI 相关环境变量(`AI_PROVIDER`,`OPENAI_*`,`OLLAMA_*`,`AI_MODEL`,`AI_TIMEOUT
|
||||
| 手动平仓冷静(小时) | |
|
||||
| 复盘情绪冷静(小时) | |
|
||||
| 日手动平仓次数上限 | |
|
||||
| 日亏损次数上限 | 默认2;达限当日冻结开仓;0=不启用 |
|
||||
| 情绪标签日冻结 | |
|
||||
|
||||
详见 [account-risk-cooldown.md](./account-risk-cooldown.md).
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
| 文档 | 实例 | 状态 |
|
||||
|------|------|------|
|
||||
| [交易执行手册-期权与Gate.md](../交易执行手册-期权与Gate.md) | 中控「策略说明」·执行手册 | 个人开单纪律 |
|
||||
| [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 |
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
# 交易执行手册(期权为主 · Gate 为辅)
|
||||
|
||||
> 个人开单纪律与仓位规则(2026-07 起)。
|
||||
> 目标:少而精、可控回撤、样本干净;**不保证收益**。
|
||||
> 工具:OKX 期权(主)+ Gate 合约(辅);其它账户暂不做。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总原则
|
||||
|
||||
1. **主做期权,合约为辅**;同一时段尽量只让一边「说话」。
|
||||
2. **看不懂不做**;过滤比频率重要。
|
||||
3. 开仓前先过三关:**方向 → 空间 → 值不值得**。不够格 → 空仓。
|
||||
4. 期权离场只认:**止盈(规则触发)** 与 **到期**;**不手动平仓**(紧急例外单不算策略样本)。
|
||||
5. 过程可控、结果随缘:用规则管仓位与次数,不追求每天打满理想上限。
|
||||
|
||||
---
|
||||
|
||||
## 2. 账户与分工
|
||||
|
||||
| 账户 | 角色 | 说明 |
|
||||
|------|------|------|
|
||||
| OKX 期权 | **主业** | 横盘对冲 / 方向单 / 偏置对冲 |
|
||||
| Gate 合约 | **辅业** | 结构清楚时的波段;与期权尽量错开 |
|
||||
| 其它 | 暂不做 | 减少分心与样本污染 |
|
||||
|
||||
**到期选择(期权)**
|
||||
|
||||
- 方向单、对冲默认 **一天期**。
|
||||
- 尽量在 **北京时间下午 4 点后** 开 **次日到期**,覆盖较完整的美盘 + 亚盘 + 欧盘窗口。
|
||||
- Gate 波段样本里最长持仓约十余小时量级 → 一天期权通常够表达;更长故事优先考虑合约,不强行拉长期权。
|
||||
|
||||
---
|
||||
|
||||
## 3. 入场逻辑(三类)
|
||||
|
||||
开仓前先判断:当前是 **买波动** 还是 **买方向**。
|
||||
|
||||
### 3.1 横盘 → 期期对冲
|
||||
|
||||
- **条件**:横盘已持续较久(例如满约 12 小时),方向不明。
|
||||
- **工具**:一天期 Call + Put(对冲);总权利金预算见仓位章。
|
||||
- **意图**:买接下来的波动,不赌单边。
|
||||
- **期间**:一般 **不再开 Gate 方向单**(已在买波动,勿叠同一宏观暴露)。
|
||||
|
||||
### 3.2 方向明确 · 结构突破 → 期权
|
||||
|
||||
- **条件**:方向、空间、值不值得均过关;结构突破成立。
|
||||
- **工具**:**一天期期权方向单**(或明显顺势结构)。
|
||||
- **离场**:目标止盈或到期;不手平。
|
||||
- **默认**:先只开期权,不上合约。
|
||||
|
||||
### 3.3 结构突破后 · 反向假突破确认 → 可加合约
|
||||
|
||||
- **条件**:已有结构突破的期权表达;随后出现反向假突破且确认失败、续原方向。
|
||||
- **工具**:Gate 合约 **小仓加强**(止损纪律见下)。
|
||||
- **注意**:BTC 合约与 ETH 期权高度相关,属加重暴露,不是分散;仓位按「一笔故事」计风险。
|
||||
- **假突破定义**需事先写死(相对哪段结构、如何确认收回),避免临场随便加仓。
|
||||
|
||||
### 3.4 独立假突破(没有先开突破期权时)
|
||||
|
||||
- 按「假破专用」处理:优先 **只做合约** 或 **空仓**,勿与「突破后再假破加仓」混用同一套仓。
|
||||
|
||||
---
|
||||
|
||||
## 4. 对冲偏好(偏置对冲)
|
||||
|
||||
在「尽量用对冲」的前提下:
|
||||
|
||||
- 对冲内常带 **做多/做空比例**;若略偏多,则 **做多一侧比例更高**。
|
||||
- 顺势侧尽量用 **实值(或更实)**:
|
||||
- 方向对了:可能 **少赚一点**(相对纯单边);
|
||||
- 方向错了:争取 **不亏或少亏**(相对虚值双买两边磨光)。
|
||||
- **总权利金仍锁在对冲预算内**(见仓位);偏置只调张数/行权远近,不偷偷加预算。
|
||||
- **偏置有度**(例如勿极端到名存实亡的单边);完全没方向时更接近均分/近平值;方向非常明确时应走单边期权,不必硬套对冲壳。
|
||||
- 复盘建议区分:**中性对冲** vs **偏多/偏空对冲**,以便检验偏置是否真压低亏损。
|
||||
|
||||
---
|
||||
|
||||
## 5. 仓位与风险预算
|
||||
|
||||
**总资金参考:约 800U。**
|
||||
|
||||
| 项目 | 规则 |
|
||||
|------|------|
|
||||
| 单笔期权 | 约 **10U** 权利金预算;**一次只持有一个期权仓位** |
|
||||
| 期期对冲 | **合计约 10U**(两腿加总,不是各 10) |
|
||||
| Gate 合约 | 日内保证金约 **50U**、约 **10 倍**;有单才用,无单为 0 |
|
||||
| 合约止损 | 一般约 **5U**;单笔最大亏损不超过约 **10U** |
|
||||
| 日损失心理框 | 期权+合约若都错:合计大约 **≤20U**;都对时期望可到 **40U+**(理想情形,非每日目标) |
|
||||
|
||||
相对 800U:单笔约 **1.25%** 量级;全错一天约 **2.5%** 量级——防守优先。
|
||||
|
||||
**叠加红线**
|
||||
|
||||
- 期权一仓 + 合约加仓同日存在时,按合计风险接受最坏约 20U,且尽量少「同向双开」。
|
||||
- 不因「期权偏置可能少亏」而放大合约。
|
||||
|
||||
---
|
||||
|
||||
## 6. 合约日纪律(Gate)
|
||||
|
||||
1. 只做 **很明确的位置**;不明确基本不做。
|
||||
2. 动手前想清:**如何进场**。
|
||||
3. **同一位置最多两次机会**:结构突破、假突破。
|
||||
4. **两次都错 → 当日不再做单**(即使后面更「看起来清楚」也留到明天)。
|
||||
5. 止损约 **5U**;波段规则(含是否时间离场)开仓前想清。
|
||||
6. 已关闭「强制清仓」误伤策略意图时,离场以结构止盈/止损为准;历史里「强制清仓但盈利」按规则结果理解,复盘看盈亏与结构。
|
||||
|
||||
---
|
||||
|
||||
## 7. 期权日纪律(OKX)
|
||||
|
||||
1. **不手动平仓**;只等规则止盈或到期(紧急手平标记为非策略样本)。
|
||||
2. 一次一仓;对冲共 10U。
|
||||
3. 横盘对冲期间一般不开 Gate 方向单。
|
||||
4. 结构突破用期权表达;假破加强才考虑合约。
|
||||
5. 默认一天期;优先完整会话窗口再开。
|
||||
|
||||
---
|
||||
|
||||
## 8. 开仓前自检清单
|
||||
|
||||
- [ ] 今天是否只动「期权 / Gate」,其它账户零操作?
|
||||
- [ ] 买波动还是买方向?工具选对了吗?
|
||||
- [ ] 方向 / 空间 / 值不值得是否都过关?
|
||||
- [ ] 期权:止盈条件与「接受到期」是否写清?
|
||||
- [ ] 对冲:比例与实值偏置是否有度?总预算是否仍 ≤10U?
|
||||
- [ ] 合约:本位置第几次机会?止损约 5U 设好了吗?
|
||||
- [ ] 若加合约:是否已有突破期权且假破确认?是否当成一笔故事控总风险?
|
||||
- [ ] 今日合约两点机会是否已用完?(用完则收工)
|
||||
|
||||
---
|
||||
|
||||
## 9. 一句话版本
|
||||
|
||||
> **横盘对冲(可偏置实值);突破用一天期权;假破确认后小仓合约加强;先过方向/空间/值不值得;期权不手平;一位置两次,错完收工;单笔小亏、组合回撤可控。**
|
||||
|
||||
---
|
||||
|
||||
## 10. 修订记录
|
||||
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-21 | 初版:根据实盘讨论整理(期权为主、Gate 为辅、仓位与日停手规则) |
|
||||
+4
-2
@@ -6,7 +6,9 @@
|
||||
|
||||
| 标签 | 指向提交 | 说明 |
|
||||
|------|----------|------|
|
||||
| `snapshot/20260721` | `2a60d47` | 2026-07-21:仓库代码统计文档、期权复盘亮色主题、对冲腿盈亏时区修复、本快照说明等 |
|
||||
| `snapshot/20260723-pre-amp-stats` | 4ccfb83 | 2026-07-23:振幅统计开发前;含执行手册进教练、日亏损冻结、手机监控 UI、振幅统计开发方案等 |
|
||||
| `snapshot/20260721-2` | `a721642` | 2026-07-21 晚:日亏损次数冻结、交易执行手册入中控策略说明、期权/Gate 执行手册文档等 |
|
||||
| `snapshot/20260721` | `1a163c0` | 2026-07-21:仓库代码统计文档、期权复盘亮色主题、对冲腿盈亏时区修复、本快照说明等 |
|
||||
|
||||
## 历史标签(节选)
|
||||
|
||||
@@ -25,7 +27,7 @@
|
||||
git tag -l 'snapshot/*'
|
||||
|
||||
# 检出快照(只读查看,勿在此分支直接开发)
|
||||
git checkout snapshot/20260721
|
||||
git checkout snapshot/20260723-pre-amp-stats
|
||||
|
||||
# 回到主线
|
||||
git checkout main
|
||||
|
||||
Vendored
+1
@@ -58,6 +58,7 @@ HOT_RELOAD_EXACT = frozenset({
|
||||
"RISK_COOLING_HOURS_MANUAL",
|
||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL",
|
||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT",
|
||||
"RISK_DAILY_LOSS_LIMIT",
|
||||
"RISK_MOOD_ISSUES_DAILY_FREEZE",
|
||||
"KEY_AUTO_ORDER_ENABLED",
|
||||
"TRADE_DIRECTION_RESTRICT_ENABLED",
|
||||
|
||||
Vendored
+2
@@ -94,6 +94,7 @@ _SHARED_SECTIONS: list[dict[str, Any]] = [
|
||||
("RISK_COOLING_HOURS_MANUAL", "手动平仓冷静(小时)", ""),
|
||||
("RISK_COOLING_HOURS_MANUAL_JOURNAL", "复盘情绪冷静(小时)", ""),
|
||||
("RISK_MANUAL_CLOSE_DAILY_LIMIT", "日手动平仓次数上限", ""),
|
||||
("RISK_DAILY_LOSS_LIMIT", "日亏损次数上限", "默认2;达限当日冻结开仓;0=不因亏损次数冻结"),
|
||||
("RISK_MOOD_ISSUES_DAILY_FREEZE", "情绪标签日冻结", ""),
|
||||
],
|
||||
},
|
||||
@@ -197,6 +198,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
||||
"RISK_COOLING_HOURS_MANUAL": "4",
|
||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
|
||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
||||
"RISK_DAILY_LOSS_LIMIT": "2",
|
||||
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||
|
||||
@@ -10,9 +10,15 @@ from typing import Any
|
||||
|
||||
from lib.paths import REPO_ROOT
|
||||
|
||||
STRATEGY_EXCHANGES: tuple[str, ...] = ("binance", "okx", "gate")
|
||||
STRATEGY_EXCHANGES: tuple[str, ...] = ("playbook", "binance", "okx", "gate")
|
||||
|
||||
STRATEGY_META: dict[str, dict[str, str]] = {
|
||||
"playbook": {
|
||||
"label": "执行手册",
|
||||
"title": "交易执行手册(期权为主 · Gate 为辅)",
|
||||
# 相对仓库根;其余条目用 md_file 相对 docs/strategy
|
||||
"md_rel": "docs/交易执行手册-期权与Gate.md",
|
||||
},
|
||||
"binance": {
|
||||
"label": "币安",
|
||||
"title": "币安·山寨多头趋势",
|
||||
@@ -43,6 +49,9 @@ def _md_path(exchange_key: str) -> Path:
|
||||
meta = STRATEGY_META.get((exchange_key or "").strip().lower())
|
||||
if not meta:
|
||||
raise KeyError(exchange_key)
|
||||
md_rel = (meta.get("md_rel") or "").strip()
|
||||
if md_rel:
|
||||
return REPO_ROOT / md_rel
|
||||
return _strategy_dir() / meta["md_file"]
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from lib.key_monitor.key_auto_order_lib import load_key_auto_order_enabled
|
||||
from lib.trade.account_risk_lib import (
|
||||
cooling_hours_manual,
|
||||
cooling_hours_manual_journal,
|
||||
daily_loss_limit,
|
||||
manual_close_daily_limit,
|
||||
max_active_positions_from_env,
|
||||
mood_issues_daily_freeze_enabled,
|
||||
@@ -113,6 +114,15 @@ def build_instance_settings_view(
|
||||
_row("手动平仓冷静", f"{cooling_hours_manual():g} 小时"),
|
||||
_row("复盘后冷静", f"{cooling_hours_manual_journal():g} 小时", "手动平仓且填写说明后可缩短"),
|
||||
_row("日手动平仓上限", f"{manual_close_daily_limit()} 次", "超限当日冻结"),
|
||||
_row(
|
||||
"日亏损次数上限",
|
||||
(
|
||||
f"{daily_loss_limit()} 次"
|
||||
if daily_loss_limit() > 0
|
||||
else "未启用"
|
||||
),
|
||||
"平仓亏损达限后当日冻结开仓;0=不启用" if daily_loss_limit() > 0 else "RISK_DAILY_LOSS_LIMIT=0",
|
||||
),
|
||||
_row(
|
||||
"复盘情绪日冻结",
|
||||
_on_off(mood_issues_daily_freeze_enabled()),
|
||||
|
||||
@@ -86,6 +86,14 @@ def manual_close_daily_limit() -> int:
|
||||
return 2
|
||||
|
||||
|
||||
def daily_loss_limit() -> int:
|
||||
"""日亏损次数上限:达限当日冻结开仓;0=不因亏损次数冻结."""
|
||||
try:
|
||||
return max(0, int(os.getenv("RISK_DAILY_LOSS_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))))
|
||||
@@ -116,6 +124,7 @@ def ensure_account_risk_schema(conn) -> None:
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
trading_day TEXT,
|
||||
manual_close_count INTEGER DEFAULT 0,
|
||||
daily_loss_count INTEGER DEFAULT 0,
|
||||
cooloff_until_ms INTEGER,
|
||||
cooloff_hours INTEGER,
|
||||
daily_frozen INTEGER DEFAULT 0,
|
||||
@@ -124,10 +133,18 @@ def ensure_account_risk_schema(conn) -> None:
|
||||
updated_at TEXT
|
||||
)"""
|
||||
)
|
||||
cols = {
|
||||
str(r[1])
|
||||
for r in conn.execute("PRAGMA table_info(account_risk_state)").fetchall()
|
||||
}
|
||||
if "daily_loss_count" not in cols:
|
||||
conn.execute(
|
||||
"ALTER TABLE account_risk_state ADD COLUMN daily_loss_count INTEGER DEFAULT 0"
|
||||
)
|
||||
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)"
|
||||
"INSERT INTO account_risk_state (id, trading_day, manual_close_count, daily_loss_count, daily_frozen) VALUES (1, '', 0, 0, 0)"
|
||||
)
|
||||
|
||||
|
||||
@@ -268,6 +285,7 @@ def _sync_trading_day(conn, trading_day: str, now: Optional[datetime] = None) ->
|
||||
"""UPDATE account_risk_state SET
|
||||
trading_day=?,
|
||||
manual_close_count=0,
|
||||
daily_loss_count=0,
|
||||
daily_frozen=0,
|
||||
cooloff_until_ms=?,
|
||||
cooloff_hours=?,
|
||||
@@ -600,6 +618,43 @@ def on_manual_close(
|
||||
)
|
||||
|
||||
|
||||
def on_closed_trade_pnl(
|
||||
conn,
|
||||
*,
|
||||
pnl_amount: Any,
|
||||
trading_day: str,
|
||||
now: Optional[datetime] = None,
|
||||
) -> None:
|
||||
"""
|
||||
已平仓交易记盈亏后调用:亏损笔数达 RISK_DAILY_LOSS_LIMIT 则当日冻结开仓.
|
||||
上限为 0 时不启用本规则.
|
||||
"""
|
||||
if not risk_control_enabled():
|
||||
return
|
||||
limit = daily_loss_limit()
|
||||
if limit <= 0:
|
||||
return
|
||||
try:
|
||||
pnl = float(pnl_amount)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if pnl >= 0:
|
||||
return
|
||||
row = _sync_trading_day(conn, trading_day, now=now)
|
||||
if int(_row_get(row, "daily_frozen") or 0) == 1:
|
||||
return
|
||||
count = int(_row_get(row, "daily_loss_count") or 0) + 1
|
||||
conn.execute(
|
||||
"""UPDATE account_risk_state SET
|
||||
daily_loss_count=?,
|
||||
updated_at=?
|
||||
WHERE id=1""",
|
||||
(count, (now or datetime.now()).strftime("%Y-%m-%d %H:%M:%S")),
|
||||
)
|
||||
if count >= limit:
|
||||
_set_daily_frozen(conn, trading_day=trading_day, now=now)
|
||||
|
||||
|
||||
def on_journal_saved(
|
||||
conn,
|
||||
*,
|
||||
@@ -762,6 +817,7 @@ def compute_account_risk_status(
|
||||
"cooloff_until_ms": None,
|
||||
"cooloff_until": None,
|
||||
"manual_close_count": 0,
|
||||
"daily_loss_count": 0,
|
||||
"daily_frozen": False,
|
||||
}
|
||||
row = _sync_trading_day(conn, trading_day, now=now)
|
||||
@@ -784,12 +840,21 @@ def compute_account_risk_status(
|
||||
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)
|
||||
daily_loss_count = int(_row_get(row, "daily_loss_count") or 0)
|
||||
loss_limit = daily_loss_limit()
|
||||
|
||||
status = STATUS_NORMAL
|
||||
reason = ""
|
||||
if daily_frozen:
|
||||
status = STATUS_DAILY
|
||||
reason = f"账户今日已冻结(手动平仓 {manual_close_count} 次或复盘情绪标签)"
|
||||
parts = []
|
||||
if loss_limit > 0 and daily_loss_count >= loss_limit:
|
||||
parts.append(f"日亏损 {daily_loss_count}/{loss_limit} 次")
|
||||
if manual_close_count >= manual_close_daily_limit():
|
||||
parts.append(f"手动平仓 {manual_close_count} 次")
|
||||
if not parts:
|
||||
parts.append("手动平仓/日亏损达限或复盘情绪标签")
|
||||
reason = "账户今日已冻结(" + "、".join(parts) + ")"
|
||||
elif cooloff_until_ms is not None:
|
||||
remaining_ms = cooloff_until_ms - now_ms
|
||||
hours = _cooloff_hours_value(row)
|
||||
@@ -818,6 +883,8 @@ def compute_account_risk_status(
|
||||
if fmt_local_ms and cooloff_until_ms
|
||||
else None,
|
||||
"manual_close_count": manual_close_count,
|
||||
"daily_loss_count": daily_loss_count,
|
||||
"daily_loss_limit": loss_limit,
|
||||
"daily_frozen": daily_frozen,
|
||||
"pending_journal_trade_id": pending,
|
||||
"freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **交易教练** | 口语化陪聊;注入三户监控快照与今日总结摘要(后台自动生成,不在页面展示) |
|
||||
| **交易教练** | 口语化陪聊;注入三户监控快照(**含 OKX 期权持仓明细**)、执行手册短摘要与今日总结摘要(后台自动生成,不在页面展示) |
|
||||
| **普通聊天** | 不绑交易数据,适合闲聊,答疑 |
|
||||
| **交易监管** | 今日长会话;手动/中控开平仓与新开仓自动推送 + 企业微信 + 可回聊(见 [交易监管说明.md](./交易监管说明.md)) |
|
||||
| **会话历史** | 右侧列表:切换,删除;消息一键复制 |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| **资金概况** | 总资金曲线、分户权益、回撤与 24h 变化 |
|
||||
| **开仓计划** | 事前写下计划、跟踪进行中、统计历史胜率 |
|
||||
| **监控区** | **核心操作台**:三所持仓卡片、全平/撤单、关键位与趋势计划摘要 |
|
||||
| **策略说明** | 三所策略 playbook + 开仓检查清单(非系统操作手册) |
|
||||
| **策略说明** | 执行手册 + 三所策略 playbook + 开仓检查清单(非系统操作手册) |
|
||||
| **使用说明** | 本页:中控与实例怎么用 |
|
||||
| **行情区** | K 线、指标、画线;可从持仓跳转带币种 |
|
||||
| **计算器** | 趋势回调 / 滚仓张数与盈亏测算(手动填价) |
|
||||
|
||||
@@ -25,6 +25,7 @@ from hub_ai.context import (
|
||||
format_chat_context_for_chat,
|
||||
format_chat_position_overview,
|
||||
)
|
||||
from hub_ai.playbook_brief import format_playbook_brief_for_chat
|
||||
from hub_ai.prompts import (
|
||||
CHAT_GENERAL_SYSTEM,
|
||||
CHAT_SYSTEM,
|
||||
@@ -217,6 +218,10 @@ def send_chat_message(
|
||||
ctx = build_chat_context(exchanges, trading_day=day)
|
||||
day = ctx["trading_day"]
|
||||
brief_ctx, excerpt = _trading_context_bundle(ctx, prior_count=prior_count)
|
||||
# 首轮带完整手册摘要;续聊缩短,避免挤占对话上下文
|
||||
playbook = format_playbook_brief_for_chat(
|
||||
max_chars=1200 if prior_count <= 0 else 700
|
||||
)
|
||||
user_prompt = build_chat_user_prompt(
|
||||
context_text=brief_ctx,
|
||||
trading_day=day,
|
||||
@@ -225,6 +230,7 @@ def send_chat_message(
|
||||
history_lines=history_tail,
|
||||
user_message=user_for_prompt,
|
||||
attachment_note=str(parsed.get("attachment_note") or ""),
|
||||
playbook_brief=playbook,
|
||||
)
|
||||
if parsed.get("text_append"):
|
||||
user_prompt += "\n\n【附件正文】\n" + _clip_text(parsed["text_append"], 3000)
|
||||
|
||||
@@ -86,7 +86,81 @@ def _filter_open_positions(positions: list) -> list[dict]:
|
||||
|
||||
|
||||
def _account_open_position_count(ac: dict) -> int:
|
||||
return len(_filter_open_positions(ac.get("positions") or []))
|
||||
perp = len(_filter_open_positions(ac.get("positions") or []))
|
||||
opt = int(ac.get("options_open_position_count") or 0)
|
||||
if opt <= 0:
|
||||
opt = len(_iter_options_position_dicts(ac))
|
||||
return perp + opt
|
||||
|
||||
|
||||
def _iter_options_position_dicts(ac: dict) -> list[dict]:
|
||||
snap = ac.get("options_snapshot")
|
||||
if not isinstance(snap, dict):
|
||||
return []
|
||||
if snap.get("ok") is False or snap.get("enabled") is False:
|
||||
return []
|
||||
out: list[dict] = []
|
||||
for p in snap.get("positions") or []:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
inst = str(p.get("inst_id") or p.get("instId") or "").strip()
|
||||
if not inst:
|
||||
continue
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def _format_options_position_detail_line(p: dict) -> str:
|
||||
inst = p.get("inst_id") or p.get("instId") or "?"
|
||||
opt_type = (p.get("opt_type") or p.get("optType") or "").upper()
|
||||
label = "Call" if opt_type == "C" else "Put" if opt_type == "P" else (opt_type or "OPT")
|
||||
src = _options_source_label(p)
|
||||
sheets = p.get("pos")
|
||||
if sheets is None:
|
||||
sheets = p.get("sheets")
|
||||
if sheets is None:
|
||||
sheets = p.get("contracts")
|
||||
if sheets is None:
|
||||
sheets = "?"
|
||||
parts = [f"期权 {inst} {label}", f"来源{src}", f"张数{sheets}"]
|
||||
paid = _safe_float(p.get("premium_paid"))
|
||||
if paid is not None:
|
||||
parts.append(f"权利金{paid:g}U")
|
||||
net: Optional[float] = None
|
||||
try:
|
||||
from lib.options.options_positions_lib import net_pnl_from_display_row
|
||||
|
||||
net = net_pnl_from_display_row(p)
|
||||
except Exception:
|
||||
net = None
|
||||
if net is None:
|
||||
net = _safe_float(p.get("net_pnl"))
|
||||
if net is None:
|
||||
net = _safe_float(p.get("upl"))
|
||||
if net is not None:
|
||||
parts.append(f"净盈亏{net:.4f}U")
|
||||
tgt = _options_target_monitor_text(p)
|
||||
if tgt and tgt not in ("—", "-", ""):
|
||||
parts.append(f"目标{tgt}")
|
||||
return " - " + " ".join(parts)
|
||||
|
||||
|
||||
def _append_options_position_lines(lines: list[str], ac: dict, *, limit: int = 6, indent: str = " - ") -> None:
|
||||
rows = _iter_options_position_dicts(ac)
|
||||
if not rows:
|
||||
return
|
||||
if indent.startswith(" "):
|
||||
# chat slim: already under account bullet
|
||||
for p in rows[:limit]:
|
||||
lines.append(f" · {_format_options_position_detail_line(p).lstrip(' - ')}")
|
||||
if len(rows) > limit:
|
||||
lines.append(f" · …共{len(rows)}笔期权持仓")
|
||||
return
|
||||
lines.append("期权持仓明细(交易所实盘,含目标位若已挂):")
|
||||
for p in rows[:limit]:
|
||||
lines.append(_format_options_position_detail_line(p))
|
||||
if len(rows) > limit:
|
||||
lines.append(f" - …共{len(rows)}笔期权持仓")
|
||||
|
||||
|
||||
def _monitor_counts(ac: dict) -> dict[str, int]:
|
||||
@@ -788,7 +862,9 @@ def format_context_text(payload: dict) -> str:
|
||||
lines.append(
|
||||
f"【合计·今日 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | "
|
||||
f"笔数 {totals.get('closed_count')}(胜{totals.get('win_count')}/负{totals.get('loss_count')})| "
|
||||
f"实盘持仓 {totals.get('open_position_count', 0)} 仓 | "
|
||||
f"实盘持仓 {totals.get('open_position_count', 0)} 仓"
|
||||
f"(永续{totals.get('perpetual_open_position_count', totals.get('open_position_count', 0))}/"
|
||||
f"期权{totals.get('options_open_position_count', 0)}) | "
|
||||
f"浮盈亏 {totals.get('float_pnl_u')}U | "
|
||||
f"资金账户合计 {_fmt_fund(totals.get('total_funding_usdt'))} | "
|
||||
f"交易账户合计 {_fmt_fund(totals.get('total_trading_usdt'))}"
|
||||
@@ -855,6 +931,7 @@ def format_context_text(payload: dict) -> str:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
lines.append(_format_position_detail_line(p, hub_mon))
|
||||
_append_options_position_lines(lines, ac, limit=8)
|
||||
lines.append(
|
||||
f"Agent合约余额:{ac.get('balance_usdt') if ac.get('balance_usdt') is not None else '未知'} USDT"
|
||||
)
|
||||
@@ -885,7 +962,9 @@ def format_summary_context_text(payload: dict) -> str:
|
||||
lines.append(
|
||||
f"【合计·今日 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | "
|
||||
f"笔数 {totals.get('closed_count')}(胜{totals.get('win_count')}/负{totals.get('loss_count')})| "
|
||||
f"实盘持仓 {totals.get('open_position_count', 0)} 仓 | "
|
||||
f"实盘持仓 {totals.get('open_position_count', 0)} 仓"
|
||||
f"(永续{totals.get('perpetual_open_position_count', totals.get('open_position_count', 0))}/"
|
||||
f"期权{totals.get('options_open_position_count', 0)}) | "
|
||||
f"浮盈亏 {totals.get('float_pnl_u')}U | "
|
||||
f"资金账户合计 {_fmt_fund(totals.get('total_funding_usdt'))} | "
|
||||
f"交易账户合计 {_fmt_fund(totals.get('total_trading_usdt'))}"
|
||||
@@ -943,6 +1022,7 @@ def format_summary_context_text(payload: dict) -> str:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
lines.append(_format_position_detail_line(p, hub_mon))
|
||||
_append_options_position_lines(lines, ac, limit=8)
|
||||
lines.append(
|
||||
f"Agent合约余额:{ac.get('balance_usdt') if ac.get('balance_usdt') is not None else '未知'} USDT"
|
||||
)
|
||||
@@ -1289,21 +1369,30 @@ def collect_closed_trades_snapshot(
|
||||
def format_chat_position_overview(payload: dict) -> str:
|
||||
totals = payload.get("totals") or {}
|
||||
total_open = int(totals.get("open_position_count") or 0)
|
||||
opt_total = int(totals.get("options_open_position_count") or 0)
|
||||
perp_total = int(
|
||||
totals.get("perpetual_open_position_count")
|
||||
if totals.get("perpetual_open_position_count") is not None
|
||||
else max(0, total_open - opt_total)
|
||||
)
|
||||
if total_open <= 0:
|
||||
head = f"【实盘持仓总览】当前空仓(监控户合计 0 仓).浮盈亏 0U 表示无持仓,不是「有仓但不动」."
|
||||
else:
|
||||
head = (
|
||||
f"【实盘持仓总览】监控户合计 {total_open} 仓,"
|
||||
f"【实盘持仓总览】监控户合计 {total_open} 仓"
|
||||
f"(永续{perp_total}/期权{opt_total}),"
|
||||
f"浮盈亏合计 {totals.get('float_pnl_u')}U."
|
||||
)
|
||||
lines = [
|
||||
head,
|
||||
"【区分】只有带「持仓明细/交易所实盘」字样的才是已开仓;趋势回调,关键位,下单监控,顺势加仓是本地计划/监控,不算持仓.持仓明细若含止损/止盈价,表示已挂条件单或监控计划中有价位.",
|
||||
"【区分】只有带「持仓明细/交易所实盘/期权持仓」字样的才是已开仓;趋势回调,关键位,下单监控,顺势加仓是本地计划/监控,不算持仓.持仓明细若含止损/止盈价,表示已挂条件单或监控计划中有价位.",
|
||||
]
|
||||
for ac in payload.get("accounts") or []:
|
||||
if ac.get("status") == "未监控":
|
||||
continue
|
||||
n = int(ac.get("open_position_count") or _account_open_position_count(ac))
|
||||
opt_n = int(ac.get("options_open_position_count") or len(_iter_options_position_dicts(ac)))
|
||||
perp_n = len(_filter_open_positions(ac.get("positions") or []))
|
||||
mc = _monitor_counts(ac)
|
||||
mon_parts = []
|
||||
if mc["trends"]:
|
||||
@@ -1319,8 +1408,11 @@ def format_chat_position_overview(payload: dict) -> str:
|
||||
lines.append(f"- {ac.get('name')}:空仓{mon_txt}")
|
||||
else:
|
||||
lines.append(
|
||||
f"- {ac.get('name')}:{n}仓 浮盈亏{ac.get('float_pnl_u')}U{mon_txt}"
|
||||
f"- {ac.get('name')}:{n}仓(永续{perp_n}/期权{opt_n}) "
|
||||
f"浮盈亏{ac.get('float_pnl_u')}U{mon_txt}"
|
||||
)
|
||||
for p in _iter_options_position_dicts(ac)[:4]:
|
||||
lines.append(f" · {_format_options_position_detail_line(p).lstrip(' - ')}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@@ -1328,11 +1420,19 @@ def format_chat_context_slim(payload: dict) -> str:
|
||||
"""聊天专用:不含 180 日资金曲线与昨日平仓明细,避免挤占对话上下文."""
|
||||
totals = payload.get("totals") or {}
|
||||
day = totals.get("trading_day")
|
||||
opt_total = int(totals.get("options_open_position_count") or 0)
|
||||
perp_total = int(
|
||||
totals.get("perpetual_open_position_count")
|
||||
if totals.get("perpetual_open_position_count") is not None
|
||||
else max(0, int(totals.get("open_position_count") or 0) - opt_total)
|
||||
)
|
||||
lines = [
|
||||
f"【今日合计 {day}】平仓盈亏 {totals.get('total_pnl_u')}U | "
|
||||
f"笔数 {totals.get('closed_count')}(胜{totals.get('win_count')}/负{totals.get('loss_count')})| "
|
||||
f"实盘持仓 {totals.get('open_position_count', 0)} 仓 | 浮盈亏 {totals.get('float_pnl_u')}U",
|
||||
"【说明】持仓=交易所实盘;趋势/关键位/监控单=本地计划,不等于已开仓.持仓行内「止损/止盈」= 交易所条件单或监控计划价(与监控页一致).",
|
||||
f"实盘持仓 {totals.get('open_position_count', 0)} 仓"
|
||||
f"(永续{perp_total}/期权{opt_total}) | 浮盈亏 {totals.get('float_pnl_u')}U",
|
||||
"【说明】持仓=交易所实盘(含期权);趋势/关键位/监控单=本地计划,不等于已开仓."
|
||||
"永续行「止损/止盈」=条件单或监控计划价;期权行含合约/来源/权利金/净盈亏/目标位.",
|
||||
]
|
||||
for ac in payload.get("accounts") or []:
|
||||
if ac.get("status") == "未监控":
|
||||
@@ -1340,7 +1440,12 @@ def format_chat_context_slim(payload: dict) -> str:
|
||||
continue
|
||||
st = ac.get("trade_stats") or {}
|
||||
open_n = int(ac.get("open_position_count") or _account_open_position_count(ac))
|
||||
pos_txt = "空仓" if open_n <= 0 else f"{open_n}仓 浮盈亏{ac.get('float_pnl_u')}U"
|
||||
opt_n = int(ac.get("options_open_position_count") or len(_iter_options_position_dicts(ac)))
|
||||
perp_n = len(_filter_open_positions(ac.get("positions") or []))
|
||||
if open_n <= 0:
|
||||
pos_txt = "空仓"
|
||||
else:
|
||||
pos_txt = f"{open_n}仓(永续{perp_n}/期权{opt_n}) 浮盈亏{ac.get('float_pnl_u')}U"
|
||||
mc = _monitor_counts(ac)
|
||||
mon = []
|
||||
if mc["trends"]:
|
||||
@@ -1369,6 +1474,7 @@ def format_chat_context_slim(payload: dict) -> str:
|
||||
if not isinstance(p, dict):
|
||||
continue
|
||||
lines.append(f" · {_format_position_detail_line(p, hub_mon).lstrip(' - ')}")
|
||||
_append_options_position_lines(lines, ac, limit=6, indent=" · ")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""交易教练用的执行手册短摘要(来源 docs/交易执行手册-期权与Gate.md)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from lib.paths import REPO_ROOT
|
||||
|
||||
# 控制 token:保持简短;手册大改时同步修订本摘要.
|
||||
_PLAYBOOK_BRIEF = """【用户策略执行手册·摘要】(来源:docs/交易执行手册-期权与Gate.md)
|
||||
一句话:横盘对冲(可偏置实值);突破用一天期权;假破确认后小仓合约加强;先过方向/空间/值不值得;期权不手平;一位置两次,错完收工;单笔小亏、组合回撤可控.
|
||||
分工:OKX 期权=主业;Gate 合约=辅业;其它账户暂不做.同一时段尽量只让一边说话.
|
||||
入场三类:①横盘较久→期期对冲(一天 Call+Put,总权利金约10U),期间一般不开 Gate;②方向/空间/值不值得过关且结构突破→一天期权方向单,默认不上合约;③已有突破期权后出现反向假破确认→Gate 小仓加强(加重暴露,按一笔故事控风险).
|
||||
仓位(总资约800U):单笔期权约10U且一次一仓;期期对冲合计约10U;Gate 保证金约50U×约10x,止损约5U,单笔最亏约≤10U;日最坏约≤20U.
|
||||
期权纪律:不手动平仓,只认规则止盈或到期(紧急手平非策略样本);默认一天期,尽量北京时间16:00后开次日到期.
|
||||
Gate 纪律:只做很明确位置;同一位置最多两次机会(结构突破/假突破);两次都错→当日收工.
|
||||
教练用法:对照上述纪律讨论执行与心态;勿另造策略或鼓励期权手平/超仓."""
|
||||
|
||||
|
||||
def playbook_md_path() -> Path:
|
||||
return REPO_ROOT / "docs" / "交易执行手册-期权与Gate.md"
|
||||
|
||||
|
||||
def format_playbook_brief_for_chat(max_chars: int = 1200) -> str:
|
||||
"""返回注入交易教练上下文的短摘要."""
|
||||
text = _PLAYBOOK_BRIEF.strip()
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
return text[: max(200, max_chars - 1)].rstrip() + "…"
|
||||
@@ -44,10 +44,12 @@ CHAT_SYSTEM = """
|
||||
- 不要「第1点第2点你应该…」;不要「作为你的教练我必须…」.
|
||||
- 不预测涨跌,不保证收益,不替用户做决定.
|
||||
- 只能依据提供的监控与交易数据说话;看不到的就说「我这边看不到,你可以去 xx 实例页确认」.
|
||||
- **持仓判定**:只有快照里「实盘持仓总览 / 持仓明细 / 交易所实盘」才算已开仓;「空仓 / 0 仓」就是没仓位.浮盈亏 0U 且空仓时,不要说「还有仓」「卡着不动」.
|
||||
- **持仓判定**:只有快照里「实盘持仓总览 / 持仓明细 / 交易所实盘 / 期权持仓」才算已开仓;「空仓 / 0 仓」就是没仓位.浮盈亏 0U 且空仓时,不要说「还有仓」「卡着不动」.
|
||||
- **期权持仓**:快照中「期权 …」行与永续同样是实盘;须分开提及.期权净盈亏/目标位以快照为准.
|
||||
- **监控单 ≠ 持仓**:趋势回调,关键位,顺势加仓,下单监控是本地计划或挂单监控,用户说已平仓时,即使还有这些监控,也不要当成手里还有仓.
|
||||
- 用户口述与快照冲突时,以快照为准并口语说明「我这边看到是空仓/有N仓」.
|
||||
- 若附带「今日总结摘要」,那是较早生成的缓存,**实盘持仓以【当前多账户快照】里的「实盘持仓总览」为准**,摘要里若提到持仓可能已过时.
|
||||
- 若附带【用户策略执行手册·摘要】,须按该纪律理解账户分工与离场规则(如期权通常不手平、Gate 一位置两次等);勿另造策略或鼓励违反摘要纪律.
|
||||
- 若用户上传图片,可结合图中可见信息讨论,看不清的明确说看不清.
|
||||
- **优先接住【用户现在说】和【对话核心摘要】**:用户聊心态,悔单,某笔操作时,先顺着这个话题回应,不要每句都复述账户资金数字.
|
||||
- **接续对话**:有【对话核心摘要】时须接着聊,不要重复开场白;整段回复必须写完,以句号/问号/感叹号收尾,不得停在半句话;编号列表每条单独一行.
|
||||
@@ -143,12 +145,20 @@ def build_chat_user_prompt(
|
||||
history_lines: str = "",
|
||||
user_message: str,
|
||||
attachment_note: str = "",
|
||||
playbook_brief: str = "",
|
||||
) -> str:
|
||||
parts = [f"【交易日】{trading_day}"]
|
||||
if rolling_summary.strip():
|
||||
parts.extend(["【对话核心摘要(须接续,勿重复开场)】", rolling_summary.strip()])
|
||||
elif history_lines.strip():
|
||||
parts.extend(["【最近对话】", history_lines.strip()])
|
||||
if playbook_brief.strip():
|
||||
parts.extend(
|
||||
[
|
||||
"【用户策略执行手册·摘要(须知悉分工与纪律)】",
|
||||
playbook_brief.strip(),
|
||||
]
|
||||
)
|
||||
parts.extend([
|
||||
"【当前多账户快照(事实参考;持仓以「实盘持仓总览」为准)】",
|
||||
context_text.strip() or "(无监控数据)",
|
||||
|
||||
@@ -4301,6 +4301,70 @@ body.login-page {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 手机:隐藏「操作·刷新/紧急全平」,桌面不变 */
|
||||
body.hub-phone #monitor-ops-fold {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 手机收起态:今日统计固定两行(左标题/交易日,右总浮盈亏) */
|
||||
body.hub-phone .monitor-stats-card.is-collapsed {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .monitor-stats-head {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 10px;
|
||||
row-gap: 2px;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .monitor-stats-head-main {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .card-title-row {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
flex-wrap: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .card-title {
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .card-sub {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
margin: 0;
|
||||
font-size: 10px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .monitor-stats-float-summary {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / span 2;
|
||||
align-self: center;
|
||||
padding: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .monitor-stats-float-summary .monitor-stat-label {
|
||||
margin-bottom: 0;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stats-card.is-collapsed .monitor-stats-float-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body.hub-phone .monitor-stat-cell {
|
||||
padding: 8px 6px;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260720-calc-equal-height" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260722-monitor-m-stats" />
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
@@ -960,7 +960,7 @@
|
||||
<div class="page-head strategy-page-head">
|
||||
<div>
|
||||
<h1><span class="head-tag">STR</span> 策略说明</h1>
|
||||
<p class="page-desc">策略正文(带目录) · 执行清单(打印对照) · 三所切换</p>
|
||||
<p class="page-desc">执行手册 · 三所策略正文(带目录) · 执行清单(打印对照)</p>
|
||||
</div>
|
||||
<div class="strategy-page-actions no-print">
|
||||
<button type="button" id="strategy-btn-download" class="ghost">下载 HTML</button>
|
||||
@@ -1405,7 +1405,7 @@
|
||||
<script src="/assets/quotes.js?v=20260717-quotes-feed"></script>
|
||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||
<script src="/assets/dashboard.js?v=20260720-dash-sl-tp"></script>
|
||||
<script src="/assets/strategy.js?v=8"></script>
|
||||
<script src="/assets/strategy.js?v=9"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
<script src="/assets/logs.js?v=1"></script>
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
const btnPrintChecklistInline = document.getElementById("strategy-btn-print-checklist-inline");
|
||||
const btnDownload = document.getElementById("strategy-btn-download");
|
||||
|
||||
let activeKey = "binance";
|
||||
let activeKey = "playbook";
|
||||
let activeView = "doc";
|
||||
let tabsMeta = [];
|
||||
let cache = {};
|
||||
@@ -128,13 +128,14 @@
|
||||
|
||||
function sectionTag(title) {
|
||||
const t = String(title || "");
|
||||
if (/账户|定位/.test(t)) return "账户";
|
||||
if (/开仓类型|开仓|入场|反转|顺势|波段|假破|结构/.test(t)) return "入场";
|
||||
if (/总原则|原则/.test(t)) return "原则";
|
||||
if (/账户|定位|分工/.test(t)) return "账户";
|
||||
if (/开仓类型|开仓|入场|反转|顺势|波段|假破|结构|对冲|方向单/.test(t)) return "入场";
|
||||
if (/周期/.test(t)) return "周期";
|
||||
if (/方向/.test(t)) return "方向";
|
||||
if (/纪律|出场|笔数|节奏/.test(t)) return "纪律";
|
||||
if (/持仓|离场|强平/.test(t)) return "离场";
|
||||
if (/资金|杠杆|计仓/.test(t)) return "仓位";
|
||||
if (/纪律|出场|笔数|节奏|止损|次数/.test(t)) return "纪律";
|
||||
if (/持仓|离场|强平|到期/.test(t)) return "离场";
|
||||
if (/资金|杠杆|计仓|仓位|预算/.test(t)) return "仓位";
|
||||
if (/系统|字段|对接/.test(t)) return "系统";
|
||||
if (/修订|记录/.test(t)) return "版本";
|
||||
if (/边界|关系|行情状态/.test(t)) return "边界";
|
||||
|
||||
@@ -20,6 +20,7 @@ from lib.trade.account_risk_lib import (
|
||||
enrich_risk_status_countdown,
|
||||
ensure_account_risk_schema,
|
||||
max_active_positions_from_env,
|
||||
on_closed_trade_pnl,
|
||||
on_journal_saved,
|
||||
on_manual_close,
|
||||
on_user_initiated_close,
|
||||
@@ -58,6 +59,7 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
os.environ["RISK_COOLING_HOURS_MANUAL"] = "4"
|
||||
os.environ["RISK_COOLING_HOURS_MANUAL_JOURNAL"] = "1"
|
||||
os.environ["RISK_MANUAL_CLOSE_DAILY_LIMIT"] = "2"
|
||||
os.environ["RISK_DAILY_LOSS_LIMIT"] = "2"
|
||||
os.environ["RISK_MOOD_ISSUES_DAILY_FREEZE"] = "1"
|
||||
os.environ["APP_TIMEZONE"] = "Asia/Shanghai"
|
||||
|
||||
@@ -521,6 +523,41 @@ class AccountRiskLibTests(unittest.TestCase):
|
||||
os.environ["MAX_ACTIVE_POSITIONS"] = "3"
|
||||
self.assertEqual(max_active_positions_from_env(), 3)
|
||||
|
||||
def test_daily_loss_limit_freezes_on_second_loss(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
on_closed_trade_pnl(conn, pnl_amount=-1.5, trading_day="2026-06-14", now=now)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["daily_loss_count"], 1)
|
||||
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||
on_closed_trade_pnl(conn, pnl_amount=-0.2, trading_day="2026-06-14", now=now)
|
||||
st2 = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st2["daily_loss_count"], 2)
|
||||
self.assertEqual(st2["status"], STATUS_DAILY)
|
||||
self.assertFalse(st2["can_trade"])
|
||||
self.assertIn("日亏损", st2["reason"])
|
||||
|
||||
def test_daily_loss_limit_zero_disables(self):
|
||||
os.environ["RISK_DAILY_LOSS_LIMIT"] = "0"
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
on_closed_trade_pnl(conn, pnl_amount=-10, trading_day="2026-06-14", now=now)
|
||||
on_closed_trade_pnl(conn, pnl_amount=-10, trading_day="2026-06-14", now=now)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["daily_loss_count"], 0)
|
||||
self.assertEqual(st["daily_loss_limit"], 0)
|
||||
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||
self.assertTrue(st["can_trade"])
|
||||
|
||||
def test_profitable_close_does_not_count_loss(self):
|
||||
conn = _mem_conn()
|
||||
now = datetime(2026, 6, 14, 12, 0, 0)
|
||||
on_closed_trade_pnl(conn, pnl_amount=3.2, trading_day="2026-06-14", now=now)
|
||||
on_closed_trade_pnl(conn, pnl_amount=0, trading_day="2026-06-14", now=now)
|
||||
st = compute_account_risk_status(conn, trading_day="2026-06-14", now=now)
|
||||
self.assertEqual(st["daily_loss_count"], 0)
|
||||
self.assertEqual(st["status"], STATUS_NORMAL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""hub_ai:期权持仓进教练上下文 + 执行手册摘要."""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from hub_ai.context import (
|
||||
format_chat_context_for_chat,
|
||||
format_chat_context_slim,
|
||||
format_chat_position_overview,
|
||||
)
|
||||
from hub_ai.playbook_brief import format_playbook_brief_for_chat
|
||||
from hub_ai.prompts import build_chat_user_prompt
|
||||
|
||||
|
||||
def _sample_payload():
|
||||
return {
|
||||
"totals": {
|
||||
"trading_day": "2026-07-22",
|
||||
"total_pnl_u": 0,
|
||||
"closed_count": 0,
|
||||
"win_count": 0,
|
||||
"loss_count": 0,
|
||||
"float_pnl_u": 1.25,
|
||||
"open_position_count": 1,
|
||||
"options_open_position_count": 1,
|
||||
"perpetual_open_position_count": 0,
|
||||
},
|
||||
"accounts": [
|
||||
{
|
||||
"name": "OKX_趋势",
|
||||
"key": "okx",
|
||||
"status": "已监控",
|
||||
"open_position_count": 1,
|
||||
"options_open_position_count": 1,
|
||||
"float_pnl_u": 1.25,
|
||||
"funding_usdt": 100,
|
||||
"trading_usdt": 50,
|
||||
"trade_stats": {"total_pnl_u": 0, "closed_count": 0, "win_count": 0, "loss_count": 0},
|
||||
"positions": [],
|
||||
"trades": [],
|
||||
"monitor_lines": {},
|
||||
"options_snapshot": {
|
||||
"ok": True,
|
||||
"enabled": True,
|
||||
"positions": [
|
||||
{
|
||||
"inst_id": "ETH-USD-260723-3500-C",
|
||||
"opt_type": "C",
|
||||
"pos": 1,
|
||||
"premium_paid": 8.5,
|
||||
"source_label": "纯期权",
|
||||
"source": "option",
|
||||
"upl": 1.25,
|
||||
"target_monitor_text": "目标 3600",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class HubAiOptionsPlaybookTests(unittest.TestCase):
|
||||
def test_chat_slim_includes_options_line(self):
|
||||
text = format_chat_context_slim(_sample_payload())
|
||||
self.assertIn("期权 ETH-USD-260723-3500-C Call", text)
|
||||
self.assertIn("永续0/期权1", text)
|
||||
self.assertIn("权利金8.5U", text)
|
||||
|
||||
def test_overview_lists_options(self):
|
||||
text = format_chat_position_overview(_sample_payload())
|
||||
self.assertIn("期权1", text)
|
||||
self.assertIn("ETH-USD-260723-3500-C", text)
|
||||
|
||||
def test_chat_bundle_keeps_options(self):
|
||||
text = format_chat_context_for_chat(_sample_payload(), max_chars=8000)
|
||||
self.assertIn("ETH-USD-260723-3500-C", text)
|
||||
self.assertIn("期权", text)
|
||||
|
||||
def test_playbook_brief_injected(self):
|
||||
brief = format_playbook_brief_for_chat()
|
||||
self.assertIn("执行手册", brief)
|
||||
self.assertIn("OKX", brief)
|
||||
self.assertIn("不手动平仓", brief)
|
||||
prompt = build_chat_user_prompt(
|
||||
context_text="快照",
|
||||
trading_day="2026-07-22",
|
||||
summary_excerpt="",
|
||||
user_message="今天怎么样",
|
||||
playbook_brief=brief,
|
||||
)
|
||||
self.assertIn("用户策略执行手册", prompt)
|
||||
self.assertIn("不手动平仓", prompt)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,10 +12,19 @@ from lib.hub.hub_strategy_lib import (
|
||||
|
||||
|
||||
class TestHubStrategyLib(unittest.TestCase):
|
||||
def test_meta_has_three_exchanges(self):
|
||||
def test_meta_has_playbook_and_exchanges(self):
|
||||
meta = strategy_meta_payload()
|
||||
keys = [x["key"] for x in meta["exchanges"]]
|
||||
self.assertEqual(keys, ["binance", "okx", "gate"])
|
||||
self.assertEqual(keys, ["playbook", "binance", "okx", "gate"])
|
||||
|
||||
def test_load_playbook_payload(self):
|
||||
p = load_strategy_payload("playbook")
|
||||
self.assertTrue(p["ok"])
|
||||
self.assertEqual(p["label"], "执行手册")
|
||||
self.assertIn("交易执行手册", p["md_source"])
|
||||
self.assertIn("strategy_html", p)
|
||||
self.assertIn("<h2", p["strategy_html"].lower())
|
||||
self.assertIn("总原则", p["strategy_html"])
|
||||
|
||||
def test_load_binance_payload(self):
|
||||
p = load_strategy_payload("binance")
|
||||
|
||||
Reference in New Issue
Block a user