p05 / backtest /ui_server.py
goldlab
D28 补漏:模拟器预设/退出线下拉落地平滑3;组合预设解耦回 raw
0580e8e
Raw History Blame Contribute Delete
5.93 kB
"""金仓 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()