技术修复: - SOCKS5代理IPv6问题:socks5:// → socks5h:// (hybrid_source.py, yfinance_source.py) 目录整理: - scripts/ → 仅保留策略入口(daily_scheduler, run_rotation, run_cci_screener) - 实验脚本移至 tests/experiments/ - 工具脚本移至 tests/utils/ - 实验记录新增 docs/experiments/ - results/ 添加到 gitignore 实验结果: 实验001 - 同大类扩充(添加标普500): ├─ 累计收益: 1467.35% → 1176.26% (-291%) ├─ CAGR: 48.10% → 43.82% (-4.28%) ├─ 调仓次数: 459 → 501 (+42次) └─ 结论: 添加同大类标的不增加跨类分散,反而侵蚀收益 实验002 - 纳指vs标普替换对比: ├─ 累计收益: 1467.35% → 1118.77% (-348%) ├─ CAGR: 48.10% → 42.87% (-5.22%) ├─ Sharpe: 2.21 → 2.08 (-0.13) ├─ MaxDD: -17.33% → -15.14% (+2.18%) └─ 结论: 纳指100优于标普500,成长风格更适合动量策略 策略建议: - 保持纳指100作为美股大类代表 - 不添加同大类新标的(避免类内切换成本) - 新增标的应优先考虑新大类(增加跨类分散)
184 lines
5.4 KiB
Python
184 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
获取159930 ETF最新10天的收盘价、净值并计算溢价率
|
||
"""
|
||
|
||
import os
|
||
import pandas as pd
|
||
import tushare as ts
|
||
from datetime import datetime, timedelta
|
||
|
||
# 设置Tushare token
|
||
def get_tushare_token():
|
||
# 首先尝试从环境变量获取
|
||
token = os.environ.get("TUSHARE_TOKEN")
|
||
if token:
|
||
return token
|
||
|
||
# 尝试从.env文件获取
|
||
try:
|
||
from dotenv import load_dotenv
|
||
load_dotenv()
|
||
token = os.environ.get("TUSHARE_TOKEN")
|
||
if token:
|
||
return token
|
||
except ImportError:
|
||
pass
|
||
|
||
# 手动读取.env文件
|
||
env_path = os.path.join(os.path.dirname(__file__), '.env')
|
||
if os.path.exists(env_path):
|
||
with open(env_path, 'r') as f:
|
||
for line in f:
|
||
if line.startswith('TUSHARE_TOKEN='):
|
||
token = line.strip().split('=', 1)[1].strip().strip('"').strip("'")
|
||
if token:
|
||
return token
|
||
|
||
raise ValueError("请设置 TUSHARE_TOKEN 环境变量或在.env文件中配置")
|
||
|
||
|
||
def fetch_etf_data(etf_code: str, days: int = 10):
|
||
"""
|
||
获取ETF最新N天的价格、净值数据
|
||
|
||
Args:
|
||
etf_code: ETF代码,如 "159930.SZ"
|
||
days: 获取天数
|
||
|
||
Returns:
|
||
DataFrame: 包含日期、收盘价、净值、溢价率
|
||
"""
|
||
pro = ts.pro_api(get_tushare_token())
|
||
|
||
# 计算日期范围(多取几天确保有足够数据)
|
||
end_date = datetime.now()
|
||
start_date = end_date - timedelta(days=days + 5)
|
||
|
||
start_str = start_date.strftime('%Y%m%d')
|
||
end_str = end_date.strftime('%Y%m%d')
|
||
|
||
# 转换代码格式
|
||
ts_code = etf_code.replace(".SS", ".SH")
|
||
|
||
print(f"获取 {etf_code} 数据...")
|
||
print(f"日期范围: {start_str} ~ {end_str}")
|
||
|
||
# 1. 获取ETF价格数据(fund_daily接口)
|
||
print("\n1. 获取ETF价格数据...")
|
||
try:
|
||
price_df = pro.fund_daily(
|
||
ts_code=ts_code,
|
||
start_date=start_str,
|
||
end_date=end_str
|
||
)
|
||
if price_df is not None and len(price_df) > 0:
|
||
price_df = price_df.sort_values('trade_date')
|
||
print(f" 获取到 {len(price_df)} 条价格数据")
|
||
print(f" 最新日期: {price_df['trade_date'].max()}")
|
||
else:
|
||
print(" 未获取到价格数据")
|
||
price_df = None
|
||
except Exception as e:
|
||
print(f" 获取价格数据失败: {e}")
|
||
price_df = None
|
||
|
||
# 2. 获取ETF净值数据(fund_nav接口)
|
||
print("\n2. 获取ETF净值数据...")
|
||
try:
|
||
# 净值通常滞后,多取一天
|
||
nav_end_date = end_date + timedelta(days=1)
|
||
nav_end_str = nav_end_date.strftime('%Y%m%d')
|
||
|
||
nav_df = pro.fund_nav(
|
||
ts_code=ts_code,
|
||
start_date=start_str,
|
||
end_date=nav_end_str
|
||
)
|
||
if nav_df is not None and len(nav_df) > 0:
|
||
nav_df = nav_df.sort_values('nav_date')
|
||
print(f" 获取到 {len(nav_df)} 条净值数据")
|
||
print(f" 最新日期: {nav_df['nav_date'].max()}")
|
||
else:
|
||
print(" 未获取到净值数据")
|
||
nav_df = None
|
||
except Exception as e:
|
||
print(f" 获取净值数据失败: {e}")
|
||
nav_df = None
|
||
|
||
# 3. 合并数据并计算溢价率
|
||
print("\n3. 合并数据并计算溢价率...")
|
||
|
||
if price_df is None:
|
||
print("错误: 没有价格数据")
|
||
return None
|
||
|
||
# 准备价格数据
|
||
price_df['date'] = pd.to_datetime(price_df['trade_date'])
|
||
price_df = price_df.set_index('date')
|
||
price_series = price_df['close']
|
||
|
||
# 准备净值数据
|
||
if nav_df is not None:
|
||
nav_df['date'] = pd.to_datetime(nav_df['nav_date'])
|
||
nav_df = nav_df.set_index('date')
|
||
nav_series = nav_df['unit_nav']
|
||
else:
|
||
nav_series = pd.Series()
|
||
|
||
# 创建结果DataFrame
|
||
result = pd.DataFrame({
|
||
'收盘价': price_series
|
||
})
|
||
|
||
# 对齐净值数据(按日期)
|
||
result = result.join(nav_series.rename('净值'), how='left')
|
||
|
||
# 计算溢价率
|
||
result['溢价率'] = (result['收盘价'] - result['净值']) / result['净值'] * 100
|
||
|
||
# 取最新N天
|
||
result = result.tail(days)
|
||
|
||
# 格式化输出
|
||
result['收盘价'] = result['收盘价'].round(3)
|
||
result['净值'] = result['净值'].round(3)
|
||
result['溢价率'] = result['溢价率'].round(2)
|
||
|
||
# 重置索引,将日期作为列
|
||
result = result.reset_index()
|
||
result['日期'] = result['date'].dt.strftime('%Y-%m-%d')
|
||
result = result[['日期', '收盘价', '净值', '溢价率']]
|
||
|
||
return result
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
etf_code = "159930.SZ"
|
||
days = 10
|
||
|
||
print("=" * 60)
|
||
print(f"ETF: {etf_code} (中证能源ETF)")
|
||
print(f"获取最近 {days} 天数据")
|
||
print("=" * 60)
|
||
|
||
df = fetch_etf_data(etf_code, days)
|
||
|
||
if df is not None and len(df) > 0:
|
||
print("\n" + "=" * 60)
|
||
print("结果表格:")
|
||
print("=" * 60)
|
||
print(df.to_string(index=False))
|
||
|
||
# 保存到CSV
|
||
output_file = f"{etf_code.replace('.', '_')}_latest_{days}days.csv"
|
||
df.to_csv(output_file, index=False, encoding='utf-8-sig')
|
||
print(f"\n数据已保存到: {output_file}")
|
||
else:
|
||
print("\n获取数据失败")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|