Compare commits

..

3 Commits

Author SHA1 Message Date
b7bf8c1eb4 fix(report): 修正溢价率计算逻辑为使用ETF收盘价替代净值
- 修改生成性能报告时溢价率计算逻辑,改用信号日期的ETF收盘价
- 溢价率仅在当天净值数据存在时计算,避免使用前一日数据
- 更新打印最新调仓信号函数,支持显示ETF收盘价而非净值
- 修改报告图表部分,显示ETF收盘价和对应溢价率
- 优化时间基准日期计算,使用信号日期或前一交易日作为数据基准
- 保持对跨市场ETF映射的兼容性和显示一致性
2026-03-26 01:27:04 +08:00
5f4470d53e fix(datasource): 修正数据日期对齐与复权问题
- 修改yfinance获取历史数据时end_date加一天,auto_adjust设置为False,以获取不复权价格
- 调整ETF净值数据获取时end_date加一天,解决净值数据滞后问题
- 数据对齐策略改为以A股最新数据日期为基准,调整交易日历范围
- 移除对非A股数据的前向/后向填充,保持原始价格数据不填充
- ETF净值数据重新索引到A股交易日但不做缺失值填充,保持NaN以表示无数据
- 增加打印输出辅助调试数据日期及交易日信息
2026-03-26 01:26:43 +08:00
ec9c808e6c refactor(momentum): 优化因子计算流程并对齐A股交易日历
- 添加辅助函数判断是否为A股指数
- 调整compute_factors函数结构,分别计算每个标的技术指标
- 严格实现T+1规则,确保信号只用T日及以前数据
- 对齐所有数据到A股交易日历,使用前向填充避免未来数据泄漏
- 增加有效代码有效性检查,剔除数据不足或缺失率过高的标的
- 完善函数注释,明确输入输出及核心逻辑说明
- 优化打印信息,清晰展示因子类型、窗口、有效标的及时间范围
2026-03-26 01:26:14 +08:00
3 changed files with 182 additions and 125 deletions

View File

@@ -338,7 +338,11 @@ class HybridDataSource:
try:
ticker = yf.Ticker(yf_code)
data = ticker.history(start=start_date, end=end_date)
# auto_adjust=False 获取不复权价格,与网页显示一致
# end_date 需要加一天,因为 yfinance 的 end 是排他的
from datetime import timedelta
end_date_obj = pd.Timestamp(end_date) + timedelta(days=1)
data = ticker.history(start=start_date, end=end_date_obj.strftime('%Y-%m-%d'), auto_adjust=False)
if len(data) == 0:
return None
@@ -585,8 +589,10 @@ class HybridDataSource:
# 获取ETF价格数据
price_data = self._fetch_etf(etf_code, start_date, end_date)
# 获取ETF净值数据
nav_data = self._fetch_etf_nav(etf_code, start_date, end_date)
# 获取ETF净值数据净值通常滞后一天所以end_date+1
from datetime import timedelta
nav_end_date = (pd.Timestamp(end_date) + timedelta(days=1)).strftime('%Y-%m-%d')
nav_data = self._fetch_etf_nav(etf_code, start_date, nav_end_date)
if price_data is not None and len(price_data) > 0:
# 使用指数代码作为列名,保持与指数数据一致
@@ -633,42 +639,38 @@ class HybridDataSource:
aggfunc='first'
)
# 以A股交易日为基准对齐所有数据
# 强制从 Tushare 获取A股交易日历不管配置中是否有A股指数
start_str = index_data.index.min().strftime('%Y%m%d')
end_str = index_data.index.max().strftime('%Y%m%d')
# 数据对齐策略:使用各标的能获取到的最新数据
# 以A股最新数据日期为基准其他市场数据对齐到该日期
# 获取A股最新数据日期
china_codes = [c for c in valid_codes if self._is_china_index(c)]
if china_codes:
a_share_latest = index_data[china_codes].dropna().index.max()
else:
# 如果没有A股使用所有数据的最早最新日期
a_share_latest = index_data.dropna().index.max()
print(f" A股最新数据日期: {a_share_latest.strftime('%Y-%m-%d')}")
# 获取A股交易日历从start_date到a_share_latest
start_str = pd.Timestamp(start_date).strftime('%Y%m%d')
end_str = a_share_latest.strftime('%Y%m%d')
import tushare as ts
pro = ts.pro_api(self._get_tushare_token())
trade_cal = pro.trade_cal(start_date=start_str, end_date=end_str, is_open='1')
a_share_dates = pd.to_datetime(trade_cal['cal_date']).sort_values()
print(f" A股交易日历: {len(a_share_dates)} 天 ({start_str} ~ {end_str})")
# 重新索引到A股交易日
# 重新索引到A股交易日只到A股最新数据日期
index_data = index_data.reindex(a_share_dates)
# 对非A股指数进行数据对齐
# 港股、美股T日收盘T+1日09:00前使用T日数据前向填充
# 加密货币、期货含夜盘T+1日09:00前使用T+1日数据后向填充
# - 加密货币UTC 00:00收盘北京时间08:00
# - 期货AU.SHF夜盘02:30收盘数据标记为T+1日
# 策略:价格数据保持原始值,不做填充
# 指标计算后会和价格作为一个整体进行向前填充
non_a_codes = [c for c in valid_codes if not self._is_china_index(c)]
yf_codes = [c for c in non_a_codes if not self._is_crypto(c) and not self._is_futures(c)]
crypto_futures_codes = [c for c in non_a_codes if self._is_crypto(c) or self._is_futures(c)]
# 港股/美股前向填充使用T日数据
for code in yf_codes:
if code in index_data.columns:
index_data[code] = index_data[code].ffill().bfill()
# 加密货币/期货后向填充使用T+1日数据
for code in crypto_futures_codes:
if code in index_data.columns:
index_data[code] = index_data[code].bfill().ffill()
if non_a_codes:
print(f" 非A股标的: {len(non_a_codes)} 只 (港股/美股:ffill, 加密货币/期货:bfill)")
print(f" 非A股标的: {len(non_a_codes)} 只 (价格保持原始值)")
print(f" 时间范围: {index_data.index[0]} ~ {index_data.index[-1]}")
print(f" 交易日数: {len(index_data)}")
@@ -717,9 +719,9 @@ class HybridDataSource:
aggfunc='first'
)
# 对齐到A股交易日并前向填充缺失值(净值数据通常T+1更新
# 对齐到A股交易日但不填充缺失值(保持原始数据,让报告层决定是否显示溢价率
etf_nav_data = etf_nav_data.reindex(a_share_dates)
etf_nav_data = etf_nav_data.ffill() # 前向填充缺失的净值数据
# 注意不做ffill填充保持NaN表示当天无净值数据
print(f" ETF净值数据: {len(etf_nav_data.columns)}")

View File

@@ -79,6 +79,11 @@ def calculate_daily_return(price_series: pd.Series) -> pd.Series:
return price_series / price_series.shift(1) - 1
def _is_china_index(code: str) -> bool:
"""判断是否为A股指数"""
return code.endswith('.SH') or code.endswith('.SZ') or code.endswith('.SS')
def compute_factors(
index_data: pd.DataFrame,
code_list: list,
@@ -88,19 +93,24 @@ def compute_factors(
code_config: dict = None,
) -> tuple[pd.DataFrame, list]:
"""
计算所有指数的因子和日收益率(支持指数-ETF双轨数据
计算所有指数的因子和日收益率(横截面策略版本
核心逻辑:
1. 每个标的按照自己的交易日历计算技术指标
2. 对齐到A股交易日历取离A股交易日最近的有效数据不使用未来数据
3. 严格控制T+1规则T日收盘计算信号使用T日及之前的数据
Args:
index_data: 指数价格数据(宽格式,用于因子计算
index_data: 指数价格数据(宽格式,已对齐到A股交易日历非A股可能有NaN
code_list: 指数代码列表
n: 动量/趋势窗口
factor_type: 'momentum''slope_r2'
etf_data: ETF价格数据宽格式用于收益计算
code_config: 代码配置字典 {code: {name, etf, market}},用于判断是否为加密货币
code_config: 代码配置字典 {code: {name, etf, market}}
Returns:
tuple: (result_df, valid_codes)
- result_df: 包含因子得分和日收益率的DataFrame
- result_df: 包含因子得分和日收益率的DataFrame按A股交易日对齐
- valid_codes: 有效代码列表
"""
code_config = code_config or {}
@@ -109,45 +119,68 @@ def compute_factors(
if etf_data is None:
etf_data = pd.DataFrame()
result = index_data.copy()
# 获取A股交易日历index_data的索引
a_share_dates = index_data.index
# 过滤掉缺失值过多的指数
total_rows = len(result)
# 过滤有效代码
valid_codes = []
for code in code_list:
if code not in result.columns:
if code not in index_data.columns:
print(f" ⚠ 跳过 {code}: 不在数据中")
continue
null_pct = result[code].isnull().sum() / total_rows
if null_pct > 0.2:
print(f" ⚠ 剔除 {code}: 缺失率 {null_pct:.1%} 过高")
result = result.drop(columns=[code])
else:
valid_codes.append(code)
valid_codes.append(code)
# 为每个标的单独计算指标然后对齐到A股交易日历
result = pd.DataFrame(index=a_share_dates)
# 对有效指数计算因子和收益率
for code in valid_codes:
# 因子基于指数价格计算
# 获取该标的的原始价格数据去除NaN
price_series = index_data[code].dropna()
if len(price_series) < n + 1:
print(f" ⚠ 剔除 {code}: 数据不足 ({len(price_series)} < {n+1})")
valid_codes.remove(code)
continue
# 按照该标的自己的交易日历计算指标
if factor_type == "momentum":
result[f"得分_{code}"] = calculate_momentum(result[code], n)
factor_series = calculate_momentum(price_series, n)
elif factor_type == "slope_r2":
result[f"得分_{code}"] = calculate_slope_r2(result[code], n)
factor_series = calculate_slope_r2(price_series, n)
else:
raise ValueError(f"不支持的因子类型: {factor_type}")
# 日收益率基于指数价格计算(回测使用指数价格)
result[f"日收益率_{code}"] = calculate_daily_return(result[code])
# 计算日收益率
return_series = calculate_daily_return(price_series)
# 按得分列做 dropna
score_cols = [f"得分_{code}" for code in valid_codes]
# 对齐到A股交易日历取离A股交易日最近的有效数据不使用未来数据
# 使用reindex + method='ffill'确保T日使用T日或之前的数据
result[code] = price_series.reindex(a_share_dates, method='ffill')
result[f"得分_{code}"] = factor_series.reindex(a_share_dates, method='ffill')
result[f"日收益率_{code}"] = return_series.reindex(a_share_dates, method='ffill')
# 过滤掉缺失值过多的指数基于A股交易日历
total_rows = len(result)
final_valid_codes = []
for code in valid_codes:
null_pct = result[code].isnull().sum() / total_rows
if null_pct > 0.2:
print(f" ⚠ 剔除 {code}: 对齐后缺失率 {null_pct:.1%} 过高")
result = result.drop(columns=[code, f"得分_{code}", f"日收益率_{code}"], errors='ignore')
else:
final_valid_codes.append(code)
# 按得分列做 dropna确保所有标的同时有数据
score_cols = [f"得分_{code}" for code in final_valid_codes]
result = result.dropna(subset=score_cols)
print("\n因子计算完成:")
print(f" 因子类型: {factor_type}")
print(f" 窗口天数: {n}")
print(f" 有效指数: {len(valid_codes)}/{len(code_list)}")
print(f" 有效指数: {len(final_valid_codes)}/{len(code_list)}")
print(f" 有效数据: {len(result)}")
if etf_data is not index_data:
print(f" 时间范围: {result.index[0].date()} ~ {result.index[-1].date()}")
if etf_data is not index_data and not etf_data.empty:
print(f" 使用ETF数据计算收益: ✓")
return result, valid_codes
return result, final_valid_codes

View File

@@ -88,28 +88,33 @@ def generate_performance_report(
# 计算溢价率需要ETF价格和ETF净值
# 溢价率 = (ETF价格 - ETF净值) / ETF净值
# 注意:信号是基于前一日数据计算的,所以使用 signal_date - 1 的数据
etf_nav_data = {}
premium_data = {}
# 使用信号日期的ETF收盘价但只有当天有净值数据时才计算溢价率
etf_close_data = {} # ETF收盘价
premium_data = {} # 溢价率(仅当当天有净值时计算)
if etf_price_data is not None and etf_nav_data_raw is not None:
# 使用信号前一天的数据(因为信号是基于前一天收盘数据计算的)
if etf_price_data 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:
# 获取信号日期的ETF收盘价
if signal_date in etf_price_data.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
if code in etf_price_data.columns:
etf_close = etf_price_data.loc[signal_date, code]
if pd.notna(etf_close):
etf_close_data[code] = etf_close
# 计算溢价率:只有当天有净值数据时才计算
if etf_nav_data_raw is not None and signal_date in etf_nav_data_raw.index:
for code in code_list:
if code in etf_close_data and code in etf_nav_data_raw.columns:
etf_nav = etf_nav_data_raw.loc[signal_date, code]
if pd.notna(etf_nav) and etf_nav > 0:
etf_close = etf_close_data[code]
premium = (etf_close - 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, code_config, etf_nav_data, premium_data)
_print_latest_signal(backtest_result, code_list, code_name_map, select_num, code_config, etf_close_data, premium_data)
# 绘制图表
_plot_report_chart(
@@ -144,18 +149,21 @@ def generate_performance_report(
}
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净值和溢价率显示)"""
def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_name_map: dict, select_num: int, code_config: dict = None, etf_close_data: dict = None, premium_data: dict = None):
"""打印最新调仓信号支持ETF映射、ETF收盘价和溢价率显示)"""
code_config = code_config or {}
etf_nav_data = etf_nav_data or {}
etf_close_data = etf_close_data or {}
premium_data = premium_data or {}
latest = _extract_latest_positions(backtest_result, code_list, code_name_map, select_num)
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)
# 数据基准日期:使用信号日期的数据
# 如果信号日期没有数据,则使用前一天
if signal_date in backtest_result.index:
data_base_date = signal_date
else:
data_base_date = signal_date - pd.Timedelta(days=1)
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
print("\n")
@@ -165,8 +173,8 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
print(f" 信号日期: {signal_date_str} (基于 {data_base_date_str} 收盘数据)")
print()
# 表头 - 添加ETF净值和溢价率列
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF净值":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
# 表头 - 添加ETF收盘价和溢价率列
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF收盘价":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
print(" " + "-" * 155)
# 下期持仓(调入/维持)
@@ -186,19 +194,19 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
if etf_code is None:
etf_code = '直接交易'
# 获取ETF净值和溢价率
# 获取ETF收盘价和溢价率
if market == 'CRYPTO':
etf_nav_str = ''
etf_close_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}'
# ETF收盘价
etf_close = etf_close_data.get(idx_code)
if etf_close is not None:
etf_close_str = f'{etf_close:>10.3f}'
else:
etf_nav_str = ''
etf_close_str = ''
# 溢价率
# 溢价率(只有当天有净值数据时才显示)
premium = premium_data.get(idx_code)
if premium is not None:
# 高溢价警告标记
@@ -207,7 +215,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
else:
premium_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}')
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_close_str} {premium_str} {flag}{pos["action"]:>4} {days_str} {pnl_str}')
# 需调出的品种
if latest["exit_positions"]:
@@ -220,7 +228,7 @@ 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 ''
score_str = '' # 调出品种无得分
# 获取ETF代码、ETF净值和溢价率
# 获取ETF代码、ETF收盘价和溢价率
idx_code = pos["code"]
cfg = code_config.get(idx_code, {})
etf_code = cfg.get('etf', '')
@@ -228,19 +236,19 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
if etf_code is None:
etf_code = '直接交易'
# 获取ETF净值和溢价率
# 获取ETF收盘价和溢价率
if market == 'CRYPTO':
etf_nav_str = ''
etf_close_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}'
# ETF收盘价
etf_close = etf_close_data.get(idx_code)
if etf_close is not None:
etf_close_str = f'{etf_close:>10.3f}'
else:
etf_nav_str = ''
etf_close_str = ''
# 溢价率
# 溢价率(只有当天有净值数据时才显示)
premium = premium_data.get(idx_code)
if premium is not None:
warning = '⚠️' if premium > 0.02 else ''
@@ -248,7 +256,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
else:
premium_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(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_close_str} {premium_str} ▼调出 {days_str} {pnl_str}')
print("=" * 160)
@@ -428,22 +436,33 @@ def _plot_report_chart(
# 准备配置数据
code_config = code_config or {}
signal_date = backtest_result.index[-1]
# 数据基准日期(信号是基于前一日数据计算的)
data_base_date = signal_date - pd.Timedelta(days=1)
# 数据基准日期:使用信号日期的数据,如果没有则使用前一天
if signal_date in backtest_result.index:
data_base_date = signal_date
else:
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:
# 计算ETF收盘价和溢价率(使用信号日期的数据)
etf_close_dict = {} # ETF收盘价
premium_dict = {} # 溢价率(仅当当天有净值时计算)
if etf_price_data is not None:
# 获取信号日期的ETF收盘价
if signal_date in etf_price_data.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
if code in etf_price_data.columns:
etf_close = etf_price_data.loc[signal_date, code]
if pd.notna(etf_close):
etf_close_dict[code] = etf_close
# 计算溢价率:只有当天有净值数据时才计算
if etf_nav_data_raw is not None and signal_date in etf_nav_data_raw.index:
for code in code_list:
if code in etf_close_dict and code in etf_nav_data_raw.columns:
etf_nav = etf_nav_data_raw.loc[signal_date, code]
if pd.notna(etf_nav) and etf_nav > 0:
etf_close = etf_close_dict[code]
premium = (etf_close - etf_nav) / etf_nav
premium_dict[code] = premium
# 计算表格行数
@@ -460,14 +479,17 @@ def _plot_report_chart(
signal_date = latest["signal_date"]
signal_date_str = signal_date.strftime("%Y-%m-%d")
# 数据基准日期(信号是基于前一日数据计算的)
data_base_date = signal_date - pd.Timedelta(days=1)
# 数据基准日期:使用信号日期的数据,如果没有则使用前一天
if signal_date in backtest_result.index:
data_base_date = signal_date
else:
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代码、ETF净值和溢价率列)
# 构建表格数据添加ETF代码、ETF收盘价和溢价率列)
table_data = []
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF净值", "溢价率", "操作", "持有天数", "盈亏"]
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF收盘价", "溢价率", "操作", "持有天数", "盈亏"]
# 下期持仓(调入/维持)
for pos in latest["positions"]:
@@ -477,7 +499,7 @@ def _plot_report_chart(
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 ""
# 获取ETF代码、ETF净值和溢价率
# 获取ETF代码、ETF收盘价和溢价率
idx_code = pos["code"]
cfg = code_config.get(idx_code, {})
market = cfg.get('market', 'A')
@@ -486,12 +508,12 @@ def _plot_report_chart(
etf_code = '直接交易'
if market == 'CRYPTO':
etf_nav_str = ""
etf_close_str = ""
premium_str = ""
else:
etf_nav = etf_nav_dict.get(idx_code)
etf_close = etf_close_dict.get(idx_code)
premium = premium_dict.get(idx_code)
etf_nav_str = f"{etf_nav:.3f}" if etf_nav is not None else ""
etf_close_str = f"{etf_close:.3f}" if etf_close is not None else ""
if premium is not None:
warning = "⚠️" if premium > 0.02 else ""
premium_str = f"{premium:+.2%}{warning}"
@@ -501,7 +523,7 @@ def _plot_report_chart(
table_data.append([
pos["name"], pos["code"], etf_code, f'{pos["weight"]:.0%}',
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
etf_nav_str, premium_str, pos["action"], days_str, pnl_str
etf_close_str, premium_str, pos["action"], days_str, pnl_str
])
# 需调出的品种
@@ -512,7 +534,7 @@ def _plot_report_chart(
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else ""
score_str = "" # 调出品种无得分
# 获取ETF代码、ETF净值和溢价率
# 获取ETF代码、ETF收盘价和溢价率
idx_code = pos["code"]
cfg = code_config.get(idx_code, {})
market = cfg.get('market', 'A')
@@ -521,12 +543,12 @@ def _plot_report_chart(
etf_code = '直接交易'
if market == 'CRYPTO':
etf_nav_str = ""
etf_close_str = ""
premium_str = ""
else:
etf_nav = etf_nav_dict.get(idx_code)
etf_close = etf_close_dict.get(idx_code)
premium = premium_dict.get(idx_code)
etf_nav_str = f"{etf_nav:.3f}" if etf_nav is not None else ""
etf_close_str = f"{etf_close:.3f}" if etf_close is not None else ""
if premium is not None:
warning = "⚠️" if premium > 0.02 else ""
premium_str = f"{premium:+.2%}{warning}"
@@ -536,7 +558,7 @@ def _plot_report_chart(
table_data.append([
pos["name"], pos["code"], etf_code, f'{pos["weight"]:.0%}',
score_str, entry_date_str, entry_str, f'{pos["current_price"]:.2f}',
etf_nav_str, premium_str, "调出", days_str, pnl_str
etf_close_str, premium_str, "调出", days_str, pnl_str
])
if table_data: