refactor(archive): move unused modules to archive/
Archive legacy framework and utility modules that are no longer referenced by the active core (datasource/ and rotation/): - framework/ -> archive/framework/ - framework_v2/ -> archive/framework_v2/ - strategies/ -> archive/strategies/ - config/ -> archive/config/ - visualization/ -> archive/visualization/ - scripts/ -> archive/scripts/ - tests/ -> archive/tests/ - run_rotation.py, run_us_rotation.py -> archive/single_files/ - compare_*.py, test_api_dates.py -> archive/single_files/
This commit is contained in:
@@ -1,477 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
导出回测逐日明细到 JSON,供 HTML 回放器加载。
|
||||
|
||||
模式 B:指数信号 + ETF 收益(2020-01-01 ~ 2026-05-19)
|
||||
|
||||
用法:
|
||||
python scripts/export_backtest_detail.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yaml
|
||||
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from datasource.tushare_source import TushareSource
|
||||
from datasource.flask_api_source import FlaskAPIDataSource
|
||||
from strategies.shared.factors.momentum import MomentumFactor
|
||||
from strategies.shared.signals.selectors import TopNSelector
|
||||
from framework.execution import BacktestExecutor
|
||||
|
||||
# ==================== 加载配置 ====================
|
||||
config_path = project_root / 'strategies' / 'rotation' / 'config.yaml'
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
CODE_LIST = config['code_list']
|
||||
SELECT_NUM = config['select_num']
|
||||
N_DAYS = config['n_days']
|
||||
TRADE_COST = config['trade_cost']
|
||||
BOND_THRESHOLD = config.get('bond_threshold', {})
|
||||
BOND_CODE = BOND_THRESHOLD.get('bond_code', '931862.CSI')
|
||||
BOND_RATIO = BOND_THRESHOLD.get('ratio', 1.0)
|
||||
|
||||
|
||||
def fetch_all_data(start_date='2018-01-01', end_date='2026-05-19'):
|
||||
ts = TushareSource()
|
||||
api = FlaskAPIDataSource() # 默认使用 k3s.tokenpluse.xyz
|
||||
|
||||
index_data = {}
|
||||
etf_data = {}
|
||||
etf_code_map = {}
|
||||
|
||||
# 统一使用 Flask API 获取所有指数数据(与 strategy.py 保持一致)
|
||||
print("[指数数据] - 通过 Flask API (k3s服务) 获取")
|
||||
index_codes = list(CODE_LIST.keys())
|
||||
index_ohlcv_data = api.fetch_batch(index_codes, start_date, end_date)
|
||||
|
||||
for code, df in index_ohlcv_data.items():
|
||||
if df is not None and 'close' in df.columns and len(df) > 0:
|
||||
index_data[code] = df
|
||||
name = CODE_LIST.get(code, {}).get('name', code)
|
||||
print(f" {code} ({name})... {len(df)}天")
|
||||
else:
|
||||
name = CODE_LIST.get(code, {}).get('name', code)
|
||||
print(f" {code} ({name})... 失败")
|
||||
|
||||
print("\n[ETF数据]")
|
||||
etf_nav_data = {}
|
||||
for code, cfg in CODE_LIST.items():
|
||||
etf_code = cfg.get('etf')
|
||||
if etf_code is None:
|
||||
continue
|
||||
etf_code_map[code] = etf_code
|
||||
name = cfg['name']
|
||||
print(f" {etf_code} ({name})...", end=' ')
|
||||
|
||||
df = ts.fetch_etf_adj(etf_code, start_date, end_date)
|
||||
if df is not None and 'close_hfq' in df.columns and len(df) > 0:
|
||||
adj_ratio = df['close_hfq'] / df['close']
|
||||
df['open_hfq'] = df['open'] * adj_ratio
|
||||
etf_data[code] = df
|
||||
print(f"{len(df)}天", end='')
|
||||
else:
|
||||
print("失败")
|
||||
continue
|
||||
|
||||
# 获取ETF净值(用于计算溢价率)
|
||||
nav_df = ts.fetch_etf_nav(etf_code, start_date, end_date)
|
||||
if nav_df is not None and 'nav' in nav_df.columns and len(nav_df) > 0:
|
||||
etf_nav_data[code] = nav_df['nav']
|
||||
print(f" nav={len(nav_df)}天")
|
||||
else:
|
||||
print(" nav=无")
|
||||
|
||||
return index_data, etf_data, etf_code_map, etf_nav_data
|
||||
|
||||
|
||||
def compute_factors(price_data, n_days, trade_dates):
|
||||
"""先在原始交易日历上计算因子,再 ffill 对齐到 A 股日历(与 strategy.py 一致)"""
|
||||
factor = MomentumFactor(n_days=n_days, weighted=True, crash_filter=True)
|
||||
factor_values = {}
|
||||
for code, df in price_data.items():
|
||||
if 'close' not in df.columns:
|
||||
continue
|
||||
close_series = df['close'].dropna()
|
||||
if len(close_series) == 0:
|
||||
continue
|
||||
values = factor.compute(pd.DataFrame({'close': close_series}))
|
||||
factor_values[code] = values.reindex(trade_dates, method='ffill')
|
||||
return pd.DataFrame(factor_values)
|
||||
|
||||
|
||||
def generate_signals(factor_df, group_mapping):
|
||||
selector = TopNSelector(
|
||||
select_num=SELECT_NUM,
|
||||
group_mapping=group_mapping,
|
||||
min_score=0.0,
|
||||
rebalance_days=1,
|
||||
rebalance_threshold=0.0,
|
||||
bond_threshold_config=BOND_THRESHOLD
|
||||
)
|
||||
return selector.generate(factor_df)
|
||||
|
||||
|
||||
def safe_val(v, decimals=4):
|
||||
if v is None or (isinstance(v, float) and (math.isnan(v) or math.isinf(v))):
|
||||
return None
|
||||
if isinstance(v, (np.floating, float)):
|
||||
return round(float(v), decimals)
|
||||
if isinstance(v, (np.integer, int)):
|
||||
return int(v)
|
||||
return v
|
||||
|
||||
|
||||
def main():
|
||||
from datetime import datetime
|
||||
backtest_start = '2020-01-01'
|
||||
backtest_end = datetime.now().strftime('%Y-%m-%d') # 动态获取当前日期
|
||||
|
||||
print("=" * 60)
|
||||
print(" 导出回测逐日明细 (模式B: 指数信号 + ETF收益)")
|
||||
print("=" * 60)
|
||||
|
||||
# 1. 获取数据
|
||||
print("\n[1] 获取数据...")
|
||||
index_data, etf_data, etf_code_map, etf_nav_data = fetch_all_data()
|
||||
|
||||
# 2. A股交易日历
|
||||
print("\n[2] 获取A股交易日历...")
|
||||
ts = TushareSource()
|
||||
a_share_dates = ts.fetch_trade_cal(backtest_start, backtest_end)
|
||||
print(f" {len(a_share_dates)} 天")
|
||||
|
||||
# 3. 分组映射
|
||||
group_mapping = {}
|
||||
for code, cfg in CODE_LIST.items():
|
||||
if isinstance(cfg, dict):
|
||||
group_mapping[code] = cfg.get('market', 'default')
|
||||
|
||||
valid_codes = [c for c in CODE_LIST if c in index_data]
|
||||
|
||||
# 4. 计算因子(指数信号)
|
||||
print("\n[3] 计算指数动量因子...")
|
||||
idx_price_data = {}
|
||||
for code in valid_codes:
|
||||
if code in index_data and 'close' in index_data[code].columns:
|
||||
idx_price_data[code] = index_data[code]
|
||||
factor_df = compute_factors(idx_price_data, N_DAYS, a_share_dates)
|
||||
print(f" {len(factor_df.columns)} 只, {len(factor_df)} 天")
|
||||
|
||||
# 5. 生成信号
|
||||
print("\n[4] 生成信号...")
|
||||
signals = generate_signals(factor_df, group_mapping)
|
||||
print(f" {len(signals)} 天")
|
||||
|
||||
# 6. 准备ETF收益率(模式B)
|
||||
print("\n[5] 准备ETF收益率...")
|
||||
etf_close_hfq_aligned = {}
|
||||
etf_close_aligned = {}
|
||||
etf_open_aligned = {}
|
||||
etf_close_hfq_raw = {}
|
||||
index_close_aligned = {}
|
||||
returns_etf = {}
|
||||
returns_idx = {}
|
||||
|
||||
for code in valid_codes:
|
||||
# 指数收盘价和收益率
|
||||
if code in index_data and 'close' in index_data[code].columns:
|
||||
ic = index_data[code]['close'].dropna()
|
||||
ic_a = ic.reindex(a_share_dates, method='ffill')
|
||||
index_close_aligned[code] = ic_a
|
||||
returns_idx[code] = ic_a.pct_change(fill_method=None)
|
||||
|
||||
# ETF价格和收益率
|
||||
etf_code = etf_code_map.get(code)
|
||||
if etf_code and code in etf_data:
|
||||
df = etf_data[code]
|
||||
chfq = df['close_hfq'].dropna()
|
||||
chfq_a = chfq.reindex(a_share_dates, method='ffill')
|
||||
etf_close_hfq_aligned[code] = chfq_a
|
||||
etf_close_hfq_raw[code] = chfq
|
||||
returns_etf[f'日收益率_{code}'] = chfq_a.pct_change(fill_method=None)
|
||||
|
||||
ec = df['close'].reindex(a_share_dates, method='ffill')
|
||||
etf_close_aligned[code] = ec
|
||||
eo = df['open'].reindex(a_share_dates, method='ffill')
|
||||
etf_open_aligned[code] = eo
|
||||
elif code in index_data and 'close' in index_data[code].columns:
|
||||
ic = index_data[code]['close'].dropna()
|
||||
ic_a = ic.reindex(a_share_dates, method='ffill')
|
||||
returns_etf[f'日收益率_{code}'] = ic_a.pct_change(fill_method=None)
|
||||
|
||||
returns_etf_df = pd.DataFrame(returns_etf)
|
||||
|
||||
# 6.5 溢价率:(ETF收盘价 - 单位净值) / 单位净值
|
||||
etf_premium_aligned = {}
|
||||
for code in valid_codes:
|
||||
if code in etf_nav_data and code in etf_close_aligned:
|
||||
nav_raw = etf_nav_data[code]
|
||||
nav_raw = nav_raw[~nav_raw.index.duplicated(keep='last')]
|
||||
nav = nav_raw.reindex(a_share_dates, method='ffill')
|
||||
close = etf_close_aligned[code]
|
||||
premium = (close - nav) / nav
|
||||
etf_premium_aligned[code] = premium
|
||||
|
||||
# 7. 执行回测获取净值
|
||||
print("\n[6] 执行回测...")
|
||||
common_dates = signals.index.intersection(returns_etf_df.index)
|
||||
signals_aligned = signals.loc[common_dates]
|
||||
returns_aligned = returns_etf_df.loc[common_dates]
|
||||
|
||||
executor = BacktestExecutor(
|
||||
initial_capital=100000,
|
||||
trade_cost=TRADE_COST,
|
||||
select_num=SELECT_NUM
|
||||
)
|
||||
portfolio = executor.execute(signals_aligned, returns_aligned)
|
||||
result = portfolio.backtest_result
|
||||
nav_series_raw = result['策略净值']
|
||||
daily_ret_raw = result['策略日收益率']
|
||||
|
||||
# 扩展到所有common_dates,信号前的日期 nav=1.0, return=0.0
|
||||
nav_series = nav_series_raw.reindex(common_dates)
|
||||
daily_ret_series = daily_ret_raw.reindex(common_dates, fill_value=0.0)
|
||||
first_valid = nav_series.first_valid_index()
|
||||
if first_valid is not None:
|
||||
nav_series.loc[:first_valid] = nav_series.loc[:first_valid].fillna(1.0)
|
||||
nav_series = nav_series.ffill()
|
||||
|
||||
print(f" 终值: {nav_series.iloc[-1]:.4f}")
|
||||
|
||||
# 8. 构建逐日明细
|
||||
print("\n[7] 构建逐日明细...")
|
||||
|
||||
# 持仓跟踪状态
|
||||
holdings_state = {} # {code: {'entry_date': str, 'entry_price': float}}
|
||||
prev_holdings = set()
|
||||
|
||||
days_list = []
|
||||
signal_col = 'signal'
|
||||
|
||||
for i, date in enumerate(common_dates):
|
||||
sig_val = signals_aligned.loc[date, signal_col] if signal_col in signals_aligned.columns else ''
|
||||
current_holdings = set(str(sig_val).split(',')) if pd.notna(sig_val) and sig_val else set()
|
||||
current_holdings.discard('')
|
||||
|
||||
# 调仓检测
|
||||
added = list(current_holdings - prev_holdings)
|
||||
removed = list(prev_holdings - current_holdings)
|
||||
is_rebalance = len(added) > 0 or len(removed) > 0
|
||||
|
||||
# 更新持仓状态
|
||||
for code in removed:
|
||||
holdings_state.pop(code, None)
|
||||
for code in added:
|
||||
entry_price_etf = None
|
||||
entry_price_idx = None
|
||||
if code in etf_close_hfq_aligned:
|
||||
ep = etf_close_hfq_aligned[code].get(date)
|
||||
if pd.notna(ep):
|
||||
entry_price_etf = float(ep)
|
||||
if code in index_close_aligned:
|
||||
ep = index_close_aligned[code].get(date)
|
||||
if pd.notna(ep):
|
||||
entry_price_idx = float(ep)
|
||||
holdings_state[code] = {
|
||||
'entry_date': date.strftime('%Y-%m-%d'),
|
||||
'entry_price_etf': entry_price_etf,
|
||||
'entry_price_idx': entry_price_idx,
|
||||
}
|
||||
|
||||
# 动态阈值
|
||||
factor_scores = {}
|
||||
for code in valid_codes:
|
||||
if code in factor_df.columns:
|
||||
v = factor_df.loc[date, code] if date in factor_df.index else np.nan
|
||||
if pd.notna(v):
|
||||
factor_scores[code] = float(v)
|
||||
|
||||
bond_score = factor_scores.get(BOND_CODE)
|
||||
if BOND_THRESHOLD.get('enabled') and bond_score is not None and bond_score >= 0:
|
||||
threshold = bond_score * BOND_RATIO
|
||||
else:
|
||||
threshold = 0.0
|
||||
|
||||
# 排名(按动量降序,排除BOND)
|
||||
non_bond_scores = {k: v for k, v in factor_scores.items()
|
||||
if group_mapping.get(k) != 'BOND'}
|
||||
sorted_codes = sorted(non_bond_scores.keys(),
|
||||
key=lambda c: non_bond_scores[c], reverse=True)
|
||||
rank_map = {c: r + 1 for r, c in enumerate(sorted_codes)}
|
||||
# BOND不参与排名
|
||||
if BOND_CODE in factor_scores:
|
||||
rank_map[BOND_CODE] = None
|
||||
|
||||
# 每标的详情
|
||||
assets = {}
|
||||
for code in valid_codes:
|
||||
asset = {}
|
||||
|
||||
# 指数收盘价
|
||||
if code in index_close_aligned:
|
||||
v = index_close_aligned[code].get(date)
|
||||
asset['index_close'] = safe_val(v, 2)
|
||||
else:
|
||||
asset['index_close'] = None
|
||||
|
||||
# 动量
|
||||
mom = factor_scores.get(code)
|
||||
asset['momentum'] = safe_val(mom, 4)
|
||||
|
||||
# 排名
|
||||
asset['rank'] = rank_map.get(code)
|
||||
|
||||
# 阈值
|
||||
asset['threshold'] = safe_val(threshold, 4)
|
||||
asset['above_threshold'] = mom >= threshold if mom is not None else False
|
||||
|
||||
# ETF价格
|
||||
if code in etf_close_aligned:
|
||||
asset['etf_close'] = safe_val(etf_close_aligned[code].get(date), 3)
|
||||
else:
|
||||
asset['etf_close'] = None
|
||||
|
||||
if code in etf_open_aligned:
|
||||
asset['etf_open'] = safe_val(etf_open_aligned[code].get(date), 3)
|
||||
else:
|
||||
asset['etf_open'] = None
|
||||
|
||||
if code in etf_close_hfq_aligned:
|
||||
asset['etf_close_hfq'] = safe_val(etf_close_hfq_aligned[code].get(date), 4)
|
||||
else:
|
||||
asset['etf_close_hfq'] = None
|
||||
|
||||
# 溢价率
|
||||
if code in etf_premium_aligned:
|
||||
asset['premium'] = safe_val(etf_premium_aligned[code].get(date), 4)
|
||||
else:
|
||||
asset['premium'] = None
|
||||
|
||||
# ETF日收益率
|
||||
ret_col = f'日收益率_{code}'
|
||||
if ret_col in returns_etf_df.columns:
|
||||
asset['etf_return_ctc'] = safe_val(returns_etf_df.loc[date, ret_col], 6)
|
||||
else:
|
||||
asset['etf_return_ctc'] = None
|
||||
|
||||
# 指数日收益率
|
||||
if code in returns_idx:
|
||||
asset['index_return'] = safe_val(returns_idx[code].get(date), 6)
|
||||
else:
|
||||
asset['index_return'] = None
|
||||
|
||||
# 持仓状态
|
||||
is_held = code in current_holdings
|
||||
asset['is_held'] = is_held
|
||||
|
||||
if is_held and code in holdings_state:
|
||||
hs = holdings_state[code]
|
||||
asset['entry_date'] = hs['entry_date']
|
||||
asset['entry_price_etf'] = safe_val(hs['entry_price_etf'], 4)
|
||||
asset['entry_price_idx'] = safe_val(hs['entry_price_idx'], 4)
|
||||
|
||||
entry_dt = pd.Timestamp(hs['entry_date'])
|
||||
trading_days_held = len(common_dates[(common_dates >= entry_dt) & (common_dates <= date)])
|
||||
asset['holding_days'] = trading_days_held
|
||||
|
||||
# ETF累计收益
|
||||
if hs['entry_price_etf'] and hs['entry_price_etf'] > 0:
|
||||
cur = etf_close_hfq_aligned[code].get(date) if code in etf_close_hfq_aligned else None
|
||||
if cur and pd.notna(cur):
|
||||
asset['cum_return_etf'] = safe_val(float(cur) / hs['entry_price_etf'] - 1, 4)
|
||||
else:
|
||||
asset['cum_return_etf'] = None
|
||||
else:
|
||||
asset['cum_return_etf'] = None
|
||||
|
||||
# 指数累计收益
|
||||
if hs['entry_price_idx'] and hs['entry_price_idx'] > 0:
|
||||
cur = index_close_aligned[code].get(date) if code in index_close_aligned else None
|
||||
if cur and pd.notna(cur):
|
||||
asset['cum_return_idx'] = safe_val(float(cur) / hs['entry_price_idx'] - 1, 4)
|
||||
else:
|
||||
asset['cum_return_idx'] = None
|
||||
else:
|
||||
asset['cum_return_idx'] = None
|
||||
else:
|
||||
asset['entry_date'] = None
|
||||
asset['entry_price_etf'] = None
|
||||
asset['entry_price_idx'] = None
|
||||
asset['holding_days'] = 0
|
||||
asset['cum_return_etf'] = None
|
||||
asset['cum_return_idx'] = None
|
||||
|
||||
assets[code] = asset
|
||||
|
||||
# 构建当天记录
|
||||
nav_val = nav_series.loc[date] if date in nav_series.index else None
|
||||
ret_val = daily_ret_series.loc[date] if date in daily_ret_series.index else None
|
||||
|
||||
day_record = {
|
||||
'date': date.strftime('%Y-%m-%d'),
|
||||
'nav': safe_val(nav_val, 4),
|
||||
'daily_return': safe_val(ret_val, 6),
|
||||
'is_rebalance': is_rebalance,
|
||||
'holdings': sorted(list(current_holdings)),
|
||||
'added': sorted(added),
|
||||
'removed': sorted(removed),
|
||||
'assets': assets
|
||||
}
|
||||
days_list.append(day_record)
|
||||
prev_holdings = current_holdings
|
||||
|
||||
# 9. 构建元数据
|
||||
codes_meta = {}
|
||||
for code, cfg in CODE_LIST.items():
|
||||
codes_meta[code] = {
|
||||
'name': cfg['name'],
|
||||
'etf': cfg.get('etf'),
|
||||
'market': cfg.get('market')
|
||||
}
|
||||
|
||||
output = {
|
||||
'meta': {
|
||||
'mode': 'B: 指数信号 + ETF收益',
|
||||
'start_date': common_dates[0].strftime('%Y-%m-%d'),
|
||||
'end_date': common_dates[-1].strftime('%Y-%m-%d'),
|
||||
'total_days': len(common_dates),
|
||||
'select_num': SELECT_NUM,
|
||||
'n_days': N_DAYS,
|
||||
'trade_cost': TRADE_COST,
|
||||
'bond_threshold': {
|
||||
'enabled': BOND_THRESHOLD.get('enabled', False),
|
||||
'bond_code': BOND_CODE,
|
||||
'ratio': BOND_RATIO
|
||||
},
|
||||
'codes': codes_meta
|
||||
},
|
||||
'days': days_list
|
||||
}
|
||||
|
||||
# 10. 输出
|
||||
output_path = project_root / 'results' / 'backtest_detail.json'
|
||||
print(f"\n[8] 写入 {output_path}...")
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(output, f, ensure_ascii=False)
|
||||
|
||||
file_size_mb = output_path.stat().st_size / 1024 / 1024
|
||||
print(f" 大小: {file_size_mb:.1f} MB")
|
||||
print(f" 天数: {len(days_list)}")
|
||||
print(f" 标的: {len(valid_codes)}")
|
||||
print(" 完成!")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,216 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
使用新框架数据生成原引擎格式的报告
|
||||
|
||||
用法:
|
||||
python scripts/generate_legacy_report.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import yaml
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# 添加项目根目录到 sys.path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# 导入新框架
|
||||
from strategies.rotation.strategy import RotationStrategy
|
||||
|
||||
# 导入原引擎报告生成模块
|
||||
archive_path = project_root / 'archive' / 'legacy_core'
|
||||
sys.path.insert(0, str(archive_path))
|
||||
from report import generate_performance_report
|
||||
from core.common.utils import calculate_cagr, calculate_max_drawdown, calculate_sharpe
|
||||
|
||||
|
||||
def run_with_legacy_report():
|
||||
"""运行新框架回测并生成原引擎格式报告"""
|
||||
|
||||
# 加载配置
|
||||
config_path = 'strategies/rotation/config.yaml'
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
# 新框架回测
|
||||
print("=" * 60)
|
||||
print(" ETF轮动策略 回测系统 (新框架)")
|
||||
print("=" * 60)
|
||||
|
||||
strategy = RotationStrategy.from_yaml(config_path)
|
||||
data = strategy.get_data()
|
||||
|
||||
# 计算因子
|
||||
print("\n计算因子...")
|
||||
factor_df = strategy.compute_factors(data)
|
||||
|
||||
# 生成信号
|
||||
print("\n生成信号...")
|
||||
signals = strategy.generate_signals(factor_df)
|
||||
|
||||
# 执行回测
|
||||
print("\n执行回测...")
|
||||
result = strategy.run_backtest(data=data)
|
||||
|
||||
# 准备原引擎格式的数据
|
||||
backtest_result = result['result'].copy()
|
||||
|
||||
if backtest_result is None:
|
||||
print("回测失败,无法生成报告")
|
||||
return
|
||||
|
||||
# 重命名列以匹配原引擎格式
|
||||
backtest_result['轮动策略净值'] = backtest_result['策略净值']
|
||||
backtest_result['轮动策略日收益率'] = backtest_result['策略日收益率']
|
||||
|
||||
# 1. 基准净值和基准日收益率
|
||||
benchmark_data = data.get('benchmark_data')
|
||||
if benchmark_data is not None:
|
||||
# benchmark_data 已经是 Series(close 价格)
|
||||
if isinstance(benchmark_data, pd.Series):
|
||||
benchmark_close = benchmark_data
|
||||
elif isinstance(benchmark_data, pd.DataFrame):
|
||||
benchmark_close = benchmark_data['close'] if 'close' in benchmark_data.columns else benchmark_data.iloc[:, 0]
|
||||
else:
|
||||
benchmark_close = None
|
||||
|
||||
if benchmark_close is not None and len(benchmark_close) > 0:
|
||||
# 对齐基准数据到回测日期
|
||||
benchmark_close_aligned = benchmark_close.reindex(backtest_result.index, method='ffill')
|
||||
|
||||
# 计算基准净值
|
||||
benchmark_nav = (1 + benchmark_close_aligned.pct_change()).cumprod()
|
||||
benchmark_nav = benchmark_nav / benchmark_nav.dropna().iloc[0] # 归一化起点为1
|
||||
|
||||
backtest_result['基准净值'] = benchmark_nav.values
|
||||
backtest_result['基准日收益率'] = benchmark_close_aligned.pct_change().values
|
||||
|
||||
# 2. 各标的净值(指数价格)- 使用index_data而非index_close
|
||||
# index_close可能对齐有问题,直接从index_data获取
|
||||
index_data = data.get('index_data')
|
||||
valid_codes = data['valid_codes']
|
||||
|
||||
for code in valid_codes:
|
||||
if index_data is not None and code in index_data:
|
||||
# 从原始OHLCV数据获取close价格
|
||||
price_df = index_data[code]
|
||||
if 'close' in price_df.columns:
|
||||
price_series = price_df['close']
|
||||
else:
|
||||
price_series = price_df.iloc[:, 0] # 取第一列
|
||||
|
||||
# 对齐到回测日期
|
||||
price_aligned = price_series.reindex(backtest_result.index, method='ffill')
|
||||
|
||||
# 处理最后几天的NaN(用最后一个有效值填充)
|
||||
price_aligned = price_aligned.ffill() # 前向填充剩余NaN
|
||||
|
||||
# 计算该标的的净值曲线
|
||||
nav_series = (1 + price_aligned.pct_change()).cumprod()
|
||||
first_valid = nav_series.dropna().iloc[0] if len(nav_series.dropna()) > 0 else 1
|
||||
nav_series = nav_series / first_valid # 归一化起点为1
|
||||
|
||||
backtest_result[f'净值_{code}'] = nav_series.values
|
||||
backtest_result[code] = price_aligned.values # 当前价格
|
||||
|
||||
# 3. 得分列(从factor_df获取)
|
||||
for code in valid_codes:
|
||||
if code in factor_df.columns:
|
||||
scores_aligned = factor_df[code].reindex(backtest_result.index, method='ffill')
|
||||
backtest_result[f'得分_{code}'] = scores_aligned.values
|
||||
|
||||
# 4. 信号列(中文名)
|
||||
backtest_result['信号'] = backtest_result['signal']
|
||||
|
||||
# 构建code_name_map和code_config
|
||||
code_config = config.get('code_list', {})
|
||||
code_name_map = {code: cfg.get('name', code) for code, cfg in code_config.items()}
|
||||
|
||||
# 准备ETF价格和净值数据(用于溢价率计算)
|
||||
etf_data = data.get('etf_data')
|
||||
etf_nav_data = data.get('etf_nav_data')
|
||||
|
||||
# ETF数据需要用ETF代码作为列名
|
||||
etf_price_data = None
|
||||
etf_nav_data_raw = None
|
||||
|
||||
if etf_data is not None:
|
||||
# 转换列名:指数代码 -> ETF代码(通过etf_code_map)
|
||||
# 并对齐到回测日期
|
||||
etf_code_map = data.get('etf_code_map', {})
|
||||
etf_price_data = pd.DataFrame(index=backtest_result.index)
|
||||
for idx_code, etf_code in etf_code_map.items():
|
||||
if etf_code in etf_data.columns:
|
||||
# 对齐ETF价格数据到回测日期
|
||||
price_aligned = etf_data[etf_code].reindex(backtest_result.index, method='ffill')
|
||||
etf_price_data[idx_code] = price_aligned.values
|
||||
|
||||
# ETF净值数据现在是字典格式 {etf_code: DataFrame}
|
||||
etf_nav_data_raw = None
|
||||
|
||||
if etf_nav_data and len(etf_nav_data) > 0:
|
||||
# etf_nav_data 是字典 {etf_code: DataFrame}
|
||||
etf_nav_data_raw = pd.DataFrame(index=backtest_result.index)
|
||||
for idx_code, etf_code in etf_code_map.items():
|
||||
if etf_code in etf_nav_data:
|
||||
# 从字典中获取净值 DataFrame
|
||||
nav_df = etf_nav_data[etf_code]
|
||||
if isinstance(nav_df, pd.DataFrame) and 'nav' in nav_df.columns:
|
||||
nav_series = nav_df['nav']
|
||||
elif isinstance(nav_df, pd.DataFrame):
|
||||
nav_series = nav_df.iloc[:, 0]
|
||||
elif isinstance(nav_df, pd.Series):
|
||||
nav_series = nav_df
|
||||
else:
|
||||
continue
|
||||
# 对齐净值数据到回测日期(使用ffill处理日期差异)
|
||||
# 先去除重复日期
|
||||
if nav_series.index.has_duplicates:
|
||||
nav_series = nav_series[~nav_series.index.duplicated(keep='last')]
|
||||
# 确保 backtest_result.index 无重复
|
||||
target_index = backtest_result.index
|
||||
if target_index.has_duplicates:
|
||||
target_index = target_index[~target_index.duplicated(keep='last')]
|
||||
nav_aligned = nav_series.reindex(target_index, method='ffill')
|
||||
etf_nav_data_raw[idx_code] = nav_aligned.values
|
||||
|
||||
# 生成原引擎格式的报告
|
||||
print("\n" + "=" * 60)
|
||||
print(" 生成原引擎格式报告")
|
||||
print("=" * 60)
|
||||
|
||||
save_path = 'results/rotation_legacy'
|
||||
os.makedirs('results', exist_ok=True)
|
||||
|
||||
# 获取index_close用于报告图表绘制
|
||||
index_close = data.get('index_close')
|
||||
|
||||
metrics = generate_performance_report(
|
||||
backtest_result=backtest_result,
|
||||
code_list=valid_codes,
|
||||
code_name_map=code_name_map,
|
||||
benchmark_name=config.get('benchmark_name', '沪深300指数'),
|
||||
save_path=save_path,
|
||||
select_num=config.get('select_num', 3),
|
||||
code_config=code_config,
|
||||
index_data=index_close,
|
||||
etf_price_data=etf_price_data,
|
||||
etf_nav_data_raw=etf_nav_data_raw,
|
||||
)
|
||||
|
||||
print(f"\n报告文件已生成:")
|
||||
print(f" - {save_path}_chart.png")
|
||||
print(f" - {save_path}_metrics.json")
|
||||
print(f" - {save_path}_nav.csv")
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_with_legacy_report()
|
||||
@@ -1,348 +0,0 @@
|
||||
"""
|
||||
获取 A 股交易日历脚本
|
||||
|
||||
使用 Flask API 交易日历服务获取 A 股交易日历
|
||||
支持多市场、多年份的交易日查询
|
||||
|
||||
用法:
|
||||
python scripts/get_trading_calendar.py
|
||||
python scripts/get_trading_calendar.py --year 2024
|
||||
python scripts/get_trading_calendar.py --start 2024-01-01 --end 2024-12-31
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# 加载环境变量
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# 导入 Flask API 数据源
|
||||
from datasource.flask_api_source import FlaskAPIDataSource
|
||||
|
||||
|
||||
def get_calendar_for_year(source: FlaskAPIDataSource, year: int, market: str = 'A'):
|
||||
"""
|
||||
获取指定年份的交易日历
|
||||
|
||||
Args:
|
||||
source: Flask API 数据源实例
|
||||
year: 年份(如 2024)
|
||||
market: 市场代码('A', 'US', 'HK')
|
||||
|
||||
Returns:
|
||||
pd.DatetimeIndex: 交易日序列
|
||||
"""
|
||||
start_date = f"{year}-01-01"
|
||||
end_date = f"{year}-12-31"
|
||||
|
||||
print(f"\n获取 {year} 年 {market} 市场交易日历...")
|
||||
|
||||
trading_dates = source.get_trading_calendar(
|
||||
market=market,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
if trading_dates is None or len(trading_dates) == 0:
|
||||
print(f"✗ {year} 年 {market} 市场无交易日数据")
|
||||
return None
|
||||
|
||||
return trading_dates
|
||||
|
||||
|
||||
def analyze_calendar(trading_dates: pd.DatetimeIndex, year: int):
|
||||
"""
|
||||
分析交易日历统计信息
|
||||
|
||||
Args:
|
||||
trading_dates: 交易日序列
|
||||
year: 年份
|
||||
"""
|
||||
if trading_dates is None or len(trading_dates) == 0:
|
||||
return
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"{year} 年 A 股交易日历分析")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
# 基本统计
|
||||
total_days = len(trading_dates)
|
||||
print(f"\n基本统计:")
|
||||
print(f" 总交易日: {total_days} 天")
|
||||
print(f" 起始日期: {trading_dates.min().strftime('%Y-%m-%d')}")
|
||||
print(f" 结束日期: {trading_dates.max().strftime('%Y-%m-%d')}")
|
||||
|
||||
# 按月份统计
|
||||
print(f"\n按月份统计:")
|
||||
monthly_counts = {}
|
||||
for date in trading_dates:
|
||||
month = date.month
|
||||
monthly_counts[month] = monthly_counts.get(month, 0) + 1
|
||||
|
||||
for month in range(1, 13):
|
||||
count = monthly_counts.get(month, 0)
|
||||
month_name = datetime(2024, month, 1).strftime('%B')
|
||||
print(f" {month:02d}月 ({month_name}): {count} 天")
|
||||
|
||||
# 按季度统计
|
||||
print(f"\n按季度统计:")
|
||||
quarterly_counts = {1: 0, 2: 0, 3: 0, 4: 0}
|
||||
for date in trading_dates:
|
||||
quarter = (date.month - 1) // 3 + 1
|
||||
quarterly_counts[quarter] += 1
|
||||
|
||||
for quarter, count in quarterly_counts.items():
|
||||
print(f" Q{quarter}: {count} 天")
|
||||
|
||||
# 特殊日期统计
|
||||
print(f"\n特殊日期:")
|
||||
first_date = trading_dates.min()
|
||||
last_date = trading_dates.max()
|
||||
print(f" 首个交易日: {first_date.strftime('%Y-%m-%d')} ({first_date.strftime('%A')})")
|
||||
print(f" 最后交易日: {last_date.strftime('%Y-%m-%d')} ({last_date.strftime('%A')})")
|
||||
|
||||
# 查找节假日后的首个交易日(通过间隔判断)
|
||||
gaps = []
|
||||
for i in range(1, len(trading_dates)):
|
||||
prev_date = trading_dates[i-1]
|
||||
curr_date = trading_dates[i]
|
||||
gap_days = (curr_date - prev_date).days
|
||||
if gap_days > 3: # 超过3天视为可能节假日
|
||||
gaps.append({
|
||||
'prev': prev_date,
|
||||
'curr': curr_date,
|
||||
'gap': gap_days
|
||||
})
|
||||
|
||||
if gaps:
|
||||
print(f"\n可能的节假日(间隔 > 3天):")
|
||||
for gap_info in gaps[:5]: # 只显示前5个
|
||||
print(f" {gap_info['prev'].strftime('%Y-%m-%d')} → {gap_info['curr'].strftime('%Y-%m-%d')} "
|
||||
f"(间隔 {gap_info['gap']} 天)")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
|
||||
|
||||
def compare_markets(source: FlaskAPIDataSource, year: int):
|
||||
"""
|
||||
比较不同市场的交易日历
|
||||
|
||||
Args:
|
||||
source: Flask API 数据源实例
|
||||
year: 年份
|
||||
"""
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"{year} 年不同市场交易日历对比")
|
||||
print(f"{'=' * 60}")
|
||||
|
||||
markets = {
|
||||
'A': 'A股(上交所/深交所)',
|
||||
'US': '美股(NYSE)',
|
||||
'HK': '港股(HKEX)'
|
||||
}
|
||||
|
||||
results = {}
|
||||
for market_code, market_name in markets.items():
|
||||
print(f"\n获取 {market_name} 交易日历...")
|
||||
trading_dates = get_calendar_for_year(source, year, market_code)
|
||||
|
||||
if trading_dates is not None and len(trading_dates) > 0:
|
||||
results[market_code] = {
|
||||
'name': market_name,
|
||||
'dates': trading_dates,
|
||||
'count': len(trading_dates)
|
||||
}
|
||||
|
||||
# 对比统计
|
||||
print(f"\n交易日对比:")
|
||||
print(f"{'市场':<20} {'交易日数':<10} {'起始日期':<12} {'结束日期':<12}")
|
||||
print("-" * 60)
|
||||
|
||||
for market_code, data in results.items():
|
||||
print(f"{data['name']:<20} {data['count']:<10} "
|
||||
f"{data['dates'].min().strftime('%Y-%m-%d'):<12} "
|
||||
f"{data['dates'].max().strftime('%Y-%m-%d'):<12}")
|
||||
|
||||
# 计算差异
|
||||
if len(results) >= 2:
|
||||
print(f"\n交易日差异:")
|
||||
market_codes = list(results.keys())
|
||||
for i in range(len(market_codes)):
|
||||
for j in range(i+1, len(market_codes)):
|
||||
m1 = market_codes[i]
|
||||
m2 = market_codes[j]
|
||||
diff = results[m1]['count'] - results[m2]['count']
|
||||
print(f" {results[m1]['name']} vs {results[m2]['name']}: "
|
||||
f"相差 {abs(diff)} 天 ({'+' if diff > 0 else ''}{diff})")
|
||||
|
||||
print(f"\n{'=' * 60}")
|
||||
|
||||
|
||||
def show_recent_dates(trading_dates: pd.DatetimeIndex, n: int = 10):
|
||||
"""
|
||||
显示最近的交易日
|
||||
|
||||
Args:
|
||||
trading_dates: 交易日序列
|
||||
n: 显示数量
|
||||
"""
|
||||
if trading_dates is None or len(trading_dates) == 0:
|
||||
return
|
||||
|
||||
print(f"\n最近 {n} 个交易日:")
|
||||
recent_dates = trading_dates[-n:] if len(trading_dates) >= n else trading_dates
|
||||
|
||||
for date in recent_dates:
|
||||
weekday = date.strftime('%A')
|
||||
print(f" {date.strftime('%Y-%m-%d')} ({weekday})")
|
||||
|
||||
|
||||
def export_calendar(trading_dates: pd.DatetimeIndex, output_path: str, year: int):
|
||||
"""
|
||||
导出交易日历到 CSV
|
||||
|
||||
Args:
|
||||
trading_dates: 交易日序列
|
||||
output_path: 输出路径
|
||||
year: 年份
|
||||
"""
|
||||
if trading_dates is None or len(trading_dates) == 0:
|
||||
return
|
||||
|
||||
# 创建 DataFrame
|
||||
df = pd.DataFrame({
|
||||
'date': trading_dates,
|
||||
'year': trading_dates.year,
|
||||
'month': trading_dates.month,
|
||||
'quarter': (trading_dates.month - 1) // 3 + 1,
|
||||
'weekday': [d.strftime('%A') for d in trading_dates]
|
||||
})
|
||||
|
||||
# 导出到 CSV
|
||||
filename = f"{output_path}/trading_calendar_A_{year}.csv"
|
||||
df.to_csv(filename, index=False)
|
||||
print(f"\n✓ 交易日历已导出到: {filename}")
|
||||
print(f" 文件包含 {len(df)} 条记录")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='获取 A 股交易日历')
|
||||
|
||||
parser.add_argument(
|
||||
'--year',
|
||||
type=int,
|
||||
default=datetime.now().year,
|
||||
help='年份(默认当前年份)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--start',
|
||||
type=str,
|
||||
help='起始日期 YYYY-MM-DD'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--end',
|
||||
type=str,
|
||||
help='结束日期 YYYY-MM-DD'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--market',
|
||||
type=str,
|
||||
default='A',
|
||||
choices=['A', 'US', 'HK'],
|
||||
help='市场代码(A=A股, US=美股, HK=港股)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--compare',
|
||||
action='store_true',
|
||||
help='对比不同市场交易日历'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--export',
|
||||
action='store_true',
|
||||
help='导出交易日历到 CSV'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--output',
|
||||
type=str,
|
||||
default='data',
|
||||
help='导出目录(默认 data)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化 Flask API 数据源
|
||||
print("\n初始化 Flask API 数据源...")
|
||||
source = FlaskAPIDataSource()
|
||||
|
||||
# 检查服务健康状态
|
||||
health = source.get_health()
|
||||
if health.get('status') != 'healthy':
|
||||
print(f"✗ Flask API 服务不可用: {health}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"✓ Flask API 服务可用 ({source.base_url})")
|
||||
|
||||
# 获取交易日历信息
|
||||
calendar_info = source.get_calendar_info()
|
||||
if 'error' not in calendar_info:
|
||||
print(f"\n交易日历服务信息:")
|
||||
print(f" 支持市场: {', '.join(calendar_info.get('markets', []))}")
|
||||
print(f" 数据源: {calendar_info.get('source', 'pandas_market_calendars')}")
|
||||
|
||||
# 执行不同功能
|
||||
if args.compare:
|
||||
# 对比不同市场
|
||||
compare_markets(source, args.year)
|
||||
|
||||
elif args.start and args.end:
|
||||
# 自定义日期范围
|
||||
print(f"\n获取 {args.market} 市场交易日历 ({args.start} ~ {args.end})...")
|
||||
trading_dates = source.get_trading_calendar(
|
||||
market=args.market,
|
||||
start_date=args.start,
|
||||
end_date=args.end
|
||||
)
|
||||
|
||||
if trading_dates is not None:
|
||||
print(f"✓ 获取到 {len(trading_dates)} 个交易日")
|
||||
show_recent_dates(trading_dates)
|
||||
|
||||
if args.export:
|
||||
export_calendar(trading_dates, args.output, args.year)
|
||||
|
||||
else:
|
||||
# 获取指定年份交易日历
|
||||
trading_dates = get_calendar_for_year(source, args.year, args.market)
|
||||
|
||||
if trading_dates is not None:
|
||||
# 分析统计
|
||||
analyze_calendar(trading_dates, args.year)
|
||||
|
||||
# 显示最近交易日
|
||||
show_recent_dates(trading_dates)
|
||||
|
||||
# 导出
|
||||
if args.export:
|
||||
export_calendar(trading_dates, args.output, args.year)
|
||||
|
||||
print("\n✓ 完成!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
轮动策略回测入口脚本
|
||||
|
||||
用法:
|
||||
python scripts/run_rotation.py --config strategies/rotation/config.yaml
|
||||
python scripts/run_rotation.py --config strategies/rotation/config.yaml --save-path results/report
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# 添加项目根目录到路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
# 加载环境变量
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from strategies.rotation.strategy import RotationStrategy
|
||||
|
||||
|
||||
def load_config(config_path: str) -> dict:
|
||||
"""加载配置"""
|
||||
import yaml
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='ETF轮动策略回测')
|
||||
parser.add_argument(
|
||||
'--config',
|
||||
type=str,
|
||||
default='strategies/rotation/config.yaml',
|
||||
help='配置文件路径'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--save-path',
|
||||
type=str,
|
||||
default=None,
|
||||
help='报告保存路径前缀'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--no-api',
|
||||
action='store_true',
|
||||
help='不使用Flask API,使用本地数据源'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
start_time = datetime.now()
|
||||
|
||||
print("=" * 60)
|
||||
print(" ETF轮动策略 回测系统")
|
||||
print("=" * 60)
|
||||
|
||||
# 加载配置
|
||||
print(f"\n加载配置: {args.config}")
|
||||
config = load_config(args.config)
|
||||
|
||||
# 显示配置摘要
|
||||
code_list = list(config.get('code_list', {}).keys())
|
||||
print(f"候选标的: {len(code_list)} 只")
|
||||
print(f"回测区间: {config.get('start_date', 'N/A')} ~ {config.get('end_date', 'N/A')}")
|
||||
print(f"因子类型: {config.get('factor_type', 'momentum')}")
|
||||
print(f"窗口天数: {config.get('n_days', 25)}")
|
||||
print(f"选股数量: {config.get('select_num', 3)}")
|
||||
print(f"调仓周期: {config.get('rebalance_days', 1)} 天")
|
||||
print(f"交易成本: {config.get('trade_cost', 0.001):.2%}")
|
||||
|
||||
# 初始化策略
|
||||
print("\n初始化策略...")
|
||||
strategy = RotationStrategy.from_yaml(args.config)
|
||||
|
||||
# 设置保存路径
|
||||
if args.save_path is None:
|
||||
report_date = datetime.now().strftime('%Y%m%d')
|
||||
args.save_path = f"results/report_{report_date}"
|
||||
|
||||
# 执行回测
|
||||
print("\n" + "=" * 60)
|
||||
print("开始回测...")
|
||||
print("=" * 60)
|
||||
|
||||
# 使用Flask API或本地数据源
|
||||
use_flask_api = not args.no_api
|
||||
data = strategy.get_data(use_flask_api=use_flask_api)
|
||||
|
||||
result = strategy.run_backtest(data=data, save_path=args.save_path)
|
||||
|
||||
# 输出结果
|
||||
if result.get('result') is not None:
|
||||
final_nav = result['result']['策略净值'].iloc[-1]
|
||||
total_return = (final_nav - 1) * 100
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("回测完成!")
|
||||
print("=" * 60)
|
||||
print(f"最终净值: {final_nav:.4f}")
|
||||
print(f"累计收益: {total_return:.2f}%")
|
||||
print(f"调仓次数: {len(result.get('rebalance_events', []))} 次")
|
||||
print(f"报告保存: {args.save_path}_*.csv")
|
||||
|
||||
elapsed = datetime.now() - start_time
|
||||
print(f"\n总耗时: {elapsed.total_seconds():.1f}秒")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user