Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 05864d72c2 | |||
| 722c511543 | |||
| 26bc19f047 | |||
| c81ba147cc | |||
| 90be23e845 | |||
| 2ce67da8e8 | |||
| d049c5d317 | |||
| 845884fc67 | |||
| c73e36309e | |||
| 21c80f2ac9 | |||
| a908dccaba | |||
| 4bcf88b5cb | |||
| f360242188 | |||
| f53f2814ab | |||
| e5051fb309 | |||
| 1dc7914701 | |||
| 58e2bf3e8b | |||
| 19debee581 | |||
| 1755f67eca | |||
| 6886de0bad | |||
| f04a91efe6 | |||
| b43e33e24f | |||
| b5a061e758 | |||
| 4ef3b40353 |
+6
-2
@@ -6,8 +6,12 @@
|
||||
|
||||
| 标签 | 指向提交 | 说明 |
|
||||
|------|----------|------|
|
||||
| `snapshot/20260728-2` | `722c511` | 2026-07-28 午后:振幅统计改为波动点数→振幅占比、两日振幅(例25日16:00→27日16:00);去掉买跨/永期对照 |
|
||||
| `snapshot/20260728` | `c73e363` | 2026-07-28:中控永期对冲计算器(由波动推仓位 / 由比例推点数)、说明文档 |
|
||||
| `snapshot/20260727` | `f53f281` | 2026-07-27:实例手机壳(下单/持仓/期权)、著作权声明、托管合同(一用户一机)、服务说明与报价说明 |
|
||||
| `snapshot/20260726-2` | `4a79e01` | 2026-07-26 午:执行手册脑图(业务主题)、`.xmind` 按二进制入库、去掉缩略图避免 Gitea raw 换行损坏 |
|
||||
| `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-pre-amp-stats` | `40be3a5` | 2026-07-23:振幅统计开发前;含执行手册进教练、日亏损冻结、手机监控 UI、振幅统计开发方案等 |
|
||||
| `snapshot/20260721-2` | `a721642` | 2026-07-21 晚:日亏损次数冻结、交易执行手册入中控策略说明、期权/Gate 执行手册文档等 |
|
||||
@@ -30,7 +34,7 @@
|
||||
git tag -l 'snapshot/*'
|
||||
|
||||
# 检出快照(只读查看,勿在此分支直接开发)
|
||||
git checkout snapshot/20260726
|
||||
git checkout snapshot/20260728-2
|
||||
|
||||
# 回到主线
|
||||
git checkout main
|
||||
|
||||
+25
-23
@@ -3,7 +3,8 @@
|
||||
中控只读工具:按自定义整点起点、**固定北京时间 16:00 收窗**,统计 OKX 上 ETH/BTC 的历史「点数振幅」档案,辅助一天期期权判断空间。
|
||||
|
||||
> 开发方案见 [ETH时段振幅统计-开发方案.md](./ETH时段振幅统计-开发方案.md)。
|
||||
> **不改下单链路**;不算 IV / 权利金。
|
||||
> **不改下单链路**;不算 IV。
|
||||
> 买跨 / 永期对冲测算请用中控 **策略计算器**,本页不再做对照盈亏。
|
||||
|
||||
---
|
||||
|
||||
@@ -21,8 +22,9 @@
|
||||
2. 选择 **标的** ETH / BTC;数据源固定 **OKX**
|
||||
3. **起点整点**(00–23);终点固定 **16:00**
|
||||
4. **周期**:1 月 / 2 月 / 3 月 / 半年 / 1 年 / 自定义天数(默认 2 个月)
|
||||
5. 点 **计算** → 下方看汇总 + 分页日表
|
||||
6. 需要留存时点 **保存到历史**;**下载 CSV** 含摘要 + 全日明细
|
||||
5. 可选填 **波动点数**(如 `50`)→ 看振幅达标占比
|
||||
6. 点 **计算** → 下方看汇总 + 振幅占比 + 分页日表
|
||||
7. 需要留存时点 **保存到历史**;**下载 CSV** 含摘要 + 全日明细
|
||||
|
||||
**跨天例子**
|
||||
|
||||
@@ -42,14 +44,15 @@
|
||||
|
||||
| 字段 | 算法 |
|
||||
|------|------|
|
||||
| 开→高 | `H − O` |
|
||||
| 开→低 | `O − L` |
|
||||
| **振幅** | `H − L`(= 开→高 + 开→低) |
|
||||
| 涨跌值 | `C − O` |
|
||||
| 开→高 | `H − O`(一边波动) |
|
||||
| 开→低 | `O − L`(另一边波动) |
|
||||
| **振幅** | `H − L`(= 开→高 + 开→低),窗为起点整点 → 当日 16:00 |
|
||||
| **两日振幅** | 同上口径,但起点再往前推 1 天;例起点 16:00、结算 27 日 → **25日16:00 → 27日16:00** |
|
||||
| 涨跌值 | `C − O`(单日窗) |
|
||||
|
||||
例:O=2000,H=2500,L=1800 → 开→高 500,开→低 200,振幅 **700**。
|
||||
|
||||
汇总必含:最大振幅(及日期)、开→高/开→低的最大与均值等。
|
||||
汇总必含:最大振幅(及日期)、两日振幅最大/均值/中位、开→高/开→低的最大与均值等。
|
||||
|
||||
K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD / BTC-USD),失败再降级永续标记。
|
||||
近期 K 线接口约仅 **1440** 根(1H≈60 天);更长周期自动续拉 `history-index-candles` / `history-candles`。
|
||||
@@ -57,22 +60,20 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
||||
|
||||
---
|
||||
|
||||
## 买跨对照(赌波动)
|
||||
## 波动点数 → 振幅占比
|
||||
|
||||
表单可填 **双边权利金(点)**,例如 `30`;旁边可填 **止盈点**(可空):
|
||||
表单可填 **波动点数**(如 `50`)。填写后下方 **振幅占比** 块显示:
|
||||
|
||||
| 汇总项 | 口径 |
|
||||
|--------|------|
|
||||
| 开→高超过权利金 | `H−O > 权利金` 的天数与占比 |
|
||||
| 开→低超过权利金 | `O−L > 权利金` 的天数与占比 |
|
||||
| \|涨跌\|超过权利金 | `\|C−O\| > 权利金` 的天数与占比 |
|
||||
| 有效波动 | 若设止盈且 `开→高≥止盈` 或 `开→低≥止盈` → 用止盈点;否则用 `\|C−O\|` |
|
||||
| 买跨收益 | `有效波动 − 权利金`(日表「收益」列同口径) |
|
||||
| 振幅≥点数 | 单日窗 `H−L ≥ 点数` 的天数与**占比**(主指标) |
|
||||
| 两日振幅≥点数 | 两日窗振幅 ≥ 点数 的天数与占比 |
|
||||
| 开→高≥点数 | `H−O ≥ 点数` 天数与占比 |
|
||||
| 开→低≥点数 | `O−L ≥ 点数` 天数与占比 |
|
||||
| \|涨跌\|≥点数 | `\|C−O\| ≥ 点数` 天数与占比 |
|
||||
|
||||
- 方向:**买跨**
|
||||
- 权利金越过:严格 **`>`**;止盈触达:**`≥`**
|
||||
- 止盈留空 / ≤0:有效波动一律按 `|涨跌|`
|
||||
- 已算出日表后,改权利金 / 止盈 / 周末筛选会**本地重算**(不重拉 K 线)
|
||||
日表保留 **开→高 / 开→低**、**振幅**、**两日振幅**(悬停可见两日窗起止),并标 **振幅达标**。
|
||||
改点数 / 周末筛选会在已有日表上**本地重算**(不重拉 K 线)。
|
||||
|
||||
### 周末
|
||||
|
||||
@@ -97,7 +98,7 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
||||
| `manual_trading_hub/amp_stats_routes.py` | API |
|
||||
| `manual_trading_hub/amp_stats_store.py` | 历史 JSON |
|
||||
| `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 | 长周期续拉 history K 线;收益列红绿着色 |
|
||||
| 2026-07-23 | 买跨对照、周末筛选、止盈点 |
|
||||
| 2026-07-28 | 永期对冲对照(后已移除) |
|
||||
| 2026-07-28 | 去掉买跨/永期;改为波动点数→振幅占比 |
|
||||
| 2026-07-28 | 增加两日振幅(例 25日16:00→27日16:00) |
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# 服务说明与报价说明
|
||||
|
||||
> 本文说明本系统的定位、适用对象、托管方式与参考报价。
|
||||
> 配套文件:`著作权声明.md`、`软件使用授权合同-模板.md`(托管服务与软件使用合同)。
|
||||
> **本系统以著作权人自用为主**;对外托管属个案合作,并非标准化「卖工具」业务。
|
||||
|
||||
---
|
||||
|
||||
## 1. 这是什么
|
||||
|
||||
`crypto_monitor`(加密货币交易监控与中控系统)由著作权人 **马建军** 历时约三个月持续开发,用于自身实盘交易中的:
|
||||
|
||||
- 多交易所实例监控与下单辅助
|
||||
- 风控与纪律约束(如日亏冻结、执行规则落地到系统)
|
||||
- 复盘、关键位、期权/合约相关流程(以实际开通功能为准)
|
||||
- 中控统一查看与管理
|
||||
|
||||
开发目的首先是:**把交易习惯钉进系统,减少情绪单与随意操作**,而不是面向市场量产销售的通用软件商品。
|
||||
|
||||
---
|
||||
|
||||
## 2. 定位与适用对象
|
||||
|
||||
### 2.1 定位
|
||||
|
||||
| 是 | 不是 |
|
||||
|----|------|
|
||||
| 全职(或准全职)交易者的执行与纪律系统 | 兼职「玩玩」的下单插件 |
|
||||
| 规则、限制、复盘一起用的工作台 | 帮你加杠杆、追涨杀跌的「发财工具」 |
|
||||
| 著作权人自用为主;对外仅少量托管 | 开源产品或标准化 SaaS 大卖场 |
|
||||
|
||||
### 2.2 适合
|
||||
|
||||
- 以交易为主要工作、愿意按规则执行的人
|
||||
- 认同执行手册与系统内限制(含开仓限制、冻结等)
|
||||
- 接受「一户一机、不交付源码、按期付费」的托管方式
|
||||
- 账户规模与付费意愿匹配(服务费不应明显高于可承受的交易成本)
|
||||
|
||||
### 2.3 不适合(一般不承接)
|
||||
|
||||
- 兼职、偶尔开几单的小散
|
||||
- 只想要更快开仓、更高杠杆,不愿接受纪律约束
|
||||
- 要求交付源码、私有仓库权限或「买断随便改」
|
||||
- 希望多人共用一台服务器以压低费用
|
||||
|
||||
**说明:** 不适合不等于否定任何人,而是产品与服务形态不匹配;强行上线往往浪费双方时间。
|
||||
|
||||
---
|
||||
|
||||
## 3. 对外怎么提供(若合作)
|
||||
|
||||
默认且唯一推荐的方式:
|
||||
|
||||
1. **著作权人提供专属服务器**(一用户一服务器,不与其他客户共用)
|
||||
2. **部署中控与实例**,配置域名 / HTTPS
|
||||
3. 客户仅获得 **访问地址 + 登录账号**
|
||||
4. **不交付源代码**、不开放 Git、不移交服务器 root(由甲方代持运维)
|
||||
|
||||
合作前建议:先阅读相关执行/行为说明,确认认同纪律设计,再谈部署与费用。
|
||||
|
||||
正式合作须签署《托管服务与软件使用合同》(见合同模板)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 费用构成
|
||||
|
||||
费用分四项,建议在报价单中分列,避免被理解成「只卖服务器」:
|
||||
|
||||
| 费用 | 含义 | 通常周期 |
|
||||
|------|------|----------|
|
||||
| 服务器费用 | 该客户专属云主机、带宽、磁盘等 | 月 / 年 |
|
||||
| 域名费用 | 域名注册或续费(代持或客户自带域名) | 年 |
|
||||
| 部署费用 | 首次装机、证书、上线、基础培训 | 一次性 |
|
||||
| 程序使用费 | 软件托管使用权、基础更新与运维响应 | 月 / 年 |
|
||||
|
||||
续费年一般不再收部署费(大改版或迁移可另议)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 参考报价(非标价,可协商)
|
||||
|
||||
以下为**面向全职交易者、个案托管**的参考区间(人民币)。
|
||||
因以自用为主、名额有限,实际以当时口头/书面报价为准,可高于下列下限。
|
||||
|
||||
### 5.1 分项参考
|
||||
|
||||
| 项目 | 参考区间 | 备注 |
|
||||
|------|----------|------|
|
||||
| 服务器费用 | **200–400 元/月** | 按机型实报或固定档;专属机,不共用 |
|
||||
| 域名费用 | **60–120 元/年** | 实报实销;客户自带域名可减免 |
|
||||
| 部署费用 | **2,000–5,000 元** | 一次性;含上线与基础使用说明 |
|
||||
| 程序使用费 | **1,000–2,500 元/月** 或 **10,000–25,000 元/年** | 年付可相当于少收 1~2 个月 |
|
||||
|
||||
### 5.2 首年打包示意(便于沟通)
|
||||
|
||||
| 档位 | 首年大约量级 | 思路 |
|
||||
|------|--------------|------|
|
||||
| 协作档 | 约 **1.5–2.5 万** | 部署中档 + 服务器 + 使用费中低 |
|
||||
| 标准档 | 约 **2–4 万** | 部署与使用费取中高,含优先响应 |
|
||||
|
||||
**不提供:** 低价引流套餐、兼职小资金特惠、源码买断(若极少数个案谈源码,须另签合同且价格远高于年使用费,默认不做)。
|
||||
|
||||
### 5.3 付款与停服
|
||||
|
||||
- 部署费 + 首周期费用:签约后约定日内支付,到账后排期部署
|
||||
- 续费:到期前支付;逾期可暂停访问,严重逾期可停服并释放专属服务器
|
||||
- 细节以合同条款为准
|
||||
|
||||
---
|
||||
|
||||
## 6. 服务边界(简要)
|
||||
|
||||
**甲方(马建军)合理范围内可提供:**
|
||||
|
||||
- 专属机上的首次部署与基础运维
|
||||
- 程序常规更新、进程异常处理
|
||||
- 约定范围内的使用说明
|
||||
|
||||
**一般不包含(除非另议):**
|
||||
|
||||
- 代客交易、代管资金、投资建议
|
||||
- 保证盈利或胜率
|
||||
- 7×24 即时响应当成「专职客服」
|
||||
- 按客户要求无限改需求而不另计定制费
|
||||
|
||||
交易盈亏由客户自行承担;系统为辅助与纪律工具。
|
||||
|
||||
---
|
||||
|
||||
## 7. 知识产权
|
||||
|
||||
- 软件与文档著作权归 **马建军** 所有,见 `著作权声明.md`
|
||||
- 托管仅授权约定范围内的使用权,**不转移著作权、不交付源码**
|
||||
- 仓库为私有保存;私有不影响著作权主张
|
||||
|
||||
---
|
||||
|
||||
## 8. 联系
|
||||
|
||||
- 著作权人 / 服务提供方:马建军
|
||||
- 电话:18364911125
|
||||
|
||||
意向合作请说明:交易经验与是否全职、大致账户规模(可不精确)、希望开通的交易所、是否接受系统纪律限制。
|
||||
**谢绝:** 仅询源码价格、要求多人共用一台服务器、明确表示不接受任何交易限制的需求。
|
||||
|
||||
---
|
||||
|
||||
*文档版本:与仓库同步维护;报价为参考,最终以双方确认的报价单与合同为准。*
|
||||
+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)「永期对冲对照」。
|
||||
@@ -0,0 +1,50 @@
|
||||
# 著作权声明
|
||||
|
||||
## 作品信息
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 作品名称 | crypto_monitor(加密货币交易监控与中控系统) |
|
||||
| 作品形式 | 计算机软件及相关技术文档 |
|
||||
| 著作权人 | 马建军 |
|
||||
| 联系电话 | 18364911125 |
|
||||
| 权利主张起始 | 2026 年(以本仓库首次提交及后续持续开发为准) |
|
||||
|
||||
## 权利声明
|
||||
|
||||
本仓库所含下列内容之著作权归 **马建军** 所有:
|
||||
|
||||
1. 源代码、脚本、配置模板与部署相关文件;
|
||||
2. 界面文案、说明文档、执行手册、策略与设计类文档;
|
||||
3. 由著作权人创作并纳入本仓库的图表、脑图及其他配套材料。
|
||||
|
||||
**Copyright © 2026 马建军. 保留所有权利。**
|
||||
|
||||
未经著作权人书面许可,任何单位或个人不得擅自:
|
||||
|
||||
- 复制、传播、公开披露本仓库全部或部分内容;
|
||||
- 出售、出租、赠与或以任何方式向第三方提供本软件或其衍生版本;
|
||||
- 删除或篡改本声明及表明著作权归属的标识。
|
||||
|
||||
本仓库计划以私有方式保存;私有并不影响著作权人对本作品享有的权利。
|
||||
|
||||
## 证明与版本痕迹
|
||||
|
||||
本作品的创作过程以 Git 提交历史、远程私有仓库记录及快照标签(如 `snapshot/*`)为时间线依据。著作权主张以本声明与上述开发痕迹为准。
|
||||
|
||||
## 免责(与著作权并列说明)
|
||||
|
||||
本软件及相关文档仅供著作权人授权范围内的交易辅助与内部使用。市场有风险,交易决策与盈亏由使用者自行承担;本声明不构成任何投资建议。
|
||||
|
||||
## 对外提供方式
|
||||
|
||||
著作权人对外提供本软件的**默认方式**为:由著作权人为每位客户提供**专属服务器**(一用户一服务器,不与其他客户共用同一台机器)与部署,客户通过访问地址与账号使用,并缴纳服务器费、域名费、部署费及程序使用费;**不交付源代码**。
|
||||
|
||||
对外托管或授权使用时,请签署《托管服务与软件使用合同》(模板见同目录 `软件使用授权合同-模板.md`)。服务定位、适用对象与参考报价见 `服务说明与报价说明.md`。未签署有效合同的,除著作权人本人外,任何人均无权使用、复制或传播本软件。
|
||||
|
||||
## 联系
|
||||
|
||||
- 著作权人:马建军
|
||||
- 电话:18364911125
|
||||
|
||||
本声明随仓库版本一并维护;如有更新,以仓库中最新文本为准。
|
||||
@@ -0,0 +1,192 @@
|
||||
# 托管服务与软件使用合同(模板)
|
||||
|
||||
> 说明:本文为**合同模板**,适用于甲方(马建军)提供**专属服务器**与部署、乙方通过网页/账号使用软件、**不交付源码**的托管模式。
|
||||
> **一用户一服务器**:每位客户单独一台(套)服务器,不与其他客户共用同一台服务器。
|
||||
> 与仓库内《著作权声明》配套:声明主张权利;本合同约定服务范围、费用与使用边界。
|
||||
> 签署前请双方核对条款;金额较大或长期合作,建议再请律师审阅。
|
||||
|
||||
---
|
||||
|
||||
**合同编号:** ________________
|
||||
**签订日期:** ______ 年 ____ 月 ____ 日
|
||||
**签订地点:** ________________
|
||||
|
||||
## 甲方(服务提供方 / 著作权人)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 姓名 | 马建军 |
|
||||
| 联系电话 | 18364911125 |
|
||||
| 身份证件号码 | ________________(签署时填写) |
|
||||
| 住址 | ________________(签署时填写,选填) |
|
||||
|
||||
## 乙方(客户 / 使用方)
|
||||
|
||||
| 项 | 内容 |
|
||||
|----|------|
|
||||
| 姓名 / 名称 | ________________ |
|
||||
| 证件类型及号码 | ________________ |
|
||||
| 联系电话 | ________________ |
|
||||
| 住址 / 住所地 | ________________(选填) |
|
||||
|
||||
甲乙双方就甲方在其控制的服务器上部署、运维 `crypto_monitor`(加密货币交易监控与中控系统,以下称「本软件」),并向乙方提供**托管使用服务**,经协商一致,订立本合同。
|
||||
|
||||
---
|
||||
|
||||
## 第一条 服务内容与交付方式
|
||||
|
||||
1.1 **服务模式**:甲方为乙方提供**专属**云服务器(或等价专属托管环境)、域名解析(或子域名)、程序部署与运行维护;乙方通过甲方提供的 **访问地址与账号** 使用本软件,**不交付、不提供** 源代码、私有仓库权限、部署脚本全集或可用于独立重建系统的技术资料。
|
||||
|
||||
1.2 **一用户一服务器**:本合同项下服务器**仅供乙方使用**,不与其他客户共用同一台服务器、同一操作系统实例或同一套生产部署环境。甲方不得将其他客户的程序、数据或账号部署于本合同约定的专属服务器上。
|
||||
|
||||
1.3 **交付物**(勾选实际提供项):
|
||||
- [ ] 专属服务器标识 / 实例 ID(选填):________________
|
||||
- [ ] 中控访问地址:________________
|
||||
- [ ] 实例访问地址(交易所):________________ / ________________ / ________________
|
||||
- [ ] 登录账号:________________(或另行发放)
|
||||
- [ ] 使用说明 / 培训(____ 次,每次 ____ 分钟,选填)
|
||||
|
||||
1.4 **不包含**(除非另签书面补充协议并另付费):源码转让、源码只读权限、独立私有化部署包、二次开发源代码交付、数据库完整镜像导出用于迁移至第三方系统、服务器 root/控制台账号移交(服务器由甲方代持运维)。
|
||||
|
||||
1.5 本软件著作权及部署架构归甲方所有。专属服务器的云账号/机器所有权或租赁关系由甲方管理,乙方取得的是**该服务器上本软件的有限使用权**,不转让著作权、商标权、服务器所有权及其他知识产权。
|
||||
|
||||
---
|
||||
|
||||
## 第二条 授权范围与使用限制
|
||||
|
||||
2.1 **授权性质**:普通、非独占、不可再许可;仅限本合同约定的**专属服务器**及域名/访问地址范围内使用。
|
||||
|
||||
2.2 **使用主体**:仅限乙方本人及经甲方书面确认的 ______ 名操作人员;账号不得转借、共享给合同外第三方。
|
||||
|
||||
2.3 **使用目的**:仅限乙方自身交易辅助、内部监控与运营;不得将本软件或实质相同的功能作为产品/服务向不特定公众或第三方收费提供。
|
||||
|
||||
2.4 **服务期限**:
|
||||
- 自 ______ 年 ____ 月 ____ 日起,至 ______ 年 ____ 月 ____ 日止;
|
||||
- 期满前 ______ 日双方可协商续签;期满未续费且未书面延期的,甲方有权停服并回收该专属服务器资源。
|
||||
|
||||
2.5 乙方不得实施下列行为:
|
||||
1. 要求或试图获取源码、Git 仓库、服务器 root/云控制台权限(合同另有约定的除外);
|
||||
2. 复制、传播、截图外传足以重建系统的架构说明、配置全集或程序文件;
|
||||
3. 对系统进行反向工程、抓包重建、或委托他人仿制同类托管产品对外经营;
|
||||
4. 将访问账号、域名、API 密钥用于合同约定外的用途或转售;
|
||||
5. 攻击、扫描本合同专属服务器或甲方其他基础设施。
|
||||
|
||||
---
|
||||
|
||||
## 第三条 费用与支付
|
||||
|
||||
3.1 乙方按下列项目向甲方支付费用(勾选并填写金额;可打包为「标准套餐价」并在备注中列明分项):
|
||||
|
||||
| 费用项目 | 说明 | 金额(元) | 计费周期 |
|
||||
|----------|------|------------|----------|
|
||||
| 服务器费用 | **乙方专属**云主机、带宽、磁盘等(不与其他客户分摊同一台机器) | ¥ ______ | □月付 □年付 |
|
||||
| 域名费用 | 域名注册/续费(域名归属:□甲方代持 □乙方自有,解析由甲方配置) | ¥ ______ | □年付 |
|
||||
| 部署费用 | 在专属服务器上首次环境搭建、证书、实例与中控上线(一次性) | ¥ ______ | 一次性 |
|
||||
| 程序使用费 | 本软件托管使用权、日常更新与基础运维 | ¥ ______ | □月付 □年付 |
|
||||
|
||||
3.2 **合计**(首年 / 首月应付):人民币(大写)________________ 元整(¥ ________)。
|
||||
|
||||
3.3 **支付方式与时间**:________________(如:签约后 ____ 日内付部署费+首周期费用;之后每 ____ 提前 ____ 日支付续费)。
|
||||
|
||||
3.4 **续费**:服务期满前,乙方按 3.1 约定支付下一周期费用;逾期超过 ______ 日未付的,甲方有权暂停服务;逾期超过 ______ 日仍未付的,甲方有权解除合同并停服,已付未消费部分按实际服务天数抵扣后退还(部署费是否退还:□不退 □按约定 ________________)。
|
||||
|
||||
3.5 **价格调整**:续签时,因云厂商涨价、域名涨价或功能范围扩大,甲方可提前 ______ 日书面通知调整后续周期价格;乙方不同意调整的,可在当前周期结束后不再续签。
|
||||
|
||||
3.6 [ ] 本次为试用 / 友情托管:期限至 ______,费用减免 ________________,乙方仍须遵守第二条全部限制。
|
||||
|
||||
---
|
||||
|
||||
## 第四条 部署、运维与更新
|
||||
|
||||
4.1 **甲方责任**(合理范围内):
|
||||
- 按约定完成首次部署并使乙方可以登录使用;
|
||||
- 程序版本更新、安全补丁、PM2/进程异常重启等**基础运维**(具体 SLA:________________,如「工作日 24 小时内响应」);
|
||||
- 因交易所 API 变更导致的**常规适配**(重大重构另议)。
|
||||
|
||||
4.2 **乙方责任**:
|
||||
- 提供合法有效的交易所 API 等密钥信息,并保证账户使用合规;
|
||||
- 妥善保管登录密码;因乙方泄露导致的损失由乙方承担;
|
||||
- 按约定及时支付各项费用。
|
||||
|
||||
4.3 **数据**:乙方在系统中的交易记录、配置等业务数据归属乙方,并存放于本合同专属服务器;甲方为运维可接触相关数据,但不得用于合同约定外的目的,亦不得将乙方数据混存于其他客户服务器。合同终止后,乙方可申请导出**业务数据**(格式:________________,费用:________________);**不包含**源码与部署环境镜像。
|
||||
|
||||
4.4 **停服与备份**:甲方在停服前 ______ 日通知乙方(因乙方欠费紧急停服除外);停服后该专属服务器上的数据保留 ______ 日,逾期可删除并释放服务器资源。
|
||||
|
||||
---
|
||||
|
||||
## 第五条 保密
|
||||
|
||||
5.1 乙方对知悉的本软件存在、界面逻辑、非公开功能、报价及甲方技术方案负有保密义务。
|
||||
|
||||
5.2 甲方对乙方的 API 密钥、账户信息负有保密义务,除运维必需与法律要求外不得向第三方披露。
|
||||
|
||||
5.3 保密期限:合同存续期间及终止后 ______ 年(未填则视为 5 年)。
|
||||
|
||||
---
|
||||
|
||||
## 第六条 免责与风险提示
|
||||
|
||||
6.1 本软件为交易辅助工具,不构成投资建议。市场有风险,乙方交易决策与盈亏自行承担。
|
||||
|
||||
6.2 因行情、交易所接口变更、网络故障、云厂商故障、乙方误操作等导致的交易或间接损失,在法律允许范围内甲方不承担责任;因甲方故意或重大过失造成的服务长时间不可用除外(可约定:连续不可用超过 ____ 小时按比例退还当期程序使用费)。
|
||||
|
||||
6.3 甲方保证其有权提供本托管服务并享有本软件著作权;乙方保证身份信息及资金账户来源合法。
|
||||
|
||||
---
|
||||
|
||||
## 第七条 违约责任
|
||||
|
||||
7.1 乙方欠费、外传账号、试图获取源码或违反第二条的,甲方有权**暂停或立即终止服务**,并要求:
|
||||
1. 停止违约行为;
|
||||
2. 支付欠费及违约金人民币 ________ 元(或按实际损失);
|
||||
3. 赔偿甲方维权合理费用。
|
||||
|
||||
7.2 甲方无正当理由逾期未完成首次部署超过 ______ 日,或恶意长期停服且无合理解释的,乙方有权解除合同并要求退还已付未消费部分(部署费处理按 3.4 约定)。
|
||||
|
||||
---
|
||||
|
||||
## 第八条 合同解除与终止
|
||||
|
||||
8.1 协商一致可书面解除。
|
||||
|
||||
8.2 一方严重违约,守约方书面通知后 ______ 日内仍未改正的,守约方可解除。
|
||||
|
||||
8.3 终止后:乙方停止使用;甲方关闭访问权限;双方按第四条、第五条履行数据与保密义务。
|
||||
|
||||
---
|
||||
|
||||
## 第九条 争议解决
|
||||
|
||||
因本合同引起的争议,双方协商解决;协商不成的,提交甲方住所地有管辖权的人民法院诉讼解决(或:提交 ________ 仲裁委员会仲裁)。
|
||||
|
||||
---
|
||||
|
||||
## 第十条 其他
|
||||
|
||||
10.1 未尽事宜可签订补充协议。
|
||||
|
||||
10.2 本合同一式贰份,甲乙双方各执壹份,具有同等法律效力。
|
||||
|
||||
10.3 附件(如有):□《著作权声明》副本 □《服务说明与报价说明》 □ 服务清单 / 报价单 □ 域名与实例列表 □ 其他:________
|
||||
|
||||
---
|
||||
|
||||
## 签署栏
|
||||
|
||||
**甲方(服务提供方 / 著作权人):**
|
||||
|
||||
签名:________________ 日期:______ 年 ____ 月 ____ 日
|
||||
|
||||
**乙方(客户):**
|
||||
|
||||
签名 / 盖章:________________ 日期:______ 年 ____ 月 ____ 日
|
||||
|
||||
---
|
||||
|
||||
## 填写提示(签署前可删本段)
|
||||
|
||||
1. **标准商业路径**:专属服务器费 + 域名费 + 部署费(首单)+ 程序使用费(按月/年)— 四项建议在报价单里写清,合同 3.1 表格与报价一致。
|
||||
2. **一用户一服务器**:新客户开新机器;不要把多名客户塞进同一台 VPS。
|
||||
3. **源码**:默认一律不交付;若客户坚持私有化,应另签高价「源码许可/买断」合同,与本托管模板分开。
|
||||
4. **自用**:著作权人本人使用无需签本合同,见《著作权声明》。
|
||||
5. **不要**在仓库添加开源 `LICENSE`(MIT 等),与「保留所有权利 + 托管授权」冲突。
|
||||
@@ -61,6 +61,11 @@
|
||||
document.querySelectorAll(".embed-top-nav [data-embed-tab]").forEach((a) => {
|
||||
a.classList.toggle("active", a.getAttribute("data-embed-tab") === tab);
|
||||
});
|
||||
if (global.InstanceMobileNav && typeof global.InstanceMobileNav.onTabChange === "function") {
|
||||
global.InstanceMobileNav.onTabChange(tab);
|
||||
} else if (global.InstanceMobileNav && typeof global.InstanceMobileNav.syncTabActive === "function") {
|
||||
global.InstanceMobileNav.syncTabActive(tab);
|
||||
}
|
||||
}
|
||||
|
||||
function pageNavAllowed(tab) {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 实例手机壳: ≤720px 底栏 +「更多」,与 embed soft-nav 同步.
|
||||
*/
|
||||
(function (global) {
|
||||
const PRIMARY = { trade: 1, key_monitor: 1, options: 1 };
|
||||
const MQ = "(max-width: 720px)";
|
||||
|
||||
function isEmbedShell() {
|
||||
return document.body && document.body.getAttribute("data-embed-shell") === "1";
|
||||
}
|
||||
|
||||
function isMobileLayout() {
|
||||
return window.matchMedia(MQ).matches;
|
||||
}
|
||||
|
||||
function syncPhoneClass() {
|
||||
if (!document.body) return;
|
||||
document.body.classList.toggle("inst-phone", isMobileLayout());
|
||||
}
|
||||
|
||||
function currentTab() {
|
||||
if (global.InstanceEmbed && typeof global.InstanceEmbed.getTab === "function") {
|
||||
return global.InstanceEmbed.getTab();
|
||||
}
|
||||
try {
|
||||
const t = new URLSearchParams(location.search).get("tab");
|
||||
if (t) return t;
|
||||
} catch (_) {}
|
||||
return (document.body && document.body.getAttribute("data-page")) || "trade";
|
||||
}
|
||||
|
||||
function closeMore() {
|
||||
document.body.classList.remove("inst-mobile-more-open");
|
||||
const more = document.getElementById("inst-mobile-more");
|
||||
const btn = document.getElementById("inst-m-tab-more");
|
||||
if (more) more.setAttribute("aria-hidden", "true");
|
||||
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||
syncTabActive(currentTab());
|
||||
}
|
||||
|
||||
function openMore() {
|
||||
if (!isMobileLayout()) return;
|
||||
document.body.classList.add("inst-mobile-more-open");
|
||||
const more = document.getElementById("inst-mobile-more");
|
||||
const btn = document.getElementById("inst-m-tab-more");
|
||||
if (more) more.setAttribute("aria-hidden", "false");
|
||||
if (btn) btn.setAttribute("aria-expanded", "true");
|
||||
syncTabActive(currentTab());
|
||||
}
|
||||
|
||||
function toggleMore() {
|
||||
if (document.body.classList.contains("inst-mobile-more-open")) closeMore();
|
||||
else openMore();
|
||||
}
|
||||
|
||||
function syncTabActive(tab) {
|
||||
const page = tab || currentTab();
|
||||
const primary = !!PRIMARY[page];
|
||||
const moreOpen = document.body.classList.contains("inst-mobile-more-open");
|
||||
document.querySelectorAll("#inst-mobile-tabbar .inst-m-tab").forEach((el) => {
|
||||
const t = el.getAttribute("data-embed-tab") || "";
|
||||
let on = false;
|
||||
if (t === "more") on = moreOpen || !primary;
|
||||
else on = !moreOpen && t === page;
|
||||
el.classList.toggle("active", on);
|
||||
});
|
||||
document.querySelectorAll("#inst-mobile-more .inst-mobile-more-nav [data-embed-tab]").forEach((a) => {
|
||||
a.classList.toggle("active", a.getAttribute("data-embed-tab") === page);
|
||||
});
|
||||
}
|
||||
|
||||
/** embed 切页时关闭「更多」并同步高亮 */
|
||||
function onTabChange(tab) {
|
||||
document.body.classList.remove("inst-mobile-more-open");
|
||||
const more = document.getElementById("inst-mobile-more");
|
||||
const btn = document.getElementById("inst-m-tab-more");
|
||||
if (more) more.setAttribute("aria-hidden", "true");
|
||||
if (btn) btn.setAttribute("aria-expanded", "false");
|
||||
syncTabActive(tab);
|
||||
}
|
||||
|
||||
function goTab(tab) {
|
||||
if (!tab || tab === "more") return;
|
||||
closeMore();
|
||||
if (global.InstanceEmbed && typeof global.InstanceEmbed.loadTab === "function") {
|
||||
if (tab === currentTab()) {
|
||||
syncTabActive(tab);
|
||||
return;
|
||||
}
|
||||
void global.InstanceEmbed.loadTab(tab);
|
||||
return;
|
||||
}
|
||||
const pathMap = {
|
||||
dashboard: "/dashboard",
|
||||
key_monitor: "/key_monitor",
|
||||
trade: "/trade",
|
||||
strategy: "/strategy",
|
||||
strategy_records: "/strategy/records",
|
||||
options: "/options",
|
||||
options_review: "/options/review",
|
||||
hedge_plan: "/hedge-plan",
|
||||
records: "/records",
|
||||
stats: "/stats",
|
||||
risk_policy: "/risk_policy",
|
||||
system_guide: "/system_guide",
|
||||
env_config: "/env_config",
|
||||
settings: "/settings",
|
||||
};
|
||||
location.href = pathMap[tab] || "/trade";
|
||||
}
|
||||
|
||||
function bindChrome() {
|
||||
const moreBtn = document.getElementById("inst-m-tab-more");
|
||||
const backdrop = document.getElementById("inst-mobile-more-backdrop");
|
||||
const closeBtn = document.getElementById("inst-mobile-more-close");
|
||||
if (moreBtn) {
|
||||
moreBtn.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
toggleMore();
|
||||
});
|
||||
}
|
||||
if (backdrop) backdrop.addEventListener("click", closeMore);
|
||||
if (closeBtn) closeBtn.addEventListener("click", closeMore);
|
||||
document.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Escape" && document.body.classList.contains("inst-mobile-more-open")) {
|
||||
closeMore();
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll("#inst-mobile-tabbar .inst-m-tab[data-embed-tab]").forEach((el) => {
|
||||
if (el.getAttribute("data-embed-tab") === "more") return;
|
||||
el.addEventListener("click", (ev) => {
|
||||
if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
|
||||
ev.preventDefault();
|
||||
goTab(el.getAttribute("data-embed-tab"));
|
||||
});
|
||||
});
|
||||
document.querySelectorAll("#inst-mobile-more .inst-mobile-more-nav [data-embed-tab]").forEach((a) => {
|
||||
a.addEventListener("click", (ev) => {
|
||||
if (ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
|
||||
ev.preventDefault();
|
||||
goTab(a.getAttribute("data-embed-tab"));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function boot() {
|
||||
if (!isEmbedShell()) return;
|
||||
if (!document.getElementById("inst-mobile-tabbar")) return;
|
||||
syncPhoneClass();
|
||||
bindChrome();
|
||||
syncTabActive(currentTab());
|
||||
let resizeTimer = null;
|
||||
window.addEventListener("resize", () => {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(() => {
|
||||
const was = document.body.classList.contains("inst-phone");
|
||||
syncPhoneClass();
|
||||
if (!isMobileLayout()) closeMore();
|
||||
else if (!was) syncTabActive(currentTab());
|
||||
}, 120);
|
||||
});
|
||||
}
|
||||
|
||||
global.InstanceMobileNav = {
|
||||
syncTabActive,
|
||||
onTabChange,
|
||||
closeMore,
|
||||
isMobileLayout,
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", boot);
|
||||
} else {
|
||||
boot();
|
||||
}
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
@@ -31,6 +31,8 @@
|
||||
function applyDisplayToNav(display) {
|
||||
const map = {
|
||||
dashboard: "show_nav_dashboard",
|
||||
key_monitor: "show_nav_key_monitor",
|
||||
trade: "show_nav_trade",
|
||||
strategy: "show_nav_strategy",
|
||||
strategy_records: "show_nav_strategy_records",
|
||||
records: "show_nav_records",
|
||||
@@ -44,14 +46,19 @@
|
||||
system_guide: "show_nav_system_guide",
|
||||
env_config: "show_nav_env_config",
|
||||
};
|
||||
document.querySelectorAll(".embed-top-nav [data-embed-tab], .top-nav a[href^='/']").forEach((a) => {
|
||||
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
||||
const key = map[tab];
|
||||
if (!key) return;
|
||||
const show = navPrefShow(display, key);
|
||||
a.classList.toggle("nav-hidden", !show);
|
||||
a.style.display = show ? "" : "none";
|
||||
});
|
||||
document
|
||||
.querySelectorAll(
|
||||
".embed-top-nav [data-embed-tab], .top-nav a[href^='/'], #inst-mobile-tabbar [data-embed-tab], #inst-mobile-more [data-embed-tab]"
|
||||
)
|
||||
.forEach((a) => {
|
||||
const tab = a.getAttribute("data-embed-tab") || (a.getAttribute("href") || "").replace(/^\//, "").split("?")[0];
|
||||
if (tab === "more") return;
|
||||
const key = map[tab];
|
||||
if (!key) return;
|
||||
const show = navPrefShow(display, key);
|
||||
a.classList.toggle("nav-hidden", !show);
|
||||
a.style.display = show ? "" : "none";
|
||||
});
|
||||
global.__INSTANCE_DISPLAY__ = display;
|
||||
}
|
||||
|
||||
@@ -59,6 +66,8 @@
|
||||
const d = DISPLAY();
|
||||
const map = {
|
||||
dashboard: "show_nav_dashboard",
|
||||
key_monitor: "show_nav_key_monitor",
|
||||
trade: "show_nav_trade",
|
||||
strategy: "show_nav_strategy",
|
||||
strategy_records: "show_nav_strategy_records",
|
||||
records: "show_nav_records",
|
||||
|
||||
@@ -5526,3 +5526,441 @@ html[data-theme="light"] .options-review-wrap .or-reviewed-table tbody tr:hover
|
||||
color: #ff8b8b;
|
||||
}
|
||||
|
||||
/* —— 实例手机壳:底栏四件套(仅 ≤720px + body.inst-phone) —— */
|
||||
.inst-mobile-tabbar,
|
||||
.inst-mobile-more,
|
||||
.instance-phone-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
:root {
|
||||
--inst-m-tabbar-h: 56px;
|
||||
--inst-m-page-pad: calc(var(--inst-m-tabbar-h) + max(16px, env(safe-area-inset-bottom)) + 12px);
|
||||
}
|
||||
|
||||
/* 仅手机壳:不碰 >720 桌面/平板。overflow 放 container,避免裁切 fixed 下单弹窗 */
|
||||
body.inst-phone {
|
||||
padding-bottom: var(--inst-m-page-pad) !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.inst-phone .embed-top-nav.top-nav {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .header {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
body.inst-phone .header h1 {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .container {
|
||||
padding-bottom: 8px !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
body.inst-phone #embed-page-root,
|
||||
body.inst-phone .embed-tab-pane,
|
||||
body.inst-phone .embed-tab-pane.is-active-pane {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body.inst-phone .card {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-header-toolbar {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-header-toolbar-end {
|
||||
width: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-phone-only {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.inst-phone .instance-header-phone-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .instance-header-phone-strip {
|
||||
border-top-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
min-width: 0;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
font-size: 12px;
|
||||
color: var(--inst-text);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-phone-chip {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip em {
|
||||
font-style: normal;
|
||||
color: var(--inst-muted);
|
||||
font-size: 11px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip b {
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-phone-chip--pnl b {
|
||||
color: var(--inst-nav-idle);
|
||||
}
|
||||
|
||||
/* 实盘/关键位等表单:窄屏拉满,避免挤成一行裁切 */
|
||||
body.inst-phone #add-order-form.form-row,
|
||||
body.inst-phone form.form-row {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
body.inst-phone #add-order-form.form-row > input:not([type="checkbox"]):not([type="radio"]),
|
||||
body.inst-phone #add-order-form.form-row > select,
|
||||
body.inst-phone #add-order-form #sltp-mode,
|
||||
body.inst-phone form.form-row > input:not([type="checkbox"]):not([type="radio"]),
|
||||
body.inst-phone form.form-row > select {
|
||||
flex: 1 1 100% !important;
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body.inst-phone #add-order-form .order-entry-model-row,
|
||||
body.inst-phone #add-order-form .order-time-close-wrap,
|
||||
body.inst-phone #add-order-form > label,
|
||||
body.inst-phone #add-order-form > button {
|
||||
flex: 1 1 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone #add-order-form .order-entry-model-row select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .order-plan-preview {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 期权:防止宽表撑破页面;表内横向滑看「操作」 */
|
||||
body.inst-phone .options-page-wrap,
|
||||
body.inst-phone .options-dual-grid,
|
||||
body.inst-phone .options-order-card,
|
||||
body.inst-phone .options-pos-card-wrap {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.inst-phone .options-dual-grid {
|
||||
grid-template-columns: minmax(0, 1fr) !important;
|
||||
}
|
||||
|
||||
body.inst-phone .options-chain-toolbar.form-row {
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
body.inst-phone .options-chain-toolbar .btn-secondary,
|
||||
body.inst-phone .options-chain-toolbar select,
|
||||
body.inst-phone .options-chain-toolbar .opt-chain-view-group,
|
||||
body.inst-phone .options-chain-toolbar .opt-type-btn-group {
|
||||
flex: 0 1 auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .options-strike-table-wrap,
|
||||
body.inst-phone .options-strike-table-wrap--t,
|
||||
body.inst-phone .options-history-table-wrap,
|
||||
body.inst-phone .table-wrap {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior-x: contain;
|
||||
touch-action: pan-x pan-y;
|
||||
}
|
||||
|
||||
body.inst-phone .options-strike-table {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .options-strike-table th,
|
||||
body.inst-phone .options-strike-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 列表视图:隐藏合约 / 买一 / 到期平衡 / 距平衡(保留行权价·类型·卖一·操作) */
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(3),
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(5),
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(6),
|
||||
body.inst-phone #opt-strike-head-list th:nth-child(7),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(3),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(5),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(6),
|
||||
body.inst-phone .opt-strike-row:not(.opt-strike-row-t) > td:nth-child(7) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 选择后下单弹窗:盖过底栏,可滚动完整显示 */
|
||||
body.inst-phone .opt-order-backdrop {
|
||||
z-index: 2400;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
}
|
||||
|
||||
body.inst-phone .opt-order-backdrop:not([hidden]) {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
body.inst-phone .opt-order-dialog {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: min(88vh, 720px);
|
||||
margin: 0;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 14px 14px calc(14px + env(safe-area-inset-bottom));
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
body.inst-phone .opt-order-dialog .options-order-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.inst-phone .options-estimate-row .opt-est-main {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
body.inst-phone .opt-size-mode-bar {
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-tabbar {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 80;
|
||||
height: calc(var(--inst-m-tabbar-h) + env(safe-area-inset-bottom));
|
||||
padding: 0 max(8px, env(safe-area-inset-right)) env(safe-area-inset-bottom)
|
||||
max(8px, env(safe-area-inset-left));
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
gap: 2px;
|
||||
background: color-mix(in srgb, #12161f 92%, transparent);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-tabbar {
|
||||
background: color-mix(in srgb, #f4f7fb 94%, transparent);
|
||||
border-top-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-tabbar .inst-m-tab.nav-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-m-tab {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 6px 2px;
|
||||
padding: 0 4px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--inst-muted);
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-m-tab:hover,
|
||||
body.inst-phone .inst-m-tab:focus-visible {
|
||||
color: var(--inst-text);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-m-tab.active {
|
||||
color: var(--inst-nav-idle);
|
||||
background: rgba(143, 200, 255, 0.12);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--inst-nav-idle) 35%, transparent);
|
||||
}
|
||||
|
||||
body.inst-phone.inst-mobile-more-open .inst-mobile-more {
|
||||
display: block;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-sheet {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
max-height: min(78vh, 560px);
|
||||
overflow: auto;
|
||||
padding: 10px 16px calc(16px + env(safe-area-inset-bottom));
|
||||
border-radius: 16px 16px 0 0;
|
||||
background: #12161f;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: none;
|
||||
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-sheet {
|
||||
background: #f4f7fb;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-handle {
|
||||
width: 36px;
|
||||
height: 4px;
|
||||
margin: 2px auto 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-handle {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 1rem;
|
||||
color: var(--inst-text);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-hint {
|
||||
margin: 0 0 14px;
|
||||
font-size: 11px;
|
||||
color: var(--inst-muted);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 44px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--inst-text);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-nav a {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav a.nav-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-nav a.active {
|
||||
border-color: color-mix(in srgb, var(--inst-nav-idle) 45%, transparent);
|
||||
background: rgba(143, 200, 255, 0.12);
|
||||
color: var(--inst-nav-idle);
|
||||
}
|
||||
|
||||
body.inst-phone .inst-mobile-more-close {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
min-height: 44px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: transparent;
|
||||
color: var(--inst-text);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
html[data-theme="light"] body.inst-phone .inst-mobile-more-close {
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+888
-833
File diff suppressed because it is too large
Load Diff
@@ -71,6 +71,7 @@ def install_instance_theme_static(app) -> None:
|
||||
"strategy_roll.js": "application/javascript; charset=utf-8",
|
||||
"instance_page.css": "text/css; charset=utf-8",
|
||||
"instance_embed.js": "application/javascript; charset=utf-8",
|
||||
"instance_mobile_nav.js": "application/javascript; charset=utf-8",
|
||||
"instance_stats.js": "application/javascript; charset=utf-8",
|
||||
"instance_live.js": "application/javascript; charset=utf-8",
|
||||
"instance_settings_prefs.js": "application/javascript; charset=utf-8",
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
"""中控永期对冲计算器:永续 1 币 + 按目标盈利反推期权仓位/波动点数(纯函数)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
from lib.trade.trade_fee_lib import estimate_roundtrip_fee_usdt, taker_fee_rate
|
||||
|
||||
DEFAULT_CT_MULT = 0.01
|
||||
PERP_COINS = 1.0
|
||||
|
||||
|
||||
def _f(v: Any) -> Optional[float]:
|
||||
if v is None or v == "":
|
||||
return None
|
||||
try:
|
||||
return float(v)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _parse_base_common(
|
||||
*,
|
||||
base: str,
|
||||
spot: Any,
|
||||
capital_usdt: Any,
|
||||
target_profit_u: Any,
|
||||
perp_leverage: Any,
|
||||
option_leverage: Any,
|
||||
ct_mult: Any,
|
||||
) -> Tuple[Optional[dict[str, float]], Optional[str]]:
|
||||
b = (base or "ETH").strip().upper()
|
||||
if b not in ("ETH", "BTC"):
|
||||
return None, "币种仅支持 BTC / ETH"
|
||||
s = _f(spot)
|
||||
capital = _f(capital_usdt)
|
||||
target = _f(target_profit_u)
|
||||
p_lev = _f(perp_leverage)
|
||||
o_lev = _f(option_leverage)
|
||||
ct = _f(ct_mult)
|
||||
if s is None or capital is None or target is None or p_lev is None or o_lev is None:
|
||||
return None, "参数格式错误"
|
||||
if ct is None or ct <= 0:
|
||||
ct = DEFAULT_CT_MULT
|
||||
if s <= 0 or capital <= 0 or p_lev <= 0 or o_lev <= 0:
|
||||
return None, "现价、资金、杠杆须大于 0"
|
||||
if target < 0:
|
||||
return None, "目标盈利不能为负"
|
||||
prem_per_coin = s / o_lev
|
||||
if prem_per_coin <= 0:
|
||||
return None, "单币权利金无效"
|
||||
margin = (s * PERP_COINS) / p_lev
|
||||
return {
|
||||
"base_ok": 1.0,
|
||||
"spot": s,
|
||||
"capital": capital,
|
||||
"target": target,
|
||||
"p_lev": p_lev,
|
||||
"o_lev": o_lev,
|
||||
"ct": ct,
|
||||
"prem_per_coin": prem_per_coin,
|
||||
"margin": margin,
|
||||
"fee_rate": taker_fee_rate(),
|
||||
}, None
|
||||
|
||||
|
||||
def _move_for_perp_correct(*, spot: float, target: float, premium: float, fee_rate: float) -> float:
|
||||
"""净利 = move − premium − fee(move) = target → 解 move.
|
||||
|
||||
fee = (2*spot + move) * fee_rate
|
||||
move*(1-fee_rate) = target + premium + 2*spot*fee_rate
|
||||
"""
|
||||
denom = 1.0 - float(fee_rate)
|
||||
if denom <= 0:
|
||||
return 0.0
|
||||
return (float(target) + float(premium) + 2.0 * float(spot) * float(fee_rate)) / denom
|
||||
|
||||
|
||||
def calc_perp_options_hedge(
|
||||
*,
|
||||
base: str = "ETH",
|
||||
spot: float,
|
||||
capital_usdt: float,
|
||||
target_profit_u: float,
|
||||
move_mode: str = "points",
|
||||
move_value: float,
|
||||
perp_leverage: float,
|
||||
option_leverage: float,
|
||||
ct_mult: float = DEFAULT_CT_MULT,
|
||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
"""由波动反推期权开仓币数/张数(calc_mode=size)."""
|
||||
common, err = _parse_base_common(
|
||||
base=base,
|
||||
spot=spot,
|
||||
capital_usdt=capital_usdt,
|
||||
target_profit_u=target_profit_u,
|
||||
perp_leverage=perp_leverage,
|
||||
option_leverage=option_leverage,
|
||||
ct_mult=ct_mult,
|
||||
)
|
||||
if err or not common:
|
||||
return None, err
|
||||
|
||||
s = common["spot"]
|
||||
capital = common["capital"]
|
||||
target = common["target"]
|
||||
p_lev = common["p_lev"]
|
||||
o_lev = common["o_lev"]
|
||||
ct = common["ct"]
|
||||
prem_per_coin = common["prem_per_coin"]
|
||||
margin = common["margin"]
|
||||
fee_rate = common["fee_rate"]
|
||||
b = (base or "ETH").strip().upper()
|
||||
|
||||
move = _f(move_value)
|
||||
mode = (move_mode or "points").strip().lower()
|
||||
if mode not in ("points", "pct", "percent", "rate"):
|
||||
return None, "波动模式须为 points 或 pct"
|
||||
if mode in ("percent", "rate"):
|
||||
mode = "pct"
|
||||
if move is None:
|
||||
return None, "参数格式错误"
|
||||
if move <= 0:
|
||||
return None, "现价、资金、波动、杠杆须大于 0"
|
||||
|
||||
if mode == "pct":
|
||||
move_points = s * (move / 100.0)
|
||||
else:
|
||||
move_points = move
|
||||
if move_points <= 0:
|
||||
return None, "波动对应价格变动须大于 0"
|
||||
|
||||
exit_px = s + move_points
|
||||
perp_gross = move_points * PERP_COINS
|
||||
fee = estimate_roundtrip_fee_usdt(s, exit_px, qty=PERP_COINS, contract_size=1.0)
|
||||
|
||||
premium_budget = perp_gross - target - fee
|
||||
if premium_budget <= 0:
|
||||
return None, "波动收益不足以覆盖目标盈利+手续费,无法开期权"
|
||||
|
||||
opt_coins = premium_budget / prem_per_coin
|
||||
opt_sheets = opt_coins / ct
|
||||
premium_total = opt_coins * prem_per_coin
|
||||
|
||||
case_a_net = perp_gross - premium_total - fee
|
||||
opt_intrinsic = opt_coins * move_points
|
||||
opt_net = opt_intrinsic - premium_total
|
||||
perp_loss = -perp_gross
|
||||
portfolio_net = opt_net + perp_loss
|
||||
|
||||
return {
|
||||
"calc_mode": "size",
|
||||
"base": b,
|
||||
"spot": round(s, 8),
|
||||
"capital_usdt": round(capital, 8),
|
||||
"target_profit_u": round(target, 8),
|
||||
"move_mode": mode,
|
||||
"move_value": round(move, 8),
|
||||
"move_points": round(move_points, 8),
|
||||
"exit_price": round(exit_px, 8),
|
||||
"perp_coins": PERP_COINS,
|
||||
"perp_leverage": round(p_lev, 8),
|
||||
"option_leverage": round(o_lev, 8),
|
||||
"ct_mult": ct,
|
||||
"prem_per_coin": round(prem_per_coin, 8),
|
||||
"perp_gross_u": round(perp_gross, 8),
|
||||
"perp_fee_u": round(fee, 8),
|
||||
"fee_rate": fee_rate,
|
||||
"premium_budget_u": round(premium_budget, 8),
|
||||
"opt_coins": round(opt_coins, 8),
|
||||
"opt_sheets": round(opt_sheets, 8),
|
||||
"premium_total_u": round(premium_total, 8),
|
||||
"perp_margin_u": round(margin, 8),
|
||||
"capital_ok": bool(capital >= margin),
|
||||
"case_a": {
|
||||
"label": "永续方向对",
|
||||
"perp_pnl_u": round(perp_gross, 8),
|
||||
"premium_u": round(premium_total, 8),
|
||||
"fee_u": round(fee, 8),
|
||||
"net_u": round(case_a_net, 8),
|
||||
},
|
||||
"case_b": {
|
||||
"label": "期权方向对",
|
||||
"opt_intrinsic_u": round(opt_intrinsic, 8),
|
||||
"premium_u": round(premium_total, 8),
|
||||
"opt_net_u": round(opt_net, 8),
|
||||
"perp_pnl_u": round(perp_loss, 8),
|
||||
"portfolio_net_u": round(portfolio_net, 8),
|
||||
},
|
||||
}, None
|
||||
|
||||
|
||||
def calc_perp_options_points(
|
||||
*,
|
||||
base: str = "ETH",
|
||||
spot: float,
|
||||
capital_usdt: float,
|
||||
target_profit_u: float,
|
||||
perp_leverage: float,
|
||||
option_leverage: float,
|
||||
ratio_perp: float = 1.0,
|
||||
ratio_opt: float = 2.0,
|
||||
ct_mult: float = DEFAULT_CT_MULT,
|
||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
"""按永续:期权比例 + 目标盈利,反推两套情景所需波动点数.
|
||||
|
||||
永续币数固定为 ratio 归一后的 1 币侧(perp_coins = PERP_COINS).
|
||||
期权币数 = PERP_COINS * (ratio_opt / ratio_perp),例 1:2 → 2 币.
|
||||
|
||||
A 永续方向对: move − premium − fee(move) = 目标盈利
|
||||
B 期权方向对:
|
||||
- 期权净利达目标: opt_coins*move − premium = 目标
|
||||
- 组合净利达目标: move*(opt_coins − perp_coins) − premium = 目标
|
||||
"""
|
||||
common, err = _parse_base_common(
|
||||
base=base,
|
||||
spot=spot,
|
||||
capital_usdt=capital_usdt,
|
||||
target_profit_u=target_profit_u,
|
||||
perp_leverage=perp_leverage,
|
||||
option_leverage=option_leverage,
|
||||
ct_mult=ct_mult,
|
||||
)
|
||||
if err or not common:
|
||||
return None, err
|
||||
|
||||
rp = _f(ratio_perp)
|
||||
ro = _f(ratio_opt)
|
||||
if rp is None or ro is None or rp <= 0 or ro <= 0:
|
||||
return None, "永续:期权比例须大于 0"
|
||||
|
||||
s = common["spot"]
|
||||
capital = common["capital"]
|
||||
target = common["target"]
|
||||
p_lev = common["p_lev"]
|
||||
o_lev = common["o_lev"]
|
||||
ct = common["ct"]
|
||||
prem_per_coin = common["prem_per_coin"]
|
||||
margin = common["margin"]
|
||||
fee_rate = common["fee_rate"]
|
||||
b = (base or "ETH").strip().upper()
|
||||
|
||||
opt_coins = PERP_COINS * (ro / rp)
|
||||
premium_total = opt_coins * prem_per_coin
|
||||
opt_sheets = opt_coins / ct
|
||||
|
||||
move_a = _move_for_perp_correct(spot=s, target=target, premium=premium_total, fee_rate=fee_rate)
|
||||
if move_a <= 0:
|
||||
return None, "无法解出永续方向对所需点数"
|
||||
|
||||
fee_a = estimate_roundtrip_fee_usdt(s, s + move_a, qty=PERP_COINS, contract_size=1.0)
|
||||
net_a = move_a * PERP_COINS - premium_total - fee_a
|
||||
|
||||
# 期权净利 = 目标
|
||||
move_b_opt = (target + premium_total) / opt_coins
|
||||
opt_net_at_b_opt = opt_coins * move_b_opt - premium_total
|
||||
portfolio_at_b_opt = opt_net_at_b_opt - move_b_opt * PERP_COINS
|
||||
|
||||
# 组合净利 = 目标
|
||||
edge = opt_coins - PERP_COINS
|
||||
if edge <= 0:
|
||||
move_b_port = None
|
||||
port_err = "期权币数须大于永续币数,组合才能在方向对时赚到目标盈利"
|
||||
else:
|
||||
move_b_port = (target + premium_total) / edge
|
||||
port_err = None
|
||||
if move_b_port is not None:
|
||||
opt_net_at_b_port = opt_coins * move_b_port - premium_total
|
||||
portfolio_at_b_port = opt_net_at_b_port - move_b_port * PERP_COINS
|
||||
else:
|
||||
opt_net_at_b_port = None
|
||||
portfolio_at_b_port = None
|
||||
|
||||
return {
|
||||
"calc_mode": "points",
|
||||
"base": b,
|
||||
"spot": round(s, 8),
|
||||
"capital_usdt": round(capital, 8),
|
||||
"target_profit_u": round(target, 8),
|
||||
"ratio_perp": round(rp, 8),
|
||||
"ratio_opt": round(ro, 8),
|
||||
"ratio_label": f"{_fmt_ratio(rp)}:{_fmt_ratio(ro)}",
|
||||
"perp_coins": PERP_COINS,
|
||||
"opt_coins": round(opt_coins, 8),
|
||||
"opt_sheets": round(opt_sheets, 8),
|
||||
"perp_leverage": round(p_lev, 8),
|
||||
"option_leverage": round(o_lev, 8),
|
||||
"ct_mult": ct,
|
||||
"prem_per_coin": round(prem_per_coin, 8),
|
||||
"premium_total_u": round(premium_total, 8),
|
||||
"fee_rate": fee_rate,
|
||||
"perp_margin_u": round(margin, 8),
|
||||
"capital_ok": bool(capital >= margin),
|
||||
"case_a": {
|
||||
"label": "永续方向对",
|
||||
"move_points": round(move_a, 8),
|
||||
"move_pct": round(move_a / s * 100.0, 8),
|
||||
"perp_pnl_u": round(move_a * PERP_COINS, 8),
|
||||
"premium_u": round(premium_total, 8),
|
||||
"fee_u": round(fee_a, 8),
|
||||
"net_u": round(net_a, 8),
|
||||
},
|
||||
"case_b": {
|
||||
"label": "期权方向对",
|
||||
"move_points_opt_net": round(move_b_opt, 8),
|
||||
"move_pct_opt_net": round(move_b_opt / s * 100.0, 8),
|
||||
"opt_net_u": round(opt_net_at_b_opt, 8),
|
||||
"portfolio_net_at_opt_target_u": round(portfolio_at_b_opt, 8),
|
||||
"move_points_portfolio": None if move_b_port is None else round(move_b_port, 8),
|
||||
"move_pct_portfolio": None
|
||||
if move_b_port is None
|
||||
else round(move_b_port / s * 100.0, 8),
|
||||
"opt_net_at_portfolio_target_u": None
|
||||
if opt_net_at_b_port is None
|
||||
else round(opt_net_at_b_port, 8),
|
||||
"portfolio_net_u": None if portfolio_at_b_port is None else round(portfolio_at_b_port, 8),
|
||||
"portfolio_error": port_err,
|
||||
"premium_u": round(premium_total, 8),
|
||||
},
|
||||
}, None
|
||||
|
||||
|
||||
def _fmt_ratio(v: float) -> str:
|
||||
if abs(v - round(v)) < 1e-9:
|
||||
return str(int(round(v)))
|
||||
s = f"{v:.4f}".rstrip("0").rstrip(".")
|
||||
return s
|
||||
|
||||
|
||||
def calc_perp_options(
|
||||
*,
|
||||
calc_mode: str = "size",
|
||||
**kwargs: Any,
|
||||
) -> Tuple[Optional[dict[str, Any]], Optional[str]]:
|
||||
"""统一入口:size=由波动推仓位;points=由比例推点数."""
|
||||
mode = (calc_mode or "size").strip().lower()
|
||||
if mode in ("points", "ratio", "move"):
|
||||
return calc_perp_options_points(**kwargs)
|
||||
# size mode: ignore ratio kwargs if present
|
||||
kwargs.pop("ratio_perp", None)
|
||||
kwargs.pop("ratio_opt", None)
|
||||
return calc_perp_options_hedge(**kwargs)
|
||||
@@ -8,7 +8,7 @@
|
||||
<link rel="stylesheet" href="/static/instance_theme_early.css?v=4">
|
||||
<link rel="stylesheet" href="/static/account_risk_badge.css?v=4">
|
||||
<link rel="stylesheet" href="/static/instance_page.css?v=11">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=105">
|
||||
<link rel="stylesheet" href="/static/instance_theme.css?v=108">
|
||||
<script src="/static/account_risk_badge.js?v=4"></script>
|
||||
<meta name="theme-color" content="#0b0d14">
|
||||
<title>{{ pwa_app_name }}</title>
|
||||
@@ -93,6 +93,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 手机端主导航(≤720px);桌面不显示 -->
|
||||
<nav id="inst-mobile-tabbar" class="inst-mobile-tabbar" aria-label="手机主导航">
|
||||
<a href="/trade" class="inst-m-tab{% if initial_tab == 'trade' %} active{% endif %}" data-embed-tab="trade"{% if not display.show_nav_trade %} style="display:none"{% endif %}>下单</a>
|
||||
<a href="/key_monitor" class="inst-m-tab{% if initial_tab == 'key_monitor' %} active{% endif %}" data-embed-tab="key_monitor"{% if not display.show_nav_key_monitor %} style="display:none"{% endif %}>关键位</a>
|
||||
{% if options_nav_visible and display.show_nav_options %}
|
||||
<a href="/options" class="inst-m-tab{% if initial_tab == 'options' %} active{% endif %}" data-embed-tab="options">期权</a>
|
||||
{% endif %}
|
||||
<button type="button" class="inst-m-tab" data-embed-tab="more" id="inst-m-tab-more" aria-haspopup="dialog" aria-expanded="false">更多</button>
|
||||
</nav>
|
||||
<div id="inst-mobile-more" class="inst-mobile-more" aria-hidden="true">
|
||||
<div class="inst-mobile-more-backdrop" id="inst-mobile-more-backdrop"></div>
|
||||
<div class="inst-mobile-more-sheet" role="dialog" aria-modal="true" aria-labelledby="inst-mobile-more-title">
|
||||
<div class="inst-mobile-more-handle" aria-hidden="true"></div>
|
||||
<h2 id="inst-mobile-more-title" class="inst-mobile-more-title">更多</h2>
|
||||
<p class="inst-mobile-more-hint">次要页面 · 完整界面请用电脑</p>
|
||||
<nav class="inst-mobile-more-nav" aria-label="更多页面">
|
||||
<a href="/dashboard" data-embed-tab="dashboard"{% if not display.show_nav_dashboard %} style="display:none"{% endif %}>数据看板</a>
|
||||
{% if display.show_nav_records %}
|
||||
<a href="/records" data-embed-tab="records">交易记录</a>
|
||||
{% endif %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy %}
|
||||
<a href="/strategy" data-embed-tab="strategy">策略交易</a>
|
||||
{% endif %}
|
||||
{% if not intraday_discipline and display.show_nav_strategy_records %}
|
||||
<a href="/strategy/records" data-embed-tab="strategy_records">策略记录</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_stats %}
|
||||
<a href="/stats" data-embed-tab="stats">统计分析</a>
|
||||
{% endif %}
|
||||
{% if options_nav_visible and display.show_nav_options_review %}
|
||||
<a href="/options/review" data-embed-tab="options_review">期权复盘</a>
|
||||
{% endif %}
|
||||
{% if hedge_plan_nav_visible and display.show_nav_hedge_plan %}
|
||||
<a href="/hedge-plan" data-embed-tab="hedge_plan">对冲计划</a>
|
||||
{% endif %}
|
||||
{% if display.show_nav_risk_policy %}
|
||||
<a href="/risk_policy" data-embed-tab="risk_policy">风控说明</a>
|
||||
{% endif %}
|
||||
<a href="/system_guide" data-embed-tab="system_guide"{% if not display.show_nav_system_guide %} style="display:none"{% endif %}>系统说明</a>
|
||||
{% if display.show_nav_env_config %}
|
||||
<a href="/env_config" data-embed-tab="env_config">env配置</a>
|
||||
{% endif %}
|
||||
<a href="/settings" data-embed-tab="settings">系统设置</a>
|
||||
</nav>
|
||||
<button type="button" class="inst-mobile-more-close" id="inst-mobile-more-close">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/instance_ui.js?v=10"></script>
|
||||
<script src="/static/journal_upload_slots.js?v=4"></script>
|
||||
<script src="/static/instance_records_mobile.js?v=2"></script>
|
||||
@@ -118,8 +166,9 @@ const ORDER_ENTRY_MODEL_CODE_TO_CATEGORY = {{ entry_model_code_to_category | toj
|
||||
<script>
|
||||
window.__INSTANCE_DISPLAY__ = {{ display | tojson }};
|
||||
</script>
|
||||
<script src="/static/instance_settings_prefs.js?v=15"></script>
|
||||
<script src="/static/instance_settings_prefs.js?v=16"></script>
|
||||
<script src="/static/instance_live.js?v=6"></script>
|
||||
<script src="/static/instance_embed.js?v=28"></script>
|
||||
<script src="/static/instance_embed.js?v=29"></script>
|
||||
<script src="/static/instance_mobile_nav.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{# 统一顶栏:状态 + 筛选(上)· 统计条(下) #}
|
||||
<div class="instance-header-panel card">
|
||||
<div class="instance-header-toolbar">
|
||||
<div class="instance-header-toolbar-filter">
|
||||
<div class="instance-header-toolbar-filter instance-desktop-only">
|
||||
<span class="list-window-label" title="列表按 UTC 时间筛选,默认本月">UTC {{ list_window.label }}</span>
|
||||
<label class="list-window-preset">预设
|
||||
<select id="win-preset-select" onchange="toggleListWindowCustom()">
|
||||
@@ -37,4 +37,18 @@
|
||||
<div class="instance-header-stats-wrap instance-desktop-only">
|
||||
{% include 'instance_header_stats.html' %}
|
||||
</div>
|
||||
<div class="instance-header-phone-strip instance-phone-only" aria-label="手机资金摘要">
|
||||
<span class="inst-phone-chip">
|
||||
<em>交易</em>
|
||||
<b data-funds-field="current-capital">{{ funds_fmt(current_capital) }}U</b>
|
||||
</span>
|
||||
<span class="inst-phone-chip">
|
||||
<em>资金</em>
|
||||
<b data-funds-field="total-capital">{% if funding_usdt is not none %}{{ funds_fmt(funding_usdt) }}U{% else %}—{% endif %}</b>
|
||||
</span>
|
||||
<span class="inst-phone-chip">
|
||||
<em>总资</em>
|
||||
<b data-funds-field="total-funds">{% if total_funds is not none %}{{ funds_fmt(total_funds) }}U{% else %}—{% endif %}</b>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,7 @@ from lib.hub.amp_stats_lib import (
|
||||
build_export_csv,
|
||||
compute_amp_stats,
|
||||
export_filename,
|
||||
normalize_straddle_premium,
|
||||
normalize_take_profit,
|
||||
normalize_move_points,
|
||||
normalize_weekend_filter,
|
||||
reframe_amp_stats,
|
||||
rows_page,
|
||||
@@ -25,8 +24,7 @@ class ComputeBody(BaseModel):
|
||||
start_hour: int = 16
|
||||
period: str = "2m"
|
||||
custom_days: Optional[int] = None
|
||||
straddle_premium: Optional[float] = None
|
||||
take_profit: Optional[float] = None
|
||||
move_points: Optional[float] = None
|
||||
weekend_filter: str = "all"
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
@@ -37,15 +35,14 @@ class SaveBody(BaseModel):
|
||||
|
||||
|
||||
class ReframeBody(BaseModel):
|
||||
"""已有日表上改周末/权利金/止盈(不拉 K 线)."""
|
||||
"""已有日表上改周末/波动点数(不拉 K 线)."""
|
||||
|
||||
rows_all: list[dict[str, Any]] = Field(default_factory=list)
|
||||
symbol: str = "eth"
|
||||
start_hour: int = 16
|
||||
period: str = "2m"
|
||||
sample_days: int = 60
|
||||
straddle_premium: Optional[float] = None
|
||||
take_profit: Optional[float] = None
|
||||
move_points: Optional[float] = None
|
||||
weekend_filter: str = "all"
|
||||
price_source: str = ""
|
||||
inst_id: str = ""
|
||||
@@ -84,7 +81,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
"default_weekend_filter": "all",
|
||||
"timeframe": "1H",
|
||||
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
||||
"straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金",
|
||||
"move_points_note": "填波动点数后统计振幅≥该点数的天数占比;日表显示开→高/开→低两边点数与振幅是否达标",
|
||||
}
|
||||
|
||||
@router.post("/compute")
|
||||
@@ -95,8 +92,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
start_hour=body.start_hour,
|
||||
period=body.period,
|
||||
custom_days=body.custom_days,
|
||||
straddle_premium=body.straddle_premium,
|
||||
take_profit=body.take_profit,
|
||||
move_points=body.move_points,
|
||||
weekend_filter=body.weekend_filter,
|
||||
)
|
||||
except ValueError as exc:
|
||||
@@ -122,8 +118,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
start_hour=body.start_hour,
|
||||
period=body.period,
|
||||
sample_days=body.sample_days,
|
||||
straddle_premium=body.straddle_premium,
|
||||
take_profit=body.take_profit,
|
||||
move_points=body.move_points,
|
||||
weekend_filter=body.weekend_filter,
|
||||
price_source=body.price_source,
|
||||
inst_id=body.inst_id,
|
||||
@@ -165,8 +160,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
start_hour: int = Query(default=16),
|
||||
period: str = Query(default="2m"),
|
||||
custom_days: Optional[int] = Query(default=None),
|
||||
straddle_premium: Optional[float] = Query(default=None),
|
||||
take_profit: Optional[float] = Query(default=None),
|
||||
move_points: Optional[float] = Query(default=None),
|
||||
weekend_filter: str = Query(default="all"),
|
||||
):
|
||||
if (history_id or "").strip():
|
||||
@@ -174,6 +168,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="历史不存在")
|
||||
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:
|
||||
payload = reframe_amp_stats(
|
||||
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),
|
||||
period=str(item.get("period") or period),
|
||||
sample_days=int(item.get("sample_days_requested") or 60),
|
||||
straddle_premium=straddle_premium
|
||||
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"),
|
||||
move_points=use_mp,
|
||||
weekend_filter=weekend_filter or item.get("weekend_filter") or "all",
|
||||
price_source=str(item.get("price_source") 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
|
||||
else:
|
||||
try:
|
||||
# validate enums early
|
||||
normalize_weekend_filter(weekend_filter)
|
||||
normalize_straddle_premium(straddle_premium)
|
||||
normalize_take_profit(take_profit)
|
||||
normalize_move_points(move_points)
|
||||
payload = compute_amp_stats(
|
||||
symbol=symbol,
|
||||
start_hour=start_hour,
|
||||
period=period,
|
||||
custom_days=custom_days,
|
||||
straddle_premium=straddle_premium,
|
||||
take_profit=take_profit,
|
||||
move_points=move_points,
|
||||
weekend_filter=weekend_filter,
|
||||
)
|
||||
except ValueError as exc:
|
||||
|
||||
@@ -1269,6 +1269,21 @@ class RollCalculatorBody(BaseModel):
|
||||
base: str = "ETH"
|
||||
|
||||
|
||||
class PerpOptionsCalculatorBody(BaseModel):
|
||||
calc_mode: str = "size"
|
||||
base: str = "ETH"
|
||||
spot: float = Field(gt=0)
|
||||
capital_usdt: float = Field(gt=0)
|
||||
target_profit_u: float = Field(ge=0)
|
||||
move_mode: str = "points"
|
||||
move_value: float | None = None
|
||||
perp_leverage: float = Field(gt=0)
|
||||
option_leverage: float = Field(gt=0)
|
||||
ct_mult: float = Field(default=0.01, gt=0)
|
||||
ratio_perp: float = Field(default=1.0, gt=0)
|
||||
ratio_opt: float = Field(default=2.0, gt=0)
|
||||
|
||||
|
||||
class CompareOptionLegBody(BaseModel):
|
||||
opt_type: str = "C"
|
||||
strike: float | None = None
|
||||
@@ -1350,6 +1365,44 @@ def api_calculator_roll(body: RollCalculatorBody):
|
||||
return {"ok": True, "data": data}
|
||||
|
||||
|
||||
@app.post("/api/calculator/perp-options")
|
||||
def api_calculator_perp_options(body: PerpOptionsCalculatorBody):
|
||||
from lib.hub.hub_perp_options_calc_lib import calc_perp_options
|
||||
|
||||
mode = (body.calc_mode or "size").strip().lower()
|
||||
if mode in ("points", "ratio", "move"):
|
||||
data, err = calc_perp_options(
|
||||
calc_mode="points",
|
||||
base=body.base,
|
||||
spot=body.spot,
|
||||
capital_usdt=body.capital_usdt,
|
||||
target_profit_u=body.target_profit_u,
|
||||
perp_leverage=body.perp_leverage,
|
||||
option_leverage=body.option_leverage,
|
||||
ct_mult=body.ct_mult,
|
||||
ratio_perp=body.ratio_perp,
|
||||
ratio_opt=body.ratio_opt,
|
||||
)
|
||||
else:
|
||||
if body.move_value is None or body.move_value <= 0:
|
||||
return JSONResponse({"ok": False, "msg": "请填写波动数值"}, status_code=400)
|
||||
data, err = calc_perp_options(
|
||||
calc_mode="size",
|
||||
base=body.base,
|
||||
spot=body.spot,
|
||||
capital_usdt=body.capital_usdt,
|
||||
target_profit_u=body.target_profit_u,
|
||||
move_mode=body.move_mode,
|
||||
move_value=body.move_value,
|
||||
perp_leverage=body.perp_leverage,
|
||||
option_leverage=body.option_leverage,
|
||||
ct_mult=body.ct_mult,
|
||||
)
|
||||
if err:
|
||||
return JSONResponse({"ok": False, "msg": err}, status_code=400)
|
||||
return {"ok": True, "data": data}
|
||||
|
||||
|
||||
@app.post("/api/compare/calc")
|
||||
def api_compare_calc(body: CompareBody):
|
||||
from lib.hub.hub_compare_lib import run_compare
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 中控振幅统计:OKX ETH/BTC + 买跨/止盈/周末筛选.
|
||||
* 中控振幅统计:OKX ETH/BTC + 波动点数振幅占比 + 周末筛选.
|
||||
*/
|
||||
(function () {
|
||||
const page = document.getElementById("page-amp-stats");
|
||||
@@ -39,16 +39,8 @@
|
||||
return (n * 100).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
function readPremium() {
|
||||
const raw = (el("amp-straddle-premium")?.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();
|
||||
function readMovePoints() {
|
||||
const raw = (el("amp-move-points")?.value || "").trim();
|
||||
if (!raw) return null;
|
||||
const n = Number(raw);
|
||||
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) {
|
||||
const box = el("amp-summary");
|
||||
if (!box) return;
|
||||
const s = summary || {};
|
||||
if (!s.sample_count) {
|
||||
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
||||
renderStraddle(null);
|
||||
renderMoveStats(null);
|
||||
return;
|
||||
}
|
||||
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.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.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_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(result && result.price_source)}</span></div>` +
|
||||
`</div>`;
|
||||
renderStraddle(s.straddle);
|
||||
renderMoveStats(s.move_points_stats);
|
||||
}
|
||||
|
||||
function renderStraddle(st) {
|
||||
const box = el("amp-straddle");
|
||||
function renderMoveStats(ms) {
|
||||
const box = el("amp-move-stats");
|
||||
if (!box) return;
|
||||
if (!st) {
|
||||
box.innerHTML = '<p class="amp-empty">填写「买跨·双边权利金」后计算,可看越过天数与买跨点数盈亏</p>';
|
||||
if (!ms) {
|
||||
box.innerHTML = '<p class="amp-empty">填写「波动点数」后计算,可看振幅≥该点数的天数占比</p>';
|
||||
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 =
|
||||
`<div class="amp-sum-grid">` +
|
||||
`<div><span class="amp-sum-k">双边权利金</span><span class="amp-sum-v">${esc(st.premium)}</span></div>` +
|
||||
tpLine +
|
||||
`<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(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(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 ${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 ${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><span class="amp-sum-k">对照点数</span><span class="amp-sum-v">${esc(ms.move_points)}</span></div>` +
|
||||
`<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(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(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(ms.down_hit_days)} 天 · ${esc(pct(ms.down_hit_ratio))}</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>`;
|
||||
}
|
||||
|
||||
@@ -165,21 +138,31 @@
|
||||
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) {
|
||||
const body = el("amp-table-body");
|
||||
const pager = el("amp-pager");
|
||||
if (!body) return;
|
||||
const rows = (pagePayload && pagePayload.rows) || [];
|
||||
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 {
|
||||
body.innerHTML = rows
|
||||
.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 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 (
|
||||
`<tr${trClass}>` +
|
||||
`<td>${dayLabel(r)}</td>` +
|
||||
@@ -188,11 +171,12 @@
|
||||
`<td>${esc(r.high)}</td>` +
|
||||
`<td>${esc(r.low)}</td>` +
|
||||
`<td>${esc(r.close)}</td>` +
|
||||
`<td>${esc(r.up_points)}</td>` +
|
||||
`<td>${esc(r.down_points)}</td>` +
|
||||
`<td><strong>${esc(r.amplitude)}</strong></td>` +
|
||||
`<td${upCls}>${esc(r.up_points)}</td>` +
|
||||
`<td${downCls}>${esc(r.down_points)}</td>` +
|
||||
`<td${ampCls}><strong>${esc(r.amplitude)}</strong></td>` +
|
||||
`<td${amp2Cls}><strong>${amp2}</strong></td>` +
|
||||
`<td>${esc(r.change)}</td>` +
|
||||
`<td>${profit}</td>` +
|
||||
`<td>${hitCell(r)}</td>` +
|
||||
`</tr>`
|
||||
);
|
||||
})
|
||||
@@ -226,7 +210,7 @@
|
||||
|
||||
async function reframe(resetPage) {
|
||||
if (!lastResult) {
|
||||
renderStraddle(null);
|
||||
renderMoveStats(null);
|
||||
return;
|
||||
}
|
||||
if (resetPage) pageNo = 1;
|
||||
@@ -242,8 +226,7 @@
|
||||
start_hour: lastResult.start_hour ?? Number(el("amp-start-hour")?.value || 16),
|
||||
period: lastResult.period || el("amp-period")?.value || "2m",
|
||||
sample_days: lastResult.sample_days_requested || 60,
|
||||
straddle_premium: readPremium(),
|
||||
take_profit: readTakeProfit(),
|
||||
move_points: readMovePoints(),
|
||||
weekend_filter: readWeekend(),
|
||||
price_source: lastResult.price_source || "",
|
||||
inst_id: lastResult.inst_id || "",
|
||||
@@ -284,8 +267,7 @@
|
||||
start_hour: startHour,
|
||||
period,
|
||||
custom_days: period === "custom" ? customDays : null,
|
||||
straddle_premium: readPremium(),
|
||||
take_profit: readTakeProfit(),
|
||||
move_points: readMovePoints(),
|
||||
weekend_filter: readWeekend(),
|
||||
page: pageNo,
|
||||
page_size: 20,
|
||||
@@ -331,8 +313,7 @@
|
||||
const startHour = Number(el("amp-start-hour")?.value || 16);
|
||||
const period = el("amp-period")?.value || "2m";
|
||||
const customDays = Number(el("amp-custom-days")?.value || 60);
|
||||
const prem = readPremium();
|
||||
const tp = readTakeProfit();
|
||||
const mp = readMovePoints();
|
||||
const q = new URLSearchParams({
|
||||
symbol,
|
||||
start_hour: String(startHour),
|
||||
@@ -340,8 +321,7 @@
|
||||
weekend_filter: readWeekend(),
|
||||
});
|
||||
if (period === "custom") q.set("custom_days", String(customDays));
|
||||
if (prem != null) q.set("straddle_premium", String(prem));
|
||||
if (tp != null) q.set("take_profit", String(tp));
|
||||
if (mp != null) q.set("move_points", String(mp));
|
||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||
}
|
||||
|
||||
@@ -375,16 +355,13 @@
|
||||
const id = card.getAttribute("data-id");
|
||||
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
||||
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
||||
const prem = readPremium();
|
||||
const tp = readTakeProfit();
|
||||
let url =
|
||||
"/api/amp-stats/export?history_id=" +
|
||||
encodeURIComponent(id) +
|
||||
"&weekend_filter=" +
|
||||
encodeURIComponent(readWeekend());
|
||||
if (prem != null) url += "&straddle_premium=" + encodeURIComponent(String(prem));
|
||||
if (tp != null) url += "&take_profit=" + encodeURIComponent(String(tp));
|
||||
window.location.href = url;
|
||||
const mp = readMovePoints();
|
||||
const q = new URLSearchParams({
|
||||
history_id: id,
|
||||
weekend_filter: readWeekend(),
|
||||
});
|
||||
if (mp != null) q.set("move_points", String(mp));
|
||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||
});
|
||||
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
||||
if (!confirm("删除该历史记录?")) return;
|
||||
@@ -405,11 +382,8 @@
|
||||
if (lastResult) {
|
||||
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 (lastResult.straddle_premium != null && el("amp-straddle-premium")) {
|
||||
el("amp-straddle-premium").value = String(lastResult.straddle_premium);
|
||||
}
|
||||
if (lastResult.take_profit != null && el("amp-take-profit")) {
|
||||
el("amp-take-profit").value = String(lastResult.take_profit);
|
||||
if (lastResult.move_points != null && el("amp-move-points")) {
|
||||
el("amp-move-points").value = String(lastResult.move_points);
|
||||
}
|
||||
if (lastResult.weekend_filter && el("amp-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-save")?.addEventListener("click", () => void saveHistory());
|
||||
el("amp-btn-download")?.addEventListener("click", downloadCurrent);
|
||||
el("amp-straddle-premium")?.addEventListener("input", scheduleReframe);
|
||||
el("amp-take-profit")?.addEventListener("input", scheduleReframe);
|
||||
el("amp-move-points")?.addEventListener("input", scheduleReframe);
|
||||
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
||||
syncCustomDays();
|
||||
}
|
||||
@@ -445,7 +418,7 @@
|
||||
bind();
|
||||
setView("stats");
|
||||
setStatus("");
|
||||
renderStraddle(null);
|
||||
renderMoveStats(null);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -4708,7 +4708,11 @@ body.login-page {
|
||||
}
|
||||
|
||||
body.hub-phone .calc-layout[data-calc-tab="trend"] [data-calc-pane="roll"],
|
||||
body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"] {
|
||||
body.hub-phone .calc-layout[data-calc-tab="trend"] [data-calc-pane="po"],
|
||||
body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"],
|
||||
body.hub-phone .calc-layout[data-calc-tab="roll"] [data-calc-pane="po"],
|
||||
body.hub-phone .calc-layout[data-calc-tab="po"] [data-calc-pane="trend"],
|
||||
body.hub-phone .calc-layout[data-calc-tab="po"] [data-calc-pane="roll"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -9776,10 +9780,33 @@ body:not(.hub-phone) #page-calculator .calc-layout {
|
||||
}
|
||||
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="trend"] [data-calc-pane="roll"],
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"] {
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="trend"] [data-calc-pane="po"],
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="roll"] [data-calc-pane="trend"],
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="roll"] [data-calc-pane="po"],
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="po"] [data-calc-pane="trend"],
|
||||
body:not(.hub-phone) #page-calculator .calc-layout[data-calc-tab="po"] [data-calc-pane="roll"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#page-calculator .calc-po-cases {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
#page-calculator .calc-po-case {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-soft, rgba(255, 255, 255, 0.08));
|
||||
border-radius: 10px;
|
||||
background: color-mix(in srgb, var(--panel, #12161f) 88%, transparent);
|
||||
}
|
||||
|
||||
#page-calculator .calc-po-case h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
body:not(.hub-phone) #page-calculator .calc-card {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
|
||||
@@ -572,8 +572,270 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderPerpOptionsResult(data) {
|
||||
const box = $("calc-po-result");
|
||||
if (!box) return;
|
||||
if ((data.calc_mode || "size") === "points") {
|
||||
renderPerpOptionsPointsResult(data);
|
||||
return;
|
||||
}
|
||||
const a = data.case_a || {};
|
||||
const b = data.case_b || {};
|
||||
const capitalHint = data.capital_ok
|
||||
? "资金充足(参考)"
|
||||
: "保证金高于交易资金(仅提示)";
|
||||
box.classList.remove("hidden");
|
||||
box.innerHTML =
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>标的</span><strong>" +
|
||||
esc(data.base || "—") +
|
||||
" · 永续 " +
|
||||
fmt(data.perp_coins, 2) +
|
||||
" 币</strong></div>" +
|
||||
"<div><span>单币权利金</span><strong>" +
|
||||
fmt(data.prem_per_coin, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>永续毛收益</span><strong class=\"" +
|
||||
pnlClass(data.perp_gross_u) +
|
||||
'">' +
|
||||
fmtU(data.perp_gross_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>永续手续费</span><strong>" +
|
||||
fmt(data.perp_fee_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>权利金预算</span><strong>" +
|
||||
fmt(data.premium_budget_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>期权开仓</span><strong>" +
|
||||
fmt(data.opt_coins, 2) +
|
||||
" 币 / " +
|
||||
fmt(data.opt_sheets, 2) +
|
||||
" 张</strong></div>" +
|
||||
"<div><span>永续保证金</span><strong>" +
|
||||
fmt(data.perp_margin_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>开仓参考</span><strong>" +
|
||||
esc(capitalHint) +
|
||||
"</strong></div>" +
|
||||
"</div>" +
|
||||
'<div class="calc-po-cases">' +
|
||||
'<section class="calc-po-case">' +
|
||||
"<h4>情景 A · 永续方向对</h4>" +
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>永续盈亏</span><strong class=\"" +
|
||||
pnlClass(a.perp_pnl_u) +
|
||||
'">' +
|
||||
fmtU(a.perp_pnl_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>权利金(全亏)</span><strong>" +
|
||||
fmt(a.premium_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>手续费</span><strong>" +
|
||||
fmt(a.fee_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>净利</span><strong class=\"" +
|
||||
pnlClass(a.net_u) +
|
||||
'">' +
|
||||
fmtU(a.net_u) +
|
||||
"</strong></div>" +
|
||||
"</div></section>" +
|
||||
'<section class="calc-po-case">' +
|
||||
"<h4>情景 B · 期权方向对</h4>" +
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>期权内在</span><strong>" +
|
||||
fmtU(b.opt_intrinsic_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>权利金</span><strong>" +
|
||||
fmt(b.premium_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>期权净利</span><strong class=\"" +
|
||||
pnlClass(b.opt_net_u) +
|
||||
'">' +
|
||||
fmtU(b.opt_net_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>永续盈亏</span><strong class=\"" +
|
||||
pnlClass(b.perp_pnl_u) +
|
||||
'">' +
|
||||
fmtU(b.perp_pnl_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>组合净利</span><strong class=\"" +
|
||||
pnlClass(b.portfolio_net_u) +
|
||||
'">' +
|
||||
fmtU(b.portfolio_net_u) +
|
||||
"</strong></div>" +
|
||||
"</div></section></div>";
|
||||
}
|
||||
|
||||
function renderPerpOptionsPointsResult(data) {
|
||||
const box = $("calc-po-result");
|
||||
if (!box) return;
|
||||
const a = data.case_a || {};
|
||||
const b = data.case_b || {};
|
||||
const capitalHint = data.capital_ok
|
||||
? "资金充足(参考)"
|
||||
: "保证金高于交易资金(仅提示)";
|
||||
let caseB =
|
||||
'<section class="calc-po-case">' +
|
||||
"<h4>情景 B · 期权方向对(以组合净利为准)</h4>" +
|
||||
'<div class="calc-summary">';
|
||||
if (b.move_points_portfolio != null) {
|
||||
caseB +=
|
||||
"<div><span>所需波动点数</span><strong>" +
|
||||
fmt(b.move_points_portfolio, 2) +
|
||||
" · " +
|
||||
fmt(b.move_pct_portfolio, 2) +
|
||||
"%</strong></div>" +
|
||||
"<div><span>组合净利</span><strong class=\"" +
|
||||
pnlClass(b.portfolio_net_u) +
|
||||
'">' +
|
||||
fmtU(b.portfolio_net_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>其中期权净利</span><strong class=\"" +
|
||||
pnlClass(b.opt_net_at_portfolio_target_u) +
|
||||
'">' +
|
||||
fmtU(b.opt_net_at_portfolio_target_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>其中永续盈亏</span><strong class=\"" +
|
||||
pnlClass(
|
||||
b.portfolio_net_u != null && b.opt_net_at_portfolio_target_u != null
|
||||
? Number(b.portfolio_net_u) - Number(b.opt_net_at_portfolio_target_u)
|
||||
: null
|
||||
) +
|
||||
'">' +
|
||||
fmtU(
|
||||
b.portfolio_net_u != null && b.opt_net_at_portfolio_target_u != null
|
||||
? Number(b.portfolio_net_u) - Number(b.opt_net_at_portfolio_target_u)
|
||||
: null
|
||||
) +
|
||||
"</strong></div>";
|
||||
} else if (b.portfolio_error) {
|
||||
caseB +=
|
||||
'<div class="calc-field-span2"><span>组合达目标</span><strong class="calc-market-err">' +
|
||||
esc(b.portfolio_error) +
|
||||
"</strong></div>";
|
||||
}
|
||||
caseB += "</div></section>";
|
||||
|
||||
box.classList.remove("hidden");
|
||||
box.innerHTML =
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>标的</span><strong>" +
|
||||
esc(data.base || "—") +
|
||||
" · 比例 " +
|
||||
esc(data.ratio_label || "—") +
|
||||
"</strong></div>" +
|
||||
"<div><span>仓位</span><strong>永续 " +
|
||||
fmt(data.perp_coins, 2) +
|
||||
" 币 / 期权 " +
|
||||
fmt(data.opt_coins, 2) +
|
||||
" 币(" +
|
||||
fmt(data.opt_sheets, 2) +
|
||||
" 张)</strong></div>" +
|
||||
"<div><span>单币权利金</span><strong>" +
|
||||
fmt(data.prem_per_coin, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>权利金总额</span><strong>" +
|
||||
fmt(data.premium_total_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>目标盈利</span><strong>" +
|
||||
fmt(data.target_profit_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>永续保证金</span><strong>" +
|
||||
fmt(data.perp_margin_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>开仓参考</span><strong>" +
|
||||
esc(capitalHint) +
|
||||
"</strong></div>" +
|
||||
"</div>" +
|
||||
'<div class="calc-po-cases">' +
|
||||
'<section class="calc-po-case">' +
|
||||
"<h4>情景 A · 永续方向对</h4>" +
|
||||
'<div class="calc-summary">' +
|
||||
"<div><span>所需波动点数</span><strong>" +
|
||||
fmt(a.move_points, 2) +
|
||||
" · " +
|
||||
fmt(a.move_pct, 2) +
|
||||
"%</strong></div>" +
|
||||
"<div><span>永续盈亏</span><strong class=\"" +
|
||||
pnlClass(a.perp_pnl_u) +
|
||||
'">' +
|
||||
fmtU(a.perp_pnl_u) +
|
||||
"</strong></div>" +
|
||||
"<div><span>权利金(全亏)</span><strong>" +
|
||||
fmt(a.premium_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>手续费</span><strong>" +
|
||||
fmt(a.fee_u, 2) +
|
||||
"U</strong></div>" +
|
||||
"<div><span>净利</span><strong class=\"" +
|
||||
pnlClass(a.net_u) +
|
||||
'">' +
|
||||
fmtU(a.net_u) +
|
||||
"</strong></div>" +
|
||||
"</div></section>" +
|
||||
caseB +
|
||||
"</div>";
|
||||
}
|
||||
|
||||
async function submitPerpOptions(e) {
|
||||
e.preventDefault();
|
||||
const calcMode = ($("calc-po-calc-mode") && $("calc-po-calc-mode").value) || "size";
|
||||
const body = {
|
||||
calc_mode: calcMode,
|
||||
base: ($("calc-po-base") && $("calc-po-base").value) || "ETH",
|
||||
spot: num("calc-po-spot"),
|
||||
capital_usdt: num("calc-po-capital"),
|
||||
target_profit_u: num("calc-po-target"),
|
||||
move_mode: ($("calc-po-move-mode") && $("calc-po-move-mode").value) || "points",
|
||||
move_value: num("calc-po-move"),
|
||||
perp_leverage: num("calc-po-perp-lev"),
|
||||
option_leverage: num("calc-po-opt-lev"),
|
||||
ct_mult: num("calc-po-ct-mult") || 0.01,
|
||||
ratio_perp: num("calc-po-ratio-perp") || 1,
|
||||
ratio_opt: num("calc-po-ratio-opt") || 2,
|
||||
};
|
||||
try {
|
||||
const r = await fetch("/api/calculator/perp-options", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const j = await r.json();
|
||||
if (!j.ok) {
|
||||
showErr("calc-po-result", j.msg || "计算失败");
|
||||
return;
|
||||
}
|
||||
renderPerpOptionsResult(j.data);
|
||||
} catch (err) {
|
||||
showErr("calc-po-result", String(err));
|
||||
}
|
||||
}
|
||||
|
||||
function syncPoMoveLabel() {
|
||||
const mode = ($("calc-po-move-mode") && $("calc-po-move-mode").value) || "points";
|
||||
const lab = $("calc-po-move-label");
|
||||
if (lab) lab.textContent = mode === "pct" ? "波动率 %" : "波动点数";
|
||||
}
|
||||
|
||||
function syncPoCalcMode() {
|
||||
const mode = ($("calc-po-calc-mode") && $("calc-po-calc-mode").value) || "size";
|
||||
const points = mode === "points";
|
||||
page.querySelectorAll(".calc-po-size-only").forEach(function (el) {
|
||||
el.classList.toggle("hidden", points);
|
||||
});
|
||||
page.querySelectorAll(".calc-po-points-only").forEach(function (el) {
|
||||
el.classList.toggle("hidden", !points);
|
||||
});
|
||||
const moveInput = $("calc-po-move");
|
||||
if (moveInput) {
|
||||
if (points) moveInput.removeAttribute("required");
|
||||
else moveInput.setAttribute("required", "required");
|
||||
}
|
||||
}
|
||||
|
||||
function applyCalcTab(tab) {
|
||||
const t = tab === "roll" ? "roll" : "trend";
|
||||
const t = tab === "roll" || tab === "po" ? tab : "trend";
|
||||
const layout = page.querySelector(".calc-layout");
|
||||
if (layout) layout.setAttribute("data-calc-tab", t);
|
||||
page.querySelectorAll(".calc-m-tab").forEach(function (btn) {
|
||||
@@ -609,13 +871,25 @@
|
||||
await loadCalculatorExchanges();
|
||||
const trendForm = $("calc-trend-form");
|
||||
const rollForm = $("calc-roll-form");
|
||||
const poForm = $("calc-po-form");
|
||||
const dirSel = $("calc-trend-direction");
|
||||
const poMode = $("calc-po-move-mode");
|
||||
const poCalcMode = $("calc-po-calc-mode");
|
||||
if (trendForm) trendForm.addEventListener("submit", submitTrend);
|
||||
if (rollForm) rollForm.addEventListener("submit", submitRoll);
|
||||
if (poForm) poForm.addEventListener("submit", submitPerpOptions);
|
||||
if (dirSel) {
|
||||
dirSel.addEventListener("change", syncTrendAddLabel);
|
||||
syncTrendAddLabel();
|
||||
}
|
||||
if (poMode) {
|
||||
poMode.addEventListener("change", syncPoMoveLabel);
|
||||
syncPoMoveLabel();
|
||||
}
|
||||
if (poCalcMode) {
|
||||
poCalcMode.addEventListener("change", syncPoCalcMode);
|
||||
syncPoCalcMode();
|
||||
}
|
||||
bindRollLegsUI();
|
||||
bindMarket("calc-trend");
|
||||
bindMarket("calc-roll");
|
||||
|
||||
@@ -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=20260724-opt-archive" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260728-po-calc" />
|
||||
<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>
|
||||
@@ -818,6 +818,9 @@
|
||||
<button type="button" class="calc-m-tab" data-calc-tab="roll" role="tab" aria-selected="false">
|
||||
<span class="calc-tab-label-mobile">滚仓</span><span class="calc-tab-label-desktop">滚仓计算器</span>
|
||||
</button>
|
||||
<button type="button" class="calc-m-tab" data-calc-tab="po" role="tab" aria-selected="false">
|
||||
<span class="calc-tab-label-mobile">永期对冲</span><span class="calc-tab-label-desktop">永期对冲计算器</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="calc-layout" data-calc-tab="trend">
|
||||
<section class="calc-card card" data-calc-pane="trend">
|
||||
@@ -958,6 +961,84 @@
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="calc-card card" data-calc-pane="po">
|
||||
<div class="calc-pane-split">
|
||||
<div class="calc-input-panel">
|
||||
<h2>永期对冲计算器</h2>
|
||||
<p class="calc-hint">永续固定 1 币;单币权利金 = 现价 / 期权杠杆;权利金按全亏;只扣永续开平手续费(各 0.05%).「推仓位」由波动反推期权数量;「推点数」按永续:期权比例反推达目标盈利所需波动.</p>
|
||||
<form id="calc-po-form" class="calc-form">
|
||||
<div class="calc-form-grid">
|
||||
<label class="calc-field">
|
||||
<span>测算模式</span>
|
||||
<select id="calc-po-calc-mode">
|
||||
<option value="size" selected>由波动推期权仓位</option>
|
||||
<option value="points">由比例推波动点数</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>币种</span>
|
||||
<select id="calc-po-base">
|
||||
<option value="ETH" selected>ETH</option>
|
||||
<option value="BTC">BTC</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>现价</span>
|
||||
<input id="calc-po-spot" type="number" min="0" step="any" value="1800" required />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>交易资金 (U·参考)</span>
|
||||
<input id="calc-po-capital" type="number" min="0.01" step="any" value="3000" required />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>目标盈利 (U)</span>
|
||||
<input id="calc-po-target" type="number" min="0" step="any" value="15" required />
|
||||
</label>
|
||||
<label class="calc-field calc-po-size-only">
|
||||
<span>波动模式</span>
|
||||
<select id="calc-po-move-mode">
|
||||
<option value="points" selected>波动点数</option>
|
||||
<option value="pct">波动率 %</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="calc-field calc-po-size-only">
|
||||
<span id="calc-po-move-label">波动点数</span>
|
||||
<input id="calc-po-move" type="number" min="0" step="any" value="50" />
|
||||
</label>
|
||||
<label class="calc-field calc-po-points-only hidden">
|
||||
<span>永续比例</span>
|
||||
<input id="calc-po-ratio-perp" type="number" min="0.01" step="any" value="1" />
|
||||
</label>
|
||||
<label class="calc-field calc-po-points-only hidden">
|
||||
<span>期权比例</span>
|
||||
<input id="calc-po-ratio-opt" type="number" min="0.01" step="any" value="2" />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>永续杠杆</span>
|
||||
<input id="calc-po-perp-lev" type="number" min="0.01" step="any" value="10" required />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>期权杠杆</span>
|
||||
<input id="calc-po-opt-lev" type="number" min="0.01" step="any" value="100" required />
|
||||
</label>
|
||||
<label class="calc-field">
|
||||
<span>合约乘数 ct_mult</span>
|
||||
<input id="calc-po-ct-mult" type="number" min="0.0001" step="any" value="0.01" required />
|
||||
</label>
|
||||
</div>
|
||||
<div class="calc-actions">
|
||||
<button type="submit" class="primary">计算</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<aside class="calc-result-panel" aria-label="永期对冲结果推算">
|
||||
<h3 class="calc-result-title">结果推算</h3>
|
||||
<p class="calc-result-placeholder">填写左侧参数后点击「计算」</p>
|
||||
<div id="calc-po-result" class="calc-result hidden"></div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1184,12 +1265,8 @@
|
||||
</select>
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>买跨·双边权利金(点)</span>
|
||||
<input id="amp-straddle-premium" type="number" min="0" step="any" placeholder="如 30" />
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>止盈点(点)</span>
|
||||
<input id="amp-take-profit" type="number" min="0" step="any" placeholder="空=按涨跌" />
|
||||
<span>波动点数</span>
|
||||
<input id="amp-move-points" type="number" min="0" step="any" placeholder="如 50" />
|
||||
</label>
|
||||
<div class="amp-actions">
|
||||
<button type="button" id="amp-btn-compute" class="primary">计算</button>
|
||||
@@ -1198,22 +1275,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div id="amp-summary" class="amp-summary"></div>
|
||||
<h3 class="amp-block-title">买跨对照</h3>
|
||||
<div id="amp-straddle" class="amp-summary amp-straddle"></div>
|
||||
<h3 class="amp-block-title">振幅占比</h3>
|
||||
<div id="amp-move-stats" class="amp-summary amp-move-stats"></div>
|
||||
<h3 class="amp-block-title">日表明细</h3>
|
||||
<div class="amp-table-wrap">
|
||||
<table class="amp-table">
|
||||
<thead>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="amp-table-body">
|
||||
<tr><td colspan="11" class="amp-empty">点击「计算」加载</td></tr>
|
||||
<tr><td colspan="12" class="amp-empty">点击「计算」加载</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1674,7 +1751,7 @@
|
||||
<script src="/assets/chart_draw.js?v=20260720-option-day-1600"></script>
|
||||
<script src="/assets/chart.js?v=20260720-option-day-1600"></script>
|
||||
<script src="/assets/plan.js?v=20260720-autofill"></script>
|
||||
<script src="/assets/calculator.js?v=20260715-calc-tabs"></script>
|
||||
<script src="/assets/calculator.js?v=20260728-po-2dp"></script>
|
||||
<script src="/assets/compare.js?v=20260723-compare"></script>
|
||||
<script src="/assets/trade_stats_calendar.js?v=3"></script>
|
||||
<script src="/assets/archive.js?v=20260724-opt-archive"></script>
|
||||
@@ -1682,7 +1759,7 @@
|
||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||
<script src="/assets/dashboard.js?v=20260723-hide-pnl"></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/logs.js?v=1"></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(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):
|
||||
now = datetime(2026, 7, 22, 10, 0, tzinfo=TZ)
|
||||
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_up_points"], 500)
|
||||
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 = [
|
||||
# |chg|=40>30 win+10; up=40>30; down=10
|
||||
{"up_points": 40, "down_points": 10, "change": 40, "amplitude": 50, "settlement_day": "2026-07-01"},
|
||||
# |chg|=10 lose-20; up=5; down=35>30
|
||||
{"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"},
|
||||
{"amplitude": 100, "up_points": 40, "down_points": 60, "change": 10, "settlement_day": "2026-07-01"},
|
||||
{"amplitude": 40, "up_points": 10, "down_points": 30, "change": -5, "settlement_day": "2026-07-02"},
|
||||
{"amplitude": 50, "up_points": 50, "down_points": 0, "change": 20, "settlement_day": "2026-07-03"},
|
||||
]
|
||||
s = summarize_rows(rows, straddle_premium=30)
|
||||
st = s["straddle"]
|
||||
self.assertEqual(st["side"], "long_straddle")
|
||||
self.assertEqual(st["premium"], 30)
|
||||
self.assertEqual(st["up_exceed_days"], 1) # only 40
|
||||
self.assertEqual(st["down_exceed_days"], 1) # only 35
|
||||
self.assertEqual(st["abs_change_exceed_days"], 1) # only 40
|
||||
self.assertAlmostEqual(st["pnl_total"], 40 - 30 + 10 - 30 + 30 - 30)
|
||||
self.assertEqual(st["win_days"], 1)
|
||||
self.assertEqual(st["win_ratio"], round(1 / 3, 4))
|
||||
s = summarize_rows(rows, move_points=50)
|
||||
ms = s["move_points_stats"]
|
||||
self.assertIsNotNone(ms)
|
||||
self.assertEqual(ms["move_points"], 50)
|
||||
self.assertEqual(ms["amp_hit_days"], 2) # 100, 50
|
||||
self.assertEqual(ms["amp_hit_ratio"], round(2 / 3, 4))
|
||||
self.assertEqual(ms["up_hit_days"], 1) # 50
|
||||
self.assertEqual(ms["down_hit_days"], 1) # 60
|
||||
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):
|
||||
from lib.hub.amp_stats_lib import (
|
||||
enrich_rows_pnl,
|
||||
filter_weekend_rows,
|
||||
reframe_amp_stats,
|
||||
)
|
||||
def test_weekend_and_reframe_move_points(self):
|
||||
from lib.hub.amp_stats_lib import enrich_rows, filter_weekend_rows, reframe_amp_stats
|
||||
|
||||
# Sat 2026-07-18, Sun 2026-07-19, Mon 2026-07-20
|
||||
rows = [
|
||||
{
|
||||
"settlement_day": "2026-07-18",
|
||||
@@ -159,39 +164,20 @@ class AmpStatsLibTests(unittest.TestCase):
|
||||
only = filter_weekend_rows(rows, "only")
|
||||
self.assertEqual(len(only), 2)
|
||||
|
||||
# TP=80: day1 hit → move 80; day2 no → |12|; day3 no → 8
|
||||
enriched = enrich_rows_pnl(rows, straddle_premium=10, take_profit=80)
|
||||
self.assertTrue(enriched[0]["take_profit_hit"])
|
||||
self.assertEqual(enriched[0]["effective_move"], 80)
|
||||
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)
|
||||
enriched = enrich_rows(rows, move_points=80)
|
||||
self.assertTrue(enriched[0]["amp_hit"])
|
||||
self.assertFalse(enriched[1]["amp_hit"])
|
||||
self.assertTrue(enriched[2]["amp_hit"])
|
||||
|
||||
reframed = reframe_amp_stats(
|
||||
rows_all=rows,
|
||||
symbol="eth",
|
||||
weekend_filter="exclude",
|
||||
straddle_premium=10,
|
||||
take_profit=80,
|
||||
move_points=80,
|
||||
)
|
||||
self.assertEqual(reframed["summary"]["sample_count"], 1)
|
||||
# Mon: 未触达止盈 → |8|-10
|
||||
self.assertEqual(reframed["rows"][0]["profit"], -2)
|
||||
self.assertIn("收益", build_export_csv(reframed))
|
||||
self.assertTrue(reframed["rows"][0]["amp_hit"])
|
||||
self.assertIn("振幅占比", build_export_csv(reframed))
|
||||
|
||||
def test_fetch_switches_to_history_endpoint(self):
|
||||
"""近期接口到头后应切 history 续拉."""
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""hub_perp_options_calc_lib 永期对冲测算."""
|
||||
|
||||
import unittest
|
||||
|
||||
from lib.hub.hub_perp_options_calc_lib import (
|
||||
calc_perp_options,
|
||||
calc_perp_options_hedge,
|
||||
calc_perp_options_points,
|
||||
)
|
||||
|
||||
|
||||
class HubPerpOptionsCalcTests(unittest.TestCase):
|
||||
def test_example_1800_50_15_100(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
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,
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
assert data is not None
|
||||
self.assertEqual(data["prem_per_coin"], 18.0)
|
||||
self.assertEqual(data["perp_gross_u"], 50.0)
|
||||
# fee = (1800+1850)*0.0005 = 1.825
|
||||
self.assertAlmostEqual(data["perp_fee_u"], 1.825, places=6)
|
||||
self.assertAlmostEqual(data["premium_budget_u"], 33.175, places=6)
|
||||
self.assertAlmostEqual(data["opt_coins"], 33.175 / 18.0, places=6)
|
||||
self.assertAlmostEqual(data["opt_sheets"], data["opt_coins"] / 0.01, places=4)
|
||||
self.assertAlmostEqual(data["case_a"]["net_u"], 15.0, places=6)
|
||||
# B: opt_net = coins*50 - coins*18 = coins*32; portfolio = opt_net - 50
|
||||
coins = data["opt_coins"]
|
||||
self.assertAlmostEqual(data["case_b"]["opt_net_u"], coins * 32.0, places=6)
|
||||
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], coins * 32.0 - 50.0, places=6)
|
||||
self.assertAlmostEqual(data["perp_margin_u"], 180.0, places=6)
|
||||
self.assertTrue(data["capital_ok"])
|
||||
|
||||
def test_pct_mode(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="BTC",
|
||||
spot=100000,
|
||||
capital_usdt=5000,
|
||||
target_profit_u=100,
|
||||
move_mode="pct",
|
||||
move_value=1,
|
||||
perp_leverage=5,
|
||||
option_leverage=50,
|
||||
ct_mult=0.01,
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
assert data is not None
|
||||
self.assertAlmostEqual(data["move_points"], 1000.0, places=6)
|
||||
self.assertAlmostEqual(data["perp_gross_u"], 1000.0, places=6)
|
||||
self.assertAlmostEqual(data["prem_per_coin"], 2000.0, places=6)
|
||||
|
||||
def test_budget_too_small(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="ETH",
|
||||
spot=1800,
|
||||
capital_usdt=3000,
|
||||
target_profit_u=60,
|
||||
move_mode="points",
|
||||
move_value=50,
|
||||
perp_leverage=10,
|
||||
option_leverage=100,
|
||||
)
|
||||
self.assertIsNone(data)
|
||||
self.assertIn("无法开期权", err or "")
|
||||
|
||||
def test_bad_base(self):
|
||||
data, err = calc_perp_options_hedge(
|
||||
base="SOL",
|
||||
spot=100,
|
||||
capital_usdt=1000,
|
||||
target_profit_u=10,
|
||||
move_mode="points",
|
||||
move_value=5,
|
||||
perp_leverage=5,
|
||||
option_leverage=20,
|
||||
)
|
||||
self.assertIsNone(data)
|
||||
self.assertIsNotNone(err)
|
||||
|
||||
def test_points_ratio_1_to_2(self):
|
||||
# spot=1800, optLev=100 → prem/coin=18; ratio 1:2 → opt=2, premium=36
|
||||
# A: move = (15+36+2*1800*0.0005)/(1-0.0005) = (51+1.8)/0.9995
|
||||
data, err = calc_perp_options_points(
|
||||
base="ETH",
|
||||
spot=1800,
|
||||
capital_usdt=3000,
|
||||
target_profit_u=15,
|
||||
perp_leverage=10,
|
||||
option_leverage=100,
|
||||
ratio_perp=1,
|
||||
ratio_opt=2,
|
||||
ct_mult=0.01,
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
assert data is not None
|
||||
self.assertEqual(data["calc_mode"], "points")
|
||||
self.assertEqual(data["opt_coins"], 2.0)
|
||||
self.assertEqual(data["premium_total_u"], 36.0)
|
||||
expect_a = (15 + 36 + 2 * 1800 * 0.0005) / 0.9995
|
||||
self.assertAlmostEqual(data["case_a"]["move_points"], expect_a, places=6)
|
||||
self.assertAlmostEqual(data["case_a"]["net_u"], 15.0, places=5)
|
||||
# 期权净利=目标: (15+36)/2 = 25.5
|
||||
self.assertAlmostEqual(data["case_b"]["move_points_opt_net"], 25.5, places=6)
|
||||
self.assertAlmostEqual(data["case_b"]["opt_net_u"], 15.0, places=6)
|
||||
# 组合净利=目标: (15+36)/(2-1) = 51
|
||||
self.assertAlmostEqual(data["case_b"]["move_points_portfolio"], 51.0, places=6)
|
||||
self.assertAlmostEqual(data["case_b"]["portfolio_net_u"], 15.0, places=6)
|
||||
|
||||
def test_points_ratio_1_to_1_no_portfolio(self):
|
||||
data, err = calc_perp_options(
|
||||
calc_mode="points",
|
||||
base="ETH",
|
||||
spot=1800,
|
||||
capital_usdt=3000,
|
||||
target_profit_u=15,
|
||||
perp_leverage=10,
|
||||
option_leverage=100,
|
||||
ratio_perp=1,
|
||||
ratio_opt=1,
|
||||
)
|
||||
self.assertIsNone(err)
|
||||
assert data is not None
|
||||
self.assertIsNone(data["case_b"]["move_points_portfolio"])
|
||||
self.assertIsNotNone(data["case_b"]["portfolio_error"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user