基于Django和Tushare的A股行情可视化系统(含可运行代码、部署指南与完整文档)

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的股票数据可视化解决方案,用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里定义的StockBasicDailyTradeIndexData三张表,不是随便写的。StockBasic存股票基础信息(代码、名称、上市日期、行业),主键是ts_code(Tushare标准编码),带索引;DailyTrade存日线数据,复合索引(ts_code, trade_date)确保按股票查日期、按日期查全市场都快;IndexData专为大盘指数设计,字段精简但保留closechangepct_chg等关键指标。这些设计背后是真实行情查询场景的倒推:你要查“贵州茅台2023年所有交易日”,数据库得在毫秒级返回;你要查“2023-01-01全市场涨跌幅前10”,索引必须覆盖trade_datepct_chg。Flask没这层ORM约束,新手容易写出N+1查询,一查500只股票就卡死;Streamlit根本没数据库概念,数据全靠内存缓存,重启就丢。

  • 权限与安全边界清晰decorators.py里的login_required_apistaff_only不是摆设。前者强制REST接口校验登录态(用Django默认session机制),后者限制只有后台管理员才能调用数据重载接口(比如手动触发全量更新)。这对应两个现实场景:一是毕设演示时,你不想让评委随便点个按钮就把你的测试数据库刷成空;二是后续加策略回测模块,得防止未授权用户调用耗资源的计算接口。Flask要自己写装饰器+session验证,容易漏掉CSRF防护;Streamlit连登录页都没有,纯靠文件系统权限硬隔离,答辩时老师问“如果别人拿到你服务器IP,能直接访问数据接口吗?”,你就只能沉默。

  • 响应结构统一且可扩展response.py封装的SuccessResponseErrorResponsePaginatedResponse,统一返回{"code": 0, "msg": "success", "data": {...}}格式。这不是为了好看,是为后续留接口。比如你明天想加MACD指标计算,只要在rests.py里新增一个/api/stock/macd/接口,返回的数据结构自动套进SuccessResponse,前端不用改任何解析逻辑;再比如加分页查询,PaginatedResponse直接把page_sizecurrent_pagetotal_count塞进meta字段,Vue或React组件拿过来就能用。而裸写Flask视图,每个接口都要手动拼JSON,字段名大小写不一致、错误码不统一,后期维护就是噩梦。

  • 配置与环境解耦config.py单独抽离TUSHARE_TOKENDB_ENGINEDEBUG_MODEsettings.py通过from config import *导入。这意味着你本地开发用SQLite(DB_ENGINE='sqlite'),部署到学校服务器用MySQL(DB_ENGINE='mysql'),只需改一行配置,manage.py migrate自动适配不同数据库的SQL语法。我见过太多毕设项目,数据库连接字符串硬编码在views.py里,答辩前两天才发现MySQL密码错了,临时改代码到处找'localhost',最后手抖把'root'改成'roor',服务起不来,当场崩溃。

  • 日志可追溯、问题可定位logger.py配置了consolefile双输出,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/01str_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.userAnonymousUser,才意识到中间件没生效。

2.4 response.py:响应不是return字典,是“协议声明”

response.pySuccessResponse类,核心是__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
        )

为什么这么写?
- 自动序列化QuerySetDailyTrade.objects.filter(...).values()返回的是ValuesQuerySetjson.dumps无法直接序列化。SuccessResponse检测到hasattr(data, 'values'),自动调用list()转成Python列表,再JSON序列化。
- 兼容Model实例get_object_or_404(StockBasic, ts_code=code)返回单个Model对象,SuccessResponsemodel_to_dict()转成字典,避免TypeError: Object of type StockBasic is not JSON serializable
- default=str兜底:Django的DateTimeFieldDecimalField等特殊类型,json.dumps不认识,default=str将其转为字符串(如"2023-01-01""32.50"),保证不报错。

注意:SuccessResponseErrorResponse都继承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.pyDATABASES,填入你的MySQL地址、用户名、密码;
4. pip install mysqlclient(Windows可能需先装VC++编译工具);
5. 再执行makemigrationsmigrate

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失败;
- TushareTokenInvalidErrorconfig.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/接口要求YYYYMMDDfetch 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.jsstatic/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()函数已预留ma5ma10字段解析逻辑,你只需在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_SOURCEconfig.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_URLsettings.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报错TushareTokenInvalidErrorconfig.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.SZ2023010120231231,接口返回{"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.pyget_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.pyDEBUG = 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 venvvenv\Scripts\activate.batpip install -r requirements.txt
- python manage.py runserver 0.0.0.0:8000
- 浏览器访问http://localhost:8000

优势:全程离线,不依赖网络;db.sqlite3已含基础数据,无需重新加载;settings.pyDEBUG=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_ROOTsettings.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一键清理;gunicornrunserver更健壮,支持多进程。

7. 写在最后:这代码不是终点,是你量化之路的第一块砖

我写这套系统时,没想着“教你怎么写MACD”,而是想给你一个能呼吸、能生长、能抗压的系统骨架。它不完美——Tushare免费版数据有延迟,SQLite不适合高并发,前端图表没做响应式——但它的每一行代码,都指向一个明确的目的:让你在答辩时,能指着models.py说“这张表设计是因为要支持按日期快速查询”,能指着rests.py说“这个缓存逻辑是为了应对Tushare的调用限制”,能指着config.py说“这里切换数据源,不用改业务代码”。

本科生毕设的价值,从来不在“功能多炫”,而在“设计是否经得起问”。当老师问“为什么用Django不用Flask?”,你答“因为Django的ORM帮我规避了N+1查询风险,这是金融数据高频查询的刚需”;当问“数据怎么保证不重复?”,你答“update_or_createrests.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,绿条一闪,你就知道这块砖,稳了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套开箱即用的股票数据可视化解决方案,用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等技术指标、回测逻辑或接入其他金融数据源。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
内容概要:本文系统研究了在有限控制集约束下,三相并网逆变器中电流功率双模态模型预测控制(MPC)的等效机理及其性能边界。通过构建精确的预测模型,设计合理的代价函数,并结合Simulink仿真Matlab代码实现,深入分析了电流预测控制功率预测控制两种策略在动态响应速度、稳态精度、谐波抑制能力抗扰性等方面的差异内在联系。研究揭示了在特定系统参数运行条件下,两种控制模式之间的等效转化机制,并界定了各自的适用范围性能极限。同时,探讨了多模态控制的切换逻辑、实时性优化及预测模型不确定性对控制性能的影响,旨在提升逆变器在复杂电网环境下的综合控制品质鲁棒性。; 适合人群:具备电力电子、自动控制或新能源并网等相关专业背景,熟悉Matlab/Simulink仿真环境,从事研究生及以上层次科研或从事高端电力电子装备研发的工程技术人员。; 使用场景及目标:①深入理解模型预测控制在并网逆变器中的具体实现方法理论基础;②掌握电流功率双模态MPC控制器的设计、仿真建模性能对比评估流程;③为高动态、高精度并网控制系统的方案选型、参数优化工程化应用提供坚实的理论依据技术参考。; 阅读建议:建议结合所提供的Simulink仿真模型Matlab源代码进行同步实验验证,重点关注预测模型的建立过程、控制律的数学推导以及不同工况下的仿真结果对比分析,宜配合现代控制理论、电力电子变换技术及并网标准等相关资料进行系统性学习。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值