Compare commits
3 Commits
e4f87b7212
...
b7bf8c1eb4
| Author | SHA1 | Date | |
|---|---|---|---|
| b7bf8c1eb4 | |||
| 5f4470d53e | |||
| ec9c808e6c |
@@ -338,7 +338,11 @@ class HybridDataSource:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
ticker = yf.Ticker(yf_code)
|
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:
|
if len(data) == 0:
|
||||||
return None
|
return None
|
||||||
@@ -585,8 +589,10 @@ class HybridDataSource:
|
|||||||
|
|
||||||
# 获取ETF价格数据
|
# 获取ETF价格数据
|
||||||
price_data = self._fetch_etf(etf_code, start_date, end_date)
|
price_data = self._fetch_etf(etf_code, start_date, end_date)
|
||||||
# 获取ETF净值数据
|
# 获取ETF净值数据(净值通常滞后一天,所以end_date+1)
|
||||||
nav_data = self._fetch_etf_nav(etf_code, start_date, end_date)
|
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:
|
if price_data is not None and len(price_data) > 0:
|
||||||
# 使用指数代码作为列名,保持与指数数据一致
|
# 使用指数代码作为列名,保持与指数数据一致
|
||||||
@@ -633,42 +639,38 @@ class HybridDataSource:
|
|||||||
aggfunc='first'
|
aggfunc='first'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 以A股交易日为基准,对齐所有数据
|
# 数据对齐策略:使用各标的能获取到的最新数据
|
||||||
# 强制从 Tushare 获取A股交易日历(不管配置中是否有A股指数)
|
# 以A股最新数据日期为基准,其他市场数据对齐到该日期
|
||||||
start_str = index_data.index.min().strftime('%Y%m%d')
|
|
||||||
end_str = index_data.index.max().strftime('%Y%m%d')
|
# 获取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
|
import tushare as ts
|
||||||
pro = ts.pro_api(self._get_tushare_token())
|
pro = ts.pro_api(self._get_tushare_token())
|
||||||
trade_cal = pro.trade_cal(start_date=start_str, end_date=end_str, is_open='1')
|
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()
|
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)
|
index_data = index_data.reindex(a_share_dates)
|
||||||
|
|
||||||
# 对非A股指数进行数据对齐
|
# 对非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)]
|
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:
|
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" 时间范围: {index_data.index[0]} ~ {index_data.index[-1]}")
|
||||||
print(f" 交易日数: {len(index_data)}")
|
print(f" 交易日数: {len(index_data)}")
|
||||||
@@ -717,9 +719,9 @@ class HybridDataSource:
|
|||||||
aggfunc='first'
|
aggfunc='first'
|
||||||
)
|
)
|
||||||
|
|
||||||
# 对齐到A股交易日,并前向填充缺失值(净值数据通常T+1更新)
|
# 对齐到A股交易日,但不填充缺失值(保持原始数据,让报告层决定是否显示溢价率)
|
||||||
etf_nav_data = etf_nav_data.reindex(a_share_dates)
|
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)} 只")
|
print(f" ETF净值数据: {len(etf_nav_data.columns)} 只")
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,11 @@ def calculate_daily_return(price_series: pd.Series) -> pd.Series:
|
|||||||
return price_series / price_series.shift(1) - 1
|
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(
|
def compute_factors(
|
||||||
index_data: pd.DataFrame,
|
index_data: pd.DataFrame,
|
||||||
code_list: list,
|
code_list: list,
|
||||||
@@ -88,19 +93,24 @@ def compute_factors(
|
|||||||
code_config: dict = None,
|
code_config: dict = None,
|
||||||
) -> tuple[pd.DataFrame, list]:
|
) -> tuple[pd.DataFrame, list]:
|
||||||
"""
|
"""
|
||||||
计算所有指数的因子和日收益率(支持指数-ETF双轨数据)
|
计算所有指数的因子和日收益率(横截面策略版本)
|
||||||
|
|
||||||
|
核心逻辑:
|
||||||
|
1. 每个标的按照自己的交易日历计算技术指标
|
||||||
|
2. 对齐到A股交易日历(取离A股交易日最近的有效数据,不使用未来数据)
|
||||||
|
3. 严格控制T+1规则:T日收盘计算信号,使用T日及之前的数据
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
index_data: 指数价格数据(宽格式,用于因子计算)
|
index_data: 指数价格数据(宽格式,已对齐到A股交易日历,非A股可能有NaN)
|
||||||
code_list: 指数代码列表
|
code_list: 指数代码列表
|
||||||
n: 动量/趋势窗口
|
n: 动量/趋势窗口
|
||||||
factor_type: 'momentum' 或 'slope_r2'
|
factor_type: 'momentum' 或 'slope_r2'
|
||||||
etf_data: ETF价格数据(宽格式,用于收益计算)
|
etf_data: ETF价格数据(宽格式,用于收益计算)
|
||||||
code_config: 代码配置字典 {code: {name, etf, market}},用于判断是否为加密货币
|
code_config: 代码配置字典 {code: {name, etf, market}}
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tuple: (result_df, valid_codes)
|
tuple: (result_df, valid_codes)
|
||||||
- result_df: 包含因子得分和日收益率的DataFrame
|
- result_df: 包含因子得分和日收益率的DataFrame(按A股交易日对齐)
|
||||||
- valid_codes: 有效代码列表
|
- valid_codes: 有效代码列表
|
||||||
"""
|
"""
|
||||||
code_config = code_config or {}
|
code_config = code_config or {}
|
||||||
@@ -109,45 +119,68 @@ def compute_factors(
|
|||||||
if etf_data is None:
|
if etf_data is None:
|
||||||
etf_data = pd.DataFrame()
|
etf_data = pd.DataFrame()
|
||||||
|
|
||||||
result = index_data.copy()
|
# 获取A股交易日历(index_data的索引)
|
||||||
|
a_share_dates = index_data.index
|
||||||
|
|
||||||
# 过滤掉缺失值过多的指数
|
# 过滤有效代码
|
||||||
total_rows = len(result)
|
|
||||||
valid_codes = []
|
valid_codes = []
|
||||||
for code in code_list:
|
for code in code_list:
|
||||||
if code not in result.columns:
|
if code not in index_data.columns:
|
||||||
print(f" ⚠ 跳过 {code}: 不在数据中")
|
print(f" ⚠ 跳过 {code}: 不在数据中")
|
||||||
continue
|
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:
|
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":
|
if factor_type == "momentum":
|
||||||
result[f"得分_{code}"] = calculate_momentum(result[code], n)
|
factor_series = calculate_momentum(price_series, n)
|
||||||
elif factor_type == "slope_r2":
|
elif factor_type == "slope_r2":
|
||||||
result[f"得分_{code}"] = calculate_slope_r2(result[code], n)
|
factor_series = calculate_slope_r2(price_series, n)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"不支持的因子类型: {factor_type}")
|
raise ValueError(f"不支持的因子类型: {factor_type}")
|
||||||
|
|
||||||
# 日收益率基于指数价格计算(回测使用指数价格)
|
# 计算日收益率
|
||||||
result[f"日收益率_{code}"] = calculate_daily_return(result[code])
|
return_series = calculate_daily_return(price_series)
|
||||||
|
|
||||||
# 按得分列做 dropna
|
# 对齐到A股交易日历:取离A股交易日最近的有效数据(不使用未来数据)
|
||||||
score_cols = [f"得分_{code}" for code in valid_codes]
|
# 使用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)
|
result = result.dropna(subset=score_cols)
|
||||||
|
|
||||||
print("\n因子计算完成:")
|
print("\n因子计算完成:")
|
||||||
print(f" 因子类型: {factor_type}")
|
print(f" 因子类型: {factor_type}")
|
||||||
print(f" 窗口天数: {n}")
|
print(f" 窗口天数: {n}")
|
||||||
print(f" 有效指数: {len(valid_codes)}/{len(code_list)}")
|
print(f" 有效指数: {len(final_valid_codes)}/{len(code_list)}")
|
||||||
print(f" 有效数据: {len(result)} 行")
|
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数据计算收益: ✓")
|
print(f" 使用ETF数据计算收益: ✓")
|
||||||
|
|
||||||
return result, valid_codes
|
return result, final_valid_codes
|
||||||
|
|||||||
@@ -88,28 +88,33 @@ def generate_performance_report(
|
|||||||
|
|
||||||
# 计算溢价率(需要ETF价格和ETF净值)
|
# 计算溢价率(需要ETF价格和ETF净值)
|
||||||
# 溢价率 = (ETF价格 - ETF净值) / ETF净值
|
# 溢价率 = (ETF价格 - ETF净值) / ETF净值
|
||||||
# 注意:信号是基于前一日数据计算的,所以使用 signal_date - 1 的数据
|
# 使用信号日期的ETF收盘价,但只有当天有净值数据时才计算溢价率
|
||||||
etf_nav_data = {}
|
etf_close_data = {} # ETF收盘价
|
||||||
premium_data = {}
|
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]
|
signal_date = backtest_result.index[-1]
|
||||||
data_base_date = signal_date - pd.Timedelta(days=1)
|
|
||||||
|
|
||||||
# 确保数据基准日期在数据范围内
|
# 获取信号日期的ETF收盘价
|
||||||
if data_base_date in etf_price_data.index and data_base_date in etf_nav_data_raw.index:
|
if signal_date in etf_price_data.index:
|
||||||
for code in code_list:
|
for code in code_list:
|
||||||
if code in etf_price_data.columns and code in etf_nav_data_raw.columns:
|
if code in etf_price_data.columns:
|
||||||
etf_price = etf_price_data.loc[data_base_date, code]
|
etf_close = etf_price_data.loc[signal_date, code]
|
||||||
etf_nav = etf_nav_data_raw.loc[data_base_date, code]
|
if pd.notna(etf_close):
|
||||||
if pd.notna(etf_price) and pd.notna(etf_nav) and etf_nav > 0:
|
etf_close_data[code] = etf_close
|
||||||
premium = (etf_price - etf_nav) / etf_nav
|
|
||||||
|
# 计算溢价率:只有当天有净值数据时才计算
|
||||||
|
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
|
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(
|
_plot_report_chart(
|
||||||
@@ -144,17 +149,20 @@ 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):
|
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净值和溢价率显示)"""
|
"""打印最新调仓信号(支持ETF映射、ETF收盘价和溢价率显示)"""
|
||||||
code_config = code_config or {}
|
code_config = code_config or {}
|
||||||
etf_nav_data = etf_nav_data or {}
|
etf_close_data = etf_close_data or {}
|
||||||
premium_data = premium_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 = latest["signal_date"]
|
signal_date = latest["signal_date"]
|
||||||
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
# 数据基准日期(信号是基于前一日数据计算的)
|
# 数据基准日期:使用信号日期的数据
|
||||||
# 根据跨市场ETF映射方案:T+1日09:00计算的信号基于T日数据
|
# 如果信号日期没有数据,则使用前一天
|
||||||
|
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 = signal_date - pd.Timedelta(days=1)
|
||||||
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@@ -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(f" 信号日期: {signal_date_str} (基于 {data_base_date_str} 收盘数据)")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# 表头 - 添加ETF净值和溢价率列
|
# 表头 - 添加ETF收盘价和溢价率列
|
||||||
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF净值":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
|
print(f' {"标的名称":<10} {"指数代码":>12} {"ETF代码":>12} {"仓位":>6} {"得分":>8} {"进场日期":>12} {"指数进场价":>10} {"指数最新价":>10} {"ETF收盘价":>10} {"溢价率":>8} {"操作":>6} {"持有天数":>8} {"盈亏":>10}')
|
||||||
print(" " + "-" * 155)
|
print(" " + "-" * 155)
|
||||||
|
|
||||||
# 下期持仓(调入/维持)
|
# 下期持仓(调入/维持)
|
||||||
@@ -186,19 +194,19 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
|||||||
if etf_code is None:
|
if etf_code is None:
|
||||||
etf_code = '直接交易'
|
etf_code = '直接交易'
|
||||||
|
|
||||||
# 获取ETF净值和溢价率
|
# 获取ETF收盘价和溢价率
|
||||||
if market == 'CRYPTO':
|
if market == 'CRYPTO':
|
||||||
etf_nav_str = ' —'
|
etf_close_str = ' —'
|
||||||
premium_str = ' —'
|
premium_str = ' —'
|
||||||
else:
|
else:
|
||||||
# ETF净值
|
# ETF收盘价
|
||||||
etf_nav = etf_nav_data.get(idx_code)
|
etf_close = etf_close_data.get(idx_code)
|
||||||
if etf_nav is not None:
|
if etf_close is not None:
|
||||||
etf_nav_str = f'{etf_nav:>10.3f}'
|
etf_close_str = f'{etf_close:>10.3f}'
|
||||||
else:
|
else:
|
||||||
etf_nav_str = ' —'
|
etf_close_str = ' —'
|
||||||
|
|
||||||
# 溢价率
|
# 溢价率(只有当天有净值数据时才显示)
|
||||||
premium = premium_data.get(idx_code)
|
premium = premium_data.get(idx_code)
|
||||||
if premium is not None:
|
if premium is not None:
|
||||||
# 高溢价警告标记
|
# 高溢价警告标记
|
||||||
@@ -207,7 +215,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
|||||||
else:
|
else:
|
||||||
premium_str = ' —'
|
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"]:
|
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 ' —'
|
entry_date_str = pos["entry_date"].strftime("%Y-%m-%d") if pos.get("entry_date") else ' —'
|
||||||
score_str = ' —' # 调出品种无得分
|
score_str = ' —' # 调出品种无得分
|
||||||
|
|
||||||
# 获取ETF代码、ETF净值和溢价率
|
# 获取ETF代码、ETF收盘价和溢价率
|
||||||
idx_code = pos["code"]
|
idx_code = pos["code"]
|
||||||
cfg = code_config.get(idx_code, {})
|
cfg = code_config.get(idx_code, {})
|
||||||
etf_code = cfg.get('etf', '—')
|
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:
|
if etf_code is None:
|
||||||
etf_code = '直接交易'
|
etf_code = '直接交易'
|
||||||
|
|
||||||
# 获取ETF净值和溢价率
|
# 获取ETF收盘价和溢价率
|
||||||
if market == 'CRYPTO':
|
if market == 'CRYPTO':
|
||||||
etf_nav_str = ' —'
|
etf_close_str = ' —'
|
||||||
premium_str = ' —'
|
premium_str = ' —'
|
||||||
else:
|
else:
|
||||||
# ETF净值
|
# ETF收盘价
|
||||||
etf_nav = etf_nav_data.get(idx_code)
|
etf_close = etf_close_data.get(idx_code)
|
||||||
if etf_nav is not None:
|
if etf_close is not None:
|
||||||
etf_nav_str = f'{etf_nav:>10.3f}'
|
etf_close_str = f'{etf_close:>10.3f}'
|
||||||
else:
|
else:
|
||||||
etf_nav_str = ' —'
|
etf_close_str = ' —'
|
||||||
|
|
||||||
# 溢价率
|
# 溢价率(只有当天有净值数据时才显示)
|
||||||
premium = premium_data.get(idx_code)
|
premium = premium_data.get(idx_code)
|
||||||
if premium is not None:
|
if premium is not None:
|
||||||
warning = '⚠️' if premium > 0.02 else ''
|
warning = '⚠️' if premium > 0.02 else ''
|
||||||
@@ -248,7 +256,7 @@ def _print_latest_signal(backtest_result: pd.DataFrame, code_list: list, code_na
|
|||||||
else:
|
else:
|
||||||
premium_str = ' —'
|
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)
|
print("=" * 160)
|
||||||
|
|
||||||
@@ -428,22 +436,33 @@ def _plot_report_chart(
|
|||||||
# 准备配置数据
|
# 准备配置数据
|
||||||
code_config = code_config or {}
|
code_config = code_config or {}
|
||||||
signal_date = backtest_result.index[-1]
|
signal_date = backtest_result.index[-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 = signal_date - pd.Timedelta(days=1)
|
||||||
|
|
||||||
# 计算ETF净值和溢价率(使用数据基准日期的数据)
|
# 计算ETF收盘价和溢价率(使用信号日期的数据)
|
||||||
etf_nav_dict = {}
|
etf_close_dict = {} # ETF收盘价
|
||||||
premium_dict = {}
|
premium_dict = {} # 溢价率(仅当当天有净值时计算)
|
||||||
if etf_price_data is not None and etf_nav_data_raw is not None:
|
|
||||||
# 确保数据基准日期在数据范围内
|
if etf_price_data is not None:
|
||||||
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:
|
for code in code_list:
|
||||||
if code in etf_price_data.columns and code in etf_nav_data_raw.columns:
|
if code in etf_price_data.columns:
|
||||||
etf_price = etf_price_data.loc[data_base_date, code]
|
etf_close = etf_price_data.loc[signal_date, code]
|
||||||
etf_nav = etf_nav_data_raw.loc[data_base_date, code]
|
if pd.notna(etf_close):
|
||||||
if pd.notna(etf_price) and pd.notna(etf_nav) and etf_nav > 0:
|
etf_close_dict[code] = etf_close
|
||||||
premium = (etf_price - etf_nav) / etf_nav
|
|
||||||
etf_nav_dict[code] = etf_nav
|
# 计算溢价率:只有当天有净值数据时才计算
|
||||||
|
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
|
premium_dict[code] = premium
|
||||||
|
|
||||||
# 计算表格行数
|
# 计算表格行数
|
||||||
@@ -460,14 +479,17 @@ def _plot_report_chart(
|
|||||||
|
|
||||||
signal_date = latest["signal_date"]
|
signal_date = latest["signal_date"]
|
||||||
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
signal_date_str = signal_date.strftime("%Y-%m-%d")
|
||||||
# 数据基准日期(信号是基于前一日数据计算的)
|
# 数据基准日期:使用信号日期的数据,如果没有则使用前一天
|
||||||
|
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 = signal_date - pd.Timedelta(days=1)
|
||||||
data_base_date_str = data_base_date.strftime("%Y-%m-%d")
|
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)
|
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 = []
|
table_data = []
|
||||||
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF净值", "溢价率", "操作", "持有天数", "盈亏"]
|
col_labels = ["标的名称", "指数代码", "ETF代码", "仓位", "得分", "进场日期", "进场价", "最新价", "ETF收盘价", "溢价率", "操作", "持有天数", "盈亏"]
|
||||||
|
|
||||||
# 下期持仓(调入/维持)
|
# 下期持仓(调入/维持)
|
||||||
for pos in latest["positions"]:
|
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 "—"
|
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代码、ETF净值和溢价率
|
# 获取ETF代码、ETF收盘价和溢价率
|
||||||
idx_code = pos["code"]
|
idx_code = pos["code"]
|
||||||
cfg = code_config.get(idx_code, {})
|
cfg = code_config.get(idx_code, {})
|
||||||
market = cfg.get('market', 'A')
|
market = cfg.get('market', 'A')
|
||||||
@@ -486,12 +508,12 @@ def _plot_report_chart(
|
|||||||
etf_code = '直接交易'
|
etf_code = '直接交易'
|
||||||
|
|
||||||
if market == 'CRYPTO':
|
if market == 'CRYPTO':
|
||||||
etf_nav_str = "—"
|
etf_close_str = "—"
|
||||||
premium_str = "—"
|
premium_str = "—"
|
||||||
else:
|
else:
|
||||||
etf_nav = etf_nav_dict.get(idx_code)
|
etf_close = etf_close_dict.get(idx_code)
|
||||||
premium = premium_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:
|
if premium is not None:
|
||||||
warning = "⚠️" if premium > 0.02 else ""
|
warning = "⚠️" if premium > 0.02 else ""
|
||||||
premium_str = f"{premium:+.2%}{warning}"
|
premium_str = f"{premium:+.2%}{warning}"
|
||||||
@@ -501,7 +523,7 @@ def _plot_report_chart(
|
|||||||
table_data.append([
|
table_data.append([
|
||||||
pos["name"], pos["code"], etf_code, f'{pos["weight"]:.0%}',
|
pos["name"], pos["code"], etf_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}',
|
||||||
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 "—"
|
entry_date_str = pos["entry_date"].strftime("%m-%d") if pos.get("entry_date") else "—"
|
||||||
score_str = "—" # 调出品种无得分
|
score_str = "—" # 调出品种无得分
|
||||||
|
|
||||||
# 获取ETF代码、ETF净值和溢价率
|
# 获取ETF代码、ETF收盘价和溢价率
|
||||||
idx_code = pos["code"]
|
idx_code = pos["code"]
|
||||||
cfg = code_config.get(idx_code, {})
|
cfg = code_config.get(idx_code, {})
|
||||||
market = cfg.get('market', 'A')
|
market = cfg.get('market', 'A')
|
||||||
@@ -521,12 +543,12 @@ def _plot_report_chart(
|
|||||||
etf_code = '直接交易'
|
etf_code = '直接交易'
|
||||||
|
|
||||||
if market == 'CRYPTO':
|
if market == 'CRYPTO':
|
||||||
etf_nav_str = "—"
|
etf_close_str = "—"
|
||||||
premium_str = "—"
|
premium_str = "—"
|
||||||
else:
|
else:
|
||||||
etf_nav = etf_nav_dict.get(idx_code)
|
etf_close = etf_close_dict.get(idx_code)
|
||||||
premium = premium_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:
|
if premium is not None:
|
||||||
warning = "⚠️" if premium > 0.02 else ""
|
warning = "⚠️" if premium > 0.02 else ""
|
||||||
premium_str = f"{premium:+.2%}{warning}"
|
premium_str = f"{premium:+.2%}{warning}"
|
||||||
@@ -536,7 +558,7 @@ def _plot_report_chart(
|
|||||||
table_data.append([
|
table_data.append([
|
||||||
pos["name"], pos["code"], etf_code, f'{pos["weight"]:.0%}',
|
pos["name"], pos["code"], etf_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}',
|
||||||
etf_nav_str, premium_str, "调出", days_str, pnl_str
|
etf_close_str, premium_str, "调出", days_str, pnl_str
|
||||||
])
|
])
|
||||||
|
|
||||||
if table_data:
|
if table_data:
|
||||||
|
|||||||
Reference in New Issue
Block a user