Add weekend filter, take-profit, and profit column to amp stats.
Long-straddle effective move uses TP on path hit (>=) else abs change; mark Sat/Sun on settlement days. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+16
-9
@@ -57,19 +57,25 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
||||
|
||||
## 买跨对照(赌波动)
|
||||
|
||||
表单可填 **双边权利金(点)**,例如 `30`:
|
||||
表单可填 **双边权利金(点)**,例如 `30`;旁边可填 **止盈点**(可空):
|
||||
|
||||
| 汇总项 | 口径 |
|
||||
|--------|------|
|
||||
| 开→高超过 | `H−O > 权利金` 的天数与占比 |
|
||||
| 开→低超过 | `O−L > 权利金` 的天数与占比 |
|
||||
| \|涨跌\|超过 | `\|C−O\| > 权利金` 的天数与占比 |
|
||||
| 买跨盈亏 | 单日 `\|C−O\| − 权利金`,再看合计 / 日均 / 胜率 |
|
||||
| 开→高超过权利金 | `H−O > 权利金` 的天数与占比 |
|
||||
| 开→低超过权利金 | `O−L > 权利金` 的天数与占比 |
|
||||
| \|涨跌\|超过权利金 | `\|C−O\| > 权利金` 的天数与占比 |
|
||||
| 有效波动 | 若设止盈且 `开→高≥止盈` 或 `开→低≥止盈` → 用止盈点;否则用 `\|C−O\|` |
|
||||
| 买跨收益 | `有效波动 − 权利金`(日表「收益」列同口径) |
|
||||
|
||||
- 方向:**买跨**(不是卖跨)
|
||||
- 比较:严格 **`>`**(刚好等于权利金不算越过)
|
||||
- 已算出日表后,改权利金会**本地重算对照**(不重拉 K 线)
|
||||
- 空着或 ≤0:不显示买跨块
|
||||
- 方向:**买跨**
|
||||
- 权利金越过:严格 **`>`**;止盈触达:**`≥`**
|
||||
- 止盈留空 / ≤0:有效波动一律按 `|涨跌|`
|
||||
- 已算出日表后,改权利金 / 止盈 / 周末筛选会**本地重算**(不重拉 K 线)
|
||||
|
||||
### 周末
|
||||
|
||||
- 下拉:**全部**(默认)/ **排除周末** / **仅周末**
|
||||
- 按 **结算日** 北京时间星期判断;表中六、日带标注并高亮
|
||||
|
||||
---
|
||||
|
||||
@@ -99,3 +105,4 @@ K 线粒度:**1H**(与整点对齐);价源优先 OKX 指数(ETH-USD /
|
||||
|------|------|
|
||||
| 2026-07-23 | 首版上线说明 |
|
||||
| 2026-07-23 | 买跨对照:可设双边权利金、越过占比与收盘盈亏 |
|
||||
| 2026-07-23 | 周末筛选/标注、止盈点(≥)、日表收益列 |
|
||||
|
||||
+222
-23
@@ -150,10 +150,15 @@ def compute_day_row(
|
||||
down = opens - lo
|
||||
amp = hi - lo
|
||||
change = close - opens
|
||||
wd = settlement.weekday() # Mon=0 … Sun=6
|
||||
is_we = wd >= 5
|
||||
return {
|
||||
"settlement_day": settlement.isoformat(),
|
||||
"window_start": start.strftime("%Y-%m-%d %H:%M"),
|
||||
"window_end": end.strftime("%Y-%m-%d %H:%M"),
|
||||
"weekday": wd,
|
||||
"weekday_label": "六" if wd == 5 else ("日" if wd == 6 else ""),
|
||||
"is_weekend": is_we,
|
||||
"open": round(opens, 4),
|
||||
"high": round(hi, 4),
|
||||
"low": round(lo, 4),
|
||||
@@ -178,15 +183,113 @@ def normalize_straddle_premium(raw: Any) -> Optional[float]:
|
||||
return v
|
||||
|
||||
|
||||
def straddle_long_stats(rows: list[dict[str, Any]], premium: float) -> dict[str, Any]:
|
||||
"""买跨(赌波动):盈亏按收盘 |C−O| − 双边权利金;越过阈值用严格 >."""
|
||||
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:
|
||||
return None
|
||||
return v
|
||||
|
||||
|
||||
def normalize_weekend_filter(raw: Any) -> str:
|
||||
"""all | exclude | only;默认全部."""
|
||||
s = (str(raw) if raw is not None else "all").strip().lower()
|
||||
if s in ("", "all", "全部"):
|
||||
return "all"
|
||||
if s in ("exclude", "exclude_weekend", "no_weekend", "排除周末"):
|
||||
return "exclude"
|
||||
if s in ("only", "weekend_only", "only_weekend", "仅周末"):
|
||||
return "only"
|
||||
raise ValueError("周末筛选须为 all / exclude / only")
|
||||
|
||||
|
||||
def filter_weekend_rows(rows: list[dict[str, Any]], weekend_filter: Any = "all") -> list[dict[str, Any]]:
|
||||
mode = normalize_weekend_filter(weekend_filter)
|
||||
if mode == "all":
|
||||
return list(rows or [])
|
||||
out: list[dict[str, Any]] = []
|
||||
for r in rows or []:
|
||||
is_we = bool(r.get("is_weekend"))
|
||||
if "is_weekend" not in r and r.get("settlement_day"):
|
||||
try:
|
||||
is_we = date.fromisoformat(str(r["settlement_day"])).weekday() >= 5
|
||||
except ValueError:
|
||||
is_we = False
|
||||
if mode == "exclude" and is_we:
|
||||
continue
|
||||
if mode == "only" and not is_we:
|
||||
continue
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def effective_move_points(row: dict[str, Any], take_profit: Optional[float]) -> float:
|
||||
"""触达止盈(≥)用止盈点,否则用 |涨跌|."""
|
||||
abs_chg = abs(float(row.get("change") or 0))
|
||||
if take_profit is None:
|
||||
return abs_chg
|
||||
tp = float(take_profit)
|
||||
up = float(row.get("up_points") or 0)
|
||||
down = float(row.get("down_points") or 0)
|
||||
if up >= tp or down >= tp:
|
||||
return tp
|
||||
return abs_chg
|
||||
|
||||
|
||||
def enrich_rows_pnl(
|
||||
rows: list[dict[str, Any]],
|
||||
*,
|
||||
straddle_premium: Optional[float] = None,
|
||||
take_profit: Optional[float] = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""为日表附加有效波动 / 是否触达止盈 / 收益(有权利金时)."""
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
tp = normalize_take_profit(take_profit)
|
||||
out: list[dict[str, Any]] = []
|
||||
for r in rows or []:
|
||||
item = dict(r)
|
||||
if "is_weekend" not in item and item.get("settlement_day"):
|
||||
try:
|
||||
wd = date.fromisoformat(str(item["settlement_day"])).weekday()
|
||||
item["weekday"] = wd
|
||||
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)
|
||||
move = effective_move_points(item, tp)
|
||||
hit = False
|
||||
if tp is not None:
|
||||
hit = float(item.get("up_points") or 0) >= tp or float(item.get("down_points") or 0) >= tp
|
||||
item["effective_move"] = round(move, 4)
|
||||
item["take_profit_hit"] = hit
|
||||
item["profit"] = round(move - prem, 4) if prem is not None else None
|
||||
out.append(item)
|
||||
return out
|
||||
|
||||
|
||||
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")
|
||||
if not rows:
|
||||
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,
|
||||
@@ -194,6 +297,8 @@ def straddle_long_stats(rows: list[dict[str, Any]], premium: float) -> dict[str,
|
||||
"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,
|
||||
@@ -201,15 +306,17 @@ def straddle_long_stats(rows: list[dict[str, Any]], premium: float) -> dict[str,
|
||||
"pnl_max": None,
|
||||
"pnl_min": None,
|
||||
}
|
||||
n = len(rows)
|
||||
up_ex = sum(1 for r in rows if float(r["up_points"]) > prem)
|
||||
down_ex = sum(1 for r in rows if float(r["down_points"]) > prem)
|
||||
abs_ex = sum(1 for r in rows if abs(float(r["change"])) > prem)
|
||||
pnls = [abs(float(r["change"])) - prem for r in rows]
|
||||
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),
|
||||
@@ -217,6 +324,8 @@ def straddle_long_stats(rows: list[dict[str, Any]], premium: float) -> dict[str,
|
||||
"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,
|
||||
@@ -230,6 +339,7 @@ def summarize_rows(
|
||||
rows: list[dict[str, Any]],
|
||||
*,
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
) -> dict[str, Any]:
|
||||
if not rows:
|
||||
out = {
|
||||
@@ -248,7 +358,7 @@ def summarize_rows(
|
||||
}
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
if prem is not None:
|
||||
out["straddle"] = straddle_long_stats([], prem)
|
||||
out["straddle"] = straddle_long_stats([], prem, take_profit=take_profit)
|
||||
return out
|
||||
amps = [float(r["amplitude"]) for r in rows]
|
||||
ups = [float(r["up_points"]) for r in rows]
|
||||
@@ -274,7 +384,7 @@ def summarize_rows(
|
||||
}
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
if prem is not None:
|
||||
out["straddle"] = straddle_long_stats(rows, prem)
|
||||
out["straddle"] = straddle_long_stats(rows, prem, take_profit=take_profit)
|
||||
return out
|
||||
|
||||
|
||||
@@ -388,6 +498,8 @@ def compute_amp_stats(
|
||||
period: str = "2m",
|
||||
custom_days: Any = None,
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
weekend_filter: Any = "all",
|
||||
now: Optional[datetime] = None,
|
||||
fetch_fn: Optional[Callable[..., list[dict[str, Any]]]] = None,
|
||||
) -> dict[str, Any]:
|
||||
@@ -396,6 +508,8 @@ def compute_amp_stats(
|
||||
if sh < 0 or sh > 23:
|
||||
raise ValueError("起点须为 0-23 整点")
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
tp = normalize_take_profit(take_profit)
|
||||
we_mode = normalize_weekend_filter(weekend_filter)
|
||||
sample_days = resolve_sample_days(period, custom_days)
|
||||
settlements = list_settlement_dates(sample_days=sample_days, now=now)
|
||||
if not settlements:
|
||||
@@ -411,37 +525,109 @@ def compute_amp_stats(
|
||||
key, since_ms=since_ms, until_ms=until_ms, fetch_fn=fetch_fn
|
||||
)
|
||||
bar_map = bars_to_map(bars)
|
||||
rows: list[dict[str, Any]] = []
|
||||
rows_all: list[dict[str, Any]] = []
|
||||
missing: list[str] = []
|
||||
for d in settlements:
|
||||
row = compute_day_row(d, sh, bar_map)
|
||||
if row is None:
|
||||
missing.append(d.isoformat())
|
||||
continue
|
||||
rows.append(row)
|
||||
summary = summarize_rows(rows, straddle_premium=prem)
|
||||
period_label = period if period != "custom" else f"custom:{sample_days}"
|
||||
rows_all.append(row)
|
||||
return build_amp_result(
|
||||
rows_all=rows_all,
|
||||
symbol_key=key,
|
||||
start_hour=sh,
|
||||
period=period,
|
||||
sample_days=sample_days,
|
||||
straddle_premium=prem,
|
||||
take_profit=tp,
|
||||
weekend_filter=we_mode,
|
||||
price_source=price_source,
|
||||
inst_id=inst_id,
|
||||
missing=missing,
|
||||
)
|
||||
|
||||
|
||||
def build_amp_result(
|
||||
*,
|
||||
rows_all: list[dict[str, Any]],
|
||||
symbol_key: str,
|
||||
start_hour: int,
|
||||
period: str,
|
||||
sample_days: int,
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
weekend_filter: Any = "all",
|
||||
price_source: str = "",
|
||||
inst_id: str = "",
|
||||
missing: Optional[list[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
tp = normalize_take_profit(take_profit)
|
||||
we_mode = normalize_weekend_filter(weekend_filter)
|
||||
filtered = filter_weekend_rows(rows_all, we_mode)
|
||||
rows = enrich_rows_pnl(filtered, straddle_premium=prem, take_profit=tp)
|
||||
summary = summarize_rows(rows, straddle_premium=prem, take_profit=tp)
|
||||
if period == "custom" or str(period).startswith("custom:"):
|
||||
period_label = period if str(period).startswith("custom:") else f"custom:{sample_days}"
|
||||
else:
|
||||
period_label = str(period)
|
||||
miss = missing or []
|
||||
return {
|
||||
"ok": True,
|
||||
"exchange": EXCHANGE,
|
||||
"symbol": key,
|
||||
"symbol_label": SYMBOLS[key]["label"],
|
||||
"start_hour": sh,
|
||||
"symbol": symbol_key,
|
||||
"symbol_label": SYMBOLS[symbol_key]["label"],
|
||||
"start_hour": start_hour,
|
||||
"end_hour": END_HOUR,
|
||||
"period": period_label,
|
||||
"sample_days_requested": sample_days,
|
||||
"straddle_premium": prem,
|
||||
"take_profit": tp,
|
||||
"weekend_filter": we_mode,
|
||||
"timeframe": TIMEFRAME,
|
||||
"price_source": price_source,
|
||||
"inst_id": inst_id,
|
||||
"timezone": "Asia/Shanghai",
|
||||
"rows_all": rows_all,
|
||||
"rows": rows,
|
||||
"summary": summary,
|
||||
"missing_days": missing[:30],
|
||||
"missing_count": len(missing),
|
||||
"missing_days": miss[:30],
|
||||
"missing_count": len(miss),
|
||||
}
|
||||
|
||||
|
||||
def reframe_amp_stats(
|
||||
*,
|
||||
rows_all: list[dict[str, Any]],
|
||||
symbol: str = "eth",
|
||||
start_hour: int = 16,
|
||||
period: str = "2m",
|
||||
sample_days: int = 60,
|
||||
straddle_premium: Any = None,
|
||||
take_profit: Any = None,
|
||||
weekend_filter: Any = "all",
|
||||
price_source: str = "",
|
||||
inst_id: str = "",
|
||||
missing: Optional[list[str]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""已有日表上改周末/权利金/止盈,不拉 K 线."""
|
||||
key = normalize_symbol(symbol)
|
||||
return build_amp_result(
|
||||
rows_all=list(rows_all or []),
|
||||
symbol_key=key,
|
||||
start_hour=int(start_hour),
|
||||
period=period,
|
||||
sample_days=int(sample_days or 60),
|
||||
straddle_premium=straddle_premium,
|
||||
take_profit=take_profit,
|
||||
weekend_filter=weekend_filter,
|
||||
price_source=price_source,
|
||||
inst_id=inst_id,
|
||||
missing=missing,
|
||||
)
|
||||
|
||||
|
||||
def rows_page(rows: list[dict[str, Any]], *, page: int = 1, page_size: int = 20) -> dict[str, Any]:
|
||||
page = max(1, int(page or 1))
|
||||
page_size = max(5, min(100, int(page_size or 20)))
|
||||
@@ -470,6 +656,7 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
w.writerow(["起点整点", f"{payload.get('start_hour')}:00"])
|
||||
w.writerow(["终点", f"{payload.get('end_hour')}:00"])
|
||||
w.writerow(["周期", payload.get("period")])
|
||||
w.writerow(["周末筛选", payload.get("weekend_filter")])
|
||||
w.writerow(["样本数", s.get("sample_count")])
|
||||
w.writerow(["最大振幅", s.get("max_amplitude"), "日期", s.get("max_amplitude_day")])
|
||||
w.writerow(["振幅均值", s.get("avg_amplitude"), "中位数", s.get("median_amplitude")])
|
||||
@@ -479,10 +666,12 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
st = s.get("straddle") or {}
|
||||
if st:
|
||||
w.writerow([])
|
||||
w.writerow(["【买跨对照·双边权利金】", st.get("premium")])
|
||||
w.writerow(["开→高超过", st.get("up_exceed_days"), "占比", st.get("up_exceed_ratio")])
|
||||
w.writerow(["开→低超过", st.get("down_exceed_days"), "占比", st.get("down_exceed_ratio")])
|
||||
w.writerow(["|涨跌|超过", st.get("abs_change_exceed_days"), "占比", st.get("abs_change_exceed_ratio")])
|
||||
w.writerow(["【买跨对照·双边权利金】", st.get("premium"), "止盈点", st.get("take_profit")])
|
||||
w.writerow(["开→高超过权利金", st.get("up_exceed_days"), "占比", st.get("up_exceed_ratio")])
|
||||
w.writerow(["开→低超过权利金", st.get("down_exceed_days"), "占比", st.get("down_exceed_ratio")])
|
||||
w.writerow(["|涨跌|超过权利金", st.get("abs_change_exceed_days"), "占比", st.get("abs_change_exceed_ratio")])
|
||||
if st.get("take_profit") is not None:
|
||||
w.writerow(["触达止盈天数", st.get("tp_hit_days"), "占比", st.get("tp_hit_ratio")])
|
||||
w.writerow(
|
||||
[
|
||||
"买跨点数盈亏合计",
|
||||
@@ -501,6 +690,8 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
w.writerow(
|
||||
[
|
||||
"结算日",
|
||||
"星期",
|
||||
"周末",
|
||||
"窗起点",
|
||||
"窗终点",
|
||||
"开盘",
|
||||
@@ -511,12 +702,17 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
"开→低",
|
||||
"振幅",
|
||||
"涨跌值",
|
||||
"有效波动",
|
||||
"触达止盈",
|
||||
"收益",
|
||||
]
|
||||
)
|
||||
for r in payload.get("rows") or []:
|
||||
w.writerow(
|
||||
[
|
||||
r.get("settlement_day"),
|
||||
r.get("weekday_label") or "",
|
||||
"是" if r.get("is_weekend") else "否",
|
||||
r.get("window_start"),
|
||||
r.get("window_end"),
|
||||
r.get("open"),
|
||||
@@ -527,6 +723,9 @@ def build_export_csv(payload: dict[str, Any]) -> str:
|
||||
r.get("down_points"),
|
||||
r.get("amplitude"),
|
||||
r.get("change"),
|
||||
r.get("effective_move"),
|
||||
"是" if r.get("take_profit_hit") else "否",
|
||||
r.get("profit"),
|
||||
]
|
||||
)
|
||||
return buf.getvalue()
|
||||
|
||||
@@ -13,9 +13,10 @@ from lib.hub.amp_stats_lib import (
|
||||
compute_amp_stats,
|
||||
export_filename,
|
||||
normalize_straddle_premium,
|
||||
normalize_take_profit,
|
||||
normalize_weekend_filter,
|
||||
reframe_amp_stats,
|
||||
rows_page,
|
||||
straddle_long_stats,
|
||||
summarize_rows,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,6 +26,8 @@ class ComputeBody(BaseModel):
|
||||
period: str = "2m"
|
||||
custom_days: Optional[int] = None
|
||||
straddle_premium: Optional[float] = None
|
||||
take_profit: Optional[float] = None
|
||||
weekend_filter: str = "all"
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
@@ -33,11 +36,21 @@ class SaveBody(BaseModel):
|
||||
result: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class StraddleBody(BaseModel):
|
||||
"""已有日表上按权利金重算买跨对照(不拉 K 线)."""
|
||||
class ReframeBody(BaseModel):
|
||||
"""已有日表上改周末/权利金/止盈(不拉 K 线)."""
|
||||
|
||||
rows: list[dict[str, Any]] = Field(default_factory=list)
|
||||
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
|
||||
weekend_filter: str = "all"
|
||||
price_source: str = ""
|
||||
inst_id: str = ""
|
||||
page: int = 1
|
||||
page_size: int = 20
|
||||
|
||||
|
||||
def create_amp_stats_router() -> APIRouter:
|
||||
@@ -62,10 +75,16 @@ def create_amp_stats_router() -> APIRouter:
|
||||
{"key": "1y", "label": "1年"},
|
||||
{"key": "custom", "label": "自定义"},
|
||||
],
|
||||
"weekend_filters": [
|
||||
{"key": "all", "label": "全部"},
|
||||
{"key": "exclude", "label": "排除周末"},
|
||||
{"key": "only", "label": "仅周末"},
|
||||
],
|
||||
"default_period": "2m",
|
||||
"default_weekend_filter": "all",
|
||||
"timeframe": "1H",
|
||||
"metric_note": "振幅与距离均为点数:振幅=最高-最低=(开→高)+(开→低)",
|
||||
"straddle_note": "买跨对照:双边权利金可设;越过用严格>;盈亏=|收-开|-权利金",
|
||||
"straddle_note": "买跨:越过权利金用>;止盈≥触达用止盈点否则|涨跌|;收益=有效波动-权利金",
|
||||
}
|
||||
|
||||
@router.post("/compute")
|
||||
@@ -77,6 +96,8 @@ def create_amp_stats_router() -> APIRouter:
|
||||
period=body.period,
|
||||
custom_days=body.custom_days,
|
||||
straddle_premium=body.straddle_premium,
|
||||
take_profit=body.take_profit,
|
||||
weekend_filter=body.weekend_filter,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
@@ -89,19 +110,28 @@ def create_amp_stats_router() -> APIRouter:
|
||||
"page": page,
|
||||
}
|
||||
|
||||
@router.post("/straddle")
|
||||
def api_straddle(body: StraddleBody):
|
||||
@router.post("/reframe")
|
||||
def api_reframe(body: ReframeBody):
|
||||
rows_all = body.rows_all or []
|
||||
if not rows_all:
|
||||
raise HTTPException(status_code=400, detail="无日表可重算")
|
||||
try:
|
||||
prem = normalize_straddle_premium(body.straddle_premium)
|
||||
result = reframe_amp_stats(
|
||||
rows_all=rows_all,
|
||||
symbol=body.symbol,
|
||||
start_hour=body.start_hour,
|
||||
period=body.period,
|
||||
sample_days=body.sample_days,
|
||||
straddle_premium=body.straddle_premium,
|
||||
take_profit=body.take_profit,
|
||||
weekend_filter=body.weekend_filter,
|
||||
price_source=body.price_source,
|
||||
inst_id=body.inst_id,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if prem is None:
|
||||
return {"ok": True, "straddle": None}
|
||||
try:
|
||||
st = straddle_long_stats(body.rows or [], prem)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return {"ok": True, "straddle": st}
|
||||
page = rows_page(result.get("rows") or [], page=body.page, page_size=body.page_size)
|
||||
return {"ok": True, "result": result, "page": page}
|
||||
|
||||
@router.get("/history")
|
||||
def api_history(symbol: str = "", limit: int = 50):
|
||||
@@ -110,7 +140,7 @@ def create_amp_stats_router() -> APIRouter:
|
||||
@router.post("/history")
|
||||
def api_history_save(body: SaveBody):
|
||||
payload = body.result if isinstance(body.result, dict) else {}
|
||||
if not payload.get("rows") and not payload.get("summary"):
|
||||
if not payload.get("rows") and not payload.get("rows_all") and not payload.get("summary"):
|
||||
raise HTTPException(status_code=400, detail="无可保存的结果")
|
||||
item = save_history(payload)
|
||||
return {"ok": True, "item": item}
|
||||
@@ -136,28 +166,46 @@ def create_amp_stats_router() -> APIRouter:
|
||||
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),
|
||||
weekend_filter: str = Query(default="all"),
|
||||
):
|
||||
if (history_id or "").strip():
|
||||
item = get_history(history_id.strip())
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="历史不存在")
|
||||
payload = dict(item)
|
||||
rows_all = item.get("rows_all") or item.get("rows") or []
|
||||
try:
|
||||
prem = normalize_straddle_premium(straddle_premium)
|
||||
payload = reframe_amp_stats(
|
||||
rows_all=rows_all,
|
||||
symbol=item.get("symbol") or symbol,
|
||||
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"),
|
||||
weekend_filter=weekend_filter or item.get("weekend_filter") or "all",
|
||||
price_source=str(item.get("price_source") or ""),
|
||||
inst_id=str(item.get("inst_id") or ""),
|
||||
missing=item.get("missing_days") or [],
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
if prem is not None:
|
||||
summary = summarize_rows(payload.get("rows") or [], straddle_premium=prem)
|
||||
payload["summary"] = summary
|
||||
payload["straddle_premium"] = prem
|
||||
else:
|
||||
try:
|
||||
# validate enums early
|
||||
normalize_weekend_filter(weekend_filter)
|
||||
normalize_straddle_premium(straddle_premium)
|
||||
normalize_take_profit(take_profit)
|
||||
payload = compute_amp_stats(
|
||||
symbol=symbol,
|
||||
start_hour=start_hour,
|
||||
period=period,
|
||||
custom_days=custom_days,
|
||||
straddle_premium=straddle_premium,
|
||||
take_profit=take_profit,
|
||||
weekend_filter=weekend_filter,
|
||||
)
|
||||
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");
|
||||
@@ -8,7 +8,7 @@
|
||||
let lastResult = null;
|
||||
let pageNo = 1;
|
||||
let bound = false;
|
||||
let straddleTimer = null;
|
||||
let reframeTimer = null;
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
|
||||
@@ -47,6 +47,18 @@
|
||||
return n;
|
||||
}
|
||||
|
||||
function readTakeProfit() {
|
||||
const raw = (el("amp-take-profit")?.value || "").trim();
|
||||
if (!raw) return null;
|
||||
const n = Number(raw);
|
||||
if (!Number.isFinite(n) || n <= 0) return null;
|
||||
return n;
|
||||
}
|
||||
|
||||
function readWeekend() {
|
||||
return el("amp-weekend-filter")?.value || "all";
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
const s = el("amp-status");
|
||||
if (s) s.textContent = msg || "";
|
||||
@@ -127,12 +139,17 @@
|
||||
: 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>` +
|
||||
`<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>` +
|
||||
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>` +
|
||||
@@ -140,6 +157,14 @@
|
||||
`</div>`;
|
||||
}
|
||||
|
||||
function dayLabel(r) {
|
||||
const day = esc(r.settlement_day);
|
||||
if (r.is_weekend && r.weekday_label) {
|
||||
return `${day}<span class="amp-wd-tag">${esc(r.weekday_label)}</span>`;
|
||||
}
|
||||
return day;
|
||||
}
|
||||
|
||||
function renderTable(pagePayload) {
|
||||
const body = el("amp-table-body");
|
||||
const pager = el("amp-pager");
|
||||
@@ -149,10 +174,15 @@
|
||||
body.innerHTML = '<tr><td colspan="11" class="amp-empty">暂无数据</td></tr>';
|
||||
} else {
|
||||
body.innerHTML = rows
|
||||
.map(
|
||||
(r) =>
|
||||
`<tr>` +
|
||||
`<td>${esc(r.settlement_day)}</td>` +
|
||||
.map((r) => {
|
||||
const profit =
|
||||
r.profit == null || r.profit === ""
|
||||
? "—"
|
||||
: `<span class="${pnlClass(r.profit)}">${esc(r.profit)}</span>`;
|
||||
const trClass = r.is_weekend ? ' class="amp-row-weekend"' : "";
|
||||
return (
|
||||
`<tr${trClass}>` +
|
||||
`<td>${dayLabel(r)}</td>` +
|
||||
`<td>${esc(r.window_start)}</td>` +
|
||||
`<td>${esc(r.open)}</td>` +
|
||||
`<td>${esc(r.high)}</td>` +
|
||||
@@ -162,8 +192,10 @@
|
||||
`<td>${esc(r.down_points)}</td>` +
|
||||
`<td><strong>${esc(r.amplitude)}</strong></td>` +
|
||||
`<td>${esc(r.change)}</td>` +
|
||||
`<td>${profit}</td>` +
|
||||
`</tr>`
|
||||
)
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
if (pager && pagePayload) {
|
||||
@@ -174,49 +206,66 @@
|
||||
el("amp-page-prev")?.addEventListener("click", () => {
|
||||
if (pageNo > 1) {
|
||||
pageNo -= 1;
|
||||
void compute(false);
|
||||
void reframe(false);
|
||||
}
|
||||
});
|
||||
el("amp-page-next")?.addEventListener("click", () => {
|
||||
if (pagePayload.page < pagePayload.total_pages) {
|
||||
pageNo += 1;
|
||||
void compute(false);
|
||||
void reframe(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function applyStraddleOnly() {
|
||||
if (!lastResult || !Array.isArray(lastResult.rows)) {
|
||||
renderStraddle(null);
|
||||
return;
|
||||
}
|
||||
const prem = readPremium();
|
||||
if (prem == null) {
|
||||
if (lastResult.summary) lastResult.summary.straddle = null;
|
||||
lastResult.straddle_premium = null;
|
||||
function rowsAllFromLast() {
|
||||
if (!lastResult) return [];
|
||||
if (Array.isArray(lastResult.rows_all) && lastResult.rows_all.length) return lastResult.rows_all;
|
||||
return lastResult.rows || [];
|
||||
}
|
||||
|
||||
async function reframe(resetPage) {
|
||||
if (!lastResult) {
|
||||
renderStraddle(null);
|
||||
return;
|
||||
}
|
||||
if (resetPage) pageNo = 1;
|
||||
const rowsAll = rowsAllFromLast();
|
||||
if (!rowsAll.length) return;
|
||||
try {
|
||||
const data = await apiFetch("/api/amp-stats/straddle", {
|
||||
const data = await apiFetch("/api/amp-stats/reframe", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rows: lastResult.rows, straddle_premium: prem }),
|
||||
body: JSON.stringify({
|
||||
rows_all: rowsAll,
|
||||
symbol: lastResult.symbol || el("amp-symbol")?.value || "eth",
|
||||
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(),
|
||||
weekend_filter: readWeekend(),
|
||||
price_source: lastResult.price_source || "",
|
||||
inst_id: lastResult.inst_id || "",
|
||||
page: pageNo,
|
||||
page_size: 20,
|
||||
}),
|
||||
});
|
||||
const st = data.straddle || null;
|
||||
if (!lastResult.summary) lastResult.summary = {};
|
||||
lastResult.summary.straddle = st;
|
||||
lastResult.straddle_premium = prem;
|
||||
renderStraddle(st);
|
||||
const prevAll = rowsAll;
|
||||
lastResult = data.result || lastResult;
|
||||
if (!lastResult.rows_all || !lastResult.rows_all.length) lastResult.rows_all = prevAll;
|
||||
renderSummary(lastResult.summary, lastResult);
|
||||
renderTable(data.page);
|
||||
setStatus(`完成 · 样本 ${(lastResult.summary || {}).sample_count || 0}`);
|
||||
} catch (e) {
|
||||
setStatus(String(e && e.message ? e.message : e));
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleStraddleRefresh() {
|
||||
if (straddleTimer) clearTimeout(straddleTimer);
|
||||
straddleTimer = setTimeout(() => void applyStraddleOnly(), 280);
|
||||
function scheduleReframe() {
|
||||
if (!lastResult) return;
|
||||
if (reframeTimer) clearTimeout(reframeTimer);
|
||||
reframeTimer = setTimeout(() => void reframe(true), 280);
|
||||
}
|
||||
|
||||
async function compute(resetPage) {
|
||||
@@ -225,7 +274,6 @@
|
||||
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 straddlePremium = readPremium();
|
||||
setStatus("计算中…(首次拉取 OKX K 线可能需数十秒)");
|
||||
try {
|
||||
const data = await apiFetch("/api/amp-stats/compute", {
|
||||
@@ -236,7 +284,9 @@
|
||||
start_hour: startHour,
|
||||
period,
|
||||
custom_days: period === "custom" ? customDays : null,
|
||||
straddle_premium: straddlePremium,
|
||||
straddle_premium: readPremium(),
|
||||
take_profit: readTakeProfit(),
|
||||
weekend_filter: readWeekend(),
|
||||
page: pageNo,
|
||||
page_size: 20,
|
||||
}),
|
||||
@@ -282,13 +332,16 @@
|
||||
const period = el("amp-period")?.value || "2m";
|
||||
const customDays = Number(el("amp-custom-days")?.value || 60);
|
||||
const prem = readPremium();
|
||||
const tp = readTakeProfit();
|
||||
const q = new URLSearchParams({
|
||||
symbol,
|
||||
start_hour: String(startHour),
|
||||
period,
|
||||
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));
|
||||
window.location.href = "/api/amp-stats/export?" + q.toString();
|
||||
}
|
||||
|
||||
@@ -323,8 +376,14 @@
|
||||
card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id));
|
||||
card.querySelector(".amp-hist-dl")?.addEventListener("click", () => {
|
||||
const prem = readPremium();
|
||||
let url = "/api/amp-stats/export?history_id=" + encodeURIComponent(id);
|
||||
const tp = readTakeProfit();
|
||||
let url =
|
||||
"/api/amp-stats/export?history_id=" +
|
||||
encodeURIComponent(id) +
|
||||
"&weekend_filter=" +
|
||||
encodeURIComponent(readWeekend());
|
||||
if (prem != null) url += "&straddle_premium=" + encodeURIComponent(String(prem));
|
||||
if (tp != null) url += "&take_profit=" + encodeURIComponent(String(tp));
|
||||
window.location.href = url;
|
||||
});
|
||||
card.querySelector(".amp-hist-del")?.addEventListener("click", async () => {
|
||||
@@ -349,18 +408,15 @@
|
||||
if (lastResult.straddle_premium != null && el("amp-straddle-premium")) {
|
||||
el("amp-straddle-premium").value = String(lastResult.straddle_premium);
|
||||
}
|
||||
renderSummary(lastResult.summary, lastResult);
|
||||
const pagePayload = {
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
total: (lastResult.rows || []).length,
|
||||
total_pages: Math.max(1, Math.ceil((lastResult.rows || []).length / 20)),
|
||||
rows: (lastResult.rows || []).slice(0, 20),
|
||||
};
|
||||
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")) {
|
||||
el("amp-weekend-filter").value = lastResult.weekend_filter;
|
||||
}
|
||||
pageNo = 1;
|
||||
renderTable(pagePayload);
|
||||
setStatus("已载入历史 " + id);
|
||||
void applyStraddleOnly();
|
||||
await reframe(true);
|
||||
}
|
||||
} catch (e) {
|
||||
setStatus(String(e && e.message ? e.message : e));
|
||||
@@ -378,7 +434,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-straddle-premium")?.addEventListener("input", scheduleStraddleRefresh);
|
||||
el("amp-straddle-premium")?.addEventListener("input", scheduleReframe);
|
||||
el("amp-take-profit")?.addEventListener("input", scheduleReframe);
|
||||
el("amp-weekend-filter")?.addEventListener("change", () => void reframe(true));
|
||||
syncCustomDays();
|
||||
}
|
||||
|
||||
|
||||
@@ -10973,6 +10973,12 @@ html[data-theme="light"] .hub-logs-card-hint {
|
||||
.amp-sum-v.is-pos { color: var(--green); }
|
||||
.amp-sum-v.is-neg { color: var(--red); }
|
||||
.amp-straddle { margin-bottom: 4px; }
|
||||
.amp-table tr.amp-row-weekend td { background: rgba(255, 180, 60, 0.08); }
|
||||
.amp-wd-tag {
|
||||
display: inline-block; margin-left: 6px; padding: 1px 6px; border-radius: 4px;
|
||||
font-size: 11px; font-weight: 600; color: #f0c14b;
|
||||
border: 1px solid rgba(240, 193, 75, 0.45);
|
||||
}
|
||||
.amp-table-wrap { overflow-x: auto; }
|
||||
.amp-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.amp-table th, .amp-table td {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-straddle" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260723-amp-tp" />
|
||||
<link rel="stylesheet" href="/assets/trade_stats_calendar.css?v=4" />
|
||||
<link rel="stylesheet" href="/assets/account_risk_badge.css?v=4" />
|
||||
<script src="/assets/account_risk_badge.js?v=4"></script>
|
||||
@@ -1051,10 +1051,22 @@
|
||||
<span>自定义天数</span>
|
||||
<input id="amp-custom-days" type="number" min="7" max="400" value="60" />
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>周末</span>
|
||||
<select id="amp-weekend-filter">
|
||||
<option value="all" selected>全部</option>
|
||||
<option value="exclude">排除周末</option>
|
||||
<option value="only">仅周末</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>买跨·双边权利金(点)</span>
|
||||
<input id="amp-straddle-premium" type="number" min="0" step="any" placeholder="如 30" />
|
||||
</label>
|
||||
<label class="amp-field">
|
||||
<span>止盈点(点)</span>
|
||||
<input id="amp-take-profit" type="number" min="0" step="any" placeholder="空=按涨跌" />
|
||||
</label>
|
||||
<div class="amp-actions">
|
||||
<button type="button" id="amp-btn-compute" class="primary">计算</button>
|
||||
<button type="button" id="amp-btn-save" class="ghost">保存到历史</button>
|
||||
@@ -1062,7 +1074,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<p id="amp-status" class="toolbar-meta amp-status"></p>
|
||||
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低(均为点数).未到 16:00 的当日不入样.买跨对照:越过用严格 >;盈亏=|收−开|−双边权利金.</p>
|
||||
<p class="amp-hint">口径:开→高=最高−开盘;开→低=开盘−最低;振幅=最高−最低.买跨收益=有效波动−权利金;止盈≥触达则有效波动=止盈点,否则用|涨跌|.周末按结算日标注/筛选.</p>
|
||||
<h3 class="amp-block-title">汇总</h3>
|
||||
<div id="amp-summary" class="amp-summary"></div>
|
||||
<h3 class="amp-block-title">买跨对照</h3>
|
||||
@@ -1073,11 +1085,11 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>结算日</th><th>窗起点</th><th>开</th><th>高</th><th>低</th><th>收</th>
|
||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th>
|
||||
<th>开→高</th><th>开→低</th><th>振幅</th><th>涨跌</th><th>收益</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="amp-table-body">
|
||||
<tr><td colspan="10" class="amp-empty">点击「计算」加载</td></tr>
|
||||
<tr><td colspan="11" class="amp-empty">点击「计算」加载</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1496,7 +1508,7 @@
|
||||
<script src="/assets/funds.js?v=20260717-funds-scroll-fix"></script>
|
||||
<script src="/assets/dashboard.js?v=20260720-dash-sl-tp"></script>
|
||||
<script src="/assets/strategy.js?v=9"></script>
|
||||
<script src="/assets/amp_stats.js?v=2"></script>
|
||||
<script src="/assets/amp_stats.js?v=3"></script>
|
||||
<script src="/assets/help.js?v=1"></script>
|
||||
<script src="/assets/logs.js?v=1"></script>
|
||||
<script src="/assets/ai_review_render.js?v=3"></script>
|
||||
|
||||
@@ -114,6 +114,83 @@ class AmpStatsLibTests(unittest.TestCase):
|
||||
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 = [
|
||||
{
|
||||
"settlement_day": "2026-07-18",
|
||||
"is_weekend": True,
|
||||
"weekday_label": "六",
|
||||
"up_points": 100,
|
||||
"down_points": 10,
|
||||
"change": -5,
|
||||
"amplitude": 110,
|
||||
},
|
||||
{
|
||||
"settlement_day": "2026-07-19",
|
||||
"is_weekend": True,
|
||||
"weekday_label": "日",
|
||||
"up_points": 20,
|
||||
"down_points": 15,
|
||||
"change": 12,
|
||||
"amplitude": 35,
|
||||
},
|
||||
{
|
||||
"settlement_day": "2026-07-20",
|
||||
"is_weekend": False,
|
||||
"weekday_label": "",
|
||||
"up_points": 50,
|
||||
"down_points": 40,
|
||||
"change": 8,
|
||||
"amplitude": 90,
|
||||
},
|
||||
]
|
||||
excl = filter_weekend_rows(rows, "exclude")
|
||||
self.assertEqual(len(excl), 1)
|
||||
self.assertEqual(excl[0]["settlement_day"], "2026-07-20")
|
||||
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)
|
||||
|
||||
reframed = reframe_amp_stats(
|
||||
rows_all=rows,
|
||||
symbol="eth",
|
||||
weekend_filter="exclude",
|
||||
straddle_premium=10,
|
||||
take_profit=80,
|
||||
)
|
||||
self.assertEqual(reframed["summary"]["sample_count"], 1)
|
||||
# Mon: 未触达止盈 → |8|-10
|
||||
self.assertEqual(reframed["rows"][0]["profit"], -2)
|
||||
self.assertIn("收益", build_export_csv(reframed))
|
||||
|
||||
def test_compute_with_mock_fetch(self):
|
||||
now = datetime(2026, 7, 22, 18, 0, tzinfo=TZ)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user