test(framework_v2): 添加配置系统测试和策略示例
配置文件: - rotation_global.yaml: 扁平化资产池配置示例,演示 group 策略分组 * 13 个标的覆盖 7 个策略分组(US_TECH, CN_GROWTH, JP_BROAD, EU_BROAD, HK_TECH, COMMODITY, FIXED_INCOME) * signal_source/trade_source 分离配置(跨市场场景) * 分散化选股配置示例(注释状态) * 默认使用 Flask API 数据源 测试用例: - test_flat_asset_pool.py: 7/7 测试通过 * 扁平配置加载验证 * 策略分组功能测试(by_group, groups, count) * 信号/交易标的获取(get_signal_codes, get_trade_codes) * 信号→交易映射(get_signal_to_trade_mapping) * 分散化配置验证 * 标的配置详情验证 - test_config.py: 配置加载器测试 - test_simple_rotation.py: 简单轮动策略端到端测试
This commit is contained in:
268
framework_v2/config/rotation_global.yaml
Normal file
268
framework_v2/config/rotation_global.yaml
Normal file
@@ -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
|
||||||
285
framework_v2/tests/test_config.py
Normal file
285
framework_v2/tests/test_config.py
Normal file
@@ -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)
|
||||||
281
framework_v2/tests/test_flat_asset_pool.py
Normal file
281
framework_v2/tests/test_flat_asset_pool.py
Normal file
@@ -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)
|
||||||
128
framework_v2/tests/test_simple_rotation.py
Normal file
128
framework_v2/tests/test_simple_rotation.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user