
简介基于Python的股票自动交易系统毕业设计源码包源自个人毕设项目答辩评审分高达95分。项目覆盖股票自动交易核心流程面向计算机、通信、人工智能、自动化等相关专业学生及从业者可作为期末课程设计、课程大作业或毕业设计参考也适合小白从零学习项目架构与代码实现。压缩包共686个文件、14.77MB其中包含131个Java源码、86个JS、67个HTML、48个CSS等前后端代码文件以及大量JPG、PNG截图和数据库文件便于查看界面效果、理解数据存储并撰写文档。目前已有336人学习下载。代码经过调试测试确保可直接运行整体结构完整、目录清晰便于定位交易逻辑、数据存储与前端展示模块基础扎实的读者可借此二次开发扩展策略或优化交互具备较高的学习与借鉴价值。1. 毕设做股票自动交易系统先想清楚这三件事毕业设计选择了“基于Python的股票自动交易系统的设计与实现源码.zip”很多同学的第一反应是是不是要对接实盘、自动买卖、躺着赚钱实际上一个能顺利答辩的股票自动交易系统重点不在“自动下单”本身而在“把交易逻辑工程化”。你需要用 Python 完成行情获取、策略计算、回测评估、模拟交易和风险控制这一整条链路。这个题目非常适合已经有 Python 基础、但还没接触过量化交易和系统设计的同学它能把股票数据分析、数据爬虫、文件持久化、面向对象设计、自动化脚本这些技能串成一个大而完整的项目。本文会按数据层、策略层、交易层、避坑点和你需要的落地路径讲清楚从零搭完一套可演示的毕设系统。2. 数据层与系统骨架用 Python 把行情数据管起来2.1 选型理由为什么自动交易系统适合用 Python 来做自动交易系统听起来高大上但拆开以后就是四个环节拿数据、算信号、做回测、发订单。这四个环节里数据获取和策略验证占了 70% 的工作量而这恰好是 Python 生态最舒服的地方。用 Java 或 C 当然也能做但你需要自己处理 HTTP 请求、CSV 解析、数据结构定义、图表可视化这些脏活。Python 的 pandas 可以直接做 Dataframe 运算akshare、tushare 这类库把沪深行情接口封装成了几行代码matplotlib 可以快速画出资金曲线和净值图。对毕设来说你更需要的不是极致性能而是“一天能改三版策略”的开发效率。要说明的是Python 并不适合高频交易场景它的解释器开销和 GIL 锁在毫秒级竞争中很吃亏。但毕设命题通常针对日线或分钟线级别的自动交易策略计算周期按小时甚至按天来算Python 完全没有性能压力。如果你准备用 A 股日线数据、模拟盘交易来交差选 Python 是性价比最高的决定。2.2 拉取沪深日线行情akshare 和 tushare 的最小用法数据层是整个系统最底层的地基。我一般建议用 akshare 起步因为不需要注册 token直接 pip 安装就能跑tushare 需要注册获取 token但数据稳定性更好。下面的代码用 akshare 拉取平安银行最近一年的日线行情。# -*- coding: utf-8 -*- # 获取沪深A股日线行情的示例脚本 import akshare as ak import pandas as pd from datetime import datetime, timedelta # 以平安银行(000001)为例拉取最近一年的日K线 end_date datetime.now().strftime(%Y%m%d) start_date (datetime.now() - timedelta(days365)).strftime(%Y%m%d) df ak.stock_zh_a_hist( symbol000001, # 股票代码注意不带交易所后缀 perioddaily, # 周期daily / weekly / monthly start_datestart_date, # 开始日期格式YYYYMMDD end_dateend_date, # 结束日期 adjustqfq, # qfq前复权hfq后复权不复权 ) # akshare返回的是中文列名改成后续方便处理的英文名 df.rename( columns{日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume}, inplaceTrue, ) # 只保留回测需要的字段顺便把日期转成时间类型 df df[[date, open, high, low, close, volume]] df[date] pd.to_datetime(df[date]) print(df.tail())这段代码的逻辑是先通过 akshare 拿到原始行情再做列名清洗和字段筛选。perioddaily的意思是拉日线如果你后面要扩展分钟级别可以改成1、5、15、30这种分钟周期值。adjustqfq非常关键它拉回来的是前复权数据除权除息日前后价格是连续的否则你会在分红日看到一个莫名其妙的向下跳空缺口直接影响均线计算和回测盈亏。初期踩坑点在于akshare 的列名是中文而且版本更新偶尔会改列名。建议你把数据清洗单独封装成一个函数后面换 tushare 时只需要改这一个函数不用在策略代码里到处改。2.3 数据存储与复权字段设计少走弯路的本地库毕设项目不需要上数据库用 CSV 或者 SQLite 就够。但存储结构要提前想清楚否则策略每次运行都要重新拉数据。# 保存为CSV同时写一个meta文件记录数据范围 import os os.makedirs(data/daily, exist_okTrue) df.to_csv(data/daily/000001_daily_qfq.csv, indexFalse) with open(data/daily/000001_daily_qfq_meta.txt, w, encodingutf-8) as f: f.write(fsymbol000001\n) f.write(fstart{df[date].min().date()}\n) f.write(fend{df[date].max().date()}\n) f.write(fadjustqfq\n)这里用文件名把股票代码、周期、复权方式都记录下来meta 文件记录数据范围。这样做的好处是当你同时测试多只股票时文件不会互相覆盖。字段设计上date、open、high、low、close、volume 是底线。但你要特别注意复权方式的一致性同一个数据目录下不要一部分是前复权、一部分是不复权。前复权数据受最新除权影响每次拉取都可能改变历史价格如果策略代码里混合使用回测结果会变得不可复现。你可以把复权方式写死在文件名和 meta 文件里甚至可以在 README 里说明“当前系统统一使用前复权日线数据”这样答辩时老师问起来也能说得清楚。3. 策略编写与回测把交易想法变成可验证的代码3.1 策略信号生成从指标到买卖点的抽象过程数据到位后接下来就是把交易思想变成代码。这里以最常见、最容易讲的“双均线策略”为例短周期均线上穿长周期均线时买入下穿时卖出。它虽然简单但足够展示完整的信号生成和回测逻辑。# strategy/double_ma.py def generate_signal(df, short5, long20): 输入含有 date 和 close 列的数据输出带 ma_short / ma_long / target_position 的副本。 target_position: 1 表示目标持仓0 表示目标空仓。 df df.sort_values(date).reset_index(dropTrue) df[ma_short] df[close].rolling(short).mean() df[ma_long] df[close].rolling(long).mean() # 短均线大于长均线时持有否则空仓 df[target_position] (df[ma_short] df[ma_long]).astype(int) return df这里我把信号表达成“target_position”而不是直接写买入卖出的动作。这个抽象很重要回测引擎只需要关注“当前应该持有还是空仓”不需要在每个 bar 上判断是否发生了金叉死叉。这样做能避免重复信号比如已经持有时再出现金叉不会被重复买入。参数 short 和 long 是策略最敏感的部分它们的值直接影响持仓周期和交易频率。在代码里用参数传入方式而不是写死rolling(5)和rolling(20)后面才能做参数寻优。3.2 最小回测引擎逐根K线模拟交易和资金曲线信号生成以后就要写回测引擎。回测引擎的本质是模拟交易规则按收盘价买入、卖出记录每天的现金和持仓市值。下面这段代码是逐行遍历 DataFrame逻辑直观适合毕设第一版。# engine/backtest_engine.py def backtest(df, init_cash100000, fee_rate0.0003): 简化版回测按收盘价成交买入按整手。 返回包含日期、总资产、现金、持仓股数的DataFrame。 df df.reset_index(dropTrue) cash init_cash position 0 equity_list [] for i, row in df.iterrows(): price row[close] target row[target_position] # 目标持仓为1且当前空仓时买入 if target 1 and position 0: position int(cash // (price * 100)) * 100 # 按整手买入 cash - position * price * (1 fee_rate) # 扣除手续费 # 目标持仓为0且当前有持仓时卖出 elif target 0 and position 0: cash position * price * (1 - fee_rate) # 卖出扣手续费 position 0 total_assets cash position * price equity_list.append({ date: row[date], total_assets: total_assets, cash: cash, position: position, }) return pd.DataFrame(equity_list)代码里的核心逻辑是每当 target_position 从 0 变成 1 时就用剩余现金尽量买入 100 的整数倍股票从 1 变成 0 时就把持仓全部卖出。为什么用整手因为 A 股买入数量必须是 100 股或其整数倍。这里没有处理涨跌停真实回测中涨停时你可能买不进跌停时卖不出这个后面在避坑章节再展开。手续费 scale 费率是双边收取也就是买入卖出各扣一次。实际 A 股还有最低 5 元佣金、印花税和过户费但对毕设来说按照 0.0003 的单边费率先行实现即可如果答辩需要进一步细化再去补最低佣金逻辑。3.3 评估指标怎么算年化收益率、最大回撤、夏普比例回测跑完以后不能只看最后的总收益率。很多新手第一次跑出几倍收益结果发现最大回撤超过一半这种策略根本没法用。你需要至少计算年化收益率、最大回撤、夏普比率三个指标。# engine/evaluate.py def evaluate(equity_df): 根据回测结果计算评估指标 equity_df[prev_high] equity_df[total_assets].cummax() equity_df[drawdown] equity_df[total_assets] / equity_df[prev_high] - 1 max_drawdown equity_df[drawdown].min() equity_df[daily_ret] equity_df[total_assets].pct_change().fillna(0) total_ret equity_df[total_assets].iloc[-1] / equity_df[total_assets].iloc[0] - 1 years len(equity_df) / 250 # 一年约250个交易日 annual_ret (1 total_ret) ** (1 / years) - 1 daily_std equity_df[daily_ret].std() sharpe 0 if daily_std 0: sharpe equity_df[daily_ret].mean() / daily_std * (250 ** 0.5) return { total_ret: total_ret, annual_ret: annual_ret, max_drawdown: max_drawdown, sharpe: sharpe, trade_days: len(equity_df), }最大回撤用cummax()算出历史最高净值再用当前净值除以最高净值减 1得到的就是从最高点往下的回撤幅度。夏普比率做了一个年化处理乘以250 ** 0.5是因为我们用的是日收益率。这里没有使用无风险利率对毕设来说已经够用。你要记住一个黄金认知回测指标只能在“样本内”给出参考不能证明策略未来会赚钱。所以后面必须做样本外验证。4. 从回测到模拟交易订单、持仓与风控落地4.1 交易通道选择模拟盘接口与前后端设计回测通过以后要让系统真正跑起来需要接入交易通道。毕设阶段不建议直接对接真实账户。更推荐的做法是接券商的模拟盘环境或者完全用内存模拟一个交易账户。写一个交易接口抽象类把“虚拟账户”和“真实接入”统一成相同的接口这样后续替换很方便。# trade/trade_api.py class TradeAPI: 交易接口抽象类定义买卖和查询持仓的统一方法 def buy(self, symbol, price, quantity): raise NotImplementedError def sell(self, symbol, price, quantity): raise NotImplementedError def query_position(self): raise NotImplementedError class VirtualTradeAPI(TradeAPI): 虚拟账户只在内存中模拟交易避免真实资金风险 def __init__(self, init_cash100000): self.cash init_cash self.position {} self.order_id 0 def buy(self, symbol, price, quantity): # 先校验资金是否充足这里不做任何风控后面接RiskManager cost price * quantity if cost self.cash: raise ValueError(资金不足) self.cash - cost self.position[symbol] self.position.get(symbol, 0) quantity self.order_id 1 return { order_id: self.order_id, symbol: symbol, side: buy, quantity: quantity, price: price, } def sell(self, symbol, price, quantity): if self.position.get(symbol, 0) quantity: raise ValueError(持仓不足) self.cash price * quantity self.position[symbol] - quantity self.order_id 1 return { order_id: self.order_id, symbol: symbol, side: sell, quantity: quantity, price: price, }这段代码把“下单”抽象成了 buy 和 sell 两个方法。为什么值得这么做因为策略层不需要关心底层是模拟还是真实接入它只需要调用buy(000001, 10.5, 100)。后面如果你想从虚拟账户切换到某券商提供的开放接口只需要新写一个RealTradeAPI类把买和卖换成调用对方 SDK策略代码完全不用改。4.2 订单状态管理与持仓更新真实交易系统里订单从提交到成交有多个状态已提交、已成交、已撤销、被拒绝。模拟盘里你也可以设计这个状态机方便演示“下单以后发生了什么”。# trade/order_status.py ORDER_PENDING PENDING # 订单已提交等待成交 ORDER_FILLED FILLED # 订单已成交持仓已更新 ORDER_CANCELLED CANCELLED # 订单已撤销 ORDER_REJECTED REJECTED # 订单被拒绝例如资金不足订单管理的核心不是把状态记下来而是在每个状态变化时同步更新现金和持仓。我之前见过不少同学把订单状态和持仓分开维护结果跑到一半现金和持仓对不上账。建议以订单为唯一事实来源只有当订单状态变为 FILLED 时才去修改 cash 和 position其他状态一律不动。这样即使订单被拒绝账本也不会乱。另一个容易忽略的点是所有下单操作都要做幂等处理。比如程序定时器触发了两遍买入信号不能对同一个信号下两张单。常见做法是给信号生成一个唯一标识比如按日期加股票代码做 key订单表里检查这个 key 是否已经存在存在就直接跳过。4.3 风控模块仓位控制、止损与防止情绪化交易风控是整个自动交易系统的安全气囊。没有风控的自动交易本质是给亏钱装上了自动扣扳机。毕设风控不需要做得很复杂但至少要有三个规则单笔止损、总仓位限制、最大回撤预警。# trade/risk_manager.py class RiskManager: 下单前的风控检查所有检查通过才能执行交易 def __init__(self, max_position_ratio0.7, stop_loss_threshold0.05): self.max_position_ratio max_position_ratio # 总仓位不能超过70% self.stop_loss_threshold stop_loss_threshold # 单只股票亏损超过5%则触发止损 def check_before_buy(self, total_assets, current_position_value, order_value): # 买入后总市值不能超过总资产的70% if current_position_value order_value total_assets * self.max_position_ratio: return False, 总仓位超过上限 return True, ok def check_stop_loss(self, cost_price, current_price): # 如果当前价格已经跌到成本价以下5%不开新仓或直接执行止损 loss_ratio (current_price - cost_price) / cost_price if loss_ratio -self.stop_loss_threshold: return False, 触发止损 return True, okmax_position_ratio0.7是总仓位上限防止策略在极端行情下满仓梭哈。stop_loss_threshold0.05是单笔止损阈值亏损超过 5% 就停止加仓或强制离场。这些参数都是经验值答辩时可以摆出你的调整过程。这里要提醒一句风控代码必须放在订单发送之前执行并且要写在策略主循环里不能只挂在回测里。回测中的风控和模拟交易中的风控要保持一致否则回测和实盘的表现会完全不同。5. 股票自动交易系统常见问题与避坑5.1 回测很美、实盘翻车未来函数和成交价格假设现象回测年化收益 30%最大回撤只有 8%一上模拟盘连续亏损资金曲线明显分叉。原因最常见的是未来函数也就是策略用了当天收盘后才能知道的信号却在同一根 K 线的收盘价成交。比如你在df[close].rolling(5).mean()的同一个 bar 里比较大小生成信号又在同一行代码里用 close 成交这就等于你提前知道今天会涨然后用今天的收盘价买入现实中根本不可能。解决把信号生成和成交价做一个时间错位。信号当天收盘后计算按下一个交易日开盘价成交。最简单的改法是在回测循环里price用下一行的open或者把target_position用shift(1)平移。这里推荐平移方式它同时适用于回测和模拟盘。# 避免未来函数信号向后平移一天用次日价格 df[target_position] df[target_position].shift(1) df.iloc[0, df.columns.get_loc(target_position)] 0这个细节是答辩时的高频提问点提前处理好能让你的系统显得专业。5.2 数据缺口和复权错误导致信号错乱现象策略对某只股票回测时资金曲线突然出现一根巨大跳线有时候一天之内收益率从 5% 变成 20%。原因股票停牌导致数据缺行或者除权除息后没有使用复权数据。停牌时当天根本没有交易akshare 不会产生那根 K 线你的 DataFrame 中间就会空一行。均线计算是用 rolling 窗口它不会因为你缺行就跳过而是直接把前一行和后一行拉在一起算出一个假交叉。解决下载数据后做日期连续性校验。拿交易日历和生产数据做对比发现缺失就直接放弃这段数据或者用最近交易日填充。另一个办法是统一字段标记从文件名里区分前复权和不复权不要混用。# 校验数据日期连续性打印缺失日期 import pandas as pd df[date] pd.to_datetime(df[date]) date_set set(df[date].dt.date) missing [] for d in pd.date_range(df[date].min(), df[date].max(), freqD): if d.date() not in date_set: missing.append(d.date()) # 输出后要人工判断周末和节假日是正常缺失工作日缺失就需要处理 print(疑似缺失日期:, missing)注意A 股节假日和周末本来就缺数据你跟日历对不上不代表出错要过滤掉非交易日再做判断。5.3 程序挂起或阻塞日志看不出原因现象定时任务今天没跑打开日志发现昨天下午 3 点之后没有任何输出进程也没有退出。原因外部接口超时卡死、akshare 请求被限频、网络断开后程序没有重试。有些命令直接阻塞在requests.get上超时时间没设置卡住几小时都不奇怪。解决所有网络请求都设置超时和重试。另外在关键步骤打点日志比如“开始拉取数据 / 数据拉取完成 / 开始计算信号 / 计算完成”。# 给akshare请求加一个超时兜底 import akshare as ak from tenacity import retry, stop_after_attempt, wait_exponential retry(stopstop_after_attempt(3), waitwait_exponential(multiplier1, min2, max10)) def fetch_daily(symbol): print(f拉取 {symbol} 行情) return ak.stock_zh_a_hist(symbolsymbol, perioddaily, adjustqfq)即使你不引入 tenacity手工写 try-except 也需要把异常和重试次数记录下来。有一个教训日志不能只打印“开始某步”还要打印“这一步耗时多少”否则你不知道是卡在数据源还是卡在本地计算上。5.4 策略过拟合历史数据上完美参数一改就崩现象一个策略设了 8 个参数通过反复调参得到一条几乎 45 度上扬的资金曲线但对 2022 年到 2023 年的分段回测表现很差参数稍微改一点就变成亏损。原因用全部历史数据反复调参策略已经把噪声“背”下来了。参数越多过拟合越容易发生这在机器学习里是常识在股票量化里同样成立。解决把数据集切成样本内和样本外样本内调参、样本外验证。同时限制参数数量一个策略有 3 到 4 个可调参数已经不少了。如果最优参数旁边稍微一调就大幅变差说明这个策略不稳定不要选它。# 参数敏感性检查在最优参数附近做小范围扰动 test_params [ {short: 4, long: 19}, {short: 5, long: 20}, {short: 6, long: 21}, ] for params in test_params: result backtest(generate_signal(df, **params)) print(params, 总收益率:, result[total_assets].iloc[-1])如果相邻参数结果差异巨大说明策略不稳定。这一条被答辩老师问到概率很高提前做一组敏感性分析放在论文里能挽回不少印象分。5.5 密钥、权限和账户安全边界现象同学把交易接口的密钥硬编码在源码里然后整个项目打包发到群里结果密钥被他人拿走使用账户资金出了风险。原因只追求“能跑”没有考虑代码会被复制、提交到代码仓库或者分享给别人。解决密钥统一从环境变量或者.env文件读取真实配置文件要加入.gitignore。同时毕设项目里尽量只使用模拟账号不要放真实资金。自动交易涉及合规问题不同地区监管要求不同答辩时也建议明确写“系统面向模拟交易与策略研究”不要承诺实盘自动交易功能。以下是.env.example的写法只给格式不给真实值# .env.example 提交到仓库时只保留这个示例文件不提交真实文件 TRADE_API_BASE_URLhttps://your-broker-api.example.com TRADE_API_KEYyour_key_here TRADE_API_SECRETyour_secret_here风险控制永远是第一位别有“真实账户跑一下就删掉”的想法自动交易出 Bug 时几秒钟就能把资金亏掉不少。6. 把毕设做到能答辩样本外验证与代码组织6.1 一份能自证的最小闭环项目答辩时老师最怕看到一堆文件但不知道从哪里开始运行。建议你把项目整理成下面这种结构保证任何一个人按 README 步骤能跑出回测结果stock_trading_system/ ├── data/ │ ├── daily/ # 本地行情CSV带复权标记 │ └── backtest_result/ # 回测输出结果 ├── strategy/ │ └── double_ma.py # 策略信号生成 ├── engine/ │ ├── backtest_engine.py # 回测引擎 │ └── evaluate.py # 评估指标计算 ├── trade/ │ ├── trade_api.py # 交易接口抽象与虚拟账户 │ ├── order_status.py # 订单状态常量 │ └── risk_manager.py # 风控管理器 ├── config.yaml # 策略参数与账户配置 ├── requirements.txt ├── run_backtest.py # 主入口拉数据、算策略、跑回测、输出指标 └── README.md这里的run_backtest.py是唯一入口负责把数据获取、策略信号、回测、评估串起来。config.yaml负责装载配置这样做的好处是老师改参数不需要去翻代码打开配置文件就能做实验。6.2 样本外验证用 80/20 的时间切分来评估设计一套自动交易系统最容易被追问的问题是“你如何证明策略在未来也能工作”最佳回答就是我做了样本外验证。把时间序列的前 80% 作为训练段后 20% 作为测试段在训练段选参数在测试段验证参数全程禁止用测试段调参。# run_backtest.py import yaml import pandas as pd from strategy.double_ma import generate_signal from engine.backtest_engine import backtest from engine.evaluate import evaluate # 读取配置文件里的参数 config yaml.safe_load(open(config.yaml, encodingutf-8)) df pd.read_csv(data/daily/000001_daily_qfq.csv, parse_dates[date]) # 按时间顺序切分 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx].copy() test_df df.iloc[split_idx:].copy() params { short: config[strategy][short], long: config[strategy][long], } # 样本内回测 train_result backtest(generate_signal(train_df, **params)) print(样本内指标:, evaluate(train_result)) # 样本外回测使用同样的参数 test_result backtest(generate_signal(test_df, **params)) print(样本外指标:, evaluate(test_result))这段代码同时展示了配置加载和时间序列切分。要注意切分前必须先按照 date 排序否则随机顺序会让样本外失去意义。这里split_idx只做一条分割线完全避免未来数据泄漏。对于毕设你还可以再往前一步做多组样本外滚动测试。比如每 6 个月作为一个区块用前 18 个月训练、后 6 个月测试滚动三轮。这样能证明系统不是只撞上一次好行情。6.3 代码组织的常用习惯与答辩自检清单最后分享一个让我自己受益比较多的习惯把“可以复现”作为写代码的底线。每次回测跑完自动把策略参数、交易区间、数据文件、评估指标同步写到当天日期的结果文件里。这样你拿着结果去对论文每一张图都能追溯。你还可以把config.yaml写得更细一些让老师看一眼就明白系统设计思路# config.yaml系统配置示例 data: symbol: 000001 start_date: 2018-01-01 end_date: 2023-12-31 adjust: qfq strategy: name: double_ma short: 5 long: 20 backtest: init_cash: 100000 fee_rate: 0.0003 min_lot: 100 trade: mode: virtual # virtual 表示虚拟账户不接真实盘答辩前你至少要点检三件事第一回测结果是否能在另一台机器上重新跑出来如果可以就说“纯 Python 配置文件”不能的话赶紧补 README第二策略代码是否清晰展示了信号的产生、回测、风控三个层次别把逻辑搅在一个文件里第三论文里的每一条资金曲线和指标都要有对应的本地代码文件和参数记录。以前我有一个项目把所有数据都用来调参答辩时被问“你对 2022 年下半年的样本外表现怎么看”当场拿不出结果。后来我养成了固定做 80/20 切分的习惯把样本外测试作为流程的一部分写进项目里。这样导师看到的不是一个堆满代码的 zip而是一套能自证、可验证的系统。希望这些思路能帮你把这个毕设题目做成真正拿得出手的作品希望帮到你。本文还有配套的精品资源点击获取