每年到毕业设计季节,我都会收到不少类似的咨询:想做一个和数据、可视化、预测相关、又能拿得出手的系统,但又怕难度控制不住、答辩讲不清楚。今天要聊的“基于Django和Vue.js的股票预测系统”,就是这类热度一直很高的选题。它把后端开发、前端可视化、机器学习预测、量化交易分析几个热门方向全部串在了一起,既沾了大数据和人工智能的热点,又能在有限时间内真正落地,而不是停留在概念PPT里。
这个系统解决的核心问题其实很实在:从真实股票数据源拉取历史行情,清洗后存入数据库,用模型预测未来几天的价格走势或涨跌方向,再叠加技术指标和回测结果生成买卖信号,最后通过网页把K线、预测曲线、交易信号、回测收益全部可视化展示出来。整个过程就是一条完整的“数据采集 - 特征计算 - 模型推理 - 策略验证 - 可视化展示”链路,比单纯做一个CRUD管理系统有深度得多。
我把这个项目从选型、建模、接口设计到前端图表、实时推送、部署答辩的经验完整拆给你,准备做类似题目的同学可以直接照着走,已经开工的也能对照着查漏补缺。
1. 项目整体设计与技术选型思路
1.1 为什么这个选题会这么火
股票预测系统在毕业设计里属于“自带亮点”的题目。它天然包含了金融数据、时间序列、机器学习、可视化这些听起来有含金量的关键词,同时又有非常落地的业务场景。很多同学选这个题,第一是因为它不用造轮子,数据接口和预测模型都有现成方案,第二是因为它好展示,PPT里放几张K线图加预测曲线,视觉冲击力远超普通管理系统。
但这个题也有它的隐形门槛。我见过不少同学一开始兴致勃勃想用LSTM预测股价,结果数据不会清洗,模型训练完不知道往哪放,前端要的数据格式和后端对不上,最后变成了一个只有登录注册和一张死图的空壳。所以选这个题之前,必须先承认一个事实:这个项目的难点不在“某个单点”,而在“把整条链路串起来”。谁能把链路串明白,谁就拿到了高分的基础。
1.2 系统架构与数据流设计
整体架构采用前后端分离模式,前端Vue.js负责交互和图表渲染,后端Django提供RESTful API和数据处理服务,数据库选用MySQL存储行情与用户数据,预测模块则单独封装成本地服务或Celery异步任务。
数据流是核心,建议按下面这条链路设计:
- 数据接入层:通过AkShare或Tushare定时拉取沪深A股日线行情,包含开高低收、成交量、成交额等基础字段
- 数据清洗层:处理停牌导致的空值、复权因子、异常跳空数据,统一存到MySQL
- 特征计算层:在Django内计算MA、MACD、RSI、KDJ等技术指标,作为预测和量化分析的输入
- 模型推理层:加载训练好的预测模型(ARIMA、Prophet或LSTM),输出未来N日价格或涨跌概率
- 策略回测层:基于预测结果和技术指标生成买卖信号,滑动回测历史区间,计算收益和回撤
- 可视化展示层:Vue.js配合ECharts渲染K线图、预测曲线、资金曲线、指标面板
这个分层的好处是每一层都能单独解释、单独测试,答辩被追问时你也好回答。比如被问“预测模块怎么优化的”,你只需要讲模型层;被问“数据怎么保证一致性”,你只需要讲清洗层。如果全揉在一个脚本里,一旦被追问细节就会非常被动。
1.3 技术栈选型的几个关键取舍
Django在这个项目里的角色不只是“写接口的工具”。它自带的ORM、Admin后台、迁移机制和用户认证,能省掉大量重复工作。特别是Django Rest Framework,序列化器加视图集几行代码就能把查询、过滤、分页、认证全部接上,非常契合毕设这种需要快速成型又必须有规范结构的场景。
Vue.js方面,我用的是Vue 3加Vite模板。Vue 3的组合式API在组织图表组件、定时器、WebSocket连接时比Options API更干净。ECharts则作为可视化的主力,它对K线图、折线图、仪表盘、热力图的支持都是最成熟的,社区文档也全,遇到问题几乎都能搜到答案。
这里有个重要的取舍建议:预测模型不要一上来就奔着神经网络。LSTM确实热门,但训练时间、数据量、归一化、序列构造这些环节对新手很不友好。更稳妥的做法是先用ARIMA或Prophet做一版能跑通的结果,把链路调顺,再考虑升级到LSTM。两个模型在系统里可以共存,通过参数切换,既展示了对比实验,又降低了翻车风险。
2. 核心功能模块拆解与实现要点
2.1 数据采集与预处理怎么做才规范
数据是整个系统的地基,但恰恰是很多同学做得最糙的一环。直接用接口拉几万条数据灌进数据库不叫完成,你得考虑字段设计、主键约束、索引和更新策略。
我建议的股票基础表结构是这样设计的:
| 字段 | 类型 | 说明 |
|---|---|---|
| ts_code | varchar(16) | 股票代码,如600519.SH |
| trade_date | date | 交易日期 |
| open | decimal(10,2) | 开盘价 |
| high | decimal(10,2) | 最高价 |
| low | decimal(10,2) | 最低价 |
| close | decimal(10,2) | 收盘价 |
| pre_close | decimal(10,2) | 昨收盘价 |
| vol | bigint | 成交量(手) |
| amount | decimal(20,2) | 成交额(元) |
| pct_chg | decimal(6,3) | 涨跌幅 |
表的联合主键建议设为(ts_code, trade_date),防止重复插入。实战中我吃过亏的是pct_chg这个字段,很多数据源返回的数字是浮点百分比,比如1.5代表涨了1.5%,如果你直接用float存,后续计算时容易忽略单位转换,导致指标算错。
数据清洗主要处理三种问题:
- 停牌缺失:当天没有交易的股票不会产生行情记录,不需要填零,直接跳过即可
- 复权处理:做长期回测一定要复权,否则除权除息日前后价格断崖式下跌会严重干扰预测和回测。我用的方案是前复权,因为前复权保证最新价格不变,画面最直观
- 异常值剔除:涨跌停、停牌复查、手误输入导致的极端值,可以通过涨跌幅超过20%的过滤规则筛掉
爬取策略上,不要每次启动项目都全量拉数据。建议本地维护一个数据版本表,记录每只股票已拉取到的最新日期,增量更新只拉“最新日期+1”到当天的数据,这样接口压力小,程序启动也快。
2.2 预测模型部分怎么落地
预测模型是这个系统的灵魂,也是答辩时最容易被深挖的部分。我建议至少做两个模型做对比:一个统计模型ARIMA,一个机器学习或深度模型LSTM,这样展示出来有层次感。
ARIMA落地时最核心的是差分阶数d的选择。股票价格通常是非平稳序列,我一般是先做ADF检验,如果p值大于0.05,就做一阶差分,再检验,通常一阶差分后就能平稳。p和q的确定可以用AIC最小值来搜索,遍历p在0到5、q在0到5的网格,选AIC最小的组合。这一步虽然简单,但写进论文里显得非常专业。
LSTM落地时有一个新手必踩的坑:数据泄漏。训练集和测试集必须严格按照时间顺序切分,绝对不能用未来的数据来做归一化。我的做法是先用训练集的均值方差拟合Scaler,训练集和测试集都调用同一个已拟合的Scaler去变换,这样就保证了测试过程看不到训练集之外的统计信息。
序列构造的细节也要注意。如果要预测明天收盘价,特征窗口我通常取60天,输入形状是(batch_size, 60, feature_dim)。特征列建议包含close、vol、pct_chg以及部分技术指标值,但不建议把raw的open、high、low全部塞进去,因为高低开和收盘价有高度共线性,会拖慢训练还容易过拟合。
保存和调用模型我用joblib加pickle。ARIMA模型小,直接pickle没问题;LSTM模型如果只用几层全连接或LSTM,模型文件也就几MB,可以放进项目目录。但注意Django进程加载模型文件时,路径最好用os.path.join(BASE_DIR, ...),避免相对路径在不同启动方式下报错。
评价指标必须做全,我用的是一组组合指标MAE、RMSE和方向准确率。方向准确率对股票场景特别有意义,因为你做预测最终是为了判断涨和跌,如果方向判断反了,就算预测值和真实值误差很小,在交易里也是亏钱的。我实测下来,沪深300指数日线级别的方向准确率能稳定在52%到56%,这个数字在答辩时已经很能说明问题了。
2.3 量化交易分析与回测模块
量化交易分析听起来高大上,落到这个项目里其实就三件事:技术指标计算、买卖信号生成、历史回测统计。
技术指标我建议做MA、MACD、RSI、KDJ四个,它们算法固定、解释性强,ECharts里画出来也好看。MACD的计算细节容易出错,EMA的初始值和平滑系数不同,结果会不一样。我在代码里固定用EMA12和EMA26,DEA用EMA9,这样和主流行情软件对齐,验证起来方便。
买卖信号生成逻辑可以直接用规则策略。比如最简单的双均线策略:MA5上穿MA20生成买入信号,MA5下穿MA20生成卖出信号。这种策略解释成本低,答辩时老师一听就懂。进阶一点可以叠加RSI超买超卖过滤,RSI大于70不追高,RSI小于30不杀跌。
回测框架有两个选择:自己写或用backtrader。毕设场景我建议自己写一个轻量级回测函数,逻辑非常清晰,还能顺便展示编程能力。核心逻辑是:
- 遍历每一天
- 第一天以开盘价买入一定仓位
- 遇到卖出信号时全部卖出
- 记录每日持仓市值和现金余额
- 计算资金曲线
回测统计指标重点输出年化收益率、最大回撤、夏普比率、胜率。特别提醒,回测时一定要把手续费和滑点算进去,A股默认佣金按万2.5算,卖出加收印花税千分之一。不加交易成本的回测曲线好看,但答辩时老师一句“你的回测考虑了手续费吗”就能让你哑口无言。
2.4 数据可视化与交互体验
可视化部分决定了老师打开你系统的第一印象。ECharts的折线图和K线图必须做扎实,我强烈建议至少包含三个核心图表:K线图加MA均线叠加强势股选择、预测价格曲线和真实价格曲线对比、资金曲线与基准收益率对比。
K线图用ECharts的candlestick系列,数据格式是[open, close, low, high],不少同学在这个顺序上栽过跟头。K线图下面建议配成交量柱状图和MACD柱状图,用dataZoom做联动缩放,这是股票软件的标准布局,做出来立刻就有专业感。
页面交互上,我做了三个实用功能:股票搜索和自动补全、时间区间选择器、指标开关。自动补全用Element Plus的el-autocomplete,从后端接口拉股票名称和代码。区间选择器直接控制图表展示的数据范围,不需要重新请求接口,性能更快。
还有一个容易被忽略的点:图表组件在Vue中必须正确处理销毁和重绘。页面切换时ECharts实例要调用dispose方法,否则会出现内存泄漏和画布重叠报错。用Vue的onBeforeUnmount钩子做清理,这个细节写进文档里也很加分。
3. 实操过程与核心代码实现
3.1 Django后端工程搭建与数据库建模
动手第一步是搭项目结构。我推荐按功能拆分app,不要所有代码堆在同一个app里。标准划分是三个app:stock负责股票数据和行情管理,prediction负责预测任务与模型加载,strategy负责指标计算和回测。
Django模型这块我贴一下核心代码:
from django.db import models class StockBasic(models.Model): ts_code = models.CharField(max_length=16, primary_key=True) symbol = models.CharField(max_length=8, db_index=True) name = models.CharField(max_length=32) industry = models.CharField(max_length=32, blank=True) list_date = models.DateField(null=True) class DailyQuote(models.Model): ts_code = models.CharField(max_length=16, db_index=True) trade_date = models.DateField(db_index=True) open = models.DecimalField(max_digits=10, decimal_places=2) high = models.DecimalField(max_digits=10, decimal_places=2) low = models.DecimalField(max_digits=10, decimal_places=2) close = models.DecimalField(max_digits=10, decimal_places=2) pct_chg = models.FloatField() vol = models.BigIntegerField() amount = models.DecimalField(max_digits=20, decimal_places=2) class Meta: unique_together = ('ts_code', 'trade_date')注意DailyQuote里的unique_together,这条约束在数据增量更新时配合get_or_create能有效防重。模型迁移后,如果数据量到百万级,记得给ts_code和trade_date加联合索引,查询速度会快很多。
API层我用DRF的ViewSet加ModelSerializer,股票列表、K线数据、技术指标、回测结果分别提供接口。接口返回格式我统一用{code, data, msg}的结构,前端好判断状态。行情数据接口要注意按trade_date排序和分页,前端K线组件一次性拿5000根K线没问题,但如果整表全量返回就会卡死。
3.2 预测接口与策略回测接口实现
预测接口的核心逻辑是:接收股票代码和窗口天数,从数据库取出最近N日行情,构造特征序列,加载模型推理,返回未来N天的预测值。
ARIMA模型在Django里的调用很简单,Pickle加载后用model.forecast(n)直接预测。但这里有个细节:ARIMA训练时的时间索引维度在模型里是固定的,换一只股票或者换一段数据就必须重新训练,所以我的设计是把模型文件按股票代码缓存,避免每次请求都重新拟合。加一个简单的字典缓存,key是ts_code,value是模型对象,实测响应时间能降低一个数量级。
LSTM推理前要处理的是特征构造和归一化。我从数据库抽出最近60天的数据,按训练时的Scaler转换,再变成(1, 60, feature_dim)的张量,推理后逆变换回价格区间。预测结果需要和真实K线合并成一个List返回给前端,格式统一成[{date, close, predict}],前段直接双折线展示。
回测接口相对独立。输入参数是股票代码、开始日期、结束日期、初始资金,输出是每日资金曲线和策略交易记录。回测代码核心逻辑是循环中判断信号并更新持仓状态,最后返回结果JSON。回测耗时一般在一秒内,可以同步执行,但如果未来要支持多股票组合回测,就得扔到Celery里异步跑。
3.3 Vue.js前端页面与图表联动
前端我用Vite创建Vue3项目,配合Vue Router和Pinia做状态管理。页面结构是左侧菜单加右侧内容区,菜单包含Dashboard总览、个股分析、预测与回测、量化策略四个模块。
个股分析页是核心页面,布局分上下两块:上面是搜索栏和股票信息卡片,下面是K线图和指标区。K线图组件封装如下:
// components/KLineChart.vue import * as echarts from 'echarts' import { onMounted, onBeforeUnmount, ref } from 'vue' const chartRef = ref(null) let chartInstance = null onMounted(() => { chartInstance = echarts.init(chartRef.value) }) onBeforeUnmount(() => { chartInstance && chartInstance.dispose() }) function renderKLine(dates, kData, ma5, ma10, volData) { chartInstance.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['K线', 'MA5', 'MA10'] }, grid: [ { left: 60, right: 20, top: 30, height: '55%' }, { left: 60, right: 20, top: '72%', height: '18%' } ], xAxis: [ { type: 'category', data: dates, boundaryGap: true }, { type: 'category', gridIndex: 1, data: dates } ], yAxis: [ { scale: true }, { gridIndex: 1, splitLine: { show: false } } ], dataZoom: [{ type: 'inside' }, { type: 'slider' }], series: [ { name: 'K线', type: 'candlestick', data: kData, itemStyle: { color: '#ef232a', color0: '#14b143', borderColor: '#ef232a', borderColor0: '#14b143' } }, { name: 'MA5', type: 'line', data: ma5, smooth: true, showSymbol: false }, { name: 'MA10', type: 'line', data: ma10, smooth: true, showSymbol: false }, { name: '成交量', type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: volData } ] }) }注意candlestick数据顺序是[open, close, lowest, highest],红涨绿跌的配色在国内股票软件是惯例,别用反了。联动的实现思路是所有子图表共用一个dataZoom索引,这样拖动缩放时K线、成交量和MACD会同步变化。
axios请求统一封装在request.js里,请求拦截器自动带上JWT Token,响应拦截器统一处理401跳转登录。项目里我用的是djangorestframework-simplejwt做认证,登录后前端把access和refresh两个token都存起来,请求头里带access,过期时用refresh换新的。
3.4 WebSocket实时推送的落地方式
毕设里如果能做实时数据推送,档次会明显提升。我做的方案是Django Channels加WebSocket,后端每5秒模拟推送一次最新的“虚拟实时行情”给前端,温度计的仪表盘实时刷新。实际生产可以对接证券行情推送,但毕设用模拟数据已经完全够用,关键是展示“后端主动推送,前端被动接收”的能力。
Django Channels的consumer代码骨架如下:
import json from channels.generic.websocket import AsyncWebsocketConsumer class QuoteConsumer(AsyncWebsocketConsumer): async def connect(self): self.group_name = 'quotes' await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def quote_send(self, event): await self.send(text_data=json.dumps(event['data'], ensure_ascii=False))前端Vue里用原生WebSocket连接,收到消息后更新图表。这里有个坑:Channels4和Daphne部署时,ASGI配置和进程数不仔细会出连接不上或推送错乱的问题。如果时间紧张,可以用一个替代方案:前端用setInterval轮询行情接口,效果接近,省去Channels的部署复杂度。但WebSocket作为亮点写进论文,含金量确实更高,建议有余力就上。
4. 常见问题与排查技巧实录
4.1 数据层面的坑
我在实际开发中遇到最多的就是数据问题。第一个坑是数据库时间字段的类型,Django的DateField对应MySQL的date类型,如果从接口拿的是datetime字符串,转换时要用pd.to_datetime然后取.date(),否则存入带时分秒的数据后,前端日期匹配会出现偏差。
第二个坑是前复权数据在回测里的使用。前复权会把历史价格整体压低,最新价保持真实值,这导致如果某个期间股票有多次分红,历史前复权价可能出现负值。负价格在计算收益率时会出奇怪结果,所以我的策略是回测前先过滤掉复权后价格为负的数据。
第三个坑是数据源接口频率限制。免费接口通常有每分钟调用次数限制,如果批量拉几千只股票的日线,很容易被限流。我的做法是拉取时加随机sleep,并且把拉取任务做成Celery周期任务,凌晨跑一次增量更新,避开白天的高峰时期。
4.2 模型与回测的坑
预测模块最常见的坑是“看起来很好,实际不能用于交易”。我在做对比实验时发现,直接用收盘价做LSTM预测,结果曲线往往比真实曲线滞后一天,因为模型学到了“今天的价格约等于昨天的价格”这个最简单规律,导致预测曲线整体右移。这种现象叫延迟预测,在答辩时一定要主动说明自己通过方向准确率和滞回检验发现过,并尝试用多步预测或残差修正来缓解,这就是亮点。
回测模块的坑主要在数据对齐和偷看未来。比如计算MA20时,第20天之前是没有MA20值的,这段区间的策略信号应该为空,不能把空值当0处理。防止偷看未来的关键在于:所有指标值都必须用历史数据算出,不允许用当天的收盘价提前知道第二天的信号。
我还遇到过回测收益率被高估的情况,原因是我用收盘价判断信号并在收盘价成交,实际操作中信号产生和成交之间的滑点完全没考虑。我后来在回测里加入了固定滑点,买入价加0.1%,卖出价减0.1%,曲线立刻变丑很多,但更真实。这种“曲线变丑但是更可信”的经验,恰恰是论文里值得写的一笔。
4.3 前后端联调与部署的坑
联调阶段遇到最多的问题是跨域。Django后端必须安装django-cors-headers,在settings里配置CORS_ALLOWED_ORIGINS,如果前端跑在5173端口,就填http://localhost:5173,不要图省事全部用星号,否则带Cookie的请求会有问题。
另一个高频问题是JWT Token的Authorization头格式。DRF的simplejwt默认要求格式是“Bearer ”,前端axios拦截器里必须加Bearer前缀,否则后端返回401。这个问题排查起来很隐蔽,因为网络请求本身是成功的,只是认证头格式不对。
部署环节我建议用Gunicorn加Nginx。Gunicorn跑Django应用,Nginx反向代理同时托管前端打包后的静态文件。后端接口路径用/api前缀,Nginx配置里把/api请求转发到Gunicorn的8000端口,其他路径直接指向dist目录。静态文件冲突是常见的麻烦,把所有接口统一挂/api前缀能轻松避开。
4.4 答辩和文档准备的建议
最后聊几句答辩的事。这套系统答辩的时候,老师大概率会问三个方向:数据从哪来、模型的准确性怎么验证、回测策略能不能赚钱。对应的答法我建议准备好:第一个方向讲你的增量更新和数据清洗流程;第二个方向讲你的评价指标体系,不要只讲准确率,要讲MAE和方向准确率;第三个方向比较敏感,明确说明系统是教学研究用途,不构成投资建议,同时坦诚讲清回测的限制条件。
文档部分,架构图用分层图加数据流图,时序图表达用户请求预测的完整链路。PPT不用太多页,按照选题背景、系统架构、功能展示、实验结果、总结展望这个顺序走,核心页面放图表截图和指标表格,别放大段代码,代码留给正文附件。
5. 最后的个人经验
这个项目我从最开始的纯Django模板渲染版本,一路改到前后端分离加WebSocket,前后删过三四版代码,最大的感悟是想清楚链路比写代码更重要。第一天就把数据流画出来,后面每一步都是往这个框架里填东西,效率会高非常多。
在预测模型上再多说一句,论文里不要只写“预测准确率高达90%”这种话,行家一看就知道是过拟合或者数据泄漏。真实市场里线性模型的预测有效性很有限,把实验过程写实、把局限性写透,反而比夸大效果更能获得老师的认可。
如果你还有时间做扩展,建议往三个方向加深:一是接入实时行情,把模拟推送换成真实接口;二是加入多因子选股模块,对全部A股做打分排序选前N只;三是把回测模块升级成参数优化器,用网格搜索自动找均线策略的最优参数。每一步都能让这个系统从“合格毕设”变成“优秀毕设”,也让你在面试讲项目的时候有更多东西可以说。
这套大而全的思路可能不是最省力的,但一定是一条能让你从头到尾讲清楚自己的项目、经得住老师连番追问的路。你按这个结构一步步做,踩坑了再回头看这篇文章,多半能对号入座找到解决办法。祝顺。