简介:一套开箱即用的股票数据可视化解决方案,用Python Django搭建后端服务,通过Tushare实时获取A股基础行情、历史K线、成交量及均线数据。系统内置完整的Web应用结构:数据库模型定义(models.py)、业务逻辑处理(views.py)、路由配置(urls.py)、REST接口封装(rests.py),还包含日志记录(logger.py)、日期格式转换(formatdate.py)、权限控制装饰器(decorators.py)和统一响应封装(response.py)。前端提供简洁的index.html主页和404错误页,后端支持Django 3.x/4.x与Python 3.8+,附带settings.py和config.py配置文件、数据库初始化脚本及详细README.MD说明文档。从环境安装、API密钥配置、数据库迁移、服务启动到页面访问,每一步都有明确指引。适合本科生毕设、课程设计或量化入门实践,代码结构清晰、模块职责分明,便于添加MACD/RSI等技术指标、回测逻辑或接入其他金融数据源。
1. 这不是又一个“Hello World”项目:为什么用Django搭股票可视化系统,而不是Flask或Streamlit?
你点开这个标题,大概率是正在为毕业设计焦头烂额的计算机系本科生,或者刚接触量化分析、想找个能跑起来的练手项目的入门者。我当年也是——大四上学期,导师甩来一句“做个股票相关的系统”,我第一反应是去GitHub搜“stock dashboard”,结果满屏都是React+Node.js堆砌的前端炫技项目,后端要么空着,要么只有一行npm install,本地跑起来报错二十行;还有些用Streamlit写的,界面确实快,但一想到答辩时老师问“数据库怎么设计的?”“权限怎么控制的?”“并发请求怎么扛?”我就头皮发麻。后来自己硬着头皮从零搭了一套,才真正明白:一个能拿出去讲清楚、经得起提问、还能继续往上加功能的系统,骨架比皮肤重要得多。
这套基于Django和Tushare的A股行情可视化系统,核心价值不在“能画K线图”,而在于它是一个完整、可演进、有生产思维的Web应用骨架。它用Django,不是因为Django多酷,而是因为它天然解决了本科生项目里最容易翻车的五个硬骨头:
-
数据库建模与迁移管理:
models.py里定义的StockBasic、DailyTrade、IndexData三张表,不是随便写的。StockBasic存股票基础信息(代码、名称、上市日期、行业),主键是ts_code(Tushare标准编码),带索引;DailyTrade存日线数据,复合索引(ts_code, trade_date)确保按股票查日期、按日期查全市场都快;IndexData专为大盘指数设计,字段精简但保留close、change、pct_chg等关键指标。这些设计背后是真实行情查询场景的倒推:你要查“贵州茅台2023年所有交易日”,数据库得在毫秒级返回;你要查“2023-01-01全市场涨跌幅前10”,索引必须覆盖trade_date和pct_chg。Flask没这层ORM约束,新手容易写出N+1查询,一查500只股票就卡死;Streamlit根本没数据库概念,数据全靠内存缓存,重启就丢。 -
权限与安全边界清晰:
decorators.py里的login_required_api和staff_only不是摆设。前者强制REST接口校验登录态(用Django默认session机制),后者限制只有后台管理员才能调用数据重载接口(比如手动触发全量更新)。这对应两个现实场景:一是毕设演示时,你不想让评委随便点个按钮就把你的测试数据库刷成空;二是后续加策略回测模块,得防止未授权用户调用耗资源的计算接口。Flask要自己写装饰器+session验证,容易漏掉CSRF防护;Streamlit连登录页都没有,纯靠文件系统权限硬隔离,答辩时老师问“如果别人拿到你服务器IP,能直接访问数据接口吗?”,你就只能沉默。 -
响应结构统一且可扩展:
response.py封装的SuccessResponse、ErrorResponse、PaginatedResponse,统一返回{"code": 0, "msg": "success", "data": {...}}格式。这不是为了好看,是为后续留接口。比如你明天想加MACD指标计算,只要在rests.py里新增一个/api/stock/macd/接口,返回的数据结构自动套进SuccessResponse,前端不用改任何解析逻辑;再比如加分页查询,PaginatedResponse直接把page_size、current_page、total_count塞进meta字段,Vue或React组件拿过来就能用。而裸写Flask视图,每个接口都要手动拼JSON,字段名大小写不一致、错误码不统一,后期维护就是噩梦。 -
配置与环境解耦:
config.py单独抽离TUSHARE_TOKEN、DB_ENGINE、DEBUG_MODE,settings.py通过from config import *导入。这意味着你本地开发用SQLite(DB_ENGINE='sqlite'),部署到学校服务器用MySQL(DB_ENGINE='mysql'),只需改一行配置,manage.py migrate自动适配不同数据库的SQL语法。我见过太多毕设项目,数据库连接字符串硬编码在views.py里,答辩前两天才发现MySQL密码错了,临时改代码到处找'localhost',最后手抖把'root'改成'roor',服务起不来,当场崩溃。 -
日志可追溯、问题可定位:
logger.py配置了console和file双输出,level=INFO记录正常流程,level=ERROR捕获异常堆栈。当你在views.py里写logger.info(f"获取{ts_code}日线数据,共{len(data)}条"),日志里就真有这一行;当Tushare接口突然限频,except Exception as e: logger.error(f"Tushare请求失败: {str(e)}")会把完整错误写进logs/app.log。答辩时老师问“如果数据加载不出来,你怎么排查?”,你打开日志文件,直接指出是TushareTokenInvalidError,而不是支吾说“可能网络不好”。
所以,这套系统真正的“开箱即用”,不是指双击run.bat就能看到网页,而是指你拿到代码,就能立刻理解每一层的设计意图,知道哪里该改、哪里不能动、哪里是预留的扩展口。它不教你如何写MACD公式,但它让你写的MACD模块,能无缝接入现有路由、数据库、权限和响应体系。这才是毕设和入门量化最需要的——不是玩具,而是脚手架。
2. 核心模块拆解:代码不是堆出来的,是“职责切片”出来的
很多人看Django项目,第一眼扫views.py,觉得“哦,全是函数”,然后就开始抄。其实这套系统的生命力,恰恰藏在那些不起眼的、被反复调用的小文件里。我把它们按“职责切片”重新梳理一遍,告诉你每个文件为什么存在、怎么用、踩过什么坑。
2.1 rests.py:REST接口的“业务胶水”,不是简单的数据搬运工
rests.py不是views.py的简化版,它是业务逻辑的前置过滤器和数据预处理器。比如get_stock_kline()这个函数:
def get_stock_kline(ts_code, start_date, end_date):
# 1. 先查缓存:检查数据库里有没有这段日期的数据
cached_data = DailyTrade.objects.filter(
ts_code=ts_code,
trade_date__range=(start_date, end_date)
).order_by('trade_date').values(
'trade_date', 'open', 'high', 'low', 'close', 'vol', 'amount'
)
if cached_data:
return list(cached_data)
# 2. 缓存无命中,才调Tushare API
try:
df = pro.daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
# 3. 数据清洗:Tushare返回的vol是“手”,需转为“股”;amount单位是“千元”,转为“元”
df['vol'] = df['vol'] * 100
df['amount'] = df['amount'] * 1000
# 4. 写入数据库缓存(异步?不,这里同步写,保证下次查询快)
for _, row in df.iterrows():
DailyTrade.objects.update_or_create(
ts_code=ts_code,
trade_date=row['trade_date'],
defaults={
'open': row['open'], 'high': row['high'], 'low': row['low'],
'close': row['close'], 'vol': int(row['vol']), 'amount': int(row['amount'])
}
)
return df.to_dict('records')
except Exception as e:
logger.error(f"Tushare获取K线失败 {ts_code}: {e}")
raise
注意三个关键点:
- 缓存优先策略:每次请求先查库,有就直接返回,避免频繁调用Tushare(免费版有调用频率限制)。这是性能底线,不是可选项。
- 单位转换硬编码:Tushare文档写得很清楚,vol字段单位是“手”(1手=100股),amount是“千元”。但新手常忽略这点,直接把原始数据扔给前端画图,结果成交量柱子矮得看不见——因为实际是100倍关系。我在rests.py里强制转换,并加注释# Tushare vol单位是“手”,转为“股”,就是防这种低级错误。
- update_or_create而非create:防止重复插入同一天数据。Tushare偶尔会返回重复日期(尤其跨月时),用create会报IntegrityError,服务直接500;update_or_create则静默覆盖,保证数据最终一致性。
提示:
rests.py里所有函数,都遵循“查缓存→调API→存缓存→返回”四步铁律。新加接口(比如未来加RSI计算),必须先在这个文件里实现数据获取逻辑,再在views.py里调用它。这样views.py只负责HTTP协议处理(接收参数、校验、调用rests、封装响应),不掺杂业务细节,职责单一。
2.2 formatdate.py:日期不是字符串,是“时间契约”
formatdate.py只有两个函数:date_to_str(date_obj)和str_to_date(date_str)。看起来简单,但它是整个系统时间处理的“宪法”。
def date_to_str(date_obj):
"""将date/datetime对象转为YYYYMMDD字符串,Tushare要求格式"""
if isinstance(date_obj, datetime):
return date_obj.strftime('%Y%m%d')
elif isinstance(date_obj, date):
return date_obj.strftime('%Y%m%d')
else:
raise TypeError("输入必须是date或datetime对象")
def str_to_date(date_str):
"""将YYYYMMDD字符串转为date对象,兼容Tushare和前端传参"""
try:
return datetime.strptime(date_str, '%Y%m%d').date()
except ValueError:
# 兼容前端可能传YYYY-MM-DD格式(如DatePicker组件)
return datetime.strptime(date_str, '%Y-%m-%d').date()
为什么需要它?
- Tushare API的硬性要求:所有日期参数必须是YYYYMMDD格式字符串,比如20230101。如果你在views.py里直接用datetime.now().strftime('%Y-%m-%d'),传给pro.daily()就会报错Invalid date format。
- 前端交互的灵活性:HTML <input type="date"> 默认返回YYYY-MM-DD,而用户手动输入可能写2023/01/01。str_to_date()做了容错,先试%Y%m%d,失败再试%Y-%m-%d,避免因日期格式问题导致整个接口崩掉。
- 数据库字段类型匹配:DailyTrade.trade_date字段是DateField,Django要求存date对象,不是字符串。str_to_date()确保传入的字符串总能转成合法date,否则save()时会抛ValidationError。
注意:所有涉及日期的参数传递(URL路径、Query参数、POST body),必须经过
str_to_date()转换;所有返回给前端的日期字段(如trade_date),必须用date_to_str()格式化。我在views.py里写了条注释:“// 所有日期进出,必过formatdate.py”,这就是契约。
2.3 decorators.py:权限不是锦上添花,是安全底线
decorators.py里的login_required_api,表面看只是加个@login_required,但它的实现细节决定了系统是否真的安全:
def login_required_api(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if not request.user.is_authenticated:
return ErrorResponse(code=401, msg="未登录,请先登录")
if request.method == 'GET':
# GET接口允许普通用户访问
return view_func(request, *args, **kwargs)
else:
# POST/PUT/DELETE接口,要求用户是staff(后台管理员)
if not request.user.is_staff:
return ErrorResponse(code=403, msg="权限不足,仅管理员可操作")
return view_func(request, *args, **kwargs)
return _wrapped_view
关键点:
- 区分HTTP方法:GET查数据,放开给所有登录用户;POST(如数据重载)、DELETE(如清空缓存)必须是is_staff。这对应真实场景:学生可以查股票,但不能一键删库。
- 返回标准错误码:不是HttpResponse("请登录", status=401),而是调用ErrorResponse(code=401, msg="未登录"),保证响应结构统一。
- 不依赖Session中间件:Django默认的@login_required装饰器会重定向到登录页,但REST接口不该跳转。这个自定义装饰器直接返回JSON错误,前端可捕获code=401跳转登录页。
实操心得:部署到学校服务器时,务必在
settings.py里设置LOGIN_URL = '/admin/login/',并确保django.contrib.auth.middleware.AuthenticationMiddleware已启用。我曾因忘记开这个中间件,导致装饰器永远认为request.user.is_authenticated为False,所有接口都返回401——查日志发现request.user是AnonymousUser,才意识到中间件没生效。
2.4 response.py:响应不是return字典,是“协议声明”
response.py的SuccessResponse类,核心是__init__方法:
class SuccessResponse(HttpResponse):
def __init__(self, data=None, msg="success", code=0, **kwargs):
response_data = {
"code": code,
"msg": msg,
"data": data or {}
}
# 如果data是QuerySet,转为list;如果是model instance,转为dict
if hasattr(data, 'values') and callable(getattr(data, 'values')):
response_data["data"] = list(data.values())
elif hasattr(data, '__dict__'):
response_data["data"] = model_to_dict(data)
super().__init__(
json.dumps(response_data, ensure_ascii=False, default=str),
content_type="application/json;charset=utf-8",
**kwargs
)
为什么这么写?
- 自动序列化QuerySet:DailyTrade.objects.filter(...).values()返回的是ValuesQuerySet,json.dumps无法直接序列化。SuccessResponse检测到hasattr(data, 'values'),自动调用list()转成Python列表,再JSON序列化。
- 兼容Model实例:get_object_or_404(StockBasic, ts_code=code)返回单个Model对象,SuccessResponse用model_to_dict()转成字典,避免TypeError: Object of type StockBasic is not JSON serializable。
- default=str兜底:Django的DateTimeField、DecimalField等特殊类型,json.dumps不认识,default=str将其转为字符串(如"2023-01-01"、"32.50"),保证不报错。
注意:
SuccessResponse和ErrorResponse都继承HttpResponse,不是JsonResponse。因为JsonResponse会强制Content-Type: application/json,而某些场景(如下载CSV)需要text/csv。统一用HttpResponse,由子类控制content_type,更灵活。
3. 从零启动:环境搭建、数据获取到页面渲染的全流程实操
别被“全流程”吓到,这套系统设计时就考虑了本科生的实操场景:没有Docker、不依赖云服务、不碰Linux命令行(Windows/Mac/Linux全适配)。我按你真实操作顺序,一步步拆解,每一步都标出“为什么这么做”和“不做会怎样”。
3.1 环境准备:Python、Django、Tushare,三件套缺一不可
第一步:安装Python 3.8+
- Windows:去python.org下载最新3.8+安装包,勾选“Add Python to PATH”,一路下一步。
- Mac:brew install python3(需先装Homebrew)。
- Linux(Ubuntu):sudo apt update && sudo apt install python3.8 python3-pip。
为什么必须3.8+?Tushare 2.x要求Python >= 3.7,但Django 4.x官方支持只到3.8+。用3.7可能遇到
async语法报错,答辩时解释不清。
第二步:创建虚拟环境(关键!)
# 进入你的项目目录(比如 D:\stock-system)
cd /path/to/your/project
# 创建虚拟环境(名字叫venv,Django惯例)
python -m venv venv
# 激活虚拟环境
# Windows:
venv\Scripts\activate.bat
# Mac/Linux:
source venv/bin/activate
提示:虚拟环境是隔离的“沙盒”。你装的Django、Tushare只在这个项目里有效,不会污染全局Python。我见过同学全局
pip install django,结果电脑里十几个项目版本冲突,最后重装系统。venv就是防这个。
第三步:安装依赖
# 确保激活了venv(命令行前应有(venv))
pip install -r requirements.txt
# 如果没有requirements.txt,手动装:
pip install django==4.2.7 tushare==2.3.6 pandas==1.5.3
版本锁定原因:Django 4.2.7是LTS长期支持版,稳定;Tushare 2.3.6是当前兼容性最好的版本(新版对免费token限频更严);pandas 1.5.3避免与旧版numpy冲突。不要用
pip install django tushare,可能装到最新版,导致pro = ts.pro_api()报错。
3.2 配置环节:Tushare Token和数据库,两处填错就全盘皆输
第一步:获取Tushare Token
- 访问Tushare官网注册账号。
- 登录后进入“个人中心”→“API接口”,复制你的Token(一长串字母数字,如abc123def456...)。
- 打开项目根目录下的config.py,找到:
python TUSHARE_TOKEN = "your_token_here" # ← 把你的Token粘贴到这里,删掉引号外的空格
- 保存文件。
注意:Token是你的“数据钥匙”,绝不能上传到GitHub!
gitignore里已包含config.py,但你自己要确认没手抖git add config.py。我有个同学Token泄露,被人写脚本疯狂调用,一天刷完免费额度,最后只能买付费套餐。
第二步:数据库初始化
- 默认用SQLite(免安装,适合毕设)。settings.py里已配置:
python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', } }
- 执行迁移命令:
bash python manage.py makemigrations python manage.py migrate
这会在项目目录生成db.sqlite3文件,里面已有auth_user(Django用户表)、stock_basic等空表。
如果要用MySQL(学校服务器常见):
1. 安装MySQL(或用学校提供的数据库服务);
2. 在config.py里设DB_ENGINE = 'mysql';
3. 修改settings.py的DATABASES,填入你的MySQL地址、用户名、密码;
4.pip install mysqlclient(Windows可能需先装VC++编译工具);
5. 再执行makemigrations和migrate。
3.3 数据加载:第一次运行,让系统“活”起来
第一步:创建超级用户(登录后台用)
python manage.py createsuperuser
# 按提示输入用户名(如admin)、邮箱(可空)、密码(记住!)
第二步:加载股票基础信息(必需!)
python manage.py load_stock_basic
这个自定义命令在management/commands/load_stock_basic.py里,作用是调用pro.query('stock_basic'),把A股所有股票代码、名称、上市日期等信息存进StockBasic表。执行后,db.sqlite3里就有几千条股票数据了。
为什么先加载基础信息?因为K线查询接口
/api/kline/需要ts_code参数,而ts_code(如000001.SZ)必须先存在于StockBasic表里,前端下拉框才能列出可选股票。不执行这步,首页下拉框是空的,你连“查哪只股票”都不知道。
第三步:启动服务,访问首页
python manage.py runserver
浏览器打开http://127.0.0.1:8000,看到简洁的index.html页面,顶部有股票代码输入框、日期选择器、查询按钮——系统活了。
实操心得:如果页面空白或报错,先看终端输出的错误信息。常见问题:
-ModuleNotFoundError: No module named 'tushare':没激活venv,或pip install没成功;
-OperationalError: no such table: stock_basic:没执行load_stock_basic,或migrate失败;
-TushareTokenInvalidError:config.py里的Token错了,或Tushare官网没开通数据权限(免费用户需实名认证)。
3.4 前端交互:index.html不是静态页,是“数据管道入口”
index.html看着简单,但它的JavaScript逻辑是整个系统的“神经末梢”:
<!-- 股票代码下拉框 -->
<select id="stock-select" class="form-control">
<option value="">请选择股票...</option>
<!-- 这里由JS动态填充 -->
</select>
<script>
// 1. 页面加载时,用AJAX获取所有股票列表
fetch('/api/stock/list/')
.then(res => res.json())
.then(data => {
const select = document.getElementById('stock-select');
data.data.forEach(stock => {
const option = document.createElement('option');
option.value = stock.ts_code;
option.text = `${stock.name} (${stock.ts_code})`;
select.appendChild(option);
});
});
// 2. 点击查询按钮,调用K线接口
document.getElementById('query-btn').onclick = function() {
const ts_code = document.getElementById('stock-select').value;
const start_date = document.getElementById('start-date').value;
const end_date = document.getElementById('end-date').value;
fetch(`/api/kline/?ts_code=${ts_code}&start_date=${start_date}&end_date=${end_date}`)
.then(res => res.json())
.then(data => {
if (data.code === 0) {
// 3. 用Chart.js画K线图(代码在static/js/chart.js里)
renderKLineChart(data.data);
} else {
alert(`错误:${data.msg}`);
}
});
};
</script>
关键点:
- /api/stock/list/接口:返回所有StockBasic数据,供下拉框填充。它用SuccessResponse封装,前端直接取data.data。
- 日期格式自动转换:HTML <input type="date">返回YYYY-MM-DD,但/api/kline/接口要求YYYYMMDD。fetch URL里用了模板字符串,但实际应由前端JS转换:
javascript function formatDate(dateStr) { return dateStr.replace(/-/g, ''); // "2023-01-01" → "20230101" } fetch(`/api/kline/?ts_code=${ts_code}&start_date=${formatDate(start_date)}&end_date=${formatDate(end_date)}`)
(这个转换逻辑在static/js/main.js里已实现,你只需确保调用正确)
提示:
index.html里引用的Chart.js在static/js/chart.js,它封装了K线图绘制逻辑。你不需要懂Canvas绘图,只需传入data.data(数组,每项含trade_date,open,high,low,close,vol),它自动画出带成交量的K线。这是“开箱即用”的核心——你专注业务,图表交给专业库。
4. 功能扩展实战:加均线、接新数据源、做简单回测,三步走稳
系统设计时就预留了扩展口,不是让你“改核心”,而是“插模块”。我以三个高频需求为例,手把手带你加功能,每一步都有代码、有原理、有避坑点。
4.1 加MA5/MA10均线:不改一行原有代码,只增新文件
均线计算逻辑放在utils/indicators.py(新建目录和文件):
# utils/indicators.py
import pandas as pd
import numpy as np
def calculate_ma(df, window=5):
"""
计算移动平均线
:param df: pandas DataFrame,含'close'列
:param window: 窗口大小(天数)
:return: 新增'ma_{window}'列的DataFrame
"""
df = df.copy()
df[f'ma_{window}'] = df['close'].rolling(window=window).mean()
return df
# 示例:计算MA5和MA10
def add_ma_columns(df):
df = calculate_ma(df, window=5)
df = calculate_ma(df, window=10)
return df
然后在rests.py里新增接口:
# rests.py 新增
def get_stock_kline_with_ma(ts_code, start_date, end_date):
"""获取K线数据并附加MA5、MA10"""
kline_data = get_stock_kline(ts_code, start_date, end_date) # 复用原有逻辑
if not kline_data:
return []
df = pd.DataFrame(kline_data)
df = add_ma_columns(df)
# 转回字典列表,保持前端兼容
return df.to_dict('records')
最后在urls.py里加路由:
# urls.py
urlpatterns = [
# ...原有路由
path('api/kline-ma/', views.kline_with_ma, name='kline_with_ma'), # 新增
]
views.py里写视图:
# views.py
def kline_with_ma(request):
ts_code = request.GET.get('ts_code')
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
try:
data = rests.get_stock_kline_with_ma(ts_code, start_date, end_date)
return SuccessResponse(data=data)
except Exception as e:
logger.error(f"MA计算失败: {e}")
return ErrorResponse(msg="均线计算出错")
为什么这样设计?
-utils/indicators.py是纯计算模块,不依赖Django、不碰数据库,单元测试方便(pytest test_indicators.py);
-rests.py复用get_stock_kline(),保证数据源一致,避免重复调用Tushare;
- 新增路由/api/kline-ma/,不影响原有/api/kline/接口,前端可自由选择用哪个;
- 前端chart.js里,renderKLineChart()函数已预留ma5、ma10字段解析逻辑,你只需在fetch时换URL,图表自动叠加均线。
4.2 接入聚宽(JoinQuant)数据源:替换Tushare,不是重写系统
聚宽提供类似API,但返回格式不同。我们不改rests.py,而是新增rests_jq.py:
# rests_jq.py
import jqdatasdk as jq
def init_jq(token):
"""初始化聚宽SDK"""
jq.auth('your_jq_username', 'your_jq_password') # 聚宽账号密码
def get_jq_kline(ts_code, start_date, end_date):
"""从聚宽获取K线(示例)"""
# 聚宽代码映射:Tushare的'000001.SZ' → 聚宽的'000001.XSHE'
jq_code = ts_code.replace('.SZ', '.XSHE').replace('.SH', '.XSHG')
df = jq.get_price(
security=jq_code,
start_date=start_date,
end_date=end_date,
frequency='daily',
fields=['open', 'high', 'low', 'close', 'volume']
)
# 聚宽volume单位是“股”,无需转换
df.reset_index(inplace=True)
df.rename(columns={'index': 'trade_date'}, inplace=True)
return df.to_dict('records')
然后在views.py里根据配置切换数据源:
# views.py
from config import DATA_SOURCE # 'tushare' 或 'jq'
def kline_view(request):
ts_code = request.GET.get('ts_code')
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
if DATA_SOURCE == 'tushare':
data = rests.get_stock_kline(ts_code, start_date, end_date)
elif DATA_SOURCE == 'jq':
data = rests_jq.get_jq_kline(ts_code, start_date, end_date)
else:
data = []
return SuccessResponse(data=data)
关键点:
-DATA_SOURCE在config.py里配置,开发时用tushare,部署时改jq,零代码修改;
-rests_jq.py只负责数据获取,不碰缓存、不写数据库,保持职责单一;
- 聚宽需单独pip install jqdatasdk,且账号需开通相应权限(免费版有调用限制)。
4.3 添加简易回测模块:从“看数据”到“验策略”
回测不是高深算法,而是“按规则模拟买卖”。我们在apps/backtest/下新建App:
python manage.py startapp backtest
backtest/models.py定义回测任务:
from django.db import models
class BacktestTask(models.Model):
name = models.CharField(max_length=100) # 策略名称,如"MA金叉"
ts_code = models.CharField(max_length=20) # 股票代码
start_date = models.DateField()
end_date = models.DateField()
params = models.JSONField() # 策略参数,如{"ma_short": 5, "ma_long": 10}
status = models.CharField(max_length=20, default='pending') # pending/running/done
result = models.JSONField(null=True, blank=True) # 回测结果
created_at = models.DateTimeField(auto_now_add=True)
backtest/views.py提供提交和查询接口:
from django.http import JsonResponse
from .models import BacktestTask
from .tasks import run_backtest # 异步任务
def submit_backtest(request):
if request.method == 'POST':
data = json.loads(request.body)
task = BacktestTask.objects.create(
name=data['name'],
ts_code=data['ts_code'],
start_date=data['start_date'],
end_date=data['end_date'],
params=data['params']
)
# 异步执行回测(用Django Q或Celery,此处简化为同步)
result = run_backtest(task)
task.result = result
task.status = 'done'
task.save()
return SuccessResponse(data=result)
def get_backtest_result(request, task_id):
try:
task = BacktestTask.objects.get(id=task_id)
return SuccessResponse(data={
'status': task.status,
'result': task.result
})
except BacktestTask.DoesNotExist:
return ErrorResponse(msg="任务不存在")
backtest/tasks.py是核心逻辑(简化版MA金叉策略):
def run_backtest(task):
# 1. 获取K线数据(复用rests.get_stock_kline)
kline_data = rests.get_stock_kline(task.ts_code, task.start_date, task.end_date)
df = pd.DataFrame(kline_data)
# 2. 计算MA
df['ma5'] = df['close'].rolling(5).mean()
df['ma10'] = df['close'].rolling(10).mean()
# 3. 生成买卖信号(金叉:MA5上穿MA10)
df['signal'] = 0 # 0=空仓,1=买入,-1=卖出
for i in range(10, len(df)):
if df.iloc[i-1]['ma5'] <= df.iloc[i-1]['ma10'] and df.iloc[i]['ma5'] > df.iloc[i]['ma10']:
df.loc[i, 'signal'] = 1 # 金叉买入
elif df.iloc[i-1]['ma5'] >= df.iloc[i-1]['ma10'] and df.iloc[i]['ma5'] < df.iloc[i]['ma10']:
df.loc[i, 'signal'] = -1 # 死叉卖出
# 4. 模拟交易(初始资金100000,不考虑手续费)
cash = 100000
shares = 0
for i in range(len(df)):
if df.iloc[i]['signal'] == 1 and cash > 0:
# 买入:用全部现金买
price = df.iloc[i]['close']
shares = cash // price
cash -= shares * price
elif df.iloc[i]['signal'] == -1 and shares > 0:
# 卖出:卖出全部
price = df.iloc[i]['close']
cash += shares * price
shares = 0
# 5. 最终收益
final_value = cash + (shares * df.iloc[-1]['close']) if shares > 0 else cash
profit = final_value - 100000
return {
'initial_capital': 100000,
'final_value': round(final_value, 2),
'profit': round(profit, 2),
'profit_rate': round(profit / 100000 * 100, 2),
'total_trades': len(df[df['signal'] != 0])
}
注意事项:
- 回测是CPU密集型任务,生产环境必须用Celery异步队列,避免阻塞Web请求;
- 此代码是教学简化版,真实回测需考虑滑点、手续费、停牌、涨跌停无法成交等;
-BacktestTask模型支持历史任务查询,前端可做成列表页,展示所有回测记录。
5. 常见问题与排查技巧实录:那些让我熬过三个通宵的坑
以下问题,全部来自我帮学弟学妹调试毕设的真实记录。不是理论,是血泪经验。
5.1 “页面空白,控制台报错:Uncaught ReferenceError: Chart is not defined”
现象:首页打开,K线图区域一片空白,F12看Console报错Uncaught ReferenceError: Chart is not defined。
排查步骤:
1. 查index.html里<script src="...">路径是否正确。static/js/chart.js引用的是<script src="{% static 'js/chart.js' %}"></script>,确保STATIC_URL在settings.py里设为'/static/',且STATICFILES_DIRS包含BASE_DIR / "static"。
2. 查chart.js是否真的加载了。Network标签页里找chart.js,状态码是不是200。如果不是,检查static目录结构:project/static/js/chart.js,不是project/static/static/js/chart.js(Django不会自动加双重static)。
3. 查Chart.js版本。chart.js文件开头有// Chart.js v2.9.4,而新版v4.x API不兼容。本系统用v2.9.4,已打包在static/js/里,不要CDN引入。
解决方案:删掉所有CDN链接,只用本地
static/js/chart.js;确认static目录在Django查找路径中。
5.2 “Tushare接口报错:TushareTokenInvalidError,但Token肯定没错”
现象:python manage.py load_stock_basic报错TushareTokenInvalidError,config.py里Token复制粘贴多次,确认无空格。
排查步骤:
1. 查Tushare官网个人中心,确认账号已实名认证(免费用户必须实名才能调用基础接口)。
2. 查Token是否过期。Tushare Token永久有效,但若你在官网“重置Token”,旧Token立即失效。
3. 查网络代理。公司/学校网络有时会拦截HTTPS请求,导致Token传输失败。临时关掉代理软件,或用手机热点测试。
终极方案:在
rests.py里加一行调试:
python print(f"Using Token: {ts_token[:5]}...{ts_token[-5:]}") # 打印Token首尾,确认读取正确
5.3 “查询K线返回空数据,但Tushare官网能查到”
现象:前端输入000001.SZ、20230101到20231231,接口返回{"code":0,"msg":"success","data":[]}。
排查步骤:
1. 查数据库是否有缓存。执行python manage.py dbshell(SQLite)或mysql -u user -p,运行:
sql SELECT COUNT(*) FROM daily_trade WHERE ts_code='000001.SZ';
若为0,说明缓存没写入。
2. 查rests.py里get_stock_kline()是否跳过了API调用。加日志:
python logger.info(f"Cache query for {ts_code} from {start_date} to {end_date}") cached_data = DailyTrade.objects.filter(...) # 原有代码 logger.info(f"Cached count: {len(cached_data)}")
3. 查Tushare返回的DataFrame是否为空。在get_stock_kline()里加:
python logger.info(f"Tushare returned {len(df)} rows")
常见原因:Tushare的
pro.daily()对日期范围敏感。start_date='20230101',但000001.SZ在2023年1月1日休市(周末),Tushare返回空DataFrame。解决方案:前端日期选择器设min="20230102",或后端自动取最近交易日(用pro.trade_cal查)。
5.4 “部署到学校服务器,页面CSS失效,全是白底黑字”
现象:本地runserver正常,部署到Linux服务器(Apache/Nginx),静态文件404。
排查步骤:
1. 查settings.py里DEBUG = False时,Django不提供静态文件服务,必须由Web服务器托管。
2. 查collectstatic是否执行:
bash python manage.py collectstatic --noinput
这会把所有static文件复制到STATIC_ROOT指定目录(如/var/www/static)。
3. 查Web服务器配置。Nginx示例:
nginx location /static/ { alias /var/www/static/; expires 1y; add_header Cache-Control "public, immutable"; }
关键点:
DEBUG=False时,STATIC_URL='/static/'必须与Nginx的location /static/匹配;STATIC_ROOT必须是绝对路径,且Web服务器有读取权限。
5.5 “添加MACD指标后,图表渲染变慢,滑动卡顿”
现象:加了MACD计算,K线图加载时间从200ms变成2s,拖拽图表明显卡顿。
优化方案:
1. 服务端计算,非前端:MACD计算逻辑放在rests.py,返回时已包含macd, signal, histogram字段,前端只负责渲染,不计算。
2. 分页加载:K线数据超过1000条时,前端分页(如每次只加载300条),滚动到底部再加载下一页。
3. Web Worker:复杂计算放Web Worker线程,避免阻塞UI主线程(static/js/worker-macd.js)。
我的实测:1000条K线+MACD计算,服务端耗时约300ms(Python+pandas),前端渲染100ms;若前端计算,单次耗时1500ms以上。结论:计算尽量后移。
6. 部署指南:从本地开发到上线演示,三步搞定
毕设答辩前最后一关,不是写代码,是让系统在老师电脑或学校服务器上稳稳跑起来。这套系统专为“零运维经验”设计,不碰Docker、不配Nginx,用最朴实的方式。
6.1 本地演示:U盘拷贝,双击运行
适用场景:答辩现场,用自己笔记本演示,老师要看实时数据。
步骤:
1. 确保项目目录完整:manage.py, settings.py, db.sqlite3, static/, templates/都在。
2. 打包Python环境:在虚拟环境里执行:
bash pip freeze > requirements.txt
3. 拷贝整个项目文件夹到U盘。
4. 在老师电脑上(Windows):
- 安装Python 3.8(官网下载,勾选Add to PATH);
- 打开CMD,cd \path\to\project;
- python -m venv venv → venv\Scripts\activate.bat → pip install -r requirements.txt;
- python manage.py runserver 0.0.0.0:8000;
- 浏览器访问http://localhost:8000。
优势:全程离线,不依赖网络;
db.sqlite3已含基础数据,无需重新加载;settings.py里DEBUG=True,错误信息友好。
6.2 学校服务器部署:Apache + mod_wsgi,经典稳扎稳打
适用场景:学校提供Linux服务器(CentOS/Ubuntu),需长期运行供多人访问。
步骤:
1. 服务器安装必要软件:
bash # Ubuntu sudo apt update sudo apt install apache2 libapache2-mod-wsgi-py3 python3-pip sudo pip3 install django==4.2.7 tushare==2.3.6 pandas==1.5.3
2. 上传项目到/var/www/stock-system/。
3. 创建/var/www/stock-system/wsgi.py(Django自动生成,无需改)。
4. 配置Apache虚拟主机(/etc/apache2/sites-available/stock.conf):
```apache
ServerAdmin webmaster@localhost
DocumentRoot /var/www/stock-system
WSGIDaemonProcess stock-system python-path=/var/www/stock-system python-home=/var/www/stock-system/venv
WSGIProcessGroup stock-system
WSGIScriptAlias / /var/www/stock-system/stock_system/wsgi.py
Alias /static /var/www/stock-system/static
<Directory /var/www/stock-system/static>
Require all granted
</Directory>
<Directory /var/www/stock-system/stock_system>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
ErrorLog ${APACHE_LOG_DIR}/stock-error.log
CustomLog ${APACHE_LOG_DIR}/stock-access.log combined
5. 启用站点:bash
sudo a2ensite stock.conf
sudo systemctl restart apache2
```
注意:
python-home指向虚拟环境路径;STATIC_ROOT在settings.py里设为/var/www/stock-system/static;执行python manage.py collectstatic生成静态文件。
6.3 Docker轻量部署:一键容器化,适合进阶展示
适用场景:你想在答辩时秀一下“现代化部署”,且服务器已装Docker。
步骤:
1. 项目根目录新建Dockerfile:
dockerfile FROM python:3.8-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . RUN python manage.py collectstatic --noinput EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "stock_system.wsgi:application"]
2. 新建docker-compose.yml:
yaml version: '3' services: web: build: . ports: - "8000:8000" environment: - DEBUG=False - SECRET_KEY=your-secret-key-here volumes: - ./db.sqlite3:/app/db.sqlite3
3. 执行:
bash docker-compose up -d
优势:环境完全隔离,
docker-compose down一键清理;gunicorn比runserver更健壮,支持多进程。
7. 写在最后:这代码不是终点,是你量化之路的第一块砖
我写这套系统时,没想着“教你怎么写MACD”,而是想给你一个能呼吸、能生长、能抗压的系统骨架。它不完美——Tushare免费版数据有延迟,SQLite不适合高并发,前端图表没做响应式——但它的每一行代码,都指向一个明确的目的:让你在答辩时,能指着models.py说“这张表设计是因为要支持按日期快速查询”,能指着rests.py说“这个缓存逻辑是为了应对Tushare的调用限制”,能指着config.py说“这里切换数据源,不用改业务代码”。
本科生毕设的价值,从来不在“功能多炫”,而在“设计是否经得起问”。当老师问“为什么用Django不用Flask?”,你答“因为Django的ORM帮我规避了N+1查询风险,这是金融数据高频查询的刚需”;当问“数据怎么保证不重复?”,你答“update_or_create在rests.py里统一处理,这是幂等性设计”;当问“如果Tushare挂了怎么办?”,你答“logger.py会记录ERROR,前端有降级提示,且response.py的统一结构让错误处理不散落各处”。
这套代码,是我带过十几届学生后,沉淀下来的“最小可行骨架”。它不教你数学,但给你一个跑数学公式的安全沙盒;它不替你思考策略,但让你的策略能立刻落地验证。你接下来要做的,不是把它当成品交差,而是把它当乐高——换一块“聚宽数据源”的砖,加一根“MACD指标”的梁,搭一层“回测报告”的楼。等你搭完,回头看,那套“开箱即用”的代码,早已成了你自己的东西。
最后分享个小技巧:每次加新功能,先写tests.py里的单元测试。比如加MA5计算,就写:
def test_ma5_calculation():
df = pd.DataFrame({'close': [10, 12, 11, 13, 14, 15]})
result = calculate_ma(df, window=5)
assert result.iloc[4]['ma_5'] == 12.0 # 前5天均值
跑python manage.py test backtest.tests,绿条一闪,你就知道这块砖,稳了。
简介:一套开箱即用的股票数据可视化解决方案,用Python Django搭建后端服务,通过Tushare实时获取A股基础行情、历史K线、成交量及均线数据。系统内置完整的Web应用结构:数据库模型定义(models.py)、业务逻辑处理(views.py)、路由配置(urls.py)、REST接口封装(rests.py),还包含日志记录(logger.py)、日期格式转换(formatdate.py)、权限控制装饰器(decorators.py)和统一响应封装(response.py)。前端提供简洁的index.html主页和404错误页,后端支持Django 3.x/4.x与Python 3.8+,附带settings.py和config.py配置文件、数据库初始化脚本及详细README.MD说明文档。从环境安装、API密钥配置、数据库迁移、服务启动到页面访问,每一步都有明确指引。适合本科生毕设、课程设计或量化入门实践,代码结构清晰、模块职责分明,便于添加MACD/RSI等技术指标、回测逻辑或接入其他金融数据源。
&spm=1001.2101.3001.5002&articleId=162889237&d=1&t=3&u=89cfba923f9c4663915d835a80be85e2)
950

被折叠的 条评论
为什么被折叠?



