From 65021bd32cd045dfd7f13151c373588c287d02f4 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 17 Jul 2026 14:38:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(factor):=20tears=20iframe=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E2=80=94=E2=80=94patch=20GridFigure.close=20=E6=94=B6?= =?UTF-8?q?=E9=9B=86=20figure=20=E7=94=9F=E6=88=90=20HTML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit alphalens-reloaded create_full_tear_sheet 内部每个 tear sheet 末尾调 GridFigure.close()→plt.close(fig),Agg 后端下 figure 被销毁,plt.savefig 只能拿到最后一个空占位 figure(实证 2.3KB 空 PNG);原代码 savefig 存 .png 却把 report_paths 记成 .html,report endpoint os.path.exists 失败 返回 404,iframe 加载空白(回退到 SPA 首页标题)。 monkey-patch GridFigure.close 收集 figure 而非销毁(同 empyrical/demean 适配思路,不改 alphalens 源码),跑完把所有 figure savefig 成 base64 嵌入 真 HTML 文件,report_paths 指向该 html。 浏览器端到端验证(VPS factor_2d01a718):report endpoint 404→200,iframe 渲染 4 张 tearsheet 图(returns/information/turnover/quantile table,2.4MB), naturalWidth>0 确认非 broken,console 无报错。 --- sanguo_factor/analyzer.py | 68 +++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/sanguo_factor/analyzer.py b/sanguo_factor/analyzer.py index 8781d02..7f2320d 100644 --- a/sanguo_factor/analyzer.py +++ b/sanguo_factor/analyzer.py @@ -290,19 +290,69 @@ def run_factor_analysis( } # Generate tears sheet(独立 try:tears 失败只标注,不覆盖上面的 IC 成功) - from io import StringIO + # + # alphalens-reloaded 的 create_full_tear_sheet 内部,每个 tear sheet + # 末尾调 GridFigure.close() → plt.close(fig)。Agg 后端下 figure 被销毁, + # 之后 plt.savefig 只能拿到最后一个空占位 figure(实证 2.3KB 空 PNG), + # 且原代码 savefig 存 .png 却把 report_paths 记成 .html → report endpoint + # 404。monkey-patch GridFigure.close 收集 figure,base64 嵌入真 HTML 文件。 + # 同 empyrical np.NINF / demean_forward_returns 适配思路,不改 alphalens 源码。 + from io import StringIO, BytesIO + import base64 + import alphalens.tears as _al_tears old_stdout = sys.stdout - sys.stdout = StringIO() # Capture stdout to avoid display issues + sys.stdout = StringIO() # Capture stdout (alphalens 打印统计表) try: - create_full_tear_sheet( - merged_data, - long_short=True, - group_neutral=False, - by_group=False + _collected_figs: list = [] + _orig_gridfig_close = _al_tears.GridFigure.close + def _collect_gridfig_close(self, _sink=_collected_figs): + fig = getattr(self, "fig", None) + if fig is not None: + _sink.append(fig) + self.gs = None # 不调 plt.close,保留 figure + _al_tears.GridFigure.close = _collect_gridfig_close + try: + plt.close("all") + create_full_tear_sheet( + merged_data, + long_short=True, + group_neutral=False, + by_group=False, + ) + finally: + _al_tears.GridFigure.close = _orig_gridfig_close + + # 合并 collected + 仍存活的 figure(plot_quantile_statistics_table + # 的表格图不经过 GridFigure,需从 fignums 补) + _all_figs = list(_collected_figs) + for _num in plt.get_fignums(): + _f = plt.figure(_num) + if _f not in _all_figs: + _all_figs.append(_f) + + _img_tags = [] + for _fig in _all_figs: + _buf = BytesIO() + _fig.savefig(_buf, format="png", dpi=72, bbox_inches="tight") + _buf.seek(0) + _b64 = base64.b64encode(_buf.read()).decode("ascii") + _img_tags.append(f'') + plt.close(_fig) + + _html = ( + f'{factor_name} tears' + '' + f'

{factor_name} — Alphalens Tearsheet

' + + "".join(_img_tags) + + "" ) factor_report_path = os.path.join(output_dir, f"{factor_name}_tears.html") - plt.savefig(factor_report_path.replace(".html", ".png")) # Save as PNG - report_paths[factor_name] = factor_report_path.replace(".png", ".html") + with open(factor_report_path, "w", encoding="utf-8") as _fp: + _fp.write(_html) + report_paths[factor_name] = factor_report_path ic_summary[factor_name]["report"] = factor_report_path except Exception as tears_e: ic_summary[factor_name]["tears_error"] = f"{type(tears_e).__name__}: {tears_e}"