完整源码链接:https://pan.quark.cn/s/1e54aa2ae950
我*终于搞完了第二版,这次用的是真实数据直接从akshare拉,再也不用那个破几何布朗运动生成假数据了。上次导师看了一眼说"你这数据走势也太完美了吧,一看就是生成的",我当场尴尬得想钻地缝。行吧那就改成真实数据呗,结果这坑比我想象的深多了。
下面看下效果展示
先说说改了什么,原来的 data_generator.py 整个删了换成了 data_fetcher.py,用 akshare 库拉真实 A 股数据。然后 main.py 改成交互式平台了,启动之后能选股票,跑全部流程出7张图。visualize.py 加了第7张综合看板,一页集成所有关键信息。
先给你们看最终的项目结构:
2-基于LSTM-Attention的股票价格预测与可视化平台/ ├── data_fetcher.py # 真实数据获取 (akshare + 代理绕过) ├── model.py # LSTM + Attention (没动) ├── train.py # 训练 (微调) ├── predict.py # 预测评估 (微调) ├── visualize.py # 可视化 + 综合看板 ├── main.py # 交互式平台主入口 ├── requirements.txt └── charts/ ├── 000001_平安银行_price_trend.png ├── 000001_平安银行_pred_vs_actual.png ├── 000001_平安银行_attention_heatmap.png ├── 000001_平安银行_loss_curves.png ├── 000001_平安银行_error_dist.png ├── 000001_平安银行_future_pred.png └── 000001_平安银行_dashboard.png好先说数据获取这块,他*的踩了一个史诗级大坑。akshare 这个库底层用的是 requests,而我的电脑上挂着****软件(C*la*h*),结果 requests 请求访问东方财富的 API,直接被拒连接。proxy 这个破问题折腾了我两天,查了各种资料最后发现在创建 Session 的时候把 proxies 强行设成 None 才行。
import requests _old_request = requests.Session.request def _new_request(*args, **kwargs): kwargs['proxies'] = {'http': None, 'https': None} return _old_request(*args, **kwargs) requests.Session.request = _new_request这行代码必须放在所有 import 的前面,在 akshare 被加载之前就把 requests 给 patch 了,不然没用。还要额外清空环境变量:
os.environ['HTTP_PROXY'] = '' os.environ['HTTPS_PROXY'] = '' os.environ['http_proxy'] = '' os.environ['https_proxy'] = ''这里有个坑是他的 agent 顺序问题,因为 akshare 自己内部也会 import requests,所以这个 monkey patch 必须在 import akshare 之前执行。我把这段代码塞到了 data_fetcher.py 最顶上,import 语句的前面,才总算把代理问题解决了。
然后还有个问题,akshare 获取全市场股票列表的那个接口ak.stock_zh_a_spot_em()经常抽风连不上,我加了缓存机制和兜底方案。缓存一个小时有效,如果接口挂了就用预设的 18 只热门股票。这 18 只够用了,平安银行、茅台、宁德时代这些。
PRESET_STOCKS = { '000001': '平安银行', '000002': '万科A', '000333': '美的集团', '000651': '格力电器', '000858': '五粮液', '002415': '海康威视', '002475': '立讯精密', '300750': '宁德时代', '600036': '招商银行', '600519': '贵州茅台', '600900': '长江电力', '601012': '隆基绿能', '601318': '中国平安', '601398': '工商银行', ... }真实数据获取用ak.stock_zh_a_hist(),参数是 symbol(6位代码)、period(daily)、start_date、end_date、adjust(qfq 前复权)。前复权这个很重要,不调整的话分红送股会导致价格跳空,模型学到错误模式。
def fetch_stock_data(symbol, start_date, end_date, max_retries=3): for attempt in range(max_retries): try: time.sleep(random.uniform(0.15, 0.35)) df = ak.stock_zh_a_hist( symbol=symbol, period='daily', start_date=start_date, end_date=end_date, adjust='qfq' ) ...随机延时 0.15 到 0.35 秒,加了三重重试,避免被东方财富封 IP。实测拉平安银行5年数据(1500多条)不到1秒就搞定了,速度还行。
数据拿到以后列名是中文的,得映射成英文。这里我当时没注意到「成交额」「振幅」「涨跌幅」这些列名返回可能有差异,不同股票返回的字段居然不一样!有的有换手率有的没有,所以用了一个字典映射,只在列存在的时候才改名。
rename_map = {} for old, new in [('日期', 'date'), ('开盘', 'open'), ('收盘', 'close'), ('最高', 'high'), ('最低', 'low'), ('成交量', 'volume'), ('成交额', 'amount'), ('振幅', 'amplitude'), ('涨跌幅', 'pct_chg'), ('涨跌额', 'price_change'), ('换手率', 'turnover')]: if old in df.columns: rename_map[old] = new df = df.rename(columns=rename_map)技术指标加的跟之前差不多,MA5、MA20、波动率、成交量变化率、价格区间位置这些。但真实数据有个问题——缺失值多。因为股票有停牌、节假日不交易,rolling 窗口计算的时候会出现 NaN。比如刚上市的新股,前20天算不了MA20。所以必须 dropna,丢掉的数据有点多但没办法。
df['ma5'] = df['close'].rolling(5).mean() df['ma20'] = df['close'].rolling(20).mean() df['volatility'] = df['ret'].rolling(20).std() df['price_position'] = ((df['close'] - df['low'].rolling(20).min()) / (df['high'].rolling(20).max() - df['low'].rolling(20).min() + 1e-8)) df = df.dropna().reset_index(drop=True)这里有个他吗的 bug 我找了半天,price_position 那一行我一开始写的是rolling(20'),多了一个单引号,Python 直接报 SyntaxError。我*这种低级错误 debug 了半小时才发现,眼瞎了属于是。
然后是主程序 main.py 改成平台化的交互模式,启动先显示 ASCII art 的 banner,然后列出18只热门股票让用户选。
平台分四个阶段跑:
- 获取真实行情数据
- 训练 LSTM-Attention 模型
- 模型评估
- 生成可视化图表(7张)
def run_platform(): code, name = select_stock() print(f"选定股票: {code} - {name}") df = fetch_and_prepare(code, years=5) result = train_model(df=df, feature_cols=FEATURE_COLS, ...) metrics = evaluate_model(...) # 生成7张图表 ...训练的时候有个问题,真实数据比生成数据噪音大得多,模型收敛慢。用真实数据跑平安银行,Loss 从 0.49 降到 0.036(验证集),MAPE 大概 4.78%。4.78% 的误差看起来还行,但方向准确率只有 49.66%,几乎就是瞎猜。这说明模型学到了价格的平均水平但完全判断不了涨跌方向。这其实很正常,股票预测要是能超过 50% 的方向准确率,我还写什么毕设早去量化私募了。
看综合看板那张图(dashboard.png),一页集成了所有信息:
- 左上大图是完整价格走势 + MA20 + 预测分界线 + 未来预测
- 中上是测试集预测对比
- 中中是注意力热力图
- 中下是损失曲线
- 左下是误差分布直方图
- 中是散点图
- 右下面板展示所有指标
Attention 热力图这张图还是挺有东西的,模型对不同的样本关注的时间段不一样。有的样本关注近期(最后几个时间步权重高),有的关注中期。说明 Attention 机制确实在学习不同的时间模式,不只是机械地看最后几天的价格。
未来30天预测那个图,因为用的是滚动预测,误差会累积。平安银行当前价格 10.76,预测30天后大概在 10.5 左右震荡。这种预测其实准确性存疑,但趋势方向可能有点参考价值。看历史数据的话平安银行从 2024 年开始就在 10-12 块之间横盘,确实没什么大波动。
有一次我训练贵州茅台(600519)的数据,MAPE 直接干到 2% 以下,因为茅台的走势太稳了几乎是一条斜线往上,模型学起来当然容易。
现在这版代码里有个问题我还没解决——获取全市场股票列表的 API 有时会超时。我加了每小时缓存的机制,第一次跑的时候如果列表 API 挂了就用预设的 18 只热门股作为备选,不影响正常使用。
还有个 bug 是数据量不够的问题。像科创板有些股票上市不到一年,数据量可能不到 120 条(60 条序列长度 + 至少 60 条训练),模型没法训练。data_fetcher 里我加了判断,不够的话自动扩大时间范围重试。
if df is None or len(df) < 120: start = end - timedelta(days=int(years * 365 * 2)) df = fetch_stock_data(symbol, start.strftime('%Y%m%d'), end.strftime('%Y%m%d')) if df is None or len(df) < 120: raise ValueError(f"无法获取 {symbol} 足够数据")SimHei 字体的问题这次也遇到了,因为换了新电脑重新装的环境,matplotlib 又找不到 SimHei 了。plt.rcParams['font.sysfont']试了半天没用,最后确认必须是plt.rcParams['font.sans-serif'] = ['SimHei'],而且axes.unicode_minus必须设 False 不然负号显示异常。
总的来说这版用真实数据比之前的好太多了,至少答辩的时候能说"数据来自东方财富真实A股行情",听起来就专业。交互式选股的功能也让这个项目更像是"平台"而不只是一个脚本。而且真实数据的噪声和不确定性让结果更有说服力——MAPE 4.78% 不是完美的结果但真实可信。如果导师问"为什么方向准确率不高",标准回答是"股票市场受多种因素影响,单纯量价数据无法完全预测涨跌方向,这恰恰说明了市场的有效性"(笑)。
最后再强调一下,跑之前确保网络能正常访问东方财富的 API,如果挂了实在不行就用我预设的18只股票,也是国内最有代表性的公司了。