尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于Django与Tushare的股票数据可视化平台构建与优化实践

基于Django与Tushare的股票数据可视化平台构建与优化实践 简介面向计算机相关专业学生与开发者的Django股票数据可视化项目资料基于Tushare金融数据接口实现行情数据的获取与可视化展示适合用于毕业设计、课程设计或项目初期演示也方便有一定基础者二次开发。资源包共265个文件大小仅2.8MB以JavaScript、Python、CSS为主包含HTML页面、SVG图标、PNG图片、Markdown文档等前台交互脚本、后端逻辑、样式布局与说明文档均有覆盖结构清晰便于对照学习。已有84人学习使用项目源码经过测试运行曾获导师认可并取得95分答辩成绩详细文档可帮助读者快速理解Django框架下的数据接口封装、前端图表渲染等关键环节是入门金融数据可视化与Web开发的实用参考。1. Tushare与Django的耦合链路从股票数据源到Web平台在Tushare官网注册、拿到token并不难难的是把接口返回的数据变成一张能交互的K线图。这个基于Django框架的股票数据可视化平台把数据获取、落库、接口、图表渲染四层串成一条完整链路Django负责Web服务和数据管理Tushare作为免费股票数据源前端用Bootstrap和ECharts完成展示。相比网上那些只贴一段爬虫脚本的教程这条链路的价值在于每一层都有明确的边界和可复现的代码。它适合正在做毕设或课设的计算机相关专业学生也适合想快速搭建一个数据可视化Demo的开发者——如果你需要采购数据、处理限频、接口鉴权、前端联动这一整套完整流程可以直接在这个框架上做二次开发。2. 搭建Django工程与Tushare数据管道Model设计、Token鉴权与入库2.1 Tushare积分机制与Token管理Tushare的数据接口不是所有权限默认全开积分决定了你能调哪些接口。日线行情接口daily需要120积分即可访问实时行情和财务数据需要更高积分交易日历接口在部分版本中需要单独授权。第一次调用如果提示“权限不足”不是代码问题是账号积分不够。Token的存放位置直接影响代码安全和部署体验我一般不会把token硬编码在settings.py里。常见做法是放在环境变量或用.env文件管理Django侧用os.getenv读取。这样代码推到仓库时不会泄露密钥部署到服务器时也只需要在环境变量里改一次。项推荐做法说明Token存储环境变量 / .env文件避免硬编码防止源码泄露后被他人盗用接口额度接口频率控制调取之间加 time.sleep免费版每分钟调用次数有限制高频请求会被限流错误重试try/except 延迟重试网络波动或接口升级时偶尔返回空数据重试能提高完整率数据落库校验检查DataFrame是否为空再入库Tushare对停牌日期返回空表直接入库会报错2.2 数据表的划分日线行情、股票列表与交易日历同步数据之前先把表结构想清楚。这个平台的核心表是股票日线行情字段直接对应Tushare的daily接口返回另外建议把股票基本信息表和交易日历表也建上。股票列表用于前端下拉框选择股票交易日历用于判断某个日期是不是交易日避免用户选了交易日外的日期导致图表空白。# stocks/models.py from django.db import models class StockBasic(models.Model): ts_code models.CharField(max_length16, primary_keyTrue) # Tushare股票代码如 000001.SZ symbol models.CharField(max_length16) # 纯数字代码 name models.CharField(max_length32) # 股票名称 industry models.CharField(max_length64, nullTrue, blankTrue) list_date models.CharField(max_length16, nullTrue, blankTrue) class Meta: db_table stock_basic class StockDaily(models.Model): ts_code models.CharField(max_length16, db_indexTrue) trade_date models.CharField(max_length16, db_indexTrue) # 格式 YYYYMMDD open models.FloatField() high models.FloatField() low models.FloatField() close models.FloatField() pre_close models.FloatField() change models.FloatField() pct_chg models.FloatField() vol models.FloatField() amount models.FloatField() class Meta: db_table stock_daily unique_together (ts_code, trade_date) # 防止重复同步这里有一个细节trade_date字段在Tushare接口返回的是字符串“YYYYMMDD”我用CharField而不是DateField存储。原因是字符串格式天然避免时区转换问题且YYYYMMDD的字典序和时序一致排序可以直接用ORDER BY trade_date查询和排序都稳定。字段注释一定写在Model里后面写视图和模板时能省掉大量回忆成本。2.3 数据同步命令把接口返回变成数据库记录数据同步不应该写在视图函数里那样用户每次打开网页都会触发一次接口调用。正确的做法是用Django的management command封装同步逻辑在命令行手动执行或挂定时任务。# stocks/management/commands/sync_stock.py import time import tushare as ts import pandas as pd from django.core.management.base import BaseCommand from django.conf import settings from stocks.models import StockBasic, StockDaily class Command(BaseCommand): help 同步Tushare日线数据到本地数据库 def handle(self, *args, **options): pro ts.pro_api(settings.TUSHARE_TOKEN) # 获取股票列表每页500条 stock_list pro.stock_basic(exchange, list_statusL, fieldsts_code,symbol,name,industry,list_date) for _, row in stock_list.iterrows(): StockBasic.objects.update_or_create( ts_coderow[ts_code], defaults{symbol: row[symbol], name: row[name], industry: row[industry], list_date: row[list_date]} ) self.stdout.write(股票列表同步完成) # 同步最近60个交易日的日线数据 cal pro.trade_cal(exchangeSSE, is_open1, start_date20250101, end_date20250401) date_list cal[cal_date].tolist() for code in stock_list[ts_code].tolist()[:20]: # 先同步20只验证 for date_str in date_list: try: df pro.daily(ts_codecode, trade_datedate_str) if df is None or df.empty: continue df[trade_date] date_str records df.to_dict(records) for rec in records: rec {k: (None if pd.isna(v) else v) for k, v in rec.items()} StockDaily.objects.update_or_create( ts_coderec[ts_code], trade_daterec[trade_date], defaultsrec ) except Exception as e: self.stderr.write(f{code} {date_str} 同步失败: {e}) time.sleep(0.2) # 控制调用频率避免Tushare限流 self.stdout.write(日线数据同步完成)代码逻辑说明先获取全量股票列表写入StockBasic再遍历交易日历逐日拉取日线数据。写入之前用pd.isna过滤NaN因为Django的FloatField入库时遇到NaN会直接抛异常——这是pandas数据进数据库最常见的坑。update_or_create保证重复执行不会产生重复记录幂等性是这类同步脚本必须具备的特性。参数调整建议同步股票数量和时间范围可以根据实际需求改如果只是演示用同步10到20只股票近三个月的日线数据足够支撑图表展示。数据量大时建议用bulk_create批量写入能减少90%以上的数据库连接开销。2.4 为什么先落库再查询而不是实时调接口可视化页面经常需要同时展示日K、成交量、均线三个图一次请求涉及多次Tushare调用实时拉取会有三个致命问题接口限频导致图表数据缺块、网络延时让页面打开要好几秒、多次重复调用会消耗账号积分配额。先落库再读数据页面查询从网络IO变成本地查询响应时间从秒级降到毫秒级这是这个平台和简单爬虫脚本最本质的区别。数据库选型方面项目初期用Django默认的SQLite就能跑通全部功能数据量到几十万条时再考虑切MySQL或PostgreSQL。切换时只需要改settings.py里的DATABASES配置Model层代码一行不用动Django的ORM把底层差异屏蔽掉了。3. 视图函数与前端可视化的数据装配ECharts联动与接口规范3.1 接口层设计一个视图返回一类图表数据前后端数据交互的格式需要统一约定这个平台里所有api接口统一返回{code: 0, data: {...}, msg: success}的结构。code为0表示成功非0表示业务异常msg里放具体的错误信息。这样做既方便前端统一做错误拦截也方便调试时直接看接口返回内容。# stocks/views.py import json from django.http import JsonResponse from django.shortcuts import render from django.views.decorators.http import require_GET from stocks.models import StockDaily require_GET def kline_data(request): 返回指定股票的K线数据日期升序排列 ts_code request.GET.get(ts_code, 000001.SZ) limit int(request.GET.get(limit, 120)) queryset StockDaily.objects.filter(ts_codets_code).order_by(-trade_date)[:limit] rows list(queryset.values(trade_date, open, high, low, close, vol, pct_chg)) rows.reverse() # 倒序取出来后再反转为升序 data { ts_code: ts_code, dates: [r[trade_date] for r in rows], kline: [[r[open], r[close], r[low], r[high]] for r in rows], # ECharts K线顺序 volumes: [r[vol] for r in rows], pct_chgs: [r[pct_chg] for r in rows], } return JsonResponse({code: 0, data: data, msg: success})逻辑说明视图接收ts_code和limit两个查询参数从数据库按交易日倒序取数据再反转成升序。ECharts的K线数据格式要求[open, close, low, high]这个顺序非常容易搞错我在开发时被这个问题坑过先在网上搜到一堆资料后来看了ECharts官方文档才算彻底确认。前端拿到这个接口的返回之后不做任何数据加工直接填入图表配置项数据格式的校验逻辑都收敛在后端。这里有个设计原则值得借鉴limit参数控制返回的数据量默认120个交易日前端切换周期时通过改参数复用同一个接口不需要为日K、周K分别写视图。3.2 ECharts在Django模板中的挂载方式Django模板和ECharts的配合方式很简单核心思路是模板负责渲染页面框架ECharts实例通过JavaScript请求api接口获取数据再调用setOption更新图表。!-- templates/kline.html -- {% extends base.html %} {% block content %} div classrow div classcol-md-12 div idkline-chart styleheight: 480px;/div /div /div {% endblock %} {% block extra_js %} script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script script $(function () { var chart echarts.init(document.getElementById(kline-chart)); $.getJSON(/api/kline?ts_code000001.SZlimit120, function (res) { if (res.code ! 0) { alert(res.msg); return; } var data res.data; chart.setOption({ title: { text: data.ts_code 日K线, left: center }, tooltip: { trigger: axis }, grid: [ { left: 60, right: 20, top: 50, height: 55% }, { left: 60, right: 20, top: 75%, height: 15% } ], xAxis: [ { type: category, data: data.dates, gridIndex: 0 }, { type: category, data: data.dates, gridIndex: 1 } ], yAxis: [ { scale: true, gridIndex: 0 }, { gridIndex: 1, axisLabel: { show: false } } ], dataZoom: [ { type: inside, xAxisIndex: [0, 1], start: 50, end: 100 } ], series: [ { name: K线, type: candlestick, data: data.kline }, { name: 成交量, type: bar, xAxisIndex: 1, yAxisIndex: 1, data: data.volumes } ] }); }); }); /script {% endblock %}初始化过程说明页面加载完成后先init创建图表实例再用jQuery的getJSON请求接口要注意的是必须在回调函数里调用setOption不能在外部直接操作还没拿到数据的chart对象。ECharts的dataZoom组件可以同时在K线图和成交量图上滑动缩放两份xAxis通过xAxisIndex参数绑定。3.3 多图表联动股票选择器与K线图的交互平台自带select2.css样式文件说明股票选择器不是普通的原生下拉框而是支持搜索的增强版组件。股票列表可能有几千条普通下拉框很难找到目标股票select2的搜索能力在这里是关键功能。// 股票选择联动K线 $(#stock-select).select2({ placeholder: 输入代码或拼音搜索股票, ajax: { url: /api/search_stock, dataType: json, delay: 250, // 防抖停止输入250ms后再发请求 data: function (params) { return { q: params.term }; }, processResults: function (res) { if (res.code ! 0) return { results: [] }; return { results: res.data.map(function (item) { return { id: item.ts_code, text: item.name ( item.ts_code ) }; }) }; } } }).on(select2:select, function (e) { var tsCode e.params.data.id; $.getJSON(/api/kline?ts_code tsCode, function (res) { if (res.code ! 0) return; var data res.data; klineChart.setOption({ title: { text: data.ts_code 日K线 }, xAxis: [{ data: data.dates }, { data: data.dates }], series: [{ data: data.kline }, { data: data.volumes }] }); // 更新股票信息统计卡片 $(#stock-name).text(data.ts_code); }); });联动的关键在select2的select事件回调里重新请求K线接口用setOption传入新的data数组覆盖旧数据。ECharts的setOption默认是合并模式只需要传变化的字段即可不需要把整个配置重新赋值。防抖参数delay设为250毫秒比较合适既不会频繁请求后端也能感知到输入停顿。admin后台在这一步顺带做了一件事把StockDaily注册到Django admin之后可以用admin的查询页面直接核对某只股票某个交易日的数据是否同步成功。调试前端图表数据异常时先查admin页面确认数据存在与否就能快速定位是后端同步问题还是前端渲染问题。4. Bootstrap静态资源落地与常见运行错误排查4.1 静态资源目录的摆放与配置资源包里的style.css、bootstrap.min.css、bootstrap-theme.min.css、font-awesome-4.0.3.css、select2.css、base.css、responsive.css、widgets.css实际上是一套完整的Bootstrap 3.x风格后台主题。拿到资源后第一步是把这些文件放到Django的static目录下并确认settings.py的配置正确。# settings.py 相关配置 import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_URL /static/ STATICFILES_DIRS [ os.path.join(BASE_DIR, static), ] # 如果开启了DEBUGFalse必须执行collectstatic收集静态文件 # STATIC_ROOT os.path.join(BASE_DIR, staticfiles)目录结构建议保持原有层级不要把所有css文件平铺到一个文件夹。因为responsive.css和widgets.css是依赖bootstrap核心样式的如果重命名或改变相对引用路径会导致样式丢失。配置完成后在浏览器访问http://127.0.0.1:8000/static/style.css如果能显示css内容说明静态文件服务正常。4.2 模板引用的顺序决定了页面长什么样这套主题的引用顺序有讲究错误顺序的典型表现是下拉框没有搜索功能、字体图标变成小方框、响应式布局失效。基模板base.html里的资源引用顺序如下!-- templates/base.html 头部资源引用 -- link relstylesheet href{% static bootstrap.min.css %} link relstylesheet href{% static bootstrap-theme.min.css %} link relstylesheet href{% static font-awesome-4.0.3.css %} link relstylesheet href{% static select2.css %} link relstylesheet href{% static style.css %} link relstylesheet href{% static responsive.css %} link relstylesheet href{% static widgets.css %}引用顺序的原则先是基础的bootstrap核心然后是对核心做外观微调的主题接着是字体图标、select2插件样式、自定义覆盖样式最后是响应式适配和组件库。style.css放在最后是为了覆盖框架默认样式如果把它放在bootstrap.min.css前面你在style.css里写的覆盖规则会因为CSS优先级相同但加载顺序靠前而被bootstrap覆盖掉导致页面看起来还是默认风格。4.3 runserver阶段最常见的三个报错把项目跑起来的过程中会遇到的问题高度集中我整理了出现频率最高的几类报错场景原因排查与处理ModuleNotFoundError: No module named tushare当前Python环境没有安装依赖pip install tushare pandas django用pip list验证版本django.db.utils.OperationalError: no such table没有执行数据库迁移python manage.py makemigrations python manage.py migrateFloatField收到NaN写入报错Tushare返回数据中有空值同步脚本中加pd.isna判断空值统一转成None页面正常但图表区域空白接口返回数据为空或前端JS报错浏览器F12查看Network中api响应内容再检查Console的JS错误静态文件404STATICFILES_DIRS路径配置错误或未restart在项目根目录下执行runserver并用DebugTrue模式调试数据库迁移这一步最容易被忽略的原因是你直接修改了models.py但没有生成迁移文件。Django的迁移系统不会自动检测Model变化必须以manage.py makemigrations显式生成迁移记录再执行migrate生效。改过字段后重复执行这两条命令即可。4.4 前端无数据时如何快速定位问题页面图表没数据时先别急着改代码按照“数据链路从底往上查”的原则逐个环节排除。第一步确认数据库有数据在manage.py shell里执行StockDaily.objects.count()如果返回0说明同步脚本没跑成功第二步确认接口有数据直接访问http://127.0.0.1:8000/api/kline?ts_code000001.SZlimit10看返回的JSON是否包含dates数组第三步确认前端能拿到数据浏览器打开页面按F12切换到Network面板重新加载找到kline接口的请求看响应内容。大多数“图表不显示”的问题最终都落在数据格式上。比如ECharts要求K线数据的每个元素是长度为4的数组如果你的接口给的是字典列表即使数字都对图表也画不出来。建议在同步脚本里加一个简单的数据格式校验函数入库前先打印几行记录确认格式无误后再批量写入。5. Redis缓存优化与接口压测给可视化大屏提速5.1 缓存层的必要性可视化大屏页面一打开会同时请求K线、成交量、个股涨幅榜等多个接口每个接口各查一次数据库。如果多块大屏同时访问数据库压力会直线上升。这些历史数据在交易日内是固定不变的每次请求都查库属于重复计算。市面上的企业级可视化项目都会在接口和数据库之间加一层缓存最常用的就是Redis。Redis适合这个场景的原因有两个一是性能好单机QPS能达到数十万级别远高于数据库查询二是支持设置过期时间数据在缓存里活多久可以精确控制。项目里需要额外安装django-redis这个库。5.2 cache_page装饰器与自定义缓存KeyDjango内置缓存框架提供了最省事的接入方式在视图函数上直接加装饰器就能启用缓存。# stocks/views.py from django.views.decorators.cache import cache_page cache_page(60 * 30, key_prefixkline_api) # 缓存30分钟 require_GET def kline_data(request): # 原有查询逻辑不变 pass# settings.py Redis缓存配置 CACHES { default: { BACKEND: django_redis.cache.RedisCache, LOCATION: redis://127.0.0.1:6379/1, OPTIONS: { CLIENT_CLASS: django_redis.client.DefaultClient, CONNECTION_POOL_KWARGS: {max_connections: 100}, } } }cache_page的作用是整个响应体缓存到Rediskey默认包含请求路径和查询参数。由于kline接口通过ts_code区分股票不同股票会生成不同的缓存key不会出现数据串台的情况。timeout参数设成1800秒意思是数据最多30分钟不访问数据库超时后自动回源查询并重建缓存。有个细节需要留意缓存key的前缀用key_prefix区分业务模块这样在Redis可视化客户端里一眼就能分辨哪些key属于K线接口方便手动清理。生产环境会加一层自定义缓存逻辑比如收盘后数据更新时主动调用cache.clear()清除当前模块的缓存而不是等它自然过期。5.3 压测验证缓存效果缓存加完之后要用压测手段验证效果。Linux和macOS自带ab命令可以直接用来模拟并发请求。# 压测命令200个请求20个并发 ab -n 200 -c 20 http://127.0.0.1:8000/api/kline?ts_code000001.SZlimit120不加缓存时接口需要做数据库查询和ORM对象组装在普通开发机上平均响应时间大约在50到100毫秒QPS在几十左右加了Redis缓存后响应体直接从内存读取平均响应时间会降到个位数毫秒QPS能提升十倍以上。压测时重点看Requests per second和Time per request两个指标如果Requests per second不升反降检查是不是Redis连接池配置太小或者缓存穿透导致每个请求都在查库。5.4 缓存更新策略与交易日节奏股票数据的更新节奏和交易日强相关盘中数据每分钟都在变收盘后当日数据不再更新。缓存过期时间的设计要匹配数据更新频率。常见做法是盘中设置30秒过期、收盘后手动刷新一次缓存。定时任务可以挂在crontab里每天15点30分执行一次同步脚本加清理缓存的命令。# crontab定时任务示例 30 15 * * 1-5 cd /path/to/project python manage.py sync_stock python manage.py clear_cache同步脚本和清缓存脚本串行执行是因为必须先确认新数据入库之后再清缓存否则清完缓存但数据没同步完下一次请求反而会把不完整的数据重新写回缓存。压测时重点看吞吐率和90%分位延迟这两个指标前者衡量系统整体处理能力后者能暴露长尾请求对用户体验的影响。本文还有配套的精品资源点击获取
返回列表