简介:基于Django框架实现的股票交易管理系统源码包,完整演示了如何运用Python Web开发中的模型定义、模板渲染、视图控制、用户认证、表单处理以及异步请求等技术,构建股票行情展示、交易下单、持仓管理和历史记录查询等核心业务功能。项目整体结构清晰,适合计算机专业学生选取为课程设计或毕业设计参考,也适合Django入门到进阶的开发者通过完整实战理解Web应用从数据层到表现层的协作方式。压缩包内共有1611个文件,整体大小约21.84MB,其中包含31个Python后端源码文件、167个HTML页面模板、162个CSS样式表、879个JavaScript脚本以及20个SQL数据库脚本,还附带大量图片、字体、说明文档等资源,属于前后端功能兼备的完整工程;前端样式覆盖了Bootstrap、AdminLTE等常见框架,数据库脚本便于快速初始化和还原演示数据。目前已有187人学习下载。仔细阅读源码,可以掌握Django项目中的路由配置、ORM数据操作、用户登录会话管理、表单验证与异步交互技巧,同时也能了解如何组织一个中型Web项目的文件结构,为独立开发同类管理系统提供直接参考。
1. 一个Django股票交易管理项目,值得拆开看的部分在哪里
股票交易系统这类业务,最考验 Web 框架的不是下单速度,而是能不能把行情、账户、订单、风控拆成清晰的对象模型。Django 的优势恰好在这里:ORM 可以直接把交易关系落库,内置 User 认证解决了“谁在下单”的问题,模板系统再配合 AdminLTE 这类后台皮肤,能在很短的时间内搭出一个可演示的股票交易管理台。这个项目里既有 Django 的 MTV 三层结构,也有外部行情数据通过批处理脚本进入数据库的完整链路:dailytosql.bat 负责把日行情清洗成 SQL,dbtosql.bat 负责把 SQL 导入 MySQL,前端再通过 AJAX 定时拉取最新价格。项目虽以课程设计常见,但拆开之后能看到 Django 在真实业务场景里的完整姿势,适合计算机专业学生作为毕业设计或课程设计,也适合想看看 Django 怎么扛业务逻辑的开发者。
2. Django 模型层:股票、订单与行情实体是怎么设计的
2.1 先分清哪些是业务实体,哪些是辅助实体
Django 项目拿到手,第一个动作不是写视图,而是跑python manage.py startapp trading,先把应用建出来,再把它注册进settings.py的INSTALLED_APPS。这一步对应热词里常说的“django 创建 app”,指令很简单:
python manage.py startapp trading创建出来的trading/models.py是后续所有业务的根。股票交易系统里真正需要落库的实体,我一般分成四类:股票基本信息、用户账户、交易订单、行情快照。前三个直接参与业务交互,第四个是外部脚本灌入的数据表。
需要特别注意的是,用户账户不要自己另建表,优先复用 Django 自带的auth.User,通过ForeignKey关联到订单表。这样注册、登录、会话、权限都有现成实现,后面的视图层可以直接用request.user拿到当前用户,这个设计比自建用户表省掉一大半安全问题。
2.2 模型字段怎么选:Decimal 与 Float 的区别必须明确
下面是一份可运行的models.py参考,覆盖股票、订单、行情快照三张表:
from django.db import models from django.conf import settings class Stock(models.Model): code = models.CharField(max_length=10, unique=True, verbose_name="股票代码") name = models.CharField(max_length=32, verbose_name="股票名称") industry = models.CharField(max_length=32, blank=True, verbose_name="所属行业") close_price = models.DecimalField(max_digits=10, decimal_places=2, default=0, verbose_name="最新价") updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") def __str__(self): return f"{self.code} {self.name}" class TradeOrder(models.Model): SIDE_CHOICES = (("BUY", "买入"), ("SELL", "卖出")) STATUS_CHOICES = (("PENDING", "待成交"), ("DONE", "已成交"), ("CANCEL", "已撤单")) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name="下单用户") stock = models.ForeignKey(Stock, on_delete=models.PROTECT, verbose_name="股票") side = models.CharField(max_length=4, choices=SIDE_CHOICES, verbose_name="方向") shares = models.PositiveIntegerField(verbose_name="数量") price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="委托价") status = models.CharField(max_length=6, choices=STATUS_CHOICES, default="PENDING", verbose_name="状态") created_at = models.DateTimeField(auto_now_add=True, verbose_name="下单时间") class StockDailyPrice(models.Model): stock = models.ForeignKey(Stock, on_delete=models.CASCADE, verbose_name="股票") trade_date = models.DateField(verbose_name="交易日期") open_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="开盘价") high_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最高价") low_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="最低价") close_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="收盘价") volume = models.BigIntegerField(default=0, verbose_name="成交量") class Meta: unique_together = ("stock", "trade_date")价格字段用DecimalField而不是FloatField,这是我强调的第一条规范。股票价格是金额,浮点数在二进制存储里会有精度误差,例如 19.99 可能存成 19.989999,订单金额一旦算错,整个系统就失去可信度。DecimalField在 Python 侧拿到的是Decimal对象,写入 MySQL 后会落成DECIMAL类型,四舍五入规则由数据库统一控制。
on_delete参数也是容易踩坑的位置。TradeOrder指向Stock时用了PROTECT,意思是某只股票如果已经存在关联订单,就不允许直接删除股票记录,避免行情数据因为外键断裂而失真。StockDailyPrice指向Stock用CASCADE,删除股票时历史行情一起清掉,适合做测试数据重置。
字段定义完之后,执行迁移命令:
python manage.py makemigrations trading python manage.py migratemakemigrations会比对模型与数据库差异,生成迁移文件;migrate把迁移应用到数据库。这个过程也是排查模型错误的最好时机,很多字段长度或外键关联问题都会在迁移阶段暴露。若迁移文件已经生成但后来又改了模型,不能用删文件的方式解决,应该继续makemigrations生成新的迁移,保持迁移链路完整。
2.3 ORM 查询、删除对象与常见误用
ORM 是 Django 对数据库操作的一层封装,使用得当可以完全避开手写 SQL,下面的表整理了高频操作与 SQL 的对应关系:
| 操作 | ORM 写法 | 对应 SQL 语义 |
|---|---|---|
| 查询全部 | Stock.objects.all() | SELECT * FROM trading_stock; |
| 条件过滤 | Stock.objects.filter(industry="科技") | WHERE industry='科技' |
| 取单条 | Stock.objects.get(code="000001") | WHERE code='000001' LIMIT 1; |
| 创建记录 | Stock.objects.create(...) | INSERT INTO ... |
| 删除对象 | Stock.objects.filter(code="000001").delete() | DELETE FROM trading_stock WHERE code='000001'; |
| 关联查询 | TradeOrder.objects.select_related("stock") | LEFT JOIN trading_stock ... |
热词里提到的“django 执行查询-删除对象”,在股票系统里最常见的场景是清理测试数据或撤单。下面的写法演示了如何先查后删,并正确处理级联关系:
from trading.models import Stock, TradeOrder # 查询所有待成交订单,select_related 避免 N+1 查询 pending_orders = TradeOrder.objects.filter(status="PENDING").select_related("stock") for order in pending_orders: print(order.user.username, order.stock.code, order.side, order.shares) # 删除某只退市股票,若有 PROTECT 外键关联会抛出 ProtectedError Stock.objects.filter(code="000001").delete()select_related的作用是让 Django 在一次查询里通过JOIN把关联表数据一起取出来,而不是每访问一条订单就再去查一次股票表。不使用它时,循环 100 条订单会产生 101 条 SQL,页面响应会肉眼可见地变慢。删除操作也要注意外键约束:如果TradeOrder使用了PROTECT,那么直接删除股票会抛出ProtectedError,失败时先确认是否有未处理的关联订单。
3. 视图、URL 路由与登录态:交易行为如何被串联起来
3.1 URL 路由设计与 reverse 解析
模型只是数据底座,真正让交易系统跑起来的是 URL 到视图函数的映射。Django 的路由系统在trading/urls.py中配置,我习惯先给应用定义一个app_name,方便在视图和模板里用reverse反向解析 URL。
下面是典型的 URL 配置:
from django.urls import path from . import views app_name = "trading" urlpatterns = [ path("stocks/", views.stock_list, name="stock_list"), path("stocks/<int:stock_id>/", views.stock_detail, name="stock_detail"), path("stocks/<int:stock_id>/price/", views.price_feed, name="price_feed"), path("orders/new/", views.create_order, name="create_order"), path("orders/", views.my_orders, name="my_orders"), ]<int:stock_id>是路径转换器,限定该参数必须是整数,这样请求/stocks/abc/时 Django 直接返回 404,不会进入视图层。name参数是 URL 的别名,业务代码里不要硬编码字符串路径,而是用reverse("trading:stock_detail", args=[stock.id])来解析。
关于热词里反复出现的“django reverse resolve”,reverse负责把视图名和参数解析成完整 URL,resolve是它的反向操作,用来根据 URL 解析出视图函数。在视图之间做重定向时,reverse比硬编码路径更安全:如果某天stock_detail的路径从/stocks/<int:stock_id>/改成/market/stock/<int:stock_id>/,只要路由 name 不变,业务代码里的 reverse 调用完全不用改。
3.2 用户认证与交易下单视图
交易系统必须知道“谁在下单”,这一层直接用 Django 内置认证体系最稳妥。在项目级urls.py中挂上认证路由:
from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("auth/", include("django.contrib.auth.urls")), path("", include("trading.urls")), ]django.contrib.auth.urls会一次性提供 login、logout、password_change 等标准视图,对应模板放到templates/registration/login.html即可。视图层需要登录才能访问的接口,用@login_required装饰器统一拦截。
下面是一个订单创建的完整视图示例:
from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from django.urls import reverse from .models import Stock, TradeOrder from .forms import OrderForm @login_required def create_order(request): if request.method == "POST": form = OrderForm(request.POST) if form.is_valid(): order = form.save(commit=False) order.user = request.user order.save() messages.success(request, "订单创建成功,等待成交") return redirect(reverse("trading:my_orders")) else: form = OrderForm() return render(request, "trading/order_form.html", {"form": form})login_required未登录时会自动跳转到登录页,并且会在 URL 上带?next=/orders/new/,用户登录成功后会回跳到原始页面,这个机制比在每个视图里手写request.user.is_authenticated判断要统一得多。
form.save(commit=False)是表单处理里的关键点:表单从请求里拿到股票、方向、数量、价格,但user字段没有暴露在表单中,先在内存中构建TradeOrder实例,再手动把当前登录用户赋值给order.user,最后真正落库。这样避免用户通过伪造请求提交user_id来冒充他人下单。
热词里提到的“django 重定向传递数据”,推荐方案是使用消息框架而不是在 URL 拼接参数。上面的messages.success(request, "订单创建成功")会把消息写入会话,在base.html模板中遍历输出:
{% if messages %} <ul class="list-unstyled"> {% for message in messages %} <li class="alert alert-info">{{ message }}</li> {% endfor %} </ul> {% endif %}URL 传参方式一旦刷新页面参数就会丢失,而消息框架读取后即清除,既保证了用户体验,也避免敏感信息暴露在地址栏里。
3.3 查询当前用户订单与权限控制
用户下单之后,需要查看自己的历史订单。这个视图要特别注意过滤条件,绝对不能把全表订单返回给普通用户:
from django.contrib.auth.decorators import login_required from .models import TradeOrder @login_required def my_orders(request): orders = TradeOrder.objects.filter(user=request.user).order_by("-created_at") return render(request, "trading/order_list.html", {"orders": orders}) @login_required def cancel_order(request, order_id): order = get_object_or_404(TradeOrder, pk=order_id, user=request.user) if order.status == "PENDING": TradeOrder.objects.filter(pk=order.id).update(status="CANCEL") order.refresh_from_db() return redirect("trading:my_orders")get_object_or_404接受两个条件参数pk=order_id和user=request.user,这样当前用户试图撤销别人的订单时直接得到 404,而不是看到订单详情。用update()更新状态也是一种更高效的做法,它直接生成一条 UPDATE SQL,不经过对象 save 流程,并且会跳过模型的save()方法,适合只改个别字段的场景。
4. 模板、AdminLTE 与 AJAX:把后台界面变成实时行情面板
4.1 用 AdminLTE 搭出可复用的基础模板
项目资源里出现AdminLTE.min.css、bootstrap.min.css和frozenui.css,说明前端基于 AdminLTE 后台皮肤搭建。AdminLTE 是建立在 Bootstrap 之上的后台管理模板,自带侧边栏、导航栏、卡片组件,最适合股票这类信息密度高的管理界面。
使用方式不是把静态文件散落到各个模板中,而是建一个templates/base.html作为全局骨架。先处理静态文件目录,在settings.py中配置:
# settings.py STATIC_URL = "/static/" STATICFILES_DIRS = [BASE_DIR / "static"]然后在base.html中引入 CSS:
{% load static %} <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <title>{% block title %}股票交易管理系统{% endblock %}</title> <link rel="stylesheet" href="{% static 'adminlte/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="{% static 'adminlte/css/AdminLTE.min.css' %}"> <link rel="stylesheet" href="{% static 'adminlte/css/skins/_all-skins.min.css' %}"> {% block extra_css %}{% endblock %} </head> <body class="hold-transition skin-blue sidebar-mini"> <div class="wrapper"> <!-- 顶部与侧边栏略 --> <div class="content-wrapper"> {% block content %}{% endblock %} </div> </div> <script src="{% static 'adminlte/js/jquery.min.js' %}"></script> {% block extra_js %}{% endblock %} </body> </html>{% static %}标签是 Django 模板系统解析静态文件路径的标准方式,不能直接写死/static/adminlte/css/AdminLTE.min.css。设置了STATICFILES_DIRS后,Django 可以在开发服务器上直接服务这些文件;部署到生产环境时,再由 Nginx 或 CDN 接管,模板代码无需改动。
4.2 AJAX 轮询接口:JSON 视图如何写
股票行情页面需要定时刷新最新价,但不能整页刷新,这里用 AJAX 轮询是最直观的方案。服务端先提供一个返回 JSON 的视图:
import json from django.http import JsonResponse from django.shortcuts import get_object_or_404 from .models import Stock def price_feed(request, stock_id): stock = get_object_or_404(Stock, pk=stock_id) data = { "code": stock.code, "name": stock.name, "price": str(stock.close_price), "timestamp": stock.updated_at.strftime("%Y-%m-%d %H:%M:%S"), } return JsonResponse(data)这里有两个细节值得关注。第一,close_price是Decimal类型,直接放进json.dumps会报错,所以先用str()转成字符串,再由前端parseFloat解析;第二,JsonResponse会自动设置Content-Type: application/json,避免中文乱码问题,同时还要注意settings.py里的DEFAULT_CHARSET保持为utf-8。
前端页面通过 jQuery 定时拉取并更新 DOM:
function refreshPrice(stockId) { $.get('/trading/stocks/' + stockId + '/price/', function (resp) { $('#stock-price').text(resp.price); $('#stock-time').text(resp.timestamp); }); } $(function () { refreshPrice(123); // 123 换成实际股票 id setInterval(function () { refreshPrice(123); }, 5000); });轮询间隔设置成 5 秒对课程设计足够,但如果要面向真实行情,5 秒一次对数据库压力不小。常见做法是后端用 Redis 缓存最近一次行情快照,AJAX 请求先读缓存,或者干脆换成 WebSocket 推送。这里的 AJAX 只是展示了 Django 视图如何同时输出 HTML 和 JSON,为后续接入 celery 定时任务或异步推送留好接口。
4.3 Django Admin 界面美化与订单审核
Django Admin 是项目自带的弱后台,虽然默认样式朴素,但通过自定义ModelAdmin可以满足大部分后台管理需求。热词里提到“django admin 界面美化”,除了改静态 CSS,更核心的是把admin.py的可读性做出来:
from django.contrib import admin from .models import Stock, TradeOrder @admin.register(Stock) class StockAdmin(admin.ModelAdmin): list_display = ("code", "name", "industry", "close_price", "updated_at") search_fields = ("code", "name") list_filter = ("industry",) @admin.register(TradeOrder) class TradeOrderAdmin(admin.ModelAdmin): list_display = ("id", "user", "stock", "side", "shares", "price", "status", "created_at") list_filter = ("status", "side") actions = ["mark_done"] @admin.action(description="标记为已成交") def mark_done(self, request, queryset): queryset.update(status="DONE")list_display控制后台列表展示哪些字段;search_fields让股票代码和名称支持搜索;list_filter在右侧生成按状态和方向的筛选栏。actions自定义批量操作,可以一秒把选中订单全部标记为已成交,比逐个编辑效率更高。后台地址仍然用/admin/访问,只是在原来的默认样式基础上拥有了更贴合交易业务的管理能力。
5. dailytosql.bat 与 dbtosql.bat:行情数据入库与 MySQL 配置
5.1 两个批处理脚本的工作流程拆解
项目资源里有两个批处理文件:dailytosql.bat和dbtosql.bat。从命名上能看出它们的配合方式:前一个负责把当日行情源文件转换成 SQL 插入语句,后一个负责真正把 SQL 执行到数据库。这种“先出文件,再入库”的阶段划分,在数据量不大时非常实用,因为你可以先打开生成的 SQL 检查数据是否正常,再决定是否入库,避免错误数据直接污染正式表。
dailytosql.bat的常见骨架如下:
@echo off chcp 65001 >nul set INPUT_FILE=day_data.txt set OUTPUT_FILE=daily.sql python gen_sql.py %INPUT_FILE% %OUTPUT_FILE% if exist %OUTPUT_FILE% ( echo 日行情 SQL 已生成:%OUTPUT_FILE% ) else ( echo 生成失败,请检查输入文件格式 )chcp 65001是为了让批处理窗口以 UTF-8 编码运行,防止源文件中中文股票名称在生成 SQL 时变成乱码。gen_sql.py是真正干活的脚本,它读取当日行情文本,逐行转换成 INSERT 语句。下面是一段简化版的生成逻辑:
import sys INPUT_FILE = sys.argv[1] OUTPUT_FILE = sys.argv[2] lines = [] with open(INPUT_FILE, "r", encoding="utf-8") as f: for line in f: code, name, close_price = line.strip().split("|") sql = f"INSERT INTO trading_stockdailyprice (stock_id, trade_date, close_price) VALUES ((SELECT id FROM trading_stock WHERE code='{code}'), CURDATE(), {close_price});" lines.append(sql) with open(OUTPUT_FILE, "w", encoding="utf-8") as f: f.write("\n".join(lines))这段代码的要点在于,行情表外键stock_id不是直接写死数值,而是用子查询从股票主表里找出对应 id,这样即使股票主表的自增 id 发生过变化,导入时也能准确关联。trade_date使用了 MySQL 的CURDATE(),如果脚本每天固定执行一次,这个写法可以自动填充当前交易日。
5.2 dbtosql.bat 入库与数据校验
dbtosql.bat负责把上一步生成的daily.sql导入 MySQL:
@echo off chcp 65001 >nul set DB_USER=root set DB_PASS=123456 set DB_NAME=stock_db mysql -u%DB_USER% -p%DB_PASS% %DB_NAME% < daily.sql if %errorlevel% == 0 ( echo 行情数据入库成功 ) else ( echo 入库失败,请检查账号权限或 SQL 语法 )mysql命令通过<重定向把daily.sql的内容交给 mysql 客户端执行,errorlevel是 Windows 批处理里判断上一条命令是否成功的标准方式。入库建议分环境执行:先在测试库跑一遍,确认行数、最新日期、价格精度无误后,再导入生产库。
导入完成后可以在 Django 的 shell 里快速校验:
python manage.py shell -c "from trading.models import StockDailyPrice; print(StockDailyPrice.objects.count()); print(StockDailyPrice.objects.order_by('-trade_date')[:3])"这个命令会输出行情总记录数和最近三天的数据对象,如果数量为 0,基本可以判断是dailytosql.bat生成的 SQL 文件为空,或者源文件路径写错。如果数量有但最新日期不是当天,重点检查CURDATE()是否被表单里trade_date字段覆盖。
5.3 MySQL 连接配置与常见坑
Django 要连接 MySQL,数据库配置在settings.py中改写:
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "stock_db", "USER": "root", "PASSWORD": "123456", "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", }, } }ENGINE指定 MySQL 驱动,Django 官方推荐的驱动是mysqlclient,安装命令通常为pip install mysqlclient。安装时如果报缺少编译环境,Windows 上可以直接安装对应的 whl 包,Linux 上需要先安装libmysqlclient-dev。OPTIONS里的charset设为utf8mb4是为了兼容股票名称里的生僻字和 emoji 符号,只设utf8在写入四字节字符时会报Incorrect string value错误。
两个批处理脚本里最容易忽略的问题是路径依赖。双击执行时,%~dp0指向脚本所在目录,更稳妥的方式是在脚本第一行加上cd /d %~dp0,确保在当前目录下寻找源文件和生成文件。MySQL 账号密码硬编码在 bat 里只是个临时方案,更严谨的做法是把连接参数放进config.ini或环境变量,避免脚本提交到仓库时泄露数据库口令。整个行情自动入库链路做到这里,日线数据就能每天定时进入 Django 的 ORM 管理范围,后续基于历史行情做均线计算、盈亏统计或图形展示,都只需要在这套模型上继续扩展。
本文还有配套的精品资源,点击获取