File size: 5,934 Bytes
0580e8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""金仓 v3 回测对比台(本地 UI,纯标准库)。

启动:python ui_server.py   →  http://127.0.0.1:8321

GET  /             前端页面(ui/index.html)
GET  /api/presets  预设方案(v2.1 / F5 / 组合 等,2013–2025 基准)
POST /api/backtest 自由参数回测:{target_vol, cash_yield, exit_line, floor, breaker, start, end}

提醒(纪律):正式研究判读窗口为 2013–2025;2026-09-12 起为前向影子期。
UI 的自由参数仅供探索与核对,正式采纳任何配置仍须走计划书预注册流程。
"""
import json
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

import engine as E                 # noqa: E402
from overlays import build_overlays   # noqa: E402
from run import one_stats, yearly_table   # noqa: E402

HERE = Path(__file__).resolve().parent
PORT = 8321
OV = build_overlays()
FLOORS = {"sma": "sma12m_cap", "mom": "mom12_1_cap", "climate": "climate_cap"}
PRESET_DEFS = [
    ("v2.1(12%,固定102)", "退役对照", 12.0, None),
    ("F5(12%,动态线)", "默认候选", 12.0, {"cb_line": OV["breakeven"]}),
    ("组合(8%+动量地板+动态线)", "参考配置(D19 后未达采纳线)", 8.0,
     {"cap_half": OV["mom12_1_cap"], "cb_line": OV["breakeven"]}),
    ("F2b 动量地板(12%)", "单项", 12.0, {"cap_half": OV["mom12_1_cap"]}),
    ("F5+动量地板(12%)", "单项组合", 12.0,
     {"cap_half": OV["mom12_1_cap"], "cb_line": OV["breakeven"]}),
]


def run_full(start, end, vol, overlay):
    summary, st, data = E.run(start, end, target_vol_ann=vol, overlay=overlay, tag=None)
    curve = data["curve"]
    return summary, st, curve, data["trades"], one_stats([c["V"] for c in curve])


_PRESET_CACHE = None


def preset_payload():
    global _PRESET_CACHE
    if _PRESET_CACHE is not None:
        return _PRESET_CACHE
    out = []
    for label, note, vol, ov in PRESET_DEFS:
        summary, _, _, _, s = run_full("2013-01-01", "2025-12-31", vol, ov)
        out.append({"label": label, "note": note, "stats": s,
                    "n_trades": summary["n_trades"], "fees": round(summary["fees"], 1)})
    _PRESET_CACHE = out
    return out


def overlay_from(p):
    ov = {}
    if p.get("cash_yield"):
        ov["cash_yield"] = float(p["cash_yield"])
    if p.get("exit_line") == "dynamic":
        ov["cb_line"] = OV["breakeven"]
    fl = p.get("floor", "none")
    if fl in FLOORS:
        ov["cap_half"] = OV[FLOORS[fl]]
    if p.get("breaker"):
        ov["breaker"] = True
    return ov


class Handler(BaseHTTPRequestHandler):
    def _send(self, code, body, ctype="application/json; charset=utf-8"):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        if self.path in ("/", "/index.html"):
            self._send(200, (HERE / "ui" / "index.html").read_bytes(), "text/html; charset=utf-8")
        elif self.path == "/api/presets":
            self._send(200, json.dumps(preset_payload(), ensure_ascii=False).encode("utf-8"))
        else:
            self._send(404, b'{"error": "not found"}')

    def do_POST(self):
        if self.path != "/api/backtest":
            self._send(404, b'{"error": "not found"}')
            return
        try:
            n = int(self.headers.get("Content-Length", 0))
            p = json.loads(self.rfile.read(n))
            start = p.get("start", "2013-01-01")
            end = p.get("end", "2025-12-31")
            vol = float(p.get("target_vol", 12.0))
            ov = overlay_from(p)
            summary, st, curve, trades, s = run_full(start, end, vol, ov)
            bh_s = one_stats([c["bh_V"] for c in curve])
            ctx = {c["date"]: c for c in curve}
            t_rows = [{"date": t["date"],
                       "action": "买入" if t["action"] == "BUY" else ("卖出" if t["action"] == "SELL" else "作废"),
                       "price": t["price"], "amount": round(t["amount_usd"], 0),
                       "reason": t["reason"], "V": round(t["V_after"], 0),
                       "sigma": round((ctx.get(t["date"], {}).get("sigma") or 0) * 100, 1),
                       "q": (ctx.get(t["date"], {}).get("q") or 0) and round(ctx[t["date"]]["q"]),
                       "pace": (ctx.get(t["date"], {}).get("pace") or 0) and round(ctx[t["date"]]["pace"])}
                      for t in trades]
            step = max(1, len(curve) // 1000)
            idx = list(range(0, len(curve), step))
            if idx[-1] != len(curve) - 1:
                idx.append(len(curve) - 1)
            payload = {"params": p,
                       "stats": s, "bh": bh_s,
                       "n_trades": summary["n_trades"], "fees": round(summary["fees"], 1),
                       "final_V": round(summary["V"], 0),
                       "yearly": yearly_table(curve),
                       "exit_spans": summary["exit_spans"],
                       "curve": {"dates": [curve[i]["date"] for i in idx],
                                 "v": [round(curve[i]["V"], 2) for i in idx],
                                 "bh": [round(curve[i]["bh_V"], 2) for i in idx]},
                       "trades": t_rows}
            self._send(200, json.dumps(payload, ensure_ascii=False).encode("utf-8"))
        except Exception as exc:                                   # noqa: BLE001
            self._send(500, json.dumps({"error": f"{type(exc).__name__}: {exc}"},
                                       ensure_ascii=False).encode("utf-8"))

    def log_message(self, *a):                                     # 静默访问日志
        pass


if __name__ == "__main__":
    print(f"启动金仓对比台 http://127.0.0.1:{PORT} (Ctrl+C 退出)")
    ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()