Python+LSTM 做 A 股量化选股:从零搭建 AI 量化交易完整策略,附可运行回测代码

【AI辅助创作声明:本文由 AI 辅助整理与撰写,内容已经过人工审校与调整。】

本文是一篇面向有Python基础但无量化经验的开发者的入门教程,手把手带你用Python+LSTM搭建完整的A股量化选股策略。从数据获取、特征工程、模型训练到策略回测,包含全部可运行代码和详细中文注释。读完本文,你将能够独立搭建一个基于深度学习的量化选股系统。

本文能让你做到什么

读完本文,你将能够独立搭建一个基于LSTM的完整A股量化选股策略,包括数据获取、特征工程、模型训练、策略回测全流程。全部代码可直接运行,无需额外配置。

环境依赖:

  • Python >= 3.8
  • akshare == 1.11.0(A股数据接口)
  • pandas == 2.1.4(数据处理)
  • numpy == 1.26.0(数值计算)
  • tensorflow == 2.15.0(LSTM模型)
  • scikit-learn == 1.3.2(数据预处理)
  • matplotlib == 3.8.2(可视化)
  • TA-Lib == 0.4.32(技术指标)

目标读者: 有Python基础但没做过量化交易的开发者。


第一步:数据获取(akshare)

1.1 为什么选择akshare?

akshare是一个开源的Python金融数据接口库,提供免费的A股行情数据、财务数据等。相比商业数据接口(如Tushare Pro),akshare无需注册付费,适合初学者入门。

1.2 安装和配置akshare

pip install akshare==1.11.0

1.3 获取股票历史数据

# 所需库:akshare==1.11.0, pandas==2.1.4
import akshare as ak
import pandas as pd
from datetime import datetime, timedelta

def get_stock_data(stock_code, start_date, end_date):
    """
    获取股票历史行情数据
    :param stock_code: 股票代码(如'000001')
    :param start_date: 开始日期(格式:'YYYYMMDD')
    :param end_date: 结束日期(格式:'YYYYMMDD')
    :return: 包含OHLCV数据的DataFrame
    """
    # 使用akshare获取日线数据
    df = ak.stock_zh_a_hist(
        symbol=stock_code,
        period="daily",
        start_date=start_date,
        end_date=end_date,
        adjust="qfq"  # 前复权(分红送股调整)
    )

    # 重命名列,方便后续处理
    df.columns = ['date', 'open', 'close', 'high', 'low', 'volume',
                  'amount', 'amplitude', 'pct_change', 'price_change', 'turnover']

    # 转换日期格式
    df['date'] = pd.to_datetime(df['date'])
    df.set_index('date', inplace=True)

    return df

# 示例:获取平安银行(000001)近一年数据
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')

df = get_stock_data('000001', start_date, end_date)
print(f"获取到 {len(df)} 条数据")
print(df.head())

1.4 获取全部A股股票列表

def get_all_stocks():
    """
    获取全部A股股票列表
    :return: 包含股票代码和名称的DataFrame
    """
    # 获取股票列表
    stock_df = ak.stock_zh_a_spot_em()

    # 筛选:排除ST股票、退市股票、上市不满一年的新股
    stock_df = stock_df[~stock_df['名称'].str.contains('ST|退', na=False)]

    return stock_df[['代码', '名称']]

# 示例:获取股票列表
stocks = get_all_stocks()
print(f"共 {len(stocks)} 只股票")
print(stocks.head(10))

避坑提示: akshare的接口可能会变动,如果遇到报错,更新akshare:pip install --upgrade akshare


第二步:特征工程(5个核心因子)

特征工程是量化策略的核心。我们选择5个具有明确金融含义的因子。

2.1 因子1:价格动量(MOM)

金融含义: 衡量价格变化的速度,价格上涨通常会延续一段时间(动量效应)。

def calculate_mom(df, period=20):
    """
    计算价格动量
    :param df: 价格DataFrame
    :param period: 计算周期(默认20天)
    :return: 动量值
    """
    # 动量 = 当前价格 / N天前价格 - 1
    mom = df['close'] / df['close'].shift(period) - 1
    return mom

2.2 因子2:波动率(VOL)

金融含义: 衡量价格波动幅度,低波动通常意味着价格更稳定。

def calculate_volatility(df, period=20):
    """
    计算价格波动率(标准差)
    :param df: 价格DataFrame
    :param period: 计算周期
    :return: 波动率值
    """
    # 计算日收益率
    returns = df['close'].pct_change()
    # 滚动标准差
    volatility = returns.rolling(window=period).std()
    return volatility

2.3 因子3:成交量比率(VR)

金融含义: 衡量成交量变化,异常放量通常伴随价格变动。

def calculate_volume_ratio(df, period=20):
    """
    计算成交量比率
    :param df: 价格DataFrame
    :param period: 计算周期
    :return: 成交量比率值
    """
    # 当前成交量 / 过去N天平均成交量
    avg_volume = df['volume'].rolling(window=period).mean()
    vr = df['volume'] / avg_volume
    return vr

2.4 因子4:均线乖离率(MA Spread)

金融含义: 衡量价格相对于均线的偏离程度,价格在均线上方表示趋势向上。

def calculate_ma_spread(df, short=5, long=20):
    """
    计算均线乖离率
    :param df: 价格DataFrame
    :param short: 短期均线周期
    :param long: 长期均线周期
    :return: 乖离率值
    """
    # 计算短期和长期均线
    ma_short = df['close'].rolling(window=short).mean()
    ma_long = df['close'].rolling(window=long).mean()

    # 乖离率 = (短期均线 - 长期均线) / 长期均线
    ma_spread = (ma_short - ma_long) / ma_long
    return ma_spread

2.5 因子5:RSI相对强弱指标

金融含义: 衡量价格变动的速度和幅度,RSI>70为超买,RSI<30为超卖。

# 所需库:TA-Lib==0.4.32
import talib

def calculate_rsi(df, period=14):
    """
    计算RSI指标
    :param df: 价格DataFrame
    :param period: RSI周期(默认14)
    :return: RSI值
    """
    rsi = talib.RSI(df['close'], timeperiod=period)
    return rsi

2.6 特征工程完整代码

def create_features(df):
    """
    创建特征集
    :param df: 价格DataFrame
    :return: 包含特征的DataFrame
    """
    features = pd.DataFrame(index=df.index)

    # 原始价格特征
    features['close'] = df['close']
    features['volume'] = df['volume']

    # 技术指标
    features['mom'] = calculate_mom(df, 20)
    features['volatility'] = calculate_volatility(df, 20)
    features['volume_ratio'] = calculate_volume_ratio(df, 20)
    features['ma_spread'] = calculate_ma_spread(df, 5, 20)
    features['rsi'] = calculate_rsi(df, 14)

    # 目标变量:未来5天收益率
    features['target'] = df['close'].shift(-5) / df['close'] - 1

    # 删除包含NaN的行
    features = features.dropna()

    return features

# 示例:创建特征
features_df = create_features(df)
print(features_df.head())
print(f"特征矩阵形状:{features_df.shape}")

第三步:模型选择(LSTM)

3.1 为什么选择LSTM?

LSTM(长短期记忆网络)是一种特殊的RNN,适合处理时间序列数据。我们选择LSTM的原因是:

  1. 时序特性: 股价是时间序列数据,LSTM能捕捉长期依赖关系。
  2. 非线性关系: 股价与因子之间存在复杂的非线性关系,LSTM能学习这些非线性模式。
  3. 广泛应用: LSTM在金融量化领域有大量成功应用案例。

3.2 LSTM模型结构

# 所需库:tensorflow==2.15.0
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout

def build_lstm_model(input_shape):
    """
    构建LSTM模型
    :param input_shape: 输入数据形状(时间步长,特征数)
    :return: 编译好的LSTM模型
    """
    model = Sequential([
        # 第一层LSTM
        # 50个神经元,返回序列用于堆叠下一层LSTM
        LSTM(50, return_sequences=True, input_shape=input_shape),
        Dropout(0.2),  # 随机丢弃20%神经元,防止过拟合

        # 第二层LSTM
        LSTM(50, return_sequences=False),
        Dropout(0.2),

        # 全连接层
        Dense(25, activation='relu'),

        # 输出层:预测收益率
        Dense(1)
    ])

    # 编译模型
    model.compile(
        optimizer='adam',  # Adam优化器,适合大多数场景
        loss='mse',        # 均方误差损失函数
        metrics=['mae']    # 平均绝对误差评估指标
    )

    return model

# 示例:创建模型
# 假设输入形状为(20, 5),即20个时间步,5个特征
model = build_lstm_model((20, 5))
model.summary()

参数解释:

  • LSTM(50): 50个神经元,既有足够容量,又不容易过拟合。
  • Dropout(0.2): 随机丢弃20%神经元,防止过拟合。
  • adam优化器: 自适应学习率,训练稳定。
  • mse损失函数: 适合回归问题。

第四步:模型训练

4.1 数据预处理

# 所需库:scikit-learn==1.3.2
from sklearn.preprocessing import MinMaxScaler
import numpy as np

def prepare_data(features_df, lookback=20):
    """
    准备训练数据
    :param features_df: 特征DataFrame
    :param lookback: 回溯窗口大小
    :return: 训练集和测试集
    """
    # 选择特征列
    feature_cols = ['mom', 'volatility', 'volume_ratio', 'ma_spread', 'rsi']
    target_col = 'target'

    # 数据归一化
    scaler_x = MinMaxScaler()
    scaler_y = MinMaxScaler()

    X = features_df[feature_cols].values
    y = features_df[target_col].values.reshape(-1, 1)

    X_scaled = scaler_x.fit_transform(X)
    y_scaled = scaler_y.fit_transform(y)

    # 创建时间序列样本
    X_seq, y_seq = [], []
    for i in range(lookback, len(X_scaled)):
        X_seq.append(X_scaled[i-lookback:i])
        y_seq.append(y_scaled[i])

    X_seq = np.array(X_seq)
    y_seq = np.array(y_seq)

    # 划分训练集和测试集(80%训练,20%测试)
    train_size = int(len(X_seq) * 0.8)
    X_train, X_test = X_seq[:train_size], X_seq[train_size:]
    y_train, y_test = y_seq[:train_size], y_seq[train_size:]

    return X_train, X_test, y_train, y_test, scaler_x, scaler_y

# 示例:准备数据
X_train, X_test, y_train, y_test, scaler_x, scaler_y = prepare_data(features_df)

print(f"训练集大小:{X_train.shape}")
print(f"测试集大小:{X_test.shape}")

4.2 模型训练

def train_model(model, X_train, y_train, X_test, y_test, epochs=50, batch_size=32):
    """
    训练LSTM模型
    :param model: LSTM模型
    :param X_train: 训练特征
    :param y_train: 训练目标
    :param X_test: 测试特征
    :param y_test: 测试目标
    :param epochs: 训练轮数
    :param batch大小: 批次大小
    :return: 训练历史
    """
    # 早停机制,防止过拟合
    early_stop = tf.keras.callbacks.EarlyStopping(
        monitor='val_loss',
        patience=10,  # 验证损失10轮不改善则停止
        restore_best_weights=True
    )

    # 训练模型
    history = model.fit(
        X_train, y_train,
        epochs=epochs,
        batch_size=batch_size,
        validation_data=(X_test, y_test),
        callbacks=[early_stop],
        verbose=1
    )

    return history

# 示例:训练模型
history = train_model(model, X_train, y_train, X_test, y_test)

# 评估模型
train_loss = model.evaluate(X_train, y_train, verbose=0)
test_loss = model.evaluate(X_test, y_test, verbose=0)

print(f"训练集损失:{train_loss[0]:.4f}")
print(f"测试集损失:{test_loss[0]:.4f}")

4.3 损失曲线可视化

# 所需库:matplotlib==3.8.2
import matplotlib.pyplot as plt

def plot_loss(history):
    """
    绘制训练损失曲线
    :param history: 训练历史
    """
    plt.figure(figsize=(12, 4))

    # 损失曲线
    plt.subplot(1, 2, 1)
    plt.plot(history.history['loss'], label='训练损失')
    plt.plot(history.history['val_loss'], label='验证损失')
    plt.title('模型损失')
    plt.xlabel('轮数')
    plt.ylabel('损失')
    plt.legend()

    # MAE曲线
    plt.subplot(1, 2, 2)
    plt.plot(history.history['mae'], label='训练MAE')
    plt.plot(history.history['val_mae'], label='验证MAE')
    plt.title('模型MAE')
    plt.xlabel('轮数')
    plt.ylabel('MAE')
    plt.legend()

    plt.tight_layout()
    plt.show()

# 示例:绘制损失曲线
plot_loss(history)

损失曲线解读:

  • 训练损失: 训练集上的损失,越低说明模型对训练数据拟合越好。
  • 验证损失: 验证集上的损失,反映泛化能力。
  • 理想状态: 训练损失和验证损失都下降并趋于稳定,两者差距不大。

第五步:策略回测

5.1 回测框架

class Backtester:
    """
    简单回测框架
    """

    def __init__(self, initial_capital=100000):
        self.initial_capital = initial_capital
        self.capital = initial_capital
        self.positions = {}  # 当前持仓
        self.trades = []     # 交易记录
        self.daily_returns = []  # 每日收益

    def predict_return(self, model, features, scaler_x):
        """
        使用模型预测收益率
        :param model: 训练好的LSTM模型
        :param features: 当前特征
        :param scaler_x: 特征归一化器
        :return: 预测收益率
        """
        # 归一化特征
        features_scaled = scaler_x.transform(features.reshape(1, -1))

        # 创建序列(简化处理,实际使用时需要回溯窗口)
        # 这里假设特征已经是回溯窗口后的输出
        pred = model.predict(features_scaled.reshape(1, 1, -1), verbose=0)

        return pred[0][0]

    def run_backtest(self, df, model, scaler_x, threshold=0.02):
        """
        运行回测
        :param df: 价格DataFrame
        :param model: 训练好的模型
        :param scaler_x: 特征归一化器
        :param threshold: 买卖阈值(预测收益率>阈值则买入)
        """
        for i in range(20, len(df)):  # 从20开始(回溯窗口)
            date = df.index[i]

            # 获取当前特征
            current_df = df.iloc[i-20:i]
            features = create_features(current_df)

            if len(features) < 1:
                continue

            latest_features = features[['mom', 'volatility', 'volume_ratio',
                                        'ma_spread', 'rsi']].iloc[-1].values

            # 预测收益率
            pred_return = self.predict_return(model, latest_features, scaler_x)

            # 交易逻辑
            if pred_return > threshold and len(self.positions) == 0:
                # 买入信号
                buy_price = df['close'].iloc[i]
                shares = int(self.capital * 0.9 / buy_price)  # 90%仓位
                if shares > 0:
                    self.positions['stock'] = {
                        'shares': shares,
                        'buy_price': buy_price,
                        'buy_date': date
                    }
                    self.capital -= shares * buy_price
                    self.trades.append({
                        'date': date,
                        'action': 'buy',
                        'price': buy_price,
                        'shares': shares
                    })

            elif pred_return < -threshold and 'stock' in self.positions:
                # 卖出信号
                sell_price = df['close'].iloc[i]
                position = self.positions['stock']
                self.capital += position['shares'] * sell_price

                return_rate = (sell_price - position['buy_price']) / position['buy_price']
                self.trades.append({
                    'date': date,
                    'action': 'sell',
                    'price': sell_price,
                    'shares': position['shares'],
                    'return': return_rate
                })

                del self.positions['stock']

            # 记录每日总市值
            total_value = self.capital
            if 'stock' in self.positions:
                total_value += self.positions['stock']['shares'] * df['close'].iloc[i]

            self.daily_returns.append({
                'date': date,
                'total_value': total_value
            })

        return self.get_performance()

    def get_performance(self):
        """
        计算回测绩效
        :return: 绩效指标
        """
        if not self.daily_returns:
            return {}

        returns_df = pd.DataFrame(self.daily_returns)
        returns_df['return'] = returns_df['total_value'].pct_change()

        # 总收益率
        total_return = (returns_df['total_value'].iloc[-1] / self.initial_capital) - 1

        # 年化收益率(假设252个交易日)
        n_days = len(returns_df)
        annual_return = (1 + total_return) ** (252 / n_days) - 1

        # 最大回撤
        returns_df['cummax'] = returns_df['total_value'].cummax()
        returns_df['drawdown'] = (returns_df['total_value'] - returns_df['cummax']) / returns_df['cummax']
        max_drawdown = returns_df['drawdown'].min()

        # 夏普比率(假设无风险利率2%)
        excess_return = returns_df['return'].mean() * 252 - 0.02
        volatility = returns_df['return'].std() * np.sqrt(252)
        sharpe_ratio = excess_return / volatility if volatility > 0 else 0

        return {
            'total_return': total_return,
            'annual_return': annual_return,
            'max_drawdown': max_drawdown,
            'sharpe_ratio': sharpe_ratio,
            'trades': len(self.trades)
        }

# 示例:运行回测
backtester = Backtester(initial_capital=100000)
performance = backtester.run_backtest(df, model, scaler_x)

print("回测结果:")
print(f"总收益率:{performance['total_return']:.2%}")
print(f"年化收益率:{performance['annual_return']:.2%}")
print(f"最大回撤:{performance['max_drawdown']:.2%}")
print(f"夏普比率:{performance['sharpe_ratio']:.2f}")
print(f"交易次数:{performance['trades']}")

5.2 基准对比

def get_benchmark_data(start_date, end_date):
    """
    获取沪深300基准数据
    :param start_date: 开始日期
    :param end_date: 结束日期
    :return: 沪深300收益率
    """
    # 获取沪深300数据
    df = ak.index_zh_a_hist(symbol="000300", period="daily",
                            start_date=start_date, end_date=end_date)

    # 计算收益率
    start_price = df['close'].iloc[0]
    end_price = df['close'].iloc[-1]
    benchmark_return = (end_price - start_price) / start_price

    return benchmark_return

# 示例:获取基准收益率
benchmark_return = get_benchmark_data(start_date, end_date)
print(f"沪深300基准收益率:{benchmark_return:.2%}")
print(f"策略超额收益:{performance['total_return'] - benchmark_return:.2%}")

第六步:风险控制

6.1 止损策略

class RiskManager:
    """
    风险管理器
    """

    def __init__(self, stop_loss=0.05, take_profit=0.10, max_position=0.3):
        """
        初始化风险参数
        :param stop_loss: 止损比例(默认5%)
        :param take_profit: 止盈比例(默认10%)
        :param max_position: 最大单仓位(默认30%)
        """
        self.stop_loss = stop_loss
        self.take_profit = take_profit
        self.max_position = max_position

    def check_stop_loss(self, position, current_price):
        """
        检查止损条件
        :param position: 持仓信息
        :param current_price: 当前价格
        :return: 是否卖出
        """
        if not position:
            return False

        return_rate = (current_price - position['buy_price']) / position['buy_price']

        # 止损或止盈
        if return_rate <= -self.stop_loss or return_rate >= self.take_profit:
            return True

        return False

    def calculate_position_size(self, capital, stock_price, confidence):
        """
        计算仓位大小
        :param capital: 总资金
        :param stock_price: 股票价格
        :param confidence: 模型置信度(0-1)
        :return: 买入股数
        """
        # 仓位大小 = 总资金 × 最大仓位比例 × 置信度
        position_value = capital * self.max_position * confidence
        shares = int(position_value / stock_price)

        return shares

# 示例:使用风险管理器
risk_mgr = RiskManager(stop_loss=0.05, take_profit=0.10)

# 实际交易中,检查止损
# if risk_mgr.check_stop_loss(position, current_price):
#     sell()

6.2 仓位管理

def position_sizing(pred_return, confidence, capital, max_position=0.3):
    """
    动态仓位管理
    :param pred_return: 预测收益率
    :param confidence: 模型置信度
    :param capital: 总资金
    :param max_position: 最大仓位比例
    :return: 仓位比例(0-1)
    """
    # 基础仓位
    base_position = 0.1

    # 根据预测收益率和置信度调整仓位
    if pred_return > 0.05 and confidence > 0.7:
        position = min(base_position * 3, max_position)  # 高置信度、高收益率,重仓
    elif pred_return > 0.02 and confidence > 0.5:
        position = base_position * 2  # 中等仓位
    else:
        position = base_position  # 低仓位

    return position

诚实声明:本策略的局限性

在你急于实盘交易之前,我必须诚实说明本策略的局限性:

1. 过拟合风险

LSTM模型容易过拟合,特别是数据量较小的情况下。回测结果可能看起来很好,但实盘表现可能很差。

解决方案:

  • 使用更多数据训练(至少3-5年)。
  • 增加Dropout比例。
  • 使用交叉验证。

2. 未考虑交易成本

上面的回测未考虑:

  • 佣金(通常万3)
  • 印花税(卖出时千1)
  • 滑点

实际影响: 高频交易会侵蚀大部分收益。

3. A股流动性问题

模型假设能立即成交,但现实中:

  • 小盘股流动性差
  • 大单会冲击价格

4. 市场机制变化

模型基于历史数据训练,但市场机制会变化:

  • 政策调整
  • 宏观环境变化
  • 投资者结构变化

引入EasyClaw:更便捷的量化工具

如果你觉得上面的代码太复杂,我推荐使用 EasyClaw。这是一款专为量化投资设计的AI自动化工具,集成了多种金融技能。

EasyClaw的金融炒股功能:

  • 妙想资讯搜索:监控东方财富新闻,自动提醒。
  • 妙想金融数据:查询A股实时行情、财务指标、股东数据等股票金融数据。
  • 股价速查:一键获取全球股票实时行情,秒级返回涨跌与成交量,助力投资决策。
  • 妙想智能选股:从东方财富股票实时数据库中筛选符合要求的A股。
  • 妙想模拟炒股:在东方财富模拟账户中进行A股买卖、查询持仓与资金,零风险练习股票投资策略。
  • 量化策略回测分析:策略开发、回测、风险指标、组合优化一站完成,结果仅供研究参考。
  • 金融图表生成:根据数据生成高质量图表图片,支持折线、柱状、K线、饼图、热力图等多种类型。
  • Tushare金融数据:通过Tushare Pro接口获取A股、港股、美股、基金、期货、债券及宏观经济等220+类金融数据。

EasyClaw下载地址: https://easyclaw.cn/?f=239

EasyClaw的优势是一键安装、无需写代码,非常适合想快速入门量化投资的新手。


本文仅供参考,不构成任何投资建议。股市有风险,投资需谨慎。文中提及的任何策略均不代表未来收益保证。


觉得有帮助的麻烦点下好文要顶,欢迎评论区交流!

posted @ 2026-04-21 17:11  PC修复电脑医生  阅读(209)  评论(0)    收藏  举报