diff --git a/framework_v2/config/rotation_global.yaml b/framework_v2/config/rotation_global.yaml new file mode 100644 index 0000000..fe0e799 --- /dev/null +++ b/framework_v2/config/rotation_global.yaml @@ -0,0 +1,268 @@ +# 跨市场轮动策略配置(扁平化设计) +# +# 配置版本: 2.0.0 +# 最后更新: 2024-04-16 +# 策略名称: rotation_global +# 描述: 全球资产大类轮动 - 扁平化资产池设计 + +# ============================================================ +# 元数据 +# ============================================================ +metadata: + version: "2.0.0" + strategy: "rotation_global" + description: "全球资产大类轮动策略 V2 - 扁平化资产池" + last_updated: "2024-04-16" + +# ============================================================ +# 资产池配置(扁平化设计) +# ============================================================ +asset_pools: + assets: + # ============================================================ + # 美股指数(通过 A 股 ETF 交易) + # ============================================================ + "NDX": + name: "纳指100" + group: "US_TECH" + signal_source: "NDX" # 纳指信号 + trade_source: "513100.SH" # A股ETF交易 + description: "纳斯达克100指数,科技股代表" + + "SPX": + name: "标普500" + group: "US_TECH" + signal_source: "SPX" + trade_source: "513500.SH" + description: "标普500指数,美股大盘" + + # ============================================================ + # A股指数(直接交易 ETF) + # ============================================================ + "399006.SZ": + name: "创业板指" + group: "CN_GROWTH" + signal_source: "399006.SZ" + trade_source: "159915.SZ" + description: "创业板指数,成长股代表" + + "000300.SH": + name: "沪深300" + group: "CN_GROWTH" + signal_source: "000300.SH" + trade_source: "510300.SH" + description: "沪深300指数,大盘蓝筹" + + "H30269.CSI": + name: "中证红利低波" + group: "CN_GROWTH" + signal_source: "H30269.CSI" + trade_source: "512890.SH" + description: "红利低波指数,价值股代表" + + # ============================================================ + # 日本股市(通过 A 股 ETF 交易) + # ============================================================ + "N225": + name: "日经225" + group: "JP_BROAD" + signal_source: "N225" + trade_source: "513520.SH" + description: "日经225指数,日本股市" + + # ============================================================ + # 欧洲股市(通过 A 股 ETF 交易) + # ============================================================ + "GDAXI": + name: "德国DAX" + group: "EU_BROAD" + signal_source: "GDAXI" + trade_source: "513030.SH" + description: "德国DAX指数,欧洲股市" + + # ============================================================ + # 港股(通过 A 股 ETF 交易) + # ============================================================ + "HSI": + name: "恒生指数" + group: "HK_TECH" + signal_source: "HSI" + trade_source: "159920.SZ" + description: "恒生指数,香港股市" + + "HSTECH.HK": + name: "恒生科技" + group: "HK_TECH" + signal_source: "HSTECH.HK" + trade_source: "513130.SH" + description: "恒生科技指数,港股科技" + + # ============================================================ + # 商品(国际期货信号 → A股ETF交易) + # ============================================================ + "GC=F": + name: "黄金" + group: "COMMODITY" + signal_source: "GC=F" # COMEX黄金期货 + trade_source: "518880.SH" # A股黄金ETF + description: "COMEX黄金期货,避险资产" + + "CL=F": + name: "原油" + group: "COMMODITY" + signal_source: "CL=F" # WTI原油期货 + trade_source: "160723.SZ" # A股原油基金 + description: "WTI原油期货,能源商品" + + "HG=F": + name: "有色金属" + group: "COMMODITY" + signal_source: "HG=F" # COMEX铜期货 + trade_source: "159980.SZ" # A股有色ETF + description: "COMEX铜期货,工业金属" + + # ============================================================ + # 固定收益(直接交易指数) + # ============================================================ + "931862.CSI": + name: "短债指数" + group: "FIXED_INCOME" + signal_source: "931862.CSI" + trade_source: "931862.CSI" # 直接交易指数(无ETF) + description: "中证0-9个月国债指数,久期<1年,防御配置" + + # ============================================================ + # 加密货币(未来扩展示例) + # ============================================================ + # "BTC": + # name: "比特币" + # group: "CRYPTO" + # signal_source: "BTC" + # trade_source: "BTC" + # description: "比特币,数字黄金" + + # ============================================================ + # 外汇(未来扩展示例) + # ============================================================ + # "EURUSD": + # name: "欧元/美元" + # group: "FOREX" + # signal_source: "EURUSD" + # trade_source: "EURUSD" + # description: "欧元/美元汇率" + +# ============================================================ +# 基准配置 +# ============================================================ +benchmark: + code: "000300.SH" + name: "沪深300" + +# ============================================================ +# 回测配置 +# ============================================================ +backtest: + start_date: "2020-01-01" + # end_date: null # null 表示至今 + +# ============================================================ +# 因子配置 +# ============================================================ +factor: + type: "weighted_momentum" # 因子类型: momentum / slope_r2 / weighted_momentum + n_days: 25 # 动量窗口期(5-250天) + + # 动态周期参数(可选) + auto_day: false # 是否启用动态周期 + min_days: 20 # 最小周期 + max_days: 60 # 最大周期 + +# ============================================================ +# 轮动配置 +# ============================================================ +rotation: + # ============================================================ + # 模式 1:全局选股(默认) + # ============================================================ + select_num: 5 # 全局选 Top-5 + diversified: false # 不分散化 + + # ============================================================ + # 模式 2:分散化选股(取消注释启用) + # ============================================================ + # diversified: true # 启用分散化 + # diversification_groups: # 按市场分组选股 + # - group: "US_TECH" + # select_num: 1 # 美股选 1 只 + # - group: "CN_GROWTH" + # select_num: 1 # A股选 1 只 + # - group: "JP_BROAD" + # select_num: 1 # 日本选 1 只 + # - group: "EU_BROAD" + # select_num: 1 # 欧洲选 1 只 + # - group: "HK_TECH" + # select_num: 1 # 港股选 1 只 + # - group: "COMMODITY" + # select_num: 1 # 商品选 1 只 + # - group: "FIXED_INCOME" + # select_num: 1 # 债券选 1 只 + + # 阈值配置(统一 V2/V3) + threshold: + mode: "dynamic" # 阈值模式: fixed / dynamic + fixed_value: 0.0 # 固定阈值(mode=fixed时使用) + + # 动态阈值配置(mode=dynamic时使用) + dynamic: + reference: "931862.CSI" # 参考标的(短债指数) + ratio: 1.0 # 阈值 = 短债动量 × ratio + fallback_enabled: true # 参考不可用时是否回退 + fallback_value: 0.0 # 回退值 + +# ============================================================ +# 调仓配置 +# ============================================================ +rebalance: + min_hold_days: 1 # 最低持有天数(1-30) + score_threshold: 0.0 # 调仓得分阈值(0-0.5,表示%) + trade_cost: 0.001 # 单次换仓成本(0-0.01,即 0.1%) + +# ============================================================ +# 溢价控制配置 +# ============================================================ +premium_control: + enabled: true # 是否启用溢价控制 + default_threshold: 0.10 # 默认溢价阈值(10%) + mode: "filter" # 控制模式: filter(排除)/ penalize(降权) + penalty_factor: 0.5 # 降权惩罚系数 + + # 按市场覆盖配置 + market_overrides: + CN_EQUITY: # A股 ETF + enabled: false # 不启用(溢价通常 < 0.5%) + HK_EQUITY: # 港股 ETF + enabled: true + threshold: 0.10 # 阈值 10% + US_EQUITY: # 美股 ETF + enabled: true + threshold: 0.10 # 阈值 10% + JP_EQUITY: # 日本 ETF + enabled: true + threshold: 0.10 # 阈值 10% + EU_EQUITY: # 欧洲 ETF + enabled: true + threshold: 0.10 # 阈值 10% + COMMODITY: # 商品 ETF + enabled: false # 不启用 + +# ============================================================ +# 数据配置 +# ============================================================ +data: + # 数据源列表(按优先级排序) + sources: + # 主数据源:Flask API + - type: "flask_api" + enabled: true + url: "${FLASK_API_URL}" # 从环境变量读取 + timeout: 120 diff --git a/framework_v2/tests/test_config.py b/framework_v2/tests/test_config.py new file mode 100644 index 0000000..899e446 --- /dev/null +++ b/framework_v2/tests/test_config.py @@ -0,0 +1,285 @@ +""" +测试配置加载和验证 + +验证: +1. 配置文件加载 +2. Pydantic Schema 验证 +3. 环境变量替换 +4. 错误处理 +""" + +import sys +from pathlib import Path +import os + +# 添加项目根目录到路径 +project_root = Path(__file__).parent.parent.parent +if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +from framework_v2.config import load_config, ConfigLoader +from framework_v2.config.schemas import RotationStrategyConfig, MarketType + + +def test_load_config(): + """测试 1: 加载配置文件""" + print("\n" + "=" * 70) + print(" 测试 1: 加载配置文件") + print("=" * 70) + + # 设置环境变量(模拟) + os.environ['FLASK_API_URL'] = 'https://k3s.tokenpluse.xyz' + os.environ['TUSHARE_TOKEN'] = 'test_token_123' + + # 加载配置 + config = load_config('rotation_example.yaml') + + print(f"\n✓ 配置加载成功") + print(f" 版本: {config.metadata.version}") + print(f" 策略: {config.metadata.strategy}") + print(f" 资产池: {len(config.asset_pools.equity)} 股票, " + f"{len(config.asset_pools.commodity)} 商品, " + f"{len(config.asset_pools.fixed_income)} 债券") + + # 验证基本字段 + assert config.metadata.version == "1.0.0" + assert config.factor.n_days == 25 + assert config.rotation.select_num == 3 + + print("\n✓ 测试通过") + + +def test_asset_pools(): + """测试 2: 资产池配置""" + print("\n" + "=" * 70) + print(" 测试 2: 资产池配置") + print("=" * 70) + + config = load_config('rotation_example.yaml') + + # 验证股票资产 + print(f"\n股票资产 ({len(config.asset_pools.equity)} 只):") + for code, asset in config.asset_pools.equity.items(): + print(f" {code}: {asset.name} ({asset.market.value})") + if asset.etf: + print(f" ETF: {asset.etf}") + + # 验证商品资产 + print(f"\n商品资产 ({len(config.asset_pools.commodity)} 只):") + for code, asset in config.asset_pools.commodity.items(): + print(f" {code}: {asset.name}") + + # 验证固定收益 + print(f"\n固定收益 ({len(config.asset_pools.fixed_income)} 只):") + for code, asset in config.asset_pools.fixed_income.items(): + print(f" {code}: {asset.name} (ETF: {asset.etf})") + + # 验证市场类型 + assert config.asset_pools.equity["399006.SZ"].market == MarketType.CN_EQUITY + assert config.asset_pools.equity["NDX"].market == MarketType.US_EQUITY + assert config.asset_pools.commodity["GC=F"].market == MarketType.COMMODITY + + print("\n✓ 测试通过") + + +def test_threshold_config(): + """测试 3: 阈值配置""" + print("\n" + "=" * 70) + print(" 测试 3: 阈值配置") + print("=" * 70) + + config = load_config('rotation_example.yaml') + + print(f"\n阈值模式: {config.rotation.threshold.mode}") + print(f" 参考标的: {config.rotation.threshold.dynamic.reference}") + print(f" 倍数: {config.rotation.threshold.dynamic.ratio}") + print(f" 回退启用: {config.rotation.threshold.dynamic.fallback_enabled}") + + assert config.rotation.threshold.mode.value == "dynamic" + assert config.rotation.threshold.dynamic.reference == "931862.CSI" + + print("\n✓ 测试通过") + + +def test_data_sources(): + """测试 4: 数据源配置""" + print("\n" + "=" * 70) + print(" 测试 4: 数据源配置") + print("=" * 70) + + config = load_config('rotation_example.yaml') + + print(f"\n数据源 ({len(config.data.sources)} 个):") + for i, source in enumerate(config.data.sources, 1): + print(f" {i}. {source.type.value}") + print(f" 启用: {source.enabled}") + print(f" 超时: {source.timeout}s") + if source.url: + print(f" URL: {source.url}") + + # 验证环境变量替换 + flask_api_source = config.data.sources[0] + assert flask_api_source.url == 'https://k3s.tokenpluse.xyz' + + print("\n✓ 测试通过") + + +def test_validation_errors(): + """测试 5: 验证错误处理""" + print("\n" + "=" * 70) + print(" 测试 5: 验证错误处理") + print("=" * 70) + + # 测试 1: n_days 超出范围 + print("\n[5.1] 测试 n_days 超出范围...") + try: + from framework_v2.config.schemas import FactorConfig + + # n_days = 1000(超出 5-250 范围) + invalid_config = { + "asset_pools": {"equity": {}, "commodity": {}, "fixed_income": {}}, + "benchmark": {"code": "000300.SH", "name": "沪深300"}, + "backtest": {"start_date": "2020-01-01"}, + "factor": {"n_days": 1000}, # 错误:超出范围 + "data": { + "sources": [{"type": "flask_api", "url": "test"}] + } + } + + RotationStrategyConfig(**invalid_config) + print(" ✗ 应该抛出验证错误") + assert False + except Exception as e: + print(f" ✓ 正确捕获验证错误: {type(e).__name__}") + + # 测试 2: 缺少必需字段 + print("\n[5.2] 测试缺少必需字段...") + try: + invalid_config = { + "asset_pools": {"equity": {}, "commodity": {}, "fixed_income": {}}, + # 缺少 benchmark + "backtest": {"start_date": "2020-01-01"}, + "data": { + "sources": [{"type": "flask_api", "url": "test"}] + } + } + + RotationStrategyConfig(**invalid_config) + print(" ✗ 应该抛出验证错误") + assert False + except Exception as e: + print(f" ✓ 正确捕获验证错误: {type(e).__name__}") + + # 测试 3: 环境变量未设置 + print("\n[5.3] 测试环境变量未设置...") + try: + # 删除环境变量 + old_value = os.environ.pop('FLASK_API_URL', None) + + invalid_config = { + "asset_pools": {"equity": {}, "commodity": {}, "fixed_income": {}}, + "benchmark": {"code": "000300.SH", "name": "沪深300"}, + "backtest": {"start_date": "2020-01-01"}, + "data": { + "sources": [{"type": "flask_api", "url": "${FLASK_API_URL}"}] + } + } + + RotationStrategyConfig(**invalid_config) + print(" ✗ 应该抛出验证错误") + assert False + except ValueError as e: + print(f" ✓ 正确捕获环境变量错误: {e}") + finally: + # 恢复环境变量 + if old_value: + os.environ['FLASK_API_URL'] = old_value + + print("\n✓ 测试通过") + + +def test_env_substitution(): + """测试 6: 环境变量替换""" + print("\n" + "=" * 70) + print(" 测试 6: 环境变量替换") + print("=" * 70) + + loader = ConfigLoader() + + # 测试 1: 基本替换 + print("\n[6.1] 基本替换...") + os.environ['TEST_VAR'] = 'test_value' + + config = { + "url": "${TEST_VAR}" + } + result = loader._substitute_env_vars(config) + assert result["url"] == "test_value" + print(f" ✓ ${{TEST_VAR}} → {result['url']}") + + # 测试 2: 默认值 + print("\n[6.2] 默认值...") + config = { + "url": "${NON_EXISTENT_VAR:default_value}" + } + result = loader._substitute_env_vars(config) + assert result["url"] == "default_value" + print(f" ${{NON_EXISTENT_VAR:default_value}} → {result['url']}") + + # 测试 3: 嵌套结构 + print("\n[6.3] 嵌套结构...") + os.environ['API_URL'] = 'https://api.example.com' + + config = { + "data": { + "sources": [ + {"url": "${API_URL}"} + ] + } + } + result = loader._substitute_env_vars(config) + assert result["data"]["sources"][0]["url"] == "https://api.example.com" + print(f" ✓ 嵌套替换成功") + + print("\n✓ 测试通过") + + +if __name__ == "__main__": + print("\n" + "=" * 70) + print(" 配置加载和验证测试") + print("=" * 70) + + tests = [ + ("加载配置文件", test_load_config), + ("资产池配置", test_asset_pools), + ("阈值配置", test_threshold_config), + ("数据源配置", test_data_sources), + ("验证错误处理", test_validation_errors), + ("环境变量替换", test_env_substitution), + ] + + passed = 0 + failed = 0 + + for name, test_func in tests: + try: + test_func() + passed += 1 + except Exception as e: + print(f"\n✗ 测试失败: {name}") + print(f" 错误: {e}") + import traceback + traceback.print_exc() + failed += 1 + + print("\n" + "=" * 70) + print(" 测试总结") + print("=" * 70) + print(f" ✓ 通过 - {passed}") + if failed > 0: + print(f" ✗ 失败 - {failed}") + print(f"\n总计: {passed}/{passed + failed} 通过") + print("=" * 70 + "\n") + + if failed > 0: + sys.exit(1) diff --git a/framework_v2/tests/test_flat_asset_pool.py b/framework_v2/tests/test_flat_asset_pool.py new file mode 100644 index 0000000..2d023fb --- /dev/null +++ b/framework_v2/tests/test_flat_asset_pool.py @@ -0,0 +1,281 @@ +""" +测试扁平化资产池配置 + +验证: +1. 扁平化配置加载 +2. 按市场分组 +3. 信号/交易标的获取 +4. 跨市场映射 +""" + +import sys +from pathlib import Path +import os + +# 添加项目根目录到路径 +project_root = Path(__file__).parent.parent +if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +from framework_v2.config import load_config +from framework_v2.config.schemas import GroupConfig + + +def test_flat_config_load(): + """测试 1: 加载扁平化配置""" + print("\n" + "=" * 70) + print(" 测试 1: 加载扁平化配置") + print("=" * 70) + + # 设置环境变量 + os.environ['FLASK_API_URL'] = 'https://k3s.tokenpluse.xyz' + os.environ['TUSHARE_TOKEN'] = 'test_token' + + # 加载配置 + config = load_config('rotation_global.yaml') + + print(f"\n✓ 配置加载成功") + print(f" 版本: {config.metadata.version}") + print(f" 策略: {config.metadata.strategy}") + print(f" 总标的数: {config.asset_pools.count()}") + + # 验证基本字段 + assert config.metadata.version == "2.0.0" + assert config.asset_pools.count() == 13 # 12 个标的 + + print("\n✓ 测试通过") + + +def test_market_grouping(): + """测试 2: 按市场分组""" + print("\n" + "=" * 70) + print(" 测试 2: 按市场分组") + print("=" * 70) + + config = load_config('rotation_global.yaml') + + # 获取所有市场 + markets = config.asset_pools.groups + print(f"\n市场类型 ({len(markets)} 个):") + for market in markets: + count = config.asset_pools.count(market) + print(f" {market}: {count} 只") + + # 按市场分组 + by_group = config.asset_pools.by_group + print(f"\n市场分组:") + for market, assets in by_group.items(): + print(f"\n {market} ({len(assets)} 只):") + for code, asset in assets.items(): + print(f" {code}: {asset.name}") + + # 验证市场数量 + assert len(markets) == 7 # US_TECH, CN_GROWTH, JP_BROAD, EU_BROAD, HK_TECH, COMMODITY, FIXED_INCOME # US_EQUITY, CN_EQUITY, JP_EQUITY, EU_EQUITY, HK_EQUITY, COMMODITY, FIXED_INCOME + assert config.asset_pools.count('US_TECH') == 2 + assert config.asset_pools.count('CN_GROWTH') == 3 + assert config.asset_pools.count('COMMODITY') == 3 + assert config.asset_pools.count('FIXED_INCOME') == 1 + + print("\n✓ 测试通过") + + +def test_signal_trade_codes(): + """测试 3: 信号和交易标的""" + print("\n" + "=" * 70) + print(" 测试 3: 信号和交易标的") + print("=" * 70) + + config = load_config('rotation_global.yaml') + + # 获取所有信号标的 + signal_codes = config.asset_pools.get_signal_codes() + print(f"\n信号标的 (13 个):") + for code in signal_codes: + print(f" {code}") + + # 获取所有交易标的 + trade_codes = config.asset_pools.get_trade_codes() + print(f"\n交易标的 (13 个):") + for code in trade_codes: + print(f" {code}") + + # 获取特定市场的信号标的 + us_signals = config.asset_pools.get_signal_codes('US_TECH') + print(f"\n美股信号标的: {us_signals}") + + # 验证 + assert len(signal_codes) == 13 + assert len(trade_codes) == 13 + assert 'NDX' in signal_codes + assert '513100.SH' in trade_codes + assert len(us_signals) == 2 + + print("\n✓ 测试通过") + + +def test_signal_to_trade_mapping(): + """测试 4: 信号→交易映射""" + print("\n" + "=" * 70) + print(" 测试 4: 信号→交易映射") + print("=" * 70) + + config = load_config('rotation_global.yaml') + + # 获取映射 + mapping = config.asset_pools.get_signal_to_trade_mapping() + + print(f"\n信号→交易映射:") + for signal, trade in mapping.items(): + asset = config.asset_pools.assets.get(signal) + cross_market = "✗" if asset.signal_source == asset.trade_source else "✓" + print(f" {cross_market} {signal} → {trade}") + + # 验证跨市场标的 + print(f"\n跨市场标的:") + for code, asset in config.asset_pools.assets.items(): + if asset.is_cross_market: + print(f" {code}: {asset.signal_source} → {asset.trade_source}") + + # 验证映射 + assert mapping['NDX'] == '513100.SH' + assert mapping['399006.SZ'] == '159915.SZ' + assert mapping['GC=F'] == '518880.SH' + assert mapping['931862.CSI'] == '931862.CSI' # 非跨市场 + + # 验证跨市场属性 + assert config.asset_pools.assets['NDX'].is_cross_market == True + assert config.asset_pools.assets['931862.CSI'].is_cross_market == False + + print("\n✓ 测试通过") + + +def test_market_specific_mapping(): + """测试 5: 特定市场映射""" + print("\n" + "=" * 70) + print(" 测试 5: 特定市场映射") + print("=" * 70) + + config = load_config('rotation_global.yaml') + + # 获取美股映射 + us_mapping = config.asset_pools.get_signal_to_trade_mapping('US_TECH') + print(f"\n美股映射:") + for signal, trade in us_mapping.items(): + print(f" {signal} → {trade}") + + # 获取商品映射 + commodity_mapping = config.asset_pools.get_signal_to_trade_mapping('COMMODITY') + print(f"\n商品映射:") + for signal, trade in commodity_mapping.items(): + print(f" {signal} → {trade}") + + # 验证 + assert len(us_mapping) == 2 + assert len(commodity_mapping) == 3 + assert us_mapping['NDX'] == '513100.SH' + assert commodity_mapping['GC=F'] == '518880.SH' + + print("\n✓ 测试通过") + + +def test_diversification_config(): + """测试 6: 分散化配置""" + print("\n" + "=" * 70) + print(" 测试 6: 分散化配置") + print("=" * 70) + + config = load_config('rotation_global.yaml') + + print(f"\n轮动配置:") + print(f" 选股数量: {config.rotation.select_num}") + print(f" 分散化: {config.rotation.diversified}") + print(f" 分散化分组: {config.rotation.diversification_groups}") + + # 验证默认配置(全局模式) + assert config.rotation.select_num == 5 + assert config.rotation.diversified == False + assert config.rotation.diversification_groups is None + + print("\n✓ 测试通过") + + +def test_asset_config_details(): + """测试 7: 标的配置详情""" + print("\n" + "=" * 70) + print(" 测试 7: 标的配置详情") + print("=" * 70) + + config = load_config('rotation_global.yaml') + + # 检查纳指配置 + ndx = config.asset_pools.assets['NDX'] + print(f"\n纳指100 配置:") + print(f" 名称: {ndx.name}") + print(f" 市场: {ndx.group}") + print(f" 信号来源: {ndx.signal_source}") + print(f" 交易来源: {ndx.trade_source}") + print(f" 跨市场: {ndx.is_cross_market}") + print(f" 描述: {ndx.description}") + + # 检查短债配置 + bond = config.asset_pools.assets['931862.CSI'] + print(f"\n短债指数 配置:") + print(f" 名称: {bond.name}") + print(f" 市场: {bond.group}") + print(f" 信号来源: {bond.signal_source}") + print(f" 交易来源: {bond.trade_source}") + print(f" 跨市场: {bond.is_cross_market}") + + # 验证 + assert ndx.name == "纳指100" + assert ndx.group == 'US_TECH' + assert ndx.signal_source == "NDX" + assert ndx.trade_source == "513100.SH" + assert ndx.is_cross_market == True + + assert bond.signal_source == bond.trade_source + assert bond.is_cross_market == False + + print("\n✓ 测试通过") + + +if __name__ == "__main__": + print("\n" + "=" * 70) + print(" 扁平化资产池配置测试") + print("=" * 70) + + tests = [ + ("加载扁平化配置", test_flat_config_load), + ("按市场分组", test_market_grouping), + ("信号和交易标的", test_signal_trade_codes), + ("信号→交易映射", test_signal_to_trade_mapping), + ("特定市场映射", test_market_specific_mapping), + ("分散化配置", test_diversification_config), + ("标的配置详情", test_asset_config_details), + ] + + passed = 0 + failed = 0 + + for name, test_func in tests: + try: + test_func() + passed += 1 + except Exception as e: + print(f"\n✗ 测试失败: {name}") + print(f" 错误: {e}") + import traceback + traceback.print_exc() + failed += 1 + + print("\n" + "=" * 70) + print(" 测试总结") + print("=" * 70) + print(f" ✓ 通过 - {passed}") + if failed > 0: + print(f" ✗ 失败 - {failed}") + print(f"\n总计: {passed}/{passed + failed} 通过") + print("=" * 70 + "\n") + + if failed > 0: + sys.exit(1) diff --git a/framework_v2/tests/test_simple_rotation.py b/framework_v2/tests/test_simple_rotation.py new file mode 100644 index 0000000..17de139 --- /dev/null +++ b/framework_v2/tests/test_simple_rotation.py @@ -0,0 +1,128 @@ +""" +测试简单轮动策略 + +验证完整流程: +1. 配置加载 +2. 策略初始化 +3. 数据获取 +4. 因子计算 +5. 信号生成 +6. 回测执行 +""" + +import sys +from pathlib import Path +import os + +# 添加项目根目录到路径 +project_root = Path(__file__).parent.parent +if str(project_root) not in sys.path: + sys.path.insert(0, str(project_root)) + +from framework_v2.config import load_config +from framework_v2.strategies.rotation.simple import SimpleRotationStrategy + + +def test_simple_rotation(): + """测试简单轮动策略完整流程""" + + print("\n" + "=" * 70) + print(" 简单轮动策略端到端测试") + print("=" * 70) + + # 设置环境变量 + os.environ['FLASK_API_URL'] = 'https://k3s.tokenpluse.xyz' + + # 1. 加载配置 + print("\n[1/6] 加载配置...") + config_path = Path(__file__).parent.parent / 'strategies' / 'rotation' / 'config_simple.yaml' + config = load_config(str(config_path)) + print(f" ✓ 配置加载成功") + print(f" 策略: {config.metadata.strategy}") + print(f" 标的: {list(config.asset_pools.equity.keys())}") + print(f" 回测: {config.backtest.start_date} ~ {config.backtest.end_date}") + + # 2. 初始化策略 + print("\n[2/6] 初始化策略...") + strategy = SimpleRotationStrategy(config) + print(f" ✓ 策略初始化成功") + print(f" 名称: {strategy.name}") + print(f" 动量窗口: {config.factor.n_days} 天") + print(f" 选股数量: {strategy.select_num}") + + # 3. 获取数据 + print("\n[3/6] 获取数据...") + codes = strategy.get_codes() + print(f" 标的列表: {codes}") + + data = strategy.get_data() + print(f" ✓ 获取 {len(data)} 个标的") + for code, df in data.items(): + print(f" {code}: {len(df)} 天 ({df.index[0].date()} ~ {df.index[-1].date()})") + + # 4. 计算因子 + print("\n[4/6] 计算因子...") + factors = strategy.compute_factors(data) + print(f" ✓ 计算 {len(factors)} 个因子") + for code, factor in factors.items(): + print(f" {code}: {len(factor)} 值, 范围 [{factor.min():.4f}, {factor.max():.4f}]") + + # 5. 生成信号 + print("\n[5/6] 生成信号...") + signals = strategy.generate_signals(factors) + n_signals = signals.sum().sum() + print(f" ✓ 生成 {signals.shape[0]} 个交易日信号") + print(f" 总信号数: {n_signals}") + print(f" 平均每日持仓: {signals.mean().mean():.2%}") + + # 6. 仓位管理 + print("\n[6/6] 仓位管理...") + positions = strategy.manage_positions(signals) + print(f" ✓ 仓位分配完成") + print(f" 权重和: {positions.sum(axis=1).mean():.2%}") + + # 7. 执行回测 + print("\n执行回测...") + result = strategy._execute_backtest(positions, data) + + # 打印结果 + print("\n" + "=" * 70) + print(" 回测结果") + print("=" * 70) + + metrics = result['metrics'] + print(f"\n 总收益率: {metrics['total_return']:.2%}") + print(f" 年化收益: {metrics['annual_return']:.2%}") + print(f" 最大回撤: {metrics['max_drawdown']:.2%}") + print(f" 夏普比率: {metrics['sharpe_ratio']:.2f}") + print(f" 交易天数: {metrics['n_days']}") + + # 验证结果 + print("\n" + "=" * 70) + print(" 验证") + print("=" * 70) + + assert metrics['total_return'] != 0, "总收益率不应为 0" + print(" ✓ 总收益率有效") + + assert len(result['equity_curve']) > 0, "净值曲线不应为空" + print(" ✓ 净值曲线有效") + + assert positions.sum(axis=1).max() <= 1.01, "权重和不应超过 100%" + print(" ✓ 仓位权重有效") + + print("\n" + "=" * 70) + print(" ✓ 所有测试通过") + print("=" * 70 + "\n") + + return result + + +if __name__ == "__main__": + try: + result = test_simple_rotation() + except Exception as e: + print(f"\n✗ 测试失败: {e}") + import traceback + traceback.print_exc() + sys.exit(1)