# -*- coding: utf-8 -*- """ 159915 日内均值回归 最终交付版(v7) 主推参数(在成本 0.02%~0.03% 口径下 12/12 季度跑赢): 正T(低吸): 15分钟均线, 低于均线0.3%买入, 回归-0.1%或止盈0.3%卖出, 止损1.0%, 仅价>昨收时做 反T(高抛): 15分钟均线, 高于均线0.4%卖出, 回归-0.1%或止盈0.5%买回, 止损0.5%, 仅价<昨收时做 同时输出 0.02/0.03/0.05/0.08% 成本敏感性,诚实暴露策略成立的前提条件 """ import os import csv import json from collections import OrderedDict import importlib.util import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from matplotlib import font_manager BASE = os.path.dirname(os.path.abspath(__file__)) spec = importlib.util.spec_from_file_location("v2", os.path.join(BASE, "均值回归_双向.py")) v2 = importlib.util.module_from_spec(spec); spec.loader.exec_module(v2) BASE_COST = v2.COST F = v2.F L = {"N": 15, "K": -0.3, "TP": 0.3, "SL": 1.0, "ED": -0.1} S = {"N": 15, "K": 0.4, "TP": 0.5, "SL": 0.5, "ED": -0.1} COSTS = [0.02, 0.03, 0.05, 0.08] MAIN = 0.02 # 主推口径:万0.5佣金 + 限价挂单成交 days = v2.load_days() print(f"载入 {len(days)} 个交易日") q_base, pc = OrderedDict(), None for d, v in days.items(): k = v2.quarter_key(d) s = pc if pc is not None else v["p"][0] / (1 + v["r"][0] / 100) q_base.setdefault(k, {"s": s, "e": v["p"][-1]}) q_base[k]["e"] = v["p"][-1] pc = v["p"][-1] q_base = {k: (x["e"] / x["s"] - 1) * 100 for k, x in q_base.items()} cache = {N: v2.make_dev_cache(days, N) for N in (15,)} tl = v2.run_side(cache[15], "long", K=L["K"], TP=L["TP"], SL=L["SL"], ED=L["ED"], trend_filter=True, skip_open=5, max_per_day=2) ts = v2.run_side(cache[15], "short", K=S["K"], TP=S["TP"], SL=S["SL"], ED=S["ED"], trend_filter=True, skip_open=5, max_per_day=2) trades = tl + ts gross = sum(t["ret"] for t in trades) + len(trades) * BASE_COST print(f"做T笔数 {len(trades)}(正T {len(tl)} / 反T {len(ts)}) 单笔毛收益 {gross/len(trades):.3f}%") def qtable(cost): q = OrderedDict() for t in trades: q.setdefault(v2.quarter_key(t["date"]), []).append((t["ret"] + BASE_COST - cost) * F) q = {k: sum(v) for k, v in sorted(q.items())} return q qts = {c: qtable(c) for c in COSTS} print(f"\n{'季度':<8}{'基准%':>9}" + "".join(f"{'增厚@'+str(c)+'%':>12}" for c in COSTS) + f"{'策略@主推%':>12}") wins = {c: 0 for c in COSTS} for q in q_base: line = f"{q:<8}{q_base[q]:>9.2f}" for c in COSTS: a = qts[c].get(q, 0.0) wins[c] += a > 0 line += f"{a:>12.2f}" line += f"{q_base[q] + qts[MAIN].get(q, 0):>12.2f}" print(line) print() for c in COSTS: tot = sum(qts[c].values()) print(f"成本{c}%: 跑赢季度 {wins[c]}/{len(q_base)} 总增厚 {tot:+.2f}% " f"年化增厚(约2.9年) {((1+tot/100)**(1/2.9)-1)*100:+.2f}%") # ==================== 图表 ==================== for f in ["Microsoft YaHei", "SimHei"]: if any(f.lower() in ff.name.lower() for ff in font_manager.fontManager.ttflist): plt.rcParams["font.family"] = f break plt.rcParams["axes.unicode_minus"] = False by_date = {c: OrderedDict() for c in COSTS} for t in trades: for c in COSTS: by_date[c].setdefault(t["date"], 0.0) by_date[c][t["date"]] += (t["ret"] + BASE_COST - c) * F xs, base_eq = [], [] eq = {c: [] for c in COSTS} ov = {c: 1.0 for c in COSTS} first, base0 = True, None for d, v in days.items(): if first: base0 = v["p"][0] / (1 + v["r"][0] / 100); first = False for c in COSTS: if d in by_date[c]: ov[c] *= (1 + by_date[c][d] / 100) be = v["p"][-1] / base0 xs.append(d); base_eq.append(be) for c in COSTS: eq[c].append(be * ov[c]) fig, axes = plt.subplots(2, 1, figsize=(12.5, 9.5), gridspec_kw={"height_ratios": [2, 1.25]}) ax = axes[0] xi = range(len(xs)) ax.plot(xi, base_eq, color="#888888", lw=1.3, ls="--", label="基准:满仓持有159915") styles = {0.02: ("#e8a33d", 1.2, "-"), 0.03: ("#d43f3f", 1.9, "-"), 0.05: ("#7a5bb5", 1.2, "-."), 0.08: ("#4a7ec8", 1.2, ":")} for c in COSTS: col, lw, ls = styles[c] lab = f"策略(成本{c}%)" + (" ←主推" if c == MAIN else "") ax.plot(xi, eq[c], color=col, lw=lw, ls=ls, label=lab) tk = list(range(0, len(xs), max(1, len(xs) // 12))) ax.set_xticks(tk) ax.set_xticklabels([xs[k][:4] + "-" + xs[k][4:6] for k in tk], rotation=30, fontsize=8) ax.set_ylabel("净值") ax.set_title(f"159915 日内均值回归做T(2023-01~2025-11)|成本0.02%: 策略{(eq[MAIN][-1]-1)*100:.2f}% vs 基准{(base_eq[-1]-1)*100:.2f}%", fontsize=12) ax.legend(loc="upper left", fontsize=8.5) ax.grid(alpha=0.25) axb = axes[1] qs = list(q_base.keys()) xq = list(range(len(qs))) w = 0.4 axb.bar([x - w / 2 for x in xq], [q_base[q] for q in qs], width=w, color="#9aa5b1", label="基准") axb.bar([x + w / 2 for x in xq], [q_base[q] + qts[MAIN].get(q, 0) for q in qs], width=w, color="#d43f3f", label="策略(主推成本0.02%)") axb.set_xticks(xq) axb.set_xticklabels(qs, rotation=30, fontsize=8) axb.axhline(0, color="#999999", lw=0.8) axb.set_ylabel("季度收益(%)") axb.set_title("分季度对比(策略 = 基准 + 做T增厚)", fontsize=11) axb.legend(fontsize=9) axb.grid(alpha=0.25, axis="y") plt.tight_layout() png = os.path.join(BASE, "均值回归_资金曲线.png") plt.savefig(png, dpi=130) csv_path = os.path.join(BASE, "均值回归_交易明细.csv") with open(csv_path, "w", newline="", encoding="utf-8-sig") as f: wr = csv.writer(f) wr.writerow(["日期", "方向", "买入/卖回时间", "平仓时间", "入场价", "出场价", "净收益%@0.02", "入场偏离%", "平仓原因"]) for t in sorted(trades, key=lambda x: (x["date"], x["t_in"])): wr.writerow([t["date"], "正T低吸" if t["side"] == "long" else "反T高抛", "%02d:%02d" % t["t_in"], "%02d:%02d" % t["t_out"], round(t["px_in"], 4), round(t["px_out"], 4), round(t["ret"] + BASE_COST - MAIN, 3), round(t["dev_in"], 2), t["reason"]]) rows = "" for q in q_base: cells = "".join(f"{qts[c].get(q,0):+.2f}%" for c in COSTS) a3 = qts[MAIN].get(q, 0) rows += (f"{q}{q_base[q]:.2f}%{cells}" f"0 else 'neg'}'>{q_base[q]+a3:.2f}%{'✅' if a3>0 else '❌'}") byr = {} for t in trades: byr.setdefault(t["reason"], []).append(t["ret"] + BASE_COST - MAIN) reason_html = "".join(f"
  • {k}:{len(v)}笔,平均 {sum(v)/len(v):.3f}%
  • " for k, v in sorted(byr.items(), key=lambda x: -len(x[1]))) html = f""" 159915 日内均值回归做T回测报告

    159915 创业板ETF · 日内均值回归(做T增强)回测报告

    区间 2023-01-03 ~ 2025-11-28|703个交易日|168,022根1分钟K线|数据:本地购买的一分钟K线

    主推口径成本0.02%
    跑赢季度{wins[MAIN]}/{len(q_base)}
    总增厚{sum(qts[MAIN].values()):+.2f}%
    做T笔数{len(trades)}
    单笔毛收益{gross/len(trades):.3f}%
    策略累计{(eq[MAIN][-1]-1)*100:.2f}%

    一、策略结构

    基准:满仓持有 159915,不做择时。
    策略:底仓满仓不动(完整保留基准收益)+ 用等额资金做日内双向T,做T增厚即超额来源,不加隔夜风险。
    正T(低吸):价格低于15分钟均线0.3%买入;回到均线(-0.1%)或上涨 0.3% 卖出;止损 1.0%;只在价格高于昨收的日子做(顺大势、逆小势)。
    反T(高抛):价格高于 15 分钟均线 0.4% 卖出;回到均线(-0.1%)或回落 0.5% 买回;止损 0.5%;只在价格低于昨收的日子做。
    执行约束:每方向每日最多 2 笔,14:55 前必须平掉。

    二、分季度对比 & 成本敏感性

    {''.join(f'' for c in COSTS)}{rows}
    季度基准增厚@{c}%策略@0.02%跑赢

    各成本口径下的整体结果:

    往返成本对应情形跑赢季度总增厚
    0.02%万0.5佣金 + 全部限价挂单成交(主推口径){wins[0.02]}/{len(q_base)}{sum(qts[0.02].values()):+.2f}%
    0.03%万1佣金 + 挂单成交{wins[0.03]}/{len(q_base)}{sum(qts[0.03].values()):+.2f}%
    0.05%万2.5佣金 + 少量滑点{wins[0.05]}/{len(q_base)}{sum(qts[0.05].values()):+.2f}%
    0.08%每笔主动吃1跳(约0.04%){wins[0.08]}/{len(q_base)}{sum(qts[0.08].values()):+.2f}%

    三、资金曲线

    资金曲线

    四、交易特征(成本0.02%口径)

    五、风险与前提(务必阅读)

    1. 成本是生死线:单笔毛收益仅 {gross/len(trades):.3f}%,与成本同量级。成本 0.02% → 总增厚 {sum(qts[0.02].values()):+.2f}%;0.05% → {sum(qts[0.05].values()):+.2f}%; 0.08% → {sum(qts[0.08].values()):+.2f}%(策略失效)。必须把往返成本控制到 0.03% 以内(万1佣金 + 限价挂单成交),这句话成立与否直接决定这套策略值不值得做。

    2. "每季度都赢"处在能力边界上:对 413 组邻近参数做稳健性检验,只有 5% 的组合能做到"最多1个季度为负", 绝大多数组合最差季度在 -1.5% 附近;且当成本放宽到 0.05% 时,最好的参数组合也只能做到 10/12 个季度跑赢。不要把它当成必然规律。
    3. 最难的环境:"阴跌 + 反弹反复"的季度(如 2024Q2、2025Q2),正T被套、反T被反弹打脸。建议识别到该环境(日线趋势向下 + 波动收窄)时,把做T仓位降到半仓。
    4. 成交假设:回测以分钟收盘价成交,实盘中封板、急速跳水时可能无法成交或滑点扩大。交易明细已导出为 CSV,可逐笔核对。
    5. 定位:这是"增强"不是"替代"——底仓必须留着,做T只是赚日内波动的钱,长期看增厚主要来自波动率而非趋势。

    """ open(os.path.join(BASE, "均值回归_报告.html"), "w", encoding="utf-8").write(html) json.dump({"params": {"long": L, "short": S}, "costs": COSTS, "wins": wins, "tot": {str(c): sum(qts[c].values()) for c in COSTS}, "q_base": q_base, "q_add": {str(c): qts[c] for c in COSTS}, "trades": len(trades), "gross_per_trade": gross / len(trades)}, open(os.path.join(BASE, "均值回归_最终参数.json"), "w", encoding="utf-8"), ensure_ascii=False, indent=1) print("\n已生成: 均值回归_报告.html / 均值回归_资金曲线.png / 均值回归_交易明细.csv / 均值回归_最终参数.json")