简介:一份基于Django框架的股票市值管理系统源码,面向希望学习Python Web开发、了解证券账户与资产跟踪实战的开发者。系统围绕港股和美股的投资场景,涵盖交易记录、分红记录、新股信息等数据模型,通过模型-视图-模板-URL的完整链路实现用户认证、持仓展示、表单录入与Ajax异步刷新等功能,展示了从数据库设计到前端交互的典型Django项目结构。压缩包内有2030个文件,主要为Python源码、HTML模板、JavaScript脚本以及CSS/SCSS/SVG等前端资源,其中SVG图标与样式文件占比较高,可支撑后台管理界面和图表展示,整体包体仅8.95MB,便于下载与部署。已有217人学习浏览,适合具备初步Python基础、希望进阶Django全栈开发或构建金融类管理系统的开发者参考,可直接分析其模型定义、URL路由与视图函数组织方式,并复用其成熟目录结构。 前阵子整理自己的项目集合,翻出来一个之前用Django做的股票市值管理系统源码包,顺手在几个开源社区分享了一下,没想到私信问细节的人还挺多。索性把整个项目的来龙去脉、设计思路和落地过程完整写一篇。如果你正好也在用Django,或者正打算写一个和股票、基金、资产记账相关的Web系统,这篇应该能给你省不少弯路。
这个系统的定位很明确:个人投资者自用的市值管理工具。它不炒概念、不做量化选股、不搞自动交易,核心就干三件事——记录你持有什么、当前值多少钱、整体盈亏怎么样。听起来简单,但真正用Django从零把这几件事做扎实,涉及数据建模、行情接口接入、定时任务、前端展示和后期部署,每一步都有值得细说的地方。
1. 为什么一门心思选Django做这个管理系统
1.1 Django在项目管理类工具里的天然优势
先说结论:选Django不是因为它新潮,而是这类"单人多角色、重数据录入、重信息展示"的系统,Django几乎是Python生态里最省力的选择。
我用过Flask写小工具,也用过Spring Boot写企业服务,最后落到Django上,核心原因是它的ORM和Admin后台太适合这种业务了。市值管理系统最绕不开的就是表和表之间的关系——用户、股票、持仓、行情、交易记录,这些实体互相之间既有外键关联又有聚合计算。Django的ORM能让我用Python对象直接描述数据关系,不用反复写SQL,开发效率至少提升一倍。
还有个容易被忽略的点:Django自带的Admin后台,在项目初期就是一套完整的数据管理界面。我开发头两天没有任何前端代码,全靠Admin在往数据库里录真实的股票代码和价格数据来验证逻辑。这个优势在项目原型期特别明显,很多Flask项目到这个阶段就得自己拼一套后台,费时费力。
1.2 与其他方案的对比:为什么不是Flask或Spring Boot
我不是无脑Django吹,选型的时候确实做过横向对比。Flask轻量灵活,适合做一个API服务或者极小的工具页,但一旦涉及用户认证、Admin后台、ORM迁移、表单校验这些"Web应用标配",Flask需要自己集成一堆第三方库。Flask-SQLAlchemy、Flask-Login、Flask-Migrate,每个库都有自己的用法和理解成本,串起来反而没有Django那种"开箱即用"的顺畅感。
Spring Boot在Java生态里很强,但对我来说太重了。写一个个人用的市值管理工具,Java整个工具链的复杂度远超所需,而且Python在处理行情数据、调用数据源接口方面有天然的库生态优势。我想在项目里快速试一个数据可视化方案,Python这边的选择比Java灵活得多。
所以最终的结论很直接:个人项目、快速迭代、数据模型复杂、需要后台管理,Django就是最均衡的选择。
2. 核心数据模型怎么设计:持仓、行情和市值快照
2.1 用户、股票与持仓:三张基础表的建模
整个系统的数据模型,我踩过几次坑之后沉淀下来的核心是四张表:用户表、股票基本信息表、持仓表和市值快照表。用户表直接复用Django内置的auth.User,这个没什么好说的。股票基本信息表记录的是股票代码、名称、所属交易所、行业分类、上市日期这些静态信息,它们不是行情数据,不需要频繁变动,但几乎每项业务都会用到。
持仓表是整个系统的心脏,字段设计上必须考虑一个现实问题:同一只股票多次买入。很多人第一版设计会把持仓表和交易记录表合并,导致修改一笔历史交易就要重新计算全部持仓。我最终的设计是把买入记录作为明细表,通过聚合生成当前持仓视图。这样记录历史流水和查看当前持仓是两个独立需求,互不干扰。
核心字段大致是这些:
user:外键指向用户stock:外键指向股票基本信息表shares:持仓数量,用PositiveIntegerFieldcost_price:成本价,用DecimalField(max_digits=10, decimal_places=3),这个细节后面会详说bought_at:买入时间
2.2 市值快照表:每日期末数据的价值
市值快照表可能是一开始最容易被忽略的设计,但恰恰是这个系统最有价值的部分。它的作用是每天收盘后,把每个用户持有的每只股票的市值、当日收盘价、当日盈亏等数据存成一条记录。这样做的最大好处是历史可回溯——你想看上周三那天账户总市值是多少,不用重新拉一遍行情,直接查快照表就行。
还有一个隐藏价值是审计和报表:如果账户是多人共同使用,或者你需要展示月度收益曲线,快照表可以按日期聚合,统计每天的持仓总市值、当日盈亏、累计收益率,这些数据全部是带时间戳的历史真相,而不是临时计算的瞬时值。
如果不用快照表,每次算历史数据都要重新抓行情,这有两个问题:一是行情接口通常有频率限制,二是当时的价格未必能拿到精确的复权结果。对于个人工具来说,冗余存储换取可靠性和速度,是值得的。
2.3 自选股和分组:提升使用体验的小设计
除了核心持仓管理,我还加了自选股功能。自选股表和持仓表不同,它只是用户关注但未买入的股票,和持仓数量、成本价没有任何关系,本质上是一个"收藏夹"。自选股表关联用户和股票基本信息表,再加一个添加时间就够用了。不需要多复杂,但它让系统的使用场景从"记账"扩展到了"盯盘"。
在自选股之上,我做了分组功能:用户可以把自选股归入"长期关注""短线观察""基金重仓"等自定义分组。这个功能加得很轻,只在自选股表里加一个group_name字符串字段,没有单独建组表。为什么这么做?从我的经验看,个人用户的自选股数量很少超过100只,分组只是一个标签需求,不是正经的多对多关系,如果为这个建一堆关联表,反而让模型复杂化。简单字段解决80%的需求,剩下20%等真有规模了再优化。
3. 行情数据拉取与市值计算的实现细节
3.1 用akshare拉取行情:按需刷新和批量更新
系统本身不产生行情数据,必须从外部数据源拿。我调研了几个方案,最终选定akshare作为主数据源,因为它是开源免费的 Python 库,支持 A 股、港股、美股,接口覆盖实时行情、历史行情、财务数据等,对个人项目非常友好。
实时行情的获取逻辑我设计了两层:
页面按需刷新:用户打开市值总览页时,前端通过Ajax请求后端接口,后端调用akshare获取当前价格,返回给前端渲染。这个频率很低,而且只在用户主动操作时触发,对数据源压力很小。
定时批量更新:每天收盘后用Celery定时任务把所有持仓股票和自选股的当日行情拉取一遍,写入属于自己的缓存表。因为大批量请求行情接口很容易触发频率限制,所以批量更新放在盘后执行,错峰拉取,然后通过市值快照表记录结果。
import akshare as ak def fetch_realtime_price(stock_code: str): """ 获取单只股票的实时行情 """ df = ak.stock_bid_ask_em(symbol=stock_code) return float(df.loc["最新", "value"])一个很关键的细节:股票代码必须规范处理。A股代码是6位数字,但akshare接口里沪市和深市的代码会出现不同前缀(比如sh600000和sz000001)。我的处理方式是统一从股票基本信息表里读取代码和交易所字段,在调用接口时拼接前缀,不依赖用户输入格式。
3.2 市值计算的核心逻辑与后端接口
市值计算本身不复杂,但有一个地方非常容易出错:持仓成本、市价和浮盈的数据类型。这里必须用Decimal而不是Float。Float在计算机中是二进制浮点数,无法精确表示像0.1这样的十进制小数,累计多笔交易后误差会悄悄变大。我刚开始用Float存成本,后来发现持仓浮盈对不上账,排查了很久才定位到是这个精度问题。改用Decimal之后,金额计算再也没有出过差异。
后端视图的接口设计上,我遵循"一次请求返回完整页面数据"的思想,而不是拆成十几个小接口让前端拼装。这个接口大概长这样:
def get_portfolio_summary(user): """ 计算用户所有持仓的市值汇总 """ positions = Position.objects.filter(user=user).select_related("stock") total_market_value = Decimal("0.00") total_cost = Decimal("0.00") items = [] for pos in positions: current_price = get_latest_price(pos.stock.code) market_value = Decimal(str(current_price)) * pos.shares profit = market_value - (pos.cost_price * pos.shares) profit_ratio = profit / (pos.cost_price * pos.shares) total_market_value += market_value total_cost += pos.cost_price * pos.shares items.append({ "name": pos.stock.name, "code": pos.stock.code, "shares": pos.shares, "cost_price": pos.cost_price, "current_price": current_price, "market_value": market_value, "profit": profit, "profit_ratio": profit_ratio, }) return { "items": items, "total_market_value": total_market_value, "total_cost": total_cost, "total_profit": total_market_value - total_cost, "total_profit_ratio": (total_market_value - total_cost) / total_cost, }注意代码里的select_related("stock"),这个优化极其重要。如果不加,Django ORM会在循环里逐条查询股票表信息,持仓一多就会产生N+1次查询,页面响应时间会明显变慢。
3.3 每日收盘后任务:用Celery定时生成持仓报表
盘后数据更新我用了django-celery-beat。Celery的接入不算复杂,但有几个需要提前规划的细节。
第一是任务的幂等性。每天跑一次更新任务,如果某一天数据源临时不可用或者任务执行到一半失败,重跑时不能产生重复记录。我的处理方式是快照表对user + stock + date建立唯一约束,当天每只股票只有一条快照记录。
第二是时间窗口。我的任务设置在每天15:30执行,因为A股15:00收盘,预留30分钟给行情数据源更新当日数据。如果执行时间太早,可能拿不到准确的收盘价格。
@app.task def generate_daily_snapshot(): today = timezone.localdate() users = User.objects.all() for user in users: positions = Position.objects.filter(user=user) daily_total_value = Decimal("0.00") for pos in positions: df = ak.stock_zh_a_hist( symbol=pos.stock.code, period="daily", start_date=today.strftime("%Y%m%d"), end_date=today.strftime("%Y%m%d"), ) if df.empty: continue close_price = Decimal(str(df.iloc[-1]["收盘"])) MarketSnapshot.objects.update_or_create( user=user, stock=pos.stock, snapshot_date=today, defaults={ "close_price": close_price, "shares": pos.shares, "market_value": close_price * pos.shares, }, ) daily_total_value += close_price * pos.shares UserDailySummary.objects.update_or_create( user=user, snapshot_date=today, defaults={"total_market_value": daily_total_value}, )4. 前端页面和交互:从自选股列表到趋势图表
4.1 页面结构:模板继承与组件化思路
前端部分我并没有引入复杂的前后端分离架构,而是用Django模板系统加少量JavaScript。这种选择完全符合项目定位——数据管理工具,不是高并发C端产品。直接用模板渲染,后端把上下文数据传给前端,服务端渲染首屏,SEO无所谓,但胜在结构简单、部署方便、不需要额外搭建Node服务。
模板继承方面,我设计了一个基础模板base.html,里面包含侧边导航栏和页面主区域。子模板只需要继承并实现content块。这样在新增页面时不用重复写导航,整个项目的前端代码量控制得很小。
4.2 用ECharts做市值趋势图
市值趋势图是系统里最有"科技感"的部分,前端用了ECharts。它支持从后端传入JSON数据,非常方便。
数据来源就是上一节说的UserDailySummary表。我写了一个视图专门返回近30天的市值趋势数据:
def portfolio_trend(request): summaries = ( UserDailySummary.objects .filter(user=request.user) .order_by("-snapshot_date")[:30] ) trend_data = { "dates": [s.snapshot_date.strftime("%m-%d") for s in summaries][::-1], "values": [float(s.total_market_value) for s in summaries][::-1], } return JsonResponse(trend_data)前端用ECharts折线图展示:
fetch("/portfolio/trend/") .then(response => response.json()) .then(data => { const chart = echarts.init(document.getElementById("trendChart")); chart.setOption({ xAxis: { type: "category", data: data.dates }, yAxis: { type: "value" }, series: [{ type: "line", data: data.values, areaStyle: {} }] }); });这个图最需要注意的地方是数据倒序。数据库里最新日期排在最前,画图时要从旧到新显示,所以我在Python里取了[:30]后又做了[::-1]反转,前端拿到的才是时间正序。
4.3 Ajax刷新实时价格的前端细节
总览页面上的实时价格,如果每次打开都重新渲染整个页面,体验会比较生硬。我用了Ajax定时刷新,每10秒请求一次后端接口,只更新价格和市值这几个数字,不让整个页面闪烁。
实现上,每个股票代码对应的DOM元素加了一个style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />