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}"