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:
+19
-77
@@ -3,8 +3,8 @@
|
||||
中控只读工具:按自定义整点起点、**固定北京时间 16:00 收窗**,统计 OKX 上 ETH/BTC 的历史「点数振幅」档案,辅助一天期期权判断空间。
|
||||
|
||||
> 开发方案见 [ETH时段振幅统计-开发方案.md](./ETH时段振幅统计-开发方案.md)。
|
||||
> 永期对冲公式见 [永期对冲计算器.md](./永期对冲计算器.md)。
|
||||
> **不改下单链路**;不算 IV。
|
||||
> **不改下单链路**;不算 IV。
|
||||
> 买跨 / 永期对冲测算请用中控 **策略计算器**,本页不再做对照盈亏。
|
||||
|
||||
---
|
||||
|
||||
@@ -22,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** 含摘要 + 全日明细
|
||||
|
||||
**跨天例子**
|
||||
|
||||
@@ -43,8 +44,8 @@
|
||||
|
||||
| 字段 | 算法 |
|
||||
|------|------|
|
||||
| 开→高 | `H − O` |
|
||||
| 开→低 | `O − L` |
|
||||
| 开→高 | `H − O`(一边波动) |
|
||||
| 开→低 | `O − L`(另一边波动) |
|
||||
| **振幅** | `H − L`(= 开→高 + 开→低) |
|
||||
| 涨跌值 | `C − O` |
|
||||
|
||||
@@ -58,35 +59,19 @@ 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 线)。
|
||||
|
||||
### 周末
|
||||
|
||||
@@ -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
|
||||
|
||||
- 仅 **保存到历史** 后出现(不会一算就自动入库)
|
||||
@@ -151,7 +96,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` | 单测 |
|
||||
|
||||
---
|
||||
|
||||
@@ -160,9 +105,6 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-07-23 | 首版上线说明 |
|
||||
| 2026-07-23 | 买跨对照:可设双边权利金、越过占比与收盘盈亏 |
|
||||
| 2026-07-23 | 周末筛选/标注、止盈点(≥)、日表收益列 |
|
||||
| 2026-07-23 | 长周期续拉 history K 线;收益列红绿着色 |
|
||||
| 2026-07-28 | 永期对冲对照:所需点数达标 + 按日组合盈亏 |
|
||||
| 2026-07-28 | 永期入场改按日开盘;买跨/永期对照模式二选一 |
|
||||
| 2026-07-28 | 永期日盈亏按目标盈利出场(开→高/低触达),未触达才收盘结算 |
|
||||
| 2026-07-23 | 买跨对照、周末筛选、止盈点 |
|
||||
| 2026-07-28 | 永期对冲对照(后已移除) |
|
||||
| 2026-07-28 | 去掉买跨/永期;改为波动点数→振幅占比 |
|
||||
|
||||
+786
-1258
File diff suppressed because it is too large
Load Diff
@@ -12,33 +12,20 @@ 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,
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
symbol: str = "eth"
|
||||
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"
|
||||
perp_hedge: Optional[PerpHedgeBody] = None
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
@@ -48,29 +35,21 @@ 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"
|
||||
perp_hedge: Optional[PerpHedgeBody] = None
|
||||
price_source: str = ""
|
||||
inst_id: str = ""
|
||||
page: int = 1
|
||||
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:
|
||||
router = APIRouter(prefix="/api/amp-stats", tags=["amp-stats"])
|
||||
|
||||
@@ -102,12 +81,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
"default_weekend_filter": "all",
|
||||
"timeframe": "1H",
|
||||
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
||||
"straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金",
|
||||
"perp_hedge_note": "永期对冲:永续多1币+买期权;入场按日开盘;比例默认1:2;与买跨二选一对照",
|
||||
"overlay_modes": [
|
||||
{"key": "straddle", "label": "买跨双边"},
|
||||
{"key": "perp", "label": "永期对冲"},
|
||||
],
|
||||
"move_points_note": "填波动点数后统计振幅≥该点数的天数占比;日表显示开→高/开→低两边点数与振幅是否达标",
|
||||
}
|
||||
|
||||
@router.post("/compute")
|
||||
@@ -118,10 +92,8 @@ 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,
|
||||
perp_hedge=_hedge_dict(body.perp_hedge),
|
||||
)
|
||||
except ValueError as 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,
|
||||
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,
|
||||
perp_hedge=_hedge_dict(body.perp_hedge),
|
||||
price_source=body.price_source,
|
||||
inst_id=body.inst_id,
|
||||
)
|
||||
@@ -190,33 +160,15 @@ 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"),
|
||||
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():
|
||||
item = get_history(history_id.strip())
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="历史不存在")
|
||||
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_hedge = hedge_q if hedge_q_ready else item_hedge
|
||||
use_mp = move_points if move_points is not None else item.get("move_points")
|
||||
try:
|
||||
payload = reframe_amp_stats(
|
||||
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),
|
||||
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",
|
||||
perp_hedge=use_hedge,
|
||||
price_source=str(item.get("price_source") or ""),
|
||||
inst_id=str(item.get("inst_id") 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
|
||||
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,
|
||||
perp_hedge=hedge_q if hedge_q_ready else None,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* 中控振幅统计:OKX ETH/BTC + 买跨/止盈/周末筛选.
|
||||
* 中控振幅统计:OKX ETH/BTC + 波动点数振幅占比 + 周末筛选.
|
||||
*/
|
||||
(function () {
|
||||
const page = document.getElementById("page-amp-stats");
|
||||
@@ -39,32 +39,8 @@
|
||||
return (n * 100).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
function overlayMode() {
|
||||
return el("amp-overlay-mode")?.value || "straddle";
|
||||
}
|
||||
|
||||
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();
|
||||
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;
|
||||
@@ -75,30 +51,6 @@
|
||||
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) {
|
||||
const s = el("amp-status");
|
||||
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) {
|
||||
const box = el("amp-summary");
|
||||
if (!box) return;
|
||||
const s = summary || {};
|
||||
syncOverlayMode();
|
||||
if (!s.sample_count) {
|
||||
box.innerHTML = '<p class="amp-empty">暂无汇总</p>';
|
||||
renderStraddle(null);
|
||||
renderPerpHedge(null);
|
||||
renderMoveStats(null);
|
||||
return;
|
||||
}
|
||||
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(result && result.price_source)}</span></div>` +
|
||||
`</div>`;
|
||||
if (overlayMode() === "perp") {
|
||||
renderStraddle(null);
|
||||
renderPerpHedge(s.perp_hedge);
|
||||
} else {
|
||||
renderPerpHedge(null);
|
||||
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>`;
|
||||
}
|
||||
|
||||
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><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.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>`;
|
||||
}
|
||||
|
||||
@@ -244,24 +135,26 @@
|
||||
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 isPerp = overlayMode() === "perp";
|
||||
syncOverlayMode();
|
||||
const rows = (pagePayload && pagePayload.rows) || [];
|
||||
if (!rows.length) {
|
||||
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
|
||||
} else {
|
||||
body.innerHTML = rows
|
||||
.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 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 (
|
||||
`<tr${trClass}>` +
|
||||
`<td>${dayLabel(r)}</td>` +
|
||||
@@ -270,11 +163,11 @@
|
||||
`<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>${esc(r.change)}</td>` +
|
||||
`<td>${pnlCell}</td>` +
|
||||
`<td>${hitCell(r)}</td>` +
|
||||
`</tr>`
|
||||
);
|
||||
})
|
||||
@@ -308,8 +201,7 @@
|
||||
|
||||
async function reframe(resetPage) {
|
||||
if (!lastResult) {
|
||||
renderStraddle(null);
|
||||
renderPerpHedge(null);
|
||||
renderMoveStats(null);
|
||||
return;
|
||||
}
|
||||
if (resetPage) pageNo = 1;
|
||||
@@ -325,10 +217,8 @@
|
||||
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(),
|
||||
perp_hedge: readPerpHedge(),
|
||||
price_source: lastResult.price_source || "",
|
||||
inst_id: lastResult.inst_id || "",
|
||||
page: pageNo,
|
||||
@@ -368,10 +258,8 @@
|
||||
start_hour: startHour,
|
||||
period,
|
||||
custom_days: period === "custom" ? customDays : null,
|
||||
straddle_premium: readPremium(),
|
||||
take_profit: readTakeProfit(),
|
||||
move_points: readMovePoints(),
|
||||
weekend_filter: readWeekend(),
|
||||
perp_hedge: readPerpHedge(),
|
||||
page: pageNo,
|
||||
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() {
|
||||
if (!lastResult) {
|
||||
setStatus("请先计算");
|
||||
@@ -427,8 +304,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),
|
||||
@@ -436,9 +312,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));
|
||||
appendHedgeQuery(q);
|
||||
if (mp != null) q.set("move_points", String(mp));
|
||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||
}
|
||||
|
||||
@@ -472,15 +346,12 @@
|
||||
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();
|
||||
const mp = readMovePoints();
|
||||
const q = new URLSearchParams({
|
||||
history_id: id,
|
||||
weekend_filter: readWeekend(),
|
||||
});
|
||||
if (prem != null) q.set("straddle_premium", String(prem));
|
||||
if (tp != null) q.set("take_profit", String(tp));
|
||||
appendHedgeQuery(q);
|
||||
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 () => {
|
||||
@@ -502,27 +373,12 @@
|
||||
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;
|
||||
}
|
||||
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;
|
||||
setStatus("已载入历史 " + id);
|
||||
await reframe(true);
|
||||
@@ -543,22 +399,9 @@
|
||||
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-overlay-mode")?.addEventListener("change", () => {
|
||||
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-move-points")?.addEventListener("input", scheduleReframe);
|
||||
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
||||
syncCustomDays();
|
||||
syncOverlayMode();
|
||||
}
|
||||
|
||||
window.hubAmpStatsPage = {
|
||||
@@ -566,9 +409,7 @@
|
||||
bind();
|
||||
setView("stats");
|
||||
setStatus("");
|
||||
syncOverlayMode();
|
||||
renderStraddle(null);
|
||||
renderPerpHedge(null);
|
||||
renderMoveStats(null);
|
||||
},
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -1265,39 +1265,8 @@
|
||||
</select>
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>对照模式</span>
|
||||
<select id="amp-overlay-mode">
|
||||
<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" />
|
||||
<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>
|
||||
@@ -1306,24 +1275,18 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<div id="amp-summary" class="amp-summary"></div>
|
||||
<div id="amp-overlay-straddle-block">
|
||||
<h3 class="amp-block-title">买跨对照</h3>
|
||||
<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>
|
||||
<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 id="amp-col-pnl">收益</th>
|
||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th><th>振幅达标</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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_up_points"], 500)
|
||||
self.assertEqual(s["max_down_points"], 200)
|
||||
self.assertIsNone(s["straddle"])
|
||||
self.assertIsNone(s["perp_hedge"])
|
||||
self.assertIsNone(s["move_points_stats"])
|
||||
|
||||
def test_perp_hedge_hit_and_pnl(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, 不再按收盘涨跌算满仓
|
||||
def test_move_points_amp_ratio(self):
|
||||
rows = [
|
||||
{"open": 1800, "close": 1860, "change": 60, "up_points": 60, "down_points": 0, "amplitude": 60, "settlement_day": "2026-07-01"},
|
||||
{"open": 1800, "close": 1740, "change": -60, "up_points": 0, "down_points": 60, "amplitude": 60, "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": 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"},
|
||||
]
|
||||
hedge = {
|
||||
"target_profit_u": 15,
|
||||
"perp_leverage": 10,
|
||||
"option_leverage": 100,
|
||||
"ratio_perp": 1,
|
||||
"ratio_opt": 2,
|
||||
"ct_mult": 0.01,
|
||||
}
|
||||
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)
|
||||
|
||||
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",
|
||||
@@ -166,43 +115,12 @@ class AmpStatsLibTests(unittest.TestCase):
|
||||
"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):
|
||||
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"},
|
||||
]
|
||||
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_weekend_and_reframe_move_points(self):
|
||||
from lib.hub.amp_stats_lib import enrich_rows, filter_weekend_rows, reframe_amp_stats
|
||||
|
||||
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 = [
|
||||
{
|
||||
"settlement_day": "2026-07-18",
|
||||
@@ -238,39 +156,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 续拉."""
|
||||
|
||||
Reference in New Issue
Block a user