refactor: 归档旧代码,保留新框架结构
归档内容: - core/ (数据源、因子计算、通用工具) → archive/legacy_core/ - strategies/rotation/engine.py, portfolio.py, report.py → archive/legacy_core/ - scripts/ (run_rotation, daily_scheduler) → archive/legacy_scripts/ - examples/ → archive/legacy_examples/ - tests/ (实验、对比测试) → archive/legacy_tests/ - 单独文件 (fetch_*.py, 动量.py, 全球市场.py等) → archive/single_files/ 保留新结构: - framework/ (抽象接口) - strategies/shared/ (定制组件) - strategies/rotation/strategy.py (新策略) - 外层配置: .env, .dockerignore, build-and-push.sh, hk_ecs.pem, README.md, requirements.txt - Docker相关: Dockerfile, Dockerfile_base, docker-compose.yml 更新README反映新框架架构
This commit is contained in:
252
archive/legacy_core/portfolio.py
Normal file
252
archive/legacy_core/portfolio.py
Normal file
@@ -0,0 +1,252 @@
|
||||
"""
|
||||
ETF轮动策略 - 持仓跟踪模块
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def track_positions(
|
||||
backtest_result: pd.DataFrame,
|
||||
code_name_map: dict = None,
|
||||
select_num: int = 1,
|
||||
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
"""
|
||||
从回测结果中提取每笔持仓记录
|
||||
|
||||
Args:
|
||||
backtest_result: 回测结果(含 '信号' 列)
|
||||
code_name_map: 代码→名称映射
|
||||
select_num: 每次选中的品种数量
|
||||
|
||||
Returns:
|
||||
tuple: (trades_df, summary_df)
|
||||
"""
|
||||
code_name_map = code_name_map or {}
|
||||
data = backtest_result.copy()
|
||||
dates = data.index.tolist()
|
||||
signals = data["信号"].tolist()
|
||||
trades = []
|
||||
|
||||
if select_num == 1:
|
||||
# 单品种轮动
|
||||
current_code = signals[0]
|
||||
entry_date = dates[0]
|
||||
entry_price = data.loc[entry_date, current_code]
|
||||
entry_nav = data.loc[entry_date, "轮动策略净值"]
|
||||
|
||||
for i in range(1, len(dates)):
|
||||
today_code = signals[i]
|
||||
|
||||
if today_code != current_code:
|
||||
exit_date = dates[i - 1]
|
||||
exit_price = data.loc[exit_date, current_code]
|
||||
exit_nav = data.loc[exit_date, "轮动策略净值"]
|
||||
holding_days = (i - 1) - dates.index(entry_date) + 1
|
||||
trade_return = exit_price / entry_price - 1 if entry_price != 0 else 0
|
||||
nav_contrib = exit_nav - entry_nav
|
||||
|
||||
trades.append({
|
||||
"序号": len(trades) + 1,
|
||||
"品种代码": current_code,
|
||||
"品种名称": code_name_map.get(current_code, current_code),
|
||||
"进场日期": entry_date,
|
||||
"出场日期": exit_date,
|
||||
"持仓天数": holding_days,
|
||||
"仓位占比": "100%",
|
||||
"进场价格": round(entry_price, 2),
|
||||
"出场价格": round(exit_price, 2),
|
||||
"持仓收益": trade_return,
|
||||
"进场净值": round(entry_nav, 4),
|
||||
"出场净值": round(exit_nav, 4),
|
||||
"净值贡献": round(nav_contrib, 4),
|
||||
})
|
||||
|
||||
current_code = today_code
|
||||
entry_date = dates[i]
|
||||
entry_price = data.loc[entry_date, current_code]
|
||||
entry_nav = data.loc[entry_date, "轮动策略净值"]
|
||||
|
||||
# 最后一笔
|
||||
exit_date = dates[-1]
|
||||
exit_price = data.loc[exit_date, current_code]
|
||||
exit_nav = data.loc[exit_date, "轮动策略净值"]
|
||||
holding_days = len(dates) - dates.index(entry_date)
|
||||
trade_return = exit_price / entry_price - 1 if entry_price != 0 else 0
|
||||
nav_contrib = exit_nav - entry_nav
|
||||
|
||||
trades.append({
|
||||
"序号": len(trades) + 1,
|
||||
"品种代码": current_code,
|
||||
"品种名称": code_name_map.get(current_code, current_code),
|
||||
"进场日期": entry_date,
|
||||
"出场日期": exit_date,
|
||||
"持仓天数": holding_days,
|
||||
"仓位占比": "100%",
|
||||
"进场价格": round(entry_price, 2),
|
||||
"出场价格": round(exit_price, 2),
|
||||
"持仓收益": trade_return,
|
||||
"进场净值": round(entry_nav, 4),
|
||||
"出场净值": round(exit_nav, 4),
|
||||
"净值贡献": round(nav_contrib, 4),
|
||||
})
|
||||
|
||||
else:
|
||||
# 多品种等权轮动
|
||||
current_signal = signals[0]
|
||||
entry_date = dates[0]
|
||||
codes = [c for c in current_signal.split(",") if c] # 过滤空字符串
|
||||
if not codes:
|
||||
# 空信号,返回空结果
|
||||
return pd.DataFrame(trades), pd.DataFrame()
|
||||
weight = 1.0 / len(codes)
|
||||
entry_prices = {c: data.loc[entry_date, c] for c in codes}
|
||||
entry_nav = data.loc[entry_date, "轮动策略净值"]
|
||||
|
||||
for i in range(1, len(dates)):
|
||||
today_signal = signals[i]
|
||||
|
||||
if today_signal != current_signal:
|
||||
exit_date = dates[i - 1]
|
||||
exit_nav = data.loc[exit_date, "轮动策略净值"]
|
||||
holding_days = (i - 1) - dates.index(entry_date) + 1
|
||||
|
||||
for c in codes:
|
||||
exit_price = data.loc[exit_date, c]
|
||||
ep = entry_prices[c]
|
||||
trade_return = exit_price / ep - 1 if ep != 0 else 0
|
||||
|
||||
trades.append({
|
||||
"序号": len(trades) + 1,
|
||||
"品种代码": c,
|
||||
"品种名称": code_name_map.get(c, c),
|
||||
"进场日期": entry_date,
|
||||
"出场日期": exit_date,
|
||||
"持仓天数": holding_days,
|
||||
"仓位占比": f"{weight:.0%}",
|
||||
"进场价格": round(ep, 2),
|
||||
"出场价格": round(exit_price, 2),
|
||||
"持仓收益": trade_return,
|
||||
"进场净值": round(entry_nav, 4),
|
||||
"出场净值": round(exit_nav, 4),
|
||||
"净值贡献": round((exit_nav - entry_nav) * weight, 4),
|
||||
})
|
||||
|
||||
current_signal = today_signal
|
||||
entry_date = dates[i]
|
||||
codes = [c for c in current_signal.split(",") if c] # 过滤空字符串
|
||||
if not codes:
|
||||
break # 空信号,结束循环
|
||||
weight = 1.0 / len(codes)
|
||||
entry_prices = {c: data.loc[entry_date, c] for c in codes}
|
||||
entry_nav = data.loc[entry_date, "轮动策略净值"]
|
||||
|
||||
# 最后一笔
|
||||
exit_date = dates[-1]
|
||||
exit_nav = data.loc[exit_date, "轮动策略净值"]
|
||||
holding_days = len(dates) - dates.index(entry_date)
|
||||
for c in codes:
|
||||
exit_price = data.loc[exit_date, c]
|
||||
ep = entry_prices[c]
|
||||
trade_return = exit_price / ep - 1 if ep != 0 else 0
|
||||
trades.append({
|
||||
"序号": len(trades) + 1,
|
||||
"品种代码": c,
|
||||
"品种名称": code_name_map.get(c, c),
|
||||
"进场日期": entry_date,
|
||||
"出场日期": exit_date,
|
||||
"持仓天数": holding_days,
|
||||
"仓位占比": f"{weight:.0%}",
|
||||
"进场价格": round(ep, 2),
|
||||
"出场价格": round(exit_price, 2),
|
||||
"持仓收益": trade_return,
|
||||
"进场净值": round(entry_nav, 4),
|
||||
"出场净值": round(exit_nav, 4),
|
||||
"净值贡献": round((exit_nav - entry_nav) * weight, 4),
|
||||
})
|
||||
|
||||
trades_df = pd.DataFrame(trades)
|
||||
summary = _summarize_by_code(trades_df, code_name_map)
|
||||
return trades_df, summary
|
||||
|
||||
|
||||
def _summarize_by_code(trades_df: pd.DataFrame, code_name_map: dict) -> pd.DataFrame:
|
||||
"""按品种汇总持仓统计"""
|
||||
if trades_df.empty:
|
||||
return pd.DataFrame()
|
||||
|
||||
groups = trades_df.groupby("品种代码")
|
||||
rows = []
|
||||
|
||||
for code, grp in groups:
|
||||
total_trades = len(grp)
|
||||
total_days = grp["持仓天数"].sum()
|
||||
avg_days = grp["持仓天数"].mean()
|
||||
win_trades = (grp["持仓收益"] > 0).sum()
|
||||
win_rate = win_trades / total_trades if total_trades > 0 else 0
|
||||
avg_return = grp["持仓收益"].mean()
|
||||
total_return = (1 + grp["持仓收益"]).prod() - 1
|
||||
max_return = grp["持仓收益"].max()
|
||||
min_return = grp["持仓收益"].min()
|
||||
|
||||
rows.append({
|
||||
"品种代码": code,
|
||||
"品种名称": code_name_map.get(code, code),
|
||||
"调仓次数": total_trades,
|
||||
"总持仓天数": total_days,
|
||||
"平均持仓天数": round(avg_days, 1),
|
||||
"胜率": win_rate,
|
||||
"平均收益": avg_return,
|
||||
"累计收益": total_return,
|
||||
"最大单次收益": max_return,
|
||||
"最大单次亏损": min_return,
|
||||
})
|
||||
|
||||
summary = pd.DataFrame(rows)
|
||||
summary = summary.sort_values("总持仓天数", ascending=False).reset_index(drop=True)
|
||||
return summary
|
||||
|
||||
|
||||
def save_trades(
|
||||
trades_df: pd.DataFrame,
|
||||
summary_df: pd.DataFrame,
|
||||
save_path: str = "report",
|
||||
) -> None:
|
||||
"""保存调仓明细和汇总到CSV"""
|
||||
import os
|
||||
os.makedirs(os.path.dirname(save_path) if os.path.dirname(save_path) else ".", exist_ok=True)
|
||||
|
||||
# 保存调仓明细
|
||||
trades_path = f"{save_path}_trades.csv"
|
||||
if not trades_df.empty:
|
||||
trades_out = trades_df.copy()
|
||||
if "持仓收益" in trades_out.columns:
|
||||
trades_out["持仓收益"] = trades_out["持仓收益"].apply(lambda x: f"{x:.2%}")
|
||||
if "进场日期" in trades_out.columns:
|
||||
trades_out["进场日期"] = trades_out["进场日期"].apply(
|
||||
lambda x: x.strftime("%Y-%m-%d") if hasattr(x, "strftime") else str(x)[:10]
|
||||
)
|
||||
if "出场日期" in trades_out.columns:
|
||||
trades_out["出场日期"] = trades_out["出场日期"].apply(
|
||||
lambda x: x.strftime("%Y-%m-%d") if hasattr(x, "strftime") else str(x)[:10]
|
||||
)
|
||||
trades_out.to_csv(trades_path, index=False, encoding="utf-8-sig")
|
||||
print(f"\n调仓明细已保存: {trades_path}")
|
||||
else:
|
||||
# 创建空文件
|
||||
pd.DataFrame().to_csv(trades_path, index=False, encoding="utf-8-sig")
|
||||
print(f"\n调仓明细为空: {trades_path}")
|
||||
|
||||
# 保存品种汇总
|
||||
summary_path = f"{save_path}_summary.csv"
|
||||
if not summary_df.empty:
|
||||
summary_out = summary_df.copy()
|
||||
for col in ["胜率", "平均收益", "累计收益", "最大单次收益", "最大单次亏损"]:
|
||||
if col in summary_out.columns:
|
||||
summary_out[col] = summary_out[col].apply(lambda x: f"{x:.2%}")
|
||||
summary_out.to_csv(summary_path, index=False, encoding="utf-8-sig")
|
||||
print(f"品种汇总已保存: {summary_path}")
|
||||
else:
|
||||
# 创建空文件
|
||||
pd.DataFrame().to_csv(summary_path, index=False, encoding="utf-8-sig")
|
||||
print(f"品种汇总为空: {summary_path}")
|
||||
Reference in New Issue
Block a user