Django 股票交易管理系统:事务与行级锁保障资金一致性

发布时间:2026/9/15 19:23:28

Django 股票交易管理系统:事务与行级锁保障资金一致性 简介这是一份基于Django框架的股票交易管理系统完整项目资源面向有一定Python基础、希望实战Web开发的开发者也适合计算机专业学生用于课程设计或毕业设计。压缩包共1611个文件约21.84MB其中包含31个Python源码文件、20个SQL数据库脚本、879个JavaScript脚本以及167个HTML页面、162个CSS样式表等前端资源图片与字体素材服务于界面渲染另有文档辅助阅读。整套资源完整覆盖了Django的MTV架构、ORM数据库操作、用户认证与权限管理、表单处理和AJAX异步更新等核心知识点能直观看到模型、视图、模板与路由之间的协作方式。项目自带可运行的页面与数据脚本能帮助快速搭建一个从用户注册登录到股票查询、交易记录展示的完整系统同时支持按需扩展适合作为课程设计、毕业设计或Python Web开发练手项目。当前已有187人学习浏览目录层次清晰便于对照源码逐模块理解一个真实项目的后端逻辑与前端呈现流程。1. 基于 Django 的股票交易管理系统先想清楚它要管什么一个基于 Django 的股票交易管理系统最容易做错的地方不是 K 线图也不是用户注册而是账目一致性。这种系统本质上是一个模拟盘闭环用户有账户、能下单、能成交、有持仓晚上还要算得出账任何一笔资金错位都会让整套演示变成“玩具”。它适合两类人一类是拿 Django 做毕设或课程项目想看看核心逻辑如何组织另一类是初级后端想搞明白事务、行级锁和模型设计如何落在一个真实业务里。下面按我从零搭这个系统时的顺序展开先把数据库立住再写交易逻辑最后处理行情展示和上线前验证。2. Django 项目脚手架与数据模型设计先把“账户-持仓-订单”三张表立住2.1 用 venv 和 django-admin 初始化一个带 MySQL 的项目常见做法是先建独立虚拟环境再创建 Django 项目和应用。股票交易系统虽然可以先用 SQLite 起步但部署到宝塔这类面板时多半会用 MySQL所以我在第一步就把数据库连上避免后边迁移出幺蛾子。mkdir stock_trading cd stock_trading python3 -m venv venv source venv/bin/activate pip install django mysqlclient pandas redis django-admin startproject config . python manage.py startapp tradingvenv隔离项目依赖避免和系统 Python 包冲突startproject config .把项目配置放在当前目录manage.py直接落在根目录startapp trading创建业务应用。执行完后修改配置连接 MySQL# config/settings.py DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: stock_trading, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } } INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, trading, ]mysqlclient在部分环境需要编译依赖宝塔里通常先安装 MySQL 扩展再pip install mysqlclient。charset必须用utf8mb4否则股票名称里的生僻字可能存不进去STRICT_TRANS_TABLES会让字段过长直接报错而不是静默截断交易系统里宁可报错也不能悄悄丢数据。2.2 股票与账户模型的三个关键约束交易系统最核心的几张表是股票、账户、持仓、订单。模型设计时先别急着写代码把金额字段和数量字段的类型定死比任何校验器都重要。# trading/models.py from django.db import models from django.contrib.auth.models import User from django.core.validators import MinValueValidator class Stock(models.Model): code models.CharField(股票代码, max_length10, uniqueTrue) name models.CharField(股票名称, max_length32) last_price models.DecimalField(最新价, max_digits12, decimal_places2, default0) updated_at models.DateTimeField(auto_nowTrue) def __str__(self): return f{self.code} {self.name} class Account(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameaccount) cash models.DecimalField(可用资金, max_digits16, decimal_places2, default1000000) base_cash models.DecimalField(初始资金, max_digits16, decimal_places2, default1000000) created_at models.DateTimeField(auto_now_addTrue) class Holding(models.Model): account models.ForeignKey(Account, on_deletemodels.CASCADE, related_nameholdings) stock models.ForeignKey(Stock, on_deletemodels.PROTECT) quantity models.IntegerField(持仓数量, default0, validators[MinValueValidator(0)]) available_quantity models.IntegerField(可卖数量, default0, validators[MinValueValidator(0)]) class Meta: unique_together (account, stock)三个关键约束我一般会盯死第一金额用DecimalField绝不用FloatField浮点数的 0.1 加 0.2 会让对账脚本永远跑不通第二available_quantity和quantity分开是为了以后做 T1 或冻结卖出时不用改表结构第三unique_together保证同一账户同一股票只能有一条持仓记录否则并发买入时会拆出多行后面查询就会乱。python manage.py makemigrations trading python manage.py migrate如果之后改了模型字段记住每次迁移都先makemigrations再migrate不要手改数据库表结构否则 Django 的迁移历史会和你实际表结构对不上。2.3 在 admin 后台把模型暴露出来并做基础美化管理员后台是这类管理系统最容易出彩的地方。先用 Django 自带的 admin 把模型注册进去再叠加一个主题美化已经很能打。pip install django-simpleui # config/settings.py 的 INSTALLED_APPS 中把 simpleui 放在 django.contrib.admin 之前# trading/admin.py from django.contrib import admin from .models import Stock, Account, Holding, Order admin.register(Stock) class StockAdmin(admin.ModelAdmin): list_display (code, name, last_price, updated_at) search_fields (code, name) list_editable (last_price,) admin.register(Account) class AccountAdmin(admin.ModelAdmin): list_display (user, cash, base_cash) readonly_fields (user,) admin.register(Holding) class HoldingAdmin(admin.ModelAdmin): list_display (account, stock, quantity, available_quantity) list_select_related (account, stock)search_fields直接给管理后台加搜索框list_editable可以让最新价在列表页直接改这对管理员手动修正行情非常方便。list_select_related能减少列表页对用户和股票的重复查询。如果你的项目走的是前后端分离路线admin 只给运营用用户端再用 Django REST Framework 或 Django Vue 提供接口不冲突。3. 订单撮合与资金结算用 Django 事务和行级锁保证账实相符3.1 订单表设计先定义订单状态和成交逻辑交易逻辑是整个系统的命门。我先定义一个订单模型它同时保留委托信息和成交信息方便以后查历史流水。# trading/models.py class Order(models.Model): STATUS ( (PENDING, 待成交), (SUCCESS, 已成交), (CANCELED, 已撤销), (FAILED, 失败), ) SIDE ( (BUY, 买入), (SELL, 卖出), ) account models.ForeignKey(Account, on_deletemodels.CASCADE, related_nameorders) stock models.ForeignKey(Stock, on_deletemodels.PROTECT) side models.CharField(方向, max_length8, choicesSIDE) price models.DecimalField(委托价格, max_digits12, decimal_places2) quantity models.IntegerField(委托数量) filled_price models.DecimalField(成交均价, max_digits12, decimal_places2, nullTrue, blankTrue) filled_quantity models.IntegerField(成交数量, default0) status models.CharField(状态, max_length16, choicesSTATUS, defaultPENDING) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue)订单状态流转尽量简单初始为PENDING模拟盘里如果按下单即成交就直接更新为SUCCESS用户主动撤单且订单尚未成交才能改成CANCELED。状态机的核心约束用表格说清楚状态可执行操作触发条件PENDING撤单用户点击撤销且未成交PENDING成交撮合成功资金/持仓结算SUCCESS不可撤单已成交只有流水记录CANCELED不可再次撤销撤单时已经冻结释放FAILED不可操作校验失败比如资金不足filled_price和filled_quantity保留实际成交价格与数量是为了以后做部分成交。模拟盘可以先只支持一次性全部成交但字段结构要留好。3.2 买入成交select_for_update 与原子事务的配合买入操作要在同一个数据库事务里完成三件事扣钱、加持仓、写订单。如果不加锁两个请求同时读到同一余额就会造成超买。from decimal import Decimal from django.db import transaction from django.db.models import F from django.core.exceptions import ValidationError transaction.atomic def create_buy_order(account_id, stock_id, price, quantity): account Account.objects.select_for_update().get(pkaccount_id) needed price * quantity if account.cash needed: raise ValidationError(可用资金不足) order Order.objects.create( account_idaccount_id, stock_idstock_id, sideBUY, priceprice, quantityquantity, filled_priceprice, filled_quantityquantity, statusSUCCESS, ) account.cash F(cash) - needed account.save(update_fields[cash]) account.refresh_from_db() holding Holding.objects.select_for_update().filter( account_idaccount_id, stock_idstock_id ).first() if holding is None: holding Holding.objects.create( account_idaccount_id, stock_idstock_id, quantity0, available_quantity0, ) holding.quantity F(quantity) quantity holding.available_quantity F(available_quantity) quantity holding.save(update_fields[quantity, available_quantity]) holding.refresh_from_db() return order这里有几个参数和动作必须说明。select_for_update()是在数据库层面锁住账户这一行必须放在transaction.atomic事务块里才生效否则锁会随非原子操作自动提交。F(cash) - needed是在数据库端做减法比先读后写在内存里减更抗并发。refresh_from_db()是因为F()表达式赋值后Python 内存里的account.cash还是旧值后续如果要记录日志或返回前端必须先刷新。买入时如果要支持挂单还需要在账户上增加“冻结资金”字段下单时先冻结成交后再扣可用资金。上面这个版本是“下单即成交”的演示逻辑已经足够说明事务和锁的配合方式。3.3 卖出成交先冻结持仓再释放避免负数持仓卖出和买入是对称操作但坑更多。最容易出现的情况是用户反复点击卖出把同一笔持仓卖两次。transaction.atomic def create_sell_order(account_id, stock_id, price, quantity): account Account.objects.select_for_update().get(pkaccount_id) try: holding Holding.objects.select_for_update().get( account_idaccount_id, stock_idstock_id ) except Holding.DoesNotExist: raise ValidationError(没有该股票持仓) if holding.available_quantity quantity: raise ValidationError(可卖数量不足) order Order.objects.create( account_idaccount_id, stock_idstock_id, sideSELL, priceprice, quantityquantity, filled_priceprice, filled_quantityquantity, statusSUCCESS, ) holding.available_quantity F(available_quantity) - quantity holding.quantity F(quantity) - quantity holding.save(update_fields[available_quantity, quantity]) holding.refresh_from_db() account.cash F(cash) price * quantity account.save(update_fields[cash]) account.refresh_from_db() return order对持仓行同样加select_for_update()锁住之后同一时刻只有第一个请求能先扣减数量第二个请求检查available_quantity时就会发现不足。注意卖出后持仓数量可能变成 0此时我倾向于保留这条Holding记录而不是删掉因为历史报表还要统计曾经持有过什么如果非要清理建议在日终任务里统一处理。3.4 用 reverse 和 resolve 写订单查询接口并验证路由后台交易逻辑完成后要给前端或管理端提供查询接口。常见的做法是 Django 项目里同时保留常规视图和接口路由方便前后端分离时联调。# trading/views.py from django.http import JsonResponse from django.urls import reverse from django.views.decorators.http import require_GET from .models import Order require_GET def order_list(request, account_id): orders Order.objects.filter(account_idaccount_id).select_related(stock)[:50] data [{ id: o.id, stock: o.stock.code, side: o.side, quantity: o.quantity, price: str(o.price), status: o.status, detail_url: request.build_absolute_uri(reverse(order-detail, args[o.id])), } for o in orders] return JsonResponse({orders: data})# trading/urls.py from django.urls import path from .views import order_list urlpatterns [ path(api/accounts/int:account_id/orders/, order_list, nameorder-list), ]参数说明account_id是路径参数Django 会自动做整数转换reverse(order-detail, args[o.id])根据路由名称反解析出 URL前端拿到 detail_url 就能继续查单笔明细。如果你要写单元测试校验路由可以用resolve(reverse(order-list, args[1]))测试里先反解析再正向解析能确认 URL 和视图函数没有脱节。4. 行情数据与 K 线展示缓存和异步更新别拖垮业务库4.1 用 pandas 把 CSV 行情一次性导入 MySQL行情数据是这个系统里数据量最大的部分。手动一条条插入不现实常见做法是准备好 CSV 文件用 Django 的管理命令批量导入。pip install pandas mkdir -p trading/management/commands touch trading/management/commands/__init__.py touch trading/management/commands/import_quotes.py# trading/management/commands/import_quotes.py import pandas as pd from django.core.management.base import BaseCommand from trading.models import Stock class Command(BaseCommand): help 从 CSV 导入股票基础信息 def add_arguments(self, parser): parser.add_argument(csv_file) def handle(self, *args, **options): df pd.read_csv(options[csv_file]) stocks [] for row in df.itertuples(): stocks.append(Stock( codestr(row.code), namerow.name, last_pricerow.last_price, )) Stock.objects.bulk_create(stocks, ignore_conflictsTrue) self.stdout.write(self.style.SUCCESS(fimported {len(stocks)} stocks))bulk_create一次插入多行比循环save()快很多ignore_conflictsTrue表示主键或唯一键冲突时跳过重复执行导入命令不会报错。注意 CSV 表头必须和你代码里的字段名一致如果是从交易所数据源拿到的文件通常还要先处理中文表头。4.2 用 django.core.cache 缓存高频查询的 K 线数据Django 自带的缓存框架足够应付 K 线查询场景。先配置 Redis 缓存再封装一个读取函数。pip install django-redis# config/settings.py CACHES { default: { BACKEND: django_redis.cache.RedisCache, LOCATION: redis://127.0.0.1:6379/1, OPTIONS: { CLIENT_CLASS: django_redis.client.DefaultClient, }, } }# trading/services.py from django.core.cache import cache from trading.models import Stock def kline_data(stock_code, days120): cache_key fkline:{stock_code}:{days} data cache.get(cache_key) if data is not None: return data stock Stock.objects.filter(codestock_code).first() if not stock: return [] quotes stock.quotes.order_by(-trade_date)[:days] data [{ date: str(q.trade_date), open: float(q.open_price), high: float(q.high_price), low: float(q.low_price), close: float(q.close_price), volume: int(q.volume), } for q in reversed(quotes)] cache.set(cache_key, data, timeout600) return data缓存键里带上股票代码和天数两个参数是最容易被忽略的细节。如果不带days用户切换 60 日和 120 日时会互相覆盖。缓存内容存 Python 的字典列表而不是 ORM 对象避免了请求结束后访问懒加载报错。timeout600表示 10 分钟过期行情更新频率不高时这个时间足够。参数用途建议timeout缓存过期时间行情 300-900 秒cache_key区分不同参数必须包含 code、daysdata 类型存储内容只用基础类型不用 ORM 对象4.3 用 StreamingHttpResponse 导出交易流水别等查询完才吐给用户管理员经常要导出一整年的订单流水。用普通HttpResponse时Django 会先拼完整张表格再发给浏览器数据量大时请求会长时间无响应。改成 Streaming 后先给响应头再一行一行输出。# trading/views.py import csv from django.http import StreamingHttpResponse from trading.models import Order class CsvEcho: def write(self, value): return value def export_orders(request): pseudo_buffer CsvEcho() writer csv.writer(pseudo_buffer) def rows(): yield writer.writerow([订单号, 账户, 股票, 方向, 数量, 价格, 状态]) orders Order.objects.select_related(account__user, stock).iterator(chunk_size2000) for o in orders: yield writer.writerow([ o.id, o.account.user.username, o.stock.code, o.side, o.quantity, o.price, o.status, ]) response StreamingHttpResponse(rows(), content_typetext/csv) response[Content-Disposition] attachment; filenameorders.csv return responseStreamingHttpResponse的content_type告诉浏览器这是 CSV 文件Content-Disposition里的attachment表示下载而不是直接打开filename指定下载后的文件名。iterator(chunk_size2000)让数据库查询按每 2000 条分块返回而不是把所有订单先加载到内存。小文件没必要用流式但股票交易系统跑上一年后订单量很容易破十万这个出口要提前留好。5. 上线前用管理命令做缓存预热和日终对账别等用户发现资产不对5.1 先跑通宝塔部署 Django 的常用配置如果你用宝塔面板部署常见做法是在站点目录里创建虚拟环境用 gunicorn 跑 Django 应用。命令大致如下cd /www/wwwroot/stock_trading python3 -m venv venv source venv/bin/activate pip install -r requirements.txt python manage.py collectstatic --noinput gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 2 --threads 4collectstatic把 admin 和 simpleui 的静态文件集中到STATIC_ROOT交给 Nginx 或者whitenoise服务--workers 2不要乱调高因为交易逻辑里用了select_for_update()worker 再多也是在同一把数据库锁上排队线程数可以稍微高一点处理 I/O。5.2 写一条 manage.py 命令做日终资产对账上线后第一件事不是看 K 线而是对账。写一条 Django 管理命令每天收盘后跑一遍扫出资产异常的账户。# trading/management/commands/daily_reconcile.py from decimal import Decimal from django.core.management.base import BaseCommand from trading.models import Account class Command(BaseCommand): help 日终对账校验现金 持仓市值是否小于初始资金 def handle(self, *args, **options): for account in Account.objects.select_related(user).iterator(): cash account.cash market_value Decimal(0) for holding in account.holdings.select_related(stock): market_value holding.quantity * holding.stock.last_price total cash market_value if total account.base_cash - Decimal(0.01): self.stderr.write( f{account.user.username} 资产异常: {total} ) else: self.stdout.write( f{account.user.username} 对账通过: {total} )对账脚本的核心不是算余额而是用“现金持仓市值”和“初始资金”做守恒校验。Decimal(0.01)是为了吸收计算时四舍五入带来的少量误差但如果你发现误差经常超过 0.01那一定不是浮点问题而是某个事务没有写成原子操作。缓存预热也可以合并到这条命令里命令执行时顺手读取当日热点股票的 K 线让 Redis 提前缓存好开盘后用户访问就不会打到 MySQL 上。把这个命令挂到宝塔的计划任务里每天收盘后执行系统就算真正有了“日终结算”的雏形。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/15 19:23:28

Remix 3、htmx 4与Rslib 1:前端构建与交互范式升级实战指南

1. 这不是一份“新闻简报”,而是一份前端工程师的实战备忘录你点开这期周刊标题——“栗子前端技术周刊第145期 - Remix 3 RC、htmx 4.0、Rslib 1.0…”——第一反应可能是:又一堆新版本号,又一堆要学的东西。但作为在一线写了八年全栈、带过…

2026/9/15 19:48:29

Loop macOS 窗口管理指南:4 个要点把杂乱桌面理顺

Loop macOS 窗口管理指南:4 个要点把杂乱桌面理顺 【免费下载链接】Loop Window management made elegant. 项目地址: https://gitcode.com/GitHub_Trending/lo/Loop 你的桌面大概是这样的:聊天、文档、浏览器互相叠在一起,拖来拖去排…

2026/9/15 19:48:29

如何用 ITCH 订单数据计算 Lee-Ready 聚合交易方向

如何用 ITCH 订单数据计算 Lee-Ready 聚合交易方向 【免费下载链接】machine-learning-for-trading Code for Machine Learning for Trading, 3rd edition — from data sourcing to live execution. 项目地址: https://gitcode.com/GitHub_Trending/ma/machine-learning-for…

2026/9/15 19:43:29

机器学习中线性代数的核心应用与优化技巧

1. 为什么机器学习离不开线性代数?第一次接触机器学习时,我完全没意识到线性代数的重要性。直到在实现第一个线性回归模型时,发现连最简单的梯度下降都写不出来,才意识到矩阵运算就像空气一样无处不在。举个实际例子:当…

2026/9/15 4:54:30

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/15 0:01:16

AI英语单词APP开发:自适应学习算法与移动端优化实践

1. 项目概述 作为一名在移动应用开发领域摸爬滚打多年的老手,我最近完成了一个AI英语单词APP的开发项目。这个项目将传统单词记忆方法与现代AI技术相结合,打造了一款能够智能适应不同用户学习习惯的英语学习工具。 市面上大多数单词APP都存在一个通病&a…

2026/9/15 0:01:16

Flutter与OpenHarmony结合开发手语学习APP实战

1. 项目背景与核心价值作为一名同时接触过Flutter和OpenHarmony的开发者,最近我完成了一个基于Flutter for OpenHarmony的手语学习APP实战项目。这个项目最大的特点在于实现了跨平台框架与国产操作系统深度结合的创新实践——用Flutter开发的应用能完美运行在OpenHa…

2026/9/15 0:01:16

六个月成为机器人工程师:从ROS2到SLAM的实战路径

1. 六个月的紧迫感从哪来:先搞清楚你要成为哪种机器人工程师说实话,六个月的期限并不是一个宽松的时间线。市面上任何一本正经的机器人学教材都超过五百页,ROS2的官方文档可以翻到你怀疑人生,再加上ABB、KUKA这些工业机器人厂家动…

2026/9/15 14:22:53

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/14 13:53:59

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/15 11:42:23

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码