Replace amp-stats straddle/perp overlays with move-points amplitude ratio.
Input points now drives amplitude hit share; table keeps both-side moves and amp达标. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+18
-76
@@ -3,8 +3,8 @@
|
|||||||
中控只读工具:按自定义整点起点、**固定北京时间 16:00 收窗**,统计 OKX 上 ETH/BTC 的历史「点数振幅」档案,辅助一天期期权判断空间。
|
中控只读工具:按自定义整点起点、**固定北京时间 16:00 收窗**,统计 OKX 上 ETH/BTC 的历史「点数振幅」档案,辅助一天期期权判断空间。
|
||||||
|
|
||||||
> 开发方案见 [ETH时段振幅统计-开发方案.md](./ETH时段振幅统计-开发方案.md)。
|
> 开发方案见 [ETH时段振幅统计-开发方案.md](./ETH时段振幅统计-开发方案.md)。
|
||||||
> 永期对冲公式见 [永期对冲计算器.md](./永期对冲计算器.md)。
|
|
||||||
> **不改下单链路**;不算 IV。
|
> **不改下单链路**;不算 IV。
|
||||||
|
> 买跨 / 永期对冲测算请用中控 **策略计算器**,本页不再做对照盈亏。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -22,8 +22,9 @@
|
|||||||
2. 选择 **标的** ETH / BTC;数据源固定 **OKX**
|
2. 选择 **标的** ETH / BTC;数据源固定 **OKX**
|
||||||
3. **起点整点**(00–23);终点固定 **16:00**
|
3. **起点整点**(00–23);终点固定 **16:00**
|
||||||
4. **周期**:1 月 / 2 月 / 3 月 / 半年 / 1 年 / 自定义天数(默认 2 个月)
|
4. **周期**:1 月 / 2 月 / 3 月 / 半年 / 1 年 / 自定义天数(默认 2 个月)
|
||||||
5. 点 **计算** → 下方看汇总 + 分页日表
|
5. 可选填 **波动点数**(如 `50`)→ 看振幅达标占比
|
||||||
6. 需要留存时点 **保存到历史**;**下载 CSV** 含摘要 + 全日明细
|
6. 点 **计算** → 下方看汇总 + 振幅占比 + 分页日表
|
||||||
|
7. 需要留存时点 **保存到历史**;**下载 CSV** 含摘要 + 全日明细
|
||||||
|
|
||||||
**跨天例子**
|
**跨天例子**
|
||||||
|
|
||||||
@@ -43,8 +44,8 @@
|
|||||||
|
|
||||||
| 字段 | 算法 |
|
| 字段 | 算法 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 开→高 | `H − O` |
|
| 开→高 | `H − O`(一边波动) |
|
||||||
| 开→低 | `O − L` |
|
| 开→低 | `O − L`(另一边波动) |
|
||||||
| **振幅** | `H − L`(= 开→高 + 开→低) |
|
| **振幅** | `H − L`(= 开→高 + 开→低) |
|
||||||
| 涨跌值 | `C − O` |
|
| 涨跌值 | `C − O` |
|
||||||
|
|
||||||
@@ -58,35 +59,19 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 对照模式(买跨 / 永期二选一)
|
## 波动点数 → 振幅占比
|
||||||
|
|
||||||
表单 **对照模式** 切换:
|
表单可填 **波动点数**(如 `50`)。填写后下方 **振幅占比** 块显示:
|
||||||
|
|
||||||
| 模式 | 表单 | 汇总块 | 日表末列 |
|
|
||||||
|------|------|--------|----------|
|
|
||||||
| 买跨双边 | 双边权利金、止盈点 | 买跨对照 | 收益 |
|
|
||||||
| 永期对冲 | 目标盈利、杠杆、比例 | 永期对冲对照 | 永期盈亏 |
|
|
||||||
|
|
||||||
同一时刻只计算 / 展示当前模式;切换后在已有日表上本地重算。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 买跨对照(赌波动)
|
|
||||||
|
|
||||||
表单可填 **双边权利金(点)**,例如 `30`;旁边可填 **止盈点**(可空):
|
|
||||||
|
|
||||||
| 汇总项 | 口径 |
|
| 汇总项 | 口径 |
|
||||||
|--------|------|
|
|--------|------|
|
||||||
| 开→高超过权利金 | `H−O > 权利金` 的天数与占比 |
|
| 振幅≥点数 | `H−L ≥ 点数` 的天数与**占比**(主指标) |
|
||||||
| 开→低超过权利金 | `O−L > 权利金` 的天数与占比 |
|
| 开→高≥点数 | `H−O ≥ 点数` 天数与占比 |
|
||||||
| \|涨跌\|超过权利金 | `\|C−O\| > 权利金` 的天数与占比 |
|
| 开→低≥点数 | `O−L ≥ 点数` 天数与占比 |
|
||||||
| 有效波动 | 若设止盈且 `开→高≥止盈` 或 `开→低≥止盈` → 用止盈点;否则用 `\|C−O\|` |
|
| \|涨跌\|≥点数 | `\|C−O\| ≥ 点数` 天数与占比 |
|
||||||
| 买跨收益 | `有效波动 − 权利金`(日表「收益」列同口径) |
|
|
||||||
|
|
||||||
- 方向:**买跨**
|
日表保留 **开→高 / 开→低**(两边波动点数),并标 **振幅达标**;达标行振幅会高亮。
|
||||||
- 权利金越过:严格 **`>`**;止盈触达:**`≥`**
|
改点数 / 周末筛选会在已有日表上**本地重算**(不重拉 K 线)。
|
||||||
- 止盈留空 / ≤0:有效波动一律按 `|涨跌|`
|
|
||||||
- 已算出日表后,改权利金 / 止盈 / 周末筛选会**本地重算**(不重拉 K 线)
|
|
||||||
|
|
||||||
### 周末
|
### 周末
|
||||||
|
|
||||||
@@ -95,46 +80,6 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 永期对冲对照
|
|
||||||
|
|
||||||
与中控 [永期对冲计算器](./永期对冲计算器.md) 同口径:**永续做多 1 币 + 买期权**(默认比例 **1:2**),在历史振幅日表上做对照。
|
|
||||||
|
|
||||||
表单填:目标盈利、永续杠杆、期权杠杆、比例(可改)。**入场价 = 当日开盘**,不再填现价。未填齐目标/杠杆时不计算该块。
|
|
||||||
|
|
||||||
```text
|
|
||||||
单币权利金(日) = 当日开盘 / 期权杠杆
|
|
||||||
期权币数 = 1 × (期权比例 / 永续比例)
|
|
||||||
权利金总额(日) = 期权币数 × 单币权利金(日)
|
|
||||||
```
|
|
||||||
|
|
||||||
推「所需点数」时,用样本 **开盘中位数** 作入场参照(汇总里展示的权利金中位同口径)。
|
|
||||||
|
|
||||||
### ① 所需点数达标
|
|
||||||
|
|
||||||
复用计算器「由比例推点数」(按**当日开盘**推 A/B;汇总展示用样本开盘中位):
|
|
||||||
|
|
||||||
| 指标 | 规则 |
|
|
||||||
|------|------|
|
|
||||||
| A 所需点数 | 永续方向对、净利=目标 |
|
|
||||||
| A 达标 | 日 `开→高 ≥ A点数` 的天数与占比 |
|
|
||||||
| B 所需点数 | 期权方向对、**组合净利**=目标 |
|
|
||||||
| B 达标 | 日 `开→低 ≥ B点数` 的天数与占比 |
|
|
||||||
|
|
||||||
### ② 按日组合盈亏(目标出场)
|
|
||||||
|
|
||||||
日盈利目标(如 **15U**)用于出场:
|
|
||||||
|
|
||||||
| 情形 | 日盈亏 |
|
|
||||||
|------|--------|
|
|
||||||
| 开→高触达 A,或 开→低触达 B | **= 目标盈利**(出场) |
|
|
||||||
| 两边都触达 | 仍按目标盈利(OHLC 未知先后) |
|
|
||||||
| 均未触达 | 收盘结算:上涨 `涨跌−权利金−手续费`;下跌 `\|涨跌\|×(期权币数−1)−权利金` |
|
|
||||||
|
|
||||||
汇总:目标出场天数 / 收盘结算天数、合计、日均、胜率、上涨日/下跌日盈亏小计、单日最大赚亏;日表 **永期盈亏** 列。
|
|
||||||
改永期参数 / 周末筛选会**本地重算**(不重拉 K 线)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 历史 Tab
|
## 历史 Tab
|
||||||
|
|
||||||
- 仅 **保存到历史** 后出现(不会一算就自动入库)
|
- 仅 **保存到历史** 后出现(不会一算就自动入库)
|
||||||
@@ -151,7 +96,7 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
| `manual_trading_hub/amp_stats_routes.py` | API |
|
| `manual_trading_hub/amp_stats_routes.py` | API |
|
||||||
| `manual_trading_hub/amp_stats_store.py` | 历史 JSON |
|
| `manual_trading_hub/amp_stats_store.py` | 历史 JSON |
|
||||||
| `manual_trading_hub/static/amp_stats.js` | 前端 |
|
| `manual_trading_hub/static/amp_stats.js` | 前端 |
|
||||||
| `tests/test_amp_stats_lib.py` | 单元测试 |
|
| `tests/test_amp_stats_lib.py` | 单测 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,9 +105,6 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
|||||||
| 日期 | 说明 |
|
| 日期 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 2026-07-23 | 首版上线说明 |
|
| 2026-07-23 | 首版上线说明 |
|
||||||
| 2026-07-23 | 买跨对照:可设双边权利金、越过占比与收盘盈亏 |
|
| 2026-07-23 | 买跨对照、周末筛选、止盈点 |
|
||||||
| 2026-07-23 | 周末筛选/标注、止盈点(≥)、日表收益列 |
|
| 2026-07-28 | 永期对冲对照(后已移除) |
|
||||||
| 2026-07-23 | 长周期续拉 history K 线;收益列红绿着色 |
|
| 2026-07-28 | 去掉买跨/永期;改为波动点数→振幅占比 |
|
||||||
| 2026-07-28 | 永期对冲对照:所需点数达标 + 按日组合盈亏 |
|
|
||||||
| 2026-07-28 | 永期入场改按日开盘;买跨/永期对照模式二选一 |
|
|
||||||
| 2026-07-28 | 永期日盈亏按目标盈利出场(开→高/低触达),未触达才收盘结算 |
|
|
||||||
|
|||||||
+105
-577
@@ -173,27 +173,14 @@ def compute_day_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def normalize_straddle_premium(raw: Any) -> Optional[float]:
|
def normalize_move_points(raw: Any) -> Optional[float]:
|
||||||
"""双边权利金(点数).空/≤0 表示不做跨式对照."""
|
"""对照波动点数.空/≤0 表示不做点数达标对照."""
|
||||||
if raw is None or raw == "":
|
if raw is None or raw == "":
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
v = float(raw)
|
v = float(raw)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
raise ValueError("双边权利金须为数字") from None
|
raise ValueError("波动点数须为数字") from None
|
||||||
if v <= 0:
|
|
||||||
return None
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_take_profit(raw: Any) -> Optional[float]:
|
|
||||||
"""止盈点.空/≤0 表示不止盈,有效波动用 |涨跌|."""
|
|
||||||
if raw is None or raw == "":
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
v = float(raw)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
raise ValueError("止盈点须为数字") from None
|
|
||||||
if v <= 0:
|
if v <= 0:
|
||||||
return None
|
return None
|
||||||
return v
|
return v
|
||||||
@@ -231,452 +218,111 @@ def filter_weekend_rows(rows: list[dict[str, Any]], weekend_filter: Any = "all")
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def effective_move_points(row: dict[str, Any], take_profit: Optional[float]) -> float:
|
def _ensure_weekend_flags(item: dict[str, Any]) -> None:
|
||||||
"""触达止盈(≥)用止盈点,否则用 |涨跌|."""
|
if "is_weekend" in item:
|
||||||
abs_chg = abs(float(row.get("change") or 0))
|
return
|
||||||
if take_profit is None:
|
if not item.get("settlement_day"):
|
||||||
return abs_chg
|
item.setdefault("weekday_label", "")
|
||||||
tp = float(take_profit)
|
item.setdefault("is_weekend", False)
|
||||||
up = float(row.get("up_points") or 0)
|
return
|
||||||
down = float(row.get("down_points") or 0)
|
try:
|
||||||
if up >= tp or down >= tp:
|
wd = date.fromisoformat(str(item["settlement_day"])).weekday()
|
||||||
return tp
|
item["weekday"] = wd
|
||||||
return abs_chg
|
item["weekday_label"] = "六" if wd == 5 else ("日" if wd == 6 else "")
|
||||||
|
item["is_weekend"] = wd >= 5
|
||||||
|
except ValueError:
|
||||||
|
item.setdefault("weekday_label", "")
|
||||||
|
item.setdefault("is_weekend", False)
|
||||||
|
|
||||||
|
|
||||||
def enrich_rows_pnl(
|
def enrich_rows(
|
||||||
rows: list[dict[str, Any]],
|
rows: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
straddle_premium: Optional[float] = None,
|
move_points: Any = None,
|
||||||
take_profit: Optional[float] = None,
|
|
||||||
perp_hedge: Any = None,
|
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
"""为日表附加有效波动 / 是否触达止盈 / 收益(有权利金时) / 永期盈亏."""
|
"""为日表附加周末标注,以及相对波动点数的两边达标."""
|
||||||
prem = normalize_straddle_premium(straddle_premium)
|
mp = normalize_move_points(move_points)
|
||||||
tp = normalize_take_profit(take_profit)
|
|
||||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for r in rows or []:
|
for r in rows or []:
|
||||||
item = dict(r)
|
item = dict(r)
|
||||||
if "is_weekend" not in item and item.get("settlement_day"):
|
_ensure_weekend_flags(item)
|
||||||
try:
|
up = float(item.get("up_points") or 0)
|
||||||
wd = date.fromisoformat(str(item["settlement_day"])).weekday()
|
down = float(item.get("down_points") or 0)
|
||||||
item["weekday"] = wd
|
amp = float(item.get("amplitude") or 0)
|
||||||
item["weekday_label"] = "六" if wd == 5 else ("日" if wd == 6 else "")
|
hit_up = bool(mp is not None and up >= mp)
|
||||||
item["is_weekend"] = wd >= 5
|
hit_down = bool(mp is not None and down >= mp)
|
||||||
except ValueError:
|
amp_hit = bool(mp is not None and amp >= mp)
|
||||||
item.setdefault("weekday_label", "")
|
item["move_points"] = mp
|
||||||
item.setdefault("is_weekend", False)
|
item["hit_up"] = hit_up
|
||||||
move = effective_move_points(item, tp)
|
item["hit_down"] = hit_down
|
||||||
hit = False
|
item["hit_either"] = hit_up or hit_down
|
||||||
if tp is not None:
|
item["hit_both"] = hit_up and hit_down
|
||||||
hit = float(item.get("up_points") or 0) >= tp or float(item.get("down_points") or 0) >= tp
|
item["amp_hit"] = amp_hit
|
||||||
item["effective_move"] = round(move, 4)
|
|
||||||
item["take_profit_hit"] = hit
|
|
||||||
item["profit"] = round(move - prem, 4) if prem is not None else None
|
|
||||||
if hedge is not None:
|
|
||||||
day = perp_hedge_day_result(item, hedge)
|
|
||||||
item["perp_hedge_pnl"] = day["pnl"]
|
|
||||||
item["perp_hedge_exit"] = day["exit"]
|
|
||||||
item["perp_hedge_hit_a"] = day["hit_a"]
|
|
||||||
item["perp_hedge_hit_b"] = day["hit_b"]
|
|
||||||
else:
|
|
||||||
item["perp_hedge_pnl"] = None
|
|
||||||
item["perp_hedge_exit"] = None
|
|
||||||
item["perp_hedge_hit_a"] = False
|
|
||||||
item["perp_hedge_hit_b"] = False
|
|
||||||
out.append(item)
|
out.append(item)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def normalize_perp_hedge_params(raw: Any) -> Optional[dict[str, float]]:
|
# 兼容旧调用名
|
||||||
"""永期对冲对照参数.缺必填则返回 None(不做对照).
|
def enrich_rows_pnl(rows: list[dict[str, Any]], **kwargs: Any) -> list[dict[str, Any]]:
|
||||||
|
return enrich_rows(rows, move_points=kwargs.get("move_points"))
|
||||||
入场价按日开盘;表单只需目标盈利/杠杆/比例.旧字段 spot 可忽略.
|
|
||||||
"""
|
|
||||||
if raw is None or raw == "":
|
|
||||||
return None
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
return None
|
|
||||||
target = _safe_float(raw.get("target_profit_u") if "target_profit_u" in raw else raw.get("target"))
|
|
||||||
p_lev = _safe_float(raw.get("perp_leverage"))
|
|
||||||
o_lev = _safe_float(raw.get("option_leverage"))
|
|
||||||
rp = _safe_float(raw.get("ratio_perp"))
|
|
||||||
ro = _safe_float(raw.get("ratio_opt"))
|
|
||||||
ct = _safe_float(raw.get("ct_mult"))
|
|
||||||
if target is None or p_lev is None or o_lev is None:
|
|
||||||
return None
|
|
||||||
if target < 0 or p_lev <= 0 or o_lev <= 0:
|
|
||||||
return None
|
|
||||||
if rp is None or rp <= 0:
|
|
||||||
rp = 1.0
|
|
||||||
if ro is None or ro <= 0:
|
|
||||||
ro = 2.0
|
|
||||||
if ct is None or ct <= 0:
|
|
||||||
ct = 0.01
|
|
||||||
opt_coins = 1.0 * (ro / rp)
|
|
||||||
return {
|
|
||||||
"target_profit_u": target,
|
|
||||||
"perp_leverage": p_lev,
|
|
||||||
"option_leverage": o_lev,
|
|
||||||
"ratio_perp": rp,
|
|
||||||
"ratio_opt": ro,
|
|
||||||
"ct_mult": ct,
|
|
||||||
"opt_coins": opt_coins,
|
|
||||||
"opt_sheets": opt_coins / ct,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def perp_hedge_day_premium(*, open_px: float, option_leverage: float, opt_coins: float) -> float:
|
def move_points_stats(rows: list[dict[str, Any]], move_points: float) -> dict[str, Any]:
|
||||||
"""单日权利金总额 = 开盘 / 期权杠杆 × 期权币数."""
|
"""波动点数达标汇总:开→高/开→低两边."""
|
||||||
o = float(open_px or 0)
|
mp = float(move_points)
|
||||||
lev = float(option_leverage or 0)
|
if mp <= 0:
|
||||||
coins = float(opt_coins or 0)
|
raise ValueError("波动点数须 > 0")
|
||||||
if o <= 0 or lev <= 0 or coins < 0:
|
work = enrich_rows(rows, move_points=mp)
|
||||||
return 0.0
|
|
||||||
return coins * (o / lev)
|
|
||||||
|
|
||||||
|
|
||||||
def perp_hedge_required_moves(
|
|
||||||
*,
|
|
||||||
open_px: float,
|
|
||||||
hedge: dict[str, float],
|
|
||||||
) -> tuple[Optional[float], Optional[float], Optional[str]]:
|
|
||||||
"""按当日开盘推 A/B 达目标盈利所需点数."""
|
|
||||||
from lib.hub.hub_perp_options_calc_lib import calc_perp_options_points
|
|
||||||
|
|
||||||
spot = float(open_px or 0)
|
|
||||||
if spot <= 0:
|
|
||||||
return None, None, "开盘价无效"
|
|
||||||
points_data, points_err = calc_perp_options_points(
|
|
||||||
base="ETH",
|
|
||||||
spot=spot,
|
|
||||||
capital_usdt=max(spot / hedge["perp_leverage"] * 2, 1000.0),
|
|
||||||
target_profit_u=hedge["target_profit_u"],
|
|
||||||
perp_leverage=hedge["perp_leverage"],
|
|
||||||
option_leverage=hedge["option_leverage"],
|
|
||||||
ratio_perp=hedge["ratio_perp"],
|
|
||||||
ratio_opt=hedge["ratio_opt"],
|
|
||||||
ct_mult=hedge["ct_mult"],
|
|
||||||
)
|
|
||||||
if not points_data:
|
|
||||||
return None, None, points_err
|
|
||||||
move_a = float((points_data.get("case_a") or {}).get("move_points") or 0) or None
|
|
||||||
mb = (points_data.get("case_b") or {}).get("move_points_portfolio")
|
|
||||||
move_b = float(mb) if mb is not None else None
|
|
||||||
return move_a, move_b, points_err
|
|
||||||
|
|
||||||
|
|
||||||
def perp_hedge_day_pnl_eod(
|
|
||||||
*,
|
|
||||||
change: float,
|
|
||||||
open_px: float,
|
|
||||||
close_px: float,
|
|
||||||
option_leverage: float,
|
|
||||||
opt_coins: float,
|
|
||||||
) -> float:
|
|
||||||
"""未触达目标时按收盘结算的组合净利.
|
|
||||||
|
|
||||||
上涨: change − 权利金 − 永续开平手续费
|
|
||||||
下跌: |change|×(opt_coins−1) − 权利金
|
|
||||||
"""
|
|
||||||
from lib.trade.trade_fee_lib import estimate_roundtrip_fee_usdt
|
|
||||||
|
|
||||||
chg = float(change or 0)
|
|
||||||
prem = perp_hedge_day_premium(
|
|
||||||
open_px=open_px, option_leverage=option_leverage, opt_coins=opt_coins
|
|
||||||
)
|
|
||||||
coins = float(opt_coins or 0)
|
|
||||||
if chg >= 0:
|
|
||||||
fee = 0.0
|
|
||||||
if open_px and close_px and open_px > 0 and close_px > 0:
|
|
||||||
fee = estimate_roundtrip_fee_usdt(open_px, close_px, qty=1.0, contract_size=1.0)
|
|
||||||
return round(chg - prem - fee, 4)
|
|
||||||
return round(abs(chg) * (coins - 1.0) - prem, 4)
|
|
||||||
|
|
||||||
|
|
||||||
def perp_hedge_day_result(row: dict[str, Any], hedge: dict[str, float]) -> dict[str, Any]:
|
|
||||||
"""单日永期结果:触达目标点数则按目标盈利出场,否则收盘结算.
|
|
||||||
|
|
||||||
A: 开→高 ≥ move_a → 出场净利 = 目标盈利
|
|
||||||
B: 开→低 ≥ move_b → 出场净利 = 目标盈利
|
|
||||||
两边都触达时仍按目标盈利(路径未知,任一边出场均约为目标).
|
|
||||||
"""
|
|
||||||
open_px = float(row.get("open") or 0)
|
|
||||||
close_px = float(row.get("close") or 0)
|
|
||||||
change = float(row.get("change") or 0)
|
|
||||||
up_pts = float(row.get("up_points") or 0)
|
|
||||||
down_pts = float(row.get("down_points") or 0)
|
|
||||||
target = float(hedge["target_profit_u"])
|
|
||||||
move_a, move_b, _err = perp_hedge_required_moves(open_px=open_px, hedge=hedge)
|
|
||||||
hit_a = bool(move_a is not None and move_a > 0 and up_pts >= move_a)
|
|
||||||
hit_b = bool(move_b is not None and move_b > 0 and down_pts >= move_b)
|
|
||||||
if hit_a or hit_b:
|
|
||||||
if hit_a and hit_b:
|
|
||||||
exit_tag = "target_both"
|
|
||||||
elif hit_a:
|
|
||||||
exit_tag = "target_a"
|
|
||||||
else:
|
|
||||||
exit_tag = "target_b"
|
|
||||||
return {
|
|
||||||
"pnl": round(target, 4),
|
|
||||||
"exit": exit_tag,
|
|
||||||
"hit_a": hit_a,
|
|
||||||
"hit_b": hit_b,
|
|
||||||
"move_a": move_a,
|
|
||||||
"move_b": move_b,
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
"pnl": perp_hedge_day_pnl_eod(
|
|
||||||
change=change,
|
|
||||||
open_px=open_px,
|
|
||||||
close_px=close_px,
|
|
||||||
option_leverage=float(hedge["option_leverage"]),
|
|
||||||
opt_coins=float(hedge["opt_coins"]),
|
|
||||||
),
|
|
||||||
"exit": "eod",
|
|
||||||
"hit_a": False,
|
|
||||||
"hit_b": False,
|
|
||||||
"move_a": move_a,
|
|
||||||
"move_b": move_b,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# 兼容旧名:默认按「目标出场」完整日结果取 pnl
|
|
||||||
def perp_hedge_day_pnl(
|
|
||||||
*,
|
|
||||||
change: float,
|
|
||||||
open_px: float,
|
|
||||||
close_px: float,
|
|
||||||
option_leverage: float,
|
|
||||||
opt_coins: float,
|
|
||||||
up_points: Optional[float] = None,
|
|
||||||
down_points: Optional[float] = None,
|
|
||||||
target_profit_u: Optional[float] = None,
|
|
||||||
perp_leverage: float = 10.0,
|
|
||||||
ratio_perp: float = 1.0,
|
|
||||||
ratio_opt: float = 2.0,
|
|
||||||
ct_mult: float = 0.01,
|
|
||||||
) -> float:
|
|
||||||
"""单日盈亏.若给了目标与开→高/低,触达则按目标出场;否则收盘结算."""
|
|
||||||
if target_profit_u is None or up_points is None or down_points is None:
|
|
||||||
return perp_hedge_day_pnl_eod(
|
|
||||||
change=change,
|
|
||||||
open_px=open_px,
|
|
||||||
close_px=close_px,
|
|
||||||
option_leverage=option_leverage,
|
|
||||||
opt_coins=opt_coins,
|
|
||||||
)
|
|
||||||
hedge = {
|
|
||||||
"target_profit_u": float(target_profit_u),
|
|
||||||
"perp_leverage": float(perp_leverage),
|
|
||||||
"option_leverage": float(option_leverage),
|
|
||||||
"ratio_perp": float(ratio_perp),
|
|
||||||
"ratio_opt": float(ratio_opt),
|
|
||||||
"ct_mult": float(ct_mult),
|
|
||||||
"opt_coins": float(opt_coins),
|
|
||||||
"opt_sheets": float(opt_coins) / float(ct_mult),
|
|
||||||
}
|
|
||||||
return float(
|
|
||||||
perp_hedge_day_result(
|
|
||||||
{
|
|
||||||
"open": open_px,
|
|
||||||
"close": close_px,
|
|
||||||
"change": change,
|
|
||||||
"up_points": up_points,
|
|
||||||
"down_points": down_points,
|
|
||||||
},
|
|
||||||
hedge,
|
|
||||||
)["pnl"]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def perp_hedge_stats(
|
|
||||||
rows: list[dict[str, Any]],
|
|
||||||
hedge: dict[str, float],
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""永期对冲:所需点数达标 + 按日组合盈亏汇总.
|
|
||||||
|
|
||||||
达标看开→高/开→低是否触达当日入场推得的 A/B 点数;
|
|
||||||
触达则日盈亏=目标盈利,否则收盘结算.汇总展示点数用样本开盘中位.
|
|
||||||
"""
|
|
||||||
opens = [float(r.get("open") or 0) for r in (rows or []) if float(r.get("open") or 0) > 0]
|
|
||||||
spot_ref = statistics.median(opens) if opens else None
|
|
||||||
prem_ref = (
|
|
||||||
perp_hedge_day_premium(
|
|
||||||
open_px=spot_ref,
|
|
||||||
option_leverage=hedge["option_leverage"],
|
|
||||||
opt_coins=hedge["opt_coins"],
|
|
||||||
)
|
|
||||||
if spot_ref is not None
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
prem_per_coin_ref = (spot_ref / hedge["option_leverage"]) if spot_ref is not None else None
|
|
||||||
|
|
||||||
move_a = None
|
|
||||||
move_b = None
|
|
||||||
points_err = None
|
|
||||||
if spot_ref is not None and spot_ref > 0:
|
|
||||||
move_a, move_b, points_err = perp_hedge_required_moves(open_px=spot_ref, hedge=hedge)
|
|
||||||
else:
|
|
||||||
points_err = "样本无有效开盘价,无法推所需点数"
|
|
||||||
|
|
||||||
work: list[dict[str, Any]] = []
|
|
||||||
for r in rows or []:
|
|
||||||
item = dict(r)
|
|
||||||
day = perp_hedge_day_result(item, hedge)
|
|
||||||
item["perp_hedge_pnl"] = day["pnl"]
|
|
||||||
item["perp_hedge_exit"] = day["exit"]
|
|
||||||
item["perp_hedge_hit_a"] = day["hit_a"]
|
|
||||||
item["perp_hedge_hit_b"] = day["hit_b"]
|
|
||||||
work.append(item)
|
|
||||||
n = len(work)
|
n = len(work)
|
||||||
empty = {
|
empty = {
|
||||||
"enabled": True,
|
"move_points": round(mp, 4),
|
||||||
"entry": "open",
|
|
||||||
"exit": "target_or_eod",
|
|
||||||
"spot": None if spot_ref is None else round(spot_ref, 4),
|
|
||||||
"target_profit_u": round(hedge["target_profit_u"], 4),
|
|
||||||
"perp_leverage": round(hedge["perp_leverage"], 4),
|
|
||||||
"option_leverage": round(hedge["option_leverage"], 4),
|
|
||||||
"ratio_perp": round(hedge["ratio_perp"], 4),
|
|
||||||
"ratio_opt": round(hedge["ratio_opt"], 4),
|
|
||||||
"ratio_label": f"{hedge['ratio_perp']:g}:{hedge['ratio_opt']:g}",
|
|
||||||
"prem_per_coin": None if prem_per_coin_ref is None else round(prem_per_coin_ref, 4),
|
|
||||||
"opt_coins": round(hedge["opt_coins"], 4),
|
|
||||||
"opt_sheets": round(hedge["opt_sheets"], 4),
|
|
||||||
"premium_total": None if prem_ref is None else round(prem_ref, 4),
|
|
||||||
"move_a": None if move_a is None else round(move_a, 4),
|
|
||||||
"move_b": None if move_b is None else round(move_b, 4),
|
|
||||||
"points_error": points_err,
|
|
||||||
"sample_count": n,
|
"sample_count": n,
|
||||||
"hit_a_days": 0,
|
"up_hit_days": 0,
|
||||||
"hit_a_ratio": None,
|
"up_hit_ratio": None,
|
||||||
"hit_b_days": 0,
|
"down_hit_days": 0,
|
||||||
"hit_b_ratio": None,
|
"down_hit_ratio": None,
|
||||||
"target_exit_days": 0,
|
"either_hit_days": 0,
|
||||||
"target_exit_ratio": None,
|
"either_hit_ratio": None,
|
||||||
"eod_days": 0,
|
"both_hit_days": 0,
|
||||||
"pnl_total": None,
|
"both_hit_ratio": None,
|
||||||
"pnl_avg": None,
|
"amp_hit_days": 0,
|
||||||
"win_days": 0,
|
"amp_hit_ratio": None,
|
||||||
"win_ratio": None,
|
"abs_change_hit_days": 0,
|
||||||
"pnl_max": None,
|
"abs_change_hit_ratio": None,
|
||||||
"pnl_min": None,
|
|
||||||
"up_days": 0,
|
|
||||||
"down_days": 0,
|
|
||||||
"up_pnl_total": None,
|
|
||||||
"down_pnl_total": None,
|
|
||||||
}
|
}
|
||||||
if n <= 0:
|
if n <= 0:
|
||||||
return empty
|
return empty
|
||||||
|
up_hit = sum(1 for r in work if r.get("hit_up"))
|
||||||
hit_a = sum(1 for r in work if r.get("perp_hedge_hit_a"))
|
down_hit = sum(1 for r in work if r.get("hit_down"))
|
||||||
hit_b = sum(1 for r in work if r.get("perp_hedge_hit_b"))
|
either = sum(1 for r in work if r.get("hit_either"))
|
||||||
target_exits = sum(1 for r in work if str(r.get("perp_hedge_exit") or "").startswith("target"))
|
both = sum(1 for r in work if r.get("hit_both"))
|
||||||
eod_days = sum(1 for r in work if r.get("perp_hedge_exit") == "eod")
|
amp_hit = sum(1 for r in work if float(r.get("amplitude") or 0) >= mp)
|
||||||
|
abs_hit = sum(1 for r in work if abs(float(r.get("change") or 0)) >= mp)
|
||||||
pnls = [float(r["perp_hedge_pnl"]) for r in work if r.get("perp_hedge_pnl") is not None]
|
|
||||||
win = sum(1 for p in pnls if p > 0)
|
|
||||||
up_rows = [r for r in work if float(r.get("change") or 0) >= 0]
|
|
||||||
down_rows = [r for r in work if float(r.get("change") or 0) < 0]
|
|
||||||
up_pnls = [float(r["perp_hedge_pnl"]) for r in up_rows if r.get("perp_hedge_pnl") is not None]
|
|
||||||
down_pnls = [float(r["perp_hedge_pnl"]) for r in down_rows if r.get("perp_hedge_pnl") is not None]
|
|
||||||
|
|
||||||
empty.update(
|
empty.update(
|
||||||
{
|
{
|
||||||
"hit_a_days": hit_a,
|
"up_hit_days": up_hit,
|
||||||
"hit_a_ratio": round(hit_a / n, 4),
|
"up_hit_ratio": round(up_hit / n, 4),
|
||||||
"hit_b_days": hit_b,
|
"down_hit_days": down_hit,
|
||||||
"hit_b_ratio": round(hit_b / n, 4),
|
"down_hit_ratio": round(down_hit / n, 4),
|
||||||
"target_exit_days": target_exits,
|
"either_hit_days": either,
|
||||||
"target_exit_ratio": round(target_exits / n, 4),
|
"either_hit_ratio": round(either / n, 4),
|
||||||
"eod_days": eod_days,
|
"both_hit_days": both,
|
||||||
"pnl_total": round(sum(pnls), 4) if pnls else None,
|
"both_hit_ratio": round(both / n, 4),
|
||||||
"pnl_avg": round(statistics.fmean(pnls), 4) if pnls else None,
|
"amp_hit_days": amp_hit,
|
||||||
"win_days": win,
|
"amp_hit_ratio": round(amp_hit / n, 4),
|
||||||
"win_ratio": round(win / n, 4),
|
"abs_change_hit_days": abs_hit,
|
||||||
"pnl_max": round(max(pnls), 4) if pnls else None,
|
"abs_change_hit_ratio": round(abs_hit / n, 4),
|
||||||
"pnl_min": round(min(pnls), 4) if pnls else None,
|
|
||||||
"up_days": len(up_rows),
|
|
||||||
"down_days": len(down_rows),
|
|
||||||
"up_pnl_total": round(sum(up_pnls), 4) if up_pnls else None,
|
|
||||||
"down_pnl_total": round(sum(down_pnls), 4) if down_pnls else None,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return empty
|
return empty
|
||||||
|
|
||||||
|
|
||||||
def straddle_long_stats(
|
|
||||||
rows: list[dict[str, Any]],
|
|
||||||
premium: float,
|
|
||||||
*,
|
|
||||||
take_profit: Any = None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""买跨:越过权利金用严格 >;收益=有效波动−权利金(止盈≥触达用止盈点,否则|涨跌|)."""
|
|
||||||
prem = float(premium)
|
|
||||||
if prem <= 0:
|
|
||||||
raise ValueError("双边权利金须 > 0")
|
|
||||||
tp = normalize_take_profit(take_profit)
|
|
||||||
enriched = enrich_rows_pnl(rows, straddle_premium=prem, take_profit=tp)
|
|
||||||
if not enriched:
|
|
||||||
return {
|
|
||||||
"side": "long_straddle",
|
|
||||||
"premium": prem,
|
|
||||||
"take_profit": tp,
|
|
||||||
"sample_count": 0,
|
|
||||||
"up_exceed_days": 0,
|
|
||||||
"up_exceed_ratio": None,
|
|
||||||
"down_exceed_days": 0,
|
|
||||||
"down_exceed_ratio": None,
|
|
||||||
"abs_change_exceed_days": 0,
|
|
||||||
"abs_change_exceed_ratio": None,
|
|
||||||
"tp_hit_days": 0,
|
|
||||||
"tp_hit_ratio": None,
|
|
||||||
"pnl_total": None,
|
|
||||||
"pnl_avg": None,
|
|
||||||
"win_days": 0,
|
|
||||||
"win_ratio": None,
|
|
||||||
"pnl_max": None,
|
|
||||||
"pnl_min": None,
|
|
||||||
}
|
|
||||||
n = len(enriched)
|
|
||||||
up_ex = sum(1 for r in enriched if float(r["up_points"]) > prem)
|
|
||||||
down_ex = sum(1 for r in enriched if float(r["down_points"]) > prem)
|
|
||||||
abs_ex = sum(1 for r in enriched if abs(float(r["change"])) > prem)
|
|
||||||
tp_hits = sum(1 for r in enriched if r.get("take_profit_hit"))
|
|
||||||
pnls = [float(r["profit"]) for r in enriched if r.get("profit") is not None]
|
|
||||||
win = sum(1 for p in pnls if p > 0)
|
|
||||||
return {
|
|
||||||
"side": "long_straddle",
|
|
||||||
"premium": round(prem, 4),
|
|
||||||
"take_profit": round(tp, 4) if tp is not None else None,
|
|
||||||
"sample_count": n,
|
|
||||||
"up_exceed_days": up_ex,
|
|
||||||
"up_exceed_ratio": round(up_ex / n, 4),
|
|
||||||
"down_exceed_days": down_ex,
|
|
||||||
"down_exceed_ratio": round(down_ex / n, 4),
|
|
||||||
"abs_change_exceed_days": abs_ex,
|
|
||||||
"abs_change_exceed_ratio": round(abs_ex / n, 4),
|
|
||||||
"tp_hit_days": tp_hits,
|
|
||||||
"tp_hit_ratio": round(tp_hits / n, 4) if tp is not None else None,
|
|
||||||
"pnl_total": round(sum(pnls), 4),
|
|
||||||
"pnl_avg": round(statistics.fmean(pnls), 4),
|
|
||||||
"win_days": win,
|
|
||||||
"win_ratio": round(win / n, 4),
|
|
||||||
"pnl_max": round(max(pnls), 4),
|
|
||||||
"pnl_min": round(min(pnls), 4),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def summarize_rows(
|
def summarize_rows(
|
||||||
rows: list[dict[str, Any]],
|
rows: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
straddle_premium: Any = None,
|
move_points: Any = None,
|
||||||
take_profit: Any = None,
|
|
||||||
perp_hedge: Any = None,
|
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
mp = normalize_move_points(move_points)
|
||||||
if not rows:
|
if not rows:
|
||||||
out = {
|
out = {
|
||||||
"sample_count": 0,
|
"sample_count": 0,
|
||||||
@@ -690,14 +336,10 @@ def summarize_rows(
|
|||||||
"avg_down_points": None,
|
"avg_down_points": None,
|
||||||
"up_day_ratio": None,
|
"up_day_ratio": None,
|
||||||
"down_day_ratio": None,
|
"down_day_ratio": None,
|
||||||
"straddle": None,
|
"move_points_stats": None,
|
||||||
"perp_hedge": None,
|
|
||||||
}
|
}
|
||||||
prem = normalize_straddle_premium(straddle_premium)
|
if mp is not None:
|
||||||
if prem is not None:
|
out["move_points_stats"] = move_points_stats([], mp)
|
||||||
out["straddle"] = straddle_long_stats([], prem, take_profit=take_profit)
|
|
||||||
if hedge is not None:
|
|
||||||
out["perp_hedge"] = perp_hedge_stats([], hedge)
|
|
||||||
return out
|
return out
|
||||||
amps = [float(r["amplitude"]) for r in rows]
|
amps = [float(r["amplitude"]) for r in rows]
|
||||||
ups = [float(r["up_points"]) for r in rows]
|
ups = [float(r["up_points"]) for r in rows]
|
||||||
@@ -719,14 +361,10 @@ def summarize_rows(
|
|||||||
"avg_down_points": round(statistics.fmean(downs), 4),
|
"avg_down_points": round(statistics.fmean(downs), 4),
|
||||||
"up_day_ratio": round(up_days / n, 4),
|
"up_day_ratio": round(up_days / n, 4),
|
||||||
"down_day_ratio": round(down_days / n, 4),
|
"down_day_ratio": round(down_days / n, 4),
|
||||||
"straddle": None,
|
"move_points_stats": None,
|
||||||
"perp_hedge": None,
|
|
||||||
}
|
}
|
||||||
prem = normalize_straddle_premium(straddle_premium)
|
if mp is not None:
|
||||||
if prem is not None:
|
out["move_points_stats"] = move_points_stats(rows, mp)
|
||||||
out["straddle"] = straddle_long_stats(rows, prem, take_profit=take_profit)
|
|
||||||
if hedge is not None:
|
|
||||||
out["perp_hedge"] = perp_hedge_stats(rows, hedge)
|
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -930,10 +568,8 @@ def compute_amp_stats(
|
|||||||
start_hour: int = 16,
|
start_hour: int = 16,
|
||||||
period: str = "2m",
|
period: str = "2m",
|
||||||
custom_days: Any = None,
|
custom_days: Any = None,
|
||||||
straddle_premium: Any = None,
|
move_points: Any = None,
|
||||||
take_profit: Any = None,
|
|
||||||
weekend_filter: Any = "all",
|
weekend_filter: Any = "all",
|
||||||
perp_hedge: Any = None,
|
|
||||||
now: Optional[datetime] = None,
|
now: Optional[datetime] = None,
|
||||||
fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None,
|
fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -941,10 +577,8 @@ def compute_amp_stats(
|
|||||||
sh = int(start_hour)
|
sh = int(start_hour)
|
||||||
if sh < 0 or sh > 23:
|
if sh < 0 or sh > 23:
|
||||||
raise ValueError("起点须为 0-23 整点")
|
raise ValueError("起点须为 0-23 整点")
|
||||||
prem = normalize_straddle_premium(straddle_premium)
|
mp = normalize_move_points(move_points)
|
||||||
tp = normalize_take_profit(take_profit)
|
|
||||||
we_mode = normalize_weekend_filter(weekend_filter)
|
we_mode = normalize_weekend_filter(weekend_filter)
|
||||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
|
||||||
sample_days = resolve_sample_days(period, custom_days)
|
sample_days = resolve_sample_days(period, custom_days)
|
||||||
settlements = list_settlement_dates(sample_days=sample_days, now=now)
|
settlements = list_settlement_dates(sample_days=sample_days, now=now)
|
||||||
if not settlements:
|
if not settlements:
|
||||||
@@ -974,10 +608,8 @@ def compute_amp_stats(
|
|||||||
start_hour=sh,
|
start_hour=sh,
|
||||||
period=period,
|
period=period,
|
||||||
sample_days=sample_days,
|
sample_days=sample_days,
|
||||||
straddle_premium=prem,
|
move_points=mp,
|
||||||
take_profit=tp,
|
|
||||||
weekend_filter=we_mode,
|
weekend_filter=we_mode,
|
||||||
perp_hedge=hedge,
|
|
||||||
price_source=price_source,
|
price_source=price_source,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
missing=missing,
|
missing=missing,
|
||||||
@@ -991,21 +623,17 @@ def build_amp_result(
|
|||||||
start_hour: int,
|
start_hour: int,
|
||||||
period: str,
|
period: str,
|
||||||
sample_days: int,
|
sample_days: int,
|
||||||
straddle_premium: Any = None,
|
move_points: Any = None,
|
||||||
take_profit: Any = None,
|
|
||||||
weekend_filter: Any = "all",
|
weekend_filter: Any = "all",
|
||||||
perp_hedge: Any = None,
|
|
||||||
price_source: str = "",
|
price_source: str = "",
|
||||||
inst_id: str = "",
|
inst_id: str = "",
|
||||||
missing: Optional[list[str]] = None,
|
missing: Optional[list[str]] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
prem = normalize_straddle_premium(straddle_premium)
|
mp = normalize_move_points(move_points)
|
||||||
tp = normalize_take_profit(take_profit)
|
|
||||||
we_mode = normalize_weekend_filter(weekend_filter)
|
we_mode = normalize_weekend_filter(weekend_filter)
|
||||||
hedge = normalize_perp_hedge_params(perp_hedge)
|
|
||||||
filtered = filter_weekend_rows(rows_all, we_mode)
|
filtered = filter_weekend_rows(rows_all, we_mode)
|
||||||
rows = enrich_rows_pnl(filtered, straddle_premium=prem, take_profit=tp, perp_hedge=hedge)
|
rows = enrich_rows(filtered, move_points=mp)
|
||||||
summary = summarize_rows(rows, straddle_premium=prem, take_profit=tp, perp_hedge=hedge)
|
summary = summarize_rows(rows, move_points=mp)
|
||||||
if period == "custom" or str(period).startswith("custom:"):
|
if period == "custom" or str(period).startswith("custom:"):
|
||||||
period_label = period if str(period).startswith("custom:") else f"custom:{sample_days}"
|
period_label = period if str(period).startswith("custom:") else f"custom:{sample_days}"
|
||||||
else:
|
else:
|
||||||
@@ -1020,10 +648,8 @@ def build_amp_result(
|
|||||||
"end_hour": END_HOUR,
|
"end_hour": END_HOUR,
|
||||||
"period": period_label,
|
"period": period_label,
|
||||||
"sample_days_requested": sample_days,
|
"sample_days_requested": sample_days,
|
||||||
"straddle_premium": prem,
|
"move_points": mp,
|
||||||
"take_profit": tp,
|
|
||||||
"weekend_filter": we_mode,
|
"weekend_filter": we_mode,
|
||||||
"perp_hedge": hedge,
|
|
||||||
"timeframe": TIMEFRAME,
|
"timeframe": TIMEFRAME,
|
||||||
"price_source": price_source,
|
"price_source": price_source,
|
||||||
"inst_id": inst_id,
|
"inst_id": inst_id,
|
||||||
@@ -1043,15 +669,13 @@ def reframe_amp_stats(
|
|||||||
start_hour: int = 16,
|
start_hour: int = 16,
|
||||||
period: str = "2m",
|
period: str = "2m",
|
||||||
sample_days: int = 60,
|
sample_days: int = 60,
|
||||||
straddle_premium: Any = None,
|
move_points: Any = None,
|
||||||
take_profit: Any = None,
|
|
||||||
weekend_filter: Any = "all",
|
weekend_filter: Any = "all",
|
||||||
perp_hedge: Any = None,
|
|
||||||
price_source: str = "",
|
price_source: str = "",
|
||||||
inst_id: str = "",
|
inst_id: str = "",
|
||||||
missing: Optional[list[str]] = None,
|
missing: Optional[list[str]] = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""已有日表上改周末/权利金/止盈/永期参数,不拉 K 线."""
|
"""已有日表上改周末/波动点数,不拉 K 线."""
|
||||||
key = normalize_symbol(symbol)
|
key = normalize_symbol(symbol)
|
||||||
return build_amp_result(
|
return build_amp_result(
|
||||||
rows_all=list(rows_all or []),
|
rows_all=list(rows_all or []),
|
||||||
@@ -1059,10 +683,8 @@ def reframe_amp_stats(
|
|||||||
start_hour=int(start_hour),
|
start_hour=int(start_hour),
|
||||||
period=period,
|
period=period,
|
||||||
sample_days=int(sample_days or 60),
|
sample_days=int(sample_days or 60),
|
||||||
straddle_premium=straddle_premium,
|
move_points=move_points,
|
||||||
take_profit=take_profit,
|
|
||||||
weekend_filter=weekend_filter,
|
weekend_filter=weekend_filter,
|
||||||
perp_hedge=perp_hedge,
|
|
||||||
price_source=price_source,
|
price_source=price_source,
|
||||||
inst_id=inst_id,
|
inst_id=inst_id,
|
||||||
missing=missing,
|
missing=missing,
|
||||||
@@ -1104,104 +726,14 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
|||||||
w.writerow(["开→高最大", s.get("max_up_points"), "均值", s.get("avg_up_points")])
|
w.writerow(["开→高最大", s.get("max_up_points"), "均值", s.get("avg_up_points")])
|
||||||
w.writerow(["开→低最大", s.get("max_down_points"), "均值", s.get("avg_down_points")])
|
w.writerow(["开→低最大", s.get("max_down_points"), "均值", s.get("avg_down_points")])
|
||||||
w.writerow(["上涨窗占比", s.get("up_day_ratio"), "下跌窗占比", s.get("down_day_ratio")])
|
w.writerow(["上涨窗占比", s.get("up_day_ratio"), "下跌窗占比", s.get("down_day_ratio")])
|
||||||
st = s.get("straddle") or {}
|
mp = s.get("move_points_stats") or {}
|
||||||
if st:
|
if mp:
|
||||||
w.writerow([])
|
w.writerow([])
|
||||||
w.writerow(["【买跨对照·双边权利金】", st.get("premium"), "止盈点", st.get("take_profit")])
|
w.writerow(["【波动点数·振幅占比】", mp.get("move_points")])
|
||||||
w.writerow(["开→高超过权利金", st.get("up_exceed_days"), "占比", st.get("up_exceed_ratio")])
|
w.writerow(["振幅≥点数天数", mp.get("amp_hit_days"), "占比", mp.get("amp_hit_ratio")])
|
||||||
w.writerow(["开→低超过权利金", st.get("down_exceed_days"), "占比", st.get("down_exceed_ratio")])
|
w.writerow(["开→高≥点数天数", mp.get("up_hit_days"), "占比", mp.get("up_hit_ratio")])
|
||||||
w.writerow(["|涨跌|超过权利金", st.get("abs_change_exceed_days"), "占比", st.get("abs_change_exceed_ratio")])
|
w.writerow(["开→低≥点数天数", mp.get("down_hit_days"), "占比", mp.get("down_hit_ratio")])
|
||||||
if st.get("take_profit") is not None:
|
w.writerow(["|涨跌|≥点数天数", mp.get("abs_change_hit_days"), "占比", mp.get("abs_change_hit_ratio")])
|
||||||
w.writerow(["触达止盈天数", st.get("tp_hit_days"), "占比", st.get("tp_hit_ratio")])
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"买跨点数盈亏合计",
|
|
||||||
st.get("pnl_total"),
|
|
||||||
"日均",
|
|
||||||
st.get("pnl_avg"),
|
|
||||||
"赚钱天数",
|
|
||||||
st.get("win_days"),
|
|
||||||
"胜率",
|
|
||||||
st.get("win_ratio"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(["单日最大赚", st.get("pnl_max"), "单日最大亏", st.get("pnl_min")])
|
|
||||||
ph = s.get("perp_hedge") or {}
|
|
||||||
if ph:
|
|
||||||
w.writerow([])
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"【永期对冲对照】",
|
|
||||||
"比例",
|
|
||||||
ph.get("ratio_label"),
|
|
||||||
"入场",
|
|
||||||
"按日开盘",
|
|
||||||
"推点数开盘中位",
|
|
||||||
ph.get("spot"),
|
|
||||||
"目标",
|
|
||||||
ph.get("target_profit_u"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"单币权利金(开盘中位)",
|
|
||||||
ph.get("prem_per_coin"),
|
|
||||||
"权利金总额(开盘中位)",
|
|
||||||
ph.get("premium_total"),
|
|
||||||
"期权币数",
|
|
||||||
ph.get("opt_coins"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"A所需点数",
|
|
||||||
ph.get("move_a"),
|
|
||||||
"A达标天(开→高)",
|
|
||||||
ph.get("hit_a_days"),
|
|
||||||
"占比",
|
|
||||||
ph.get("hit_a_ratio"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"B所需点数(组合)",
|
|
||||||
ph.get("move_b"),
|
|
||||||
"B达标天(开→低)",
|
|
||||||
ph.get("hit_b_days"),
|
|
||||||
"占比",
|
|
||||||
ph.get("hit_b_ratio"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"目标出场天",
|
|
||||||
ph.get("target_exit_days"),
|
|
||||||
"收盘结算天",
|
|
||||||
ph.get("eod_days"),
|
|
||||||
"目标盈利",
|
|
||||||
ph.get("target_profit_u"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"组合盈亏合计",
|
|
||||||
ph.get("pnl_total"),
|
|
||||||
"日均",
|
|
||||||
ph.get("pnl_avg"),
|
|
||||||
"胜率",
|
|
||||||
ph.get("win_ratio"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow(
|
|
||||||
[
|
|
||||||
"上涨日盈亏",
|
|
||||||
ph.get("up_pnl_total"),
|
|
||||||
"下跌日盈亏",
|
|
||||||
ph.get("down_pnl_total"),
|
|
||||||
"最大赚/亏",
|
|
||||||
f"{ph.get('pnl_max')} / {ph.get('pnl_min')}",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
w.writerow([])
|
w.writerow([])
|
||||||
w.writerow(["【日表明细】"])
|
w.writerow(["【日表明细】"])
|
||||||
w.writerow(
|
w.writerow(
|
||||||
@@ -1219,10 +751,8 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
|||||||
"开→低",
|
"开→低",
|
||||||
"振幅",
|
"振幅",
|
||||||
"涨跌值",
|
"涨跌值",
|
||||||
"有效波动",
|
"对照点数",
|
||||||
"触达止盈",
|
"振幅达标",
|
||||||
"收益",
|
|
||||||
"永期盈亏",
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for r in payload.get("rows") or []:
|
for r in payload.get("rows") or []:
|
||||||
@@ -1241,10 +771,8 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
|||||||
r.get("down_points"),
|
r.get("down_points"),
|
||||||
r.get("amplitude"),
|
r.get("amplitude"),
|
||||||
r.get("change"),
|
r.get("change"),
|
||||||
r.get("effective_move"),
|
r.get("move_points") if r.get("move_points") is not None else "",
|
||||||
"是" if r.get("take_profit_hit") else "否",
|
"是" if r.get("amp_hit") else ("否" if r.get("move_points") is not None else ""),
|
||||||
r.get("profit"),
|
|
||||||
r.get("perp_hedge_pnl"),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return buf.getvalue()
|
return buf.getvalue()
|
||||||
|
|||||||
@@ -12,33 +12,20 @@ from lib.hub.amp_stats_lib import (
|
|||||||
build_export_csv,
|
build_export_csv,
|
||||||
compute_amp_stats,
|
compute_amp_stats,
|
||||||
export_filename,
|
export_filename,
|
||||||
normalize_straddle_premium,
|
normalize_move_points,
|
||||||
normalize_take_profit,
|
|
||||||
normalize_weekend_filter,
|
normalize_weekend_filter,
|
||||||
reframe_amp_stats,
|
reframe_amp_stats,
|
||||||
rows_page,
|
rows_page,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PerpHedgeBody(BaseModel):
|
|
||||||
spot: Optional[float] = None
|
|
||||||
target_profit_u: Optional[float] = None
|
|
||||||
perp_leverage: Optional[float] = None
|
|
||||||
option_leverage: Optional[float] = None
|
|
||||||
ratio_perp: float = 1.0
|
|
||||||
ratio_opt: float = 2.0
|
|
||||||
ct_mult: float = 0.01
|
|
||||||
|
|
||||||
|
|
||||||
class ComputeBody(BaseModel):
|
class ComputeBody(BaseModel):
|
||||||
symbol: str = "eth"
|
symbol: str = "eth"
|
||||||
start_hour: int = 16
|
start_hour: int = 16
|
||||||
period: str = "2m"
|
period: str = "2m"
|
||||||
custom_days: Optional[int] = None
|
custom_days: Optional[int] = None
|
||||||
straddle_premium: Optional[float] = None
|
move_points: Optional[float] = None
|
||||||
take_profit: Optional[float] = None
|
|
||||||
weekend_filter: str = "all"
|
weekend_filter: str = "all"
|
||||||
perp_hedge: Optional[PerpHedgeBody] = None
|
|
||||||
page: int = 1
|
page: int = 1
|
||||||
page_size: int = 20
|
page_size: int = 20
|
||||||
|
|
||||||
@@ -48,29 +35,21 @@ class SaveBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ReframeBody(BaseModel):
|
class ReframeBody(BaseModel):
|
||||||
"""已有日表上改周末/权利金/止盈/永期参数(不拉 K 线)."""
|
"""已有日表上改周末/波动点数(不拉 K 线)."""
|
||||||
|
|
||||||
rows_all: list[dict[str, Any]] = Field(default_factory=list)
|
rows_all: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
symbol: str = "eth"
|
symbol: str = "eth"
|
||||||
start_hour: int = 16
|
start_hour: int = 16
|
||||||
period: str = "2m"
|
period: str = "2m"
|
||||||
sample_days: int = 60
|
sample_days: int = 60
|
||||||
straddle_premium: Optional[float] = None
|
move_points: Optional[float] = None
|
||||||
take_profit: Optional[float] = None
|
|
||||||
weekend_filter: str = "all"
|
weekend_filter: str = "all"
|
||||||
perp_hedge: Optional[PerpHedgeBody] = None
|
|
||||||
price_source: str = ""
|
price_source: str = ""
|
||||||
inst_id: str = ""
|
inst_id: str = ""
|
||||||
page: int = 1
|
page: int = 1
|
||||||
page_size: int = 20
|
page_size: int = 20
|
||||||
|
|
||||||
|
|
||||||
def _hedge_dict(body_hedge: Optional[PerpHedgeBody]) -> Optional[dict[str, Any]]:
|
|
||||||
if body_hedge is None:
|
|
||||||
return None
|
|
||||||
return body_hedge.model_dump()
|
|
||||||
|
|
||||||
|
|
||||||
def create_amp_stats_router() -> APIRouter:
|
def create_amp_stats_router() -> APIRouter:
|
||||||
router = APIRouter(prefix="/api/amp-stats", tags=["amp-stats"])
|
router = APIRouter(prefix="/api/amp-stats", tags=["amp-stats"])
|
||||||
|
|
||||||
@@ -102,12 +81,7 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
"default_weekend_filter": "all",
|
"default_weekend_filter": "all",
|
||||||
"timeframe": "1H",
|
"timeframe": "1H",
|
||||||
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
||||||
"straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金",
|
"move_points_note": "填波动点数后统计振幅≥该点数的天数占比;日表显示开→高/开→低两边点数与振幅是否达标",
|
||||||
"perp_hedge_note": "永期对冲:永续多1币+买期权;入场按日开盘;比例默认1:2;与买跨二选一对照",
|
|
||||||
"overlay_modes": [
|
|
||||||
{"key": "straddle", "label": "买跨双边"},
|
|
||||||
{"key": "perp", "label": "永期对冲"},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@router.post("/compute")
|
@router.post("/compute")
|
||||||
@@ -118,10 +92,8 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour=body.start_hour,
|
start_hour=body.start_hour,
|
||||||
period=body.period,
|
period=body.period,
|
||||||
custom_days=body.custom_days,
|
custom_days=body.custom_days,
|
||||||
straddle_premium=body.straddle_premium,
|
move_points=body.move_points,
|
||||||
take_profit=body.take_profit,
|
|
||||||
weekend_filter=body.weekend_filter,
|
weekend_filter=body.weekend_filter,
|
||||||
perp_hedge=_hedge_dict(body.perp_hedge),
|
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
@@ -146,10 +118,8 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour=body.start_hour,
|
start_hour=body.start_hour,
|
||||||
period=body.period,
|
period=body.period,
|
||||||
sample_days=body.sample_days,
|
sample_days=body.sample_days,
|
||||||
straddle_premium=body.straddle_premium,
|
move_points=body.move_points,
|
||||||
take_profit=body.take_profit,
|
|
||||||
weekend_filter=body.weekend_filter,
|
weekend_filter=body.weekend_filter,
|
||||||
perp_hedge=_hedge_dict(body.perp_hedge),
|
|
||||||
price_source=body.price_source,
|
price_source=body.price_source,
|
||||||
inst_id=body.inst_id,
|
inst_id=body.inst_id,
|
||||||
)
|
)
|
||||||
@@ -190,33 +160,15 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour: int = Query(default=16),
|
start_hour: int = Query(default=16),
|
||||||
period: str = Query(default="2m"),
|
period: str = Query(default="2m"),
|
||||||
custom_days: Optional[int] = Query(default=None),
|
custom_days: Optional[int] = Query(default=None),
|
||||||
straddle_premium: Optional[float] = Query(default=None),
|
move_points: Optional[float] = Query(default=None),
|
||||||
take_profit: Optional[float] = Query(default=None),
|
|
||||||
weekend_filter: str = Query(default="all"),
|
weekend_filter: str = Query(default="all"),
|
||||||
hedge_spot: Optional[float] = Query(default=None),
|
|
||||||
hedge_target: Optional[float] = Query(default=None),
|
|
||||||
hedge_perp_lev: Optional[float] = Query(default=None),
|
|
||||||
hedge_opt_lev: Optional[float] = Query(default=None),
|
|
||||||
hedge_ratio_perp: float = Query(default=1.0),
|
|
||||||
hedge_ratio_opt: float = Query(default=2.0),
|
|
||||||
hedge_ct_mult: float = Query(default=0.01),
|
|
||||||
):
|
):
|
||||||
hedge_q = {
|
|
||||||
"target_profit_u": hedge_target,
|
|
||||||
"perp_leverage": hedge_perp_lev,
|
|
||||||
"option_leverage": hedge_opt_lev,
|
|
||||||
"ratio_perp": hedge_ratio_perp,
|
|
||||||
"ratio_opt": hedge_ratio_opt,
|
|
||||||
"ct_mult": hedge_ct_mult,
|
|
||||||
}
|
|
||||||
hedge_q_ready = hedge_target is not None and hedge_perp_lev is not None and hedge_opt_lev is not None
|
|
||||||
if (history_id or "").strip():
|
if (history_id or "").strip():
|
||||||
item = get_history(history_id.strip())
|
item = get_history(history_id.strip())
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=404, detail="历史不存在")
|
raise HTTPException(status_code=404, detail="历史不存在")
|
||||||
rows_all = item.get("rows_all") or item.get("rows") or []
|
rows_all = item.get("rows_all") or item.get("rows") or []
|
||||||
item_hedge = item.get("perp_hedge") if isinstance(item.get("perp_hedge"), dict) else None
|
use_mp = move_points if move_points is not None else item.get("move_points")
|
||||||
use_hedge = hedge_q if hedge_q_ready else item_hedge
|
|
||||||
try:
|
try:
|
||||||
payload = reframe_amp_stats(
|
payload = reframe_amp_stats(
|
||||||
rows_all=rows_all,
|
rows_all=rows_all,
|
||||||
@@ -224,12 +176,8 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
start_hour=int(item.get("start_hour") if item.get("start_hour") is not None else start_hour),
|
start_hour=int(item.get("start_hour") if item.get("start_hour") is not None else start_hour),
|
||||||
period=str(item.get("period") or period),
|
period=str(item.get("period") or period),
|
||||||
sample_days=int(item.get("sample_days_requested") or 60),
|
sample_days=int(item.get("sample_days_requested") or 60),
|
||||||
straddle_premium=straddle_premium
|
move_points=use_mp,
|
||||||
if straddle_premium is not None
|
|
||||||
else item.get("straddle_premium"),
|
|
||||||
take_profit=take_profit if take_profit is not None else item.get("take_profit"),
|
|
||||||
weekend_filter=weekend_filter or item.get("weekend_filter") or "all",
|
weekend_filter=weekend_filter or item.get("weekend_filter") or "all",
|
||||||
perp_hedge=use_hedge,
|
|
||||||
price_source=str(item.get("price_source") or ""),
|
price_source=str(item.get("price_source") or ""),
|
||||||
inst_id=str(item.get("inst_id") or ""),
|
inst_id=str(item.get("inst_id") or ""),
|
||||||
missing=item.get("missing_days") or [],
|
missing=item.get("missing_days") or [],
|
||||||
@@ -238,19 +186,15 @@ def create_amp_stats_router() -> APIRouter:
|
|||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# validate enums early
|
|
||||||
normalize_weekend_filter(weekend_filter)
|
normalize_weekend_filter(weekend_filter)
|
||||||
normalize_straddle_premium(straddle_premium)
|
normalize_move_points(move_points)
|
||||||
normalize_take_profit(take_profit)
|
|
||||||
payload = compute_amp_stats(
|
payload = compute_amp_stats(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
start_hour=start_hour,
|
start_hour=start_hour,
|
||||||
period=period,
|
period=period,
|
||||||
custom_days=custom_days,
|
custom_days=custom_days,
|
||||||
straddle_premium=straddle_premium,
|
move_points=move_points,
|
||||||
take_profit=take_profit,
|
|
||||||
weekend_filter=weekend_filter,
|
weekend_filter=weekend_filter,
|
||||||
perp_hedge=hedge_q if hedge_q_ready else None,
|
|
||||||
)
|
)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* 中控振幅统计:OKX ETH/BTC + 买跨/止盈/周末筛选.
|
* 中控振幅统计:OKX ETH/BTC + 波动点数振幅占比 + 周末筛选.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
const page = document.getElementById("page-amp-stats");
|
const page = document.getElementById("page-amp-stats");
|
||||||
@@ -39,32 +39,8 @@
|
|||||||
return (n * 100).toFixed(1) + "%";
|
return (n * 100).toFixed(1) + "%";
|
||||||
}
|
}
|
||||||
|
|
||||||
function overlayMode() {
|
function readMovePoints() {
|
||||||
return el("amp-overlay-mode")?.value || "straddle";
|
const raw = (el("amp-move-points")?.value || "").trim();
|
||||||
}
|
|
||||||
|
|
||||||
function syncOverlayMode() {
|
|
||||||
const isPerp = overlayMode() === "perp";
|
|
||||||
page.querySelectorAll(".amp-overlay-straddle").forEach((n) => n.classList.toggle("hidden", isPerp));
|
|
||||||
page.querySelectorAll(".amp-overlay-perp").forEach((n) => n.classList.toggle("hidden", !isPerp));
|
|
||||||
el("amp-overlay-straddle-block")?.classList.toggle("hidden", isPerp);
|
|
||||||
el("amp-overlay-perp-block")?.classList.toggle("hidden", !isPerp);
|
|
||||||
const col = el("amp-col-pnl");
|
|
||||||
if (col) col.textContent = isPerp ? "永期盈亏" : "收益";
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPremium() {
|
|
||||||
if (overlayMode() !== "straddle") return null;
|
|
||||||
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() {
|
|
||||||
if (overlayMode() !== "straddle") return null;
|
|
||||||
const raw = (el("amp-take-profit")?.value || "").trim();
|
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
if (!Number.isFinite(n) || n <= 0) return null;
|
if (!Number.isFinite(n) || n <= 0) return null;
|
||||||
@@ -75,30 +51,6 @@
|
|||||||
return el("amp-weekend-filter")?.value || "all";
|
return el("amp-weekend-filter")?.value || "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
function readNum(id) {
|
|
||||||
const raw = (el(id)?.value || "").trim();
|
|
||||||
if (!raw) return null;
|
|
||||||
const n = Number(raw);
|
|
||||||
return Number.isFinite(n) ? n : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readPerpHedge() {
|
|
||||||
if (overlayMode() !== "perp") return null;
|
|
||||||
const target = readNum("amp-hedge-target");
|
|
||||||
const perpLev = readNum("amp-hedge-perp-lev");
|
|
||||||
const optLev = readNum("amp-hedge-opt-lev");
|
|
||||||
if (target == null || perpLev == null || optLev == null) return null;
|
|
||||||
if (target < 0 || perpLev <= 0 || optLev <= 0) return null;
|
|
||||||
return {
|
|
||||||
target_profit_u: target,
|
|
||||||
perp_leverage: perpLev,
|
|
||||||
option_leverage: optLev,
|
|
||||||
ratio_perp: readNum("amp-hedge-ratio-perp") || 1,
|
|
||||||
ratio_opt: readNum("amp-hedge-ratio-opt") || 2,
|
|
||||||
ct_mult: 0.01,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setStatus(msg) {
|
function setStatus(msg) {
|
||||||
const s = el("amp-status");
|
const s = el("amp-status");
|
||||||
if (s) s.textContent = msg || "";
|
if (s) s.textContent = msg || "";
|
||||||
@@ -135,21 +87,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pnlClass(v) {
|
|
||||||
const n = Number(v);
|
|
||||||
if (!Number.isFinite(n) || n === 0) return "";
|
|
||||||
return n > 0 ? "is-pos" : "is-neg";
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSummary(summary, result) {
|
function renderSummary(summary, result) {
|
||||||
const box = el("amp-summary");
|
const box = el("amp-summary");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
const s = summary || {};
|
const s = summary || {};
|
||||||
syncOverlayMode();
|
|
||||||
if (!s.sample_count) {
|
if (!s.sample_count) {
|
||||||
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
||||||
renderStraddle(null);
|
renderMoveStats(null);
|
||||||
renderPerpHedge(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
@@ -163,76 +107,23 @@
|
|||||||
`<div><span class="amp-sum-k">涨/跌窗占比</span><span class="amp-sum-v">${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}</span></div>` +
|
`<div><span class="amp-sum-k">涨/跌窗占比</span><span class="amp-sum-v">${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">价源</span><span class="amp-sum-v">${esc(result && result.price_source)}</span></div>` +
|
`<div><span class="amp-sum-k">价源</span><span class="amp-sum-v">${esc(result && result.price_source)}</span></div>` +
|
||||||
`</div>`;
|
`</div>`;
|
||||||
if (overlayMode() === "perp") {
|
renderMoveStats(s.move_points_stats);
|
||||||
renderStraddle(null);
|
|
||||||
renderPerpHedge(s.perp_hedge);
|
|
||||||
} else {
|
|
||||||
renderPerpHedge(null);
|
|
||||||
renderStraddle(s.straddle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStraddle(st) {
|
function renderMoveStats(ms) {
|
||||||
const box = el("amp-straddle");
|
const box = el("amp-move-stats");
|
||||||
if (!box) return;
|
if (!box) return;
|
||||||
if (!st) {
|
if (!ms) {
|
||||||
box.innerHTML = '<p class="amp-empty">填写「买跨·双边权利金」后计算,可看越过天数与买跨点数盈亏</p>';
|
box.innerHTML = '<p class="amp-empty">填写「波动点数」后计算,可看振幅≥该点数的天数占比</p>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const verdict =
|
|
||||||
st.pnl_total == null
|
|
||||||
? "—"
|
|
||||||
: Number(st.pnl_total) > 0
|
|
||||||
? "样本合计盈利"
|
|
||||||
: Number(st.pnl_total) < 0
|
|
||||||
? "样本合计亏损"
|
|
||||||
: "样本合计持平";
|
|
||||||
const tpLine =
|
|
||||||
st.take_profit != null
|
|
||||||
? `<div><span class="amp-sum-k">止盈点 / 触达</span><span class="amp-sum-v">${esc(st.take_profit)} · ${esc(st.tp_hit_days)} 天 · ${esc(pct(st.tp_hit_ratio))}</span></div>`
|
|
||||||
: `<div><span class="amp-sum-k">止盈点</span><span class="amp-sum-v">未设(按|涨跌|)</span></div>`;
|
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
`<div class="amp-sum-grid">` +
|
`<div class="amp-sum-grid">` +
|
||||||
`<div><span class="amp-sum-k">双边权利金</span><span class="amp-sum-v">${esc(st.premium)}</span></div>` +
|
`<div><span class="amp-sum-k">对照点数</span><span class="amp-sum-v">${esc(ms.move_points)}</span></div>` +
|
||||||
tpLine +
|
`<div><span class="amp-sum-k">振幅≥点数</span><span class="amp-sum-v">${esc(ms.amp_hit_days)} 天 · <strong>${esc(pct(ms.amp_hit_ratio))}</strong></span></div>` +
|
||||||
`<div><span class="amp-sum-k">开→高超过权利金</span><span class="amp-sum-v">${esc(st.up_exceed_days)} 天 · ${esc(pct(st.up_exceed_ratio))}</span></div>` +
|
`<div><span class="amp-sum-k">开→高≥点数</span><span class="amp-sum-v">${esc(ms.up_hit_days)} 天 · ${esc(pct(ms.up_hit_ratio))}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">开→低超过权利金</span><span class="amp-sum-v">${esc(st.down_exceed_days)} 天 · ${esc(pct(st.down_exceed_ratio))}</span></div>` +
|
`<div><span class="amp-sum-k">开→低≥点数</span><span class="amp-sum-v">${esc(ms.down_hit_days)} 天 · ${esc(pct(ms.down_hit_ratio))}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">|涨跌|超过权利金</span><span class="amp-sum-v">${esc(st.abs_change_exceed_days)} 天 · ${esc(pct(st.abs_change_exceed_ratio))}</span></div>` +
|
`<div><span class="amp-sum-k">|涨跌|≥点数</span><span class="amp-sum-v">${esc(ms.abs_change_hit_days)} 天 · ${esc(pct(ms.abs_change_hit_ratio))}</span></div>` +
|
||||||
`<div><span class="amp-sum-k">买跨盈亏合计</span><span class="amp-sum-v ${pnlClass(st.pnl_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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPerpHedge(ph) {
|
|
||||||
const box = el("amp-perp-hedge");
|
|
||||||
if (!box) return;
|
|
||||||
if (!ph) {
|
|
||||||
box.innerHTML =
|
|
||||||
'<p class="amp-empty">填写「目标 / 杠杆」后计算;入场按日开盘;触达目标点数按目标盈利出场,否则收盘结算</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const err =
|
|
||||||
ph.points_error
|
|
||||||
? `<div><span class="amp-sum-k">推点数提示</span><span class="amp-sum-v">${esc(ph.points_error)}</span></div>`
|
|
||||||
: "";
|
|
||||||
box.innerHTML =
|
|
||||||
`<div class="amp-sum-grid">` +
|
|
||||||
`<div><span class="amp-sum-k">入场 / 出场</span><span class="amp-sum-v">开盘 · 目标 ${esc(ph.target_profit_u)}U 或收盘</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">比例 / 期权仓</span><span class="amp-sum-v">${esc(ph.ratio_label)} · ${esc(ph.opt_coins)} 币</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">单币/总权利金(中位)</span><span class="amp-sum-v">${esc(ph.prem_per_coin)} / ${esc(ph.premium_total)}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">A所需点数(永续对)</span><span class="amp-sum-v">${esc(ph.move_a)}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">A达标(开→高)</span><span class="amp-sum-v">${esc(ph.hit_a_days)} 天 · ${esc(pct(ph.hit_a_ratio))}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">B所需点数(组合)</span><span class="amp-sum-v">${esc(ph.move_b)}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">B达标(开→低)</span><span class="amp-sum-v">${esc(ph.hit_b_days)} 天 · ${esc(pct(ph.hit_b_ratio))}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">目标出场 / 收盘结算</span><span class="amp-sum-v">${esc(ph.target_exit_days)} / ${esc(ph.eod_days)} 天</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">组合盈亏合计</span><span class="amp-sum-v ${pnlClass(ph.pnl_total)}">${esc(ph.pnl_total)}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">日均 / 胜率</span><span class="amp-sum-v ${pnlClass(ph.pnl_avg)}">${esc(ph.pnl_avg)} · ${esc(pct(ph.win_ratio))}</span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">上涨日盈亏</span><span class="amp-sum-v ${pnlClass(ph.up_pnl_total)}">${esc(ph.up_pnl_total)} <small>(${esc(ph.up_days)}天)</small></span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">下跌日盈亏</span><span class="amp-sum-v ${pnlClass(ph.down_pnl_total)}">${esc(ph.down_pnl_total)} <small>(${esc(ph.down_days)}天)</small></span></div>` +
|
|
||||||
`<div><span class="amp-sum-k">单日最大赚/亏</span><span class="amp-sum-v">${esc(ph.pnl_max)} / ${esc(ph.pnl_min)}</span></div>` +
|
|
||||||
err +
|
|
||||||
`</div>`;
|
`</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,24 +135,26 @@
|
|||||||
return day;
|
return day;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hitCell(r) {
|
||||||
|
if (r.move_points == null) return "—";
|
||||||
|
if (r.amp_hit) return '<span class="amp-pnl is-pos">是</span>';
|
||||||
|
return '<span class="amp-pnl is-neg">否</span>';
|
||||||
|
}
|
||||||
|
|
||||||
function renderTable(pagePayload) {
|
function renderTable(pagePayload) {
|
||||||
const body = el("amp-table-body");
|
const body = el("amp-table-body");
|
||||||
const pager = el("amp-pager");
|
const pager = el("amp-pager");
|
||||||
if (!body) return;
|
if (!body) return;
|
||||||
const isPerp = overlayMode() === "perp";
|
|
||||||
syncOverlayMode();
|
|
||||||
const rows = (pagePayload && pagePayload.rows) || [];
|
const rows = (pagePayload && pagePayload.rows) || [];
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
|
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
|
||||||
} else {
|
} else {
|
||||||
body.innerHTML = rows
|
body.innerHTML = rows
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
const pnlVal = isPerp ? r.perp_hedge_pnl : r.profit;
|
|
||||||
const pnlCell =
|
|
||||||
pnlVal == null || pnlVal === ""
|
|
||||||
? "—"
|
|
||||||
: `<span class="amp-pnl ${pnlClass(pnlVal)}">${esc(pnlVal)}</span>`;
|
|
||||||
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
|
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
|
||||||
|
const upCls = r.hit_up ? ' class="amp-pnl is-pos"' : "";
|
||||||
|
const downCls = r.hit_down ? ' class="amp-pnl is-pos"' : "";
|
||||||
|
const ampCls = r.amp_hit ? ' class="amp-pnl is-pos"' : "";
|
||||||
return (
|
return (
|
||||||
`<tr${trClass}>` +
|
`<tr${trClass}>` +
|
||||||
`<td>${dayLabel(r)}</td>` +
|
`<td>${dayLabel(r)}</td>` +
|
||||||
@@ -270,11 +163,11 @@
|
|||||||
`<td>${esc(r.high)}</td>` +
|
`<td>${esc(r.high)}</td>` +
|
||||||
`<td>${esc(r.low)}</td>` +
|
`<td>${esc(r.low)}</td>` +
|
||||||
`<td>${esc(r.close)}</td>` +
|
`<td>${esc(r.close)}</td>` +
|
||||||
`<td>${esc(r.up_points)}</td>` +
|
`<td${upCls}>${esc(r.up_points)}</td>` +
|
||||||
`<td>${esc(r.down_points)}</td>` +
|
`<td${downCls}>${esc(r.down_points)}</td>` +
|
||||||
`<td><strong>${esc(r.amplitude)}</strong></td>` +
|
`<td${ampCls}><strong>${esc(r.amplitude)}</strong></td>` +
|
||||||
`<td>${esc(r.change)}</td>` +
|
`<td>${esc(r.change)}</td>` +
|
||||||
`<td>${pnlCell}</td>` +
|
`<td>${hitCell(r)}</td>` +
|
||||||
`</tr>`
|
`</tr>`
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -308,8 +201,7 @@
|
|||||||
|
|
||||||
async function reframe(resetPage) {
|
async function reframe(resetPage) {
|
||||||
if (!lastResult) {
|
if (!lastResult) {
|
||||||
renderStraddle(null);
|
renderMoveStats(null);
|
||||||
renderPerpHedge(null);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (resetPage) pageNo = 1;
|
if (resetPage) pageNo = 1;
|
||||||
@@ -325,10 +217,8 @@
|
|||||||
start_hour: lastResult.start_hour ?? Number(el("amp-start-hour")?.value || 16),
|
start_hour: lastResult.start_hour ?? Number(el("amp-start-hour")?.value || 16),
|
||||||
period: lastResult.period || el("amp-period")?.value || "2m",
|
period: lastResult.period || el("amp-period")?.value || "2m",
|
||||||
sample_days: lastResult.sample_days_requested || 60,
|
sample_days: lastResult.sample_days_requested || 60,
|
||||||
straddle_premium: readPremium(),
|
move_points: readMovePoints(),
|
||||||
take_profit: readTakeProfit(),
|
|
||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
perp_hedge: readPerpHedge(),
|
|
||||||
price_source: lastResult.price_source || "",
|
price_source: lastResult.price_source || "",
|
||||||
inst_id: lastResult.inst_id || "",
|
inst_id: lastResult.inst_id || "",
|
||||||
page: pageNo,
|
page: pageNo,
|
||||||
@@ -368,10 +258,8 @@
|
|||||||
start_hour: startHour,
|
start_hour: startHour,
|
||||||
period,
|
period,
|
||||||
custom_days: period === "custom" ? customDays : null,
|
custom_days: period === "custom" ? customDays : null,
|
||||||
straddle_premium: readPremium(),
|
move_points: readMovePoints(),
|
||||||
take_profit: readTakeProfit(),
|
|
||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
perp_hedge: readPerpHedge(),
|
|
||||||
page: pageNo,
|
page: pageNo,
|
||||||
page_size: 20,
|
page_size: 20,
|
||||||
}),
|
}),
|
||||||
@@ -407,17 +295,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendHedgeQuery(q) {
|
|
||||||
const h = readPerpHedge();
|
|
||||||
if (!h) return;
|
|
||||||
q.set("hedge_target", String(h.target_profit_u));
|
|
||||||
q.set("hedge_perp_lev", String(h.perp_leverage));
|
|
||||||
q.set("hedge_opt_lev", String(h.option_leverage));
|
|
||||||
q.set("hedge_ratio_perp", String(h.ratio_perp));
|
|
||||||
q.set("hedge_ratio_opt", String(h.ratio_opt));
|
|
||||||
q.set("hedge_ct_mult", String(h.ct_mult || 0.01));
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadCurrent() {
|
function downloadCurrent() {
|
||||||
if (!lastResult) {
|
if (!lastResult) {
|
||||||
setStatus("请先计算");
|
setStatus("请先计算");
|
||||||
@@ -427,8 +304,7 @@
|
|||||||
const startHour = Number(el("amp-start-hour")?.value || 16);
|
const startHour = Number(el("amp-start-hour")?.value || 16);
|
||||||
const period = el("amp-period")?.value || "2m";
|
const period = el("amp-period")?.value || "2m";
|
||||||
const customDays = Number(el("amp-custom-days")?.value || 60);
|
const customDays = Number(el("amp-custom-days")?.value || 60);
|
||||||
const prem = readPremium();
|
const mp = readMovePoints();
|
||||||
const tp = readTakeProfit();
|
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
symbol,
|
symbol,
|
||||||
start_hour: String(startHour),
|
start_hour: String(startHour),
|
||||||
@@ -436,9 +312,7 @@
|
|||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
});
|
});
|
||||||
if (period === "custom") q.set("custom_days", String(customDays));
|
if (period === "custom") q.set("custom_days", String(customDays));
|
||||||
if (prem != null) q.set("straddle_premium", String(prem));
|
if (mp != null) q.set("move_points", String(mp));
|
||||||
if (tp != null) q.set("take_profit", String(tp));
|
|
||||||
appendHedgeQuery(q);
|
|
||||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,15 +346,12 @@
|
|||||||
const id = card.getAttribute("data-id");
|
const id = card.getAttribute("data-id");
|
||||||
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
||||||
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
||||||
const prem = readPremium();
|
const mp = readMovePoints();
|
||||||
const tp = readTakeProfit();
|
|
||||||
const q = new URLSearchParams({
|
const q = new URLSearchParams({
|
||||||
history_id: id,
|
history_id: id,
|
||||||
weekend_filter: readWeekend(),
|
weekend_filter: readWeekend(),
|
||||||
});
|
});
|
||||||
if (prem != null) q.set("straddle_premium", String(prem));
|
if (mp != null) q.set("move_points", String(mp));
|
||||||
if (tp != null) q.set("take_profit", String(tp));
|
|
||||||
appendHedgeQuery(q);
|
|
||||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||||
});
|
});
|
||||||
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
||||||
@@ -502,27 +373,12 @@
|
|||||||
if (lastResult) {
|
if (lastResult) {
|
||||||
if (el("amp-symbol")) el("amp-symbol").value = lastResult.symbol || "eth";
|
if (el("amp-symbol")) el("amp-symbol").value = lastResult.symbol || "eth";
|
||||||
if (el("amp-start-hour")) el("amp-start-hour").value = String(lastResult.start_hour ?? 16);
|
if (el("amp-start-hour")) el("amp-start-hour").value = String(lastResult.start_hour ?? 16);
|
||||||
if (lastResult.straddle_premium != null && el("amp-straddle-premium")) {
|
if (lastResult.move_points != null && el("amp-move-points")) {
|
||||||
el("amp-straddle-premium").value = String(lastResult.straddle_premium);
|
el("amp-move-points").value = String(lastResult.move_points);
|
||||||
}
|
|
||||||
if (lastResult.take_profit != null && el("amp-take-profit")) {
|
|
||||||
el("amp-take-profit").value = String(lastResult.take_profit);
|
|
||||||
}
|
}
|
||||||
if (lastResult.weekend_filter && el("amp-weekend-filter")) {
|
if (lastResult.weekend_filter && el("amp-weekend-filter")) {
|
||||||
el("amp-weekend-filter").value = lastResult.weekend_filter;
|
el("amp-weekend-filter").value = lastResult.weekend_filter;
|
||||||
}
|
}
|
||||||
const h = lastResult.perp_hedge;
|
|
||||||
if (h && typeof h === "object") {
|
|
||||||
if (el("amp-overlay-mode")) el("amp-overlay-mode").value = "perp";
|
|
||||||
if (h.target_profit_u != null && el("amp-hedge-target")) el("amp-hedge-target").value = String(h.target_profit_u);
|
|
||||||
if (h.perp_leverage != null && el("amp-hedge-perp-lev")) el("amp-hedge-perp-lev").value = String(h.perp_leverage);
|
|
||||||
if (h.option_leverage != null && el("amp-hedge-opt-lev")) el("amp-hedge-opt-lev").value = String(h.option_leverage);
|
|
||||||
if (h.ratio_perp != null && el("amp-hedge-ratio-perp")) el("amp-hedge-ratio-perp").value = String(h.ratio_perp);
|
|
||||||
if (h.ratio_opt != null && el("amp-hedge-ratio-opt")) el("amp-hedge-ratio-opt").value = String(h.ratio_opt);
|
|
||||||
} else if (lastResult.straddle_premium != null && el("amp-overlay-mode")) {
|
|
||||||
el("amp-overlay-mode").value = "straddle";
|
|
||||||
}
|
|
||||||
syncOverlayMode();
|
|
||||||
pageNo = 1;
|
pageNo = 1;
|
||||||
setStatus("已载入历史 " + id);
|
setStatus("已载入历史 " + id);
|
||||||
await reframe(true);
|
await reframe(true);
|
||||||
@@ -543,22 +399,9 @@
|
|||||||
el("amp-btn-compute")?.addEventListener("click", () => void compute(true));
|
el("amp-btn-compute")?.addEventListener("click", () => void compute(true));
|
||||||
el("amp-btn-save")?.addEventListener("click", () => void saveHistory());
|
el("amp-btn-save")?.addEventListener("click", () => void saveHistory());
|
||||||
el("amp-btn-download")?.addEventListener("click", downloadCurrent);
|
el("amp-btn-download")?.addEventListener("click", downloadCurrent);
|
||||||
el("amp-overlay-mode")?.addEventListener("change", () => {
|
el("amp-move-points")?.addEventListener("input", scheduleReframe);
|
||||||
syncOverlayMode();
|
|
||||||
void reframe(true);
|
|
||||||
});
|
|
||||||
el("amp-straddle-premium")?.addEventListener("input", scheduleReframe);
|
|
||||||
el("amp-take-profit")?.addEventListener("input", scheduleReframe);
|
|
||||||
[
|
|
||||||
"amp-hedge-target",
|
|
||||||
"amp-hedge-perp-lev",
|
|
||||||
"amp-hedge-opt-lev",
|
|
||||||
"amp-hedge-ratio-perp",
|
|
||||||
"amp-hedge-ratio-opt",
|
|
||||||
].forEach((id) => el(id)?.addEventListener("input", scheduleReframe));
|
|
||||||
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
||||||
syncCustomDays();
|
syncCustomDays();
|
||||||
syncOverlayMode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.hubAmpStatsPage = {
|
window.hubAmpStatsPage = {
|
||||||
@@ -566,9 +409,7 @@
|
|||||||
bind();
|
bind();
|
||||||
setView("stats");
|
setView("stats");
|
||||||
setStatus("");
|
setStatus("");
|
||||||
syncOverlayMode();
|
renderMoveStats(null);
|
||||||
renderStraddle(null);
|
|
||||||
renderPerpHedge(null);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1265,39 +1265,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="amp-field">
|
<label class="amp-field">
|
||||||
<span>对照模式</span>
|
<span>波动点数</span>
|
||||||
<select id="amp-overlay-mode">
|
<input id="amp-move-points" type="number" min="0" step="any" placeholder="如 50" />
|
||||||
<option value="straddle" selected>买跨双边</option>
|
|
||||||
<option value="perp">永期对冲</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-straddle">
|
|
||||||
<span>买跨·双边权利金(点)</span>
|
|
||||||
<input id="amp-straddle-premium" type="number" min="0" step="any" placeholder="如 30" />
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-straddle">
|
|
||||||
<span>止盈点(点)</span>
|
|
||||||
<input id="amp-take-profit" type="number" min="0" step="any" placeholder="空=按涨跌" />
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-perp hidden">
|
|
||||||
<span>永期·目标盈利(U)</span>
|
|
||||||
<input id="amp-hedge-target" type="number" min="0" step="any" value="15" />
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-perp hidden">
|
|
||||||
<span>永期·永续杠杆</span>
|
|
||||||
<input id="amp-hedge-perp-lev" type="number" min="0.01" step="any" value="10" />
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-perp hidden">
|
|
||||||
<span>永期·期权杠杆</span>
|
|
||||||
<input id="amp-hedge-opt-lev" type="number" min="0.01" step="any" value="100" />
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-perp hidden">
|
|
||||||
<span>永期·永续比例</span>
|
|
||||||
<input id="amp-hedge-ratio-perp" type="number" min="0.01" step="any" value="1" />
|
|
||||||
</label>
|
|
||||||
<label class="amp-field amp-overlay-perp hidden">
|
|
||||||
<span>永期·期权比例</span>
|
|
||||||
<input id="amp-hedge-ratio-opt" type="number" min="0.01" step="any" value="2" />
|
|
||||||
</label>
|
</label>
|
||||||
<div class="amp-actions">
|
<div class="amp-actions">
|
||||||
<button type="button" id="amp-btn-compute" class="primary">计算</button>
|
<button type="button" id="amp-btn-compute" class="primary">计算</button>
|
||||||
@@ -1306,24 +1275,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p id="amp-status" class="toolbar-meta amp-status"></p>
|
<p id="amp-status" class="toolbar-meta amp-status"></p>
|
||||||
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.对照模式二选一:买跨收益=有效波动−权利金;永期对冲=永续多1币+买期权(默认1:2),入场按日开盘,触达目标点数按目标盈利出场否则收盘结算.周末按结算日标注/筛选.</p>
|
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.填写波动点数后看振幅≥该点数的天数占比;日表显示两边波动(开→高/开→低)与振幅是否达标.周末按结算日标注/筛选.</p>
|
||||||
<h3 class="amp-block-title">汇总</h3>
|
<h3 class="amp-block-title">汇总</h3>
|
||||||
<div id="amp-summary" class="amp-summary"></div>
|
<div id="amp-summary" class="amp-summary"></div>
|
||||||
<div id="amp-overlay-straddle-block">
|
<h3 class="amp-block-title">振幅占比</h3>
|
||||||
<h3 class="amp-block-title">买跨对照</h3>
|
<div id="amp-move-stats" class="amp-summary amp-move-stats"></div>
|
||||||
<div id="amp-straddle" class="amp-summary amp-straddle"></div>
|
|
||||||
</div>
|
|
||||||
<div id="amp-overlay-perp-block" class="hidden">
|
|
||||||
<h3 class="amp-block-title">永期对冲对照</h3>
|
|
||||||
<div id="amp-perp-hedge" class="amp-summary amp-perp-hedge"></div>
|
|
||||||
</div>
|
|
||||||
<h3 class="amp-block-title">日表明细</h3>
|
<h3 class="amp-block-title">日表明细</h3>
|
||||||
<div class="amp-table-wrap">
|
<div class="amp-table-wrap">
|
||||||
<table class="amp-table">
|
<table class="amp-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>结算日</th><th>窗起点</th><th>开</th><th>高</th><th>低</th><th>收</th>
|
<th>结算日</th><th>窗起点</th><th>开</th><th>高</th><th>低</th><th>收</th>
|
||||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th><th id="amp-col-pnl">收益</th>
|
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th><th>振幅达标</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="amp-table-body">
|
<tbody id="amp-table-body">
|
||||||
|
|||||||
+24
-125
@@ -89,73 +89,22 @@ class AmpStatsLibTests(unittest.TestCase):
|
|||||||
self.assertEqual(s["max_amplitude_day"], "2026-07-02")
|
self.assertEqual(s["max_amplitude_day"], "2026-07-02")
|
||||||
self.assertEqual(s["max_up_points"], 500)
|
self.assertEqual(s["max_up_points"], 500)
|
||||||
self.assertEqual(s["max_down_points"], 200)
|
self.assertEqual(s["max_down_points"], 200)
|
||||||
self.assertIsNone(s["straddle"])
|
self.assertIsNone(s["move_points_stats"])
|
||||||
self.assertIsNone(s["perp_hedge"])
|
|
||||||
|
|
||||||
def test_perp_hedge_hit_and_pnl(self):
|
def test_move_points_amp_ratio(self):
|
||||||
from lib.hub.amp_stats_lib import perp_hedge_day_pnl, perp_hedge_day_pnl_eod
|
|
||||||
|
|
||||||
# 开盘=1800 optLev=100 → prem=36; A≈52.83; B=51
|
|
||||||
# 触达目标出场 → 日盈亏=15, 不再按收盘涨跌算满仓
|
|
||||||
rows = [
|
rows = [
|
||||||
{"open": 1800, "close": 1860, "change": 60, "up_points": 60, "down_points": 0, "amplitude": 60, "settlement_day": "2026-07-01"},
|
{"amplitude": 100, "up_points": 40, "down_points": 60, "change": 10, "settlement_day": "2026-07-01"},
|
||||||
{"open": 1800, "close": 1740, "change": -60, "up_points": 0, "down_points": 60, "amplitude": 60, "settlement_day": "2026-07-02"},
|
{"amplitude": 40, "up_points": 10, "down_points": 30, "change": -5, "settlement_day": "2026-07-02"},
|
||||||
{"open": 1800, "close": 1820, "change": 20, "up_points": 20, "down_points": 0, "amplitude": 20, "settlement_day": "2026-07-03"},
|
{"amplitude": 50, "up_points": 50, "down_points": 0, "change": 20, "settlement_day": "2026-07-03"},
|
||||||
]
|
]
|
||||||
hedge = {
|
s = summarize_rows(rows, move_points=50)
|
||||||
"target_profit_u": 15,
|
ms = s["move_points_stats"]
|
||||||
"perp_leverage": 10,
|
self.assertIsNotNone(ms)
|
||||||
"option_leverage": 100,
|
self.assertEqual(ms["move_points"], 50)
|
||||||
"ratio_perp": 1,
|
self.assertEqual(ms["amp_hit_days"], 2) # 100, 50
|
||||||
"ratio_opt": 2,
|
self.assertEqual(ms["amp_hit_ratio"], round(2 / 3, 4))
|
||||||
"ct_mult": 0.01,
|
self.assertEqual(ms["up_hit_days"], 1) # 50
|
||||||
}
|
self.assertEqual(ms["down_hit_days"], 1) # 60
|
||||||
s = summarize_rows(rows, perp_hedge=hedge)
|
|
||||||
ph = s["perp_hedge"]
|
|
||||||
self.assertIsNotNone(ph)
|
|
||||||
self.assertEqual(ph["entry"], "open")
|
|
||||||
self.assertEqual(ph["exit"], "target_or_eod")
|
|
||||||
self.assertEqual(ph["spot"], 1800.0)
|
|
||||||
self.assertEqual(ph["opt_coins"], 2.0)
|
|
||||||
self.assertEqual(ph["premium_total"], 36.0)
|
|
||||||
self.assertAlmostEqual(ph["move_b"], 51.0, places=4)
|
|
||||||
self.assertEqual(ph["hit_a_days"], 1) # up 60 >= A
|
|
||||||
self.assertEqual(ph["hit_b_days"], 1) # down 60 >= B
|
|
||||||
self.assertEqual(ph["target_exit_days"], 2)
|
|
||||||
self.assertEqual(ph["eod_days"], 1)
|
|
||||||
# 触达目标 → 15U
|
|
||||||
self.assertAlmostEqual(ph["pnl_max"], 15.0, places=4)
|
|
||||||
up_pnl = perp_hedge_day_pnl(
|
|
||||||
change=60,
|
|
||||||
open_px=1800,
|
|
||||||
close_px=1860,
|
|
||||||
option_leverage=100,
|
|
||||||
opt_coins=2,
|
|
||||||
up_points=60,
|
|
||||||
down_points=0,
|
|
||||||
target_profit_u=15,
|
|
||||||
)
|
|
||||||
down_pnl = perp_hedge_day_pnl(
|
|
||||||
change=-60,
|
|
||||||
open_px=1800,
|
|
||||||
close_px=1740,
|
|
||||||
option_leverage=100,
|
|
||||||
opt_coins=2,
|
|
||||||
up_points=0,
|
|
||||||
down_points=60,
|
|
||||||
target_profit_u=15,
|
|
||||||
)
|
|
||||||
self.assertAlmostEqual(up_pnl, 15.0, places=4)
|
|
||||||
self.assertAlmostEqual(down_pnl, 15.0, places=4)
|
|
||||||
self.assertAlmostEqual(ph["down_pnl_total"], 15.0, places=4)
|
|
||||||
# 未触达:收盘结算
|
|
||||||
eod = perp_hedge_day_pnl_eod(
|
|
||||||
change=20, open_px=1800, close_px=1820, option_leverage=100, opt_coins=2
|
|
||||||
)
|
|
||||||
self.assertLess(eod, 0)
|
|
||||||
self.assertEqual(ph["up_days"], 2)
|
|
||||||
self.assertEqual(ph["down_days"], 1)
|
|
||||||
|
|
||||||
csv_text = build_export_csv(
|
csv_text = build_export_csv(
|
||||||
{
|
{
|
||||||
"exchange": "okx",
|
"exchange": "okx",
|
||||||
@@ -166,43 +115,12 @@ class AmpStatsLibTests(unittest.TestCase):
|
|||||||
"end_hour": 16,
|
"end_hour": 16,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertIn("永期对冲对照", csv_text)
|
self.assertIn("振幅占比", csv_text)
|
||||||
self.assertIn("永期盈亏", csv_text)
|
self.assertIn("振幅达标", csv_text)
|
||||||
self.assertIn("按日开盘", csv_text)
|
|
||||||
|
|
||||||
def test_long_straddle_stats(self):
|
def test_weekend_and_reframe_move_points(self):
|
||||||
rows = [
|
from lib.hub.amp_stats_lib import enrich_rows, filter_weekend_rows, reframe_amp_stats
|
||||||
# |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"},
|
|
||||||
]
|
|
||||||
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))
|
|
||||||
csv_text = build_export_csv(
|
|
||||||
{"exchange": "okx", "symbol_label": "ETH", "summary": s, "rows": rows, "start_hour": 22, "end_hour": 16}
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sat 2026-07-18, Sun 2026-07-19, Mon 2026-07-20
|
|
||||||
rows = [
|
rows = [
|
||||||
{
|
{
|
||||||
"settlement_day": "2026-07-18",
|
"settlement_day": "2026-07-18",
|
||||||
@@ -238,39 +156,20 @@ class AmpStatsLibTests(unittest.TestCase):
|
|||||||
only = filter_weekend_rows(rows, "only")
|
only = filter_weekend_rows(rows, "only")
|
||||||
self.assertEqual(len(only), 2)
|
self.assertEqual(len(only), 2)
|
||||||
|
|
||||||
# TP=80: day1 hit → move 80; day2 no → |12|; day3 no → 8
|
enriched = enrich_rows(rows, move_points=80)
|
||||||
enriched = enrich_rows_pnl(rows, straddle_premium=10, take_profit=80)
|
self.assertTrue(enriched[0]["amp_hit"])
|
||||||
self.assertTrue(enriched[0]["take_profit_hit"])
|
self.assertFalse(enriched[1]["amp_hit"])
|
||||||
self.assertEqual(enriched[0]["effective_move"], 80)
|
self.assertTrue(enriched[2]["amp_hit"])
|
||||||
self.assertEqual(enriched[0]["profit"], 70)
|
|
||||||
self.assertFalse(enriched[1]["take_profit_hit"])
|
|
||||||
self.assertEqual(enriched[1]["effective_move"], 12)
|
|
||||||
self.assertEqual(enriched[1]["profit"], 2)
|
|
||||||
# TP empty → use |change|
|
|
||||||
no_tp = enrich_rows_pnl(rows[:1], straddle_premium=10, take_profit=None)
|
|
||||||
self.assertEqual(no_tp[0]["effective_move"], 5)
|
|
||||||
self.assertEqual(no_tp[0]["profit"], -5)
|
|
||||||
|
|
||||||
# TP boundary >= : up=80 counts as hit
|
|
||||||
edge = enrich_rows_pnl(
|
|
||||||
[{"up_points": 80, "down_points": 1, "change": 2, "settlement_day": "2026-07-20", "is_weekend": False}],
|
|
||||||
straddle_premium=10,
|
|
||||||
take_profit=80,
|
|
||||||
)
|
|
||||||
self.assertTrue(edge[0]["take_profit_hit"])
|
|
||||||
self.assertEqual(edge[0]["profit"], 70)
|
|
||||||
|
|
||||||
reframed = reframe_amp_stats(
|
reframed = reframe_amp_stats(
|
||||||
rows_all=rows,
|
rows_all=rows,
|
||||||
symbol="eth",
|
symbol="eth",
|
||||||
weekend_filter="exclude",
|
weekend_filter="exclude",
|
||||||
straddle_premium=10,
|
move_points=80,
|
||||||
take_profit=80,
|
|
||||||
)
|
)
|
||||||
self.assertEqual(reframed["summary"]["sample_count"], 1)
|
self.assertEqual(reframed["summary"]["sample_count"], 1)
|
||||||
# Mon: 未触达止盈 → |8|-10
|
self.assertTrue(reframed["rows"][0]["amp_hit"])
|
||||||
self.assertEqual(reframed["rows"][0]["profit"], -2)
|
self.assertIn("振幅占比", build_export_csv(reframed))
|
||||||
self.assertIn("收益", build_export_csv(reframed))
|
|
||||||
|
|
||||||
def test_fetch_switches_to_history_endpoint(self):
|
def test_fetch_switches_to_history_endpoint(self):
|
||||||
"""近期接口到头后应切 history 续拉."""
|
"""近期接口到头后应切 history 续拉."""
|
||||||
|
|||||||
Reference in New Issue
Block a user