Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 26bc19f047 | |||
| c81ba147cc | |||
| 90be23e845 | |||
| 2ce67da8e8 | |||
| d049c5d317 | |||
| 845884fc67 |
+4
-3
@@ -6,10 +6,11 @@
|
|||||||
|
|
||||||
| 标签 | 指向提交 | 说明 |
|
| 标签 | 指向提交 | 说明 |
|
||||||
|------|----------|------|
|
|------|----------|------|
|
||||||
| `snapshot/20260727` | `f53f281` | 2026-07-27:实例手机壳(下单/关键位/期权)、著作权声明、托管合同(一用户一机)、服务说明与报价说明 |
|
| `snapshot/20260728` | `c73e363` | 2026-07-28:中控永期对冲计算器(由波动推仓位 / 由比例推点数)、说明文档 |
|
||||||
|
| `snapshot/20260727` | `f53f281` | 2026-07-27:实例手机壳(下单/持仓/期权)、著作权声明、托管合同(一用户一机)、服务说明与报价说明 |
|
||||||
| `snapshot/20260726-2` | `4a79e01` | 2026-07-26 午:执行手册脑图(业务主题)、`.xmind` 按二进制入库、去掉缩略图避免 Gitea raw 换行损坏 |
|
| `snapshot/20260726-2` | `4a79e01` | 2026-07-26 午:执行手册脑图(业务主题)、`.xmind` 按二进制入库、去掉缩略图避免 Gitea raw 换行损坏 |
|
||||||
| `snapshot/20260726` | `a2075ba` | 2026-07-26:Gate划转币种大写修复、系统设置划转页签停留、自动划转账户/币种下拉默认、期权「按可用余额打满」=min(余额,单笔预算)及说明 |
|
| `snapshot/20260726` | `a2075ba` | 2026-07-26:Gate划转币种大写修复、系统设置划转页签停留、自动划转账户/币种下拉默认、期权「按可用余额打满」=min(余额,单笔预算)及说明 |
|
||||||
| `snapshot/20260724` | `890659f` | 2026-07-24:执行手册v2(无对冲)、监控/策略页签显隐、内照明心期权档案同步、期权开平仓微信必发、实例导航显隐关键位/实盘下单等 |
|
| `snapshot/20260724` | `890659f` | 2026-07-24:执行手册v2(无对冲)、监控/策略页签显隐、内照明心期权档案同步、期权开平仓微信必发、实例导航显隐持仓/实盘下单等 |
|
||||||
| `snapshot/20260723-2` | `9e0591c` | 2026-07-23:策略对比页(合约/单期权/期期7:3)、监控与看板隐藏浮盈偏好、对比页卡片内边距等 |
|
| `snapshot/20260723-2` | `9e0591c` | 2026-07-23:策略对比页(合约/单期权/期期7:3)、监控与看板隐藏浮盈偏好、对比页卡片内边距等 |
|
||||||
| `snapshot/20260723-pre-amp-stats` | `40be3a5` | 2026-07-23:振幅统计开发前;含执行手册进教练、日亏损冻结、手机监控 UI、振幅统计开发方案等 |
|
| `snapshot/20260723-pre-amp-stats` | `40be3a5` | 2026-07-23:振幅统计开发前;含执行手册进教练、日亏损冻结、手机监控 UI、振幅统计开发方案等 |
|
||||||
| `snapshot/20260721-2` | `a721642` | 2026-07-21 晚:日亏损次数冻结、交易执行手册入中控策略说明、期权/Gate 执行手册文档等 |
|
| `snapshot/20260721-2` | `a721642` | 2026-07-21 晚:日亏损次数冻结、交易执行手册入中控策略说明、期权/Gate 执行手册文档等 |
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
git tag -l 'snapshot/*'
|
git tag -l 'snapshot/*'
|
||||||
|
|
||||||
# 检出快照(只读查看,勿在此分支直接开发)
|
# 检出快照(只读查看,勿在此分支直接开发)
|
||||||
git checkout snapshot/20260727
|
git checkout snapshot/20260728
|
||||||
|
|
||||||
# 回到主线
|
# 回到主线
|
||||||
git checkout main
|
git checkout main
|
||||||
|
|||||||
+25
-23
@@ -3,7 +3,8 @@
|
|||||||
中控只读工具:按自定义整点起点、**固定北京时间 16:00 收窗**,统计 OKX 上 ETH/BTC 的历史「点数振幅」档案,辅助一天期期权判断空间。
|
中控只读工具:按自定义整点起点、**固定北京时间 16:00 收窗**,统计 OKX 上 ETH/BTC 的历史「点数振幅」档案,辅助一天期期权判断空间。
|
||||||
|
|
||||||
> 开发方案见 [ETH时段振幅统计-开发方案.md](./ETH时段振幅统计-开发方案.md)。
|
> 开发方案见 [ETH时段振幅统计-开发方案.md](./ETH时段振幅统计-开发方案.md)。
|
||||||
> **不改下单链路**;不算 IV / 权利金。
|
> **不改下单链路**;不算 IV。
|
||||||
|
> 买跨 / 永期对冲测算请用中控 **策略计算器**,本页不再做对照盈亏。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,8 +22,9 @@
|
|||||||
2. 选择 **标的** ETH / BTC;数据源固定 **OKX**
|
2. 选择 **标的** ETH / BTC;数据源固定 **OKX**
|
||||||
3. **起点整点**(00–23);终点固定 **16:00**
|
3. **起点整点**(00–23);终点固定 **16:00**
|
||||||
4. **周期**:1 月 / 2 月 / 3 月 / 半年 / 1 年 / 自定义天数(默认 2 个月)
|
4. **周期**:1 月 / 2 月 / 3 月 / 半年 / 1 年 / 自定义天数(默认 2 个月)
|
||||||
5. 点 **计算** → 下方看汇总 + 分页日表
|
5. 可选填 **波动点数**(如 `50`)→ 看振幅达标占比
|
||||||
6. 需要留存时点 **保存到历史**;**下载 CSV** 含摘要 + 全日明细
|
6. 点 **计算** → 下方看汇总 + 振幅占比 + 分页日表
|
||||||
|
7. 需要留存时点 **保存到历史**;**下载 CSV** 含摘要 + 全日明细
|
||||||
|
|
||||||
**跨天例子**
|
**跨天例子**
|
||||||
|
|
||||||
@@ -42,14 +44,15 @@
|
|||||||
|
|
||||||
| 字段 | 算法 |
|
| 字段 | 算法 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 开→高 | `H − O` |
|
| 开→高 | `H − O`(一边波动) |
|
||||||
| 开→低 | `O − L` |
|
| 开→低 | `O − L`(另一边波动) |
|
||||||
| **振幅** | `H − L`(= 开→高 + 开→低) |
|
| **振幅** | `H − L`(= 开→高 + 开→低),窗为起点整点 → 当日 16:00 |
|
||||||
| 涨跌值 | `C − O` |
|
| **两日振幅** | 同上口径,但起点再往前推 1 天;例起点 16:00、结算 27 日 → **25日16:00 → 27日16:00** |
|
||||||
|
| 涨跌值 | `C − O`(单日窗) |
|
||||||
|
|
||||||
例:O=2000,H=2500,L=1800 → 开→高 500,开→低 200,振幅 **700**。
|
例:O=2000,H=2500,L=1800 → 开→高 500,开→低 200,振幅 **700**。
|
||||||
|
|
||||||
汇总必含:最大振幅(及日期)、开→高/开→低的最大与均值等。
|
汇总必含:最大振幅(及日期)、两日振幅最大/均值/中位、开→高/开→低的最大与均值等。
|
||||||
|
|
||||||
K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / BTC-USD),失败再降级永续标记。
|
K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / BTC-USD),失败再降级永续标记。
|
||||||
近期 K 线接口约仅 **1440** 根(1H≈60 天);更长周期自动续拉 `history-index-candles` / `history-candles`。
|
近期 K 线接口约仅 **1440** 根(1H≈60 天);更长周期自动续拉 `history-index-candles` / `history-candles`。
|
||||||
@@ -57,22 +60,20 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 买跨对照(赌波动)
|
## 波动点数 → 振幅占比
|
||||||
|
|
||||||
表单可填 **双边权利金(点)**,例如 `30`;旁边可填 **止盈点**(可空):
|
表单可填 **波动点数**(如 `50`)。填写后下方 **振幅占比** 块显示:
|
||||||
|
|
||||||
| 汇总项 | 口径 |
|
| 汇总项 | 口径 |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| 开→高超过权利金 | `H−O > 权利金` 的天数与占比 |
|
| 振幅≥点数 | 单日窗 `H−L ≥ 点数` 的天数与**占比**(主指标) |
|
||||||
| 开→低超过权利金 | `O−L > 权利金` 的天数与占比 |
|
| 两日振幅≥点数 | 两日窗振幅 ≥ 点数 的天数与占比 |
|
||||||
| \|涨跌\|超过权利金 | `\|C−O\| > 权利金` 的天数与占比 |
|
| 开→高≥点数 | `H−O ≥ 点数` 天数与占比 |
|
||||||
| 有效波动 | 若设止盈且 `开→高≥止盈` 或 `开→低≥止盈` → 用止盈点;否则用 `\|C−O\|` |
|
| 开→低≥点数 | `O−L ≥ 点数` 天数与占比 |
|
||||||
| 买跨收益 | `有效波动 − 权利金`(日表「收益」列同口径) |
|
| \|涨跌\|≥点数 | `\|C−O\| ≥ 点数` 天数与占比 |
|
||||||
|
|
||||||
- 方向:**买跨**
|
日表保留 **开→高 / 开→低**、**振幅**、**两日振幅**(悬停可见两日窗起止),并标 **振幅达标**。
|
||||||
- 权利金越过:严格 **`>`**;止盈触达:**`≥`**
|
改点数 / 周末筛选会在已有日表上**本地重算**(不重拉 K 线)。
|
||||||
- 止盈留空 / ≤0:有效波动一律按 `|涨跌|`
|
|
||||||
- 已算出日表后,改权利金 / 止盈 / 周末筛选会**本地重算**(不重拉 K 线)
|
|
||||||
|
|
||||||
### 周末
|
### 周末
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
| `manual_trading_hub/amp_stats_routes.py` | API |
|
| `manual_trading_hub/amp_stats_routes.py` | API |
|
||||||
| `manual_trading_hub/amp_stats_store.py` | 历史 JSON |
|
| `manual_trading_hub/amp_stats_store.py` | 历史 JSON |
|
||||||
| `manual_trading_hub/static/amp_stats.js` | 前端 |
|
| `manual_trading_hub/static/amp_stats.js` | 前端 |
|
||||||
| `tests/test_amp_stats_lib.py` | 单元测试 |
|
| `tests/test_amp_stats_lib.py` | 单测 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
| 日期 | 说明 |
|
| 日期 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 2026-07-23 | 首版上线说明 |
|
| 2026-07-23 | 首版上线说明 |
|
||||||
| 2026-07-23 | 买跨对照:可设双边权利金、越过占比与收盘盈亏 |
|
| 2026-07-23 | 买跨对照、周末筛选、止盈点 |
|
||||||
| 2026-07-23 | 周末筛选/标注、止盈点(≥)、日表收益列 |
|
| 2026-07-28 | 永期对冲对照(后已移除) |
|
||||||
| 2026-07-23 | 长周期续拉 history K 线;收益列红绿着色 |
|
| 2026-07-28 | 去掉买跨/永期;改为波动点数→振幅占比 |
|
||||||
|
| 2026-07-28 | 增加两日振幅(例 25日16:00→27日16:00) |
|
||||||
|
|||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
# 永期对冲计算器
|
||||||
|
|
||||||
|
中控 **策略计算器** 第 3 个 tab:永期对冲。用于离线测算「永续 1 币 + 买方期权」在目标盈利口径下的期权仓位,或按永续:期权比例反推达目标所需波动点数。
|
||||||
|
|
||||||
|
入口:中控 → 策略计算器 → **永期对冲**。
|
||||||
|
API:`POST /api/calculator/perp-options`
|
||||||
|
逻辑库:`lib/hub/hub_perp_options_calc_lib.py`
|
||||||
|
单测:`tests/test_hub_perp_options_calc_lib.py`
|
||||||
|
|
||||||
|
与实例页「对冲计划」不同:本页**不实盘下单、不拉期权链**,价格与杠杆均为手填。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 共同假设
|
||||||
|
|
||||||
|
| 项 | 口径 |
|
||||||
|
|----|------|
|
||||||
|
| 品种 | BTC / ETH |
|
||||||
|
| 永续仓位 | 固定 **1 币** |
|
||||||
|
| 单币权利金 | `现价 / 期权杠杆`(例:1800÷100=18U) |
|
||||||
|
| 权利金 | **按全亏**计入;忽略时间价值 / Theta |
|
||||||
|
| 永续手续费 | 开+平各 `0.05%`(`PERP_TAKER_FEE_RATE`,默认 0.0005) |
|
||||||
|
| 期权手续费 | **不算** |
|
||||||
|
| 交易资金 | 仅参考:与 `现价/永续杠杆` 比保证金是否够开 |
|
||||||
|
| `ct_mult` | 默认 0.01;张数 = 期权币数 / ct_mult |
|
||||||
|
| 展示 | 金额与点数统一 **小数点后两位** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模式一:由波动推期权仓位(`calc_mode=size`)
|
||||||
|
|
||||||
|
已知波动(点数或波动率%)、目标盈利、期权杠杆 → 反推期权开多少币/张。
|
||||||
|
|
||||||
|
### 公式
|
||||||
|
|
||||||
|
```text
|
||||||
|
单币权利金 = 现价 / 期权杠杆
|
||||||
|
永续毛收益 = 波动点数 × 1
|
||||||
|
(波动率模式:现价 × 波动率% × 1)
|
||||||
|
平仓价 ≈ 现价 + 波动点数(永续方向对按上涨测算)
|
||||||
|
永续手续费 = (开仓名义 + 平仓名义) × 0.05%
|
||||||
|
|
||||||
|
权利金预算 = 永续毛收益 − 目标盈利 − 永续手续费
|
||||||
|
期权币数 = 权利金预算 / 单币权利金
|
||||||
|
期权张数 = 期权币数 / ct_mult
|
||||||
|
```
|
||||||
|
|
||||||
|
若权利金预算 ≤ 0:提示「波动收益不足以覆盖目标盈利+手续费,无法开期权」。
|
||||||
|
|
||||||
|
### 情景
|
||||||
|
|
||||||
|
**A · 永续方向对(期权全亏)**
|
||||||
|
|
||||||
|
```text
|
||||||
|
净利 = 永续毛收益 − 权利金总额 − 永续手续费
|
||||||
|
(设计上 ≈ 目标盈利)
|
||||||
|
```
|
||||||
|
|
||||||
|
**B · 期权方向对(永续 1 币反向亏同等波动)**
|
||||||
|
|
||||||
|
```text
|
||||||
|
期权内在 = 期权币数 × 波动点数
|
||||||
|
期权净利 = 期权内在 − 权利金总额
|
||||||
|
永续亏损 = −永续毛收益
|
||||||
|
组合净利 = 期权净利 + 永续亏损
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手测示例
|
||||||
|
|
||||||
|
现价 1800、波动 50 点、目标盈利 15、期权杠杆 100、永续杠杆 10:
|
||||||
|
|
||||||
|
| 量 | 约值 |
|
||||||
|
|----|------|
|
||||||
|
| 单币权利金 | 18U |
|
||||||
|
| 永续手续费 | 1.83U |
|
||||||
|
| 权利金预算 | 33.18U |
|
||||||
|
| 期权币数 / 张数 | ≈1.84 币 / ≈184 张 |
|
||||||
|
| A 净利 | ≈15U |
|
||||||
|
| B 期权净利 / 组合 | ≈59U / ≈9U |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 模式二:由比例推波动点数(`calc_mode=points`)
|
||||||
|
|
||||||
|
已知永续:期权比例(如 **1:2**)、目标盈利、期权杠杆 → 反推两套情景要涨/跌多少点才能达到目标。
|
||||||
|
|
||||||
|
### 仓位
|
||||||
|
|
||||||
|
```text
|
||||||
|
永续币数 = 1
|
||||||
|
期权币数 = 1 × (期权比例 / 永续比例) # 1:2 → 2 币
|
||||||
|
权利金总额 = 期权币数 × (现价 / 期权杠杆)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 情景 A · 永续方向对
|
||||||
|
|
||||||
|
净利 = 目标盈利:
|
||||||
|
|
||||||
|
```text
|
||||||
|
move − 权利金 − fee(move) = 目标
|
||||||
|
fee(move) = (2×现价 + move) × 0.05%
|
||||||
|
|
||||||
|
move = (目标 + 权利金 + 2×现价×0.05%) / (1 − 0.05%)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 情景 B · 期权方向对(以组合净利为准)
|
||||||
|
|
||||||
|
组合净利 = 目标盈利:
|
||||||
|
|
||||||
|
```text
|
||||||
|
组合 = 期权币数×move − 权利金 − 1×move
|
||||||
|
= move×(期权币数 − 1) − 权利金
|
||||||
|
|
||||||
|
move = (目标 + 权利金) / (期权币数 − 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
要求期权币数 > 永续币数(比例须使期权侧更重,如 1:2);若为 1:1,组合恒为 −权利金,无法解出正目标。
|
||||||
|
|
||||||
|
结果区展示:所需波动点数(及折合%)、组合净利、其中期权净利、其中永续盈亏。
|
||||||
|
|
||||||
|
### 手测示例
|
||||||
|
|
||||||
|
现价 1800、目标 15、期权杠杆 100、比例 1:2 → 权利金总额 36U:
|
||||||
|
|
||||||
|
| 情景 | 所需点数(约) |
|
||||||
|
|------|----------------|
|
||||||
|
| A 永续方向对(净利=15) | ≈52.83 |
|
||||||
|
| B 组合净利=15 | 51.00 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 请求体(摘要)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"calc_mode": "size | points",
|
||||||
|
"base": "ETH",
|
||||||
|
"spot": 1800,
|
||||||
|
"capital_usdt": 3000,
|
||||||
|
"target_profit_u": 15,
|
||||||
|
"move_mode": "points",
|
||||||
|
"move_value": 50,
|
||||||
|
"perp_leverage": 10,
|
||||||
|
"option_leverage": 100,
|
||||||
|
"ct_mult": 0.01,
|
||||||
|
"ratio_perp": 1,
|
||||||
|
"ratio_opt": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `size` 模式必填 `move_value`;`points` 模式用 `ratio_perp` / `ratio_opt`,可不填波动。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
| 路径 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `lib/hub/hub_perp_options_calc_lib.py` | 纯函数测算 |
|
||||||
|
| `manual_trading_hub/hub.py` | `POST /api/calculator/perp-options` |
|
||||||
|
| `manual_trading_hub/static/index.html` | 计算器 tab UI |
|
||||||
|
| `manual_trading_hub/static/calculator.js` | 提交与结果渲染 |
|
||||||
|
| `lib/trade/trade_fee_lib.py` | 永续双边手续费 |
|
||||||
|
|
||||||
|
## 不做
|
||||||
|
|
||||||
|
实盘开平仓、拉 OKX 期权链卖一、把本页结果自动写入对冲计划。
|
||||||
|
|
||||||
|
振幅统计页可对历史日表做同口径对照,见 [振幅统计说明.md](./振幅统计说明.md)「永期对冲对照」。
|
||||||
+888
-833
File diff suppressed because it is too large
Load Diff
@@ -12,8 +12,7 @@ from lib.hub.amp_stats_lib import (
|
|||||||
build_export_csv,
|
build_export_csv,
|
||||||
compute_amp_stats,
|
compute_amp_stats,
|
||||||
export_filename,
|
export_filename,
|
||||||
normalize_straddle_premium,
|
normalize_move_points,
|
||||||
normalize_take_profit,
|
|
||||||
normalize_weekend_filter,
|
normalize_weekend_filter,
|
||||||
reframe_amp_stats,
|
reframe_amp_stats,
|
||||||
rows_page,
|
rows_page,
|
||||||
@@ -25,8 +24,7 @@ class ComputeBody(BaseModel):
|
|||||||
start_hour: int = 16
|
start_hour: int = 16
|
||||||
period: str = "2m"
|
period: str = "2m"
|
||||||
custom_days: Optional[int] = None
|
custom_days: Optional[int] = None
|
||||||
straddle_premium: Optional[float] = None
|
move_points: Optional[float] = None
|
||||||
take_profit: Optional[float] = None
|
|
||||||
weekend_filter: str = "all"
|
weekend_filter: str = "all"
|
||||||
page: int = 1
|
page: int = 1
|
||||||
page_size: int = 20
|
page_size: int = 20
|
||||||
@@ -37,15 +35,14 @@ class SaveBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ReframeBody(BaseModel):
|
class ReframeBody(BaseModel):
|
||||||
"""已有日表上改周末/权利金/止盈(不拉 K 线)."""
|
"""已有日表上改周末/波动点数(不拉 K 线)."""
|
||||||
|
|
||||||
rows_all: list[dict[str, Any]] = Field(default_factory=list)
|
rows_all: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
symbol: str = "eth"
|
symbol: str = "eth"
|
||||||
start_hour: int = 16
|
start_hour: int = 16
|
||||||
period: str = "2m"
|
period: str = "2m"
|
||||||
sample_days: int = 60
|
sample_days: int = 60
|
||||||
straddle_premium: Optional[float] = None
|
move_points: Optional[float] = None
|
||||||
take_profit: Optional[float] = None
|
|
||||||
weekend_filter: str = "all"
|
weekend_filter: str = "all"
|
||||||
price_source: str = ""
|
price_source: str = ""
|
||||||
inst_id: str = ""
|
inst_id: str = ""
|
||||||
@@ -84,7 +81,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
"default_weekend_filter": "all",
|
"default_weekend_filter": "all",
|
||||||
"timeframe": "1H",
|
"timeframe": "1H",
|
||||||
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
||||||
"straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金",
|
"move_points_note": "填波动点数后统计振幅≥该点数的天数占比;日表显示开→高/开→低两边点数与振幅是否达标",
|
||||||
}
|
}
|
||||||
|
|
||||||
@router.post("/compute")
|
@router.post("/compute")
|
||||||
@@ -95,8 +92,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour=body.start_hour,
|
start_hour=body.start_hour,
|
||||||
period=body.period,
|
period=body.period,
|
||||||
custom_days=body.custom_days,
|
custom_days=body.custom_days,
|
||||||
straddle_premium=body.straddle_premium,
|
move_points=body.move_points,
|
||||||
take_profit=body.take_profit,
|
|
||||||
weekend_filter=body.weekend_filter,
|
weekend_filter=body.weekend_filter,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
@@ -122,8 +118,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour=body.start_hour,
|
start_hour=body.start_hour,
|
||||||
period=body.period,
|
period=body.period,
|
||||||
sample_days=body.sample_days,
|
sample_days=body.sample_days,
|
||||||
straddle_premium=body.straddle_premium,
|
move_points=body.move_points,
|
||||||
take_profit=body.take_profit,
|
|
||||||
weekend_filter=body.weekend_filter,
|
weekend_filter=body.weekend_filter,
|
||||||
price_source=body.price_source,
|
price_source=body.price_source,
|
||||||
inst_id=body.inst_id,
|
inst_id=body.inst_id,
|
||||||
@@ -165,8 +160,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour: int = Query(default=16),
|
start_hour: int = Query(default=16),
|
||||||
period: str = Query(default="2m"),
|
period: str = Query(default="2m"),
|
||||||
custom_days: Optional[int] = Query(default=None),
|
custom_days: Optional[int] = Query(default=None),
|
||||||
straddle_premium: Optional[float] = Query(default=None),
|
move_points: Optional[float] = Query(default=None),
|
||||||
take_profit: Optional[float] = Query(default=None),
|
|
||||||
weekend_filter: str = Query(default="all"),
|
weekend_filter: str = Query(default="all"),
|
||||||
):
|
):
|
||||||
if (history_id or "").strip():
|
if (history_id or "").strip():
|
||||||
@@ -174,6 +168,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="历史不存在")
|
raise HTTPException(status_code=404, detail="历史不存在")
|
||||||
rows_all = item.get("rows_all") or item.get("rows") or []
|
rows_all = item.get("rows_all") or item.get("rows") or []
|
||||||
|
use_mp = move_points if move_points is not None else item.get("move_points")
|
||||||
try:
|
try:
|
||||||
payload = reframe_amp_stats(
|
payload = reframe_amp_stats(
|
||||||
rows_all=rows_all,
|
rows_all=rows_all,
|
||||||
@@ -181,10 +176,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour=int(item.get("start_hour") if item.get("start_hour") is not None else start_hour),
|
start_hour=int(item.get("start_hour") if item.get("start_hour") is not None else start_hour),
|
||||||
period=str(item.get("period") or period),
|
period=str(item.get("period") or period),
|
||||||
sample_days=int(item.get("sample_days_requested") or 60),
|
sample_days=int(item.get("sample_days_requested") or 60),
|
||||||
straddle_premium=straddle_premium
|
move_points=use_mp,
|
||||||
if straddle_premium is not None
|
|
||||||
else item.get("straddle_premium"),
|
|
||||||
take_profit=take_profit if take_profit is not None else item.get("take_profit"),
|
|
||||||
weekend_filter=weekend_filter or item.get("weekend_filter") or "all",
|
weekend_filter=weekend_filter or item.get("weekend_filter") or "all",
|
||||||
price_source=str(item.get("price_source") or ""),
|
price_source=str(item.get("price_source") or ""),
|
||||||
inst_id=str(item.get("inst_id") or ""),
|
inst_id=str(item.get("inst_id") or ""),
|
||||||
@@ -194,17 +186,14 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# validate enums early
|
|
||||||
normalize_weekend_filter(weekend_filter)
|
normalize_weekend_filter(weekend_filter)
|
||||||
normalize_straddle_premium(straddle_premium)
|
normalize_move_points(move_points)
|
||||||
normalize_take_profit(take_profit)
|
|
||||||
payload = compute_amp_stats(
|
payload = compute_amp_stats(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
start_hour=start_hour,
|
start_hour=start_hour,
|
||||||
period=period,
|
period=period,
|
||||||
custom_days=custom_days,
|
custom_days=custom_days,
|
||||||
straddle_premium=straddle_premium,
|
move_points=move_points,
|
||||||
take_profit=take_profit,
|
|
||||||
weekend_filter=weekend_filter,
|
weekend_filter=weekend_filter,
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 中控振幅统计:OKX ETH/BTC + 买跨/止盈/周末筛选.
|
* 中控振幅统计:OKX ETH/BTC + 波动点数振幅占比 + 周末筛选.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
const page = document.getElementById("page-amp-stats");
|
const page = document.getElementById("page-amp-stats");
|
||||||
@@ -39,16 +39,8 @@
|
|||||||
return (n * 100).toFixed(1) + "%";
|
return (n * 100).toFixed(1) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPremium() {
|
function readMovePoints() {
|
||||||
const raw = (el("amp-straddle-premium")?.value || "").trim();
|
const raw = (el("amp-move-points")?.value || "").trim();
|
||||||
if (!raw) return null;
|
|
||||||
const n = Number(raw);
|
|
||||||
if (!Number.isFinite(n) || n <= 0) return null;
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readTakeProfit() {
|
|
||||||
const raw = (el("amp-take-profit")?.value || "").trim();
|
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
if (!Number.isFinite(n) || n <= 0) return null;
|
if (!Number.isFinite(n) || n <= 0) return null;
|
||||||
@@ -95,19 +87,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pnlClass(v) {
|
|
||||||
const n = Number(v);
|
|
||||||
if (!Number.isFinite(n) || n === 0) return "";
|
|
||||||
return n > 0 ? "is-pos" : "is-neg";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSummary(summary, result) {
|
function renderSummary(summary, result) {
|
||||||
const box = el("amp-summary");
|
const box = el("amp-summary");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
const s = summary || {};
|
const s = summary || {};
|
||||||
if (!s.sample_count) {
|
if (!s.sample_count) {
|
||||||
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
||||||
renderStraddle(null);
|
renderMoveStats(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
@@ -116,44 +102,31 @@
|
|||||||
`<div><span class="amp-sum-k">最大振幅</span><span class="amp-sum-v">${esc(s.max_amplitude)} <small>(${esc(s.max_amplitude_day)})</small></span></div>` +
|
`<div><span class="amp-sum-k">最大振幅</span><span class="amp-sum-v">${esc(s.max_amplitude)} <small>(${esc(s.max_amplitude_day)})</small></span></div>` +
|
||||||
`<div><span class="amp-sum-k">振幅均值</span><span class="amp-sum-v">${esc(s.avg_amplitude)}</span></div>` +
|
`<div><span class="amp-sum-k">振幅均值</span><span class="amp-sum-v">${esc(s.avg_amplitude)}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">振幅中位</span><span class="amp-sum-v">${esc(s.median_amplitude)}</span></div>` +
|
`<div><span class="amp-sum-k">振幅中位</span><span class="amp-sum-v">${esc(s.median_amplitude)}</span></div>` +
|
||||||
|
`<div><span class="amp-sum-k">两日最大振幅</span><span class="amp-sum-v">${esc(s.max_amplitude_2d)} <small>(${esc(s.max_amplitude_2d_day)})</small></span></div>` +
|
||||||
|
`<div><span class="amp-sum-k">两日振幅均值/中位</span><span class="amp-sum-v">${esc(s.avg_amplitude_2d)} / ${esc(s.median_amplitude_2d)}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">开→高最大/均</span><span class="amp-sum-v">${esc(s.max_up_points)} / ${esc(s.avg_up_points)}</span></div>` +
|
`<div><span class="amp-sum-k">开→高最大/均</span><span class="amp-sum-v">${esc(s.max_up_points)} / ${esc(s.avg_up_points)}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">开→低最大/均</span><span class="amp-sum-v">${esc(s.max_down_points)} / ${esc(s.avg_down_points)}</span></div>` +
|
`<div><span class="amp-sum-k">开→低最大/均</span><span class="amp-sum-v">${esc(s.max_down_points)} / ${esc(s.avg_down_points)}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">涨/跌窗占比</span><span class="amp-sum-v">${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}</span></div>` +
|
`<div><span class="amp-sum-k">涨/跌窗占比</span><span class="amp-sum-v">${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">价源</span><span class="amp-sum-v">${esc(result && result.price_source)}</span></div>` +
|
`<div><span class="amp-sum-k">价源</span><span class="amp-sum-v">${esc(result && result.price_source)}</span></div>` +
|
||||||
`</div>`;
|
`</div>`;
|
||||||
renderStraddle(s.straddle);
|
renderMoveStats(s.move_points_stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStraddle(st) {
|
function renderMoveStats(ms) {
|
||||||
const box = el("amp-straddle");
|
const box = el("amp-move-stats");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
if (!st) {
|
if (!ms) {
|
||||||
box.innerHTML = '<p class="amp-empty">填写「买跨·双边权利金」后计算,可看越过天数与买跨点数盈亏</p>';
|
box.innerHTML = '<p class="amp-empty">填写「波动点数」后计算,可看振幅≥该点数的天数占比</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const verdict =
|
|
||||||
st.pnl_total == null
|
|
||||||
? "—"
|
|
||||||
: Number(st.pnl_total) > 0
|
|
||||||
? "样本合计盈利"
|
|
||||||
: Number(st.pnl_total) < 0
|
|
||||||
? "样本合计亏损"
|
|
||||||
: "样本合计持平";
|
|
||||||
const tpLine =
|
|
||||||
st.take_profit != null
|
|
||||||
? `<div><span class="amp-sum-k">止盈点 / 触达</span><span class="amp-sum-v">${esc(st.take_profit)} · ${esc(st.tp_hit_days)} 天 · ${esc(pct(st.tp_hit_ratio))}</span></div>`
|
|
||||||
: `<div><span class="amp-sum-k">止盈点</span><span class="amp-sum-v">未设(按|涨跌|)</span></div>`;
|
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
`<div class="amp-sum-grid">` +
|
`<div class="amp-sum-grid">` +
|
||||||
`<div><span class="amp-sum-k">双边权利金</span><span class="amp-sum-v">${esc(st.premium)}</span></div>` +
|
`<div><span class="amp-sum-k">对照点数</span><span class="amp-sum-v">${esc(ms.move_points)}</span></div>` +
|
||||||
tpLine +
|
`<div><span class="amp-sum-k">振幅≥点数</span><span class="amp-sum-v">${esc(ms.amp_hit_days)} 天 · <strong>${esc(pct(ms.amp_hit_ratio))}</strong></span></div>` +
|
||||||
`<div><span class="amp-sum-k">开→高超过权利金</span><span class="amp-sum-v">${esc(st.up_exceed_days)} 天 · ${esc(pct(st.up_exceed_ratio))}</span></div>` +
|
`<div><span class="amp-sum-k">两日振幅≥点数</span><span class="amp-sum-v">${esc(ms.amp_2d_hit_days)} 天 · <strong>${esc(pct(ms.amp_2d_hit_ratio))}</strong></span></div>` +
|
||||||
`<div><span class="amp-sum-k">开→低超过权利金</span><span class="amp-sum-v">${esc(st.down_exceed_days)} 天 · ${esc(pct(st.down_exceed_ratio))}</span></div>` +
|
`<div><span class="amp-sum-k">开→高≥点数</span><span class="amp-sum-v">${esc(ms.up_hit_days)} 天 · ${esc(pct(ms.up_hit_ratio))}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">|涨跌|超过权利金</span><span class="amp-sum-v">${esc(st.abs_change_exceed_days)} 天 · ${esc(pct(st.abs_change_exceed_ratio))}</span></div>` +
|
`<div><span class="amp-sum-k">开→低≥点数</span><span class="amp-sum-v">${esc(ms.down_hit_days)} 天 · ${esc(pct(ms.down_hit_ratio))}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">买跨盈亏合计</span><span class="amp-sum-v ${pnlClass(st.pnl_total)}">${esc(st.pnl_total)} <small>(${esc(verdict)})</small></span></div>` +
|
`<div><span class="amp-sum-k">|涨跌|≥点数</span><span class="amp-sum-v">${esc(ms.abs_change_hit_days)} 天 · ${esc(pct(ms.abs_change_hit_ratio))}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">日均盈亏</span><span class="amp-sum-v ${pnlClass(st.pnl_avg)}">${esc(st.pnl_avg)}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">赚钱天数/胜率</span><span class="amp-sum-v">${esc(st.win_days)} · ${esc(pct(st.win_ratio))}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">单日最大赚/亏</span><span class="amp-sum-v">${esc(st.pnl_max)} / ${esc(st.pnl_min)}</span></div>` +
|
|
||||||
`</div>`;
|
`</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,21 +138,31 @@
|
|||||||
return day;
|
return day;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hitCell(r) {
|
||||||
|
if (r.move_points == null) return "—";
|
||||||
|
if (r.amp_hit) return '<span class="amp-pnl is-pos">是</span>';
|
||||||
|
return '<span class="amp-pnl is-neg">否</span>';
|
||||||
|
}
|
||||||
|
|
||||||
function renderTable(pagePayload) {
|
function renderTable(pagePayload) {
|
||||||
const body = el("amp-table-body");
|
const body = el("amp-table-body");
|
||||||
const pager = el("amp-pager");
|
const pager = el("amp-pager");
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
const rows = (pagePayload && pagePayload.rows) || [];
|
const rows = (pagePayload && pagePayload.rows) || [];
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
|
body.innerHTML = '<tr><td colspan="12" class="amp-empty">暂无数据</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
body.innerHTML = rows
|
body.innerHTML = rows
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
const profit =
|
|
||||||
r.profit == null || r.profit === ""
|
|
||||||
? "—"
|
|
||||||
: `<span class="amp-pnl ${pnlClass(r.profit)}">${esc(r.profit)}</span>`;
|
|
||||||
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
|
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
|
||||||
|
const upCls = r.hit_up ? ' class="amp-pnl is-pos"' : "";
|
||||||
|
const downCls = r.hit_down ? ' class="amp-pnl is-pos"' : "";
|
||||||
|
const ampCls = r.amp_hit ? ' class="amp-pnl is-pos"' : "";
|
||||||
|
const amp2Cls = r.amp_hit_2d ? ' class="amp-pnl is-pos"' : "";
|
||||||
|
const amp2 =
|
||||||
|
r.amplitude_2d == null || r.amplitude_2d === ""
|
||||||
|
? "—"
|
||||||
|
: `<span title="${esc(r.window2_start || "")} → ${esc(r.window2_end || "")}">${esc(r.amplitude_2d)}</span>`;
|
||||||
return (
|
return (
|
||||||
`<tr${trClass}>` +
|
`<tr${trClass}>` +
|
||||||
`<td>${dayLabel(r)}</td>` +
|
`<td>${dayLabel(r)}</td>` +
|
||||||
@@ -188,11 +171,12 @@
|
|||||||
`<td>${esc(r.high)}</td>` +
|
`<td>${esc(r.high)}</td>` +
|
||||||
`<td>${esc(r.low)}</td>` +
|
`<td>${esc(r.low)}</td>` +
|
||||||
`<td>${esc(r.close)}</td>` +
|
`<td>${esc(r.close)}</td>` +
|
||||||
`<td>${esc(r.up_points)}</td>` +
|
`<td${upCls}>${esc(r.up_points)}</td>` +
|
||||||
`<td>${esc(r.down_points)}</td>` +
|
`<td${downCls}>${esc(r.down_points)}</td>` +
|
||||||
`<td><strong>${esc(r.amplitude)}</strong></td>` +
|
`<td${ampCls}><strong>${esc(r.amplitude)}</strong></td>` +
|
||||||
|
`<td${amp2Cls}><strong>${amp2}</strong></td>` +
|
||||||
`<td>${esc(r.change)}</td>` +
|
`<td>${esc(r.change)}</td>` +
|
||||||
`<td>${profit}</td>` +
|
`<td>${hitCell(r)}</td>` +
|
||||||
`</tr>`
|
`</tr>`
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -226,7 +210,7 @@
|
|||||||
|
|
||||||
async function reframe(resetPage) {
|
async function reframe(resetPage) {
|
||||||
if (!lastResult) {
|
if (!lastResult) {
|
||||||
renderStraddle(null);
|
renderMoveStats(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resetPage) pageNo = 1;
|
if (resetPage) pageNo = 1;
|
||||||
@@ -242,8 +226,7 @@
|
|||||||
start_hour: lastResult.start_hour ?? Number(el("amp-start-hour")?.value || 16),
|
start_hour: lastResult.start_hour ?? Number(el("amp-start-hour")?.value || 16),
|
||||||
period: lastResult.period || el("amp-period")?.value || "2m",
|
period: lastResult.period || el("amp-period")?.value || "2m",
|
||||||
sample_days: lastResult.sample_days_requested || 60,
|
sample_days: lastResult.sample_days_requested || 60,
|
||||||
straddle_premium: readPremium(),
|
move_points: readMovePoints(),
|
||||||
take_profit: readTakeProfit(),
|
|
||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
price_source: lastResult.price_source || "",
|
price_source: lastResult.price_source || "",
|
||||||
inst_id: lastResult.inst_id || "",
|
inst_id: lastResult.inst_id || "",
|
||||||
@@ -284,8 +267,7 @@
|
|||||||
start_hour: startHour,
|
start_hour: startHour,
|
||||||
period,
|
period,
|
||||||
custom_days: period === "custom" ? customDays : null,
|
custom_days: period === "custom" ? customDays : null,
|
||||||
straddle_premium: readPremium(),
|
move_points: readMovePoints(),
|
||||||
take_profit: readTakeProfit(),
|
|
||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
page: pageNo,
|
page: pageNo,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
@@ -331,8 +313,7 @@
|
|||||||
const startHour = Number(el("amp-start-hour")?.value || 16);
|
const startHour = Number(el("amp-start-hour")?.value || 16);
|
||||||
const period = el("amp-period")?.value || "2m";
|
const period = el("amp-period")?.value || "2m";
|
||||||
const customDays = Number(el("amp-custom-days")?.value || 60);
|
const customDays = Number(el("amp-custom-days")?.value || 60);
|
||||||
const prem = readPremium();
|
const mp = readMovePoints();
|
||||||
const tp = readTakeProfit();
|
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
symbol,
|
symbol,
|
||||||
start_hour: String(startHour),
|
start_hour: String(startHour),
|
||||||
@@ -340,8 +321,7 @@
|
|||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
});
|
});
|
||||||
if (period === "custom") q.set("custom_days", String(customDays));
|
if (period === "custom") q.set("custom_days", String(customDays));
|
||||||
if (prem != null) q.set("straddle_premium", String(prem));
|
if (mp != null) q.set("move_points", String(mp));
|
||||||
if (tp != null) q.set("take_profit", String(tp));
|
|
||||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,16 +355,13 @@
|
|||||||
const id = card.getAttribute("data-id");
|
const id = card.getAttribute("data-id");
|
||||||
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
||||||
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
||||||
const prem = readPremium();
|
const mp = readMovePoints();
|
||||||
const tp = readTakeProfit();
|
const q = new URLSearchParams({
|
||||||
let url =
|
history_id: id,
|
||||||
"/api/amp-stats/export?history_id=" +
|
weekend_filter: readWeekend(),
|
||||||
encodeURIComponent(id) +
|
});
|
||||||
"&weekend_filter=" +
|
if (mp != null) q.set("move_points", String(mp));
|
||||||
encodeURIComponent(readWeekend());
|
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||||
if (prem != null) url += "&straddle_premium=" + encodeURIComponent(String(prem));
|
|
||||||
if (tp != null) url += "&take_profit=" + encodeURIComponent(String(tp));
|
|
||||||
window.location.href = url;
|
|
||||||
});
|
});
|
||||||
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
||||||
if (!confirm("删除该历史记录?")) return;
|
if (!confirm("删除该历史记录?")) return;
|
||||||
@@ -405,11 +382,8 @@
|
|||||||
if (lastResult) {
|
if (lastResult) {
|
||||||
if (el("amp-symbol")) el("amp-symbol").value = lastResult.symbol || "eth";
|
if (el("amp-symbol")) el("amp-symbol").value = lastResult.symbol || "eth";
|
||||||
if (el("amp-start-hour")) el("amp-start-hour").value = String(lastResult.start_hour ?? 16);
|
if (el("amp-start-hour")) el("amp-start-hour").value = String(lastResult.start_hour ?? 16);
|
||||||
if (lastResult.straddle_premium != null && el("amp-straddle-premium")) {
|
if (lastResult.move_points != null && el("amp-move-points")) {
|
||||||
el("amp-straddle-premium").value = String(lastResult.straddle_premium);
|
el("amp-move-points").value = String(lastResult.move_points);
|
||||||
}
|
|
||||||
if (lastResult.take_profit != null && el("amp-take-profit")) {
|
|
||||||
el("amp-take-profit").value = String(lastResult.take_profit);
|
|
||||||
}
|
}
|
||||||
if (lastResult.weekend_filter && el("amp-weekend-filter")) {
|
if (lastResult.weekend_filter && el("amp-weekend-filter")) {
|
||||||
el("amp-weekend-filter").value = lastResult.weekend_filter;
|
el("amp-weekend-filter").value = lastResult.weekend_filter;
|
||||||
@@ -434,8 +408,7 @@
|
|||||||
el("amp-btn-compute")?.addEventListener("click", () => void compute(true));
|
el("amp-btn-compute")?.addEventListener("click", () => void compute(true));
|
||||||
el("amp-btn-save")?.addEventListener("click", () => void saveHistory());
|
el("amp-btn-save")?.addEventListener("click", () => void saveHistory());
|
||||||
el("amp-btn-download")?.addEventListener("click", downloadCurrent);
|
el("amp-btn-download")?.addEventListener("click", downloadCurrent);
|
||||||
el("amp-straddle-premium")?.addEventListener("input", scheduleReframe);
|
el("amp-move-points")?.addEventListener("input", scheduleReframe);
|
||||||
el("amp-take-profit")?.addEventListener("input", scheduleReframe);
|
|
||||||
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
||||||
syncCustomDays();
|
syncCustomDays();
|
||||||
}
|
}
|
||||||
@@ -445,7 +418,7 @@
|
|||||||
bind();
|
bind();
|
||||||
setView("stats");
|
setView("stats");
|
||||||
setStatus("");
|
setStatus("");
|
||||||
renderStraddle(null);
|
renderMoveStats(null);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1265,12 +1265,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="amp-field">
|
<label class="amp-field">
|
||||||
<span>买跨·双边权利金(点)</span>
|
<span>波动点数</span>
|
||||||
<input id="amp-straddle-premium" type="number" min="0" step="any" placeholder="如 30" />
|
<input id="amp-move-points" type="number" min="0" step="any" placeholder="如 50" />
|
||||||
</label>
|
|
||||||
<label class="amp-field">
|
|
||||||
<span>止盈点(点)</span>
|
|
||||||
<input id="amp-take-profit" type="number" min="0" step="any" placeholder="空=按涨跌" />
|
|
||||||
</label>
|
</label>
|
||||||
<div class="amp-actions">
|
<div class="amp-actions">
|
||||||
<button type="button" id="amp-btn-compute" class="primary">计算</button>
|
<button type="button" id="amp-btn-compute" class="primary">计算</button>
|
||||||
@@ -1279,22 +1275,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="amp-status" class="toolbar-meta amp-status"></p>
|
<p id="amp-status" class="toolbar-meta amp-status"></p>
|
||||||
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.买跨收益=有效波动−权利金;止盈≥触达则有效波动=止盈点,否则用|涨跌|.周末按结算日标注/筛选.</p>
|
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.两日振幅=起点再往前推1天到当日16:00(例:25日16:00→27日16:00).填写波动点数后看振幅/两日振幅≥该点数的天数占比.周末按结算日标注/筛选.</p>
|
||||||
<h3 class="amp-block-title">汇总</h3>
|
<h3 class="amp-block-title">汇总</h3>
|
||||||
<div id="amp-summary" class="amp-summary"></div>
|
<div id="amp-summary" class="amp-summary"></div>
|
||||||
<h3 class="amp-block-title">买跨对照</h3>
|
<h3 class="amp-block-title">振幅占比</h3>
|
||||||
<div id="amp-straddle" class="amp-summary amp-straddle"></div>
|
<div id="amp-move-stats" class="amp-summary amp-move-stats"></div>
|
||||||
<h3 class="amp-block-title">日表明细</h3>
|
<h3 class="amp-block-title">日表明细</h3>
|
||||||
<div class="amp-table-wrap">
|
<div class="amp-table-wrap">
|
||||||
<table class="amp-table">
|
<table class="amp-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>结算日</th><th>窗起点</th><th>开</th><th>高</th><th>低</th><th>收</th>
|
<th>结算日</th><th>窗起点</th><th>开</th><th>高</th><th>低</th><th>收</th>
|
||||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th><th>收益</th>
|
<th>开→高</th><th>开→低</th><th>振幅</th><th>两日振幅</th><th>涨跌</th><th>振幅达标</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="amp-table-body">
|
<tbody id="amp-table-body">
|
||||||
<tr><td colspan="11" class="amp-empty">点击「计算」加载</td></tr>
|
<tr><td colspan="12" class="amp-empty">点击「计算」加载</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -1763,7 +1759,7 @@
|
|||||||
<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=20260723-hide-pnl"></script>
|
<script src="/assets/dashboard.js?v=20260723-hide-pnl"></script>
|
||||||
<script src="/assets/strategy.js?v=11"></script>
|
<script src="/assets/strategy.js?v=11"></script>
|
||||||
<script src="/assets/amp_stats.js?v=5"></script>
|
<script src="/assets/amp_stats.js?v=20260728-hedge"></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>
|
||||||
|
|||||||
+40
-54
@@ -34,6 +34,14 @@ class AmpStatsLibTests(unittest.TestCase):
|
|||||||
self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2026-07-22 08:00")
|
self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2026-07-22 08:00")
|
||||||
self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2026-07-22 16:00")
|
self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2026-07-22 16:00")
|
||||||
|
|
||||||
|
def test_window_two_day_16_to_16(self):
|
||||||
|
# 结算 27 日 → 两日窗 25日16:00 → 27日16:00
|
||||||
|
start, end = window_bounds_for_settlement(date(2026, 7, 27), 16, span_days=2)
|
||||||
|
self.assertEqual(start.strftime("%Y-%m-%d %H:%M"), "2026-07-25 16:00")
|
||||||
|
self.assertEqual(end.strftime("%Y-%m-%d %H:%M"), "2026-07-27 16:00")
|
||||||
|
one_start, _ = window_bounds_for_settlement(date(2026, 7, 27), 16, span_days=1)
|
||||||
|
self.assertEqual(one_start.strftime("%Y-%m-%d %H:%M"), "2026-07-26 16:00")
|
||||||
|
|
||||||
def test_settlement_excludes_incomplete_today(self):
|
def test_settlement_excludes_incomplete_today(self):
|
||||||
now = datetime(2026, 7, 22, 10, 0, tzinfo=TZ)
|
now = datetime(2026, 7, 22, 10, 0, tzinfo=TZ)
|
||||||
days = list_settlement_dates(sample_days=3, now=now)
|
days = list_settlement_dates(sample_days=3, now=now)
|
||||||
@@ -89,41 +97,38 @@ class AmpStatsLibTests(unittest.TestCase):
|
|||||||
self.assertEqual(s["max_amplitude_day"], "2026-07-02")
|
self.assertEqual(s["max_amplitude_day"], "2026-07-02")
|
||||||
self.assertEqual(s["max_up_points"], 500)
|
self.assertEqual(s["max_up_points"], 500)
|
||||||
self.assertEqual(s["max_down_points"], 200)
|
self.assertEqual(s["max_down_points"], 200)
|
||||||
self.assertIsNone(s["straddle"])
|
self.assertIsNone(s["move_points_stats"])
|
||||||
|
|
||||||
def test_long_straddle_stats(self):
|
def test_move_points_amp_ratio(self):
|
||||||
rows = [
|
rows = [
|
||||||
# |chg|=40>30 win+10; up=40>30; down=10
|
{"amplitude": 100, "up_points": 40, "down_points": 60, "change": 10, "settlement_day": "2026-07-01"},
|
||||||
{"up_points": 40, "down_points": 10, "change": 40, "amplitude": 50, "settlement_day": "2026-07-01"},
|
{"amplitude": 40, "up_points": 10, "down_points": 30, "change": -5, "settlement_day": "2026-07-02"},
|
||||||
# |chg|=10 lose-20; up=5; down=35>30
|
{"amplitude": 50, "up_points": 50, "down_points": 0, "change": 20, "settlement_day": "2026-07-03"},
|
||||||
{"up_points": 5, "down_points": 35, "change": -10, "amplitude": 40, "settlement_day": "2026-07-02"},
|
|
||||||
# |chg|=30 not >30 lose-30; boundary
|
|
||||||
{"up_points": 30, "down_points": 30, "change": 30, "amplitude": 60, "settlement_day": "2026-07-03"},
|
|
||||||
]
|
]
|
||||||
s = summarize_rows(rows, straddle_premium=30)
|
s = summarize_rows(rows, move_points=50)
|
||||||
st = s["straddle"]
|
ms = s["move_points_stats"]
|
||||||
self.assertEqual(st["side"], "long_straddle")
|
self.assertIsNotNone(ms)
|
||||||
self.assertEqual(st["premium"], 30)
|
self.assertEqual(ms["move_points"], 50)
|
||||||
self.assertEqual(st["up_exceed_days"], 1) # only 40
|
self.assertEqual(ms["amp_hit_days"], 2) # 100, 50
|
||||||
self.assertEqual(st["down_exceed_days"], 1) # only 35
|
self.assertEqual(ms["amp_hit_ratio"], round(2 / 3, 4))
|
||||||
self.assertEqual(st["abs_change_exceed_days"], 1) # only 40
|
self.assertEqual(ms["up_hit_days"], 1) # 50
|
||||||
self.assertAlmostEqual(st["pnl_total"], 40 - 30 + 10 - 30 + 30 - 30)
|
self.assertEqual(ms["down_hit_days"], 1) # 60
|
||||||
self.assertEqual(st["win_days"], 1)
|
|
||||||
self.assertEqual(st["win_ratio"], round(1 / 3, 4))
|
|
||||||
csv_text = build_export_csv(
|
csv_text = build_export_csv(
|
||||||
{"exchange": "okx", "symbol_label": "ETH", "summary": s, "rows": rows, "start_hour": 22, "end_hour": 16}
|
{
|
||||||
|
"exchange": "okx",
|
||||||
|
"symbol_label": "ETH",
|
||||||
|
"summary": s,
|
||||||
|
"rows": rows,
|
||||||
|
"start_hour": 16,
|
||||||
|
"end_hour": 16,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
self.assertIn("买跨对照", csv_text)
|
self.assertIn("振幅占比", csv_text)
|
||||||
self.assertIn("买跨点数盈亏合计", csv_text)
|
self.assertIn("振幅达标", csv_text)
|
||||||
|
|
||||||
def test_take_profit_and_weekend(self):
|
def test_weekend_and_reframe_move_points(self):
|
||||||
from lib.hub.amp_stats_lib import (
|
from lib.hub.amp_stats_lib import enrich_rows, filter_weekend_rows, reframe_amp_stats
|
||||||
enrich_rows_pnl,
|
|
||||||
filter_weekend_rows,
|
|
||||||
reframe_amp_stats,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sat 2026-07-18, Sun 2026-07-19, Mon 2026-07-20
|
|
||||||
rows = [
|
rows = [
|
||||||
{
|
{
|
||||||
"settlement_day": "2026-07-18",
|
"settlement_day": "2026-07-18",
|
||||||
@@ -159,39 +164,20 @@ class AmpStatsLibTests(unittest.TestCase):
|
|||||||
only = filter_weekend_rows(rows, "only")
|
only = filter_weekend_rows(rows, "only")
|
||||||
self.assertEqual(len(only), 2)
|
self.assertEqual(len(only), 2)
|
||||||
|
|
||||||
# TP=80: day1 hit → move 80; day2 no → |12|; day3 no → 8
|
enriched = enrich_rows(rows, move_points=80)
|
||||||
enriched = enrich_rows_pnl(rows, straddle_premium=10, take_profit=80)
|
self.assertTrue(enriched[0]["amp_hit"])
|
||||||
self.assertTrue(enriched[0]["take_profit_hit"])
|
self.assertFalse(enriched[1]["amp_hit"])
|
||||||
self.assertEqual(enriched[0]["effective_move"], 80)
|
self.assertTrue(enriched[2]["amp_hit"])
|
||||||
self.assertEqual(enriched[0]["profit"], 70)
|
|
||||||
self.assertFalse(enriched[1]["take_profit_hit"])
|
|
||||||
self.assertEqual(enriched[1]["effective_move"], 12)
|
|
||||||
self.assertEqual(enriched[1]["profit"], 2)
|
|
||||||
# TP empty → use |change|
|
|
||||||
no_tp = enrich_rows_pnl(rows[:1], straddle_premium=10, take_profit=None)
|
|
||||||
self.assertEqual(no_tp[0]["effective_move"], 5)
|
|
||||||
self.assertEqual(no_tp[0]["profit"], -5)
|
|
||||||
|
|
||||||
# TP boundary >= : up=80 counts as hit
|
|
||||||
edge = enrich_rows_pnl(
|
|
||||||
[{"up_points": 80, "down_points": 1, "change": 2, "settlement_day": "2026-07-20", "is_weekend": False}],
|
|
||||||
straddle_premium=10,
|
|
||||||
take_profit=80,
|
|
||||||
)
|
|
||||||
self.assertTrue(edge[0]["take_profit_hit"])
|
|
||||||
self.assertEqual(edge[0]["profit"], 70)
|
|
||||||
|
|
||||||
reframed = reframe_amp_stats(
|
reframed = reframe_amp_stats(
|
||||||
rows_all=rows,
|
rows_all=rows,
|
||||||
symbol="eth",
|
symbol="eth",
|
||||||
weekend_filter="exclude",
|
weekend_filter="exclude",
|
||||||
straddle_premium=10,
|
move_points=80,
|
||||||
take_profit=80,
|
|
||||||
)
|
)
|
||||||
self.assertEqual(reframed["summary"]["sample_count"], 1)
|
self.assertEqual(reframed["summary"]["sample_count"], 1)
|
||||||
# Mon: 未触达止盈 → |8|-10
|
self.assertTrue(reframed["rows"][0]["amp_hit"])
|
||||||
self.assertEqual(reframed["rows"][0]["profit"], -2)
|
self.assertIn("振幅占比", build_export_csv(reframed))
|
||||||
self.assertIn("收益", build_export_csv(reframed))
|
|
||||||
|
|
||||||
def test_fetch_switches_to_history_endpoint(self):
|
def test_fetch_switches_to_history_endpoint(self):
|
||||||
"""近期接口到头后应切 history 续拉."""
|
"""近期接口到头后应切 history 续拉."""
|
||||||
|
|||||||
Reference in New Issue
Block a user