fix(factor): tears iframe 渲染——patch GridFigure.close 收集 figure 生成 HTML
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 无报错。
This commit is contained in:
@@ -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'<img src="data:image/png;base64,{_b64}"/>')
|
||||
plt.close(_fig)
|
||||
|
||||
_html = (
|
||||
f'<html><head><meta charset="utf-8"><title>{factor_name} tears</title>'
|
||||
'<style>body{font-family:-apple-system,Segoe UI,sans-serif;margin:0;'
|
||||
'padding:12px;background:#fff;}h2{margin:0 0 12px;font-size:18px;}'
|
||||
'img{display:block;max-width:100%;margin:0 0 14px;border:1px solid #eee;}'
|
||||
'</style></head><body>'
|
||||
f'<h2>{factor_name} — Alphalens Tearsheet</h2>'
|
||||
+ "".join(_img_tags)
|
||||
+ "</body></html>"
|
||||
)
|
||||
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}"
|
||||
|
||||
Reference in New Issue
Block a user