9. 多股票交易策略实战

9.多股票交易策略实战

从单股票到多股票,看似只是数量的增加,实则是一次维度跃迁。当交易标的从一个扩展到几十个甚至上百个时,整个问题的复杂度会呈指数级增长。状态空间、动作空间、数据吞吐量、训练稳定性,每个环节都会面临新的挑战。这不再是简单的"重复三十次单股票交易",而是要在更高维度上解决资产配置、风险分散和策略协同的问题。

多股票交易的核心难点在于组合管理。假设我们同时交易道琼斯30指数的成分股,每只股票都有自己的价格序列、技术指标、成交量特征。智能体不仅要判断单只股票的买卖时机,还要考虑资金在组合内的分配比例、持仓集中度、行业风险暴露等组合层面的因素。更棘手的是,这些股票之间存在复杂的联动关系——苹果公司的财报可能带动整个科技板块,美联储的利率决议会影响所有股票。这种相关性让状态空间的设计变得极为关键。

多股票组合管理挑战

与单股票交易相比,多股票场景下的状态空间维度会急剧膨胀。如果每只股票提取10个技术指标,30只股票就产生300维的观测数据。这还没算上持仓信息、账户余额等组合层面的状态。维度灾难不仅带来计算压力,更重要的是导致训练过程极度不稳定。同样的超参数在单股票上表现良好,放到多股票环境里可能完全无法收敛。

动作空间的设计也更具挑战性。单股票场景下,动作通常是 {-k, ..., -1, 0, 1, ..., k} 这样的离散集合,表示买卖固定数量的股票。但在多股票环境中,我们需要为每只股票独立输出动作。如果采用穷举法,30只股票、每只股票21个动作(假设k=10),组合起来的动作空间将达到 21³⁰,这是一个天文数字。实践中,我们会为每只股票独立输出一个连续动作值,范围通常在 [-1, 1] 之间,再通过 h_max 参数映射为实际交易股数。这种方式将动作空间压缩到30维连续向量,显著降低了学习难度。

奖励函数的设计需要更加精细。简单的组合价值变化 r = v(t+1) - v(t) 在多股票场景下可能产生误导。当市场普涨时,即使随机策略也能获得正收益;当市场暴跌时,再好的选股策略也难以幸免。因此,引入风险调整后的收益指标变得必要。夏普比率、最大回撤、波动率惩罚项等都可以融入奖励函数,引导智能体在收益和风险之间取得平衡。FinRL 中使用的金融湍流指数(Financial Turbulence Index)就是为了在市场剧烈波动时主动降低仓位,不过计算开销较大,实时交易场景中可以用 VIX 指数替代。

批量数据获取与预处理

数据是策略的燃料,多股票交易首先需要解决批量数据获取的问题。FinRL 提供了 YahooDownloader 类,可以一次性下载多只股票的历史数据。这个类封装了 yfinance 库,能够获取开盘价、最高价、最低价、收盘价和成交量等基础数据。

from finrl.meta.preprocessor.yahoodownloader import YahooDownloader

# 定义要交易的股票列表
ticker_list = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 
               'META', 'NVDA', 'JPM', 'V', 'JNJ']

# 批量下载数据
df = YahooDownloader(start_date='2020-01-01',
                    end_date='2023-12-31',
                    ticker_list=ticker_list).fetch_data()

这段代码会返回一个 pandas DataFrame,包含所有指定股票在时间段内的日线数据。DataFrame 的每一行代表一只股票在某一交易日的观测,列包括 date、tic、open、high、low、close、volume 等。tic 列存储股票代码,用于区分不同股票。

获取原始数据后,需要进行特征工程。FinRL 的 FeatureEngineer 类可以自动计算多种技术指标,包括 MACD、RSI、CCI、ADX 等。这些指标从不同角度刻画市场状态,为智能体提供更丰富的决策依据。

from finrl.meta.preprocessor.preprocessors import FeatureEngineer

# 初始化特征工程器
fe = FeatureEngineer(use_technical_indicator=True,
                    tech_indicator_list=['macd', 'rsi', 'cci', 'adx'],
                    use_turbulence=True,
                    user_defined_feature=False)

# 生成特征
processed = fe.preprocess_data(df)

这个过程会为每只股票计算指定的技术指标,并添加到 DataFrame 中。use_turbulence=True 会计算湍流指数,用于风险预警。处理后的数据包含原始价格信息和衍生特征,维度显著增加。

接下来需要划分训练集、验证集和测试集。FinRL 采用 training-testing-trading 流水线,确保数据不会泄露。训练集用于策略学习,验证集用于调整超参数,测试集用于最终评估。这种划分方式保证了回测结果的可靠性。

from finrl.meta.preprocessor.preprocessors import data_split

# 按时间划分数据集
train = data_split(processed, '2020-01-01', '2021-12-31')
trade = data_split(processed, '2022-01-01', '2023-12-31')

data_split 函数根据日期范围筛选数据,返回对应时间段的子集。训练集应该足够长,覆盖不同的市场周期,包括牛市、熊市和震荡市,这样训练出的策略才具有鲁棒性。

复杂环境状态设计

环境是智能体与市场交互的接口,其设计直接决定了策略的上限。FinRL 的 StockTradingEnv 类实现了多股票交易环境,内部封装了状态计算、订单执行、奖励计算等逻辑。

状态空间的设计需要综合考虑市场信息和组合信息。市场信息包括价格序列和技术指标,组合信息包括当前持仓、账户余额、已实现盈亏等。FinRL 将这两类信息拼接成一个高维向量作为状态。

from finrl.meta.env_stock_trading.env_stocktrading import StockTradingEnv

# 环境参数配置
env_kwargs = {
    'stock_dim': len(ticker_list),          # 股票数量
    'hmax': 100,                            # 最大交易股数
    'initial_amount': 1000000,              # 初始资金
    'num_stock_shares': [0]*len(ticker_list), # 初始持仓
    'buy_cost_pct': 0.001,                  # 买入成本
    'sell_cost_pct': 0.001,                 # 卖出成本
    'reward_scaling': 1e-4,                 # 奖励缩放系数
    'state_space': len(train.tic.unique()) * (len(tech_indicator_list) + 3), # 状态维度
    'action_space': len(train.tic.unique()), # 动作维度
    'tech_indicator_list': tech_indicator_list
}

# 创建训练环境
env_train = StockTradingEnv(df=train, **env_kwargs)

这个配置定义了一个包含10只股票的交易环境。state_space 的计算考虑了每只股票的技术指标数量加上3个基础价格特征(open、close、volume)。动作空间维度等于股票数量,每只股票独立输出一个动作值。

环境内部的状态更新逻辑较为复杂。在每个时间步,环境会根据当前日期筛选出所有股票的数据,提取价格特征和技术指标。然后结合账户信息(余额、持仓)构建完整状态向量。动作执行时,环境会计算实际交易金额,更新持仓和余额,并扣除交易成本。

交易成本的建模至关重要。现实中每笔交易都会产生佣金和滑点,忽略这些因素会导致回测结果过于乐观。FinRL 通过 buy_cost_pct 和 sell_cost_pct 参数引入交易成本,通常设置为0.1%到0.3%。这意味着买入10万元的股票,实际成本是10万零100元,卖出时同样扣除相应比例。

状态归一化是稳定训练的关键技巧。不同股票的价格差异巨大,特斯拉的股价可能高达数百美元,而其他股票可能只有几十美元。如果不做归一化,高价股的特征会主导网络学习。FinRL 在环境内部自动对价格特征进行归一化处理,通常使用滚动窗口的均值和标准差进行标准化。

多进程加速训练实践

多股票环境的训练计算量巨大。假设我们有30只股票,每只股票20个特征,状态空间就是600维。智能体需要处理这么大规模的输入,并在数百万个时间步上反复试错,单进程训练可能需要数天甚至数周时间。

FinRL 借鉴了 Isaac Gym 的向量环境技术,实现了多进程并行训练。核心思想是在多个 CUDA 核心上同时运行多个环境实例,每个实例独立与智能体交互,产生的经验数据被收集到共享的 replay buffer 中,供 learner 统一更新策略。

from finrl.agents.stablebaselines3.models import DRLAgent
from stable_baselines3 import PPO

# 创建向量环境
env_train_vec = env_train.get_sb_env()  # 获取Stable Baselines兼容的环境

# 初始化PPO智能体
agent = DRLAgent(env=env_train_vec)
model_ppo = agent.get_model('ppo', model_kwargs={
    'policy': 'MlpPolicy',
    'verbose': 0,
    'tensorboard_log': './tensorboard_log'
})

# 开始训练
trained_model = agent.train_model(model=model_ppo, 
                                 tb_log_name='ppo_multi_stock',
                                 total_timesteps=50000)

get_sb_env() 方法会将 FinRL 环境包装成 Stable Baselines3 兼容的向量环境。Stable Baselines3 内部会自动处理多进程逻辑,根据可用的 CPU 核心数创建并行环境。在 GPU 服务器上,可以显著加速训练过程。

PPO 算法在多股票场景中表现稳定。它采用策略梯度方法,通过 clipped surrogate objective 限制策略更新幅度,避免训练崩溃。MlpPolicy 表示使用多层感知机网络,适合处理扁平化的状态向量。对于更复杂的场景,可以考虑使用 LstmPolicy 捕捉时序依赖。

训练过程中的监控非常重要。tensorboard_log 参数会记录训练指标,包括策略损失、价值损失、熵等。通过 TensorBoard 可以实时观察训练曲线,判断模型是否收敛。如果奖励值长期震荡不上升,可能需要调整学习率或网络结构。

total_timesteps 参数控制训练步数。多股票场景需要更多训练步数才能充分探索状态空间。通常建议至少5万步,复杂场景可能需要10万步以上。但步数过多可能导致过拟合,需要在验证集上监控性能。

多进程训练的一个技巧是调整环境数量。Stable Baselines3 默认使用 CPU 核心数作为环境数量。在 GPU 服务器上,可以适当增加环境数量,充分利用计算资源。但环境过多会增加内存开销,需要权衡。

# 自定义环境数量
from stable_baselines3.common.vec_env import SubprocVecEnv

def make_env():
    return StockTradingEnv(df=train, **env_kwargs)

env_vec = SubprocVecEnv([make_env for _ in range(8)])  # 8个并行环境

SubprocVecEnv 可以显式指定环境数量。每个环境在独立进程中运行,通过进程间通信传递数据。这种方式避免了 Python 的全局解释器锁(GIL)限制,真正实现并行加速。

策略泛化能力评估

训练好的模型需要在未见过的数据上验证其泛化能力。这是区分真实有效策略和过拟合策略的关键步骤。FinRL 提供了完整的回测框架,可以在测试集上评估策略表现。

# 创建测试环境
env_trade = StockTradingEnv(df=trade, **env_kwargs)

# 执行回测
from finrl.plot import backtest_stats, backtest_plot

# 使用训练好的模型进行预测
df_account_value, df_actions = DRLAgent.DRL_prediction(
    model=trained_model,
    environment=env_trade
)

# 计算绩效指标
perf_stats = backtest_stats(df_account_value)
print(perf_stats)

DRL_prediction 方法会在测试集上运行训练好的模型,记录每个时间步的账户价值和执行的动作。backtest_stats 函数计算夏普比率、年化收益、最大回撤等关键指标。这些指标比单纯的最终收益更能反映策略质量。

夏普比率衡量单位风险带来的超额收益,是评估策略的黄金标准。通常认为夏普比率大于1的策略是合格的,大于2则非常优秀。最大回撤反映策略在最坏情况下的亏损幅度,控制在20%以内较为理想。

基准对比是评估的必要环节。需要将策略表现与买入持有(Buy & Hold)等简单策略比较。如果复杂策略无法显著跑赢基准,说明模型可能没有学到有效模式。

# 获取基准收益
baseline_df = get_baseline(ticker='^DJI', 
                          start='2022-01-01',
                          end='2023-12-31')

# 可视化对比
backtest_plot(df_account_value, 
              baseline_df,
              value_col_name='account_value')

get_baseline 函数下载基准指数(如道琼斯指数)的历史数据,backtest_plot 会绘制策略净值曲线和基准曲线的对比图。通过可视化可以直观判断策略在不同市场环境下的表现。

过拟合是深度学习策略的常见问题。模型可能在训练集上记住了特定模式,但在新数据上失效。防范过拟合的方法包括:增加训练数据量、使用正则化、早停策略、在验证集上选择模型等。

早停策略通过监控验证集性能来决定何时终止训练。当验证集奖励连续多个回合不再提升时,即使训练集还在改善,也应该停止训练。这可以防止模型过度拟合训练数据的噪声。

# 早停回调
from stable_baselines3.common.callbacks import EvalCallback, StopTrainingOnRewardThreshold

# 创建验证环境
env_val = StockTradingEnv(df=val, **env_kwargs)

# 定义早停条件
callback = StopTrainingOnRewardThreshold(reward_threshold=1000, verbose=1)

# 训练时传入回调
trained_model = agent.train_model(model=model_ppo, 
                                 callback=callback,
                                 total_timesteps=50000)

EvalCallback 会定期在验证环境上评估模型,当奖励达到阈值时触发早停。这种方式自动选择最优模型,避免手动监控。

策略的稳定性同样重要。多次训练应该产生相似的结果,如果每次训练得到的策略差异巨大,说明训练过程不稳定。可以通过固定随机种子、增加训练步数、调整网络结构来提升稳定性。

多股票策略的评估还需要关注个股层面的表现。某些股票可能贡献主要收益,而其他股票持续亏损。分析个股交易记录有助于发现策略的偏好和盲点。

# 分析个股交易
df_actions.to_csv('actions.csv')  # 保存交易记录

# 计算每只股票的交易频率
action_counts = df_actions.abs().sum()
print(action_counts)

交易频率过高的股票可能意味着策略过度交易,增加了成本。长期未交易的股票可能说明策略对其缺乏有效信号。这些信息可以指导后续的特征工程或股票池调整。

多股票交易是量化投资的终极战场。它要求模型同时处理宏观市场、行业轮动和个股选择三个层次的信息。FinRL 提供的框架降低了入门门槛,但要构建真正有效的策略,还需要深入理解市场机制、精心设计特征工程、耐心调优训练参数。下一章将探讨投资组合优化,在更抽象的层面解决资产配置问题。