feat(report): 支持ETF净值和溢价率的绩效报告展示
- 在生成绩效报告接口中新增code_config、index_data、etf_price_data和etf_nav_data_raw参数 - 计算溢价率并基于信号前一日数据进行校验和计算 - 打印最新调仓信号时增加ETF代码、ETF净值、溢价率及高溢价警告显示 - 调整信号数据基准日期展示,更准确反映信号计算依据 - 报告图表支持显示ETF净值和溢价率列,完善调仓信息视觉效果 - 统一处理跨市场ETF映射和特殊市场(如加密货币)情况,避免溢价率误报 - 完善打印表格和图表的列宽和格式,增强可读性
This commit is contained in:
@@ -19,6 +19,10 @@ def generate_performance_report(
|
|||||||
benchmark_name: str = "沪深300指数",
|
benchmark_name: str = "沪深300指数",
|
||||||
save_path: str = "report",
|
save_path: str = "report",
|
||||||
select_num: int = 1,
|
select_num: int = 1,
|
||||||
|
code_config: dict = None,
|
||||||
|
index_data: pd.DataFrame = None,
|
||||||
|
etf_price_data: pd.DataFrame = None,
|
||||||
|
etf_nav_data_raw: pd.DataFrame = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
生成完整的绩效报告
|
生成完整的绩效报告
|
||||||
@@ -30,6 +34,10 @@ def generate_performance_report(
|
|||||||
benchmark_name: 基准名称
|
benchmark_name: 基准名称
|
||||||
save_path: 报告保存路径前缀
|
save_path: 报告保存路径前缀
|
||||||
select_num: 选中数量
|
select_num: 选中数量
|
||||||
|
code_config: 代码配置(包含 name, etf, market),用于显示ETF映射
|
||||||
|
index_data: 指数价格数据
|
||||||
|
etf_price_data: ETF价格数据(用于计算溢价率)
|
||||||
|
etf_nav_data_raw: ETF净值数据(用于计算溢价率)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: 绩效指标字典
|
dict: 绩效指标字典
|
||||||
@@ -38,6 +46,7 @@ def generate_performance_report(
|
|||||||
os.makedirs(os.path.dirname(save_path) if os.path.dirname(save_path) else ".", exist_ok=True)
|
os.makedirs(os.path.dirname(save_path) if os.path.dirname(save_path) else ".", exist_ok=True)
|
||||||
|
|
||||||
code_name_map = code_name_map or {}
|
code_name_map = code_name_map or {}
|
||||||
|
code_config = code_config or {}
|
||||||
strategy_nav = backtest_result["轮动策略净值"]
|
strategy_nav = backtest_result["轮动策略净值"]
|
||||||
strategy_ret = backtest_result["轮动策略日收益率"]
|
strategy_ret = backtest_result["轮动策略日收益率"]
|
||||||
benchmark_nav = backtest_result["基准净值"]
|
benchmark_nav = backtest_result["基准净值"]
|
||||||
@@ -77,8 +86,30 @@ def generate_performance_report(
|
|||||||
print(f' {"最大回撤区间":<22} {str(s_dd_start.date()):>10} ~ {str(s_dd_end.date())}')
|
print(f' {"最大回撤区间":<22} {str(s_dd_start.date()):>10} ~ {str(s_dd_end.date())}')
|
||||||
print("=" * 70)
|
print("=" * 70)
|
||||||
|
|
||||||
|
# 计算溢价率(需要ETF价格和ETF净值)
|
||||||
|
# 溢价率 = (ETF价格 - ETF净值) / ETF净值
|
||||||
|
# 注意:信号是基于前一日数据计算的,所以使用 signal_date - 1 的数据
|
||||||
|
etf_nav_data = {}
|
||||||
|
premium_data = {}
|
||||||
|
|
||||||
|
if etf_price_data is not None and etf_nav_data_raw is not None:
|
||||||
|
# 使用信号前一天的数据(因为信号是基于前一天收盘数据计算的)
|
||||||
|
signal_date = backtest_result.index[-1]
|
||||||
|
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||||
|
|
||||||
|
# 确保数据基准日期在数据范围内
|
||||||
|
if data_base_date in etf_price_data.index and data_base_date in etf_nav_data_raw.index:
|
||||||
|
for code in code_list:
|
||||||
|
if code in etf_price_data.columns and code in etf_nav_data_raw.columns:
|
||||||
|
etf_price = etf_price_data.loc[data_base_date, code]
|
||||||
|
etf_nav = etf_nav_data_raw.loc[data_base_date, code]
|
||||||
|
if pd.notna(etf_price) and pd.notna(etf_nav) and etf_nav > 0:
|
||||||
|
premium = (etf_price - etf_nav) / etf_nav
|
||||||
|
premium_data[code] = premium
|
||||||
|
etf_nav_data[code] = etf_nav
|
||||||
|
|
||||||
# 打印最新调仓信号
|
# 打印最新调仓信号
|
||||||
_print_latest_signal(backtest_result, code_list, code_name_map, select_num)
|
_print_latest_signal(backtest_result, code_list, code_name_map, select_num, code_config, etf_nav_data, premium_data)
|
||||||
|
|
||||||
# 绘制图表
|
# 绘制图表
|
||||||
_plot_report_chart(
|
_plot_report_chart(
|
||||||
@@ -91,7 +122,10 @@ def generate_performance_report(
|
|||||||
"最大回撤": s_max_dd,
|
"最大回撤": s_max_dd,
|
||||||
"Calmar比率": s_calmar,
|
"Calmar比率": s_calmar,
|
||||||
"日胜率": s_win_rate,
|
"日胜率": s_win_rate,
|
||||||
}
|
},
|
||||||
|
code_config=code_config,
|
||||||
|
etf_price_data=etf_price_data,
|
||||||
|
etf_nav_data_raw=etf_nav_data_raw,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 返回指标字典
|
# 返回指标字典
|
||||||
@@ -110,21 +144,30 @@ def generate_performance_report(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int):
|
def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int, code_config: dict = None, etf_nav_data: dict = None, premium_data: dict = None):
|
||||||
"""打印最新调仓信号"""
|
"""打印最新调仓信号(支持ETF映射、ETF净值和溢价率显示)"""
|
||||||
|
code_config = code_config or {}
|
||||||
|
etf_nav_data = etf_nav_data or {}
|
||||||
|
premium_data = premium_data or {}
|
||||||
latest = _extract_latest_positions(backtest_result, code_list, code_name_map, select_num)
|
latest = _extract_latest_positions(backtest_result, code_list, code_name_map, select_num)
|
||||||
signal_date_str = latest["signal_date"].strftime("%Y-%m-%d")
|
signal_date = latest["signal_date"]
|
||||||
|
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
# 数据基准日期(信号是基于前一日数据计算的)
|
||||||
|
# 根据跨市场ETF映射方案:T+1日09:00计算的信号基于T日数据
|
||||||
|
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||||
|
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
print("\n")
|
print("\n")
|
||||||
print("=" * 100)
|
print("=" * 135)
|
||||||
print(" 最新调仓信号 (下一交易日执行)")
|
print(" 最新调仓信号 (下一交易日执行)")
|
||||||
print("=" * 100)
|
print("=" * 135)
|
||||||
print(f" 数据截止: {signal_date_str}")
|
print(f" 信号日期: {signal_date_str} (基于 {data_base_date_str} 收盘数据)")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# 表头
|
# 表头 - 添加ETF净值和溢价率列
|
||||||
print(f' {"标的名称":<8} {"代码":>10} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"进场价":>10} {"最新价":>10} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
|
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF净值":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
|
||||||
print(" " + "-" * 115)
|
print(" " + "-" * 155)
|
||||||
|
|
||||||
# 下期持仓(调入/维持)
|
# 下期持仓(调入/维持)
|
||||||
for pos in latest["positions"]:
|
for pos in latest["positions"]:
|
||||||
@@ -134,8 +177,37 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
|||||||
entry_date_str = pos["entry_date"].strftime("%Y-%m-%d") if pos.get("entry_date") else ' —'
|
entry_date_str = pos["entry_date"].strftime("%Y-%m-%d") if pos.get("entry_date") else ' —'
|
||||||
score_str = f'{pos["score"]:>8.2f}' if pos["score"] is not None else ' —'
|
score_str = f'{pos["score"]:>8.2f}' if pos["score"] is not None else ' —'
|
||||||
flag = '▲' if pos["action"] == "调入" else ' '
|
flag = '▲' if pos["action"] == "调入" else ' '
|
||||||
|
|
||||||
|
# 获取ETF代码、ETF净值和溢价率
|
||||||
|
idx_code = pos["code"]
|
||||||
|
cfg = code_config.get(idx_code, {})
|
||||||
|
etf_code = cfg.get('etf', '—')
|
||||||
|
market = cfg.get('market', 'A')
|
||||||
|
if etf_code is None:
|
||||||
|
etf_code = '直接交易'
|
||||||
|
|
||||||
|
# 获取ETF净值和溢价率
|
||||||
|
if market == 'CRYPTO':
|
||||||
|
etf_nav_str = ' —'
|
||||||
|
premium_str = ' —'
|
||||||
|
else:
|
||||||
|
# ETF净值
|
||||||
|
etf_nav = etf_nav_data.get(idx_code)
|
||||||
|
if etf_nav is not None:
|
||||||
|
etf_nav_str = f'{etf_nav:>10.3f}'
|
||||||
|
else:
|
||||||
|
etf_nav_str = ' —'
|
||||||
|
|
||||||
|
# 溢价率
|
||||||
|
premium = premium_data.get(idx_code)
|
||||||
|
if premium is not None:
|
||||||
|
# 高溢价警告标记
|
||||||
|
warning = '⚠️' if premium > 0.02 else ''
|
||||||
|
premium_str = f'{premium:>+7.2%}{warning}'
|
||||||
|
else:
|
||||||
|
premium_str = ' —'
|
||||||
|
|
||||||
print(f' {pos["name"]:<8} {pos["code"]:>10} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {flag}{pos["action"]:>4} {days_str} {pnl_str}')
|
print(f' {pos["name"]:<10} {idx_code:>12} {etf_code:>12} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {etf_nav_str} {premium_str} {flag}{pos["action"]:>4} {days_str} {pnl_str}')
|
||||||
|
|
||||||
# 需调出的品种
|
# 需调出的品种
|
||||||
if latest["exit_positions"]:
|
if latest["exit_positions"]:
|
||||||
@@ -147,10 +219,38 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
|||||||
entry_str = f'{pos["entry_price"]:>10.2f}' if pos["entry_price"] is not None else ' —'
|
entry_str = f'{pos["entry_price"]:>10.2f}' if pos["entry_price"] is not None else ' —'
|
||||||
entry_date_str = pos["entry_date"].strftime("%Y-%m-%d") if pos.get("entry_date") else ' —'
|
entry_date_str = pos["entry_date"].strftime("%Y-%m-%d") if pos.get("entry_date") else ' —'
|
||||||
score_str = ' —' # 调出品种无得分
|
score_str = ' —' # 调出品种无得分
|
||||||
|
|
||||||
|
# 获取ETF代码、ETF净值和溢价率
|
||||||
|
idx_code = pos["code"]
|
||||||
|
cfg = code_config.get(idx_code, {})
|
||||||
|
etf_code = cfg.get('etf', '—')
|
||||||
|
market = cfg.get('market', 'A')
|
||||||
|
if etf_code is None:
|
||||||
|
etf_code = '直接交易'
|
||||||
|
|
||||||
|
# 获取ETF净值和溢价率
|
||||||
|
if market == 'CRYPTO':
|
||||||
|
etf_nav_str = ' —'
|
||||||
|
premium_str = ' —'
|
||||||
|
else:
|
||||||
|
# ETF净值
|
||||||
|
etf_nav = etf_nav_data.get(idx_code)
|
||||||
|
if etf_nav is not None:
|
||||||
|
etf_nav_str = f'{etf_nav:>10.3f}'
|
||||||
|
else:
|
||||||
|
etf_nav_str = ' —'
|
||||||
|
|
||||||
|
# 溢价率
|
||||||
|
premium = premium_data.get(idx_code)
|
||||||
|
if premium is not None:
|
||||||
|
warning = '⚠️' if premium > 0.02 else ''
|
||||||
|
premium_str = f'{premium:>+7.2%}{warning}'
|
||||||
|
else:
|
||||||
|
premium_str = ' —'
|
||||||
|
|
||||||
print(f' {pos["name"]:<8} {pos["code"]:>10} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} ▼调出 {days_str} {pnl_str}')
|
print(f' {pos["name"]:<10} {idx_code:>12} {etf_code:>12} {pos["weight"]:>6.0%} {score_str} {entry_date_str:>12} {entry_str} {pos["current_price"]:>10.2f} {etf_nav_str} {premium_str} ▼调出 {days_str} {pnl_str}')
|
||||||
|
|
||||||
print("=" * 120)
|
print("=" * 160)
|
||||||
|
|
||||||
|
|
||||||
def _extract_latest_positions(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int) -> dict:
|
def _extract_latest_positions(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int) -> dict:
|
||||||
@@ -290,8 +390,11 @@ def _plot_report_chart(
|
|||||||
save_path: str,
|
save_path: str,
|
||||||
select_num: int,
|
select_num: int,
|
||||||
metrics: dict = None,
|
metrics: dict = None,
|
||||||
|
code_config: dict = None,
|
||||||
|
etf_price_data: pd.DataFrame = None,
|
||||||
|
etf_nav_data_raw: pd.DataFrame = None,
|
||||||
):
|
):
|
||||||
"""绘制报告图表"""
|
"""绘制报告图表(支持ETF净值和溢价率显示)"""
|
||||||
# 设置中文字体(macOS: Arial Unicode MS, Linux: WenQuanYi Zen Hei)
|
# 设置中文字体(macOS: Arial Unicode MS, Linux: WenQuanYi Zen Hei)
|
||||||
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "WenQuanYi Zen Hei", "DejaVu Sans"]
|
plt.rcParams["font.sans-serif"] = ["Arial Unicode MS", "WenQuanYi Zen Hei", "DejaVu Sans"]
|
||||||
plt.rcParams["axes.unicode_minus"] = False
|
plt.rcParams["axes.unicode_minus"] = False
|
||||||
@@ -321,6 +424,27 @@ def _plot_report_chart(
|
|||||||
|
|
||||||
# 提取最新调仓信息
|
# 提取最新调仓信息
|
||||||
latest = _extract_latest_positions(backtest_result, code_list, code_name_map, select_num)
|
latest = _extract_latest_positions(backtest_result, code_list, code_name_map, select_num)
|
||||||
|
|
||||||
|
# 准备配置数据
|
||||||
|
code_config = code_config or {}
|
||||||
|
signal_date = backtest_result.index[-1]
|
||||||
|
# 数据基准日期(信号是基于前一日数据计算的)
|
||||||
|
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||||
|
|
||||||
|
# 计算ETF净值和溢价率(使用数据基准日期的数据)
|
||||||
|
etf_nav_dict = {}
|
||||||
|
premium_dict = {}
|
||||||
|
if etf_price_data is not None and etf_nav_data_raw is not None:
|
||||||
|
# 确保数据基准日期在数据范围内
|
||||||
|
if data_base_date in etf_price_data.index and data_base_date in etf_nav_data_raw.index:
|
||||||
|
for code in code_list:
|
||||||
|
if code in etf_price_data.columns and code in etf_nav_data_raw.columns:
|
||||||
|
etf_price = etf_price_data.loc[data_base_date, code]
|
||||||
|
etf_nav = etf_nav_data_raw.loc[data_base_date, code]
|
||||||
|
if pd.notna(etf_price) and pd.notna(etf_nav) and etf_nav > 0:
|
||||||
|
premium = (etf_price - etf_nav) / etf_nav
|
||||||
|
etf_nav_dict[code] = etf_nav
|
||||||
|
premium_dict[code] = premium
|
||||||
|
|
||||||
# 计算表格行数
|
# 计算表格行数
|
||||||
n_table_rows = len(latest["positions"]) + len(latest["exit_positions"])
|
n_table_rows = len(latest["positions"]) + len(latest["exit_positions"])
|
||||||
@@ -334,12 +458,16 @@ def _plot_report_chart(
|
|||||||
ax0 = fig.add_subplot(gs[0])
|
ax0 = fig.add_subplot(gs[0])
|
||||||
ax0.axis("off")
|
ax0.axis("off")
|
||||||
|
|
||||||
signal_date_str = latest["signal_date"].strftime("%Y-%m-%d")
|
signal_date = latest["signal_date"]
|
||||||
ax0.set_title(f"最新调仓信号 (数据截止: {signal_date_str},下一交易日执行)", fontsize=14, fontweight="bold", loc="left", pad=15)
|
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
||||||
|
# 数据基准日期(信号是基于前一日数据计算的)
|
||||||
|
data_base_date = signal_date - pd.Timedelta(days=1)
|
||||||
|
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
||||||
|
ax0.set_title(f"最新调仓信号 (信号日期: {signal_date_str},基于 {data_base_date_str} 数据,下一交易日执行)", fontsize=14, fontweight="bold", loc="left", pad=15)
|
||||||
|
|
||||||
# 构建表格数据
|
# 构建表格数据(添加ETF净值和溢价率列)
|
||||||
table_data = []
|
table_data = []
|
||||||
col_labels = ["标的名称", "代码", "仓位", "得分", "进场日期", "进场价", "最新价", "操作", "持有天数", "盈亏"]
|
col_labels = ["标的名称", "指数代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF净值", "溢价率", "操作", "持有天数", "盈亏"]
|
||||||
|
|
||||||
# 下期持仓(调入/维持)
|
# 下期持仓(调入/维持)
|
||||||
for pos in latest["positions"]:
|
for pos in latest["positions"]:
|
||||||
@@ -348,11 +476,29 @@ def _plot_report_chart(
|
|||||||
entry_str = f'{pos["entry_price"]:.2f}' if pos["entry_price"] is not None else "—"
|
entry_str = f'{pos["entry_price"]:.2f}' if pos["entry_price"] is not None else "—"
|
||||||
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
||||||
score_str = f'{pos["score"]:.2f}' if pos["score"] is not None else "—"
|
score_str = f'{pos["score"]:.2f}' if pos["score"] is not None else "—"
|
||||||
|
|
||||||
|
# 获取ETF净值和溢价率
|
||||||
|
idx_code = pos["code"]
|
||||||
|
cfg = code_config.get(idx_code, {})
|
||||||
|
market = cfg.get('market', 'A')
|
||||||
|
|
||||||
|
if market == 'CRYPTO':
|
||||||
|
etf_nav_str = "—"
|
||||||
|
premium_str = "—"
|
||||||
|
else:
|
||||||
|
etf_nav = etf_nav_dict.get(idx_code)
|
||||||
|
premium = premium_dict.get(idx_code)
|
||||||
|
etf_nav_str = f"{etf_nav:.3f}" if etf_nav is not None else "—"
|
||||||
|
if premium is not None:
|
||||||
|
warning = "⚠️" if premium > 0.02 else ""
|
||||||
|
premium_str = f"{premium:+.2%}{warning}"
|
||||||
|
else:
|
||||||
|
premium_str = "—"
|
||||||
|
|
||||||
table_data.append([
|
table_data.append([
|
||||||
pos["name"], pos["code"], f'{pos["weight"]:.0%}',
|
pos["name"], pos["code"], f'{pos["weight"]:.0%}',
|
||||||
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
|
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
|
||||||
pos["action"], days_str, pnl_str
|
etf_nav_str, premium_str, pos["action"], days_str, pnl_str
|
||||||
])
|
])
|
||||||
|
|
||||||
# 需调出的品种
|
# 需调出的品种
|
||||||
@@ -362,11 +508,29 @@ def _plot_report_chart(
|
|||||||
entry_str = f'{pos["entry_price"]:.2f}' if pos["entry_price"] is not None else "—"
|
entry_str = f'{pos["entry_price"]:.2f}' if pos["entry_price"] is not None else "—"
|
||||||
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
||||||
score_str = "—" # 调出品种无得分
|
score_str = "—" # 调出品种无得分
|
||||||
|
|
||||||
|
# 获取ETF净值和溢价率
|
||||||
|
idx_code = pos["code"]
|
||||||
|
cfg = code_config.get(idx_code, {})
|
||||||
|
market = cfg.get('market', 'A')
|
||||||
|
|
||||||
|
if market == 'CRYPTO':
|
||||||
|
etf_nav_str = "—"
|
||||||
|
premium_str = "—"
|
||||||
|
else:
|
||||||
|
etf_nav = etf_nav_dict.get(idx_code)
|
||||||
|
premium = premium_dict.get(idx_code)
|
||||||
|
etf_nav_str = f"{etf_nav:.3f}" if etf_nav is not None else "—"
|
||||||
|
if premium is not None:
|
||||||
|
warning = "⚠️" if premium > 0.02 else ""
|
||||||
|
premium_str = f"{premium:+.2%}{warning}"
|
||||||
|
else:
|
||||||
|
premium_str = "—"
|
||||||
|
|
||||||
table_data.append([
|
table_data.append([
|
||||||
pos["name"], pos["code"], f'{pos["weight"]:.0%}',
|
pos["name"], pos["code"], f'{pos["weight"]:.0%}',
|
||||||
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
|
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
|
||||||
"调出", days_str, pnl_str
|
etf_nav_str, premium_str, "调出", days_str, pnl_str
|
||||||
])
|
])
|
||||||
|
|
||||||
if table_data:
|
if table_data:
|
||||||
@@ -375,7 +539,7 @@ def _plot_report_chart(
|
|||||||
colLabels=col_labels,
|
colLabels=col_labels,
|
||||||
loc="center",
|
loc="center",
|
||||||
cellLoc="center",
|
cellLoc="center",
|
||||||
colWidths=[0.10, 0.10, 0.07, 0.08, 0.08, 0.08, 0.08, 0.07, 0.08, 0.08],
|
colWidths=[0.09, 0.09, 0.06, 0.07, 0.07, 0.07, 0.07, 0.07, 0.08, 0.06, 0.07, 0.07],
|
||||||
bbox=[0, 0, 1, 1], # 使用完整宽度
|
bbox=[0, 0, 1, 1], # 使用完整宽度
|
||||||
)
|
)
|
||||||
table.auto_set_font_size(False)
|
table.auto_set_font_size(False)
|
||||||
|
|||||||
Reference in New Issue
Block a user