Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7216428ab | |||
| 77f66bf200 | |||
| 488b931959 | |||
| b89cba3b6e | |||
| e7f8e9201e | |||
| 301a464f29 | |||
| a4be294c06 |
@@ -158,6 +158,8 @@ RISK_CONTROL_ENABLED=true
|
|||||||
RISK_COOLING_HOURS_MANUAL=4
|
RISK_COOLING_HOURS_MANUAL=4
|
||||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||||
|
# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用
|
||||||
|
RISK_DAILY_LOSS_LIMIT=2
|
||||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||||
|
|
||||||
# 资金与仓位刷新周期(秒)
|
# 资金与仓位刷新周期(秒)
|
||||||
|
|||||||
@@ -2751,6 +2751,17 @@ def insert_trade_record(
|
|||||||
opened_at_ms=open_ts_ms,
|
opened_at_ms=open_ts_ms,
|
||||||
closed_at_ms=close_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
|
return tid
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ RISK_CONTROL_ENABLED=true
|
|||||||
RISK_COOLING_HOURS_MANUAL=4
|
RISK_COOLING_HOURS_MANUAL=4
|
||||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||||
|
# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用
|
||||||
|
RISK_DAILY_LOSS_LIMIT=2
|
||||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||||
|
|
||||||
# 资金与仓位刷新周期(秒)
|
# 资金与仓位刷新周期(秒)
|
||||||
|
|||||||
@@ -2445,6 +2445,17 @@ def insert_trade_record(
|
|||||||
sync_trade_records_from_exchange(conn, force=False)
|
sync_trade_records_from_exchange(conn, force=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return tid
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ RISK_CONTROL_ENABLED=true
|
|||||||
RISK_COOLING_HOURS_MANUAL=4
|
RISK_COOLING_HOURS_MANUAL=4
|
||||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||||
|
# 日亏损次数上限:平仓盈亏<0 计1次;达限当日冻结开仓;0=不启用
|
||||||
|
RISK_DAILY_LOSS_LIMIT=2
|
||||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||||
|
|
||||||
# 资金与仓位刷新周期(秒)
|
# 资金与仓位刷新周期(秒)
|
||||||
|
|||||||
@@ -2364,6 +2364,17 @@ def insert_trade_record(
|
|||||||
sync_trade_records_from_exchange(conn, force=False)
|
sync_trade_records_from_exchange(conn, force=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return tid
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 第 1 次用户主动平仓 | 默认 **4h** 冷静期 |
|
| 第 1 次用户主动平仓 | 默认 **4h** 冷静期 |
|
||||||
| 第 2 次用户主动平仓(同一交易日) | **日冻结** |
|
| 第 2 次用户主动平仓(同一交易日) | **日冻结** |
|
||||||
|
| 平仓亏损达 `RISK_DAILY_LOSS_LIMIT` 次(同一交易日) | **日冻结**(默认 2 次;`0`=不启用) |
|
||||||
| 复盘勾选任意情绪标签 | **日冻结** |
|
| 复盘勾选任意情绪标签 | **日冻结** |
|
||||||
| 复盘:离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**(须处于 4h 档冷静期中) |
|
| 复盘:离场=手动平仓 且说明非空 | 将当前冷静期降为 **1h**(须处于 4h 档冷静期中) |
|
||||||
|
|
||||||
@@ -77,11 +78,15 @@ RISK_CONTROL_ENABLED=true
|
|||||||
RISK_COOLING_HOURS_MANUAL=4
|
RISK_COOLING_HOURS_MANUAL=4
|
||||||
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
RISK_COOLING_HOURS_MANUAL_JOURNAL=1
|
||||||
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
RISK_MANUAL_CLOSE_DAILY_LIMIT=2
|
||||||
|
RISK_DAILY_LOSS_LIMIT=2
|
||||||
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
RISK_MOOD_ISSUES_DAILY_FREEZE=true
|
||||||
TRADING_DAY_RESET_HOUR=8
|
TRADING_DAY_RESET_HOUR=8
|
||||||
APP_TIMEZONE=Asia/Shanghai
|
APP_TIMEZONE=Asia/Shanghai
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `RISK_DAILY_LOSS_LIMIT`:任意已平仓交易若盈亏 < 0 计 1 次(含止损/止盈后仍亏损等);达上限当日冻结开仓;`0` 表示不因亏损次数冻结.
|
||||||
|
- `RISK_MANUAL_CLOSE_DAILY_LIMIT`:仅计**用户主动平仓**次数(与亏损次数独立).
|
||||||
|
|
||||||
`RISK_COOLING_HOURS_EXTERNAL` 已废弃(外部平仓不再触发风控).
|
`RISK_COOLING_HOURS_EXTERNAL` 已废弃(外部平仓不再触发风控).
|
||||||
|
|
||||||
## API 与 `risk_status` 字段
|
## API 与 `risk_status` 字段
|
||||||
@@ -102,6 +107,7 @@ APP_TIMEZONE=Asia/Shanghai
|
|||||||
| `can_trade` | 是否允许新开仓(仅风控维度) |
|
| `can_trade` | 是否允许新开仓(仅风控维度) |
|
||||||
| `reason` | 悬停提示文案 |
|
| `reason` | 悬停提示文案 |
|
||||||
| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env` 中 `MAX_ACTIVE_POSITIONS` |
|
| `active_count` / `max_active_positions` | 当前活跃持仓与 `.env` 中 `MAX_ACTIVE_POSITIONS` |
|
||||||
|
| `daily_loss_count` / `daily_loss_limit` | 当日亏损笔数与上限(`0` 上限表示未启用) |
|
||||||
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
|
| `cooloff_until_ms` | 1h/4h 冷静期结束时间戳(毫秒) |
|
||||||
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
| `freeze_until_ms` | 倒计时结束时间戳(日冻结为下一交易日切点) |
|
||||||
| `freeze_remaining_sec` | 服务端计算的剩余秒数(供调试) |
|
| `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`
|
- `hub_bridge.py` — `/api/hub/account-risk/user-close`
|
||||||
- `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close
|
- `manual_trading_hub/hub.py` — 中控平仓成功后调用 user-close
|
||||||
- `strategy_trend_register.py` — `stop_trend_pullback` 结束计划时登记风控
|
- `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).
|
详见 [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 讨论稿 |
|
| [binance-alt-trend-long.md](./binance-alt-trend-long.md) | 币安山寨·多头趋势 | v0.4 讨论稿 |
|
||||||
| [okx-trend-both.md](./okx-trend-both.md) | OKX·多空趋势 | v0.4 讨论稿 |
|
| [okx-trend-both.md](./okx-trend-both.md) | OKX·多空趋势 | v0.4 讨论稿 |
|
||||||
| [gate-intraday.md](./gate-intraday.md) | Gate·BTC 日内 | v0.2 |
|
| [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,8 @@
|
|||||||
|
|
||||||
| 标签 | 指向提交 | 说明 |
|
| 标签 | 指向提交 | 说明 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| `snapshot/20260721` | `2a60d47` | 2026-07-21:仓库代码统计文档、期权复盘亮色主题、对冲腿盈亏时区修复、本快照说明等 |
|
| `snapshot/20260721-2` | `77f66bf` | 2026-07-21 晚:日亏损次数冻结、交易执行手册入中控策略说明、期权/Gate 执行手册文档等 |
|
||||||
|
| `snapshot/20260721` | `1a163c0` | 2026-07-21:仓库代码统计文档、期权复盘亮色主题、对冲腿盈亏时区修复、本快照说明等 |
|
||||||
|
|
||||||
## 历史标签(节选)
|
## 历史标签(节选)
|
||||||
|
|
||||||
@@ -25,10 +26,11 @@
|
|||||||
git tag -l 'snapshot/*'
|
git tag -l 'snapshot/*'
|
||||||
|
|
||||||
# 检出快照(只读查看,勿在此分支直接开发)
|
# 检出快照(只读查看,勿在此分支直接开发)
|
||||||
git checkout snapshot/20260721
|
git checkout snapshot/20260721-2
|
||||||
|
|
||||||
# 回到主线
|
# 回到主线
|
||||||
git checkout main
|
git checkout main
|
||||||
```
|
```
|
||||||
|
|
||||||
数据备份(SQLite / 中控 JSON)走中控备份或各所 `scripts/backup_data.sh`,**不要**把含密钥的 `.env` 与库文件提交进 Git。
|
数据备份(SQLite / 中控 JSON)走中控备份或各所 `scripts/backup_data.sh`,**不要**把含密钥的 `.env` 与库文件提交进 Git。
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -58,6 +58,7 @@ HOT_RELOAD_EXACT = frozenset({
|
|||||||
"RISK_COOLING_HOURS_MANUAL",
|
"RISK_COOLING_HOURS_MANUAL",
|
||||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL",
|
"RISK_COOLING_HOURS_MANUAL_JOURNAL",
|
||||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT",
|
"RISK_MANUAL_CLOSE_DAILY_LIMIT",
|
||||||
|
"RISK_DAILY_LOSS_LIMIT",
|
||||||
"RISK_MOOD_ISSUES_DAILY_FREEZE",
|
"RISK_MOOD_ISSUES_DAILY_FREEZE",
|
||||||
"KEY_AUTO_ORDER_ENABLED",
|
"KEY_AUTO_ORDER_ENABLED",
|
||||||
"TRADE_DIRECTION_RESTRICT_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", "手动平仓冷静(小时)", ""),
|
||||||
("RISK_COOLING_HOURS_MANUAL_JOURNAL", "复盘情绪冷静(小时)", ""),
|
("RISK_COOLING_HOURS_MANUAL_JOURNAL", "复盘情绪冷静(小时)", ""),
|
||||||
("RISK_MANUAL_CLOSE_DAILY_LIMIT", "日手动平仓次数上限", ""),
|
("RISK_MANUAL_CLOSE_DAILY_LIMIT", "日手动平仓次数上限", ""),
|
||||||
|
("RISK_DAILY_LOSS_LIMIT", "日亏损次数上限", "默认2;达限当日冻结开仓;0=不因亏损次数冻结"),
|
||||||
("RISK_MOOD_ISSUES_DAILY_FREEZE", "情绪标签日冻结", ""),
|
("RISK_MOOD_ISSUES_DAILY_FREEZE", "情绪标签日冻结", ""),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -197,6 +198,7 @@ _RUNTIME_ENV_DEFAULTS: dict[str, str] = {
|
|||||||
"RISK_COOLING_HOURS_MANUAL": "4",
|
"RISK_COOLING_HOURS_MANUAL": "4",
|
||||||
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
|
"RISK_COOLING_HOURS_MANUAL_JOURNAL": "1",
|
||||||
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
"RISK_MANUAL_CLOSE_DAILY_LIMIT": "2",
|
||||||
|
"RISK_DAILY_LOSS_LIMIT": "2",
|
||||||
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
"RISK_MOOD_ISSUES_DAILY_FREEZE": "true",
|
||||||
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
"HEDGE_PLAN_SHOW_PERP_OPTIONS": "true",
|
||||||
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
"HEDGE_PLAN_SHOW_OPTIONS_OPTIONS": "true",
|
||||||
|
|||||||
@@ -10,9 +10,15 @@ from typing import Any
|
|||||||
|
|
||||||
from lib.paths import REPO_ROOT
|
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]] = {
|
STRATEGY_META: dict[str, dict[str, str]] = {
|
||||||
|
"playbook": {
|
||||||
|
"label": "执行手册",
|
||||||
|
"title": "交易执行手册(期权为主 · Gate 为辅)",
|
||||||
|
# 相对仓库根;其余条目用 md_file 相对 docs/strategy
|
||||||
|
"md_rel": "docs/交易执行手册-期权与Gate.md",
|
||||||
|
},
|
||||||
"binance": {
|
"binance": {
|
||||||
"label": "币安",
|
"label": "币安",
|
||||||
"title": "币安·山寨多头趋势",
|
"title": "币安·山寨多头趋势",
|
||||||
@@ -43,6 +49,9 @@ def _md_path(exchange_key: str) -> Path:
|
|||||||
meta = STRATEGY_META.get((exchange_key or "").strip().lower())
|
meta = STRATEGY_META.get((exchange_key or "").strip().lower())
|
||||||
if not meta:
|
if not meta:
|
||||||
raise KeyError(exchange_key)
|
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"]
|
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 (
|
from lib.trade.account_risk_lib import (
|
||||||
cooling_hours_manual,
|
cooling_hours_manual,
|
||||||
cooling_hours_manual_journal,
|
cooling_hours_manual_journal,
|
||||||
|
daily_loss_limit,
|
||||||
manual_close_daily_limit,
|
manual_close_daily_limit,
|
||||||
max_active_positions_from_env,
|
max_active_positions_from_env,
|
||||||
mood_issues_daily_freeze_enabled,
|
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():g} 小时"),
|
||||||
_row("复盘后冷静", f"{cooling_hours_manual_journal():g} 小时", "手动平仓且填写说明后可缩短"),
|
_row("复盘后冷静", f"{cooling_hours_manual_journal():g} 小时", "手动平仓且填写说明后可缩短"),
|
||||||
_row("日手动平仓上限", f"{manual_close_daily_limit()} 次", "超限当日冻结"),
|
_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(
|
_row(
|
||||||
"复盘情绪日冻结",
|
"复盘情绪日冻结",
|
||||||
_on_off(mood_issues_daily_freeze_enabled()),
|
_on_off(mood_issues_daily_freeze_enabled()),
|
||||||
|
|||||||
@@ -86,6 +86,14 @@ def manual_close_daily_limit() -> int:
|
|||||||
return 2
|
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:
|
def max_active_positions_from_env(default: int = 1) -> int:
|
||||||
try:
|
try:
|
||||||
return max(1, int(os.getenv("MAX_ACTIVE_POSITIONS", str(default))))
|
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),
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
trading_day TEXT,
|
trading_day TEXT,
|
||||||
manual_close_count INTEGER DEFAULT 0,
|
manual_close_count INTEGER DEFAULT 0,
|
||||||
|
daily_loss_count INTEGER DEFAULT 0,
|
||||||
cooloff_until_ms INTEGER,
|
cooloff_until_ms INTEGER,
|
||||||
cooloff_hours INTEGER,
|
cooloff_hours INTEGER,
|
||||||
daily_frozen INTEGER DEFAULT 0,
|
daily_frozen INTEGER DEFAULT 0,
|
||||||
@@ -124,10 +133,18 @@ def ensure_account_risk_schema(conn) -> None:
|
|||||||
updated_at TEXT
|
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()
|
row = conn.execute("SELECT id FROM account_risk_state WHERE id=1").fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
conn.execute(
|
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
|
"""UPDATE account_risk_state SET
|
||||||
trading_day=?,
|
trading_day=?,
|
||||||
manual_close_count=0,
|
manual_close_count=0,
|
||||||
|
daily_loss_count=0,
|
||||||
daily_frozen=0,
|
daily_frozen=0,
|
||||||
cooloff_until_ms=?,
|
cooloff_until_ms=?,
|
||||||
cooloff_hours=?,
|
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(
|
def on_journal_saved(
|
||||||
conn,
|
conn,
|
||||||
*,
|
*,
|
||||||
@@ -762,6 +817,7 @@ def compute_account_risk_status(
|
|||||||
"cooloff_until_ms": None,
|
"cooloff_until_ms": None,
|
||||||
"cooloff_until": None,
|
"cooloff_until": None,
|
||||||
"manual_close_count": 0,
|
"manual_close_count": 0,
|
||||||
|
"daily_loss_count": 0,
|
||||||
"daily_frozen": False,
|
"daily_frozen": False,
|
||||||
}
|
}
|
||||||
row = _sync_trading_day(conn, trading_day, now=now)
|
row = _sync_trading_day(conn, trading_day, now=now)
|
||||||
@@ -784,12 +840,21 @@ def compute_account_risk_status(
|
|||||||
row = _load_state(conn)
|
row = _load_state(conn)
|
||||||
cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms)
|
cooloff_until_ms = _resolved_cooloff_until_ms(row, now_ms)
|
||||||
manual_close_count = int(_row_get(row, "manual_close_count") or 0)
|
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
|
status = STATUS_NORMAL
|
||||||
reason = ""
|
reason = ""
|
||||||
if daily_frozen:
|
if daily_frozen:
|
||||||
status = STATUS_DAILY
|
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:
|
elif cooloff_until_ms is not None:
|
||||||
remaining_ms = cooloff_until_ms - now_ms
|
remaining_ms = cooloff_until_ms - now_ms
|
||||||
hours = _cooloff_hours_value(row)
|
hours = _cooloff_hours_value(row)
|
||||||
@@ -818,6 +883,8 @@ def compute_account_risk_status(
|
|||||||
if fmt_local_ms and cooloff_until_ms
|
if fmt_local_ms and cooloff_until_ms
|
||||||
else None,
|
else None,
|
||||||
"manual_close_count": manual_close_count,
|
"manual_close_count": manual_close_count,
|
||||||
|
"daily_loss_count": daily_loss_count,
|
||||||
|
"daily_loss_limit": loss_limit,
|
||||||
"daily_frozen": daily_frozen,
|
"daily_frozen": daily_frozen,
|
||||||
"pending_journal_trade_id": pending,
|
"pending_journal_trade_id": pending,
|
||||||
"freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0,
|
"freeze_remaining_sec": freeze_remaining_sec if not can_trade else 0,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| **资金概况** | 总资金曲线、分户权益、回撤与 24h 变化 |
|
| **资金概况** | 总资金曲线、分户权益、回撤与 24h 变化 |
|
||||||
| **开仓计划** | 事前写下计划、跟踪进行中、统计历史胜率 |
|
| **开仓计划** | 事前写下计划、跟踪进行中、统计历史胜率 |
|
||||||
| **监控区** | **核心操作台**:三所持仓卡片、全平/撤单、关键位与趋势计划摘要 |
|
| **监控区** | **核心操作台**:三所持仓卡片、全平/撤单、关键位与趋势计划摘要 |
|
||||||
| **策略说明** | 三所策略 playbook + 开仓检查清单(非系统操作手册) |
|
| **策略说明** | 执行手册 + 三所策略 playbook + 开仓检查清单(非系统操作手册) |
|
||||||
| **使用说明** | 本页:中控与实例怎么用 |
|
| **使用说明** | 本页:中控与实例怎么用 |
|
||||||
| **行情区** | K 线、指标、画线;可从持仓跳转带币种 |
|
| **行情区** | K 线、指标、画线;可从持仓跳转带币种 |
|
||||||
| **计算器** | 趋势回调 / 滚仓张数与盈亏测算(手动填价) |
|
| **计算器** | 趋势回调 / 滚仓张数与盈亏测算(手动填价) |
|
||||||
|
|||||||
@@ -960,7 +960,7 @@
|
|||||||
<div class="page-head strategy-page-head">
|
<div class="page-head strategy-page-head">
|
||||||
<div>
|
<div>
|
||||||
<h1><span class="head-tag">STR</span> 策略说明</h1>
|
<h1><span class="head-tag">STR</span> 策略说明</h1>
|
||||||
<p class="page-desc">策略正文(带目录) · 执行清单(打印对照) · 三所切换</p>
|
<p class="page-desc">执行手册 · 三所策略正文(带目录) · 执行清单(打印对照)</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="strategy-page-actions no-print">
|
<div class="strategy-page-actions no-print">
|
||||||
<button type="button" id="strategy-btn-download" class="ghost">下载 HTML</button>
|
<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/quotes.js?v=20260717-quotes-feed"></script>
|
||||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></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/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/help.js?v=1"></script>
|
||||||
<script src="/assets/logs.js?v=1"></script>
|
<script src="/assets/logs.js?v=1"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=3"></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 btnPrintChecklistInline = document.getElementById("strategy-btn-print-checklist-inline");
|
||||||
const btnDownload = document.getElementById("strategy-btn-download");
|
const btnDownload = document.getElementById("strategy-btn-download");
|
||||||
|
|
||||||
let activeKey = "binance";
|
let activeKey = "playbook";
|
||||||
let activeView = "doc";
|
let activeView = "doc";
|
||||||
let tabsMeta = [];
|
let tabsMeta = [];
|
||||||
let cache = {};
|
let cache = {};
|
||||||
@@ -128,13 +128,14 @@
|
|||||||
|
|
||||||
function sectionTag(title) {
|
function sectionTag(title) {
|
||||||
const t = String(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 "系统";
|
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,
|
enrich_risk_status_countdown,
|
||||||
ensure_account_risk_schema,
|
ensure_account_risk_schema,
|
||||||
max_active_positions_from_env,
|
max_active_positions_from_env,
|
||||||
|
on_closed_trade_pnl,
|
||||||
on_journal_saved,
|
on_journal_saved,
|
||||||
on_manual_close,
|
on_manual_close,
|
||||||
on_user_initiated_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"] = "4"
|
||||||
os.environ["RISK_COOLING_HOURS_MANUAL_JOURNAL"] = "1"
|
os.environ["RISK_COOLING_HOURS_MANUAL_JOURNAL"] = "1"
|
||||||
os.environ["RISK_MANUAL_CLOSE_DAILY_LIMIT"] = "2"
|
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["RISK_MOOD_ISSUES_DAILY_FREEZE"] = "1"
|
||||||
os.environ["APP_TIMEZONE"] = "Asia/Shanghai"
|
os.environ["APP_TIMEZONE"] = "Asia/Shanghai"
|
||||||
|
|
||||||
@@ -521,6 +523,41 @@ class AccountRiskLibTests(unittest.TestCase):
|
|||||||
os.environ["MAX_ACTIVE_POSITIONS"] = "3"
|
os.environ["MAX_ACTIVE_POSITIONS"] = "3"
|
||||||
self.assertEqual(max_active_positions_from_env(), 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -12,10 +12,19 @@ from lib.hub.hub_strategy_lib import (
|
|||||||
|
|
||||||
|
|
||||||
class TestHubStrategyLib(unittest.TestCase):
|
class TestHubStrategyLib(unittest.TestCase):
|
||||||
def test_meta_has_three_exchanges(self):
|
def test_meta_has_playbook_and_exchanges(self):
|
||||||
meta = strategy_meta_payload()
|
meta = strategy_meta_payload()
|
||||||
keys = [x["key"] for x in meta["exchanges"]]
|
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):
|
def test_load_binance_payload(self):
|
||||||
p = load_strategy_payload("binance")
|
p = load_strategy_payload("binance")
|
||||||
|
|||||||
Reference in New Issue
Block a user